Unify the native display config between legacy display and MUI (#6838)

* Add missed include

* Another Warning fix

* Add another HAS_SCREEN

* Namespace fixes

* Removed depricated destination types and re-factored destination screen

* Get rid of Arduino Strings

* Clean up after Copilot

* SixthLine Def, Screen Rename

Added Sixth Line Definition Screen Rename, and Automatic Line Adjustment

* Consistency is hard - fixed "Sixth"

* System Frame Updates

Adjusted line construction to ensure we fit maximum content per screen.

* Fix up notifications

* Add a couple more ifdef HAS_SCREEN lines

* Add screen->isOverlayBannerShowing()

* Don't forget the invert!

* Adjust Nodelist Center Divider

Adjust Nodelist Center Divider

* Fix variable casting

* Fix entryText variable as empty before update to fix validation

* Altitude is int32_t

* Update PowerTelemetry to have correct data type

* Fix cppcheck warnings (#6945)

* Fix cppcheck warnings

* Adjust logic in Power.cpp for power sensor

---------

Co-authored-by: Jason P <applewiz@mac.com>

* More pixel wrangling so things line up NodeList edition

* Adjust NodeList alignments and plumb some background padding for a possible title fix

* Better alignment for banner notifications

* Move title into drawCommonHeader; initial screen tested

* Fonts make spacing items difficult

* Improved beeping booping and other buzzer based feedback (#6947)

* Improved beeping booping and other buzzer based feedback

* audible button feedback (#6949)

* Refactor

---------

Co-authored-by: todd-herbert <herbert.todd@gmail.com>

* Sandpapered the corners of the notification popup

* Finalize drawCommonHeader migration

* Update Title of Favorite Node Screens

* Update node metric alignment on LoRa screen

* Update the border for popups to separate it from background

* Update PaxcounterModule.cpp with CommonHeader

* Update WiFi screen with CommonHeader and related data reflow

* It was not, in fact, pointing up

* Fix build on wismeshtap

* T-deck trackball debounce

* Fix uptime on Device Focused page to actually detail

* Update Sys screen for new uptime, add label to Freq/Chan on LoRa

* Don't display DOP any longer, make Uptime consistent

* Revert Uptime change on Favorites, Apply to Device Focused

* Label the satelite number to avoid confusion

* Boop boop boop boop

* Correct GPS positioning and string consistency across strings for GPS

* Fix GPS text alignment

* Enable canned messages by default

* Don't wake screen on new nodes

* Cannedmessage list emote support added

* Fn+e emote picker for freetext screen

* Actually block CannedInput actions while display is shown

* Add selection menu to bannerOverlay

* Off by one

* Move to unified text layouts and spacing

* Still my Fav without an "e"

* Fully remove EVENT_NODEDB_UPDATED

* Simply LoRa screen

* Make some char pointers const to fix compilation on native targets

* Update drawCompassNorth to include radius

* Fix warning

* button thread cleanup

* Pull OneButton handling from PowerFSM and add MUI switch (#6973)

* Trunk

* Onebutton Menu Support

* Add temporary clock icon

* Add gps location to fsi

* Banner message state reset

* Cast to char to satisfy compiler

* Better fast handling of input during banner

* Fix warning

* Derp

* oops

* Update ref

* Wire buzzer_mode

* remove legacy string->print()

* Only init screen if one found

* Unsigned Char

* More buttonThread cleaning

* screen.cpp button handling cleanup

* The Great Event Rename of 2025

* Fix the Radiomaster

* Missed trackball type change

* Remove unused function

* Make ButtonThread an InputBroker

* Coffee hadn't kicked in yet

* Add clock icon for Navigation Bar

* Restore clock screen definition code - whoops

* ExternalNotifications now observe inputBroker

* Clock rework (#6992)

* Move Clock bits into ClockRenderer space

* Rework clock into all device navigation

* T-Watch Actually Builds Different

* Compile fix

---------

Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>

* Add AM/PM to Digital Clock

* Flip Seconds and AM/PM on Clock Display

* Tik-tok pixels are hard

* Fix builds on Thinknode M1

* Check for GPS and don't crash

* Don't endif til the end

* Rework the OneButton thread to be much less of a mess. (#6997)

* Rework the OneButton thread to be much less of a mess. And break lots of targets temporarily

* Update src/input/ButtonThread.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix GPS toggle

* Send the shutdown event, not just the kbchar

* Honor the back button in a notificaiton popup

* Draw the right size box for popup with options

* Try to un-break all the things

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* 24-hour Clock Should have leading zero, but not 12-hour

* Fixup some compile errors

* Add intRoutine to ButtonThread init, to get more responsive user button back

* Add Timezone picker

* Fix Warning

* Optionally set the initial selection for the chooser popup

* Make back buttons work in canned messages

* Drop the wrapper classes

* LonPressTime now configurable

* Clock Frame can not longer be blank; just add valid time

* Back buttons everywhere!

* Key Verification confirm banner

* Make Elecrow M* top button a back button

* Add settings saves

* EInk responsiveness fixes

* Linux Input Fixes

* Add Native Trackball/Joystick support, and move UserButton to Input

* No Flight Stick Mode

* Send input event

* Add Channel Utilization to Device Focused frame

* Don't shift screens when we draw new ones

* Add showOverlayBanner arguments to no-op

* trunk

* Default Native trackball to NC

* Fix crash in simulator mode

* Add longLong button press

* Get the args right

* Adjust Bluetooth Pairing Screen to account for bottom navigation.

* Trackball everywhere, and unPhone buttons

* Remap visionmaster secondary button to TB_UP

* Kill ScanAndSelect

* trunk

* No longer need the canned messages input filter

* All Canned All the time

* Fix stm32 compile error regarding inputBroker

* Unify tft lineheights (#7033)

* Create variable line heights based upon SCREEN_HEIGHT

* Refactor textPositions into method -> getTextPositions

* Update SharedUIDisplay.h

---------

Co-authored-by: Jason P <applewiz@mac.com>

* Adjust top distance for larger displays

* Adjust icon sizes for larger displays

* Fix Paxcounter compile errors after code updates

* Pixel wrangling to make larger screens fit better

* Alert frame has precedence over banner -- for now

* Unify on ALT_BUTTON

* Align AM/PM to the digit, not the segment on larger displays

* Move some global pin defines into configuration.h

* Scaffolding for BMM150 9-axis gyro

* Alt button behavior

* Don't add the blank GPS frames without HAS_GPS

* EVENT_NODEDB_UPDATED has been retired

* Clean out LOG_WARN messages from debugging

* Add dismiss message function

* Minor buttonThread cleanup

* Add BMM150 support

* Clean up last warning from dev

* Simplify bmm150 init return logic

* Add option to reply to messages

* Add minimal menu upon selecting home screen

* Move Messages to slot 2, rename GPS to Position, move variables nearer functional usage in Screen.cpp

* Properly dismiss message

* T-Deck Trackball press is not user button

* Add select on favorite frame to launch cannedMessage DM

* Minor wording change

* Less capital letters

* Fix empty message check, time isn't reliable

* drop dead code

* Make UIRenderer a static class instead of namespace

* Fix the select on favorite

* Check if message is empty early and then 'return'

* Add kb_found, and show the option to launch freetype if appropriate

* Ignore impossible touchscreen touches

* Auto scroll fix

* Move linebreak after "from" for banners to maximize screen usage.

* Center "No messages to show" on Message frame

* Start consolidating buzzer behavior

* Fixed signed / unsigned warning

* Cast second parameter of max() to make some targets happy

* Cast kbchar to (char) to make arduino string happy

* Shorten the notice of "No messages"

* Add buzzer mode chooser

* Add regionPicker to Lora icon

* Reduce line spacing and reorder Position screen to resolve overlapping issues

* Update message titles, fix GPS icons, add Back options

* Leftover boops

* Remove chirp

* Make the region selection dismissable when a region is already set

* Add read-aloud functionality on messages w/ esp8266sam

* "Last Heard" is a better label

* tweak the beep

* 5 options

* properly tear down freetext upon cancel

* de-convelute canned messages just a bit

* Correct height of Mail icon in navigation bar

* Remove unused warning

* Consolidate time methods into TimeFormatters

* Oops

* Change LoRa Picker Cancel to Back

* Tweak selection characters on Banner

* Message render not scrolling on 5th line

* More fixes for message scrolling

* Remove the safety next on text overflow - we found that root cause

* Add pin definitions to fix compilation for obscure target

* Don't let the touchscreen send unitialized kbchar values

* Make virtual KB just a bit quicker

* No more double tap, swipe!

* Left is left, and Right is right

* Update horizontal lightning bolt design

* Move from solid to dashed separator for Message Frame

* Single emote feature fix

* Manually sort overlapping elements for now

* Freetext and clearer choices

* Fix ESP32 InkHUD builds on the unify-tft branch (#7087)

* Remove BaseUI branding

* Capitalization is fun

* Revert Meshtastic Boot Frame Changes

* Add ANZ_433 LoRa region to picker

* Update settings.json

---------

Co-authored-by: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Jason P <applewiz@mac.com>
Co-authored-by: todd-herbert <herbert.todd@gmail.com>
Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Jonathan Bennett
2025-06-21 06:36:04 -05:00
committed by GitHub
parent 82b7cb5dd0
commit 4feaec651f
154 changed files with 9852 additions and 4501 deletions

View File

@@ -1,6 +1,6 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "Default.h"
@@ -11,12 +11,15 @@
#include "RTC.h"
#include "Router.h"
#include "UnitConversions.h"
#include "buzz.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/images.h"
#include "main.h"
#include "modules/ExternalNotificationModule.h"
#include "power.h"
#include "sleep.h"
#include "target_specific.h"
#include <OLEDDisplay.h>
#include <OLEDDisplayUi.h>
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL
@@ -25,6 +28,10 @@
#include "Sensor/RCWL9620Sensor.h"
#include "Sensor/nullSensor.h"
namespace graphics
{
extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr);
}
#if __has_include(<Adafruit_AHTX0.h>)
#include "Sensor/AHT10.h"
AHT10Sensor aht10Sensor;
@@ -344,120 +351,152 @@ 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);
int line = 1;
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");
// === Set Title
const char *titleStr = (SCREEN_WIDTH > 128) ? "Environment" : "Env.";
// === Header ===
graphics::drawCommonHeader(display, x, y, titleStr);
// === Row spacing setup ===
const int rowHeight = FONT_HEIGHT_SMALL - 4;
int currentY = graphics::getTextPositions(display)[line++];
// === 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);
const char *bannerMsg = nullptr; // Default: no banner
if (m.iaq <= 25)
aqi += " (Excellent)";
else if (m.iaq <= 50)
aqi += " (Good)";
else if (m.iaq <= 100)
aqi += " (Moderate)";
else if (m.iaq <= 150)
aqi += " (Poor)";
else if (m.iaq <= 200) {
aqi += " (Unhealthy)";
bannerMsg = "Unhealthy IAQ";
} else if (m.iaq <= 300) {
aqi += " (Very Unhealthy)";
bannerMsg = "Very Unhealthy IAQ";
} else {
aqi += " (Hazardous)";
bannerMsg = "Hazardous IAQ";
}
sensorData[sensorCount++] =
"Temp/Hum: " + last_temp + " / " + String(lastMeasurement.variant.environment_metrics.relative_humidity, 0) + "%";
}
entries.push_back(aqi);
if (lastMeasurement.variant.environment_metrics.barometric_pressure != 0) {
sensorData[sensorCount++] =
"Press: " + String(lastMeasurement.variant.environment_metrics.barometric_pressure, 0) + "hPA";
}
// === IAQ alert logic ===
static uint32_t lastAlertTime = 0;
uint32_t now = millis();
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";
}
bool isOwnTelemetry = lastMeasurementPacket->from == nodeDB->getNodeNum();
bool isCooldownOver = (now - lastAlertTime > 60000);
if (lastMeasurement.variant.environment_metrics.iaq != 0) {
sensorData[sensorCount++] = "IAQ: " + String(lastMeasurement.variant.environment_metrics.iaq);
}
if (isOwnTelemetry && bannerMsg && isCooldownOver) {
LOG_INFO("drawFrame: IAQ %d (own) — showing banner: %s", m.iaq, bannerMsg);
screen->showOverlayBanner(bannerMsg, 3000);
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;
}
// Only buzz if IAQ is over 200
if (m.iaq > 200 && moduleConfig.external_notification.enabled && !externalNotificationModule->getMute()) {
playLongBeep();
}
lastScrollTime = millis();
lastAlertTime = now;
}
}
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]);
}
currentY += rowHeight;
}
}
bool EnvironmentTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t)