From e2f8e4ff56bf7a0a48d0cac2b88ae782f74f38b2 Mon Sep 17 00:00:00 2001 From: andreas Date: Fri, 15 Oct 2021 19:05:51 +0200 Subject: [PATCH] initial import --- .gitignore | 5 + platformio.ini | 19 ++ src/BoatData.h | 38 +++ src/List.h | 459 +++++++++++++++++++++++++++++++++++ src/N2kDataToNMEA0183.cpp | 356 +++++++++++++++++++++++++++ src/N2kDataToNMEA0183.h | 113 +++++++++ src/index_html.h | 156 ++++++++++++ src/main.cpp | 495 ++++++++++++++++++++++++++++++++++++++ 8 files changed, 1641 insertions(+) create mode 100644 .gitignore create mode 100644 platformio.ini create mode 100644 src/BoatData.h create mode 100644 src/List.h create mode 100644 src/N2kDataToNMEA0183.cpp create mode 100644 src/N2kDataToNMEA0183.h create mode 100644 src/index_html.h create mode 100644 src/main.cpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..89cc49c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..1a3f364 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,19 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:m5stack-atom] +platform = espressif32 +board = m5stack-atom +framework = arduino +lib_deps = + ttlappalainen/NMEA2000-library @ ^4.17.2 + ttlappalainen/NMEA2000_esp32 @ ^1.0.3 + ttlappalainen/NMEA0183 @ ^1.7.1 + bblanchon/ArduinoJson@^6.18.5 diff --git a/src/BoatData.h b/src/BoatData.h new file mode 100644 index 0000000..60868c3 --- /dev/null +++ b/src/BoatData.h @@ -0,0 +1,38 @@ +#ifndef _BoatData_H_ +#define _BoatData_H_ + +struct tBoatData { + unsigned long DaysSince1970; // Days since 1970-01-01 + + double Heading,SOG,COG,STW,Variation,AWS,TWS,MaxAws,MaxTws,AWA,TWA,AWD,TWD,TripLog,Log,RudderPosition,WaterTemperature, + WaterDepth, GPSTime,// Secs since midnight, + Latitude, Longitude, Altitude; + +public: + tBoatData() { + Heading=0; + Latitude=0; + Longitude=0; + SOG=0; + COG=0; + STW=0; + AWS=0; + TWS=0; + MaxAws=0; + MaxTws=0; + AWA=0; + TWA=0; + TWD=0; + TripLog=0; + Log=0; + RudderPosition=0; + WaterTemperature=0; + WaterDepth=0; + Variation=0; + Altitude=0; + GPSTime=0; + DaysSince1970=0; + }; +}; + +#endif // _BoatData_H_ diff --git a/src/List.h b/src/List.h new file mode 100644 index 0000000..1d314dc --- /dev/null +++ b/src/List.h @@ -0,0 +1,459 @@ +/* + The MIT License + + Copyright (c) 2016 Thomas Sarlandie thomas@sarlandie.net + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + + A big thanks to NorthWestern University ~riesbeck on implementing iterators. + Could not have done it without you! + http://www.cs.northwestern.edu/~riesbeck/programming/c++/stl-iterator-define.html +*/ + +#pragma once + +//#define _USE_STD_LIST_FOR_LINKED_LIST_ + +#ifdef _USE_STD_LIST_FOR_LINKED_LIST_ +#include + +template using LinkedList = std::list; +#else + +template class LinkedListIterator; +template class LinkedListConstIterator; +template class LinkedListCircularIterator; + +template class LinkedList { + private: + class node { + public: + T value; + node *next; + + node(const T &v) : value(v), next(0) {}; + }; + + node *head; + + friend class LinkedListIterator; + friend class LinkedListConstIterator; + friend class LinkedListCircularIterator; + + public: + typedef LinkedListIterator iterator; + typedef LinkedListConstIterator const_iterator; + using constIterator=const_iterator; + typedef LinkedListCircularIterator circularIterator; + + LinkedList() : head(0) {}; + + LinkedList(const LinkedList &l) : head(0) { + for (constIterator it = l.begin(); it != l.end(); it++) { + add(*it); + } + }; + + ~LinkedList() { + clear(); + }; + + LinkedList& operator=(const LinkedList &l) { + clear(); + for (constIterator it = l.begin(); it != l.end(); it++) { + add(*it); + } + return *this; + }; + + void push_back(const T &v) { + if (head == 0) { + head = new node(v); + } + else { + node *n = head; + while (n->next != 0) { + n = n->next; + } + n->next = new node(v); + } + }; + + inline void add(const T &v) { push_back(v); } + + iterator insert(iterator pos, const T& v) { + if ( head==0 || pos._current==head ) { + head=new node(v); + head->next=pos._current; + pos._current=head; + } else { + node *n; + for ( n=head;n->next!=pos._current; n=n->next ); + n->next=new node(v); + n->next->next=pos._current; + pos._current=n->next; + } + + return pos; + } + + T& front() { + return head->value; + } + + T& back() { + node *n = head; + while ( n!=0 && n->next!=0 ) { + n=n->next; + } + return n->value; + } + + void remove(const T &v) { + node *n = head; + // remove from beginning + while ( n!=0 && n->value==v ) { + head=n->next; + delete(n); + n=head; + } + + while ( n!=0 ) { + node *del=n->next; + if ( del!=0 && del->value==v ) { + n->next=del->next; + delete(del); + } + n=n->next; + } + } + + void clear() { + node *n = head; + while (n != 0) { + node *next = n->next; + delete(n); + n = next; + } + head = 0; + }; + + iterator erase(LinkedListIterator l) { + if ( l==end() ) return l; + node *dn = l._current; + l++; + if ( dn==head ) { + head=dn->next; + } else { + node *n=head; + for ( ;n->next!=dn;n=n->next ); + n->next=dn->next; + } + delete dn; + return l; + } + + void removeFirst() { + node *n = head; + if (head) { + head = head->next; + delete(n); + } + }; + + iterator begin() { + return iterator(head); + }; + + constIterator begin() const { + return constIterator(head); + }; + + iterator end() { + return iterator(0); + }; + + constIterator end() const { + return constIterator(0); + }; + + circularIterator circularBegin() { + return circularIterator(head); + }; + + circularIterator circularEnd() { + return circularIterator(0); + }; + + int size() const { + LinkedList::node *n = head; + int sz = 0; + while (n != 0) { + n = n->next; + sz++; + } + return sz; + }; + + bool operator==(const LinkedList &l) const { + auto it=begin(); + auto oit=l.begin(); + for (;it!=end() && oit!=l.end() && (*it)==(*oit); it++, oit++); + return ( (*it)==(*oit) ); + }; + + bool operator!=(const LinkedList &l) const { + return !(*this==l); + }; + + template< class Compare > + void sort( Compare comp ) { + if ( head==0 || head->next==0 ) return; // Empty list or 1 element list + bool sortDone; + do { + sortDone=true; + node *pa=head; + node *a=head; + node *b=a->next; + while ( b!=0 ) { + if ( !(*comp)(b->value,a->value) ) { + pa=a; + } else { // sort required + sortDone=false; + a->next=b->next; + b->next=a; + if ( a==head ) { + head=b; + } else { + pa->next=b; + } + } + a=pa->next; + b=a->next; + } + + } while ( !sortDone ); + } +protected: + template friend void MoveListItemForward(LinkedList &list, const L &v); + template friend void MoveListItemBack(LinkedList &list, const L &v); + void moveBack(const T &v) { + node *n = head; + + if ( n==0 || n->value==v ) return; // can not move + + node *p=n; + n=n->next; + if ( n!=0 && n->value==v ) { // move second + head->next=n->next; + head=n; + n->next=p; + } + + node *pp=p; + for (p=n,n=n->next;n!=0 && n->value!=v; pp=p, p=n, n=n->next ); + if ( n==0 ) return; + + p->next=n->next; + pp->next=n; + n->next=p; + } + + void moveForward(const T &v) { + node *n = head; + + if ( n==0 || n->next==0 ) return; // can not move, only one value + + node *p=n; + n=n->next; + if ( p->value==v ) { // move first + head=n; + p->next=n->next; + n->next=p; + } + + node *pp=p; + for (p=n,n=n->next;n!=0 && p->value!=v; pp=p, p=n, n=n->next ); + if ( n==0 ) return; + pp->next=n; + p->next=n->next; + n->next=p; + } +}; + +template class LinkedListIterator { + friend class LinkedList; + protected: + typename LinkedList::node *_current; + + public: + LinkedListIterator(typename LinkedList::node *node=0) : _current(node) {}; + + LinkedListIterator& operator=(const LinkedListIterator &l) { + _current = l._current; + return *this; + }; + + LinkedListIterator& operator=(const LinkedListConstIterator &l) { + _current = l._current; + return *this; + }; + + bool operator==(LinkedListIterator l) { + return l._current == _current; + } + + bool operator!=(LinkedListIterator l) { + return l._current != _current; + } + + LinkedListIterator & operator++() { + if ( _current!=0) _current = _current->next; + return *this; + } + + // Postfix operator takes an argument (that we do not use) + LinkedListIterator operator++(int) { + LinkedListIterator clone(*this); + ++(*this); + return clone; + } + + T & operator*() { + return _current->value; + } + + T * operator->() { + return &(operator*()); + } +}; + +template class LinkedListConstIterator { + protected: + friend class LinkedListIterator; + typename LinkedList::node const *_current; + + public: + LinkedListConstIterator(typename LinkedList::node const *node) : _current(node) {}; + + bool operator==(LinkedListConstIterator l) { + return l._current == _current; + } + + bool operator!=(LinkedListConstIterator l) { + return l._current != _current; + } + + LinkedListConstIterator & operator++() { + if ( _current!=0) _current = _current->next; + return *this; + } + + // Postfix operator takes an argument (that we do not use) + LinkedListConstIterator operator++(int) { + LinkedListConstIterator clone(*this); + _current = _current->next; + return clone; + } + + const T & operator*() { + return _current->value; + } +}; + +template class LinkedListCircularIterator : public LinkedListIterator { + protected: + typename LinkedList::node *_head; + + public: + LinkedListCircularIterator(typename LinkedList::node *head) : LinkedListIterator(head), _head(head) {}; + + LinkedListCircularIterator & operator++() { + if (this->_current->next) { + this->_current = this->_current->next; + } + else { + this->_current = this->_head; + } + return *this; + }; + + LinkedListIterator operator++(int) { + LinkedListIterator clone(*this); + ++(*this); + return clone; + }; +}; + +#ifdef DEBUG_TEST_LINKED_LIST +LinkedList DoubleList; + +void PrintTestList() { + for (auto it=DoubleList.begin(); it!=DoubleList.end(); it++) { + Serial.println((*it)); + } +} +void TestList() { + DoubleList.add(5); + DoubleList.add(6); + DoubleList.add(9); + PrintTestList(); + DoubleList.remove(6); + PrintTestList(); +} +#endif + +#endif + +template +void MoveListItemForward(LinkedList &list, const T &v) { + #ifdef _USE_STD_LIST_FOR_LINKED_LIST_ + auto it=list.begin(); + if ( it==list.end() ) return; // Empty list + + for ( ;it!=list.end() && (*it)!=v; it++); + if ( it==list.end() ) return; // not found + auto itn=it; + itn++; + if ( itn==list.end() ) return; // v is last value + itn++; + list.erase(it); + list.insert(itn,v); + #else + list.moveForward(v); + #endif +} + +template +void MoveListItemBack(LinkedList &list, const T &v) { +#ifdef _USE_STD_LIST_FOR_LINKED_LIST_ + auto it=list.begin(); + if ( it==list.end() ) return; // Empty list + if ( (*it)==v ) return; // v at begin + + auto itp=it; + it++; + for ( ;it!=list.end() && (*it)!=v; itp=it, it++); + if ( it==list.end() ) return; // not found + list.erase(it); + list.insert(itp,v); +#else + list.moveBack(v); +#endif +} diff --git a/src/N2kDataToNMEA0183.cpp b/src/N2kDataToNMEA0183.cpp new file mode 100644 index 0000000..d146a32 --- /dev/null +++ b/src/N2kDataToNMEA0183.cpp @@ -0,0 +1,356 @@ +/* + N2kDataToNMEA0183.cpp + + Copyright (c) 2015-2018 Timo Lappalainen, Kave Oy, www.kave.fi + + Permission is hereby granted, free of charge, to any person obtaining a copy of + this software and associated documentation files (the "Software"), to deal in + the Software without restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + Software, and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +#include +#include +#include + +#include "N2kDataToNMEA0183.h" +#include "BoatData.h" + +const double radToDeg = 180.0 / M_PI; + +//***************************************************************************** +void tN2kDataToNMEA0183::HandleMsg(const tN2kMsg &N2kMsg) { + switch (N2kMsg.PGN) { + case 127250UL: HandleHeading(N2kMsg); + case 127258UL: HandleVariation(N2kMsg); + case 128259UL: HandleBoatSpeed(N2kMsg); + case 128267UL: HandleDepth(N2kMsg); + case 129025UL: HandlePosition(N2kMsg); + case 129026UL: HandleCOGSOG(N2kMsg); + case 129029UL: HandleGNSS(N2kMsg); + case 130306UL: HandleWind(N2kMsg); + case 128275UL: HandleLog(N2kMsg); + case 127245UL: HandleRudder(N2kMsg); + case 130310UL: HandleWaterTemp(N2kMsg); + } +} + +//***************************************************************************** +long tN2kDataToNMEA0183::Update(tBoatData *BoatData) { + SendRMC(); + if ( LastHeadingTime + 2000 < millis() ) Heading = N2kDoubleNA; + if ( LastCOGSOGTime + 2000 < millis() ) { + COG = N2kDoubleNA; + SOG = N2kDoubleNA; + } + if ( LastPositionTime + 4000 < millis() ) { + Latitude = N2kDoubleNA; + Longitude = N2kDoubleNA; + } + if ( LastWindTime + 2000 < millis() ) { + AWS = N2kDoubleNA; + AWA = N2kDoubleNA; + TWS = N2kDoubleNA; + TWA = N2kDoubleNA; + TWD = N2kDoubleNA; + } + + BoatData->Latitude=Latitude; + BoatData->Longitude=Longitude; + BoatData->Altitude=Altitude; + BoatData->Heading=Heading * radToDeg; + BoatData->COG=COG * radToDeg; + BoatData->SOG=SOG * 3600.0/1852.0; + BoatData->STW=STW * 3600.0/1852.0; + BoatData->AWS=AWS * 3600.0/1852.0; + BoatData->TWS=TWS * 3600.0/1852.0; + BoatData->MaxAws=MaxAws * 3600.0/1852.0;; + BoatData->MaxTws=MaxTws * 3600.0/1852.0;; + BoatData->AWA=AWA * radToDeg; + BoatData->TWA=TWA * radToDeg; + BoatData->TWD=TWD * radToDeg; + BoatData->TripLog=TripLog / 1825.0; + BoatData->Log=Log / 1825.0; + BoatData->RudderPosition=RudderPosition * radToDeg; + BoatData->WaterTemperature=KelvinToC(WaterTemperature) ; + BoatData->WaterDepth=WaterDepth; + BoatData->Variation=Variation *radToDeg; + BoatData->GPSTime=SecondsSinceMidnight; + BoatData->DaysSince1970=DaysSince1970; + + +if (SecondsSinceMidnight!=N2kDoubleNA && DaysSince1970!=N2kUInt16NA){ + return((DaysSince1970*3600*24)+SecondsSinceMidnight); // Needed for SD Filename and time + } else { + return(0); // Needed for SD Filename and time + } +} + +//***************************************************************************** +void tN2kDataToNMEA0183::SendMessage(const tNMEA0183Msg &NMEA0183Msg) { + if ( pNMEA0183 != 0 ) pNMEA0183->SendMessage(NMEA0183Msg); + if ( SendNMEA0183MessageCallback != 0 ) SendNMEA0183MessageCallback(NMEA0183Msg); +} + +//***************************************************************************** +void tN2kDataToNMEA0183::HandleHeading(const tN2kMsg &N2kMsg) { + unsigned char SID; + tN2kHeadingReference ref; + double _Deviation = 0; + double _Variation; + tNMEA0183Msg NMEA0183Msg; + + if ( ParseN2kHeading(N2kMsg, SID, Heading, _Deviation, _Variation, ref) ) { + if ( ref == N2khr_magnetic ) { + if ( !N2kIsNA(_Variation) ) Variation = _Variation; // Update Variation + if ( !N2kIsNA(Heading) && !N2kIsNA(Variation) ) Heading -= Variation; + } + LastHeadingTime = millis(); + if ( NMEA0183SetHDG(NMEA0183Msg, Heading, _Deviation, Variation) ) { + SendMessage(NMEA0183Msg); + } + } +} + +//***************************************************************************** +void tN2kDataToNMEA0183::HandleVariation(const tN2kMsg &N2kMsg) { + unsigned char SID; + tN2kMagneticVariation Source; + uint16_t DaysSince1970; + + ParseN2kMagneticVariation(N2kMsg, SID, Source, DaysSince1970, Variation); +} + +//***************************************************************************** +void tN2kDataToNMEA0183::HandleBoatSpeed(const tN2kMsg &N2kMsg) { + unsigned char SID; + double WaterReferenced; + double GroundReferenced; + tN2kSpeedWaterReferenceType SWRT; + + if ( ParseN2kBoatSpeed(N2kMsg, SID, WaterReferenced, GroundReferenced, SWRT) ) { + tNMEA0183Msg NMEA0183Msg; + STW=WaterReferenced; + double MagneticHeading = ( !N2kIsNA(Heading) && !N2kIsNA(Variation) ? Heading + Variation : NMEA0183DoubleNA); + if ( NMEA0183SetVHW(NMEA0183Msg, Heading, MagneticHeading, WaterReferenced) ) { + SendMessage(NMEA0183Msg); + } + } +} + +//***************************************************************************** +void tN2kDataToNMEA0183::HandleDepth(const tN2kMsg &N2kMsg) { + unsigned char SID; + double DepthBelowTransducer; + double Offset; + double Range; + + if ( ParseN2kWaterDepth(N2kMsg, SID, DepthBelowTransducer, Offset, Range) ) { + + WaterDepth=DepthBelowTransducer+Offset; + + tNMEA0183Msg NMEA0183Msg; + if ( NMEA0183SetDPT(NMEA0183Msg, DepthBelowTransducer, Offset) ) { + SendMessage(NMEA0183Msg); + } + if ( NMEA0183SetDBx(NMEA0183Msg, DepthBelowTransducer, Offset) ) { + SendMessage(NMEA0183Msg); + } + } +} + +//***************************************************************************** +void tN2kDataToNMEA0183::HandlePosition(const tN2kMsg &N2kMsg) { + + if ( ParseN2kPGN129025(N2kMsg, Latitude, Longitude) ) { + LastPositionTime = millis(); + } +} + +//***************************************************************************** +void tN2kDataToNMEA0183::HandleCOGSOG(const tN2kMsg &N2kMsg) { + unsigned char SID; + tN2kHeadingReference HeadingReference; + tNMEA0183Msg NMEA0183Msg; + + if ( ParseN2kCOGSOGRapid(N2kMsg, SID, HeadingReference, COG, SOG) ) { + LastCOGSOGTime = millis(); + double MCOG = ( !N2kIsNA(COG) && !N2kIsNA(Variation) ? COG - Variation : NMEA0183DoubleNA ); + if ( HeadingReference == N2khr_magnetic ) { + MCOG = COG; + if ( !N2kIsNA(Variation) ) COG -= Variation; + } + if ( NMEA0183SetVTG(NMEA0183Msg, COG, MCOG, SOG) ) { + SendMessage(NMEA0183Msg); + } + } +} + +//***************************************************************************** +void tN2kDataToNMEA0183::HandleGNSS(const tN2kMsg &N2kMsg) { + unsigned char SID; + tN2kGNSStype GNSStype; + tN2kGNSSmethod GNSSmethod; + unsigned char nSatellites; + double HDOP; + double PDOP; + double GeoidalSeparation; + unsigned char nReferenceStations; + tN2kGNSStype ReferenceStationType; + uint16_t ReferenceSationID; + double AgeOfCorrection; + + if ( ParseN2kGNSS(N2kMsg, SID, DaysSince1970, SecondsSinceMidnight, Latitude, Longitude, Altitude, GNSStype, GNSSmethod, + nSatellites, HDOP, PDOP, GeoidalSeparation, + nReferenceStations, ReferenceStationType, ReferenceSationID, AgeOfCorrection) ) { + LastPositionTime = millis(); + } +} + +//***************************************************************************** +void tN2kDataToNMEA0183::HandleWind(const tN2kMsg &N2kMsg) { + unsigned char SID; + tN2kWindReference WindReference; + tNMEA0183WindReference NMEA0183Reference = NMEA0183Wind_True; + + double x, y; + double WindAngle, WindSpeed; + + // double TWS, TWA, AWD; + + if ( ParseN2kWindSpeed(N2kMsg, SID, WindSpeed, WindAngle, WindReference) ) { + tNMEA0183Msg NMEA0183Msg; + LastWindTime = millis(); + + if ( WindReference == N2kWind_Apparent ) { + NMEA0183Reference = NMEA0183Wind_Apparent; + AWA=WindAngle; + AWS=WindSpeed; + if (AWS > MaxAws) MaxAws=AWS; + } + + if ( NMEA0183SetMWV(NMEA0183Msg, WindAngle*radToDeg, NMEA0183Reference , WindSpeed)) SendMessage(NMEA0183Msg); + + if (WindReference == N2kWind_Apparent && SOG != N2kDoubleNA) { // Lets calculate and send TWS/TWA if SOG is available + + AWD=WindAngle*radToDeg + Heading*radToDeg; + if (AWD>360) AWD=AWD-360; + if (AWD<0) AWD=AWD+360; + + x = WindSpeed * cos(WindAngle); + y = WindSpeed * sin(WindAngle); + + TWA = atan2(y, -SOG + x); + TWS = sqrt(( y*y) + ((-SOG+x)*(-SOG+x))); + + if (TWS > MaxTws) MaxTws=TWS; + + TWA = TWA * radToDeg +360; + + if (TWA>360) TWA=TWA-360; + if (TWA<0) TWA=TWA+360; + + NMEA0183Reference = NMEA0183Wind_True; + if ( NMEA0183SetMWV(NMEA0183Msg, TWA, NMEA0183Reference , TWS)) SendMessage(NMEA0183Msg); + + if ( !NMEA0183Msg.Init("MWD", "GP") ) return; + if ( !NMEA0183Msg.AddDoubleField(AWD) ) return; + if ( !NMEA0183Msg.AddStrField("T") ) return; + if ( !NMEA0183Msg.AddDoubleField(AWD) ) return; + if ( !NMEA0183Msg.AddStrField("M") ) return; + if ( !NMEA0183Msg.AddDoubleField(TWS/0.514444) ) return; + if ( !NMEA0183Msg.AddStrField("N") ) return; + if ( !NMEA0183Msg.AddDoubleField(TWS) ) return; + if ( !NMEA0183Msg.AddStrField("M") ) return; + + SendMessage(NMEA0183Msg); + + TWA=TWA/radToDeg; + + } + } +} + //***************************************************************************** + void tN2kDataToNMEA0183::SendRMC() { + if ( NextRMCSend <= millis() && !N2kIsNA(Latitude) ) { + tNMEA0183Msg NMEA0183Msg; + if ( NMEA0183SetRMC(NMEA0183Msg, SecondsSinceMidnight, Latitude, Longitude, COG, SOG, DaysSince1970, Variation) ) { + SendMessage(NMEA0183Msg); + } + SetNextRMCSend(); + } + } + + + //***************************************************************************** + void tN2kDataToNMEA0183::HandleLog(const tN2kMsg & N2kMsg) { + uint16_t DaysSince1970; + double SecondsSinceMidnight; + + if ( ParseN2kDistanceLog(N2kMsg, DaysSince1970, SecondsSinceMidnight, Log, TripLog) ) { + + tNMEA0183Msg NMEA0183Msg; + + if ( !NMEA0183Msg.Init("VLW", "GP") ) return; + if ( !NMEA0183Msg.AddDoubleField(Log / 1852.0) ) return; + if ( !NMEA0183Msg.AddStrField("N") ) return; + if ( !NMEA0183Msg.AddDoubleField(TripLog / 1852.0) ) return; + if ( !NMEA0183Msg.AddStrField("N") ) return; + + SendMessage(NMEA0183Msg); + } + } + + //***************************************************************************** + void tN2kDataToNMEA0183::HandleRudder(const tN2kMsg & N2kMsg) { + + unsigned char Instance; + tN2kRudderDirectionOrder RudderDirectionOrder; + double AngleOrder; + + if ( ParseN2kRudder(N2kMsg, RudderPosition, Instance, RudderDirectionOrder, AngleOrder) ) { + + if(Instance!=0) return; + + tNMEA0183Msg NMEA0183Msg; + + if ( !NMEA0183Msg.Init("RSA", "GP") ) return; + if ( !NMEA0183Msg.AddDoubleField(RudderPosition * radToDeg) ) return; + if ( !NMEA0183Msg.AddStrField("A") ) return; + if ( !NMEA0183Msg.AddDoubleField(0.0) ) return; + if ( !NMEA0183Msg.AddStrField("A") ) return; + + SendMessage(NMEA0183Msg); + } + } + +//***************************************************************************** + void tN2kDataToNMEA0183::HandleWaterTemp(const tN2kMsg & N2kMsg) { + + unsigned char SID; + double OutsideAmbientAirTemperature; + double AtmosphericPressure; + + if ( ParseN2kPGN130310(N2kMsg, SID, WaterTemperature, OutsideAmbientAirTemperature, AtmosphericPressure) ) { + + tNMEA0183Msg NMEA0183Msg; + + if ( !NMEA0183Msg.Init("MTW", "GP") ) return; + if ( !NMEA0183Msg.AddDoubleField(KelvinToC(WaterTemperature))) return; + if ( !NMEA0183Msg.AddStrField("C") ) return; + + SendMessage(NMEA0183Msg); + } + } diff --git a/src/N2kDataToNMEA0183.h b/src/N2kDataToNMEA0183.h new file mode 100644 index 0000000..78c8a1c --- /dev/null +++ b/src/N2kDataToNMEA0183.h @@ -0,0 +1,113 @@ +/* +N2kDataToNMEA0183.h + +Copyright (c) 2015-2018 Timo Lappalainen, Kave Oy, www.kave.fi + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +#include +#include + +#include "BoatData.h" + +//------------------------------------------------------------------------------ +class tN2kDataToNMEA0183 : public tNMEA2000::tMsgHandler { +public: + using tSendNMEA0183MessageCallback=void (*)(const tNMEA0183Msg &NMEA0183Msg); + +protected: + static const unsigned long RMCPeriod=500; + double Latitude; + double Longitude; + double Altitude; + double Variation; + double Heading; + double COG; + double SOG; + double STW; + + double TWS; + double TWA; + double TWD; + + double AWS; + double AWA; + double AWD; + + double MaxAws; + double MaxTws; + + double RudderPosition; + double WaterTemperature; + double WaterDepth; + + uint32_t TripLog; + uint32_t Log; + + uint16_t DaysSince1970; + double SecondsSinceMidnight; + + unsigned long LastHeadingTime; + unsigned long LastCOGSOGTime; + unsigned long LastPositionTime; + unsigned long LastPosSend; + unsigned long LastWindTime; + unsigned long NextRMCSend; + + tNMEA0183 *pNMEA0183; + tSendNMEA0183MessageCallback SendNMEA0183MessageCallback; + +protected: + void HandleHeading(const tN2kMsg &N2kMsg); // 127250 + void HandleVariation(const tN2kMsg &N2kMsg); // 127258 + void HandleBoatSpeed(const tN2kMsg &N2kMsg); // 128259 + void HandleDepth(const tN2kMsg &N2kMsg); // 128267 + void HandlePosition(const tN2kMsg &N2kMsg); // 129025 + void HandleCOGSOG(const tN2kMsg &N2kMsg); // 129026 + void HandleGNSS(const tN2kMsg &N2kMsg); // 129029 + void HandleWind(const tN2kMsg &N2kMsg); // 130306 + void HandleLog(const tN2kMsg &N2kMsg); // 128275 + void HandleRudder(const tN2kMsg &N2kMsg); // 127245 + void HandleWaterTemp(const tN2kMsg &N2kMsg); // 130310 + + + void SetNextRMCSend() { NextRMCSend=millis()+RMCPeriod; } + void SendRMC(); + void SendMessage(const tNMEA0183Msg &NMEA0183Msg); + +public: + tN2kDataToNMEA0183(tNMEA2000 *_pNMEA2000, tNMEA0183 *_pNMEA0183) : tNMEA2000::tMsgHandler(0,_pNMEA2000) { + SendNMEA0183MessageCallback=0; + pNMEA0183=_pNMEA0183; + Latitude=N2kDoubleNA; Longitude=N2kDoubleNA; Altitude=N2kDoubleNA; + Variation=N2kDoubleNA; Heading=N2kDoubleNA; COG=N2kDoubleNA; SOG=N2kDoubleNA; + SecondsSinceMidnight=N2kDoubleNA; DaysSince1970=N2kUInt16NA; + LastPosSend=0; + NextRMCSend=millis()+RMCPeriod; + LastHeadingTime=0; + LastCOGSOGTime=0; + LastPositionTime=0; + LastWindTime=0; + } + void HandleMsg(const tN2kMsg &N2kMsg); + void SetSendNMEA0183MessageCallback(tSendNMEA0183MessageCallback _SendNMEA0183MessageCallback) { + SendNMEA0183MessageCallback=_SendNMEA0183MessageCallback; + } + long Update(tBoatData *BoatData); +}; diff --git a/src/index_html.h b/src/index_html.h new file mode 100644 index 0000000..416336c --- /dev/null +++ b/src/index_html.h @@ -0,0 +1,156 @@ + +const char indexHTML[] PROGMEM = R"=====( + + + + +ADC Anzeigeinstrument + + + + + + +
+
+Anzeigeinstrument für Kühlschranktemperatur +
+
+
+
+
+
+ + +)=====" ; + + +const char gauge[] PROGMEM = R"=====( +var Gauge=function(b){function l(a,b){for(var c in b)"object"==typeof b[c]&&"[object Array]"!==Object.prototype.toString.call(b[c])&&"renderTo"!=c?("object"!=typeof a[c]&&(a[c]={}),l(a[c],b[c])):a[c]=b[c]}function q(){z.width=b.width;z.height=b.height;A=z.cloneNode(!0);B=A.getContext("2d");C=z.width;D=z.height;t=C/2;u=D/2;f=tc;c++)b.majorTicks.push(w(b.minValue+h*c));b.majorTicks.push(w(b.maxValue))}for(c=0;ca;a=Math.abs(a);if(0n?Math.abs(b.minValue-n):0b.maxValue?b.maxValue+d:a=(7-4*b)/11){a=-Math.pow((11-6*b-11*a)/4,2)+Math.pow(c,2);break a}a=void 0}return 1-a},elastic:function(a){a=1-a;return 1-Math.pow(2,10*(a-1))*Math.cos(30*Math.PI/3*a)}},G=null;a.lineCap="round";this.draw=function(){if(!A.i8d){B.clearRect(-t,-u,C,D);B.save();var g={ctx:a};a=B;p();N();J();d();s();b.title&&(a.save(),a.font=24*(f/200)+"px Arial",a.fillStyle=b.colors.title,a.textAlign="center",a.fillText(b.title,0,-f/4.25),a.restore());b.units&&(a.save(),a.font=22*(f/200)+"px Arial", +a.fillStyle=b.colors.units,a.textAlign="center",a.fillText(b.units,0,f/3.25),a.restore());A.i8d=!0;a=g.ctx;delete g.ctx}a.clearRect(-t,-u,C,D);a.save();a.drawImage(A,-t,-u,C,D);if(Gauge.initialized)L(),K(),H||(E.onready&&E.onready(),H=!0);else var e=setInterval(function(){Gauge.initialized&&(clearInterval(e),L(),K(),H||(E.onready&&E.onready(),H=!0))},10);return this}};Gauge.initialized=!1; +(function(){var b=document,l=b.getElementsByTagName("head")[0],q=-1!=navigator.userAgent.toLocaleLowerCase().indexOf("msie"),v="@font-face {font-family: 'Led';src: url('fonts/digital-7-mono."+(q?"eot":"ttf")+"');}",k=b.createElement("style");k.type="text/css";if(q)l.appendChild(k),l=k.styleSheet,l.cssText=v;else{try{k.appendChild(b.createTextNode(v))}catch(e){k.cssText=v}l.appendChild(k);l=k.styleSheet?k.styleSheet:k.sheet||b.styleSheets[b.styleSheets.length-1]}var g=setInterval(function(){if(b.body){clearInterval(g); +var e=b.createElement("div");e.style.fontFamily="Led";e.style.position="absolute";e.style.height=e.style.width=0;e.style.overflow="hidden";e.innerHTML=".";b.body.appendChild(e);setTimeout(function(){Gauge.initialized=!0;e.parentNode.removeChild(e)},250)}},1)})();Gauge.Collection=[]; +Gauge.Collection.get=function(b){if("string"==typeof b)for(var l=0,q=this.length;l +#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 Ereignis_Index() // Wenn "http:///" aufgerufen wurde +{ + webserver.send(200, "text/html", indexHTML); //dann Index Webseite senden +} + +void Ereignis_js() // Wenn "http:///gauge.min.js" aufgerufen wurde +{ + webserver.send(200, "text/html", gauge); // dann gauge.min.js senden +} + + +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); + + // 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(500); + 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("/", Ereignis_Index); + webserver.on("/gauge.min.js", Ereignis_js); + 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(100); + } + if (wifi_retry >= 5) { + Serial.println("\nReboot"); // Did not work -> restart ESP32 + ESP.restart(); + } + } +}