diff --git a/lib/obp60task/OBP60Formatter.cpp b/lib/obp60task/OBP60Formatter.cpp index cfdcc96..f51c5fa 100644 --- a/lib/obp60task/OBP60Formatter.cpp +++ b/lib/obp60task/OBP60Formatter.cpp @@ -55,6 +55,8 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ static int dayoffset = 0; double rawvalue = 0; + result.cvalue = value->value; + // Load configuration values String stimeZone = commondata.config->getString(commondata.config->timeZone); // [UTC -14.00...+12.00] double timeZone = stimeZone.toDouble(); @@ -149,6 +151,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ val = modf(val*3600.0/60.0, &intmin); modf(val*60.0,&intsec); snprintf(buffer, bsize, "%02.0f:%02.0f:%02.0f", inthr, intmin, intsec); + result.cvalue = timeInSeconds; } else{ static long sec; @@ -158,6 +161,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ } sec = sec % 60; snprintf(buffer, bsize, "11:36:%02i", int(sec)); + result.cvalue = sec; lasttime = millis(); } if(timeZone == 0){ @@ -178,6 +182,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ snprintf(buffer, bsize, "%3.0f", rawvalue); } result.unit = ""; + result.cvalue = rawvalue; } //######################################################## else if (value->getFormat() == "formatCourse" || value->getFormat() == "formatWind"){ @@ -195,6 +200,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ // Format 3 numbers with prefix zero snprintf(buffer,bsize,"%03.0f",course); result.unit = "Deg"; + result.cvalue = course; } //######################################################## else if (value->getFormat() == "formatKnots" && (value->getName() == "SOG" || value->getName() == "STW")){ @@ -228,6 +234,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ else { snprintf(buffer, bsize, fmt_dec_100, speed); } + result.cvalue = speed; } //######################################################## else if (value->getFormat() == "formatKnots" && (value->getName() == "AWS" || value->getName() == "TWS" || value->getName() == "MaxAws" || value->getName() == "MaxTws")){ @@ -298,16 +305,18 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ snprintf(buffer, bsize, "%2.0f", speed); } else{ - if (speed < 10){ + speed = std::round(speed * 100) / 100; // in rare cases, speed could be 9.9999 kn instead of 10.0 kn + if (speed < 10.0){ snprintf(buffer, bsize, fmt_dec_1, speed); } - else if (speed < 100){ + else if (speed < 100.0){ snprintf(buffer, bsize, fmt_dec_10, speed); } else { snprintf(buffer, bsize, fmt_dec_100, speed); } } + result.cvalue = speed; } //######################################################## else if (value->getFormat() == "formatRot"){ @@ -334,6 +343,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ if (rotation <= -10 || rotation >= 10){ snprintf(buffer, bsize, "%3.0f", rotation); } + result.cvalue = rotation; } //######################################################## else if (value->getFormat() == "formatDop"){ @@ -359,6 +369,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ else { snprintf(buffer, bsize, fmt_dec_100, dop); } + result.cvalue = dop; } //######################################################## else if (value->getFormat() == "formatLatitude"){ @@ -383,6 +394,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ rawvalue = 35.0 + float(random(0, 10)) / 10000.0; snprintf(buffer, bsize, " 51\" %2.4f' N", rawvalue); } + result.cvalue = rawvalue; } //######################################################## else if (value->getFormat() == "formatLongitude"){ @@ -407,6 +419,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ rawvalue = 6.0 + float(random(0, 10)) / 100000.0; snprintf(buffer, bsize, " 15\" %2.4f'", rawvalue); } + result.cvalue = rawvalue; } //######################################################## else if (value->getFormat() == "formatDepth"){ @@ -435,6 +448,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ else { snprintf(buffer, bsize, fmt_dec_100, depth); } + result.cvalue = depth; } //######################################################## else if (value->getFormat() == "formatXte"){ @@ -467,6 +481,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ if(xte >= 100){ snprintf(buffer,bsize,"%3.0f",xte); } + result.cvalue = xte; } //######################################################## else if (value->getFormat() == "kelvinToC"){ @@ -499,6 +514,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ else { snprintf(buffer, bsize, fmt_dec_100, temp); } + result.cvalue = temp; } //######################################################## else if (value->getFormat() == "mtr2nm"){ @@ -531,6 +547,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ else { snprintf(buffer, bsize, fmt_dec_100, distance); } + result.cvalue = distance; } //######################################################## // Special XDR formats @@ -549,6 +566,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ } snprintf(buffer, bsize, "%4.0f", pressure); result.unit = "hPa"; + result.cvalue = pressure; } //######################################################## else if (value->getFormat() == "formatXdr:P:B"){ @@ -564,6 +582,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ } snprintf(buffer, bsize, "%4.0f", pressure); result.unit = "mBar"; + result.cvalue = pressure; } //######################################################## else if (value->getFormat() == "formatXdr:U:V"){ @@ -583,6 +602,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ snprintf(buffer, bsize, fmt_dec_10, voltage); } result.unit = "V"; + result.cvalue = voltage; } //######################################################## else if (value->getFormat() == "formatXdr:I:A"){ @@ -605,6 +625,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ snprintf(buffer, bsize, fmt_dec_100, current); } result.unit = "A"; + result.cvalue = current; } //######################################################## else if (value->getFormat() == "formatXdr:C:K"){ @@ -627,6 +648,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ snprintf(buffer, bsize, fmt_dec_100, temperature); } result.unit = "Deg C"; + result.cvalue = temperature; } //######################################################## else if (value->getFormat() == "formatXdr:C:C"){ @@ -649,6 +671,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ snprintf(buffer, bsize, fmt_dec_100, temperature); } result.unit = "Deg C"; + result.cvalue = temperature; } //######################################################## else if (value->getFormat() == "formatXdr:H:P"){ @@ -671,6 +694,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ snprintf(buffer, bsize, fmt_dec_100, humidity); } result.unit = "%"; + result.cvalue = humidity; } //######################################################## else if (value->getFormat() == "formatXdr:V:P"){ @@ -693,6 +717,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ snprintf(buffer, bsize, fmt_dec_100, volume); } result.unit = "%"; + result.cvalue = volume; } //######################################################## else if (value->getFormat() == "formatXdr:V:M"){ @@ -715,6 +740,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ snprintf(buffer, bsize, fmt_dec_100, volume); } result.unit = "l"; + result.cvalue = volume; } //######################################################## else if (value->getFormat() == "formatXdr:R:I"){ @@ -737,6 +763,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ snprintf(buffer, bsize, fmt_dec_100, flow); } result.unit = "l/min"; + result.cvalue = flow; } //######################################################## else if (value->getFormat() == "formatXdr:G:"){ @@ -759,6 +786,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ snprintf(buffer, bsize, fmt_dec_100, generic); } result.unit = ""; + result.cvalue = generic; } //######################################################## else if (value->getFormat() == "formatXdr:A:P"){ @@ -781,6 +809,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ snprintf(buffer, bsize, fmt_dec_100, dplace); } result.unit = "%"; + result.cvalue = dplace; } //######################################################## else if (value->getFormat() == "formatXdr:A:D"){ @@ -801,6 +830,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ snprintf(buffer,bsize,"%3.0f",angle); } result.unit = "Deg"; + result.cvalue = angle; } //######################################################## else if (value->getFormat() == "formatXdr:T:R"){ @@ -823,6 +853,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ snprintf(buffer, bsize, fmt_dec_100, rpm); } result.unit = "rpm"; + result.cvalue = rpm; } //######################################################## // Default format @@ -838,6 +869,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ snprintf(buffer, bsize, fmt_dec_100, value->value); } result.unit = ""; + result.cvalue = value->value; } buffer[bsize] = 0; result.value = rawvalue; // Return value is only necessary in case of simulation of graphic pointer diff --git a/lib/obp60task/OBPDataOperations.cpp b/lib/obp60task/OBPDataOperations.cpp index 73908ac..c68ef53 100644 --- a/lib/obp60task/OBPDataOperations.cpp +++ b/lib/obp60task/OBPDataOperations.cpp @@ -1,15 +1,19 @@ #include "OBPDataOperations.h" +#include "BoatDataCalibration.h" // Functions lib for data instance calibration +#include // --- Class HstryBuf --------------- + // Init history buffers for selected boat data void HstryBuf::init(BoatValueList* boatValues, GwLog *log) { logger = log; int hstryUpdFreq = 1000; // Update frequency for history buffers in ms - int hstryMinVal = 0; // Minimum value for these history buffers - twdHstryMax = 6283; // Max value for wind direction (TWD, AWD) in rad [0...2*PI], shifted by 1000 for 3 decimals - twsHstryMax = 65000; // Max value for wind speed (TWS, AWS) in m/s [0..65], shifted by 1000 for 3 decimals + int mltplr = 1000; // Multiplier which transforms original value into buffer type format + double hstryMinVal = 0; // Minimum value for these history buffers + twdHstryMax = 2 * M_PI; // Max value for wind direction (TWD, AWD) in rad [0...2*PI] + twsHstryMax = 65; // Max value for wind speed (TWS, AWS) in m/s [0..65] (limit due to type capacity of buffer - shifted by ) awdHstryMax = twdHstryMax; awsHstryMax = twsHstryMax; twdHstryMin = hstryMinVal; @@ -19,10 +23,12 @@ void HstryBuf::init(BoatValueList* boatValues, GwLog *log) { const double DBL_MAX = std::numeric_limits::max(); // Initialize history buffers with meta data - hstryBufList.twdHstry->setMetaData("TWD", "formatCourse", hstryUpdFreq, hstryMinVal, twdHstryMax); - hstryBufList.twsHstry->setMetaData("TWS", "formatKnots", hstryUpdFreq, hstryMinVal, twsHstryMax); - hstryBufList.awdHstry->setMetaData("AWD", "formatCourse", hstryUpdFreq, hstryMinVal, twdHstryMax); - hstryBufList.awsHstry->setMetaData("AWS", "formatKnots", hstryUpdFreq, hstryMinVal, twsHstryMax); + mltplr = 10000; // Store 4 decimals for course data + hstryBufList.twdHstry->setMetaData("TWD", "formatCourse", hstryUpdFreq, mltplr, hstryMinVal, twdHstryMax); + hstryBufList.awdHstry->setMetaData("AWD", "formatCourse", hstryUpdFreq, mltplr, hstryMinVal, twdHstryMax); + mltplr = 1000; // Store 3 decimals for windspeed data + hstryBufList.twsHstry->setMetaData("TWS", "formatKnots", hstryUpdFreq, mltplr, hstryMinVal, twsHstryMax); + hstryBufList.awsHstry->setMetaData("AWS", "formatKnots", hstryUpdFreq, mltplr, hstryMinVal, twsHstryMax); // create boat values for history data types, if they don't exist yet twdBVal = boatValues->findValueOrCreate(hstryBufList.twdHstry->getName()); @@ -49,30 +55,32 @@ void HstryBuf::init(BoatValueList* boatValues, GwLog *log) { //void HstryBuf::handleHstryBuf(GwApi* api, BoatValueList* boatValues, bool useSimuData) { void HstryBuf::handleHstryBuf(bool useSimuData) { - static int16_t twd = 20; //initial value only relevant if we use simulation data - static uint16_t tws = 20; //initial value only relevant if we use simulation data - static double awd, aws, hdt = 20; //initial value only relevant if we use simulation data + static double twd, tws, awd, aws, hdt = 20; //initial value only relevant if we use simulation data GwApi::BoatValue *calBVal; // temp variable just for data calibration -> we don't want to calibrate the original data here LOG_DEBUG(GwLog::DEBUG,"obp60task handleHstryBuf: TWD_isValid? %d, twdBVal: %.1f, twaBVal: %.1f, twsBVal: %.1f", twdBVal->valid, twdBVal->value * RAD_TO_DEG, twaBVal->value * RAD_TO_DEG, twsBVal->value * 3.6 / 1.852); if (twdBVal->valid) { +// if (!useSimuData) { calBVal = new GwApi::BoatValue("TWD"); // temporary solution for calibration of history buffer values calBVal->setFormat(twdBVal->getFormat()); calBVal->value = twdBVal->value; calBVal->valid = twdBVal->valid; calibrationData.calibrateInstance(calBVal, logger); // Check if boat data value is to be calibrated - twd = static_cast(std::round(calBVal->value * 1000.0)); + twd = calBVal->value; if (twd >= twdHstryMin && twd <= twdHstryMax) { hstryBufList.twdHstry->add(twd); + LOG_DEBUG(GwLog::DEBUG,"obp60task handleHstryBuf: calBVal.value %.2f, twd: %.2f, twdHstryMin: %.1f, twdHstryMax: %.2f", calBVal->value, twd, twdHstryMin, twdHstryMax); } delete calBVal; calBVal = nullptr; } else if (useSimuData) { +// } else { twd += random(-20, 20); - twd = WindUtils::to360(twd); - hstryBufList.twdHstry->add(static_cast(DegToRad(twd) * 1000.0)); + twd += static_cast(random(-349, 349) / 1000.0); // add up to +/- 20 degree in RAD + twd = WindUtils::to2PI(twd); + hstryBufList.twdHstry->add(twd); } if (twsBVal->valid) { @@ -81,15 +89,16 @@ void HstryBuf::handleHstryBuf(bool useSimuData) { calBVal->value = twsBVal->value; calBVal->valid = twsBVal->valid; calibrationData.calibrateInstance(calBVal, logger); // Check if boat data value is to be calibrated - tws = static_cast(std::round(calBVal->value * 1000)); + tws = calBVal->value; if (tws >= twsHstryMin && tws <= twsHstryMax) { hstryBufList.twsHstry->add(tws); } delete calBVal; calBVal = nullptr; } else if (useSimuData) { - tws += random(-5000, 5000); // TWS value in m/s; expands to 3 decimals - tws = constrain(tws, 0, 25000); // Limit TWS to [0..25] m/s + // tws += random(-5000, 5000); // TWS value in m/s; expands to 3 decimals + tws += static_cast(random(-5000, 5000) / 1000.0); // add up to +/- 5 m/s TWS speed + tws = constrain(tws, 0, 40); // Limit TWS to [0..40] m/s hstryBufList.twsHstry->add(tws); } @@ -109,16 +118,16 @@ void HstryBuf::handleHstryBuf(bool useSimuData) { calibrationData.calibrateInstance(calBVal, logger); // Check if boat data value is to be calibrated awdBVal->value = calBVal->value; awdBVal->valid = true; - awd = std::round(calBVal->value * 1000.0); + awd = calBVal->value; if (awd >= awdHstryMin && awd <= awdHstryMax) { - hstryBufList.awdHstry->add(static_cast(awd)); + hstryBufList.awdHstry->add(awd); } delete calBVal; calBVal = nullptr; } else if (useSimuData) { - awd += random(-20, 20); - awd = WindUtils::to360(awd); - hstryBufList.awdHstry->add(static_cast(DegToRad(awd) * 1000.0)); + awd += static_cast(random(-349, 349) / 1000.0); // add up to +/- 20 degree in RAD + awd = WindUtils::to2PI(awd); + hstryBufList.awdHstry->add(awd); } if (awsBVal->valid) { @@ -127,26 +136,28 @@ void HstryBuf::handleHstryBuf(bool useSimuData) { calBVal->value = awsBVal->value; calBVal->valid = awsBVal->valid; calibrationData.calibrateInstance(calBVal, logger); // Check if boat data value is to be calibrated - aws = std::round(calBVal->value * 1000); + aws = calBVal->value; if (aws >= awsHstryMin && aws <= awsHstryMax) { - hstryBufList.awsHstry->add(static_cast(aws)); + hstryBufList.awsHstry->add(aws); } delete calBVal; calBVal = nullptr; } else if (useSimuData) { - aws += random(-5000, 5000); // TWS value in m/s; expands to 1 decimal - aws = constrain(aws, 0, 25000); // Limit TWS to [0..25] m/s + aws += static_cast(random(-5000, 5000) / 1000.0); // add up to +/- 5 m/s TWS speed + aws = constrain(aws, 0, 40); // Limit TWS to [0..40] m/s hstryBufList.awsHstry->add(aws); } + LOG_DEBUG(GwLog::DEBUG,"obp60task handleHstryBuf-End: Buffer twdHstry: %.3f, twsHstry: %.3f, awdHstry: %.3f, awsHstry: %.3f", hstryBufList.twdHstry->getLast(), hstryBufList.twsHstry->getLast(), + hstryBufList.awdHstry->getLast(),hstryBufList.awsHstry->getLast()); } // --- Class HstryBuf --------------- // --- Class WindUtils -------------- double WindUtils::to2PI(double a) { - a = fmod(a, 2 * M_PI); + a = fmod(a, M_TWOPI); if (a < 0.0) { - a += 2 * M_PI; + a += M_TWOPI; } return a; } @@ -162,18 +173,18 @@ double WindUtils::toPI(double a) double WindUtils::to360(double a) { - a = fmod(a, 360); + a = fmod(a, 360.0); if (a < 0.0) { - a += 360; + a += 360.0; } return a; } double WindUtils::to180(double a) { - a += 180; + a += 180.0; a = to360(a); - a -= 180; + a -= 180.0; return a; } @@ -263,7 +274,7 @@ bool WindUtils::calcTrueWind(const double* awaVal, const double* awsVal, // If STW and SOG are not available, we cannot calculate true wind return false; } - // Serial.println("\ncalcTrueWind: HDT: " + String(hdt) + ", CTW: " + String(ctw) + ", STW: " + String(stw)); +// Serial.println("\ncalcTrueWind: HDT: " + String(hdt) + ", CTW: " + String(ctw) + ", STW: " + String(stw)); if ((*awaVal == DBL_MAX) || (*awsVal == DBL_MAX)) { // Cannot calculate true wind without valid AWA, AWS; other checks are done earlier diff --git a/lib/obp60task/OBPDataOperations.h b/lib/obp60task/OBPDataOperations.h index 12158c1..8422894 100644 --- a/lib/obp60task/OBPDataOperations.h +++ b/lib/obp60task/OBPDataOperations.h @@ -1,14 +1,12 @@ +// Function lib for history buffer handling, true wind calculation, and other operations on boat data #pragma once -#include #include "OBPRingBuffer.h" -#include "BoatDataCalibration.h" // Functions lib for data instance calibration #include "obp60task.h" -#include typedef struct { - RingBuffer* twdHstry; + RingBuffer* twdHstry; RingBuffer* twsHstry; - RingBuffer* awdHstry; + RingBuffer* awdHstry; RingBuffer* awsHstry; } tBoatHstryData; // Holds pointers to all history buffers for boat data @@ -16,18 +14,18 @@ class HstryBuf { private: GwLog *logger; - RingBuffer twdHstry; // Circular buffer to store true wind direction values + RingBuffer twdHstry; // Circular buffer to store true wind direction values RingBuffer twsHstry; // Circular buffer to store true wind speed values (TWS) - RingBuffer awdHstry; // Circular buffer to store apparant wind direction values - RingBuffer awsHstry; // Circular buffer to store apparant xwind speed values (AWS) - int16_t twdHstryMin; // Min value for wind direction (TWD) in history buffer - int16_t twdHstryMax; // Max value for wind direction (TWD) in history buffer - uint16_t twsHstryMin; - uint16_t twsHstryMax; - int16_t awdHstryMin; - int16_t awdHstryMax; - uint16_t awsHstryMin; - uint16_t awsHstryMax; + RingBuffer awdHstry; // Circular buffer to store apparent wind direction values + RingBuffer awsHstry; // Circular buffer to store apparent xwind speed values (AWS) + double twdHstryMin; // Min value for wind direction (TWD) in history buffer + double twdHstryMax; // Max value for wind direction (TWD) in history buffer + double twsHstryMin; + double twsHstryMax; + double awdHstryMin; + double awdHstryMax; + double awsHstryMin; + double awsHstryMax; // boat values for buffers and for true wind calculation GwApi::BoatValue *twdBVal, *twsBVal, *twaBVal, *awdBVal, *awsBVal; @@ -71,6 +69,7 @@ public: hdmBVal = boatValues->findValueOrCreate("HDM"); varBVal = boatValues->findValueOrCreate("VAR"); }; + static double to2PI(double a); static double toPI(double a); static double to360(double a); diff --git a/lib/obp60task/OBPRingBuffer.h b/lib/obp60task/OBPRingBuffer.h index 4d13da6..970245e 100644 --- a/lib/obp60task/OBPRingBuffer.h +++ b/lib/obp60task/OBPRingBuffer.h @@ -1,11 +1,8 @@ #pragma once +#include "FreeRTOS.h" #include "GwSynchronized.h" -#include "WString.h" -#include "esp_heap_caps.h" -#include -#include -#include #include +#include template struct PSRAMAllocator { @@ -41,7 +38,6 @@ bool operator!=(const PSRAMAllocator&, const PSRAMAllocator&) { return fal template class RingBuffer { private: - // std::vector buffer; // THE buffer vector std::vector> buffer; // THE buffer vector, allocated in PSRAM size_t capacity; size_t head; // Points to the next insertion position @@ -51,49 +47,52 @@ private: bool is_Full; // Indicates that all buffer elements are used and ringing is in use T MIN_VAL; // lowest possible value of buffer of type T MAX_VAL; // highest possible value of buffer of type -> indicates invalid value in buffer + double dblMIN_VAL, dblMAX_VAL; // MIN_VAL, MAX_VAL in double format mutable SemaphoreHandle_t bufLocker; // metadata for buffer String dataName; // Name of boat data in buffer String dataFmt; // Format of boat data in buffer int updFreq; // Update frequency in milliseconds - T smallest; // Value range of buffer: smallest value; needs to be => MIN_VAL - T largest; // Value range of buffer: biggest value; needs to be < MAX_VAL, since MAX_VAL indicates invalid entries + double mltplr; // Multiplier which transforms original value into buffer type format + double smallest; // Value range of buffer: smallest value; needs to be => MIN_VAL + double largest; // Value range of buffer: biggest value; needs to be < MAX_VAL, since MAX_VAL indicates invalid entries void initCommon(); public: RingBuffer(); RingBuffer(size_t size); - void setMetaData(String name, String format, int updateFrequency, T minValue, T maxValue); // Set meta data for buffer - bool getMetaData(String& name, String& format, int& updateFrequency, T& minValue, T& maxValue); // Get meta data of buffer + void setMetaData(String name, String format, int updateFrequency, double multiplier, double minValue, double maxValue); // Set meta data for buffer + bool getMetaData(String& name, String& format, int& updateFrequency, double& multiplier, double& minValue, double& maxValue); // Get meta data of buffer bool getMetaData(String& name, String& format); String getName() const; // Get buffer name String getFormat() const; // Get buffer data format - void add(const T& value); // Add a new value to buffer - T get(size_t index) const; // Get value at specific position (0-based index from oldest to newest) - T getFirst() const; // Get the first (oldest) value in buffer - T getLast() const; // Get the last (newest) value in buffer - T getMin() const; // Get the lowest value in buffer - T getMin(size_t amount) const; // Get minimum value of the last values of buffer - T getMax() const; // Get the highest value in buffer - T getMax(size_t amount) const; // Get maximum value of the last values of buffer - T getMid() const; // Get mid value between and value in buffer - T getMid(size_t amount) const; // Get mid value between and value of the last values of buffer - T getMedian() const; // Get the median value in buffer - T getMedian(size_t amount) const; // Get the median value of the last values of buffer + void add(const double& value); // Add a new value to buffer + double get(size_t index) const; // Get value at specific position (0-based index from oldest to newest) + double getFirst() const; // Get the first (oldest) value in buffer + double getLast() const; // Get the last (newest) value in buffer + double getMin() const; // Get the lowest value in buffer + double getMin(size_t amount) const; // Get minimum value of the last values of buffer + double getMax() const; // Get the highest value in buffer + double getMax(size_t amount) const; // Get maximum value of the last values of buffer + double getMid() const; // Get mid value between and value in buffer + double getMid(size_t amount) const; // Get mid value between and value of the last values of buffer + double getMedian() const; // Get the median value in buffer + double getMedian(size_t amount) const; // Get the median value of the last values of buffer size_t getCapacity() const; // Get the buffer capacity (maximum size) size_t getCurrentSize() const; // Get the current number of elements in buffer size_t getFirstIdx() const; // Get the index of oldest value in buffer size_t getLastIdx() const; // Get the index of newest value in buffer bool isEmpty() const; // Check if buffer is empty bool isFull() const; // Check if buffer is full - T getMinVal() const; // Get lowest possible value for buffer - T getMaxVal() const; // Get highest possible value for buffer; used for unset/invalid buffer data + double getMinVal() const; // Get lowest possible value for buffer + double getMaxVal() const; // Get highest possible value for buffer; used for unset/invalid buffer data void clear(); // Clear buffer void resize(size_t size); // Delete buffer and set new size - T operator[](size_t index) const; // Operator[] for convenient access (same as get()) - std::vector getAllValues() const; // Get all current values as a vector + double operator[](size_t index) const; // Operator[] for convenient access (same as get()) + std::vector getAllValues() const; // Get all current values in native buffer format as a vector + std::vector getAllValues(size_t amount) const; // Get last values in native buffer format as a vector }; #include "OBPRingBuffer.tpp" \ No newline at end of file diff --git a/lib/obp60task/OBPRingBuffer.tpp b/lib/obp60task/OBPRingBuffer.tpp index 9174568..7d73f46 100644 --- a/lib/obp60task/OBPRingBuffer.tpp +++ b/lib/obp60task/OBPRingBuffer.tpp @@ -1,14 +1,21 @@ #include "OBPRingBuffer.h" +#include +#include +#include template -void RingBuffer::initCommon() { +void RingBuffer::initCommon() +{ MIN_VAL = std::numeric_limits::lowest(); MAX_VAL = std::numeric_limits::max(); + dblMIN_VAL = static_cast(MIN_VAL); + dblMAX_VAL = static_cast(MAX_VAL); dataName = ""; dataFmt = ""; updFreq = -1; - smallest = MIN_VAL; - largest = MAX_VAL; + mltplr = 1; + smallest = dblMIN_VAL; + largest = dblMAX_VAL; bufLocker = xSemaphoreCreateMutex(); } @@ -42,19 +49,20 @@ RingBuffer::RingBuffer(size_t size) // Specify meta data of buffer content template -void RingBuffer::setMetaData(String name, String format, int updateFrequency, T minValue, T maxValue) +void RingBuffer::setMetaData(String name, String format, int updateFrequency, double multiplier, double minValue, double maxValue) { GWSYNCHRONIZED(&bufLocker); dataName = name; dataFmt = format; updFreq = updateFrequency; - smallest = std::max(MIN_VAL, minValue); - largest = std::min(MAX_VAL, maxValue); + mltplr = multiplier; + smallest = std::max(dblMIN_VAL, minValue); + largest = std::min(dblMAX_VAL, maxValue); } // Get meta data of buffer content template -bool RingBuffer::getMetaData(String& name, String& format, int& updateFrequency, T& minValue, T& maxValue) +bool RingBuffer::getMetaData(String& name, String& format, int& updateFrequency, double& multiplier, double& minValue, double& maxValue) { if (dataName == "" || dataFmt == "" || updFreq == -1) { return false; // Meta data not set @@ -64,6 +72,7 @@ bool RingBuffer::getMetaData(String& name, String& format, int& updateFrequen name = dataName; format = dataFmt; updateFrequency = updFreq; + multiplier = mltplr; minValue = smallest; maxValue = largest; return true; @@ -99,13 +108,13 @@ String RingBuffer::getFormat() const // Add a new value to buffer template -void RingBuffer::add(const T& value) +void RingBuffer::add(const double& value) { GWSYNCHRONIZED(&bufLocker); if (value < smallest || value > largest) { buffer[head] = MAX_VAL; // Store MAX_VAL if value is out of range } else { - buffer[head] = value; + buffer[head] = static_cast(std::round(value * mltplr)); } last = head; @@ -117,63 +126,63 @@ void RingBuffer::add(const T& value) is_Full = true; } } - + // Serial.printf("Ringbuffer: value %.3f, multiplier: %.1f, buffer: %d\n", value, mltplr, buffer[head]); head = (head + 1) % capacity; } // Get value at specific position (0-based index from oldest to newest) template -T RingBuffer::get(size_t index) const +double RingBuffer::get(size_t index) const { GWSYNCHRONIZED(&bufLocker); if (isEmpty() || index < 0 || index >= count) { - return MAX_VAL; + return dblMAX_VAL; } size_t realIndex = (first + index) % capacity; - return buffer[realIndex]; + return static_cast(buffer[realIndex] / mltplr); } // Operator[] for convenient access (same as get()) template -T RingBuffer::operator[](size_t index) const +double RingBuffer::operator[](size_t index) const { return get(index); } // Get the first (oldest) value in the buffer template -T RingBuffer::getFirst() const +double RingBuffer::getFirst() const { if (isEmpty()) { - return MAX_VAL; + return dblMAX_VAL; } return get(0); } // Get the last (newest) value in the buffer template -T RingBuffer::getLast() const +double RingBuffer::getLast() const { if (isEmpty()) { - return MAX_VAL; + return dblMAX_VAL; } return get(count - 1); } // Get the lowest value in the buffer template -T RingBuffer::getMin() const +double RingBuffer::getMin() const { if (isEmpty()) { - return MAX_VAL; + return dblMAX_VAL; } - T minVal = MAX_VAL; - T value; + double minVal = dblMAX_VAL; + double value; for (size_t i = 0; i < count; i++) { value = get(i); - if (value < minVal && value != MAX_VAL) { + if (value < minVal && value != dblMAX_VAL) { minVal = value; } } @@ -182,19 +191,19 @@ T RingBuffer::getMin() const // Get minimum value of the last values of buffer template -T RingBuffer::getMin(size_t amount) const +double RingBuffer::getMin(size_t amount) const { if (isEmpty() || amount <= 0) { - return MAX_VAL; + return dblMAX_VAL; } if (amount > count) amount = count; - T minVal = MAX_VAL; - T value; + double minVal = dblMAX_VAL; + double value; for (size_t i = 0; i < amount; i++) { value = get(count - 1 - i); - if (value < minVal && value != MAX_VAL) { + if (value < minVal && value != dblMAX_VAL) { minVal = value; } } @@ -203,75 +212,81 @@ T RingBuffer::getMin(size_t amount) const // Get the highest value in the buffer template -T RingBuffer::getMax() const +double RingBuffer::getMax() const { if (isEmpty()) { - return MAX_VAL; + return dblMAX_VAL; } - T maxVal = MIN_VAL; - T value; + double maxVal = dblMIN_VAL; + double value; for (size_t i = 0; i < count; i++) { value = get(i); - if (value > maxVal && value != MAX_VAL) { + if (value > maxVal && value != dblMAX_VAL) { maxVal = value; } } + if (maxVal == dblMIN_VAL) { // no change of initial value -> buffer has only invalid values (MAX_VAL) + maxVal = dblMAX_VAL; + } return maxVal; } // Get maximum value of the last values of buffer template -T RingBuffer::getMax(size_t amount) const +double RingBuffer::getMax(size_t amount) const { if (isEmpty() || amount <= 0) { - return MAX_VAL; + return dblMAX_VAL; } if (amount > count) amount = count; - T maxVal = MIN_VAL; - T value; + double maxVal = dblMIN_VAL; + double value; for (size_t i = 0; i < amount; i++) { value = get(count - 1 - i); - if (value > maxVal && value != MAX_VAL) { + if (value > maxVal && value != dblMAX_VAL) { maxVal = value; } } + if (maxVal == dblMIN_VAL) { // no change of initial value -> buffer has only invalid values (MAX_VAL) + maxVal = dblMAX_VAL; + } return maxVal; } // Get mid value between and value in the buffer template -T RingBuffer::getMid() const +double RingBuffer::getMid() const { if (isEmpty()) { - return MAX_VAL; + return dblMAX_VAL; } - return (getMin() + getMax()) / static_cast(2); + return (getMin() + getMax()) / 2; } // Get mid value between and value of the last values of buffer template -T RingBuffer::getMid(size_t amount) const +double RingBuffer::getMid(size_t amount) const { if (isEmpty() || amount <= 0) { - return MAX_VAL; + return dblMAX_VAL; } if (amount > count) amount = count; - return (getMin(amount) + getMax(amount)) / static_cast(2); + return (getMin(amount) + getMax(amount)) / 2; } // Get the median value in the buffer template -T RingBuffer::getMedian() const +double RingBuffer::getMedian() const { if (isEmpty()) { - return MAX_VAL; + return dblMAX_VAL; } // Create a temporary vector with current valid elements @@ -287,20 +302,20 @@ T RingBuffer::getMedian() const if (count % 2 == 1) { // Odd number of elements - return temp[count / 2]; + return static_cast(temp[count / 2]); } else { // Even number of elements - return average of middle two // Note: For integer types, this truncates. For floating point, it's exact. - return (temp[count / 2 - 1] + temp[count / 2]) / 2; + return static_cast((temp[count / 2 - 1] + temp[count / 2]) / 2); } } // Get the median value of the last values of buffer template -T RingBuffer::getMedian(size_t amount) const +double RingBuffer::getMedian(size_t amount) const { if (isEmpty() || amount <= 0) { - return MAX_VAL; + return dblMAX_VAL; } if (amount > count) amount = count; @@ -310,7 +325,7 @@ T RingBuffer::getMedian(size_t amount) const temp.reserve(amount); for (size_t i = 0; i < amount; i++) { - temp.push_back(get(i)); + temp.push_back(get(count - 1 - i)); } // Sort to find median @@ -318,11 +333,11 @@ T RingBuffer::getMedian(size_t amount) const if (amount % 2 == 1) { // Odd number of elements - return temp[amount / 2]; + return static_cast(temp[amount / 2]); } else { // Even number of elements - return average of middle two // Note: For integer types, this truncates. For floating point, it's exact. - return (temp[amount / 2 - 1] + temp[amount / 2]) / 2; + return static_cast((temp[amount / 2 - 1] + temp[amount / 2]) / 2); } } @@ -370,16 +385,16 @@ bool RingBuffer::isFull() const // Get lowest possible value for buffer template -T RingBuffer::getMinVal() const +double RingBuffer::getMinVal() const { - return MIN_VAL; + return dblMIN_VAL; } // Get highest possible value for buffer; used for unset/invalid buffer data template -T RingBuffer::getMaxVal() const +double RingBuffer::getMaxVal() const { - return MAX_VAL; + return dblMAX_VAL; } // Clear buffer @@ -411,16 +426,37 @@ void RingBuffer::resize(size_t newSize) buffer.resize(newSize, MAX_VAL); } -// Get all current values as a vector +// Get all current values in native buffer format as a vector template -std::vector RingBuffer::getAllValues() const +std::vector RingBuffer::getAllValues() const { - std::vector result; + std::vector result; result.reserve(count); for (size_t i = 0; i < count; i++) { result.push_back(get(i)); } + return result; +} + +// Get last values in native buffer format as a vector +template +std::vector RingBuffer::getAllValues(size_t amount) const +{ + std::vector result; + + if (isEmpty() || amount <= 0) { + return result; + } + if (amount > count) + amount = count; + + result.reserve(amount); + + for (size_t i = 0; i < amount; i++) { + result.push_back(get(count - 1 - i)); + } + return result; } \ No newline at end of file diff --git a/lib/obp60task/OBPcharts.cpp b/lib/obp60task/OBPcharts.cpp new file mode 100644 index 0000000..17caf75 --- /dev/null +++ b/lib/obp60task/OBPcharts.cpp @@ -0,0 +1,609 @@ +// Function lib for display of boat data in various chart formats +#include "OBPcharts.h" +#include "OBP60Extensions.h" +#include "OBPRingBuffer.h" + +// --- Class Chart --------------- +template +Chart::Chart(RingBuffer& dataBuf, int8_t chrtDir, int8_t chrtSz, double dfltRng, CommonData& common, bool useSimuData) + : dataBuf(dataBuf) + , chrtDir(chrtDir) + , chrtSz(chrtSz) + , dfltRng(dfltRng) + , commonData(&common) + , useSimuData(useSimuData) +{ + logger = commonData->logger; + fgColor = commonData->fgcolor; + bgColor = commonData->bgcolor; + + // LOG_DEBUG(GwLog::DEBUG, "Chart Init: Chart::dataBuf: %p, passed dataBuf: %p", (void*)&this->dataBuf, (void*)&dataBuf); + dWidth = getdisplay().width(); + dHeight = getdisplay().height(); + + if (chrtDir == 0) { + // horizontal chart timeline direction + timAxis = dWidth; + switch (chrtSz) { + case 0: + valAxis = dHeight - top - bottom; + cStart = { 0, top }; + break; + case 1: + valAxis = (dHeight - top - bottom) / 2 - hGap; + cStart = { 0, top }; + break; + case 2: + valAxis = (dHeight - top - bottom) / 2 - hGap; + cStart = { 0, top + (valAxis + hGap) + hGap }; + break; + default: + LOG_DEBUG(GwLog::ERROR, "displayChart: wrong init parameter"); + return; + } + } else if (chrtDir == 1) { + // vertical chart timeline direction + timAxis = dHeight - top - bottom; + switch (chrtSz) { + case 0: + valAxis = dWidth; + cStart = { 0, top }; + break; + case 1: + valAxis = dWidth / 2 - vGap - 1; + cStart = { 0, top }; + break; + case 2: + valAxis = dWidth / 2 - vGap - 1; + cStart = { dWidth / 2 + vGap, top }; + break; + default: + LOG_DEBUG(GwLog::ERROR, "displayChart: wrong init parameter"); + return; + } + } else { + LOG_DEBUG(GwLog::ERROR, "displayChart: wrong init parameter"); + return; + } + + dataBuf.getMetaData(dbName, dbFormat); + dbMIN_VAL = dataBuf.getMinVal(); + dbMAX_VAL = dataBuf.getMaxVal(); + bufSize = dataBuf.getCapacity(); + + if (dbFormat == "formatCourse" || dbFormat == "FormatWind" || dbFormat == "FormatRot") { + + if (dbFormat == "FormatRot") { + chrtDataFmt = 2; // Chart is showing data of rotational format + } else { + chrtDataFmt = 1; // Chart is showing data of course / wind format + } + rngStep = M_TWOPI / 360.0 * 10.0; // +/-10 degrees on each end of chrtMid; we are calculating with SI values + + } else { + chrtDataFmt = 0; // Chart is showing any other data format than + rngStep = 5.0; // +/- 10 for all other values (eg. m/s, m, K, mBar) + } + + chrtMin = 0; + chrtMax = 0; + chrtMid = dbMAX_VAL; + chrtRng = dfltRng; + recalcRngCntr = true; // initialize on first screen call + + LOG_DEBUG(GwLog::DEBUG, "Chart Init: dWidth: %d, dHeight: %d, timAxis: %d, valAxis: %d, cStart {x,y}: %d, %d, dbname: %s, rngStep: %.4f", dWidth, dHeight, timAxis, valAxis, cStart.x, cStart.y, dbName, rngStep); +}; + +template +Chart::~Chart() +{ +} + +// Perform all actions to draw chart +// Parameters are chart time interval, and the current boat data value to be printed +template +void Chart::showChrt(int8_t chrtIntv, GwApi::BoatValue currValue) +{ + drawChrt(chrtIntv, currValue); + drawChrtTimeAxis(chrtIntv); + drawChrtValAxis(); + + if (bufDataValid) { + // uses BoatValue temp variable to format latest buffer value + // doesn't work unfortunately when 'simulation data' is active, because OBP60Formatter generates own simulation value in that case + currValue.value = dataBuf.getLast(); + currValue.valid = currValue.value != dbMAX_VAL; + Chart::prntCurrValue(currValue); + LOG_DEBUG(GwLog::DEBUG, "Chart drawChrt: currValue-value: %.1f, Valid: %d, Name: %s, Address: %p", currValue.value, currValue.valid, currValue.getName(), (void*)&currValue); + } +} + +// draw chart +template +void Chart::drawChrt(int8_t chrtIntv, GwApi::BoatValue& currValue) +{ + double chrtVal; // Current data value + double chrtScl; // Scale for data values in pixels per value + static double chrtPrevVal; // Last data value in chart area + // bool bufDataValid = false; // Flag to indicate if buffer data is valid + static int numNoData; // Counter for multiple invalid data values in a row + + int x, y; // x and y coordinates for drawing + static int prevX, prevY; // Last x and y coordinates for drawing + + // Identify buffer size and buffer start position for chart + count = dataBuf.getCurrentSize(); + currIdx = dataBuf.getLastIdx(); + numAddedBufVals = (currIdx - lastAddedIdx + bufSize) % bufSize; // Number of values added to buffer since last display + + if (chrtIntv != oldChrtIntv || count == 1) { + // new data interval selected by user; this is only x * 230 values instead of 240 seconds (4 minutes) per interval step + // intvBufSize = timAxis * chrtIntv; // obsolete + numBufVals = min(count, (timAxis - 60) * chrtIntv); // keep free or release 60 values on chart for plotting of new values + bufStart = max(0, count - numBufVals); + lastAddedIdx = currIdx; + oldChrtIntv = chrtIntv; + + } else { + numBufVals = numBufVals + numAddedBufVals; + lastAddedIdx = currIdx; + if (count == bufSize) { + bufStart = max(0, bufStart - numAddedBufVals); + } + } + + calcChrtBorders(chrtMid, chrtMin, chrtMax, chrtRng); + chrtScl = double(valAxis) / chrtRng; // Chart scale: pixels per value step + + // Do we have valid buffer data? + if (dataBuf.getMax() == dbMAX_VAL) { // only values in buffer -> no valid wind data available + bufDataValid = false; + } else if (!currValue.valid && !useSimuData) { // currently no valid boat data available and no simulation mode + numNoData++; + bufDataValid = true; + if (numNoData > 3) { // If more than 4 invalid values in a row, send message + bufDataValid = false; + } + } else { + numNoData = 0; // reset data error counter + bufDataValid = true; // At least some wind data available + } + + // Draw wind values in chart + //*********************************************************************** + if (bufDataValid) { + for (int i = 0; i < (numBufVals / chrtIntv); i++) { + chrtVal = dataBuf.get(bufStart + (i * chrtIntv)); // show the latest wind values in buffer; keep 1st value constant in a rolling buffer + if (chrtVal == dbMAX_VAL) { + chrtPrevVal = dbMAX_VAL; + } else { + + if (chrtDir == 0) { // horizontal chart + x = cStart.x + i; // Position in chart area + if (chrtDataFmt == 0) { + y = cStart.y + static_cast(((chrtVal - chrtMin) * chrtScl) + 0.5); // calculate chart point and round + } else { // degree type value + y = cStart.y + static_cast((WindUtils::to2PI(chrtVal - chrtMin) * chrtScl) + 0.5); // calculate chart point and round + } + } else { // vertical chart + y = cStart.y + timAxis - i; // Position in chart area + if (chrtDataFmt == 0) { + x = cStart.x + static_cast(((chrtVal - chrtMin) * chrtScl) + 0.5); // calculate chart point and round + } else { // degree type value + x = cStart.x + static_cast((WindUtils::to2PI(chrtVal - chrtMin) * chrtScl) + 0.5); // calculate chart point and round + } + } + + // if (i >= (numBufVals / chrtIntv) - 5) // log chart data of 1 line (adjust for test purposes) + // LOG_DEBUG(GwLog::DEBUG, "PageWindPlot Chart: i: %d, chrtVal: %.4f, {x,y} {%d,%d}", i, chrtVal, x, y); + + if ((i == 0) || (chrtPrevVal == dbMAX_VAL)) { + // just a dot for 1st chart point or after some invalid values + prevX = x; + prevY = y; + + } else if (chrtDataFmt != 0) { + // cross borders check for degree values; shift values to [-PI..0..PI]; when crossing borders, range is 2x PI degrees + double normCurr = WindUtils::to2PI(chrtVal - chrtMin); + double normPrev = WindUtils::to2PI(chrtPrevVal - chrtMin); + // Check if pixel positions are far apart (crossing chart boundary); happens when one value is near chrtMax and the other near chrtMin + bool crossedBorders = std::abs(normCurr - normPrev) > (chrtRng / 2.0); + + if (crossedBorders) { // If current value crosses chart borders compared to previous value, split line + // LOG_DEBUG(GwLog::DEBUG, "PageWindPlot Chart: crossedBorders: %d, chrtVal: %.2f, chrtPrevVal: %.2f", crossedBorders, chrtVal, chrtPrevVal); + bool wrappingFromHighToLow = normCurr < normPrev; // Determine which edge we're crossing + if (chrtDir == 0) { + int ySplit = wrappingFromHighToLow ? (cStart.y + valAxis) : cStart.y; + getdisplay().drawLine(prevX, prevY, x, ySplit, fgColor); + if (x != prevX) { // line with some horizontal trend + getdisplay().drawLine(prevX, prevY - 1, x, ySplit - 1, fgColor); + } else { + getdisplay().drawLine(prevX, prevY - 1, x - 1, ySplit, fgColor); + } + prevY = wrappingFromHighToLow ? cStart.y : (cStart.y + valAxis); + } else { // vertical chart + int xSplit = wrappingFromHighToLow ? (cStart.x + valAxis) : cStart.x; + getdisplay().drawLine(prevX, prevY, xSplit, y, fgColor); + getdisplay().drawLine(prevX, prevY - 1, ((xSplit != prevX) ? xSplit : xSplit - 1), ((xSplit != prevX) ? y - 1 : y), fgColor); + prevX = wrappingFromHighToLow ? cStart.x : (cStart.x + valAxis); + } + } + } + + // Draw line with 2 pixels width + make sure vertical lines are drawn correctly + if (chrtDir == 0 || x == prevX) { // horizontal chart & vertical line +// if (x == prevX) { // vertical line + getdisplay().drawLine(prevX, prevY, x, y, fgColor); + getdisplay().drawLine(prevX - 1, prevY, x - 1, y, fgColor); + } else if (chrtDir == 1 || x != prevX) { // vertical chart & line with some horizontal trend -> normal state +// } else { // line with some horizontal trend -> normal state + getdisplay().drawLine(prevX, prevY, x, y, fgColor); + getdisplay().drawLine(prevX, prevY - 1, x, y - 1, fgColor); + } + chrtPrevVal = chrtVal; + prevX = x; + prevY = y; + } + + // Reaching chart area bottom end + if (i >= timAxis - 1) { + oldChrtIntv = 0; // force reset of buffer start and number of values to show in next display loop + + if (chrtDataFmt == 1) { // degree of course or wind + recalcRngCntr = true; + LOG_DEBUG(GwLog::DEBUG, "PageWindPlot FreeTop: timAxis: %d, i: %d, bufStart: %d, numBufVals: %d, recalcRngCntr: %d", timAxis, i, bufStart, numBufVals, recalcRngCntr); + } + break; + } + } + + } else { + // No valid data available + getdisplay().setFont(&Ubuntu_Bold10pt8b); + + int pX, pY; + if (chrtDir == 0) { // horizontal chart + pX = cStart.x + (timAxis / 2); + pY = cStart.y + (valAxis / 2) - 10; + } else { // vertical chart + pX = cStart.x + (valAxis / 2); + pY = cStart.y + (timAxis / 2) - 10; + } + + getdisplay().fillRect(pX - 33, pY - 10, 66, 24, bgColor); // Clear area for message + drawTextCenter(pX, pY, "No data"); + LOG_DEBUG(GwLog::LOG, "PageWindPlot: No valid data available"); + } +} + +// Get maximum difference of last of dataBuf ringbuffer values to center chart +template +double Chart::getRng(double center, size_t amount) +{ + size_t count = dataBuf.getCurrentSize(); + + if (dataBuf.isEmpty() || amount <= 0) { + return dbMAX_VAL; + } + if (amount > count) + amount = count; + + double value = 0; + double range = 0; + double maxRng = dbMIN_VAL; + + // Start from the newest value (last) and go backwards x times + for (size_t i = 0; i < amount; i++) { + value = dataBuf.get(count - 1 - i); + + if (value == dbMAX_VAL) { + continue; // ignore invalid values + } + + range = abs(fmod((value - center + (M_TWOPI + M_PI)), M_TWOPI) - M_PI); + if (range > maxRng) + maxRng = range; + } + + if (maxRng > M_PI) { + maxRng = M_PI; + } + + return (maxRng != dbMIN_VAL ? maxRng : dbMAX_VAL); // Return range from to +} + +// check and adjust chart range and set range borders and range middle +template +void Chart::calcChrtBorders(double& rngMid, double& rngMin, double& rngMax, double& rng) +{ + if (chrtDataFmt == 0) { + // Chart data is of any type but 'degree' + + double oldRngMin = rngMin; + double oldRngMax = rngMax; + + // Chart starts at lowest range value, but at least '0' or includes even negative values + double currMinVal = dataBuf.getMin(numBufVals); + LOG_DEBUG(GwLog::DEBUG, "calcChrtRange0a: currMinVal: %.1f, currMaxVal: %.1f, rngMin: %.1f, rngMid: %.1f, rngMax: %.1f, rng: %.1f, rngStep: %.1f, oldRngMin: %.1f, oldRngMax: %.1f, dfltRng: %.1f, numBufVals: %d", + currMinVal, dataBuf.getMax(numBufVals), rngMin, rngMid, rngMax, rng, rngStep, oldRngMin, oldRngMax, dfltRng, numBufVals); + + if (currMinVal != dbMAX_VAL) { // current min value is valid + if (currMinVal > 0 && dbMIN_VAL == 0) { // Chart range starts at least at '0' or includes negative values + rngMin = 0; + } else if (currMinVal < oldRngMin || (oldRngMin < 0 && (currMinVal > (oldRngMin + rngStep)))) { // decrease rngMin if required or increase if lowest value is higher than old rngMin + rngMin = std::floor(currMinVal / rngStep) * rngStep; + } + } // otherwise keep rngMin unchanged + + double currMaxVal = dataBuf.getMax(numBufVals); + if (currMaxVal != dbMAX_VAL) { // current max value is valid + if ((currMaxVal > oldRngMax) || (currMaxVal < (oldRngMax - rngStep))) { // increase rngMax if required or decrease if lowest value is lower than old rngMax + rngMax = std::ceil(currMaxVal / rngStep) * rngStep; + rngMax = std::max(rngMax, rngMin + dfltRng); // keep at least default chart range + } + } // otherwise keep rngMax unchanged + + rngMid = (rngMin + rngMax) / 2.0; + rng = rngMax - rngMin; + LOG_DEBUG(GwLog::DEBUG, "calcChrtRange1a: currMinVal: %.1f, currMaxVal: %.1f, rngMin: %.1f, rngMid: %.1f, rngMax: %.1f, rng: %.1f, rngStep: %.1f, oldRngMin: %.1f, oldRngMax: %.1f, dfltRng: %.1f, numBufVals: %d", + currMinVal, currMaxVal, rngMin, rngMid, rngMax, rng, rngStep, oldRngMin, oldRngMax, dfltRng, numBufVals); + } else { + + if (chrtDataFmt == 1) { + // Chart data is of type 'course' or 'wind' + + if ((count == 1 && rngMid == 0) || rngMid == dbMAX_VAL) { + recalcRngCntr = true; // initialize + } + + // Set rngMid + if (recalcRngCntr) { + rngMid = dataBuf.getMid(numBufVals); + if (rngMid == dbMAX_VAL) { + rngMid = 0; + } else { + rngMid = std::round(rngMid / rngStep) * rngStep; // Set new center value; round to next value + + // Check if range between 'min' and 'max' is > 180° or crosses '0' + rngMin = dataBuf.getMin(numBufVals); + rngMax = dataBuf.getMax(numBufVals); + rng = (rngMax >= rngMin ? rngMax - rngMin : M_TWOPI - rngMin + rngMax); + rng = max(rng, dfltRng); // keep at least default chart range + if (rng > M_PI) { // If wind range > 180°, adjust wndCenter to smaller wind range end + rngMid = WindUtils::to2PI(rngMid + M_PI); + } + } + recalcRngCntr = false; // Reset flag for determination + LOG_DEBUG(GwLog::DEBUG, "calcChrtRange1b: rngMid: %.1f°, rngMin: %.1f°, rngMax: %.1f°, rng: %.1f°, rngStep: %.1f°", rngMid * RAD_TO_DEG, rngMin * RAD_TO_DEG, rngMax * RAD_TO_DEG, + rng * RAD_TO_DEG, rngStep * RAD_TO_DEG); + } + + } else if (chrtDataFmt == 2) { + // Chart data is of type 'rotation'; then we want to have always to be '0' + rngMid = 0; + } + + // check and adjust range between left, center, and right chart limit + double halfRng = rng / 2.0; // we calculate with range between and edges + double diffRng = getRng(rngMid, numBufVals); + // LOG_DEBUG(GwLog::DEBUG, "calcChrtRange2: diffRng: %.1f°, halfRng: %.1f°", diffRng * RAD_TO_DEG, halfRng * RAD_TO_DEG); + diffRng = (diffRng == dbMAX_VAL ? 0 : std::ceil(diffRng / rngStep) * rngStep); + // LOG_DEBUG(GwLog::DEBUG, "calcChrtRange2: diffRng: %.1f°, halfRng: %.1f°", diffRng * RAD_TO_DEG, halfRng * RAD_TO_DEG); + + if (diffRng > halfRng) { + halfRng = diffRng; // round to next value + } else if (diffRng + rngStep < halfRng) { // Reduce chart range for higher resolution if possible + halfRng = max(dfltRng / 2.0, diffRng); + } + + rngMin = WindUtils::to2PI(rngMid - halfRng); + rngMax = (halfRng < M_PI ? rngMid + halfRng : rngMid + halfRng - (M_TWOPI / 360)); // if chart range is 360°, then make 1° smaller than + rngMax = WindUtils::to2PI(rngMax); + // LOG_DEBUG(GwLog::DEBUG, "calcChrtRange2: diffRng: %.1f°, halfRng: %.1f°", diffRng * RAD_TO_DEG, halfRng * RAD_TO_DEG); + + rng = halfRng * 2.0; + LOG_DEBUG(GwLog::DEBUG, "calcChrtRange2b: rngMid: %.1f°, rngMin: %.1f°, rngMax: %.1f°, diffRng: %.1f°, rng: %.1f°, rngStep: %.1f°", rngMid * RAD_TO_DEG, rngMin * RAD_TO_DEG, rngMax * RAD_TO_DEG, + diffRng * RAD_TO_DEG, rng * RAD_TO_DEG, rngStep * RAD_TO_DEG); + } +} + +// chart time axis label + lines +template +void Chart::drawChrtTimeAxis(int8_t chrtIntv) +{ + int timeRng; + float slots, intv, i; + char sTime[6]; + getdisplay().setFont(&Ubuntu_Bold8pt8b); + getdisplay().setTextColor(fgColor); + + if (chrtDir == 0) { // horizontal chart + getdisplay().fillRect(0, cStart.y, dWidth, 2, fgColor); + + timeRng = chrtIntv * 4; // Chart time interval: [1] 4 min., [2] 8 min., [3] 12 min., [4] 16 min., [8] 32 min. + slots = timAxis / 80.0; // number of axis labels + intv = timeRng / slots; // minutes per chart axis interval + i = timeRng; // Chart axis label start at -32, -16, -12, ... minutes + + for (int j = 0; j < timAxis - 30; j += 80) { // fill time axis with values but keep area free on right hand side for value label + // LOG_DEBUG(GwLog::DEBUG, "ChartTimeAxis: timAxis: %d, {x,y}: {%d,%d}, i: %.1f, j: %d, chrtIntv: %d, intv: %.1f, slots: %.1f", timAxis, cStart.x, cStart.y, i, j, chrtIntv, intv, slots); + + // Format time label based on interval + if (chrtIntv < 3) { + snprintf(sTime, sizeof(sTime), "-%.1f", i); + } else { + snprintf(sTime, sizeof(sTime), "-%.0f", std::round(i)); + } + + // draw text with appropriate offset + // int tOffset = (j == 0) ? 13 : (chrtIntv < 3 ? -4 : -4); + int tOffset = j == 0 ? 13 : -4; + drawTextCenter(cStart.x + j + tOffset, cStart.y - 8, sTime); + getdisplay().drawLine(cStart.x + j, cStart.y, cStart.x + j, cStart.y + 5, fgColor); // draw short vertical time mark + + i -= intv; + } + + } else { // vertical chart + timeRng = chrtIntv * 4; // chart time interval: [1] 4 min., [2] 8 min., [3] 12 min., [4] 16 min., [8] 32 min. + slots = timAxis / 75.0; // number of axis labels + intv = timeRng / slots; // minutes per chart axis interval + i = -intv; // chart axis label start at -32, -16, -12, ... minutes + + for (int j = 75; j < (timAxis - 75); j += 75) { // don't print time label at upper and lower end of time axis + if (chrtIntv < 3) { // print 1 decimal if time range is single digit (4 or 8 minutes) + snprintf(sTime, sizeof(sTime), "%.1f", i); + } else { + snprintf(sTime, sizeof(sTime), "%.0f", std::floor(i)); + } + + getdisplay().drawLine(cStart.x, cStart.y + j, cStart.x + valAxis, cStart.y + j, fgColor); // Grid line + + if (chrtSz == 0) { // full size chart + getdisplay().fillRect(0, cStart.y + j - 9, 32, 15, bgColor); // clear small area to remove potential chart lines + getdisplay().setCursor((4 - strlen(sTime)) * 7, cStart.y + j + 3); // time value; print left screen; value right-formated + getdisplay().printf("%s", sTime); // Range value + } else if (chrtSz == 2) { // half size chart; right side + drawTextCenter(dWidth / 2, cStart.y + j, sTime); // time value; print mid screen + } + + i -= intv; + } + } +} + +// chart value axis labels + lines +template +void Chart::drawChrtValAxis() +{ + double slots; + int i, intv; + double cVal, cchrtRng, crngMin; + char sVal[6]; + int sLen; + std::unique_ptr tmpBVal; // Temp variable to get formatted and converted data value from OBP60Formatter + tmpBVal = std::unique_ptr(new GwApi::BoatValue(dataBuf.getName())); + tmpBVal->setFormat(dataBuf.getFormat()); + tmpBVal->valid = true; + + if (chrtDir == 0) { // horizontal chart + slots = valAxis / 60.0; // number of axis labels + tmpBVal->value = chrtRng; + cchrtRng = formatValue(tmpBVal.get(), *commonData).cvalue; // value (converted) + intv = static_cast(round(cchrtRng / slots)); + i = intv; + + if (chrtSz == 0) { // full size chart -> print multiple value lines + getdisplay().setFont(&Ubuntu_Bold12pt8b); + for (int j = 60; j < valAxis - 30; j += 60) { + getdisplay().drawLine(cStart.x, cStart.y + j, cStart.x + timAxis, cStart.y + j, fgColor); + + getdisplay().fillRect(cStart.x, cStart.y + j - 11, 42, 21, bgColor); // Clear small area to remove potential chart lines + String sVal = String(i); + getdisplay().setCursor((3 - sVal.length()) * 10, cStart.y + j + 7); // value right-formated + getdisplay().printf("%s", sVal); // Range value + + i += intv; + } + } else { // half size chart -> print just edge values + middle chart line + getdisplay().setFont(&Ubuntu_Bold10pt8b); + + tmpBVal->value = chrtMin; + cVal = formatValue(tmpBVal.get(), *commonData).cvalue; // value (converted) + sLen = snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); + getdisplay().fillRect(cStart.x, cStart.y + 2, 42, 16, bgColor); // Clear small area to remove potential chart lines + getdisplay().setCursor(cStart.x + ((3 - sLen) * 10), cStart.y + 16); + getdisplay().printf("%s", sVal); // Range low end + + tmpBVal->value = chrtMid; + cVal = formatValue(tmpBVal.get(), *commonData).cvalue; // value (converted) + sLen = snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); + getdisplay().fillRect(cStart.x, cStart.y + (valAxis / 2) - 9, 42, 16, bgColor); // Clear small area to remove potential chart lines + getdisplay().setCursor(cStart.x + ((3 - sLen) * 10), cStart.y + (valAxis / 2) + 5); + getdisplay().printf("%s", sVal); // Range mid value + getdisplay().drawLine(cStart.x + 43, cStart.y + (valAxis / 2), cStart.x + timAxis, cStart.y + (valAxis / 2), fgColor); + + tmpBVal->value = chrtMax; + cVal = formatValue(tmpBVal.get(), *commonData).cvalue; // value (converted) + sLen = snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); + getdisplay().fillRect(cStart.x, cStart.y + valAxis - 16, 42, 16, bgColor); // Clear small area to remove potential chart lines + getdisplay().setCursor(cStart.x + ((3 - sLen) * 10), cStart.y + valAxis - 1); + getdisplay().printf("%s", sVal); // Range high end + getdisplay().drawLine(cStart.x + 43, cStart.y + valAxis, cStart.x + timAxis, cStart.y + valAxis, fgColor); + } + + getdisplay().setFont(&Ubuntu_Bold12pt8b); + drawTextRalign(cStart.x + timAxis, cStart.y - 3, dbName); // buffer data name + + } else { // vertical chart + if (chrtSz == 0) { // full size chart -> use larger font + getdisplay().setFont(&Ubuntu_Bold12pt8b); + drawTextCenter(cStart.x + (valAxis / 4) + 25, cStart.y - 10, dbName); // buffer data name + } else { + getdisplay().setFont(&Ubuntu_Bold10pt8b); + } + getdisplay().fillRect(cStart.x, top, valAxis, 2, fgColor); // top chart line + + tmpBVal->value = chrtMin; + cVal = formatValue(tmpBVal.get(), *commonData).cvalue; // value (converted) + snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); + getdisplay().setCursor(cStart.x, cStart.y - 2); + getdisplay().printf("%s", sVal); // Range low end + + tmpBVal->value = chrtMid; + cVal = formatValue(tmpBVal.get(), *commonData).cvalue; // value (converted) + snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); + drawTextCenter(cStart.x + (valAxis / 2), cStart.y - 10, sVal); // Range mid end + + tmpBVal->value = chrtMax; + cVal = formatValue(tmpBVal.get(), *commonData).cvalue; // value (converted) + snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); + drawTextRalign(cStart.x + valAxis - 2, cStart.y - 2, sVal); // Range high end + + for (int j = 0; j <= valAxis + 2; j += ((valAxis + 2) / 2)) { + getdisplay().drawLine(cStart.x + j, cStart.y, cStart.x + j, cStart.y + timAxis, fgColor); + } + + // if (chrtSz == 0) { + // getdisplay().setFont(&Ubuntu_Bold12pt8b); + // drawTextCenter(cStart.x + (valAxis / 4) + 15, cStart.y - 11, dbName); // buffer data name + // } + } +} + +// Print current data value +template +void Chart::prntCurrValue(GwApi::BoatValue& currValue) +{ + const int xPosVal = (chrtDir == 0) ? cStart.x + (timAxis / 2) - 56 : cStart.x + 32; + const int yPosVal = (chrtDir == 0) ? cStart.y + valAxis - 7 : cStart.y + timAxis - 7; + + FormattedData frmtDbData = formatValue(&currValue, *commonData); + double testdbValue = frmtDbData.value; + String sdbValue = frmtDbData.svalue; // value (string) + String dbUnit = frmtDbData.unit; // Unit of value + // LOG_DEBUG(GwLog::DEBUG, "Chart CurrValue: dbValue: %.2f, sdbValue: %s, fmrtDbValue: %.2f, dbFormat: %s, dbUnit: %s, Valid: %d, Name: %s, Address: %p", currValue.value, sdbValue, + // testdbValue, currValue.getFormat(), dbUnit, currValue.valid, currValue.getName(), currValue); + + getdisplay().fillRect(xPosVal - 1, yPosVal - 34, 125, 41, bgColor); // Clear area for TWS value + getdisplay().drawRect(xPosVal, yPosVal - 33, 123, 39, fgColor); // Draw box for TWS value + getdisplay().setFont(&DSEG7Classic_BoldItalic16pt7b); + getdisplay().setCursor(xPosVal + 1, yPosVal); + if (useSimuData) { + getdisplay().printf("%2.1f", currValue.value); // Value + } else { + getdisplay().print(sdbValue); // Value + } + + getdisplay().setFont(&Ubuntu_Bold10pt8b); + getdisplay().setCursor(xPosVal + 76, yPosVal - 17); + getdisplay().print(dbName); // Name + + getdisplay().setFont(&Ubuntu_Bold8pt8b); + getdisplay().setCursor(xPosVal + 76, yPosVal + 0); + getdisplay().print(dbUnit); // Unit +} + +// Explicitly instantiate class with required data types to avoid linker errors +template class Chart; +// --- Class Chart --------------- diff --git a/lib/obp60task/OBPcharts.h b/lib/obp60task/OBPcharts.h new file mode 100644 index 0000000..db298a5 --- /dev/null +++ b/lib/obp60task/OBPcharts.h @@ -0,0 +1,70 @@ +// Function lib for display of boat data in various graphical chart formats +#pragma once +#include "Pagedata.h" + +struct Pos { + int x; + int y; +}; +template class RingBuffer; +class GwLog; + +template +class Chart { +protected: + CommonData *commonData; + GwLog *logger; + + RingBuffer &dataBuf; // Buffer to display + int8_t chrtDir; // Chart timeline direction: [0] = horizontal, [1] = vertical + int8_t chrtSz; // Chart size: [0] = full size, [1] = half size left/top, [2] half size right/bottom + double dfltRng; // Default range of chart, e.g. 30 = [0..30] + uint16_t fgColor; // color code for any screen writing + uint16_t bgColor; // color code for screen background + bool useSimuData; // flag to indicate if simulation data is active + + int top = 48; // display top header lines + int bottom = 22; // display bottom lines + int hGap = 11; // gap between 2 horizontal charts; actual gap is 2x + int vGap = 20; // gap between 2 vertical charts; actual gap is 2x + int xOffset = 33; // offset for horizontal axis (time/value), because of space for left vertical axis labeling + int yOffset = 10; // offset for vertical axis (time/value), because of space for top horizontal axis labeling + int dWidth; // Display width + int dHeight; // Display height + int timAxis, valAxis; // size of time and value chart axis + Pos cStart; // start point of chart area + double chrtRng; // Range of buffer values from min to max value + double chrtMin; // Range low end value + double chrtMax; // Range high end value + double chrtMid; // Range mid value + double rngStep; // Defines the step of adjustment (e.g. 10 m/s) for value axis range + bool recalcRngCntr = false; // Flag for re-calculation of mid value of chart for wind data types + + String dbName, dbFormat; // Name and format of data buffer + int chrtDataFmt; // Data format of chart: [0] size values; [1] degree of course or wind; [2] rotational degrees + double dbMIN_VAL; // Lowest possible value of buffer of type + double dbMAX_VAL; // Highest possible value of buffer of type ; indicates invalid value in buffer + size_t bufSize; // History buffer size: 1.920 values for 32 min. history chart + int count; // current size of buffer + int numBufVals; // number of wind values available for current interval selection + int bufStart; // 1st data value in buffer to show + int numAddedBufVals; // Number of values added to buffer since last display + size_t currIdx; // Current index in TWD history buffer + size_t lastIdx; // Last index of TWD history buffer + size_t lastAddedIdx = 0; // Last index of TWD history buffer when new data was added + bool bufDataValid = false; // Flag to indicate if buffer data is valid + int oldChrtIntv = 0; // remember recent user selection of data interval + + void drawChrt(int8_t chrtIntv, GwApi::BoatValue& currValue); // Draw chart line + double getRng(double center, size_t amount); // Calculate range between chart center and edges + void calcChrtBorders(double& rngMid, double& rngMin, double& rngMax, double& rng); // Calculate chart points for value axis and return range between and + void drawChrtTimeAxis(int8_t chrtIntv); // Draw time axis of chart, value and lines + void drawChrtValAxis(); // Draw value axis of chart, value and lines + void prntCurrValue(GwApi::BoatValue& currValue); // Add current boat data value to chart + +public: + Chart(RingBuffer& dataBuf, int8_t chrtDir, int8_t chrtSz, double dfltRng, CommonData& common, bool useSimuData); // Chart object of data chart + ~Chart(); + void showChrt(int8_t chrtIntv, GwApi::BoatValue currValue); // Perform all actions to draw chart + +}; \ No newline at end of file diff --git a/lib/obp60task/PageWindPlot.cpp b/lib/obp60task/PageWindPlot.cpp index 98e1a71..0b4e884 100644 --- a/lib/obp60task/PageWindPlot.cpp +++ b/lib/obp60task/PageWindPlot.cpp @@ -2,84 +2,20 @@ #include "Pagedata.h" #include "OBP60Extensions.h" -#include "OBPRingBuffer.h" -#include "OBPDataOperations.h" -#include "BoatDataCalibration.h" -#include - -static const double radToDeg = 180.0 / M_PI; // Conversion factor from radians to degrees - -// Get maximum difference of last of TWD ringbuffer values to center chart; returns "0" if data is not valid -int getCntr(const RingBuffer& windDirHstry, size_t amount) -{ - const int MAX_VAL = windDirHstry.getMaxVal(); - size_t count = windDirHstry.getCurrentSize(); - - if (windDirHstry.isEmpty() || amount <= 0) { - return 0; - } - if (amount > count) - amount = count; - - uint16_t midWndDir, minWndDir, maxWndDir = 0; - int wndCenter = 0; - - midWndDir = windDirHstry.getMid(amount); - if (midWndDir != MAX_VAL) { - midWndDir = midWndDir / 1000.0 * radToDeg; - wndCenter = int((midWndDir + (midWndDir >= 0 ? 5 : -5)) / 10) * 10; // Set new center value; round to nearest 10 degree value - minWndDir = windDirHstry.getMin(amount) / 1000.0 * radToDeg; - maxWndDir = windDirHstry.getMax(amount) / 1000.0 * radToDeg; - if ((maxWndDir - minWndDir) > 180 && !(minWndDir > maxWndDir)) { // if wind range is > 180 and no 0° crossover, adjust wndCenter to smaller wind range end - wndCenter = WindUtils::to360(wndCenter + 180); - } - } - - return wndCenter; -} - -// Get maximum difference of last of TWD ringbuffer values to center chart -int getRng(const RingBuffer& windDirHstry, int center, size_t amount) -{ - int minVal = windDirHstry.getMinVal(); - const int MAX_VAL = windDirHstry.getMaxVal(); - size_t count = windDirHstry.getCurrentSize(); - - if (windDirHstry.isEmpty() || amount <= 0) { - return MAX_VAL; - } - if (amount > count) - amount = count; - - int value = 0; - int rng = 0; - int maxRng = minVal; - // Start from the newest value (last) and go backwards x times - for (size_t i = 0; i < amount; i++) { - value = windDirHstry.get(count - 1 - i); - - if (value == MAX_VAL) { - continue; // ignore invalid values - } - - value = value / 1000.0 * radToDeg; - rng = abs(((value - center + 540) % 360) - 180); - if (rng > maxRng) - maxRng = rng; - } - if (maxRng > 180) { - maxRng = 180; - } - - return (maxRng != minVal ? maxRng : MAX_VAL); -} +#include "OBPcharts.h" // **************************************************************** class PageWindPlot : public Page { +private: + GwLog* logger; + + int width; // Screen width + int height; // Screen height + bool keylock = false; // Keylock char chrtMode = 'D'; // Chart mode: 'D' for TWD, 'S' for TWS, 'B' for both - bool showTruW = true; // Show true wind or apparant wind in chart area + bool showTruW = true; // Show true wind or apparent wind in chart area bool oldShowTruW = false; // remember recent user selection of wind data type int dataIntv = 1; // Update interval for wind history chart: @@ -92,20 +28,20 @@ public: PageWindPlot(CommonData& common) { commonData = &common; - common.logger->logDebug(GwLog::LOG, "Instantiate PageWindPlot"); + logger = commonData->logger; + LOG_DEBUG(GwLog::LOG, "Instantiate PageWindPlot"); // Get config data useSimuData = common.config->getBool(common.config->useSimuData); // holdValues = common.config->getBool(common.config->holdvalues); flashLED = common.config->getString(common.config->flashLED); backlightMode = common.config->getString(common.config->backlight); - } virtual void setupKeys() { Page::setupKeys(); - // commonData->keydata[0].label = "MODE"; + commonData->keydata[0].label = "MODE"; #if defined BOARD_OBP60S3 commonData->keydata[1].label = "SRC"; commonData->keydata[4].label = "INTV"; @@ -117,7 +53,7 @@ public: // Key functions virtual int handleKey(int key) { - // Set chart mode TWD | TWS -> to be implemented + // Set chart mode TWD | TWS if (key == 1) { if (chrtMode == 'D') { chrtMode = 'S'; @@ -163,99 +99,59 @@ public: return key; } - virtual void displayNew(PageData &pageData){ + virtual void displayNew(PageData& pageData) + { #ifdef BOARD_OBP40S3 - String wndSrc; // Wind source true/apparant wind - preselection for OBP40 + String wndSrc; // Wind source true/apparent wind - preselection for OBP40 wndSrc = commonData->config->getString("page" + String(pageData.pageNumber) + "wndsrc"); - if (wndSrc =="True wind") { + if (wndSrc == "True wind") { showTruW = true; } else { - showTruW = false; // Wind source is apparant wind + showTruW = false; // Wind source is apparent wind } - commonData->logger->logDebug(GwLog::LOG,"New PageWindPlot: wind source=%s", wndSrc); + LOG_DEBUG(GwLog::LOG, "New PageWindPlot; wind source=%s", wndSrc); #endif oldShowTruW = !showTruW; // makes wind source being initialized at initial page call + + width = getdisplay().width(); // Screen width + height = getdisplay().height(); // Screen height } int displayPage(PageData& pageData) { GwConfigHandler* config = commonData->config; - GwLog* logger = commonData->logger; - static RingBuffer* wdHstry; // Wind direction data buffer + static RingBuffer* wdHstry; // Wind direction data buffer static RingBuffer* wsHstry; // Wind speed data buffer static String wdName, wdFormat; // Wind direction name and format static String wsName, wsFormat; // Wind speed name and format - static int16_t wdMAX_VAL; // Max. value of wd history buffer, indicating invalid values - float wsValue; // Wind speed value in chart area - String wsUnit; // Wind speed unit in chart area - static GwApi::BoatValue* wsBVal = new GwApi::BoatValue("TWS"); // temp BoatValue for wind speed unit identification; required by OBP60Formater - // current boat data values; TWD/AWD only for validation test - const int numBoatData = 2; - GwApi::BoatValue* bvalue; - bool BDataValid[numBoatData]; + // Separate chart objects for true wind and apparent wind + static std::unique_ptr> twdFlChart, awdFlChart; // chart object for wind direction chart, full size + static std::unique_ptr> twsFlChart, awsFlChart; // chart object for wind speed chart, full size + static std::unique_ptr> twdHfChart, awdHfChart; // chart object for wind direction chart, half size + static std::unique_ptr> twsHfChart, awsHfChart; // chart object for wind speed chart, half size + // Pointers to the currently active charts + static Chart* wdFlChart; + static Chart* wsFlChart; + static Chart* wdHfChart; + static Chart* wsHfChart; - static bool isInitialized = false; // Flag to indicate that page is initialized - static bool wndDataValid = false; // Flag to indicate if wind data is valid - static int numNoData; // Counter for multiple invalid data values in a row + static GwApi::BoatValue* wdBVal = new GwApi::BoatValue("TWD"); // temp BoatValue for wind direction unit identification; required by OBP60Formater + static GwApi::BoatValue* wsBVal = new GwApi::BoatValue("TWS"); // temp BoatValue for wind speed unit identification; required by OBP60Formater */ + double dfltRngWd = 60.0 * DEG_TO_RAD; // default range for course chart from min to max value in RAD + double dfltRngWs = 7.5; // default range for wind speed chart from min to max value in m/s - static int width; // Screen width - static int height; // Screen height - static int xCenter; // Center of screen in x direction - static const int yOffset = 48; // Offset for y coordinates of chart area - static int cHeight; // height of chart area - static int bufSize; // History buffer size: 1.920 values for 32 min. history chart - static int intvBufSize; // Buffer size used for currently selected time interval - int count; // current size of buffer - static int numWndVals; // number of wind values available for current interval selection - static int bufStart; // 1st data value in buffer to show - int numAddedBufVals; // Number of values added to buffer since last display - size_t currIdx; // Current index in TWD history buffer - static size_t lastIdx; // Last index of TWD history buffer - static size_t lastAddedIdx = 0; // Last index of TWD history buffer when new data was added - static int oldDataIntv; // remember recent user selection of data interval - - static int wndCenter; // chart wind center value position - static int wndLeft; // chart wind left value position - static int wndRight; // chart wind right value position - static int chrtRng; // Range of wind values from mid wind value to min/max wind value in degrees - int diffRng; // Difference between mid and current wind value - static const int dfltRng = 60; // Default range for chart - int midWndDir; // New value for wndCenter after chart start / shift - - int x, y; // x and y coordinates for drawing - static int prevX, prevY; // Last x and y coordinates for drawing - static float chrtScl; // Scale for wind values in pixels per degree - int chrtVal; // Current wind value - static int chrtPrevVal; // Last wind value in chart area for check if value crosses 180 degree line + const int numBoatData = 4; + GwApi::BoatValue* bvalue[numBoatData]; // current boat data values LOG_DEBUG(GwLog::LOG, "Display PageWindPlot"); - ulong timer = millis(); + ulong pageTime = millis(); - if (!isInitialized) { - width = getdisplay().width(); - height = getdisplay().height(); - xCenter = width / 2; - cHeight = height - yOffset - 22; - numNoData = 0; - bufStart = 0; - oldDataIntv = 0; - wsValue = 0; - numAddedBufVals, currIdx, lastIdx = 0; - wndCenter = INT_MAX; - midWndDir = 0; - diffRng = dfltRng; - chrtRng = dfltRng; - - isInitialized = true; // Set flag to indicate that page is now initialized - } - - // read boat data values; TWD only for validation test, TWS for display of current value + // read boat data values for (int i = 0; i < numBoatData; i++) { - bvalue = pageData.values[i]; - BDataValid[i] = bvalue->valid; + bvalue[i] = pageData.values[i]; } // Optical warning by limit violation (unused) @@ -265,248 +161,88 @@ public: } if (showTruW != oldShowTruW) { + if (!twdFlChart) { // Create true wind charts if they don't exist + + LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: Creating true wind charts"); + auto* twdHstry = pageData.boatHstry->hstryBufList.twdHstry; + auto* twsHstry = pageData.boatHstry->hstryBufList.twsHstry; + // LOG_DEBUG(GwLog::DEBUG,"History Buffer addresses PageWindPlot: twdBuf: %p, twsBuf: %p", (void*)pageData.boatHstry->hstryBufList.twdHstry, + // (void*)pageData.boatHstry->hstryBufList.twsHstry); + + twdFlChart = std::unique_ptr>(new Chart(*twdHstry, 1, 0, dfltRngWd, *commonData, useSimuData)); + twsFlChart = std::unique_ptr>(new Chart(*twsHstry, 0, 0, dfltRngWs, *commonData, useSimuData)); + twdHfChart = std::unique_ptr>(new Chart(*twdHstry, 1, 1, dfltRngWd, *commonData, useSimuData)); + twsHfChart = std::unique_ptr>(new Chart(*twsHstry, 1, 2, dfltRngWs, *commonData, useSimuData)); + // twdHfChart = std::unique_ptr>(new Chart(*twdHstry, 0, 1, dfltRngWd, *commonData, useSimuData)); + // twsHfChart = std::unique_ptr>(new Chart(*twsHstry, 0, 2, dfltRngWs, *commonData, useSimuData)); + // LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: twdHstry: %p, twsHstry: %p", (void*)twdHstry, (void*)twsHstry); + } + + if (!awdFlChart) { // Create apparent wind charts if they don't exist + LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: Creating apparent wind charts"); + auto* awdHstry = pageData.boatHstry->hstryBufList.awdHstry; + auto* awsHstry = pageData.boatHstry->hstryBufList.awsHstry; + + awdFlChart = std::unique_ptr>(new Chart(*awdHstry, 1, 0, dfltRngWd, *commonData, useSimuData)); + awsFlChart = std::unique_ptr>(new Chart(*awsHstry, 0, 0, dfltRngWs, *commonData, useSimuData)); + awdHfChart = std::unique_ptr>(new Chart(*awdHstry, 1, 1, dfltRngWd, *commonData, useSimuData)); + awsHfChart = std::unique_ptr>(new Chart(*awsHstry, 1, 2, dfltRngWs, *commonData, useSimuData)); + } + + // Switch active charts based on showTruW if (showTruW) { wdHstry = pageData.boatHstry->hstryBufList.twdHstry; wsHstry = pageData.boatHstry->hstryBufList.twsHstry; + wdFlChart = twdFlChart.get(); + wsFlChart = twsFlChart.get(); + wdHfChart = twdHfChart.get(); + wsHfChart = twsHfChart.get(); } else { wdHstry = pageData.boatHstry->hstryBufList.awdHstry; wsHstry = pageData.boatHstry->hstryBufList.awsHstry; + wdFlChart = awdFlChart.get(); + wsFlChart = awsFlChart.get(); + wdHfChart = awdHfChart.get(); + wsHfChart = awsHfChart.get(); } + wdHstry->getMetaData(wdName, wdFormat); wsHstry->getMetaData(wsName, wsFormat); - wdMAX_VAL = wdHstry->getMaxVal(); - bufSize = wdHstry->getCapacity(); - wsBVal->setFormat(wsHstry->getFormat()); - lastAddedIdx = wdHstry->getLastIdx(); - + oldShowTruW = showTruW; } - // Identify buffer size and buffer start position for chart - count = wdHstry->getCurrentSize(); - currIdx = wdHstry->getLastIdx(); - numAddedBufVals = (currIdx - lastAddedIdx + bufSize) % bufSize; // Number of values added to buffer since last display - if (dataIntv != oldDataIntv || count == 1) { - // new data interval selected by user; this is only x * 230 values instead of 240 seconds (4 minutes) per interval step - intvBufSize = cHeight * dataIntv; - numWndVals = min(count, (cHeight - 60) * dataIntv); - bufStart = max(0, count - numWndVals); - lastAddedIdx = currIdx; - oldDataIntv = dataIntv; - } else { - numWndVals = numWndVals + numAddedBufVals; - lastAddedIdx = currIdx; - if (count == bufSize) { - bufStart = max(0, bufStart - numAddedBufVals); - } - } - // LOG_DEBUG(GwLog::DEBUG,"PSRAM Size: %d kByte; free: %d Byte", ESP.getPsramSize()/1024, ESP.getFreePsram()); - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot Dataset: count: %d, xWD: %.1f, xWS: %.2f, xWD_valid? %d, intvBufSize: %d, numWndVals: %d, bufStart: %d, numAddedBufVals: %d, lastIdx: %d, wind source: %s", - count, wdHstry->getLast() / 1000.0 * radToDeg, wsHstry->getLast() / 1000.0 * 1.94384, BDataValid[0], intvBufSize, numWndVals, bufStart, numAddedBufVals, wdHstry->getLastIdx(), - showTruW ? "True" : "App"); - - // Set wndCenter from 1st real buffer value - if (wndCenter == INT_MAX || (wndCenter == 0 && count == 1)) { - wndCenter = getCntr(*wdHstry, numWndVals); - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot Range Init: count: %d, xWD: %.1f, wndCenter: %d, diffRng: %d, chrtRng: %d, Min: %.0f, Max: %.0f", count, wdHstry->getLast() / 1000.0 * radToDeg, - wndCenter, diffRng, chrtRng, wdHstry->getMin(numWndVals) / 1000.0 * radToDeg, wdHstry->getMax(numWndVals) / 1000.0 * radToDeg); - } else { - // check and adjust range between left, center, and right chart limit - diffRng = getRng(*wdHstry, wndCenter, numWndVals); - diffRng = (diffRng == wdMAX_VAL ? 0 : diffRng); - if (diffRng > chrtRng) { - chrtRng = int((diffRng + (diffRng >= 0 ? 9 : -1)) / 10) * 10; // Round up to next 10 degree value - } else if (diffRng + 10 < chrtRng) { // Reduce chart range for higher resolution if possible - chrtRng = max(dfltRng, int((diffRng + (diffRng >= 0 ? 9 : -1)) / 10) * 10); - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot Range adjust: wndCenter: %d, diffRng: %d, chrtRng: %d, Min: %.0f, Max: %.0f", wndCenter, diffRng, chrtRng, - wdHstry->getMin(numWndVals) / 1000.0 * radToDeg, wdHstry->getMax(numWndVals) / 1000.0 * radToDeg); - } - } - chrtScl = float(width) / float(chrtRng) / 2.0; // Chart scale: pixels per degree - wndLeft = wndCenter - chrtRng; - if (wndLeft < 0) - wndLeft += 360; - wndRight = (chrtRng < 180 ? wndCenter + chrtRng : wndCenter + chrtRng - 1); - if (wndRight >= 360) - wndRight -= 360; - // Draw page - //*********************************************************************** + //*********************************************************** // Set display in partial refresh mode getdisplay().setPartialWindow(0, 0, width, height); // Set partial update getdisplay().setTextColor(commonData->fgcolor); - // chart lines - getdisplay().fillRect(0, yOffset, width, 2, commonData->fgcolor); - getdisplay().fillRect(xCenter, yOffset, 1, cHeight, commonData->fgcolor); + if (chrtMode == 'D') { + wdBVal->value = wdHstry->getLast(); + wdBVal->valid = wdBVal->value != wdHstry->getMaxVal(); + wdFlChart->showChrt(dataIntv, *bvalue[0]); - // chart labels - char sWndLbl[4]; // char buffer for Wind angle label - getdisplay().setFont(&Ubuntu_Bold12pt8b); - getdisplay().setCursor(xCenter - 88, yOffset - 3); - getdisplay().print(wdName); // Wind data name - snprintf(sWndLbl, 4, "%03d", (wndCenter < 0) ? (wndCenter + 360) : wndCenter); - drawTextCenter(xCenter, yOffset - 11, sWndLbl); - getdisplay().drawCircle(xCenter + 25, yOffset - 17, 2, commonData->fgcolor); // symbol - getdisplay().drawCircle(xCenter + 25, yOffset - 17, 3, commonData->fgcolor); // symbol - getdisplay().setCursor(1, yOffset - 3); - snprintf(sWndLbl, 4, "%03d", (wndLeft < 0) ? (wndLeft + 360) : wndLeft); - getdisplay().print(sWndLbl); // Wind left value - getdisplay().drawCircle(46, yOffset - 17, 2, commonData->fgcolor); // symbol - getdisplay().drawCircle(46, yOffset - 17, 3, commonData->fgcolor); // symbol - getdisplay().setCursor(width - 50, yOffset - 3); - snprintf(sWndLbl, 4, "%03d", (wndRight < 0) ? (wndRight + 360) : wndRight); - getdisplay().print(sWndLbl); // Wind right value - getdisplay().drawCircle(width - 5, yOffset - 17, 2, commonData->fgcolor); // symbol - getdisplay().drawCircle(width - 5, yOffset - 17, 3, commonData->fgcolor); // symbol + } else if (chrtMode == 'S') { + wsBVal->value = wsHstry->getLast(); + wsBVal->valid = wsBVal->value != wsHstry->getMaxVal(); + wsFlChart->showChrt(dataIntv, *bvalue[1]); - if (wdHstry->getMax() == wdMAX_VAL) { - // only values in buffer -> no valid wind data available - wndDataValid = false; - } else if (!BDataValid[0] && !useSimuData) { - // currently no valid xWD data available and no simulation mode - numNoData++; - wndDataValid = true; - if (numNoData > 3) { - // If more than 4 invalid values in a row, send message - wndDataValid = false; - } - } else { - numNoData = 0; // reset data error counter - wndDataValid = true; // At least some wind data available - } - // Draw wind values in chart - //*********************************************************************** - if (wndDataValid) { - for (int i = 0; i < (numWndVals / dataIntv); i++) { - chrtVal = static_cast(wdHstry->get(bufStart + (i * dataIntv))); // show the latest wind values in buffer; keep 1st value constant in a rolling buffer - if (chrtVal == wdMAX_VAL) { - chrtPrevVal = wdMAX_VAL; - } else { - chrtVal = static_cast((chrtVal / 1000.0 * radToDeg) + 0.5); // Convert to degrees and round - x = ((chrtVal - wndLeft + 360) % 360) * chrtScl; - y = yOffset + cHeight - i; // Position in chart area - - if (i >= (numWndVals / dataIntv) - 1) // log chart data of 1 line (adjust for test purposes) - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot Chart: i: %d, chrtVal: %d, bufStart: %d, count: %d, linesToShow: %d", i, chrtVal, bufStart, count, (numWndVals / dataIntv)); - - if ((i == 0) || (chrtPrevVal == wdMAX_VAL)) { - // just a dot for 1st chart point or after some invalid values - prevX = x; - prevY = y; - } else { - // cross borders check; shift values to [-180..0..180]; when crossing borders, range is 2x 180 degrees - int wndLeftDlt = -180 - ((wndLeft >= 180) ? (wndLeft - 360) : wndLeft); - int chrtVal180 = ((chrtVal + wndLeftDlt + 180) % 360 + 360) % 360 - 180; - int chrtPrevVal180 = ((chrtPrevVal + wndLeftDlt + 180) % 360 + 360) % 360 - 180; - if (((chrtPrevVal180 >= -180) && (chrtPrevVal180 < -90) && (chrtVal180 > 90)) || ((chrtPrevVal180 <= 179) && (chrtPrevVal180 > 90) && chrtVal180 <= -90)) { - // If current value crosses chart borders compared to previous value, split line - int xSplit = (((chrtPrevVal180 > 0 ? wndRight : wndLeft) - wndLeft + 360) % 360) * chrtScl; - getdisplay().drawLine(prevX, prevY, xSplit, y, commonData->fgcolor); - getdisplay().drawLine(prevX, prevY - 1, ((xSplit != prevX) ? xSplit : xSplit - 1), ((xSplit != prevX) ? y - 1 : y), commonData->fgcolor); - prevX = (((chrtVal180 > 0 ? wndRight : wndLeft) - wndLeft + 360) % 360) * chrtScl; - } - } - - // Draw line with 2 pixels width + make sure vertical line are drawn correctly - getdisplay().drawLine(prevX, prevY, x, y, commonData->fgcolor); - getdisplay().drawLine(prevX, prevY - 1, ((x != prevX) ? x : x - 1), ((x != prevX) ? y - 1 : y), commonData->fgcolor); - chrtPrevVal = chrtVal; - prevX = x; - prevY = y; - } - // Reaching chart area top end - if (i >= (cHeight - 1)) { - oldDataIntv = 0; // force reset of buffer start and number of values to show in next display loop - - int minWndDir = wdHstry->getMin(numWndVals) / 1000.0 * radToDeg; - int maxWndDir = wdHstry->getMax(numWndVals) / 1000.0 * radToDeg; - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot FreeTop: Minimum: %d, Maximum: %d, OldwndCenter: %d", minWndDir, maxWndDir, wndCenter); - // if (((minWndDir - wndCenter >= 0) && (minWndDir - wndCenter < 180)) || ((maxWndDir - wndCenter <= 0) && (maxWndDir - wndCenter >=180))) { - if ((wndRight > wndCenter && (minWndDir >= wndCenter && minWndDir <= wndRight)) || (wndRight <= wndCenter && (minWndDir >= wndCenter || minWndDir <= wndRight)) || (wndLeft < wndCenter && (maxWndDir <= wndCenter && maxWndDir >= wndLeft)) || (wndLeft >= wndCenter && (maxWndDir <= wndCenter || maxWndDir >= wndLeft))) { - // Check if all wind value are left or right of center value -> optimize chart center - wndCenter = getCntr(*wdHstry, numWndVals); - } - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot FreeTop: cHeight: %d, bufStart: %d, numWndVals: %d, wndCenter: %d", cHeight, bufStart, numWndVals, wndCenter); - break; - } - } - - // Print wind speed value - int currentZone; - static int lastZone = 0; - static bool flipTws = false; - int xPosTws; - static const int yPosTws = yOffset + 40; - - xPosTws = flipTws ? 20 : width - 145; - currentZone = (y >= yPosTws - 38) && (y <= yPosTws + 6) && (x >= xPosTws - 4) && (x <= xPosTws + 146) ? 1 : 0; // Define current zone for TWS value - if (currentZone != lastZone) { - // Only flip when x moves to a different zone - if ((y >= yPosTws - 38) && (y <= yPosTws + 6) && (x >= xPosTws - 4) && (x <= xPosTws + 146)) { - flipTws = !flipTws; - xPosTws = flipTws ? 20 : width - 145; - } - } - lastZone = currentZone; - - wsValue = wsHstry->getLast(); - wsBVal->value = wsValue / 1000.0; // temp variable to retreive data unit from OBP60Formater - wsBVal->valid = (static_cast(wsValue) != wsHstry->getMinVal()); - String swsValue = formatValue(wsBVal, *commonData).svalue; // value (string) - wsUnit = formatValue(wsBVal, *commonData).unit; // Unit of value - getdisplay().fillRect(xPosTws - 4, yPosTws - 38, 142, 44, commonData->bgcolor); // Clear area for TWS value - getdisplay().setFont(&DSEG7Classic_BoldItalic16pt7b); - getdisplay().setCursor(xPosTws, yPosTws); - getdisplay().print(swsValue); // Value -/* if (!wsBVal->valid) { - getdisplay().print("--.-"); - } else { - wsValue = wsValue / 1000.0 * 1.94384; // Wind speed value in knots - if (wsValue < 10.0) { - getdisplay().printf("!%3.1f", wsValue); // Value, round to 1 decimal - } else { - getdisplay().printf("%4.1f", wsValue); // Value, round to 1 decimal - } - } */ - getdisplay().setFont(&Ubuntu_Bold12pt8b); - getdisplay().setCursor(xPosTws + 82, yPosTws - 14); - getdisplay().print(wsName); // Name - getdisplay().setFont(&Ubuntu_Bold8pt8b); - getdisplay().setCursor(xPosTws + 82, yPosTws + 1); - getdisplay().print(wsUnit); // Unit - - } else { - // No valid data available - LOG_DEBUG(GwLog::LOG, "PageWindPlot: No valid data available"); - getdisplay().setFont(&Ubuntu_Bold10pt8b); - getdisplay().fillRect(xCenter - 33, height / 2 - 20, 66, 24, commonData->bgcolor); // Clear area for message - drawTextCenter(xCenter, height / 2 - 10, "No data"); + } else if (chrtMode == 'B') { + wdBVal->value = wdHstry->getLast(); + wdBVal->valid = wdBVal->value != wdHstry->getMaxVal(); + wsBVal->value = wsHstry->getLast(); + wsBVal->valid = wsBVal->value != wsHstry->getMaxVal(); + LOG_DEBUG(GwLog::DEBUG, "PageWindPlot showChrt: wsBVal.name: %s, format: %s, wsBVal.value: %.1f, valid: %d, address: %p", wsBVal->getName(), wsBVal->getFormat(), wsBVal->value, + wsBVal->valid, wsBVal); + wdHfChart->showChrt(dataIntv, *bvalue[0]); + wsHfChart->showChrt(dataIntv, *bvalue[1]); } - // chart Y axis labels; print at last to overwrite potential chart lines in label area - int yPos; - int chrtLbl; - getdisplay().setFont(&Ubuntu_Bold8pt8b); - for (int i = 1; i <= 3; i++) { - yPos = yOffset + (i * 60); - getdisplay().fillRect(0, yPos, width, 1, commonData->fgcolor); - getdisplay().fillRect(0, yPos - 8, 24, 16, commonData->bgcolor); // Clear small area to remove potential chart lines - getdisplay().setCursor(1, yPos + 4); - if (count >= intvBufSize) { - // Calculate minute value for label - chrtLbl = ((i - 1 + (prevY < yOffset + 30)) * dataIntv) * -1; // change label if last data point is more than 30 lines (= seconds) from chart line - } else { - int j = 3 - i; - chrtLbl = (int((((numWndVals / dataIntv) - 50) * dataIntv / 60) + 1) - (j * dataIntv)) * -1; // 50 lines left below last chart line - } - getdisplay().printf("%3d", chrtLbl); // Wind value label - } - - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot time: %ld", millis() - timer); + LOG_DEBUG(GwLog::LOG, "PageWindPlot: page time %ldms", millis() - pageTime); return PAGE_UPDATE; - }; + } }; static Page* createPage(CommonData& common) @@ -523,7 +259,7 @@ PageDescription registerPageWindPlot( "WindPlot", // Page name createPage, // Action 0, // Number of bus values depends on selection in Web configuration - { "TWD", "AWD" }, // Bus values we need in the page + { "TWD", "TWS", "AWD", "AWS" }, // Bus values we need in the page true // Show display header on/off ); diff --git a/lib/obp60task/Pagedata.h b/lib/obp60task/Pagedata.h index 3f56a7c..9c515b4 100644 --- a/lib/obp60task/Pagedata.h +++ b/lib/obp60task/Pagedata.h @@ -195,9 +195,10 @@ String formatLongitude(double lon); // Structure for formatted boat values typedef struct{ - double value; - String svalue; - String unit; + double value; // SI value of boat data value + double cvalue; // value converted to target unit + String svalue; // value converted to target unit and formatted + String unit; // target value unit } FormattedData; // Formatter for boat values diff --git a/lib/obp60task/config.json b/lib/obp60task/config.json index a0080a4..088001a 100644 --- a/lib/obp60task/config.json +++ b/lib/obp60task/config.json @@ -224,7 +224,7 @@ "label": "Calculate True Wind", "type": "boolean", "default": "false", - "description": "If not available, calculate true wind data from appearant wind and other boat data", + "description": "If not available, calculate true wind data from apparent wind and other boat data", "category": "OBP60 Settings", "capabilities": { "obp60": "true" diff --git a/lib/obp60task/config_obp40.json b/lib/obp60task/config_obp40.json index ff84a89..eaca892 100644 --- a/lib/obp60task/config_obp40.json +++ b/lib/obp60task/config_obp40.json @@ -224,7 +224,7 @@ "label": "Calculate True Wind", "type": "boolean", "default": "false", - "description": "If not available, calculate true wind data from appearant wind and other boat data", + "description": "If not available, calculate true wind data from apparent wind and other boat data", "category": "OBP40 Settings", "capabilities": { "obp40": "true" @@ -1654,7 +1654,7 @@ "description": "Wind source for page 1: [true|apparent]", "list": [ "True wind", - "Apparant wind" + "apparent wind" ], "category": "OBP40 Page 1", "capabilities": { @@ -1975,7 +1975,7 @@ "description": "Wind source for page 2: [true|apparent]", "list": [ "True wind", - "Apparant wind" + "apparent wind" ], "category": "OBP40 Page 2", "capabilities": { @@ -2287,7 +2287,7 @@ "description": "Wind source for page 3: [true|apparent]", "list": [ "True wind", - "Apparant wind" + "apparent wind" ], "category": "OBP40 Page 3", "capabilities": { @@ -2590,7 +2590,7 @@ "description": "Wind source for page 4: [true|apparent]", "list": [ "True wind", - "Apparant wind" + "apparent wind" ], "category": "OBP40 Page 4", "capabilities": { @@ -2884,7 +2884,7 @@ "description": "Wind source for page 5: [true|apparent]", "list": [ "True wind", - "Apparant wind" + "apparent wind" ], "category": "OBP40 Page 5", "capabilities": { @@ -3169,7 +3169,7 @@ "description": "Wind source for page 6: [true|apparent]", "list": [ "True wind", - "Apparant wind" + "apparent wind" ], "category": "OBP40 Page 6", "capabilities": { @@ -3445,7 +3445,7 @@ "description": "Wind source for page 7: [true|apparent]", "list": [ "True wind", - "Apparant wind" + "apparent wind" ], "category": "OBP40 Page 7", "capabilities": { @@ -3712,7 +3712,7 @@ "description": "Wind source for page 8: [true|apparent]", "list": [ "True wind", - "Apparant wind" + "apparent wind" ], "category": "OBP40 Page 8", "capabilities": { @@ -3970,7 +3970,7 @@ "description": "Wind source for page 9: [true|apparent]", "list": [ "True wind", - "Apparant wind" + "apparent wind" ], "category": "OBP40 Page 9", "capabilities": { @@ -4219,7 +4219,7 @@ "description": "Wind source for page 10: [true|apparent]", "list": [ "True wind", - "Apparant wind" + "apparent wind" ], "category": "OBP40 Page 10", "capabilities": { diff --git a/lib/obp60task/gen_set.py b/lib/obp60task/gen_set.py index 9d1b0ff..97ca0cf 100755 --- a/lib/obp60task/gen_set.py +++ b/lib/obp60task/gen_set.py @@ -157,7 +157,7 @@ def create_json(device, no_of_pages, pagedata): "description": f"Wind source for page {page_no}: [true|apparent]", "list": [ "True wind", - "Apparant wind" + "Apparent wind" ], "category": category, "capabilities": capabilities, diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp index a8590e0..17e586a 100644 --- a/lib/obp60task/obp60task.cpp +++ b/lib/obp60task/obp60task.cpp @@ -808,7 +808,7 @@ void OBP60Task(GwApi *api){ if (calcTrueWnds) { trueWind.addTrueWind(api, &boatValues, logger); } - // Handle history buffers for TWD, TWS for wind plot page and other usage + // Handle history buffers for certain boat data for windplot page and other usage hstryBufList.handleHstryBuf(useSimuData); // Clear display