Compare commits

..

7 Commits

Author SHA1 Message Date
Jonathan Bennett
28058527e0 Better handle prepared statements 2025-12-17 17:36:14 -06:00
Jonathan Bennett
1847271b4f Wrong hash 2025-12-16 22:30:54 -06:00
Jonathan Bennett
a6c7d239d0 refactoring 2025-12-16 22:04:52 -06:00
Jonathan Bennett
2691e6cd4b Initial work on sfpp 2025-12-16 21:28:56 -06:00
Jonathan Bennett
ebab74a360 Merge branch 'develop' into snfpp 2025-12-16 21:26:00 -06:00
Jonathan Bennett
6c3315025a router.* make the encrypted packet copy available for modules to access 2025-12-15 22:51:10 -06:00
Jonathan Bennett
11c2b67223 make channels.h getHash public 2025-12-15 22:49:58 -06:00
128 changed files with 2878 additions and 3540 deletions

View File

@@ -103,13 +103,17 @@ lib_deps =
thingsboard/TBPubSubClient@2.12.1 thingsboard/TBPubSubClient@2.12.1
# renovate: datasource=custom.pio depName=NTPClient packageName=arduino-libraries/library/NTPClient # renovate: datasource=custom.pio depName=NTPClient packageName=arduino-libraries/library/NTPClient
arduino-libraries/NTPClient@3.2.1 arduino-libraries/NTPClient@3.2.1
; Extra TCP/IP networking libs for supported devices
[networking_extra]
lib_deps =
# renovate: datasource=custom.pio depName=Syslog packageName=arcao/library/Syslog # renovate: datasource=custom.pio depName=Syslog packageName=arcao/library/Syslog
arcao/Syslog@2.0.0 arcao/Syslog@2.0.0
; Minimal networking libs for nrf52 (excludes Syslog to save flash)
[nrf52_networking_base]
lib_deps =
# renovate: datasource=custom.pio depName=TBPubSubClient packageName=thingsboard/library/TBPubSubClient
thingsboard/TBPubSubClient@2.12.1
# renovate: datasource=custom.pio depName=NTPClient packageName=arduino-libraries/library/NTPClient
arduino-libraries/NTPClient@3.2.1
[radiolib_base] [radiolib_base]
lib_deps = lib_deps =
# renovate: datasource=custom.pio depName=RadioLib packageName=jgromes/library/RadioLib # renovate: datasource=custom.pio depName=RadioLib packageName=jgromes/library/RadioLib
@@ -158,8 +162,8 @@ lib_deps =
emotibit/EmotiBit MLX90632@1.0.8 emotibit/EmotiBit MLX90632@1.0.8
# renovate: datasource=custom.pio depName=Adafruit MLX90614 packageName=adafruit/library/Adafruit MLX90614 Library # renovate: datasource=custom.pio depName=Adafruit MLX90614 packageName=adafruit/library/Adafruit MLX90614 Library
adafruit/Adafruit MLX90614 Library@2.1.5 adafruit/Adafruit MLX90614 Library@2.1.5
# renovate: datasource=git-refs depName=INA3221 packageName=https://github.com/sgtwilko/INA3221 gitBranch=FixOverflow # renovate: datasource=github-tags depName=INA3221 packageName=sgtwilko/INA3221
https://github.com/sgtwilko/INA3221/archive/bb03d7e9bfcc74fc798838a54f4f99738f29fc6a.zip https://github.com/sgtwilko/INA3221#bb03d7e9bfcc74fc798838a54f4f99738f29fc6a
# renovate: datasource=custom.pio depName=QMC5883L Compass packageName=mprograms/library/QMC5883LCompass # renovate: datasource=custom.pio depName=QMC5883L Compass packageName=mprograms/library/QMC5883LCompass
mprograms/QMC5883LCompass@1.2.3 mprograms/QMC5883LCompass@1.2.3
# renovate: datasource=custom.pio depName=DFRobot_RTU packageName=dfrobot/library/DFRobot_RTU # renovate: datasource=custom.pio depName=DFRobot_RTU packageName=dfrobot/library/DFRobot_RTU

View File

@@ -1,426 +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 all matches from the end
for (auto it = deque.rbegin(); it != deque.rend();) {
if (pred(*it)) {
it = std::deque<StoredMessage>::reverse_iterator(deque.erase(std::next(it).base()));
} else {
++it;
}
}
} else {
// Manual forward search to erase all matches
for (auto it = deque.begin(); it != deque.end();) {
if (pred(*it)) {
it = deque.erase(it);
} else {
++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/ * For more information, see: https://meshtastic.org/
*/ */
#include "power.h" #include "power.h"
#include "MessageStore.h"
#include "NodeDB.h" #include "NodeDB.h"
#include "PowerFSM.h" #include "PowerFSM.h"
#include "Throttle.h" #include "Throttle.h"
@@ -787,9 +786,7 @@ void Power::shutdown()
playShutdownMelody(); playShutdownMelody();
#endif #endif
nodeDB->saveToDisk(); nodeDB->saveToDisk();
#if HAS_SCREEN
messageStore.saveToFlash();
#endif
#if defined(ARCH_NRF52) || defined(ARCH_ESP32) || defined(ARCH_RP2040) #if defined(ARCH_NRF52) || defined(ARCH_ESP32) || defined(ARCH_RP2040)
#ifdef PIN_LED1 #ifdef PIN_LED1
ledOff(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 hour = hms / SEC_PER_HOUR;
int min = (hms % SEC_PER_HOUR) / SEC_PER_MIN; int min = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN
#ifdef ARCH_PORTDUINO #ifdef ARCH_PORTDUINO
::printf("%s ", logLevel); ::printf("%s ", logLevel);
if (color) { if (color) {

View File

@@ -46,7 +46,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#endif #endif
#include "FSCommon.h" #include "FSCommon.h"
#include "MeshService.h" #include "MeshService.h"
#include "MessageStore.h"
#include "RadioLibInterface.h" #include "RadioLibInterface.h"
#include "error.h" #include "error.h"
#include "gps/GeoCoord.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 "modules/WaypointModule.h"
#include "sleep.h" #include "sleep.h"
#include "target_specific.h" #include "target_specific.h"
extern MessageStore messageStore;
using graphics::Emote;
using graphics::emotes;
using graphics::numEmotes;
#if USE_TFTDISPLAY #if USE_TFTDISPLAY
extern uint16_t TFT_MESH; 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. // we'll need to hold onto pointers for the modules that can draw a frame.
std::vector<MeshModule *> moduleFrames; std::vector<MeshModule *> moduleFrames;
// Global variables for screen function overlay symbols
std::vector<std::string> functionSymbol;
std::string functionSymbolString;
#if HAS_GPS #if HAS_GPS
// GeoCoord object for the screen // GeoCoord object for the screen
GeoCoord geoCoord; GeoCoord geoCoord;
@@ -257,11 +263,19 @@ static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int
} else { } else {
// otherwise, just display the module frame that's aligned with the current frame // otherwise, just display the module frame that's aligned with the current frame
module_frame = state->currentFrame; 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); MeshModule &pi = *moduleFrames.at(module_frame);
pi.drawFrame(display, state, x, y); 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. * 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]; 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; int32_t rawRGB = uiconfig.screen_rgb_color;
// Only validate the combined value once
if (rawRGB > 0 && rawRGB <= 255255255) { if (rawRGB > 0 && rawRGB <= 255255255) {
// Extract each component as a normal int first uint8_t TFT_MESH_r = (rawRGB >> 16) & 0xFF;
int r = (rawRGB >> 16) & 0xFF; uint8_t TFT_MESH_g = (rawRGB >> 8) & 0xFF;
int g = (rawRGB >> 8) & 0xFF; uint8_t TFT_MESH_b = rawRGB & 0xFF;
int b = rawRGB & 0xFF; LOG_INFO("Values of r,g,b: %d, %d, %d", TFT_MESH_r, TFT_MESH_g, TFT_MESH_b);
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)); 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() void Screen::setup()
{ {
// Enable display rendering // === Enable display rendering ===
useDisplay = true; 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 // For OLED displays (SSD1306), default brightness is 255 if not set
if (uiconfig.screen_brightness == 0) { if (uiconfig.screen_brightness == 0) {
#if defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) #if defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107)
@@ -551,7 +565,7 @@ void Screen::setup()
brightness = uiconfig.screen_brightness; brightness = uiconfig.screen_brightness;
} }
// Detect OLED subtype (if supported by board variant) // === Detect OLED subtype (if supported by board variant) ===
#ifdef AutoOLEDWire_h #ifdef AutoOLEDWire_h
if (isAUTOOled) if (isAUTOOled)
static_cast<AutoOLEDWire *>(dispdev)->setDetected(model); static_cast<AutoOLEDWire *>(dispdev)->setDetected(model);
@@ -573,7 +587,7 @@ void Screen::setup()
static_cast<ST7796Spi *>(dispdev)->setRGB(TFT_MESH); static_cast<ST7796Spi *>(dispdev)->setRGB(TFT_MESH);
#endif #endif
// Initialize display and UI system // === Initialize display and UI system ===
ui->init(); ui->init();
displayWidth = dispdev->width(); displayWidth = dispdev->width();
displayHeight = dispdev->height(); displayHeight = dispdev->height();
@@ -585,7 +599,7 @@ void Screen::setup()
ui->disableAllIndicators(); // Disable page indicator dots ui->disableAllIndicators(); // Disable page indicator dots
ui->getUiState()->userData = this; // Allow static callbacks to access Screen instance ui->getUiState()->userData = this; // Allow static callbacks to access Screen instance
// Apply loaded brightness // === Apply loaded brightness ===
#if defined(ST7789_CS) #if defined(ST7789_CS)
static_cast<TFTDisplay *>(dispdev)->setDisplayBrightness(brightness); static_cast<TFTDisplay *>(dispdev)->setDisplayBrightness(brightness);
#elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SPISSD1306) #elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SPISSD1306)
@@ -593,20 +607,20 @@ void Screen::setup()
#endif #endif
LOG_INFO("Applied screen brightness: %d", brightness); LOG_INFO("Applied screen brightness: %d", brightness);
// Set custom overlay callbacks // === Set custom overlay callbacks ===
static OverlayCallback overlays[] = { static OverlayCallback overlays[] = {
graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame
}; };
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
// Enable UTF-8 to display mapping // === Enable UTF-8 to display mapping ===
dispdev->setFontTableLookupFunction(customFontTableLookup); dispdev->setFontTableLookupFunction(customFontTableLookup);
#ifdef USERPREFS_OEM_TEXT #ifdef USERPREFS_OEM_TEXT
logo_timeout *= 2; // Give more time for branded boot logos logo_timeout *= 2; // Give more time for branded boot logos
#endif #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 EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip slow refresh
alertFrames[0] = [this](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { alertFrames[0] = [this](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) {
#ifdef ARCH_ESP32 #ifdef ARCH_ESP32
@@ -622,10 +636,10 @@ void Screen::setup()
ui->setFrames(alertFrames, 1); ui->setFrames(alertFrames, 1);
ui->disableAutoTransition(); // Require manual navigation between frames 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); 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 #ifdef SCREEN_MIRROR
dispdev->mirrorScreen(); dispdev->mirrorScreen();
#else #else
@@ -643,7 +657,7 @@ void Screen::setup()
} }
#endif #endif
// Generate device ID from MAC address // === Generate device ID from MAC address ===
uint8_t dmac[6]; uint8_t dmac[6];
getMacAddr(dmac); getMacAddr(dmac);
snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]); snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]);
@@ -652,9 +666,9 @@ void Screen::setup()
handleSetOn(false); // Ensure proper init for Arduino targets handleSetOn(false); // Ensure proper init for Arduino targets
#endif #endif
// Turn on display and trigger first draw // === Turn on display and trigger first draw ===
handleSetOn(true); handleSetOn(true);
graphics::currentResolution = graphics::determineScreenResolution(dispdev->height(), dispdev->width()); determineResolution(dispdev->height(), dispdev->width());
ui->update(); ui->update();
#ifndef USE_EINK #ifndef USE_EINK
ui->update(); // Some SSD1306 clones drop the first draw, so run twice ui->update(); // Some SSD1306 clones drop the first draw, so run twice
@@ -675,7 +689,7 @@ void Screen::setup()
touchScreenImpl1->init(); touchScreenImpl1->init();
#endif #endif
// Subscribe to device status updates // === Subscribe to device status updates ===
powerStatusObserver.observe(&powerStatus->onNewStatus); powerStatusObserver.observe(&powerStatus->onNewStatus);
gpsStatusObserver.observe(&gpsStatus->onNewStatus); gpsStatusObserver.observe(&gpsStatus->onNewStatus);
nodeStatusObserver.observe(&nodeStatus->onNewStatus); nodeStatusObserver.observe(&nodeStatus->onNewStatus);
@@ -683,14 +697,12 @@ void Screen::setup()
#if !MESHTASTIC_EXCLUDE_ADMIN #if !MESHTASTIC_EXCLUDE_ADMIN
adminMessageObserver.observe(adminModule); adminMessageObserver.observe(adminModule);
#endif #endif
if (textMessageModule)
textMessageObserver.observe(textMessageModule);
if (inputBroker) if (inputBroker)
inputObserver.observe(inputBroker); inputObserver.observe(inputBroker);
// Load persisted messages into RAM // === Notify modules that support UI events ===
messageStore.loadFromFlash();
LOG_INFO("MessageStore loaded from flash");
// Notify modules that support UI events
MeshModule::observeUIEvents(&uiFrameEventObserver); MeshModule::observeUIEvents(&uiFrameEventObserver);
} }
@@ -761,23 +773,6 @@ int32_t Screen::runOnce()
if (displayHeight == 0) { if (displayHeight == 0) {
displayHeight = dispdev->getHeight(); 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); menuHandler::handleMenuSwitch(dispdev);
// Show boot screen for first logo_timeout seconds, then switch to normal operation. // Show boot screen for first logo_timeout seconds, then switch to normal operation.
@@ -833,17 +828,17 @@ int32_t Screen::runOnce()
break; break;
case Cmd::ON_PRESS: case Cmd::ON_PRESS:
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
showFrame(FrameDirection::NEXT); handleOnPress();
} }
break; break;
case Cmd::SHOW_PREV_FRAME: case Cmd::SHOW_PREV_FRAME:
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
showFrame(FrameDirection::PREVIOUS); handleShowPrevFrame();
} }
break; break;
case Cmd::SHOW_NEXT_FRAME: case Cmd::SHOW_NEXT_FRAME:
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
showFrame(FrameDirection::NEXT); handleShowNextFrame();
} }
break; break;
case Cmd::START_ALERT_FRAME: { case Cmd::START_ALERT_FRAME: {
@@ -864,7 +859,6 @@ int32_t Screen::runOnce()
break; break;
case Cmd::STOP_ALERT_FRAME: case Cmd::STOP_ALERT_FRAME:
NotificationRenderer::pauseBanner = false; NotificationRenderer::pauseBanner = false;
break;
case Cmd::STOP_BOOT_SCREEN: case Cmd::STOP_BOOT_SCREEN:
EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
@@ -1035,6 +1029,9 @@ void Screen::setFrames(FrameFocus focus)
} }
#endif #endif
// Declare this early so its available in FOCUS_PRESERVE block
bool willInsertTextMessage = shouldDrawMessage(&devicestate.rx_text_message);
if (!hiddenFrames.home) { if (!hiddenFrames.home) {
fsi.positions.home = numframes; fsi.positions.home = numframes;
normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused; normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused;
@@ -1046,16 +1043,11 @@ void Screen::setFrames(FrameFocus focus)
indicatorIcons.push_back(icon_mail); indicatorIcons.push_back(icon_mail);
#ifndef USE_EINK #ifndef USE_EINK
if (!hiddenFrames.nodelist_nodes) { if (!hiddenFrames.nodelist) {
fsi.positions.nodelist_nodes = numframes; fsi.positions.nodelist = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicListScreen_Nodes; normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicNodeListScreen;
indicatorIcons.push_back(icon_nodes); 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 #endif
// Show detailed node views only on E-Ink builds // Show detailed node views only on E-Ink builds
@@ -1077,13 +1069,11 @@ void Screen::setFrames(FrameFocus focus)
} }
#endif #endif
#if HAS_GPS #if HAS_GPS
#ifdef USE_EINK
if (!hiddenFrames.nodelist_bearings) { if (!hiddenFrames.nodelist_bearings) {
fsi.positions.nodelist_bearings = numframes; fsi.positions.nodelist_bearings = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses; normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses;
indicatorIcons.push_back(icon_list); indicatorIcons.push_back(icon_list);
} }
#endif
if (!hiddenFrames.gps) { if (!hiddenFrames.gps) {
fsi.positions.gps = numframes; fsi.positions.gps = numframes;
normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen; 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 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); LOG_DEBUG("Finished build frames. numframes: %d", numframes);
ui->setFrames(normalFrames, numframes); ui->setFrames(normalFrames, numframes);
@@ -1203,6 +1193,10 @@ void Screen::setFrames(FrameFocus focus)
case FOCUS_FAULT: case FOCUS_FAULT:
ui->switchToFrame(fsi.positions.fault); ui->switchToFrame(fsi.positions.fault);
break; break;
case FOCUS_TEXTMESSAGE:
hasUnreadMessage = false; // ✅ Clear when message is *viewed*
ui->switchToFrame(fsi.positions.textMessage);
break;
case FOCUS_MODULE: case FOCUS_MODULE:
// Whichever frame was marked by MeshModule::requestFocus(), if any // Whichever frame was marked by MeshModule::requestFocus(), if any
// If no module requested focus, will show the first frame instead // 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) void Screen::toggleFrameVisibility(const std::string &frameName)
{ {
#ifndef USE_EINK #ifndef USE_EINK
if (frameName == "nodelist_nodes") { if (frameName == "nodelist") {
hiddenFrames.nodelist_nodes = !hiddenFrames.nodelist_nodes; hiddenFrames.nodelist = !hiddenFrames.nodelist;
}
if (frameName == "nodelist_location") {
hiddenFrames.nodelist_location = !hiddenFrames.nodelist_location;
} }
#endif #endif
#ifdef USE_EINK #ifdef USE_EINK
@@ -1264,11 +1255,9 @@ void Screen::toggleFrameVisibility(const std::string &frameName)
} }
#endif #endif
#if HAS_GPS #if HAS_GPS
#ifdef USE_EINK
if (frameName == "nodelist_bearings") { if (frameName == "nodelist_bearings") {
hiddenFrames.nodelist_bearings = !hiddenFrames.nodelist_bearings; hiddenFrames.nodelist_bearings = !hiddenFrames.nodelist_bearings;
} }
#endif
if (frameName == "gps") { if (frameName == "gps") {
hiddenFrames.gps = !hiddenFrames.gps; hiddenFrames.gps = !hiddenFrames.gps;
} }
@@ -1290,10 +1279,8 @@ void Screen::toggleFrameVisibility(const std::string &frameName)
bool Screen::isFrameHidden(const std::string &frameName) const bool Screen::isFrameHidden(const std::string &frameName) const
{ {
#ifndef USE_EINK #ifndef USE_EINK
if (frameName == "nodelist_nodes") if (frameName == "nodelist")
return hiddenFrames.nodelist_nodes; return hiddenFrames.nodelist;
if (frameName == "nodelist_location")
return hiddenFrames.nodelist_location;
#endif #endif
#ifdef USE_EINK #ifdef USE_EINK
if (frameName == "nodelist_lastheard") if (frameName == "nodelist_lastheard")
@@ -1304,10 +1291,8 @@ bool Screen::isFrameHidden(const std::string &frameName) const
return hiddenFrames.nodelist_distance; return hiddenFrames.nodelist_distance;
#endif #endif
#if HAS_GPS #if HAS_GPS
#ifdef USE_EINK
if (frameName == "nodelist_bearings") if (frameName == "nodelist_bearings")
return hiddenFrames.nodelist_bearings; return hiddenFrames.nodelist_bearings;
#endif
if (frameName == "gps") if (frameName == "gps")
return hiddenFrames.gps; return hiddenFrames.gps;
#endif #endif
@@ -1323,6 +1308,37 @@ bool Screen::isFrameHidden(const std::string &frameName) const
return false; 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() void Screen::handleStartFirmwareUpdateScreen()
{ {
LOG_DEBUG("Show firmware screen"); 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*/ /* 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() void Screen::handleOnPress()
{ {
// If screen was off, just wake it, otherwise advance to next frame // 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) { if (ui->getUiState()->frameState == FIXED) {
if (direction == FrameDirection::NEXT) {
ui->nextFrame();
} else {
ui->previousFrame(); ui->previousFrame();
lastScreenTransition = millis();
setFastFramerate();
}
} }
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(); lastScreenTransition = millis();
setFastFramerate(); setFastFramerate();
} }
@@ -1422,6 +1466,7 @@ void Screen::setFastFramerate()
int Screen::handleStatusUpdate(const meshtastic::Status *arg) int Screen::handleStatusUpdate(const meshtastic::Status *arg)
{ {
// LOG_DEBUG("Screen got status update %d", arg->getStatusType());
switch (arg->getStatusType()) { switch (arg->getStatusType()) {
case STATUS_TYPE_NODE: case STATUS_TYPE_NODE:
if (showingNormalScreen && nodeStatus->getLastNumTotal() != nodeStatus->getNumTotal()) { if (showingNormalScreen && nodeStatus->getLastNumTotal() != nodeStatus->getNumTotal()) {
@@ -1539,11 +1584,11 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
screen->showSimpleBanner(banner, 3000); screen->showSimpleBanner(banner, 3000);
} else if (!channel.settings.has_module_settings || !channel.settings.module_settings.is_muted) { } else if (!channel.settings.has_module_settings || !channel.settings.module_settings.is_muted) {
if (longName && longName[0]) { if (longName && longName[0]) {
if (currentResolution == ScreenResolution::UltraLow) { #if defined(M5STACK_UNITC6L)
strcpy(banner, "New Message"); strcpy(banner, "New Message");
} else { #else
snprintf(banner, sizeof(banner), "New Message from\n%s", longName); snprintf(banner, sizeof(banner), "New Message from\n%s", longName);
} #endif
} else { } else {
strcpy(banner, "New Message"); strcpy(banner, "New Message");
} }
@@ -1579,28 +1624,18 @@ int Screen::handleUIFrameEvent(const UIFrameEvent *event)
if (showingNormalScreen) { if (showingNormalScreen) {
// Regenerate the frameset, potentially honoring a module's internal requestFocus() call // 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); setFrames(FOCUS_MODULE);
}
// Regenerate the frameset, while attempting to maintain focus on the current frame // Regenerate the frameset, while Attempt to maintain focus on the current frame
else if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET_BACKGROUND) { else if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET_BACKGROUND)
setFrames(FOCUS_PRESERVE); setFrames(FOCUS_PRESERVE);
}
// Don't regenerate the frameset, just re-draw whatever is on screen ASAP // 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(); 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; return 0;
} }
@@ -1636,48 +1671,7 @@ int Screen::handleInputEvent(const InputEvent *event)
menuHandler::handleMenuSwitch(dispdev); menuHandler::handleMenuSwitch(dispdev);
return 0; 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;
}
}
}
// UP/DOWN in node list screens scrolls through node pages
if (ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_nodes ||
ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_location ||
ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard ||
ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal ||
ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance ||
ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_bearings) {
if (event->inputEvent == INPUT_BROKER_UP) {
graphics::NodeListRenderer::scrollUp();
setFastFramerate();
return 0;
}
if (event->inputEvent == INPUT_BROKER_DOWN) {
graphics::NodeListRenderer::scrollDown();
setFastFramerate();
return 0;
}
}
// Use left or right input from a keyboard to move between frames, // 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 // so long as a mesh module isn't using these events for some other purpose
if (showingNormalScreen) { if (showingNormalScreen) {
@@ -1691,39 +1685,16 @@ int Screen::handleInputEvent(const InputEvent *event)
// If no modules are using the input, move between frames // If no modules are using the input, move between frames
if (!inputIntercepted) { 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) { 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) { } 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) { } else if (event->inputEvent == INPUT_BROKER_UP_LONG) {
// Long press up button for fast frame switching // Long press up button for fast frame switching
showPrevFrame(); showPrevFrame();
} else if (event->inputEvent == INPUT_BROKER_DOWN_LONG) { } else if (event->inputEvent == INPUT_BROKER_DOWN_LONG) {
// Long press down button for fast frame switching // Long press down button for fast frame switching
showNextFrame(); 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) { } else if (event->inputEvent == INPUT_BROKER_SELECT) {
if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) { if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) {
menuHandler::homeBaseMenu(); menuHandler::homeBaseMenu();
@@ -1738,21 +1709,20 @@ int Screen::handleInputEvent(const InputEvent *event)
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) { } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) {
menuHandler::loraMenu(); menuHandler::loraMenu();
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) { } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) {
if (!messageStore.getMessages().empty()) { if (devicestate.rx_text_message.from) {
menuHandler::messageResponseMenu(); menuHandler::messageResponseMenu();
} else { } else {
if (currentResolution == ScreenResolution::UltraLow) { #if defined(M5STACK_UNITC6L)
menuHandler::textMessageMenu(); menuHandler::textMessageMenu();
} else { #else
menuHandler::textMessageBaseMenu(); menuHandler::textMessageBaseMenu();
} #endif
} }
} else if (framesetInfo.positions.firstFavorite != 255 && } else if (framesetInfo.positions.firstFavorite != 255 &&
this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite && this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite &&
this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) { this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) {
menuHandler::favoriteBaseMenu(); menuHandler::favoriteBaseMenu();
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_nodes || } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist ||
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_location ||
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard || this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard ||
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal || this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal ||
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance || this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance ||
@@ -1763,7 +1733,7 @@ int Screen::handleInputEvent(const InputEvent *event)
menuHandler::wifiBaseMenu(); menuHandler::wifiBaseMenu();
} }
} else if (event->inputEvent == INPUT_BROKER_BACK) { } else if (event->inputEvent == INPUT_BROKER_BACK) {
showFrame(FrameDirection::PREVIOUS); showPrevFrame();
} else if (event->inputEvent == INPUT_BROKER_CANCEL) { } else if (event->inputEvent == INPUT_BROKER_CANCEL) {
setOn(false); setOn(false);
} }

View File

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

View File

@@ -15,49 +15,27 @@
namespace graphics namespace graphics
{ {
ScreenResolution determineScreenResolution(int16_t screenheight, int16_t screenwidth) void determineResolution(int16_t screenheight, int16_t screenwidth)
{ {
#ifdef FORCE_LOW_RES #ifdef FORCE_LOW_RES
return ScreenResolution::Low; isHighResolution = false;
#else
// Unit C6L and other ultra low res screens
if (screenwidth <= 64 || screenheight <= 48) {
return ScreenResolution::UltraLow;
}
// Standard OLED screens
if (screenwidth > 128 && screenheight <= 64) {
return ScreenResolution::Low;
}
// High Resolutions screens like T114, TDeck, TLora Pager, etc
if (screenwidth > 128) {
return ScreenResolution::High;
}
// Default to low resolution
return ScreenResolution::Low;
#endif
}
void decomposeTime(uint32_t rtc_sec, int &hour, int &minute, int &second)
{
hour = 0;
minute = 0;
second = 0;
if (rtc_sec == 0)
return; return;
uint32_t hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY; #endif
hour = hms / SEC_PER_HOUR;
minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; if (screenwidth > 128) {
second = hms % SEC_PER_MIN; isHighResolution = true;
}
if (screenwidth > 128 && screenheight <= 64) {
isHighResolution = false;
}
} }
// === Shared External State === // === Shared External State ===
bool hasUnreadMessage = false; bool hasUnreadMessage = false;
bool isMuted = false; bool isMuted = false;
ScreenResolution currentResolution = ScreenResolution::Low; bool isHighResolution = false;
// === Internal State === // === Internal State ===
bool isBoltVisibleShared = true; bool isBoltVisibleShared = true;
@@ -113,7 +91,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
display->setColor(BLACK); display->setColor(BLACK);
display->fillRect(0, 0, screenW, highlightHeight + 2); display->fillRect(0, 0, screenW, highlightHeight + 2);
display->setColor(WHITE); display->setColor(WHITE);
if (currentResolution == ScreenResolution::High) { if (isHighResolution) {
display->drawLine(0, 20, screenW, 20); display->drawLine(0, 20, screenW, 20);
} else { } else {
display->drawLine(0, 14, screenW, 14); display->drawLine(0, 14, screenW, 14);
@@ -151,7 +129,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
} }
#endif #endif
bool useHorizontalBattery = (currentResolution == ScreenResolution::High && screenW >= screenH); bool useHorizontalBattery = (isHighResolution && screenW >= screenH);
const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2;
int batteryX = 1; int batteryX = 1;
@@ -161,7 +139,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
if (usbPowered && !isCharging) { // This is a basic check to determine USB Powered is flagged but not charging if (usbPowered && !isCharging) { // This is a basic check to determine USB Powered is flagged but not charging
batteryX += 1; batteryX += 1;
batteryY += 2; batteryY += 2;
if (currentResolution == ScreenResolution::High) { if (isHighResolution) {
display->drawXbm(batteryX, batteryY, 19, 12, imgUSB_HighResolution); display->drawXbm(batteryX, batteryY, 19, 12, imgUSB_HighResolution);
batteryX += 20; // Icon + 1 pixel batteryX += 20; // Icon + 1 pixel
} else { } else {
@@ -222,8 +200,8 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
if (rtc_sec > 0) { if (rtc_sec > 0) {
// === Build Time String === // === Build Time String ===
long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY; long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY;
int hour, minute, second; int hour = hms / SEC_PER_HOUR;
graphics::decomposeTime(rtc_sec, hour, minute, second); int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute); snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute);
// === Build Date String === // === Build Date String ===
@@ -231,7 +209,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, false); UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, false);
char dateLine[40]; char dateLine[40];
if (currentResolution == ScreenResolution::High) { if (isHighResolution) {
snprintf(dateLine, sizeof(dateLine), "%s", datetimeStr); snprintf(dateLine, sizeof(dateLine), "%s", datetimeStr);
} else { } else {
if (hasUnreadMessage) { if (hasUnreadMessage) {
@@ -307,7 +285,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
display->drawXbm(iconX, iconY, mail_width, mail_height, mail); display->drawXbm(iconX, iconY, mail_width, mail_height, mail);
} }
} else if (isMuted) { } else if (isMuted) {
if (currentResolution == ScreenResolution::High) { if (isHighResolution) {
int iconX = iconRightEdge - mute_symbol_big_width; int iconX = iconRightEdge - mute_symbol_big_width;
int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2; int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2;
@@ -384,7 +362,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
display->drawXbm(iconX, iconY, mail_width, mail_height, mail); display->drawXbm(iconX, iconY, mail_width, mail_height, mail);
} }
} else if (isMuted) { } else if (isMuted) {
if (currentResolution == ScreenResolution::High) { if (isHighResolution) {
int iconX = iconRightEdge - mute_symbol_big_width; int iconX = iconRightEdge - mute_symbol_big_width;
int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2; int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2;
display->drawXbm(iconX, iconY, mute_symbol_big_width, mute_symbol_big_height, mute_symbol_big); display->drawXbm(iconX, iconY, mute_symbol_big_width, mute_symbol_big_height, mute_symbol_big);
@@ -403,7 +381,7 @@ const int *getTextPositions(OLEDDisplay *display)
{ {
static int textPositions[7]; // Static array that persists beyond function scope static int textPositions[7]; // Static array that persists beyond function scope
if (currentResolution == ScreenResolution::High) { if (isHighResolution) {
textPositions[0] = textZeroLine; textPositions[0] = textZeroLine;
textPositions[1] = textFirstLine_medium; textPositions[1] = textFirstLine_medium;
textPositions[2] = textSecondLine_medium; textPositions[2] = textSecondLine_medium;
@@ -436,12 +414,8 @@ void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y)
} }
if (drawConnectionState) { if (drawConnectionState) {
const int scale = (currentResolution == ScreenResolution::High) ? 2 : 1; if (isHighResolution) {
display->setColor(BLACK); const int scale = 2;
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 (currentResolution == ScreenResolution::High) {
const int bytesPerRow = (connection_icon_width + 7) / 8; const int bytesPerRow = (connection_icon_width + 7) / 8;
int iconX = 0; int iconX = 0;
int iconY = SCREEN_HEIGHT - (connection_icon_height * 2); int iconY = SCREEN_HEIGHT - (connection_icon_height * 2);

View File

@@ -42,11 +42,8 @@ namespace graphics
// Shared state (declare inside namespace) // Shared state (declare inside namespace)
extern bool hasUnreadMessage; extern bool hasUnreadMessage;
extern bool isMuted; extern bool isMuted;
enum class ScreenResolution : uint8_t { UltraLow = 0, Low = 1, High = 2 }; extern bool isHighResolution;
extern ScreenResolution currentResolution; void determineResolution(int16_t screenheight, int16_t screenwidth);
ScreenResolution determineScreenResolution(int16_t screenheight, int16_t screenwidth);
void decomposeTime(uint32_t rtc_sec, int &hour, int &minute, int &second);
// Rounded highlight (used for inverted headers) // 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); 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) { if (screenHeight <= 64) {
textY = boxY + (boxHeight - inputLineH) / 2; textY = boxY + (boxHeight - inputLineH) / 2;
} else { } else {
const int innerLeft = boxX + 1;
const int innerRight = boxX + boxWidth - 2;
const int innerTop = boxY + 1; const int innerTop = boxY + 1;
const int innerBottom = boxY + boxHeight - 2; const int innerBottom = boxY + boxHeight - 2;

View File

@@ -1,10 +1,15 @@
#include "configuration.h" #include "configuration.h"
#if HAS_SCREEN #if HAS_SCREEN
#include "ClockRenderer.h" #include "ClockRenderer.h"
#include "NodeDB.h"
#include "UIRenderer.h"
#include "configuration.h"
#include "gps/GeoCoord.h"
#include "gps/RTC.h" #include "gps/RTC.h"
#include "graphics/ScreenFonts.h" #include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h" #include "graphics/SharedUIDisplay.h"
#include "graphics/draw/UIRenderer.h" #include "graphics/draw/UIRenderer.h"
#include "graphics/emotes.h"
#include "graphics/images.h" #include "graphics/images.h"
#include "main.h" #include "main.h"
@@ -18,31 +23,6 @@ namespace graphics
namespace ClockRenderer 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) void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale)
{ {
uint16_t segmentWidth = SEGMENT_WIDTH * 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 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; 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) void drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t number, float scale)
{ {
// Read 7-segment pattern for the digit from flash // the numbers 0-9, each expressed as an array of seven boolean (0|1) values encoding the on/off state of
uint8_t seg[7]; // segment {innerIndex + 1}
for (uint8_t i = 0; i < 7; i++) { // e.g., to display the numeral '0', segments 1-6 are on, and segment 7 is off.
seg[i] = pgm_read_byte(&digitSegments[number][i]); 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 segmentWidth = SEGMENT_WIDTH * scale;
uint16_t segmentHeight = SEGMENT_HEIGHT * scale; uint16_t segmentHeight = SEGMENT_HEIGHT * scale;
// Precompute segment positions // segment x and y coordinates
uint16_t segmentOneX = x + segmentHeight + 2; uint16_t segmentOneX = x + segmentHeight + 2;
uint16_t segmentOneY = y; 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 segmentSevenX = segmentOneX;
uint16_t segmentSevenY = segmentTwoY + segmentWidth + 2; uint16_t segmentSevenY = segmentTwoY + segmentWidth + 2;
// Draw only the active segments if (numbers[number][0]) {
if (seg[0]) graphics::ClockRenderer::drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight);
drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight); }
if (seg[1])
drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight); if (numbers[number][1]) {
if (seg[2]) graphics::ClockRenderer::drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight);
drawVerticalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight); }
if (seg[3])
drawHorizontalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight); if (numbers[number][2]) {
if (seg[4]) graphics::ClockRenderer::drawVerticalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight);
drawVerticalSegment(display, segmentFiveX, segmentFiveY, segmentWidth, segmentHeight); }
if (seg[5])
drawVerticalSegment(display, segmentSixX, segmentSixY, segmentWidth, segmentHeight); if (numbers[number][3]) {
if (seg[6]) graphics::ClockRenderer::drawHorizontalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight);
drawHorizontalSegment(display, segmentSevenX, segmentSevenY, 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) 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); 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) void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{ {
display->clear(); display->clear();
@@ -146,6 +192,7 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
const char *titleStr = ""; const char *titleStr = "";
// === Header === // === Header ===
graphics::drawCommonHeader(display, x, y, titleStr, true, true); graphics::drawCommonHeader(display, x, y, titleStr, true, true);
int line = 0;
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
char timeString[16]; char timeString[16];
@@ -190,7 +237,7 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
float target_width = display->getWidth() * screenwidth_target_ratio; float target_width = display->getWidth() * screenwidth_target_ratio;
float target_height = float target_height =
display->getHeight() - display->getHeight() -
((currentResolution == ScreenResolution::High) (isHighResolution
? 46 ? 46
: 33); // Be careful adjusting this number, we have to account for header and the text under the time : 33); // Be careful adjusting this number, we have to account for header and the text under the time
@@ -221,9 +268,10 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
scaleInitialized = true; scaleInitialized = true;
} }
// calculate hours:minutes string width
size_t len = strlen(timeString); 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++) { for (size_t i = 0; i < len; i++) {
char character = timeString[i]; char character = timeString[i];
@@ -262,16 +310,9 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
// draw seconds string + AM/PM // draw seconds string + AM/PM
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
int xOffset = -1; int xOffset = (isHighResolution) ? 0 : -1;
if (currentResolution == ScreenResolution::High) {
xOffset = 0;
}
if (hour >= 10) { if (hour >= 10) {
if (currentResolution == ScreenResolution::High) { xOffset += (isHighResolution) ? 32 : 18;
xOffset += 32;
} else {
xOffset += 18;
}
} }
if (config.display.use_12h_clock) { if (config.display.use_12h_clock) {
@@ -279,7 +320,7 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
} }
#ifndef USE_EINK #ifndef USE_EINK
xOffset = (currentResolution == ScreenResolution::High) ? 18 : 10; xOffset = (isHighResolution) ? 18 : 10;
if (scale >= 2.0f) { if (scale >= 2.0f) {
xOffset -= (int)(4.5f * scale); xOffset -= (int)(4.5f * scale);
} }
@@ -298,13 +339,19 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
const char *titleStr = ""; const char *titleStr = "";
// === Header === // === Header ===
graphics::drawCommonHeader(display, x, y, titleStr, true, true); graphics::drawCommonHeader(display, x, y, titleStr, true, true);
int line = 0;
// clock face center coordinates // clock face center coordinates
int16_t centerX = display->getWidth() / 2; int16_t centerX = display->getWidth() / 2;
int16_t centerY = display->getHeight() / 2; int16_t centerY = display->getHeight() / 2;
// clock face radius // 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 #ifdef T_WATCH_S3
radius = (display->getWidth() / 2) * 0.8; radius = (display->getWidth() / 2) * 0.8;
#endif #endif
@@ -319,8 +366,17 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
// tick mark outer y coordinate; (first nested circle) // tick mark outer y coordinate; (first nested circle)
int16_t tickMarkOuterNoonY = secondHandNoonY; int16_t tickMarkOuterNoonY = secondHandNoonY;
double secondsTickMarkInnerNoonY = noonY + ((currentResolution == ScreenResolution::High) ? 8 : 4); // seconds tick mark inner y coordinate; (second nested circle)
double hoursTickMarkInnerNoonY = noonY + ((currentResolution == ScreenResolution::High) ? 16 : 6); 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 // minute hand y coordinate
int16_t minuteHandNoonY = secondsTickMarkInnerNoonY + 4; int16_t minuteHandNoonY = secondsTickMarkInnerNoonY + 4;
@@ -330,7 +386,7 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
// hour hand radius and y coordinate // hour hand radius and y coordinate
int16_t hourHandRadius = radius * 0.35; int16_t hourHandRadius = radius * 0.35;
if (currentResolution == ScreenResolution::High) { if (isHighResolution) {
hourHandRadius = radius * 0.55; hourHandRadius = radius * 0.55;
} }
int16_t hourHandNoonY = centerY - hourHandRadius; int16_t hourHandNoonY = centerY - hourHandRadius;
@@ -340,13 +396,19 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
if (rtc_sec > 0) { if (rtc_sec > 0) {
int hour, minute, second; long hms = rtc_sec % SEC_PER_DAY;
decomposeTime(rtc_sec, hour, minute, second); 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
if (config.display.use_12h_clock) {
bool isPM = hour >= 12; bool isPM = hour >= 12;
if (config.display.use_12h_clock) {
isPM = hour >= 12;
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
int yOffset = (currentResolution == ScreenResolution::High) ? 1 : 0; int yOffset = isHighResolution ? 1 : 0;
#ifdef USE_EINK #ifdef USE_EINK
yOffset += 3; yOffset += 3;
#endif #endif
@@ -437,13 +499,12 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt); display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt);
#else #else
#ifdef USE_EINK #ifdef USE_EINK
if (currentResolution == ScreenResolution::High) { if (isHighResolution) {
// draw hour number // draw hour number
display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt); display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt);
} }
#else #else
if (currentResolution == ScreenResolution::High && if (isHighResolution && (hourInt == 3 || hourInt == 6 || hourInt == 9 || hourInt == 12)) {
(hourInt == 3 || hourInt == 6 || hourInt == 9 || hourInt == 12)) {
// draw hour number // draw hour number
display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt); display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt);
} }
@@ -455,7 +516,7 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
double startX = sineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + noonX; double startX = sineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + noonX;
double startY = cosineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + centerY; double startY = cosineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + centerY;
if (currentResolution == ScreenResolution::High) { if (isHighResolution) {
// draw minute tick mark // draw minute tick mark
display->drawLine(startX, startY, endX, endY); display->drawLine(startX, startY, endX, endY);
} }

View File

@@ -48,7 +48,7 @@ void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY,
// This could draw a "N" indicator or north arrow // This could draw a "N" indicator or north arrow
// For now, we'll draw a simple north indicator // For now, we'll draw a simple north indicator
// const float radius = 17.0f; // const float radius = 17.0f;
if (currentResolution == ScreenResolution::High) { if (isHighResolution) {
radius += 4; radius += 4;
} }
Point north(0, -radius); Point north(0, -radius);
@@ -59,7 +59,7 @@ void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY,
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
display->setTextAlignment(TEXT_ALIGN_CENTER); display->setTextAlignment(TEXT_ALIGN_CENTER);
display->setColor(BLACK); display->setColor(BLACK);
if (currentResolution == ScreenResolution::High) { if (isHighResolution) {
display->fillRect(north.x - 8, north.y - 1, display->getStringWidth("N") + 3, FONT_HEIGHT_SMALL - 6); display->fillRect(north.x - 8, north.y - 1, display->getStringWidth("N") + 3, FONT_HEIGHT_SMALL - 6);
} else { } else {
display->fillRect(north.x - 4, north.y - 1, display->getStringWidth("N") + 2, FONT_HEIGHT_SMALL - 6); display->fillRect(north.x - 4, north.y - 1, display->getStringWidth("N") + 2, FONT_HEIGHT_SMALL - 6);

View File

@@ -282,13 +282,13 @@ void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
std::string uptime = UIRenderer::drawTimeDelta(days, hours, minutes, seconds); std::string uptime = UIRenderer::drawTimeDelta(days, hours, minutes, seconds);
// Line 1 (Still) // Line 1 (Still)
if (currentResolution != graphics::ScreenResolution::UltraLow) { #if !defined(M5STACK_UNITC6L)
display->drawString(x + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str()); display->drawString(x + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str());
if (config.display.heading_bold) if (config.display.heading_bold)
display->drawString(x - 1 + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str()); display->drawString(x - 1 + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str());
display->setColor(WHITE); display->setColor(WHITE);
} #endif
// Setup string to assemble analogClock string // Setup string to assemble analogClock string
std::string analogClock = ""; std::string analogClock = "";
@@ -301,8 +301,9 @@ void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
// Tear apart hms into h:m:s // Tear apart hms into h:m:s
int hour, min, sec; int hour = hms / SEC_PER_HOUR;
graphics::decomposeTime(rtc_sec, hour, min, sec); 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]; char timebuf[12];
@@ -378,7 +379,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
int line = 1; int line = 1;
// === Set Title // === Set Title
const char *titleStr = (currentResolution == ScreenResolution::High) ? "LoRa Info" : "LoRa"; const char *titleStr = (isHighResolution) ? "LoRa Info" : "LoRa";
// === Header === // === Header ===
graphics::drawCommonHeader(display, x, y, titleStr); graphics::drawCommonHeader(display, x, y, titleStr);
@@ -390,11 +391,11 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
char shortnameble[35]; char shortnameble[35];
getMacAddr(dmac); getMacAddr(dmac);
snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]); snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]);
if (currentResolution == ScreenResolution::UltraLow) { #if defined(M5STACK_UNITC6L)
snprintf(shortnameble, sizeof(shortnameble), "%s", screen->ourId); snprintf(shortnameble, sizeof(shortnameble), "%s", screen->ourId);
} else { #else
snprintf(shortnameble, sizeof(shortnameble), "BLE: %s", screen->ourId); snprintf(shortnameble, sizeof(shortnameble), "BLE: %s", screen->ourId);
} #endif
int textWidth = display->getStringWidth(shortnameble); int textWidth = display->getStringWidth(shortnameble);
int nameX = (SCREEN_WIDTH - textWidth); int nameX = (SCREEN_WIDTH - textWidth);
display->drawString(nameX, getTextPositions(display)[line++], shortnameble); display->drawString(nameX, getTextPositions(display)[line++], shortnameble);
@@ -413,11 +414,11 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
char regionradiopreset[25]; char regionradiopreset[25];
const char *region = myRegion ? myRegion->name : NULL; const char *region = myRegion ? myRegion->name : NULL;
if (region != nullptr) { if (region != nullptr) {
if (currentResolution == ScreenResolution::UltraLow) { #if defined(M5STACK_UNITC6L)
snprintf(regionradiopreset, sizeof(regionradiopreset), "%s", region); snprintf(regionradiopreset, sizeof(regionradiopreset), "%s", region);
} else { #else
snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode); snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode);
} #endif
} }
textWidth = display->getStringWidth(regionradiopreset); textWidth = display->getStringWidth(regionradiopreset);
nameX = (SCREEN_WIDTH - textWidth) / 2; nameX = (SCREEN_WIDTH - textWidth) / 2;
@@ -429,17 +430,17 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
float freq = RadioLibInterface::instance->getFreq(); float freq = RadioLibInterface::instance->getFreq();
snprintf(freqStr, sizeof(freqStr), "%.3f", freq); snprintf(freqStr, sizeof(freqStr), "%.3f", freq);
if (config.lora.channel_num == 0) { if (config.lora.channel_num == 0) {
if (currentResolution == ScreenResolution::UltraLow) { #if defined(M5STACK_UNITC6L)
snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz", freqStr); snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz", freqStr);
} else { #else
snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %sMHz", freqStr); snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %sMHz", freqStr);
} #endif
} else { } else {
if (currentResolution == ScreenResolution::UltraLow) { #if defined(M5STACK_UNITC6L)
snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz (%d)", freqStr, config.lora.channel_num); snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz (%d)", freqStr, config.lora.channel_num);
} else { #else
snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Ch: %sMHz (%d)", freqStr, config.lora.channel_num); snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Ch: %sMHz (%d)", freqStr, config.lora.channel_num);
} #endif
} }
size_t len = strlen(frequencyslot); size_t len = strlen(frequencyslot);
if (len >= 4 && strcmp(frequencyslot + len - 4, " (0)") == 0) { if (len >= 4 && strcmp(frequencyslot + len - 4, " (0)") == 0) {
@@ -455,13 +456,12 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
char chUtilPercentage[10]; char chUtilPercentage[10];
snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent()); snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent());
int chUtil_x = (currentResolution == ScreenResolution::High) ? display->getStringWidth(chUtil) + 10 int chUtil_x = (isHighResolution) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5;
: display->getStringWidth(chUtil) + 5;
int chUtil_y = getTextPositions(display)[line] + 3; int chUtil_y = getTextPositions(display)[line] + 3;
int chutil_bar_width = (currentResolution == ScreenResolution::High) ? 100 : 50; int chutil_bar_width = (isHighResolution) ? 100 : 50;
int chutil_bar_height = (currentResolution == ScreenResolution::High) ? 12 : 7; int chutil_bar_height = (isHighResolution) ? 12 : 7;
int extraoffset = (currentResolution == ScreenResolution::High) ? 6 : 3; int extraoffset = (isHighResolution) ? 6 : 3;
int chutil_percent = airTime->channelUtilizationPercent(); int chutil_percent = airTime->channelUtilizationPercent();
int centerofscreen = SCREEN_WIDTH / 2; int centerofscreen = SCREEN_WIDTH / 2;
@@ -530,18 +530,17 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
int line = 1; int line = 1;
const int barHeight = 6; const int barHeight = 6;
const int labelX = x; const int labelX = x;
int barsOffset = (currentResolution == ScreenResolution::High) ? 24 : 0; int barsOffset = (isHighResolution) ? 24 : 0;
#ifdef USE_EINK #ifdef USE_EINK
#ifndef T_DECK_PRO #ifndef T_DECK_PRO
barsOffset -= 12; barsOffset -= 12;
#endif #endif
#endif #endif
int barX = x + barsOffset; #if defined(M5STACK_UNITC6L)
if (currentResolution == ScreenResolution::UltraLow) { const int barX = x + 45 + barsOffset;
barX += 45; #else
} else { const int barX = x + 40 + barsOffset;
barX += 40; #endif
}
auto drawUsageRow = [&](const char *label, uint32_t used, uint32_t total, bool isHeap = false) { auto drawUsageRow = [&](const char *label, uint32_t used, uint32_t total, bool isHeap = false) {
if (total == 0) if (total == 0)
return; return;
@@ -549,7 +548,7 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
int percent = (used * 100) / total; int percent = (used * 100) / total;
char combinedStr[24]; char combinedStr[24];
if (currentResolution == ScreenResolution::High) { if (isHighResolution) {
snprintf(combinedStr, sizeof(combinedStr), "%s%3d%% %u/%uKB", (percent > 80) ? "! " : "", percent, used / 1024, snprintf(combinedStr, sizeof(combinedStr), "%s%3d%% %u/%uKB", (percent > 80) ? "! " : "", percent, used / 1024,
total / 1024); total / 1024);
} else { } else {
@@ -629,33 +628,25 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
line += 1; line += 1;
char appversionstr[35]; char appversionstr[35];
snprintf(appversionstr, sizeof(appversionstr), "Ver: %s", optstr(APP_VERSION));
char appversionstr_formatted[40]; char appversionstr_formatted[40];
char *lastDot = strrchr(appversionstr, '.');
const char *ver = optstr(APP_VERSION); #if defined(M5STACK_UNITC6L)
char verbuf[32];
strncpy(verbuf, ver, sizeof(verbuf) - 1);
verbuf[sizeof(verbuf) - 1] = '\0';
char *lastDot = strrchr(verbuf, '.');
if (currentResolution == ScreenResolution::UltraLow) {
if (lastDot != nullptr) { if (lastDot != nullptr) {
*lastDot = '\0'; *lastDot = '\0'; // truncate string
} }
snprintf(appversionstr, sizeof(appversionstr), "Ver: %s", verbuf); #else
} else {
if (lastDot) { if (lastDot) {
size_t prefixLen = (size_t)(lastDot - verbuf); size_t prefixLen = lastDot - appversionstr;
snprintf(appversionstr_formatted, sizeof(appversionstr_formatted), "Ver: %.*s", (int)prefixLen, verbuf); strncpy(appversionstr_formatted, appversionstr, prefixLen);
appversionstr_formatted[prefixLen] = '\0';
strncat(appversionstr_formatted, " (", sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1); strncat(appversionstr_formatted, " (", sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1);
strncat(appversionstr_formatted, lastDot + 1, sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1); strncat(appversionstr_formatted, lastDot + 1, sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1);
strncat(appversionstr_formatted, ")", sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1); strncat(appversionstr_formatted, ")", sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1);
strncpy(appversionstr, appversionstr_formatted, sizeof(appversionstr) - 1); strncpy(appversionstr, appversionstr_formatted, sizeof(appversionstr) - 1);
appversionstr[sizeof(appversionstr) - 1] = '\0'; appversionstr[sizeof(appversionstr) - 1] = '\0';
} else {
snprintf(appversionstr, sizeof(appversionstr), "Ver: %s", verbuf);
}
} }
#endif
int textWidth = display->getStringWidth(appversionstr); int textWidth = display->getStringWidth(appversionstr);
int nameX = (SCREEN_WIDTH - textWidth) / 2; int nameX = (SCREEN_WIDTH - textWidth) / 2;
@@ -674,7 +665,7 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
const char *clientWord = nullptr; const char *clientWord = nullptr;
// Determine if narrow or wide screen // Determine if narrow or wide screen
if (currentResolution == ScreenResolution::High) { if (isHighResolution) {
clientWord = "Client"; clientWord = "Client";
} else { } else {
clientWord = "App"; clientWord = "App";
@@ -715,23 +706,11 @@ void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int1
int iconX = SCREEN_WIDTH - chirpy_width - (chirpy_width / 3); int iconX = SCREEN_WIDTH - chirpy_width - (chirpy_width / 3);
int iconY = (SCREEN_HEIGHT - chirpy_height) / 2; int iconY = (SCREEN_HEIGHT - chirpy_height) / 2;
int textX_offset = 10; int textX_offset = 10;
if (currentResolution == ScreenResolution::High) { if (isHighResolution) {
iconX = SCREEN_WIDTH - chirpy_width_hirez - (chirpy_width_hirez / 3);
iconY = (SCREEN_HEIGHT - chirpy_height_hirez) / 2;
textX_offset = textX_offset * 4; textX_offset = textX_offset * 4;
const int scale = 2; display->drawXbm(iconX, iconY, chirpy_width_hirez, chirpy_height_hirez, chirpy_hirez);
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);
}
}
}
} else { } else {
display->drawXbm(iconX, iconY, chirpy_width, chirpy_height, chirpy); display->drawXbm(iconX, iconY, chirpy_width, chirpy_height, chirpy);
} }

View File

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

View File

@@ -1,17 +1,14 @@
#include "configuration.h" #include "configuration.h"
#if HAS_SCREEN #if HAS_SCREEN
#include "ClockRenderer.h" #include "ClockRenderer.h"
#include "Default.h"
#include "GPS.h" #include "GPS.h"
#include "MenuHandler.h" #include "MenuHandler.h"
#include "MeshRadio.h" #include "MeshRadio.h"
#include "MeshService.h" #include "MeshService.h"
#include "MessageStore.h"
#include "NodeDB.h" #include "NodeDB.h"
#include "buzz.h" #include "buzz.h"
#include "graphics/Screen.h" #include "graphics/Screen.h"
#include "graphics/SharedUIDisplay.h" #include "graphics/SharedUIDisplay.h"
#include "graphics/draw/MessageRenderer.h"
#include "graphics/draw/UIRenderer.h" #include "graphics/draw/UIRenderer.h"
#include "input/RotaryEncoderInterruptImpl1.h" #include "input/RotaryEncoderInterruptImpl1.h"
#include "input/UpDownInterruptImpl1.h" #include "input/UpDownInterruptImpl1.h"
@@ -137,10 +134,11 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
"NP_865", "NP_865",
"BR_902"}; "BR_902"};
BannerOverlayOptions bannerOptions; BannerOverlayOptions bannerOptions;
bannerOptions.message = "Set the LoRa region"; #if defined(M5STACK_UNITC6L)
if (currentResolution == ScreenResolution::UltraLow) {
bannerOptions.message = "LoRa Region"; bannerOptions.message = "LoRa Region";
} #else
bannerOptions.message = "Set the LoRa region";
#endif
bannerOptions.durationMs = duration; bannerOptions.durationMs = duration;
bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 27; bannerOptions.optionsCount = 27;
@@ -428,415 +426,60 @@ void menuHandler::clockMenu()
}; };
screen->showOverlayBanner(bannerOptions); screen->showOverlayBanner(bannerOptions);
} }
void menuHandler::messageResponseMenu() 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]; if (kb_found) {
static int optionsEnumArray[enumEnd]; optionsArray[options] = "Reply via Freetext";
int options = 0; optionsEnumArray[options++] = Freetext;
}
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;
#ifdef HAS_I2S #ifdef HAS_I2S
optionsArray[options] = "Read Aloud"; optionsArray[options] = "Read Aloud";
optionsEnumArray[options++] = Aloud; optionsEnumArray[options++] = Aloud;
#endif #endif
BannerOverlayOptions bannerOptions; BannerOverlayOptions bannerOptions;
if (currentResolution == ScreenResolution::UltraLow) { #if defined(M5STACK_UNITC6L)
bannerOptions.message = "Message"; bannerOptions.message = "Message";
} else { #else
bannerOptions.message = "Message Action"; bannerOptions.message = "Message Action";
}
bannerOptions.optionsArrayPtr = optionsArray;
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);
}
// Delete all messages
} else if (selected == DeleteAll) {
messageStore.clearAllMessages();
graphics::MessageRenderer::clearThreadRegistries();
graphics::MessageRenderer::clearMessageCache();
#ifdef HAS_I2S
} 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 #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.optionsArrayPtr = optionsArray;
bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.optionsCount = options; bannerOptions.optionsCount = options;
bannerOptions.InitialSelected = 1;
bannerOptions.bannerCallback = [](int selected) -> void { bannerOptions.bannerCallback = [](int selected) -> void {
auto mode = graphics::MessageRenderer::getThreadMode(); if (selected == Dismiss) {
int ch = graphics::MessageRenderer::getThreadChannel(); screen->hideCurrentFrame();
uint32_t peer = graphics::MessageRenderer::getThreadPeer(); } else if (selected == Preset) {
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) { if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel); cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
} else { } else {
cannedMessageModule->LaunchWithDestination(devicestate.rx_text_message.from); cannedMessageModule->LaunchWithDestination(devicestate.rx_text_message.from);
} }
} } else if (selected == Freetext) {
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) { if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel); cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
} else { } else {
cannedMessageModule->LaunchFreetextWithDestination(devicestate.rx_text_message.from); cannedMessageModule->LaunchFreetextWithDestination(devicestate.rx_text_message.from);
} }
} }
#ifdef HAS_I2S
else if (selected == Aloud) {
const meshtastic_MeshPacket &mp = devicestate.rx_text_message;
const char *msg = reinterpret_cast<const char *>(mp.decoded.payload.bytes);
return; audioThread->readAloud(msg);
}
};
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 (currentResolution == ScreenResolution::UltraLow) {
optionsArray[options] = "Delete All";
} else {
optionsArray[options] = "Delete All Chats";
}
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);
}
} }
#endif
}; };
screen->showOverlayBanner(bannerOptions); screen->showOverlayBanner(bannerOptions);
} }
@@ -862,12 +505,23 @@ void menuHandler::homeBaseMenu()
optionsArray[options] = "Send Node Info"; optionsArray[options] = "Send Node Info";
} }
optionsEnumArray[options++] = Position; 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; BannerOverlayOptions bannerOptions;
bannerOptions.message = "Home Action"; #if defined(M5STACK_UNITC6L)
if (currentResolution == ScreenResolution::UltraLow) {
bannerOptions.message = "Home"; bannerOptions.message = "Home";
} #else
bannerOptions.message = "Home Action";
#endif
bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.optionsCount = options; bannerOptions.optionsCount = options;
@@ -952,22 +606,21 @@ void menuHandler::systemBaseMenu()
optionsArray[options] = "Display Options"; optionsArray[options] = "Display Options";
optionsEnumArray[options++] = ScreenOptions; optionsEnumArray[options++] = ScreenOptions;
if (currentResolution == ScreenResolution::UltraLow) { #if defined(M5STACK_UNITC6L)
optionsArray[options] = "Bluetooth"; optionsArray[options] = "Bluetooth";
} else { #else
optionsArray[options] = "Bluetooth Toggle"; optionsArray[options] = "Bluetooth Toggle";
} #endif
optionsEnumArray[options++] = Bluetooth; optionsEnumArray[options++] = Bluetooth;
#if HAS_WIFI && !defined(ARCH_PORTDUINO) #if HAS_WIFI && !defined(ARCH_PORTDUINO)
optionsArray[options] = "WiFi Toggle"; optionsArray[options] = "WiFi Toggle";
optionsEnumArray[options++] = WiFiToggle; optionsEnumArray[options++] = WiFiToggle;
#endif #endif
#if defined(M5STACK_UNITC6L)
if (currentResolution == ScreenResolution::UltraLow) {
optionsArray[options] = "Power"; optionsArray[options] = "Power";
} else { #else
optionsArray[options] = "Reboot/Shutdown"; optionsArray[options] = "Reboot/Shutdown";
} #endif
optionsEnumArray[options++] = PowerMenu; optionsEnumArray[options++] = PowerMenu;
if (test_enabled) { if (test_enabled) {
@@ -976,10 +629,11 @@ void menuHandler::systemBaseMenu()
} }
BannerOverlayOptions bannerOptions; BannerOverlayOptions bannerOptions;
bannerOptions.message = "System Action"; #if defined(M5STACK_UNITC6L)
if (currentResolution == ScreenResolution::UltraLow) {
bannerOptions.message = "System"; bannerOptions.message = "System";
} #else
bannerOptions.message = "System Action";
#endif
bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = options; bannerOptions.optionsCount = options;
bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.optionsEnumPtr = optionsEnumArray;
@@ -1016,49 +670,32 @@ void menuHandler::systemBaseMenu()
void menuHandler::favoriteBaseMenu() void menuHandler::favoriteBaseMenu()
{ {
enum optionsNumbers { Back, Preset, Freetext, GoToChat, Remove, TraceRoute, enumEnd }; enum optionsNumbers { Back, Preset, Freetext, Remove, TraceRoute, enumEnd };
#if defined(M5STACK_UNITC6L)
static const char *optionsArray[enumEnd] = {"Back"}; static const char *optionsArray[enumEnd] = {"Back", "New Preset"};
static int optionsEnumArray[enumEnd] = {Back}; #else
int options = 1; static const char *optionsArray[enumEnd] = {"Back", "New Preset Msg"};
#endif
// Only show "View Conversation" if a message exists with this node static int optionsEnumArray[enumEnd] = {Back, Preset};
uint32_t peer = graphics::UIRenderer::currentFavoriteNodeNum; int options = 2;
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;
}
if (currentResolution == ScreenResolution::UltraLow) {
optionsArray[options] = "New Preset";
} else {
optionsArray[options] = "New Preset Msg";
}
optionsEnumArray[options++] = Preset;
if (kb_found) { if (kb_found) {
optionsArray[options] = "New Freetext Msg"; optionsArray[options] = "New Freetext Msg";
optionsEnumArray[options++] = Freetext; optionsEnumArray[options++] = Freetext;
} }
#if !defined(M5STACK_UNITC6L)
if (currentResolution != ScreenResolution::UltraLow) {
optionsArray[options] = "Trace Route"; optionsArray[options] = "Trace Route";
optionsEnumArray[options++] = TraceRoute; optionsEnumArray[options++] = TraceRoute;
} #endif
optionsArray[options] = "Remove Favorite"; optionsArray[options] = "Remove Favorite";
optionsEnumArray[options++] = Remove; optionsEnumArray[options++] = Remove;
BannerOverlayOptions bannerOptions; BannerOverlayOptions bannerOptions;
bannerOptions.message = "Favorites Action"; #if defined(M5STACK_UNITC6L)
if (currentResolution == ScreenResolution::UltraLow) {
bannerOptions.message = "Favorites"; bannerOptions.message = "Favorites";
} #else
bannerOptions.message = "Favorites Action";
#endif
bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.optionsCount = options; bannerOptions.optionsCount = options;
@@ -1067,17 +704,6 @@ void menuHandler::favoriteBaseMenu()
cannedMessageModule->LaunchWithDestination(graphics::UIRenderer::currentFavoriteNodeNum); cannedMessageModule->LaunchWithDestination(graphics::UIRenderer::currentFavoriteNodeNum);
} else if (selected == Freetext) { } else if (selected == Freetext) {
cannedMessageModule->LaunchFreetextWithDestination(graphics::UIRenderer::currentFavoriteNodeNum); 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) { } else if (selected == Remove) {
menuHandler::menuQueue = menuHandler::remove_favorite; menuHandler::menuQueue = menuHandler::remove_favorite;
screen->runNow(); screen->runNow();
@@ -1127,33 +753,20 @@ void menuHandler::positionBaseMenu()
void menuHandler::nodeListMenu() void menuHandler::nodeListMenu()
{ {
enum optionsNumbers { Back, Favorite, TraceRoute, Verify, Reset, NodeNameLength, enumEnd }; enum optionsNumbers { Back, Favorite, TraceRoute, Verify, Reset, enumEnd };
static const char *optionsArray[enumEnd] = {"Back"}; #if defined(M5STACK_UNITC6L)
static int optionsEnumArray[enumEnd] = {Back}; static const char *optionsArray[] = {"Back", "Add Favorite", "Reset Node"};
int options = 1; #else
static const char *optionsArray[] = {"Back", "Add Favorite", "Trace Route", "Key Verification", "Reset NodeDB"};
optionsArray[options] = "Add Favorite"; #endif
optionsEnumArray[options++] = Favorite;
optionsArray[options] = "Trace Route";
optionsEnumArray[options++] = TraceRoute;
if (currentResolution != ScreenResolution::UltraLow) {
optionsArray[options] = "Key Verification";
optionsEnumArray[options++] = Verify;
}
if (currentResolution != ScreenResolution::UltraLow) {
optionsArray[options] = "Show Long/Short Name";
optionsEnumArray[options++] = NodeNameLength;
}
optionsArray[options] = "Reset NodeDB";
optionsEnumArray[options++] = Reset;
BannerOverlayOptions bannerOptions; BannerOverlayOptions bannerOptions;
bannerOptions.message = "Node Action"; bannerOptions.message = "Node Action";
bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = options; #if defined(M5STACK_UNITC6L)
bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.optionsCount = 3;
#else
bannerOptions.optionsCount = 5;
#endif
bannerOptions.bannerCallback = [](int selected) -> void { bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Favorite) { if (selected == Favorite) {
menuQueue = add_favorite; menuQueue = add_favorite;
@@ -1167,9 +780,6 @@ void menuHandler::nodeListMenu()
} else if (selected == TraceRoute) { } else if (selected == TraceRoute) {
menuQueue = trace_route_menu; menuQueue = trace_route_menu;
screen->runNow(); screen->runNow();
} else if (selected == NodeNameLength) {
menuHandler::menuQueue = menuHandler::node_name_length_menu;
screen->runNow();
} }
}; };
screen->showOverlayBanner(bannerOptions); screen->showOverlayBanner(bannerOptions);
@@ -1193,7 +803,7 @@ void menuHandler::nodeNameLengthMenu()
LOG_INFO("Setting names to short"); LOG_INFO("Setting names to short");
config.display.use_long_node_name = false; config.display.use_long_node_name = false;
} else if (selected == Back) { } else if (selected == Back) {
menuQueue = node_base_menu; menuQueue = screen_options_menu;
screen->runNow(); screen->runNow();
} }
}; };
@@ -1221,9 +831,6 @@ void menuHandler::resetNodeDBMenu()
LOG_INFO("Initiate node-db reset but keeping favorites"); LOG_INFO("Initiate node-db reset but keeping favorites");
nodeDB->resetNodes(1); nodeDB->resetNodes(1);
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
} else if (selected == 0) {
menuQueue = node_base_menu;
screen->runNow();
} }
}; };
screen->showOverlayBanner(bannerOptions); screen->showOverlayBanner(bannerOptions);
@@ -1297,14 +904,13 @@ void menuHandler::GPSFormatMenu()
{ {
static const char *optionsArray[] = {"Back", static const char *optionsArray[] = {"Back",
(currentResolution == ScreenResolution::High) ? "Decimal Degrees" : "DEC", isHighResolution ? "Decimal Degrees" : "DEC",
(currentResolution == ScreenResolution::High) ? "Degrees Minutes Seconds" : "DMS", isHighResolution ? "Degrees Minutes Seconds" : "DMS",
(currentResolution == ScreenResolution::High) ? "Universal Transverse Mercator" : "UTM", isHighResolution ? "Universal Transverse Mercator" : "UTM",
(currentResolution == ScreenResolution::High) ? "Military Grid Reference System" isHighResolution ? "Military Grid Reference System" : "MGRS",
: "MGRS", isHighResolution ? "Open Location Code" : "OLC",
(currentResolution == ScreenResolution::High) ? "Open Location Code" : "OLC", isHighResolution ? "Ordnance Survey Grid Ref" : "OSGR",
(currentResolution == ScreenResolution::High) ? "Ordnance Survey Grid Ref" : "OSGR", isHighResolution ? "Maidenhead Locator" : "MLS"};
(currentResolution == ScreenResolution::High) ? "Maidenhead Locator" : "MLS"};
BannerOverlayOptions bannerOptions; BannerOverlayOptions bannerOptions;
bannerOptions.message = "GPS Format"; bannerOptions.message = "GPS Format";
bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsArrayPtr = optionsArray;
@@ -1352,10 +958,11 @@ void menuHandler::BluetoothToggleMenu()
{ {
static const char *optionsArray[] = {"Back", "Enabled", "Disabled"}; static const char *optionsArray[] = {"Back", "Enabled", "Disabled"};
BannerOverlayOptions bannerOptions; BannerOverlayOptions bannerOptions;
bannerOptions.message = "Toggle Bluetooth"; #if defined(M5STACK_UNITC6L)
if (currentResolution == ScreenResolution::UltraLow) {
bannerOptions.message = "Bluetooth"; bannerOptions.message = "Bluetooth";
} #else
bannerOptions.message = "Toggle Bluetooth";
#endif
bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 3; bannerOptions.optionsCount = 3;
bannerOptions.bannerCallback = [](int selected) -> void { bannerOptions.bannerCallback = [](int selected) -> void {
@@ -1571,17 +1178,17 @@ void menuHandler::rebootMenu()
{ {
static const char *optionsArray[] = {"Back", "Confirm"}; static const char *optionsArray[] = {"Back", "Confirm"};
BannerOverlayOptions bannerOptions; BannerOverlayOptions bannerOptions;
bannerOptions.message = "Reboot Device?"; #if defined(M5STACK_UNITC6L)
if (currentResolution == ScreenResolution::UltraLow) {
bannerOptions.message = "Reboot"; bannerOptions.message = "Reboot";
} #else
bannerOptions.message = "Reboot Device?";
#endif
bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 2; bannerOptions.optionsCount = 2;
bannerOptions.bannerCallback = [](int selected) -> void { bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == 1) { if (selected == 1) {
IF_SCREEN(screen->showSimpleBanner("Rebooting...", 0)); IF_SCREEN(screen->showSimpleBanner("Rebooting...", 0));
nodeDB->saveToDisk(); nodeDB->saveToDisk();
messageStore.saveToFlash();
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
} else { } else {
menuQueue = power_menu; menuQueue = power_menu;
@@ -1595,10 +1202,11 @@ void menuHandler::shutdownMenu()
{ {
static const char *optionsArray[] = {"Back", "Confirm"}; static const char *optionsArray[] = {"Back", "Confirm"};
BannerOverlayOptions bannerOptions; BannerOverlayOptions bannerOptions;
bannerOptions.message = "Shutdown Device?"; #if defined(M5STACK_UNITC6L)
if (currentResolution == ScreenResolution::UltraLow) {
bannerOptions.message = "Shutdown"; bannerOptions.message = "Shutdown";
} #else
bannerOptions.message = "Shutdown Device?";
#endif
bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 2; bannerOptions.optionsCount = 2;
bannerOptions.bannerCallback = [](int selected) -> void { bannerOptions.bannerCallback = [](int selected) -> void {
@@ -1615,13 +1223,12 @@ void menuHandler::shutdownMenu()
void menuHandler::addFavoriteMenu() void menuHandler::addFavoriteMenu()
{ {
const char *NODE_PICKER_TITLE; #if defined(M5STACK_UNITC6L)
if (currentResolution == ScreenResolution::UltraLow) { screen->showNodePicker("Node Favorite", 30000, [](uint32_t nodenum) -> void {
NODE_PICKER_TITLE = "Node Favorite"; #else
} else { screen->showNodePicker("Node To Favorite", 30000, [](uint32_t nodenum) -> void {
NODE_PICKER_TITLE = "Node To Favorite";
} #endif
screen->showNodePicker(NODE_PICKER_TITLE, 30000, [](uint32_t nodenum) -> void {
LOG_WARN("Nodenum: %u", nodenum); LOG_WARN("Nodenum: %u", nodenum);
nodeDB->set_favorite(true, nodenum); nodeDB->set_favorite(true, nodenum);
screen->setFrames(graphics::Screen::FOCUS_PRESERVE); screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
@@ -1786,11 +1393,16 @@ void menuHandler::screenOptionsMenu()
hasSupportBrightness = false; hasSupportBrightness = false;
#endif #endif
enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits }; enum optionsNumbers { Back, NodeNameLength, Brightness, ScreenColor, FrameToggles, DisplayUnits };
static const char *optionsArray[5] = {"Back"}; static const char *optionsArray[5] = {"Back"};
static int optionsEnumArray[5] = {Back}; static int optionsEnumArray[5] = {Back};
int options = 1; 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 // Only show brightness for B&W displays
if (hasSupportBrightness) { if (hasSupportBrightness) {
optionsArray[options] = "Brightness"; optionsArray[options] = "Brightness";
@@ -1804,7 +1416,7 @@ void menuHandler::screenOptionsMenu()
optionsEnumArray[options++] = ScreenColor; optionsEnumArray[options++] = ScreenColor;
#endif #endif
optionsArray[options] = "Frame Visibility"; optionsArray[options] = "Frame Visibility Toggle";
optionsEnumArray[options++] = FrameToggles; optionsEnumArray[options++] = FrameToggles;
optionsArray[options] = "Display Units"; optionsArray[options] = "Display Units";
@@ -1822,6 +1434,9 @@ void menuHandler::screenOptionsMenu()
} else if (selected == ScreenColor) { } else if (selected == ScreenColor) {
menuHandler::menuQueue = menuHandler::tftcolormenupicker; menuHandler::menuQueue = menuHandler::tftcolormenupicker;
screen->runNow(); screen->runNow();
} else if (selected == NodeNameLength) {
menuHandler::menuQueue = menuHandler::node_name_length_menu;
screen->runNow();
} else if (selected == FrameToggles) { } else if (selected == FrameToggles) {
menuHandler::menuQueue = menuHandler::FrameToggles; menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow(); screen->runNow();
@@ -1856,10 +1471,11 @@ void menuHandler::powerMenu()
#endif #endif
BannerOverlayOptions bannerOptions; BannerOverlayOptions bannerOptions;
bannerOptions.message = "Reboot / Shutdown"; #if defined(M5STACK_UNITC6L)
if (currentResolution == ScreenResolution::UltraLow) {
bannerOptions.message = "Power"; bannerOptions.message = "Power";
} #else
bannerOptions.message = "Reboot / Shutdown";
#endif
bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = options; bannerOptions.optionsCount = options;
bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.optionsEnumPtr = optionsEnumArray;
@@ -1916,8 +1532,7 @@ void menuHandler::FrameToggles_menu()
{ {
enum optionsNumbers { enum optionsNumbers {
Finish, Finish,
nodelist_nodes, nodelist,
nodelist_location,
nodelist_lastheard, nodelist_lastheard,
nodelist_hopsignal, nodelist_hopsignal,
nodelist_distance, nodelist_distance,
@@ -1938,25 +1553,20 @@ void menuHandler::FrameToggles_menu()
static int lastSelectedIndex = 0; static int lastSelectedIndex = 0;
#ifndef USE_EINK #ifndef USE_EINK
optionsArray[options] = screen->isFrameHidden("nodelist_nodes") ? "Show Node Lists" : "Hide Node Lists"; optionsArray[options] = screen->isFrameHidden("nodelist") ? "Show Node List" : "Hide Node List";
optionsEnumArray[options++] = nodelist_nodes; optionsEnumArray[options++] = nodelist;
#else #endif
#ifdef USE_EINK
optionsArray[options] = screen->isFrameHidden("nodelist_lastheard") ? "Show NL - Last Heard" : "Hide NL - Last Heard"; optionsArray[options] = screen->isFrameHidden("nodelist_lastheard") ? "Show NL - Last Heard" : "Hide NL - Last Heard";
optionsEnumArray[options++] = nodelist_lastheard; optionsEnumArray[options++] = nodelist_lastheard;
optionsArray[options] = screen->isFrameHidden("nodelist_hopsignal") ? "Show NL - Hops/Signal" : "Hide NL - Hops/Signal"; optionsArray[options] = screen->isFrameHidden("nodelist_hopsignal") ? "Show NL - Hops/Signal" : "Hide NL - Hops/Signal";
optionsEnumArray[options++] = nodelist_hopsignal; optionsEnumArray[options++] = nodelist_hopsignal;
#endif
#if HAS_GPS
#ifndef USE_EINK
optionsArray[options] = screen->isFrameHidden("nodelist_location") ? "Show Position Lists" : "Hide Position Lists";
optionsEnumArray[options++] = nodelist_location;
#else
optionsArray[options] = screen->isFrameHidden("nodelist_distance") ? "Show NL - Distance" : "Hide NL - Distance"; optionsArray[options] = screen->isFrameHidden("nodelist_distance") ? "Show NL - Distance" : "Hide NL - Distance";
optionsEnumArray[options++] = nodelist_distance; optionsEnumArray[options++] = nodelist_distance;
optionsArray[options] = screen->isFrameHidden("nodelist_bearings") ? "Show NL - Bearings" : "Hide NL - Bearings";
optionsEnumArray[options++] = nodelist_bearings;
#endif #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"; optionsArray[options] = screen->isFrameHidden("gps") ? "Show Position" : "Hide Position";
optionsEnumArray[options++] = gps; optionsEnumArray[options++] = gps;
@@ -1995,12 +1605,8 @@ void menuHandler::FrameToggles_menu()
if (selected == Finish) { if (selected == Finish) {
screen->setFrames(Screen::FOCUS_DEFAULT); screen->setFrames(Screen::FOCUS_DEFAULT);
} else if (selected == nodelist_nodes) { } else if (selected == nodelist) {
screen->toggleFrameVisibility("nodelist_nodes"); screen->toggleFrameVisibility("nodelist");
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == nodelist_location) {
screen->toggleFrameVisibility("nodelist_location");
menuHandler::menuQueue = menuHandler::FrameToggles; menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow(); screen->runNow();
} else if (selected == nodelist_lastheard) { } else if (selected == nodelist_lastheard) {
@@ -2116,9 +1722,6 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
case position_base_menu: case position_base_menu:
positionBaseMenu(); positionBaseMenu();
break; break;
case node_base_menu:
nodeListMenu();
break;
#if !MESHTASTIC_EXCLUDE_GPS #if !MESHTASTIC_EXCLUDE_GPS
case gps_toggle_menu: case gps_toggle_menu:
GPSToggleMenu(); GPSToggleMenu();
@@ -2199,18 +1802,6 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
case throttle_message: case throttle_message:
screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000); screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000);
break; 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; menuQueue = menu_none;
} }

View File

@@ -19,7 +19,6 @@ class menuHandler
clock_face_picker, clock_face_picker,
clock_menu, clock_menu,
position_base_menu, position_base_menu,
node_base_menu,
gps_toggle_menu, gps_toggle_menu,
gps_format_menu, gps_format_menu,
compass_point_north_menu, compass_point_north_menu,
@@ -44,10 +43,6 @@ class menuHandler
key_verification_final_prompt, key_verification_final_prompt,
trace_route_menu, trace_route_menu,
throttle_message, throttle_message,
message_response_menu,
message_viewmode_menu,
reply_menu,
delete_messages_menu,
node_name_length_menu, node_name_length_menu,
FrameToggles, FrameToggles,
DisplayUnits DisplayUnits
@@ -66,9 +61,6 @@ class menuHandler
static void TwelveHourPicker(); static void TwelveHourPicker();
static void ClockFacePicker(); static void ClockFacePicker();
static void messageResponseMenu(); static void messageResponseMenu();
static void messageViewModeMenu();
static void replyMenu();
static void deleteMessagesMenu();
static void homeBaseMenu(); static void homeBaseMenu();
static void textMessageBaseMenu(); static void textMessageBaseMenu();
static void systemBaseMenu(); static void systemBaseMenu();

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,7 @@
#pragma once #pragma once
#include "MessageStore.h" // for StoredMessage
#if HAS_SCREEN
#include "OLEDDisplay.h" #include "OLEDDisplay.h"
#include "OLEDDisplayUi.h" #include "OLEDDisplayUi.h"
#include "graphics/emotes.h" #include "graphics/emotes.h"
#include "mesh/generated/meshtastic/mesh.pb.h" // for meshtastic_MeshPacket
#include <cstdint>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -14,27 +10,6 @@ namespace graphics
namespace MessageRenderer 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 // Text and emote rendering
void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount); 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); std::vector<std::string> generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth);
// Function to calculate heights for each line // Function to calculate heights for each line
std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, const Emote *emotes, std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, const Emote *emotes);
const std::vector<bool> &isHeaderVec);
// Reset scroll state when new messages arrive // Function to render the message content
void resetScrollState(); 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);
// 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();
} // namespace MessageRenderer } // namespace MessageRenderer
} // namespace graphics } // namespace graphics
#endif

View File

@@ -23,6 +23,7 @@ extern graphics::Screen *screen;
#if defined(M5STACK_UNITC6L) #if defined(M5STACK_UNITC6L)
static uint32_t lastSwitchTime = 0; static uint32_t lastSwitchTime = 0;
#else
#endif #endif
namespace graphics namespace graphics
{ {
@@ -45,119 +46,79 @@ void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *
} }
// Static variables for dynamic cycling // Static variables for dynamic cycling
static ListMode_Node currentMode_Nodes = MODE_LAST_HEARD; static NodeListMode currentMode = MODE_LAST_HEARD;
static ListMode_Location currentMode_Location = MODE_DISTANCE;
static int scrollIndex = 0; static int scrollIndex = 0;
// Popup overlay state
static uint32_t popupTime = 0;
static int popupTotal = 0;
static int popupStart = 0;
static int popupEnd = 0;
static int popupPage = 1;
static int popupMaxPage = 1;
static const uint32_t POPUP_DURATION_MS = 1000; // 1 second visible
// =============================
// Scrolling Logic
// =============================
void scrollUp()
{
if (scrollIndex > 0)
scrollIndex--;
popupTime = millis(); // show popup
}
void scrollDown()
{
scrollIndex++;
popupTime = millis();
}
// ============================= // =============================
// Utility Functions // Utility Functions
// ============================= // =============================
const char *getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int columnWidth) const char *getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node)
{ {
static char nodeName[25]; // single static buffer we return const char *name = NULL;
nodeName[0] = '\0'; static char nodeName[16] = "?";
if (config.display.use_long_node_name == true) {
auto writeFallbackId = [&] { if (node->has_user && strlen(node->user.long_name) > 0) {
std::snprintf(nodeName, sizeof(nodeName), "(%04X)", static_cast<uint16_t>(node ? (node->num & 0xFFFF) : 0)); name = node->user.long_name;
};
// 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();
} else { } else {
// %.*s ensures null-termination and safe truncation to buffer size - 1 snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF));
std::snprintf(nodeName, sizeof(nodeName), "%.*s", static_cast<int>(sizeof(nodeName) - 1), s.c_str()); }
} else {
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) // Use sanitizeString() function and copy directly into nodeName
if (config.display.use_long_node_name && display) { std::string sanitized_name = sanitizeString(name ? name : "");
int availWidth = columnWidth - ((currentResolution == ScreenResolution::High) ? 65 : 38);
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) if (availWidth < 0)
availWidth = 0; availWidth = 0;
const size_t beforeLen = std::strlen(nodeName); size_t origLen = strlen(nodeName);
while (nodeName[0] && display->getStringWidth(nodeName) > availWidth) {
// Trim from the end until it fits or is empty nodeName[strlen(nodeName) - 1] = '\0';
size_t len = beforeLen;
while (len && display->getStringWidth(nodeName) > availWidth) {
nodeName[--len] = '\0';
} }
// If truncated, append "..." (respect buffer size) // If we actually truncated, append "..." (ensure space remains in buffer)
if (len < beforeLen) { if (strlen(nodeName) < origLen) {
// Make sure there's room for "..." and '\0' size_t len = strlen(nodeName);
const size_t capForText = sizeof(nodeName) - 1; // leaving space for '\0' size_t maxLen = sizeof(nodeName) - 4; // 3 for "..." and 1 for '\0'
const size_t needed = 3; // "..." if (len > maxLen) {
if (len > capForText - needed) { nodeName[maxLen] = '\0';
len = capForText - needed; len = maxLen;
nodeName[len] = '\0';
} }
std::strcat(nodeName, "..."); strcat(nodeName, "...");
} }
} }
return nodeName; return nodeName;
} }
const char *getCurrentModeTitle_Nodes(int screenWidth) const char *getCurrentModeTitle(int screenWidth)
{ {
switch (currentMode_Nodes) { switch (currentMode) {
case MODE_LAST_HEARD: case MODE_LAST_HEARD:
return "Last Heard"; return "Last Heard";
case MODE_HOP_SIGNAL: case MODE_HOP_SIGNAL:
#ifdef USE_EINK #ifdef USE_EINK
return "Hops/Sig"; return "Hops/Sig";
#else #else
return (currentResolution == ScreenResolution::High) ? "Hops/Signal" : "Hops/Sig"; return (isHighResolution) ? "Hops/Signal" : "Hops/Sig";
#endif #endif
default:
return "Nodes";
}
}
const char *getCurrentModeTitle_Location(int screenWidth)
{
switch (currentMode_Location) {
case MODE_DISTANCE: case MODE_DISTANCE:
return "Distance"; return "Distance";
case MODE_BEARING:
return "Bearings";
default: default:
return "Nodes"; return "Nodes";
} }
@@ -176,8 +137,10 @@ int calculateMaxScroll(int totalEntries, int visibleRows)
void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd) 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) { for (int y = yStart; y <= yEnd; y += 2) {
display->setPixel(x, y); display->setPixel(separatorX, y);
} }
} }
@@ -189,8 +152,7 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries,
int scrollbarX = display->getWidth() - 2; int scrollbarX = display->getWidth() - 2;
int scrollbarHeight = display->getHeight() - scrollStartY - 10; int scrollbarHeight = display->getHeight() - scrollStartY - 10;
int thumbHeight = std::max(4, (scrollbarHeight * visibleNodeRows * columns) / totalEntries); int thumbHeight = std::max(4, (scrollbarHeight * visibleNodeRows * columns) / totalEntries);
int perPage = visibleNodeRows * columns; int maxScroll = calculateMaxScroll(totalEntries, visibleNodeRows);
int maxScroll = std::max(0, (totalEntries - 1) / perPage);
int thumbY = scrollStartY + (scrollIndex * (scrollbarHeight - thumbHeight)) / std::max(1, maxScroll); int thumbY = scrollStartY + (scrollIndex * (scrollbarHeight - thumbHeight)) / std::max(1, maxScroll);
for (int i = 0; i < thumbHeight; i++) { for (int i = 0; i < thumbHeight; i++) {
@@ -205,9 +167,9 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries,
void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
{ {
bool isLeftCol = (x < SCREEN_WIDTH / 2); bool isLeftCol = (x < SCREEN_WIDTH / 2);
int timeOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7); int timeOffset = (isHighResolution) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7);
const char *nodeName = getSafeNodeName(display, node, columnWidth); const char *nodeName = getSafeNodeName(display, node);
char timeStr[10]; char timeStr[10];
uint32_t seconds = sinceLastSeen(node); uint32_t seconds = sinceLastSeen(node);
@@ -226,9 +188,9 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
display->drawString(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nodeName); display->drawString(x + ((isHighResolution) ? 6 : 3), y, nodeName);
if (node->is_favorite) { if (node->is_favorite) {
if (currentResolution == ScreenResolution::High) { if (isHighResolution) {
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
} else { } else {
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
@@ -247,19 +209,19 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
bool isLeftCol = (x < SCREEN_WIDTH / 2); bool isLeftCol = (x < SCREEN_WIDTH / 2);
int nameMaxWidth = columnWidth - 25; int nameMaxWidth = columnWidth - 25;
int barsOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19); int barsOffset = (isHighResolution) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19);
int hopOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17); int hopOffset = (isHighResolution) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17);
int barsXOffset = columnWidth - barsOffset; int barsXOffset = columnWidth - barsOffset;
const char *nodeName = getSafeNodeName(display, node, columnWidth); const char *nodeName = getSafeNodeName(display, node);
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
display->drawStringMaxWidth(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nameMaxWidth, nodeName); display->drawStringMaxWidth(x + ((isHighResolution) ? 6 : 3), y, nameMaxWidth, nodeName);
if (node->is_favorite) { if (node->is_favorite) {
if (currentResolution == ScreenResolution::High) { if (isHighResolution) {
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
} else { } else {
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
@@ -294,10 +256,9 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
void drawNodeDistance(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)
{ {
bool isLeftCol = (x < SCREEN_WIDTH / 2); bool isLeftCol = (x < SCREEN_WIDTH / 2);
int nameMaxWidth = int nameMaxWidth = columnWidth - (isHighResolution ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
const char *nodeName = getSafeNodeName(display, node, columnWidth); const char *nodeName = getSafeNodeName(display, node);
char distStr[10] = ""; char distStr[10] = "";
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
@@ -350,9 +311,9 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
display->drawStringMaxWidth(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nameMaxWidth, nodeName); display->drawStringMaxWidth(x + ((isHighResolution) ? 6 : 3), y, nameMaxWidth, nodeName);
if (node->is_favorite) { if (node->is_favorite) {
if (currentResolution == ScreenResolution::High) { if (isHighResolution) {
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
} else { } else {
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
@@ -360,8 +321,7 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
} }
if (strlen(distStr) > 0) { if (strlen(distStr) > 0) {
int offset = (currentResolution == ScreenResolution::High) int offset = (isHighResolution) ? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column)
? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column)
: (isLeftCol ? 4 : 7); // Offset for Narrow Screens (Left Column:Right Column) : (isLeftCol ? 4 : 7); // Offset for Narrow Screens (Left Column:Right Column)
int rightEdge = x + columnWidth - offset; int rightEdge = x + columnWidth - offset;
int textWidth = display->getStringWidth(distStr); int textWidth = display->getStringWidth(distStr);
@@ -369,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: case MODE_LAST_HEARD:
drawEntryLastHeard(display, node, x, y, columnWidth); drawEntryLastHeard(display, node, x, y, columnWidth);
break; break;
case MODE_HOP_SIGNAL: case MODE_HOP_SIGNAL:
drawEntryHopSignal(display, node, x, y, columnWidth); drawEntryHopSignal(display, node, x, y, columnWidth);
break; break;
case MODE_DISTANCE:
drawNodeDistance(display, node, x, y, columnWidth);
break;
default: default:
break; break;
} }
@@ -388,16 +351,15 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
bool isLeftCol = (x < SCREEN_WIDTH / 2); bool isLeftCol = (x < SCREEN_WIDTH / 2);
// Adjust max text width depending on column and screen width // Adjust max text width depending on column and screen width
int nameMaxWidth = int nameMaxWidth = columnWidth - (isHighResolution ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
const char *nodeName = getSafeNodeName(display, node, columnWidth); const char *nodeName = getSafeNodeName(display, node);
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
display->drawStringMaxWidth(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nameMaxWidth, nodeName); display->drawStringMaxWidth(x + ((isHighResolution) ? 6 : 3), y, nameMaxWidth, nodeName);
if (node->is_favorite) { if (node->is_favorite) {
if (currentResolution == ScreenResolution::High) { if (isHighResolution) {
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
} else { } else {
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
@@ -412,7 +374,7 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
return; return;
bool isLeftCol = (x < SCREEN_WIDTH / 2); bool isLeftCol = (x < SCREEN_WIDTH / 2);
int arrowXOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18); int arrowXOffset = (isHighResolution) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18);
int centerX = x + columnWidth - arrowXOffset; int centerX = x + columnWidth - arrowXOffset;
int centerY = y + FONT_HEIGHT_SMALL / 2; int centerY = y + FONT_HEIGHT_SMALL / 2;
@@ -469,6 +431,11 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
locationScreen = true; locationScreen = true;
else if (strcmp(title, "Distance") == 0) else if (strcmp(title, "Distance") == 0)
locationScreen = true; locationScreen = true;
#if defined(M5STACK_UNITC6L)
int columnWidth = display->getWidth();
#else
int columnWidth = display->getWidth() / 2;
#endif
display->clear(); display->clear();
// Draw the battery/time header // Draw the battery/time header
@@ -477,74 +444,39 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
// Space below header // Space below header
y += COMMON_HEADER_HEIGHT; y += COMMON_HEADER_HEIGHT;
int totalColumns = 1; // Default to 1 column
if (config.display.use_long_node_name) {
if (SCREEN_WIDTH <= 240) {
totalColumns = 1;
} else if (SCREEN_WIDTH > 240) {
totalColumns = 2;
}
} else {
if (SCREEN_WIDTH <= 64) {
totalColumns = 1;
} else if (SCREEN_WIDTH > 64 && SCREEN_WIDTH <= 240) {
totalColumns = 2;
} else {
totalColumns = 3;
}
}
int columnWidth = display->getWidth() / totalColumns;
int totalEntries = nodeDB->getNumMeshNodes(); int totalEntries = nodeDB->getNumMeshNodes();
int totalRowsAvailable = (display->getHeight() - y) / rowYOffset; int totalRowsAvailable = (display->getHeight() - y) / rowYOffset;
int numskipped = 0; int numskipped = 0;
int visibleNodeRows = totalRowsAvailable; int visibleNodeRows = totalRowsAvailable;
#if defined(M5STACK_UNITC6L)
// Build filtered + ordered list int totalColumns = 1;
std::vector<int> drawList; #else
drawList.reserve(totalEntries); int totalColumns = 2;
for (int i = 0; i < totalEntries; i++) { #endif
auto *n = nodeDB->getMeshNodeByIndex(i);
if (!n)
continue;
if (n->num == nodeDB->getNodeNum())
continue;
if (locationScreen && !n->has_position)
continue;
drawList.push_back(n->num);
}
totalEntries = drawList.size();
int perPage = visibleNodeRows * totalColumns;
int maxScroll = 0;
if (perPage > 0) {
maxScroll = std::max(0, (totalEntries - 1) / perPage);
}
if (scrollIndex > maxScroll)
scrollIndex = maxScroll;
int startIndex = scrollIndex * visibleNodeRows * totalColumns; int startIndex = scrollIndex * visibleNodeRows * totalColumns;
if (nodeDB->getMeshNodeByIndex(startIndex)->num == nodeDB->getNodeNum()) {
startIndex++; // skip own node
}
int endIndex = std::min(startIndex + visibleNodeRows * totalColumns, totalEntries); int endIndex = std::min(startIndex + visibleNodeRows * totalColumns, totalEntries);
int yOffset = 0; int yOffset = 0;
int col = 0; int col = 0;
int lastNodeY = y; int lastNodeY = y;
int shownCount = 0; int shownCount = 0;
int rowCount = 0; int rowCount = 0;
for (int idx = startIndex; idx < endIndex; idx++) { for (int i = startIndex; i < endIndex; ++i) {
uint32_t nodeNum = drawList[idx]; if (locationScreen && !nodeDB->getMeshNodeByIndex(i)->has_position) {
auto *node = nodeDB->getMeshNode(nodeNum); numskipped++;
continue;
}
int xPos = x + (col * columnWidth); int xPos = x + (col * columnWidth);
int yPos = y + yOffset; int yPos = y + yOffset;
renderer(display, nodeDB->getMeshNodeByIndex(i), xPos, yPos, columnWidth);
renderer(display, node, xPos, yPos, columnWidth); if (extras) {
extras(display, nodeDB->getMeshNodeByIndex(i), xPos, yPos, columnWidth, heading, lat, lon);
if (extras) }
extras(display, node, xPos, yPos, columnWidth, heading, lat, lon);
lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL); lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL);
yOffset += rowYOffset; yOffset += rowYOffset;
@@ -563,73 +495,17 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
// This should correct the scrollbar // This should correct the scrollbar
totalEntries -= numskipped; totalEntries -= numskipped;
#if !defined(M5STACK_UNITC6L)
// Draw column separator // Draw column separator
if (currentResolution != ScreenResolution::UltraLow && shownCount > 0) { if (shownCount > 0) {
const int firstNodeY = y + 3; const int firstNodeY = y + 3;
for (int horizontal_offset = 1; horizontal_offset < totalColumns; horizontal_offset++) { drawColumnSeparator(display, x, firstNodeY, lastNodeY);
drawColumnSeparator(display, columnWidth * horizontal_offset, firstNodeY, lastNodeY);
}
} }
#endif
const int scrollStartY = y + 3; const int scrollStartY = y + 3;
drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, totalColumns, scrollStartY); drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, scrollStartY);
graphics::drawCommonFooter(display, x, y); graphics::drawCommonFooter(display, x, y);
// Scroll Popup Overlay
if (millis() - popupTime < POPUP_DURATION_MS) {
popupTotal = totalEntries;
int perPage = visibleNodeRows * totalColumns;
popupStart = startIndex + 1;
popupEnd = std::min(startIndex + perPage, totalEntries);
popupPage = (scrollIndex + 1);
popupMaxPage = std::max(1, (totalEntries + perPage - 1) / perPage);
char buf[32];
snprintf(buf, sizeof(buf), "%d-%d/%d Pg %d/%d", popupStart, popupEnd, popupTotal, popupPage, popupMaxPage);
display->setTextAlignment(TEXT_ALIGN_LEFT);
// Box padding
int padding = 2;
int textW = display->getStringWidth(buf);
int textH = FONT_HEIGHT_SMALL;
int boxWidth = textW + padding * 3;
int boxHeight = textH + padding * 2;
// Center of usable screen area:
int headerHeight = FONT_HEIGHT_SMALL - 1;
int footerHeight = FONT_HEIGHT_SMALL + 2;
int usableTop = headerHeight;
int usableBottom = display->getHeight() - footerHeight;
int usableHeight = usableBottom - usableTop;
// Center point inside usable area
int boxLeft = (display->getWidth() - boxWidth) / 2;
int boxTop = usableTop + (usableHeight - boxHeight) / 2;
// Draw Box
display->setColor(BLACK);
display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2);
display->fillRect(boxLeft, boxTop - 2, boxWidth, 1);
display->fillRect(boxLeft, boxTop + boxHeight + 1, boxWidth, 1);
display->fillRect(boxLeft - 2, boxTop, 1, boxHeight);
display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight);
display->setColor(WHITE);
display->drawRect(boxLeft, boxTop, boxWidth, 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);
// Text
display->drawString(boxLeft + padding, boxTop + padding, buf);
}
} }
// ============================= // =============================
@@ -637,11 +513,10 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
// ============================= // =============================
#ifndef USE_EINK #ifndef USE_EINK
// Node list for Last Heard and Hop Signal views void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
void drawDynamicListScreen_Nodes(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{ {
// Static variables to track mode and duration // Static variables to track mode and duration
static ListMode_Node lastRenderedMode = MODE_COUNT_NODE; static NodeListMode lastRenderedMode = MODE_COUNT;
static unsigned long modeStartTime = 0; static unsigned long modeStartTime = 0;
unsigned long now = millis(); unsigned long now = millis();
@@ -654,65 +529,23 @@ void drawDynamicListScreen_Nodes(OLEDDisplay *display, OLEDDisplayUiState *state
} }
#endif #endif
// On very first call (on boot or state enter) // On very first call (on boot or state enter)
if (lastRenderedMode == MODE_COUNT_NODE) { if (lastRenderedMode == MODE_COUNT) {
currentMode_Nodes = MODE_LAST_HEARD; currentMode = MODE_LAST_HEARD;
modeStartTime = now; modeStartTime = now;
} }
// Time to switch to next mode? // Time to switch to next mode?
if (now - modeStartTime >= getModeCycleIntervalMs()) { 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; modeStartTime = now;
} }
// Render screen based on currentMode // Render screen based on currentMode
const char *title = getCurrentModeTitle_Nodes(display->getWidth()); const char *title = getCurrentModeTitle(display->getWidth());
drawNodeListScreen(display, state, x, y, title, drawEntryDynamic_Nodes); drawNodeListScreen(display, state, x, y, title, drawEntryDynamic);
// Track the last mode to avoid reinitializing modeStartTime // Track the last mode to avoid reinitializing modeStartTime
lastRenderedMode = currentMode_Nodes; lastRenderedMode = currentMode;
}
// 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;
} }
#endif #endif
@@ -733,12 +566,14 @@ void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_
#endif #endif
drawNodeListScreen(display, state, x, y, title, drawEntryHopSignal); drawNodeListScreen(display, state, x, y, title, drawEntryHopSignal);
} }
void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{ {
const char *title = "Distance"; const char *title = "Distance";
drawNodeListScreen(display, state, x, y, title, drawNodeDistance); drawNodeListScreen(display, state, x, y, title, drawNodeDistance);
} }
#endif #endif
void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{ {
float heading = 0; float heading = 0;

View File

@@ -23,11 +23,8 @@ namespace NodeListRenderer
typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int); 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); 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 // Node list mode enumeration
enum ListMode_Node { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_COUNT_NODE = 2 }; enum NodeListMode { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_DISTANCE = 2, MODE_COUNT = 3 };
// Node list mode enumeration for Distance and Bearings views
enum ListMode_Location { MODE_DISTANCE = 0, MODE_BEARING = 1, MODE_COUNT_LOCATION = 2 };
// Main node list screen function // Main node list screen function
void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, 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 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 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 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); void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
// Extras renderers // Extras renderers
@@ -49,20 +46,14 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); 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 drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
void drawDistanceScreen(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 drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
void drawDynamicListScreen_Location(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
// Utility functions // Utility functions
const char *getCurrentModeTitle_Nodes(int screenWidth); const char *getCurrentModeTitle(int screenWidth);
const char *getCurrentModeTitle_Location(int screenWidth); const char *getSafeNodeName(meshtastic_NodeInfoLite *node);
const char *getSafeNodeName(meshtastic_NodeInfoLite *node, int columnWidth);
void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields); void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields);
// Scrolling controls
void scrollUp();
void scrollDown();
// Bitmap drawing function // Bitmap drawing function
void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *bitmapXBM, OLEDDisplay *display); void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *bitmapXBM, OLEDDisplay *display);

View File

@@ -1,6 +1,6 @@
#include "configuration.h" #include "configuration.h"
#if HAS_SCREEN #if HAS_SCREEN
#include "DisplayFormatters.h" #include "DisplayFormatters.h"
#include "NodeDB.h" #include "NodeDB.h"
#include "NotificationRenderer.h" #include "NotificationRenderer.h"
@@ -38,7 +38,7 @@ extern bool hasUnreadMessage;
namespace graphics namespace graphics
{ {
int bannerSignalBars = -1;
InputEvent NotificationRenderer::inEvent; InputEvent NotificationRenderer::inEvent;
int8_t NotificationRenderer::curSelected = 0; int8_t NotificationRenderer::curSelected = 0;
char NotificationRenderer::alertBannerMessage[256] = {0}; char NotificationRenderer::alertBannerMessage[256] = {0};
@@ -321,7 +321,7 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
} }
if (i == curSelected) { if (i == curSelected) {
selectedNodenum = nodeDB->getMeshNodeByIndex(i + 1)->num; selectedNodenum = nodeDB->getMeshNodeByIndex(i + 1)->num;
if (currentResolution == ScreenResolution::High) { if (isHighResolution) {
strncpy(scratchLineBuffer[scratchLineNum], "> ", 3); strncpy(scratchLineBuffer[scratchLineNum], "> ", 3);
strncpy(scratchLineBuffer[scratchLineNum] + 2, temp_name, 36); strncpy(scratchLineBuffer[scratchLineNum] + 2, temp_name, 36);
strncpy(scratchLineBuffer[scratchLineNum] + strlen(temp_name) + 2, " <", 3); strncpy(scratchLineBuffer[scratchLineNum] + strlen(temp_name) + 2, " <", 3);
@@ -449,7 +449,7 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp
for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) { for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) {
if (i == curSelected) { if (i == curSelected) {
if (currentResolution == ScreenResolution::High) { if (isHighResolution) {
strncpy(lineBuffer, "> ", 3); strncpy(lineBuffer, "> ", 3);
strncpy(lineBuffer + 2, optionsArrayPtr[i], 36); strncpy(lineBuffer + 2, optionsArrayPtr[i], 36);
strncpy(lineBuffer + strlen(optionsArrayPtr[i]) + 2, " <", 3); strncpy(lineBuffer + strlen(optionsArrayPtr[i]) + 2, " <", 3);
@@ -477,7 +477,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
bool is_picker = false; bool is_picker = false;
uint16_t lineCount = 0; uint16_t lineCount = 0;
// Layout Configuration // === Layout Configuration ===
constexpr uint16_t hPadding = 5; constexpr uint16_t hPadding = 5;
constexpr uint16_t vPadding = 2; constexpr uint16_t vPadding = 2;
bool needs_bell = false; bool needs_bell = false;
@@ -491,32 +491,13 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
display->setTextAlignment(TEXT_ALIGN_LEFT); 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) { while (lines[lineCount] != nullptr) {
auto newlinePointer = strchr(lines[lineCount], '\n'); auto newlinePointer = strchr(lines[lineCount], '\n');
if (newlinePointer) if (newlinePointer)
lineLengths[lineCount] = (newlinePointer - lines[lineCount]); // Check for newlines first lineLengths[lineCount] = (newlinePointer - lines[lineCount]); // Check for newlines first
else // if the newline wasn't found, then pull string length from strlen else // if the newline wasn't found, then pull string length from strlen
lineLengths[lineCount] = strlen(lines[lineCount]); lineLengths[lineCount] = strlen(lines[lineCount]);
lineWidths[lineCount] = display->getStringWidth(lines[lineCount], lineLengths[lineCount], true); 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) { if (!is_picker) {
needs_bell |= (strstr(alertBannerMessage, "Alert Received") != nullptr); needs_bell |= (strstr(alertBannerMessage, "Alert Received") != nullptr);
if (lineWidths[lineCount] > maxWidth) if (lineWidths[lineCount] > maxWidth)
@@ -526,16 +507,12 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
} }
// count lines // count lines
// Ensure box accounts for signal bars if present
if (widestLineWithBars > maxWidth)
maxWidth = widestLineWithBars;
uint16_t boxWidth = hPadding * 2 + maxWidth; uint16_t boxWidth = hPadding * 2 + maxWidth;
#if defined(M5STACK_UNITC6L)
if (needs_bell) { if (needs_bell) {
if ((currentResolution == ScreenResolution::High) && boxWidth <= 150) if (isHighResolution && boxWidth <= 150)
boxWidth += 26; boxWidth += 26;
if ((currentResolution == ScreenResolution::Low || currentResolution == ScreenResolution::UltraLow) && boxWidth <= 100) if (!isHighResolution && boxWidth <= 100)
boxWidth += 20; boxWidth += 20;
} }
@@ -544,17 +521,14 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
uint8_t visibleTotalLines = std::min<uint8_t>(lineCount, (screenHeight - vPadding * 2) / effectiveLineHeight); uint8_t visibleTotalLines = std::min<uint8_t>(lineCount, (screenHeight - vPadding * 2) / effectiveLineHeight);
uint16_t contentHeight = visibleTotalLines * effectiveLineHeight; uint16_t contentHeight = visibleTotalLines * effectiveLineHeight;
uint16_t boxHeight = contentHeight + vPadding * 2; uint16_t boxHeight = contentHeight + vPadding * 2;
if (visibleTotalLines == 1) { if (visibleTotalLines == 1)
boxHeight += (currentResolution == ScreenResolution::High) ? 4 : 3; boxHeight += (isHighResolution ? 4 : 3);
}
int16_t boxLeft = (display->width() / 2) - (boxWidth / 2); int16_t boxLeft = (display->width() / 2) - (boxWidth / 2);
if (totalLines > visibleTotalLines) { if (totalLines > visibleTotalLines)
boxWidth += (currentResolution == ScreenResolution::High) ? 4 : 2; boxWidth += (isHighResolution ? 4 : 2);
}
int16_t boxTop = (display->height() / 2) - (boxHeight / 2); int16_t boxTop = (display->height() / 2) - (boxHeight / 2);
boxHeight += (currentResolution == ScreenResolution::High) ? 2 : 1;
#if defined(M5STACK_UNITC6L)
if (visibleTotalLines == 1) { if (visibleTotalLines == 1) {
boxTop += 25; boxTop += 25;
} }
@@ -565,9 +539,127 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
if (boxTop < 0) if (boxTop < 0)
boxTop = 0; boxTop = 0;
} }
#endif
// Draw Box // === 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;
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);
// === Draw Box ===
display->setColor(BLACK); display->setColor(BLACK);
display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2);
display->fillRect(boxLeft, boxTop - 2, boxWidth, 1); 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->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1);
display->setColor(WHITE); display->setColor(WHITE);
// Draw Content // === Draw Content ===
int16_t lineY = boxTop + vPadding; int16_t lineY = boxTop + vPadding;
for (int i = 0; i < lineCount; i++) { for (int i = 0; i < lineCount; i++) {
int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2; int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2;
@@ -612,47 +704,17 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
lineY += (effectiveLineHeight - 2 - background_yOffset); lineY += (effectiveLineHeight - 2 - background_yOffset);
} else { } else {
// Pop-up // 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); lineY += (effectiveLineHeight);
} }
} }
// Scroll Bar (Thicker, inside box, not over title) // === Scroll Bar (Thicker, inside box, not over title) ===
if (totalLines > visibleTotalLines) { if (totalLines > visibleTotalLines) {
const uint8_t scrollBarWidth = 5; const uint8_t scrollBarWidth = 5;
int16_t scrollBarX = boxLeft + boxWidth - scrollBarWidth - 2; 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; uint16_t scrollBarHeight = boxHeight - vPadding * 2 - effectiveLineHeight;
float ratio = (float)visibleTotalLines / totalLines; float ratio = (float)visibleTotalLines / totalLines;
@@ -663,6 +725,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight); display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight);
display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight); display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight);
} }
#endif
} }
/// Draw the last text message we received /// Draw the last text message we received

View File

@@ -6,7 +6,10 @@
#include "NodeListRenderer.h" #include "NodeListRenderer.h"
#include "UIRenderer.h" #include "UIRenderer.h"
#include "airtime.h" #include "airtime.h"
#include "configuration.h"
#include "gps/GeoCoord.h" #include "gps/GeoCoord.h"
#include "graphics/Screen.h"
#include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h" #include "graphics/SharedUIDisplay.h"
#include "graphics/TimeFormatters.h" #include "graphics/TimeFormatters.h"
#include "graphics/images.h" #include "graphics/images.h"
@@ -26,16 +29,6 @@ namespace graphics
NodeNum UIRenderer::currentFavoriteNodeNum = 0; NodeNum UIRenderer::currentFavoriteNodeNum = 0;
std::vector<meshtastic_NodeInfoLite *> graphics::UIRenderer::favoritedNodes; std::vector<meshtastic_NodeInfoLite *> graphics::UIRenderer::favoritedNodes;
static inline void drawSatelliteIcon(OLEDDisplay *display, int16_t x, int16_t y)
{
int yOffset = (currentResolution == ScreenResolution::High) ? -5 : 1;
if (currentResolution == ScreenResolution::High) {
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() void graphics::UIRenderer::rebuildFavoritedNodes()
{ {
favoritedNodes.clear(); favoritedNodes.clear();
@@ -63,7 +56,7 @@ extern uint32_t dopThresholds[5];
void UIRenderer::drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps) void UIRenderer::drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps)
{ {
// Draw satellite image // Draw satellite image
if (currentResolution == ScreenResolution::High) { if (isHighResolution) {
NodeListRenderer::drawScaledXBitmap16x16(x, y - 2, imgSatellite_width, imgSatellite_height, imgSatellite, display); NodeListRenderer::drawScaledXBitmap16x16(x, y - 2, imgSatellite_width, imgSatellite_height, imgSatellite, display);
} else { } else {
display->drawXbm(x + 1, y + 1, imgSatellite_width, imgSatellite_height, imgSatellite); display->drawXbm(x + 1, y + 1, imgSatellite_width, imgSatellite_height, imgSatellite);
@@ -83,7 +76,7 @@ void UIRenderer::drawGps(OLEDDisplay *display, int16_t x, int16_t y, const mesht
} else { } else {
snprintf(textString, sizeof(textString), "%u sats", gps->getNumSatellites()); snprintf(textString, sizeof(textString), "%u sats", gps->getNumSatellites());
} }
if (currentResolution == ScreenResolution::High) { if (isHighResolution) {
display->drawString(x + 18, y, textString); display->drawString(x + 18, y, textString);
} else { } else {
display->drawString(x + 11, y, textString); display->drawString(x + 11, y, textString);
@@ -251,16 +244,16 @@ void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y,
// Draw nodes status // Draw nodes status
void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, int node_offset, 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]; char usersString[20];
int nodes_online = (nodeStatus->getNumOnline() > 0) ? nodeStatus->getNumOnline() + node_offset : 0; 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) { if (show_total) {
int nodes_total = (nodeStatus->getNumTotal() > 0) ? nodeStatus->getNumTotal() + node_offset : 0; 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) || \ #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
@@ -268,19 +261,19 @@ void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const mes
defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796)) && \ defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796)) && \
!defined(DISPLAY_FORCE_SMALL_FONTS) !defined(DISPLAY_FORCE_SMALL_FONTS)
if (currentResolution == ScreenResolution::High) { if (isHighResolution) {
NodeListRenderer::drawScaledXBitmap16x16(x, y - 1, 8, 8, imgUser, display); NodeListRenderer::drawScaledXBitmap16x16(x, y - 1, 8, 8, imgUser, display);
} else { } else {
display->drawFastImage(x, y + 3, 8, 8, imgUser); display->drawFastImage(x, y + 3, 8, 8, imgUser);
} }
#else #else
if (currentResolution == ScreenResolution::High) { if (isHighResolution) {
NodeListRenderer::drawScaledXBitmap16x16(x, y - 1, 8, 8, imgUser, display); NodeListRenderer::drawScaledXBitmap16x16(x, y - 1, 8, 8, imgUser, display);
} else { } else {
display->drawFastImage(x, y + 1, 8, 8, imgUser); display->drawFastImage(x, y + 1, 8, 8, imgUser);
} }
#endif #endif
int string_offset = (currentResolution == ScreenResolution::High) ? 9 : 0; int string_offset = (isHighResolution) ? 9 : 0;
display->drawString(x + 10 + string_offset, y - 2, usersString); display->drawString(x + 10 + string_offset, y - 2, usersString);
} }
@@ -328,12 +321,11 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
int line = 1; // which slot to use next int line = 1; // which slot to use next
std::string usernameStr; std::string usernameStr;
// === 1. Long Name (always try to show first) === // === 1. Long Name (always try to show first) ===
const char *username; #if defined(M5STACK_UNITC6L)
if (currentResolution == ScreenResolution::UltraLow) { const char *username = (node->has_user && node->user.long_name[0]) ? node->user.short_name : nullptr;
username = (node->has_user && node->user.long_name[0]) ? node->user.short_name : nullptr; #else
} else { const char *username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr;
username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr; #endif
}
if (username) { if (username) {
usernameStr = sanitizeString(username); // Sanitize the incoming long_name just in case usernameStr = sanitizeString(username); // Sanitize the incoming long_name just in case
@@ -509,7 +501,7 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
const int margin = 4; const int margin = 4;
// --------- PATCH FOR EINK NAV BAR (ONLY CHANGE BELOW) ----------- // --------- PATCH FOR EINK NAV BAR (ONLY CHANGE BELOW) -----------
#if defined(USE_EINK) #if defined(USE_EINK)
const int iconSize = (currentResolution == ScreenResolution::High) ? 16 : 8; const int iconSize = (isHighResolution) ? 16 : 8;
const int navBarHeight = iconSize + 6; const int navBarHeight = iconSize + 6;
#else #else
const int navBarHeight = 0; const int navBarHeight = 0;
@@ -567,11 +559,11 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
// === Header === // === Header ===
if (currentResolution == ScreenResolution::UltraLow) { #if defined(M5STACK_UNITC6L)
graphics::drawCommonHeader(display, x, y, "Home"); graphics::drawCommonHeader(display, x, y, "Home");
} else { #else
graphics::drawCommonHeader(display, x, y, ""); graphics::drawCommonHeader(display, x, y, "");
} #endif
// === Content below header === // === Content below header ===
@@ -586,15 +578,15 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
config.display.heading_bold = false; config.display.heading_bold = false;
// Display Region and Channel Utilization // Display Region and Channel Utilization
if (currentResolution == ScreenResolution::UltraLow) { #if defined(M5STACK_UNITC6L)
drawNodes(display, x, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online"); drawNodes(display, x, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online");
} else { #else
drawNodes(display, x + 1, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online"); drawNodes(display, x + 1, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online");
} #endif
char uptimeStr[32] = ""; char uptimeStr[32] = "";
if (currentResolution != ScreenResolution::UltraLow) { #if !defined(M5STACK_UNITC6L)
getUptimeStr(millis(), "Up", uptimeStr, sizeof(uptimeStr)); getUptimeStr(millis(), "Up", uptimeStr, sizeof(uptimeStr));
} #endif
display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeStr), getTextPositions(display)[line++], uptimeStr); display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeStr), getTextPositions(display)[line++], uptimeStr);
// === Second Row: Satellites and Voltage === // === Second Row: Satellites and Voltage ===
@@ -608,8 +600,15 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
} else { } else {
displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; 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;
int xOffset = (currentResolution == ScreenResolution::High) ? 6 : 0; 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); display->drawString(x + 11 + xOffset, getTextPositions(display)[line], displayLine);
} else { } else {
UIRenderer::drawGps(display, 0, getTextPositions(display)[line], gpsStatus); UIRenderer::drawGps(display, 0, getTextPositions(display)[line], gpsStatus);
@@ -648,22 +647,21 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
char chUtilPercentage[10]; char chUtilPercentage[10];
snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent()); snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent());
int chUtil_x = (currentResolution == ScreenResolution::High) ? display->getStringWidth(chUtil) + 10 int chUtil_x = (isHighResolution) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5;
: display->getStringWidth(chUtil) + 5;
int chUtil_y = getTextPositions(display)[line] + 3; int chUtil_y = getTextPositions(display)[line] + 3;
int chutil_bar_width = (currentResolution == ScreenResolution::High) ? 100 : 50; int chutil_bar_width = (isHighResolution) ? 100 : 50;
if (!config.bluetooth.enabled) { if (!config.bluetooth.enabled) {
#if defined(USE_EINK) #if defined(USE_EINK)
chutil_bar_width = (currentResolution == ScreenResolution::High) ? 50 : 30; chutil_bar_width = (isHighResolution) ? 50 : 30;
#else #else
chutil_bar_width = (currentResolution == ScreenResolution::High) ? 80 : 40; chutil_bar_width = (isHighResolution) ? 80 : 40;
#endif #endif
} }
int chutil_bar_height = (currentResolution == ScreenResolution::High) ? 12 : 7; int chutil_bar_height = (isHighResolution) ? 12 : 7;
int extraoffset = (currentResolution == ScreenResolution::High) ? 6 : 3; int extraoffset = (isHighResolution) ? 6 : 3;
if (!config.bluetooth.enabled) { if (!config.bluetooth.enabled) {
extraoffset = (currentResolution == ScreenResolution::High) ? 6 : 1; extraoffset = (isHighResolution) ? 6 : 1;
} }
int chutil_percent = airTime->channelUtilizationPercent(); int chutil_percent = airTime->channelUtilizationPercent();
@@ -723,7 +721,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
// === Fourth & Fifth Rows: Node Identity === // === Fourth & Fifth Rows: Node Identity ===
int textWidth = 0; int textWidth = 0;
int nameX = 0; int nameX = 0;
int yOffset = (currentResolution == ScreenResolution::High) ? 0 : 5; int yOffset = (isHighResolution) ? 0 : 5;
std::string longNameStr; std::string longNameStr;
if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) { if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) {
@@ -761,7 +759,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
// Start Functions to write date/time to the screen // Start Functions to write date/time to the screen
// Helper function to check if a year is a leap year // 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)); return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0));
} }
@@ -992,8 +990,15 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
} else { } else {
displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; 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;
int xOffset = (currentResolution == ScreenResolution::High) ? 6 : 0; 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); display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine);
} else { } else {
// Onboard GPS // Onboard GPS
@@ -1151,7 +1156,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
void UIRenderer::drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) void UIRenderer::drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{ {
static const uint8_t xbm[] = USERPREFS_OEM_IMAGE_DATA; static const uint8_t xbm[] = USERPREFS_OEM_IMAGE_DATA;
if (currentResolution == ScreenResolution::High) { if (isHighResolution) {
display->drawXbm(x + (SCREEN_WIDTH - USERPREFS_OEM_IMAGE_WIDTH) / 2, display->drawXbm(x + (SCREEN_WIDTH - USERPREFS_OEM_IMAGE_WIDTH) / 2,
y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - USERPREFS_OEM_IMAGE_HEIGHT) / 2 + 2, USERPREFS_OEM_IMAGE_WIDTH, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - USERPREFS_OEM_IMAGE_HEIGHT) / 2 + 2, USERPREFS_OEM_IMAGE_WIDTH,
USERPREFS_OEM_IMAGE_HEIGHT, xbm); USERPREFS_OEM_IMAGE_HEIGHT, xbm);
@@ -1176,7 +1181,7 @@ void UIRenderer::drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, O
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
const char *title = USERPREFS_OEM_TEXT; const char *title = USERPREFS_OEM_TEXT;
if (currentResolution == ScreenResolution::High) { if (isHighResolution) {
display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title); display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title);
} }
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
@@ -1220,15 +1225,15 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta
lastFrameChangeTime = millis(); lastFrameChangeTime = millis();
} }
const int iconSize = (currentResolution == ScreenResolution::High) ? 16 : 8; const int iconSize = isHighResolution ? 16 : 8;
const int spacing = (currentResolution == ScreenResolution::High) ? 8 : 4; const int spacing = isHighResolution ? 8 : 4;
const int bigOffset = (currentResolution == ScreenResolution::High) ? 1 : 0; const int bigOffset = isHighResolution ? 1 : 0;
const size_t totalIcons = screen->indicatorIcons.size(); const size_t totalIcons = screen->indicatorIcons.size();
if (totalIcons == 0) if (totalIcons == 0)
return; return;
const int navPadding = (currentResolution == ScreenResolution::High) ? 24 : 12; // padding per side const int navPadding = isHighResolution ? 24 : 12; // padding per side
int usableWidth = SCREEN_WIDTH - (navPadding * 2); int usableWidth = SCREEN_WIDTH - (navPadding * 2);
if (usableWidth < iconSize) if (usableWidth < iconSize)
@@ -1295,7 +1300,7 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta
display->setColor(BLACK); display->setColor(BLACK);
} }
if (currentResolution == ScreenResolution::High) { if (isHighResolution) {
NodeListRenderer::drawScaledXBitmap16x16(x, y, 8, 8, icon, display); NodeListRenderer::drawScaledXBitmap16x16(x, y, 8, 8, icon, display);
} else { } else {
display->drawXbm(x, y, iconSize, iconSize, icon); display->drawXbm(x, y, iconSize, iconSize, icon);
@@ -1310,7 +1315,7 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta
auto drawArrow = [&](bool rightSide) { auto drawArrow = [&](bool rightSide) {
display->setColor(WHITE); display->setColor(WHITE);
const int offset = (currentResolution == ScreenResolution::High) ? 3 : 1; const int offset = isHighResolution ? 3 : 1;
const int halfH = rectHeight / 2; const int halfH = rectHeight / 2;
const int top = (y - 2) + (rectHeight - halfH) / 2; const int top = (y - 2) + (rectHeight - halfH) / 2;

View File

@@ -34,7 +34,7 @@ class UIRenderer
public: public:
// Common UI elements // Common UI elements
static void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, 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 // GPS status functions
static void drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); 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 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); 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 // Overlay and special screens
static void drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *text); 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 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); 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) // Check if the display can render a string (detect special chars; emoji)
static bool haveGlyphs(const char *str); static bool haveGlyphs(const char *str);
}; // namespace UIRenderer }; // 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, 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}; 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_width 8
#define chirpy_small_image_height 8 #define chirpy_small_image_height 8
const uint8_t chirpy_small[] = {0x7f, 0x41, 0x55, 0x55, 0x55, 0x55, 0x41, 0x7f}; const uint8_t chirpy_small[] = {0x7f, 0x41, 0x55, 0x55, 0x55, 0x55, 0x41, 0x7f};

View File

@@ -8,5 +8,4 @@ build_flags =
-D MESHTASTIC_EXCLUDE_INPUTBROKER ; Suppress default input handling -D MESHTASTIC_EXCLUDE_INPUTBROKER ; Suppress default input handling
-D HAS_BUTTON=0 ; Suppress default ButtonThread -D HAS_BUTTON=0 ; Suppress default ButtonThread
lib_deps = lib_deps =
# TODO renovate
https://github.com/ZinggJM/GFX_Root#2.0.0 ; Used by InkHUD as a "slimmer" version of AdafruitGFX https://github.com/ZinggJM/GFX_Root#2.0.0 ; Used by InkHUD as a "slimmer" version of AdafruitGFX

View File

@@ -489,6 +489,8 @@ int32_t KbI2cBase::runOnce()
case 0x90: // fn+r INPUT_BROKER_MSG_REBOOT case 0x90: // fn+r INPUT_BROKER_MSG_REBOOT
case 0x91: // fn+t case 0x91: // fn+t
case 0xac: // fn+m INPUT_BROKER_MSG_MUTE_TOGGLE 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 0xAA: // fn+b INPUT_BROKER_MSG_BLUETOOTH_TOGGLE
case 0x8F: // fn+e INPUT_BROKER_MSG_EMOTE_LIST case 0x8F: // fn+e INPUT_BROKER_MSG_EMOTE_LIST
// just pass those unmodified // just pass those unmodified

View File

@@ -447,11 +447,9 @@ void setup()
LOG_INFO("\n\n//\\ E S H T /\\ S T / C\n"); LOG_INFO("\n\n//\\ E S H T /\\ S T / C\n");
#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) #if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM)
#ifndef SENSECAP_INDICATOR
// use PSRAM for malloc calls > 256 bytes // use PSRAM for malloc calls > 256 bytes
heap_caps_malloc_extmem_enable(256); heap_caps_malloc_extmem_enable(256);
#endif #endif
#endif
#if defined(DEBUG_MUTE) && defined(DEBUG_PORT) #if defined(DEBUG_MUTE) && defined(DEBUG_PORT)
DEBUG_PORT.printf("\r\n\r\n//\\ E S H T /\\ S T / C\r\n"); DEBUG_PORT.printf("\r\n\r\n//\\ E S H T /\\ S T / C\r\n");

View File

@@ -44,7 +44,6 @@ struct UIFrameEvent {
REDRAW_ONLY, // Don't change which frames are show, just redraw, asap REDRAW_ONLY, // Don't change which frames are show, just redraw, asap
REGENERATE_FRAMESET, // Regenerate (change? add? remove?) screen frames, honoring requestFocus() REGENERATE_FRAMESET, // Regenerate (change? add? remove?) screen frames, honoring requestFocus()
REGENERATE_FRAMESET_BACKGROUND, // Regenerate screen frames, Attempt to remain on the same frame throughout 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; } action = REDRAW_ONLY;
// We might want to pass additional data inside this struct at some point // We might want to pass additional data inside this struct at some point

View File

@@ -7,12 +7,10 @@
#include "../concurrency/Periodic.h" #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 "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 "MeshService.h"
#include "MessageStore.h"
#include "NodeDB.h" #include "NodeDB.h"
#include "PowerFSM.h" #include "PowerFSM.h"
#include "RTC.h" #include "RTC.h"
#include "TypeConversions.h" #include "TypeConversions.h"
#include "graphics/draw/MessageRenderer.h"
#include "main.h" #include "main.h"
#include "mesh-pb-constants.h" #include "mesh-pb-constants.h"
#include "meshUtils.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.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 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 // Send the packet into the mesh
DEBUG_HEAP_BEFORE; DEBUG_HEAP_BEFORE;
auto a = packetPool.allocCopy(p); auto a = packetPool.allocCopy(p);

View File

@@ -758,7 +758,6 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
} }
packetPool.release(p_encrypted); // Release the encrypted packet packetPool.release(p_encrypted); // Release the encrypted packet
p_encrypted = nullptr;
} }
void Router::perhapsHandleReceived(meshtastic_MeshPacket *p) void Router::perhapsHandleReceived(meshtastic_MeshPacket *p)

View File

@@ -92,7 +92,7 @@ class Router : protected concurrency::OSThread, protected PacketHistory
uint32_t rxDupe = 0, txRelayCanceled = 0; uint32_t rxDupe = 0, txRelayCanceled = 0;
// pointer to the encrypted packet // pointer to the encrypted packet
meshtastic_MeshPacket *p_encrypted = nullptr; meshtastic_MeshPacket *p_encrypted;
protected: protected:
friend class RoutingModule; friend class RoutingModule;

View File

@@ -24,6 +24,9 @@ PB_BIND(meshtastic_Data, meshtastic_Data, 2)
PB_BIND(meshtastic_KeyVerification, meshtastic_KeyVerification, AUTO) PB_BIND(meshtastic_KeyVerification, meshtastic_KeyVerification, AUTO)
PB_BIND(meshtastic_StoreForwardPlusPlus, meshtastic_StoreForwardPlusPlus, 2)
PB_BIND(meshtastic_Waypoint, meshtastic_Waypoint, AUTO) PB_BIND(meshtastic_Waypoint, meshtastic_Waypoint, AUTO)
@@ -121,6 +124,8 @@ PB_BIND(meshtastic_ChunkedPayloadResponse, meshtastic_ChunkedPayloadResponse, AU

View File

@@ -478,6 +478,17 @@ typedef enum _meshtastic_Routing_Error {
meshtastic_Routing_Error_RATE_LIMIT_EXCEEDED = 38 meshtastic_Routing_Error_RATE_LIMIT_EXCEEDED = 38
} meshtastic_Routing_Error; } meshtastic_Routing_Error;
/* enum message type? */
typedef enum _meshtastic_StoreForwardPlusPlus_SFPP_message_type {
/* message hash without chain hash means that no, it is not on the chain */
meshtastic_StoreForwardPlusPlus_SFPP_message_type_CANON_ANNOUNCE = 0,
meshtastic_StoreForwardPlusPlus_SFPP_message_type_CHAIN_QUERY = 1,
meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_REQUEST = 3,
meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE = 4,
meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_FIRSTHALF = 5,
meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_SECONDHALF = 6
} meshtastic_StoreForwardPlusPlus_SFPP_message_type;
/* The priority of this message for sending. /* The priority of this message for sending.
Higher priorities are sent first (when managing the transmit queue). Higher priorities are sent first (when managing the transmit queue).
This field is never sent over the air, it is only used internally inside of a local device node. This field is never sent over the air, it is only used internally inside of a local device node.
@@ -782,6 +793,24 @@ typedef struct _meshtastic_KeyVerification {
meshtastic_KeyVerification_hash2_t hash2; meshtastic_KeyVerification_hash2_t hash2;
} meshtastic_KeyVerification; } meshtastic_KeyVerification;
typedef PB_BYTES_ARRAY_T(32) meshtastic_StoreForwardPlusPlus_message_hash_t;
typedef PB_BYTES_ARRAY_T(32) meshtastic_StoreForwardPlusPlus_commit_hash_t;
typedef PB_BYTES_ARRAY_T(32) meshtastic_StoreForwardPlusPlus_root_hash_t;
typedef PB_BYTES_ARRAY_T(240) meshtastic_StoreForwardPlusPlus_message_t;
/* The actual over-the-mesh message doing store and forward++ */
typedef struct _meshtastic_StoreForwardPlusPlus { /* */
meshtastic_StoreForwardPlusPlus_SFPP_message_type sfpp_message_type;
meshtastic_StoreForwardPlusPlus_message_hash_t message_hash;
meshtastic_StoreForwardPlusPlus_commit_hash_t commit_hash;
meshtastic_StoreForwardPlusPlus_root_hash_t root_hash;
/* encapsulated message to share (may be split in half) */
meshtastic_StoreForwardPlusPlus_message_t message;
uint32_t encapsulated_id;
uint32_t encapsulated_to;
uint32_t encapsulated_from;
uint32_t encapsulated_rxtime;
} meshtastic_StoreForwardPlusPlus;
/* Waypoint message, used to share arbitrary locations across the mesh */ /* Waypoint message, used to share arbitrary locations across the mesh */
typedef struct _meshtastic_Waypoint { typedef struct _meshtastic_Waypoint {
/* Id of the waypoint */ /* Id of the waypoint */
@@ -1310,6 +1339,10 @@ extern "C" {
#define _meshtastic_Routing_Error_MAX meshtastic_Routing_Error_RATE_LIMIT_EXCEEDED #define _meshtastic_Routing_Error_MAX meshtastic_Routing_Error_RATE_LIMIT_EXCEEDED
#define _meshtastic_Routing_Error_ARRAYSIZE ((meshtastic_Routing_Error)(meshtastic_Routing_Error_RATE_LIMIT_EXCEEDED+1)) #define _meshtastic_Routing_Error_ARRAYSIZE ((meshtastic_Routing_Error)(meshtastic_Routing_Error_RATE_LIMIT_EXCEEDED+1))
#define _meshtastic_StoreForwardPlusPlus_SFPP_message_type_MIN meshtastic_StoreForwardPlusPlus_SFPP_message_type_CANON_ANNOUNCE
#define _meshtastic_StoreForwardPlusPlus_SFPP_message_type_MAX meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_SECONDHALF
#define _meshtastic_StoreForwardPlusPlus_SFPP_message_type_ARRAYSIZE ((meshtastic_StoreForwardPlusPlus_SFPP_message_type)(meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_SECONDHALF+1))
#define _meshtastic_MeshPacket_Priority_MIN meshtastic_MeshPacket_Priority_UNSET #define _meshtastic_MeshPacket_Priority_MIN meshtastic_MeshPacket_Priority_UNSET
#define _meshtastic_MeshPacket_Priority_MAX meshtastic_MeshPacket_Priority_MAX #define _meshtastic_MeshPacket_Priority_MAX meshtastic_MeshPacket_Priority_MAX
#define _meshtastic_MeshPacket_Priority_ARRAYSIZE ((meshtastic_MeshPacket_Priority)(meshtastic_MeshPacket_Priority_MAX+1)) #define _meshtastic_MeshPacket_Priority_ARRAYSIZE ((meshtastic_MeshPacket_Priority)(meshtastic_MeshPacket_Priority_MAX+1))
@@ -1338,6 +1371,8 @@ extern "C" {
#define meshtastic_Data_portnum_ENUMTYPE meshtastic_PortNum #define meshtastic_Data_portnum_ENUMTYPE meshtastic_PortNum
#define meshtastic_StoreForwardPlusPlus_sfpp_message_type_ENUMTYPE meshtastic_StoreForwardPlusPlus_SFPP_message_type
#define meshtastic_MeshPacket_priority_ENUMTYPE meshtastic_MeshPacket_Priority #define meshtastic_MeshPacket_priority_ENUMTYPE meshtastic_MeshPacket_Priority
@@ -1380,6 +1415,7 @@ extern "C" {
#define meshtastic_Routing_init_default {0, {meshtastic_RouteDiscovery_init_default}} #define meshtastic_Routing_init_default {0, {meshtastic_RouteDiscovery_init_default}}
#define meshtastic_Data_init_default {_meshtastic_PortNum_MIN, {0, {0}}, 0, 0, 0, 0, 0, 0, false, 0} #define meshtastic_Data_init_default {_meshtastic_PortNum_MIN, {0, {0}}, 0, 0, 0, 0, 0, 0, false, 0}
#define meshtastic_KeyVerification_init_default {0, {0, {0}}, {0, {0}}} #define meshtastic_KeyVerification_init_default {0, {0, {0}}, {0, {0}}}
#define meshtastic_StoreForwardPlusPlus_init_default {_meshtastic_StoreForwardPlusPlus_SFPP_message_type_MIN, {0, {0}}, {0, {0}}, {0, {0}}, {0, {0}}, 0, 0, 0, 0}
#define meshtastic_Waypoint_init_default {0, false, 0, false, 0, 0, 0, "", "", 0} #define meshtastic_Waypoint_init_default {0, false, 0, false, 0, 0, 0, "", "", 0}
#define meshtastic_MqttClientProxyMessage_init_default {"", 0, {{0, {0}}}, 0} #define meshtastic_MqttClientProxyMessage_init_default {"", 0, {{0, {0}}}, 0}
#define meshtastic_MeshPacket_init_default {0, 0, 0, 0, {meshtastic_Data_init_default}, 0, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, 0, _meshtastic_MeshPacket_Delayed_MIN, 0, 0, {0, {0}}, 0, 0, 0, 0, _meshtastic_MeshPacket_TransportMechanism_MIN} #define meshtastic_MeshPacket_init_default {0, 0, 0, 0, {meshtastic_Data_init_default}, 0, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, 0, _meshtastic_MeshPacket_Delayed_MIN, 0, 0, {0, {0}}, 0, 0, 0, 0, _meshtastic_MeshPacket_TransportMechanism_MIN}
@@ -1411,6 +1447,7 @@ extern "C" {
#define meshtastic_Routing_init_zero {0, {meshtastic_RouteDiscovery_init_zero}} #define meshtastic_Routing_init_zero {0, {meshtastic_RouteDiscovery_init_zero}}
#define meshtastic_Data_init_zero {_meshtastic_PortNum_MIN, {0, {0}}, 0, 0, 0, 0, 0, 0, false, 0} #define meshtastic_Data_init_zero {_meshtastic_PortNum_MIN, {0, {0}}, 0, 0, 0, 0, 0, 0, false, 0}
#define meshtastic_KeyVerification_init_zero {0, {0, {0}}, {0, {0}}} #define meshtastic_KeyVerification_init_zero {0, {0, {0}}, {0, {0}}}
#define meshtastic_StoreForwardPlusPlus_init_zero {_meshtastic_StoreForwardPlusPlus_SFPP_message_type_MIN, {0, {0}}, {0, {0}}, {0, {0}}, {0, {0}}, 0, 0, 0, 0}
#define meshtastic_Waypoint_init_zero {0, false, 0, false, 0, 0, 0, "", "", 0} #define meshtastic_Waypoint_init_zero {0, false, 0, false, 0, 0, 0, "", "", 0}
#define meshtastic_MqttClientProxyMessage_init_zero {"", 0, {{0, {0}}}, 0} #define meshtastic_MqttClientProxyMessage_init_zero {"", 0, {{0, {0}}}, 0}
#define meshtastic_MeshPacket_init_zero {0, 0, 0, 0, {meshtastic_Data_init_zero}, 0, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, 0, _meshtastic_MeshPacket_Delayed_MIN, 0, 0, {0, {0}}, 0, 0, 0, 0, _meshtastic_MeshPacket_TransportMechanism_MIN} #define meshtastic_MeshPacket_init_zero {0, 0, 0, 0, {meshtastic_Data_init_zero}, 0, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, 0, _meshtastic_MeshPacket_Delayed_MIN, 0, 0, {0, {0}}, 0, 0, 0, 0, _meshtastic_MeshPacket_TransportMechanism_MIN}
@@ -1489,6 +1526,15 @@ extern "C" {
#define meshtastic_KeyVerification_nonce_tag 1 #define meshtastic_KeyVerification_nonce_tag 1
#define meshtastic_KeyVerification_hash1_tag 2 #define meshtastic_KeyVerification_hash1_tag 2
#define meshtastic_KeyVerification_hash2_tag 3 #define meshtastic_KeyVerification_hash2_tag 3
#define meshtastic_StoreForwardPlusPlus_sfpp_message_type_tag 1
#define meshtastic_StoreForwardPlusPlus_message_hash_tag 2
#define meshtastic_StoreForwardPlusPlus_commit_hash_tag 3
#define meshtastic_StoreForwardPlusPlus_root_hash_tag 4
#define meshtastic_StoreForwardPlusPlus_message_tag 5
#define meshtastic_StoreForwardPlusPlus_encapsulated_id_tag 6
#define meshtastic_StoreForwardPlusPlus_encapsulated_to_tag 7
#define meshtastic_StoreForwardPlusPlus_encapsulated_from_tag 8
#define meshtastic_StoreForwardPlusPlus_encapsulated_rxtime_tag 9
#define meshtastic_Waypoint_id_tag 1 #define meshtastic_Waypoint_id_tag 1
#define meshtastic_Waypoint_latitude_i_tag 2 #define meshtastic_Waypoint_latitude_i_tag 2
#define meshtastic_Waypoint_longitude_i_tag 3 #define meshtastic_Waypoint_longitude_i_tag 3
@@ -1705,6 +1751,19 @@ X(a, STATIC, SINGULAR, BYTES, hash2, 3)
#define meshtastic_KeyVerification_CALLBACK NULL #define meshtastic_KeyVerification_CALLBACK NULL
#define meshtastic_KeyVerification_DEFAULT NULL #define meshtastic_KeyVerification_DEFAULT NULL
#define meshtastic_StoreForwardPlusPlus_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UENUM, sfpp_message_type, 1) \
X(a, STATIC, SINGULAR, BYTES, message_hash, 2) \
X(a, STATIC, SINGULAR, BYTES, commit_hash, 3) \
X(a, STATIC, SINGULAR, BYTES, root_hash, 4) \
X(a, STATIC, SINGULAR, BYTES, message, 5) \
X(a, STATIC, SINGULAR, UINT32, encapsulated_id, 6) \
X(a, STATIC, SINGULAR, UINT32, encapsulated_to, 7) \
X(a, STATIC, SINGULAR, UINT32, encapsulated_from, 8) \
X(a, STATIC, SINGULAR, UINT32, encapsulated_rxtime, 9)
#define meshtastic_StoreForwardPlusPlus_CALLBACK NULL
#define meshtastic_StoreForwardPlusPlus_DEFAULT NULL
#define meshtastic_Waypoint_FIELDLIST(X, a) \ #define meshtastic_Waypoint_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, id, 1) \ X(a, STATIC, SINGULAR, UINT32, id, 1) \
X(a, STATIC, OPTIONAL, SFIXED32, latitude_i, 2) \ X(a, STATIC, OPTIONAL, SFIXED32, latitude_i, 2) \
@@ -1980,6 +2039,7 @@ extern const pb_msgdesc_t meshtastic_RouteDiscovery_msg;
extern const pb_msgdesc_t meshtastic_Routing_msg; extern const pb_msgdesc_t meshtastic_Routing_msg;
extern const pb_msgdesc_t meshtastic_Data_msg; extern const pb_msgdesc_t meshtastic_Data_msg;
extern const pb_msgdesc_t meshtastic_KeyVerification_msg; extern const pb_msgdesc_t meshtastic_KeyVerification_msg;
extern const pb_msgdesc_t meshtastic_StoreForwardPlusPlus_msg;
extern const pb_msgdesc_t meshtastic_Waypoint_msg; extern const pb_msgdesc_t meshtastic_Waypoint_msg;
extern const pb_msgdesc_t meshtastic_MqttClientProxyMessage_msg; extern const pb_msgdesc_t meshtastic_MqttClientProxyMessage_msg;
extern const pb_msgdesc_t meshtastic_MeshPacket_msg; extern const pb_msgdesc_t meshtastic_MeshPacket_msg;
@@ -2013,6 +2073,7 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg;
#define meshtastic_Routing_fields &meshtastic_Routing_msg #define meshtastic_Routing_fields &meshtastic_Routing_msg
#define meshtastic_Data_fields &meshtastic_Data_msg #define meshtastic_Data_fields &meshtastic_Data_msg
#define meshtastic_KeyVerification_fields &meshtastic_KeyVerification_msg #define meshtastic_KeyVerification_fields &meshtastic_KeyVerification_msg
#define meshtastic_StoreForwardPlusPlus_fields &meshtastic_StoreForwardPlusPlus_msg
#define meshtastic_Waypoint_fields &meshtastic_Waypoint_msg #define meshtastic_Waypoint_fields &meshtastic_Waypoint_msg
#define meshtastic_MqttClientProxyMessage_fields &meshtastic_MqttClientProxyMessage_msg #define meshtastic_MqttClientProxyMessage_fields &meshtastic_MqttClientProxyMessage_msg
#define meshtastic_MeshPacket_fields &meshtastic_MeshPacket_msg #define meshtastic_MeshPacket_fields &meshtastic_MeshPacket_msg
@@ -2069,6 +2130,7 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg;
#define meshtastic_QueueStatus_size 23 #define meshtastic_QueueStatus_size 23
#define meshtastic_RouteDiscovery_size 256 #define meshtastic_RouteDiscovery_size 256
#define meshtastic_Routing_size 259 #define meshtastic_Routing_size 259
#define meshtastic_StoreForwardPlusPlus_size 371
#define meshtastic_ToRadio_size 504 #define meshtastic_ToRadio_size 504
#define meshtastic_User_size 115 #define meshtastic_User_size 115
#define meshtastic_Waypoint_size 165 #define meshtastic_Waypoint_size 165

View File

@@ -86,6 +86,9 @@ typedef enum _meshtastic_PortNum {
/* Paxcounter lib included in the firmware /* Paxcounter lib included in the firmware
ENCODING: protobuf */ ENCODING: protobuf */
meshtastic_PortNum_PAXCOUNTER_APP = 34, meshtastic_PortNum_PAXCOUNTER_APP = 34,
/* Store and Forward++ module included in the firmware
ENCODING: protobuf */
meshtastic_PortNum_STORE_FORWARD_PLUSPLUS_APP = 35,
/* Provides a hardware serial interface to send and receive from the Meshtastic network. /* Provides a hardware serial interface to send and receive from the Meshtastic network.
Connect to the RX/TX pins of a device with 38400 8N1. Packets received from the Meshtastic Connect to the RX/TX pins of a device with 38400 8N1. Packets received from the Meshtastic
network is forwarded to the RX pin while sending a packet to TX will go out to the Mesh network. network is forwarded to the RX pin while sending a packet to TX will go out to the Mesh network.

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 === // === State/UI ===
bool shouldDraw(); bool shouldDraw();
bool hasMessages(); bool hasMessages();
void showTemporaryMessage(const String &message);
void resetSearch(); void resetSearch();
void updateDestinationSelectionList(); void updateDestinationSelectionList();
void drawDestinationSelectionScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); 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; unsigned long lastUpdateMillis = 0;
String searchQuery; String searchQuery;
String freetext; String freetext;
String temporaryMessage;
// === Message Storage === // === 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]; char *messages[CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT];
int messagesCount = 0; int messagesCount = 0;
int currentMessageIndex = -1; int currentMessageIndex = -1;
@@ -167,9 +169,12 @@ class CannedMessageModule : public SinglePortModule, public Observable<const UIF
bool ack = false; // True = ACK received, False = NACK or failed 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 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) 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) 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
// === State Tracking === // === State Tracking ===
cannedMessageModuleRunState runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; cannedMessageModuleRunState runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;

View File

@@ -60,7 +60,9 @@ meshtastic_MeshPacket *DropzoneModule::sendConditions()
long hms = rtc_sec % SEC_PER_DAY; long hms = rtc_sec % SEC_PER_DAY;
hms = (hms + SEC_PER_DAY) % 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 // Check if the dropzone is open or closed by reading the analog pin

View File

@@ -61,6 +61,7 @@
#if ARCH_PORTDUINO #if ARCH_PORTDUINO
#include "input/LinuxInputImpl.h" #include "input/LinuxInputImpl.h"
#include "input/SeesawRotary.h" #include "input/SeesawRotary.h"
#include "modules/Native/StoreForwardPlusPlus.h"
#include "modules/Telemetry/HostMetrics.h" #include "modules/Telemetry/HostMetrics.h"
#if !MESHTASTIC_EXCLUDE_STOREFORWARD #if !MESHTASTIC_EXCLUDE_STOREFORWARD
#include "modules/StoreForwardModule.h" #include "modules/StoreForwardModule.h"
@@ -243,6 +244,7 @@ void setupModules()
#endif #endif
#if ARCH_PORTDUINO #if ARCH_PORTDUINO
new HostMetricsModule(); new HostMetricsModule();
new StoreForwardPlusPlusModule();
#endif #endif
#if HAS_TELEMETRY #if HAS_TELEMETRY
new DeviceTelemetryModule(); new DeviceTelemetryModule();

View File

@@ -0,0 +1,976 @@
// I've done a lot of this in SQLite for now, but honestly it needs to happen in memory, and get saved to sqlite during downtime
// TODO: Put some channel usage limits on this: Should be limited to 25% utilization, for instance
// TODO: custom hops. 1 maybe 0
// TODO: non-stratum0 nodes need to be pointed at their upstream source? Maybe
// TODO: Work without sending some of the hashes/ short hashes
// things may get weird if there are multiple stratum-0 nodes on a single mesh. Come up with mitigations
// Basic design:
// This module watches a channel for text messages.
// each message gets sha256 summed, and then appended to a git-style blockchain. Probably need a counter, too
// then the message, metadata, hash, and git hash information are saved. sqlite?
// nodes/sub-controllers can subscribe to a database
// A node can DM the controller, querying if a single message is on the chain, or asking for the last message hash
// if the message is not on the chain, the node can resend the message
// if the node lacks messages, it can request them
// will need the concept of sub-controllers, that subscribe to the central controller, can help push updates
// catch-up messages only go out when the mesh is low use %
// Normal firmware will only attempt to sync the chain a few times, then just ask for the latest few messages. A phone app can try
// harder
// host will periodically advertise its presence
// at least initially, there can only be one authoritative host
// message objects get a hash value
// the message chain gets a commit hash
//
#include "StoreForwardPlusPlus.h"
#include "MeshService.h"
#include "RTC.h"
#include "SHA256.h"
#include "meshUtils.h"
#include "modules/RoutingModule.h"
StoreForwardPlusPlusModule::StoreForwardPlusPlusModule()
: ProtobufModule("StoreForwardpp", meshtastic_PortNum_STORE_FORWARD_PLUSPLUS_APP, &meshtastic_StoreForwardPlusPlus_msg),
concurrency::OSThread("StoreForwardpp")
{
LOG_WARN("StoreForwardPlusPlusModule init");
if (portduino_config.sfpp_stratum0)
LOG_WARN("SF++ stratum0");
int res = sqlite3_open("test.db", &ppDb);
LOG_WARN("Result1 %u", res);
char *err = nullptr;
res = sqlite3_exec(ppDb, " \
CREATE TABLE channel_messages( \
destination INT NOT NULL, \
sender INT NOT NULL, \
packet_id INT NOT NULL, \
rx_time INT NOT NULL, \
root_hash BLOB NOT NULL, \
encrypted_bytes BLOB NOT NULL, \
message_hash BLOB NOT NULL, \
commit_hash BLOB NOT NULL, \
payload TEXT, \
PRIMARY KEY (message_hash) \
);",
NULL, NULL, &err);
LOG_WARN("Result2 %u", res);
if (err != nullptr)
LOG_ERROR("%s", err);
sqlite3_free(err);
res = sqlite3_exec(ppDb, " \
CREATE TABLE local_messages( \
destination INT NOT NULL, \
sender INT NOT NULL, \
packet_id INT NOT NULL, \
rx_time INT NOT NULL, \
channel_hash INT NOT NULL, \
encrypted_bytes BLOB NOT NULL, \
message_hash BLOB NOT NULL, \
payload TEXT, \
PRIMARY KEY (message_hash) \
);",
NULL, NULL, &err);
LOG_WARN("Result2 %u", res);
if (err != nullptr)
LOG_ERROR("%s", err);
sqlite3_free(err);
// create table DMs
res = sqlite3_exec(ppDb, " \
CREATE TABLE direct_messages( \
destination INT NOT NULL, \
sender INT NOT NULL, \
packet_id INT NOT NULL, \
rx_time INT NOT NULL, \
channel_hash INT NOT NULL, \
commit_hash BLOB NOT NULL, \
encrypted_bytes BLOB NOT NULL, \
message_hash BLOB NOT NULL, \
payload TEXT, \
PRIMARY KEY (message_hash) \
);",
NULL, NULL, &err);
LOG_WARN("Result2 %u", res);
if (err != nullptr)
LOG_ERROR("%s", err);
sqlite3_free(err);
// mappings table -- connects the root hashes to channel hashes and DM identifiers
res = sqlite3_exec(ppDb, " \
CREATE TABLE mappings( \
chain_type INT NOT NULL, \
identifier INT NOT NULL, \
root_hash BLOB NOT NULL, \
PRIMARY KEY (identifier) \
);",
NULL, NULL, &err);
LOG_WARN("Result2 %u", res);
if (err != nullptr)
LOG_ERROR("%s", err);
sqlite3_free(err);
// store schema version somewhere
// prepared statements *should* make this faster.
sqlite3_prepare_v2(ppDb, "INSERT INTO channel_messages (destination, sender, packet_id, root_hash, \
encrypted_bytes, message_hash, rx_time, commit_hash, payload) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?);",
-1, &chain_insert_stmt, NULL);
sqlite3_prepare_v2(ppDb, "INSERT INTO local_messages (destination, sender, packet_id, channel_hash, \
encrypted_bytes, message_hash, rx_time, payload) VALUES(?, ?, ?, ?, ?, ?, ?, ?);",
-1, &scratch_insert_stmt, NULL);
sqlite3_prepare_v2(ppDb, "select destination, sender, packet_id, encrypted_bytes, message_hash, rx_time, channel_hash \
from local_messages where channel_hash=? order by rx_time asc LIMIT 1;", // earliest first
-1, &fromScratchStmt, NULL);
sqlite3_prepare_v2(ppDb,
"select destination, sender, packet_id, encrypted_bytes, message_hash, rx_time, channel_hash, payload \
from local_messages where message_hash=? order by rx_time asc LIMIT 1;", // earliest first
-1, &fromScratchByHashStmt, NULL);
sqlite3_prepare_v2(ppDb, "SELECT COUNT(*) from channel_messages where message_hash=?", -1, &checkDup, NULL);
sqlite3_prepare_v2(ppDb, "SELECT COUNT(*) from local_messages where message_hash=?", -1, &checkScratch, NULL);
sqlite3_prepare_v2(ppDb, "DELETE from local_messages where message_hash=?", -1, &removeScratch, NULL);
sqlite3_prepare_v2(ppDb, "UPDATE channel_messages SET payload=? WHERE message_hash=?", -1, &updatePayloadStmt, NULL);
sqlite3_prepare_v2(ppDb, "select commit_hash from channel_messages where root_hash=? order by rowid ASC;", -1,
&getNextHashStmt, NULL);
sqlite3_prepare_v2(
ppDb, "select commit_hash, message_hash, rx_time from channel_messages where root_hash=? order by rowid desc LIMIT 1;",
-1, &getChainEndStmt, NULL);
sqlite3_prepare_v2(ppDb, "select destination, sender, packet_id, encrypted_bytes, message_hash, rx_time \
from channel_messages where commit_hash=?;",
-1, &getLinkStmt, NULL);
encryptedOk = true;
// wait about 15 seconds after boot for the first runOnce()
// TODO: When not doing active development, adjust this to a longer time
this->setInterval(15 * 1000);
}
int32_t StoreForwardPlusPlusModule::runOnce()
{
LOG_WARN("StoreForward++ runONce");
if (getRTCQuality() < RTCQualityNTP) {
LOG_WARN("StoreForward++ deferred due to time quality %u", getRTCQuality());
return 5 * 60 * 1000;
}
uint8_t root_hash_bytes[32] = {0};
ChannelHash hash = channels.getHash(0);
getOrAddRootFromChannelHash(hash, root_hash_bytes);
// get tip of chain for this channel
uint8_t last_message_commit_hash[32] = {0};
uint8_t last_message_hash[32] = {0};
uint32_t chain_end_rx_time = getChainEnd(hash, last_message_commit_hash, last_message_hash);
if (chain_end_rx_time == 0) {
LOG_WARN("Store and Forward++ database lookup returned null");
return 60 * 60 * 1000;
}
// broadcast the tip of the chain
canonAnnounce(last_message_hash, last_message_commit_hash, root_hash_bytes, chain_end_rx_time);
// eventually timeout things on the scratch queue
return 60 * 60 * 1000;
}
bool StoreForwardPlusPlusModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_StoreForwardPlusPlus *t)
{
LOG_WARN("in handleReceivedProtobuf");
LOG_WARN("Sfp++ node %u sent us sf++ packet", mp.from);
printBytes("commit_hash ", t->commit_hash.bytes, t->commit_hash.size);
printBytes("root_hash ", t->root_hash.bytes, t->root_hash.size);
if (t->sfpp_message_type == meshtastic_StoreForwardPlusPlus_SFPP_message_type_CANON_ANNOUNCE) {
// check commit_hash.size
if (portduino_config.sfpp_stratum0) {
LOG_WARN("Received a CANON_ANNOUNCE while stratum 0");
uint8_t next_commit_hash[32] = {0};
if (getNextHash(t->root_hash.bytes, t->commit_hash.bytes, next_commit_hash)) {
printBytes("next chain hash: ", next_commit_hash, 32);
broadcastLink(next_commit_hash, t->root_hash.bytes);
}
} else {
uint8_t tmp_root_hash_bytes[32] = {0};
LOG_WARN("Received a CANON_ANNOUNCE");
if (getRootFromChannelHash(router->p_encrypted->channel, tmp_root_hash_bytes)) {
// we found the hash, check if it's the right one
if (memcmp(tmp_root_hash_bytes, t->root_hash.bytes, 32) != 0) {
LOG_WARN("Found root hash, and it doesn't match!");
return true;
}
} else {
addRootToMappings(router->p_encrypted->channel, t->root_hash.bytes);
LOG_WARN("Adding root hash to mappings");
}
// get tip of chain for this channel
uint8_t last_message_commit_hash[32] = {0};
uint8_t last_message_hash[32] = {0};
// get chain tip
if (getChainEnd(router->p_encrypted->channel, last_message_commit_hash, last_message_hash)) {
if (memcmp(last_message_commit_hash, t->commit_hash.bytes, 32) == 0) {
LOG_WARN("End of chain matches!");
sendFromScratch(router->p_encrypted->channel);
// TODO: Send a message from the local queue
} else {
("End of chain does not match!");
// We just got an end of chain announce, checking if we have seen this message and have it in scratch.
if (isInScratch(t->message_hash.bytes)) {
link_object scratch_object = getFromScratch(t->message_hash.bytes, t->message_hash.size);
// if this matches, we don't need to request the message
// we know exactly what it is
if (checkCommitHash(scratch_object, t->commit_hash.bytes, t->message_hash.size)) {
scratch_object.has_commit_hash = true;
memcpy(scratch_object.commit_hash, t->commit_hash.bytes, 32);
addToChain(scratch_object);
removeFromScratch(t->message_hash.bytes);
// short circuit and return
// falls through to a request for the message
return true;
}
}
requestNextMessage(t->root_hash.bytes, last_message_commit_hash);
}
} else { // if chainEnd()
LOG_WARN("No Messages on this chain, request!");
requestNextMessage(t->root_hash.bytes, t->root_hash.bytes);
}
}
} else if (t->sfpp_message_type == meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_REQUEST) {
uint8_t next_commit_hash[32] = {0};
LOG_WARN("Received link request");
if (getNextHash(t->root_hash.bytes, t->commit_hash.bytes, next_commit_hash)) {
printBytes("next chain hash: ", next_commit_hash, 32);
broadcastLink(next_commit_hash, t->root_hash.bytes);
}
// if root and chain hashes are the same, grab the first message on the chain
// if different, get the message directly after.
// check if the root
} else if (t->sfpp_message_type == meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE) {
LOG_WARN("Link Provide received!");
// TODO: Check for root hash in mappings
link_object incoming_link = ingestLinkMessage(t);
if (portduino_config.sfpp_stratum0) {
if (isInDB(incoming_link.message_hash)) {
LOG_WARN("Received link already in chain");
// TODO: respond with last link?
}
if (!getRootFromChannelHash(router->p_encrypted->channel, incoming_link.root_hash)) {
LOG_WARN("Hash bytes not found for incoming link");
return true;
}
// calculate the commit_hash
addToChain(incoming_link);
// not super thrilled about doing two broadcasts. Maybe just schedule the canonAnnounce?
canonAnnounce(incoming_link.message_hash, incoming_link.commit_hash, incoming_link.root_hash, incoming_link.rx_time);
rebroadcastLinkObject(incoming_link);
} else {
addToChain(incoming_link);
if (isInScratch(incoming_link.message_hash)) {
link_object scratch_object = getFromScratch(incoming_link.message_hash, 32);
if (scratch_object.payload != "") {
updatePayload(incoming_link.message_hash, scratch_object.payload);
}
removeFromScratch(incoming_link.message_hash);
} else {
// TODO: compare the time, and don't rebroadcast really old messages
// if this packet is new to us, we rebroadcast it
LOG_WARN("Attempting to Rebroadcast2");
rebroadcastLinkObject(incoming_link);
}
requestNextMessage(t->root_hash.bytes, t->commit_hash.bytes);
}
}
return true;
}
ProcessMessage StoreForwardPlusPlusModule::handleReceived(const meshtastic_MeshPacket &mp)
{
// To avoid terrible time problems, require NTP or GPS time
if (getRTCQuality() < RTCQualityNTP) {
return ProcessMessage::CONTINUE;
}
// For the moment, this is strictly LoRa
if (mp.transport_mechanism != meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA) {
return ProcessMessage::CONTINUE; // Let others look at this message also if they want
}
// will eventually host DMs and other undecodable messages
if (mp.which_payload_variant != meshtastic_MeshPacket_decoded_tag) {
return ProcessMessage::CONTINUE; // Let others look at this message also if they want
}
LOG_WARN("in handleReceived");
if (mp.decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP && mp.to == NODENUM_BROADCAST) {
link_object lo = ingestTextPacket(mp, router->p_encrypted);
if (isInDB(lo.message_hash)) {
LOG_WARN("found message in db");
// We may have this message already, but we may not have the payload
// if we do, we can update the payload in the database
if (lo.payload != "")
updatePayload(lo.message_hash, lo.payload);
return ProcessMessage::CONTINUE;
}
if (!portduino_config.sfpp_stratum0) {
if (!isInDB(lo.message_hash)) {
addToScratch(lo);
LOG_WARN("added message to scratch");
// send link to upstream?
}
return ProcessMessage::CONTINUE;
}
addToChain(lo);
// TODO: Limit to 25% bandwidth
canonAnnounce(lo.message_hash, lo.commit_hash, lo.root_hash, lo.rx_time);
return ProcessMessage::CONTINUE; // Let others look at this message also if they want
} else if (mp.decoded.portnum == meshtastic_PortNum_STORE_FORWARD_PLUSPLUS_APP) {
LOG_WARN("Got a STORE_FORWARD++ packet");
meshtastic_StoreForwardPlusPlus scratch;
pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, meshtastic_StoreForwardPlusPlus_fields, &scratch);
handleReceivedProtobuf(mp, &scratch);
return ProcessMessage::CONTINUE;
}
return ProcessMessage::CONTINUE;
}
bool StoreForwardPlusPlusModule::getRootFromChannelHash(ChannelHash _ch_hash, uint8_t *_root_hash)
{
bool found = false;
sqlite3_stmt *getHash;
int rc = sqlite3_prepare_v2(ppDb, "select root_hash from mappings where identifier=?;", -1, &getHash, NULL);
sqlite3_bind_int(getHash, 1, _ch_hash);
sqlite3_step(getHash);
uint8_t *tmp_root_hash = (uint8_t *)sqlite3_column_blob(getHash, 0);
if (tmp_root_hash) {
LOG_WARN("Found root hash!");
memcpy(_root_hash, tmp_root_hash, 32);
found = true;
}
sqlite3_finalize(getHash);
return found;
}
ChannelHash StoreForwardPlusPlusModule::getChannelHashFromRoot(uint8_t *_root_hash)
{
sqlite3_stmt *getHash;
int rc = sqlite3_prepare_v2(ppDb, "select identifier from mappings where root_hash=?;", -1, &getHash, NULL);
sqlite3_bind_blob(getHash, 1, _root_hash, 32, NULL);
sqlite3_step(getHash);
ChannelHash tmp_hash = (ChannelHash)sqlite3_column_int(getHash, 0);
sqlite3_finalize(getHash);
return tmp_hash;
}
// return code indicates newly created chain
bool StoreForwardPlusPlusModule::getOrAddRootFromChannelHash(ChannelHash _ch_hash, uint8_t *_root_hash)
{
LOG_WARN("getOrAddRootFromChannelHash()");
bool isNew = !getRootFromChannelHash(_ch_hash, _root_hash);
if (isNew) {
if (portduino_config.sfpp_stratum0) {
LOG_WARN("Generating Root hash!");
// generate root hash
SHA256 commit_hash;
commit_hash.update(&_ch_hash, sizeof(_ch_hash));
NodeNum ourNode = nodeDB->getNodeNum();
commit_hash.update(&ourNode, sizeof(ourNode));
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true);
commit_hash.update(&rtc_sec, sizeof(rtc_sec));
commit_hash.finalize(_root_hash, 32);
addRootToMappings(_ch_hash, _root_hash);
}
}
return isNew;
}
bool StoreForwardPlusPlusModule::addRootToMappings(ChannelHash _ch_hash, uint8_t *_root_hash)
{
LOG_WARN("addRootToMappings()");
printBytes("_root_hash", _root_hash, 32);
sqlite3_stmt *getHash;
// write to the table
int rc =
sqlite3_prepare_v2(ppDb, "INSERT INTO mappings (chain_type, identifier, root_hash) VALUES(?, ?, ?);", -1, &getHash, NULL);
LOG_WARN("%d", rc);
int type = chain_types::channel_chain;
// note, must be an int variable
sqlite3_bind_int(getHash, 1, type);
sqlite3_bind_int(getHash, 2, _ch_hash);
sqlite3_bind_blob(getHash, 3, _root_hash, 32, NULL);
// sqlite3_bind_int(getHash, 4, nodeToAdd);
rc = sqlite3_step(getHash);
LOG_WARN("result %u, %s", rc, sqlite3_errmsg(ppDb));
sqlite3_finalize(getHash);
return true;
}
uint32_t StoreForwardPlusPlusModule::getChainEnd(ChannelHash _ch_hash, uint8_t *_commit_hash, uint8_t *_message_hash)
{
LOG_WARN("getChainEnd");
uint8_t _root_hash[32] = {0};
if (!getRootFromChannelHash(_ch_hash, _root_hash)) {
LOG_WARN("No root hash found for channel %u", _ch_hash);
return 0;
}
int rc;
sqlite3_bind_blob(getChainEndStmt, 1, _root_hash, 32, NULL);
sqlite3_step(getChainEndStmt);
uint8_t *last_message_commit_hash = (uint8_t *)sqlite3_column_blob(getChainEndStmt, 0);
uint8_t *last_message_hash = (uint8_t *)sqlite3_column_blob(getChainEndStmt, 1);
uint32_t _rx_time = sqlite3_column_int(getChainEndStmt, 2);
if (last_message_commit_hash != nullptr) {
memcpy(_commit_hash, last_message_commit_hash, 32);
}
if (last_message_hash != nullptr) {
memcpy(_message_hash, last_message_hash, 32);
}
if (last_message_commit_hash == nullptr || last_message_hash == nullptr) {
LOG_WARN("Store and Forward++ database lookup returned null");
sqlite3_reset(getChainEndStmt);
return 0;
}
sqlite3_reset(getChainEndStmt);
return _rx_time;
}
void StoreForwardPlusPlusModule::requestNextMessage(uint8_t *_root_hash, uint8_t *_commit_hash)
{
meshtastic_StoreForwardPlusPlus storeforward = meshtastic_StoreForwardPlusPlus_init_zero;
storeforward.sfpp_message_type = meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_REQUEST;
// set root hash
// set chain hash
storeforward.commit_hash.size = 32;
memcpy(storeforward.commit_hash.bytes, _commit_hash, 32);
// set root hash
storeforward.root_hash.size = 32;
memcpy(storeforward.root_hash.bytes, _root_hash, 32);
// storeforward.
meshtastic_MeshPacket *p = allocDataProtobuf(storeforward);
p->to = NODENUM_BROADCAST;
p->decoded.want_response = false;
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
p->channel = 0;
LOG_INFO("Send packet to mesh");
service->sendToMesh(p, RX_SRC_LOCAL, true);
}
bool StoreForwardPlusPlusModule::getNextHash(uint8_t *_root_hash, uint8_t *_commit_hash, uint8_t *next_commit_hash)
{
LOG_WARN("getNextHash");
ChannelHash _channel_hash = getChannelHashFromRoot(_root_hash);
LOG_WARN("_channel_hash %u", _channel_hash);
int rc;
sqlite3_bind_blob(getNextHashStmt, 1, _root_hash, 32, NULL);
bool next_hash = false;
// asking for the first entry on the chain
if (memcmp(_root_hash, _commit_hash, 32) == 0) {
rc = sqlite3_step(getNextHashStmt);
if (rc != SQLITE_OK) {
LOG_WARN("here2 %u, %s", rc, sqlite3_errmsg(ppDb));
}
uint8_t *tmp_commit_hash = (uint8_t *)sqlite3_column_blob(getNextHashStmt, 0);
printBytes("commit_hash", tmp_commit_hash, 32);
memcpy(next_commit_hash, tmp_commit_hash, 32);
next_hash = true;
} else {
bool found_hash = false;
LOG_WARN("Looking for next hashes");
uint8_t *tmp_commit_hash;
while (sqlite3_step(getNextHashStmt) != SQLITE_DONE) {
tmp_commit_hash = (uint8_t *)sqlite3_column_blob(getNextHashStmt, 0);
if (found_hash) {
LOG_WARN("Found hash");
memcpy(next_commit_hash, tmp_commit_hash, 32);
next_hash = true;
break;
}
if (memcmp(tmp_commit_hash, _commit_hash, 32) == 0)
found_hash = true;
}
}
sqlite3_reset(getNextHashStmt);
return next_hash;
}
bool StoreForwardPlusPlusModule::broadcastLink(uint8_t *_commit_hash, uint8_t *_root_hash)
{
int rc;
LOG_WARN("%d", rc);
if (rc != SQLITE_OK) {
LOG_WARN("here2 %u, %s", rc, sqlite3_errmsg(ppDb));
}
sqlite3_bind_blob(getLinkStmt, 1, _commit_hash, 32, NULL);
sqlite3_step(getLinkStmt);
meshtastic_StoreForwardPlusPlus storeforward = meshtastic_StoreForwardPlusPlus_init_zero;
storeforward.sfpp_message_type = meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE;
storeforward.encapsulated_to = sqlite3_column_int(getLinkStmt, 0);
storeforward.encapsulated_from = sqlite3_column_int(getLinkStmt, 1);
storeforward.encapsulated_id = sqlite3_column_int(getLinkStmt, 2);
uint8_t *_payload = (uint8_t *)sqlite3_column_blob(getLinkStmt, 3);
storeforward.message.size = sqlite3_column_bytes(getLinkStmt, 3);
memcpy(storeforward.message.bytes, _payload, storeforward.message.size);
uint8_t *_message_hash = (uint8_t *)sqlite3_column_blob(getLinkStmt, 4);
storeforward.message_hash.size = 32;
memcpy(storeforward.message_hash.bytes, _message_hash, storeforward.message_hash.size);
storeforward.encapsulated_rxtime = sqlite3_column_int(getLinkStmt, 5);
storeforward.commit_hash.size = 32;
memcpy(storeforward.commit_hash.bytes, _commit_hash, 32);
storeforward.root_hash.size = 32;
memcpy(storeforward.root_hash.bytes, _root_hash, 32);
sqlite3_reset(getLinkStmt);
meshtastic_MeshPacket *p = allocDataProtobuf(storeforward);
p->to = NODENUM_BROADCAST;
p->decoded.want_response = false;
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
p->channel = 0;
LOG_INFO("Send link to mesh");
service->sendToMesh(p, RX_SRC_LOCAL, true);
return true;
}
bool StoreForwardPlusPlusModule::sendFromScratch(uint8_t _channel_hash)
{
LOG_WARN("sendFromScratch");
// "select destination, sender, packet_id, channel_hash, encrypted_bytes, message_hash, rx_time \
// from local_messages order by rx_time desc LIMIT 1;"
sqlite3_bind_int(fromScratchStmt, 1, _channel_hash);
if (sqlite3_step(fromScratchStmt) == SQLITE_DONE) {
LOG_WARN("No messages in scratch to forward");
return false;
}
uint8_t _root_hash[32] = {0};
if (!getRootFromChannelHash(_channel_hash, _root_hash)) {
LOG_ERROR("Error getting root hash");
return false;
}
meshtastic_StoreForwardPlusPlus storeforward = meshtastic_StoreForwardPlusPlus_init_zero;
storeforward.sfpp_message_type = meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE;
storeforward.encapsulated_to = sqlite3_column_int(fromScratchStmt, 0);
storeforward.encapsulated_from = sqlite3_column_int(fromScratchStmt, 1);
storeforward.encapsulated_id = sqlite3_column_int(fromScratchStmt, 2);
uint8_t *_encrypted = (uint8_t *)sqlite3_column_blob(fromScratchStmt, 3);
storeforward.message.size = sqlite3_column_bytes(fromScratchStmt, 3);
memcpy(storeforward.message.bytes, _encrypted, storeforward.message.size);
uint8_t *_message_hash = (uint8_t *)sqlite3_column_blob(fromScratchStmt, 4);
storeforward.message_hash.size = 32;
memcpy(storeforward.message_hash.bytes, _message_hash, storeforward.message_hash.size);
storeforward.encapsulated_rxtime = sqlite3_column_int(fromScratchStmt, 5);
storeforward.root_hash.size = 32;
memcpy(storeforward.root_hash.bytes, _root_hash, 32);
sqlite3_reset(fromScratchStmt);
meshtastic_MeshPacket *p = allocDataProtobuf(storeforward);
p->to = NODENUM_BROADCAST;
p->decoded.want_response = false;
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
p->channel = 0;
LOG_INFO("Send link to mesh");
service->sendToMesh(p, RX_SRC_LOCAL, true);
return true;
}
bool StoreForwardPlusPlusModule::addToChain(link_object &lo)
{
LOG_WARN("Add to chain");
// we may need to calculate the commit hash at this point
if (!lo.has_commit_hash) {
SHA256 commit_hash;
uint8_t last_message_hash[32] = {0};
uint8_t last_commit_hash[32] = {0};
commit_hash.reset();
if (getChainEnd(lo.channel_hash, last_commit_hash, last_message_hash)) {
printBytes("last message: 0x", last_commit_hash, 32);
commit_hash.update(last_commit_hash, 32);
} else {
printBytes("new chain root: 0x", lo.root_hash, 32);
commit_hash.update(lo.root_hash, 32);
}
commit_hash.update(lo.message_hash, 32);
// message_hash.update(&mp.rx_time, sizeof(mp.rx_time));
commit_hash.finalize(lo.commit_hash, 32);
}
// push a message into the local chain DB
// destination
sqlite3_bind_int(chain_insert_stmt, 1, lo.to);
// sender
sqlite3_bind_int(chain_insert_stmt, 2, lo.from);
// packet_id
sqlite3_bind_int(chain_insert_stmt, 3, lo.id);
// root_hash
sqlite3_bind_blob(chain_insert_stmt, 4, lo.root_hash, 32, NULL);
// encrypted_bytes
sqlite3_bind_blob(chain_insert_stmt, 5, lo.encrypted_bytes, lo.encrypted_len, NULL);
// message_hash
sqlite3_bind_blob(chain_insert_stmt, 6, lo.message_hash, 32, NULL);
// rx_time
sqlite3_bind_int(chain_insert_stmt, 7, lo.rx_time);
// commit_hash
sqlite3_bind_blob(chain_insert_stmt, 8, lo.commit_hash, 32, NULL);
// payload
sqlite3_bind_text(chain_insert_stmt, 9, lo.payload.c_str(), lo.payload.length(), NULL);
sqlite3_step(chain_insert_stmt);
sqlite3_reset(chain_insert_stmt);
return true;
}
bool StoreForwardPlusPlusModule::addToScratch(link_object &lo)
{
// TODO: Make a data structure for this data
// push a message into the local chain DB
// destination
sqlite3_bind_int(scratch_insert_stmt, 1, lo.to);
// sender
sqlite3_bind_int(scratch_insert_stmt, 2, lo.from);
// packet_id
sqlite3_bind_int(scratch_insert_stmt, 3, lo.id);
// root_hash
sqlite3_bind_blob(scratch_insert_stmt, 4, lo.root_hash, 32, NULL);
// encrypted_bytes
sqlite3_bind_blob(scratch_insert_stmt, 5, lo.encrypted_bytes, lo.encrypted_len, NULL);
// message_hash
sqlite3_bind_blob(scratch_insert_stmt, 6, lo.message_hash, 32, NULL);
// rx_time
sqlite3_bind_int(scratch_insert_stmt, 7, lo.rx_time);
// payload
sqlite3_bind_text(scratch_insert_stmt, 8, lo.payload.c_str(), lo.payload.length(), NULL);
const char *_error_mesg = sqlite3_errmsg(ppDb);
LOG_WARN("step %u, %s", sqlite3_step(scratch_insert_stmt), _error_mesg);
sqlite3_reset(scratch_insert_stmt);
return true;
}
void StoreForwardPlusPlusModule::canonAnnounce(uint8_t *_message_hash, uint8_t *_commit_hash, uint8_t *_root_hash,
uint32_t _rx_time)
{
meshtastic_StoreForwardPlusPlus storeforward = meshtastic_StoreForwardPlusPlus_init_zero;
storeforward.sfpp_message_type = meshtastic_StoreForwardPlusPlus_SFPP_message_type_CANON_ANNOUNCE;
// set root hash
// set message hash
storeforward.message_hash.size = 32;
memcpy(storeforward.message_hash.bytes, _message_hash, 32);
// set chain hash
storeforward.commit_hash.size = 32;
memcpy(storeforward.commit_hash.bytes, _commit_hash, 32);
// set root hash
storeforward.root_hash.size = 32;
memcpy(storeforward.root_hash.bytes, _root_hash, 32);
storeforward.encapsulated_rxtime = _rx_time;
// storeforward.
meshtastic_MeshPacket *p = allocDataProtobuf(storeforward);
p->to = NODENUM_BROADCAST;
p->decoded.want_response = false;
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
p->channel = 0;
LOG_INFO("Send packet to mesh");
service->sendToMesh(p, RX_SRC_LOCAL, true);
}
bool StoreForwardPlusPlusModule::isInDB(uint8_t *message_hash_bytes)
{
sqlite3_bind_blob(checkDup, 1, message_hash_bytes, 32, NULL);
sqlite3_step(checkDup);
int numberFound = sqlite3_column_int(checkDup, 0);
sqlite3_reset(checkDup);
if (numberFound > 0)
return true;
return false;
}
bool StoreForwardPlusPlusModule::isInScratch(uint8_t *message_hash_bytes)
{
LOG_WARN("isInScratch");
sqlite3_bind_blob(checkScratch, 1, message_hash_bytes, 32, NULL);
sqlite3_step(checkScratch);
int numberFound = sqlite3_column_int(checkScratch, 0);
sqlite3_reset(checkScratch);
if (numberFound > 0)
return true;
return false;
}
void StoreForwardPlusPlusModule::removeFromScratch(uint8_t *message_hash_bytes)
{
LOG_WARN("removeFromScratch");
sqlite3_bind_blob(removeScratch, 1, message_hash_bytes, 32, NULL);
sqlite3_step(removeScratch);
int numberFound = sqlite3_column_int(removeScratch, 0);
sqlite3_reset(removeScratch);
}
void StoreForwardPlusPlusModule::updatePayload(uint8_t *message_hash_bytes, std::string payload)
{
LOG_WARN("updatePayload");
sqlite3_bind_text(updatePayloadStmt, 1, payload.c_str(), payload.length(), NULL);
sqlite3_bind_blob(updatePayloadStmt, 2, message_hash_bytes, 32, NULL);
auto res = sqlite3_step(updatePayloadStmt);
const char *_error_mesg = sqlite3_errmsg(ppDb);
LOG_WARN("step %u, %s", res, _error_mesg);
sqlite3_reset(updatePayloadStmt);
}
StoreForwardPlusPlusModule::link_object StoreForwardPlusPlusModule::getFromScratch(uint8_t *message_hash_bytes, size_t hash_len)
{
// vscode wrote this
LOG_WARN("getFromScratch");
link_object lo;
sqlite3_bind_blob(fromScratchByHashStmt, 1, message_hash_bytes, hash_len, NULL);
auto res = sqlite3_step(fromScratchByHashStmt);
const char *_error_mesg = sqlite3_errmsg(ppDb);
LOG_WARN("step %u, %s", res, _error_mesg);
lo.to = sqlite3_column_int(fromScratchByHashStmt, 0);
lo.from = sqlite3_column_int(fromScratchByHashStmt, 1);
lo.id = sqlite3_column_int(fromScratchByHashStmt, 2);
uint8_t *encrypted_bytes = (uint8_t *)sqlite3_column_blob(fromScratchByHashStmt, 3);
lo.encrypted_len = sqlite3_column_bytes(fromScratchByHashStmt, 3);
memcpy(lo.encrypted_bytes, encrypted_bytes, lo.encrypted_len);
uint8_t *message_hash = (uint8_t *)sqlite3_column_blob(fromScratchByHashStmt, 4);
memcpy(lo.message_hash, message_hash, 32);
lo.rx_time = sqlite3_column_int(fromScratchByHashStmt, 5);
lo.channel_hash - sqlite3_column_int(fromScratchByHashStmt, 6);
lo.payload =
std::string((char *)sqlite3_column_text(fromScratchByHashStmt, 7), sqlite3_column_bytes(fromScratchByHashStmt, 7));
sqlite3_reset(fromScratchByHashStmt);
return lo;
}
StoreForwardPlusPlusModule::link_object
StoreForwardPlusPlusModule::ingestTextPacket(const meshtastic_MeshPacket &mp, const meshtastic_MeshPacket *encrypted_meshpacket)
{
link_object lo;
SHA256 message_hash;
lo.to = mp.to;
lo.from = mp.from;
lo.id = mp.id;
lo.rx_time = mp.rx_time;
lo.channel_hash = encrypted_meshpacket->channel;
memcpy(lo.encrypted_bytes, encrypted_meshpacket->encrypted.bytes, encrypted_meshpacket->encrypted.size);
lo.encrypted_len = encrypted_meshpacket->encrypted.size;
lo.payload = std::string((char *)mp.decoded.payload.bytes, mp.decoded.payload.size);
message_hash.reset();
message_hash.update(encrypted_meshpacket->encrypted.bytes, encrypted_meshpacket->encrypted.size);
message_hash.update(&mp.to, sizeof(mp.to));
message_hash.update(&mp.from, sizeof(mp.from));
message_hash.update(&mp.id, sizeof(mp.id));
message_hash.finalize(lo.message_hash, 32);
getOrAddRootFromChannelHash(encrypted_meshpacket->channel, lo.root_hash);
return lo;
}
StoreForwardPlusPlusModule::link_object StoreForwardPlusPlusModule::ingestLinkMessage(meshtastic_StoreForwardPlusPlus *t)
{
link_object lo;
lo.to = t->encapsulated_to;
lo.from = t->encapsulated_from;
lo.id = t->encapsulated_id;
lo.rx_time = t->encapsulated_rxtime;
// What if we don't have this root hash? Should drop this packet before this point.
lo.channel_hash = getChannelHashFromRoot(t->root_hash.bytes);
SHA256 message_hash;
memcpy(lo.encrypted_bytes, t->message.bytes, t->message.size);
lo.encrypted_len = t->message.size;
memcpy(lo.message_hash, t->message_hash.bytes, t->message_hash.size);
memcpy(lo.root_hash, t->root_hash.bytes, t->root_hash.size);
memcpy(lo.commit_hash, t->commit_hash.bytes, t->commit_hash.size);
if (t->commit_hash.size == 32)
lo.has_commit_hash = true;
// we don't ever get the payload here, so it's always an empty string
lo.payload = "";
return lo;
}
void StoreForwardPlusPlusModule::rebroadcastLinkObject(StoreForwardPlusPlusModule::link_object &lo)
{
LOG_WARN("Attempting to Rebroadcast1");
meshtastic_MeshPacket *p = router->allocForSending();
p->to = lo.to;
p->from = lo.from;
p->id = lo.id;
p->channel = lo.channel_hash;
p->which_payload_variant = meshtastic_MeshPacket_encrypted_tag;
p->encrypted.size = lo.encrypted_len;
memcpy(p->encrypted.bytes, lo.encrypted_bytes, lo.encrypted_len);
p->transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA; // only a tiny white lie
service->sendToMesh(p, RX_SRC_RADIO, true); // Send to mesh, cc to phone
}
bool StoreForwardPlusPlusModule::checkCommitHash(StoreForwardPlusPlusModule::link_object &lo, uint8_t *commit_hash_bytes,
size_t hash_len)
{
SHA256 commit_hash;
uint8_t last_message_hash[32] = {0};
uint8_t last_commit_hash[32] = {0};
commit_hash.reset();
if (getChainEnd(lo.channel_hash, last_commit_hash, last_message_hash)) {
printBytes("last message: 0x", last_commit_hash, 32);
commit_hash.update(last_commit_hash, 32);
} else {
printBytes("new chain root: 0x", lo.root_hash, 32);
commit_hash.update(lo.root_hash, 32);
}
commit_hash.update(lo.message_hash, 32);
commit_hash.finalize(commit_hash_bytes, 32);
if (memcmp(commit_hash_bytes, lo.commit_hash, 32) == 0) {
return true;
}
return false;
}
// announce latest hash
// chain_end_announce
// check if hash is known
// hash_query
// request next message
// link_request
// send encapsulated message
// link_provide_whole
// link_provide_half1
// link_provide_half2
// onboard request message?
// get x from top?
// messages
// Given this chain root, do you have a packet that matches this message hash?
// responds with chain hash etc
// given this chain root, what is your last chain and message hash?
// given this chain root, what is your next message after this chain hash? (do we have an overhead problem here?) (blegh,
// fragmentation) (but also, trunking)
// broadcast on this chain root, here is my last chain hash
// consider third-order nodes
// I can't talk directly to strata, I can talk to a satellite. Inform sat of a message. Sat stores it as if had seen it locally,
// and pushes it to central
// message Eventually works out through chain
// sat can capture time of receipt
// terms:
// CANON
// stratum
// chain
// links on the chain
// the sender+destination pair is an interesting unique id (though ordering) (smaller one goes first?)
// so messages with a unique pair become a chain
// These get a table
// message to broadcast get a chain per channel hash
// second table
// for now, channel messages are limited to decryptable
// limited to text messages
// create a unique-from-nodenums() class that returns a 64-bit value

View File

@@ -0,0 +1,125 @@
#pragma once
#include "Channels.h"
#include "ProtobufModule.h"
#include "Router.h"
#include "SinglePortModule.h"
#include "sqlite3.h"
/**
* A simple example module that just replies with "Message received" to any message it receives.
*/
class StoreForwardPlusPlusModule : public ProtobufModule<meshtastic_StoreForwardPlusPlus>, private concurrency::OSThread
{
struct link_object {
uint32_t to;
uint32_t from;
uint32_t id;
uint32_t rx_time;
ChannelHash channel_hash;
uint8_t encrypted_bytes[256] = {0};
size_t encrypted_len;
uint8_t message_hash[32] = {0};
uint8_t root_hash[32] = {0};
uint8_t commit_hash[32] = {0};
// TODO: Make these sizes instead?
bool has_commit_hash = false;
std::string payload;
};
public:
/** Constructor
* name is for debugging output
*/
StoreForwardPlusPlusModule();
/*
-Override the wantPacket method.
*/
virtual bool wantPacket(const meshtastic_MeshPacket *p) override
{
// if encrypted but not too FFFF
// want
switch (p->decoded.portnum) {
case meshtastic_PortNum_TEXT_MESSAGE_APP:
case 35:
return true;
default:
return false;
}
}
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
*/
virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_StoreForwardPlusPlus *t) override;
virtual int32_t runOnce() override;
private:
sqlite3 *ppDb;
sqlite3_stmt *chain_insert_stmt;
sqlite3_stmt *scratch_insert_stmt;
sqlite3_stmt *checkDup;
sqlite3_stmt *checkScratch;
sqlite3_stmt *removeScratch;
sqlite3_stmt *updatePayloadStmt;
sqlite3_stmt *getPayloadFromScratchStmt;
sqlite3_stmt *fromScratchStmt;
sqlite3_stmt *fromScratchByHashStmt;
sqlite3_stmt *getNextHashStmt;
sqlite3_stmt *getChainEndStmt;
sqlite3_stmt *getLinkStmt;
// returns wasfound
bool getRootFromChannelHash(ChannelHash, uint8_t *);
ChannelHash getChannelHashFromRoot(uint8_t *_root_hash);
bool getNextHash(uint8_t *_root_hash, uint8_t *, uint8_t *);
// returns isnew
bool getOrAddRootFromChannelHash(ChannelHash, uint8_t *);
bool addRootToMappings(ChannelHash, uint8_t *);
// return indicates message found
uint32_t getChainEnd(ChannelHash, uint8_t *, uint8_t *);
void requestNextMessage(uint8_t *, uint8_t *);
bool broadcastLink(uint8_t *, uint8_t *);
bool sendFromScratch(uint8_t);
bool addToChain(link_object &);
bool addToScratch(link_object &);
void canonAnnounce(uint8_t *, uint8_t *, uint8_t *, uint32_t);
bool isInDB(uint8_t *);
bool isInScratch(uint8_t *);
link_object getFromScratch(uint8_t *, size_t);
void removeFromScratch(uint8_t *);
void updatePayload(uint8_t *, std::string);
// does not set the root hash
link_object ingestTextPacket(const meshtastic_MeshPacket &, const meshtastic_MeshPacket *);
link_object ingestLinkMessage(meshtastic_StoreForwardPlusPlus *);
void rebroadcastLinkObject(link_object &);
bool checkCommitHash(link_object &lo, uint8_t *commit_hash_bytes, size_t hash_len);
enum chain_types {
channel_chain = 0,
};
};

View File

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

View File

@@ -378,7 +378,7 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt
int line = 1; int line = 1;
// === Set Title // === Set Title
const char *titleStr = (graphics::currentResolution == graphics::ScreenResolution::High) ? "Environment" : "Env."; const char *titleStr = (graphics::isHighResolution) ? "Environment" : "Env.";
// === Header === // === Header ===
graphics::drawCommonHeader(display, x, y, titleStr); graphics::drawCommonHeader(display, x, y, titleStr);

View File

@@ -117,7 +117,7 @@ void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *s
int line = 1; int line = 1;
// === Set Title // === Set Title
const char *titleStr = (graphics::currentResolution == graphics::ScreenResolution::High) ? "Power Telem." : "Power"; const char *titleStr = (graphics::isHighResolution) ? "Power Telem." : "Power";
// === Header === // === Header ===
graphics::drawCommonHeader(display, x, y, titleStr); graphics::drawCommonHeader(display, x, y, titleStr);

View File

@@ -1,14 +1,10 @@
#include "TextMessageModule.h" #include "TextMessageModule.h"
#include "MeshService.h" #include "MeshService.h"
#include "MessageStore.h"
#include "NodeDB.h" #include "NodeDB.h"
#include "PowerFSM.h" #include "PowerFSM.h"
#include "buzz.h" #include "buzz.h"
#include "configuration.h" #include "configuration.h"
#include "graphics/Screen.h" #include "graphics/Screen.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/draw/MessageRenderer.h"
#include "main.h"
TextMessageModule *textMessageModule; TextMessageModule *textMessageModule;
ProcessMessage TextMessageModule::handleReceived(const meshtastic_MeshPacket &mp) ProcessMessage TextMessageModule::handleReceived(const meshtastic_MeshPacket &mp)
@@ -19,26 +15,14 @@ ProcessMessage TextMessageModule::handleReceived(const meshtastic_MeshPacket &mp
#endif #endif
// We only store/display messages destined for us. // We only store/display messages destined for us.
// Keep a copy of the most recent text message.
devicestate.rx_text_message = mp; devicestate.rx_text_message = mp;
devicestate.has_rx_text_message = true; 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 // Only trigger screen wake if configuration allows it
if (shouldWakeOnReceivedMessage()) { if (shouldWakeOnReceivedMessage()) {
powerFSM.trigger(EVENT_RECEIVED_MSG); powerFSM.trigger(EVENT_RECEIVED_MSG);
} }
// Notify any observers (e.g. external modules that care about packets)
notifyObservers(&mp); notifyObservers(&mp);
return ProcessMessage::CONTINUE; // Let others look at this message also if they want return ProcessMessage::CONTINUE; // Let others look at this message also if they want

View File

@@ -3,13 +3,7 @@
#include "SinglePortModule.h" #include "SinglePortModule.h"
/** /**
* Text message handling for Meshtastic. * Text message handling for meshtastic - draws on the OLED display the most recent received message
*
* 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.
*/ */
class TextMessageModule : public SinglePortModule, public Observable<const meshtastic_MeshPacket *> class TextMessageModule : public SinglePortModule, public Observable<const meshtastic_MeshPacket *>
{ {
@@ -21,9 +15,9 @@ class TextMessageModule : public SinglePortModule, public Observable<const mesht
protected: protected:
/** Called to handle a particular incoming message /** Called to handle a particular incoming message
*
* @return ProcessMessage::STOP if you've guaranteed you've handled this @return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be considered for
* message and no other handlers should be considered for it. it
*/ */
virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
virtual bool wantPacket(const meshtastic_MeshPacket *p) override; virtual bool wantPacket(const meshtastic_MeshPacket *p) override;

View File

@@ -2,7 +2,6 @@
#include "NodeDB.h" #include "NodeDB.h"
#include "PowerFSM.h" #include "PowerFSM.h"
#include "configuration.h" #include "configuration.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/draw/CompassRenderer.h" #include "graphics/draw/CompassRenderer.h"
#if HAS_SCREEN #if HAS_SCREEN
@@ -15,15 +14,6 @@
WaypointModule *waypointModule; 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) ProcessMessage WaypointModule::handleReceived(const meshtastic_MeshPacket &mp)
{ {
#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
@@ -62,15 +52,31 @@ ProcessMessage WaypointModule::handleReceived(const meshtastic_MeshPacket &mp)
bool WaypointModule::shouldDraw() bool WaypointModule::shouldDraw()
{ {
#if !MESHTASTIC_EXCLUDE_WAYPOINT #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; 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, if (pb_decode_from_bytes(devicestate.rx_waypoint.decoded.payload.bytes, devicestate.rx_waypoint.decoded.payload.size,
&meshtastic_Waypoint_msg, &wp)) { &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 #else
return false; return false;
#endif #endif
@@ -79,46 +85,53 @@ bool WaypointModule::shouldDraw()
/// Draw the last waypoint we received /// Draw the last waypoint we received
void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{ {
if (!screen) if (screen == nullptr)
return; return;
display->clear(); // Prepare to draw
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
int line = 1; display->setTextAlignment(TEXT_ALIGN_LEFT);
// === Set Title // Handle inverted display
const char *titleStr = "Waypoint"; // Unsure of expected behavior: for now, copy drawNodeInfo
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED)
// === Header === display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL);
graphics::drawCommonHeader(display, x, y, titleStr);
const int w = display->getWidth();
const int h = display->getHeight();
// Decode the waypoint // Decode the waypoint
const meshtastic_MeshPacket &mp = devicestate.rx_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)) { 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; devicestate.has_rx_waypoint = false;
return; return;
} }
// Get timestamp info. Will pass as a field to drawColumns // Get timestamp info. Will pass as a field to drawColumns
char lastStr[20]; static char lastStr[20];
getTimeAgoStr(sinceReceived(&mp), lastStr, sizeof(lastStr)); getTimeAgoStr(sinceReceived(&mp), lastStr, sizeof(lastStr));
// Will contain distance information, passed as a field to drawColumns // 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 // Get our node, to use our own position
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); 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 // Dimensions / co-ordinates for the compass/circle
const uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(w, h); int16_t compassX = 0, compassY = 0;
const int16_t compassX = x + w - (compassDiam / 2) - 5; uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(display->getWidth(), display->getHeight());
const int16_t compassY = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT)
? y + h / 2 if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) {
: y + FONT_HEIGHT_SMALL + (h - FONT_HEIGHT_SMALL) / 2; 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 our node has a position:
if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading())) { if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading())) {
@@ -128,7 +141,7 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state,
myHeading = 0; myHeading = 0;
} else { } else {
if (screen->hasHeading()) if (screen->hasHeading())
myHeading = degToRad(screen->getHeading()); myHeading = (screen->getHeading()) * PI / 180; // gotta convert compass degrees to Radians
else else
myHeading = screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); 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); graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther);
float bearingToOtherDegrees = (bearingToOther < 0) ? bearingToOther + 2 * PI : bearingToOther; float bearingToOtherDegrees = (bearingToOther < 0) ? bearingToOther + 2 * PI : bearingToOther;
bearingToOtherDegrees = radToDeg(bearingToOtherDegrees); bearingToOtherDegrees = bearingToOtherDegrees * 180 / PI;
// Distance to Waypoint // Distance to Waypoint
float d = GeoCoord::latLongToMeter(DegD(wp.latitude_i), DegD(wp.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); 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) { if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
float feet = d * METERS_TO_FEET; if (d < (2 * MILES_TO_FEET))
snprintf(distStr, sizeof(distStr), feet < (2 * MILES_TO_FEET) ? "%.0fft %.0f°" : "%.1fmi %.0f°", snprintf(distStr, sizeof(distStr), "%.0fft %.0f°", d * METERS_TO_FEET, bearingToOtherDegrees);
feet < (2 * MILES_TO_FEET) ? feet : feet / MILES_TO_FEET, bearingToOtherDegrees); else
snprintf(distStr, sizeof(distStr), "%.1fmi %.0f°", d * METERS_TO_FEET / MILES_TO_FEET, bearingToOtherDegrees);
} else { } else {
snprintf(distStr, sizeof(distStr), d < 2000 ? "%.0fm %.0f°" : "%.1fkm %.0f°", d < 2000 ? d : d / 1000, if (d < 2000)
bearingToOtherDegrees); 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 { else {
// ? in the compass
display->drawString(compassX - FONT_HEIGHT_SMALL / 4, compassY - FONT_HEIGHT_SMALL / 2, "?"); display->drawString(compassX - FONT_HEIGHT_SMALL / 4, compassY - FONT_HEIGHT_SMALL / 2, "?");
// ? in the distance field // ? in the distance field
snprintf(distStr, sizeof(distStr), "? %s ?°", if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL)
(config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) ? "mi" : "km"); strncpy(distStr, "? mi ?°", sizeof(distStr));
else
strncpy(distStr, "? km ?°", sizeof(distStr));
} }
// Draw compass circle // Draw compass circle
display->drawCircle(compassX, compassY, compassDiam / 2); display->drawCircle(compassX, compassY, compassDiam / 2);
display->setTextAlignment(TEXT_ALIGN_LEFT); // Something above me changes to a different alignment, forcing a fix here! // Undo color-inversion, if set prior to drawing header
display->drawString(0, graphics::getTextPositions(display)[line++], lastStr); // Unsure of expected behavior? For now: copy drawNodeInfo
display->drawString(0, graphics::getTextPositions(display)[line++], wp.name); if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) {
display->drawString(0, graphics::getTextPositions(display)[line++], wp.description); display->setColor(BLACK);
display->drawString(0, graphics::getTextPositions(display)[line++], distStr); }
// Must be after distStr is populated
graphics::NodeListRenderer::drawColumns(display, x, y, fields);
} }
#endif #endif

View File

@@ -427,13 +427,11 @@ void portduinoSetup()
} }
getMacAddr(dmac); getMacAddr(dmac);
#ifndef UNIT_TEST
if (dmac[0] == 0 && dmac[1] == 0 && dmac[2] == 0 && dmac[3] == 0 && dmac[4] == 0 && dmac[5] == 0) { 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 << "*** Blank MAC Address not allowed!" << std::endl;
std::cout << "Please set a MAC Address in config.yaml using either MACAddress or MACAddressSource." << std::endl; std::cout << "Please set a MAC Address in config.yaml using either MACAddress or MACAddressSource." << std::endl;
exit(EXIT_FAILURE); 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]); 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. // Rather important to set this, if not running simulated.
randomSeed(time(NULL)); randomSeed(time(NULL));
@@ -788,6 +786,10 @@ bool loadConfig(const char *configPath)
} }
} }
if (yamlConfig["StoreAndForward"]) {
portduino_config.sfpp_stratum0 = (yamlConfig["StoreAndForward"]["Stratum0"]).as<bool>(false);
}
if (yamlConfig["General"]) { if (yamlConfig["General"]) {
portduino_config.MaxNodes = (yamlConfig["General"]["MaxNodes"]).as<int>(200); portduino_config.MaxNodes = (yamlConfig["General"]["MaxNodes"]).as<int>(200);
portduino_config.maxtophone = (yamlConfig["General"]["MaxMessageQueue"]).as<int>(100); portduino_config.maxtophone = (yamlConfig["General"]["MaxMessageQueue"]).as<int>(100);

View File

@@ -169,6 +169,9 @@ extern struct portduino_config_struct {
int configDisplayMode = 0; int configDisplayMode = 0;
bool has_configDisplayMode = false; bool has_configDisplayMode = false;
// Store and Forward++
bool sfpp_stratum0 = false;
// General // General
std::string mac_address = ""; std::string mac_address = "";
bool mac_address_explicit = false; bool mac_address_explicit = false;

View File

@@ -15,5 +15,4 @@ upload_protocol = esptool
upload_speed = 460800 upload_speed = 460800
lib_deps = lib_deps =
${esp32_base.lib_deps} ${esp32_base.lib_deps}
# renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel
adafruit/Adafruit NeoPixel@1.15.2 adafruit/Adafruit NeoPixel@1.15.2

View File

@@ -9,5 +9,4 @@ build_flags =
lib_deps = lib_deps =
${esp32_base.lib_deps} ${esp32_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.7 lovyan03/LovyanGFX@1.2.7

View File

@@ -53,7 +53,6 @@ build_flags =
lib_deps = lib_deps =
${arduino_base.lib_deps} ${arduino_base.lib_deps}
${networking_base.lib_deps} ${networking_base.lib_deps}
${networking_extra.lib_deps}
${environmental_base.lib_deps} ${environmental_base.lib_deps}
${environmental_extra.lib_deps} ${environmental_extra.lib_deps}
${radiolib_base.lib_deps} ${radiolib_base.lib_deps}
@@ -63,8 +62,8 @@ lib_deps =
h2zero/NimBLE-Arduino@1.4.3 h2zero/NimBLE-Arduino@1.4.3
# renovate: datasource=git-refs depName=libpax packageName=https://github.com/dbinfrago/libpax gitBranch=master # renovate: datasource=git-refs depName=libpax packageName=https://github.com/dbinfrago/libpax gitBranch=master
https://github.com/dbinfrago/libpax/archive/3cdc0371c375676a97967547f4065607d4c53fd1.zip https://github.com/dbinfrago/libpax/archive/3cdc0371c375676a97967547f4065607d4c53fd1.zip
# renovate: datasource=custom.pio depName=XPowersLib packageName=lewisxhe/library/XPowersLib # renovate: datasource=github-tags depName=XPowersLib packageName=lewisxhe/XPowersLib
lewisxhe/XPowersLib@0.3.2 https://github.com/lewisxhe/XPowersLib/archive/v0.3.2.zip
# renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master # renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master
https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip
# renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto

View File

@@ -25,5 +25,4 @@ lib_ignore =
m5stack-core m5stack-core
lib_deps = lib_deps =
${esp32_base.lib_deps} ${esp32_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.7 lovyan03/LovyanGFX@1.2.7

View File

@@ -18,9 +18,7 @@ build_flags =
-DM5STACK -DM5STACK
lib_deps = lib_deps =
${esp32_base.lib_deps} ${esp32_base.lib_deps}
# renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2
zinggjm/GxEPD2@1.6.5 zinggjm/GxEPD2@1.6.5
# renovate: datasource=custom.pio depName=PCF8563 packageName=lewisxhe/library/PCF8563_Library
lewisxhe/PCF8563_Library@1.0.1 lewisxhe/PCF8563_Library@1.0.1
lib_ignore = lib_ignore =
m5stack-coreink m5stack-coreink

View File

@@ -13,5 +13,4 @@ board_build.f_cpu = 240000000L
upload_protocol = esptool upload_protocol = esptool
lib_deps = lib_deps =
${esp32_base.lib_deps} ${esp32_base.lib_deps}
# renovate: datasource=github-tags depName=STK8xxx-Accelerometer packageName=gjelsoe/STK8xxx-Accelerometer
https://github.com/gjelsoe/STK8xxx-Accelerometer/archive/v0.1.1.zip https://github.com/gjelsoe/STK8xxx-Accelerometer/archive/v0.1.1.zip

View File

@@ -20,7 +20,5 @@ build_flags =
lib_deps = lib_deps =
${env:tbeam.lib_deps} ${env:tbeam.lib_deps}
# renovate: datasource=github-tags depName=meshtastic-st7796 packageName=meshtastic/st7796 https://github.com/meshtastic/st7796/archive/refs/tags/1.0.5.zip ; display addon
https://github.com/meshtastic/st7796/archive/1.0.5.zip lewisxhe/SensorLib@0.3.1 ; touchscreen addon
# renovate: datasource=custom.pio depName=lewisxhe-SensorLib packageName=lewisxhe/library/SensorLib
lewisxhe/SensorLib@0.3.1

View File

@@ -10,9 +10,6 @@ build_flags =
-I variants/esp32/wiphone -I variants/esp32/wiphone
lib_deps = lib_deps =
${esp32_base.lib_deps} ${esp32_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.7 lovyan03/LovyanGFX@1.2.7
# renovate: datasource=custom.pio depName=SX1509 IO Expander packageName=sparkfun/library/SX1509 IO Expander
sparkfun/SX1509 IO Expander@3.0.6 sparkfun/SX1509 IO Expander@3.0.6
# renovate: datasource=custom.pio depName=APA102 packageName=pololu/library/APA102
pololu/APA102@3.0.0 pololu/APA102@3.0.0

View File

@@ -6,5 +6,4 @@ build_flags =
-D HELTEC_HRU_3601 -D HELTEC_HRU_3601
-I variants/esp32c3/heltec_hru_3601 -I variants/esp32c3/heltec_hru_3601
lib_deps = ${esp32c3_base.lib_deps} lib_deps = ${esp32c3_base.lib_deps}
# renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel
adafruit/Adafruit NeoPixel@1.15.2 adafruit/Adafruit NeoPixel@1.15.2

View File

@@ -12,9 +12,7 @@ build_unflags =
-D HAS_WIFI -D HAS_WIFI
lib_deps = lib_deps =
${esp32c6_base.lib_deps} ${esp32c6_base.lib_deps}
# renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel
adafruit/Adafruit NeoPixel@1.15.2 adafruit/Adafruit NeoPixel@1.15.2
# renovate: datasource=custom.pio depName=NimBLE-Arduino packageName=h2zero/library/NimBLE-Arduino
h2zero/NimBLE-Arduino@2.3.7 h2zero/NimBLE-Arduino@2.3.7
build_flags = build_flags =
${esp32c6_base.build_flags} ${esp32c6_base.build_flags}

View File

@@ -16,9 +16,6 @@ build_flags =
-DEINK_BACKGROUND_USES_FAST ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached. -DEINK_BACKGROUND_USES_FAST ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached.
lib_deps = ${esp32s3_base.lib_deps} lib_deps = ${esp32s3_base.lib_deps}
# renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master
https://github.com/meshtastic/GxEPD2/archive/1655054ba298e0e29fc2044741940f927f9c2a43.zip https://github.com/meshtastic/GxEPD2/archive/1655054ba298e0e29fc2044741940f927f9c2a43.zip
# renovate: datasource=custom.pio depName=PCF8563 packageName=lewisxhe/library/PCF8563_Library
lewisxhe/PCF8563_Library@1.0.1 lewisxhe/PCF8563_Library@1.0.1
# renovate: datasource=custom.pio depName=PCA9557-arduino packageName=maxpromer/library/PCA9557-arduino
maxpromer/PCA9557-arduino@1.0.0 maxpromer/PCA9557-arduino@1.0.0

View File

@@ -9,7 +9,6 @@ upload_protocol = esptool
;upload_port = /dev/ttyACM2 ;upload_port = /dev/ttyACM2
lib_deps = lib_deps =
${esp32s3_base.lib_deps} ${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=caveman99-ESP32_Codec2 packageName=caveman99/library/ESP32 Codec2
caveman99/ESP32 Codec2@1.0.1 caveman99/ESP32 Codec2@1.0.1
build_flags = build_flags =
${esp32s3_base.build_flags} ${esp32s3_base.build_flags}

View File

@@ -25,7 +25,6 @@ build_flags =
;-DEINK_LIMIT_RATE_RESPONSIVE_SEC=1 ;-DEINK_LIMIT_RATE_RESPONSIVE_SEC=1
lib_deps = lib_deps =
${esp32s3_base.lib_deps} ${esp32s3_base.lib_deps}
# renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master
https://github.com/meshtastic/GxEPD2/archive/33db3fa8ee6fc47d160bdb44f8f127c9a9203a10.zip https://github.com/meshtastic/GxEPD2/archive/33db3fa8ee6fc47d160bdb44f8f127c9a9203a10.zip
[env:crowpanel-esp32s3-4-epaper] [env:crowpanel-esp32s3-4-epaper]
@@ -55,7 +54,6 @@ build_flags =
;-DEINK_LIMIT_RATE_RESPONSIVE_SEC=1 ;-DEINK_LIMIT_RATE_RESPONSIVE_SEC=1
lib_deps = lib_deps =
${esp32s3_base.lib_deps} ${esp32s3_base.lib_deps}
# renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master
https://github.com/meshtastic/GxEPD2/archive/33db3fa8ee6fc47d160bdb44f8f127c9a9203a10.zip https://github.com/meshtastic/GxEPD2/archive/33db3fa8ee6fc47d160bdb44f8f127c9a9203a10.zip
[env:crowpanel-esp32s3-2-epaper] [env:crowpanel-esp32s3-2-epaper]
@@ -85,5 +83,4 @@ build_flags =
;-DEINK_LIMIT_RATE_RESPONSIVE_SEC=1 ;-DEINK_LIMIT_RATE_RESPONSIVE_SEC=1
lib_deps = lib_deps =
${esp32s3_base.lib_deps} ${esp32s3_base.lib_deps}
# renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master
https://github.com/meshtastic/GxEPD2/archive/33db3fa8ee6fc47d160bdb44f8f127c9a9203a10.zip https://github.com/meshtastic/GxEPD2/archive/33db3fa8ee6fc47d160bdb44f8f127c9a9203a10.zip

View File

@@ -10,9 +10,7 @@ upload_protocol = esptool
upload_speed = 921600 upload_speed = 921600
lib_deps = lib_deps =
${esp32s3_base.lib_deps} ${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2
zinggjm/GxEPD2@1.6.5 zinggjm/GxEPD2@1.6.5
# renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel
adafruit/Adafruit NeoPixel@1.15.2 adafruit/Adafruit NeoPixel@1.15.2
build_unflags = build_unflags =
${esp32s3_base.build_unflags} ${esp32s3_base.build_unflags}

View File

@@ -10,7 +10,6 @@ upload_protocol = esptool
upload_speed = 921600 upload_speed = 921600
lib_deps = lib_deps =
${esp32s3_base.lib_deps} ${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel
adafruit/Adafruit NeoPixel@1.15.2 adafruit/Adafruit NeoPixel@1.15.2
build_unflags = build_unflags =
${esp32s3_base.build_unflags} ${esp32s3_base.build_unflags}

View File

@@ -12,9 +12,7 @@ build_flags =
-D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_CDC_ON_BOOT=1
lib_deps = ${esp32s3_base.lib_deps} lib_deps = ${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=ESP8266Audio packageName=earlephilhower/library/ESP8266Audio
earlephilhower/ESP8266Audio@1.9.9 earlephilhower/ESP8266Audio@1.9.9
# renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM
earlephilhower/ESP8266SAM@1.1.0 earlephilhower/ESP8266SAM@1.1.0
[env:dreamcatcher-2206] [env:dreamcatcher-2206]

View File

@@ -41,13 +41,9 @@ build_flags = ${esp32s3_base.build_flags} -Os
lib_deps = ${esp32s3_base.lib_deps} lib_deps = ${esp32s3_base.lib_deps}
${device-ui_base.lib_deps} ${device-ui_base.lib_deps}
# renovate: datasource=custom.pio depName=ESP8266Audio packageName=earlephilhower/library/ESP8266Audio
earlephilhower/ESP8266Audio@1.9.9 earlephilhower/ESP8266Audio@1.9.9
# renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM
earlephilhower/ESP8266SAM@1.0.1 earlephilhower/ESP8266SAM@1.0.1
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.0 ; note: v1.2.7 breaks the elecrow 7" display functionality lovyan03/LovyanGFX@1.2.0 ; note: v1.2.7 breaks the elecrow 7" display functionality
# renovate: datasource=custom.pio depName=TCA9534 packageName=hideakitai/library/TCA9534
hideakitai/TCA9534@0.1.1 hideakitai/TCA9534@0.1.1
[crowpanel_small_esp32s3_base] ; 2.4, 2.8, 3.5 inch [crowpanel_small_esp32s3_base] ; 2.4, 2.8, 3.5 inch

View File

@@ -22,7 +22,5 @@ build_flags = ${esp32s3_base.build_flags}
-DEINK_HEIGHT=128 -DEINK_HEIGHT=128
lib_deps = ${esp32s3_base.lib_deps} lib_deps = ${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2
zinggjm/GxEPD2@1.6.5 zinggjm/GxEPD2@1.6.5
# renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel
adafruit/Adafruit NeoPixel@1.15.2 adafruit/Adafruit NeoPixel@1.15.2

View File

@@ -12,5 +12,4 @@ build_flags = ${esp32s3_base.build_flags}
-I variants/esp32s3/hackaday-communicator -I variants/esp32s3/hackaday-communicator
lib_deps = ${esp32s3_base.lib_deps} lib_deps = ${esp32s3_base.lib_deps}
# renovate: datasource=git-refs depName=meshtastic-Arduino_GFX packageName=https://github.com/meshtastic/Arduino_GFX gitBranch=master
https://github.com/meshtastic/Arduino_GFX/archive/054e81ffaf23784830a734e3c184346789349406.zip https://github.com/meshtastic/Arduino_GFX/archive/054e81ffaf23784830a734e3c184346789349406.zip

View File

@@ -9,5 +9,4 @@ build_flags =
-D HELTEC_SENSOR_HUB -D HELTEC_SENSOR_HUB
lib_deps = ${esp32s3_base.lib_deps} lib_deps = ${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel
adafruit/Adafruit NeoPixel@1.15.2 adafruit/Adafruit NeoPixel@1.15.2

View File

@@ -21,6 +21,8 @@ build_flags =
-D I2C_SCL=18 -D I2C_SCL=18
-D I2C_SDA1=4 -D I2C_SDA1=4
-D I2C_SCL1=3 -D I2C_SCL1=3
lib_deps =
${heltec_v4_base.lib_deps}
[env:heltec-v4-tft] [env:heltec-v4-tft]
extends = heltec_v4_base extends = heltec_v4_base
@@ -103,10 +105,6 @@ build_flags =
lib_deps = ${heltec_v4_base.lib_deps} lib_deps = ${heltec_v4_base.lib_deps}
; ${device-ui_base.lib_deps} ; ${device-ui_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.0 lovyan03/LovyanGFX@1.2.0
# renovate: datasource=git-refs depName=Quency-D_chsc6x packageName=https://github.com/Quency-D/chsc6x gitBranch=master
https://github.com/Quency-D/chsc6x/archive/5cbead829d6b432a8d621ed1aafd4eb474fd4f27.zip https://github.com/Quency-D/chsc6x/archive/5cbead829d6b432a8d621ed1aafd4eb474fd4f27.zip
; TODO revert to official device-ui (when merged)
# renovate: datasource=git-refs depName=Quency-D_device-ui packageName=https://github.com/Quency-D/device-ui gitBranch=heltec-v4-tft
https://github.com/Quency-D/device-ui/archive/7c9870b8016641190b059bdd90fe16c1012a39eb.zip https://github.com/Quency-D/device-ui/archive/7c9870b8016641190b059bdd90fe16c1012a39eb.zip

View File

@@ -17,9 +17,7 @@ build_flags =
-DEINK_HASQUIRK_GHOSTING ; Display model is identified as "prone to ghosting" -DEINK_HASQUIRK_GHOSTING ; Display model is identified as "prone to ghosting"
lib_deps = lib_deps =
${esp32s3_base.lib_deps} ${esp32s3_base.lib_deps}
# renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master
https://github.com/meshtastic/GxEPD2/archive/1655054ba298e0e29fc2044741940f927f9c2a43.zip https://github.com/meshtastic/GxEPD2/archive/1655054ba298e0e29fc2044741940f927f9c2a43.zip
# renovate: datasource=custom.pio depName=PCF8563 packageName=lewisxhe/library/PCF8563_Library
lewisxhe/PCF8563_Library@1.0.1 lewisxhe/PCF8563_Library@1.0.1
upload_speed = 115200 upload_speed = 115200

View File

@@ -20,9 +20,7 @@ build_flags =
lib_deps = lib_deps =
${esp32s3_base.lib_deps} ${esp32s3_base.lib_deps}
# renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master
https://github.com/meshtastic/GxEPD2/archive/448c8538129fde3d02a7cb5e6fc81971ad92547f.zip https://github.com/meshtastic/GxEPD2/archive/448c8538129fde3d02a7cb5e6fc81971ad92547f.zip
# renovate: datasource=custom.pio depName=PCF8563 packageName=lewisxhe/library/PCF8563_Library
lewisxhe/PCF8563_Library@1.0.1 lewisxhe/PCF8563_Library@1.0.1
upload_speed = 115200 upload_speed = 115200

View File

@@ -8,8 +8,6 @@ build_flags =
-D HELTEC_VISION_MASTER_T190 -D HELTEC_VISION_MASTER_T190
lib_deps = lib_deps =
${esp32s3_base.lib_deps} ${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=PCF8563 packageName=lewisxhe/library/PCF8563_Library
lewisxhe/PCF8563_Library@1.0.1 lewisxhe/PCF8563_Library@1.0.1
# renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main
https://github.com/meshtastic/st7789/archive/bd33ea58ddfe4a5e4a66d53300ccbd38d66ac21f.zip https://github.com/meshtastic/st7789/archive/bd33ea58ddfe4a5e4a66d53300ccbd38d66ac21f.zip
upload_speed = 921600 upload_speed = 921600

View File

@@ -18,9 +18,7 @@ build_flags =
-D EINK_HASQUIRK_GHOSTING ; Display model is identified as "prone to ghosting" -D EINK_HASQUIRK_GHOSTING ; Display model is identified as "prone to ghosting"
lib_deps = lib_deps =
${esp32s3_base.lib_deps} ${esp32s3_base.lib_deps}
# renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master
https://github.com/meshtastic/GxEPD2/archive/1655054ba298e0e29fc2044741940f927f9c2a43.zip https://github.com/meshtastic/GxEPD2/archive/1655054ba298e0e29fc2044741940f927f9c2a43.zip
# renovate: datasource=custom.pio depName=PCF8563 packageName=lewisxhe/library/PCF8563_Library
lewisxhe/PCF8563_Library@1.0.1 lewisxhe/PCF8563_Library@1.0.1
upload_speed = 115200 upload_speed = 115200

View File

@@ -15,8 +15,6 @@ build_flags =
-D EINK_LIMIT_GHOSTING_PX=2000 ; (Optional) How much image ghosting is tolerated -D EINK_LIMIT_GHOSTING_PX=2000 ; (Optional) How much image ghosting is tolerated
lib_deps = lib_deps =
${esp32s3_base.lib_deps} ${esp32s3_base.lib_deps}
# renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master
https://github.com/meshtastic/GxEPD2/archive/55f618961db45a23eff0233546430f1e5a80f63a.zip https://github.com/meshtastic/GxEPD2/archive/55f618961db45a23eff0233546430f1e5a80f63a.zip
# renovate: datasource=custom.pio depName=PCF8563 packageName=lewisxhe/library/PCF8563_Library
lewisxhe/PCF8563_Library@1.0.1 lewisxhe/PCF8563_Library@1.0.1
upload_speed = 115200 upload_speed = 115200

View File

@@ -12,5 +12,4 @@ build_flags =
lib_deps = lib_deps =
${esp32s3_base.lib_deps} ${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.7 lovyan03/LovyanGFX@1.2.7

View File

@@ -11,5 +11,4 @@ build_flags =
;-D DEBUG_DISABLED ; uncomment this line to disable DEBUG output ;-D DEBUG_DISABLED ; uncomment this line to disable DEBUG output
lib_deps = lib_deps =
${esp32s3_base.lib_deps} ${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.7 lovyan03/LovyanGFX@1.2.7

View File

@@ -10,5 +10,4 @@ build_flags =
-D HELTEC_WIRELESS_TRACKER_V2 -D HELTEC_WIRELESS_TRACKER_V2
lib_deps = lib_deps =
${esp32s3_base.lib_deps} ${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.7 lovyan03/LovyanGFX@1.2.7

View File

@@ -7,7 +7,6 @@ board_build.mcu = esp32s3
board_build.partitions = default_8MB.csv board_build.partitions = default_8MB.csv
upload_protocol = esptool upload_protocol = esptool
upload_speed = 921600 upload_speed = 921600
; TODO renovate or remove
platform_packages = platformio/framework-arduinoespressif32@https://github.com/PowerFeather/powerfeather-meshtastic-arduino-lib/releases/download/2.0.16a/esp32-2.0.16.zip platform_packages = platformio/framework-arduinoespressif32@https://github.com/PowerFeather/powerfeather-meshtastic-arduino-lib/releases/download/2.0.16a/esp32-2.0.16.zip
build_unflags = build_unflags =
${esp32s3_base.build_unflags} ${esp32s3_base.build_unflags}

View File

@@ -50,7 +50,6 @@ build_src_filter = ${esp32s3_base.build_src_filter}
lib_deps = lib_deps =
${esp32s3_base.lib_deps} ${esp32s3_base.lib_deps}
${device-ui_base.lib_deps} ${device-ui_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.7 lovyan03/LovyanGFX@1.2.7
[mesh_tab_xpt2046] [mesh_tab_xpt2046]

View File

@@ -15,7 +15,6 @@ build_flags =
lib_deps = lib_deps =
${esp32s3_base.lib_deps} ${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.7 lovyan03/LovyanGFX@1.2.7
build_src_filter = build_src_filter =

View File

@@ -15,7 +15,6 @@ build_flags =
lib_deps = lib_deps =
${esp32s3_base.lib_deps} ${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.7 lovyan03/LovyanGFX@1.2.7
[ft5x06] [ft5x06]

View File

@@ -24,13 +24,11 @@ build_flags = ${esp32s3_base.build_flags}
-DUSE_ARDUINO_HAL_GPIO -DUSE_ARDUINO_HAL_GPIO
lib_deps = ${esp32s3_base.lib_deps} lib_deps = ${esp32s3_base.lib_deps}
; TODO switch back to official LovyanGFX
https://github.com/mverch67/LovyanGFX/archive/4c76238c1344162a234ae917b27651af146d6fb2.zip https://github.com/mverch67/LovyanGFX/archive/4c76238c1344162a234ae917b27651af146d6fb2.zip
# renovate: datasource=custom.pio depName=ESP8266Audio packageName=earlephilhower/library/ESP8266Audio
earlephilhower/ESP8266Audio@1.9.9 earlephilhower/ESP8266Audio@1.9.9
# renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM
earlephilhower/ESP8266SAM@1.1.0 earlephilhower/ESP8266SAM@1.1.0
[env:seeed-sensecap-indicator-tft] [env:seeed-sensecap-indicator-tft]
extends = env:seeed-sensecap-indicator extends = env:seeed-sensecap-indicator
board_level = pr board_level = pr
@@ -66,5 +64,4 @@ build_flags =
lib_deps = lib_deps =
${env:seeed-sensecap-indicator.lib_deps} ${env:seeed-sensecap-indicator.lib_deps}
${device-ui_base.lib_deps} ${device-ui_base.lib_deps}
; TODO switch back to official bb_captouch
https://github.com/mverch67/bb_captouch/archive/8626412fe650d808a267791c0eae6e5860c85a5d.zip ; alternative touch library supporting FT6x36 https://github.com/mverch67/bb_captouch/archive/8626412fe650d808a267791c0eae6e5860c85a5d.zip ; alternative touch library supporting FT6x36

View File

@@ -17,11 +17,7 @@ build_flags =
lib_deps = lib_deps =
${esp32s3_base.lib_deps} ${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2
zinggjm/GxEPD2@1.6.4 zinggjm/GxEPD2@1.6.4
# renovate: datasource=git-refs depName=CSE_Touch packageName=https://github.com/CIRCUITSTATE/CSE_Touch gitBranch=main
https://github.com/CIRCUITSTATE/CSE_Touch/archive/b44f23b6f870b848f1fbe453c190879bc6cfaafa.zip https://github.com/CIRCUITSTATE/CSE_Touch/archive/b44f23b6f870b848f1fbe453c190879bc6cfaafa.zip
# renovate: datasource=github-tags depName=CSE_CST328 packageName=CIRCUITSTATE/CSE_CST328
https://github.com/CIRCUITSTATE/CSE_CST328/archive/refs/tags/v0.0.4.zip https://github.com/CIRCUITSTATE/CSE_CST328/archive/refs/tags/v0.0.4.zip
# renovate: datasource=git-refs depName=BQ27220 packageName=https://github.com/mverch67/BQ27220 gitBranch=main
https://github.com/mverch67/BQ27220/archive/07d92be846abd8a0258a50c23198dac0858b22ed.zip https://github.com/mverch67/BQ27220/archive/07d92be846abd8a0258a50c23198dac0858b22ed.zip

View File

@@ -12,11 +12,8 @@ build_flags = ${esp32s3_base.build_flags}
-I variants/esp32s3/t-deck -I variants/esp32s3/t-deck
lib_deps = ${esp32s3_base.lib_deps} lib_deps = ${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.7 lovyan03/LovyanGFX@1.2.7
# renovate: datasource=custom.pio depName=ESP8266Audio packageName=earlephilhower/library/ESP8266Audio
earlephilhower/ESP8266Audio@1.9.9 earlephilhower/ESP8266Audio@1.9.9
# renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM
earlephilhower/ESP8266SAM@1.1.0 earlephilhower/ESP8266SAM@1.1.0
[env:t-deck-tft] [env:t-deck-tft]
@@ -71,5 +68,4 @@ build_flags =
lib_deps = lib_deps =
${env:t-deck.lib_deps} ${env:t-deck.lib_deps}
${device-ui_base.lib_deps} ${device-ui_base.lib_deps}
# renovate: datasource=github-tags depName=bb_captouch packageName=bitbank2/bb_captouch
https://github.com/bitbank2/bb_captouch/archive/refs/tags/1.3.1.zip https://github.com/bitbank2/bb_captouch/archive/refs/tags/1.3.1.zip

View File

@@ -15,5 +15,4 @@ lib_ignore =
lib_deps = lib_deps =
${esp32s3_base.lib_deps} ${esp32s3_base.lib_deps}
# renovate: datasource=github-tags depName=ETHClass2 packageName=meshtastic/ETHClass2
https://github.com/meshtastic/ETHClass2/archive/v1.0.0.zip https://github.com/meshtastic/ETHClass2/archive/v1.0.0.zip

View File

@@ -13,15 +13,9 @@ build_flags = ${esp32s3_base.build_flags}
-DHAS_BMA423=1 -DHAS_BMA423=1
lib_deps = ${esp32s3_base.lib_deps} lib_deps = ${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.7 lovyan03/LovyanGFX@1.2.7
# renovate: datasource=custom.pio depName=PCF8563 packageName=lewisxhe/library/PCF8563_Library
lewisxhe/PCF8563_Library@1.0.1 lewisxhe/PCF8563_Library@1.0.1
# renovate: datasource=custom.pio depName=Adafruit DRV2605 packageName=adafruit/library/Adafruit DRV2605 Library
adafruit/Adafruit DRV2605 Library@1.2.4 adafruit/Adafruit DRV2605 Library@1.2.4
# renovate: datasource=custom.pio depName=ESP8266Audio packageName=earlephilhower/library/ESP8266Audio
earlephilhower/ESP8266Audio@1.9.9 earlephilhower/ESP8266Audio@1.9.9
# renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM
earlephilhower/ESP8266SAM@1.1.0 earlephilhower/ESP8266SAM@1.1.0
# renovate: datasource=custom.pio depName=lewisxhe-SensorLib packageName=lewisxhe/library/SensorLib
lewisxhe/SensorLib@0.2.0 lewisxhe/SensorLib@0.2.0

View File

@@ -7,7 +7,6 @@ board_check = true
lib_deps = lib_deps =
${esp32s3_base.lib_deps} ${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=PCF8563 packageName=lewisxhe/library/PCF8563_Library
lewisxhe/PCF8563_Library@1.0.1 lewisxhe/PCF8563_Library@1.0.1
build_flags = build_flags =

View File

@@ -17,23 +17,14 @@ build_flags = ${esp32s3_base.build_flags}
-D ROTARY_BUXTRONICS -D ROTARY_BUXTRONICS
lib_deps = ${esp32s3_base.lib_deps} lib_deps = ${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.7 lovyan03/LovyanGFX@1.2.7
# renovate: datasource=custom.pio depName=ESP8266Audio packageName=earlephilhower/library/ESP8266Audio
earlephilhower/ESP8266Audio@1.9.9 earlephilhower/ESP8266Audio@1.9.9
# renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM
earlephilhower/ESP8266SAM@1.0.1 earlephilhower/ESP8266SAM@1.0.1
# renovate: datasource=custom.pio depName=Adafruit DRV2605 packageName=adafruit/library/Adafruit DRV2605 Library
adafruit/Adafruit DRV2605 Library@1.2.4 adafruit/Adafruit DRV2605 Library@1.2.4
# renovate: datasource=custom.pio depName=PCF8563 packageName=lewisxhe/library/PCF8563_Library
lewisxhe/PCF8563_Library@1.0.1 lewisxhe/PCF8563_Library@1.0.1
# renovate: datasource=custom.pio depName=lewisxhe-SensorLib packageName=lewisxhe/library/SensorLib
lewisxhe/SensorLib@0.3.1 lewisxhe/SensorLib@0.3.1
# renovate: datasource=github-tags depName=pschatzmann_arduino-audio-driver packageName=pschatzmann/arduino-audio-driver https://github.com/pschatzmann/arduino-audio-driver/archive/refs/tags/v0.1.3.zip
https://github.com/pschatzmann/arduino-audio-driver/archive/v0.1.3.zip
# TODO renovate
https://github.com/mverch67/BQ27220/archive/07d92be846abd8a0258a50c23198dac0858b22ed.zip https://github.com/mverch67/BQ27220/archive/07d92be846abd8a0258a50c23198dac0858b22ed.zip
# TODO renovate
https://github.com/mverch67/RotaryEncoder/archive/da958a21389cbcd485989705df602a33e092dd88.zip https://github.com/mverch67/RotaryEncoder/archive/da958a21389cbcd485989705df602a33e092dd88.zip
[env:tlora-pager-tft] [env:tlora-pager-tft]

View File

@@ -20,7 +20,6 @@ build_flags =
lib_deps = lib_deps =
${esp32s3_base.lib_deps} ${esp32s3_base.lib_deps}
# renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master
https://github.com/meshtastic/GxEPD2/archive/b202ebfec6a4821e098cf7a625ba0f6f2400292d.zip https://github.com/meshtastic/GxEPD2/archive/b202ebfec6a4821e098cf7a625ba0f6f2400292d.zip
[env:tlora-t3s3-epaper-inkhud] [env:tlora-t3s3-epaper-inkhud]

View File

@@ -12,7 +12,6 @@ build_flags =
lib_deps = lib_deps =
${esp32s3_base.lib_deps} ${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.7 lovyan03/LovyanGFX@1.2.7
[env:tracksenger-lcd] [env:tracksenger-lcd]
@@ -29,7 +28,6 @@ build_flags =
lib_deps = lib_deps =
${esp32s3_base.lib_deps} ${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.7 lovyan03/LovyanGFX@1.2.7
[env:tracksenger-oled] [env:tracksenger-oled]

View File

@@ -27,13 +27,11 @@ build_src_filter =
+<../variants/esp32s3/unphone> +<../variants/esp32s3/unphone>
lib_deps = ${esp32s3_base.lib_deps} lib_deps = ${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.0 lovyan03/LovyanGFX@1.2.0
# TODO renovate
https://gitlab.com/hamishcunningham/unphonelibrary#meshtastic@9.0.0 https://gitlab.com/hamishcunningham/unphonelibrary#meshtastic@9.0.0
# renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel
adafruit/Adafruit NeoPixel@1.15.2 adafruit/Adafruit NeoPixel@1.15.2
[env:unphone-tft] [env:unphone-tft]
board_level = extra board_level = extra
extends = env:unphone extends = env:unphone

View File

@@ -21,7 +21,6 @@ build_src_filter =
lib_deps = lib_deps =
${env.lib_deps} ${env.lib_deps}
${networking_base.lib_deps} ${networking_base.lib_deps}
${networking_extra.lib_deps}
${radiolib_base.lib_deps} ${radiolib_base.lib_deps}
${environmental_base.lib_deps} ${environmental_base.lib_deps}
# renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto

View File

@@ -6,7 +6,6 @@ board = cross_platform
board_level = extra board_level = extra
lib_deps = lib_deps =
${portduino_base.lib_deps} ${portduino_base.lib_deps}
# renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028
melopero/Melopero RV3028@1.2.0 melopero/Melopero RV3028@1.2.0
build_src_filter = ${portduino_base.build_src_filter} build_src_filter = ${portduino_base.build_src_filter}
@@ -46,6 +45,7 @@ build_flags = ${native_base.build_flags} -Os -lX11 -linput -lxkbcommon -ffunctio
!pkg-config --libs openssl --silence-errors || : !pkg-config --libs openssl --silence-errors || :
!pkg-config --cflags --libs sdl2 --silence-errors || : !pkg-config --cflags --libs sdl2 --silence-errors || :
!pkg-config --cflags --libs libbsd-overlay --silence-errors || : !pkg-config --cflags --libs libbsd-overlay --silence-errors || :
!pkg-config --cflags --libs sqlite3 --silence-errors || :
build_src_filter = build_src_filter =
${native_base.build_src_filter} ${native_base.build_src_filter}

View File

@@ -11,6 +11,5 @@ build_flags = ${nrf52840_base.build_flags}
build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/Dongle_nRF52840-pca10059-v1> build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/Dongle_nRF52840-pca10059-v1>
lib_deps = lib_deps =
${nrf52840_base.lib_deps} ${nrf52840_base.lib_deps}
# renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2
zinggjm/GxEPD2@1.6.5 zinggjm/GxEPD2@1.6.5
debug_tool = jlink debug_tool = jlink

View File

@@ -23,11 +23,8 @@ build_flags = ${nrf52840_base.build_flags}
build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ELECROW-ThinkNode-M1> build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ELECROW-ThinkNode-M1>
lib_deps = lib_deps =
${nrf52840_base.lib_deps} ${nrf52840_base.lib_deps}
# renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master
https://github.com/meshtastic/GxEPD2/archive/33db3fa8ee6fc47d160bdb44f8f127c9a9203a10.zip https://github.com/meshtastic/GxEPD2/archive/33db3fa8ee6fc47d160bdb44f8f127c9a9203a10.zip
# renovate: datasource=custom.pio depName=PCF8563 packageName=lewisxhe/library/PCF8563_Library
lewisxhe/PCF8563_Library@1.0.1 lewisxhe/PCF8563_Library@1.0.1
# renovate: datasource=custom.pio depName=nRF52_PWM packageName=khoih-prog/library/nRF52_PWM
khoih-prog/nRF52_PWM@1.0.1 khoih-prog/nRF52_PWM@1.0.1
;upload_protocol = fs ;upload_protocol = fs
@@ -48,5 +45,4 @@ build_src_filter =
lib_deps = lib_deps =
${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX
${nrf52840_base.lib_deps} ${nrf52840_base.lib_deps}
# renovate: datasource=custom.pio depName=PCF8563 packageName=lewisxhe/library/PCF8563_Library
lewisxhe/PCF8563_Library@1.0.1 lewisxhe/PCF8563_Library@1.0.1

View File

@@ -13,7 +13,5 @@ build_flags =
build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ELECROW-ThinkNode-M3> build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ELECROW-ThinkNode-M3>
lib_deps = lib_deps =
${nrf52840_base.lib_deps} ${nrf52840_base.lib_deps}
# renovate: datasource=custom.pio depName=nRF52_PWM packageName=khoih-prog/library/nRF52_PWM
khoih-prog/nRF52_PWM@1.0.1 khoih-prog/nRF52_PWM@1.0.1
# renovate: datasource=custom.pio depName=PCF8563 packageName=lewisxhe/library/PCF8563_Library
lewisxhe/PCF8563_Library@1.0.1 lewisxhe/PCF8563_Library@1.0.1

View File

@@ -12,5 +12,4 @@ build_flags = ${nrf52840_base.build_flags}
build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ELECROW-ThinkNode-M6> build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ELECROW-ThinkNode-M6>
lib_deps = lib_deps =
${nrf52840_base.lib_deps} ${nrf52840_base.lib_deps}
# renovate: datasource=custom.pio depName=PCF8563 packageName=lewisxhe/library/PCF8563_Library
lewisxhe/PCF8563_Library@1.0.1 lewisxhe/PCF8563_Library@1.0.1

View File

@@ -15,7 +15,6 @@ board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld
build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ME25LS01-4Y10TD_e-ink> build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ME25LS01-4Y10TD_e-ink>
lib_deps = lib_deps =
${nrf52840_base.lib_deps} ${nrf52840_base.lib_deps}
# renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2
zinggjm/GxEPD2@1.6.5 zinggjm/GxEPD2@1.6.5
; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm)
upload_protocol = nrfutil upload_protocol = nrfutil

View File

@@ -12,9 +12,7 @@ build_flags = ${nrf52840_base.build_flags}
build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/MakePython_nRF52840_eink> build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/MakePython_nRF52840_eink>
lib_deps = lib_deps =
${nrf52840_base.lib_deps} ${nrf52840_base.lib_deps}
# renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master
https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip
# renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2
zinggjm/GxEPD2@1.6.5 zinggjm/GxEPD2@1.6.5
debug_tool = jlink debug_tool = jlink
;upload_port = /dev/ttyACM4 ;upload_port = /dev/ttyACM4

View File

@@ -8,6 +8,5 @@ build_flags = ${nrf52840_base.build_flags}
build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/MakePython_nRF52840_oled> build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/MakePython_nRF52840_oled>
lib_deps = lib_deps =
${nrf52840_base.lib_deps} ${nrf52840_base.lib_deps}
# renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master
https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip
debug_tool = jlink debug_tool = jlink

Some files were not shown because too many files have changed in this diff Show More