initial import

This commit is contained in:
andreas 2021-10-15 19:05:51 +02:00
commit e2f8e4ff56
8 changed files with 1641 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
.pio
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch

19
platformio.ini Normal file
View File

@ -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

38
src/BoatData.h Normal file
View File

@ -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_

459
src/List.h Normal file
View File

@ -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 <list>
template <class T> using LinkedList = std::list<T>;
#else
template <class T> class LinkedListIterator;
template <class T> class LinkedListConstIterator;
template <class T> class LinkedListCircularIterator;
template <class T> class LinkedList {
private:
class node {
public:
T value;
node *next;
node(const T &v) : value(v), next(0) {};
};
node *head;
friend class LinkedListIterator<T>;
friend class LinkedListConstIterator<T>;
friend class LinkedListCircularIterator<T>;
public:
typedef LinkedListIterator<T> iterator;
typedef LinkedListConstIterator<T> const_iterator;
using constIterator=const_iterator;
typedef LinkedListCircularIterator<T> circularIterator;
LinkedList() : head(0) {};
LinkedList(const LinkedList &l) : head(0) {
for (constIterator it = l.begin(); it != l.end(); it++) {
add(*it);
}
};
~LinkedList() {
clear();
};
LinkedList<T>& operator=(const LinkedList<T> &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<T> 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<T>::node *n = head;
int sz = 0;
while (n != 0) {
n = n->next;
sz++;
}
return sz;
};
bool operator==(const LinkedList<T> &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<T> &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<class L> friend void MoveListItemForward(LinkedList<L> &list, const L &v);
template<class L> friend void MoveListItemBack(LinkedList<L> &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 T> class LinkedListIterator {
friend class LinkedList<T>;
protected:
typename LinkedList<T>::node *_current;
public:
LinkedListIterator(typename LinkedList<T>::node *node=0) : _current(node) {};
LinkedListIterator<T>& operator=(const LinkedListIterator<T> &l) {
_current = l._current;
return *this;
};
LinkedListIterator<T>& operator=(const LinkedListConstIterator<T> &l) {
_current = l._current;
return *this;
};
bool operator==(LinkedListIterator<T> l) {
return l._current == _current;
}
bool operator!=(LinkedListIterator<T> l) {
return l._current != _current;
}
LinkedListIterator<T> & operator++() {
if ( _current!=0) _current = _current->next;
return *this;
}
// Postfix operator takes an argument (that we do not use)
LinkedListIterator<T> operator++(int) {
LinkedListIterator<T> clone(*this);
++(*this);
return clone;
}
T & operator*() {
return _current->value;
}
T * operator->() {
return &(operator*());
}
};
template <class T> class LinkedListConstIterator {
protected:
friend class LinkedListIterator<T>;
typename LinkedList<T>::node const *_current;
public:
LinkedListConstIterator(typename LinkedList<T>::node const *node) : _current(node) {};
bool operator==(LinkedListConstIterator<T> l) {
return l._current == _current;
}
bool operator!=(LinkedListConstIterator<T> l) {
return l._current != _current;
}
LinkedListConstIterator<T> & operator++() {
if ( _current!=0) _current = _current->next;
return *this;
}
// Postfix operator takes an argument (that we do not use)
LinkedListConstIterator<T> operator++(int) {
LinkedListConstIterator<T> clone(*this);
_current = _current->next;
return clone;
}
const T & operator*() {
return _current->value;
}
};
template <class T> class LinkedListCircularIterator : public LinkedListIterator<T> {
protected:
typename LinkedList<T>::node *_head;
public:
LinkedListCircularIterator(typename LinkedList<T>::node *head) : LinkedListIterator<T>(head), _head(head) {};
LinkedListCircularIterator<T> & operator++() {
if (this->_current->next) {
this->_current = this->_current->next;
}
else {
this->_current = this->_head;
}
return *this;
};
LinkedListIterator<T> operator++(int) {
LinkedListIterator<T> clone(*this);
++(*this);
return clone;
};
};
#ifdef DEBUG_TEST_LINKED_LIST
LinkedList<double> 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 <class T>
void MoveListItemForward(LinkedList<T> &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 <class T>
void MoveListItemBack(LinkedList<T> &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
}

356
src/N2kDataToNMEA0183.cpp Normal file
View File

@ -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 <N2kMessages.h>
#include <NMEA0183Messages.h>
#include <math.h>
#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);
}
}

113
src/N2kDataToNMEA0183.h Normal file
View File

@ -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 <NMEA0183.h>
#include <NMEA2000.h>
#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);
};

156
src/index_html.h Normal file
View File

@ -0,0 +1,156 @@
const char indexHTML[] PROGMEM = R"=====(
<!DOCTYPE html>
<html><head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
<title>ADC Anzeigeinstrument</title>
<script src="gauge.min.js"></script>
<script type="text/javascript">
var myAjax = false;
window.onload = start;
function start ()
{
Url="/";
if (window.XMLHttpRequest) { // Mozilla, Safari,...
myAjax = new XMLHttpRequest();
if (myAjax.overrideMimeType) {
myAjax.overrideMimeType('text/xml');
}
} else if (window.ActiveXObject) { // IE
try {
myAjax = new ActiveXObject("Msxml2.XMLHTTP");
} catch (e) {
try {
myAjax = new ActiveXObject("Microsoft.XMLHTTP");
} catch (e) {}
}
}
myAjax.onreadystatechange=LesenAjax;
}
function aktualisieren()
{
document.getElementById('Online').style.background = "red";
if (!myAjax)
{
return false;
}
else
{
myAjax.open("GET",Url+"ADC.txt",true);
myAjax.send();
}
}
function LesenAjax()
{
if (myAjax.readyState==4 && myAjax.status==200)
{
var Temp=0.5;
Temp = parseFloat (myAjax.responseText);
Gauge.Collection.get('gauge1').setValue(Temp);
document.getElementById('Online').style.background = "#ddd";
}
}
</script>
<style type="text/css">
#wrap
{
text-align: center;
vertical-align: middle;
max-width: 900px;
margin:0 auto;
background:#dcd;
border-radius: 10px;
padding:10px; /*innen*/
}
.Form-Einstellungen
{
border: 1px solid #666666;
margin: 10px; /*aussen*/
padding:5px; /*innen*/
border-radius: 10px;
background:#ddd;
}
.but
{
border-radius: 5px;
background:lightgreen;
}
#Online
{
display: inline-block;
float: left;
border: 1px solid #666666;
border-radius: 10px;
width: 20px;
}
</style>
</head><body>
<div id="wrap">
<div class="Form-Einstellungen">
Anzeigeinstrument für Kühlschranktemperatur
<div id="Online"> &ensp;</div>
</div>
<div class="Anzeige">
<canvas id="gauge1" width="500" height="500"
data-type="canv-gauge"
data-title="Temperatur"
data-min-value="-4"
data-max-value="15"
data-major-ticks="-4 -3 -2 -1 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15"
data-minor-ticks="2"
data-stroke-ticks="true"
data-units="°C"
data-value-format="2.2"
data-glow="true"
data-animation-delay="5"
data-animation-duration="2000"
data-animation-fn="bounce"
data-colors-needle="#f00 #00f"
data-highlights="0 30 #fff"
data-onready="setInterval(aktualisieren,1000);"
></canvas><br>
</div>
</div>
</body>
</html>
)=====" ;
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=t<u?t:u;A.i8d=!1;B.translate(t,u);B.save();a.translate(t,u);a.save()}function v(a){var b=new Date;G=setInterval(function(){var c=(new Date-b)/a.duration;1<c&&(c=1);var f=("function"==
typeof a.delta?a.delta:M[a.delta])(c);a.step(f);1==c&&clearInterval(G)},a.delay||10)}function k(){G&&clearInterval(G);var a=I-n,h=n,c=b.animation;v({delay:c.delay,duration:c.duration,delta:c.fn,step:function(b){n=parseFloat(h)+a*b;E.draw()}})}function e(a){return a*Math.PI/180}function g(b,h,c){c=a.createLinearGradient(0,0,0,c);c.addColorStop(0,b);c.addColorStop(1,h);return c}function p(){var m=93*(f/100),h=f-m,c=91*(f/100),e=88*(f/100),d=85*(f/100);a.save();b.glow&&(a.shadowBlur=h,a.shadowColor=
"rgba(0, 0, 0, 0.5)");a.beginPath();a.arc(0,0,m,0,2*Math.PI,!0);a.fillStyle=g("#ddd","#aaa",m);a.fill();a.restore();a.beginPath();a.arc(0,0,c,0,2*Math.PI,!0);a.fillStyle=g("#fafafa","#ccc",c);a.fill();a.beginPath();a.arc(0,0,e,0,2*Math.PI,!0);a.fillStyle=g("#eee","#f0f0f0",e);a.fill();a.beginPath();a.arc(0,0,d,0,2*Math.PI,!0);a.fillStyle=b.colors.plate;a.fill();a.save()}function w(a){var h=!1;a=0===b.majorTicksFormat.dec?Math.round(a).toString():a.toFixed(b.majorTicksFormat.dec);return 1<b.majorTicksFormat["int"]?
(h=-1<a.indexOf("."),-1<a.indexOf("-")?"-"+(b.majorTicksFormat["int"]+b.majorTicksFormat.dec+2+(h?1:0)-a.length)+a.replace("-",""):""+(b.majorTicksFormat["int"]+b.majorTicksFormat.dec+1+(h?1:0)-a.length)+a):a}function d(){var m=81*(f/100);a.lineWidth=2;a.strokeStyle=b.colors.majorTicks;a.save();if(0===b.majorTicks.length){for(var h=(b.maxValue-b.minValue)/5,c=0;5>c;c++)b.majorTicks.push(w(b.minValue+h*c));b.majorTicks.push(w(b.maxValue))}for(c=0;c<b.majorTicks.length;++c)a.rotate(e(45+c*(270/(b.majorTicks.length-
1)))),a.beginPath(),a.moveTo(0,m),a.lineTo(0,m-15*(f/100)),a.stroke(),a.restore(),a.save();b.strokeTicks&&(a.rotate(e(90)),a.beginPath(),a.arc(0,0,m,e(45),e(315),!1),a.stroke(),a.restore(),a.save())}function J(){var m=81*(f/100);a.lineWidth=1;a.strokeStyle=b.colors.minorTicks;a.save();for(var h=b.minorTicks*(b.majorTicks.length-1),c=0;c<h;++c)a.rotate(e(45+c*(270/h))),a.beginPath(),a.moveTo(0,m),a.lineTo(0,m-7.5*(f/100)),a.stroke(),a.restore(),a.save()}function s(){for(var m=55*(f/100),h=0;h<b.majorTicks.length;++h){var c=
F(m,e(45+h*(270/(b.majorTicks.length-1))));a.font=20*(f/200)+"px Arial";a.fillStyle=b.colors.numbers;a.lineWidth=0;a.textAlign="center";a.fillText(b.majorTicks[h],c.x,c.y+3)}}function x(a){var h=b.valueFormat.dec,c=b.valueFormat["int"];a=parseFloat(a);var f=0>a;a=Math.abs(a);if(0<h){a=a.toFixed(h).toString().split(".");h=0;for(c-=a[0].length;h<c;++h)a[0]="0"+a[0];a=(f?"-":"")+a[0]+"."+a[1]}else{a=Math.round(a).toString();h=0;for(c-=a.length;h<c;++h)a="0"+a;a=(f?"-":"")+a}return a}function F(a,b){var c=
Math.sin(b),f=Math.cos(b);return{x:0*f-a*c,y:0*c+a*f}}function N(){a.save();for(var m=81*(f/100),h=m-15*(f/100),c=0,g=b.highlights.length;c<g;c++){var d=b.highlights[c],r=(b.maxValue-b.minValue)/270,k=e(45+(d.from-b.minValue)/r),r=e(45+(d.to-b.minValue)/r);a.beginPath();a.rotate(e(90));a.arc(0,0,m,k,r,!1);a.restore();a.save();var l=F(h,k),p=F(m,k);a.moveTo(l.x,l.y);a.lineTo(p.x,p.y);var p=F(m,r),n=F(h,r);a.lineTo(p.x,p.y);a.lineTo(n.x,n.y);a.lineTo(l.x,l.y);a.closePath();a.fillStyle=d.color;a.fill();
a.beginPath();a.rotate(e(90));a.arc(0,0,h,k-0.2,r+0.2,!1);a.restore();a.closePath();a.fillStyle=b.colors.plate;a.fill();a.save()}}function K(){var m=12*(f/100),h=8*(f/100),c=77*(f/100),d=20*(f/100),k=4*(f/100),r=2*(f/100),l=function(){a.shadowOffsetX=2;a.shadowOffsetY=2;a.shadowBlur=10;a.shadowColor="rgba(188, 143, 143, 0.45)"};l();a.save();n=0>n?Math.abs(b.minValue-n):0<b.minValue?n-b.minValue:Math.abs(b.minValue)+n;a.rotate(e(45+n/((b.maxValue-b.minValue)/270)));a.beginPath();a.moveTo(-r,-d);a.lineTo(-k,
0);a.lineTo(-1,c);a.lineTo(1,c);a.lineTo(k,0);a.lineTo(r,-d);a.closePath();a.fillStyle=g(b.colors.needle.start,b.colors.needle.end,c-d);a.fill();a.beginPath();a.lineTo(-0.5,c);a.lineTo(-1,c);a.lineTo(-k,0);a.lineTo(-r,-d);a.lineTo(r/2-2,-d);a.closePath();a.fillStyle="rgba(255, 255, 255, 0.2)";a.fill();a.restore();l();a.beginPath();a.arc(0,0,m,0,2*Math.PI,!0);a.fillStyle=g("#f0f0f0","#ccc",m);a.fill();a.restore();a.beginPath();a.arc(0,0,h,0,2*Math.PI,!0);a.fillStyle=g("#e8e8e8","#f5f5f5",h);a.fill()}
function L(){a.save();a.font=40*(f/200)+"px Led";var b=x(y),h=a.measureText("-"+x(0)).width,c=f-33*(f/100),g=0.12*f;a.save();var d=-h/2-0.025*f,e=c-g-0.04*f,h=h+0.05*f,g=g+0.07*f,k=0.025*f;a.beginPath();a.moveTo(d+k,e);a.lineTo(d+h-k,e);a.quadraticCurveTo(d+h,e,d+h,e+k);a.lineTo(d+h,e+g-k);a.quadraticCurveTo(d+h,e+g,d+h-k,e+g);a.lineTo(d+k,e+g);a.quadraticCurveTo(d,e+g,d,e+g-k);a.lineTo(d,e+k);a.quadraticCurveTo(d,e,d+k,e);a.closePath();d=a.createRadialGradient(0,c-0.12*f-0.025*f+(0.12*f+0.045*f)/
2,f/10,0,c-0.12*f-0.025*f+(0.12*f+0.045*f)/2,f/5);d.addColorStop(0,"#888");d.addColorStop(1,"#666");a.strokeStyle=d;a.lineWidth=0.05*f;a.stroke();a.shadowBlur=0.012*f;a.shadowColor="rgba(0, 0, 0, 1)";a.fillStyle="#babab2";a.fill();a.restore();a.shadowOffsetX=0.004*f;a.shadowOffsetY=0.004*f;a.shadowBlur=0.012*f;a.shadowColor="rgba(0, 0, 0, 0.3)";a.fillStyle="#444";a.textAlign="center";a.fillText(b,-0,c);a.restore()}Gauge.Collection.push(this);this.config={renderTo:null,width:200,height:200,title:!1,
maxValue:100,minValue:0,majorTicks:[],minorTicks:10,strokeTicks:!0,units:!1,valueFormat:{"int":3,dec:2},majorTicksFormat:{"int":1,dec:0},glow:!0,animation:{delay:10,duration:250,fn:"cycle"},colors:{plate:"#fff",majorTicks:"#444",minorTicks:"#666",title:"#888",units:"#888",numbers:"#444",needle:{start:"rgba(240, 128, 128, 1)",end:"rgba(255, 160, 122, .9)"}},highlights:[{from:20,to:60,color:"#eee"},{from:60,to:80,color:"#ccc"},{from:80,to:100,color:"#999"}]};var y=0,E=this,n=0,I=0,H=!1;this.setValue=
function(a){n=b.animation?y:a;var d=(b.maxValue-b.minValue)/100;I=a>b.maxValue?b.maxValue+d:a<b.minValue?b.minValue-d:a;y=a;b.animation?k():this.draw();return this};this.setRawValue=function(a){n=y=a;this.draw();return this};this.clear=function(){y=n=I=this.config.minValue;this.draw();return this};this.getValue=function(){return y};this.onready=function(){};l(this.config,b);this.config.minValue=parseFloat(this.config.minValue);this.config.maxValue=parseFloat(this.config.maxValue);b=this.config;n=
y=b.minValue;if(!b.renderTo)throw Error("Canvas element was not specified when creating the Gauge object!");var z=b.renderTo.tagName?b.renderTo:document.getElementById(b.renderTo),a=z.getContext("2d"),A,C,D,t,u,f,B;q();this.updateConfig=function(a){l(this.config,a);q();this.draw();return this};var M={linear:function(a){return a},quad:function(a){return Math.pow(a,2)},quint:function(a){return Math.pow(a,5)},cycle:function(a){return 1-Math.sin(Math.acos(a))},bounce:function(a){a:{a=1-a;for(var b=0,
c=1;;b+=c,c/=2)if(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<q;l++){if((this[l].config.renderTo.tagName?this[l].config.renderTo:document.getElementById(this[l].config.renderTo)).getAttribute("id")==b)return this[l]}else return"number"==typeof b?this[b]:null};function domReady(b){window.addEventListener?window.addEventListener("DOMContentLoaded",b,!1):window.attachEvent("onload",b)}
domReady(function(){function b(b){for(var e=b[0],d=1,g=b.length;d<g;d++)e+=b[d].substr(0,1).toUpperCase()+b[d].substr(1,b[d].length-1);return e}for(var l=document.getElementsByTagName("canvas"),q=0,v=l.length;q<v;q++)if("canv-gauge"==l[q].getAttribute("data-type")){var k=l[q],e={},g,p=parseInt(k.getAttribute("width"),10),w=parseInt(k.getAttribute("height"),10);e.renderTo=k;p&&(e.width=p);w&&(e.height=w);p=0;for(w=k.attributes.length;p<w;p++)if(g=k.attributes.item(p).nodeName,"data-type"!=g&&"data-"==
g.substr(0,5)){var d=g.substr(5,g.length-5).toLowerCase().split("-");if(g=k.getAttribute(g))switch(d[0]){case "colors":d[1]&&(e.colors||(e.colors={}),"needle"==d[1]?(d=g.split(/\s+/),e.colors.needle=d[0]&&d[1]?{start:d[0],end:d[1]}:g):(d.shift(),e.colors[b(d)]=g));break;case "highlights":e.highlights||(e.highlights=[]);g=g.match(/(?:(?:-?\d*\.)?(-?\d+){1,2} ){2}(?:(?:#|0x)?(?:[0-9A-F|a-f]){3,8}|rgba?\(.*?\))/g);for(var d=0,J=g.length;d<J;d++){var s=g[d].replace(/^\s+|\s+$/g,"").split(/\s+/),x={};
s[0]&&""!=s[0]&&(x.from=s[0]);s[1]&&""!=s[1]&&(x.to=s[1]);s[2]&&""!=s[2]&&(x.color=s[2]);e.highlights.push(x)}break;case "animation":d[1]&&(e.animation||(e.animation={}),"fn"==d[1]&&/^\s*function\s*\(/.test(g)&&(g=eval("("+g+")")),e.animation[d[1]]=g);break;default:d=b(d);if("onready"==d)continue;if("majorTicks"==d)g=g.split(/\s+/);else if("strokeTicks"==d||"glow"==d)g="true"==g?!0:!1;else if("valueFormat"==d)if(g=g.split("."),2==g.length)g={"int":parseInt(g[0],10),dec:parseInt(g[1],10)};else continue;
e[d]=g}}e=new Gauge(e);k.getAttribute("data-value")&&e.setRawValue(parseFloat(k.getAttribute("data-value")));k.getAttribute("data-onready")&&(e.onready=function(){eval(this.config.renderTo.getAttribute("data-onready"))});e.draw()}});window.Gauge=Gauge;
)=====" ;

495
src/main.cpp Normal file
View File

@ -0,0 +1,495 @@
/*
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
#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
#include <Arduino.h>
#include <NMEA2000_CAN.h> // This will automatically choose right CAN library and create suitable NMEA2000 object
#include <Seasmart.h>
#include <N2kMessages.h>
#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>
#include <ArduinoJson.h>
#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<WiFiClient>;
LinkedList<tWiFiClientPtr> 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://<ip address>/" aufgerufen wurde
{
webserver.send(200, "text/html", indexHTML); //dann Index Webseite senden
}
void Ereignis_js() // Wenn "http://<ip address>/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<tWiFiClientPtr>::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();
}
}
}