mirror of
https://github.com/meshtastic/firmware.git
synced 2025-12-15 15:22:34 +00:00
Compare commits
12 Commits
multi-mess
...
sfpp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7373fce2e1 | ||
|
|
f10d2fb8c2 | ||
|
|
4552efce81 | ||
|
|
ecdcbe5c09 | ||
|
|
4dd43c7753 | ||
|
|
471da2e632 | ||
|
|
03961161af | ||
|
|
dfe74653f2 | ||
|
|
6b2bd09dd8 | ||
|
|
4fd7960371 | ||
|
|
5f0e86a27e | ||
|
|
9ce1a7ff79 |
2
.github/actions/build-variant/action.yml
vendored
2
.github/actions/build-variant/action.yml
vendored
@@ -76,7 +76,7 @@ runs:
|
||||
done
|
||||
|
||||
- name: PlatformIO ${{ inputs.arch }} download cache
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.platformio/.cache
|
||||
key: pio-cache-${{ inputs.arch }}-${{ hashFiles('.github/actions/**', '**.ini') }}
|
||||
|
||||
@@ -9,16 +9,16 @@ plugins:
|
||||
lint:
|
||||
enabled:
|
||||
- checkov@3.2.495
|
||||
- renovate@42.48.0
|
||||
- renovate@42.42.2
|
||||
- prettier@3.7.4
|
||||
- trufflehog@3.92.3
|
||||
- trufflehog@3.92.1
|
||||
- yamllint@1.37.1
|
||||
- bandit@1.9.2
|
||||
- trivy@0.68.1
|
||||
- taplo@0.10.0
|
||||
- ruff@0.14.9
|
||||
- ruff@0.14.8
|
||||
- isort@7.0.0
|
||||
- markdownlint@0.47.0
|
||||
- markdownlint@0.46.0
|
||||
- oxipng@10.0.0
|
||||
- svgo@4.0.0
|
||||
- actionlint@1.7.9
|
||||
|
||||
@@ -184,8 +184,6 @@ Input:
|
||||
Logging:
|
||||
LogLevel: info # debug, info, warn, error
|
||||
# TraceFile: /var/log/meshtasticd.json
|
||||
# JSONFile: /packets.json # File location for JSON output of decoded packets
|
||||
# JSONFilter: position # filter for packets to save to JSON file
|
||||
# AsciiLogs: true # default if not specified is !isatty() on stdout
|
||||
|
||||
Webserver:
|
||||
|
||||
@@ -123,7 +123,7 @@ lib_deps =
|
||||
[device-ui_base]
|
||||
lib_deps =
|
||||
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
|
||||
https://github.com/meshtastic/device-ui/archive/2746a1ce3804998460a2cb319b8ea8a238dfd8c9.zip
|
||||
https://github.com/meshtastic/device-ui/archive/4fb5f24787caa841b58dbf623a52c4c5861d6722.zip
|
||||
|
||||
; Common libs for environmental measurements in telemetry module
|
||||
[environmental_base]
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -11,7 +11,6 @@
|
||||
* For more information, see: https://meshtastic.org/
|
||||
*/
|
||||
#include "power.h"
|
||||
#include "MessageStore.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "Throttle.h"
|
||||
@@ -787,9 +786,7 @@ void Power::shutdown()
|
||||
playShutdownMelody();
|
||||
#endif
|
||||
nodeDB->saveToDisk();
|
||||
#if HAS_SCREEN
|
||||
messageStore.saveToFlash();
|
||||
#endif
|
||||
|
||||
#if defined(ARCH_NRF52) || defined(ARCH_ESP32) || defined(ARCH_RP2040)
|
||||
#ifdef PIN_LED1
|
||||
ledOff(PIN_LED1);
|
||||
|
||||
@@ -131,7 +131,6 @@ void RedirectablePrint::log_to_serial(const char *logLevel, const char *format,
|
||||
int hour = hms / SEC_PER_HOUR;
|
||||
int min = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||||
int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN
|
||||
|
||||
#ifdef ARCH_PORTDUINO
|
||||
::printf("%s ", logLevel);
|
||||
if (color) {
|
||||
|
||||
@@ -46,7 +46,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#endif
|
||||
#include "FSCommon.h"
|
||||
#include "MeshService.h"
|
||||
#include "MessageStore.h"
|
||||
#include "RadioLibInterface.h"
|
||||
#include "error.h"
|
||||
#include "gps/GeoCoord.h"
|
||||
@@ -65,7 +64,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#include "modules/WaypointModule.h"
|
||||
#include "sleep.h"
|
||||
#include "target_specific.h"
|
||||
extern MessageStore messageStore;
|
||||
|
||||
using graphics::Emote;
|
||||
using graphics::emotes;
|
||||
using graphics::numEmotes;
|
||||
|
||||
#if USE_TFTDISPLAY
|
||||
extern uint16_t TFT_MESH;
|
||||
@@ -117,6 +119,10 @@ uint32_t dopThresholds[5] = {2000, 1000, 500, 200, 100};
|
||||
// we'll need to hold onto pointers for the modules that can draw a frame.
|
||||
std::vector<MeshModule *> moduleFrames;
|
||||
|
||||
// Global variables for screen function overlay symbols
|
||||
std::vector<std::string> functionSymbol;
|
||||
std::string functionSymbolString;
|
||||
|
||||
#if HAS_GPS
|
||||
// GeoCoord object for the screen
|
||||
GeoCoord geoCoord;
|
||||
@@ -257,11 +263,19 @@ static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int
|
||||
} else {
|
||||
// otherwise, just display the module frame that's aligned with the current frame
|
||||
module_frame = state->currentFrame;
|
||||
// LOG_DEBUG("Screen is not in transition. Frame: %d", module_frame);
|
||||
}
|
||||
// LOG_DEBUG("Draw Module Frame %d", module_frame);
|
||||
MeshModule &pi = *moduleFrames.at(module_frame);
|
||||
pi.drawFrame(display, state, x, y);
|
||||
}
|
||||
|
||||
// Ignore messages originating from phone (from the current node 0x0) unless range test or store and forward module are enabled
|
||||
static bool shouldDrawMessage(const meshtastic_MeshPacket *packet)
|
||||
{
|
||||
return packet->from != 0 && !moduleConfig.store_forward.enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a recent lat/lon return a guess of the heading the user is walking on.
|
||||
*
|
||||
@@ -308,16 +322,16 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
|
||||
{
|
||||
graphics::normalFrames = new FrameCallback[MAX_NUM_NODES + NUM_EXTRA_FRAMES];
|
||||
|
||||
LOG_INFO("Protobuf Value uiconfig.screen_rgb_color: %d", uiconfig.screen_rgb_color);
|
||||
int32_t rawRGB = uiconfig.screen_rgb_color;
|
||||
|
||||
// Only validate the combined value once
|
||||
if (rawRGB > 0 && rawRGB <= 255255255) {
|
||||
// Extract each component as a normal int first
|
||||
int r = (rawRGB >> 16) & 0xFF;
|
||||
int g = (rawRGB >> 8) & 0xFF;
|
||||
int b = rawRGB & 0xFF;
|
||||
if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) {
|
||||
TFT_MESH = COLOR565(static_cast<uint8_t>(r), static_cast<uint8_t>(g), static_cast<uint8_t>(b));
|
||||
uint8_t TFT_MESH_r = (rawRGB >> 16) & 0xFF;
|
||||
uint8_t TFT_MESH_g = (rawRGB >> 8) & 0xFF;
|
||||
uint8_t TFT_MESH_b = rawRGB & 0xFF;
|
||||
LOG_INFO("Values of r,g,b: %d, %d, %d", TFT_MESH_r, TFT_MESH_g, TFT_MESH_b);
|
||||
|
||||
if (TFT_MESH_r <= 255 && TFT_MESH_g <= 255 && TFT_MESH_b <= 255) {
|
||||
TFT_MESH = COLOR565(TFT_MESH_r, TFT_MESH_g, TFT_MESH_b);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -536,10 +550,10 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
|
||||
void Screen::setup()
|
||||
{
|
||||
|
||||
// Enable display rendering
|
||||
// === Enable display rendering ===
|
||||
useDisplay = true;
|
||||
|
||||
// Load saved brightness from UI config
|
||||
// === Load saved brightness from UI config ===
|
||||
// For OLED displays (SSD1306), default brightness is 255 if not set
|
||||
if (uiconfig.screen_brightness == 0) {
|
||||
#if defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107)
|
||||
@@ -551,7 +565,7 @@ void Screen::setup()
|
||||
brightness = uiconfig.screen_brightness;
|
||||
}
|
||||
|
||||
// Detect OLED subtype (if supported by board variant)
|
||||
// === Detect OLED subtype (if supported by board variant) ===
|
||||
#ifdef AutoOLEDWire_h
|
||||
if (isAUTOOled)
|
||||
static_cast<AutoOLEDWire *>(dispdev)->setDetected(model);
|
||||
@@ -573,7 +587,7 @@ void Screen::setup()
|
||||
static_cast<ST7796Spi *>(dispdev)->setRGB(TFT_MESH);
|
||||
#endif
|
||||
|
||||
// Initialize display and UI system
|
||||
// === Initialize display and UI system ===
|
||||
ui->init();
|
||||
displayWidth = dispdev->width();
|
||||
displayHeight = dispdev->height();
|
||||
@@ -585,7 +599,7 @@ void Screen::setup()
|
||||
ui->disableAllIndicators(); // Disable page indicator dots
|
||||
ui->getUiState()->userData = this; // Allow static callbacks to access Screen instance
|
||||
|
||||
// Apply loaded brightness
|
||||
// === Apply loaded brightness ===
|
||||
#if defined(ST7789_CS)
|
||||
static_cast<TFTDisplay *>(dispdev)->setDisplayBrightness(brightness);
|
||||
#elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SPISSD1306)
|
||||
@@ -593,20 +607,20 @@ void Screen::setup()
|
||||
#endif
|
||||
LOG_INFO("Applied screen brightness: %d", brightness);
|
||||
|
||||
// Set custom overlay callbacks
|
||||
// === Set custom overlay callbacks ===
|
||||
static OverlayCallback overlays[] = {
|
||||
graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame
|
||||
};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
|
||||
// Enable UTF-8 to display mapping
|
||||
// === Enable UTF-8 to display mapping ===
|
||||
dispdev->setFontTableLookupFunction(customFontTableLookup);
|
||||
|
||||
#ifdef USERPREFS_OEM_TEXT
|
||||
logo_timeout *= 2; // Give more time for branded boot logos
|
||||
#endif
|
||||
|
||||
// Configure alert frames (e.g., "Resuming..." or region name)
|
||||
// === Configure alert frames (e.g., "Resuming..." or region name) ===
|
||||
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip slow refresh
|
||||
alertFrames[0] = [this](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) {
|
||||
#ifdef ARCH_ESP32
|
||||
@@ -622,10 +636,10 @@ void Screen::setup()
|
||||
ui->setFrames(alertFrames, 1);
|
||||
ui->disableAutoTransition(); // Require manual navigation between frames
|
||||
|
||||
// Log buffer for on-screen logs (3 lines max)
|
||||
// === Log buffer for on-screen logs (3 lines max) ===
|
||||
dispdev->setLogBuffer(3, 32);
|
||||
|
||||
// Optional screen mirroring or flipping (e.g. for T-Beam orientation)
|
||||
// === Optional screen mirroring or flipping (e.g. for T-Beam orientation) ===
|
||||
#ifdef SCREEN_MIRROR
|
||||
dispdev->mirrorScreen();
|
||||
#else
|
||||
@@ -643,7 +657,7 @@ void Screen::setup()
|
||||
}
|
||||
#endif
|
||||
|
||||
// Generate device ID from MAC address
|
||||
// === Generate device ID from MAC address ===
|
||||
uint8_t dmac[6];
|
||||
getMacAddr(dmac);
|
||||
snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]);
|
||||
@@ -652,7 +666,7 @@ void Screen::setup()
|
||||
handleSetOn(false); // Ensure proper init for Arduino targets
|
||||
#endif
|
||||
|
||||
// Turn on display and trigger first draw
|
||||
// === Turn on display and trigger first draw ===
|
||||
handleSetOn(true);
|
||||
determineResolution(dispdev->height(), dispdev->width());
|
||||
ui->update();
|
||||
@@ -675,7 +689,7 @@ void Screen::setup()
|
||||
touchScreenImpl1->init();
|
||||
#endif
|
||||
|
||||
// Subscribe to device status updates
|
||||
// === Subscribe to device status updates ===
|
||||
powerStatusObserver.observe(&powerStatus->onNewStatus);
|
||||
gpsStatusObserver.observe(&gpsStatus->onNewStatus);
|
||||
nodeStatusObserver.observe(&nodeStatus->onNewStatus);
|
||||
@@ -683,14 +697,12 @@ void Screen::setup()
|
||||
#if !MESHTASTIC_EXCLUDE_ADMIN
|
||||
adminMessageObserver.observe(adminModule);
|
||||
#endif
|
||||
if (textMessageModule)
|
||||
textMessageObserver.observe(textMessageModule);
|
||||
if (inputBroker)
|
||||
inputObserver.observe(inputBroker);
|
||||
|
||||
// Load persisted messages into RAM
|
||||
messageStore.loadFromFlash();
|
||||
LOG_INFO("MessageStore loaded from flash");
|
||||
|
||||
// Notify modules that support UI events
|
||||
// === Notify modules that support UI events ===
|
||||
MeshModule::observeUIEvents(&uiFrameEventObserver);
|
||||
}
|
||||
|
||||
@@ -761,23 +773,6 @@ int32_t Screen::runOnce()
|
||||
if (displayHeight == 0) {
|
||||
displayHeight = dispdev->getHeight();
|
||||
}
|
||||
|
||||
// Detect frame transitions and clear message cache when leaving text message screen
|
||||
{
|
||||
static int8_t lastFrameIndex = -1;
|
||||
int8_t currentFrameIndex = ui->getUiState()->currentFrame;
|
||||
int8_t textMsgIndex = framesetInfo.positions.textMessage;
|
||||
|
||||
if (lastFrameIndex != -1 && currentFrameIndex != lastFrameIndex) {
|
||||
|
||||
if (lastFrameIndex == textMsgIndex && currentFrameIndex != textMsgIndex) {
|
||||
graphics::MessageRenderer::clearMessageCache();
|
||||
}
|
||||
}
|
||||
|
||||
lastFrameIndex = currentFrameIndex;
|
||||
}
|
||||
|
||||
menuHandler::handleMenuSwitch(dispdev);
|
||||
|
||||
// Show boot screen for first logo_timeout seconds, then switch to normal operation.
|
||||
@@ -833,17 +828,17 @@ int32_t Screen::runOnce()
|
||||
break;
|
||||
case Cmd::ON_PRESS:
|
||||
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||
showFrame(FrameDirection::NEXT);
|
||||
handleOnPress();
|
||||
}
|
||||
break;
|
||||
case Cmd::SHOW_PREV_FRAME:
|
||||
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||
showFrame(FrameDirection::PREVIOUS);
|
||||
handleShowPrevFrame();
|
||||
}
|
||||
break;
|
||||
case Cmd::SHOW_NEXT_FRAME:
|
||||
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||
showFrame(FrameDirection::NEXT);
|
||||
handleShowNextFrame();
|
||||
}
|
||||
break;
|
||||
case Cmd::START_ALERT_FRAME: {
|
||||
@@ -864,7 +859,6 @@ int32_t Screen::runOnce()
|
||||
break;
|
||||
case Cmd::STOP_ALERT_FRAME:
|
||||
NotificationRenderer::pauseBanner = false;
|
||||
break;
|
||||
case Cmd::STOP_BOOT_SCREEN:
|
||||
EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame
|
||||
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||
@@ -1035,6 +1029,9 @@ void Screen::setFrames(FrameFocus focus)
|
||||
}
|
||||
#endif
|
||||
|
||||
// Declare this early so it’s available in FOCUS_PRESERVE block
|
||||
bool willInsertTextMessage = shouldDrawMessage(&devicestate.rx_text_message);
|
||||
|
||||
if (!hiddenFrames.home) {
|
||||
fsi.positions.home = numframes;
|
||||
normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused;
|
||||
@@ -1046,16 +1043,11 @@ void Screen::setFrames(FrameFocus focus)
|
||||
indicatorIcons.push_back(icon_mail);
|
||||
|
||||
#ifndef USE_EINK
|
||||
if (!hiddenFrames.nodelist_nodes) {
|
||||
fsi.positions.nodelist_nodes = numframes;
|
||||
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicListScreen_Nodes;
|
||||
if (!hiddenFrames.nodelist) {
|
||||
fsi.positions.nodelist = numframes;
|
||||
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicNodeListScreen;
|
||||
indicatorIcons.push_back(icon_nodes);
|
||||
}
|
||||
if (!hiddenFrames.nodelist_location) {
|
||||
fsi.positions.nodelist_location = numframes;
|
||||
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicListScreen_Location;
|
||||
indicatorIcons.push_back(icon_list);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Show detailed node views only on E-Ink builds
|
||||
@@ -1077,13 +1069,11 @@ void Screen::setFrames(FrameFocus focus)
|
||||
}
|
||||
#endif
|
||||
#if HAS_GPS
|
||||
#ifdef USE_EINK
|
||||
if (!hiddenFrames.nodelist_bearings) {
|
||||
fsi.positions.nodelist_bearings = numframes;
|
||||
normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses;
|
||||
indicatorIcons.push_back(icon_list);
|
||||
}
|
||||
#endif
|
||||
if (!hiddenFrames.gps) {
|
||||
fsi.positions.gps = numframes;
|
||||
normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen;
|
||||
@@ -1183,7 +1173,7 @@ void Screen::setFrames(FrameFocus focus)
|
||||
}
|
||||
|
||||
fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE
|
||||
this->frameCount = numframes; // Save frame count for use in custom overlay
|
||||
this->frameCount = numframes; // ✅ Save frame count for use in custom overlay
|
||||
LOG_DEBUG("Finished build frames. numframes: %d", numframes);
|
||||
|
||||
ui->setFrames(normalFrames, numframes);
|
||||
@@ -1203,6 +1193,10 @@ void Screen::setFrames(FrameFocus focus)
|
||||
case FOCUS_FAULT:
|
||||
ui->switchToFrame(fsi.positions.fault);
|
||||
break;
|
||||
case FOCUS_TEXTMESSAGE:
|
||||
hasUnreadMessage = false; // ✅ Clear when message is *viewed*
|
||||
ui->switchToFrame(fsi.positions.textMessage);
|
||||
break;
|
||||
case FOCUS_MODULE:
|
||||
// Whichever frame was marked by MeshModule::requestFocus(), if any
|
||||
// If no module requested focus, will show the first frame instead
|
||||
@@ -1245,11 +1239,8 @@ void Screen::setFrameImmediateDraw(FrameCallback *drawFrames)
|
||||
void Screen::toggleFrameVisibility(const std::string &frameName)
|
||||
{
|
||||
#ifndef USE_EINK
|
||||
if (frameName == "nodelist_nodes") {
|
||||
hiddenFrames.nodelist_nodes = !hiddenFrames.nodelist_nodes;
|
||||
}
|
||||
if (frameName == "nodelist_location") {
|
||||
hiddenFrames.nodelist_location = !hiddenFrames.nodelist_location;
|
||||
if (frameName == "nodelist") {
|
||||
hiddenFrames.nodelist = !hiddenFrames.nodelist;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_EINK
|
||||
@@ -1264,11 +1255,9 @@ void Screen::toggleFrameVisibility(const std::string &frameName)
|
||||
}
|
||||
#endif
|
||||
#if HAS_GPS
|
||||
#ifdef USE_EINK
|
||||
if (frameName == "nodelist_bearings") {
|
||||
hiddenFrames.nodelist_bearings = !hiddenFrames.nodelist_bearings;
|
||||
}
|
||||
#endif
|
||||
if (frameName == "gps") {
|
||||
hiddenFrames.gps = !hiddenFrames.gps;
|
||||
}
|
||||
@@ -1290,10 +1279,8 @@ void Screen::toggleFrameVisibility(const std::string &frameName)
|
||||
bool Screen::isFrameHidden(const std::string &frameName) const
|
||||
{
|
||||
#ifndef USE_EINK
|
||||
if (frameName == "nodelist_nodes")
|
||||
return hiddenFrames.nodelist_nodes;
|
||||
if (frameName == "nodelist_location")
|
||||
return hiddenFrames.nodelist_location;
|
||||
if (frameName == "nodelist")
|
||||
return hiddenFrames.nodelist;
|
||||
#endif
|
||||
#ifdef USE_EINK
|
||||
if (frameName == "nodelist_lastheard")
|
||||
@@ -1304,10 +1291,8 @@ bool Screen::isFrameHidden(const std::string &frameName) const
|
||||
return hiddenFrames.nodelist_distance;
|
||||
#endif
|
||||
#if HAS_GPS
|
||||
#ifdef USE_EINK
|
||||
if (frameName == "nodelist_bearings")
|
||||
return hiddenFrames.nodelist_bearings;
|
||||
#endif
|
||||
if (frameName == "gps")
|
||||
return hiddenFrames.gps;
|
||||
#endif
|
||||
@@ -1323,6 +1308,37 @@ bool Screen::isFrameHidden(const std::string &frameName) const
|
||||
return false;
|
||||
}
|
||||
|
||||
// Dismisses the currently displayed screen frame, if possible
|
||||
// Relevant for text message, waypoint, others in future?
|
||||
// Triggered with a CardKB keycombo
|
||||
void Screen::hideCurrentFrame()
|
||||
{
|
||||
uint8_t currentFrame = ui->getUiState()->currentFrame;
|
||||
bool dismissed = false;
|
||||
if (currentFrame == framesetInfo.positions.textMessage && devicestate.has_rx_text_message) {
|
||||
LOG_INFO("Hide Text Message");
|
||||
devicestate.has_rx_text_message = false;
|
||||
memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message));
|
||||
} else if (currentFrame == framesetInfo.positions.waypoint && devicestate.has_rx_waypoint) {
|
||||
LOG_DEBUG("Hide Waypoint");
|
||||
devicestate.has_rx_waypoint = false;
|
||||
hiddenFrames.waypoint = true;
|
||||
dismissed = true;
|
||||
} else if (currentFrame == framesetInfo.positions.wifi) {
|
||||
LOG_DEBUG("Hide WiFi Screen");
|
||||
hiddenFrames.wifi = true;
|
||||
dismissed = true;
|
||||
} else if (currentFrame == framesetInfo.positions.lora) {
|
||||
LOG_INFO("Hide LoRa");
|
||||
hiddenFrames.lora = true;
|
||||
dismissed = true;
|
||||
}
|
||||
|
||||
if (dismissed) {
|
||||
setFrames(FOCUS_DEFAULT); // You could also use FOCUS_PRESERVE
|
||||
}
|
||||
}
|
||||
|
||||
void Screen::handleStartFirmwareUpdateScreen()
|
||||
{
|
||||
LOG_DEBUG("Show firmware screen");
|
||||
@@ -1375,6 +1391,28 @@ void Screen::decreaseBrightness()
|
||||
/* TO DO: add little popup in center of screen saying what brightness level it is set to*/
|
||||
}
|
||||
|
||||
void Screen::setFunctionSymbol(std::string sym)
|
||||
{
|
||||
if (std::find(functionSymbol.begin(), functionSymbol.end(), sym) == functionSymbol.end()) {
|
||||
functionSymbol.push_back(sym);
|
||||
functionSymbolString = "";
|
||||
for (auto symbol : functionSymbol) {
|
||||
functionSymbolString = symbol + " " + functionSymbolString;
|
||||
}
|
||||
setFastFramerate();
|
||||
}
|
||||
}
|
||||
|
||||
void Screen::removeFunctionSymbol(std::string sym)
|
||||
{
|
||||
functionSymbol.erase(std::remove(functionSymbol.begin(), functionSymbol.end(), sym), functionSymbol.end());
|
||||
functionSymbolString = "";
|
||||
for (auto symbol : functionSymbol) {
|
||||
functionSymbolString = symbol + " " + functionSymbolString;
|
||||
}
|
||||
setFastFramerate();
|
||||
}
|
||||
|
||||
void Screen::handleOnPress()
|
||||
{
|
||||
// If screen was off, just wake it, otherwise advance to next frame
|
||||
@@ -1386,17 +1424,23 @@ void Screen::handleOnPress()
|
||||
}
|
||||
}
|
||||
|
||||
void Screen::showFrame(FrameDirection direction)
|
||||
void Screen::handleShowPrevFrame()
|
||||
{
|
||||
// Only advance frames when UI is stable
|
||||
// If screen was off, just wake it, otherwise go back to previous frame
|
||||
// If we are in a transition, the press must have bounced, drop it.
|
||||
if (ui->getUiState()->frameState == FIXED) {
|
||||
ui->previousFrame();
|
||||
lastScreenTransition = millis();
|
||||
setFastFramerate();
|
||||
}
|
||||
}
|
||||
|
||||
if (direction == FrameDirection::NEXT) {
|
||||
ui->nextFrame();
|
||||
} else {
|
||||
ui->previousFrame();
|
||||
}
|
||||
|
||||
void Screen::handleShowNextFrame()
|
||||
{
|
||||
// If screen was off, just wake it, otherwise advance to next frame
|
||||
// If we are in a transition, the press must have bounced, drop it.
|
||||
if (ui->getUiState()->frameState == FIXED) {
|
||||
ui->nextFrame();
|
||||
lastScreenTransition = millis();
|
||||
setFastFramerate();
|
||||
}
|
||||
@@ -1422,6 +1466,7 @@ void Screen::setFastFramerate()
|
||||
|
||||
int Screen::handleStatusUpdate(const meshtastic::Status *arg)
|
||||
{
|
||||
// LOG_DEBUG("Screen got status update %d", arg->getStatusType());
|
||||
switch (arg->getStatusType()) {
|
||||
case STATUS_TYPE_NODE:
|
||||
if (showingNormalScreen && nodeStatus->getLastNumTotal() != nodeStatus->getNumTotal()) {
|
||||
@@ -1579,26 +1624,16 @@ int Screen::handleUIFrameEvent(const UIFrameEvent *event)
|
||||
|
||||
if (showingNormalScreen) {
|
||||
// Regenerate the frameset, potentially honoring a module's internal requestFocus() call
|
||||
if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET) {
|
||||
if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET)
|
||||
setFrames(FOCUS_MODULE);
|
||||
}
|
||||
|
||||
// Regenerate the frameset, while attempting to maintain focus on the current frame
|
||||
else if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET_BACKGROUND) {
|
||||
// Regenerate the frameset, while Attempt to maintain focus on the current frame
|
||||
else if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET_BACKGROUND)
|
||||
setFrames(FOCUS_PRESERVE);
|
||||
}
|
||||
|
||||
// Don't regenerate the frameset, just re-draw whatever is on screen ASAP
|
||||
else if (event->action == UIFrameEvent::Action::REDRAW_ONLY) {
|
||||
else if (event->action == UIFrameEvent::Action::REDRAW_ONLY)
|
||||
setFastFramerate();
|
||||
}
|
||||
|
||||
// Jump directly to the Text Message screen
|
||||
else if (event->action == UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE) {
|
||||
setFrames(FOCUS_PRESERVE); // preserve current frame ordering
|
||||
ui->switchToFrame(framesetInfo.positions.textMessage);
|
||||
setFastFramerate(); // force redraw ASAP
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
@@ -1636,48 +1671,7 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
menuHandler::handleMenuSwitch(dispdev);
|
||||
return 0;
|
||||
}
|
||||
// UP/DOWN in message screen scrolls through message threads
|
||||
if (ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) {
|
||||
|
||||
if (event->inputEvent == INPUT_BROKER_UP) {
|
||||
if (messageStore.getMessages().empty()) {
|
||||
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
|
||||
} else {
|
||||
graphics::MessageRenderer::scrollUp();
|
||||
setFastFramerate(); // match existing behavior
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (event->inputEvent == INPUT_BROKER_DOWN) {
|
||||
if (messageStore.getMessages().empty()) {
|
||||
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
|
||||
} else {
|
||||
graphics::MessageRenderer::scrollDown();
|
||||
setFastFramerate();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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,
|
||||
// so long as a mesh module isn't using these events for some other purpose
|
||||
if (showingNormalScreen) {
|
||||
@@ -1691,39 +1685,16 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
|
||||
// If no modules are using the input, move between frames
|
||||
if (!inputIntercepted) {
|
||||
#if defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2
|
||||
bool handledEncoderScroll = false;
|
||||
const bool isTextMessageFrame = (framesetInfo.positions.textMessage != 255 &&
|
||||
this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage &&
|
||||
!messageStore.getMessages().empty());
|
||||
if (isTextMessageFrame) {
|
||||
if (event->inputEvent == INPUT_BROKER_UP_LONG) {
|
||||
graphics::MessageRenderer::nudgeScroll(-1);
|
||||
handledEncoderScroll = true;
|
||||
} else if (event->inputEvent == INPUT_BROKER_DOWN_LONG) {
|
||||
graphics::MessageRenderer::nudgeScroll(1);
|
||||
handledEncoderScroll = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (handledEncoderScroll) {
|
||||
setFastFramerate();
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
if (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_ALT_PRESS) {
|
||||
showFrame(FrameDirection::PREVIOUS);
|
||||
showPrevFrame();
|
||||
} else if (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS) {
|
||||
showFrame(FrameDirection::NEXT);
|
||||
showNextFrame();
|
||||
} else if (event->inputEvent == INPUT_BROKER_UP_LONG) {
|
||||
// Long press up button for fast frame switching
|
||||
showPrevFrame();
|
||||
} else if (event->inputEvent == INPUT_BROKER_DOWN_LONG) {
|
||||
// Long press down button for fast frame switching
|
||||
showNextFrame();
|
||||
} else if ((event->inputEvent == INPUT_BROKER_UP || event->inputEvent == INPUT_BROKER_DOWN) &&
|
||||
this->ui->getUiState()->currentFrame == framesetInfo.positions.home) {
|
||||
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
|
||||
} else if (event->inputEvent == INPUT_BROKER_SELECT) {
|
||||
if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) {
|
||||
menuHandler::homeBaseMenu();
|
||||
@@ -1738,7 +1709,7 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) {
|
||||
menuHandler::loraMenu();
|
||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) {
|
||||
if (!messageStore.getMessages().empty()) {
|
||||
if (devicestate.rx_text_message.from) {
|
||||
menuHandler::messageResponseMenu();
|
||||
} else {
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
@@ -1751,8 +1722,7 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite &&
|
||||
this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) {
|
||||
menuHandler::favoriteBaseMenu();
|
||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_nodes ||
|
||||
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_location ||
|
||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist ||
|
||||
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard ||
|
||||
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal ||
|
||||
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance ||
|
||||
@@ -1763,7 +1733,7 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
menuHandler::wifiBaseMenu();
|
||||
}
|
||||
} else if (event->inputEvent == INPUT_BROKER_BACK) {
|
||||
showFrame(FrameDirection::PREVIOUS);
|
||||
showPrevFrame();
|
||||
} else if (event->inputEvent == INPUT_BROKER_CANCEL) {
|
||||
setOn(false);
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ class Screen
|
||||
FOCUS_DEFAULT, // No specific frame
|
||||
FOCUS_PRESERVE, // Return to the previous frame
|
||||
FOCUS_FAULT,
|
||||
FOCUS_TEXTMESSAGE,
|
||||
FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus
|
||||
FOCUS_CLOCK,
|
||||
FOCUS_SYSTEM,
|
||||
@@ -54,6 +55,8 @@ class Screen
|
||||
void startFirmwareUpdateScreen() {}
|
||||
void increaseBrightness() {}
|
||||
void decreaseBrightness() {}
|
||||
void setFunctionSymbol(std::string) {}
|
||||
void removeFunctionSymbol(std::string) {}
|
||||
void startAlert(const char *) {}
|
||||
void showSimpleBanner(const char *message, uint32_t durationMs = 0) {}
|
||||
void showOverlayBanner(BannerOverlayOptions) {}
|
||||
@@ -169,8 +172,6 @@ class Point
|
||||
namespace graphics
|
||||
{
|
||||
|
||||
enum class FrameDirection { NEXT, PREVIOUS };
|
||||
|
||||
// Forward declarations
|
||||
class Screen;
|
||||
|
||||
@@ -210,6 +211,8 @@ class Screen : public concurrency::OSThread
|
||||
CallbackObserver<Screen, const meshtastic::Status *>(this, &Screen::handleStatusUpdate);
|
||||
CallbackObserver<Screen, const meshtastic::Status *> nodeStatusObserver =
|
||||
CallbackObserver<Screen, const meshtastic::Status *>(this, &Screen::handleStatusUpdate);
|
||||
CallbackObserver<Screen, const meshtastic_MeshPacket *> textMessageObserver =
|
||||
CallbackObserver<Screen, const meshtastic_MeshPacket *>(this, &Screen::handleTextMessage);
|
||||
CallbackObserver<Screen, const UIFrameEvent *> uiFrameEventObserver =
|
||||
CallbackObserver<Screen, const UIFrameEvent *>(this, &Screen::handleUIFrameEvent); // Sent by Mesh Modules
|
||||
CallbackObserver<Screen, const InputEvent *> inputObserver =
|
||||
@@ -220,10 +223,6 @@ class Screen : public concurrency::OSThread
|
||||
public:
|
||||
OLEDDisplay *getDisplayDevice() { return dispdev; }
|
||||
explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY);
|
||||
|
||||
// Screen dimension accessors
|
||||
inline int getHeight() const { return displayHeight; }
|
||||
inline int getWidth() const { return displayWidth; }
|
||||
size_t frameCount = 0; // Total number of active frames
|
||||
~Screen();
|
||||
|
||||
@@ -232,6 +231,7 @@ class Screen : public concurrency::OSThread
|
||||
FOCUS_DEFAULT, // No specific frame
|
||||
FOCUS_PRESERVE, // Return to the previous frame
|
||||
FOCUS_FAULT,
|
||||
FOCUS_TEXTMESSAGE,
|
||||
FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus
|
||||
FOCUS_CLOCK,
|
||||
FOCUS_SYSTEM,
|
||||
@@ -279,7 +279,6 @@ class Screen : public concurrency::OSThread
|
||||
void onPress() { enqueueCmd(ScreenCmd{.cmd = Cmd::ON_PRESS}); }
|
||||
void showPrevFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_PREV_FRAME}); }
|
||||
void showNextFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_NEXT_FRAME}); }
|
||||
void showFrame(FrameDirection direction);
|
||||
|
||||
// generic alert start
|
||||
void startAlert(FrameCallback _alertFrame)
|
||||
@@ -347,6 +346,9 @@ class Screen : public concurrency::OSThread
|
||||
void increaseBrightness();
|
||||
void decreaseBrightness();
|
||||
|
||||
void setFunctionSymbol(std::string sym);
|
||||
void removeFunctionSymbol(std::string sym);
|
||||
|
||||
/// Stops showing the boot screen.
|
||||
void stopBootScreen() { enqueueCmd(ScreenCmd{.cmd = Cmd::STOP_BOOT_SCREEN}); }
|
||||
|
||||
@@ -577,7 +579,7 @@ class Screen : public concurrency::OSThread
|
||||
|
||||
// Handle observer events
|
||||
int handleStatusUpdate(const meshtastic::Status *arg);
|
||||
int handleTextMessage(const meshtastic_MeshPacket *packet);
|
||||
int handleTextMessage(const meshtastic_MeshPacket *arg);
|
||||
int handleUIFrameEvent(const UIFrameEvent *arg);
|
||||
int handleInputEvent(const InputEvent *arg);
|
||||
int handleAdminMessage(AdminModule_ObserverData *arg);
|
||||
@@ -588,6 +590,9 @@ class Screen : public concurrency::OSThread
|
||||
/// Draws our SSL cert screen during boot (called from WebServer)
|
||||
void setSSLFrames();
|
||||
|
||||
// Dismiss the currently focussed frame, if possible (e.g. text message, waypoint)
|
||||
void hideCurrentFrame();
|
||||
|
||||
// Menu-driven Show / Hide Toggle
|
||||
void toggleFrameVisibility(const std::string &frameName);
|
||||
bool isFrameHidden(const std::string &frameName) const;
|
||||
@@ -635,6 +640,8 @@ class Screen : public concurrency::OSThread
|
||||
// Implementations of various commands, called from doTask().
|
||||
void handleSetOn(bool on, FrameCallback einkScreensaver = NULL);
|
||||
void handleOnPress();
|
||||
void handleShowNextFrame();
|
||||
void handleShowPrevFrame();
|
||||
void handleStartFirmwareUpdateScreen();
|
||||
|
||||
// Info collected by setFrames method.
|
||||
@@ -654,8 +661,7 @@ class Screen : public concurrency::OSThread
|
||||
uint8_t gps = 255;
|
||||
uint8_t home = 255;
|
||||
uint8_t textMessage = 255;
|
||||
uint8_t nodelist_nodes = 255;
|
||||
uint8_t nodelist_location = 255;
|
||||
uint8_t nodelist = 255;
|
||||
uint8_t nodelist_lastheard = 255;
|
||||
uint8_t nodelist_hopsignal = 255;
|
||||
uint8_t nodelist_distance = 255;
|
||||
@@ -678,8 +684,7 @@ class Screen : public concurrency::OSThread
|
||||
bool home = false;
|
||||
bool clock = false;
|
||||
#ifndef USE_EINK
|
||||
bool nodelist_nodes = false;
|
||||
bool nodelist_location = false;
|
||||
bool nodelist = false;
|
||||
#endif
|
||||
#ifdef USE_EINK
|
||||
bool nodelist_lastheard = false;
|
||||
@@ -687,9 +692,7 @@ class Screen : public concurrency::OSThread
|
||||
bool nodelist_distance = false;
|
||||
#endif
|
||||
#if HAS_GPS
|
||||
#ifdef USE_EINK
|
||||
bool nodelist_bearings = false;
|
||||
#endif
|
||||
bool gps = false;
|
||||
#endif
|
||||
bool lora = false;
|
||||
|
||||
@@ -32,19 +32,6 @@ void determineResolution(int16_t screenheight, int16_t screenwidth)
|
||||
}
|
||||
}
|
||||
|
||||
void decomposeTime(uint32_t rtc_sec, int &hour, int &minute, int &second)
|
||||
{
|
||||
hour = 0;
|
||||
minute = 0;
|
||||
second = 0;
|
||||
if (rtc_sec == 0)
|
||||
return;
|
||||
uint32_t hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY;
|
||||
hour = hms / SEC_PER_HOUR;
|
||||
minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||||
second = hms % SEC_PER_MIN;
|
||||
}
|
||||
|
||||
// === Shared External State ===
|
||||
bool hasUnreadMessage = false;
|
||||
bool isMuted = false;
|
||||
@@ -213,8 +200,8 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
if (rtc_sec > 0) {
|
||||
// === Build Time String ===
|
||||
long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY;
|
||||
int hour, minute, second;
|
||||
graphics::decomposeTime(rtc_sec, hour, minute, second);
|
||||
int hour = hms / SEC_PER_HOUR;
|
||||
int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||||
snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute);
|
||||
|
||||
// === Build Date String ===
|
||||
@@ -427,12 +414,8 @@ void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y)
|
||||
}
|
||||
|
||||
if (drawConnectionState) {
|
||||
const int scale = isHighResolution ? 2 : 1;
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(0, SCREEN_HEIGHT - (1 * scale) - (connection_icon_height * scale), (connection_icon_width * scale),
|
||||
(connection_icon_height * scale) + (2 * scale));
|
||||
display->setColor(WHITE);
|
||||
if (isHighResolution) {
|
||||
const int scale = 2;
|
||||
const int bytesPerRow = (connection_icon_width + 7) / 8;
|
||||
int iconX = 0;
|
||||
int iconY = SCREEN_HEIGHT - (connection_icon_height * 2);
|
||||
|
||||
@@ -45,8 +45,6 @@ extern bool isMuted;
|
||||
extern bool isHighResolution;
|
||||
void determineResolution(int16_t screenheight, int16_t screenwidth);
|
||||
|
||||
void decomposeTime(uint32_t rtc_sec, int &hour, int &minute, int &second);
|
||||
|
||||
// Rounded highlight (used for inverted headers)
|
||||
void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r);
|
||||
|
||||
|
||||
@@ -354,6 +354,8 @@ void VirtualKeyboard::drawInputArea(OLEDDisplay *display, int16_t offsetX, int16
|
||||
if (screenHeight <= 64) {
|
||||
textY = boxY + (boxHeight - inputLineH) / 2;
|
||||
} else {
|
||||
const int innerLeft = boxX + 1;
|
||||
const int innerRight = boxX + boxWidth - 2;
|
||||
const int innerTop = boxY + 1;
|
||||
const int innerBottom = boxY + boxHeight - 2;
|
||||
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
#include "configuration.h"
|
||||
#if HAS_SCREEN
|
||||
#include "ClockRenderer.h"
|
||||
#include "NodeDB.h"
|
||||
#include "UIRenderer.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/GeoCoord.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/draw/UIRenderer.h"
|
||||
#include "graphics/emotes.h"
|
||||
#include "graphics/images.h"
|
||||
#include "main.h"
|
||||
|
||||
@@ -18,31 +23,6 @@ namespace graphics
|
||||
namespace ClockRenderer
|
||||
{
|
||||
|
||||
// Segment bitmaps for numerals 0-9 stored in flash to save RAM.
|
||||
// Each row is a digit, each column is a segment state (1 = on, 0 = off).
|
||||
// Segment layout reference:
|
||||
//
|
||||
// ___1___
|
||||
// 6 | | 2
|
||||
// |_7___|
|
||||
// 5 | | 3
|
||||
// |___4_|
|
||||
//
|
||||
// Segment order: [1, 2, 3, 4, 5, 6, 7]
|
||||
//
|
||||
static const uint8_t PROGMEM digitSegments[10][7] = {
|
||||
{1, 1, 1, 1, 1, 1, 0}, // 0
|
||||
{0, 1, 1, 0, 0, 0, 0}, // 1
|
||||
{1, 1, 0, 1, 1, 0, 1}, // 2
|
||||
{1, 1, 1, 1, 0, 0, 1}, // 3
|
||||
{0, 1, 1, 0, 0, 1, 1}, // 4
|
||||
{1, 0, 1, 1, 0, 1, 1}, // 5
|
||||
{1, 0, 1, 1, 1, 1, 1}, // 6
|
||||
{1, 1, 1, 0, 0, 1, 0}, // 7
|
||||
{1, 1, 1, 1, 1, 1, 1}, // 8
|
||||
{1, 1, 1, 1, 0, 1, 1} // 9
|
||||
};
|
||||
|
||||
void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale)
|
||||
{
|
||||
uint16_t segmentWidth = SEGMENT_WIDTH * scale;
|
||||
@@ -50,7 +30,7 @@ void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale)
|
||||
|
||||
uint16_t cellHeight = (segmentWidth * 2) + (segmentHeight * 3) + 8;
|
||||
|
||||
uint16_t topAndBottomX = x + static_cast<uint16_t>(4 * scale);
|
||||
uint16_t topAndBottomX = x + (4 * scale);
|
||||
|
||||
uint16_t quarterCellHeight = cellHeight / 4;
|
||||
|
||||
@@ -63,16 +43,34 @@ void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale)
|
||||
|
||||
void drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t number, float scale)
|
||||
{
|
||||
// Read 7-segment pattern for the digit from flash
|
||||
uint8_t seg[7];
|
||||
for (uint8_t i = 0; i < 7; i++) {
|
||||
seg[i] = pgm_read_byte(&digitSegments[number][i]);
|
||||
}
|
||||
// the numbers 0-9, each expressed as an array of seven boolean (0|1) values encoding the on/off state of
|
||||
// segment {innerIndex + 1}
|
||||
// e.g., to display the numeral '0', segments 1-6 are on, and segment 7 is off.
|
||||
uint8_t numbers[10][7] = {
|
||||
{1, 1, 1, 1, 1, 1, 0}, // 0 Display segment key
|
||||
{0, 1, 1, 0, 0, 0, 0}, // 1 1
|
||||
{1, 1, 0, 1, 1, 0, 1}, // 2 ___
|
||||
{1, 1, 1, 1, 0, 0, 1}, // 3 6 | | 2
|
||||
{0, 1, 1, 0, 0, 1, 1}, // 4 |_7̲_|
|
||||
{1, 0, 1, 1, 0, 1, 1}, // 5 5 | | 3
|
||||
{1, 0, 1, 1, 1, 1, 1}, // 6 |___|
|
||||
{1, 1, 1, 0, 0, 1, 0}, // 7
|
||||
{1, 1, 1, 1, 1, 1, 1}, // 8 4
|
||||
{1, 1, 1, 1, 0, 1, 1}, // 9
|
||||
};
|
||||
|
||||
// the width and height of each segment's central rectangle:
|
||||
// _____________________
|
||||
// ⋰| (only this part, |⋱
|
||||
// ⋰ | not including | ⋱
|
||||
// ⋱ | the triangles | ⋰
|
||||
// ⋱| on the ends) |⋰
|
||||
// ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
|
||||
|
||||
uint16_t segmentWidth = SEGMENT_WIDTH * scale;
|
||||
uint16_t segmentHeight = SEGMENT_HEIGHT * scale;
|
||||
|
||||
// Precompute segment positions
|
||||
// segment x and y coordinates
|
||||
uint16_t segmentOneX = x + segmentHeight + 2;
|
||||
uint16_t segmentOneY = y;
|
||||
|
||||
@@ -94,21 +92,33 @@ void drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t n
|
||||
uint16_t segmentSevenX = segmentOneX;
|
||||
uint16_t segmentSevenY = segmentTwoY + segmentWidth + 2;
|
||||
|
||||
// Draw only the active segments
|
||||
if (seg[0])
|
||||
drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight);
|
||||
if (seg[1])
|
||||
drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight);
|
||||
if (seg[2])
|
||||
drawVerticalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight);
|
||||
if (seg[3])
|
||||
drawHorizontalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight);
|
||||
if (seg[4])
|
||||
drawVerticalSegment(display, segmentFiveX, segmentFiveY, segmentWidth, segmentHeight);
|
||||
if (seg[5])
|
||||
drawVerticalSegment(display, segmentSixX, segmentSixY, segmentWidth, segmentHeight);
|
||||
if (seg[6])
|
||||
drawHorizontalSegment(display, segmentSevenX, segmentSevenY, segmentWidth, segmentHeight);
|
||||
if (numbers[number][0]) {
|
||||
graphics::ClockRenderer::drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight);
|
||||
}
|
||||
|
||||
if (numbers[number][1]) {
|
||||
graphics::ClockRenderer::drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight);
|
||||
}
|
||||
|
||||
if (numbers[number][2]) {
|
||||
graphics::ClockRenderer::drawVerticalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight);
|
||||
}
|
||||
|
||||
if (numbers[number][3]) {
|
||||
graphics::ClockRenderer::drawHorizontalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight);
|
||||
}
|
||||
|
||||
if (numbers[number][4]) {
|
||||
graphics::ClockRenderer::drawVerticalSegment(display, segmentFiveX, segmentFiveY, segmentWidth, segmentHeight);
|
||||
}
|
||||
|
||||
if (numbers[number][5]) {
|
||||
graphics::ClockRenderer::drawVerticalSegment(display, segmentSixX, segmentSixY, segmentWidth, segmentHeight);
|
||||
}
|
||||
|
||||
if (numbers[number][6]) {
|
||||
graphics::ClockRenderer::drawHorizontalSegment(display, segmentSevenX, segmentSevenY, segmentWidth, segmentHeight);
|
||||
}
|
||||
}
|
||||
|
||||
void drawHorizontalSegment(OLEDDisplay *display, int x, int y, int width, int height)
|
||||
@@ -137,6 +147,42 @@ void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int heig
|
||||
display->fillTriangle(x, y + width, x + height - 1, y + width, x + halfHeight, y + width + halfHeight);
|
||||
}
|
||||
|
||||
/*
|
||||
void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode, float scale)
|
||||
{
|
||||
uint16_t segmentWidth = SEGMENT_WIDTH * scale;
|
||||
uint16_t segmentHeight = SEGMENT_HEIGHT * scale;
|
||||
|
||||
if (digitalMode) {
|
||||
uint16_t radius = (segmentWidth + (segmentHeight * 2) + 4) / 2;
|
||||
uint16_t centerX = (x + segmentHeight + 2) + (radius / 2);
|
||||
uint16_t centerY = (y + segmentHeight + 2) + (radius / 2);
|
||||
|
||||
display->drawCircle(centerX, centerY, radius);
|
||||
display->drawCircle(centerX, centerY, radius + 1);
|
||||
display->drawLine(centerX, centerY, centerX, centerY - radius + 3);
|
||||
display->drawLine(centerX, centerY, centerX + radius - 3, centerY);
|
||||
} else {
|
||||
uint16_t segmentOneX = x + segmentHeight + 2;
|
||||
uint16_t segmentOneY = y;
|
||||
|
||||
uint16_t segmentTwoX = segmentOneX + segmentWidth + 2;
|
||||
uint16_t segmentTwoY = segmentOneY + segmentHeight + 2;
|
||||
|
||||
uint16_t segmentThreeX = segmentOneX;
|
||||
uint16_t segmentThreeY = segmentTwoY + segmentWidth + 2;
|
||||
|
||||
uint16_t segmentFourX = x;
|
||||
uint16_t segmentFourY = y + segmentHeight + 2;
|
||||
|
||||
drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight);
|
||||
drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight);
|
||||
drawHorizontalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight);
|
||||
drawVerticalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight);
|
||||
}
|
||||
}
|
||||
*/
|
||||
// Draw a digital clock
|
||||
void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
display->clear();
|
||||
@@ -146,6 +192,7 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
|
||||
const char *titleStr = "";
|
||||
// === Header ===
|
||||
graphics::drawCommonHeader(display, x, y, titleStr, true, true);
|
||||
int line = 0;
|
||||
|
||||
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
|
||||
char timeString[16];
|
||||
@@ -221,9 +268,10 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
|
||||
scaleInitialized = true;
|
||||
}
|
||||
|
||||
// calculate hours:minutes string width
|
||||
size_t len = strlen(timeString);
|
||||
uint16_t timeStringWidth = len * 5;
|
||||
|
||||
// calculate hours:minutes string width
|
||||
uint16_t timeStringWidth = len * 5; // base spacing between characters
|
||||
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
char character = timeString[i];
|
||||
@@ -291,13 +339,19 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
const char *titleStr = "";
|
||||
// === Header ===
|
||||
graphics::drawCommonHeader(display, x, y, titleStr, true, true);
|
||||
int line = 0;
|
||||
|
||||
// clock face center coordinates
|
||||
int16_t centerX = display->getWidth() / 2;
|
||||
int16_t centerY = display->getHeight() / 2;
|
||||
|
||||
// clock face radius
|
||||
int16_t radius = (std::min(display->getWidth(), display->getHeight()) / 2) * 0.9;
|
||||
int16_t radius = 0;
|
||||
if (display->getHeight() < display->getWidth()) {
|
||||
radius = (display->getHeight() / 2) * 0.9;
|
||||
} else {
|
||||
radius = (display->getWidth() / 2) * 0.9;
|
||||
}
|
||||
#ifdef T_WATCH_S3
|
||||
radius = (display->getWidth() / 2) * 0.8;
|
||||
#endif
|
||||
@@ -312,8 +366,17 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
// tick mark outer y coordinate; (first nested circle)
|
||||
int16_t tickMarkOuterNoonY = secondHandNoonY;
|
||||
|
||||
double secondsTickMarkInnerNoonY = noonY + (isHighResolution ? 8 : 4);
|
||||
double hoursTickMarkInnerNoonY = noonY + (isHighResolution ? 16 : 6);
|
||||
// seconds tick mark inner y coordinate; (second nested circle)
|
||||
double secondsTickMarkInnerNoonY = (double)noonY + 4;
|
||||
if (isHighResolution) {
|
||||
secondsTickMarkInnerNoonY = (double)noonY + 8;
|
||||
}
|
||||
|
||||
// hours tick mark inner y coordinate; (third nested circle)
|
||||
double hoursTickMarkInnerNoonY = (double)noonY + 6;
|
||||
if (isHighResolution) {
|
||||
hoursTickMarkInnerNoonY = (double)noonY + 16;
|
||||
}
|
||||
|
||||
// minute hand y coordinate
|
||||
int16_t minuteHandNoonY = secondsTickMarkInnerNoonY + 4;
|
||||
@@ -333,11 +396,17 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
|
||||
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
|
||||
if (rtc_sec > 0) {
|
||||
int hour, minute, second;
|
||||
decomposeTime(rtc_sec, hour, minute, second);
|
||||
long hms = rtc_sec % SEC_PER_DAY;
|
||||
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
|
||||
|
||||
// Tear apart hms into h:m:s
|
||||
int hour = hms / SEC_PER_HOUR;
|
||||
int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||||
int second = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN
|
||||
|
||||
bool isPM = hour >= 12;
|
||||
if (config.display.use_12h_clock) {
|
||||
bool isPM = hour >= 12;
|
||||
isPM = hour >= 12;
|
||||
display->setFont(FONT_SMALL);
|
||||
int yOffset = isHighResolution ? 1 : 0;
|
||||
#ifdef USE_EINK
|
||||
|
||||
@@ -301,8 +301,9 @@ void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
||||
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
|
||||
|
||||
// Tear apart hms into h:m:s
|
||||
int hour, min, sec;
|
||||
graphics::decomposeTime(rtc_sec, hour, min, sec);
|
||||
int hour = hms / SEC_PER_HOUR;
|
||||
int min = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||||
int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN
|
||||
|
||||
char timebuf[12];
|
||||
|
||||
@@ -706,22 +707,10 @@ void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int1
|
||||
int iconY = (SCREEN_HEIGHT - chirpy_height) / 2;
|
||||
int textX_offset = 10;
|
||||
if (isHighResolution) {
|
||||
iconX = SCREEN_WIDTH - chirpy_width_hirez - (chirpy_width_hirez / 3);
|
||||
iconY = (SCREEN_HEIGHT - chirpy_height_hirez) / 2;
|
||||
textX_offset = textX_offset * 4;
|
||||
const int scale = 2;
|
||||
const int bytesPerRow = (chirpy_width + 7) / 8;
|
||||
|
||||
for (int yy = 0; yy < chirpy_height; ++yy) {
|
||||
iconX = SCREEN_WIDTH - (chirpy_width * 2) - ((chirpy_width * 2) / 3);
|
||||
iconY = (SCREEN_HEIGHT - (chirpy_height * 2)) / 2;
|
||||
const uint8_t *rowPtr = chirpy + yy * bytesPerRow;
|
||||
for (int xx = 0; xx < chirpy_width; ++xx) {
|
||||
const uint8_t byteVal = pgm_read_byte(rowPtr + (xx >> 3));
|
||||
const uint8_t bitMask = 1U << (xx & 7); // XBM is LSB-first
|
||||
if (byteVal & bitMask) {
|
||||
display->fillRect(iconX + xx * scale, iconY + yy * scale, scale, scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
display->drawXbm(iconX, iconY, chirpy_width_hirez, chirpy_height_hirez, chirpy_hirez);
|
||||
} else {
|
||||
display->drawXbm(iconX, iconY, chirpy_width, chirpy_height, chirpy);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "graphics/draw/CompassRenderer.h"
|
||||
#include "graphics/draw/DebugRenderer.h"
|
||||
#include "graphics/draw/NodeListRenderer.h"
|
||||
#include "graphics/draw/ScreenRenderer.h"
|
||||
#include "graphics/draw/UIRenderer.h"
|
||||
|
||||
namespace graphics
|
||||
@@ -29,6 +30,8 @@ using namespace ClockRenderer;
|
||||
using namespace CompassRenderer;
|
||||
using namespace DebugRenderer;
|
||||
using namespace NodeListRenderer;
|
||||
using namespace ScreenRenderer;
|
||||
using namespace UIRenderer;
|
||||
|
||||
} // namespace DrawRenderers
|
||||
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
#include "configuration.h"
|
||||
#if HAS_SCREEN
|
||||
#include "ClockRenderer.h"
|
||||
#include "Default.h"
|
||||
#include "GPS.h"
|
||||
#include "MenuHandler.h"
|
||||
#include "MeshRadio.h"
|
||||
#include "MeshService.h"
|
||||
#include "MessageStore.h"
|
||||
#include "NodeDB.h"
|
||||
#include "buzz.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/draw/MessageRenderer.h"
|
||||
#include "graphics/draw/UIRenderer.h"
|
||||
#include "input/RotaryEncoderInterruptImpl1.h"
|
||||
#include "input/UpDownInterruptImpl1.h"
|
||||
@@ -410,35 +407,27 @@ void menuHandler::clockMenu()
|
||||
};
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
void menuHandler::messageResponseMenu()
|
||||
{
|
||||
enum optionsNumbers { Back = 0, ViewMode, DeleteAll, DeleteOldest, ReplyMenu, Aloud, enumEnd };
|
||||
enum optionsNumbers { Back = 0, Dismiss = 1, Preset = 2, Freetext = 3, Aloud = 4, enumEnd = 5 };
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
static const char *optionsArray[enumEnd] = {"Back", "Dismiss", "Reply Preset"};
|
||||
#else
|
||||
static const char *optionsArray[enumEnd] = {"Back", "Dismiss", "Reply via Preset"};
|
||||
#endif
|
||||
static int optionsEnumArray[enumEnd] = {Back, Dismiss, Preset};
|
||||
int options = 3;
|
||||
|
||||
static const char *optionsArray[enumEnd];
|
||||
static int optionsEnumArray[enumEnd];
|
||||
int options = 0;
|
||||
|
||||
auto mode = graphics::MessageRenderer::getThreadMode();
|
||||
|
||||
optionsArray[options] = "Back";
|
||||
optionsEnumArray[options++] = Back;
|
||||
|
||||
// New Reply submenu (replaces Preset and Freetext directly in this menu)
|
||||
optionsArray[options] = "Reply";
|
||||
optionsEnumArray[options++] = ReplyMenu;
|
||||
|
||||
optionsArray[options] = "View Chats";
|
||||
optionsEnumArray[options++] = ViewMode;
|
||||
|
||||
// Delete submenu
|
||||
optionsArray[options] = "Delete";
|
||||
optionsEnumArray[options++] = 900;
|
||||
if (kb_found) {
|
||||
optionsArray[options] = "Reply via Freetext";
|
||||
optionsEnumArray[options++] = Freetext;
|
||||
}
|
||||
|
||||
#ifdef HAS_I2S
|
||||
optionsArray[options] = "Read Aloud";
|
||||
optionsEnumArray[options++] = Aloud;
|
||||
#endif
|
||||
|
||||
BannerOverlayOptions bannerOptions;
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
bannerOptions.message = "Message";
|
||||
@@ -449,376 +438,29 @@ void menuHandler::messageResponseMenu()
|
||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||
bannerOptions.optionsCount = options;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
LOG_DEBUG("messageResponseMenu: selected %d", selected);
|
||||
|
||||
auto mode = graphics::MessageRenderer::getThreadMode();
|
||||
int ch = graphics::MessageRenderer::getThreadChannel();
|
||||
uint32_t peer = graphics::MessageRenderer::getThreadPeer();
|
||||
|
||||
LOG_DEBUG("[ReplyCtx] mode=%d ch=%d peer=0x%08x", (int)mode, ch, (unsigned int)peer);
|
||||
|
||||
if (selected == ViewMode) {
|
||||
menuHandler::menuQueue = menuHandler::message_viewmode_menu;
|
||||
screen->runNow();
|
||||
|
||||
// Reply submenu
|
||||
} else if (selected == ReplyMenu) {
|
||||
menuHandler::menuQueue = menuHandler::reply_menu;
|
||||
screen->runNow();
|
||||
|
||||
// Delete submenu
|
||||
} else if (selected == 900) {
|
||||
menuHandler::menuQueue = menuHandler::delete_messages_menu;
|
||||
screen->runNow();
|
||||
|
||||
// Delete oldest FIRST (only change)
|
||||
} else if (selected == DeleteOldest) {
|
||||
auto mode = graphics::MessageRenderer::getThreadMode();
|
||||
int ch = graphics::MessageRenderer::getThreadChannel();
|
||||
uint32_t peer = graphics::MessageRenderer::getThreadPeer();
|
||||
|
||||
if (mode == graphics::MessageRenderer::ThreadMode::ALL) {
|
||||
// Global oldest
|
||||
messageStore.deleteOldestMessage();
|
||||
} else if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
|
||||
// Oldest in current channel
|
||||
messageStore.deleteOldestMessageInChannel(ch);
|
||||
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
|
||||
// Oldest in current DM
|
||||
messageStore.deleteOldestMessageWithPeer(peer);
|
||||
if (selected == Dismiss) {
|
||||
screen->hideCurrentFrame();
|
||||
} else if (selected == Preset) {
|
||||
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
|
||||
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
|
||||
} else {
|
||||
cannedMessageModule->LaunchWithDestination(devicestate.rx_text_message.from);
|
||||
}
|
||||
|
||||
// Delete all messages
|
||||
} else if (selected == DeleteAll) {
|
||||
messageStore.clearAllMessages();
|
||||
graphics::MessageRenderer::clearThreadRegistries();
|
||||
graphics::MessageRenderer::clearMessageCache();
|
||||
|
||||
} else if (selected == Freetext) {
|
||||
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
|
||||
cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
|
||||
} else {
|
||||
cannedMessageModule->LaunchFreetextWithDestination(devicestate.rx_text_message.from);
|
||||
}
|
||||
}
|
||||
#ifdef HAS_I2S
|
||||
} else if (selected == Aloud) {
|
||||
else if (selected == Aloud) {
|
||||
const meshtastic_MeshPacket &mp = devicestate.rx_text_message;
|
||||
const char *msg = reinterpret_cast<const char *>(mp.decoded.payload.bytes);
|
||||
|
||||
audioThread->readAloud(msg);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
};
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
void menuHandler::replyMenu()
|
||||
{
|
||||
enum replyOptions { Back = 0, ReplyPreset, ReplyFreetext, enumEnd };
|
||||
|
||||
static const char *optionsArray[enumEnd];
|
||||
static int optionsEnumArray[enumEnd];
|
||||
int options = 0;
|
||||
|
||||
// Back
|
||||
optionsArray[options] = "Back";
|
||||
optionsEnumArray[options++] = Back;
|
||||
|
||||
// Preset reply
|
||||
optionsArray[options] = "With Preset";
|
||||
optionsEnumArray[options++] = ReplyPreset;
|
||||
|
||||
// Freetext reply (only when keyboard exists)
|
||||
if (kb_found) {
|
||||
optionsArray[options] = "With Freetext";
|
||||
optionsEnumArray[options++] = ReplyFreetext;
|
||||
}
|
||||
|
||||
BannerOverlayOptions bannerOptions;
|
||||
|
||||
// Dynamic title based on thread mode
|
||||
auto mode = graphics::MessageRenderer::getThreadMode();
|
||||
if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
|
||||
bannerOptions.message = "Reply to Channel";
|
||||
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
|
||||
bannerOptions.message = "Reply to DM";
|
||||
} else {
|
||||
// View All
|
||||
bannerOptions.message = "Reply to Last Msg";
|
||||
}
|
||||
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||
bannerOptions.optionsCount = options;
|
||||
bannerOptions.InitialSelected = 1;
|
||||
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
auto mode = graphics::MessageRenderer::getThreadMode();
|
||||
int ch = graphics::MessageRenderer::getThreadChannel();
|
||||
uint32_t peer = graphics::MessageRenderer::getThreadPeer();
|
||||
|
||||
if (selected == Back) {
|
||||
menuHandler::menuQueue = menuHandler::message_response_menu;
|
||||
screen->runNow();
|
||||
return;
|
||||
}
|
||||
|
||||
// Preset reply
|
||||
if (selected == ReplyPreset) {
|
||||
|
||||
if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
|
||||
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, ch);
|
||||
|
||||
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
|
||||
cannedMessageModule->LaunchWithDestination(peer);
|
||||
|
||||
} else {
|
||||
// Fallback for last received message
|
||||
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
|
||||
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
|
||||
} else {
|
||||
cannedMessageModule->LaunchWithDestination(devicestate.rx_text_message.from);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Freetext reply
|
||||
if (selected == ReplyFreetext) {
|
||||
|
||||
if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
|
||||
cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, ch);
|
||||
|
||||
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
|
||||
cannedMessageModule->LaunchFreetextWithDestination(peer);
|
||||
|
||||
} else {
|
||||
// Fallback for last received message
|
||||
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
|
||||
cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
|
||||
} else {
|
||||
cannedMessageModule->LaunchFreetextWithDestination(devicestate.rx_text_message.from);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
};
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
void menuHandler::deleteMessagesMenu()
|
||||
{
|
||||
enum optionsNumbers { Back = 0, DeleteOldest, DeleteThis, DeleteAll, enumEnd };
|
||||
|
||||
static const char *optionsArray[enumEnd];
|
||||
static int optionsEnumArray[enumEnd];
|
||||
int options = 0;
|
||||
|
||||
auto mode = graphics::MessageRenderer::getThreadMode();
|
||||
|
||||
optionsArray[options] = "Back";
|
||||
optionsEnumArray[options++] = Back;
|
||||
|
||||
optionsArray[options] = "Delete Oldest";
|
||||
optionsEnumArray[options++] = DeleteOldest;
|
||||
|
||||
// If viewing ALL chats → hide “Delete This Chat”
|
||||
if (mode != graphics::MessageRenderer::ThreadMode::ALL) {
|
||||
optionsArray[options] = "Delete This Chat";
|
||||
optionsEnumArray[options++] = DeleteThis;
|
||||
}
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
optionsArray[options] = "Delete All";
|
||||
#else
|
||||
optionsArray[options] = "Delete All Chats";
|
||||
#endif
|
||||
optionsEnumArray[options++] = DeleteAll;
|
||||
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Delete Messages";
|
||||
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||
bannerOptions.optionsCount = options;
|
||||
bannerOptions.bannerCallback = [mode](int selected) -> void {
|
||||
int ch = graphics::MessageRenderer::getThreadChannel();
|
||||
uint32_t peer = graphics::MessageRenderer::getThreadPeer();
|
||||
|
||||
if (selected == Back) {
|
||||
menuHandler::menuQueue = menuHandler::message_response_menu;
|
||||
screen->runNow();
|
||||
return;
|
||||
}
|
||||
|
||||
if (selected == DeleteAll) {
|
||||
LOG_INFO("Deleting all messages");
|
||||
messageStore.clearAllMessages();
|
||||
graphics::MessageRenderer::clearThreadRegistries();
|
||||
graphics::MessageRenderer::clearMessageCache();
|
||||
return;
|
||||
}
|
||||
|
||||
if (selected == DeleteOldest) {
|
||||
LOG_INFO("Deleting oldest message");
|
||||
|
||||
if (mode == graphics::MessageRenderer::ThreadMode::ALL) {
|
||||
messageStore.deleteOldestMessage();
|
||||
} else if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
|
||||
messageStore.deleteOldestMessageInChannel(ch);
|
||||
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
|
||||
messageStore.deleteOldestMessageWithPeer(peer);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// This only appears in non-ALL modes
|
||||
if (selected == DeleteThis) {
|
||||
LOG_INFO("Deleting all messages in this thread");
|
||||
|
||||
if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
|
||||
messageStore.deleteAllMessagesInChannel(ch);
|
||||
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
|
||||
messageStore.deleteAllMessagesWithPeer(peer);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
void menuHandler::messageViewModeMenu()
|
||||
{
|
||||
auto encodeChannelId = [](int ch) -> int { return 100 + ch; };
|
||||
auto isChannelSel = [](int id) -> bool { return id >= 100 && id < 200; };
|
||||
|
||||
static std::vector<std::string> labels;
|
||||
static std::vector<int> ids;
|
||||
static std::vector<uint32_t> idToPeer; // DM lookup
|
||||
|
||||
labels.clear();
|
||||
ids.clear();
|
||||
idToPeer.clear();
|
||||
|
||||
labels.push_back("Back");
|
||||
ids.push_back(-1);
|
||||
labels.push_back("View All Chats");
|
||||
ids.push_back(-2);
|
||||
|
||||
// Channels with messages
|
||||
for (int ch = 0; ch < 8; ++ch) {
|
||||
auto msgs = messageStore.getChannelMessages((uint8_t)ch);
|
||||
if (!msgs.empty()) {
|
||||
char buf[40];
|
||||
const char *cname = channels.getName(ch);
|
||||
snprintf(buf, sizeof(buf), cname && cname[0] ? "#%s" : "#Ch%d", cname ? cname : "", ch);
|
||||
labels.push_back(buf);
|
||||
ids.push_back(encodeChannelId(ch));
|
||||
LOG_DEBUG("messageViewModeMenu: Added live channel %s (id=%d)", buf, encodeChannelId(ch));
|
||||
}
|
||||
}
|
||||
|
||||
// Registry channels
|
||||
for (int ch : graphics::MessageRenderer::getSeenChannels()) {
|
||||
if (ch < 0 || ch >= 8)
|
||||
continue;
|
||||
auto msgs = messageStore.getChannelMessages((uint8_t)ch);
|
||||
if (msgs.empty())
|
||||
continue;
|
||||
int enc = encodeChannelId(ch);
|
||||
if (std::find(ids.begin(), ids.end(), enc) == ids.end()) {
|
||||
char buf[40];
|
||||
const char *cname = channels.getName(ch);
|
||||
snprintf(buf, sizeof(buf), cname && cname[0] ? "#%s" : "#Ch%d", cname ? cname : "", ch);
|
||||
labels.push_back(buf);
|
||||
ids.push_back(enc);
|
||||
LOG_DEBUG("messageViewModeMenu: Added registry channel %s (id=%d)", buf, enc);
|
||||
}
|
||||
}
|
||||
|
||||
// Gather unique peers
|
||||
auto dms = messageStore.getDirectMessages();
|
||||
std::vector<uint32_t> uniquePeers;
|
||||
for (auto &m : dms) {
|
||||
uint32_t peer = (m.sender == nodeDB->getNodeNum()) ? m.dest : m.sender;
|
||||
if (peer != nodeDB->getNodeNum() && std::find(uniquePeers.begin(), uniquePeers.end(), peer) == uniquePeers.end())
|
||||
uniquePeers.push_back(peer);
|
||||
}
|
||||
for (uint32_t peer : graphics::MessageRenderer::getSeenPeers()) {
|
||||
if (peer != nodeDB->getNodeNum() && std::find(uniquePeers.begin(), uniquePeers.end(), peer) == uniquePeers.end())
|
||||
uniquePeers.push_back(peer);
|
||||
}
|
||||
std::sort(uniquePeers.begin(), uniquePeers.end());
|
||||
|
||||
// Encode peers
|
||||
for (size_t i = 0; i < uniquePeers.size(); ++i) {
|
||||
uint32_t peer = uniquePeers[i];
|
||||
auto node = nodeDB->getMeshNode(peer);
|
||||
std::string name;
|
||||
if (node && node->has_user)
|
||||
name = sanitizeString(node->user.long_name).substr(0, 15);
|
||||
else {
|
||||
char buf[20];
|
||||
snprintf(buf, sizeof(buf), "Node %08X", peer);
|
||||
name = buf;
|
||||
}
|
||||
labels.push_back("@" + name);
|
||||
int encPeer = 1000 + (int)idToPeer.size();
|
||||
ids.push_back(encPeer);
|
||||
idToPeer.push_back(peer);
|
||||
LOG_DEBUG("messageViewModeMenu: Added DM %s peer=0x%08x id=%d", name.c_str(), (unsigned int)peer, encPeer);
|
||||
}
|
||||
|
||||
// Active ID
|
||||
int activeId = -2;
|
||||
auto mode = graphics::MessageRenderer::getThreadMode();
|
||||
if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL)
|
||||
activeId = encodeChannelId(graphics::MessageRenderer::getThreadChannel());
|
||||
else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
|
||||
uint32_t cur = graphics::MessageRenderer::getThreadPeer();
|
||||
for (size_t i = 0; i < idToPeer.size(); ++i)
|
||||
if (idToPeer[i] == cur) {
|
||||
activeId = 1000 + (int)i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
LOG_DEBUG("messageViewModeMenu: Active thread id=%d", activeId);
|
||||
|
||||
// Build banner
|
||||
static std::vector<const char *> options;
|
||||
static std::vector<int> optionIds;
|
||||
options.clear();
|
||||
optionIds.clear();
|
||||
|
||||
int initialIndex = 0;
|
||||
for (size_t i = 0; i < labels.size(); i++) {
|
||||
options.push_back(labels[i].c_str());
|
||||
optionIds.push_back(ids[i]);
|
||||
if (ids[i] == activeId)
|
||||
initialIndex = (int)i;
|
||||
}
|
||||
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Select Conversation";
|
||||
bannerOptions.optionsArrayPtr = options.data();
|
||||
bannerOptions.optionsEnumPtr = optionIds.data();
|
||||
bannerOptions.optionsCount = options.size();
|
||||
bannerOptions.InitialSelected = initialIndex;
|
||||
|
||||
bannerOptions.bannerCallback = [=](int selected) -> void {
|
||||
LOG_DEBUG("messageViewModeMenu: selected=%d", selected);
|
||||
if (selected == -1) {
|
||||
menuHandler::menuQueue = menuHandler::message_response_menu;
|
||||
screen->runNow();
|
||||
} else if (selected == -2) {
|
||||
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::ALL);
|
||||
} else if (isChannelSel(selected)) {
|
||||
int ch = selected - 100;
|
||||
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::CHANNEL, ch);
|
||||
} else if (selected >= 1000) {
|
||||
int idx = selected - 1000;
|
||||
if (idx >= 0 && (size_t)idx < idToPeer.size()) {
|
||||
uint32_t peer = idToPeer[idx];
|
||||
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::DIRECT, -1, peer);
|
||||
}
|
||||
}
|
||||
};
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
@@ -844,6 +486,16 @@ void menuHandler::homeBaseMenu()
|
||||
optionsArray[options] = "Send Node Info";
|
||||
}
|
||||
optionsEnumArray[options++] = Position;
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
optionsArray[options] = "New Preset";
|
||||
#else
|
||||
optionsArray[options] = "New Preset Msg";
|
||||
#endif
|
||||
optionsEnumArray[options++] = Preset;
|
||||
if (kb_found) {
|
||||
optionsArray[options] = "New Freetext Msg";
|
||||
optionsEnumArray[options++] = Freetext;
|
||||
}
|
||||
|
||||
BannerOverlayOptions bannerOptions;
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
@@ -999,37 +651,19 @@ void menuHandler::systemBaseMenu()
|
||||
|
||||
void menuHandler::favoriteBaseMenu()
|
||||
{
|
||||
enum optionsNumbers { Back, Preset, Freetext, GoToChat, Remove, TraceRoute, enumEnd };
|
||||
|
||||
static const char *optionsArray[enumEnd] = {"Back"};
|
||||
static int optionsEnumArray[enumEnd] = {Back};
|
||||
int options = 1;
|
||||
|
||||
// Only show "View Conversation" if a message exists with this node
|
||||
uint32_t peer = graphics::UIRenderer::currentFavoriteNodeNum;
|
||||
bool hasConversation = false;
|
||||
for (const auto &m : messageStore.getMessages()) {
|
||||
if ((m.sender == peer || m.dest == peer)) {
|
||||
hasConversation = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hasConversation) {
|
||||
optionsArray[options] = "Go To Chat";
|
||||
optionsEnumArray[options++] = GoToChat;
|
||||
}
|
||||
enum optionsNumbers { Back, Preset, Freetext, Remove, TraceRoute, enumEnd };
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
optionsArray[options] = "New Preset";
|
||||
static const char *optionsArray[enumEnd] = {"Back", "New Preset"};
|
||||
#else
|
||||
optionsArray[options] = "New Preset Msg";
|
||||
static const char *optionsArray[enumEnd] = {"Back", "New Preset Msg"};
|
||||
#endif
|
||||
optionsEnumArray[options++] = Preset;
|
||||
static int optionsEnumArray[enumEnd] = {Back, Preset};
|
||||
int options = 2;
|
||||
|
||||
if (kb_found) {
|
||||
optionsArray[options] = "New Freetext Msg";
|
||||
optionsEnumArray[options++] = Freetext;
|
||||
}
|
||||
|
||||
#if !defined(M5STACK_UNITC6L)
|
||||
optionsArray[options] = "Trace Route";
|
||||
optionsEnumArray[options++] = TraceRoute;
|
||||
@@ -1051,17 +685,6 @@ void menuHandler::favoriteBaseMenu()
|
||||
cannedMessageModule->LaunchWithDestination(graphics::UIRenderer::currentFavoriteNodeNum);
|
||||
} else if (selected == Freetext) {
|
||||
cannedMessageModule->LaunchFreetextWithDestination(graphics::UIRenderer::currentFavoriteNodeNum);
|
||||
}
|
||||
// Handle new Go To Thread action
|
||||
else if (selected == GoToChat) {
|
||||
// Switch thread to direct conversation with this node
|
||||
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::DIRECT, -1,
|
||||
graphics::UIRenderer::currentFavoriteNodeNum);
|
||||
|
||||
// Manually create and send a UIFrameEvent to trigger the jump
|
||||
UIFrameEvent evt;
|
||||
evt.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE;
|
||||
screen->handleUIFrameEvent(&evt);
|
||||
} else if (selected == Remove) {
|
||||
menuHandler::menuQueue = menuHandler::remove_favorite;
|
||||
screen->runNow();
|
||||
@@ -1111,31 +734,20 @@ void menuHandler::positionBaseMenu()
|
||||
|
||||
void menuHandler::nodeListMenu()
|
||||
{
|
||||
enum optionsNumbers { Back, Favorite, TraceRoute, Verify, Reset, NodeNameLength, enumEnd };
|
||||
static const char *optionsArray[enumEnd] = {"Back"};
|
||||
static int optionsEnumArray[enumEnd] = {Back};
|
||||
int options = 1;
|
||||
|
||||
optionsArray[options] = "Add Favorite";
|
||||
optionsEnumArray[options++] = Favorite;
|
||||
optionsArray[options] = "Trace Route";
|
||||
optionsEnumArray[options++] = TraceRoute;
|
||||
|
||||
#if !defined(M5STACK_UNITC6L)
|
||||
optionsArray[options] = "Key Verification";
|
||||
optionsEnumArray[options++] = Verify;
|
||||
enum optionsNumbers { Back, Favorite, TraceRoute, Verify, Reset, enumEnd };
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
static const char *optionsArray[] = {"Back", "Add Favorite", "Reset Node"};
|
||||
#else
|
||||
static const char *optionsArray[] = {"Back", "Add Favorite", "Trace Route", "Key Verification", "Reset NodeDB"};
|
||||
#endif
|
||||
|
||||
optionsArray[options] = "Show Long/Short Name";
|
||||
optionsEnumArray[options++] = NodeNameLength;
|
||||
optionsArray[options] = "Reset NodeDB";
|
||||
optionsEnumArray[options++] = Reset;
|
||||
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Node Action";
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsCount = options;
|
||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
bannerOptions.optionsCount = 3;
|
||||
#else
|
||||
bannerOptions.optionsCount = 5;
|
||||
#endif
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == Favorite) {
|
||||
menuQueue = add_favorite;
|
||||
@@ -1149,9 +761,6 @@ void menuHandler::nodeListMenu()
|
||||
} else if (selected == TraceRoute) {
|
||||
menuQueue = trace_route_menu;
|
||||
screen->runNow();
|
||||
} else if (selected == NodeNameLength) {
|
||||
menuHandler::menuQueue = menuHandler::node_name_length_menu;
|
||||
screen->runNow();
|
||||
}
|
||||
};
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
@@ -1175,7 +784,7 @@ void menuHandler::nodeNameLengthMenu()
|
||||
LOG_INFO("Setting names to short");
|
||||
config.display.use_long_node_name = false;
|
||||
} else if (selected == Back) {
|
||||
menuQueue = node_base_menu;
|
||||
menuQueue = screen_options_menu;
|
||||
screen->runNow();
|
||||
}
|
||||
};
|
||||
@@ -1203,9 +812,6 @@ void menuHandler::resetNodeDBMenu()
|
||||
LOG_INFO("Initiate node-db reset but keeping favorites");
|
||||
nodeDB->resetNodes(1);
|
||||
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
||||
} else if (selected == 0) {
|
||||
menuQueue = node_base_menu;
|
||||
screen->runNow();
|
||||
}
|
||||
};
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
@@ -1564,7 +1170,6 @@ void menuHandler::rebootMenu()
|
||||
if (selected == 1) {
|
||||
IF_SCREEN(screen->showSimpleBanner("Rebooting...", 0));
|
||||
nodeDB->saveToDisk();
|
||||
messageStore.saveToFlash();
|
||||
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
|
||||
} else {
|
||||
menuQueue = power_menu;
|
||||
@@ -1769,11 +1374,16 @@ void menuHandler::screenOptionsMenu()
|
||||
hasSupportBrightness = false;
|
||||
#endif
|
||||
|
||||
enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits };
|
||||
enum optionsNumbers { Back, NodeNameLength, Brightness, ScreenColor, FrameToggles, DisplayUnits };
|
||||
static const char *optionsArray[5] = {"Back"};
|
||||
static int optionsEnumArray[5] = {Back};
|
||||
int options = 1;
|
||||
|
||||
#if defined(T_DECK) || defined(T_LORA_PAGER) || defined(HACKADAY_COMMUNICATOR)
|
||||
optionsArray[options] = "Show Long/Short Name";
|
||||
optionsEnumArray[options++] = NodeNameLength;
|
||||
#endif
|
||||
|
||||
// Only show brightness for B&W displays
|
||||
if (hasSupportBrightness) {
|
||||
optionsArray[options] = "Brightness";
|
||||
@@ -1787,7 +1397,7 @@ void menuHandler::screenOptionsMenu()
|
||||
optionsEnumArray[options++] = ScreenColor;
|
||||
#endif
|
||||
|
||||
optionsArray[options] = "Frame Visibility";
|
||||
optionsArray[options] = "Frame Visibility Toggle";
|
||||
optionsEnumArray[options++] = FrameToggles;
|
||||
|
||||
optionsArray[options] = "Display Units";
|
||||
@@ -1805,6 +1415,9 @@ void menuHandler::screenOptionsMenu()
|
||||
} else if (selected == ScreenColor) {
|
||||
menuHandler::menuQueue = menuHandler::tftcolormenupicker;
|
||||
screen->runNow();
|
||||
} else if (selected == NodeNameLength) {
|
||||
menuHandler::menuQueue = menuHandler::node_name_length_menu;
|
||||
screen->runNow();
|
||||
} else if (selected == FrameToggles) {
|
||||
menuHandler::menuQueue = menuHandler::FrameToggles;
|
||||
screen->runNow();
|
||||
@@ -1900,8 +1513,7 @@ void menuHandler::FrameToggles_menu()
|
||||
{
|
||||
enum optionsNumbers {
|
||||
Finish,
|
||||
nodelist_nodes,
|
||||
nodelist_location,
|
||||
nodelist,
|
||||
nodelist_lastheard,
|
||||
nodelist_hopsignal,
|
||||
nodelist_distance,
|
||||
@@ -1922,25 +1534,20 @@ void menuHandler::FrameToggles_menu()
|
||||
static int lastSelectedIndex = 0;
|
||||
|
||||
#ifndef USE_EINK
|
||||
optionsArray[options] = screen->isFrameHidden("nodelist_nodes") ? "Show Node Lists" : "Hide Node Lists";
|
||||
optionsEnumArray[options++] = nodelist_nodes;
|
||||
#else
|
||||
optionsArray[options] = screen->isFrameHidden("nodelist") ? "Show Node List" : "Hide Node List";
|
||||
optionsEnumArray[options++] = nodelist;
|
||||
#endif
|
||||
#ifdef USE_EINK
|
||||
optionsArray[options] = screen->isFrameHidden("nodelist_lastheard") ? "Show NL - Last Heard" : "Hide NL - Last Heard";
|
||||
optionsEnumArray[options++] = nodelist_lastheard;
|
||||
optionsArray[options] = screen->isFrameHidden("nodelist_hopsignal") ? "Show NL - Hops/Signal" : "Hide NL - Hops/Signal";
|
||||
optionsEnumArray[options++] = nodelist_hopsignal;
|
||||
#endif
|
||||
|
||||
#if HAS_GPS
|
||||
#ifndef USE_EINK
|
||||
optionsArray[options] = screen->isFrameHidden("nodelist_location") ? "Show Position Lists" : "Hide Position Lists";
|
||||
optionsEnumArray[options++] = nodelist_location;
|
||||
#else
|
||||
optionsArray[options] = screen->isFrameHidden("nodelist_distance") ? "Show NL - Distance" : "Hide NL - Distance";
|
||||
optionsEnumArray[options++] = nodelist_distance;
|
||||
optionsArray[options] = screen->isFrameHidden("nodelist_bearings") ? "Show NL - Bearings" : "Hide NL - Bearings";
|
||||
optionsEnumArray[options++] = nodelist_bearings;
|
||||
#endif
|
||||
#if HAS_GPS
|
||||
optionsArray[options] = screen->isFrameHidden("nodelist_bearings") ? "Show Bearings" : "Hide Bearings";
|
||||
optionsEnumArray[options++] = nodelist_bearings;
|
||||
|
||||
optionsArray[options] = screen->isFrameHidden("gps") ? "Show Position" : "Hide Position";
|
||||
optionsEnumArray[options++] = gps;
|
||||
@@ -1979,12 +1586,8 @@ void menuHandler::FrameToggles_menu()
|
||||
|
||||
if (selected == Finish) {
|
||||
screen->setFrames(Screen::FOCUS_DEFAULT);
|
||||
} else if (selected == nodelist_nodes) {
|
||||
screen->toggleFrameVisibility("nodelist_nodes");
|
||||
menuHandler::menuQueue = menuHandler::FrameToggles;
|
||||
screen->runNow();
|
||||
} else if (selected == nodelist_location) {
|
||||
screen->toggleFrameVisibility("nodelist_location");
|
||||
} else if (selected == nodelist) {
|
||||
screen->toggleFrameVisibility("nodelist");
|
||||
menuHandler::menuQueue = menuHandler::FrameToggles;
|
||||
screen->runNow();
|
||||
} else if (selected == nodelist_lastheard) {
|
||||
@@ -2100,9 +1703,6 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
|
||||
case position_base_menu:
|
||||
positionBaseMenu();
|
||||
break;
|
||||
case node_base_menu:
|
||||
nodeListMenu();
|
||||
break;
|
||||
#if !MESHTASTIC_EXCLUDE_GPS
|
||||
case gps_toggle_menu:
|
||||
GPSToggleMenu();
|
||||
@@ -2183,18 +1783,6 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
|
||||
case throttle_message:
|
||||
screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000);
|
||||
break;
|
||||
case message_response_menu:
|
||||
messageResponseMenu();
|
||||
break;
|
||||
case reply_menu:
|
||||
replyMenu();
|
||||
break;
|
||||
case delete_messages_menu:
|
||||
deleteMessagesMenu();
|
||||
break;
|
||||
case message_viewmode_menu:
|
||||
messageViewModeMenu();
|
||||
break;
|
||||
}
|
||||
menuQueue = menu_none;
|
||||
}
|
||||
@@ -2206,4 +1794,4 @@ void menuHandler::saveUIConfig()
|
||||
|
||||
} // namespace graphics
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -19,7 +19,6 @@ class menuHandler
|
||||
clock_face_picker,
|
||||
clock_menu,
|
||||
position_base_menu,
|
||||
node_base_menu,
|
||||
gps_toggle_menu,
|
||||
gps_format_menu,
|
||||
compass_point_north_menu,
|
||||
@@ -44,10 +43,6 @@ class menuHandler
|
||||
key_verification_final_prompt,
|
||||
trace_route_menu,
|
||||
throttle_message,
|
||||
message_response_menu,
|
||||
message_viewmode_menu,
|
||||
reply_menu,
|
||||
delete_messages_menu,
|
||||
node_name_length_menu,
|
||||
FrameToggles,
|
||||
DisplayUnits
|
||||
@@ -66,9 +61,6 @@ class menuHandler
|
||||
static void TwelveHourPicker();
|
||||
static void ClockFacePicker();
|
||||
static void messageResponseMenu();
|
||||
static void messageViewModeMenu();
|
||||
static void replyMenu();
|
||||
static void deleteMessagesMenu();
|
||||
static void homeBaseMenu();
|
||||
static void textMessageBaseMenu();
|
||||
static void systemBaseMenu();
|
||||
@@ -108,4 +100,4 @@ class menuHandler
|
||||
};
|
||||
|
||||
} // namespace graphics
|
||||
#endif
|
||||
#endif
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,7 @@
|
||||
#pragma once
|
||||
#include "MessageStore.h" // for StoredMessage
|
||||
#if HAS_SCREEN
|
||||
#include "OLEDDisplay.h"
|
||||
#include "OLEDDisplayUi.h"
|
||||
#include "graphics/emotes.h"
|
||||
#include "mesh/generated/meshtastic/mesh.pb.h" // for meshtastic_MeshPacket
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -14,27 +10,6 @@ namespace graphics
|
||||
namespace MessageRenderer
|
||||
{
|
||||
|
||||
// Thread filter modes
|
||||
enum class ThreadMode { ALL, CHANNEL, DIRECT };
|
||||
|
||||
// Setter for switching thread mode
|
||||
void setThreadMode(ThreadMode mode, int channel = -1, uint32_t peer = 0);
|
||||
|
||||
// Getter for current mode
|
||||
ThreadMode getThreadMode();
|
||||
|
||||
// Getter for current channel (valid if mode == CHANNEL)
|
||||
int getThreadChannel();
|
||||
|
||||
// Getter for current peer (valid if mode == DIRECT)
|
||||
uint32_t getThreadPeer();
|
||||
|
||||
// Registry accessors for menuHandler
|
||||
const std::vector<int> &getSeenChannels();
|
||||
const std::vector<uint32_t> &getSeenPeers();
|
||||
|
||||
void clearThreadRegistries();
|
||||
|
||||
// Text and emote rendering
|
||||
void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount);
|
||||
|
||||
@@ -45,27 +20,11 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
std::vector<std::string> generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth);
|
||||
|
||||
// Function to calculate heights for each line
|
||||
std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, const Emote *emotes,
|
||||
const std::vector<bool> &isHeaderVec);
|
||||
std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, const Emote *emotes);
|
||||
|
||||
// Reset scroll state when new messages arrive
|
||||
void resetScrollState();
|
||||
|
||||
// Manual scroll control for encoder-style inputs
|
||||
void nudgeScroll(int8_t direction);
|
||||
|
||||
// Helper to auto-select the correct thread mode from a message
|
||||
void setThreadFor(const StoredMessage &sm, const meshtastic_MeshPacket &packet);
|
||||
|
||||
// Handles a new incoming/outgoing message: banner, wake, thread select, scroll reset
|
||||
void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const meshtastic_MeshPacket &packet);
|
||||
|
||||
// Clear Message Line Cache from Message Renderer
|
||||
void clearMessageCache();
|
||||
|
||||
void scrollUp();
|
||||
void scrollDown();
|
||||
// Function to render the message content
|
||||
void renderMessageContent(OLEDDisplay *display, const std::vector<std::string> &lines, const std::vector<int> &rowHeights, int x,
|
||||
int yOffset, int scrollBottom, const Emote *emotes, int numEmotes, bool isInverted, bool isBold);
|
||||
|
||||
} // namespace MessageRenderer
|
||||
} // namespace graphics
|
||||
#endif
|
||||
@@ -46,99 +46,69 @@ void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *
|
||||
}
|
||||
|
||||
// Static variables for dynamic cycling
|
||||
static ListMode_Node currentMode_Nodes = MODE_LAST_HEARD;
|
||||
static ListMode_Location currentMode_Location = MODE_DISTANCE;
|
||||
static NodeListMode currentMode = MODE_LAST_HEARD;
|
||||
static int scrollIndex = 0;
|
||||
// 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
|
||||
// =============================
|
||||
|
||||
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
|
||||
nodeName[0] = '\0';
|
||||
|
||||
auto writeFallbackId = [&] {
|
||||
std::snprintf(nodeName, sizeof(nodeName), "(%04X)", static_cast<uint16_t>(node ? (node->num & 0xFFFF) : 0));
|
||||
};
|
||||
|
||||
// 1) Choose target candidate (long vs short) only if present
|
||||
const char *raw = nullptr;
|
||||
if (node && node->has_user) {
|
||||
raw = config.display.use_long_node_name ? node->user.long_name : node->user.short_name;
|
||||
}
|
||||
|
||||
// 2) Sanitize (empty if raw is null/empty)
|
||||
std::string s = (raw && *raw) ? sanitizeString(raw) : std::string{};
|
||||
|
||||
// 3) Fallback if sanitize yields empty; otherwise copy safely (truncate if needed)
|
||||
if (s.empty() || s == "¿" || s.find_first_not_of("¿") == std::string::npos) {
|
||||
writeFallbackId();
|
||||
const char *name = NULL;
|
||||
static char nodeName[16] = "?";
|
||||
if (config.display.use_long_node_name == true) {
|
||||
if (node->has_user && strlen(node->user.long_name) > 0) {
|
||||
name = node->user.long_name;
|
||||
} else {
|
||||
snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF));
|
||||
}
|
||||
} else {
|
||||
// %.*s ensures null-termination and safe truncation to buffer size - 1
|
||||
std::snprintf(nodeName, sizeof(nodeName), "%.*s", static_cast<int>(sizeof(nodeName) - 1), s.c_str());
|
||||
if (node->has_user && strlen(node->user.short_name) > 0) {
|
||||
name = node->user.short_name;
|
||||
} else {
|
||||
snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF));
|
||||
}
|
||||
}
|
||||
|
||||
// 4) Width-based truncation + ellipsis (long-name mode only)
|
||||
if (config.display.use_long_node_name && display) {
|
||||
int availWidth = columnWidth - (isHighResolution ? 65 : 38);
|
||||
// Use sanitizeString() function and copy directly into nodeName
|
||||
std::string sanitized_name = sanitizeString(name ? name : "");
|
||||
|
||||
if (!sanitized_name.empty()) {
|
||||
strncpy(nodeName, sanitized_name.c_str(), sizeof(nodeName) - 1);
|
||||
nodeName[sizeof(nodeName) - 1] = '\0';
|
||||
} else {
|
||||
snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF));
|
||||
}
|
||||
|
||||
if (config.display.use_long_node_name == true) {
|
||||
int availWidth = (SCREEN_WIDTH / 2) - 65;
|
||||
if (availWidth < 0)
|
||||
availWidth = 0;
|
||||
|
||||
const size_t beforeLen = std::strlen(nodeName);
|
||||
|
||||
// Trim from the end until it fits or is empty
|
||||
size_t len = beforeLen;
|
||||
while (len && display->getStringWidth(nodeName) > availWidth) {
|
||||
nodeName[--len] = '\0';
|
||||
size_t origLen = strlen(nodeName);
|
||||
while (nodeName[0] && display->getStringWidth(nodeName) > availWidth) {
|
||||
nodeName[strlen(nodeName) - 1] = '\0';
|
||||
}
|
||||
|
||||
// If truncated, append "..." (respect buffer size)
|
||||
if (len < beforeLen) {
|
||||
// Make sure there's room for "..." and '\0'
|
||||
const size_t capForText = sizeof(nodeName) - 1; // leaving space for '\0'
|
||||
const size_t needed = 3; // "..."
|
||||
if (len > capForText - needed) {
|
||||
len = capForText - needed;
|
||||
nodeName[len] = '\0';
|
||||
// If we actually truncated, append "..." (ensure space remains in buffer)
|
||||
if (strlen(nodeName) < origLen) {
|
||||
size_t len = strlen(nodeName);
|
||||
size_t maxLen = sizeof(nodeName) - 4; // 3 for "..." and 1 for '\0'
|
||||
if (len > maxLen) {
|
||||
nodeName[maxLen] = '\0';
|
||||
len = maxLen;
|
||||
}
|
||||
std::strcat(nodeName, "...");
|
||||
strcat(nodeName, "...");
|
||||
}
|
||||
}
|
||||
|
||||
return nodeName;
|
||||
}
|
||||
|
||||
const char *getCurrentModeTitle_Nodes(int screenWidth)
|
||||
const char *getCurrentModeTitle(int screenWidth)
|
||||
{
|
||||
switch (currentMode_Nodes) {
|
||||
switch (currentMode) {
|
||||
case MODE_LAST_HEARD:
|
||||
return "Last Heard";
|
||||
case MODE_HOP_SIGNAL:
|
||||
@@ -147,18 +117,8 @@ const char *getCurrentModeTitle_Nodes(int screenWidth)
|
||||
#else
|
||||
return (isHighResolution) ? "Hops/Signal" : "Hops/Sig";
|
||||
#endif
|
||||
default:
|
||||
return "Nodes";
|
||||
}
|
||||
}
|
||||
|
||||
const char *getCurrentModeTitle_Location(int screenWidth)
|
||||
{
|
||||
switch (currentMode_Location) {
|
||||
case MODE_DISTANCE:
|
||||
return "Distance";
|
||||
case MODE_BEARING:
|
||||
return "Bearings";
|
||||
default:
|
||||
return "Nodes";
|
||||
}
|
||||
@@ -177,8 +137,10 @@ int calculateMaxScroll(int totalEntries, int visibleRows)
|
||||
|
||||
void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd)
|
||||
{
|
||||
int columnWidth = display->getWidth() / 2;
|
||||
int separatorX = x + columnWidth - 2;
|
||||
for (int y = yStart; y <= yEnd; y += 2) {
|
||||
display->setPixel(x, y);
|
||||
display->setPixel(separatorX, y);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,8 +152,7 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries,
|
||||
int scrollbarX = display->getWidth() - 2;
|
||||
int scrollbarHeight = display->getHeight() - scrollStartY - 10;
|
||||
int thumbHeight = std::max(4, (scrollbarHeight * visibleNodeRows * columns) / totalEntries);
|
||||
int perPage = visibleNodeRows * columns;
|
||||
int maxScroll = std::max(0, (totalEntries - 1) / perPage);
|
||||
int maxScroll = calculateMaxScroll(totalEntries, visibleNodeRows);
|
||||
int thumbY = scrollStartY + (scrollIndex * (scrollbarHeight - thumbHeight)) / std::max(1, maxScroll);
|
||||
|
||||
for (int i = 0; i < thumbHeight; i++) {
|
||||
@@ -208,7 +169,7 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
||||
bool isLeftCol = (x < SCREEN_WIDTH / 2);
|
||||
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];
|
||||
uint32_t seconds = sinceLastSeen(node);
|
||||
@@ -253,7 +214,7 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
||||
|
||||
int barsXOffset = columnWidth - barsOffset;
|
||||
|
||||
const char *nodeName = getSafeNodeName(display, node, columnWidth);
|
||||
const char *nodeName = getSafeNodeName(display, node);
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
@@ -297,7 +258,7 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
bool isLeftCol = (x < SCREEN_WIDTH / 2);
|
||||
int nameMaxWidth = columnWidth - (isHighResolution ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
|
||||
|
||||
const char *nodeName = getSafeNodeName(display, node, columnWidth);
|
||||
const char *nodeName = getSafeNodeName(display, node);
|
||||
char distStr[10] = "";
|
||||
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
@@ -368,15 +329,18 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
}
|
||||
}
|
||||
|
||||
void drawEntryDynamic_Nodes(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
|
||||
void drawEntryDynamic(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
|
||||
{
|
||||
switch (currentMode_Nodes) {
|
||||
switch (currentMode) {
|
||||
case MODE_LAST_HEARD:
|
||||
drawEntryLastHeard(display, node, x, y, columnWidth);
|
||||
break;
|
||||
case MODE_HOP_SIGNAL:
|
||||
drawEntryHopSignal(display, node, x, y, columnWidth);
|
||||
break;
|
||||
case MODE_DISTANCE:
|
||||
drawNodeDistance(display, node, x, y, columnWidth);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -389,7 +353,7 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
// Adjust max text width depending on column and screen width
|
||||
int nameMaxWidth = columnWidth - (isHighResolution ? (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->setFont(FONT_SMALL);
|
||||
@@ -467,6 +431,11 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
||||
locationScreen = true;
|
||||
else if (strcmp(title, "Distance") == 0)
|
||||
locationScreen = true;
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
int columnWidth = display->getWidth();
|
||||
#else
|
||||
int columnWidth = display->getWidth() / 2;
|
||||
#endif
|
||||
display->clear();
|
||||
|
||||
// Draw the battery/time header
|
||||
@@ -475,74 +444,39 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
||||
// Space below header
|
||||
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 totalRowsAvailable = (display->getHeight() - y) / rowYOffset;
|
||||
int numskipped = 0;
|
||||
int visibleNodeRows = totalRowsAvailable;
|
||||
|
||||
// Build filtered + ordered list
|
||||
std::vector<int> drawList;
|
||||
drawList.reserve(totalEntries);
|
||||
for (int i = 0; i < totalEntries; i++) {
|
||||
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;
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
int totalColumns = 1;
|
||||
#else
|
||||
int totalColumns = 2;
|
||||
#endif
|
||||
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 yOffset = 0;
|
||||
int col = 0;
|
||||
int lastNodeY = y;
|
||||
int shownCount = 0;
|
||||
int rowCount = 0;
|
||||
|
||||
for (int idx = startIndex; idx < endIndex; idx++) {
|
||||
uint32_t nodeNum = drawList[idx];
|
||||
auto *node = nodeDB->getMeshNode(nodeNum);
|
||||
for (int i = startIndex; i < endIndex; ++i) {
|
||||
if (locationScreen && !nodeDB->getMeshNodeByIndex(i)->has_position) {
|
||||
numskipped++;
|
||||
continue;
|
||||
}
|
||||
int xPos = x + (col * columnWidth);
|
||||
int yPos = y + yOffset;
|
||||
renderer(display, nodeDB->getMeshNodeByIndex(i), xPos, yPos, columnWidth);
|
||||
|
||||
renderer(display, node, xPos, yPos, columnWidth);
|
||||
|
||||
if (extras)
|
||||
extras(display, node, xPos, yPos, columnWidth, heading, lat, lon);
|
||||
if (extras) {
|
||||
extras(display, nodeDB->getMeshNodeByIndex(i), xPos, yPos, columnWidth, heading, lat, lon);
|
||||
}
|
||||
|
||||
lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL);
|
||||
yOffset += rowYOffset;
|
||||
@@ -565,71 +499,13 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
||||
// Draw column separator
|
||||
if (shownCount > 0) {
|
||||
const int firstNodeY = y + 3;
|
||||
for (int horizontal_offset = 1; horizontal_offset < totalColumns; horizontal_offset++) {
|
||||
drawColumnSeparator(display, columnWidth * horizontal_offset, firstNodeY, lastNodeY);
|
||||
}
|
||||
drawColumnSeparator(display, x, firstNodeY, lastNodeY);
|
||||
}
|
||||
|
||||
#endif
|
||||
const int scrollStartY = y + 3;
|
||||
drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, totalColumns, scrollStartY);
|
||||
drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, scrollStartY);
|
||||
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
|
||||
// Node list for Last Heard and Hop Signal views
|
||||
void drawDynamicListScreen_Nodes(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
// Static variables to track mode and duration
|
||||
static ListMode_Node lastRenderedMode = MODE_COUNT_NODE;
|
||||
static NodeListMode lastRenderedMode = MODE_COUNT;
|
||||
static unsigned long modeStartTime = 0;
|
||||
|
||||
unsigned long now = millis();
|
||||
@@ -654,65 +529,23 @@ void drawDynamicListScreen_Nodes(OLEDDisplay *display, OLEDDisplayUiState *state
|
||||
}
|
||||
#endif
|
||||
// On very first call (on boot or state enter)
|
||||
if (lastRenderedMode == MODE_COUNT_NODE) {
|
||||
currentMode_Nodes = MODE_LAST_HEARD;
|
||||
if (lastRenderedMode == MODE_COUNT) {
|
||||
currentMode = MODE_LAST_HEARD;
|
||||
modeStartTime = now;
|
||||
}
|
||||
|
||||
// Time to switch to next mode?
|
||||
if (now - modeStartTime >= getModeCycleIntervalMs()) {
|
||||
currentMode_Nodes = static_cast<ListMode_Node>((currentMode_Nodes + 1) % MODE_COUNT_NODE);
|
||||
currentMode = static_cast<NodeListMode>((currentMode + 1) % MODE_COUNT);
|
||||
modeStartTime = now;
|
||||
}
|
||||
|
||||
// Render screen based on currentMode
|
||||
const char *title = getCurrentModeTitle_Nodes(display->getWidth());
|
||||
drawNodeListScreen(display, state, x, y, title, drawEntryDynamic_Nodes);
|
||||
const char *title = getCurrentModeTitle(display->getWidth());
|
||||
drawNodeListScreen(display, state, x, y, title, drawEntryDynamic);
|
||||
|
||||
// Track the last mode to avoid reinitializing modeStartTime
|
||||
lastRenderedMode = currentMode_Nodes;
|
||||
}
|
||||
|
||||
// Node list for Distance and Bearings views
|
||||
void drawDynamicListScreen_Location(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
// Static variables to track mode and duration
|
||||
static ListMode_Location lastRenderedMode = MODE_COUNT_LOCATION;
|
||||
static unsigned long modeStartTime = 0;
|
||||
|
||||
unsigned long now = millis();
|
||||
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
display->clear();
|
||||
if (now - lastSwitchTime >= 3000) {
|
||||
display->display();
|
||||
lastSwitchTime = now;
|
||||
}
|
||||
#endif
|
||||
// On very first call (on boot or state enter)
|
||||
if (lastRenderedMode == MODE_COUNT_LOCATION) {
|
||||
currentMode_Location = MODE_DISTANCE;
|
||||
modeStartTime = now;
|
||||
}
|
||||
|
||||
// Time to switch to next mode?
|
||||
if (now - modeStartTime >= getModeCycleIntervalMs()) {
|
||||
currentMode_Location = static_cast<ListMode_Location>((currentMode_Location + 1) % MODE_COUNT_LOCATION);
|
||||
modeStartTime = now;
|
||||
}
|
||||
|
||||
// Render screen based on currentMode
|
||||
const char *title = getCurrentModeTitle_Location(display->getWidth());
|
||||
|
||||
// Render screen based on currentMode_Location
|
||||
if (currentMode_Location == MODE_DISTANCE) {
|
||||
drawNodeListScreen(display, state, x, y, title, drawNodeDistance);
|
||||
} else if (currentMode_Location == MODE_BEARING) {
|
||||
drawNodeListWithCompasses(display, state, x, y);
|
||||
}
|
||||
|
||||
// Track the last mode to avoid reinitializing modeStartTime
|
||||
lastRenderedMode = currentMode_Location;
|
||||
lastRenderedMode = currentMode;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -733,12 +566,14 @@ void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_
|
||||
#endif
|
||||
drawNodeListScreen(display, state, x, y, title, drawEntryHopSignal);
|
||||
}
|
||||
|
||||
void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
const char *title = "Distance";
|
||||
drawNodeListScreen(display, state, x, y, title, drawNodeDistance);
|
||||
}
|
||||
#endif
|
||||
|
||||
void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
float heading = 0;
|
||||
|
||||
@@ -23,11 +23,8 @@ namespace NodeListRenderer
|
||||
typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int);
|
||||
typedef void (*NodeExtrasRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int, float, double, double);
|
||||
|
||||
// Node list mode enumeration for Last Heard and Hop Signal views
|
||||
enum ListMode_Node { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_COUNT_NODE = 2 };
|
||||
|
||||
// Node list mode enumeration for Distance and Bearings views
|
||||
enum ListMode_Location { MODE_DISTANCE = 0, MODE_BEARING = 1, MODE_COUNT_LOCATION = 2 };
|
||||
// Node list mode enumeration
|
||||
enum NodeListMode { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_DISTANCE = 2, MODE_COUNT = 3 };
|
||||
|
||||
// Main node list screen function
|
||||
void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title,
|
||||
@@ -38,7 +35,7 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
||||
void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
|
||||
void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
|
||||
void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
|
||||
void drawEntryDynamic_Nodes(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
|
||||
void drawEntryDynamic(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
|
||||
void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
|
||||
|
||||
// Extras renderers
|
||||
@@ -49,20 +46,14 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
void drawDynamicListScreen_Nodes(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
void drawDynamicListScreen_Location(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
// Utility functions
|
||||
const char *getCurrentModeTitle_Nodes(int screenWidth);
|
||||
const char *getCurrentModeTitle_Location(int screenWidth);
|
||||
const char *getSafeNodeName(meshtastic_NodeInfoLite *node, int columnWidth);
|
||||
const char *getCurrentModeTitle(int screenWidth);
|
||||
const char *getSafeNodeName(meshtastic_NodeInfoLite *node);
|
||||
void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields);
|
||||
|
||||
// Scrolling controls
|
||||
void scrollUp();
|
||||
void scrollDown();
|
||||
|
||||
// Bitmap drawing function
|
||||
void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *bitmapXBM, OLEDDisplay *display);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if HAS_SCREEN
|
||||
|
||||
#include "DisplayFormatters.h"
|
||||
#include "NodeDB.h"
|
||||
#include "NotificationRenderer.h"
|
||||
@@ -38,7 +38,7 @@ extern bool hasUnreadMessage;
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
int bannerSignalBars = -1;
|
||||
|
||||
InputEvent NotificationRenderer::inEvent;
|
||||
int8_t NotificationRenderer::curSelected = 0;
|
||||
char NotificationRenderer::alertBannerMessage[256] = {0};
|
||||
@@ -477,7 +477,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
|
||||
bool is_picker = false;
|
||||
uint16_t lineCount = 0;
|
||||
// Layout Configuration
|
||||
// === Layout Configuration ===
|
||||
constexpr uint16_t hPadding = 5;
|
||||
constexpr uint16_t vPadding = 2;
|
||||
bool needs_bell = false;
|
||||
@@ -491,32 +491,13 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
display->setFont(FONT_SMALL);
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
|
||||
// Track widest line INCLUDING bars (but don't change per-line widths)
|
||||
uint16_t widestLineWithBars = 0;
|
||||
|
||||
while (lines[lineCount] != nullptr) {
|
||||
auto newlinePointer = strchr(lines[lineCount], '\n');
|
||||
if (newlinePointer)
|
||||
lineLengths[lineCount] = (newlinePointer - lines[lineCount]); // Check for newlines first
|
||||
else // if the newline wasn't found, then pull string length from strlen
|
||||
lineLengths[lineCount] = strlen(lines[lineCount]);
|
||||
|
||||
lineWidths[lineCount] = display->getStringWidth(lines[lineCount], lineLengths[lineCount], true);
|
||||
|
||||
// Consider extra width for signal bars on lines that contain "Signal:"
|
||||
uint16_t potentialWidth = lineWidths[lineCount];
|
||||
if (graphics::bannerSignalBars >= 0 && strncmp(lines[lineCount], "Signal:", 7) == 0) {
|
||||
const int totalBars = 5;
|
||||
const int barWidth = 3;
|
||||
const int barSpacing = 2;
|
||||
const int gap = 6; // space between text and bars
|
||||
int barsWidth = totalBars * barWidth + (totalBars - 1) * barSpacing + gap;
|
||||
potentialWidth += barsWidth;
|
||||
}
|
||||
|
||||
if (potentialWidth > widestLineWithBars)
|
||||
widestLineWithBars = potentialWidth;
|
||||
|
||||
if (!is_picker) {
|
||||
needs_bell |= (strstr(alertBannerMessage, "Alert Received") != nullptr);
|
||||
if (lineWidths[lineCount] > maxWidth)
|
||||
@@ -526,12 +507,136 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
}
|
||||
// count lines
|
||||
|
||||
// Ensure box accounts for signal bars if present
|
||||
if (widestLineWithBars > maxWidth)
|
||||
maxWidth = widestLineWithBars;
|
||||
|
||||
uint16_t boxWidth = hPadding * 2 + maxWidth;
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
if (needs_bell) {
|
||||
if (isHighResolution && boxWidth <= 150)
|
||||
boxWidth += 26;
|
||||
if (!isHighResolution && boxWidth <= 100)
|
||||
boxWidth += 20;
|
||||
}
|
||||
|
||||
uint16_t screenHeight = display->height();
|
||||
uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3;
|
||||
uint8_t visibleTotalLines = std::min<uint8_t>(lineCount, (screenHeight - vPadding * 2) / effectiveLineHeight);
|
||||
uint16_t contentHeight = visibleTotalLines * effectiveLineHeight;
|
||||
uint16_t boxHeight = contentHeight + vPadding * 2;
|
||||
if (visibleTotalLines == 1)
|
||||
boxHeight += (isHighResolution ? 4 : 3);
|
||||
|
||||
int16_t boxLeft = (display->width() / 2) - (boxWidth / 2);
|
||||
if (totalLines > visibleTotalLines)
|
||||
boxWidth += (isHighResolution ? 4 : 2);
|
||||
int16_t boxTop = (display->height() / 2) - (boxHeight / 2);
|
||||
|
||||
if (visibleTotalLines == 1) {
|
||||
boxTop += 25;
|
||||
}
|
||||
if (alertBannerOptions < 3) {
|
||||
int missingLines = 3 - alertBannerOptions;
|
||||
int moveUp = missingLines * (effectiveLineHeight / 2);
|
||||
boxTop -= moveUp;
|
||||
if (boxTop < 0)
|
||||
boxTop = 0;
|
||||
}
|
||||
|
||||
// === Draw Box ===
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(boxLeft, boxTop, boxWidth, boxHeight);
|
||||
display->setColor(WHITE);
|
||||
display->drawRect(boxLeft, boxTop, boxWidth, boxHeight);
|
||||
display->fillRect(boxLeft, boxTop - 2, boxWidth, 1);
|
||||
display->fillRect(boxLeft - 2, boxTop, 1, boxHeight);
|
||||
display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight);
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(boxLeft, boxTop, 1, 1);
|
||||
display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1);
|
||||
display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1);
|
||||
display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1);
|
||||
display->setColor(WHITE);
|
||||
int16_t lineY = boxTop + vPadding;
|
||||
int swingRange = 8;
|
||||
static int swingOffset = 0;
|
||||
static bool swingRight = true;
|
||||
static unsigned long lastSwingTime = 0;
|
||||
unsigned long now = millis();
|
||||
int swingSpeedMs = 10 / (swingRange * 2);
|
||||
if (now - lastSwingTime >= (unsigned long)swingSpeedMs) {
|
||||
lastSwingTime = now;
|
||||
if (swingRight) {
|
||||
swingOffset++;
|
||||
if (swingOffset >= swingRange)
|
||||
swingRight = false;
|
||||
} else {
|
||||
swingOffset--;
|
||||
if (swingOffset <= 0)
|
||||
swingRight = true;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < lineCount; i++) {
|
||||
bool isTitle = (i == 0);
|
||||
int globalOptionIndex = (i - 1) + firstOptionToShow;
|
||||
bool isSelectedOption = (!isTitle && globalOptionIndex >= 0 && globalOptionIndex == curSelected);
|
||||
|
||||
uint16_t visibleWidth = 64 - hPadding * 2;
|
||||
if (totalLines > visibleTotalLines)
|
||||
visibleWidth -= 6;
|
||||
char lineBuffer[lineLengths[i] + 1];
|
||||
strncpy(lineBuffer, lines[i], lineLengths[i]);
|
||||
lineBuffer[lineLengths[i]] = '\0';
|
||||
|
||||
if (isTitle) {
|
||||
if (visibleTotalLines == 1) {
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(boxLeft, boxTop, boxWidth, effectiveLineHeight);
|
||||
display->setColor(WHITE);
|
||||
display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, boxTop, lineBuffer);
|
||||
} else {
|
||||
display->setColor(WHITE);
|
||||
display->fillRect(boxLeft, boxTop, boxWidth, effectiveLineHeight);
|
||||
display->setColor(BLACK);
|
||||
display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, boxTop, lineBuffer);
|
||||
display->setColor(WHITE);
|
||||
if (needs_bell) {
|
||||
int bellY = boxTop + (FONT_HEIGHT_SMALL - 8) / 2;
|
||||
display->drawXbm(boxLeft + (boxWidth - lineWidths[i]) / 2 - 10, bellY, 8, 8, bell_alert);
|
||||
display->drawXbm(boxLeft + (boxWidth + lineWidths[i]) / 2 + 2, bellY, 8, 8, bell_alert);
|
||||
}
|
||||
}
|
||||
lineY = boxTop + effectiveLineHeight + 1;
|
||||
} else if (isSelectedOption) {
|
||||
display->setColor(WHITE);
|
||||
display->fillRect(boxLeft, lineY, boxWidth, effectiveLineHeight);
|
||||
display->setColor(BLACK);
|
||||
if (lineLengths[i] > 15 && lineWidths[i] > visibleWidth) {
|
||||
int textX = boxLeft + hPadding + swingOffset;
|
||||
display->drawString(textX, lineY - 1, lineBuffer);
|
||||
} else {
|
||||
display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, lineY - 1, lineBuffer);
|
||||
}
|
||||
display->setColor(WHITE);
|
||||
lineY += effectiveLineHeight;
|
||||
} else {
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(boxLeft, lineY, boxWidth, effectiveLineHeight);
|
||||
display->setColor(WHITE);
|
||||
display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, lineY, lineBuffer);
|
||||
lineY += effectiveLineHeight;
|
||||
}
|
||||
}
|
||||
if (totalLines > visibleTotalLines) {
|
||||
const uint8_t scrollBarWidth = 5;
|
||||
int16_t scrollBarX = boxLeft + boxWidth - scrollBarWidth - 2;
|
||||
int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight;
|
||||
uint16_t scrollBarHeight = boxHeight - vPadding * 2 - effectiveLineHeight;
|
||||
float ratio = (float)visibleTotalLines / totalLines;
|
||||
uint16_t indicatorHeight = std::max((int)(scrollBarHeight * ratio), 4);
|
||||
float scrollRatio = (float)(firstOptionToShow + lineCount - visibleTotalLines) / (totalLines - visibleTotalLines);
|
||||
uint16_t indicatorY = scrollBarY + scrollRatio * (scrollBarHeight - indicatorHeight);
|
||||
display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight);
|
||||
display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight);
|
||||
}
|
||||
#else
|
||||
if (needs_bell) {
|
||||
if (isHighResolution && boxWidth <= 150)
|
||||
boxWidth += 26;
|
||||
@@ -553,21 +658,8 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
boxWidth += (isHighResolution) ? 4 : 2;
|
||||
}
|
||||
int16_t boxTop = (display->height() / 2) - (boxHeight / 2);
|
||||
boxHeight += (isHighResolution) ? 2 : 1;
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
if (visibleTotalLines == 1) {
|
||||
boxTop += 25;
|
||||
}
|
||||
if (alertBannerOptions < 3) {
|
||||
int missingLines = 3 - alertBannerOptions;
|
||||
int moveUp = missingLines * (effectiveLineHeight / 2);
|
||||
boxTop -= moveUp;
|
||||
if (boxTop < 0)
|
||||
boxTop = 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Draw Box
|
||||
// === Draw Box ===
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2);
|
||||
display->fillRect(boxLeft, boxTop - 2, boxWidth, 1);
|
||||
@@ -583,7 +675,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1);
|
||||
display->setColor(WHITE);
|
||||
|
||||
// Draw Content
|
||||
// === Draw Content ===
|
||||
int16_t lineY = boxTop + vPadding;
|
||||
for (int i = 0; i < lineCount; i++) {
|
||||
int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2;
|
||||
@@ -612,47 +704,17 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
lineY += (effectiveLineHeight - 2 - background_yOffset);
|
||||
} else {
|
||||
// Pop-up
|
||||
// If this is the Signal line, center text + bars as one group
|
||||
bool isSignalLine = (graphics::bannerSignalBars >= 0 && strstr(lineBuffer, "Signal:") != nullptr);
|
||||
if (isSignalLine) {
|
||||
const int totalBars = 5;
|
||||
const int barWidth = 3;
|
||||
const int barSpacing = 2;
|
||||
const int barHeightStep = 2;
|
||||
const int gap = 6;
|
||||
|
||||
int textWidth = display->getStringWidth(lineBuffer, strlen(lineBuffer), true);
|
||||
int barsWidth = totalBars * barWidth + (totalBars - 1) * barSpacing + gap;
|
||||
int totalWidth = textWidth + barsWidth;
|
||||
int groupStartX = boxLeft + (boxWidth - totalWidth) / 2;
|
||||
|
||||
display->drawString(groupStartX, lineY, lineBuffer);
|
||||
|
||||
int baseX = groupStartX + textWidth + gap;
|
||||
int baseY = lineY + effectiveLineHeight - 1;
|
||||
for (int b = 0; b < totalBars; b++) {
|
||||
int barHeight = (b + 1) * barHeightStep;
|
||||
int x = baseX + b * (barWidth + barSpacing);
|
||||
int y = baseY - barHeight;
|
||||
|
||||
if (b < graphics::bannerSignalBars) {
|
||||
display->fillRect(x, y, barWidth, barHeight);
|
||||
} else {
|
||||
display->drawRect(x, y, barWidth, barHeight);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
display->drawString(textX, lineY, lineBuffer);
|
||||
}
|
||||
display->drawString(textX, lineY, lineBuffer);
|
||||
lineY += (effectiveLineHeight);
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll Bar (Thicker, inside box, not over title)
|
||||
// === Scroll Bar (Thicker, inside box, not over title) ===
|
||||
if (totalLines > visibleTotalLines) {
|
||||
const uint8_t scrollBarWidth = 5;
|
||||
|
||||
int16_t scrollBarX = boxLeft + boxWidth - scrollBarWidth - 2;
|
||||
int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight;
|
||||
int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight; // start after title line
|
||||
uint16_t scrollBarHeight = boxHeight - vPadding * 2 - effectiveLineHeight;
|
||||
|
||||
float ratio = (float)visibleTotalLines / totalLines;
|
||||
@@ -663,6 +725,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight);
|
||||
display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Draw the last text message we received
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
#include "NodeListRenderer.h"
|
||||
#include "UIRenderer.h"
|
||||
#include "airtime.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/GeoCoord.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/TimeFormatters.h"
|
||||
#include "graphics/images.h"
|
||||
@@ -26,16 +29,6 @@ namespace graphics
|
||||
NodeNum UIRenderer::currentFavoriteNodeNum = 0;
|
||||
std::vector<meshtastic_NodeInfoLite *> graphics::UIRenderer::favoritedNodes;
|
||||
|
||||
static inline void drawSatelliteIcon(OLEDDisplay *display, int16_t x, int16_t y)
|
||||
{
|
||||
int yOffset = (isHighResolution) ? -5 : 1;
|
||||
if (isHighResolution) {
|
||||
NodeListRenderer::drawScaledXBitmap16x16(x, y + yOffset, imgSatellite_width, imgSatellite_height, imgSatellite, display);
|
||||
} else {
|
||||
display->drawXbm(x + 1, y + yOffset, imgSatellite_width, imgSatellite_height, imgSatellite);
|
||||
}
|
||||
}
|
||||
|
||||
void graphics::UIRenderer::rebuildFavoritedNodes()
|
||||
{
|
||||
favoritedNodes.clear();
|
||||
@@ -251,16 +244,16 @@ void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y,
|
||||
|
||||
// Draw nodes status
|
||||
void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, int node_offset,
|
||||
bool show_total, const char *additional_words)
|
||||
bool show_total, String additional_words)
|
||||
{
|
||||
char usersString[20];
|
||||
int nodes_online = (nodeStatus->getNumOnline() > 0) ? nodeStatus->getNumOnline() + node_offset : 0;
|
||||
|
||||
snprintf(usersString, sizeof(usersString), "%d %s", nodes_online, additional_words);
|
||||
snprintf(usersString, sizeof(usersString), "%d %s", nodes_online, additional_words.c_str());
|
||||
|
||||
if (show_total) {
|
||||
int nodes_total = (nodeStatus->getNumTotal() > 0) ? nodeStatus->getNumTotal() + node_offset : 0;
|
||||
snprintf(usersString, sizeof(usersString), "%d/%d %s", nodes_online, nodes_total, additional_words);
|
||||
snprintf(usersString, sizeof(usersString), "%d/%d %s", nodes_online, nodes_total, additional_words.c_str());
|
||||
}
|
||||
|
||||
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
|
||||
@@ -607,7 +600,14 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
|
||||
} else {
|
||||
displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off";
|
||||
}
|
||||
drawSatelliteIcon(display, x, getTextPositions(display)[line]);
|
||||
int yOffset = (isHighResolution) ? 3 : 1;
|
||||
if (isHighResolution) {
|
||||
NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width,
|
||||
imgSatellite_height, imgSatellite, display);
|
||||
} else {
|
||||
display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height,
|
||||
imgSatellite);
|
||||
}
|
||||
int xOffset = (isHighResolution) ? 6 : 0;
|
||||
display->drawString(x + 11 + xOffset, getTextPositions(display)[line], displayLine);
|
||||
} else {
|
||||
@@ -759,7 +759,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
|
||||
|
||||
// Start Functions to write date/time to the screen
|
||||
// Helper function to check if a year is a leap year
|
||||
constexpr bool isLeapYear(int year)
|
||||
bool isLeapYear(int year)
|
||||
{
|
||||
return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0));
|
||||
}
|
||||
@@ -990,7 +990,14 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
||||
} else {
|
||||
displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off";
|
||||
}
|
||||
drawSatelliteIcon(display, x, getTextPositions(display)[line]);
|
||||
int yOffset = (isHighResolution) ? 3 : 1;
|
||||
if (isHighResolution) {
|
||||
NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width,
|
||||
imgSatellite_height, imgSatellite, display);
|
||||
} else {
|
||||
display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height,
|
||||
imgSatellite);
|
||||
}
|
||||
int xOffset = (isHighResolution) ? 6 : 0;
|
||||
display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine);
|
||||
} else {
|
||||
|
||||
@@ -34,7 +34,7 @@ class UIRenderer
|
||||
public:
|
||||
// Common UI elements
|
||||
static void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus,
|
||||
int node_offset = 0, bool show_total = true, const char *additional_words = "");
|
||||
int node_offset = 0, bool show_total = true, String additional_words = "");
|
||||
|
||||
// GPS status functions
|
||||
static void drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
|
||||
@@ -43,6 +43,9 @@ class UIRenderer
|
||||
static void drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
|
||||
static void drawGpsPowerStatus(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
|
||||
|
||||
// Layout and utility functions
|
||||
static void drawScrollbar(OLEDDisplay *display, int visibleItems, int totalItems, int scrollIndex, int x, int startY);
|
||||
|
||||
// Overlay and special screens
|
||||
static void drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *text);
|
||||
|
||||
@@ -80,6 +83,8 @@ class UIRenderer
|
||||
static std::string drawTimeDelta(uint32_t days, uint32_t hours, uint32_t minutes, uint32_t seconds);
|
||||
static int formatDateTime(char *buffer, size_t bufferSize, uint32_t rtc_sec, OLEDDisplay *display, bool showTime);
|
||||
|
||||
// Message filtering
|
||||
static bool shouldDrawMessage(const meshtastic_MeshPacket *packet);
|
||||
// Check if the display can render a string (detect special chars; emoji)
|
||||
static bool haveGlyphs(const char *str);
|
||||
}; // namespace UIRenderer
|
||||
|
||||
@@ -304,6 +304,58 @@ const uint8_t chirpy[] = {
|
||||
0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01,
|
||||
0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0xfe, 0xff, 0xff, 0xff, 0xdf};
|
||||
|
||||
#define chirpy_width_hirez 76
|
||||
#define chirpy_height_hirez 100
|
||||
const uint8_t chirpy_hirez[] = {
|
||||
0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0x03,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00,
|
||||
0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc,
|
||||
0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03,
|
||||
0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xf0, 0xff, 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0,
|
||||
0xff, 0xf0, 0xff, 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f,
|
||||
0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0,
|
||||
0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff,
|
||||
0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f,
|
||||
0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0,
|
||||
0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff,
|
||||
0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00,
|
||||
0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc,
|
||||
0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03,
|
||||
0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0,
|
||||
0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f,
|
||||
0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0,
|
||||
0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xf0, 0xff,
|
||||
0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xf0, 0xff, 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff,
|
||||
0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f,
|
||||
0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc,
|
||||
0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03,
|
||||
0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00,
|
||||
0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0, 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00,
|
||||
0xc0, 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0, 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0,
|
||||
0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x0f, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00,
|
||||
0x0f, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x0f, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x0f,
|
||||
0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c,
|
||||
0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, 0x00,
|
||||
0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00,
|
||||
0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, 0xfc,
|
||||
0x03, 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03,
|
||||
0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00,
|
||||
0xf0, 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0, 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0,
|
||||
0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0, 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
0xff, 0xff, 0xf3, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3};
|
||||
|
||||
#define chirpy_small_image_width 8
|
||||
#define chirpy_small_image_height 8
|
||||
const uint8_t chirpy_small[] = {0x7f, 0x41, 0x55, 0x55, 0x55, 0x55, 0x41, 0x7f};
|
||||
|
||||
@@ -53,7 +53,6 @@ typedef struct _InputEvent {
|
||||
class InputPollable
|
||||
{
|
||||
public:
|
||||
virtual ~InputPollable() = default;
|
||||
virtual void pollOnce() = 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -3,9 +3,6 @@
|
||||
#include "RotaryEncoderImpl.h"
|
||||
#include "InputBroker.h"
|
||||
#include "RotaryEncoder.h"
|
||||
#ifdef ARCH_ESP32
|
||||
#include "sleep.h"
|
||||
#endif
|
||||
|
||||
#define ORIGIN_NAME "RotaryEncoder"
|
||||
|
||||
@@ -14,20 +11,6 @@ RotaryEncoderImpl *rotaryEncoderImpl;
|
||||
RotaryEncoderImpl::RotaryEncoderImpl()
|
||||
{
|
||||
rotary = nullptr;
|
||||
#ifdef ARCH_ESP32
|
||||
isFirstInit = true;
|
||||
#endif
|
||||
}
|
||||
|
||||
RotaryEncoderImpl::~RotaryEncoderImpl()
|
||||
{
|
||||
LOG_DEBUG("RotaryEncoderImpl destructor");
|
||||
detachRotaryEncoderInterrupts();
|
||||
|
||||
if (rotary != nullptr) {
|
||||
delete rotary;
|
||||
rotary = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool RotaryEncoderImpl::init()
|
||||
@@ -42,22 +25,15 @@ bool RotaryEncoderImpl::init()
|
||||
eventCcw = static_cast<input_broker_event>(moduleConfig.canned_message.inputbroker_event_ccw);
|
||||
eventPressed = static_cast<input_broker_event>(moduleConfig.canned_message.inputbroker_event_press);
|
||||
|
||||
if (rotary == nullptr) {
|
||||
rotary = new RotaryEncoder(moduleConfig.canned_message.inputbroker_pin_a, moduleConfig.canned_message.inputbroker_pin_b,
|
||||
moduleConfig.canned_message.inputbroker_pin_press);
|
||||
}
|
||||
rotary = new RotaryEncoder(moduleConfig.canned_message.inputbroker_pin_a, moduleConfig.canned_message.inputbroker_pin_b,
|
||||
moduleConfig.canned_message.inputbroker_pin_press);
|
||||
rotary->resetButton();
|
||||
|
||||
attachRotaryEncoderInterrupts();
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
// Register callbacks for before and after lightsleep
|
||||
// Used to detach and reattach interrupts
|
||||
if (isFirstInit) {
|
||||
lsObserver.observe(¬ifyLightSleep);
|
||||
lsEndObserver.observe(¬ifyLightSleepEnd);
|
||||
isFirstInit = false;
|
||||
}
|
||||
#endif
|
||||
interruptInstance = this;
|
||||
auto interruptHandler = []() { inputBroker->requestPollSoon(interruptInstance); };
|
||||
attachInterrupt(moduleConfig.canned_message.inputbroker_pin_a, interruptHandler, CHANGE);
|
||||
attachInterrupt(moduleConfig.canned_message.inputbroker_pin_b, interruptHandler, CHANGE);
|
||||
attachInterrupt(moduleConfig.canned_message.inputbroker_pin_press, interruptHandler, CHANGE);
|
||||
|
||||
LOG_INFO("RotaryEncoder initialized pins(%d, %d, %d), events(%d, %d, %d)", moduleConfig.canned_message.inputbroker_pin_a,
|
||||
moduleConfig.canned_message.inputbroker_pin_b, moduleConfig.canned_message.inputbroker_pin_press, eventCw, eventCcw,
|
||||
@@ -95,50 +71,6 @@ void RotaryEncoderImpl::pollOnce()
|
||||
}
|
||||
}
|
||||
|
||||
void RotaryEncoderImpl::detachRotaryEncoderInterrupts()
|
||||
{
|
||||
LOG_DEBUG("RotaryEncoderImpl detach button interrupts");
|
||||
if (interruptInstance == this) {
|
||||
detachInterrupt(moduleConfig.canned_message.inputbroker_pin_a);
|
||||
detachInterrupt(moduleConfig.canned_message.inputbroker_pin_b);
|
||||
detachInterrupt(moduleConfig.canned_message.inputbroker_pin_press);
|
||||
interruptInstance = nullptr;
|
||||
} else {
|
||||
LOG_WARN("RotaryEncoderImpl: interrupts already detached");
|
||||
}
|
||||
}
|
||||
|
||||
void RotaryEncoderImpl::attachRotaryEncoderInterrupts()
|
||||
{
|
||||
LOG_DEBUG("RotaryEncoderImpl attach button interrupts");
|
||||
if (rotary != nullptr && interruptInstance == nullptr) {
|
||||
rotary->resetButton();
|
||||
|
||||
interruptInstance = this;
|
||||
auto interruptHandler = []() { inputBroker->requestPollSoon(interruptInstance); };
|
||||
attachInterrupt(moduleConfig.canned_message.inputbroker_pin_a, interruptHandler, CHANGE);
|
||||
attachInterrupt(moduleConfig.canned_message.inputbroker_pin_b, interruptHandler, CHANGE);
|
||||
attachInterrupt(moduleConfig.canned_message.inputbroker_pin_press, interruptHandler, CHANGE);
|
||||
} else {
|
||||
LOG_WARN("RotaryEncoderImpl: interrupts already attached");
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
|
||||
int RotaryEncoderImpl::beforeLightSleep(void *unused)
|
||||
{
|
||||
detachRotaryEncoderInterrupts();
|
||||
return 0; // Indicates success;
|
||||
}
|
||||
|
||||
int RotaryEncoderImpl::afterLightSleep(esp_sleep_wakeup_cause_t cause)
|
||||
{
|
||||
attachRotaryEncoderInterrupts();
|
||||
return 0; // Indicates success;
|
||||
}
|
||||
#endif
|
||||
|
||||
RotaryEncoderImpl *RotaryEncoderImpl::interruptInstance;
|
||||
|
||||
#endif
|
||||
@@ -8,18 +8,12 @@
|
||||
|
||||
class RotaryEncoder;
|
||||
|
||||
class RotaryEncoderImpl final : public InputPollable
|
||||
class RotaryEncoderImpl : public InputPollable
|
||||
{
|
||||
public:
|
||||
RotaryEncoderImpl();
|
||||
~RotaryEncoderImpl() override;
|
||||
bool init();
|
||||
bool init(void);
|
||||
virtual void pollOnce() override;
|
||||
// Disconnect and reconnect interrupts for light sleep
|
||||
#ifdef ARCH_ESP32
|
||||
int beforeLightSleep(void *unused);
|
||||
int afterLightSleep(esp_sleep_wakeup_cause_t cause);
|
||||
#endif
|
||||
|
||||
protected:
|
||||
static RotaryEncoderImpl *interruptInstance;
|
||||
@@ -29,21 +23,6 @@ class RotaryEncoderImpl final : public InputPollable
|
||||
input_broker_event eventPressed = INPUT_BROKER_NONE;
|
||||
|
||||
RotaryEncoder *rotary;
|
||||
|
||||
private:
|
||||
#ifdef ARCH_ESP32
|
||||
bool isFirstInit;
|
||||
#endif
|
||||
void detachRotaryEncoderInterrupts();
|
||||
void attachRotaryEncoderInterrupts();
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
// Get notified when lightsleep begins and ends
|
||||
CallbackObserver<RotaryEncoderImpl, void *> lsObserver =
|
||||
CallbackObserver<RotaryEncoderImpl, void *>(this, &RotaryEncoderImpl::beforeLightSleep);
|
||||
CallbackObserver<RotaryEncoderImpl, esp_sleep_wakeup_cause_t> lsEndObserver =
|
||||
CallbackObserver<RotaryEncoderImpl, esp_sleep_wakeup_cause_t>(this, &RotaryEncoderImpl::afterLightSleep);
|
||||
#endif
|
||||
};
|
||||
|
||||
extern RotaryEncoderImpl *rotaryEncoderImpl;
|
||||
|
||||
@@ -489,6 +489,8 @@ int32_t KbI2cBase::runOnce()
|
||||
case 0x90: // fn+r INPUT_BROKER_MSG_REBOOT
|
||||
case 0x91: // fn+t
|
||||
case 0xac: // fn+m INPUT_BROKER_MSG_MUTE_TOGGLE
|
||||
|
||||
case 0x8b: // fn+del INPUT_BROKEN_MSG_DISMISS_FRAME
|
||||
case 0xAA: // fn+b INPUT_BROKER_MSG_BLUETOOTH_TOGGLE
|
||||
case 0x8F: // fn+e INPUT_BROKER_MSG_EMOTE_LIST
|
||||
// just pass those unmodified
|
||||
|
||||
@@ -428,10 +428,17 @@ void setup()
|
||||
#endif
|
||||
|
||||
#if ARCH_PORTDUINO
|
||||
RTCQuality ourQuality = RTCQualityDevice;
|
||||
|
||||
std::string timeCommandResult = exec("timedatectl status | grep synchronized | grep yes -c");
|
||||
if (timeCommandResult[0] == '1') {
|
||||
ourQuality = RTCQualityNTP;
|
||||
}
|
||||
|
||||
struct timeval tv;
|
||||
tv.tv_sec = time(NULL);
|
||||
tv.tv_usec = 0;
|
||||
perhapsSetRTC(RTCQualityDevice, &tv);
|
||||
perhapsSetRTC(ourQuality, &tv);
|
||||
#endif
|
||||
|
||||
powerMonInit();
|
||||
|
||||
@@ -96,6 +96,8 @@ class Channels
|
||||
|
||||
bool setDefaultPresetCryptoForHash(ChannelHash channelHash);
|
||||
|
||||
int16_t getHash(ChannelIndex i) { return hashes[i]; }
|
||||
|
||||
private:
|
||||
/** Given a channel index, change to use the crypto key specified by that index
|
||||
*
|
||||
@@ -113,8 +115,6 @@ class Channels
|
||||
*/
|
||||
int16_t generateHash(ChannelIndex channelNum);
|
||||
|
||||
int16_t getHash(ChannelIndex i) { return hashes[i]; }
|
||||
|
||||
/**
|
||||
* Validate a channel, fixing any errors as needed
|
||||
*/
|
||||
|
||||
@@ -44,7 +44,6 @@ struct UIFrameEvent {
|
||||
REDRAW_ONLY, // Don't change which frames are show, just redraw, asap
|
||||
REGENERATE_FRAMESET, // Regenerate (change? add? remove?) screen frames, honoring requestFocus()
|
||||
REGENERATE_FRAMESET_BACKGROUND, // Regenerate screen frames, Attempt to remain on the same frame throughout
|
||||
SWITCH_TO_TEXTMESSAGE // Jump directly to the Text Message screen
|
||||
} action = REDRAW_ONLY;
|
||||
|
||||
// We might want to pass additional data inside this struct at some point
|
||||
|
||||
@@ -7,12 +7,10 @@
|
||||
#include "../concurrency/Periodic.h"
|
||||
#include "BluetoothCommon.h" // needed for updateBatteryLevel, FIXME, eventually when we pull mesh out into a lib we shouldn't be whacking bluetooth from here
|
||||
#include "MeshService.h"
|
||||
#include "MessageStore.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "RTC.h"
|
||||
#include "TypeConversions.h"
|
||||
#include "graphics/draw/MessageRenderer.h"
|
||||
#include "main.h"
|
||||
#include "mesh-pb-constants.h"
|
||||
#include "meshUtils.h"
|
||||
@@ -194,16 +192,8 @@ void MeshService::handleToRadio(meshtastic_MeshPacket &p)
|
||||
p.id = generatePacketId(); // If the phone didn't supply one, then pick one
|
||||
|
||||
p.rx_time = getValidTime(RTCQualityFromNet); // Record the time the packet arrived from the phone
|
||||
// (so we update our nodedb for the local node)
|
||||
|
||||
#if HAS_SCREEN
|
||||
if (p.decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP && p.decoded.payload.size > 0 && p.to != NODENUM_BROADCAST &&
|
||||
p.to != 0) // DM only
|
||||
{
|
||||
perhapsDecode(&p);
|
||||
const StoredMessage &sm = messageStore.addFromPacket(p);
|
||||
graphics::MessageRenderer::handleNewMessage(nullptr, sm, p); // notify UI
|
||||
}
|
||||
#endif
|
||||
// Send the packet into the mesh
|
||||
DEBUG_HEAP_BEFORE;
|
||||
auto a = packetPool.allocCopy(p);
|
||||
|
||||
@@ -526,10 +526,6 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
||||
#elif ARCH_PORTDUINO
|
||||
if (portduino_config.traceFilename != "" || portduino_config.logoutputlevel == level_trace) {
|
||||
LOG_TRACE("%s", MeshPacketSerializer::JsonSerialize(p, false).c_str());
|
||||
} else if (portduino_config.JSONFilename != "") {
|
||||
if (portduino_config.JSONFilter == (_meshtastic_PortNum)0 || portduino_config.JSONFilter == p->decoded.portnum) {
|
||||
JSONFile << MeshPacketSerializer::JsonSerialize(p, false) << std::endl;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return DecodeState::DECODE_SUCCESS;
|
||||
@@ -692,7 +688,7 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
|
||||
|
||||
// Store a copy of encrypted packet for MQTT
|
||||
DEBUG_HEAP_BEFORE;
|
||||
meshtastic_MeshPacket *p_encrypted = packetPool.allocCopy(*p);
|
||||
p_encrypted = packetPool.allocCopy(*p);
|
||||
DEBUG_HEAP_AFTER("Router::handleReceived", p_encrypted);
|
||||
|
||||
// Take those raw bytes and convert them back into a well structured protobuf we can understand
|
||||
|
||||
@@ -91,6 +91,9 @@ class Router : protected concurrency::OSThread, protected PacketHistory
|
||||
before us */
|
||||
uint32_t rxDupe = 0, txRelayCanceled = 0;
|
||||
|
||||
// pointer to the encrypted packet
|
||||
meshtastic_MeshPacket *p_encrypted;
|
||||
|
||||
protected:
|
||||
friend class RoutingModule;
|
||||
|
||||
|
||||
@@ -24,6 +24,9 @@ PB_BIND(meshtastic_Data, meshtastic_Data, 2)
|
||||
PB_BIND(meshtastic_KeyVerification, meshtastic_KeyVerification, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_StoreForwardPlusPlus, meshtastic_StoreForwardPlusPlus, 2)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_Waypoint, meshtastic_Waypoint, AUTO)
|
||||
|
||||
|
||||
@@ -121,6 +124,8 @@ PB_BIND(meshtastic_ChunkedPayloadResponse, meshtastic_ChunkedPayloadResponse, AU
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -478,6 +478,17 @@ typedef enum _meshtastic_Routing_Error {
|
||||
meshtastic_Routing_Error_RATE_LIMIT_EXCEEDED = 38
|
||||
} 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.
|
||||
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.
|
||||
@@ -782,6 +793,24 @@ typedef struct _meshtastic_KeyVerification {
|
||||
meshtastic_KeyVerification_hash2_t hash2;
|
||||
} meshtastic_KeyVerification;
|
||||
|
||||
typedef PB_BYTES_ARRAY_T(32) meshtastic_StoreForwardPlusPlus_message_hash_t;
|
||||
typedef PB_BYTES_ARRAY_T(32) meshtastic_StoreForwardPlusPlus_chain_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_chain_hash_t chain_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 */
|
||||
typedef struct _meshtastic_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_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_MAX meshtastic_MeshPacket_Priority_MAX
|
||||
#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_StoreForwardPlusPlus_sfpp_message_type_ENUMTYPE meshtastic_StoreForwardPlusPlus_SFPP_message_type
|
||||
|
||||
|
||||
|
||||
#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_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_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_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}
|
||||
@@ -1411,6 +1447,7 @@ extern "C" {
|
||||
#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_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_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}
|
||||
@@ -1489,6 +1526,15 @@ extern "C" {
|
||||
#define meshtastic_KeyVerification_nonce_tag 1
|
||||
#define meshtastic_KeyVerification_hash1_tag 2
|
||||
#define meshtastic_KeyVerification_hash2_tag 3
|
||||
#define meshtastic_StoreForwardPlusPlus_sfpp_message_type_tag 1
|
||||
#define meshtastic_StoreForwardPlusPlus_message_hash_tag 2
|
||||
#define meshtastic_StoreForwardPlusPlus_chain_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_latitude_i_tag 2
|
||||
#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_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, chain_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) \
|
||||
X(a, STATIC, SINGULAR, UINT32, id, 1) \
|
||||
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_Data_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_MqttClientProxyMessage_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_Data_fields &meshtastic_Data_msg
|
||||
#define meshtastic_KeyVerification_fields &meshtastic_KeyVerification_msg
|
||||
#define meshtastic_StoreForwardPlusPlus_fields &meshtastic_StoreForwardPlusPlus_msg
|
||||
#define meshtastic_Waypoint_fields &meshtastic_Waypoint_msg
|
||||
#define meshtastic_MqttClientProxyMessage_fields &meshtastic_MqttClientProxyMessage_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_RouteDiscovery_size 256
|
||||
#define meshtastic_Routing_size 259
|
||||
#define meshtastic_StoreForwardPlusPlus_size 371
|
||||
#define meshtastic_ToRadio_size 504
|
||||
#define meshtastic_User_size 115
|
||||
#define meshtastic_Waypoint_size 165
|
||||
|
||||
@@ -86,6 +86,9 @@ typedef enum _meshtastic_PortNum {
|
||||
/* Paxcounter lib included in the firmware
|
||||
ENCODING: protobuf */
|
||||
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.
|
||||
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.
|
||||
|
||||
@@ -417,9 +417,6 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
||||
}
|
||||
case meshtastic_AdminMessage_enter_dfu_mode_request_tag: {
|
||||
LOG_INFO("Client requesting to enter DFU mode");
|
||||
#if HAS_SCREEN
|
||||
IF_SCREEN(screen->showSimpleBanner("Device is rebooting\ninto DFU mode.", 0));
|
||||
#endif
|
||||
#if defined(ARCH_NRF52) || defined(ARCH_RP2040)
|
||||
enterDfuMode();
|
||||
#endif
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -75,6 +75,7 @@ class CannedMessageModule : public SinglePortModule, public Observable<const UIF
|
||||
// === State/UI ===
|
||||
bool shouldDraw();
|
||||
bool hasMessages();
|
||||
void showTemporaryMessage(const String &message);
|
||||
void resetSearch();
|
||||
void updateDestinationSelectionList();
|
||||
void drawDestinationSelectionScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
@@ -152,9 +153,10 @@ class CannedMessageModule : public SinglePortModule, public Observable<const UIF
|
||||
unsigned long lastUpdateMillis = 0;
|
||||
String searchQuery;
|
||||
String freetext;
|
||||
String temporaryMessage;
|
||||
|
||||
// === Message Storage ===
|
||||
char messageBuffer[CANNED_MESSAGE_MODULE_MESSAGES_SIZE + 1];
|
||||
char messageStore[CANNED_MESSAGE_MODULE_MESSAGES_SIZE + 1];
|
||||
char *messages[CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT];
|
||||
int messagesCount = 0;
|
||||
int currentMessageIndex = -1;
|
||||
@@ -165,11 +167,14 @@ class CannedMessageModule : public SinglePortModule, public Observable<const UIF
|
||||
NodeNum lastSentNode = 0; // Tracks the most recent node we sent a message to (for UI display)
|
||||
ChannelIndex channel = 0; // Channel index used when sending a message
|
||||
|
||||
bool ack = false; // True = ACK received, False = NACK or failed
|
||||
bool waitingForAck = false; // True if we're expecting an ACK and should monitor routing packets
|
||||
float lastRxSnr = 0; // SNR from last received ACK (used for diagnostics/UI)
|
||||
int32_t lastRxRssi = 0; // RSSI from last received ACK (used for diagnostics/UI)
|
||||
uint32_t lastRequestId = 0; // tracks the request_id of our last sent packet
|
||||
bool ack = false; // True = ACK received, False = NACK or failed
|
||||
bool waitingForAck = false; // True if we're expecting an ACK and should monitor routing packets
|
||||
bool lastAckWasRelayed = false; // True if the ACK was relayed through intermediate nodes
|
||||
uint8_t lastAckHopStart = 0; // Hop start value from the received ACK packet
|
||||
uint8_t lastAckHopLimit = 0; // Hop limit value from the received ACK packet
|
||||
|
||||
float lastRxSnr = 0; // SNR from last received ACK (used for diagnostics/UI)
|
||||
int32_t lastRxRssi = 0; // RSSI from last received ACK (used for diagnostics/UI)
|
||||
|
||||
// === State Tracking ===
|
||||
cannedMessageModuleRunState runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
||||
|
||||
@@ -60,7 +60,9 @@ meshtastic_MeshPacket *DropzoneModule::sendConditions()
|
||||
long hms = rtc_sec % SEC_PER_DAY;
|
||||
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
|
||||
|
||||
graphics::decomposeTime(rtc_sec, hour, min, sec);
|
||||
hour = hms / SEC_PER_HOUR;
|
||||
min = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||||
sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN;
|
||||
}
|
||||
|
||||
// Check if the dropzone is open or closed by reading the analog pin
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
#if ARCH_PORTDUINO
|
||||
#include "input/LinuxInputImpl.h"
|
||||
#include "input/SeesawRotary.h"
|
||||
#include "modules/Native/StoreForwardPlusPlus.h"
|
||||
#include "modules/Telemetry/HostMetrics.h"
|
||||
#if !MESHTASTIC_EXCLUDE_STOREFORWARD
|
||||
#include "modules/StoreForwardModule.h"
|
||||
@@ -243,6 +244,7 @@ void setupModules()
|
||||
#endif
|
||||
#if ARCH_PORTDUINO
|
||||
new HostMetricsModule();
|
||||
new StoreForwardPlusPlusModule();
|
||||
#endif
|
||||
#if HAS_TELEMETRY
|
||||
new DeviceTelemetryModule();
|
||||
|
||||
653
src/modules/Native/StoreForwardPlusPlus.cpp
Normal file
653
src/modules/Native/StoreForwardPlusPlus.cpp
Normal file
@@ -0,0 +1,653 @@
|
||||
// create second module for satellites?
|
||||
|
||||
// The central node will generate a 256 or 128 bit value as its seed. This is the value other nodes subscribe to, and serves as
|
||||
// the root of the chain
|
||||
|
||||
//
|
||||
|
||||
// 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
|
||||
|
||||
// first draft is just to save channel 0 in a git-style database
|
||||
|
||||
// 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"
|
||||
|
||||
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, \
|
||||
want_ack BOOL NOT NULL, \
|
||||
channel_hash INT NOT NULL, \
|
||||
encrypted_bytes BLOB NOT NULL, \
|
||||
message_hash BLOB NOT NULL, \
|
||||
rx_time INT 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);
|
||||
|
||||
// create table DMs
|
||||
res = sqlite3_exec(ppDb, " \
|
||||
CREATE TABLE direct_messages( \
|
||||
destination INT NOT NULL, \
|
||||
sender INT NOT NULL, \
|
||||
packet_id INT NOT NULL, \
|
||||
want_ack BOOL NOT NULL, \
|
||||
channel_hash INT NOT NULL, \
|
||||
commit_hash BLOB NOT NULL, \
|
||||
encrypted_bytes BLOB NOT NULL, \
|
||||
message_hash BLOB NOT NULL, \
|
||||
payload TEXT, \
|
||||
rx_time INT NOT NULL, \
|
||||
PRIMARY KEY (message_hash) \
|
||||
);",
|
||||
NULL, NULL, &err);
|
||||
LOG_WARN("Result2 %u", res);
|
||||
if (err != nullptr)
|
||||
;
|
||||
LOG_ERROR("%s", err);
|
||||
sqlite3_free(err);
|
||||
|
||||
// create table mappings
|
||||
// create table DMs
|
||||
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
|
||||
|
||||
std::string insert_statement = "INSERT INTO channel_messages (destination, sender, packet_id, want_ack, channel_hash, \
|
||||
encrypted_bytes, message_hash, rx_time, commit_hash, payload) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?);";
|
||||
sqlite3_prepare_v2(ppDb, insert_statement.c_str(), insert_statement.length(), &stmt, NULL);
|
||||
|
||||
encryptedOk = true;
|
||||
|
||||
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 60 * 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_chain_hash[32] = {0};
|
||||
uint8_t last_message_hash[32] = {0};
|
||||
LOG_WARN("here5");
|
||||
|
||||
if (!getChainEnd(hash, last_message_chain_hash, last_message_hash)) {
|
||||
LOG_WARN("Store and Forward++ database lookup returned null");
|
||||
return 60 * 60 * 1000;
|
||||
}
|
||||
|
||||
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, last_message_hash, 32);
|
||||
|
||||
// set chain hash
|
||||
storeforward.chain_hash.size = 32;
|
||||
memcpy(storeforward.chain_hash.bytes, last_message_chain_hash, 32);
|
||||
|
||||
// set root hash
|
||||
storeforward.root_hash.size = 32;
|
||||
memcpy(storeforward.root_hash.bytes, root_hash_bytes, 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);
|
||||
|
||||
return 60 * 60 * 1000;
|
||||
}
|
||||
|
||||
bool StoreForwardPlusPlusModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_StoreForwardPlusPlus *t)
|
||||
{
|
||||
LOG_WARN("in handleReceivedProtobuf");
|
||||
LOG_WARN("Sfp++ node %u is informing us of packet", mp.from);
|
||||
printBytes("chain_hash ", t->chain_hash.bytes, t->chain_hash.size);
|
||||
if (t->sfpp_message_type == meshtastic_StoreForwardPlusPlus_SFPP_message_type_CANON_ANNOUNCE) {
|
||||
// check chain_hash.size
|
||||
|
||||
if (portduino_config.sfpp_stratum0) {
|
||||
LOG_WARN("Received a CANON_ANNOUNCE while stratum 0");
|
||||
} 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 {
|
||||
// There's the possibility that
|
||||
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_chain_hash[32] = {0};
|
||||
uint8_t last_message_hash[32] = {0};
|
||||
|
||||
// get chain tip
|
||||
if (getChainEnd(router->p_encrypted->channel, last_message_chain_hash, last_message_hash)) {
|
||||
if (memcmp(last_message_chain_hash, t->chain_hash.bytes, 32) == 0) {
|
||||
LOG_WARN("End of chain matches!");
|
||||
} else
|
||||
("End of chain does not match!");
|
||||
} else {
|
||||
LOG_WARN("No Messages on this chain, request!");
|
||||
requestNextMessage(t->root_hash.bytes, t->root_hash.bytes);
|
||||
}
|
||||
|
||||
// compare to chain tip in incoming message
|
||||
|
||||
// if not found, request the next message
|
||||
}
|
||||
} else if (t->sfpp_message_type == meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_REQUEST) {
|
||||
uint8_t next_chain_hash[32] = {0};
|
||||
|
||||
LOG_WARN("Received link request");
|
||||
getNextHash(t->root_hash.bytes, t->chain_hash.bytes, next_chain_hash);
|
||||
printBytes("next chain hash: ", next_chain_hash, 32);
|
||||
|
||||
broadcastLink(next_chain_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!");
|
||||
ChannelHash _channel_hash = getChannelHashFromRoot(t->root_hash.bytes);
|
||||
|
||||
addToChain(t->encapsulated_to, t->encapsulated_from, t->encapsulated_id, false, _channel_hash, t->message.bytes,
|
||||
t->message.size, t->message_hash.bytes, t->chain_hash.bytes, t->root_hash.bytes, t->encapsulated_rxtime, "",
|
||||
0);
|
||||
}
|
||||
|
||||
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; // Let others look at this message also if they want
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
SHA256 message_hash, chain_hash;
|
||||
uint8_t message_hash_bytes[32] = {0};
|
||||
uint8_t chain_hash_bytes[32] = {0};
|
||||
uint8_t root_hash_bytes[32] = {0};
|
||||
|
||||
// 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
|
||||
}
|
||||
// refuse without valid time?
|
||||
LOG_WARN("in handleReceived");
|
||||
if (mp.decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP && mp.to == NODENUM_BROADCAST) {
|
||||
|
||||
// do not include rxtime in the message hash. We want these to match when more then one node receives and compares notes.
|
||||
// maybe include it in the commit hash
|
||||
|
||||
message_hash.reset();
|
||||
message_hash.update(router->p_encrypted->encrypted.bytes, router->p_encrypted->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(message_hash_bytes, 32);
|
||||
|
||||
sqlite3_stmt *checkDup;
|
||||
sqlite3_prepare_v2(ppDb, "SELECT COUNT(*) from channel_messages where commit_hash=?", -1, &checkDup, NULL);
|
||||
sqlite3_bind_blob(checkDup, 1, message_hash_bytes, 32, NULL);
|
||||
sqlite3_step(checkDup);
|
||||
int numberFound = sqlite3_column_int(checkDup, 0);
|
||||
LOG_WARN("found %u times in db", numberFound);
|
||||
|
||||
if (numberFound != 0)
|
||||
return ProcessMessage::CONTINUE;
|
||||
|
||||
if (!portduino_config.sfpp_stratum0) {
|
||||
LOG_WARN("TODO: downstream collection of messages");
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
// need to resolve the channel hash to the root hash
|
||||
getRootFromChannelHash(router->p_encrypted->channel, root_hash_bytes);
|
||||
|
||||
std::string getEntry_string =
|
||||
"select commit_hash from channel_messages where channel_hash=? order by rowid desc LIMIT 1;";
|
||||
sqlite3_stmt *getEntry;
|
||||
int rc = sqlite3_prepare_v2(ppDb, getEntry_string.c_str(), getEntry_string.size(), &getEntry, NULL);
|
||||
sqlite3_bind_int(getEntry, 1, router->p_encrypted->channel);
|
||||
sqlite3_step(getEntry);
|
||||
|
||||
// this is allocated by sqlite3 and will be deleted when finalize is called
|
||||
uint8_t *last_message_hash = (uint8_t *)sqlite3_column_blob(getEntry, 0);
|
||||
if (last_message_hash) {
|
||||
printBytes("last message: 0x", last_message_hash, 32);
|
||||
} else {
|
||||
printBytes("new chain root: 0x", root_hash_bytes, 32);
|
||||
}
|
||||
|
||||
// look for message_hash_bytes in db
|
||||
// if found, we bail early
|
||||
|
||||
chain_hash.reset();
|
||||
if (last_message_hash) {
|
||||
chain_hash.update(last_message_hash, 32);
|
||||
} else {
|
||||
chain_hash.update(root_hash_bytes, 32);
|
||||
}
|
||||
chain_hash.update(message_hash_bytes, 32);
|
||||
// message_hash.update(&mp.rx_time, sizeof(mp.rx_time));
|
||||
chain_hash.finalize(chain_hash_bytes, 32);
|
||||
|
||||
sqlite3_finalize(getEntry);
|
||||
|
||||
// select HEX(commit_hash),HEX(channel_hash), payload, destination from channel_messages order by rowid desc;
|
||||
|
||||
// push a message into the local chain DB
|
||||
|
||||
addToChain(mp.to, mp.from, mp.id, mp.want_ack, router->p_encrypted->channel, router->p_encrypted->encrypted.bytes,
|
||||
router->p_encrypted->encrypted.size, message_hash_bytes, chain_hash_bytes, root_hash_bytes, mp.rx_time,
|
||||
(char *)mp.decoded.payload.bytes, mp.decoded.payload.size);
|
||||
|
||||
return ProcessMessage::CONTINUE; // Let others look at this message also if they want
|
||||
|
||||
// one of the command messages
|
||||
} 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);
|
||||
// when we get an update this way, if the message isn't on the chain, this node hasn't seen it, and can rebroadcast.
|
||||
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 chain_hash;
|
||||
chain_hash.update(&_ch_hash, sizeof(_ch_hash));
|
||||
NodeNum ourNode = nodeDB->getNodeNum();
|
||||
chain_hash.update(&ourNode, sizeof(ourNode));
|
||||
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true);
|
||||
chain_hash.update(&rtc_sec, sizeof(rtc_sec));
|
||||
chain_hash.finalize(_root_hash, 32);
|
||||
|
||||
addRootToMappings(_ch_hash, _root_hash);
|
||||
LOG_WARN("here4");
|
||||
}
|
||||
}
|
||||
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);
|
||||
LOG_WARN("here1");
|
||||
// sqlite3_bind_int(getHash, 4, nodeToAdd);
|
||||
rc = sqlite3_step(getHash);
|
||||
LOG_WARN("here2 %u, %s", rc, sqlite3_errmsg(ppDb));
|
||||
sqlite3_finalize(getHash);
|
||||
LOG_WARN("here3");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool StoreForwardPlusPlusModule::getChainEnd(ChannelHash _ch_hash, uint8_t *_chain_hash, uint8_t *_message_hash)
|
||||
{
|
||||
LOG_WARN("getChainEnd");
|
||||
|
||||
std::string getEntry_string =
|
||||
"select commit_hash, message_hash from channel_messages where channel_hash=? order by rowid desc LIMIT 1;";
|
||||
sqlite3_stmt *getEntry;
|
||||
int rc = sqlite3_prepare_v2(ppDb, getEntry_string.c_str(), getEntry_string.size(), &getEntry, NULL);
|
||||
sqlite3_bind_int(getEntry, 1, _ch_hash);
|
||||
sqlite3_step(getEntry);
|
||||
uint8_t *last_message_chain_hash = (uint8_t *)sqlite3_column_blob(getEntry, 0);
|
||||
uint8_t *last_message_hash = (uint8_t *)sqlite3_column_blob(getEntry, 1);
|
||||
|
||||
if (last_message_chain_hash == nullptr || last_message_hash == nullptr) {
|
||||
LOG_WARN("Store and Forward++ database lookup returned null");
|
||||
sqlite3_finalize(getEntry);
|
||||
|
||||
return false;
|
||||
}
|
||||
memcpy(_chain_hash, last_message_chain_hash, 32);
|
||||
memcpy(_message_hash, last_message_hash, 32);
|
||||
sqlite3_finalize(getEntry);
|
||||
return true;
|
||||
}
|
||||
|
||||
void StoreForwardPlusPlusModule::requestNextMessage(uint8_t *_root_hash, uint8_t *_chain_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.chain_hash.size = 32;
|
||||
memcpy(storeforward.chain_hash.bytes, _chain_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 *_chain_hash, uint8_t *next_chain_hash)
|
||||
{
|
||||
LOG_WARN("getNextHash");
|
||||
|
||||
ChannelHash _channel_hash = getChannelHashFromRoot(_root_hash);
|
||||
LOG_WARN("_channel_hash %u", _channel_hash);
|
||||
|
||||
sqlite3_stmt *getHash;
|
||||
int rc = sqlite3_prepare_v2(ppDb, "select commit_hash from channel_messages where channel_hash=? order by rowid ASC;", -1,
|
||||
&getHash, NULL);
|
||||
|
||||
LOG_WARN("%d", rc);
|
||||
if (rc != SQLITE_OK) {
|
||||
LOG_WARN("here2 %u, %s", rc, sqlite3_errmsg(ppDb));
|
||||
}
|
||||
sqlite3_bind_int(getHash, 1, _channel_hash);
|
||||
sqlite3_step(getHash);
|
||||
bool found_hash = false;
|
||||
|
||||
// asking for the first entry on the chain
|
||||
if (memcmp(_root_hash, _chain_hash, 32) == 0) {
|
||||
uint8_t *tmp_chain_hash = (uint8_t *)sqlite3_column_blob(getHash, 0);
|
||||
memcpy(next_chain_hash, tmp_chain_hash, 32);
|
||||
found_hash = true;
|
||||
} else {
|
||||
uint8_t *tmp_chain_hash;
|
||||
while (sqlite3_step(getHash) != SQLITE_DONE) {
|
||||
tmp_chain_hash = (uint8_t *)sqlite3_column_blob(getHash, 0);
|
||||
if (found_hash) {
|
||||
memcpy(next_chain_hash, tmp_chain_hash, 32);
|
||||
break;
|
||||
}
|
||||
if (memcmp(tmp_chain_hash, _chain_hash, 32) == 0)
|
||||
found_hash = true;
|
||||
}
|
||||
}
|
||||
|
||||
sqlite3_finalize(getHash);
|
||||
return found_hash;
|
||||
}
|
||||
|
||||
bool StoreForwardPlusPlusModule::broadcastLink(uint8_t *_chain_hash, uint8_t *_root_hash)
|
||||
{
|
||||
sqlite3_stmt *getHash;
|
||||
int rc = sqlite3_prepare_v2(ppDb, "select destination, sender, packet_id, encrypted_bytes, message_hash, rx_time \
|
||||
from channel_messages where commit_hash=?;",
|
||||
-1, &getHash, NULL);
|
||||
|
||||
LOG_WARN("%d", rc);
|
||||
if (rc != SQLITE_OK) {
|
||||
LOG_WARN("here2 %u, %s", rc, sqlite3_errmsg(ppDb));
|
||||
}
|
||||
sqlite3_bind_blob(getHash, 1, _chain_hash, 32, NULL);
|
||||
sqlite3_step(getHash);
|
||||
|
||||
meshtastic_StoreForwardPlusPlus storeforward = meshtastic_StoreForwardPlusPlus_init_zero;
|
||||
storeforward.sfpp_message_type = meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE;
|
||||
|
||||
storeforward.encapsulated_to = sqlite3_column_int(getHash, 0);
|
||||
storeforward.encapsulated_from = sqlite3_column_int(getHash, 1);
|
||||
storeforward.encapsulated_id = sqlite3_column_int(getHash, 2);
|
||||
|
||||
uint8_t *_payload = (uint8_t *)sqlite3_column_blob(getHash, 3);
|
||||
storeforward.message.size = sqlite3_column_bytes(getHash, 3);
|
||||
memcpy(storeforward.message.bytes, _payload, storeforward.message.size);
|
||||
|
||||
uint8_t *_message_hash = (uint8_t *)sqlite3_column_blob(getHash, 4);
|
||||
storeforward.message_hash.size = 32;
|
||||
memcpy(storeforward.message_hash.bytes, _message_hash, storeforward.message_hash.size);
|
||||
|
||||
storeforward.encapsulated_rxtime = sqlite3_column_int(getHash, 5);
|
||||
|
||||
storeforward.chain_hash.size = 32;
|
||||
memcpy(storeforward.chain_hash.bytes, _chain_hash, 32);
|
||||
|
||||
storeforward.root_hash.size = 32;
|
||||
memcpy(storeforward.root_hash.bytes, _root_hash, 32);
|
||||
|
||||
sqlite3_finalize(getHash);
|
||||
|
||||
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(uint32_t to, uint32_t from, uint32_t id, bool want_ack, ChannelHash channel_hash,
|
||||
uint8_t *encrypted_bytes, size_t encrypted_len, uint8_t *_message_hash,
|
||||
uint8_t *_chain_hash, uint8_t *_root_hash, uint32_t _rx_time, char *payload_bytes,
|
||||
size_t payload_len)
|
||||
|
||||
{
|
||||
// push a message into the local chain DB
|
||||
// destination
|
||||
sqlite3_bind_int(stmt, 1, to);
|
||||
// sender
|
||||
sqlite3_bind_int(stmt, 2, from);
|
||||
// packet_id
|
||||
sqlite3_bind_int(stmt, 3, id);
|
||||
// want_ack
|
||||
sqlite3_bind_int(stmt, 4, want_ack);
|
||||
// channel_hash
|
||||
sqlite3_bind_int(stmt, 5, channel_hash);
|
||||
// encrypted_bytes
|
||||
sqlite3_bind_blob(stmt, 6, encrypted_bytes, encrypted_len, NULL);
|
||||
|
||||
// message_hash
|
||||
sqlite3_bind_blob(stmt, 7, _message_hash, 32, NULL);
|
||||
// rx_time
|
||||
sqlite3_bind_int(stmt, 8, _rx_time);
|
||||
|
||||
// commit_hash
|
||||
sqlite3_bind_blob(stmt, 9, _chain_hash, 32, NULL);
|
||||
// payload
|
||||
sqlite3_bind_text(stmt, 10, payload_bytes, payload_len, NULL);
|
||||
|
||||
sqlite3_step(stmt);
|
||||
sqlite3_reset(stmt);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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
|
||||
74
src/modules/Native/StoreForwardPlusPlus.h
Normal file
74
src/modules/Native/StoreForwardPlusPlus.h
Normal file
@@ -0,0 +1,74 @@
|
||||
#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
|
||||
{
|
||||
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 *stmt;
|
||||
|
||||
// returns wasfound
|
||||
bool getRootFromChannelHash(ChannelHash, uint8_t *);
|
||||
|
||||
ChannelHash getChannelHashFromRoot(uint8_t *_root_hash);
|
||||
|
||||
bool getNextHash(uint8_t *_root_hash, uint8_t *_chain_hash, uint8_t *next_chain_hash);
|
||||
|
||||
// returns isnew
|
||||
bool getOrAddRootFromChannelHash(ChannelHash, uint8_t *);
|
||||
|
||||
bool addRootToMappings(ChannelHash, uint8_t *);
|
||||
|
||||
// return indicates message found
|
||||
bool getChainEnd(ChannelHash, uint8_t *, uint8_t *);
|
||||
|
||||
void requestNextMessage(uint8_t *, uint8_t *);
|
||||
|
||||
bool broadcastLink(uint8_t *_chain_hash, uint8_t *_root_hash);
|
||||
|
||||
bool addToChain(uint32_t, uint32_t, uint32_t, bool, ChannelHash, uint8_t *, size_t, uint8_t *, uint8_t *, uint8_t *, uint32_t,
|
||||
char *, size_t);
|
||||
|
||||
enum chain_types {
|
||||
channel_chain = 0,
|
||||
};
|
||||
};
|
||||
235
src/modules/Native/StoreForwardPlusPlusSat.cpp
Normal file
235
src/modules/Native/StoreForwardPlusPlusSat.cpp
Normal file
@@ -0,0 +1,235 @@
|
||||
// create second module for satellites?
|
||||
|
||||
// The central node will generate a 256 or 128 bit value as its seed. This is the value other nodes subscribe to, and serves as
|
||||
// the root of the chain
|
||||
|
||||
//
|
||||
|
||||
// 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
|
||||
|
||||
// first draft is just to save channel 0 in a git-style database
|
||||
|
||||
// message objects get a hash value
|
||||
// the message chain gets a commit hash
|
||||
//
|
||||
|
||||
#include "StoreForwardPlusPlusSat.h"
|
||||
#include "SHA256.h"
|
||||
#include "meshUtils.h"
|
||||
|
||||
StoreForwardPlusPlusSatModule::StoreForwardPlusPlusSatModule() : SinglePortModule("StoreForwardPlusPlus", (_meshtastic_PortNum)35)
|
||||
{
|
||||
LOG_WARN("StoreForwardPlusPlusSatModule init");
|
||||
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, \
|
||||
want_ack BOOL NOT NULL, \
|
||||
channel_hash INT NOT NULL, \
|
||||
encrypted_bytes BLOB NOT NULL, \
|
||||
message_hash BLOB NOT NULL, \
|
||||
rx_time INT 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);
|
||||
|
||||
// create table DMs
|
||||
res = sqlite3_exec(ppDb, " \
|
||||
CREATE TABLE direct_messages( \
|
||||
destination INT NOT NULL, \
|
||||
sender INT NOT NULL, \
|
||||
packet_id INT NOT NULL, \
|
||||
want_ack BOOL NOT NULL, \
|
||||
channel_hash INT NOT NULL, \
|
||||
commit_hash BLOB NOT NULL, \
|
||||
encrypted_bytes BLOB NOT NULL, \
|
||||
message_hash BLOB NOT NULL, \
|
||||
payload TEXT, \
|
||||
rx_time INT NOT NULL, \
|
||||
PRIMARY KEY (message_hash) \
|
||||
);",
|
||||
NULL, NULL, &err);
|
||||
LOG_WARN("Result2 %u", res);
|
||||
if (err != nullptr)
|
||||
;
|
||||
LOG_ERROR("%s", err);
|
||||
sqlite3_free(err);
|
||||
|
||||
// create table mappings
|
||||
// create table DMs
|
||||
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);
|
||||
// type
|
||||
// sha256hash
|
||||
// channelhash or 64 bit combination
|
||||
// The sat version needs a scratch database of messages that have not been checked in
|
||||
|
||||
// store schema version somewhere
|
||||
|
||||
std::string insert_statement = "INSERT INTO channel_messages (destination, sender, packet_id, want_ack, channel_hash, \
|
||||
encrypted_bytes, message_hash, rx_time, commit_hash, payload) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?);";
|
||||
sqlite3_prepare(ppDb, insert_statement.c_str(), insert_statement.length(), &stmt, NULL);
|
||||
|
||||
encryptedOk = true;
|
||||
}
|
||||
|
||||
ProcessMessage StoreForwardPlusPlusSatModule::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
|
||||
// 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
|
||||
|
||||
SHA256 message_hash, chain_hash;
|
||||
uint8_t message_hash_bytes[32] = {0};
|
||||
uint8_t chain_hash_bytes[32] = {0};
|
||||
|
||||
// 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
|
||||
}
|
||||
// refuse without valid time?
|
||||
LOG_WARN("in handleReceived");
|
||||
std::string getEntry_string = "select commit_hash from channel_messages where channel_hash=? order by rowid desc LIMIT 1;";
|
||||
sqlite3_stmt *getEntry;
|
||||
int rc = sqlite3_prepare(ppDb, getEntry_string.c_str(), getEntry_string.size(), &getEntry, NULL);
|
||||
sqlite3_bind_int(getEntry, 1, router->p_encrypted->channel);
|
||||
sqlite3_step(getEntry);
|
||||
|
||||
// this is allocated by sqlite3 and will be deleted when finalize is called
|
||||
uint8_t *last_message_hash = (uint8_t *)sqlite3_column_blob(getEntry, 0);
|
||||
if (last_message_hash) {
|
||||
printBytes("last message: 0x", last_message_hash, 32);
|
||||
} else {
|
||||
// generate root hash and populate lookup table
|
||||
}
|
||||
|
||||
// do not include rxtime in the message hash. We want these to match when more then one node receives and compares notes.
|
||||
// feel free to include it in the commit hash
|
||||
|
||||
message_hash.reset();
|
||||
message_hash.update(router->p_encrypted->encrypted.bytes, router->p_encrypted->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(message_hash_bytes, 32);
|
||||
|
||||
chain_hash.reset();
|
||||
if (last_message_hash) {
|
||||
chain_hash.update(last_message_hash, 32);
|
||||
}
|
||||
chain_hash.update(message_hash_bytes, 32);
|
||||
// message_hash.update(&mp.rx_time, sizeof(mp.rx_time));
|
||||
chain_hash.finalize(chain_hash_bytes, 32);
|
||||
|
||||
sqlite3_finalize(getEntry);
|
||||
|
||||
// select HEX(commit_hash),HEX(channel_hash), payload, destination from channel_messages order by rowid desc;
|
||||
|
||||
// push a message into the local chain DB
|
||||
// destination
|
||||
sqlite3_bind_int(stmt, 1, mp.to);
|
||||
// sender
|
||||
sqlite3_bind_int(stmt, 2, mp.from);
|
||||
// packet_id
|
||||
sqlite3_bind_int(stmt, 3, mp.id);
|
||||
// want_ack
|
||||
sqlite3_bind_int(stmt, 4, mp.want_ack);
|
||||
// channel_hash
|
||||
sqlite3_bind_int(stmt, 5, router->p_encrypted->channel);
|
||||
// encrypted_bytes
|
||||
sqlite3_bind_blob(stmt, 6, router->p_encrypted->encrypted.bytes, router->p_encrypted->encrypted.size, NULL);
|
||||
|
||||
// message_hash
|
||||
sqlite3_bind_blob(stmt, 7, message_hash_bytes, 32, NULL);
|
||||
// rx_time
|
||||
sqlite3_bind_int(stmt, 8, mp.rx_time);
|
||||
|
||||
// commit_hash
|
||||
sqlite3_bind_blob(stmt, 9, chain_hash_bytes, 32, NULL);
|
||||
// payload
|
||||
sqlite3_bind_text(stmt, 10, (char *)mp.decoded.payload.bytes, mp.decoded.payload.size, NULL);
|
||||
|
||||
sqlite3_step(stmt);
|
||||
sqlite3_reset(stmt);
|
||||
|
||||
return ProcessMessage::CONTINUE; // Let others look at this message also if they want
|
||||
}
|
||||
|
||||
// when we get an update this way, if the message isn't on the chain or in the scratch table, this node hasn't seen it, and can
|
||||
// rebroadcast.
|
||||
|
||||
// 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
|
||||
41
src/modules/Native/StoreForwardPlusPlusSat.h
Normal file
41
src/modules/Native/StoreForwardPlusPlusSat.h
Normal file
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
#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 StoreForwardPlusPlusSatModule : public SinglePortModule // should probably derive this from the main class
|
||||
{
|
||||
public:
|
||||
/** Constructor
|
||||
* name is for debugging output
|
||||
*/
|
||||
StoreForwardPlusPlusSatModule();
|
||||
|
||||
/*
|
||||
-Override the wantPacket method.
|
||||
*/
|
||||
virtual bool wantPacket(const meshtastic_MeshPacket *p) override
|
||||
{
|
||||
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;
|
||||
|
||||
private:
|
||||
sqlite3 *ppDb;
|
||||
sqlite3_stmt *stmt;
|
||||
};
|
||||
@@ -1,13 +1,10 @@
|
||||
#include "SystemCommandsModule.h"
|
||||
#include "input/InputBroker.h"
|
||||
#include "meshUtils.h"
|
||||
|
||||
#if HAS_SCREEN
|
||||
#include "MessageStore.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#endif
|
||||
|
||||
#include "GPS.h"
|
||||
#include "MeshService.h"
|
||||
#include "Module.h"
|
||||
@@ -31,7 +28,10 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event)
|
||||
switch (event->kbchar) {
|
||||
// Fn key symbols
|
||||
case INPUT_BROKER_MSG_FN_SYMBOL_ON:
|
||||
IF_SCREEN(screen->setFunctionSymbol("Fn"));
|
||||
return 0;
|
||||
case INPUT_BROKER_MSG_FN_SYMBOL_OFF:
|
||||
IF_SCREEN(screen->removeFunctionSymbol("Fn"));
|
||||
return 0;
|
||||
// Brightness
|
||||
case INPUT_BROKER_MSG_BRIGHTNESS_UP:
|
||||
@@ -78,9 +78,6 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event)
|
||||
case INPUT_BROKER_MSG_REBOOT:
|
||||
IF_SCREEN(screen->showSimpleBanner("Rebooting...", 0));
|
||||
nodeDB->saveToDisk();
|
||||
#if HAS_SCREEN
|
||||
messageStore.saveToFlash();
|
||||
#endif
|
||||
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
|
||||
// runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
||||
return true;
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
#include "TextMessageModule.h"
|
||||
#include "MeshService.h"
|
||||
#include "MessageStore.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "buzz.h"
|
||||
#include "configuration.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/draw/MessageRenderer.h"
|
||||
#include "main.h"
|
||||
TextMessageModule *textMessageModule;
|
||||
|
||||
ProcessMessage TextMessageModule::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
@@ -19,26 +15,14 @@ ProcessMessage TextMessageModule::handleReceived(const meshtastic_MeshPacket &mp
|
||||
#endif
|
||||
|
||||
// We only store/display messages destined for us.
|
||||
// Keep a copy of the most recent text message.
|
||||
devicestate.rx_text_message = mp;
|
||||
devicestate.has_rx_text_message = true;
|
||||
#if HAS_SCREEN
|
||||
// Guard against running in MeshtasticUI
|
||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||
// Store in the central message history
|
||||
const StoredMessage &sm = messageStore.addFromPacket(mp);
|
||||
|
||||
// Pass message to renderer (banner + thread switching + scroll reset)
|
||||
// Use the global Screen singleton to retrieve the current OLED display
|
||||
auto *display = screen ? screen->getDisplayDevice() : nullptr;
|
||||
graphics::MessageRenderer::handleNewMessage(display, sm, mp);
|
||||
}
|
||||
#endif
|
||||
// Only trigger screen wake if configuration allows it
|
||||
if (shouldWakeOnReceivedMessage()) {
|
||||
powerFSM.trigger(EVENT_RECEIVED_MSG);
|
||||
}
|
||||
|
||||
// Notify any observers (e.g. external modules that care about packets)
|
||||
notifyObservers(&mp);
|
||||
|
||||
return ProcessMessage::CONTINUE; // Let others look at this message also if they want
|
||||
|
||||
@@ -3,13 +3,7 @@
|
||||
#include "SinglePortModule.h"
|
||||
|
||||
/**
|
||||
* Text message handling for Meshtastic.
|
||||
*
|
||||
* This module is responsible for receiving and storing incoming text messages
|
||||
* from the mesh. It updates device state and notifies observers so that other
|
||||
* components (such as the MessageRenderer) can later display or process them.
|
||||
*
|
||||
* Rendering of messages on screen is no longer done here.
|
||||
* Text message handling for meshtastic - draws on the OLED display the most recent received message
|
||||
*/
|
||||
class TextMessageModule : public SinglePortModule, public Observable<const meshtastic_MeshPacket *>
|
||||
{
|
||||
@@ -21,10 +15,10 @@ class TextMessageModule : public SinglePortModule, public Observable<const mesht
|
||||
|
||||
protected:
|
||||
/** Called to handle a particular incoming message
|
||||
*
|
||||
* @return ProcessMessage::STOP if you've guaranteed you've handled this
|
||||
* message and no other handlers should be considered for it.
|
||||
*/
|
||||
|
||||
@return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be considered for
|
||||
it
|
||||
*/
|
||||
virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||
virtual bool wantPacket(const meshtastic_MeshPacket *p) override;
|
||||
};
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
#include "NodeDB.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "configuration.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/draw/CompassRenderer.h"
|
||||
|
||||
#if HAS_SCREEN
|
||||
@@ -15,15 +14,6 @@
|
||||
|
||||
WaypointModule *waypointModule;
|
||||
|
||||
static inline float degToRad(float deg)
|
||||
{
|
||||
return deg * PI / 180.0f;
|
||||
}
|
||||
static inline float radToDeg(float rad)
|
||||
{
|
||||
return rad * 180.0f / PI;
|
||||
}
|
||||
|
||||
ProcessMessage WaypointModule::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
|
||||
@@ -62,15 +52,31 @@ ProcessMessage WaypointModule::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
bool WaypointModule::shouldDraw()
|
||||
{
|
||||
#if !MESHTASTIC_EXCLUDE_WAYPOINT
|
||||
if (!screen || !devicestate.has_rx_waypoint)
|
||||
if (screen == nullptr)
|
||||
return false;
|
||||
// If no waypoint to show
|
||||
if (!devicestate.has_rx_waypoint)
|
||||
return false;
|
||||
|
||||
meshtastic_Waypoint wp{}; // <- replaces memset
|
||||
// Decode the message, to find the expiration time (is waypoint still valid)
|
||||
// This handles "deletion" as well as expiration
|
||||
meshtastic_Waypoint wp;
|
||||
memset(&wp, 0, sizeof(wp));
|
||||
if (pb_decode_from_bytes(devicestate.rx_waypoint.decoded.payload.bytes, devicestate.rx_waypoint.decoded.payload.size,
|
||||
&meshtastic_Waypoint_msg, &wp)) {
|
||||
return wp.expire > getTime();
|
||||
// Valid waypoint
|
||||
if (wp.expire > getTime())
|
||||
return devicestate.has_rx_waypoint = true;
|
||||
|
||||
// Expired, or deleted
|
||||
else
|
||||
return devicestate.has_rx_waypoint = false;
|
||||
}
|
||||
return false; // no LOG_ERROR, no flag writes
|
||||
|
||||
// If decoding failed
|
||||
LOG_ERROR("Failed to decode waypoint");
|
||||
devicestate.has_rx_waypoint = false;
|
||||
return false;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
@@ -79,46 +85,53 @@ bool WaypointModule::shouldDraw()
|
||||
/// Draw the last waypoint we received
|
||||
void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
if (!screen)
|
||||
if (screen == nullptr)
|
||||
return;
|
||||
display->clear();
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
// Prepare to draw
|
||||
display->setFont(FONT_SMALL);
|
||||
int line = 1;
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
|
||||
// === Set Title
|
||||
const char *titleStr = "Waypoint";
|
||||
|
||||
// === Header ===
|
||||
graphics::drawCommonHeader(display, x, y, titleStr);
|
||||
|
||||
const int w = display->getWidth();
|
||||
const int h = display->getHeight();
|
||||
// Handle inverted display
|
||||
// Unsure of expected behavior: for now, copy drawNodeInfo
|
||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED)
|
||||
display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL);
|
||||
|
||||
// Decode the waypoint
|
||||
const meshtastic_MeshPacket &mp = devicestate.rx_waypoint;
|
||||
meshtastic_Waypoint wp{};
|
||||
meshtastic_Waypoint wp;
|
||||
memset(&wp, 0, sizeof(wp));
|
||||
if (!pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_Waypoint_msg, &wp)) {
|
||||
// This *should* be caught by shouldDrawWaypoint, but we'll short-circuit here just in case
|
||||
display->drawStringMaxWidth(0 + x, 0 + y, x + display->getWidth(), "Couldn't decode waypoint");
|
||||
devicestate.has_rx_waypoint = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Get timestamp info. Will pass as a field to drawColumns
|
||||
char lastStr[20];
|
||||
static char lastStr[20];
|
||||
getTimeAgoStr(sinceReceived(&mp), lastStr, sizeof(lastStr));
|
||||
|
||||
// Will contain distance information, passed as a field to drawColumns
|
||||
char distStr[20];
|
||||
static char distStr[20];
|
||||
|
||||
// Get our node, to use our own position
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
|
||||
// Text fields to draw (left of compass)
|
||||
// Last element must be NULL. This signals the end of the char*[] to drawColumns
|
||||
const char *fields[] = {"Waypoint", lastStr, wp.name, distStr, NULL};
|
||||
|
||||
// Dimensions / co-ordinates for the compass/circle
|
||||
const uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(w, h);
|
||||
const int16_t compassX = x + w - (compassDiam / 2) - 5;
|
||||
const int16_t compassY = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT)
|
||||
? y + h / 2
|
||||
: y + FONT_HEIGHT_SMALL + (h - FONT_HEIGHT_SMALL) / 2;
|
||||
int16_t compassX = 0, compassY = 0;
|
||||
uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(display->getWidth(), display->getHeight());
|
||||
|
||||
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) {
|
||||
compassX = x + display->getWidth() - compassDiam / 2 - 5;
|
||||
compassY = y + display->getHeight() / 2;
|
||||
} else {
|
||||
compassX = x + display->getWidth() - compassDiam / 2 - 5;
|
||||
compassY = y + FONT_HEIGHT_SMALL + (display->getHeight() - FONT_HEIGHT_SMALL) / 2;
|
||||
}
|
||||
|
||||
// If our node has a position:
|
||||
if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading())) {
|
||||
@@ -128,7 +141,7 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state,
|
||||
myHeading = 0;
|
||||
} else {
|
||||
if (screen->hasHeading())
|
||||
myHeading = degToRad(screen->getHeading());
|
||||
myHeading = (screen->getHeading()) * PI / 180; // gotta convert compass degrees to Radians
|
||||
else
|
||||
myHeading = screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i));
|
||||
}
|
||||
@@ -144,35 +157,46 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state,
|
||||
graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther);
|
||||
|
||||
float bearingToOtherDegrees = (bearingToOther < 0) ? bearingToOther + 2 * PI : bearingToOther;
|
||||
bearingToOtherDegrees = radToDeg(bearingToOtherDegrees);
|
||||
bearingToOtherDegrees = bearingToOtherDegrees * 180 / PI;
|
||||
|
||||
// Distance to Waypoint
|
||||
float d = GeoCoord::latLongToMeter(DegD(wp.latitude_i), DegD(wp.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i));
|
||||
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
|
||||
float feet = d * METERS_TO_FEET;
|
||||
snprintf(distStr, sizeof(distStr), feet < (2 * MILES_TO_FEET) ? "%.0fft %.0f°" : "%.1fmi %.0f°",
|
||||
feet < (2 * MILES_TO_FEET) ? feet : feet / MILES_TO_FEET, bearingToOtherDegrees);
|
||||
if (d < (2 * MILES_TO_FEET))
|
||||
snprintf(distStr, sizeof(distStr), "%.0fft %.0f°", d * METERS_TO_FEET, bearingToOtherDegrees);
|
||||
else
|
||||
snprintf(distStr, sizeof(distStr), "%.1fmi %.0f°", d * METERS_TO_FEET / MILES_TO_FEET, bearingToOtherDegrees);
|
||||
} else {
|
||||
snprintf(distStr, sizeof(distStr), d < 2000 ? "%.0fm %.0f°" : "%.1fkm %.0f°", d < 2000 ? d : d / 1000,
|
||||
bearingToOtherDegrees);
|
||||
if (d < 2000)
|
||||
snprintf(distStr, sizeof(distStr), "%.0fm %.0f°", d, bearingToOtherDegrees);
|
||||
else
|
||||
snprintf(distStr, sizeof(distStr), "%.1fkm %.0f°", d / 1000, bearingToOtherDegrees);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// If our node doesn't have position
|
||||
else {
|
||||
// ? in the compass
|
||||
display->drawString(compassX - FONT_HEIGHT_SMALL / 4, compassY - FONT_HEIGHT_SMALL / 2, "?");
|
||||
|
||||
// ? in the distance field
|
||||
snprintf(distStr, sizeof(distStr), "? %s ?°",
|
||||
(config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) ? "mi" : "km");
|
||||
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL)
|
||||
strncpy(distStr, "? mi ?°", sizeof(distStr));
|
||||
else
|
||||
strncpy(distStr, "? km ?°", sizeof(distStr));
|
||||
}
|
||||
|
||||
// Draw compass circle
|
||||
display->drawCircle(compassX, compassY, compassDiam / 2);
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT); // Something above me changes to a different alignment, forcing a fix here!
|
||||
display->drawString(0, graphics::getTextPositions(display)[line++], lastStr);
|
||||
display->drawString(0, graphics::getTextPositions(display)[line++], wp.name);
|
||||
display->drawString(0, graphics::getTextPositions(display)[line++], wp.description);
|
||||
display->drawString(0, graphics::getTextPositions(display)[line++], distStr);
|
||||
// Undo color-inversion, if set prior to drawing header
|
||||
// Unsure of expected behavior? For now: copy drawNodeInfo
|
||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) {
|
||||
display->setColor(BLACK);
|
||||
}
|
||||
|
||||
// Must be after distStr is populated
|
||||
graphics::NodeListRenderer::drawColumns(display, x, y, fields);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
|
||||
portduino_config_struct portduino_config;
|
||||
std::ofstream traceFile;
|
||||
std::ofstream JSONFile;
|
||||
Ch341Hal *ch341Hal = nullptr;
|
||||
char *configPath = nullptr;
|
||||
char *optionMac = nullptr;
|
||||
@@ -427,13 +426,11 @@ void portduinoSetup()
|
||||
}
|
||||
|
||||
getMacAddr(dmac);
|
||||
#ifndef UNIT_TEST
|
||||
if (dmac[0] == 0 && dmac[1] == 0 && dmac[2] == 0 && dmac[3] == 0 && dmac[4] == 0 && dmac[5] == 0) {
|
||||
std::cout << "*** Blank MAC Address not allowed!" << std::endl;
|
||||
std::cout << "Please set a MAC Address in config.yaml using either MACAddress or MACAddressSource." << std::endl;
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
#endif
|
||||
printf("MAC ADDRESS: %02X:%02X:%02X:%02X:%02X:%02X\n", dmac[0], dmac[1], dmac[2], dmac[3], dmac[4], dmac[5]);
|
||||
// Rather important to set this, if not running simulated.
|
||||
randomSeed(time(NULL));
|
||||
@@ -466,7 +463,6 @@ void portduinoSetup()
|
||||
if (portduino_config.lora_spi_dev != "" && portduino_config.lora_spi_dev != "ch341") {
|
||||
SPI.begin(portduino_config.lora_spi_dev.c_str());
|
||||
}
|
||||
|
||||
if (portduino_config.traceFilename != "") {
|
||||
try {
|
||||
traceFile.open(portduino_config.traceFilename, std::ios::out | std::ios::app);
|
||||
@@ -474,21 +470,6 @@ void portduinoSetup()
|
||||
std::cout << "*** traceFile Exception " << e.what() << std::endl;
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
if (!traceFile.is_open()) {
|
||||
std::cout << "*** traceFile open failure" << std::endl;
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
} else if (portduino_config.JSONFilename != "") {
|
||||
try {
|
||||
JSONFile.open(portduino_config.JSONFilename, std::ios::out | std::ios::app);
|
||||
} catch (std::ofstream::failure &e) {
|
||||
std::cout << "*** JSONFile Exception " << e.what() << std::endl;
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
if (!JSONFile.is_open()) {
|
||||
std::cout << "*** JSONFile open failure" << std::endl;
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
if (verboseEnabled && portduino_config.logoutputlevel != level_trace) {
|
||||
portduino_config.logoutputlevel = level_debug;
|
||||
@@ -536,29 +517,6 @@ bool loadConfig(const char *configPath)
|
||||
portduino_config.logoutputlevel = level_error;
|
||||
}
|
||||
portduino_config.traceFilename = yamlConfig["Logging"]["TraceFile"].as<std::string>("");
|
||||
portduino_config.JSONFilename = yamlConfig["Logging"]["JSONFile"].as<std::string>("");
|
||||
portduino_config.JSONFilter = (_meshtastic_PortNum)yamlConfig["Logging"]["JSONFilter"].as<int>(0);
|
||||
if (yamlConfig["Logging"]["JSONFilter"].as<std::string>("") == "textmessage")
|
||||
portduino_config.JSONFilter = meshtastic_PortNum_TEXT_MESSAGE_APP;
|
||||
else if (yamlConfig["Logging"]["JSONFilter"].as<std::string>("") == "telemetry")
|
||||
portduino_config.JSONFilter = meshtastic_PortNum_TELEMETRY_APP;
|
||||
else if (yamlConfig["Logging"]["JSONFilter"].as<std::string>("") == "nodeinfo")
|
||||
portduino_config.JSONFilter = meshtastic_PortNum_NODEINFO_APP;
|
||||
else if (yamlConfig["Logging"]["JSONFilter"].as<std::string>("") == "position")
|
||||
portduino_config.JSONFilter = meshtastic_PortNum_POSITION_APP;
|
||||
else if (yamlConfig["Logging"]["JSONFilter"].as<std::string>("") == "waypoint")
|
||||
portduino_config.JSONFilter = meshtastic_PortNum_WAYPOINT_APP;
|
||||
else if (yamlConfig["Logging"]["JSONFilter"].as<std::string>("") == "neighborinfo")
|
||||
portduino_config.JSONFilter = meshtastic_PortNum_NEIGHBORINFO_APP;
|
||||
else if (yamlConfig["Logging"]["JSONFilter"].as<std::string>("") == "traceroute")
|
||||
portduino_config.JSONFilter = meshtastic_PortNum_TRACEROUTE_APP;
|
||||
else if (yamlConfig["Logging"]["JSONFilter"].as<std::string>("") == "detection")
|
||||
portduino_config.JSONFilter = meshtastic_PortNum_DETECTION_SENSOR_APP;
|
||||
else if (yamlConfig["Logging"]["JSONFilter"].as<std::string>("") == "paxcounter")
|
||||
portduino_config.JSONFilter = meshtastic_PortNum_PAXCOUNTER_APP;
|
||||
else if (yamlConfig["Logging"]["JSONFilter"].as<std::string>("") == "remotehardware")
|
||||
portduino_config.JSONFilter = meshtastic_PortNum_REMOTE_HARDWARE_APP;
|
||||
|
||||
if (yamlConfig["Logging"]["AsciiLogs"]) {
|
||||
// Default is !isatty(1) but can be set explicitly in config.yaml
|
||||
portduino_config.ascii_logs = yamlConfig["Logging"]["AsciiLogs"].as<bool>();
|
||||
@@ -788,6 +746,10 @@ bool loadConfig(const char *configPath)
|
||||
}
|
||||
}
|
||||
|
||||
if (yamlConfig["StoreAndForward"]) {
|
||||
portduino_config.sfpp_stratum0 = (yamlConfig["StoreAndForward"]["Stratum0"]).as<bool>(false);
|
||||
}
|
||||
|
||||
if (yamlConfig["General"]) {
|
||||
portduino_config.MaxNodes = (yamlConfig["General"]["MaxNodes"]).as<int>(200);
|
||||
portduino_config.maxtophone = (yamlConfig["General"]["MaxMessageQueue"]).as<int>(100);
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
#include "LR11x0Interface.h"
|
||||
#include "Module.h"
|
||||
#include "mesh/generated/meshtastic/mesh.pb.h"
|
||||
#include "platform/portduino/USBHal.h"
|
||||
#include "yaml-cpp/yaml.h"
|
||||
|
||||
@@ -47,8 +46,6 @@ struct pinMapping {
|
||||
};
|
||||
|
||||
extern std::ofstream traceFile;
|
||||
extern std::ofstream JSONFile;
|
||||
|
||||
extern Ch341Hal *ch341Hal;
|
||||
int initGPIOPin(int pinNum, std::string gpioChipname, int line);
|
||||
bool loadConfig(const char *configPath);
|
||||
@@ -151,9 +148,6 @@ extern struct portduino_config_struct {
|
||||
bool ascii_logs = !isatty(1);
|
||||
bool ascii_logs_explicit = false;
|
||||
|
||||
std::string JSONFilename;
|
||||
meshtastic_PortNum JSONFilter = (_meshtastic_PortNum)0;
|
||||
|
||||
// Webserver
|
||||
std::string webserver_root_path = "";
|
||||
std::string webserver_ssl_key_path = "/etc/meshtasticd/ssl/private_key.pem";
|
||||
@@ -169,6 +163,9 @@ extern struct portduino_config_struct {
|
||||
int configDisplayMode = 0;
|
||||
bool has_configDisplayMode = false;
|
||||
|
||||
// Store and Forward++
|
||||
bool sfpp_stratum0 = false;
|
||||
|
||||
// General
|
||||
std::string mac_address = "";
|
||||
bool mac_address_explicit = false;
|
||||
@@ -419,29 +416,6 @@ extern struct portduino_config_struct {
|
||||
}
|
||||
if (traceFilename != "")
|
||||
out << YAML::Key << "TraceFile" << YAML::Value << traceFilename;
|
||||
if (JSONFilename != "") {
|
||||
out << YAML::Key << "JSONFile" << YAML::Value << JSONFilename;
|
||||
if (JSONFilter == meshtastic_PortNum_TEXT_MESSAGE_APP)
|
||||
out << YAML::Key << "JSONFilter" << YAML::Value << "textmessage";
|
||||
else if (JSONFilter == meshtastic_PortNum_TELEMETRY_APP)
|
||||
out << YAML::Key << "JSONFilter" << YAML::Value << "telemetry";
|
||||
else if (JSONFilter == meshtastic_PortNum_NODEINFO_APP)
|
||||
out << YAML::Key << "JSONFilter" << YAML::Value << "nodeinfo";
|
||||
else if (JSONFilter == meshtastic_PortNum_POSITION_APP)
|
||||
out << YAML::Key << "JSONFilter" << YAML::Value << "position";
|
||||
else if (JSONFilter == meshtastic_PortNum_WAYPOINT_APP)
|
||||
out << YAML::Key << "JSONFilter" << YAML::Value << "waypoint";
|
||||
else if (JSONFilter == meshtastic_PortNum_NEIGHBORINFO_APP)
|
||||
out << YAML::Key << "JSONFilter" << YAML::Value << "neighborinfo";
|
||||
else if (JSONFilter == meshtastic_PortNum_TRACEROUTE_APP)
|
||||
out << YAML::Key << "JSONFilter" << YAML::Value << "traceroute";
|
||||
else if (JSONFilter == meshtastic_PortNum_DETECTION_SENSOR_APP)
|
||||
out << YAML::Key << "JSONFilter" << YAML::Value << "detection";
|
||||
else if (JSONFilter == meshtastic_PortNum_PAXCOUNTER_APP)
|
||||
out << YAML::Key << "JSONFilter" << YAML::Value << "paxcounter";
|
||||
else if (JSONFilter == meshtastic_PortNum_REMOTE_HARDWARE_APP)
|
||||
out << YAML::Key << "JSONFilter" << YAML::Value << "remotehardware";
|
||||
}
|
||||
if (ascii_logs_explicit) {
|
||||
out << YAML::Key << "AsciiLogs" << YAML::Value << ascii_logs;
|
||||
}
|
||||
|
||||
@@ -605,13 +605,12 @@ void test_receiveAcksOwnSentMessages(void)
|
||||
|
||||
unitTest->publish(&p, nodeDB->getNodeId().c_str());
|
||||
|
||||
// FIXME: Better assertion for this test
|
||||
// TEST_ASSERT_TRUE(mockRouter->packets_.empty());
|
||||
// TEST_ASSERT_EQUAL(1, mockRoutingModule->ackNacks_.size());
|
||||
// const auto &[err, to, idFrom, chIndex, hopLimit] = mockRoutingModule->ackNacks_.front();
|
||||
// TEST_ASSERT_EQUAL(meshtastic_Routing_Error_NONE, err);
|
||||
// TEST_ASSERT_EQUAL(myNodeInfo.my_node_num, to);
|
||||
// TEST_ASSERT_EQUAL(p.id, idFrom);
|
||||
TEST_ASSERT_TRUE(mockRouter->packets_.empty());
|
||||
TEST_ASSERT_EQUAL(1, mockRoutingModule->ackNacks_.size());
|
||||
const auto &[err, to, idFrom, chIndex, hopLimit] = mockRoutingModule->ackNacks_.front();
|
||||
TEST_ASSERT_EQUAL(meshtastic_Routing_Error_NONE, err);
|
||||
TEST_ASSERT_EQUAL(myNodeInfo.my_node_num, to);
|
||||
TEST_ASSERT_EQUAL(p.id, idFrom);
|
||||
}
|
||||
|
||||
// Should ignore our own messages from MQTT that were heard by other nodes.
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
[env:meshtastic-dr-dev]
|
||||
extends = esp32_base
|
||||
board = esp32doit-devkit-v1
|
||||
board_level = extra
|
||||
board_upload.maximum_size = 4194304
|
||||
board_upload.maximum_ram_size = 532480
|
||||
build_flags =
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
[env:tbeam]
|
||||
extends = esp32_base
|
||||
board = ttgo-t-beam
|
||||
board_level = extra
|
||||
board_level = pr
|
||||
board_check = true
|
||||
lib_deps = ${esp32_base.lib_deps}
|
||||
build_flags = ${esp32_base.build_flags}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
[env:link32-s3-v1]
|
||||
extends = esp32s3_base
|
||||
board = esp32-s3-devkitc-1
|
||||
board_level = extra
|
||||
build_flags =
|
||||
${esp32_base.build_flags}
|
||||
-D LINK_32
|
||||
|
||||
@@ -45,6 +45,7 @@ build_flags = ${native_base.build_flags} -Os -lX11 -linput -lxkbcommon -ffunctio
|
||||
!pkg-config --libs openssl --silence-errors || :
|
||||
!pkg-config --cflags --libs sdl2 --silence-errors || :
|
||||
!pkg-config --cflags --libs libbsd-overlay --silence-errors || :
|
||||
!pkg-config --cflags --libs sqlite3 --silence-errors || :
|
||||
build_src_filter =
|
||||
${native_base.build_src_filter}
|
||||
|
||||
|
||||
@@ -63,20 +63,9 @@ void initVariant()
|
||||
// called from main-nrf52.cpp during the cpuDeepSleep() function
|
||||
void variant_shutdown()
|
||||
{
|
||||
digitalWrite(red_LED_PIN, HIGH);
|
||||
digitalWrite(green_LED_PIN, HIGH);
|
||||
digitalWrite(LED_BLUE, HIGH);
|
||||
|
||||
digitalWrite(PIN_EN1, LOW);
|
||||
digitalWrite(PIN_EN2, LOW);
|
||||
digitalWrite(EEPROM_POWER, LOW);
|
||||
digitalWrite(KEY_POWER, LOW);
|
||||
digitalWrite(DHT_POWER, LOW);
|
||||
digitalWrite(ACC_POWER, LOW);
|
||||
digitalWrite(Battery_POWER, LOW);
|
||||
digitalWrite(GPS_POWER, LOW);
|
||||
|
||||
// This sets the pin to OUTPUT and LOW for the pins *not* in the if block.
|
||||
for (int pin = 0; pin < 48; pin++) {
|
||||
if (pin == PIN_POWER_USB || pin == BUTTON_PIN || pin == PIN_EN1 || pin == PIN_EN2 || pin == DHT_POWER ||
|
||||
pin == ACC_POWER || pin == Battery_POWER || pin == GPS_POWER || pin == LR1110_SPI_MISO_PIN ||
|
||||
|
||||
@@ -46,14 +46,6 @@ build_flags = ${heltec_mesh_solar_base.build_flags}
|
||||
-DEINK_LIMIT_RATE_RESPONSIVE_SEC=1 ; Minimum interval between RESPONSIVE updates
|
||||
-DEINK_BACKGROUND_USES_FAST ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached.
|
||||
-DEINK_HASQUIRK_GHOSTING ; Display model is identified as "prone to ghosting"
|
||||
-DENABLE_MESSAGE_PERSISTENCE=0 ; Disable flash persistence for space-limited build
|
||||
-DMESHTASTIC_EXCLUDE_WIFI=1
|
||||
-DMESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION=1
|
||||
-DMESHTASTIC_EXCLUDE_PAXCOUNTER=1
|
||||
-DMESHTASTIC_EXCLUDE_REMOTEHARDWARE=1
|
||||
-DMESHTASTIC_EXCLUDE_STOREFORWARD=1
|
||||
-DMESHTASTIC_EXCLUDE_CANNEDMESSAGES=1
|
||||
-DMESHTASTIC_EXCLUDE_WAYPOINT=1
|
||||
lib_deps =
|
||||
${heltec_mesh_solar_base.lib_deps}
|
||||
https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip
|
||||
|
||||
@@ -19,6 +19,7 @@ build_type = release
|
||||
build_flags =
|
||||
-include variants/nrf52840/cpp_overrides/lfs_util.h
|
||||
${arduino_base.build_flags}
|
||||
-DSERIAL_BUFFER_SIZE=1024
|
||||
-Wno-unused-variable
|
||||
-Isrc/platform/nrf52
|
||||
-DLFS_NO_ASSERT ; Disable LFS assertions , see https://github.com/meshtastic/firmware/pull/3818
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
[nrf52832_base]
|
||||
extends = nrf52_base
|
||||
|
||||
build_flags =
|
||||
${nrf52_base.build_flags}
|
||||
-DSERIAL_BUFFER_SIZE=1024
|
||||
build_flags = ${nrf52_base.build_flags}
|
||||
|
||||
lib_deps =
|
||||
${nrf52_base.lib_deps}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
[nrf52840_base]
|
||||
extends = nrf52_base
|
||||
|
||||
build_flags =
|
||||
${nrf52_base.build_flags}
|
||||
-DSERIAL_BUFFER_SIZE=4096
|
||||
build_flags = ${nrf52_base.build_flags}
|
||||
|
||||
lib_deps =
|
||||
${nrf52_base.lib_deps}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
[env:feather_rp2040_rfm95]
|
||||
extends = rp2040_base
|
||||
board = adafruit_feather
|
||||
board_level = extra
|
||||
upload_protocol = picotool
|
||||
# add our variants files to the include and src paths
|
||||
build_flags =
|
||||
|
||||
Reference in New Issue
Block a user