Compare commits

..

6 Commits

Author SHA1 Message Date
Jonathan Bennett
4fd7960371 Drop debug warning 2025-12-13 12:22:22 -06:00
Jonathan Bennett
5f0e86a27e Merge branch 'develop' into native-time 2025-12-13 12:10:18 -06:00
Jonathan Bennett
9ce1a7ff79 Detect if NTP is active on native 2025-12-13 12:08:24 -06:00
Austin
bf32f17f28 Actions: Compact manifest job output summary (#8957) 2025-12-13 12:32:01 +11:00
GUVWAF
68250dc937 Mark implicit ACK for MQTT as MQTT transport (#8939)
* Mark implicit ACK for MQTT as MQTT transport

* TRUNK

* Fix build

* Make sure implicit ACKs from MQTT do not stop retransmissions in ReliableRouter

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2025-12-12 05:19:32 -06:00
Austin
bcfe069997 Optimize builds to reduce duplicate dependency checks (#8943)
'mtjson' will now build all required pieces when they don't exist
2025-12-11 19:01:31 -06:00
48 changed files with 1446 additions and 2951 deletions

View File

@@ -56,16 +56,18 @@ jobs:
ota_firmware_source: ${{ steps.ota_dir.outputs.src || '' }}
ota_firmware_target: ${{ steps.ota_dir.outputs.tgt || '' }}
- name: Echo manifest from release/firmware-*.mt.json to job summary
if: ${{ always() }}
- name: Job summary
env:
PIO_ENV: ${{ inputs.pio_env }}
run: |
echo "## Manifest: \`$PIO_ENV\`" >> $GITHUB_STEP_SUMMARY
echo "## $PIO_ENV" >> $GITHUB_STEP_SUMMARY
echo "<details><summary><strong>Manifest</strong></summary>" >> $GITHUB_STEP_SUMMARY
echo '' >> $GITHUB_STEP_SUMMARY
echo '```json' >> $GITHUB_STEP_SUMMARY
cat release/firmware-*.mt.json >> $GITHUB_STEP_SUMMARY
echo '' >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "</details>" >> $GITHUB_STEP_SUMMARY
- name: Store binaries as an artifact
uses: actions/upload-artifact@v5

View File

@@ -22,5 +22,5 @@ jobs:
days-before-stale: 45
stale-issue-message: This issue has not had any comment or update in the last month. If it is still relevant, please post update comments. If no comments are made, this issue will be closed automagically in 7 days.
close-issue-message: This issue has not had any comment since the last notice. It has been closed automatically. If this is incorrect, or the issue becomes relevant again, please request that it is reopened.
exempt-issue-labels: pinned,3.0
exempt-pr-labels: pinned,3.0
exempt-issue-labels: pinned,3.0,triaged,backlog
exempt-pr-labels: pinned,3.0,triaged,backlog

View File

@@ -22,7 +22,7 @@ export APP_VERSION=$VERSION
basename=firmware-$1-$VERSION
pio run --environment $1 # -v
pio run --environment $1 -t mtjson # -v
cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf
@@ -32,20 +32,10 @@ cp $BUILDDIR/$basename.factory.bin $OUTDIR/$basename.factory.bin
echo "Copying ESP32 update bin file"
cp $BUILDDIR/$basename.bin $OUTDIR/$basename.bin
echo "Building Filesystem for ESP32 targets"
# If you want to build the webui, uncomment the following lines
# pio run --environment $1 -t buildfs
# cp .pio/build/$1/littlefs.bin $OUTDIR/littlefswebui-$1-$VERSION.bin
# # Remove webserver files from the filesystem and rebuild
# ls -l data/static # Diagnostic list of files
# rm -rf data/static
pio run --environment $1 -t buildfs --disable-auto-clean
echo "Copying Filesystem for ESP32 targets"
cp $BUILDDIR/littlefs-$1-$VERSION.bin $OUTDIR/littlefs-$1-$VERSION.bin
cp bin/device-install.* $OUTDIR/
cp bin/device-update.* $OUTDIR/
# Generate the manifest file
echo "Generating Meshtastic manifest"
TIMEFORMAT="Generated manifest in %E seconds"
time pio run --environment $1 -t mtjson --silent --disable-auto-clean
echo "Copying manifest"
cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json

View File

@@ -22,7 +22,7 @@ export APP_VERSION=$VERSION
basename=firmware-$1-$VERSION
pio run --environment $1 # -v
pio run --environment $1 -t mtjson # -v
cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf
@@ -47,8 +47,5 @@ if (echo $1 | grep -q "rak4631"); then
cp $SRCHEX $OUTDIR/
fi
# Generate the manifest file
echo "Generating Meshtastic manifest"
TIMEFORMAT="Generated manifest in %E seconds"
time pio run --environment $1 -t mtjson --silent --disable-auto-clean
echo "Copying manifest"
cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json

View File

@@ -22,15 +22,12 @@ export APP_VERSION=$VERSION
basename=firmware-$1-$VERSION
pio run --environment $1 # -v
pio run --environment $1 -t mtjson # -v
cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf
echo "Copying uf2 file"
cp $BUILDDIR/$basename.uf2 $OUTDIR/$basename.uf2
# Generate the manifest file
echo "Generating Meshtastic manifest"
TIMEFORMAT="Generated manifest in %E seconds"
time pio run --environment $1 -t mtjson --silent --disable-auto-clean
echo "Copying manifest"
cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json

View File

@@ -22,15 +22,12 @@ export APP_VERSION=$VERSION
basename=firmware-$1-$VERSION
pio run --environment $1 # -v
pio run --environment $1 -t mtjson # -v
cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf
echo "Copying STM32 bin file"
cp $BUILDDIR/$basename.bin $OUTDIR/$basename.bin
# Generate the manifest file
echo "Generating Meshtastic manifest"
TIMEFORMAT="Generated manifest in %E seconds"
time pio run --environment $1 -t mtjson --silent --disable-auto-clean
echo "Copying manifest"
cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json

View File

@@ -159,20 +159,22 @@ def load_boot_logo(source, target, env):
# Load the boot logo on TFT builds
if ("HAS_TFT", 1) in env.get("CPPDEFINES", []):
env.AddPreAction('$BUILD_DIR/littlefs.bin', load_boot_logo)
env.AddPreAction(f"$BUILD_DIR/{lfsbin}", load_boot_logo)
# Rename (mv) littlefs.bin to include the PROGNAME
# This ensures the littlefs.bin is named consistently with the firmware
env.AddPostAction('$BUILD_DIR/littlefs.bin', env.VerboseAction(
f'mv $BUILD_DIR/littlefs.bin $BUILD_DIR/{lfsbin}',
f'Renaming littlefs.bin to {lfsbin}'
))
mtjson_deps = ["buildprog"]
if platform.name == "espressif32":
# Build littlefs image as part of mtjson target
# Equivalent to `pio run -t buildfs`
target_lfs = env.DataToBin(
join("$BUILD_DIR", "${ESP32_FS_IMAGE_NAME}"), "$PROJECT_DATA_DIR"
)
mtjson_deps.append(target_lfs)
env.AddCustomTarget(
name="mtjson",
dependencies=None,
dependencies=mtjson_deps,
actions=[manifest_gather],
title="Meshtastic Manifest",
description="Generating Meshtastic manifest JSON + Checksums",
always_build=True,
always_build=False,
)

View File

@@ -11,6 +11,9 @@ else:
prefsLoc = env["PROJECT_DIR"] + "/version.properties"
verObj = readProps(prefsLoc)
env.Replace(PROGNAME=f"firmware-{env.get('PIOENV')}-{verObj['long']}")
env.Replace(ESP32_FS_IMAGE_NAME=f"littlefs-{env.get('PIOENV')}-{verObj['long']}")
# Print the new program name for verification
print(f"PROGNAME: {env.get('PROGNAME')}")
if platform.name == "espressif32":
print(f"ESP32_FS_IMAGE_NAME: {env.get('ESP32_FS_IMAGE_NAME')}")

View File

@@ -1,425 +0,0 @@
#include "configuration.h"
#if HAS_SCREEN
#include "FSCommon.h"
#include "MessageStore.h"
#include "NodeDB.h"
#include "SPILock.h"
#include "SafeFile.h"
#include "gps/RTC.h"
#include "graphics/draw/MessageRenderer.h"
#include <cstring> // memcpy
#ifndef MESSAGE_TEXT_POOL_SIZE
#define MESSAGE_TEXT_POOL_SIZE (MAX_MESSAGES_SAVED * MAX_MESSAGE_SIZE)
#endif
// Global message text pool and state
static char *g_messagePool = nullptr;
static size_t g_poolWritePos = 0;
// Reset pool (called on boot or clear)
static inline void resetMessagePool()
{
if (!g_messagePool) {
g_messagePool = static_cast<char *>(malloc(MESSAGE_TEXT_POOL_SIZE));
if (!g_messagePool) {
LOG_ERROR("MessageStore: Failed to allocate %d bytes for message pool", MESSAGE_TEXT_POOL_SIZE);
return;
}
}
g_poolWritePos = 0;
memset(g_messagePool, 0, MESSAGE_TEXT_POOL_SIZE);
}
// Allocate text in pool and return offset
// If not enough space remains, wrap around (ring buffer style)
static inline uint16_t storeTextInPool(const char *src, size_t len)
{
if (len >= MAX_MESSAGE_SIZE)
len = MAX_MESSAGE_SIZE - 1;
// Wrap pool if out of space
if (g_poolWritePos + len + 1 >= MESSAGE_TEXT_POOL_SIZE) {
g_poolWritePos = 0;
}
uint16_t offset = g_poolWritePos;
memcpy(&g_messagePool[g_poolWritePos], src, len);
g_messagePool[g_poolWritePos + len] = '\0';
g_poolWritePos += (len + 1);
return offset;
}
// Retrieve a const pointer to message text by offset
static inline const char *getTextFromPool(uint16_t offset)
{
if (!g_messagePool || offset >= MESSAGE_TEXT_POOL_SIZE)
return "";
return &g_messagePool[offset];
}
// Helper: assign a timestamp (RTC if available, else boot-relative)
static inline void assignTimestamp(StoredMessage &sm)
{
uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true);
if (nowSecs) {
sm.timestamp = nowSecs;
sm.isBootRelative = false;
} else {
sm.timestamp = millis() / 1000;
sm.isBootRelative = true;
}
}
// Generic push with cap (used by live + persisted queues)
template <typename T> static inline void pushWithLimit(std::deque<T> &queue, const T &msg)
{
if (queue.size() >= MAX_MESSAGES_SAVED)
queue.pop_front();
queue.push_back(msg);
}
template <typename T> static inline void pushWithLimit(std::deque<T> &queue, T &&msg)
{
if (queue.size() >= MAX_MESSAGES_SAVED)
queue.pop_front();
queue.emplace_back(std::move(msg));
}
MessageStore::MessageStore(const std::string &label)
{
filename = "/Messages_" + label + ".msgs";
resetMessagePool(); // initialize text pool on boot
}
// Live message handling (RAM only)
void MessageStore::addLiveMessage(StoredMessage &&msg)
{
pushWithLimit(liveMessages, std::move(msg));
}
void MessageStore::addLiveMessage(const StoredMessage &msg)
{
pushWithLimit(liveMessages, msg);
}
// Add from incoming/outgoing packet
const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &packet)
{
StoredMessage sm;
assignTimestamp(sm);
sm.channelIndex = packet.channel;
const char *payload = reinterpret_cast<const char *>(packet.decoded.payload.bytes);
size_t len = strnlen(payload, MAX_MESSAGE_SIZE - 1);
sm.textOffset = storeTextInPool(payload, len);
sm.textLength = len;
// Determine sender
uint32_t localNode = nodeDB->getNodeNum();
sm.sender = (packet.from == 0) ? localNode : packet.from;
sm.dest = packet.to;
bool isDM = (sm.dest != 0 && sm.dest != NODENUM_BROADCAST);
if (packet.from == 0) {
sm.type = isDM ? MessageType::DM_TO_US : MessageType::BROADCAST;
sm.ackStatus = AckStatus::NONE;
} else {
sm.type = isDM ? MessageType::DM_TO_US : MessageType::BROADCAST;
sm.ackStatus = AckStatus::ACKED;
}
addLiveMessage(sm);
return liveMessages.back();
}
// Outgoing/manual message
void MessageStore::addFromString(uint32_t sender, uint8_t channelIndex, const std::string &text)
{
StoredMessage sm;
// Always use our local time (helper handles RTC vs boot time)
assignTimestamp(sm);
sm.sender = sender;
sm.channelIndex = channelIndex;
sm.textOffset = storeTextInPool(text.c_str(), text.size());
sm.textLength = text.size();
// Use the provided destination
sm.dest = sender;
sm.type = MessageType::DM_TO_US;
// Outgoing messages always start with unknown ack status
sm.ackStatus = AckStatus::NONE;
addLiveMessage(sm);
}
#if ENABLE_MESSAGE_PERSISTENCE
// Compact, fixed-size on-flash representation using offset + length
struct __attribute__((packed)) StoredMessageRecord {
uint32_t timestamp;
uint32_t sender;
uint8_t channelIndex;
uint32_t dest;
uint8_t isBootRelative;
uint8_t ackStatus; // static_cast<uint8_t>(AckStatus)
uint8_t type; // static_cast<uint8_t>(MessageType)
uint16_t textLength; // message length
char text[MAX_MESSAGE_SIZE]; // store actual text here
};
// Serialize one StoredMessage to flash
static inline void writeMessageRecord(SafeFile &f, const StoredMessage &m)
{
StoredMessageRecord rec = {};
rec.timestamp = m.timestamp;
rec.sender = m.sender;
rec.channelIndex = m.channelIndex;
rec.dest = m.dest;
rec.isBootRelative = m.isBootRelative;
rec.ackStatus = static_cast<uint8_t>(m.ackStatus);
rec.type = static_cast<uint8_t>(m.type);
rec.textLength = m.textLength;
// Copy the actual text into the record from RAM pool
const char *txt = getTextFromPool(m.textOffset);
strncpy(rec.text, txt, MAX_MESSAGE_SIZE - 1);
rec.text[MAX_MESSAGE_SIZE - 1] = '\0';
f.write(reinterpret_cast<const uint8_t *>(&rec), sizeof(rec));
}
// Deserialize one StoredMessage from flash; returns false on short read
static inline bool readMessageRecord(File &f, StoredMessage &m)
{
StoredMessageRecord rec = {};
if (f.readBytes(reinterpret_cast<char *>(&rec), sizeof(rec)) != sizeof(rec))
return false;
m.timestamp = rec.timestamp;
m.sender = rec.sender;
m.channelIndex = rec.channelIndex;
m.dest = rec.dest;
m.isBootRelative = rec.isBootRelative;
m.ackStatus = static_cast<AckStatus>(rec.ackStatus);
m.type = static_cast<MessageType>(rec.type);
m.textLength = rec.textLength;
// 💡 Re-store text into pool and update offset
m.textLength = strnlen(rec.text, MAX_MESSAGE_SIZE - 1);
m.textOffset = storeTextInPool(rec.text, m.textLength);
return true;
}
void MessageStore::saveToFlash()
{
#ifdef FSCom
// Ensure root exists
spiLock->lock();
FSCom.mkdir("/");
spiLock->unlock();
SafeFile f(filename.c_str(), false);
spiLock->lock();
uint8_t count = static_cast<uint8_t>(liveMessages.size());
if (count > MAX_MESSAGES_SAVED)
count = MAX_MESSAGES_SAVED;
f.write(&count, 1);
for (uint8_t i = 0; i < count; ++i) {
writeMessageRecord(f, liveMessages[i]);
}
spiLock->unlock();
f.close();
#endif
}
void MessageStore::loadFromFlash()
{
std::deque<StoredMessage>().swap(liveMessages);
resetMessagePool(); // reset pool when loading
#ifdef FSCom
concurrency::LockGuard guard(spiLock);
if (!FSCom.exists(filename.c_str()))
return;
auto f = FSCom.open(filename.c_str(), FILE_O_READ);
if (!f)
return;
uint8_t count = 0;
f.readBytes(reinterpret_cast<char *>(&count), 1);
if (count > MAX_MESSAGES_SAVED)
count = MAX_MESSAGES_SAVED;
for (uint8_t i = 0; i < count; ++i) {
StoredMessage m;
if (!readMessageRecord(f, m))
break;
liveMessages.push_back(m);
}
f.close();
#endif
}
#else
// If persistence is disabled, these functions become no-ops
void MessageStore::saveToFlash() {}
void MessageStore::loadFromFlash() {}
#endif
// Clear all messages (RAM + persisted queue)
void MessageStore::clearAllMessages()
{
std::deque<StoredMessage>().swap(liveMessages);
resetMessagePool();
#ifdef FSCom
SafeFile f(filename.c_str(), false);
uint8_t count = 0;
f.write(&count, 1); // write "0 messages"
f.close();
#endif
}
// Internal helper: erase first or last message matching a predicate
template <typename Predicate> static void eraseIf(std::deque<StoredMessage> &deque, Predicate pred, bool fromBack = false)
{
if (fromBack) {
// Iterate from the back and erase the first match from the end
for (auto it = deque.rbegin(); it != deque.rend(); ++it) {
if (pred(*it)) {
deque.erase(std::next(it).base());
break;
}
}
} else {
// Manual forward search to avoid std::find_if
auto it = deque.begin();
for (; it != deque.end(); ++it) {
if (pred(*it))
break;
}
if (it != deque.end())
deque.erase(it);
}
}
// Delete oldest message (RAM + persisted queue)
void MessageStore::deleteOldestMessage()
{
eraseIf(liveMessages, [](StoredMessage &) { return true; });
saveToFlash();
}
// Delete oldest message in a specific channel
void MessageStore::deleteOldestMessageInChannel(uint8_t channel)
{
auto pred = [channel](const StoredMessage &m) { return m.type == MessageType::BROADCAST && m.channelIndex == channel; };
eraseIf(liveMessages, pred);
saveToFlash();
}
void MessageStore::deleteAllMessagesInChannel(uint8_t channel)
{
auto pred = [channel](const StoredMessage &m) { return m.type == MessageType::BROADCAST && m.channelIndex == channel; };
eraseIf(liveMessages, pred, false /* delete ALL, not just first */);
saveToFlash();
}
void MessageStore::deleteAllMessagesWithPeer(uint32_t peer)
{
uint32_t local = nodeDB->getNodeNum();
auto pred = [&](const StoredMessage &m) {
if (m.type != MessageType::DM_TO_US)
return false;
uint32_t other = (m.sender == local) ? m.dest : m.sender;
return other == peer;
};
eraseIf(liveMessages, pred, false);
saveToFlash();
}
// Delete oldest message in a direct chat with a node
void MessageStore::deleteOldestMessageWithPeer(uint32_t peer)
{
auto pred = [peer](const StoredMessage &m) {
if (m.type != MessageType::DM_TO_US)
return false;
uint32_t other = (m.sender == nodeDB->getNodeNum()) ? m.dest : m.sender;
return other == peer;
};
eraseIf(liveMessages, pred);
saveToFlash();
}
std::deque<StoredMessage> MessageStore::getChannelMessages(uint8_t channel) const
{
std::deque<StoredMessage> result;
for (const auto &m : liveMessages) {
if (m.type == MessageType::BROADCAST && m.channelIndex == channel) {
result.push_back(m);
}
}
return result;
}
std::deque<StoredMessage> MessageStore::getDirectMessages() const
{
std::deque<StoredMessage> result;
for (const auto &m : liveMessages) {
if (m.type == MessageType::DM_TO_US) {
result.push_back(m);
}
}
return result;
}
// Upgrade boot-relative timestamps once RTC is valid
// Only same-boot boot-relative messages are healed.
// Persisted boot-relative messages from old boots stay ??? forever.
void MessageStore::upgradeBootRelativeTimestamps()
{
uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true);
if (nowSecs == 0)
return; // Still no valid RTC
uint32_t bootNow = millis() / 1000;
auto fix = [&](std::deque<StoredMessage> &dq) {
for (auto &m : dq) {
if (m.isBootRelative && m.timestamp <= bootNow) {
uint32_t bootOffset = nowSecs - bootNow;
m.timestamp += bootOffset;
m.isBootRelative = false;
}
}
};
fix(liveMessages);
}
const char *MessageStore::getText(const StoredMessage &msg)
{
// Wrapper around the internal helper
return getTextFromPool(msg.textOffset);
}
uint16_t MessageStore::storeText(const char *src, size_t len)
{
// Wrapper around the internal helper
return storeTextInPool(src, len);
}
// Global definition
MessageStore messageStore("default");
#endif

View File

@@ -1,131 +0,0 @@
#pragma once
#if HAS_SCREEN
// Disable debug logging entirely on release builds of HELTEC_MESH_SOLAR for space constraints
#if defined(HELTEC_MESH_SOLAR)
#define LOG_DEBUG(...)
#endif
// Enable or disable message persistence (flash storage)
// Define -DENABLE_MESSAGE_PERSISTENCE=0 in build_flags to disable it entirely
#ifndef ENABLE_MESSAGE_PERSISTENCE
#define ENABLE_MESSAGE_PERSISTENCE 1
#endif
#include "mesh/generated/meshtastic/mesh.pb.h"
#include <cstdint>
#include <deque>
#include <string>
// How many messages are stored (RAM + flash).
// Define -DMESSAGE_HISTORY_LIMIT=N in build_flags to control memory usage.
#ifndef MESSAGE_HISTORY_LIMIT
#define MESSAGE_HISTORY_LIMIT 20
#endif
// Internal alias used everywhere in code do NOT redefine elsewhere.
#define MAX_MESSAGES_SAVED MESSAGE_HISTORY_LIMIT
// Maximum text payload size per message in bytes.
// This still defines the max message length, but we no longer reserve this space per message.
#define MAX_MESSAGE_SIZE 220
// Total shared text pool size for all messages combined.
// The text pool is RAM-only. Text is re-stored from flash into the pool on boot.
#ifndef MESSAGE_TEXT_POOL_SIZE
#define MESSAGE_TEXT_POOL_SIZE (MAX_MESSAGES_SAVED * MAX_MESSAGE_SIZE)
#endif
// Explicit message classification
enum class MessageType : uint8_t {
BROADCAST = 0, // broadcast message
DM_TO_US = 1 // direct message addressed to this node
};
// Delivery status for messages we sent
enum class AckStatus : uint8_t {
NONE = 0, // just sent, waiting (no symbol shown)
ACKED = 1, // got a valid ACK from destination
NACKED = 2, // explicitly failed
TIMEOUT = 3, // no ACK after retry window
RELAYED = 4 // got an ACK from relay, not destination
};
struct StoredMessage {
uint32_t timestamp; // When message was created (secs since boot or RTC)
uint32_t sender; // NodeNum of sender
uint8_t channelIndex; // Channel index used
uint32_t dest; // Destination node (broadcast or direct)
MessageType type; // Derived from dest (explicit classification)
bool isBootRelative; // true = millis()/1000 fallback; false = epoch/RTC absolute
AckStatus ackStatus; // Delivery status (only meaningful for our own sent messages)
// Text storage metadata — rebuilt from flash at boot
uint16_t textOffset; // Offset into global text pool (valid only after loadFromFlash())
uint16_t textLength; // Length of text in bytes
// Default constructor initializes all fields safely
StoredMessage()
: timestamp(0), sender(0), channelIndex(0), dest(0xffffffff), type(MessageType::BROADCAST), isBootRelative(false),
ackStatus(AckStatus::NONE), textOffset(0), textLength(0)
{
}
};
class MessageStore
{
public:
explicit MessageStore(const std::string &label);
// Live RAM methods (always current, used by UI and runtime)
void addLiveMessage(StoredMessage &&msg);
void addLiveMessage(const StoredMessage &msg); // convenience overload
const std::deque<StoredMessage> &getLiveMessages() const { return liveMessages; }
// Add new messages from packets or manual input
const StoredMessage &addFromPacket(const meshtastic_MeshPacket &mp); // Incoming/outgoing → RAM only
void addFromString(uint32_t sender, uint8_t channelIndex, const std::string &text); // Manual add
// Persistence methods (used only on boot/shutdown)
void saveToFlash(); // Save messages to flash
void loadFromFlash(); // Load messages from flash
// Clear all messages (RAM + persisted queue + text pool)
void clearAllMessages();
// Delete helpers
void deleteOldestMessage(); // remove oldest from RAM (and flash on save)
void deleteOldestMessageInChannel(uint8_t channel);
void deleteOldestMessageWithPeer(uint32_t peer);
void deleteAllMessagesInChannel(uint8_t channel);
void deleteAllMessagesWithPeer(uint32_t peer);
// Unified accessor (for UI code, defaults to RAM buffer)
const std::deque<StoredMessage> &getMessages() const { return liveMessages; }
// Helper filters for future use
std::deque<StoredMessage> getChannelMessages(uint8_t channel) const; // Only broadcast messages on a channel
std::deque<StoredMessage> getDirectMessages() const; // Only direct messages
// Upgrade boot-relative timestamps once RTC is valid
void upgradeBootRelativeTimestamps();
// Retrieve the C-string text for a stored message
static const char *getText(const StoredMessage &msg);
// Allocate text into pool (used by sender-side code)
static uint16_t storeText(const char *src, size_t len);
// Used when loading from flash to rebuild the text pool
static uint16_t rebuildTextFromFlash(const char *src, size_t len);
private:
std::deque<StoredMessage> liveMessages; // Single in-RAM message buffer (also used for persistence)
std::string filename; // Flash filename for persistence
};
// Global instance (defined in MessageStore.cpp)
extern MessageStore messageStore;
#endif

View File

@@ -11,7 +11,6 @@
* For more information, see: https://meshtastic.org/
*/
#include "power.h"
#include "MessageStore.h"
#include "NodeDB.h"
#include "PowerFSM.h"
#include "Throttle.h"
@@ -787,9 +786,7 @@ void Power::shutdown()
playShutdownMelody();
#endif
nodeDB->saveToDisk();
#if HAS_SCREEN
messageStore.saveToFlash();
#endif
#if defined(ARCH_NRF52) || defined(ARCH_ESP32) || defined(ARCH_RP2040)
#ifdef PIN_LED1
ledOff(PIN_LED1);

View File

@@ -131,7 +131,6 @@ void RedirectablePrint::log_to_serial(const char *logLevel, const char *format,
int hour = hms / SEC_PER_HOUR;
int min = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN
#ifdef ARCH_PORTDUINO
::printf("%s ", logLevel);
if (color) {

View File

@@ -46,7 +46,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#endif
#include "FSCommon.h"
#include "MeshService.h"
#include "MessageStore.h"
#include "RadioLibInterface.h"
#include "error.h"
#include "gps/GeoCoord.h"
@@ -65,7 +64,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "modules/WaypointModule.h"
#include "sleep.h"
#include "target_specific.h"
extern MessageStore messageStore;
using graphics::Emote;
using graphics::emotes;
using graphics::numEmotes;
#if USE_TFTDISPLAY
extern uint16_t TFT_MESH;
@@ -117,6 +119,10 @@ uint32_t dopThresholds[5] = {2000, 1000, 500, 200, 100};
// we'll need to hold onto pointers for the modules that can draw a frame.
std::vector<MeshModule *> moduleFrames;
// Global variables for screen function overlay symbols
std::vector<std::string> functionSymbol;
std::string functionSymbolString;
#if HAS_GPS
// GeoCoord object for the screen
GeoCoord geoCoord;
@@ -257,11 +263,19 @@ static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int
} else {
// otherwise, just display the module frame that's aligned with the current frame
module_frame = state->currentFrame;
// LOG_DEBUG("Screen is not in transition. Frame: %d", module_frame);
}
// LOG_DEBUG("Draw Module Frame %d", module_frame);
MeshModule &pi = *moduleFrames.at(module_frame);
pi.drawFrame(display, state, x, y);
}
// Ignore messages originating from phone (from the current node 0x0) unless range test or store and forward module are enabled
static bool shouldDrawMessage(const meshtastic_MeshPacket *packet)
{
return packet->from != 0 && !moduleConfig.store_forward.enabled;
}
/**
* Given a recent lat/lon return a guess of the heading the user is walking on.
*
@@ -308,16 +322,16 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
{
graphics::normalFrames = new FrameCallback[MAX_NUM_NODES + NUM_EXTRA_FRAMES];
LOG_INFO("Protobuf Value uiconfig.screen_rgb_color: %d", uiconfig.screen_rgb_color);
int32_t rawRGB = uiconfig.screen_rgb_color;
// Only validate the combined value once
if (rawRGB > 0 && rawRGB <= 255255255) {
// Extract each component as a normal int first
int r = (rawRGB >> 16) & 0xFF;
int g = (rawRGB >> 8) & 0xFF;
int b = rawRGB & 0xFF;
if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) {
TFT_MESH = COLOR565(static_cast<uint8_t>(r), static_cast<uint8_t>(g), static_cast<uint8_t>(b));
uint8_t TFT_MESH_r = (rawRGB >> 16) & 0xFF;
uint8_t TFT_MESH_g = (rawRGB >> 8) & 0xFF;
uint8_t TFT_MESH_b = rawRGB & 0xFF;
LOG_INFO("Values of r,g,b: %d, %d, %d", TFT_MESH_r, TFT_MESH_g, TFT_MESH_b);
if (TFT_MESH_r <= 255 && TFT_MESH_g <= 255 && TFT_MESH_b <= 255) {
TFT_MESH = COLOR565(TFT_MESH_r, TFT_MESH_g, TFT_MESH_b);
}
}
@@ -536,10 +550,10 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
void Screen::setup()
{
// Enable display rendering
// === Enable display rendering ===
useDisplay = true;
// Load saved brightness from UI config
// === Load saved brightness from UI config ===
// For OLED displays (SSD1306), default brightness is 255 if not set
if (uiconfig.screen_brightness == 0) {
#if defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107)
@@ -551,7 +565,7 @@ void Screen::setup()
brightness = uiconfig.screen_brightness;
}
// Detect OLED subtype (if supported by board variant)
// === Detect OLED subtype (if supported by board variant) ===
#ifdef AutoOLEDWire_h
if (isAUTOOled)
static_cast<AutoOLEDWire *>(dispdev)->setDetected(model);
@@ -573,7 +587,7 @@ void Screen::setup()
static_cast<ST7796Spi *>(dispdev)->setRGB(TFT_MESH);
#endif
// Initialize display and UI system
// === Initialize display and UI system ===
ui->init();
displayWidth = dispdev->width();
displayHeight = dispdev->height();
@@ -585,7 +599,7 @@ void Screen::setup()
ui->disableAllIndicators(); // Disable page indicator dots
ui->getUiState()->userData = this; // Allow static callbacks to access Screen instance
// Apply loaded brightness
// === Apply loaded brightness ===
#if defined(ST7789_CS)
static_cast<TFTDisplay *>(dispdev)->setDisplayBrightness(brightness);
#elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SPISSD1306)
@@ -593,20 +607,20 @@ void Screen::setup()
#endif
LOG_INFO("Applied screen brightness: %d", brightness);
// Set custom overlay callbacks
// === Set custom overlay callbacks ===
static OverlayCallback overlays[] = {
graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame
};
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
// Enable UTF-8 to display mapping
// === Enable UTF-8 to display mapping ===
dispdev->setFontTableLookupFunction(customFontTableLookup);
#ifdef USERPREFS_OEM_TEXT
logo_timeout *= 2; // Give more time for branded boot logos
#endif
// Configure alert frames (e.g., "Resuming..." or region name)
// === Configure alert frames (e.g., "Resuming..." or region name) ===
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip slow refresh
alertFrames[0] = [this](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) {
#ifdef ARCH_ESP32
@@ -622,10 +636,10 @@ void Screen::setup()
ui->setFrames(alertFrames, 1);
ui->disableAutoTransition(); // Require manual navigation between frames
// Log buffer for on-screen logs (3 lines max)
// === Log buffer for on-screen logs (3 lines max) ===
dispdev->setLogBuffer(3, 32);
// Optional screen mirroring or flipping (e.g. for T-Beam orientation)
// === Optional screen mirroring or flipping (e.g. for T-Beam orientation) ===
#ifdef SCREEN_MIRROR
dispdev->mirrorScreen();
#else
@@ -643,7 +657,7 @@ void Screen::setup()
}
#endif
// Generate device ID from MAC address
// === Generate device ID from MAC address ===
uint8_t dmac[6];
getMacAddr(dmac);
snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]);
@@ -652,7 +666,7 @@ void Screen::setup()
handleSetOn(false); // Ensure proper init for Arduino targets
#endif
// Turn on display and trigger first draw
// === Turn on display and trigger first draw ===
handleSetOn(true);
determineResolution(dispdev->height(), dispdev->width());
ui->update();
@@ -675,7 +689,7 @@ void Screen::setup()
touchScreenImpl1->init();
#endif
// Subscribe to device status updates
// === Subscribe to device status updates ===
powerStatusObserver.observe(&powerStatus->onNewStatus);
gpsStatusObserver.observe(&gpsStatus->onNewStatus);
nodeStatusObserver.observe(&nodeStatus->onNewStatus);
@@ -683,14 +697,12 @@ void Screen::setup()
#if !MESHTASTIC_EXCLUDE_ADMIN
adminMessageObserver.observe(adminModule);
#endif
if (textMessageModule)
textMessageObserver.observe(textMessageModule);
if (inputBroker)
inputObserver.observe(inputBroker);
// Load persisted messages into RAM
messageStore.loadFromFlash();
LOG_INFO("MessageStore loaded from flash");
// Notify modules that support UI events
// === Notify modules that support UI events ===
MeshModule::observeUIEvents(&uiFrameEventObserver);
}
@@ -761,23 +773,6 @@ int32_t Screen::runOnce()
if (displayHeight == 0) {
displayHeight = dispdev->getHeight();
}
// Detect frame transitions and clear message cache when leaving text message screen
{
static int8_t lastFrameIndex = -1;
int8_t currentFrameIndex = ui->getUiState()->currentFrame;
int8_t textMsgIndex = framesetInfo.positions.textMessage;
if (lastFrameIndex != -1 && currentFrameIndex != lastFrameIndex) {
if (lastFrameIndex == textMsgIndex && currentFrameIndex != textMsgIndex) {
graphics::MessageRenderer::clearMessageCache();
}
}
lastFrameIndex = currentFrameIndex;
}
menuHandler::handleMenuSwitch(dispdev);
// Show boot screen for first logo_timeout seconds, then switch to normal operation.
@@ -833,17 +828,17 @@ int32_t Screen::runOnce()
break;
case Cmd::ON_PRESS:
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
showFrame(FrameDirection::NEXT);
handleOnPress();
}
break;
case Cmd::SHOW_PREV_FRAME:
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
showFrame(FrameDirection::PREVIOUS);
handleShowPrevFrame();
}
break;
case Cmd::SHOW_NEXT_FRAME:
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
showFrame(FrameDirection::NEXT);
handleShowNextFrame();
}
break;
case Cmd::START_ALERT_FRAME: {
@@ -864,7 +859,6 @@ int32_t Screen::runOnce()
break;
case Cmd::STOP_ALERT_FRAME:
NotificationRenderer::pauseBanner = false;
break;
case Cmd::STOP_BOOT_SCREEN:
EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
@@ -1035,6 +1029,9 @@ void Screen::setFrames(FrameFocus focus)
}
#endif
// Declare this early so its available in FOCUS_PRESERVE block
bool willInsertTextMessage = shouldDrawMessage(&devicestate.rx_text_message);
if (!hiddenFrames.home) {
fsi.positions.home = numframes;
normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused;
@@ -1046,16 +1043,11 @@ void Screen::setFrames(FrameFocus focus)
indicatorIcons.push_back(icon_mail);
#ifndef USE_EINK
if (!hiddenFrames.nodelist_nodes) {
fsi.positions.nodelist_nodes = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicListScreen_Nodes;
if (!hiddenFrames.nodelist) {
fsi.positions.nodelist = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicNodeListScreen;
indicatorIcons.push_back(icon_nodes);
}
if (!hiddenFrames.nodelist_location) {
fsi.positions.nodelist_location = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicListScreen_Location;
indicatorIcons.push_back(icon_list);
}
#endif
// Show detailed node views only on E-Ink builds
@@ -1077,13 +1069,11 @@ void Screen::setFrames(FrameFocus focus)
}
#endif
#if HAS_GPS
#ifdef USE_EINK
if (!hiddenFrames.nodelist_bearings) {
fsi.positions.nodelist_bearings = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses;
indicatorIcons.push_back(icon_list);
}
#endif
if (!hiddenFrames.gps) {
fsi.positions.gps = numframes;
normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen;
@@ -1183,7 +1173,7 @@ void Screen::setFrames(FrameFocus focus)
}
fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE
this->frameCount = numframes; // Save frame count for use in custom overlay
this->frameCount = numframes; // Save frame count for use in custom overlay
LOG_DEBUG("Finished build frames. numframes: %d", numframes);
ui->setFrames(normalFrames, numframes);
@@ -1203,6 +1193,10 @@ void Screen::setFrames(FrameFocus focus)
case FOCUS_FAULT:
ui->switchToFrame(fsi.positions.fault);
break;
case FOCUS_TEXTMESSAGE:
hasUnreadMessage = false; // ✅ Clear when message is *viewed*
ui->switchToFrame(fsi.positions.textMessage);
break;
case FOCUS_MODULE:
// Whichever frame was marked by MeshModule::requestFocus(), if any
// If no module requested focus, will show the first frame instead
@@ -1245,11 +1239,8 @@ void Screen::setFrameImmediateDraw(FrameCallback *drawFrames)
void Screen::toggleFrameVisibility(const std::string &frameName)
{
#ifndef USE_EINK
if (frameName == "nodelist_nodes") {
hiddenFrames.nodelist_nodes = !hiddenFrames.nodelist_nodes;
}
if (frameName == "nodelist_location") {
hiddenFrames.nodelist_location = !hiddenFrames.nodelist_location;
if (frameName == "nodelist") {
hiddenFrames.nodelist = !hiddenFrames.nodelist;
}
#endif
#ifdef USE_EINK
@@ -1264,11 +1255,9 @@ void Screen::toggleFrameVisibility(const std::string &frameName)
}
#endif
#if HAS_GPS
#ifdef USE_EINK
if (frameName == "nodelist_bearings") {
hiddenFrames.nodelist_bearings = !hiddenFrames.nodelist_bearings;
}
#endif
if (frameName == "gps") {
hiddenFrames.gps = !hiddenFrames.gps;
}
@@ -1290,10 +1279,8 @@ void Screen::toggleFrameVisibility(const std::string &frameName)
bool Screen::isFrameHidden(const std::string &frameName) const
{
#ifndef USE_EINK
if (frameName == "nodelist_nodes")
return hiddenFrames.nodelist_nodes;
if (frameName == "nodelist_location")
return hiddenFrames.nodelist_location;
if (frameName == "nodelist")
return hiddenFrames.nodelist;
#endif
#ifdef USE_EINK
if (frameName == "nodelist_lastheard")
@@ -1304,10 +1291,8 @@ bool Screen::isFrameHidden(const std::string &frameName) const
return hiddenFrames.nodelist_distance;
#endif
#if HAS_GPS
#ifdef USE_EINK
if (frameName == "nodelist_bearings")
return hiddenFrames.nodelist_bearings;
#endif
if (frameName == "gps")
return hiddenFrames.gps;
#endif
@@ -1323,6 +1308,37 @@ bool Screen::isFrameHidden(const std::string &frameName) const
return false;
}
// Dismisses the currently displayed screen frame, if possible
// Relevant for text message, waypoint, others in future?
// Triggered with a CardKB keycombo
void Screen::hideCurrentFrame()
{
uint8_t currentFrame = ui->getUiState()->currentFrame;
bool dismissed = false;
if (currentFrame == framesetInfo.positions.textMessage && devicestate.has_rx_text_message) {
LOG_INFO("Hide Text Message");
devicestate.has_rx_text_message = false;
memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message));
} else if (currentFrame == framesetInfo.positions.waypoint && devicestate.has_rx_waypoint) {
LOG_DEBUG("Hide Waypoint");
devicestate.has_rx_waypoint = false;
hiddenFrames.waypoint = true;
dismissed = true;
} else if (currentFrame == framesetInfo.positions.wifi) {
LOG_DEBUG("Hide WiFi Screen");
hiddenFrames.wifi = true;
dismissed = true;
} else if (currentFrame == framesetInfo.positions.lora) {
LOG_INFO("Hide LoRa");
hiddenFrames.lora = true;
dismissed = true;
}
if (dismissed) {
setFrames(FOCUS_DEFAULT); // You could also use FOCUS_PRESERVE
}
}
void Screen::handleStartFirmwareUpdateScreen()
{
LOG_DEBUG("Show firmware screen");
@@ -1375,6 +1391,28 @@ void Screen::decreaseBrightness()
/* TO DO: add little popup in center of screen saying what brightness level it is set to*/
}
void Screen::setFunctionSymbol(std::string sym)
{
if (std::find(functionSymbol.begin(), functionSymbol.end(), sym) == functionSymbol.end()) {
functionSymbol.push_back(sym);
functionSymbolString = "";
for (auto symbol : functionSymbol) {
functionSymbolString = symbol + " " + functionSymbolString;
}
setFastFramerate();
}
}
void Screen::removeFunctionSymbol(std::string sym)
{
functionSymbol.erase(std::remove(functionSymbol.begin(), functionSymbol.end(), sym), functionSymbol.end());
functionSymbolString = "";
for (auto symbol : functionSymbol) {
functionSymbolString = symbol + " " + functionSymbolString;
}
setFastFramerate();
}
void Screen::handleOnPress()
{
// If screen was off, just wake it, otherwise advance to next frame
@@ -1386,17 +1424,23 @@ void Screen::handleOnPress()
}
}
void Screen::showFrame(FrameDirection direction)
void Screen::handleShowPrevFrame()
{
// Only advance frames when UI is stable
// If screen was off, just wake it, otherwise go back to previous frame
// If we are in a transition, the press must have bounced, drop it.
if (ui->getUiState()->frameState == FIXED) {
ui->previousFrame();
lastScreenTransition = millis();
setFastFramerate();
}
}
if (direction == FrameDirection::NEXT) {
ui->nextFrame();
} else {
ui->previousFrame();
}
void Screen::handleShowNextFrame()
{
// If screen was off, just wake it, otherwise advance to next frame
// If we are in a transition, the press must have bounced, drop it.
if (ui->getUiState()->frameState == FIXED) {
ui->nextFrame();
lastScreenTransition = millis();
setFastFramerate();
}
@@ -1422,6 +1466,7 @@ void Screen::setFastFramerate()
int Screen::handleStatusUpdate(const meshtastic::Status *arg)
{
// LOG_DEBUG("Screen got status update %d", arg->getStatusType());
switch (arg->getStatusType()) {
case STATUS_TYPE_NODE:
if (showingNormalScreen && nodeStatus->getLastNumTotal() != nodeStatus->getNumTotal()) {
@@ -1579,26 +1624,16 @@ int Screen::handleUIFrameEvent(const UIFrameEvent *event)
if (showingNormalScreen) {
// Regenerate the frameset, potentially honoring a module's internal requestFocus() call
if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET) {
if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET)
setFrames(FOCUS_MODULE);
}
// Regenerate the frameset, while attempting to maintain focus on the current frame
else if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET_BACKGROUND) {
// Regenerate the frameset, while Attempt to maintain focus on the current frame
else if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET_BACKGROUND)
setFrames(FOCUS_PRESERVE);
}
// Don't regenerate the frameset, just re-draw whatever is on screen ASAP
else if (event->action == UIFrameEvent::Action::REDRAW_ONLY) {
else if (event->action == UIFrameEvent::Action::REDRAW_ONLY)
setFastFramerate();
}
// Jump directly to the Text Message screen
else if (event->action == UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE) {
setFrames(FOCUS_PRESERVE); // preserve current frame ordering
ui->switchToFrame(framesetInfo.positions.textMessage);
setFastFramerate(); // force redraw ASAP
}
}
return 0;
@@ -1636,29 +1671,7 @@ int Screen::handleInputEvent(const InputEvent *event)
menuHandler::handleMenuSwitch(dispdev);
return 0;
}
// UP/DOWN in message screen scrolls through message threads
if (ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) {
if (event->inputEvent == INPUT_BROKER_UP) {
if (messageStore.getMessages().empty()) {
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
} else {
graphics::MessageRenderer::scrollUp();
setFastFramerate(); // match existing behavior
return 0;
}
}
if (event->inputEvent == INPUT_BROKER_DOWN) {
if (messageStore.getMessages().empty()) {
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
} else {
graphics::MessageRenderer::scrollDown();
setFastFramerate();
return 0;
}
}
}
// Use left or right input from a keyboard to move between frames,
// so long as a mesh module isn't using these events for some other purpose
if (showingNormalScreen) {
@@ -1672,39 +1685,16 @@ int Screen::handleInputEvent(const InputEvent *event)
// If no modules are using the input, move between frames
if (!inputIntercepted) {
#if defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2
bool handledEncoderScroll = false;
const bool isTextMessageFrame = (framesetInfo.positions.textMessage != 255 &&
this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage &&
!messageStore.getMessages().empty());
if (isTextMessageFrame) {
if (event->inputEvent == INPUT_BROKER_UP_LONG) {
graphics::MessageRenderer::nudgeScroll(-1);
handledEncoderScroll = true;
} else if (event->inputEvent == INPUT_BROKER_DOWN_LONG) {
graphics::MessageRenderer::nudgeScroll(1);
handledEncoderScroll = true;
}
}
if (handledEncoderScroll) {
setFastFramerate();
return 0;
}
#endif
if (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_ALT_PRESS) {
showFrame(FrameDirection::PREVIOUS);
showPrevFrame();
} else if (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS) {
showFrame(FrameDirection::NEXT);
showNextFrame();
} else if (event->inputEvent == INPUT_BROKER_UP_LONG) {
// Long press up button for fast frame switching
showPrevFrame();
} else if (event->inputEvent == INPUT_BROKER_DOWN_LONG) {
// Long press down button for fast frame switching
showNextFrame();
} else if ((event->inputEvent == INPUT_BROKER_UP || event->inputEvent == INPUT_BROKER_DOWN) &&
this->ui->getUiState()->currentFrame == framesetInfo.positions.home) {
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
} else if (event->inputEvent == INPUT_BROKER_SELECT) {
if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) {
menuHandler::homeBaseMenu();
@@ -1719,7 +1709,7 @@ int Screen::handleInputEvent(const InputEvent *event)
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) {
menuHandler::loraMenu();
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) {
if (!messageStore.getMessages().empty()) {
if (devicestate.rx_text_message.from) {
menuHandler::messageResponseMenu();
} else {
#if defined(M5STACK_UNITC6L)
@@ -1732,8 +1722,7 @@ int Screen::handleInputEvent(const InputEvent *event)
this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite &&
this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) {
menuHandler::favoriteBaseMenu();
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_nodes ||
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_location ||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist ||
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard ||
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal ||
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance ||
@@ -1744,7 +1733,7 @@ int Screen::handleInputEvent(const InputEvent *event)
menuHandler::wifiBaseMenu();
}
} else if (event->inputEvent == INPUT_BROKER_BACK) {
showFrame(FrameDirection::PREVIOUS);
showPrevFrame();
} else if (event->inputEvent == INPUT_BROKER_CANCEL) {
setOn(false);
}

View File

@@ -40,6 +40,7 @@ class Screen
FOCUS_DEFAULT, // No specific frame
FOCUS_PRESERVE, // Return to the previous frame
FOCUS_FAULT,
FOCUS_TEXTMESSAGE,
FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus
FOCUS_CLOCK,
FOCUS_SYSTEM,
@@ -54,6 +55,8 @@ class Screen
void startFirmwareUpdateScreen() {}
void increaseBrightness() {}
void decreaseBrightness() {}
void setFunctionSymbol(std::string) {}
void removeFunctionSymbol(std::string) {}
void startAlert(const char *) {}
void showSimpleBanner(const char *message, uint32_t durationMs = 0) {}
void showOverlayBanner(BannerOverlayOptions) {}
@@ -169,8 +172,6 @@ class Point
namespace graphics
{
enum class FrameDirection { NEXT, PREVIOUS };
// Forward declarations
class Screen;
@@ -210,6 +211,8 @@ class Screen : public concurrency::OSThread
CallbackObserver<Screen, const meshtastic::Status *>(this, &Screen::handleStatusUpdate);
CallbackObserver<Screen, const meshtastic::Status *> nodeStatusObserver =
CallbackObserver<Screen, const meshtastic::Status *>(this, &Screen::handleStatusUpdate);
CallbackObserver<Screen, const meshtastic_MeshPacket *> textMessageObserver =
CallbackObserver<Screen, const meshtastic_MeshPacket *>(this, &Screen::handleTextMessage);
CallbackObserver<Screen, const UIFrameEvent *> uiFrameEventObserver =
CallbackObserver<Screen, const UIFrameEvent *>(this, &Screen::handleUIFrameEvent); // Sent by Mesh Modules
CallbackObserver<Screen, const InputEvent *> inputObserver =
@@ -220,10 +223,6 @@ class Screen : public concurrency::OSThread
public:
OLEDDisplay *getDisplayDevice() { return dispdev; }
explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY);
// Screen dimension accessors
inline int getHeight() const { return displayHeight; }
inline int getWidth() const { return displayWidth; }
size_t frameCount = 0; // Total number of active frames
~Screen();
@@ -232,6 +231,7 @@ class Screen : public concurrency::OSThread
FOCUS_DEFAULT, // No specific frame
FOCUS_PRESERVE, // Return to the previous frame
FOCUS_FAULT,
FOCUS_TEXTMESSAGE,
FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus
FOCUS_CLOCK,
FOCUS_SYSTEM,
@@ -279,7 +279,6 @@ class Screen : public concurrency::OSThread
void onPress() { enqueueCmd(ScreenCmd{.cmd = Cmd::ON_PRESS}); }
void showPrevFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_PREV_FRAME}); }
void showNextFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_NEXT_FRAME}); }
void showFrame(FrameDirection direction);
// generic alert start
void startAlert(FrameCallback _alertFrame)
@@ -347,6 +346,9 @@ class Screen : public concurrency::OSThread
void increaseBrightness();
void decreaseBrightness();
void setFunctionSymbol(std::string sym);
void removeFunctionSymbol(std::string sym);
/// Stops showing the boot screen.
void stopBootScreen() { enqueueCmd(ScreenCmd{.cmd = Cmd::STOP_BOOT_SCREEN}); }
@@ -577,7 +579,7 @@ class Screen : public concurrency::OSThread
// Handle observer events
int handleStatusUpdate(const meshtastic::Status *arg);
int handleTextMessage(const meshtastic_MeshPacket *packet);
int handleTextMessage(const meshtastic_MeshPacket *arg);
int handleUIFrameEvent(const UIFrameEvent *arg);
int handleInputEvent(const InputEvent *arg);
int handleAdminMessage(AdminModule_ObserverData *arg);
@@ -588,6 +590,9 @@ class Screen : public concurrency::OSThread
/// Draws our SSL cert screen during boot (called from WebServer)
void setSSLFrames();
// Dismiss the currently focussed frame, if possible (e.g. text message, waypoint)
void hideCurrentFrame();
// Menu-driven Show / Hide Toggle
void toggleFrameVisibility(const std::string &frameName);
bool isFrameHidden(const std::string &frameName) const;
@@ -635,6 +640,8 @@ class Screen : public concurrency::OSThread
// Implementations of various commands, called from doTask().
void handleSetOn(bool on, FrameCallback einkScreensaver = NULL);
void handleOnPress();
void handleShowNextFrame();
void handleShowPrevFrame();
void handleStartFirmwareUpdateScreen();
// Info collected by setFrames method.
@@ -654,8 +661,7 @@ class Screen : public concurrency::OSThread
uint8_t gps = 255;
uint8_t home = 255;
uint8_t textMessage = 255;
uint8_t nodelist_nodes = 255;
uint8_t nodelist_location = 255;
uint8_t nodelist = 255;
uint8_t nodelist_lastheard = 255;
uint8_t nodelist_hopsignal = 255;
uint8_t nodelist_distance = 255;
@@ -678,8 +684,7 @@ class Screen : public concurrency::OSThread
bool home = false;
bool clock = false;
#ifndef USE_EINK
bool nodelist_nodes = false;
bool nodelist_location = false;
bool nodelist = false;
#endif
#ifdef USE_EINK
bool nodelist_lastheard = false;
@@ -687,9 +692,7 @@ class Screen : public concurrency::OSThread
bool nodelist_distance = false;
#endif
#if HAS_GPS
#ifdef USE_EINK
bool nodelist_bearings = false;
#endif
bool gps = false;
#endif
bool lora = false;

View File

@@ -32,19 +32,6 @@ void determineResolution(int16_t screenheight, int16_t screenwidth)
}
}
void decomposeTime(uint32_t rtc_sec, int &hour, int &minute, int &second)
{
hour = 0;
minute = 0;
second = 0;
if (rtc_sec == 0)
return;
uint32_t hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY;
hour = hms / SEC_PER_HOUR;
minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
second = hms % SEC_PER_MIN;
}
// === Shared External State ===
bool hasUnreadMessage = false;
bool isMuted = false;
@@ -213,8 +200,8 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
if (rtc_sec > 0) {
// === Build Time String ===
long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY;
int hour, minute, second;
graphics::decomposeTime(rtc_sec, hour, minute, second);
int hour = hms / SEC_PER_HOUR;
int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute);
// === Build Date String ===
@@ -427,12 +414,8 @@ void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y)
}
if (drawConnectionState) {
const int scale = isHighResolution ? 2 : 1;
display->setColor(BLACK);
display->fillRect(0, SCREEN_HEIGHT - (1 * scale) - (connection_icon_height * scale), (connection_icon_width * scale),
(connection_icon_height * scale) + (2 * scale));
display->setColor(WHITE);
if (isHighResolution) {
const int scale = 2;
const int bytesPerRow = (connection_icon_width + 7) / 8;
int iconX = 0;
int iconY = SCREEN_HEIGHT - (connection_icon_height * 2);

View File

@@ -45,8 +45,6 @@ extern bool isMuted;
extern bool isHighResolution;
void determineResolution(int16_t screenheight, int16_t screenwidth);
void decomposeTime(uint32_t rtc_sec, int &hour, int &minute, int &second);
// 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);

View File

@@ -354,6 +354,8 @@ void VirtualKeyboard::drawInputArea(OLEDDisplay *display, int16_t offsetX, int16
if (screenHeight <= 64) {
textY = boxY + (boxHeight - inputLineH) / 2;
} else {
const int innerLeft = boxX + 1;
const int innerRight = boxX + boxWidth - 2;
const int innerTop = boxY + 1;
const int innerBottom = boxY + boxHeight - 2;

View File

@@ -1,10 +1,15 @@
#include "configuration.h"
#if HAS_SCREEN
#include "ClockRenderer.h"
#include "NodeDB.h"
#include "UIRenderer.h"
#include "configuration.h"
#include "gps/GeoCoord.h"
#include "gps/RTC.h"
#include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/draw/UIRenderer.h"
#include "graphics/emotes.h"
#include "graphics/images.h"
#include "main.h"
@@ -18,31 +23,6 @@ namespace graphics
namespace ClockRenderer
{
// Segment bitmaps for numerals 0-9 stored in flash to save RAM.
// Each row is a digit, each column is a segment state (1 = on, 0 = off).
// Segment layout reference:
//
// ___1___
// 6 | | 2
// |_7___|
// 5 | | 3
// |___4_|
//
// Segment order: [1, 2, 3, 4, 5, 6, 7]
//
static const uint8_t PROGMEM digitSegments[10][7] = {
{1, 1, 1, 1, 1, 1, 0}, // 0
{0, 1, 1, 0, 0, 0, 0}, // 1
{1, 1, 0, 1, 1, 0, 1}, // 2
{1, 1, 1, 1, 0, 0, 1}, // 3
{0, 1, 1, 0, 0, 1, 1}, // 4
{1, 0, 1, 1, 0, 1, 1}, // 5
{1, 0, 1, 1, 1, 1, 1}, // 6
{1, 1, 1, 0, 0, 1, 0}, // 7
{1, 1, 1, 1, 1, 1, 1}, // 8
{1, 1, 1, 1, 0, 1, 1} // 9
};
void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale)
{
uint16_t segmentWidth = SEGMENT_WIDTH * scale;
@@ -50,7 +30,7 @@ void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale)
uint16_t cellHeight = (segmentWidth * 2) + (segmentHeight * 3) + 8;
uint16_t topAndBottomX = x + static_cast<uint16_t>(4 * scale);
uint16_t topAndBottomX = x + (4 * scale);
uint16_t quarterCellHeight = cellHeight / 4;
@@ -63,16 +43,34 @@ void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale)
void drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t number, float scale)
{
// Read 7-segment pattern for the digit from flash
uint8_t seg[7];
for (uint8_t i = 0; i < 7; i++) {
seg[i] = pgm_read_byte(&digitSegments[number][i]);
}
// the numbers 0-9, each expressed as an array of seven boolean (0|1) values encoding the on/off state of
// segment {innerIndex + 1}
// e.g., to display the numeral '0', segments 1-6 are on, and segment 7 is off.
uint8_t numbers[10][7] = {
{1, 1, 1, 1, 1, 1, 0}, // 0 Display segment key
{0, 1, 1, 0, 0, 0, 0}, // 1 1
{1, 1, 0, 1, 1, 0, 1}, // 2 ___
{1, 1, 1, 1, 0, 0, 1}, // 3 6 | | 2
{0, 1, 1, 0, 0, 1, 1}, // 4 |_7̲_|
{1, 0, 1, 1, 0, 1, 1}, // 5 5 | | 3
{1, 0, 1, 1, 1, 1, 1}, // 6 |___|
{1, 1, 1, 0, 0, 1, 0}, // 7
{1, 1, 1, 1, 1, 1, 1}, // 8 4
{1, 1, 1, 1, 0, 1, 1}, // 9
};
// the width and height of each segment's central rectangle:
// _____________________
// ⋰| (only this part, |⋱
// ⋰ | not including | ⋱
// ⋱ | the triangles | ⋰
// ⋱| on the ends) |⋰
// ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
uint16_t segmentWidth = SEGMENT_WIDTH * scale;
uint16_t segmentHeight = SEGMENT_HEIGHT * scale;
// Precompute segment positions
// segment x and y coordinates
uint16_t segmentOneX = x + segmentHeight + 2;
uint16_t segmentOneY = y;
@@ -94,21 +92,33 @@ void drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t n
uint16_t segmentSevenX = segmentOneX;
uint16_t segmentSevenY = segmentTwoY + segmentWidth + 2;
// Draw only the active segments
if (seg[0])
drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight);
if (seg[1])
drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight);
if (seg[2])
drawVerticalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight);
if (seg[3])
drawHorizontalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight);
if (seg[4])
drawVerticalSegment(display, segmentFiveX, segmentFiveY, segmentWidth, segmentHeight);
if (seg[5])
drawVerticalSegment(display, segmentSixX, segmentSixY, segmentWidth, segmentHeight);
if (seg[6])
drawHorizontalSegment(display, segmentSevenX, segmentSevenY, segmentWidth, segmentHeight);
if (numbers[number][0]) {
graphics::ClockRenderer::drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight);
}
if (numbers[number][1]) {
graphics::ClockRenderer::drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight);
}
if (numbers[number][2]) {
graphics::ClockRenderer::drawVerticalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight);
}
if (numbers[number][3]) {
graphics::ClockRenderer::drawHorizontalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight);
}
if (numbers[number][4]) {
graphics::ClockRenderer::drawVerticalSegment(display, segmentFiveX, segmentFiveY, segmentWidth, segmentHeight);
}
if (numbers[number][5]) {
graphics::ClockRenderer::drawVerticalSegment(display, segmentSixX, segmentSixY, segmentWidth, segmentHeight);
}
if (numbers[number][6]) {
graphics::ClockRenderer::drawHorizontalSegment(display, segmentSevenX, segmentSevenY, segmentWidth, segmentHeight);
}
}
void drawHorizontalSegment(OLEDDisplay *display, int x, int y, int width, int height)
@@ -137,6 +147,42 @@ void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int heig
display->fillTriangle(x, y + width, x + height - 1, y + width, x + halfHeight, y + width + halfHeight);
}
/*
void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode, float scale)
{
uint16_t segmentWidth = SEGMENT_WIDTH * scale;
uint16_t segmentHeight = SEGMENT_HEIGHT * scale;
if (digitalMode) {
uint16_t radius = (segmentWidth + (segmentHeight * 2) + 4) / 2;
uint16_t centerX = (x + segmentHeight + 2) + (radius / 2);
uint16_t centerY = (y + segmentHeight + 2) + (radius / 2);
display->drawCircle(centerX, centerY, radius);
display->drawCircle(centerX, centerY, radius + 1);
display->drawLine(centerX, centerY, centerX, centerY - radius + 3);
display->drawLine(centerX, centerY, centerX + radius - 3, centerY);
} else {
uint16_t segmentOneX = x + segmentHeight + 2;
uint16_t segmentOneY = y;
uint16_t segmentTwoX = segmentOneX + segmentWidth + 2;
uint16_t segmentTwoY = segmentOneY + segmentHeight + 2;
uint16_t segmentThreeX = segmentOneX;
uint16_t segmentThreeY = segmentTwoY + segmentWidth + 2;
uint16_t segmentFourX = x;
uint16_t segmentFourY = y + segmentHeight + 2;
drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight);
drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight);
drawHorizontalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight);
drawVerticalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight);
}
}
*/
// Draw a digital clock
void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
display->clear();
@@ -146,6 +192,7 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
const char *titleStr = "";
// === Header ===
graphics::drawCommonHeader(display, x, y, titleStr, true, true);
int line = 0;
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
char timeString[16];
@@ -221,9 +268,10 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
scaleInitialized = true;
}
// calculate hours:minutes string width
size_t len = strlen(timeString);
uint16_t timeStringWidth = len * 5;
// calculate hours:minutes string width
uint16_t timeStringWidth = len * 5; // base spacing between characters
for (size_t i = 0; i < len; i++) {
char character = timeString[i];
@@ -291,13 +339,19 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
const char *titleStr = "";
// === Header ===
graphics::drawCommonHeader(display, x, y, titleStr, true, true);
int line = 0;
// clock face center coordinates
int16_t centerX = display->getWidth() / 2;
int16_t centerY = display->getHeight() / 2;
// clock face radius
int16_t radius = (std::min(display->getWidth(), display->getHeight()) / 2) * 0.9;
int16_t radius = 0;
if (display->getHeight() < display->getWidth()) {
radius = (display->getHeight() / 2) * 0.9;
} else {
radius = (display->getWidth() / 2) * 0.9;
}
#ifdef T_WATCH_S3
radius = (display->getWidth() / 2) * 0.8;
#endif
@@ -312,8 +366,17 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
// tick mark outer y coordinate; (first nested circle)
int16_t tickMarkOuterNoonY = secondHandNoonY;
double secondsTickMarkInnerNoonY = noonY + (isHighResolution ? 8 : 4);
double hoursTickMarkInnerNoonY = noonY + (isHighResolution ? 16 : 6);
// seconds tick mark inner y coordinate; (second nested circle)
double secondsTickMarkInnerNoonY = (double)noonY + 4;
if (isHighResolution) {
secondsTickMarkInnerNoonY = (double)noonY + 8;
}
// hours tick mark inner y coordinate; (third nested circle)
double hoursTickMarkInnerNoonY = (double)noonY + 6;
if (isHighResolution) {
hoursTickMarkInnerNoonY = (double)noonY + 16;
}
// minute hand y coordinate
int16_t minuteHandNoonY = secondsTickMarkInnerNoonY + 4;
@@ -333,11 +396,17 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
if (rtc_sec > 0) {
int hour, minute, second;
decomposeTime(rtc_sec, hour, minute, second);
long hms = rtc_sec % SEC_PER_DAY;
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
// Tear apart hms into h:m:s
int hour = hms / SEC_PER_HOUR;
int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
int second = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN
bool isPM = hour >= 12;
if (config.display.use_12h_clock) {
bool isPM = hour >= 12;
isPM = hour >= 12;
display->setFont(FONT_SMALL);
int yOffset = isHighResolution ? 1 : 0;
#ifdef USE_EINK

View File

@@ -301,8 +301,9 @@ void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
// Tear apart hms into h:m:s
int hour, min, sec;
graphics::decomposeTime(rtc_sec, hour, min, sec);
int hour = hms / SEC_PER_HOUR;
int min = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN
char timebuf[12];
@@ -706,22 +707,10 @@ void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int1
int iconY = (SCREEN_HEIGHT - chirpy_height) / 2;
int textX_offset = 10;
if (isHighResolution) {
iconX = SCREEN_WIDTH - chirpy_width_hirez - (chirpy_width_hirez / 3);
iconY = (SCREEN_HEIGHT - chirpy_height_hirez) / 2;
textX_offset = textX_offset * 4;
const int scale = 2;
const int bytesPerRow = (chirpy_width + 7) / 8;
for (int yy = 0; yy < chirpy_height; ++yy) {
iconX = SCREEN_WIDTH - (chirpy_width * 2) - ((chirpy_width * 2) / 3);
iconY = (SCREEN_HEIGHT - (chirpy_height * 2)) / 2;
const uint8_t *rowPtr = chirpy + yy * bytesPerRow;
for (int xx = 0; xx < chirpy_width; ++xx) {
const uint8_t byteVal = pgm_read_byte(rowPtr + (xx >> 3));
const uint8_t bitMask = 1U << (xx & 7); // XBM is LSB-first
if (byteVal & bitMask) {
display->fillRect(iconX + xx * scale, iconY + yy * scale, scale, scale);
}
}
}
display->drawXbm(iconX, iconY, chirpy_width_hirez, chirpy_height_hirez, chirpy_hirez);
} else {
display->drawXbm(iconX, iconY, chirpy_width, chirpy_height, chirpy);
}

View File

@@ -11,6 +11,7 @@
#include "graphics/draw/CompassRenderer.h"
#include "graphics/draw/DebugRenderer.h"
#include "graphics/draw/NodeListRenderer.h"
#include "graphics/draw/ScreenRenderer.h"
#include "graphics/draw/UIRenderer.h"
namespace graphics
@@ -29,6 +30,8 @@ using namespace ClockRenderer;
using namespace CompassRenderer;
using namespace DebugRenderer;
using namespace NodeListRenderer;
using namespace ScreenRenderer;
using namespace UIRenderer;
} // namespace DrawRenderers

View File

@@ -1,17 +1,14 @@
#include "configuration.h"
#if HAS_SCREEN
#include "ClockRenderer.h"
#include "Default.h"
#include "GPS.h"
#include "MenuHandler.h"
#include "MeshRadio.h"
#include "MeshService.h"
#include "MessageStore.h"
#include "NodeDB.h"
#include "buzz.h"
#include "graphics/Screen.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/draw/MessageRenderer.h"
#include "graphics/draw/UIRenderer.h"
#include "input/RotaryEncoderInterruptImpl1.h"
#include "input/UpDownInterruptImpl1.h"
@@ -410,35 +407,27 @@ void menuHandler::clockMenu()
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::messageResponseMenu()
{
enum optionsNumbers { Back = 0, ViewMode, DeleteAll, DeleteOldest, ReplyMenu, Aloud, enumEnd };
enum optionsNumbers { Back = 0, Dismiss = 1, Preset = 2, Freetext = 3, Aloud = 4, enumEnd = 5 };
#if defined(M5STACK_UNITC6L)
static const char *optionsArray[enumEnd] = {"Back", "Dismiss", "Reply Preset"};
#else
static const char *optionsArray[enumEnd] = {"Back", "Dismiss", "Reply via Preset"};
#endif
static int optionsEnumArray[enumEnd] = {Back, Dismiss, Preset};
int options = 3;
static const char *optionsArray[enumEnd];
static int optionsEnumArray[enumEnd];
int options = 0;
auto mode = graphics::MessageRenderer::getThreadMode();
optionsArray[options] = "Back";
optionsEnumArray[options++] = Back;
// New Reply submenu (replaces Preset and Freetext directly in this menu)
optionsArray[options] = "Reply";
optionsEnumArray[options++] = ReplyMenu;
optionsArray[options] = "View Chats";
optionsEnumArray[options++] = ViewMode;
// Delete submenu
optionsArray[options] = "Delete";
optionsEnumArray[options++] = 900;
if (kb_found) {
optionsArray[options] = "Reply via Freetext";
optionsEnumArray[options++] = Freetext;
}
#ifdef HAS_I2S
optionsArray[options] = "Read Aloud";
optionsEnumArray[options++] = Aloud;
#endif
BannerOverlayOptions bannerOptions;
#if defined(M5STACK_UNITC6L)
bannerOptions.message = "Message";
@@ -449,376 +438,29 @@ void menuHandler::messageResponseMenu()
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.optionsCount = options;
bannerOptions.bannerCallback = [](int selected) -> void {
LOG_DEBUG("messageResponseMenu: selected %d", selected);
auto mode = graphics::MessageRenderer::getThreadMode();
int ch = graphics::MessageRenderer::getThreadChannel();
uint32_t peer = graphics::MessageRenderer::getThreadPeer();
LOG_DEBUG("[ReplyCtx] mode=%d ch=%d peer=0x%08x", (int)mode, ch, (unsigned int)peer);
if (selected == ViewMode) {
menuHandler::menuQueue = menuHandler::message_viewmode_menu;
screen->runNow();
// Reply submenu
} else if (selected == ReplyMenu) {
menuHandler::menuQueue = menuHandler::reply_menu;
screen->runNow();
// Delete submenu
} else if (selected == 900) {
menuHandler::menuQueue = menuHandler::delete_messages_menu;
screen->runNow();
// Delete oldest FIRST (only change)
} else if (selected == DeleteOldest) {
auto mode = graphics::MessageRenderer::getThreadMode();
int ch = graphics::MessageRenderer::getThreadChannel();
uint32_t peer = graphics::MessageRenderer::getThreadPeer();
if (mode == graphics::MessageRenderer::ThreadMode::ALL) {
// Global oldest
messageStore.deleteOldestMessage();
} else if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
// Oldest in current channel
messageStore.deleteOldestMessageInChannel(ch);
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
// Oldest in current DM
messageStore.deleteOldestMessageWithPeer(peer);
if (selected == Dismiss) {
screen->hideCurrentFrame();
} else if (selected == Preset) {
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
} else {
cannedMessageModule->LaunchWithDestination(devicestate.rx_text_message.from);
}
// Delete all messages
} else if (selected == DeleteAll) {
messageStore.clearAllMessages();
graphics::MessageRenderer::clearThreadRegistries();
graphics::MessageRenderer::clearMessageCache();
} else if (selected == Freetext) {
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
} else {
cannedMessageModule->LaunchFreetextWithDestination(devicestate.rx_text_message.from);
}
}
#ifdef HAS_I2S
} else if (selected == Aloud) {
else if (selected == Aloud) {
const meshtastic_MeshPacket &mp = devicestate.rx_text_message;
const char *msg = reinterpret_cast<const char *>(mp.decoded.payload.bytes);
audioThread->readAloud(msg);
}
#endif
}
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::replyMenu()
{
enum replyOptions { Back = 0, ReplyPreset, ReplyFreetext, enumEnd };
static const char *optionsArray[enumEnd];
static int optionsEnumArray[enumEnd];
int options = 0;
// Back
optionsArray[options] = "Back";
optionsEnumArray[options++] = Back;
// Preset reply
optionsArray[options] = "With Preset";
optionsEnumArray[options++] = ReplyPreset;
// Freetext reply (only when keyboard exists)
if (kb_found) {
optionsArray[options] = "With Freetext";
optionsEnumArray[options++] = ReplyFreetext;
}
BannerOverlayOptions bannerOptions;
// Dynamic title based on thread mode
auto mode = graphics::MessageRenderer::getThreadMode();
if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
bannerOptions.message = "Reply to Channel";
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
bannerOptions.message = "Reply to DM";
} else {
// View All
bannerOptions.message = "Reply to Last Msg";
}
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.optionsCount = options;
bannerOptions.InitialSelected = 1;
bannerOptions.bannerCallback = [](int selected) -> void {
auto mode = graphics::MessageRenderer::getThreadMode();
int ch = graphics::MessageRenderer::getThreadChannel();
uint32_t peer = graphics::MessageRenderer::getThreadPeer();
if (selected == Back) {
menuHandler::menuQueue = menuHandler::message_response_menu;
screen->runNow();
return;
}
// Preset reply
if (selected == ReplyPreset) {
if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, ch);
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
cannedMessageModule->LaunchWithDestination(peer);
} else {
// Fallback for last received message
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
} else {
cannedMessageModule->LaunchWithDestination(devicestate.rx_text_message.from);
}
}
return;
}
// Freetext reply
if (selected == ReplyFreetext) {
if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, ch);
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
cannedMessageModule->LaunchFreetextWithDestination(peer);
} else {
// Fallback for last received message
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
} else {
cannedMessageModule->LaunchFreetextWithDestination(devicestate.rx_text_message.from);
}
}
return;
}
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::deleteMessagesMenu()
{
enum optionsNumbers { Back = 0, DeleteOldest, DeleteThis, DeleteAll, enumEnd };
static const char *optionsArray[enumEnd];
static int optionsEnumArray[enumEnd];
int options = 0;
auto mode = graphics::MessageRenderer::getThreadMode();
optionsArray[options] = "Back";
optionsEnumArray[options++] = Back;
optionsArray[options] = "Delete Oldest";
optionsEnumArray[options++] = DeleteOldest;
// If viewing ALL chats → hide “Delete This Chat”
if (mode != graphics::MessageRenderer::ThreadMode::ALL) {
optionsArray[options] = "Delete This Chat";
optionsEnumArray[options++] = DeleteThis;
}
#if defined(M5STACK_UNITC6L)
optionsArray[options] = "Delete All";
#else
optionsArray[options] = "Delete All Chats";
#endif
optionsEnumArray[options++] = DeleteAll;
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Delete Messages";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.optionsCount = options;
bannerOptions.bannerCallback = [mode](int selected) -> void {
int ch = graphics::MessageRenderer::getThreadChannel();
uint32_t peer = graphics::MessageRenderer::getThreadPeer();
if (selected == Back) {
menuHandler::menuQueue = menuHandler::message_response_menu;
screen->runNow();
return;
}
if (selected == DeleteAll) {
LOG_INFO("Deleting all messages");
messageStore.clearAllMessages();
graphics::MessageRenderer::clearThreadRegistries();
graphics::MessageRenderer::clearMessageCache();
return;
}
if (selected == DeleteOldest) {
LOG_INFO("Deleting oldest message");
if (mode == graphics::MessageRenderer::ThreadMode::ALL) {
messageStore.deleteOldestMessage();
} else if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
messageStore.deleteOldestMessageInChannel(ch);
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
messageStore.deleteOldestMessageWithPeer(peer);
}
return;
}
// This only appears in non-ALL modes
if (selected == DeleteThis) {
LOG_INFO("Deleting all messages in this thread");
if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
messageStore.deleteAllMessagesInChannel(ch);
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
messageStore.deleteAllMessagesWithPeer(peer);
}
return;
}
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::messageViewModeMenu()
{
auto encodeChannelId = [](int ch) -> int { return 100 + ch; };
auto isChannelSel = [](int id) -> bool { return id >= 100 && id < 200; };
static std::vector<std::string> labels;
static std::vector<int> ids;
static std::vector<uint32_t> idToPeer; // DM lookup
labels.clear();
ids.clear();
idToPeer.clear();
labels.push_back("Back");
ids.push_back(-1);
labels.push_back("View All Chats");
ids.push_back(-2);
// Channels with messages
for (int ch = 0; ch < 8; ++ch) {
auto msgs = messageStore.getChannelMessages((uint8_t)ch);
if (!msgs.empty()) {
char buf[40];
const char *cname = channels.getName(ch);
snprintf(buf, sizeof(buf), cname && cname[0] ? "#%s" : "#Ch%d", cname ? cname : "", ch);
labels.push_back(buf);
ids.push_back(encodeChannelId(ch));
LOG_DEBUG("messageViewModeMenu: Added live channel %s (id=%d)", buf, encodeChannelId(ch));
}
}
// Registry channels
for (int ch : graphics::MessageRenderer::getSeenChannels()) {
if (ch < 0 || ch >= 8)
continue;
auto msgs = messageStore.getChannelMessages((uint8_t)ch);
if (msgs.empty())
continue;
int enc = encodeChannelId(ch);
if (std::find(ids.begin(), ids.end(), enc) == ids.end()) {
char buf[40];
const char *cname = channels.getName(ch);
snprintf(buf, sizeof(buf), cname && cname[0] ? "#%s" : "#Ch%d", cname ? cname : "", ch);
labels.push_back(buf);
ids.push_back(enc);
LOG_DEBUG("messageViewModeMenu: Added registry channel %s (id=%d)", buf, enc);
}
}
// Gather unique peers
auto dms = messageStore.getDirectMessages();
std::vector<uint32_t> uniquePeers;
for (auto &m : dms) {
uint32_t peer = (m.sender == nodeDB->getNodeNum()) ? m.dest : m.sender;
if (peer != nodeDB->getNodeNum() && std::find(uniquePeers.begin(), uniquePeers.end(), peer) == uniquePeers.end())
uniquePeers.push_back(peer);
}
for (uint32_t peer : graphics::MessageRenderer::getSeenPeers()) {
if (peer != nodeDB->getNodeNum() && std::find(uniquePeers.begin(), uniquePeers.end(), peer) == uniquePeers.end())
uniquePeers.push_back(peer);
}
std::sort(uniquePeers.begin(), uniquePeers.end());
// Encode peers
for (size_t i = 0; i < uniquePeers.size(); ++i) {
uint32_t peer = uniquePeers[i];
auto node = nodeDB->getMeshNode(peer);
std::string name;
if (node && node->has_user)
name = sanitizeString(node->user.long_name).substr(0, 15);
else {
char buf[20];
snprintf(buf, sizeof(buf), "Node %08X", peer);
name = buf;
}
labels.push_back("@" + name);
int encPeer = 1000 + (int)idToPeer.size();
ids.push_back(encPeer);
idToPeer.push_back(peer);
LOG_DEBUG("messageViewModeMenu: Added DM %s peer=0x%08x id=%d", name.c_str(), (unsigned int)peer, encPeer);
}
// Active ID
int activeId = -2;
auto mode = graphics::MessageRenderer::getThreadMode();
if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL)
activeId = encodeChannelId(graphics::MessageRenderer::getThreadChannel());
else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
uint32_t cur = graphics::MessageRenderer::getThreadPeer();
for (size_t i = 0; i < idToPeer.size(); ++i)
if (idToPeer[i] == cur) {
activeId = 1000 + (int)i;
break;
}
}
LOG_DEBUG("messageViewModeMenu: Active thread id=%d", activeId);
// Build banner
static std::vector<const char *> options;
static std::vector<int> optionIds;
options.clear();
optionIds.clear();
int initialIndex = 0;
for (size_t i = 0; i < labels.size(); i++) {
options.push_back(labels[i].c_str());
optionIds.push_back(ids[i]);
if (ids[i] == activeId)
initialIndex = (int)i;
}
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Select Conversation";
bannerOptions.optionsArrayPtr = options.data();
bannerOptions.optionsEnumPtr = optionIds.data();
bannerOptions.optionsCount = options.size();
bannerOptions.InitialSelected = initialIndex;
bannerOptions.bannerCallback = [=](int selected) -> void {
LOG_DEBUG("messageViewModeMenu: selected=%d", selected);
if (selected == -1) {
menuHandler::menuQueue = menuHandler::message_response_menu;
screen->runNow();
} else if (selected == -2) {
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::ALL);
} else if (isChannelSel(selected)) {
int ch = selected - 100;
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::CHANNEL, ch);
} else if (selected >= 1000) {
int idx = selected - 1000;
if (idx >= 0 && (size_t)idx < idToPeer.size()) {
uint32_t peer = idToPeer[idx];
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::DIRECT, -1, peer);
}
}
};
screen->showOverlayBanner(bannerOptions);
}
@@ -844,6 +486,16 @@ void menuHandler::homeBaseMenu()
optionsArray[options] = "Send Node Info";
}
optionsEnumArray[options++] = Position;
#if defined(M5STACK_UNITC6L)
optionsArray[options] = "New Preset";
#else
optionsArray[options] = "New Preset Msg";
#endif
optionsEnumArray[options++] = Preset;
if (kb_found) {
optionsArray[options] = "New Freetext Msg";
optionsEnumArray[options++] = Freetext;
}
BannerOverlayOptions bannerOptions;
#if defined(M5STACK_UNITC6L)
@@ -999,37 +651,19 @@ void menuHandler::systemBaseMenu()
void menuHandler::favoriteBaseMenu()
{
enum optionsNumbers { Back, Preset, Freetext, GoToChat, Remove, TraceRoute, enumEnd };
static const char *optionsArray[enumEnd] = {"Back"};
static int optionsEnumArray[enumEnd] = {Back};
int options = 1;
// Only show "View Conversation" if a message exists with this node
uint32_t peer = graphics::UIRenderer::currentFavoriteNodeNum;
bool hasConversation = false;
for (const auto &m : messageStore.getMessages()) {
if ((m.sender == peer || m.dest == peer)) {
hasConversation = true;
break;
}
}
if (hasConversation) {
optionsArray[options] = "Go To Chat";
optionsEnumArray[options++] = GoToChat;
}
enum optionsNumbers { Back, Preset, Freetext, Remove, TraceRoute, enumEnd };
#if defined(M5STACK_UNITC6L)
optionsArray[options] = "New Preset";
static const char *optionsArray[enumEnd] = {"Back", "New Preset"};
#else
optionsArray[options] = "New Preset Msg";
static const char *optionsArray[enumEnd] = {"Back", "New Preset Msg"};
#endif
optionsEnumArray[options++] = Preset;
static int optionsEnumArray[enumEnd] = {Back, Preset};
int options = 2;
if (kb_found) {
optionsArray[options] = "New Freetext Msg";
optionsEnumArray[options++] = Freetext;
}
#if !defined(M5STACK_UNITC6L)
optionsArray[options] = "Trace Route";
optionsEnumArray[options++] = TraceRoute;
@@ -1051,17 +685,6 @@ void menuHandler::favoriteBaseMenu()
cannedMessageModule->LaunchWithDestination(graphics::UIRenderer::currentFavoriteNodeNum);
} else if (selected == Freetext) {
cannedMessageModule->LaunchFreetextWithDestination(graphics::UIRenderer::currentFavoriteNodeNum);
}
// Handle new Go To Thread action
else if (selected == GoToChat) {
// Switch thread to direct conversation with this node
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::DIRECT, -1,
graphics::UIRenderer::currentFavoriteNodeNum);
// Manually create and send a UIFrameEvent to trigger the jump
UIFrameEvent evt;
evt.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE;
screen->handleUIFrameEvent(&evt);
} else if (selected == Remove) {
menuHandler::menuQueue = menuHandler::remove_favorite;
screen->runNow();
@@ -1111,33 +734,20 @@ void menuHandler::positionBaseMenu()
void menuHandler::nodeListMenu()
{
enum optionsNumbers { Back, Favorite, TraceRoute, Verify, Reset, NodeNameLength, enumEnd };
static const char *optionsArray[enumEnd] = {"Back"};
static int optionsEnumArray[enumEnd] = {Back};
int options = 1;
optionsArray[options] = "Add Favorite";
optionsEnumArray[options++] = Favorite;
optionsArray[options] = "Trace Route";
optionsEnumArray[options++] = TraceRoute;
#if !defined(M5STACK_UNITC6L)
optionsArray[options] = "Key Verification";
optionsEnumArray[options++] = Verify;
enum optionsNumbers { Back, Favorite, TraceRoute, Verify, Reset, enumEnd };
#if defined(M5STACK_UNITC6L)
static const char *optionsArray[] = {"Back", "Add Favorite", "Reset Node"};
#else
static const char *optionsArray[] = {"Back", "Add Favorite", "Trace Route", "Key Verification", "Reset NodeDB"};
#endif
#if defined(T_DECK) || defined(T_LORA_PAGER) || defined(HACKADAY_COMMUNICATOR)
optionsArray[options] = "Show Long/Short Name";
optionsEnumArray[options++] = NodeNameLength;
#endif
optionsArray[options] = "Reset NodeDB";
optionsEnumArray[options++] = Reset;
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Node Action";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = options;
bannerOptions.optionsEnumPtr = optionsEnumArray;
#if defined(M5STACK_UNITC6L)
bannerOptions.optionsCount = 3;
#else
bannerOptions.optionsCount = 5;
#endif
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Favorite) {
menuQueue = add_favorite;
@@ -1151,9 +761,6 @@ void menuHandler::nodeListMenu()
} else if (selected == TraceRoute) {
menuQueue = trace_route_menu;
screen->runNow();
} else if (selected == NodeNameLength) {
menuHandler::menuQueue = menuHandler::node_name_length_menu;
screen->runNow();
}
};
screen->showOverlayBanner(bannerOptions);
@@ -1563,7 +1170,6 @@ void menuHandler::rebootMenu()
if (selected == 1) {
IF_SCREEN(screen->showSimpleBanner("Rebooting...", 0));
nodeDB->saveToDisk();
messageStore.saveToFlash();
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
} else {
menuQueue = power_menu;
@@ -1768,11 +1374,16 @@ void menuHandler::screenOptionsMenu()
hasSupportBrightness = false;
#endif
enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits };
enum optionsNumbers { Back, NodeNameLength, Brightness, ScreenColor, FrameToggles, DisplayUnits };
static const char *optionsArray[5] = {"Back"};
static int optionsEnumArray[5] = {Back};
int options = 1;
#if defined(T_DECK) || defined(T_LORA_PAGER) || defined(HACKADAY_COMMUNICATOR)
optionsArray[options] = "Show Long/Short Name";
optionsEnumArray[options++] = NodeNameLength;
#endif
// Only show brightness for B&W displays
if (hasSupportBrightness) {
optionsArray[options] = "Brightness";
@@ -1804,6 +1415,9 @@ void menuHandler::screenOptionsMenu()
} else if (selected == ScreenColor) {
menuHandler::menuQueue = menuHandler::tftcolormenupicker;
screen->runNow();
} else if (selected == NodeNameLength) {
menuHandler::menuQueue = menuHandler::node_name_length_menu;
screen->runNow();
} else if (selected == FrameToggles) {
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
@@ -1899,8 +1513,7 @@ void menuHandler::FrameToggles_menu()
{
enum optionsNumbers {
Finish,
nodelist_nodes,
nodelist_location,
nodelist,
nodelist_lastheard,
nodelist_hopsignal,
nodelist_distance,
@@ -1921,25 +1534,20 @@ void menuHandler::FrameToggles_menu()
static int lastSelectedIndex = 0;
#ifndef USE_EINK
optionsArray[options] = screen->isFrameHidden("nodelist_nodes") ? "Show Node List" : "Hide Node List";
optionsEnumArray[options++] = nodelist_nodes;
#else
optionsArray[options] = screen->isFrameHidden("nodelist") ? "Show Node List" : "Hide Node List";
optionsEnumArray[options++] = nodelist;
#endif
#ifdef USE_EINK
optionsArray[options] = screen->isFrameHidden("nodelist_lastheard") ? "Show NL - Last Heard" : "Hide NL - Last Heard";
optionsEnumArray[options++] = nodelist_lastheard;
optionsArray[options] = screen->isFrameHidden("nodelist_hopsignal") ? "Show NL - Hops/Signal" : "Hide NL - Hops/Signal";
optionsEnumArray[options++] = nodelist_hopsignal;
#endif
#if HAS_GPS
#ifndef USE_EINK
optionsArray[options] = screen->isFrameHidden("nodelist_location") ? "Show Node Location List" : "Hide Node Location List";
optionsEnumArray[options++] = nodelist_location;
#else
optionsArray[options] = screen->isFrameHidden("nodelist_distance") ? "Show NL - Distance" : "Hide NL - Distance";
optionsEnumArray[options++] = nodelist_distance;
optionsArray[options] = screen->isFrameHidden("nodelist_bearings") ? "Show NL - Bearings" : "Hide NL - Bearings";
optionsEnumArray[options++] = nodelist_bearings;
#endif
#if HAS_GPS
optionsArray[options] = screen->isFrameHidden("nodelist_bearings") ? "Show Bearings" : "Hide Bearings";
optionsEnumArray[options++] = nodelist_bearings;
optionsArray[options] = screen->isFrameHidden("gps") ? "Show Position" : "Hide Position";
optionsEnumArray[options++] = gps;
@@ -1978,12 +1586,8 @@ void menuHandler::FrameToggles_menu()
if (selected == Finish) {
screen->setFrames(Screen::FOCUS_DEFAULT);
} else if (selected == nodelist_nodes) {
screen->toggleFrameVisibility("nodelist_nodes");
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == nodelist_location) {
screen->toggleFrameVisibility("nodelist_location");
} else if (selected == nodelist) {
screen->toggleFrameVisibility("nodelist");
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == nodelist_lastheard) {
@@ -2179,18 +1783,6 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
case throttle_message:
screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000);
break;
case message_response_menu:
messageResponseMenu();
break;
case reply_menu:
replyMenu();
break;
case delete_messages_menu:
deleteMessagesMenu();
break;
case message_viewmode_menu:
messageViewModeMenu();
break;
}
menuQueue = menu_none;
}
@@ -2202,4 +1794,4 @@ void menuHandler::saveUIConfig()
} // namespace graphics
#endif
#endif

View File

@@ -43,10 +43,6 @@ class menuHandler
key_verification_final_prompt,
trace_route_menu,
throttle_message,
message_response_menu,
message_viewmode_menu,
reply_menu,
delete_messages_menu,
node_name_length_menu,
FrameToggles,
DisplayUnits
@@ -65,9 +61,6 @@ class menuHandler
static void TwelveHourPicker();
static void ClockFacePicker();
static void messageResponseMenu();
static void messageViewModeMenu();
static void replyMenu();
static void deleteMessagesMenu();
static void homeBaseMenu();
static void textMessageBaseMenu();
static void systemBaseMenu();
@@ -107,4 +100,4 @@ class menuHandler
};
} // namespace graphics
#endif
#endif

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,7 @@
#pragma once
#include "MessageStore.h" // for StoredMessage
#if HAS_SCREEN
#include "OLEDDisplay.h"
#include "OLEDDisplayUi.h"
#include "graphics/emotes.h"
#include "mesh/generated/meshtastic/mesh.pb.h" // for meshtastic_MeshPacket
#include <cstdint>
#include <string>
#include <vector>
@@ -14,27 +10,6 @@ namespace graphics
namespace MessageRenderer
{
// Thread filter modes
enum class ThreadMode { ALL, CHANNEL, DIRECT };
// Setter for switching thread mode
void setThreadMode(ThreadMode mode, int channel = -1, uint32_t peer = 0);
// Getter for current mode
ThreadMode getThreadMode();
// Getter for current channel (valid if mode == CHANNEL)
int getThreadChannel();
// Getter for current peer (valid if mode == DIRECT)
uint32_t getThreadPeer();
// Registry accessors for menuHandler
const std::vector<int> &getSeenChannels();
const std::vector<uint32_t> &getSeenPeers();
void clearThreadRegistries();
// Text and emote rendering
void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount);
@@ -45,27 +20,11 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
std::vector<std::string> generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth);
// Function to calculate heights for each line
std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, const Emote *emotes,
const std::vector<bool> &isHeaderVec);
std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, const Emote *emotes);
// Reset scroll state when new messages arrive
void resetScrollState();
// Manual scroll control for encoder-style inputs
void nudgeScroll(int8_t direction);
// Helper to auto-select the correct thread mode from a message
void setThreadFor(const StoredMessage &sm, const meshtastic_MeshPacket &packet);
// Handles a new incoming/outgoing message: banner, wake, thread select, scroll reset
void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const meshtastic_MeshPacket &packet);
// Clear Message Line Cache from Message Renderer
void clearMessageCache();
void scrollUp();
void scrollDown();
// Function to render the message content
void renderMessageContent(OLEDDisplay *display, const std::vector<std::string> &lines, const std::vector<int> &rowHeights, int x,
int yOffset, int scrollBottom, const Emote *emotes, int numEmotes, bool isInverted, bool isBold);
} // namespace MessageRenderer
} // namespace graphics
#endif

View File

@@ -46,8 +46,7 @@ void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *
}
// Static variables for dynamic cycling
static ListMode_Node currentMode_Nodes = MODE_LAST_HEARD;
static ListMode_Location currentMode_Location = MODE_DISTANCE;
static NodeListMode currentMode = MODE_LAST_HEARD;
static int scrollIndex = 0;
// =============================
@@ -56,63 +55,60 @@ static int scrollIndex = 0;
const char *getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node)
{
static char nodeName[25]; // single static buffer we return
nodeName[0] = '\0';
auto writeFallbackId = [&] {
std::snprintf(nodeName, sizeof(nodeName), "(%04X)", static_cast<uint16_t>(node ? (node->num & 0xFFFF) : 0));
};
// 1) Choose target candidate (long vs short) only if present
const char *raw = nullptr;
if (node && node->has_user) {
raw = config.display.use_long_node_name ? node->user.long_name : node->user.short_name;
}
// 2) Sanitize (empty if raw is null/empty)
std::string s = (raw && *raw) ? sanitizeString(raw) : std::string{};
// 3) Fallback if sanitize yields empty; otherwise copy safely (truncate if needed)
if (s.empty() || s == "¿" || s.find_first_not_of("¿") == std::string::npos) {
writeFallbackId();
const char *name = NULL;
static char nodeName[16] = "?";
if (config.display.use_long_node_name == true) {
if (node->has_user && strlen(node->user.long_name) > 0) {
name = node->user.long_name;
} else {
snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF));
}
} else {
// %.*s ensures null-termination and safe truncation to buffer size - 1
std::snprintf(nodeName, sizeof(nodeName), "%.*s", static_cast<int>(sizeof(nodeName) - 1), s.c_str());
if (node->has_user && strlen(node->user.short_name) > 0) {
name = node->user.short_name;
} else {
snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF));
}
}
// 4) Width-based truncation + ellipsis (long-name mode only)
if (config.display.use_long_node_name && display) {
// Use sanitizeString() function and copy directly into nodeName
std::string sanitized_name = sanitizeString(name ? name : "");
if (!sanitized_name.empty()) {
strncpy(nodeName, sanitized_name.c_str(), sizeof(nodeName) - 1);
nodeName[sizeof(nodeName) - 1] = '\0';
} else {
snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF));
}
if (config.display.use_long_node_name == true) {
int availWidth = (SCREEN_WIDTH / 2) - 65;
if (availWidth < 0)
availWidth = 0;
const size_t beforeLen = std::strlen(nodeName);
// Trim from the end until it fits or is empty
size_t len = beforeLen;
while (len && display->getStringWidth(nodeName) > availWidth) {
nodeName[--len] = '\0';
size_t origLen = strlen(nodeName);
while (nodeName[0] && display->getStringWidth(nodeName) > availWidth) {
nodeName[strlen(nodeName) - 1] = '\0';
}
// If truncated, append "..." (respect buffer size)
if (len < beforeLen) {
// Make sure there's room for "..." and '\0'
const size_t capForText = sizeof(nodeName) - 1; // leaving space for '\0'
const size_t needed = 3; // "..."
if (len > capForText - needed) {
len = capForText - needed;
nodeName[len] = '\0';
// If we actually truncated, append "..." (ensure space remains in buffer)
if (strlen(nodeName) < origLen) {
size_t len = strlen(nodeName);
size_t maxLen = sizeof(nodeName) - 4; // 3 for "..." and 1 for '\0'
if (len > maxLen) {
nodeName[maxLen] = '\0';
len = maxLen;
}
std::strcat(nodeName, "...");
strcat(nodeName, "...");
}
}
return nodeName;
}
const char *getCurrentModeTitle_Nodes(int screenWidth)
const char *getCurrentModeTitle(int screenWidth)
{
switch (currentMode_Nodes) {
switch (currentMode) {
case MODE_LAST_HEARD:
return "Last Heard";
case MODE_HOP_SIGNAL:
@@ -121,18 +117,8 @@ const char *getCurrentModeTitle_Nodes(int screenWidth)
#else
return (isHighResolution) ? "Hops/Signal" : "Hops/Sig";
#endif
default:
return "Nodes";
}
}
const char *getCurrentModeTitle_Location(int screenWidth)
{
switch (currentMode_Location) {
case MODE_DISTANCE:
return "Distance";
case MODE_BEARING:
return "Bearings";
default:
return "Nodes";
}
@@ -151,8 +137,10 @@ int calculateMaxScroll(int totalEntries, int visibleRows)
void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd)
{
int columnWidth = display->getWidth() / 2;
int separatorX = x + columnWidth - 2;
for (int y = yStart; y <= yEnd; y += 2) {
display->setPixel(x, y);
display->setPixel(separatorX, y);
}
}
@@ -341,15 +329,18 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
}
}
void drawEntryDynamic_Nodes(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
void drawEntryDynamic(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
{
switch (currentMode_Nodes) {
switch (currentMode) {
case MODE_LAST_HEARD:
drawEntryLastHeard(display, node, x, y, columnWidth);
break;
case MODE_HOP_SIGNAL:
drawEntryHopSignal(display, node, x, y, columnWidth);
break;
case MODE_DISTANCE:
drawNodeDistance(display, node, x, y, columnWidth);
break;
default:
break;
}
@@ -442,15 +433,9 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
locationScreen = true;
#if defined(M5STACK_UNITC6L)
int columnWidth = display->getWidth();
#elif defined(T_LORA_PAGER)
int columnWidth = display->getWidth() / 3;
if (config.display.use_long_node_name) {
columnWidth = display->getWidth() / 2;
}
#else
int columnWidth = display->getWidth() / 2;
#endif
display->clear();
// Draw the battery/time header
@@ -465,11 +450,6 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
int visibleNodeRows = totalRowsAvailable;
#if defined(M5STACK_UNITC6L)
int totalColumns = 1;
#elif defined(T_LORA_PAGER)
int totalColumns = 3;
if (config.display.use_long_node_name) {
totalColumns = 2;
}
#else
int totalColumns = 2;
#endif
@@ -519,9 +499,7 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
// Draw column separator
if (shownCount > 0) {
const int firstNodeY = y + 3;
for (int horizontal_offset = 1; horizontal_offset < totalColumns; horizontal_offset++) {
drawColumnSeparator(display, columnWidth * horizontal_offset, firstNodeY, lastNodeY);
}
drawColumnSeparator(display, x, firstNodeY, lastNodeY);
}
#endif
@@ -535,11 +513,10 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
// =============================
#ifndef USE_EINK
// Node list for Last Heard and Hop Signal views
void drawDynamicListScreen_Nodes(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
// Static variables to track mode and duration
static ListMode_Node lastRenderedMode = MODE_COUNT_NODE;
static NodeListMode lastRenderedMode = MODE_COUNT;
static unsigned long modeStartTime = 0;
unsigned long now = millis();
@@ -552,65 +529,23 @@ void drawDynamicListScreen_Nodes(OLEDDisplay *display, OLEDDisplayUiState *state
}
#endif
// On very first call (on boot or state enter)
if (lastRenderedMode == MODE_COUNT_NODE) {
currentMode_Nodes = MODE_LAST_HEARD;
if (lastRenderedMode == MODE_COUNT) {
currentMode = MODE_LAST_HEARD;
modeStartTime = now;
}
// Time to switch to next mode?
if (now - modeStartTime >= getModeCycleIntervalMs()) {
currentMode_Nodes = static_cast<ListMode_Node>((currentMode_Nodes + 1) % MODE_COUNT_NODE);
currentMode = static_cast<NodeListMode>((currentMode + 1) % MODE_COUNT);
modeStartTime = now;
}
// Render screen based on currentMode
const char *title = getCurrentModeTitle_Nodes(display->getWidth());
drawNodeListScreen(display, state, x, y, title, drawEntryDynamic_Nodes);
const char *title = getCurrentModeTitle(display->getWidth());
drawNodeListScreen(display, state, x, y, title, drawEntryDynamic);
// Track the last mode to avoid reinitializing modeStartTime
lastRenderedMode = currentMode_Nodes;
}
// Node list for Distance and Bearings views
void drawDynamicListScreen_Location(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
// Static variables to track mode and duration
static ListMode_Location lastRenderedMode = MODE_COUNT_LOCATION;
static unsigned long modeStartTime = 0;
unsigned long now = millis();
#if defined(M5STACK_UNITC6L)
display->clear();
if (now - lastSwitchTime >= 3000) {
display->display();
lastSwitchTime = now;
}
#endif
// On very first call (on boot or state enter)
if (lastRenderedMode == MODE_COUNT_LOCATION) {
currentMode_Location = MODE_DISTANCE;
modeStartTime = now;
}
// Time to switch to next mode?
if (now - modeStartTime >= getModeCycleIntervalMs()) {
currentMode_Location = static_cast<ListMode_Location>((currentMode_Location + 1) % MODE_COUNT_LOCATION);
modeStartTime = now;
}
// Render screen based on currentMode
const char *title = getCurrentModeTitle_Location(display->getWidth());
// Render screen based on currentMode_Location
if (currentMode_Location == MODE_DISTANCE) {
drawNodeListScreen(display, state, x, y, title, drawNodeDistance);
} else if (currentMode_Location == MODE_BEARING) {
drawNodeListWithCompasses(display, state, x, y);
}
// Track the last mode to avoid reinitializing modeStartTime
lastRenderedMode = currentMode_Location;
lastRenderedMode = currentMode;
}
#endif
@@ -631,12 +566,14 @@ void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_
#endif
drawNodeListScreen(display, state, x, y, title, drawEntryHopSignal);
}
void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
const char *title = "Distance";
drawNodeListScreen(display, state, x, y, title, drawNodeDistance);
}
#endif
void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
float heading = 0;

View File

@@ -23,11 +23,8 @@ namespace NodeListRenderer
typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int);
typedef void (*NodeExtrasRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int, float, double, double);
// Node list mode enumeration for Last Heard and Hop Signal views
enum ListMode_Node { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_COUNT_NODE = 2 };
// Node list mode enumeration for Distance and Bearings views
enum ListMode_Location { MODE_DISTANCE = 0, MODE_BEARING = 1, MODE_COUNT_LOCATION = 2 };
// Node list mode enumeration
enum NodeListMode { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_DISTANCE = 2, MODE_COUNT = 3 };
// Main node list screen function
void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title,
@@ -38,7 +35,7 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
void drawEntryDynamic_Nodes(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
void drawEntryDynamic(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
// Extras renderers
@@ -49,13 +46,11 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
void drawDynamicListScreen_Nodes(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
void drawDynamicListScreen_Location(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
// Utility functions
const char *getCurrentModeTitle_Nodes(int screenWidth);
const char *getCurrentModeTitle_Location(int screenWidth);
const char *getCurrentModeTitle(int screenWidth);
const char *getSafeNodeName(meshtastic_NodeInfoLite *node);
void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields);

View File

@@ -1,6 +1,6 @@
#include "configuration.h"
#if HAS_SCREEN
#include "DisplayFormatters.h"
#include "NodeDB.h"
#include "NotificationRenderer.h"
@@ -38,7 +38,7 @@ extern bool hasUnreadMessage;
namespace graphics
{
int bannerSignalBars = -1;
InputEvent NotificationRenderer::inEvent;
int8_t NotificationRenderer::curSelected = 0;
char NotificationRenderer::alertBannerMessage[256] = {0};
@@ -477,7 +477,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
bool is_picker = false;
uint16_t lineCount = 0;
// Layout Configuration
// === Layout Configuration ===
constexpr uint16_t hPadding = 5;
constexpr uint16_t vPadding = 2;
bool needs_bell = false;
@@ -491,32 +491,13 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
display->setFont(FONT_SMALL);
display->setTextAlignment(TEXT_ALIGN_LEFT);
// Track widest line INCLUDING bars (but don't change per-line widths)
uint16_t widestLineWithBars = 0;
while (lines[lineCount] != nullptr) {
auto newlinePointer = strchr(lines[lineCount], '\n');
if (newlinePointer)
lineLengths[lineCount] = (newlinePointer - lines[lineCount]); // Check for newlines first
else // if the newline wasn't found, then pull string length from strlen
lineLengths[lineCount] = strlen(lines[lineCount]);
lineWidths[lineCount] = display->getStringWidth(lines[lineCount], lineLengths[lineCount], true);
// Consider extra width for signal bars on lines that contain "Signal:"
uint16_t potentialWidth = lineWidths[lineCount];
if (graphics::bannerSignalBars >= 0 && strncmp(lines[lineCount], "Signal:", 7) == 0) {
const int totalBars = 5;
const int barWidth = 3;
const int barSpacing = 2;
const int gap = 6; // space between text and bars
int barsWidth = totalBars * barWidth + (totalBars - 1) * barSpacing + gap;
potentialWidth += barsWidth;
}
if (potentialWidth > widestLineWithBars)
widestLineWithBars = potentialWidth;
if (!is_picker) {
needs_bell |= (strstr(alertBannerMessage, "Alert Received") != nullptr);
if (lineWidths[lineCount] > maxWidth)
@@ -526,12 +507,136 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
}
// count lines
// Ensure box accounts for signal bars if present
if (widestLineWithBars > maxWidth)
maxWidth = widestLineWithBars;
uint16_t boxWidth = hPadding * 2 + maxWidth;
#if defined(M5STACK_UNITC6L)
if (needs_bell) {
if (isHighResolution && boxWidth <= 150)
boxWidth += 26;
if (!isHighResolution && boxWidth <= 100)
boxWidth += 20;
}
uint16_t screenHeight = display->height();
uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3;
uint8_t visibleTotalLines = std::min<uint8_t>(lineCount, (screenHeight - vPadding * 2) / effectiveLineHeight);
uint16_t contentHeight = visibleTotalLines * effectiveLineHeight;
uint16_t boxHeight = contentHeight + vPadding * 2;
if (visibleTotalLines == 1)
boxHeight += (isHighResolution ? 4 : 3);
int16_t boxLeft = (display->width() / 2) - (boxWidth / 2);
if (totalLines > visibleTotalLines)
boxWidth += (isHighResolution ? 4 : 2);
int16_t boxTop = (display->height() / 2) - (boxHeight / 2);
if (visibleTotalLines == 1) {
boxTop += 25;
}
if (alertBannerOptions < 3) {
int missingLines = 3 - alertBannerOptions;
int moveUp = missingLines * (effectiveLineHeight / 2);
boxTop -= moveUp;
if (boxTop < 0)
boxTop = 0;
}
// === Draw Box ===
display->setColor(BLACK);
display->fillRect(boxLeft, boxTop, boxWidth, boxHeight);
display->setColor(WHITE);
display->drawRect(boxLeft, boxTop, boxWidth, boxHeight);
display->fillRect(boxLeft, boxTop - 2, boxWidth, 1);
display->fillRect(boxLeft - 2, boxTop, 1, boxHeight);
display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight);
display->setColor(BLACK);
display->fillRect(boxLeft, boxTop, 1, 1);
display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1);
display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1);
display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1);
display->setColor(WHITE);
int16_t lineY = boxTop + vPadding;
int swingRange = 8;
static int swingOffset = 0;
static bool swingRight = true;
static unsigned long lastSwingTime = 0;
unsigned long now = millis();
int swingSpeedMs = 10 / (swingRange * 2);
if (now - lastSwingTime >= (unsigned long)swingSpeedMs) {
lastSwingTime = now;
if (swingRight) {
swingOffset++;
if (swingOffset >= swingRange)
swingRight = false;
} else {
swingOffset--;
if (swingOffset <= 0)
swingRight = true;
}
}
for (int i = 0; i < lineCount; i++) {
bool isTitle = (i == 0);
int globalOptionIndex = (i - 1) + firstOptionToShow;
bool isSelectedOption = (!isTitle && globalOptionIndex >= 0 && globalOptionIndex == curSelected);
uint16_t visibleWidth = 64 - hPadding * 2;
if (totalLines > visibleTotalLines)
visibleWidth -= 6;
char lineBuffer[lineLengths[i] + 1];
strncpy(lineBuffer, lines[i], lineLengths[i]);
lineBuffer[lineLengths[i]] = '\0';
if (isTitle) {
if (visibleTotalLines == 1) {
display->setColor(BLACK);
display->fillRect(boxLeft, boxTop, boxWidth, effectiveLineHeight);
display->setColor(WHITE);
display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, boxTop, lineBuffer);
} else {
display->setColor(WHITE);
display->fillRect(boxLeft, boxTop, boxWidth, effectiveLineHeight);
display->setColor(BLACK);
display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, boxTop, lineBuffer);
display->setColor(WHITE);
if (needs_bell) {
int bellY = boxTop + (FONT_HEIGHT_SMALL - 8) / 2;
display->drawXbm(boxLeft + (boxWidth - lineWidths[i]) / 2 - 10, bellY, 8, 8, bell_alert);
display->drawXbm(boxLeft + (boxWidth + lineWidths[i]) / 2 + 2, bellY, 8, 8, bell_alert);
}
}
lineY = boxTop + effectiveLineHeight + 1;
} else if (isSelectedOption) {
display->setColor(WHITE);
display->fillRect(boxLeft, lineY, boxWidth, effectiveLineHeight);
display->setColor(BLACK);
if (lineLengths[i] > 15 && lineWidths[i] > visibleWidth) {
int textX = boxLeft + hPadding + swingOffset;
display->drawString(textX, lineY - 1, lineBuffer);
} else {
display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, lineY - 1, lineBuffer);
}
display->setColor(WHITE);
lineY += effectiveLineHeight;
} else {
display->setColor(BLACK);
display->fillRect(boxLeft, lineY, boxWidth, effectiveLineHeight);
display->setColor(WHITE);
display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, lineY, lineBuffer);
lineY += effectiveLineHeight;
}
}
if (totalLines > visibleTotalLines) {
const uint8_t scrollBarWidth = 5;
int16_t scrollBarX = boxLeft + boxWidth - scrollBarWidth - 2;
int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight;
uint16_t scrollBarHeight = boxHeight - vPadding * 2 - effectiveLineHeight;
float ratio = (float)visibleTotalLines / totalLines;
uint16_t indicatorHeight = std::max((int)(scrollBarHeight * ratio), 4);
float scrollRatio = (float)(firstOptionToShow + lineCount - visibleTotalLines) / (totalLines - visibleTotalLines);
uint16_t indicatorY = scrollBarY + scrollRatio * (scrollBarHeight - indicatorHeight);
display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight);
display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight);
}
#else
if (needs_bell) {
if (isHighResolution && boxWidth <= 150)
boxWidth += 26;
@@ -553,21 +658,8 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
boxWidth += (isHighResolution) ? 4 : 2;
}
int16_t boxTop = (display->height() / 2) - (boxHeight / 2);
boxHeight += (isHighResolution) ? 2 : 1;
#if defined(M5STACK_UNITC6L)
if (visibleTotalLines == 1) {
boxTop += 25;
}
if (alertBannerOptions < 3) {
int missingLines = 3 - alertBannerOptions;
int moveUp = missingLines * (effectiveLineHeight / 2);
boxTop -= moveUp;
if (boxTop < 0)
boxTop = 0;
}
#endif
// Draw Box
// === Draw Box ===
display->setColor(BLACK);
display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2);
display->fillRect(boxLeft, boxTop - 2, boxWidth, 1);
@@ -583,7 +675,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1);
display->setColor(WHITE);
// Draw Content
// === Draw Content ===
int16_t lineY = boxTop + vPadding;
for (int i = 0; i < lineCount; i++) {
int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2;
@@ -612,47 +704,17 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
lineY += (effectiveLineHeight - 2 - background_yOffset);
} else {
// Pop-up
// If this is the Signal line, center text + bars as one group
bool isSignalLine = (graphics::bannerSignalBars >= 0 && strstr(lineBuffer, "Signal:") != nullptr);
if (isSignalLine) {
const int totalBars = 5;
const int barWidth = 3;
const int barSpacing = 2;
const int barHeightStep = 2;
const int gap = 6;
int textWidth = display->getStringWidth(lineBuffer, strlen(lineBuffer), true);
int barsWidth = totalBars * barWidth + (totalBars - 1) * barSpacing + gap;
int totalWidth = textWidth + barsWidth;
int groupStartX = boxLeft + (boxWidth - totalWidth) / 2;
display->drawString(groupStartX, lineY, lineBuffer);
int baseX = groupStartX + textWidth + gap;
int baseY = lineY + effectiveLineHeight - 1;
for (int b = 0; b < totalBars; b++) {
int barHeight = (b + 1) * barHeightStep;
int x = baseX + b * (barWidth + barSpacing);
int y = baseY - barHeight;
if (b < graphics::bannerSignalBars) {
display->fillRect(x, y, barWidth, barHeight);
} else {
display->drawRect(x, y, barWidth, barHeight);
}
}
} else {
display->drawString(textX, lineY, lineBuffer);
}
display->drawString(textX, lineY, lineBuffer);
lineY += (effectiveLineHeight);
}
}
// Scroll Bar (Thicker, inside box, not over title)
// === Scroll Bar (Thicker, inside box, not over title) ===
if (totalLines > visibleTotalLines) {
const uint8_t scrollBarWidth = 5;
int16_t scrollBarX = boxLeft + boxWidth - scrollBarWidth - 2;
int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight;
int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight; // start after title line
uint16_t scrollBarHeight = boxHeight - vPadding * 2 - effectiveLineHeight;
float ratio = (float)visibleTotalLines / totalLines;
@@ -663,6 +725,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight);
display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight);
}
#endif
}
/// Draw the last text message we received

View File

@@ -6,7 +6,10 @@
#include "NodeListRenderer.h"
#include "UIRenderer.h"
#include "airtime.h"
#include "configuration.h"
#include "gps/GeoCoord.h"
#include "graphics/Screen.h"
#include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/TimeFormatters.h"
#include "graphics/images.h"
@@ -26,16 +29,6 @@ namespace graphics
NodeNum UIRenderer::currentFavoriteNodeNum = 0;
std::vector<meshtastic_NodeInfoLite *> graphics::UIRenderer::favoritedNodes;
static inline void drawSatelliteIcon(OLEDDisplay *display, int16_t x, int16_t y)
{
int yOffset = (isHighResolution) ? -5 : 1;
if (isHighResolution) {
NodeListRenderer::drawScaledXBitmap16x16(x, y + yOffset, imgSatellite_width, imgSatellite_height, imgSatellite, display);
} else {
display->drawXbm(x + 1, y + yOffset, imgSatellite_width, imgSatellite_height, imgSatellite);
}
}
void graphics::UIRenderer::rebuildFavoritedNodes()
{
favoritedNodes.clear();
@@ -251,16 +244,16 @@ void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y,
// Draw nodes status
void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, int node_offset,
bool show_total, const char *additional_words)
bool show_total, String additional_words)
{
char usersString[20];
int nodes_online = (nodeStatus->getNumOnline() > 0) ? nodeStatus->getNumOnline() + node_offset : 0;
snprintf(usersString, sizeof(usersString), "%d %s", nodes_online, additional_words);
snprintf(usersString, sizeof(usersString), "%d %s", nodes_online, additional_words.c_str());
if (show_total) {
int nodes_total = (nodeStatus->getNumTotal() > 0) ? nodeStatus->getNumTotal() + node_offset : 0;
snprintf(usersString, sizeof(usersString), "%d/%d %s", nodes_online, nodes_total, additional_words);
snprintf(usersString, sizeof(usersString), "%d/%d %s", nodes_online, nodes_total, additional_words.c_str());
}
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
@@ -607,7 +600,14 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
} else {
displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off";
}
drawSatelliteIcon(display, x, getTextPositions(display)[line]);
int yOffset = (isHighResolution) ? 3 : 1;
if (isHighResolution) {
NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width,
imgSatellite_height, imgSatellite, display);
} else {
display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height,
imgSatellite);
}
int xOffset = (isHighResolution) ? 6 : 0;
display->drawString(x + 11 + xOffset, getTextPositions(display)[line], displayLine);
} else {
@@ -759,7 +759,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
// Start Functions to write date/time to the screen
// Helper function to check if a year is a leap year
constexpr bool isLeapYear(int year)
bool isLeapYear(int year)
{
return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0));
}
@@ -990,7 +990,14 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
} else {
displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off";
}
drawSatelliteIcon(display, x, getTextPositions(display)[line]);
int yOffset = (isHighResolution) ? 3 : 1;
if (isHighResolution) {
NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width,
imgSatellite_height, imgSatellite, display);
} else {
display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height,
imgSatellite);
}
int xOffset = (isHighResolution) ? 6 : 0;
display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine);
} else {

View File

@@ -34,7 +34,7 @@ class UIRenderer
public:
// Common UI elements
static void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus,
int node_offset = 0, bool show_total = true, const char *additional_words = "");
int node_offset = 0, bool show_total = true, String additional_words = "");
// GPS status functions
static void drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
@@ -43,6 +43,9 @@ class UIRenderer
static void drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
static void drawGpsPowerStatus(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
// Layout and utility functions
static void drawScrollbar(OLEDDisplay *display, int visibleItems, int totalItems, int scrollIndex, int x, int startY);
// Overlay and special screens
static void drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *text);
@@ -80,6 +83,8 @@ class UIRenderer
static std::string drawTimeDelta(uint32_t days, uint32_t hours, uint32_t minutes, uint32_t seconds);
static int formatDateTime(char *buffer, size_t bufferSize, uint32_t rtc_sec, OLEDDisplay *display, bool showTime);
// Message filtering
static bool shouldDrawMessage(const meshtastic_MeshPacket *packet);
// Check if the display can render a string (detect special chars; emoji)
static bool haveGlyphs(const char *str);
}; // namespace UIRenderer

View File

@@ -304,6 +304,58 @@ const uint8_t chirpy[] = {
0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01,
0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0xfe, 0xff, 0xff, 0xff, 0xdf};
#define chirpy_width_hirez 76
#define chirpy_height_hirez 100
const uint8_t chirpy_hirez[] = {
0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0x03,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00,
0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc,
0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03,
0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xf0, 0xff, 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0,
0xff, 0xf0, 0xff, 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f,
0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0,
0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff,
0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f,
0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0,
0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff,
0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00,
0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc,
0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03,
0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0,
0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f,
0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0,
0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xf0, 0xff,
0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xf0, 0xff, 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f,
0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc,
0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03,
0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00,
0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0, 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00,
0xc0, 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0, 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0,
0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x0f, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00,
0x0f, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x0f, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x0f,
0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c,
0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, 0x00,
0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00,
0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, 0xfc,
0x03, 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03,
0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00,
0xf0, 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0, 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0,
0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0, 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xf3, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3};
#define chirpy_small_image_width 8
#define chirpy_small_image_height 8
const uint8_t chirpy_small[] = {0x7f, 0x41, 0x55, 0x55, 0x55, 0x55, 0x41, 0x7f};

View File

@@ -489,6 +489,8 @@ int32_t KbI2cBase::runOnce()
case 0x90: // fn+r INPUT_BROKER_MSG_REBOOT
case 0x91: // fn+t
case 0xac: // fn+m INPUT_BROKER_MSG_MUTE_TOGGLE
case 0x8b: // fn+del INPUT_BROKEN_MSG_DISMISS_FRAME
case 0xAA: // fn+b INPUT_BROKER_MSG_BLUETOOTH_TOGGLE
case 0x8F: // fn+e INPUT_BROKER_MSG_EMOTE_LIST
// just pass those unmodified

View File

@@ -428,10 +428,17 @@ void setup()
#endif
#if ARCH_PORTDUINO
RTCQuality ourQuality = RTCQualityDevice;
std::string timeCommandResult = exec("timedatectl status | grep synchronized | grep yes -c");
if (timeCommandResult[0] == '1') {
ourQuality = RTCQualityNTP;
}
struct timeval tv;
tv.tv_sec = time(NULL);
tv.tv_usec = 0;
perhapsSetRTC(RTCQualityDevice, &tv);
perhapsSetRTC(ourQuality, &tv);
#endif
powerMonInit();

View File

@@ -44,7 +44,6 @@ struct UIFrameEvent {
REDRAW_ONLY, // Don't change which frames are show, just redraw, asap
REGENERATE_FRAMESET, // Regenerate (change? add? remove?) screen frames, honoring requestFocus()
REGENERATE_FRAMESET_BACKGROUND, // Regenerate screen frames, Attempt to remain on the same frame throughout
SWITCH_TO_TEXTMESSAGE // Jump directly to the Text Message screen
} action = REDRAW_ONLY;
// We might want to pass additional data inside this struct at some point
@@ -226,4 +225,4 @@ class MeshModule
/** set the destination and packet parameters of packet p intended as a reply to a particular "to" packet
* This ensures that if the request packet was sent reliably, the reply is sent that way as well.
*/
void setReplyTo(meshtastic_MeshPacket *p, const meshtastic_MeshPacket &to);
void setReplyTo(meshtastic_MeshPacket *p, const meshtastic_MeshPacket &to);

View File

@@ -7,12 +7,10 @@
#include "../concurrency/Periodic.h"
#include "BluetoothCommon.h" // needed for updateBatteryLevel, FIXME, eventually when we pull mesh out into a lib we shouldn't be whacking bluetooth from here
#include "MeshService.h"
#include "MessageStore.h"
#include "NodeDB.h"
#include "PowerFSM.h"
#include "RTC.h"
#include "TypeConversions.h"
#include "graphics/draw/MessageRenderer.h"
#include "main.h"
#include "mesh-pb-constants.h"
#include "meshUtils.h"
@@ -194,16 +192,8 @@ void MeshService::handleToRadio(meshtastic_MeshPacket &p)
p.id = generatePacketId(); // If the phone didn't supply one, then pick one
p.rx_time = getValidTime(RTCQualityFromNet); // Record the time the packet arrived from the phone
// (so we update our nodedb for the local node)
#if HAS_SCREEN
if (p.decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP && p.decoded.payload.size > 0 && p.to != NODENUM_BROADCAST &&
p.to != 0) // DM only
{
perhapsDecode(&p);
const StoredMessage &sm = messageStore.addFromPacket(p);
graphics::MessageRenderer::handleNewMessage(nullptr, sm, p); // notify UI
}
#endif
// Send the packet into the mesh
DEBUG_HEAP_BEFORE;
auto a = packetPool.allocCopy(p);

View File

@@ -150,7 +150,9 @@ void ReliableRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtas
PacketId nakId = (c && c->error_reason != meshtastic_Routing_Error_NONE) ? p->decoded.request_id : 0;
// We intentionally don't check wasSeenRecently, because it is harmless to delete non existent retransmission records
if (ackId || nakId) {
if ((ackId || nakId) &&
// Implicit ACKs from MQTT should not stop retransmissions
!(isFromUs(p) && p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT)) {
LOG_DEBUG("Received a %s for 0x%x, stopping retransmissions", ackId ? "ACK" : "NAK", ackId);
if (ackId) {
stopRetransmission(p->to, ackId);

File diff suppressed because it is too large Load Diff

View File

@@ -75,6 +75,7 @@ class CannedMessageModule : public SinglePortModule, public Observable<const UIF
// === State/UI ===
bool shouldDraw();
bool hasMessages();
void showTemporaryMessage(const String &message);
void resetSearch();
void updateDestinationSelectionList();
void drawDestinationSelectionScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
@@ -152,9 +153,10 @@ class CannedMessageModule : public SinglePortModule, public Observable<const UIF
unsigned long lastUpdateMillis = 0;
String searchQuery;
String freetext;
String temporaryMessage;
// === Message Storage ===
char messageBuffer[CANNED_MESSAGE_MODULE_MESSAGES_SIZE + 1];
char messageStore[CANNED_MESSAGE_MODULE_MESSAGES_SIZE + 1];
char *messages[CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT];
int messagesCount = 0;
int currentMessageIndex = -1;
@@ -165,11 +167,14 @@ class CannedMessageModule : public SinglePortModule, public Observable<const UIF
NodeNum lastSentNode = 0; // Tracks the most recent node we sent a message to (for UI display)
ChannelIndex channel = 0; // Channel index used when sending a message
bool ack = false; // True = ACK received, False = NACK or failed
bool waitingForAck = false; // True if we're expecting an ACK and should monitor routing packets
float lastRxSnr = 0; // SNR from last received ACK (used for diagnostics/UI)
int32_t lastRxRssi = 0; // RSSI from last received ACK (used for diagnostics/UI)
uint32_t lastRequestId = 0; // tracks the request_id of our last sent packet
bool ack = false; // True = ACK received, False = NACK or failed
bool waitingForAck = false; // True if we're expecting an ACK and should monitor routing packets
bool lastAckWasRelayed = false; // True if the ACK was relayed through intermediate nodes
uint8_t lastAckHopStart = 0; // Hop start value from the received ACK packet
uint8_t lastAckHopLimit = 0; // Hop limit value from the received ACK packet
float lastRxSnr = 0; // SNR from last received ACK (used for diagnostics/UI)
int32_t lastRxRssi = 0; // RSSI from last received ACK (used for diagnostics/UI)
// === State Tracking ===
cannedMessageModuleRunState runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;

View File

@@ -60,7 +60,9 @@ meshtastic_MeshPacket *DropzoneModule::sendConditions()
long hms = rtc_sec % SEC_PER_DAY;
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
graphics::decomposeTime(rtc_sec, hour, min, sec);
hour = hms / SEC_PER_HOUR;
min = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN;
}
// Check if the dropzone is open or closed by reading the analog pin

View File

@@ -75,6 +75,12 @@ uint8_t RoutingModule::getHopLimitForResponse(uint8_t hopStart, uint8_t hopLimit
return Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); // Use the default hop limit
}
meshtastic_MeshPacket *RoutingModule::allocAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex,
uint8_t hopLimit)
{
return MeshModule::allocAckNak(err, to, idFrom, chIndex, hopLimit);
}
RoutingModule::RoutingModule() : ProtobufModule("routing", meshtastic_PortNum_ROUTING_APP, &meshtastic_Routing_msg)
{
isPromiscuous = true;

View File

@@ -16,6 +16,9 @@ class RoutingModule : public ProtobufModule<meshtastic_Routing>
virtual void sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit = 0,
bool ackWantsAck = false);
meshtastic_MeshPacket *allocAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex,
uint8_t hopLimit = 0);
// Given the hopStart and hopLimit upon reception of a request, return the hop limit to use for the response
uint8_t getHopLimitForResponse(uint8_t hopStart, uint8_t hopLimit);

View File

@@ -1,13 +1,10 @@
#include "SystemCommandsModule.h"
#include "input/InputBroker.h"
#include "meshUtils.h"
#if HAS_SCREEN
#include "MessageStore.h"
#include "graphics/Screen.h"
#include "graphics/SharedUIDisplay.h"
#endif
#include "GPS.h"
#include "MeshService.h"
#include "Module.h"
@@ -31,7 +28,10 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event)
switch (event->kbchar) {
// Fn key symbols
case INPUT_BROKER_MSG_FN_SYMBOL_ON:
IF_SCREEN(screen->setFunctionSymbol("Fn"));
return 0;
case INPUT_BROKER_MSG_FN_SYMBOL_OFF:
IF_SCREEN(screen->removeFunctionSymbol("Fn"));
return 0;
// Brightness
case INPUT_BROKER_MSG_BRIGHTNESS_UP:
@@ -78,9 +78,6 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event)
case INPUT_BROKER_MSG_REBOOT:
IF_SCREEN(screen->showSimpleBanner("Rebooting...", 0));
nodeDB->saveToDisk();
#if HAS_SCREEN
messageStore.saveToFlash();
#endif
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
// runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
return true;

View File

@@ -1,14 +1,10 @@
#include "TextMessageModule.h"
#include "MeshService.h"
#include "MessageStore.h"
#include "NodeDB.h"
#include "PowerFSM.h"
#include "buzz.h"
#include "configuration.h"
#include "graphics/Screen.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/draw/MessageRenderer.h"
#include "main.h"
TextMessageModule *textMessageModule;
ProcessMessage TextMessageModule::handleReceived(const meshtastic_MeshPacket &mp)
@@ -19,26 +15,14 @@ ProcessMessage TextMessageModule::handleReceived(const meshtastic_MeshPacket &mp
#endif
// We only store/display messages destined for us.
// Keep a copy of the most recent text message.
devicestate.rx_text_message = mp;
devicestate.has_rx_text_message = true;
#if HAS_SCREEN
// Guard against running in MeshtasticUI
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
// Store in the central message history
const StoredMessage &sm = messageStore.addFromPacket(mp);
// Pass message to renderer (banner + thread switching + scroll reset)
// Use the global Screen singleton to retrieve the current OLED display
auto *display = screen ? screen->getDisplayDevice() : nullptr;
graphics::MessageRenderer::handleNewMessage(display, sm, mp);
}
#endif
// Only trigger screen wake if configuration allows it
if (shouldWakeOnReceivedMessage()) {
powerFSM.trigger(EVENT_RECEIVED_MSG);
}
// Notify any observers (e.g. external modules that care about packets)
notifyObservers(&mp);
return ProcessMessage::CONTINUE; // Let others look at this message also if they want

View File

@@ -3,13 +3,7 @@
#include "SinglePortModule.h"
/**
* Text message handling for Meshtastic.
*
* This module is responsible for receiving and storing incoming text messages
* from the mesh. It updates device state and notifies observers so that other
* components (such as the MessageRenderer) can later display or process them.
*
* Rendering of messages on screen is no longer done here.
* Text message handling for meshtastic - draws on the OLED display the most recent received message
*/
class TextMessageModule : public SinglePortModule, public Observable<const meshtastic_MeshPacket *>
{
@@ -21,10 +15,10 @@ class TextMessageModule : public SinglePortModule, public Observable<const mesht
protected:
/** Called to handle a particular incoming message
*
* @return ProcessMessage::STOP if you've guaranteed you've handled this
* message and no other handlers should be considered for it.
*/
@return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be considered for
it
*/
virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
virtual bool wantPacket(const meshtastic_MeshPacket *p) override;
};

View File

@@ -2,7 +2,6 @@
#include "NodeDB.h"
#include "PowerFSM.h"
#include "configuration.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/draw/CompassRenderer.h"
#if HAS_SCREEN
@@ -15,15 +14,6 @@
WaypointModule *waypointModule;
static inline float degToRad(float deg)
{
return deg * PI / 180.0f;
}
static inline float radToDeg(float rad)
{
return rad * 180.0f / PI;
}
ProcessMessage WaypointModule::handleReceived(const meshtastic_MeshPacket &mp)
{
#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
@@ -62,15 +52,31 @@ ProcessMessage WaypointModule::handleReceived(const meshtastic_MeshPacket &mp)
bool WaypointModule::shouldDraw()
{
#if !MESHTASTIC_EXCLUDE_WAYPOINT
if (!screen || !devicestate.has_rx_waypoint)
if (screen == nullptr)
return false;
// If no waypoint to show
if (!devicestate.has_rx_waypoint)
return false;
meshtastic_Waypoint wp{}; // <- replaces memset
// Decode the message, to find the expiration time (is waypoint still valid)
// This handles "deletion" as well as expiration
meshtastic_Waypoint wp;
memset(&wp, 0, sizeof(wp));
if (pb_decode_from_bytes(devicestate.rx_waypoint.decoded.payload.bytes, devicestate.rx_waypoint.decoded.payload.size,
&meshtastic_Waypoint_msg, &wp)) {
return wp.expire > getTime();
// Valid waypoint
if (wp.expire > getTime())
return devicestate.has_rx_waypoint = true;
// Expired, or deleted
else
return devicestate.has_rx_waypoint = false;
}
return false; // no LOG_ERROR, no flag writes
// If decoding failed
LOG_ERROR("Failed to decode waypoint");
devicestate.has_rx_waypoint = false;
return false;
#else
return false;
#endif
@@ -79,46 +85,53 @@ bool WaypointModule::shouldDraw()
/// Draw the last waypoint we received
void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
if (!screen)
if (screen == nullptr)
return;
display->clear();
display->setTextAlignment(TEXT_ALIGN_LEFT);
// Prepare to draw
display->setFont(FONT_SMALL);
int line = 1;
display->setTextAlignment(TEXT_ALIGN_LEFT);
// === Set Title
const char *titleStr = "Waypoint";
// === Header ===
graphics::drawCommonHeader(display, x, y, titleStr);
const int w = display->getWidth();
const int h = display->getHeight();
// Handle inverted display
// Unsure of expected behavior: for now, copy drawNodeInfo
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED)
display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL);
// Decode the waypoint
const meshtastic_MeshPacket &mp = devicestate.rx_waypoint;
meshtastic_Waypoint wp{};
meshtastic_Waypoint wp;
memset(&wp, 0, sizeof(wp));
if (!pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_Waypoint_msg, &wp)) {
// This *should* be caught by shouldDrawWaypoint, but we'll short-circuit here just in case
display->drawStringMaxWidth(0 + x, 0 + y, x + display->getWidth(), "Couldn't decode waypoint");
devicestate.has_rx_waypoint = false;
return;
}
// Get timestamp info. Will pass as a field to drawColumns
char lastStr[20];
static char lastStr[20];
getTimeAgoStr(sinceReceived(&mp), lastStr, sizeof(lastStr));
// Will contain distance information, passed as a field to drawColumns
char distStr[20];
static char distStr[20];
// Get our node, to use our own position
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
// Text fields to draw (left of compass)
// Last element must be NULL. This signals the end of the char*[] to drawColumns
const char *fields[] = {"Waypoint", lastStr, wp.name, distStr, NULL};
// Dimensions / co-ordinates for the compass/circle
const uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(w, h);
const int16_t compassX = x + w - (compassDiam / 2) - 5;
const int16_t compassY = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT)
? y + h / 2
: y + FONT_HEIGHT_SMALL + (h - FONT_HEIGHT_SMALL) / 2;
int16_t compassX = 0, compassY = 0;
uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(display->getWidth(), display->getHeight());
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) {
compassX = x + display->getWidth() - compassDiam / 2 - 5;
compassY = y + display->getHeight() / 2;
} else {
compassX = x + display->getWidth() - compassDiam / 2 - 5;
compassY = y + FONT_HEIGHT_SMALL + (display->getHeight() - FONT_HEIGHT_SMALL) / 2;
}
// If our node has a position:
if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading())) {
@@ -128,7 +141,7 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state,
myHeading = 0;
} else {
if (screen->hasHeading())
myHeading = degToRad(screen->getHeading());
myHeading = (screen->getHeading()) * PI / 180; // gotta convert compass degrees to Radians
else
myHeading = screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i));
}
@@ -144,35 +157,46 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state,
graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther);
float bearingToOtherDegrees = (bearingToOther < 0) ? bearingToOther + 2 * PI : bearingToOther;
bearingToOtherDegrees = radToDeg(bearingToOtherDegrees);
bearingToOtherDegrees = bearingToOtherDegrees * 180 / PI;
// Distance to Waypoint
float d = GeoCoord::latLongToMeter(DegD(wp.latitude_i), DegD(wp.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i));
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
float feet = d * METERS_TO_FEET;
snprintf(distStr, sizeof(distStr), feet < (2 * MILES_TO_FEET) ? "%.0fft %.0f°" : "%.1fmi %.0f°",
feet < (2 * MILES_TO_FEET) ? feet : feet / MILES_TO_FEET, bearingToOtherDegrees);
if (d < (2 * MILES_TO_FEET))
snprintf(distStr, sizeof(distStr), "%.0fft %.0f°", d * METERS_TO_FEET, bearingToOtherDegrees);
else
snprintf(distStr, sizeof(distStr), "%.1fmi %.0f°", d * METERS_TO_FEET / MILES_TO_FEET, bearingToOtherDegrees);
} else {
snprintf(distStr, sizeof(distStr), d < 2000 ? "%.0fm %.0f°" : "%.1fkm %.0f°", d < 2000 ? d : d / 1000,
bearingToOtherDegrees);
if (d < 2000)
snprintf(distStr, sizeof(distStr), "%.0fm %.0f°", d, bearingToOtherDegrees);
else
snprintf(distStr, sizeof(distStr), "%.1fkm %.0f°", d / 1000, bearingToOtherDegrees);
}
}
// If our node doesn't have position
else {
// ? in the compass
display->drawString(compassX - FONT_HEIGHT_SMALL / 4, compassY - FONT_HEIGHT_SMALL / 2, "?");
// ? in the distance field
snprintf(distStr, sizeof(distStr), "? %s ?°",
(config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) ? "mi" : "km");
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL)
strncpy(distStr, "? mi ?°", sizeof(distStr));
else
strncpy(distStr, "? km ?°", sizeof(distStr));
}
// Draw compass circle
display->drawCircle(compassX, compassY, compassDiam / 2);
display->setTextAlignment(TEXT_ALIGN_LEFT); // Something above me changes to a different alignment, forcing a fix here!
display->drawString(0, graphics::getTextPositions(display)[line++], lastStr);
display->drawString(0, graphics::getTextPositions(display)[line++], wp.name);
display->drawString(0, graphics::getTextPositions(display)[line++], wp.description);
display->drawString(0, graphics::getTextPositions(display)[line++], distStr);
// Undo color-inversion, if set prior to drawing header
// Unsure of expected behavior? For now: copy drawNodeInfo
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) {
display->setColor(BLACK);
}
// Must be after distStr is populated
graphics::NodeListRenderer::drawColumns(display, x, y, fields);
}
#endif

View File

@@ -87,10 +87,13 @@ inline void onReceiveProto(char *topic, byte *payload, size_t length)
// Generate an implicit ACK towards ourselves (handled and processed only locally!) for this message.
// We do this because packets are not rebroadcasted back into MQTT anymore and we assume that at least one node
// receives it when we get our own packet back. Then we'll stop our retransmissions.
if (isFromUs(e.packet))
routingModule->sendAckNak(meshtastic_Routing_Error_NONE, getFrom(e.packet), e.packet->id, ch.index);
else
if (isFromUs(e.packet)) {
auto pAck = routingModule->allocAckNak(meshtastic_Routing_Error_NONE, getFrom(e.packet), e.packet->id, ch.index);
pAck->transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT;
router->sendLocal(pAck);
} else {
LOG_INFO("Ignore downlink message we originally sent");
}
return;
}
if (isFromUs(e.packet)) {

View File

@@ -337,7 +337,7 @@ void cpuDeepSleep(uint32_t msecToWake)
#endif
#ifdef TTGO_T_ECHO
// To power off the T-Echo, the display must be set
// To power off the T-Echo, the display must be set
// as an input pin; otherwise, there will be leakage current.
pinMode(PIN_EINK_CS, INPUT);
pinMode(PIN_EINK_DC, INPUT);

View File

@@ -426,13 +426,11 @@ void portduinoSetup()
}
getMacAddr(dmac);
#ifndef UNIT_TEST
if (dmac[0] == 0 && dmac[1] == 0 && dmac[2] == 0 && dmac[3] == 0 && dmac[4] == 0 && dmac[5] == 0) {
std::cout << "*** Blank MAC Address not allowed!" << std::endl;
std::cout << "Please set a MAC Address in config.yaml using either MACAddress or MACAddressSource." << std::endl;
exit(EXIT_FAILURE);
}
#endif
printf("MAC ADDRESS: %02X:%02X:%02X:%02X:%02X:%02X\n", dmac[0], dmac[1], dmac[2], dmac[3], dmac[4], dmac[5]);
// Rather important to set this, if not running simulated.
randomSeed(time(NULL));

View File

@@ -46,14 +46,6 @@ build_flags = ${heltec_mesh_solar_base.build_flags}
-DEINK_LIMIT_RATE_RESPONSIVE_SEC=1 ; Minimum interval between RESPONSIVE updates
-DEINK_BACKGROUND_USES_FAST ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached.
-DEINK_HASQUIRK_GHOSTING ; Display model is identified as "prone to ghosting"
-DENABLE_MESSAGE_PERSISTENCE=0 ; Disable flash persistence for space-limited build
-DMESHTASTIC_EXCLUDE_WIFI=1
-DMESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION=1
-DMESHTASTIC_EXCLUDE_PAXCOUNTER=1
-DMESHTASTIC_EXCLUDE_REMOTEHARDWARE=1
-DMESHTASTIC_EXCLUDE_STOREFORWARD=1
-DMESHTASTIC_EXCLUDE_CANNEDMESSAGES=1
-DMESHTASTIC_EXCLUDE_WAYPOINT=1
lib_deps =
${heltec_mesh_solar_base.lib_deps}
https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip