Created new page SkyView. Additionally some graphics improvements.

This commit is contained in:
Thomas Hooge 2025-08-23 09:53:26 +02:00
parent a21ce00260
commit 3eb2c8093e
9 changed files with 296 additions and 45 deletions

View File

@ -0,0 +1,25 @@
/*
Generic graphics functions
*/
#include <math.h>
#include "Graphics.h"
Point rotatePoint(const Point& origin, const Point& p, double angle) {
// rotate poind around origin by degrees
Point rotated;
double phi = angle * M_PI / 180.0;
double dx = p.x - origin.x;
double dy = p.y - origin.y;
rotated.x = origin.x + cos(phi) * dx - sin(phi) * dy;
rotated.y = origin.y + sin(phi) * dx + cos(phi) * dy;
return rotated;
}
std::vector<Point> rotatePoints(const Point& origin, const std::vector<Point>& pts, double angle) {
std::vector<Point> rotatedPoints;
for (const auto& p : pts) {
rotatedPoints.push_back(rotatePoint(origin, p, angle));
}
return rotatedPoints;
}

17
lib/obp60task/Graphics.h Normal file
View File

@ -0,0 +1,17 @@
#pragma once
#include <vector>
struct Point {
double x;
double y;
};
struct Rect {
double x;
double y;
double w;
double h;
};
Point rotatePoint(const Point& origin, const Point& p, double angle);
std::vector<Point> rotatePoints(const Point& origin, const std::vector<Point>& pts, double angle);

View File

@ -362,30 +362,20 @@ String xdrDelete(String input){
return input; return input;
} }
Point rotatePoint(const Point& origin, const Point& p, double angle) {
// rotate poind around origin by degrees
Point rotated;
double phi = angle * M_PI / 180.0;
double dx = p.x - origin.x;
double dy = p.y - origin.y;
rotated.x = origin.x + cos(phi) * dx - sin(phi) * dy;
rotated.y = origin.y + sin(phi) * dx + cos(phi) * dy;
return rotated;
}
std::vector<Point> rotatePoints(const Point& origin, const std::vector<Point>& pts, double angle) {
std::vector<Point> rotatedPoints;
for (const auto& p : pts) {
rotatedPoints.push_back(rotatePoint(origin, p, angle));
}
return rotatedPoints;
}
void fillPoly4(const std::vector<Point>& p4, uint16_t color) { void fillPoly4(const std::vector<Point>& p4, uint16_t color) {
getdisplay().fillTriangle(p4[0].x, p4[0].y, p4[1].x, p4[1].y, p4[2].x, p4[2].y, color); getdisplay().fillTriangle(p4[0].x, p4[0].y, p4[1].x, p4[1].y, p4[2].x, p4[2].y, color);
getdisplay().fillTriangle(p4[0].x, p4[0].y, p4[2].x, p4[2].y, p4[3].x, p4[3].y, color); getdisplay().fillTriangle(p4[0].x, p4[0].y, p4[2].x, p4[2].y, p4[3].x, p4[3].y, color);
} }
void drawPoly(const std::vector<Point>& points, uint16_t color) {
size_t polysize = points.size();
for (size_t i = 0; i < polysize - 1; i++) {
getdisplay().drawLine(points[i].x, points[i].y, points[i+1].x, points[i+1].y, color);
}
// close path
getdisplay().drawLine(points[polysize-1].x, points[polysize-1].y, points[0].x, points[0].y, color);
}
// Split string into words, whitespace separated // Split string into words, whitespace separated
std::vector<String> split(const String &s) { std::vector<String> split(const String &s) {
std::vector<String> words; std::vector<String> words;
@ -447,6 +437,24 @@ void drawTextRalign(int16_t x, int16_t y, String text) {
getdisplay().print(text); getdisplay().print(text);
} }
// Draw text inside box, normal or inverted
void drawTextBoxed(Rect box, String text, uint16_t fg, uint16_t bg, bool inverted, bool border) {
if (inverted) {
getdisplay().fillRect(box.x, box.y, box.w, box.h, fg);
getdisplay().setTextColor(bg);
} else {
if (border) {
getdisplay().fillRect(box.x + 1, box.y + 1, box.w - 2, box.h - 2, bg);
getdisplay().drawRect(box.x, box.y, box.w, box.h, fg);
}
getdisplay().setTextColor(fg);
}
uint16_t border_offset = box.h / 4; // 25% of box height
getdisplay().setCursor(box.x + border_offset, box.y + box.h - border_offset);
getdisplay().print(text);
getdisplay().setTextColor(fg);
}
// Show a triangle for trend direction high (x, y is the left edge) // Show a triangle for trend direction high (x, y is the left edge)
void displayTrendHigh(int16_t x, int16_t y, uint16_t size, uint16_t color){ void displayTrendHigh(int16_t x, int16_t y, uint16_t size, uint16_t color){
getdisplay().fillTriangle(x, y, x+size*2, y, x+size, y-size*2, color); getdisplay().fillTriangle(x, y, x+size*2, y, x+size, y-size*2, color);

View File

@ -4,6 +4,7 @@
#include <Arduino.h> #include <Arduino.h>
#include "OBP60Hardware.h" #include "OBP60Hardware.h"
#include "LedSpiTask.h" #include "LedSpiTask.h"
#include "Graphics.h"
#include <GxEPD2_BW.h> // E-paper lib V2 #include <GxEPD2_BW.h> // E-paper lib V2
#include <Adafruit_FRAM_I2C.h> // I2C FRAM #include <Adafruit_FRAM_I2C.h> // I2C FRAM
@ -73,13 +74,8 @@ GxEPD2_BW<GxEPD2_420_SE0420NQ04, GxEPD2_420_SE0420NQ04::HEIGHT> & getdisplay();
#define PAGE_UPDATE 1 // page wants display to update #define PAGE_UPDATE 1 // page wants display to update
#define PAGE_HIBERNATE 2 // page wants displey to hibernate #define PAGE_HIBERNATE 2 // page wants displey to hibernate
struct Point {
double x;
double y;
};
Point rotatePoint(const Point& origin, const Point& p, double angle);
std::vector<Point> rotatePoints(const Point& origin, const std::vector<Point>& pts, double angle);
void fillPoly4(const std::vector<Point>& p4, uint16_t color); void fillPoly4(const std::vector<Point>& p4, uint16_t color);
void drawPoly(const std::vector<Point>& points, uint16_t color);
void deepSleep(CommonData &common); void deepSleep(CommonData &common);
@ -108,6 +104,7 @@ String xdrDelete(String input); // Delete xdr prefix from string
void drawTextCenter(int16_t cx, int16_t cy, String text); void drawTextCenter(int16_t cx, int16_t cy, String text);
void drawTextRalign(int16_t x, int16_t y, String text); void drawTextRalign(int16_t x, int16_t y, String text);
void drawTextBoxed(Rect box, String text, uint16_t fg, uint16_t bg, bool inverted, bool border);
void displayTrendHigh(int16_t x, int16_t y, uint16_t size, uint16_t color); void displayTrendHigh(int16_t x, int16_t y, uint16_t size, uint16_t color);
void displayTrendLow(int16_t x, int16_t y, uint16_t size, uint16_t color); void displayTrendLow(int16_t x, int16_t y, uint16_t size, uint16_t color);

View File

@ -0,0 +1,180 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#if defined BOARD_OBP60S3 || defined BOARD_OBP40S3
#include "Pagedata.h"
#include "OBP60Extensions.h"
#include <vector>
#include <algorithm> // for vector sorting
/*
* SkyView / Satellites
*/
class PageSkyView : public Page
{
private:
String flashLED;
GwBoatData *bd;
public:
PageSkyView(CommonData &common)
{
commonData = &common;
// task name access is for example purpose only
TaskHandle_t currentTaskHandle = xTaskGetCurrentTaskHandle();
const char* taskName = pcTaskGetName(currentTaskHandle);
common.logger->logDebug(GwLog::LOG, "Instantiate PageSkyView in task '%s'", taskName);
flashLED = common.config->getString(common.config->flashLED);
}
int handleKey(int key) {
// return 0 to mark the key handled completely
// return the key to allow further action
if (key == 11) {
commonData->keylock = !commonData->keylock;
return 0;
}
return key;
}
void displayNew(PageData &pageData) {
#ifdef BOARD_OBP60S3
// Clear optical warning
if (flashLED == "Limit Violation") {
setBlinkingLED(false);
setFlashLED(false);
}
#endif
bd = pageData.api->getBoatData();
};
// Comparator function to sort by SNR
static bool compareBySNR(const GwSatInfo& a, const GwSatInfo& b) {
return a.SNR > b.SNR; // Sort in descending order
}
int displayPage(PageData &pageData) {
GwLog *logger = commonData->logger;
std::vector<GwSatInfo> sats;
int nSat = bd->SatInfo->getNumSats();
logger->logDebug(GwLog::LOG, "Drawing at PageSkyView, %d satellites", nSat);
for (int i = 0; i < nSat; i++) {
sats.push_back(*bd->SatInfo->getAt(i));
}
std::sort(sats.begin(), sats.end(), compareBySNR);
// Draw page
//***********************************************************
// Set display in partial refresh mode
getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update
// current position
getdisplay().setFont(&Ubuntu_Bold8pt8b);
// sky view
Point c = {130, 148};
uint16_t r = 125;
uint16_t r1 = r / 2;
getdisplay().fillCircle(c.x, c.y, r, commonData->bgcolor);
getdisplay().drawCircle(c.x, c.y, r + 1, commonData->fgcolor);
getdisplay().drawCircle(c.x, c.y, r + 2, commonData->fgcolor);
getdisplay().drawCircle(c.x, c.y, r1, commonData->fgcolor);
// separation lines
getdisplay().drawLine(c.x - r, c.y, c.x + r, c.y, commonData->fgcolor);
getdisplay().drawLine(c.x, c.y - r, c.x, c.y + r, commonData->fgcolor);
Point p = {c.x, c.y - r};
Point p1, p2;
p1 = rotatePoint(c, p, 45);
p2 = rotatePoint(c, p, 45 + 180);
getdisplay().drawLine(p1.x, p1.y, p2.x, p2.y, commonData->fgcolor);
p1 = rotatePoint(c, p, -45);
p2 = rotatePoint(c, p, -45 + 180);
getdisplay().drawLine(p1.x, p1.y, p2.x, p2.y, commonData->fgcolor);
// directions
int16_t x1, y1;
uint16_t w, h;
getdisplay().setFont(&Ubuntu_Bold12pt8b);
getdisplay().getTextBounds("N", 0, 150, &x1, &y1, &w, &h);
getdisplay().setCursor(c.x - w / 2, c.y - r + h + 2);
getdisplay().print("N");
getdisplay().getTextBounds("S", 0, 150, &x1, &y1, &w, &h);
getdisplay().setCursor(c.x - w / 2, c.y + r - 2);
getdisplay().print("S");
getdisplay().getTextBounds("E", 0, 150, &x1, &y1, &w, &h);
getdisplay().setCursor(c.x + r - w - 2, c.y + h / 2);
getdisplay().print("E");
getdisplay().getTextBounds("W", 0, 150, &x1, &y1, &w, &h);
getdisplay().setCursor(c.x - r + 2 , c.y + h / 2);
getdisplay().print("W");
getdisplay().setFont(&Ubuntu_Bold8pt8b);
// show satellites in "map"
for (int i = 0; i < nSat; i++) {
float arad = sats[i].Azimut * M_PI / 180.0;
float erad = sats[i].Elevation * M_PI / 180.0;
uint16_t x = c.x + sin(arad) * erad * r;
uint16_t y = c.y + cos(arad) * erad * r;
getdisplay().drawRect(x-4, y-4, 8, 8, commonData->fgcolor);
}
// Signal / Noise bars
getdisplay().setCursor(325, 34);
getdisplay().print("SNR");
getdisplay().drawRect(270, 20, 125, 257, commonData->fgcolor);
int maxsat = std::min(nSat, 12);
for (int i = 0; i < maxsat; i++) {
uint16_t y = 29 + (i + 1) * 20;
getdisplay().setCursor(276, y);
char buffer[3];
snprintf(buffer, 3, "%02d", static_cast<int>(sats[i].PRN));
getdisplay().print(String(buffer));
getdisplay().drawRect(305, y-12, 85, 14, commonData->fgcolor);
getdisplay().setCursor(315, y);
// TODO SNR as number or as bar via mode key?
if (sats[i].SNR <= 100) {
// getdisplay().print(sats[i].SNR);
getdisplay().fillRect(307, y-10, int(81 * sats[i].SNR / 100.0), 10, commonData->fgcolor);
} else {
getdisplay().print("n/a");
}
}
return PAGE_UPDATE;
};
};
static Page* createPage(CommonData &common){
return new PageSkyView(common);
}
/**
* with the code below we make this page known to the PageTask
* we give it a type (name) that can be selected in the config
* we define which function is to be called
* and we provide the number of user parameters we expect
* this will be number of BoatValue pointers in pageData.values
*/
PageDescription registerPageSkyView(
"SkyView", // Page name
createPage, // Action
0, // Number of bus values depends on selection in Web configuration
true // Show display header on/off
);
#endif

View File

@ -12,6 +12,7 @@
typedef std::vector<GwApi::BoatValue *> ValueList; typedef std::vector<GwApi::BoatValue *> ValueList;
typedef struct{ typedef struct{
GwApi *api;
String pageName; String pageName;
uint8_t pageNumber; // page number in sequence of visible pages uint8_t pageNumber; // page number in sequence of visible pages
//the values will always contain the user defined values first //the values will always contain the user defined values first
@ -100,15 +101,15 @@ typedef struct{
typedef struct{ typedef struct{
GwApi::Status status; GwApi::Status status;
GwLog *logger=NULL; GwLog *logger = nullptr;
GwConfigHandler *config=NULL; GwConfigHandler *config = nullptr;
SensorData data; SensorData data;
SunData sundata; SunData sundata;
TouchKeyData keydata[6]; TouchKeyData keydata[6];
BacklightData backlight; BacklightData backlight;
AlarmData alarm; AlarmData alarm;
GwApi::BoatValue *time=NULL; GwApi::BoatValue *time = nullptr;
GwApi::BoatValue *date=NULL; GwApi::BoatValue *date = nullptr;
uint16_t fgcolor; uint16_t fgcolor;
uint16_t bgcolor; uint16_t bgcolor;
bool keylock = false; bool keylock = false;
@ -182,9 +183,9 @@ class PageDescription{
class PageStruct{ class PageStruct{
public: public:
Page *page=NULL; Page *page = nullptr;
PageData parameters; PageData parameters;
PageDescription *description=NULL; PageDescription *description = nullptr;
}; };
// Standard format functions without overhead // Standard format functions without overhead

View File

@ -1333,6 +1333,7 @@
"RollPitch", "RollPitch",
"RudderPosition", "RudderPosition",
"SixValues", "SixValues",
"SkyView",
"Solar", "Solar",
"ThreeValues", "ThreeValues",
"TwoValues", "TwoValues",
@ -1608,6 +1609,7 @@
"RollPitch", "RollPitch",
"RudderPosition", "RudderPosition",
"SixValues", "SixValues",
"SkyView",
"Solar", "Solar",
"ThreeValues", "ThreeValues",
"TwoValues", "TwoValues",
@ -1880,6 +1882,7 @@
"RollPitch", "RollPitch",
"RudderPosition", "RudderPosition",
"SixValues", "SixValues",
"SkyView",
"Solar", "Solar",
"ThreeValues", "ThreeValues",
"TwoValues", "TwoValues",
@ -2149,6 +2152,7 @@
"RollPitch", "RollPitch",
"RudderPosition", "RudderPosition",
"SixValues", "SixValues",
"SkyView",
"Solar", "Solar",
"ThreeValues", "ThreeValues",
"TwoValues", "TwoValues",
@ -2415,6 +2419,7 @@
"RollPitch", "RollPitch",
"RudderPosition", "RudderPosition",
"SixValues", "SixValues",
"SkyView",
"Solar", "Solar",
"ThreeValues", "ThreeValues",
"TwoValues", "TwoValues",
@ -2678,6 +2683,7 @@
"RollPitch", "RollPitch",
"RudderPosition", "RudderPosition",
"SixValues", "SixValues",
"SkyView",
"Solar", "Solar",
"ThreeValues", "ThreeValues",
"TwoValues", "TwoValues",
@ -2938,6 +2944,7 @@
"RollPitch", "RollPitch",
"RudderPosition", "RudderPosition",
"SixValues", "SixValues",
"SkyView",
"Solar", "Solar",
"ThreeValues", "ThreeValues",
"TwoValues", "TwoValues",
@ -3195,6 +3202,7 @@
"RollPitch", "RollPitch",
"RudderPosition", "RudderPosition",
"SixValues", "SixValues",
"SkyView",
"Solar", "Solar",
"ThreeValues", "ThreeValues",
"TwoValues", "TwoValues",
@ -3449,6 +3457,7 @@
"RollPitch", "RollPitch",
"RudderPosition", "RudderPosition",
"SixValues", "SixValues",
"SkyView",
"Solar", "Solar",
"ThreeValues", "ThreeValues",
"TwoValues", "TwoValues",
@ -3700,6 +3709,7 @@
"RollPitch", "RollPitch",
"RudderPosition", "RudderPosition",
"SixValues", "SixValues",
"SkyView",
"Solar", "Solar",
"ThreeValues", "ThreeValues",
"TwoValues", "TwoValues",

View File

@ -1356,6 +1356,7 @@
"RollPitch", "RollPitch",
"RudderPosition", "RudderPosition",
"SixValues", "SixValues",
"SkyView",
"Solar", "Solar",
"ThreeValues", "ThreeValues",
"TwoValues", "TwoValues",
@ -1657,6 +1658,7 @@
"RollPitch", "RollPitch",
"RudderPosition", "RudderPosition",
"SixValues", "SixValues",
"SkyView",
"Solar", "Solar",
"ThreeValues", "ThreeValues",
"TwoValues", "TwoValues",
@ -1955,6 +1957,7 @@
"RollPitch", "RollPitch",
"RudderPosition", "RudderPosition",
"SixValues", "SixValues",
"SkyView",
"Solar", "Solar",
"ThreeValues", "ThreeValues",
"TwoValues", "TwoValues",
@ -2250,6 +2253,7 @@
"RollPitch", "RollPitch",
"RudderPosition", "RudderPosition",
"SixValues", "SixValues",
"SkyView",
"Solar", "Solar",
"ThreeValues", "ThreeValues",
"TwoValues", "TwoValues",
@ -2542,6 +2546,7 @@
"RollPitch", "RollPitch",
"RudderPosition", "RudderPosition",
"SixValues", "SixValues",
"SkyView",
"Solar", "Solar",
"ThreeValues", "ThreeValues",
"TwoValues", "TwoValues",
@ -2831,6 +2836,7 @@
"RollPitch", "RollPitch",
"RudderPosition", "RudderPosition",
"SixValues", "SixValues",
"SkyView",
"Solar", "Solar",
"ThreeValues", "ThreeValues",
"TwoValues", "TwoValues",
@ -3117,6 +3123,7 @@
"RollPitch", "RollPitch",
"RudderPosition", "RudderPosition",
"SixValues", "SixValues",
"SkyView",
"Solar", "Solar",
"ThreeValues", "ThreeValues",
"TwoValues", "TwoValues",
@ -3400,6 +3407,7 @@
"RollPitch", "RollPitch",
"RudderPosition", "RudderPosition",
"SixValues", "SixValues",
"SkyView",
"Solar", "Solar",
"ThreeValues", "ThreeValues",
"TwoValues", "TwoValues",
@ -3680,6 +3688,7 @@
"RollPitch", "RollPitch",
"RudderPosition", "RudderPosition",
"SixValues", "SixValues",
"SkyView",
"Solar", "Solar",
"ThreeValues", "ThreeValues",
"TwoValues", "TwoValues",
@ -3957,6 +3966,7 @@
"RollPitch", "RollPitch",
"RudderPosition", "RudderPosition",
"SixValues", "SixValues",
"SkyView",
"Solar", "Solar",
"ThreeValues", "ThreeValues",
"TwoValues", "TwoValues",

View File

@ -122,8 +122,8 @@ void OBP60Init(GwApi *api){
typedef struct { typedef struct {
int page0=0; int page0=0;
QueueHandle_t queue; QueueHandle_t queue;
GwLog* logger = NULL; GwLog* logger = nullptr;
// GwApi* api = NULL; // GwApi* api = nullptr;
uint sensitivity = 100; uint sensitivity = 100;
bool use_syspage = true; bool use_syspage = true;
} MyData; } MyData;
@ -266,6 +266,8 @@ void registerAllPages(PageList &list){
list.add(&registerPageXTETrack); list.add(&registerPageXTETrack);
extern PageDescription registerPageFluid; extern PageDescription registerPageFluid;
list.add(&registerPageFluid); list.add(&registerPageFluid);
extern PageDescription registerPageSkyView;
list.add(&registerPageSkyView);
} }
// Undervoltage detection for shutdown display // Undervoltage detection for shutdown display
@ -309,7 +311,6 @@ void underVoltageError(CommonData &common) {
getdisplay().nextPage(); // Partial update getdisplay().nextPage(); // Partial update
getdisplay().powerOff(); // Display power off getdisplay().powerOff(); // Display power off
#endif #endif
// Stop system
while (true) { while (true) {
esp_deep_sleep_start(); // Deep Sleep without wakeup. Wakeup only after power cycle (restart). esp_deep_sleep_start(); // Deep Sleep without wakeup. Wakeup only after power cycle (restart).
} }
@ -324,7 +325,6 @@ inline bool underVoltageDetection(float voffset, float vslope) {
float actVoltage = (float(analogRead(OBP_ANALOG0)) * 3.3 / 4096 + 0.17) * 20; // Vin = 1/20 for OBP60 float actVoltage = (float(analogRead(OBP_ANALOG0)) * 3.3 / 4096 + 0.17) * 20; // Vin = 1/20 for OBP60
float minVoltage = MIN_VOLTAGE; float minVoltage = MIN_VOLTAGE;
#endif #endif
// TODO Why double here?
float calVoltage = actVoltage * vslope + voffset; // Calibration float calVoltage = actVoltage * vslope + voffset; // Calibration
return (calVoltage < minVoltage); return (calVoltage < minVoltage);
} }
@ -665,6 +665,7 @@ void OBP60Task(GwApi *api){
pages[i].page=description->creator(commonData); pages[i].page=description->creator(commonData);
pages[i].parameters.pageName=pageType; pages[i].parameters.pageName=pageType;
pages[i].parameters.pageNumber = i + 1; pages[i].parameters.pageNumber = i + 1;
pages[i].parameters.api = api;
LOG_DEBUG(GwLog::DEBUG,"found page %s for number %d",pageType.c_str(),i); LOG_DEBUG(GwLog::DEBUG,"found page %s for number %d",pageType.c_str(),i);
//fill in all the user defined parameters //fill in all the user defined parameters
for (int uid=0;uid<description->userParam;uid++){ for (int uid=0;uid<description->userParam;uid++){
@ -787,6 +788,7 @@ void OBP60Task(GwApi *api){
// Undervoltage detection // Undervoltage detection
if (uvoltage == true) { if (uvoltage == true) {
if (underVoltageDetection(voffset, vslope)) { if (underVoltageDetection(voffset, vslope)) {
LOG_DEBUG(GwLog::ERROR, "Undervoltage detected, shutting down!");
underVoltageError(commonData); underVoltageError(commonData);
} }
} }
@ -1032,6 +1034,7 @@ void OBP60Task(GwApi *api){
if (systemPage) { if (systemPage) {
displayFooter(commonData); displayFooter(commonData);
PageData sysparams; // empty PageData sysparams; // empty
sysparams.api = api;
if (systemPageNew) { if (systemPageNew) {
syspage->displayNew(sysparams); syspage->displayNew(sysparams);
systemPageNew = false; systemPageNew = false;