diff --git a/lib/boatData/GwBoatData.cpp b/lib/boatData/GwBoatData.cpp index 467d71b..65ba442 100644 --- a/lib/boatData/GwBoatData.cpp +++ b/lib/boatData/GwBoatData.cpp @@ -1,4 +1,256 @@ #include "GwBoatData.h" +#include <ArduinoJson/Json/TextFormatter.hpp> +#define GWTYPE_DOUBLE 1 +#define GWTYPE_UINT32 2 +#define GWTYPE_UINT16 3 +#define GWTYPE_INT16 4 +#define GWTYPE_USER 100 +class GwBoatItemTypes{ + public: + static int getType(const uint32_t &x){return GWTYPE_UINT32;} + static int getType(const uint16_t &x){return GWTYPE_UINT16;} + static int getType(const int16_t &x){return GWTYPE_INT16;} + static int getType(const double &x){return GWTYPE_DOUBLE;} + static int getType(const GwSatInfoList &x){ return GWTYPE_USER+1;} +}; + +bool GwBoatItemBase::isValid(unsigned long now) const +{ + if (lastSet == 0) + return false; + if (invalidTime == 0) + return true; + if (now == 0) + now = millis(); + return (lastSet + invalidTime) >= now; +} +GwBoatItemBase::GwBoatItemBase(String name, String format, unsigned long invalidTime) +{ + lastSet = 0; + this->invalidTime = invalidTime; + this->name = name; + this->format = format; + this->type = 0; + this->lastUpdateSource=-1; +} +#define STRING_SIZE 40 +GwBoatItemBase::StringWriter::StringWriter(){ + buffer=new uint8_t[STRING_SIZE]; + wp=buffer; + bufferSize=STRING_SIZE; + buffer [0]=0; +}; +const char *GwBoatItemBase::StringWriter::c_str() const{ + return (const char *)buffer; +} +int GwBoatItemBase::StringWriter::getSize() const{ + return wp-buffer; +} +void GwBoatItemBase::StringWriter::reset(){ + wp=buffer; + *wp=0; +} +void GwBoatItemBase::StringWriter::ensure(size_t size){ + size_t fill=wp-buffer; + size_t newSize=bufferSize; + while ((fill+size) >= (newSize-1) ){ + newSize+=STRING_SIZE; + } + if (newSize != bufferSize){ + uint8_t *newBuffer=new uint8_t[newSize]; + memcpy(newBuffer,buffer,fill); + newBuffer[fill]=0; + delete buffer; + buffer=newBuffer; + wp=newBuffer+fill; + bufferSize=newSize; + } +} +size_t GwBoatItemBase::StringWriter::write(uint8_t c){ + ensure(1); + *wp=c; + wp++; + *wp=0; + return 1; +} +size_t GwBoatItemBase::StringWriter::write(const uint8_t* s, size_t n){ + ensure(n); + memcpy(wp,s,n); + wp+=n; + *wp=0; + return n; +} +template<class T> GwBoatItem<T>::GwBoatItem(String name,String formatInfo,unsigned long invalidTime,GwBoatItemMap *map): + GwBoatItemBase(name,formatInfo,invalidTime){ + T dummy; + this->type=GwBoatItemTypes::getType(dummy); + if (map){ + (*map)[name]=this; + } +} + +template <class T> +bool GwBoatItem<T>::update(T nv, int source) +{ + unsigned long now = millis(); + if (isValid(now)) + { + //priority handling + //sources with lower ids will win + //and we will not overwrite their value + if (lastUpdateSource < source && lastUpdateSource >= 0) + { + return false; + } + } + data = nv; + lastUpdateSource = source; + uls(now); + return true; +} +template <class T> +bool GwBoatItem<T>::updateMax(T nv, int sourceId) +{ + unsigned long now = millis(); + if (!isValid(now)) + { + return update(nv, sourceId); + } + if (getData() < nv) + { + data = nv; + lastUpdateSource = sourceId; + uls(now); + return true; + } + return false; +} +template <class T> +void GwBoatItem<T>::toJsonDoc(JsonDocument *doc, unsigned long minTime) +{ + JsonObject o = doc->createNestedObject(name); + o[F("value")] = getData(); + o[F("update")] = minTime - lastSet; + o[F("source")] = lastUpdateSource; + o[F("valid")] = isValid(minTime); + o[F("format")] = format; +} + + +class WriterWrapper{ + GwBoatItemBase::StringWriter *writer=NULL; + public: + WriterWrapper(GwBoatItemBase::StringWriter *w){ + writer=w; + } + size_t write(uint8_t c){ + if (! writer) return 0; + return writer->write(c); + } + size_t write(const uint8_t* s, size_t n){ + if (! writer) return 0; + return writer->write(s,n); + } +}; +typedef ARDUINOJSON_NAMESPACE::TextFormatter<WriterWrapper> GwTextWriter; + +static void writeToString(GwTextWriter *writer,const double &value){ + writer->writeFloat(value); +} +static void writeToString(GwTextWriter *writer,const uint16_t &value){ + writer->writeInteger(value); +} +static void writeToString(GwTextWriter *writer,const uint32_t &value){ + writer->writeInteger(value); +} +static void writeToString(GwTextWriter *writer,const int16_t &value){ + writer->writeInteger(value); +} +static void writeToString(GwTextWriter *writer,GwSatInfoList &value){ + writer->writeInteger(value.getNumSats()); +} + +template <class T> +void GwBoatItem<T>::fillString(){ + bool valid=isValid(); + if (writer.getSize() && (valid == lastStringValid)) return; + lastStringValid=valid; + writer.reset(); + WriterWrapper wrapper(&writer); + GwTextWriter stringWriter(wrapper); + stringWriter.writeRaw(name.c_str()); + stringWriter.writeChar(','); + stringWriter.writeInteger(valid?1:0); + stringWriter.writeChar(','); + stringWriter.writeInteger(lastSet); + stringWriter.writeChar(','); + stringWriter.writeInteger(lastUpdateSource); + stringWriter.writeChar(','); + stringWriter.writeRaw(format.c_str()); + stringWriter.writeChar(','); + writeToString(&stringWriter,data); +} + +template class GwBoatItem<double>; +template class GwBoatItem<uint32_t>; +template class GwBoatItem<uint16_t>; +template class GwBoatItem<int16_t>; +void GwSatInfoList::houseKeeping(unsigned long ts) +{ + if (ts == 0) + ts = millis(); + sats.erase(std::remove_if( + sats.begin(), + sats.end(), + [ts, this](const GwSatInfo &info) + { + return (info.timestamp + lifeTime) < ts; + }), + sats.end()); +} +void GwSatInfoList::update(GwSatInfo entry) +{ + unsigned long now = millis(); + entry.timestamp = now; + for (auto it = sats.begin(); it != sats.end(); it++) + { + if (it->PRN == entry.PRN) + { + *it = entry; + houseKeeping(); + return; + } + } + houseKeeping(); + sats.push_back(entry); +} + +GwBoatDataSatList::GwBoatDataSatList(String name, String formatInfo, unsigned long invalidTime , GwBoatItemMap *map) : + GwBoatItem<GwSatInfoList>(name, formatInfo, invalidTime, map) {} + +bool GwBoatDataSatList::update(GwSatInfo info, int source) +{ + unsigned long now = millis(); + if (isValid(now)) + { + //priority handling + //sources with lower ids will win + //and we will not overwrite their value + if (lastUpdateSource < source) + { + return false; + } + } + lastUpdateSource = source; + uls(now); + data.update(info); + return true; +} +void GwBoatDataSatList::toJsonDoc(JsonDocument *doc, unsigned long minTime) +{ + data.houseKeeping(); + GwBoatItem<GwSatInfoList>::toJsonDoc(doc, minTime); +} GwBoatData::GwBoatData(GwLog *logger){ this->logger=logger; @@ -23,7 +275,7 @@ template<class T> GwBoatItem<T> *GwBoatData::getOrCreate(T initial, GwBoatItemNa } return (GwBoatItem<T>*)(it->second); } - GwBoatItem<T> *rt=new GwBoatItem<T>(GwBoatItemTypes::getType(initial), name, + GwBoatItem<T> *rt=new GwBoatItem<T>(name, provider->getBoatItemFormat(), provider->getInvalidTime(), &values); @@ -65,7 +317,14 @@ String GwBoatData::toJson() const { serializeJson(json,buf); return buf; } - +String GwBoatData::toString(){ + String rt; + for (auto it=values.begin() ; it != values.end();it++){ + rt+=it->second->getDataString(); + rt+="\n"; + } + return rt; +} double formatCourse(double cv) { double rt = cv * 180.0 / M_PI; @@ -101,4 +360,19 @@ double mtr2nm(double m) bool convertToJson(const GwSatInfoList &si,JsonVariant &variant){ return variant.set(si.getNumSats()); -} \ No newline at end of file +} + +#ifdef _UNDEF +#include <ArduinoJson/Json/TextFormatter.hpp> + +class XWriter{ + public: + void write(uint8_t c) { + } + + void write(const uint8_t* s, size_t n) { + } +}; +static XWriter xwriter; +ARDUINOJSON_NAMESPACE::TextFormatter<XWriter> testWriter(xwriter); +#endif \ No newline at end of file diff --git a/lib/boatData/GwBoatData.h b/lib/boatData/GwBoatData.h index 866d086..c2e68d6 100644 --- a/lib/boatData/GwBoatData.h +++ b/lib/boatData/GwBoatData.h @@ -7,20 +7,22 @@ #include <map> #define GW_BOAT_VALUE_LEN 32 #define GWSC(name) static constexpr const __FlashStringHelper* name=F(#name) -#define GWTYPE_DOUBLE 1 -#define GWTYPE_UINT32 2 -#define GWTYPE_UINT16 3 -#define GWTYPE_INT16 4 -#define GWTYPE_USER 100 -class GwBoatItemTypes{ - public: - static int getType(const uint32_t &x){return GWTYPE_UINT32;} - static int getType(const uint16_t &x){return GWTYPE_UINT16;} - static int getType(const int16_t &x){return GWTYPE_INT16;} - static int getType(const double &x){return GWTYPE_DOUBLE;} -}; + class GwBoatItemBase{ public: + class StringWriter{ + uint8_t *buffer =NULL; + uint8_t *wp=NULL; + size_t bufferSize=0; + void ensure(size_t size); + public: + StringWriter(); + size_t write(uint8_t c); + size_t write(const uint8_t* s, size_t n); + const char * c_str() const; + int getSize() const; + void reset(); + }; static const unsigned long INVALID_TIME=60000; //the formatter names that must be known in js GWSC(formatCourse); @@ -42,33 +44,30 @@ class GwBoatItemBase{ unsigned long invalidTime=INVALID_TIME; String name; String format; + StringWriter writer; void uls(unsigned long ts=0){ if (ts) lastSet=ts; else lastSet=millis(); + writer.reset(); //value has changed } + int lastUpdateSource; public: int getCurrentType(){return type;} unsigned long getLastSet() const {return lastSet;} - bool isValid(unsigned long now=0) const { - if (lastSet == 0) return false; - if (invalidTime == 0) return true; - if (now == 0) now=millis(); - return (lastSet + invalidTime) >= now; - } - GwBoatItemBase(String name,String format,unsigned long invalidTime=INVALID_TIME){ - lastSet=0; - this->invalidTime=invalidTime; - this->name=name; - this->format=format; - this->type=0; - } + bool isValid(unsigned long now=0) const ; + GwBoatItemBase(String name,String format,unsigned long invalidTime=INVALID_TIME); virtual ~GwBoatItemBase(){} void invalidate(){ lastSet=0; } + const char *getDataString(){ + fillString(); + return writer.c_str(); + } + virtual void fillString()=0; virtual void toJsonDoc(JsonDocument *doc, unsigned long minTime)=0; virtual size_t getJsonSize(){return JSON_OBJECT_SIZE(10);} - virtual int getLastSource()=0; + virtual int getLastSource(){return lastUpdateSource;} virtual void refresh(unsigned long ts=0){uls(ts);} String getName(){return name;} }; @@ -76,45 +75,12 @@ class GwBoatData; template<class T> class GwBoatItem : public GwBoatItemBase{ protected: T data; - int lastUpdateSource; + bool lastStringValid=false; public: - GwBoatItem(int type,String name,String formatInfo,unsigned long invalidTime=INVALID_TIME,GwBoatItemMap *map=NULL): - GwBoatItemBase(name,formatInfo,invalidTime){ - this->type=type; - if (map){ - (*map)[name]=this; - } - lastUpdateSource=-1; - } + GwBoatItem(String name,String formatInfo,unsigned long invalidTime=INVALID_TIME,GwBoatItemMap *map=NULL); virtual ~GwBoatItem(){} - bool update(T nv, int source=-1){ - unsigned long now=millis(); - if (isValid(now)){ - //priority handling - //sources with lower ids will win - //and we will not overwrite their value - if (lastUpdateSource < source && lastUpdateSource >= 0){ - return false; - } - } - data=nv; - lastUpdateSource=source; - uls(now); - return true; - } - bool updateMax(T nv,int sourceId=-1){ - unsigned long now=millis(); - if (! isValid(now)){ - return update(nv,sourceId); - } - if (getData() < nv){ - data=nv; - lastUpdateSource=sourceId; - uls(now); - return true; - } - return false; - } + bool update(T nv, int source=-1); + bool updateMax(T nv,int sourceId=-1); T getData(){ return data; } @@ -122,14 +88,8 @@ template<class T> class GwBoatItem : public GwBoatItemBase{ if (! isValid(millis())) return defaultv; return data; } - virtual void toJsonDoc(JsonDocument *doc, unsigned long minTime){ - JsonObject o=doc->createNestedObject(name); - o[F("value")]=getData(); - o[F("update")]=minTime-lastSet; - o[F("source")]=lastUpdateSource; - o[F("valid")]=isValid(minTime); - o[F("format")]=format; - } + virtual void fillString(); + virtual void toJsonDoc(JsonDocument *doc, unsigned long minTime); virtual int getLastSource(){return lastUpdateSource;} }; double formatCourse(double cv); @@ -151,29 +111,8 @@ class GwSatInfoList{ public: static const unsigned long lifeTime=32000; std::vector<GwSatInfo> sats; - void houseKeeping(unsigned long ts=0){ - if (ts == 0) ts=millis(); - sats.erase(std::remove_if( - sats.begin(), - sats.end(), - [ts,this](const GwSatInfo &info){ - return (info.timestamp + lifeTime) < ts; - } - ),sats.end()); - } - void update(GwSatInfo entry){ - unsigned long now=millis(); - entry.timestamp=now; - for (auto it=sats.begin();it!=sats.end();it++){ - if (it->PRN == entry.PRN){ - *it=entry; - houseKeeping(); - return; - } - } - houseKeeping(); - sats.push_back(entry); - } + void houseKeeping(unsigned long ts=0); + void update(GwSatInfo entry); int getNumSats() const{ return sats.size(); } @@ -183,34 +122,12 @@ class GwSatInfoList{ } }; -bool convertToJson(const GwSatInfoList &si,JsonVariant &variant); class GwBoatDataSatList : public GwBoatItem<GwSatInfoList> { public: - GwBoatDataSatList(String name, String formatInfo, unsigned long invalidTime = INVALID_TIME, GwBoatItemMap *map = NULL) : - GwBoatItem<GwSatInfoList>(GWTYPE_USER+1, name, formatInfo, invalidTime, map) {} - bool update(GwSatInfo info, int source) - { - unsigned long now = millis(); - if (isValid(now)) - { - //priority handling - //sources with lower ids will win - //and we will not overwrite their value - if (lastUpdateSource < source) - { - return false; - } - } - lastUpdateSource = source; - uls(now); - data.update(info); - return true; - } - virtual void toJsonDoc(JsonDocument *doc, unsigned long minTime){ - data.houseKeeping(); - GwBoatItem<GwSatInfoList>::toJsonDoc(doc,minTime); - } + GwBoatDataSatList(String name, String formatInfo, unsigned long invalidTime = INVALID_TIME, GwBoatItemMap *map = NULL); + bool update(GwSatInfo info, int source); + virtual void toJsonDoc(JsonDocument *doc, unsigned long minTime); GwSatInfo *getAt(int idx){ if (! isValid()) return NULL; return data.getAt(idx); @@ -231,7 +148,7 @@ public: virtual ~GwBoatItemNameProvider() {} }; #define GWBOATDATA(type,name,time,fmt) \ - GwBoatItem<type> *name=new GwBoatItem<type>(GwBoatItemTypes::getType((type)0),F(#name),GwBoatItemBase::fmt,time,&values) ; + GwBoatItem<type> *name=new GwBoatItem<type>(F(#name),GwBoatItemBase::fmt,time,&values) ; #define GWSPECBOATDATA(clazz,name,time,fmt) \ clazz *name=new clazz(F(#name),GwBoatItemBase::fmt,time,&values) ; class GwBoatData{ @@ -283,6 +200,7 @@ class GwBoatData{ template<class T> bool update(T value,int source,GwBoatItemNameProvider *provider); template<class T> T getDataWithDefault(T defaultv, GwBoatItemNameProvider *provider); String toJson() const; + String toString(); }; diff --git a/src/main.cpp b/src/main.cpp index a2fa2e0..dc73a54 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -420,6 +420,17 @@ protected: result = boatData.toJson(); } }; +class BoatDataStringRequest : public GwRequestMessage +{ +public: + BoatDataStringRequest() : GwRequestMessage(F("text/plain"),F("boatDataString")){}; + +protected: + virtual void processRequest() + { + result = boatData.toString(); + } +}; class XdrExampleRequest : public GwRequestMessage { @@ -562,6 +573,8 @@ void setup() { { return new ResetConfigRequest(); }); webserver.registerMainHandler("/api/boatData", [](AsyncWebServerRequest *request)->GwRequestMessage * { return new BoatDataRequest(); }); + webserver.registerMainHandler("/api/boatDataString", [](AsyncWebServerRequest *request)->GwRequestMessage * + { return new BoatDataStringRequest(); }); webserver.registerMainHandler("/api/xdrExample", [](AsyncWebServerRequest *request)->GwRequestMessage * { String mapping=request->arg("mapping"); diff --git a/web/index.js b/web/index.js index 2b76e50..697431a 100644 --- a/web/index.js +++ b/web/index.js @@ -1035,6 +1035,7 @@ function resizeFont(el,reset,maxIt){ } } function createDashboardItem(name, def, parent) { + if (! def.name) return; let frame = addEl('div', 'dash', parent); let title = addEl('span', 'dashTitle', frame, name); let value = addEl('span', 'dashValue', frame); @@ -1044,22 +1045,35 @@ function createDashboardItem(name, def, parent) { let footer = addEl('div','footer',frame); let src= addEl('span','source',footer); src.setAttribute('id','source_'+name); - let u=fmt?fmt.u:''; - if (! fmt && def.format.match(/formatXdr/)){ + let u=fmt?fmt.u:' '; + if (! fmt && def.format && def.format.match(/formatXdr/)){ u=def.format.replace(/formatXdr/,''); } addEl('span','unit',footer,u); return value; } +function parseBoatDataLine(line){ + let rt={}; + let parts=line.split(','); + rt.name=parts[0]; + rt.valid=parts[1] === '1'; + rt.update=parseInt(parts[2]); + rt.source=parseInt(parts[3]); + rt.format=parts[4]; + rt.value=parts[5]; + return rt; +} function createDashboard() { let frame = document.getElementById('dashboardPage'); if (!frame) return; - getJson("api/boatData").then(function (json) { + getText("api/boatDataString").then(function (txt) { frame.innerHTML = ''; - for (let n in json) { - createDashboardItem(n, json[n], frame); + let values=txt.split('\n'); + for (let n in values) { + let def=parseBoatDataLine(values[n]); + createDashboardItem(def.name, def, frame); } - updateDashboard(json); + updateDashboard(values); }); } function sourceName(v){ @@ -1071,30 +1085,34 @@ function sourceName(v){ } function updateDashboard(data) { let frame = document.getElementById('dashboardPage'); + let names={}; for (let n in data) { - let de = document.getElementById('data_' + n); + let current=parseBoatDataLine(data[n]); + if (! current.name) continue; + names[current.name]=true; + let de = document.getElementById('data_' + current.name); if (! de && frame){ - de=createDashboardItem(n,data[n],frame); + de=createDashboardItem(current.name,current,frame); } if (de) { let newContent='----'; - if (data[n].valid) { + if (current.valid) { let formatter; - if (data[n].format && data[n].format != "NULL") { - let key = data[n].format.replace(/^\&/, ''); + if (current.format && current.format != "NULL") { + let key = current.format.replace(/^\&/, ''); formatter = valueFormatters[key]; } if (formatter) { - newContent = formatter.f(data[n].value); + newContent = formatter.f(current.value); } else { - let v = parseFloat(data[n].value); + let v = parseFloat(current.value); if (!isNaN(v)) { v = v.toFixed(3) newContent = v; } else { - newContent = data[n].value; + newContent = current.value; } } } @@ -1104,15 +1122,16 @@ function updateDashboard(data) { resizeFont(de,true); } } - let src=document.getElementById('source_'+n); + let src=document.getElementById('source_'+current.name); if (src){ - src.textContent=sourceName(data[n].source); + src.textContent=sourceName(current.source); } } + console.log("update"); forEl('.dashValue',function(el){ let id=el.getAttribute('id'); if (id){ - if (! data[id.replace(/^data_/,'')]){ + if (! names[id.replace(/^data_/,'')]){ el.parentElement.remove(); } } @@ -1123,8 +1142,8 @@ window.setInterval(update, 1000); window.setInterval(function () { let dp = document.getElementById('dashboardPage'); if (dp.classList.contains('hidden')) return; - getJson('api/boatData').then(function (data) { - updateDashboard(data); + getText('api/boatDataString').then(function (data) { + updateDashboard(data.split('\n')); }); }, 1000); window.addEventListener('load', function () {