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 () {