mirror of
https://github.com/meshtastic/firmware.git
synced 2025-06-08 06:02:05 +00:00
Telemetry Screen Module
This commit is contained in:
parent
66a06230c4
commit
849e06497a
@ -52,6 +52,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#include "modules/WaypointModule.h"
|
||||
#include "sleep.h"
|
||||
#include "target_specific.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
|
||||
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
|
||||
#include "mesh/wifi/WiFiAPClient.h"
|
||||
@ -115,23 +116,6 @@ GeoCoord geoCoord;
|
||||
static bool heartbeat = false;
|
||||
#endif
|
||||
|
||||
// Quick access to screen dimensions from static drawing functions
|
||||
// DEPRECATED. To-do: move static functions inside Screen class
|
||||
#define SCREEN_WIDTH display->getWidth()
|
||||
#define SCREEN_HEIGHT display->getHeight()
|
||||
|
||||
// Pre-defined lines; this is intended to be used AFTER the common header
|
||||
#define compactFirstLine ((FONT_HEIGHT_SMALL - 1) * 1)
|
||||
#define compactSecondLine ((FONT_HEIGHT_SMALL - 1) * 2) - 2
|
||||
#define compactThirdLine ((FONT_HEIGHT_SMALL - 1) * 3) - 4
|
||||
#define compactFourthLine ((FONT_HEIGHT_SMALL - 1) * 4) - 6
|
||||
#define compactFifthLine ((FONT_HEIGHT_SMALL - 1) * 5) - 8
|
||||
|
||||
#define standardFirstLine (FONT_HEIGHT_SMALL + 1) * 1
|
||||
#define standardSecondLine (FONT_HEIGHT_SMALL + 1) * 2
|
||||
#define standardThirdLine (FONT_HEIGHT_SMALL + 1) * 3
|
||||
#define standardFourthLine (FONT_HEIGHT_SMALL + 1) * 4
|
||||
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include <Throttle.h>
|
||||
|
||||
@ -995,188 +979,6 @@ bool deltaToTimestamp(uint32_t secondsAgo, uint8_t *hours, uint8_t *minutes, int
|
||||
return validCached;
|
||||
}
|
||||
// *********************************
|
||||
// *Rounding Header when inverted *
|
||||
// *********************************
|
||||
void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r)
|
||||
{
|
||||
display->fillRect(x + r, y, w - 2 * r, h);
|
||||
display->fillRect(x, y + r, r, h - 2 * r);
|
||||
display->fillRect(x + w - r, y + r, r, h - 2 * r);
|
||||
display->fillCircle(x + r + 1, y + r, r);
|
||||
display->fillCircle(x + w - r - 1, y + r, r);
|
||||
display->fillCircle(x + r + 1, y + h - r - 1, r);
|
||||
display->fillCircle(x + w - r - 1, y + h - r - 1, r);
|
||||
}
|
||||
bool isBoltVisible = true;
|
||||
uint32_t lastBlink = 0;
|
||||
const uint32_t blinkInterval = 500;
|
||||
static uint32_t lastMailBlink = 0;
|
||||
static bool isMailIconVisible = true;
|
||||
constexpr uint32_t mailBlinkInterval = 500;
|
||||
|
||||
// ***********************
|
||||
// * Common Header *
|
||||
// ***********************
|
||||
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y)
|
||||
{
|
||||
constexpr int HEADER_OFFSET_Y = 1;
|
||||
y += HEADER_OFFSET_Y;
|
||||
|
||||
const bool isInverted = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED);
|
||||
const bool isBold = config.display.heading_bold;
|
||||
const int xOffset = 4;
|
||||
const int highlightHeight = FONT_HEIGHT_SMALL - 1;
|
||||
|
||||
display->setFont(FONT_SMALL);
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
|
||||
// === Background highlight ===
|
||||
if (isInverted) {
|
||||
drawRoundedHighlight(display, x, y, SCREEN_WIDTH, highlightHeight, 2);
|
||||
display->setColor(BLACK);
|
||||
}
|
||||
|
||||
// === Battery Vertical and Horizontal ===
|
||||
int chargePercent = powerStatus->getBatteryChargePercent();
|
||||
bool isCharging = powerStatus->getIsCharging() == OptionalBool::OptTrue;
|
||||
uint32_t now = millis();
|
||||
if (isCharging && now - lastBlink > blinkInterval) {
|
||||
isBoltVisible = !isBoltVisible;
|
||||
lastBlink = now;
|
||||
}
|
||||
|
||||
// Hybrid condition: wide screen AND landscape layout
|
||||
bool useHorizontalBattery = (SCREEN_WIDTH > 128 && SCREEN_WIDTH > SCREEN_HEIGHT);
|
||||
|
||||
if (useHorizontalBattery) {
|
||||
// === Horizontal battery ===
|
||||
int batteryX = 2;
|
||||
int batteryY = HEADER_OFFSET_Y + 2;
|
||||
|
||||
display->drawXbm(batteryX, batteryY, 29, 15, batteryBitmap_h);
|
||||
|
||||
if (isCharging && isBoltVisible) {
|
||||
display->drawXbm(batteryX + 9, batteryY + 1, 9, 13, lightning_bolt_h);
|
||||
} else if (isCharging && !isBoltVisible) {
|
||||
display->drawXbm(batteryX + 8, batteryY, 12, 15, batteryBitmap_sidegaps_h);
|
||||
} else {
|
||||
display->drawXbm(batteryX + 8, batteryY, 12, 15, batteryBitmap_sidegaps_h);
|
||||
int fillWidth = 24 * chargePercent / 100;
|
||||
int fillX = batteryX + fillWidth;
|
||||
display->fillRect(batteryX + 1, batteryY + 1, fillX, 13);
|
||||
}
|
||||
} else {
|
||||
// === Vertical battery ===
|
||||
int batteryX = 1;
|
||||
int batteryY = HEADER_OFFSET_Y + 1;
|
||||
#ifdef USE_EINK
|
||||
batteryY = batteryY + 2;
|
||||
#endif
|
||||
|
||||
display->drawXbm(batteryX, batteryY, 7, 11, batteryBitmap_v);
|
||||
|
||||
if (isCharging && isBoltVisible) {
|
||||
display->drawXbm(batteryX + 1, batteryY + 3, 5, 5, lightning_bolt_v);
|
||||
} else if (isCharging && !isBoltVisible) {
|
||||
display->drawXbm(batteryX - 1, batteryY + 4, 8, 3, batteryBitmap_sidegaps_v);
|
||||
} else {
|
||||
display->drawXbm(batteryX - 1, batteryY + 4, 8, 3, batteryBitmap_sidegaps_v);
|
||||
int fillHeight = 8 * chargePercent / 100;
|
||||
int fillY = batteryY - fillHeight;
|
||||
display->fillRect(batteryX + 1, fillY + 10, 5, fillHeight);
|
||||
}
|
||||
}
|
||||
|
||||
// === Text baseline ===
|
||||
const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2;
|
||||
|
||||
// === Battery % Text ===
|
||||
char chargeStr[4];
|
||||
snprintf(chargeStr, sizeof(chargeStr), "%d", chargePercent);
|
||||
|
||||
int chargeNumWidth = display->getStringWidth(chargeStr);
|
||||
const int batteryOffset = useHorizontalBattery ? 28 : 6;
|
||||
#ifdef USE_EINK
|
||||
const int percentX = x + xOffset + batteryOffset - 2;
|
||||
#else
|
||||
const int percentX = x + xOffset + batteryOffset;
|
||||
#endif
|
||||
|
||||
display->drawString(percentX, textY, chargeStr);
|
||||
display->drawString(percentX + chargeNumWidth - 1, textY, "%");
|
||||
|
||||
if (isBold) {
|
||||
display->drawString(percentX + 1, textY, chargeStr);
|
||||
display->drawString(percentX + chargeNumWidth, textY, "%");
|
||||
}
|
||||
// === Time string (right-aligned) ===
|
||||
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true);
|
||||
if (rtc_sec > 0) {
|
||||
long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY;
|
||||
int hour = hms / SEC_PER_HOUR;
|
||||
int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||||
|
||||
char timeStr[10];
|
||||
|
||||
snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute);
|
||||
if (config.display.use_12h_clock) {
|
||||
bool isPM = hour >= 12;
|
||||
hour = hour % 12;
|
||||
if (hour == 0)
|
||||
hour = 12;
|
||||
snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "p" : "a");
|
||||
}
|
||||
|
||||
int timeStrWidth = display->getStringWidth(timeStr);
|
||||
int timeX = SCREEN_WIDTH - xOffset - timeStrWidth + 4; // time to the right by 4
|
||||
|
||||
// Mail icon next to time (drawn as 'M' in a tight square)
|
||||
if (hasUnreadMessage) {
|
||||
if (now - lastMailBlink > mailBlinkInterval) {
|
||||
isMailIconVisible = !isMailIconVisible;
|
||||
lastMailBlink = now;
|
||||
}
|
||||
|
||||
if (isMailIconVisible) {
|
||||
const bool isWide = useHorizontalBattery;
|
||||
|
||||
if (isWide) {
|
||||
// Dimensions for the wide mail icon
|
||||
const int iconW = 16;
|
||||
const int iconH = 12;
|
||||
|
||||
const int iconX = timeX - iconW - 3;
|
||||
const int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1;
|
||||
|
||||
// Draw envelope rectangle
|
||||
display->drawRect(iconX, iconY, iconW, iconH);
|
||||
|
||||
// Define envelope corners and center
|
||||
const int leftX = iconX + 1;
|
||||
const int rightX = iconX + iconW - 2;
|
||||
const int topY = iconY + 1;
|
||||
const int bottomY = iconY + iconH - 2;
|
||||
const int centerX = iconX + iconW / 2;
|
||||
const int peakY = bottomY - 1;
|
||||
|
||||
// Draw "M" diagonals
|
||||
display->drawLine(leftX, topY, centerX, peakY);
|
||||
display->drawLine(rightX, topY, centerX, peakY);
|
||||
} else {
|
||||
// Small icon for non-wide screens
|
||||
const int iconX = timeX - mail_width;
|
||||
const int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2;
|
||||
display->drawXbm(iconX, iconY, mail_width, mail_height, mail);
|
||||
}
|
||||
}
|
||||
}
|
||||
display->drawString(timeX, textY, timeStr);
|
||||
if (isBold)
|
||||
display->drawString(timeX - 1, textY, timeStr);
|
||||
}
|
||||
|
||||
display->setColor(WHITE);
|
||||
}
|
||||
|
||||
// ****************************
|
||||
// * Text Message Screen *
|
||||
@ -1767,7 +1569,7 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_
|
||||
display->setFont(FONT_SMALL);
|
||||
|
||||
// === Header ===
|
||||
drawCommonHeader(display, x, y);
|
||||
graphics::drawCommonHeader(display, x, y);
|
||||
|
||||
// === Reset color in case inverted mode left it BLACK ===
|
||||
display->setColor(WHITE);
|
||||
@ -2081,7 +1883,7 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
||||
display->clear();
|
||||
|
||||
// === Draw the battery/time header ===
|
||||
drawCommonHeader(display, x, y);
|
||||
graphics::drawCommonHeader(display, x, y);
|
||||
|
||||
// === Manually draw the centered title within the header ===
|
||||
const int highlightHeight = COMMON_HEADER_HEIGHT;
|
||||
@ -2502,7 +2304,7 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i
|
||||
display->setFont(FONT_SMALL);
|
||||
|
||||
// === Header ===
|
||||
drawCommonHeader(display, x, y);
|
||||
graphics::drawCommonHeader(display, x, y);
|
||||
|
||||
// === Content below header ===
|
||||
|
||||
@ -2599,7 +2401,7 @@ static void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int
|
||||
display->setFont(FONT_SMALL);
|
||||
|
||||
// === Header ===
|
||||
drawCommonHeader(display, x, y);
|
||||
graphics::drawCommonHeader(display, x, y);
|
||||
|
||||
// === Draw title (aligned with header baseline) ===
|
||||
const int highlightHeight = FONT_HEIGHT_SMALL - 1;
|
||||
@ -2720,7 +2522,7 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat
|
||||
display->setFont(FONT_SMALL);
|
||||
|
||||
// === Header ===
|
||||
drawCommonHeader(display, x, y);
|
||||
graphics::drawCommonHeader(display, x, y);
|
||||
|
||||
// === Draw title ===
|
||||
const int highlightHeight = FONT_HEIGHT_SMALL - 1;
|
||||
@ -2850,7 +2652,7 @@ static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, in
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
|
||||
// === Header ===
|
||||
drawCommonHeader(display, x, y);
|
||||
graphics::drawCommonHeader(display, x, y);
|
||||
|
||||
// === Draw title ===
|
||||
const int highlightHeight = FONT_HEIGHT_SMALL - 1;
|
||||
@ -3152,8 +2954,7 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
|
||||
}
|
||||
static int8_t lastFrameIndex = -1;
|
||||
static uint32_t lastFrameChangeTime = 0;
|
||||
// constexpr uint32_t ICON_DISPLAY_DURATION_MS = 1250;
|
||||
constexpr uint32_t ICON_DISPLAY_DURATION_MS = 10250;
|
||||
constexpr uint32_t ICON_DISPLAY_DURATION_MS = 1250;
|
||||
|
||||
// Bottom navigation icons
|
||||
void drawCustomFrameIcons(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||
|
164
src/graphics/SharedUIDisplay.cpp
Normal file
164
src/graphics/SharedUIDisplay.cpp
Normal file
@ -0,0 +1,164 @@
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "main.h"
|
||||
#include "power.h"
|
||||
#include "meshtastic/config.pb.h"
|
||||
#include "RTC.h"
|
||||
#include <OLEDDisplay.h>
|
||||
#include <graphics/images.h>
|
||||
|
||||
namespace graphics {
|
||||
|
||||
// === Shared External State ===
|
||||
bool hasUnreadMessage = false;
|
||||
|
||||
// === Internal State ===
|
||||
bool isBoltVisibleShared = true;
|
||||
uint32_t lastBlinkShared = 0;
|
||||
bool isMailIconVisible = true;
|
||||
uint32_t lastMailBlink = 0;
|
||||
|
||||
// *********************************
|
||||
// * Rounded Header when inverted *
|
||||
// *********************************
|
||||
void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r)
|
||||
{
|
||||
display->fillRect(x + r, y, w - 2 * r, h);
|
||||
display->fillRect(x, y + r, r, h - 2 * r);
|
||||
display->fillRect(x + w - r, y + r, r, h - 2 * r);
|
||||
display->fillCircle(x + r + 1, y + r, r);
|
||||
display->fillCircle(x + w - r - 1, y + r, r);
|
||||
display->fillCircle(x + r + 1, y + h - r - 1, r);
|
||||
display->fillCircle(x + w - r - 1, y + h - r - 1, r);
|
||||
}
|
||||
|
||||
// *************************
|
||||
// * Common Header Drawing *
|
||||
// *************************
|
||||
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y)
|
||||
{
|
||||
constexpr int HEADER_OFFSET_Y = 1;
|
||||
y += HEADER_OFFSET_Y;
|
||||
|
||||
display->setFont(FONT_SMALL);
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
|
||||
const int xOffset = 4;
|
||||
const int highlightHeight = FONT_HEIGHT_SMALL - 1;
|
||||
const bool isInverted = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED);
|
||||
const bool isBold = config.display.heading_bold;
|
||||
|
||||
const int screenW = display->getWidth();
|
||||
const int screenH = display->getHeight();
|
||||
|
||||
if (isInverted) {
|
||||
drawRoundedHighlight(display, x, y, screenW, highlightHeight, 2);
|
||||
display->setColor(BLACK);
|
||||
}
|
||||
|
||||
int chargePercent = powerStatus->getBatteryChargePercent();
|
||||
bool isCharging = powerStatus->getIsCharging() == meshtastic::OptionalBool::OptTrue;
|
||||
uint32_t now = millis();
|
||||
if (isCharging && now - lastBlinkShared > 500) {
|
||||
isBoltVisibleShared = !isBoltVisibleShared;
|
||||
lastBlinkShared = now;
|
||||
}
|
||||
|
||||
bool useHorizontalBattery = (screenW > 128 && screenW > screenH);
|
||||
const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2;
|
||||
|
||||
// === Battery Icons ===
|
||||
if (useHorizontalBattery) {
|
||||
int batteryX = 2;
|
||||
int batteryY = HEADER_OFFSET_Y + 2;
|
||||
display->drawXbm(batteryX, batteryY, 29, 15, batteryBitmap_h);
|
||||
if (isCharging && isBoltVisibleShared) {
|
||||
display->drawXbm(batteryX + 9, batteryY + 1, 9, 13, lightning_bolt_h);
|
||||
} else {
|
||||
display->drawXbm(batteryX + 8, batteryY, 12, 15, batteryBitmap_sidegaps_h);
|
||||
int fillWidth = 24 * chargePercent / 100;
|
||||
display->fillRect(batteryX + 1, batteryY + 1, fillWidth, 13);
|
||||
}
|
||||
} else {
|
||||
int batteryX = 1;
|
||||
int batteryY = HEADER_OFFSET_Y + 1;
|
||||
#ifdef USE_EINK
|
||||
batteryY += 2;
|
||||
#endif
|
||||
display->drawXbm(batteryX, batteryY, 7, 11, batteryBitmap_v);
|
||||
if (isCharging && isBoltVisibleShared) {
|
||||
display->drawXbm(batteryX + 1, batteryY + 3, 5, 5, lightning_bolt_v);
|
||||
} else {
|
||||
display->drawXbm(batteryX - 1, batteryY + 4, 8, 3, batteryBitmap_sidegaps_v);
|
||||
int fillHeight = 8 * chargePercent / 100;
|
||||
int fillY = batteryY - fillHeight;
|
||||
display->fillRect(batteryX + 1, fillY + 10, 5, fillHeight);
|
||||
}
|
||||
}
|
||||
|
||||
// === Battery % Text ===
|
||||
char chargeStr[4];
|
||||
snprintf(chargeStr, sizeof(chargeStr), "%d", chargePercent);
|
||||
int chargeNumWidth = display->getStringWidth(chargeStr);
|
||||
const int batteryOffset = useHorizontalBattery ? 28 : 6;
|
||||
#ifdef USE_EINK
|
||||
const int percentX = x + xOffset + batteryOffset - 2;
|
||||
#else
|
||||
const int percentX = x + xOffset + batteryOffset;
|
||||
#endif
|
||||
display->drawString(percentX, textY, chargeStr);
|
||||
display->drawString(percentX + chargeNumWidth - 1, textY, "%");
|
||||
if (isBold) {
|
||||
display->drawString(percentX + 1, textY, chargeStr);
|
||||
display->drawString(percentX + chargeNumWidth, textY, "%");
|
||||
}
|
||||
|
||||
// === Time and Mail Icon ===
|
||||
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true);
|
||||
if (rtc_sec > 0) {
|
||||
long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY;
|
||||
int hour = hms / SEC_PER_HOUR;
|
||||
int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||||
|
||||
char timeStr[10];
|
||||
snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute);
|
||||
if (config.display.use_12h_clock) {
|
||||
bool isPM = hour >= 12;
|
||||
hour %= 12;
|
||||
if (hour == 0) hour = 12;
|
||||
snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "p" : "a");
|
||||
}
|
||||
|
||||
int timeStrWidth = display->getStringWidth(timeStr);
|
||||
int timeX = screenW - xOffset - timeStrWidth + 4;
|
||||
|
||||
if (hasUnreadMessage) {
|
||||
if (now - lastMailBlink > 500) {
|
||||
isMailIconVisible = !isMailIconVisible;
|
||||
lastMailBlink = now;
|
||||
}
|
||||
|
||||
if (isMailIconVisible) {
|
||||
if (useHorizontalBattery) {
|
||||
int iconW = 16, iconH = 12;
|
||||
int iconX = timeX - iconW - 3;
|
||||
int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1;
|
||||
display->drawRect(iconX, iconY, iconW, iconH);
|
||||
display->drawLine(iconX + 1, iconY + 1, iconX + iconW / 2, iconY + iconH - 2);
|
||||
display->drawLine(iconX + iconW - 2, iconY + 1, iconX + iconW / 2, iconY + iconH - 2);
|
||||
} else {
|
||||
int iconX = timeX - mail_width;
|
||||
int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2;
|
||||
display->drawXbm(iconX, iconY, mail_width, mail_height, mail);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
display->drawString(timeX, textY, timeStr);
|
||||
if (isBold) display->drawString(timeX - 1, textY, timeStr);
|
||||
}
|
||||
|
||||
display->setColor(WHITE);
|
||||
}
|
||||
|
||||
} // namespace graphics
|
37
src/graphics/SharedUIDisplay.h
Normal file
37
src/graphics/SharedUIDisplay.h
Normal file
@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <OLEDDisplay.h>
|
||||
|
||||
namespace graphics {
|
||||
|
||||
// =======================
|
||||
// Shared UI Helpers
|
||||
// =======================
|
||||
|
||||
// Compact line layout
|
||||
#define compactFirstLine ((FONT_HEIGHT_SMALL - 1) * 1)
|
||||
#define compactSecondLine ((FONT_HEIGHT_SMALL - 1) * 2) - 2
|
||||
#define compactThirdLine ((FONT_HEIGHT_SMALL - 1) * 3) - 4
|
||||
#define compactFourthLine ((FONT_HEIGHT_SMALL - 1) * 4) - 6
|
||||
#define compactFifthLine ((FONT_HEIGHT_SMALL - 1) * 5) - 8
|
||||
|
||||
// Standard line layout
|
||||
#define standardFirstLine (FONT_HEIGHT_SMALL + 1) * 1
|
||||
#define standardSecondLine (FONT_HEIGHT_SMALL + 1) * 2
|
||||
#define standardThirdLine (FONT_HEIGHT_SMALL + 1) * 3
|
||||
#define standardFourthLine (FONT_HEIGHT_SMALL + 1) * 4
|
||||
|
||||
// Quick screen access
|
||||
#define SCREEN_WIDTH display->getWidth()
|
||||
#define SCREEN_HEIGHT display->getHeight()
|
||||
|
||||
// Shared state (declare inside namespace)
|
||||
extern bool hasUnreadMessage;
|
||||
|
||||
// Rounded highlight (used for inverted headers)
|
||||
void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r);
|
||||
|
||||
// Shared battery/time/mail header
|
||||
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y);
|
||||
|
||||
} // namespace graphics
|
@ -17,6 +17,8 @@
|
||||
#include "target_specific.h"
|
||||
#include <OLEDDisplay.h>
|
||||
#include <OLEDDisplayUi.h>
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/images.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL
|
||||
// Sensors
|
||||
@ -25,6 +27,9 @@
|
||||
#include "Sensor/RCWL9620Sensor.h"
|
||||
#include "Sensor/nullSensor.h"
|
||||
|
||||
namespace graphics {
|
||||
extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y);
|
||||
}
|
||||
#if __has_include(<Adafruit_AHTX0.h>)
|
||||
#include "Sensor/AHT10.h"
|
||||
AHT10Sensor aht10Sensor;
|
||||
@ -312,119 +317,130 @@ bool EnvironmentTelemetryModule::wantUIFrame()
|
||||
|
||||
void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
// === Setup display ===
|
||||
display->clear();
|
||||
display->setFont(FONT_SMALL);
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
|
||||
if (lastMeasurementPacket == nullptr) {
|
||||
// If there's no valid packet, display "Environment"
|
||||
display->drawString(x, y, "Environment");
|
||||
display->drawString(x, y += _fontHeight(FONT_SMALL), "No measurement");
|
||||
// Draw shared header bar (battery, time, etc.)
|
||||
graphics::drawCommonHeader(display, x, y);
|
||||
|
||||
// === Draw Title (Centered under header) ===
|
||||
const int highlightHeight = FONT_HEIGHT_SMALL - 1;
|
||||
const int titleY = y + 1 + (highlightHeight - FONT_HEIGHT_SMALL) / 2;
|
||||
const char *titleStr = "Environment";
|
||||
const int centerX = x + SCREEN_WIDTH / 2;
|
||||
|
||||
// Use black text on white background if in inverted mode
|
||||
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED)
|
||||
display->setColor(BLACK);
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
display->drawString(centerX, titleY, titleStr); // Centered title
|
||||
if (config.display.heading_bold)
|
||||
display->drawString(centerX + 1, titleY, titleStr); // Bold effect via 1px offset
|
||||
|
||||
// Restore text color & alignment
|
||||
display->setColor(WHITE);
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
|
||||
// === Row spacing setup ===
|
||||
const int rowHeight = FONT_HEIGHT_SMALL - 4;
|
||||
int currentY = compactFirstLine;
|
||||
|
||||
// === Show "No Telemetry" if no data available ===
|
||||
if (!lastMeasurementPacket) {
|
||||
display->drawString(x, currentY, "No Telemetry");
|
||||
return;
|
||||
}
|
||||
|
||||
// Decode the last measurement packet
|
||||
meshtastic_Telemetry lastMeasurement;
|
||||
uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket);
|
||||
const char *lastSender = getSenderShortName(*lastMeasurementPacket);
|
||||
|
||||
// Decode the telemetry message from the latest received packet
|
||||
const meshtastic_Data &p = lastMeasurementPacket->decoded;
|
||||
if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &lastMeasurement)) {
|
||||
display->drawString(x, y, "Measurement Error");
|
||||
LOG_ERROR("Unable to decode last packet");
|
||||
meshtastic_Telemetry telemetry;
|
||||
if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &telemetry)) {
|
||||
display->drawString(x, currentY, "No Telemetry");
|
||||
return;
|
||||
}
|
||||
|
||||
// Display "Env. From: ..." on its own
|
||||
display->drawString(x, y, "Env. From: " + String(lastSender) + " (" + String(agoSecs) + "s)");
|
||||
const auto &m = telemetry.variant.environment_metrics;
|
||||
|
||||
// Prepare sensor data strings
|
||||
String sensorData[10];
|
||||
int sensorCount = 0;
|
||||
// Check if any telemetry field has valid data
|
||||
bool hasAny = m.has_temperature || m.has_relative_humidity || m.barometric_pressure != 0 ||
|
||||
m.iaq != 0 || m.voltage != 0 || m.current != 0 || m.lux != 0 ||
|
||||
m.white_lux != 0 || m.weight != 0 || m.distance != 0 || m.radiation != 0;
|
||||
|
||||
if (lastMeasurement.variant.environment_metrics.has_temperature ||
|
||||
lastMeasurement.variant.environment_metrics.has_relative_humidity) {
|
||||
String last_temp = String(lastMeasurement.variant.environment_metrics.temperature, 0) + "°C";
|
||||
if (moduleConfig.telemetry.environment_display_fahrenheit) {
|
||||
last_temp =
|
||||
String(UnitConversions::CelsiusToFahrenheit(lastMeasurement.variant.environment_metrics.temperature), 0) + "°F";
|
||||
if (!hasAny) {
|
||||
display->drawString(x, currentY, "No Telemetry");
|
||||
return;
|
||||
}
|
||||
|
||||
// === First line: Show sender name + time since received (left), and first metric (right) ===
|
||||
const char *sender = getSenderShortName(*lastMeasurementPacket);
|
||||
uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket);
|
||||
String agoStr = (agoSecs > 864000) ? "?" :
|
||||
(agoSecs > 3600) ? String(agoSecs / 3600) + "h" :
|
||||
(agoSecs > 60) ? String(agoSecs / 60) + "m" :
|
||||
String(agoSecs) + "s";
|
||||
|
||||
String leftStr = String(sender) + " (" + agoStr + ")";
|
||||
display->drawString(x, currentY, leftStr); // Left side: who and when
|
||||
|
||||
// === Collect sensor readings as label strings (no icons) ===
|
||||
std::vector<String> entries;
|
||||
|
||||
if (m.has_temperature) {
|
||||
String tempStr = moduleConfig.telemetry.environment_display_fahrenheit
|
||||
? "Tmp: " + String(UnitConversions::CelsiusToFahrenheit(m.temperature), 1) + "°F"
|
||||
: "Tmp: " + String(m.temperature, 1) + "°C";
|
||||
entries.push_back(tempStr);
|
||||
}
|
||||
if (m.has_relative_humidity)
|
||||
entries.push_back("Hum: " + String(m.relative_humidity, 0) + "%");
|
||||
if (m.barometric_pressure != 0)
|
||||
entries.push_back("Prss: " + String(m.barometric_pressure, 0) + " hPa");
|
||||
if (m.iaq != 0) {
|
||||
String aqi = "IAQ: " + String(m.iaq);
|
||||
if (m.iaq < 50) aqi += " (Good)";
|
||||
else if (m.iaq < 100) aqi += " (Moderate)";
|
||||
else aqi += " (!)";
|
||||
entries.push_back(aqi);
|
||||
}
|
||||
if (m.voltage != 0 || m.current != 0)
|
||||
entries.push_back(String(m.voltage, 1) + "V / " + String(m.current, 0) + "mA");
|
||||
if (m.lux != 0)
|
||||
entries.push_back("Light: " + String(m.lux, 0) + "lx");
|
||||
if (m.white_lux != 0)
|
||||
entries.push_back("White: " + String(m.white_lux, 0) + "lx");
|
||||
if (m.weight != 0)
|
||||
entries.push_back("Weight: " + String(m.weight, 0) + "kg");
|
||||
if (m.distance != 0)
|
||||
entries.push_back("Level: " + String(m.distance, 0) + "mm");
|
||||
if (m.radiation != 0)
|
||||
entries.push_back("Rad: " + String(m.radiation, 2) + " µR/h");
|
||||
|
||||
// === Show first available metric on top-right of first line ===
|
||||
if (!entries.empty()) {
|
||||
String valueStr = entries.front();
|
||||
int rightX = SCREEN_WIDTH - display->getStringWidth(valueStr);
|
||||
display->drawString(rightX, currentY, valueStr);
|
||||
entries.erase(entries.begin()); // Remove from queue
|
||||
}
|
||||
|
||||
// === Advance to next line for remaining telemetry entries ===
|
||||
currentY += rowHeight;
|
||||
|
||||
// === Draw remaining entries in 2-column format (left and right) ===
|
||||
for (size_t i = 0; i < entries.size(); i += 2) {
|
||||
// Left column
|
||||
display->drawString(x, currentY, entries[i]);
|
||||
|
||||
// Right column if it exists
|
||||
if (i + 1 < entries.size()) {
|
||||
int rightX = SCREEN_WIDTH / 2;
|
||||
display->drawString(rightX, currentY, entries[i + 1]);
|
||||
}
|
||||
|
||||
sensorData[sensorCount++] =
|
||||
"Temp/Hum: " + last_temp + " / " + String(lastMeasurement.variant.environment_metrics.relative_humidity, 0) + "%";
|
||||
}
|
||||
|
||||
if (lastMeasurement.variant.environment_metrics.barometric_pressure != 0) {
|
||||
sensorData[sensorCount++] =
|
||||
"Press: " + String(lastMeasurement.variant.environment_metrics.barometric_pressure, 0) + "hPA";
|
||||
}
|
||||
|
||||
if (lastMeasurement.variant.environment_metrics.voltage != 0) {
|
||||
sensorData[sensorCount++] = "Volt/Cur: " + String(lastMeasurement.variant.environment_metrics.voltage, 0) + "V / " +
|
||||
String(lastMeasurement.variant.environment_metrics.current, 0) + "mA";
|
||||
}
|
||||
|
||||
if (lastMeasurement.variant.environment_metrics.iaq != 0) {
|
||||
sensorData[sensorCount++] = "IAQ: " + String(lastMeasurement.variant.environment_metrics.iaq);
|
||||
}
|
||||
|
||||
if (lastMeasurement.variant.environment_metrics.distance != 0) {
|
||||
sensorData[sensorCount++] = "Water Level: " + String(lastMeasurement.variant.environment_metrics.distance, 0) + "mm";
|
||||
}
|
||||
|
||||
if (lastMeasurement.variant.environment_metrics.weight != 0) {
|
||||
sensorData[sensorCount++] = "Weight: " + String(lastMeasurement.variant.environment_metrics.weight, 0) + "kg";
|
||||
}
|
||||
|
||||
if (lastMeasurement.variant.environment_metrics.radiation != 0) {
|
||||
sensorData[sensorCount++] = "Rad: " + String(lastMeasurement.variant.environment_metrics.radiation, 2) + "µR/h";
|
||||
}
|
||||
|
||||
if (lastMeasurement.variant.environment_metrics.lux != 0) {
|
||||
sensorData[sensorCount++] = "Illuminance: " + String(lastMeasurement.variant.environment_metrics.lux, 2) + "lx";
|
||||
}
|
||||
|
||||
if (lastMeasurement.variant.environment_metrics.white_lux != 0) {
|
||||
sensorData[sensorCount++] = "W_Lux: " + String(lastMeasurement.variant.environment_metrics.white_lux, 2) + "lx";
|
||||
}
|
||||
|
||||
static int scrollOffset = 0;
|
||||
static bool scrollingDown = true;
|
||||
static uint32_t lastScrollTime = millis();
|
||||
|
||||
// Determine how many lines we can fit on display
|
||||
// Calculated once only: display dimensions don't change during runtime.
|
||||
static int maxLines = 0;
|
||||
if (!maxLines) {
|
||||
const int16_t paddingTop = _fontHeight(FONT_SMALL); // Heading text
|
||||
const int16_t paddingBottom = 8; // Indicator dots
|
||||
maxLines = (display->getHeight() - paddingTop - paddingBottom) / _fontHeight(FONT_SMALL);
|
||||
assert(maxLines > 0);
|
||||
}
|
||||
|
||||
// Draw as many lines of data as we can fit
|
||||
int linesToShow = min(maxLines, sensorCount);
|
||||
for (int i = 0; i < linesToShow; i++) {
|
||||
int index = (scrollOffset + i) % sensorCount;
|
||||
display->drawString(x, y += _fontHeight(FONT_SMALL), sensorData[index]);
|
||||
}
|
||||
|
||||
// Only scroll if there are more than 3 sensor data lines
|
||||
if (sensorCount > 3) {
|
||||
// Update scroll offset every 5 seconds
|
||||
if (millis() - lastScrollTime > 5000) {
|
||||
if (scrollingDown) {
|
||||
scrollOffset++;
|
||||
if (scrollOffset + linesToShow >= sensorCount) {
|
||||
scrollingDown = false;
|
||||
}
|
||||
} else {
|
||||
scrollOffset--;
|
||||
if (scrollOffset <= 0) {
|
||||
scrollingDown = true;
|
||||
}
|
||||
}
|
||||
lastScrollTime = millis();
|
||||
}
|
||||
currentY += rowHeight;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,7 @@
|
||||
#include "power.h"
|
||||
#include "sleep.h"
|
||||
#include "target_specific.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
|
||||
#define FAILED_STATE_SENSOR_READ_MULTIPLIER 10
|
||||
#define DISPLAY_RECEIVEID_MEASUREMENTS_ON_SCREEN true
|
||||
@ -21,6 +22,10 @@
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include <Throttle.h>
|
||||
|
||||
namespace graphics {
|
||||
extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y);
|
||||
}
|
||||
|
||||
int32_t PowerTelemetryModule::runOnce()
|
||||
{
|
||||
if (sleepOnNextExecution == true) {
|
||||
@ -103,13 +108,33 @@ bool PowerTelemetryModule::wantUIFrame()
|
||||
|
||||
void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
display->clear();
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
|
||||
graphics::drawCommonHeader(display, x, y); // Shared UI header
|
||||
|
||||
// === Draw title (aligned with header baseline) ===
|
||||
const int highlightHeight = FONT_HEIGHT_SMALL - 1;
|
||||
const int textY = y + 1 + (highlightHeight - FONT_HEIGHT_SMALL) / 2;
|
||||
const char *titleStr = (SCREEN_WIDTH > 128) ? "Power Telem." : "Power";
|
||||
const int centerX = x + SCREEN_WIDTH / 2;
|
||||
|
||||
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) {
|
||||
display->setColor(BLACK);
|
||||
}
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
display->drawString(centerX, textY, titleStr);
|
||||
if (config.display.heading_bold) {
|
||||
display->drawString(centerX + 1, textY, titleStr);
|
||||
}
|
||||
display->setColor(WHITE);
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
|
||||
if (lastMeasurementPacket == nullptr) {
|
||||
// In case of no valid packet, display "Power Telemetry", "No measurement"
|
||||
display->drawString(x, y, "Power Telemetry");
|
||||
display->drawString(x, y += _fontHeight(FONT_SMALL), "No measurement");
|
||||
// In case of no valid packet, display "Power Telemetry", "No measurement"
|
||||
display->drawString(x, compactFirstLine, "No measurement");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -120,29 +145,31 @@ void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *s
|
||||
|
||||
const meshtastic_Data &p = lastMeasurementPacket->decoded;
|
||||
if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &lastMeasurement)) {
|
||||
display->drawString(x, y, "Measurement Error");
|
||||
display->drawString(x, compactFirstLine, "Measurement Error");
|
||||
LOG_ERROR("Unable to decode last packet");
|
||||
return;
|
||||
}
|
||||
|
||||
// Display "Pow. From: ..."
|
||||
display->drawString(x, y, "Pow. From: " + String(lastSender) + "(" + String(agoSecs) + "s)");
|
||||
display->drawString(x, compactFirstLine, "Pow. From: " + String(lastSender) + " (" + String(agoSecs) + "s)");
|
||||
|
||||
// Display current and voltage based on ...power_metrics.has_[channel/voltage/current]... flags
|
||||
if (lastMeasurement.variant.power_metrics.has_ch1_voltage || lastMeasurement.variant.power_metrics.has_ch1_current) {
|
||||
display->drawString(x, y += _fontHeight(FONT_SMALL),
|
||||
"Ch1: " + String(lastMeasurement.variant.power_metrics.ch1_voltage, 2) + "V " +
|
||||
String(lastMeasurement.variant.power_metrics.ch1_current, 0) + "mA");
|
||||
const auto &m = lastMeasurement.variant.power_metrics;
|
||||
int lineY = compactSecondLine;
|
||||
|
||||
auto drawLine = [&](const char *label, float voltage, float current) {
|
||||
display->drawString(x, lineY, String(label) + ": " + String(voltage, 2) + "V " + String(current, 0) + "mA");
|
||||
lineY += _fontHeight(FONT_SMALL);
|
||||
};
|
||||
|
||||
if (m.has_ch1_voltage || m.has_ch1_current) {
|
||||
drawLine("Ch1", m.ch1_voltage, m.ch1_current);
|
||||
}
|
||||
if (lastMeasurement.variant.power_metrics.has_ch2_voltage || lastMeasurement.variant.power_metrics.has_ch2_current) {
|
||||
display->drawString(x, y += _fontHeight(FONT_SMALL),
|
||||
"Ch2: " + String(lastMeasurement.variant.power_metrics.ch2_voltage, 2) + "V " +
|
||||
String(lastMeasurement.variant.power_metrics.ch2_current, 0) + "mA");
|
||||
if (m.has_ch2_voltage || m.has_ch2_current) {
|
||||
drawLine("Ch2", m.ch2_voltage, m.ch2_current);
|
||||
}
|
||||
if (lastMeasurement.variant.power_metrics.has_ch3_voltage || lastMeasurement.variant.power_metrics.has_ch3_current) {
|
||||
display->drawString(x, y += _fontHeight(FONT_SMALL),
|
||||
"Ch3: " + String(lastMeasurement.variant.power_metrics.ch3_voltage, 2) + "V " +
|
||||
String(lastMeasurement.variant.power_metrics.ch3_current, 0) + "mA");
|
||||
if (m.has_ch3_voltage || m.has_ch3_current) {
|
||||
drawLine("Ch3", m.ch3_voltage, m.ch3_current);
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user