mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-16 08:31:35 +10:00
Merge develop into packet authentication policy
This commit is contained in:
@@ -27,6 +27,9 @@ const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaC
|
||||
case PRESET(MEDIUM_FAST):
|
||||
return useShortName ? "MedF" : "MediumFast";
|
||||
break;
|
||||
case PRESET(MEDIUM_TURBO):
|
||||
return useShortName ? "MedT" : "MediumTurbo";
|
||||
break;
|
||||
case PRESET(LONG_SLOW):
|
||||
return useShortName ? "LongS" : "LongSlow";
|
||||
break;
|
||||
|
||||
+7
-2
@@ -57,11 +57,16 @@ using namespace Adafruit_LittleFS_Namespace;
|
||||
#endif
|
||||
|
||||
void fsInit();
|
||||
void fsListFiles();
|
||||
bool copyFile(const char *from, const char *to);
|
||||
bool renameFile(const char *pathFrom, const char *pathTo);
|
||||
bool fsFormat();
|
||||
std::vector<meshtastic_FileInfo> getFiles(const char *dirname, uint8_t levels, size_t maxCount = 64, bool *wasLimited = nullptr);
|
||||
void listDir(const char *dirname, uint8_t levels, bool del = false);
|
||||
void rmDir(const char *dirname);
|
||||
void setupSDCard();
|
||||
void setupSDCard();
|
||||
|
||||
#if defined(HAS_SDCARD) && !defined(SDCARD_USE_SOFT_SPI) && defined(SDCARD_USE_SPI1)
|
||||
#include <SPI.h>
|
||||
// HSPI bus set up by setupSDCard(). Reuse this for other devices on the same bus.
|
||||
extern SPIClass SPI_HSPI;
|
||||
#endif
|
||||
+108
-21
@@ -7,6 +7,7 @@
|
||||
#include "SafeFile.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "memory/MemAudit.h"
|
||||
#include <cassert>
|
||||
#include <cstring> // memcpy
|
||||
|
||||
#ifndef MESSAGE_TEXT_POOL_SIZE
|
||||
@@ -65,6 +66,15 @@ static inline const char *getTextFromPool(uint16_t offset)
|
||||
return &g_messagePool[offset];
|
||||
}
|
||||
|
||||
static inline bool isIgnoredNodeNum(uint32_t nodeNum)
|
||||
{
|
||||
if (nodeNum == 0 || nodeNum == NODENUM_BROADCAST)
|
||||
return false;
|
||||
|
||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeNum);
|
||||
return nodeInfoLiteIsIgnored(node);
|
||||
}
|
||||
|
||||
// Helper: assign a timestamp (RTC if available, else boot-relative)
|
||||
static inline void assignTimestamp(StoredMessage &sm)
|
||||
{
|
||||
@@ -163,9 +173,44 @@ static inline void autosaveTick(MessageStore *store)
|
||||
}
|
||||
#endif
|
||||
|
||||
// Add from incoming/outgoing packet
|
||||
const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &packet)
|
||||
bool MessageStore::shouldStorePacket(const meshtastic_MeshPacket &packet) const
|
||||
{
|
||||
const uint32_t localNode = nodeDB->getNodeNum();
|
||||
const bool isDM = packet.to != 0 && packet.to != NODENUM_BROADCAST;
|
||||
if (isDM) {
|
||||
const bool outgoing = packet.from == 0 || packet.from == localNode;
|
||||
const uint32_t peer = outgoing ? packet.to : packet.from;
|
||||
return !isIgnoredNodeNum(peer);
|
||||
}
|
||||
|
||||
if (packet.from != 0 && packet.from != localNode)
|
||||
return !isIgnoredNodeNum(packet.from);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MessageStore::isMessageVisible(const StoredMessage &msg) const
|
||||
{
|
||||
const uint32_t localNode = nodeDB->getNodeNum();
|
||||
if (msg.type == MessageType::DM_TO_US) {
|
||||
const uint32_t peer = (msg.sender == localNode) ? msg.dest : msg.sender;
|
||||
return !isIgnoredNodeNum(peer);
|
||||
}
|
||||
|
||||
if (msg.sender != 0 && msg.sender != localNode)
|
||||
return !isIgnoredNodeNum(msg.sender);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Add from incoming/outgoing packet
|
||||
const StoredMessage *MessageStore::tryAddFromPacket(const meshtastic_MeshPacket &packet)
|
||||
{
|
||||
if (!shouldStorePacket(packet)) {
|
||||
LOG_DEBUG("Drop store 0x%08x", packet.from);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
StoredMessage sm;
|
||||
assignTimestamp(sm);
|
||||
sm.channelIndex = packet.channel;
|
||||
@@ -196,7 +241,14 @@ const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &pa
|
||||
markMessageStoreUnsaved();
|
||||
#endif
|
||||
|
||||
return liveMessages.back();
|
||||
return &liveMessages.back();
|
||||
}
|
||||
|
||||
const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &packet)
|
||||
{
|
||||
const StoredMessage *stored = tryAddFromPacket(packet);
|
||||
assert(stored);
|
||||
return *stored;
|
||||
}
|
||||
|
||||
// Outgoing/manual message
|
||||
@@ -323,28 +375,33 @@ void MessageStore::loadFromFlash()
|
||||
resetMessagePool(); // reset pool when loading
|
||||
|
||||
#ifdef FSCom
|
||||
concurrency::LockGuard guard(spiLock);
|
||||
{
|
||||
concurrency::LockGuard guard(spiLock);
|
||||
|
||||
if (!FSCom.exists(filename.c_str()))
|
||||
return;
|
||||
if (!FSCom.exists(filename.c_str()))
|
||||
return;
|
||||
|
||||
auto f = FSCom.open(filename.c_str(), FILE_O_READ);
|
||||
if (!f)
|
||||
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;
|
||||
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);
|
||||
for (uint8_t i = 0; i < count; ++i) {
|
||||
StoredMessage m;
|
||||
if (!readMessageRecord(f, m))
|
||||
break;
|
||||
liveMessages.push_back(m);
|
||||
}
|
||||
|
||||
f.close();
|
||||
}
|
||||
|
||||
f.close();
|
||||
if (pruneHiddenMessages())
|
||||
saveToFlash();
|
||||
#endif
|
||||
// Loading messages does not trigger an autosave
|
||||
g_messageStoreHasUnsavedChanges = false;
|
||||
@@ -406,6 +463,13 @@ template <typename Predicate> static void eraseAllMatches(std::deque<StoredMessa
|
||||
}
|
||||
}
|
||||
|
||||
bool MessageStore::pruneHiddenMessages()
|
||||
{
|
||||
const size_t before = liveMessages.size();
|
||||
eraseAllMatches(liveMessages, [&](const StoredMessage &m) { return !isMessageVisible(m); });
|
||||
return liveMessages.size() != before;
|
||||
}
|
||||
|
||||
// Delete oldest message (RAM + persisted queue)
|
||||
void MessageStore::deleteOldestMessage()
|
||||
{
|
||||
@@ -443,6 +507,20 @@ void MessageStore::deleteAllMessagesWithPeer(uint32_t peer)
|
||||
saveToFlash();
|
||||
}
|
||||
|
||||
void MessageStore::deleteAllMessagesFromNode(uint32_t nodeNum)
|
||||
{
|
||||
const uint32_t local = nodeDB->getNodeNum();
|
||||
auto pred = [&](const StoredMessage &m) {
|
||||
if (m.sender == nodeNum)
|
||||
return true;
|
||||
if (m.type != MessageType::DM_TO_US)
|
||||
return false;
|
||||
return m.sender == local ? m.dest == nodeNum : m.sender == nodeNum;
|
||||
};
|
||||
eraseAllMatches(liveMessages, pred);
|
||||
saveToFlash();
|
||||
}
|
||||
|
||||
// Delete oldest message in a direct chat with a node
|
||||
void MessageStore::deleteOldestMessageWithPeer(uint32_t peer)
|
||||
{
|
||||
@@ -460,7 +538,7 @@ std::deque<StoredMessage> MessageStore::getChannelMessages(uint8_t channel) cons
|
||||
{
|
||||
std::deque<StoredMessage> result;
|
||||
for (const auto &m : liveMessages) {
|
||||
if (m.type == MessageType::BROADCAST && m.channelIndex == channel) {
|
||||
if (isMessageVisible(m) && m.type == MessageType::BROADCAST && m.channelIndex == channel) {
|
||||
result.push_back(m);
|
||||
}
|
||||
}
|
||||
@@ -471,13 +549,22 @@ std::deque<StoredMessage> MessageStore::getDirectMessages() const
|
||||
{
|
||||
std::deque<StoredMessage> result;
|
||||
for (const auto &m : liveMessages) {
|
||||
if (m.type == MessageType::DM_TO_US) {
|
||||
if (isMessageVisible(m) && m.type == MessageType::DM_TO_US) {
|
||||
result.push_back(m);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool MessageStore::hasVisibleMessages() const
|
||||
{
|
||||
for (const auto &m : liveMessages) {
|
||||
if (isMessageVisible(m))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
||||
+6
-2
@@ -93,7 +93,7 @@ class MessageStore
|
||||
void addLiveMessage(StoredMessage &&msg);
|
||||
void addLiveMessage(const StoredMessage &msg); // convenience overload
|
||||
const std::deque<StoredMessage> &getLiveMessages() const { return liveMessages; }
|
||||
|
||||
const StoredMessage *tryAddFromPacket(const meshtastic_MeshPacket &mp); // Incoming/outgoing -> RAM only
|
||||
// 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
|
||||
@@ -111,13 +111,16 @@ class MessageStore
|
||||
void deleteOldestMessageWithPeer(uint32_t peer);
|
||||
void deleteAllMessagesInChannel(uint8_t channel);
|
||||
void deleteAllMessagesWithPeer(uint32_t peer);
|
||||
|
||||
void deleteAllMessagesFromNode(uint32_t nodeNum);
|
||||
// Unified accessor (for UI code, defaults to RAM buffer)
|
||||
const std::deque<StoredMessage> &getMessages() const { return liveMessages; }
|
||||
bool hasVisibleMessages() const;
|
||||
|
||||
// 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
|
||||
bool shouldStorePacket(const meshtastic_MeshPacket &mp) const;
|
||||
bool isMessageVisible(const StoredMessage &msg) const;
|
||||
|
||||
// Upgrade boot-relative timestamps once RTC is valid
|
||||
void upgradeBootRelativeTimestamps();
|
||||
@@ -129,6 +132,7 @@ class MessageStore
|
||||
static uint16_t storeText(const char *src, size_t len);
|
||||
|
||||
private:
|
||||
bool pruneHiddenMessages();
|
||||
std::deque<StoredMessage> liveMessages; // Single in-RAM message buffer (also used for persistence)
|
||||
std::string filename; // Flash filename for persistence
|
||||
};
|
||||
|
||||
+4
-1
@@ -37,6 +37,7 @@
|
||||
#if defined(ARCH_PORTDUINO)
|
||||
#include "api/WiFiServerAPI.h"
|
||||
#include "input/LinuxInputImpl.h"
|
||||
#include "input/LinuxJoystick.h"
|
||||
#endif
|
||||
|
||||
// Working USB detection for powered/charging states on the RAK platform
|
||||
@@ -860,6 +861,8 @@ void Power::reboot()
|
||||
#ifdef __linux__
|
||||
if (aLinuxInputImpl)
|
||||
aLinuxInputImpl->deInit();
|
||||
if (aLinuxJoystick)
|
||||
aLinuxJoystick->deInit();
|
||||
#endif
|
||||
SPI.end();
|
||||
Wire.end();
|
||||
@@ -903,7 +906,7 @@ void Power::shutdown()
|
||||
#if HAS_SCREEN
|
||||
messageStore.saveToFlash();
|
||||
#endif
|
||||
#if defined(ARCH_NRF52) || defined(ARCH_ESP32) || defined(ARCH_RP2040)
|
||||
#if defined(ARCH_NRF52) || defined(ARCH_ESP32) || defined(ARCH_RP2040) || defined(ARCH_STM32WL)
|
||||
#ifdef PIN_LED1
|
||||
ledOff(PIN_LED1);
|
||||
#endif
|
||||
|
||||
@@ -51,9 +51,6 @@ class PowerStatus : public Status
|
||||
|
||||
bool getHasUSB() const { return hasUSB == OptTrue; }
|
||||
|
||||
/// Can we even know if this board has USB power or not
|
||||
bool knowsUSB() const { return hasUSB != OptUnknown; }
|
||||
|
||||
bool getIsCharging() const { return isCharging == OptTrue; }
|
||||
|
||||
int getBatteryVoltageMv() const { return batteryVoltageMv; }
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#include "RedirectablePrint.h"
|
||||
#include "NodeDB.h"
|
||||
#include "RTC.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "main.h"
|
||||
#include "memGet.h"
|
||||
#include "mesh/generated/meshtastic/mesh.pb.h"
|
||||
|
||||
+116
-10
@@ -3,7 +3,9 @@
|
||||
#include "NodeDB.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "Throttle.h"
|
||||
#include "concurrency/LockGuard.h"
|
||||
#include "configuration.h"
|
||||
#include "main.h"
|
||||
#include "time.h"
|
||||
|
||||
#if defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT
|
||||
@@ -28,6 +30,17 @@
|
||||
|
||||
SerialConsole *console;
|
||||
|
||||
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||||
// Last-seen USB-CDC host link (DTR/mount) state, sampled each runOnce() so a
|
||||
// physical unplug/replug re-locks the per-connection admin auth (see runOnce()).
|
||||
// Kept at file scope rather than as a member both because there is exactly one
|
||||
// console singleton and because adding per-instance members to the PhoneAPI
|
||||
// hierarchy has historically perturbed nRF52 USB-CDC enumeration (see PhoneAPI.h).
|
||||
// Only compiled on lockdown (nRF52) builds.
|
||||
static bool s_serialLinkUp = false;
|
||||
#endif
|
||||
|
||||
/// Create the shared serial console once and register receive wakeups.
|
||||
void consoleInit()
|
||||
{
|
||||
if (console) {
|
||||
@@ -44,6 +57,7 @@ void consoleInit()
|
||||
DEBUG_PORT.rpInit(); // Simply sets up semaphore
|
||||
}
|
||||
|
||||
/// Print and flush an unclassified formatted console message.
|
||||
void consolePrintf(const char *format, ...)
|
||||
{
|
||||
va_list arg;
|
||||
@@ -53,6 +67,7 @@ void consolePrintf(const char *format, ...)
|
||||
console->flush();
|
||||
}
|
||||
|
||||
/// Initialize console, protobuf transport, serial port, and worker thread state.
|
||||
SerialConsole::SerialConsole() : StreamAPI(&Port), RedirectablePrint(&Port), concurrency::OSThread("SerialConsole")
|
||||
{
|
||||
api_type = TYPE_SERIAL;
|
||||
@@ -80,8 +95,33 @@ SerialConsole::SerialConsole() : StreamAPI(&Port), RedirectablePrint(&Port), con
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Service one serial API iteration and select the next polling interval.
|
||||
int32_t SerialConsole::runOnce()
|
||||
{
|
||||
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||||
// Lockdown (nRF52) builds only. The SerialConsole is a process-lifetime
|
||||
// singleton, so its inherited PhoneAPI object - and therefore its entry in
|
||||
// the per-connection admin-auth slot table (keyed by PhoneAPI*) - is reused
|
||||
// for every USB/serial client for the whole boot. Nothing re-locks that slot
|
||||
// when the operator unplugs and a different client plugs in before the
|
||||
// 15-minute inactivity timeout fires, so a fresh client would inherit the
|
||||
// prior operator's admin authorization. Re-lock when the physical USB-CDC link
|
||||
// drops - the serial analog of the BLE onDisconnect() -> close() session reset.
|
||||
//
|
||||
// On the nRF52 TinyUSB (Adafruit) core, (bool)Port == tud_cdc_n_connected():
|
||||
// it goes false on cable unplug or host port-close (DTR de-assert). close()
|
||||
// frees the auth slot and resets PhoneAPI state, so whoever connects next
|
||||
// re-locks via handleStartConfig()'s !isConnected() branch on their first
|
||||
// want_config - the same physical-link boundary BLE enforces in onConnect().
|
||||
// Console transports without a real DTR line (e.g. a UART USER_DEBUG_PORT) hold
|
||||
// this constant, so no edge fires and we fall back to the existing inactivity
|
||||
// timeout - no worse than the pre-fix behavior.
|
||||
const bool linkUp = static_cast<bool>(Port);
|
||||
if (s_serialLinkUp && !linkUp)
|
||||
close();
|
||||
s_serialLinkUp = linkUp;
|
||||
#endif
|
||||
|
||||
#ifdef HELTEC_MESH_SOLAR
|
||||
// After enabling the mesh solar serial port module configuration, command processing is handled by the serial port module.
|
||||
if (moduleConfig.serial.enabled && moduleConfig.serial.override_console_serial_port &&
|
||||
@@ -100,29 +140,48 @@ int32_t SerialConsole::runOnce()
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Flush raw output while preserving queued protobuf frames.
|
||||
void SerialConsole::flush()
|
||||
{
|
||||
// HWCDC::flush()'s no-progress path discards queued TX bytes, which would tear a
|
||||
// framed protobuf stream; framed output is drained by the TX interrupt instead.
|
||||
if (usingProtobufs)
|
||||
return;
|
||||
|
||||
Port.flush();
|
||||
}
|
||||
|
||||
// trigger tx of serial data
|
||||
/// Write raw console data only before protobuf framing becomes active.
|
||||
size_t SerialConsole::write(uint8_t c)
|
||||
{
|
||||
// Once a protobuf client is active, unframed bytes would corrupt its stream.
|
||||
if (usingProtobufs)
|
||||
return 1;
|
||||
|
||||
if (c == '\n')
|
||||
RedirectablePrint::write('\r');
|
||||
return RedirectablePrint::write(c);
|
||||
}
|
||||
|
||||
/// Wake the serial worker when PhoneAPI queues output.
|
||||
void SerialConsole::onNowHasData(uint32_t fromRadioNum)
|
||||
{
|
||||
setIntervalFromNow(0);
|
||||
}
|
||||
|
||||
// trigger rx of serial data
|
||||
/// Wake the serial worker when receive activity is signaled.
|
||||
void SerialConsole::rxInt()
|
||||
{
|
||||
setIntervalFromNow(0);
|
||||
}
|
||||
|
||||
// For the serial port we can't really detect if any client is on the other side, so instead just look for recent messages
|
||||
/// Infer serial client connectivity from recent API contact.
|
||||
bool SerialConsole::checkIsConnected()
|
||||
{
|
||||
return Throttle::isWithinTimespanMs(lastContactMsec, SERIAL_CONNECTION_TIMEOUT);
|
||||
}
|
||||
|
||||
/// Select bounded or non-blocking HWCDC writes based on host liveness.
|
||||
void SerialConsole::setHostDraining(bool draining)
|
||||
{
|
||||
#ifdef IS_USB_SERIAL
|
||||
@@ -134,17 +193,59 @@ void SerialConsole::setHostDraining(bool draining)
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Update HWCDC timeout mode around generic connection handling.
|
||||
void SerialConsole::onConnectionChanged(bool connected)
|
||||
{
|
||||
// Order matters on disconnect: make console TX non-blocking *before* the
|
||||
// PowerFSM/close handling below emits more log lines to a dead port.
|
||||
if (!connected)
|
||||
if (!connected) {
|
||||
setHostDraining(false);
|
||||
// Keep any retained tail: HWCDC may still hold its prefix, and dropping metadata
|
||||
// would let the next frame header land inside that frame's declared payload.
|
||||
}
|
||||
StreamAPI::onConnectionChanged(connected);
|
||||
if (connected)
|
||||
setHostDraining(true);
|
||||
}
|
||||
|
||||
/// Continue retained USB CDC output under the shared stream lock.
|
||||
bool SerialConsole::finishPendingFrame()
|
||||
{
|
||||
#ifdef IS_USB_SERIAL
|
||||
concurrency::LockGuard guard(&streamLock);
|
||||
return frameWriter.finishPendingFrame(Port);
|
||||
#else
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Protect the retained log buffer from being overwritten.
|
||||
bool SerialConsole::canEncodeLogRecord()
|
||||
{
|
||||
#ifdef IS_USB_SERIAL
|
||||
concurrency::LockGuard guard(&streamLock);
|
||||
return frameWriter.isIdle();
|
||||
#else
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Frame USB CDC output and retain any unwritten tail.
|
||||
bool SerialConsole::writeFrame(uint8_t *buf, size_t len, bool bestEffort)
|
||||
{
|
||||
#ifdef IS_USB_SERIAL
|
||||
if (len == 0 || !canWrite)
|
||||
return false;
|
||||
|
||||
const size_t totalLen = buildFrameHeader(buf, len);
|
||||
|
||||
concurrency::LockGuard guard(&streamLock);
|
||||
return frameWriter.writeFrame(Port, buf, totalLen, bestEffort);
|
||||
#else
|
||||
return StreamAPI::writeFrame(buf, len, bestEffort);
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* we override this to notice when we've received a protobuf over the serial
|
||||
* stream. Then we shut off debug serial output.
|
||||
@@ -167,12 +268,17 @@ bool SerialConsole::handleToRadio(const uint8_t *buf, size_t len)
|
||||
}
|
||||
}
|
||||
|
||||
/// Route logs without allowing raw bytes into an active protobuf stream.
|
||||
void SerialConsole::log_to_serial(const char *logLevel, const char *format, va_list arg)
|
||||
{
|
||||
if (usingProtobufs && config.security.debug_log_api_enabled) {
|
||||
meshtastic_LogRecord_Level ll = RedirectablePrint::getLogLevel(logLevel);
|
||||
auto thread = concurrency::OSThread::currentThread;
|
||||
emitLogRecord(ll, thread ? thread->ThreadName.c_str() : "", format, arg);
|
||||
} else
|
||||
RedirectablePrint::log_to_serial(logLevel, format, arg);
|
||||
if (usingProtobufs) {
|
||||
if (config.security.debug_log_api_enabled && !pauseBluetoothLogging) {
|
||||
meshtastic_LogRecord_Level ll = RedirectablePrint::getLogLevel(logLevel);
|
||||
auto thread = concurrency::OSThread::currentThread;
|
||||
emitLogRecord(ll, thread ? thread->ThreadName.c_str() : "", format, arg);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
RedirectablePrint::log_to_serial(logLevel, format, arg);
|
||||
}
|
||||
|
||||
+20
-7
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "RedirectablePrint.h"
|
||||
#include "StreamAPI.h"
|
||||
#include "mesh/StreamFrameWriter.h"
|
||||
/**
|
||||
* Provides both debug printing and, if the client starts sending protobufs to us, switches to send/receive protobufs
|
||||
* (and starts dropping debug printing - FIXME, eventually those prints should be encapsulated in protobufs).
|
||||
@@ -14,6 +15,7 @@ class SerialConsole : public StreamAPI, public RedirectablePrint, private concur
|
||||
bool usingProtobufs = false;
|
||||
|
||||
public:
|
||||
/// Initialize the shared serial stream for console and protobuf traffic.
|
||||
SerialConsole();
|
||||
|
||||
/**
|
||||
@@ -22,35 +24,46 @@ class SerialConsole : public StreamAPI, public RedirectablePrint, private concur
|
||||
*/
|
||||
virtual bool handleToRadio(const uint8_t *buf, size_t len) override;
|
||||
|
||||
virtual size_t write(uint8_t c) override
|
||||
{
|
||||
if (c == '\n') // prefix any newlines with carriage return
|
||||
RedirectablePrint::write('\r');
|
||||
return RedirectablePrint::write(c);
|
||||
}
|
||||
/// Write a raw console byte unless the stream is in protobuf mode.
|
||||
virtual size_t write(uint8_t c) override;
|
||||
|
||||
/// Service serial input, pending output, and connection state.
|
||||
virtual int32_t runOnce() override;
|
||||
|
||||
/// Flush raw console output when explicitly requested.
|
||||
void flush();
|
||||
/// Wake the serial thread after receive activity.
|
||||
void rxInt();
|
||||
|
||||
protected:
|
||||
/// Check the current underlying physical link to see if the client is currently connected
|
||||
virtual bool checkIsConnected() override;
|
||||
|
||||
/// Wake the serial thread when PhoneAPI queues output.
|
||||
virtual void onNowHasData(uint32_t fromRadioNum) override;
|
||||
|
||||
/// Track serial API connect/disconnect so we can make console writes
|
||||
/// non-blocking while no host is listening (see setHostDraining()).
|
||||
virtual void onConnectionChanged(bool connected) override;
|
||||
|
||||
/// Possibly switch to protobufs if we see a valid protobuf message
|
||||
/// Emit a framed API log when enabled, or raw output before protobuf mode.
|
||||
virtual void log_to_serial(const char *logLevel, const char *format, va_list arg);
|
||||
|
||||
/// Continue retained USB CDC output before PhoneAPI advances.
|
||||
virtual bool finishPendingFrame() override;
|
||||
/// Return whether the dedicated log buffer can be safely overwritten.
|
||||
virtual bool canEncodeLogRecord() override;
|
||||
/// Write or retain one framed USB CDC message.
|
||||
virtual bool writeFrame(uint8_t *buf, size_t len, bool bestEffort) override;
|
||||
|
||||
private:
|
||||
/// On USB CDC targets, keep console TX non-blocking unless a host is draining the
|
||||
/// port, so a dead host can't stall the main loop and trip the task watchdog.
|
||||
void setHostDraining(bool draining);
|
||||
|
||||
#if defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT
|
||||
StreamFrameWriter frameWriter;
|
||||
#endif
|
||||
};
|
||||
|
||||
// A simple wrapper to allow non class aware code write to the console
|
||||
|
||||
@@ -88,6 +88,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#define MESHTASTIC_PREHOP_DROP 1
|
||||
#endif
|
||||
|
||||
// Debug/test only: let a wired client (serial/TCP) inject frames into the RX pipeline as if they had
|
||||
// arrived over LoRa - a SIMULATOR_APP ToRadio packet is delivered through the real receive path on real
|
||||
// hardware (see MeshService::injectAsReceived). This forges over-the-air traffic, so it MUST stay 0 in
|
||||
// any shipping build; enable per-build with -D MESHTASTIC_ENABLE_FRAME_INJECTION=1.
|
||||
#ifndef MESHTASTIC_ENABLE_FRAME_INJECTION
|
||||
#define MESHTASTIC_ENABLE_FRAME_INJECTION 0
|
||||
#endif
|
||||
|
||||
/// Convert a preprocessor name into a quoted string
|
||||
#define xstr(s) ystr(s)
|
||||
#define ystr(s) #s
|
||||
@@ -416,6 +424,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#ifndef HAS_TFT
|
||||
#define HAS_TFT 0
|
||||
#endif
|
||||
// Opt-in: build the BaseUI games frame (Snake). Off by default; enable per build/variant with
|
||||
// -DBASEUI_HAS_GAMES=1 (requires HAS_SCREEN and a non-color BaseUI display).
|
||||
#ifndef BASEUI_HAS_GAMES
|
||||
#define BASEUI_HAS_GAMES 0
|
||||
#endif
|
||||
#ifndef HAS_WIRE
|
||||
#define HAS_WIRE 0
|
||||
#endif
|
||||
|
||||
+1
-1
@@ -8,10 +8,10 @@
|
||||
#include "GpioLogic.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PowerMon.h"
|
||||
#include "RTC.h"
|
||||
#include "Throttle.h"
|
||||
#include "buzz.h"
|
||||
#include "concurrency/Periodic.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "meshUtils.h"
|
||||
|
||||
#include "main.h" // pmu_found
|
||||
|
||||
+38
-5
@@ -36,19 +36,52 @@ GeoCoord::GeoCoord(double lat, double lon, int32_t alt) : _altitude(alt)
|
||||
GeoCoord::setCoords();
|
||||
}
|
||||
|
||||
// Initialize all the coordinate systems
|
||||
// Initialize DMS eagerly (cheap, most commonly used); UTM/MGRS/OSGR/OLC are computed lazily on
|
||||
// first access via their getters (see ensure*() below), since most callers never touch them.
|
||||
void GeoCoord::setCoords()
|
||||
{
|
||||
double lat = _latitude * 1e-7;
|
||||
double lon = _longitude * 1e-7;
|
||||
GeoCoord::latLongToDMS(lat, lon, _dms);
|
||||
GeoCoord::latLongToUTM(lat, lon, _utm);
|
||||
GeoCoord::latLongToMGRS(lat, lon, _mgrs);
|
||||
GeoCoord::latLongToOSGR(lat, lon, _osgr);
|
||||
GeoCoord::latLongToOLC(lat, lon, _olc);
|
||||
_utmValid = false;
|
||||
_mgrsValid = false;
|
||||
_osgrValid = false;
|
||||
_olcValid = false;
|
||||
_dirty = false;
|
||||
}
|
||||
|
||||
void GeoCoord::ensureUTM() const
|
||||
{
|
||||
if (!_utmValid) {
|
||||
GeoCoord::latLongToUTM(_latitude * 1e-7, _longitude * 1e-7, _utm);
|
||||
_utmValid = true;
|
||||
}
|
||||
}
|
||||
|
||||
void GeoCoord::ensureMGRS() const
|
||||
{
|
||||
if (!_mgrsValid) {
|
||||
GeoCoord::latLongToMGRS(_latitude * 1e-7, _longitude * 1e-7, _mgrs);
|
||||
_mgrsValid = true;
|
||||
}
|
||||
}
|
||||
|
||||
void GeoCoord::ensureOSGR() const
|
||||
{
|
||||
if (!_osgrValid) {
|
||||
GeoCoord::latLongToOSGR(_latitude * 1e-7, _longitude * 1e-7, _osgr);
|
||||
_osgrValid = true;
|
||||
}
|
||||
}
|
||||
|
||||
void GeoCoord::ensureOLC() const
|
||||
{
|
||||
if (!_olcValid) {
|
||||
GeoCoord::latLongToOLC(_latitude * 1e-7, _longitude * 1e-7, _olc);
|
||||
_olcValid = true;
|
||||
}
|
||||
}
|
||||
|
||||
void GeoCoord::updateCoords(int32_t lat, int32_t lon, int32_t alt)
|
||||
{
|
||||
// If marked dirty or new coordinates
|
||||
|
||||
+93
-23
@@ -65,14 +65,24 @@ class GeoCoord
|
||||
int32_t _altitude = 0;
|
||||
|
||||
DMS _dms = {};
|
||||
UTM _utm = {};
|
||||
MGRS _mgrs = {};
|
||||
OSGR _osgr = {};
|
||||
OLC _olc = {};
|
||||
// Computed lazily on first access via ensure*() below; mutable so const getters can populate
|
||||
// them on demand.
|
||||
mutable UTM _utm = {};
|
||||
mutable MGRS _mgrs = {};
|
||||
mutable OSGR _osgr = {};
|
||||
mutable OLC _olc = {};
|
||||
mutable bool _utmValid = false;
|
||||
mutable bool _mgrsValid = false;
|
||||
mutable bool _osgrValid = false;
|
||||
mutable bool _olcValid = false;
|
||||
|
||||
bool _dirty = true;
|
||||
|
||||
void setCoords();
|
||||
void ensureUTM() const;
|
||||
void ensureMGRS() const;
|
||||
void ensureOSGR() const;
|
||||
void ensureOLC() const;
|
||||
|
||||
public:
|
||||
GeoCoord();
|
||||
@@ -123,26 +133,86 @@ class GeoCoord
|
||||
uint32_t getDMSLonSec() const { return _dms.lonSec; }
|
||||
char getDMSLonCP() const { return _dms.lonCP; }
|
||||
|
||||
// UTM getters
|
||||
uint8_t getUTMZone() const { return _utm.zone; }
|
||||
char getUTMBand() const { return _utm.band; }
|
||||
uint32_t getUTMEasting() const { return _utm.easting; }
|
||||
uint32_t getUTMNorthing() const { return _utm.northing; }
|
||||
// UTM getters (computed on first access - see ensureUTM())
|
||||
uint8_t getUTMZone() const
|
||||
{
|
||||
ensureUTM();
|
||||
return _utm.zone;
|
||||
}
|
||||
char getUTMBand() const
|
||||
{
|
||||
ensureUTM();
|
||||
return _utm.band;
|
||||
}
|
||||
uint32_t getUTMEasting() const
|
||||
{
|
||||
ensureUTM();
|
||||
return _utm.easting;
|
||||
}
|
||||
uint32_t getUTMNorthing() const
|
||||
{
|
||||
ensureUTM();
|
||||
return _utm.northing;
|
||||
}
|
||||
|
||||
// MGRS getters
|
||||
uint8_t getMGRSZone() const { return _mgrs.zone; }
|
||||
char getMGRSBand() const { return _mgrs.band; }
|
||||
char getMGRSEast100k() const { return _mgrs.east100k; }
|
||||
char getMGRSNorth100k() const { return _mgrs.north100k; }
|
||||
uint32_t getMGRSEasting() const { return _mgrs.easting; }
|
||||
uint32_t getMGRSNorthing() const { return _mgrs.northing; }
|
||||
// MGRS getters (computed on first access - see ensureMGRS())
|
||||
uint8_t getMGRSZone() const
|
||||
{
|
||||
ensureMGRS();
|
||||
return _mgrs.zone;
|
||||
}
|
||||
char getMGRSBand() const
|
||||
{
|
||||
ensureMGRS();
|
||||
return _mgrs.band;
|
||||
}
|
||||
char getMGRSEast100k() const
|
||||
{
|
||||
ensureMGRS();
|
||||
return _mgrs.east100k;
|
||||
}
|
||||
char getMGRSNorth100k() const
|
||||
{
|
||||
ensureMGRS();
|
||||
return _mgrs.north100k;
|
||||
}
|
||||
uint32_t getMGRSEasting() const
|
||||
{
|
||||
ensureMGRS();
|
||||
return _mgrs.easting;
|
||||
}
|
||||
uint32_t getMGRSNorthing() const
|
||||
{
|
||||
ensureMGRS();
|
||||
return _mgrs.northing;
|
||||
}
|
||||
|
||||
// OSGR getters
|
||||
char getOSGRE100k() const { return _osgr.e100k; }
|
||||
char getOSGRN100k() const { return _osgr.n100k; }
|
||||
uint32_t getOSGREasting() const { return _osgr.easting; }
|
||||
uint32_t getOSGRNorthing() const { return _osgr.northing; }
|
||||
// OSGR getters (computed on first access - see ensureOSGR())
|
||||
char getOSGRE100k() const
|
||||
{
|
||||
ensureOSGR();
|
||||
return _osgr.e100k;
|
||||
}
|
||||
char getOSGRN100k() const
|
||||
{
|
||||
ensureOSGR();
|
||||
return _osgr.n100k;
|
||||
}
|
||||
uint32_t getOSGREasting() const
|
||||
{
|
||||
ensureOSGR();
|
||||
return _osgr.easting;
|
||||
}
|
||||
uint32_t getOSGRNorthing() const
|
||||
{
|
||||
ensureOSGR();
|
||||
return _osgr.northing;
|
||||
}
|
||||
|
||||
// OLC getter
|
||||
void getOLCCode(char *code) { strncpy(code, _olc.code, OLC_CODE_LEN + 1); } // +1 for null termination
|
||||
// OLC getter (computed on first access - see ensureOLC())
|
||||
void getOLCCode(char *code)
|
||||
{
|
||||
ensureOLC();
|
||||
strncpy(code, _olc.code, OLC_CODE_LEN + 1); // +1 for null termination
|
||||
}
|
||||
};
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
#if !MESHTASTIC_EXCLUDE_GPS
|
||||
#include "NMEAWPL.h"
|
||||
#include "GeoCoord.h"
|
||||
#include "RTC.h"
|
||||
#include "gps/RTC.h"
|
||||
#include <time.h>
|
||||
|
||||
/* -------------------------------------------
|
||||
|
||||
+45
-4
@@ -1,4 +1,4 @@
|
||||
#include "RTC.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "configuration.h"
|
||||
#include "detect/ScanI2C.h"
|
||||
#include "main.h"
|
||||
@@ -7,6 +7,10 @@
|
||||
#include <sys/time.h>
|
||||
#include <time.h>
|
||||
|
||||
#if HAS_LSE
|
||||
#include <STM32RTC.h>
|
||||
#endif
|
||||
|
||||
static RTCQuality currentQuality = RTCQualityNone;
|
||||
uint32_t lastSetFromPhoneNtpOrGps = 0;
|
||||
|
||||
@@ -211,6 +215,30 @@ RTCSetResult readFromRTC()
|
||||
return RTCSetResultSuccess;
|
||||
}
|
||||
}
|
||||
#elif HAS_LSE
|
||||
if (stm32wlRtcAvailable()) {
|
||||
uint32_t now = millis();
|
||||
tv.tv_sec = STM32RTC::getInstance().getEpoch();
|
||||
tv.tv_usec = 0;
|
||||
uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms
|
||||
#ifdef BUILD_EPOCH
|
||||
if (tv.tv_sec < BUILD_EPOCH) {
|
||||
if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) {
|
||||
LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
|
||||
lastTimeValidationWarning = millis();
|
||||
}
|
||||
return RTCSetResultInvalidTime;
|
||||
}
|
||||
#endif
|
||||
if (currentQuality == RTCQualityNone) {
|
||||
RTCQuality oldQuality = currentQuality;
|
||||
timeStartMsec = now;
|
||||
zeroOffsetSecs = tv.tv_sec;
|
||||
currentQuality = RTCQualityDevice;
|
||||
triggerNodeInfoCheckOnTimeSource(oldQuality, currentQuality);
|
||||
}
|
||||
return RTCSetResultSuccess;
|
||||
}
|
||||
#else
|
||||
return readFromSystemTimeFallback();
|
||||
#endif
|
||||
@@ -291,7 +319,10 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
|
||||
#else
|
||||
rtc.initI2C();
|
||||
#endif
|
||||
tm *t = gmtime(&tv->tv_sec);
|
||||
// tv_sec is a long, which is not time_t everywhere: on Windows
|
||||
// time_t is 64-bit while long is 32-bit. Copy before taking &.
|
||||
time_t setSecs = tv->tv_sec;
|
||||
tm *t = gmtime(&setSecs);
|
||||
rtc.setTime(t->tm_year + 1900, t->tm_mon + 1, t->tm_wday, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec);
|
||||
LOG_DEBUG("RV3028_RTC setTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", t->tm_year + 1900, t->tm_mon + 1, t->tm_mday,
|
||||
t->tm_hour, t->tm_min, t->tm_sec, printableEpoch);
|
||||
@@ -313,7 +344,10 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
|
||||
#else
|
||||
rtc.begin(Wire);
|
||||
#endif
|
||||
tm *t = gmtime(&tv->tv_sec);
|
||||
// tv_sec is a long, which is not time_t everywhere: on Windows
|
||||
// time_t is 64-bit while long is 32-bit. Copy before taking &.
|
||||
time_t setSecs = tv->tv_sec;
|
||||
tm *t = gmtime(&setSecs);
|
||||
rtc.setDateTime(*t);
|
||||
LOG_DEBUG("%s setDateTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", rtc.getChipName(), t->tm_year + 1900, t->tm_mon + 1,
|
||||
t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec, printableEpoch);
|
||||
@@ -327,7 +361,10 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
|
||||
#else
|
||||
ArtronShop_RX8130CE rtc(&Wire);
|
||||
#endif
|
||||
tm *t = gmtime(&tv->tv_sec);
|
||||
// tv_sec is a long, which is not time_t everywhere: on Windows
|
||||
// time_t is 64-bit while long is 32-bit. Copy before taking &.
|
||||
time_t setSecs = tv->tv_sec;
|
||||
tm *t = gmtime(&setSecs);
|
||||
if (rtc.setTime(*t)) {
|
||||
LOG_DEBUG("RX8130CE setDateTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", t->tm_year + 1900, t->tm_mon + 1,
|
||||
t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec, printableEpoch);
|
||||
@@ -335,6 +372,10 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
|
||||
LOG_WARN("Failed to set time for RX8130CE");
|
||||
}
|
||||
}
|
||||
#elif HAS_LSE
|
||||
if (stm32wlRtcAvailable()) {
|
||||
STM32RTC::getInstance().setEpoch(tv->tv_sec);
|
||||
}
|
||||
#elif defined(ARCH_ESP32) || defined(ARCH_RP2040)
|
||||
settimeofday(tv, NULL);
|
||||
#endif
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
#include <ArtronShop_RX8130CE.h>
|
||||
#endif
|
||||
|
||||
#if HAS_LSE
|
||||
// True once the STM32WL LSE crystal has locked and the hardware RTC is running (see stm32wlSetup()).
|
||||
bool stm32wlRtcAvailable();
|
||||
#endif
|
||||
|
||||
enum RTCQuality {
|
||||
|
||||
/// We haven't had our RTC set yet
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#if defined(USE_EINK) && !defined(USE_EINK_PARALLELDISPLAY)
|
||||
#include "EInkDisplay2.h"
|
||||
#include "FSCommon.h"
|
||||
#include "SPILock.h"
|
||||
#include "main.h"
|
||||
#include <SPI.h>
|
||||
@@ -214,9 +215,23 @@ bool EInkDisplay::connect()
|
||||
defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(CROWPANEL_ESP32S3_4_EPAPER) || defined(CROWPANEL_ESP32S3_2_EPAPER) || \
|
||||
defined(MINI_EPAPER_S3)
|
||||
{
|
||||
#if defined(TLORA_T3S3_EPAPER)
|
||||
// T3-S3 shares HSPI with the SD card; preconfigure the panel control pins.
|
||||
hspi = &SPI_HSPI;
|
||||
pinMode(PIN_EINK_CS, OUTPUT);
|
||||
pinMode(PIN_EINK_DC, OUTPUT);
|
||||
pinMode(PIN_EINK_BUSY, INPUT);
|
||||
if (PIN_EINK_RES >= 0) {
|
||||
pinMode(PIN_EINK_RES, OUTPUT);
|
||||
digitalWrite(PIN_EINK_RES, HIGH);
|
||||
}
|
||||
digitalWrite(PIN_EINK_CS, HIGH);
|
||||
digitalWrite(PIN_EINK_DC, HIGH);
|
||||
#else
|
||||
// Start HSPI
|
||||
hspi = new SPIClass(HSPI);
|
||||
hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); // SCLK, MISO, MOSI, SS
|
||||
#endif
|
||||
// VExt already enabled in setup()
|
||||
// RTC GPIO hold disabled in setup()
|
||||
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if defined(USE_HUB75)
|
||||
|
||||
#include "HUB75Display.h"
|
||||
#include "TFTColorRegions.h"
|
||||
#include "TFTPalette.h"
|
||||
#include <ESP32-HUB75-MatrixPanel-I2S-DMA.h>
|
||||
#include <string.h>
|
||||
|
||||
HUB75Display::HUB75Display(uint8_t, int, int, OLEDDISPLAY_GEOMETRY, HW_I2C)
|
||||
{
|
||||
// The BaseUI treats the panel as a generic raw framebuffer (not an SSD1306
|
||||
// page layout). Geometry is fixed by the wired panel size.
|
||||
#if defined(SCREEN_ROTATE)
|
||||
setGeometry(GEOMETRY_RAWMODE, TFT_HEIGHT, TFT_WIDTH);
|
||||
#else
|
||||
setGeometry(GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT);
|
||||
#endif
|
||||
LOG_DEBUG("HUB75Display %dx%d", (int)TFT_WIDTH, (int)TFT_HEIGHT);
|
||||
}
|
||||
|
||||
HUB75Display::~HUB75Display()
|
||||
{
|
||||
if (matrix) {
|
||||
delete matrix;
|
||||
matrix = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// Bring up the matrix DMA driver from the wired HUB75 pins.
|
||||
bool HUB75Display::connect()
|
||||
{
|
||||
LOG_INFO("Do HUB75 init");
|
||||
|
||||
HUB75_I2S_CFG::i2s_pins pins = {HUB75_R1, HUB75_G1, HUB75_B1, HUB75_R2, HUB75_G2, HUB75_B2, HUB75_A,
|
||||
HUB75_B, HUB75_C, HUB75_D, HUB75_E, HUB75_LAT, HUB75_OE, HUB75_CLK};
|
||||
|
||||
HUB75_I2S_CFG cfg(TFT_WIDTH, TFT_HEIGHT, 1 /* chain length */, pins);
|
||||
|
||||
cfg.i2sspeed = HUB75_I2S_CFG::HZ_8M; // timing headroom (signal integrity)
|
||||
cfg.latch_blanking = 4; // clean row transitions
|
||||
cfg.clkphase = false; // inverted clock phase: fixes 1px skew of lower half
|
||||
cfg.double_buff = false; // single buffer: halves the internal-SRAM DMA
|
||||
// footprint. display() draws directly into the
|
||||
// live buffer with a dirty-diff
|
||||
|
||||
matrix = new MatrixPanel_I2S_DMA(cfg);
|
||||
bool ok = matrix->begin();
|
||||
if (!ok) {
|
||||
LOG_ERROR("HUB75 matrix->begin() failed (DMA buffer alloc?)");
|
||||
delete matrix;
|
||||
matrix = nullptr;
|
||||
return false;
|
||||
}
|
||||
matrix->setBrightness8(brightness);
|
||||
matrix->clearScreen();
|
||||
return true;
|
||||
}
|
||||
|
||||
void HUB75Display::display()
|
||||
{
|
||||
if (!matrix)
|
||||
return;
|
||||
|
||||
const uint16_t onNative = graphics::TFTPalette::White;
|
||||
const uint16_t offNative = graphics::getThemeBodyBg();
|
||||
|
||||
const uint16_t onBe = (uint16_t)((onNative >> 8) | (onNative << 8));
|
||||
const uint16_t offBe = (uint16_t)((offNative >> 8) | (offNative << 8));
|
||||
|
||||
bool forceFull = firstFrame || !haveThemeDefaults || onBe != lastOnBe || offBe != lastOffBe;
|
||||
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
const bool hasColorRegions = graphics::getTFTColorRegionCount() > 0;
|
||||
const uint32_t colorSig = graphics::getTFTColorFrameSignature();
|
||||
if (colorSig != lastColorSig)
|
||||
forceFull = true;
|
||||
#endif
|
||||
|
||||
for (uint16_t y = 0; y < displayHeight; y++) {
|
||||
const uint32_t yByteIndex = (y / 8) * displayWidth;
|
||||
const uint8_t yByteMask = (uint8_t)(1 << (y & 7));
|
||||
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
if (hasColorRegions)
|
||||
graphics::beginTFTColorRow((int16_t)y);
|
||||
#endif
|
||||
|
||||
for (uint16_t x = 0; x < displayWidth; x++) {
|
||||
const bool isset = (buffer[x + yByteIndex] & yByteMask) != 0;
|
||||
|
||||
// Skip pixels whose mono bit is unchanged (unless a full repaint is
|
||||
// forced). The panel is single-buffered, so untouched pixels persist.
|
||||
if (!forceFull && (((buffer_back[x + yByteIndex] & yByteMask) != 0) == isset))
|
||||
continue;
|
||||
|
||||
uint16_t be;
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
if (hasColorRegions)
|
||||
be = graphics::resolveTFTColorPixelRow((int16_t)x, isset, onBe, offBe);
|
||||
else
|
||||
be = isset ? onBe : offBe;
|
||||
#else
|
||||
be = isset ? onBe : offBe;
|
||||
#endif
|
||||
|
||||
const uint16_t c = (uint16_t)((be >> 8) | (be << 8)); // back to native RGB565
|
||||
const uint8_t r = (uint8_t)(((c >> 11) & 0x1F) << 3);
|
||||
const uint8_t g = (uint8_t)(((c >> 5) & 0x3F) << 2);
|
||||
const uint8_t b = (uint8_t)((c & 0x1F) << 3);
|
||||
matrix->drawPixelRGB888(x, y, r, g, b);
|
||||
}
|
||||
}
|
||||
|
||||
// Remember what the panel now shows so the next frame can diff against it.
|
||||
memcpy(buffer_back, buffer, displayBufferSize);
|
||||
|
||||
haveThemeDefaults = true;
|
||||
lastOnBe = onBe;
|
||||
lastOffBe = offBe;
|
||||
firstFrame = false;
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
lastColorSig = colorSig;
|
||||
// Regions are re-registered every frame by the renderers; clear so they
|
||||
// don't accumulate across frames.
|
||||
graphics::clearTFTColorRegions();
|
||||
#endif
|
||||
}
|
||||
|
||||
void HUB75Display::sendCommand(uint8_t com)
|
||||
{
|
||||
if (!matrix)
|
||||
return;
|
||||
|
||||
switch (com) {
|
||||
case DISPLAYON:
|
||||
matrix->setBrightness8(brightness);
|
||||
break;
|
||||
case DISPLAYOFF:
|
||||
matrix->setBrightness8(0);
|
||||
break;
|
||||
default:
|
||||
// Drop all other SSD1306 init/config commands - not meaningful for the matrix.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void HUB75Display::setDisplayBrightness(uint8_t _brightness)
|
||||
{
|
||||
brightness = _brightness;
|
||||
if (matrix)
|
||||
matrix->setBrightness8(brightness);
|
||||
}
|
||||
|
||||
#endif // USE_HUB75
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#if defined(USE_HUB75)
|
||||
|
||||
#include <OLEDDisplay.h>
|
||||
|
||||
#ifndef HUB75_BRIGHTNESS_DEFAULT
|
||||
#define HUB75_BRIGHTNESS_DEFAULT 180
|
||||
#endif
|
||||
|
||||
class MatrixPanel_I2S_DMA;
|
||||
|
||||
/**
|
||||
* A color display backend that drives a HUB75 RGB LED matrix panel from the
|
||||
* BaseUI color path.
|
||||
*/
|
||||
class HUB75Display : public OLEDDisplay
|
||||
{
|
||||
public:
|
||||
HUB75Display(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY geometry, HW_I2C i2cBus);
|
||||
~HUB75Display();
|
||||
|
||||
// Write the buffer to the matrix (full-frame, region-aware color expansion).
|
||||
void display() override;
|
||||
|
||||
void setDisplayBrightness(uint8_t brightness);
|
||||
|
||||
protected:
|
||||
int getBufferOffset(void) override { return 0; }
|
||||
|
||||
void sendCommand(uint8_t com) override;
|
||||
|
||||
bool connect() override;
|
||||
|
||||
private:
|
||||
MatrixPanel_I2S_DMA *matrix = nullptr;
|
||||
uint8_t brightness = HUB75_BRIGHTNESS_DEFAULT;
|
||||
|
||||
bool firstFrame = true;
|
||||
bool haveThemeDefaults = false;
|
||||
uint16_t lastOnBe = 0;
|
||||
uint16_t lastOffBe = 0;
|
||||
uint32_t lastColorSig = 0;
|
||||
};
|
||||
|
||||
#endif // USE_HUB75
|
||||
+110
-9
@@ -29,6 +29,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#if HAS_SCREEN
|
||||
#include "EInkParallelDisplay.h"
|
||||
#include <OLEDDisplay.h>
|
||||
#if defined(USE_HUB75)
|
||||
#include "graphics/HUB75Display.h" // ESP32 HUB75 (I2S-DMA)
|
||||
#endif
|
||||
#if defined(HAS_HUB75_NATIVE)
|
||||
#include "HUB75Native.h" // native/Portduino HUB75 (rpi-rgb-led-matrix), from variants/native/portduino
|
||||
#endif
|
||||
|
||||
#include "DisplayFormatters.h"
|
||||
#include "TimeFormatters.h"
|
||||
@@ -67,6 +73,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#include "mesh/Default.h"
|
||||
#include "mesh/generated/meshtastic/deviceonly.pb.h"
|
||||
#include "modules/ExternalNotificationModule.h"
|
||||
#if BASEUI_HAS_GAMES
|
||||
#include "modules/games/GamesModule.h"
|
||||
#endif
|
||||
#include "modules/WaypointModule.h"
|
||||
#include "sleep.h"
|
||||
#include "target_specific.h"
|
||||
@@ -99,7 +108,11 @@ namespace graphics
|
||||
#define COMPASS_ACTIVE_FRAMERATE 20
|
||||
|
||||
// DEBUG
|
||||
#if BASEUI_HAS_GAMES
|
||||
#define NUM_EXTRA_FRAMES 4 // text message, debug frame, and the always-present games frame
|
||||
#else
|
||||
#define NUM_EXTRA_FRAMES 3 // text message and debug frame
|
||||
#endif
|
||||
// if defined a pixel will blink to show redraws
|
||||
// #define SHOW_REDRAWS
|
||||
#define ASCII_BELL '\x07'
|
||||
@@ -343,7 +356,7 @@ void Screen::showNodePicker(const char *message, uint32_t durationMs, std::funct
|
||||
}
|
||||
|
||||
// Called to trigger a banner with custom message and duration
|
||||
void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits,
|
||||
void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, bool useBase16,
|
||||
std::function<void(uint32_t)> bannerCallback)
|
||||
{
|
||||
#ifdef USE_EINK
|
||||
@@ -356,7 +369,10 @@ void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t
|
||||
NotificationRenderer::alertBannerCallback = bannerCallback;
|
||||
NotificationRenderer::pauseBanner = false;
|
||||
NotificationRenderer::curSelected = 0;
|
||||
NotificationRenderer::current_notification_type = notificationTypeEnum::number_picker;
|
||||
if (useBase16)
|
||||
NotificationRenderer::current_notification_type = notificationTypeEnum::hex_picker;
|
||||
else
|
||||
NotificationRenderer::current_notification_type = notificationTypeEnum::number_picker;
|
||||
NotificationRenderer::numDigits = digits;
|
||||
NotificationRenderer::currentNumber = 0;
|
||||
|
||||
@@ -366,6 +382,43 @@ void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t
|
||||
updateUiFrame(ui);
|
||||
}
|
||||
|
||||
// Called to trigger an arcade-style initials picker (see showNumberPicker for the sibling flow).
|
||||
void Screen::showAlphanumericPicker(const char *message, const char *initialText, uint32_t durationMs, uint8_t length,
|
||||
std::function<void(const std::string &)> bannerCallback)
|
||||
{
|
||||
#ifdef USE_EINK
|
||||
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus
|
||||
#endif
|
||||
if (length >= sizeof(NotificationRenderer::alphanumericValue))
|
||||
length = sizeof(NotificationRenderer::alphanumericValue) - 1;
|
||||
|
||||
strncpy(NotificationRenderer::alertBannerMessage, message, 255);
|
||||
NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination
|
||||
NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs;
|
||||
NotificationRenderer::textInputCallback = bannerCallback;
|
||||
NotificationRenderer::pauseBanner = false;
|
||||
NotificationRenderer::curSelected = 0;
|
||||
NotificationRenderer::current_notification_type = notificationTypeEnum::alphanumeric_picker;
|
||||
NotificationRenderer::numDigits = length;
|
||||
|
||||
// Seed each position from initialText (uppercased & filtered to A-Z/0-9), defaulting to 'A'.
|
||||
const size_t seedLen = initialText ? strnlen(initialText, length) : 0;
|
||||
for (uint8_t i = 0; i < length; i++) {
|
||||
char c = (i < seedLen) ? initialText[i] : 'A';
|
||||
if (c >= 'a' && c <= 'z')
|
||||
c = static_cast<char>(c - 'a' + 'A');
|
||||
if (!((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')))
|
||||
c = 'A';
|
||||
NotificationRenderer::alphanumericValue[i] = c;
|
||||
}
|
||||
NotificationRenderer::alphanumericValue[length] = '\0';
|
||||
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, 2);
|
||||
ui->setTargetFPS(60);
|
||||
updateUiFrame(ui);
|
||||
}
|
||||
|
||||
void Screen::showTextInput(const char *header, const char *initialText, uint32_t durationMs,
|
||||
std::function<void(const std::string &)> textCallback)
|
||||
{
|
||||
@@ -409,6 +462,17 @@ static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int
|
||||
pi.drawFrame(display, state, x, y);
|
||||
}
|
||||
|
||||
#if BASEUI_HAS_GAMES
|
||||
// The games frame is a dedicated, always-present frame (unlike generic module frames it is placed
|
||||
// at a fixed position right after home), so it draws through its own trampoline rather than the
|
||||
// moduleFrames lockstep used by drawModuleFrame.
|
||||
static void drawGamesFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
if (gamesModule)
|
||||
gamesModule->drawFrame(display, state, x, y);
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Given a recent lat/lon return a guess of the heading the user is walking on.
|
||||
*
|
||||
@@ -496,6 +560,8 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
|
||||
#else
|
||||
dispdev = new ST7796Spi(&SPI1, ST7796_RESET, ST7796_RS, ST7796_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT);
|
||||
#endif
|
||||
#elif defined(USE_HUB75)
|
||||
dispdev = new HUB75Display(address.address, -1, -1, GEOMETRY_RAWMODE, HW_I2C::I2C_ONE);
|
||||
#elif defined(USE_SSD1306)
|
||||
dispdev = new SSD1306Wire(address.address, -1, -1, geometry,
|
||||
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
||||
@@ -514,7 +580,17 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
|
||||
LOG_INFO("SSD1306 init success");
|
||||
}
|
||||
#elif ARCH_PORTDUINO
|
||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||
|
||||
// HUB75 RGB matrix (hzeller/rpi-rgb-led-matrix) is a BaseUI framebuffer panel, selected at
|
||||
// runtime via config.yaml Display: Panel: HUB75.
|
||||
if (portduino_config.displayPanel == hub75) {
|
||||
#if defined(HAS_HUB75_NATIVE)
|
||||
LOG_DEBUG("Make HUB75Native!");
|
||||
dispdev = new HUB75Native(address.address, -1, -1, GEOMETRY_RAWMODE, HW_I2C::I2C_ONE);
|
||||
#else
|
||||
LOG_ERROR("HUB75 panel requested but rpi-rgb-led-matrix not compiled in!");
|
||||
#endif
|
||||
} else if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||
if (portduino_config.displayPanel != no_screen) {
|
||||
LOG_DEBUG("Make TFTDisplay!");
|
||||
dispdev = new TFTDisplay(address.address, -1, -1, geometry,
|
||||
@@ -1301,6 +1377,15 @@ void Screen::setFrames(FrameFocus focus)
|
||||
indicatorIcons.push_back(icon_home);
|
||||
}
|
||||
|
||||
#if BASEUI_HAS_GAMES
|
||||
// Games frame: always present (even with no game running), positioned directly after home.
|
||||
if (gamesModule) {
|
||||
fsi.positions.games = numframes;
|
||||
normalFrames[numframes++] = drawGamesFrame;
|
||||
indicatorIcons.push_back(joystick_small);
|
||||
}
|
||||
#endif
|
||||
|
||||
fsi.positions.textMessage = numframes;
|
||||
normalFrames[numframes++] = graphics::MessageRenderer::drawTextMessageFrame;
|
||||
indicatorIcons.push_back(icon_mail);
|
||||
@@ -2025,7 +2110,7 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
if (ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) {
|
||||
|
||||
if (event->inputEvent == INPUT_BROKER_UP) {
|
||||
if (messageStore.getMessages().empty()) {
|
||||
if (!messageStore.hasVisibleMessages()) {
|
||||
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
|
||||
} else {
|
||||
graphics::MessageRenderer::scrollUp();
|
||||
@@ -2035,7 +2120,7 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
}
|
||||
|
||||
if (event->inputEvent == INPUT_BROKER_DOWN) {
|
||||
if (messageStore.getMessages().empty()) {
|
||||
if (!messageStore.hasVisibleMessages()) {
|
||||
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
|
||||
} else {
|
||||
graphics::MessageRenderer::scrollDown();
|
||||
@@ -2073,14 +2158,20 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
if (module && module->interceptingKeyboardInput())
|
||||
inputIntercepted = true;
|
||||
}
|
||||
#if BASEUI_HAS_GAMES
|
||||
// The games frame isn't a moduleFrame, so check it explicitly: while a game is running it
|
||||
// owns the D-pad (turns/pause) and we must not switch frames or open menus underneath it.
|
||||
if (gamesModule && gamesModule->interceptingKeyboardInput())
|
||||
inputIntercepted = true;
|
||||
#endif
|
||||
|
||||
// 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());
|
||||
const bool isTextMessageFrame =
|
||||
(framesetInfo.positions.textMessage != 255 &&
|
||||
this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage && messageStore.hasVisibleMessages());
|
||||
if (isTextMessageFrame) {
|
||||
if (event->inputEvent == INPUT_BROKER_UP_LONG) {
|
||||
graphics::MessageRenderer::nudgeScroll(-1);
|
||||
@@ -2147,6 +2238,11 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
} else if (event->inputEvent == INPUT_BROKER_SELECT) {
|
||||
if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) {
|
||||
menuHandler::homeBaseMenu();
|
||||
#if BASEUI_HAS_GAMES
|
||||
} else if (gamesModule && framesetInfo.positions.games != 255 &&
|
||||
this->ui->getUiState()->currentFrame == framesetInfo.positions.games) {
|
||||
gamesModule->launchGame(); // launch the game shown on the attract screen
|
||||
#endif
|
||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.system) {
|
||||
menuHandler::systemBaseMenu();
|
||||
#if HAS_GPS
|
||||
@@ -2158,7 +2254,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 (messageStore.hasVisibleMessages()) {
|
||||
menuHandler::messageResponseMenu();
|
||||
} else {
|
||||
if (currentResolution == ScreenResolution::UltraLow) {
|
||||
@@ -2214,6 +2310,11 @@ bool Screen::isOverlayBannerShowing()
|
||||
return NotificationRenderer::isOverlayBannerShowing();
|
||||
}
|
||||
|
||||
bool Screen::isGamesFrameShown()
|
||||
{
|
||||
return framesetInfo.positions.games != 255 && ui && ui->getUiState()->currentFrame == framesetInfo.positions.games;
|
||||
}
|
||||
|
||||
} // namespace graphics
|
||||
|
||||
#else
|
||||
|
||||
+15
-1
@@ -26,6 +26,9 @@ enum notificationTypeEnum {
|
||||
// LOCKED frame. Without this, a first-pair on a locked device cannot
|
||||
// complete because the PIN never renders.
|
||||
pairing_pin,
|
||||
// Arcade-style initials entry: like number_picker/hex_picker, but each position cycles
|
||||
// through A-Z and 0-9. The assembled string is returned via a text (std::string) callback.
|
||||
alphanumeric_picker,
|
||||
};
|
||||
|
||||
struct BannerOverlayOptions {
|
||||
@@ -284,6 +287,10 @@ class Screen : public concurrency::OSThread
|
||||
|
||||
bool isOverlayBannerShowing();
|
||||
|
||||
// True if the always-present games frame is the one currently on screen. Lets the games module
|
||||
// ignore D-pad input when the player has navigated to a different frame.
|
||||
bool isGamesFrameShown();
|
||||
|
||||
bool isScreenOn() { return screenOn; }
|
||||
|
||||
// Stores the last 4 of our hardware ID, to make finding the device for pairing easier
|
||||
@@ -344,7 +351,13 @@ class Screen : public concurrency::OSThread
|
||||
void showOverlayBanner(BannerOverlayOptions);
|
||||
|
||||
void showNodePicker(const char *message, uint32_t durationMs, std::function<void(uint32_t)> bannerCallback);
|
||||
void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, std::function<void(uint32_t)> bannerCallback);
|
||||
void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, bool useBase16,
|
||||
std::function<void(uint32_t)> bannerCallback);
|
||||
// Arcade-style initials entry. `length` positions each cycle A-Z/0-9 (UP/DOWN), LEFT/RIGHT
|
||||
// moves the cursor, SELECT advances; the assembled string is delivered to `bannerCallback`.
|
||||
// `initialText` pre-seeds the positions (uppercased & filtered), defaulting to 'A'.
|
||||
void showAlphanumericPicker(const char *message, const char *initialText, uint32_t durationMs, uint8_t length,
|
||||
std::function<void(const std::string &)> bannerCallback);
|
||||
void showTextInput(const char *header, const char *initialText, uint32_t durationMs,
|
||||
std::function<void(const std::string &)> textCallback);
|
||||
|
||||
@@ -734,6 +747,7 @@ class Screen : public concurrency::OSThread
|
||||
uint8_t system = 255;
|
||||
uint8_t gps = 255;
|
||||
uint8_t home = 255;
|
||||
uint8_t games = 255;
|
||||
uint8_t textMessage = 255;
|
||||
uint8_t nodelist_nodes = 255;
|
||||
uint8_t nodelist_location = 255;
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "Power.h"
|
||||
#include "RTC.h"
|
||||
#include "draw/NodeListRenderer.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/TFTColorRegions.h"
|
||||
@@ -147,8 +147,8 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
// Transparent clock headers should inherit whatever body off-color is
|
||||
// already active under the header (important for light/inverted themes).
|
||||
const uint16_t transparentBgColor = resolveTFTOffColorAt(0, headerHeight + 1, getThemeBodyBg());
|
||||
setAndRegisterTFTColorRole(TFTColorRole::HeaderBackground, transparentBgColor, transparentBgColor, 0, 0, screenW,
|
||||
headerHeight);
|
||||
// Intentionally skip the HeaderBackground region, as small screens draw the clock in the unused space in this region,
|
||||
// and the transparent call was erasing segments from the clock
|
||||
setTFTColorRole(TFTColorRole::HeaderTitle, headerTitleColorForRole, transparentBgColor);
|
||||
setTFTColorRole(TFTColorRole::HeaderStatus, headerStatusColor, transparentBgColor);
|
||||
} else if (useInvertedHeaderStyle) {
|
||||
|
||||
@@ -16,8 +16,9 @@ struct TFTColorRegion {
|
||||
// Required by ST7789 driver: it scans until the first disabled entry.
|
||||
bool enabled = false;
|
||||
};
|
||||
|
||||
#ifndef MAX_TFT_COLOR_REGIONS
|
||||
static constexpr size_t MAX_TFT_COLOR_REGIONS = 48;
|
||||
#endif
|
||||
extern TFTColorRegion colorRegions[MAX_TFT_COLOR_REGIONS];
|
||||
|
||||
enum class TFTColorRole : uint8_t {
|
||||
@@ -39,7 +40,7 @@ enum class TFTColorRole : uint8_t {
|
||||
Count
|
||||
};
|
||||
|
||||
#if HAS_TFT || defined(HAS_SPI_TFT)
|
||||
#if HAS_TFT || defined(HAS_SPI_TFT) || defined(HAS_HUB75_NATIVE)
|
||||
#define GRAPHICS_TFT_COLORING_ENABLED 1
|
||||
#else
|
||||
#define GRAPHICS_TFT_COLORING_ENABLED 0
|
||||
|
||||
@@ -121,7 +121,6 @@ static void rak14014_tpIntHandle(void)
|
||||
|
||||
#elif defined(HACKADAY_COMMUNICATOR)
|
||||
#include <Arduino_GFX_Library.h>
|
||||
Arduino_DataBus *bus = nullptr;
|
||||
Arduino_GFX *tft = nullptr;
|
||||
|
||||
#elif defined(ST72xx_DE)
|
||||
@@ -1601,17 +1600,21 @@ bool TFTDisplay::connect()
|
||||
{
|
||||
concurrency::LockGuard g(spiLock);
|
||||
LOG_INFO("Do TFT init");
|
||||
// connect() re-runs on every display wake on variants whose handleSetOn() re-inits
|
||||
// the UI (see the gates in Screen::handleSetOn); construct the driver exactly once.
|
||||
if (!tft) {
|
||||
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) || defined(HELTEC_MESH_NODE_T1)
|
||||
tft = new TFT_eSPI;
|
||||
tft = new TFT_eSPI;
|
||||
#elif defined(HACKADAY_COMMUNICATOR)
|
||||
bus = new Arduino_ESP32SPI(TFT_DC, TFT_CS, 38 /* SCK */, 21 /* MOSI */, GFX_NOT_DEFINED /* MISO */, HSPI /* spi_num */);
|
||||
tft = new Arduino_NV3007(bus, 40, 0 /* rotation */, false /* IPS */, 142 /* width */, 428 /* height */, 12 /* col offset 1 */,
|
||||
0 /* row offset 1 */, 14 /* col offset 2 */, 0 /* row offset 2 */, nv3007_279_init_operations,
|
||||
sizeof(nv3007_279_init_operations));
|
||||
|
||||
Arduino_DataBus *bus =
|
||||
new Arduino_ESP32SPI(TFT_DC, TFT_CS, 38 /* SCK */, 21 /* MOSI */, GFX_NOT_DEFINED /* MISO */, HSPI /* spi_num */);
|
||||
tft = new Arduino_NV3007(bus, 40, 0 /* rotation */, false /* IPS */, 142 /* width */, 428 /* height */,
|
||||
12 /* col offset 1 */, 0 /* row offset 1 */, 14 /* col offset 2 */, 0 /* row offset 2 */,
|
||||
nv3007_279_init_operations, sizeof(nv3007_279_init_operations));
|
||||
#else
|
||||
tft = new LGFX;
|
||||
tft = new LGFX;
|
||||
#endif
|
||||
}
|
||||
|
||||
backlightEnable->set(true);
|
||||
LOG_INFO("Power to TFT Backlight");
|
||||
|
||||
@@ -743,17 +743,18 @@ void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int1
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
int line = 1;
|
||||
int scale = 1;
|
||||
int iconX = SCREEN_WIDTH - chirpy_width - (chirpy_width / 3);
|
||||
int iconY = (SCREEN_HEIGHT - chirpy_height) / 2;
|
||||
int textX_offset = 10;
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
textX_offset = textX_offset * 4;
|
||||
const int scale = 2;
|
||||
scale = 2;
|
||||
iconX = SCREEN_WIDTH - (chirpy_width * 2) - ((chirpy_width * 2) / 3);
|
||||
iconY = (SCREEN_HEIGHT - (chirpy_height * 2)) / 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));
|
||||
@@ -767,6 +768,19 @@ void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int1
|
||||
display->drawXbm(iconX, iconY, chirpy_width, chirpy_height, chirpy);
|
||||
}
|
||||
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
// Colour Chirpy on colour displays. The glyph is a filled head silhouette whose eyes are holes
|
||||
// (off pixels), so two stacked regions render the proper mascot without a second bitmap:
|
||||
// A) whole glyph -> green body / frame / legs
|
||||
// B) the eye band -> black face, with the eye holes turning white via the region's off-colour
|
||||
// Start the face band one column in from the head's left edge (col 6) so that edge stays green,
|
||||
// matching the green column already left on the right edge (col 31).
|
||||
graphics::registerTFTColorRegionDirect(iconX, iconY, chirpy_width * scale, chirpy_height * scale,
|
||||
graphics::TFTPalette::MeshtasticGreen, graphics::getThemeBodyBg());
|
||||
graphics::registerTFTColorRegionDirect(iconX + 7 * scale, iconY + 12 * scale, 24 * scale, 16 * scale,
|
||||
graphics::TFTPalette::Black, graphics::TFTPalette::White);
|
||||
#endif
|
||||
|
||||
int textX = (display->getWidth() / 2) - textX_offset - (display->getStringWidth("Hello") / 2);
|
||||
display->drawString(textX, getTextPositions(display)[line++], "Hello");
|
||||
textX = (display->getWidth() / 2) - textX_offset - (display->getStringWidth("World!") / 2);
|
||||
|
||||
@@ -70,12 +70,15 @@ const StoredMessage *getNewestMessageForActiveThread()
|
||||
const uint32_t peer = graphics::MessageRenderer::getThreadPeer();
|
||||
const uint32_t localNode = nodeDB->getNodeNum();
|
||||
|
||||
if (mode == graphics::MessageRenderer::ThreadMode::ALL) {
|
||||
return &messages.back();
|
||||
}
|
||||
|
||||
for (auto it = messages.rbegin(); it != messages.rend(); ++it) {
|
||||
const StoredMessage &m = *it;
|
||||
if (!messageStore.isMessageVisible(m)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mode == graphics::MessageRenderer::ThreadMode::ALL) {
|
||||
return &m;
|
||||
}
|
||||
|
||||
if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
|
||||
if (m.type == MessageType::BROADCAST && static_cast<int>(m.channelIndex) == channel) {
|
||||
@@ -242,7 +245,6 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
|
||||
{"TH", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_TH},
|
||||
{"LORA_24", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_LORA_24},
|
||||
{"UA_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_UA_433},
|
||||
{"UA_868", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_UA_868},
|
||||
{"MY_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_MY_433},
|
||||
{"MY_919", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_MY_919},
|
||||
{"SG_923", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_SG_923},
|
||||
@@ -2323,8 +2325,10 @@ void menuHandler::testMenu()
|
||||
|
||||
void menuHandler::numberTest()
|
||||
{
|
||||
screen->showNumberPicker("Pick a number\n ", 30000, 4,
|
||||
[](int number_picked) -> void { LOG_WARN("Nodenum: %u", number_picked); });
|
||||
screen->showNumberPicker("Verify Nodenum:\n ", 30000, 8, true, [](uint32_t number_picked) -> void {
|
||||
LOG_DEBUG("Nodenum: 0x%08x", number_picked);
|
||||
keyVerificationModule->sendInitialRequest(number_picked);
|
||||
});
|
||||
}
|
||||
|
||||
void menuHandler::wifiBaseMenu()
|
||||
@@ -2508,8 +2512,7 @@ void menuHandler::keyVerificationFinalPrompt()
|
||||
options.notificationType = graphics::notificationTypeEnum::selection_picker;
|
||||
options.bannerCallback = [=](int selected) {
|
||||
if (selected == 1) {
|
||||
auto remoteNodePtr = nodeDB->getMeshNode(keyVerificationModule->getCurrentRemoteNode());
|
||||
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
||||
keyVerificationModule->commitVerifiedRemoteNode();
|
||||
}
|
||||
};
|
||||
screen->showOverlayBanner(options);
|
||||
|
||||
@@ -403,6 +403,8 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
// Filter messages based on thread mode
|
||||
std::deque<StoredMessage> filtered;
|
||||
for (const auto &m : messageStore.getLiveMessages()) {
|
||||
if (!messageStore.isMessageVisible(m))
|
||||
continue;
|
||||
bool include = false;
|
||||
switch (currentMode) {
|
||||
case ThreadMode::ALL:
|
||||
|
||||
@@ -54,6 +54,7 @@ bool NotificationRenderer::pauseBanner = false;
|
||||
notificationTypeEnum NotificationRenderer::current_notification_type = notificationTypeEnum::none;
|
||||
uint32_t NotificationRenderer::numDigits = 0;
|
||||
uint32_t NotificationRenderer::currentNumber = 0;
|
||||
char NotificationRenderer::alphanumericValue[16] = {0};
|
||||
VirtualKeyboard *NotificationRenderer::virtualKeyboard = nullptr;
|
||||
std::function<void(const std::string &)> NotificationRenderer::textInputCallback = nullptr;
|
||||
|
||||
@@ -277,6 +278,9 @@ void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayU
|
||||
case notificationTypeEnum::hex_picker:
|
||||
drawHexPicker(display, state);
|
||||
break;
|
||||
case notificationTypeEnum::alphanumeric_picker:
|
||||
drawAlphanumericPicker(display, state);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,6 +466,96 @@ void NotificationRenderer::drawHexPicker(OLEDDisplay *display, OLEDDisplayUiStat
|
||||
drawNotificationBox(display, state, linePointers, totalLines, 0);
|
||||
}
|
||||
|
||||
// Arcade-style initials entry. Mirrors drawHexPicker's cursor/confirm flow, but each position
|
||||
// holds a character from ALPHANUMERIC_CHARS (cycled with UP/DOWN) instead of a packed digit, and
|
||||
// the assembled string is returned through textInputCallback.
|
||||
void NotificationRenderer::drawAlphanumericPicker(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||
{
|
||||
static const char ALPHANUMERIC_CHARS[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
constexpr int ALPHANUMERIC_COUNT = sizeof(ALPHANUMERIC_CHARS) - 1; // exclude the NUL
|
||||
|
||||
const char *lineStarts[MAX_LINES + 1] = {0};
|
||||
uint16_t lineCount = 0;
|
||||
|
||||
// Parse lines (identical to the number/hex pickers)
|
||||
char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage));
|
||||
lineStarts[lineCount] = alertBannerMessage;
|
||||
while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) {
|
||||
lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n');
|
||||
if (lineStarts[lineCount + 1][0] == '\n')
|
||||
lineStarts[lineCount + 1] += 1;
|
||||
lineCount++;
|
||||
}
|
||||
|
||||
auto alphaIndex = [&](char c) -> int {
|
||||
for (int i = 0; i < ALPHANUMERIC_COUNT; i++)
|
||||
if (ALPHANUMERIC_CHARS[i] == c)
|
||||
return i;
|
||||
return 0;
|
||||
};
|
||||
|
||||
// Handle input
|
||||
if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS ||
|
||||
inEvent.inputEvent == INPUT_BROKER_UP_LONG) {
|
||||
int idx = (alphaIndex(alphanumericValue[curSelected]) + 1) % ALPHANUMERIC_COUNT;
|
||||
alphanumericValue[curSelected] = ALPHANUMERIC_CHARS[idx];
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_USER_PRESS ||
|
||||
inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) {
|
||||
int idx = (alphaIndex(alphanumericValue[curSelected]) + ALPHANUMERIC_COUNT - 1) % ALPHANUMERIC_COUNT;
|
||||
alphanumericValue[curSelected] = ALPHANUMERIC_CHARS[idx];
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_ANYKEY) {
|
||||
char k = inEvent.kbchar;
|
||||
if (k >= 'a' && k <= 'z')
|
||||
k = static_cast<char>(k - 'a' + 'A');
|
||||
if ((k >= 'A' && k <= 'Z') || (k >= '0' && k <= '9')) { // direct keyboard entry
|
||||
alphanumericValue[curSelected] = k;
|
||||
curSelected++;
|
||||
}
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT || inEvent.inputEvent == INPUT_BROKER_RIGHT) {
|
||||
curSelected++;
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_LEFT) {
|
||||
curSelected--;
|
||||
} else if ((inEvent.inputEvent == INPUT_BROKER_CANCEL || inEvent.inputEvent == INPUT_BROKER_ALT_LONG) &&
|
||||
alertBannerUntil != 0) {
|
||||
resetBanner();
|
||||
return;
|
||||
}
|
||||
|
||||
if (curSelected < 0)
|
||||
curSelected = 0;
|
||||
if (curSelected == static_cast<int8_t>(numDigits)) {
|
||||
auto callback = textInputCallback; // capture before clearing to avoid re-entrancy surprises
|
||||
std::string result(alphanumericValue, numDigits);
|
||||
textInputCallback = nullptr;
|
||||
resetBanner();
|
||||
if (callback)
|
||||
callback(result);
|
||||
return;
|
||||
}
|
||||
|
||||
inEvent.inputEvent = INPUT_BROKER_NONE;
|
||||
if (alertBannerMessage[0] == '\0')
|
||||
return;
|
||||
|
||||
uint16_t totalLines = lineCount + 2;
|
||||
const char *linePointers[totalLines + 1] = {0}; // this is sort of a dynamic allocation
|
||||
|
||||
for (uint16_t i = 0; i < lineCount; i++) {
|
||||
linePointers[i] = lineStarts[i];
|
||||
}
|
||||
std::string chars = " ";
|
||||
std::string arrowPointer = " ";
|
||||
for (uint16_t i = 0; i < numDigits; i++) {
|
||||
chars += std::string(1, alphanumericValue[i]) + " ";
|
||||
arrowPointer += (curSelected == static_cast<int8_t>(i)) ? "^ " : "_ ";
|
||||
}
|
||||
|
||||
linePointers[lineCount++] = chars.c_str();
|
||||
linePointers[lineCount++] = arrowPointer.c_str();
|
||||
|
||||
drawNotificationBox(display, state, linePointers, totalLines, 0);
|
||||
}
|
||||
|
||||
void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||
{
|
||||
static uint32_t selectedNodenum = 0;
|
||||
@@ -1038,10 +1132,9 @@ void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiStat
|
||||
// Cancel virtual keyboard - call callback with empty string to indicate timeout
|
||||
auto callback = textInputCallback; // Store callback before clearing
|
||||
|
||||
// Clean up first to prevent re-entry
|
||||
delete virtualKeyboard;
|
||||
virtualKeyboard = nullptr;
|
||||
textInputCallback = nullptr;
|
||||
// Clean up first to prevent re-entry. The keyboard belongs to OnScreenKeyboardModule; only stop()
|
||||
// may free it, and it clears virtualKeyboard/textInputCallback for us.
|
||||
OnScreenKeyboardModule::instance().stop(false);
|
||||
resetBanner();
|
||||
|
||||
// Call callback after cleanup
|
||||
@@ -1060,9 +1153,7 @@ void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiStat
|
||||
bool handled = OnScreenKeyboardModule::processVirtualKeyboardInput(inEvent, virtualKeyboard);
|
||||
if (!handled && inEvent.inputEvent == INPUT_BROKER_CANCEL) {
|
||||
auto callback = textInputCallback;
|
||||
delete virtualKeyboard;
|
||||
virtualKeyboard = nullptr;
|
||||
textInputCallback = nullptr;
|
||||
OnScreenKeyboardModule::instance().stop(false); // sole owner of the keyboard; also clears our aliases
|
||||
resetBanner();
|
||||
if (callback) {
|
||||
callback("");
|
||||
|
||||
@@ -26,6 +26,7 @@ class NotificationRenderer
|
||||
static std::function<void(int)> alertBannerCallback;
|
||||
static uint32_t numDigits;
|
||||
static uint32_t currentNumber;
|
||||
static char alphanumericValue[16]; // working buffer for the alphanumeric_picker
|
||||
static VirtualKeyboard *virtualKeyboard;
|
||||
static std::function<void(const std::string &)> textInputCallback;
|
||||
|
||||
@@ -43,6 +44,7 @@ class NotificationRenderer
|
||||
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawHexPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawAlphanumericPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1],
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||
#include "modules/StatusMessageModule.h"
|
||||
#endif
|
||||
#if BASEUI_HAS_GAMES
|
||||
#include "modules/games/GamesModule.h"
|
||||
#endif
|
||||
#include "UIRenderer.h"
|
||||
#include "airtime.h"
|
||||
#include "gps/GeoCoord.h"
|
||||
@@ -21,8 +24,8 @@
|
||||
#include "main.h"
|
||||
#include "target_specific.h"
|
||||
#include <OLEDDisplay.h>
|
||||
#include <RTC.h>
|
||||
#include <cstring>
|
||||
#include <gps/RTC.h>
|
||||
|
||||
// External variables
|
||||
extern graphics::Screen *screen;
|
||||
@@ -844,6 +847,7 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
|
||||
return -6.0f;
|
||||
case PRESET(MEDIUM_SLOW):
|
||||
case PRESET(MEDIUM_FAST):
|
||||
case PRESET(MEDIUM_TURBO):
|
||||
return -5.5f;
|
||||
case PRESET(SHORT_SLOW):
|
||||
case PRESET(SHORT_FAST):
|
||||
@@ -1816,6 +1820,13 @@ constexpr uint32_t ICON_DISPLAY_DURATION_MS = 2000;
|
||||
// cppcheck-suppress constParameterPointer; signature must match OverlayCallback typedef from OLEDDisplayUi library
|
||||
void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||
{
|
||||
#if BASEUI_HAS_GAMES
|
||||
// Hide the navigation bar while a game owns the screen (the attract screen doesn't intercept,
|
||||
// so the nav bar stays visible there).
|
||||
if (gamesModule && gamesModule->interceptingKeyboardInput())
|
||||
return;
|
||||
#endif
|
||||
|
||||
uint8_t frameToHighlight = state->currentFrame;
|
||||
if (state->frameState == IN_TRANSITION && state->transitionFrameTarget < screen->indicatorIcons.size()) {
|
||||
frameToHighlight = state->transitionFrameTarget;
|
||||
|
||||
@@ -319,6 +319,46 @@ const uint8_t chirpy_small[] = {0x7f, 0x41, 0x55, 0x55, 0x55, 0x55, 0x41, 0x7f};
|
||||
#define connection_icon_height 5
|
||||
const uint8_t connection_icon[] = {0x36, 0x41, 0x5D, 0x41, 0x36};
|
||||
|
||||
// Joystick icon (16x16): a round knob on a shaft over an elliptical base. Drawn on the Snake
|
||||
// attract screen as a "use the D-pad" controls hint.
|
||||
#define joystick_width 16
|
||||
#define joystick_height 16
|
||||
static const uint8_t joystick[] PROGMEM = {0xC0, 0x03, 0xE0, 0x07, 0xF0, 0x0F, 0xF0, 0x0F, 0xE0, 0x07, 0xC0,
|
||||
0x03, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0xF8, 0x1F,
|
||||
0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0xF8, 0x1F};
|
||||
|
||||
// 8x8 joystick, for the navigation bar (which draws 8x8 glyphs, scaled up on hi-res displays).
|
||||
#define joystick_small_width 8
|
||||
#define joystick_small_height 8
|
||||
static const uint8_t joystick_small[] PROGMEM = {0x18, 0x3C, 0x3C, 0x18, 0x7E, 0x99, 0xC3, 0x7E};
|
||||
|
||||
#define snake_width 16
|
||||
#define snake_height 16
|
||||
static const uint8_t snake[] PROGMEM = {0x80, 0x1F, 0x40, 0x20, 0x20, 0x48, 0x20, 0x40, 0xA0, 0x44, 0x20,
|
||||
0x39, 0x40, 0xDE, 0x86, 0x44, 0x0A, 0x19, 0x32, 0x22, 0xC4, 0x47,
|
||||
0x1C, 0x4D, 0x6A, 0x4A, 0xFA, 0x47, 0x04, 0x20, 0xF8, 0x1F};
|
||||
|
||||
#define tetris_width 16
|
||||
#define tetris_height 16
|
||||
static const uint8_t tetris[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x01, 0x80, 0xFD, 0xBF, 0xFD,
|
||||
0xBF, 0x81, 0x81, 0xBF, 0xFD, 0xA0, 0x05, 0xA0, 0x05, 0xA0, 0x05,
|
||||
0x20, 0x04, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
#define breakout_width 16
|
||||
#define breakout_height 16
|
||||
// Three staggered brick courses, a ball, and a paddle. XBM: 2 bytes/row, LSB = leftmost pixel.
|
||||
static const uint8_t breakout[] PROGMEM = {0x00, 0x00, 0x77, 0x77, 0x77, 0x77, 0x00, 0x00, 0xDD, 0xDD, 0xDD,
|
||||
0xDD, 0x00, 0x00, 0x77, 0x77, 0x77, 0x77, 0x00, 0x00, 0x80, 0x01,
|
||||
0x80, 0x01, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0xF0, 0x0F};
|
||||
|
||||
// Tiny Chirpy runner sprite: square head with two pill eyes + mouth, antenna, and long legs.
|
||||
// XBM, 2 bytes/row, LSB = leftmost pixel. Used by the Chirpy Runner game.
|
||||
#define chirpy_run_width 12
|
||||
#define chirpy_run_height 16
|
||||
static const uint8_t chirpy_run[] PROGMEM = {0x40, 0x00, 0x20, 0x00, 0x40, 0x00, 0xfc, 0x03, 0x04, 0x02, 0x94,
|
||||
0x02, 0x94, 0x02, 0x94, 0x02, 0x04, 0x02, 0xf4, 0x02, 0xfc, 0x03,
|
||||
0x90, 0x00, 0x90, 0x00, 0x90, 0x00, 0x90, 0x00, 0x08, 0x01};
|
||||
|
||||
#ifdef OLED_TINY
|
||||
#include "img/icon_small.xbm"
|
||||
#else
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
#include "main.h"
|
||||
|
||||
#include "RTC.h"
|
||||
#include "gps/RTC.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "RTC.h"
|
||||
#include "gps/RTC.h"
|
||||
|
||||
#include "GeoCoord.h"
|
||||
#include "NodeDB.h"
|
||||
|
||||
@@ -57,7 +57,6 @@ enum MenuAction {
|
||||
SET_REGION_TH,
|
||||
SET_REGION_LORA_24,
|
||||
SET_REGION_UA_433,
|
||||
SET_REGION_UA_868,
|
||||
SET_REGION_MY_433,
|
||||
SET_REGION_MY_919,
|
||||
SET_REGION_SG_923,
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
#include "MeshService.h"
|
||||
#include "MessageStore.h"
|
||||
#include "Power.h"
|
||||
#include "RTC.h"
|
||||
#include "Router.h"
|
||||
#include "airtime.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h"
|
||||
#include "graphics/niche/Utils/FlashData.h"
|
||||
#include "main.h"
|
||||
@@ -835,10 +835,6 @@ void InkHUD::MenuApplet::execute(MenuItem item)
|
||||
applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_UA_433);
|
||||
break;
|
||||
|
||||
case SET_REGION_UA_868:
|
||||
applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_UA_868);
|
||||
break;
|
||||
|
||||
case SET_REGION_MY_433:
|
||||
applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_MY_433);
|
||||
break;
|
||||
@@ -1735,7 +1731,6 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
||||
items.push_back(MenuItem("TH", MenuAction::SET_REGION_TH, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("LoRa 2.4", MenuAction::SET_REGION_LORA_24, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("UA 433", MenuAction::SET_REGION_UA_433, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("UA 868", MenuAction::SET_REGION_UA_868, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("MY 433", MenuAction::SET_REGION_MY_433, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("MY 919", MenuAction::SET_REGION_MY_919, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("SG 923", MenuAction::SET_REGION_SG_923, MenuPage::EXIT));
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#include "meshUtils.h"
|
||||
#include "modules/TextMessageModule.h"
|
||||
|
||||
#include "RTC.h"
|
||||
#include "gps/RTC.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
@@ -234,7 +234,8 @@ std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvaila
|
||||
// Pick source of message
|
||||
const StoredMessage *message =
|
||||
msgIsBroadcast ? &inkhud->persistence->latestMessage.broadcast : &inkhud->persistence->latestMessage.dm;
|
||||
|
||||
if (!message->sender || !messageStore.isMessageVisible(*message))
|
||||
return parse(text);
|
||||
// Find info about the sender
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(message->sender);
|
||||
|
||||
@@ -270,4 +271,4 @@ std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvaila
|
||||
return parse(text);
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -46,7 +46,7 @@ void InkHUD::AllMessageApplet::onRender(bool full)
|
||||
message = &latestMessage->dm;
|
||||
|
||||
// Short circuit: no text message
|
||||
if (!message->sender) {
|
||||
if (!message->sender || !messageStore.isMessageVisible(*message)) {
|
||||
printAt(X(0.5), Y(0.5), "No Message", CENTER, MIDDLE);
|
||||
return;
|
||||
}
|
||||
@@ -138,4 +138,4 @@ bool InkHUD::AllMessageApplet::approveNotification(Notification &n)
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -42,7 +42,7 @@ int InkHUD::DMApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p)
|
||||
void InkHUD::DMApplet::onRender(bool full)
|
||||
{
|
||||
// Abort if no text message
|
||||
if (!latestMessage->dm.sender) {
|
||||
if (!latestMessage->dm.sender || !messageStore.isMessageVisible(latestMessage->dm)) {
|
||||
printAt(X(0.5), Y(0.5), "No DMs", CENTER, MIDDLE);
|
||||
return;
|
||||
}
|
||||
@@ -131,4 +131,4 @@ bool InkHUD::DMApplet::approveNotification(Notification &n)
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "RTC.h"
|
||||
#include "gps/RTC.h"
|
||||
|
||||
#include "gps/GeoCoord.h"
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#include "./RecentsListApplet.h"
|
||||
|
||||
#include "RTC.h"
|
||||
#include "gps/RTC.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#include "./ThreadedMessageApplet.h"
|
||||
|
||||
#include "RTC.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "mesh/NodeDB.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
@@ -191,7 +191,8 @@ ProcessMessage InkHUD::ThreadedMessageApplet::handleReceived(const meshtastic_Me
|
||||
return ProcessMessage::CONTINUE;
|
||||
|
||||
// Store in the global messageStore - this handles sender, timestamp, channel, text, and ack status
|
||||
messageStore.addFromPacket(mp);
|
||||
if (!messageStore.tryAddFromPacket(mp))
|
||||
return ProcessMessage::CONTINUE;
|
||||
|
||||
// If this was an incoming message, suggest that our applet becomes foreground, if permitted
|
||||
if (getFrom(&mp) != nodeDB->getNodeNum())
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
#include "MessageStore.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "RTC.h"
|
||||
#include "buzz.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "modules/ExternalNotificationModule.h"
|
||||
#include "modules/TextMessageModule.h"
|
||||
#include "sleep.h"
|
||||
@@ -534,13 +534,19 @@ int InkHUD::Events::onReceiveTextMessage(const meshtastic_MeshPacket *packet)
|
||||
if (getFrom(packet) == nodeDB->getNodeNum())
|
||||
return 0;
|
||||
|
||||
if (!messageStore.shouldStorePacket(*packet))
|
||||
return 0;
|
||||
|
||||
bool isBroadcastMsg = isBroadcast(packet->to);
|
||||
inkhud->persistence->latestMessage.wasBroadcast = isBroadcastMsg;
|
||||
|
||||
if (!isBroadcastMsg) {
|
||||
// DMs never pass through ThreadedMessageApplet, so add them to the global store here
|
||||
// so they survive reboots. Derive the latestMessage cache entry from the stored result.
|
||||
inkhud->persistence->latestMessage.dm = messageStore.addFromPacket(*packet);
|
||||
const StoredMessage *stored = messageStore.tryAddFromPacket(*packet);
|
||||
if (!stored)
|
||||
return 0;
|
||||
inkhud->persistence->latestMessage.dm = *stored;
|
||||
} else {
|
||||
// Broadcasts are added to the global store by ThreadedMessageApplet::handleReceived().
|
||||
// Here we only update the latestMessage cache used by AllMessageApplet / NotificationApplet.
|
||||
|
||||
@@ -26,6 +26,10 @@ void InkHUD::Persistence::loadLatestMessage()
|
||||
|
||||
int lastBroadcastPos = -1, lastDMPos = -1, pos = 0;
|
||||
for (const StoredMessage &m : messageStore.getLiveMessages()) {
|
||||
if (!messageStore.isMessageVisible(m)) {
|
||||
pos++;
|
||||
continue;
|
||||
}
|
||||
if (m.type == MessageType::BROADCAST) {
|
||||
latestMessage.broadcast = m;
|
||||
lastBroadcastPos = pos;
|
||||
|
||||
@@ -1,7 +1,22 @@
|
||||
[inkhud]
|
||||
build_src_filter =
|
||||
build_src_filter =
|
||||
+<graphics/niche/>; Include the nicheGraphics directory
|
||||
build_flags =
|
||||
-<graphics/draw/> ; BaseUI renderer stack is disabled for InkHUD
|
||||
-<graphics/fonts/> ; InkHUD uses GFX_Root fonts instead of BaseUI font tables
|
||||
-<graphics/EInkDisplay2.cpp> ; InkHUD drives e-ink through niche drivers
|
||||
-<graphics/EInkDynamicDisplay.cpp>
|
||||
-<graphics/EInkParallelDisplay.cpp>
|
||||
-<graphics/EmoteRenderer.cpp>
|
||||
-<graphics/emotes.cpp>
|
||||
-<graphics/Panel_sdl.cpp>
|
||||
-<graphics/ScreenGlobals.cpp>
|
||||
-<graphics/SharedUIDisplay.cpp>
|
||||
-<graphics/TFTColorRegions.cpp>
|
||||
-<graphics/TFTDisplay.cpp>
|
||||
-<graphics/tftSetup.cpp>
|
||||
-<graphics/TimeFormatters.cpp>
|
||||
-<graphics/VirtualKeyboard.cpp>
|
||||
build_flags =
|
||||
-D MESHTASTIC_INCLUDE_NICHE_GRAPHICS ; Use NicheGraphics
|
||||
-D MESHTASTIC_INCLUDE_INKHUD ; Use InkHUD (a NicheGraphics UI)
|
||||
-D MESHTASTIC_EXCLUDE_SCREEN ; Suppress default Screen class
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
#if ARCH_PORTDUINO
|
||||
#include "input/LinuxInputImpl.h"
|
||||
#include "input/LinuxJoystick.h"
|
||||
#include "input/SeesawRotary.h"
|
||||
#include "platform/portduino/PortduinoGlue.h"
|
||||
#endif
|
||||
@@ -430,6 +431,9 @@ void InputBroker::Init()
|
||||
// Linux evdev keyboard input only - macOS has no <linux/input.h>.
|
||||
aLinuxInputImpl = new LinuxInputImpl();
|
||||
aLinuxInputImpl->init();
|
||||
// Linux evdev gamepad/joystick input (D-pad + confirm/cancel buttons).
|
||||
aLinuxJoystick = new LinuxJoystick("LinuxJoystick");
|
||||
aLinuxJoystick->init();
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -25,8 +25,14 @@ LinuxInput::LinuxInput(const char *name) : concurrency::OSThread(name)
|
||||
|
||||
void LinuxInput::deInit()
|
||||
{
|
||||
// reboot() on native does execv() in-place, which only closes O_CLOEXEC fds.
|
||||
// Close both descriptors here so we don't leak one per restart.
|
||||
if (fd >= 0)
|
||||
close(fd);
|
||||
if (epollfd >= 0)
|
||||
close(epollfd);
|
||||
fd = -1;
|
||||
epollfd = -1;
|
||||
}
|
||||
|
||||
int32_t LinuxInput::runOnce()
|
||||
@@ -35,14 +41,14 @@ int32_t LinuxInput::runOnce()
|
||||
if (firstTime) {
|
||||
if (portduino_config.keyboardDevice == "")
|
||||
return disable();
|
||||
fd = open(portduino_config.keyboardDevice.c_str(), O_RDWR);
|
||||
fd = open(portduino_config.keyboardDevice.c_str(), O_RDWR | O_CLOEXEC);
|
||||
if (fd < 0)
|
||||
return disable();
|
||||
ret = ioctl(fd, EVIOCGRAB, (void *)1);
|
||||
if (ret != 0)
|
||||
return disable();
|
||||
|
||||
epollfd = epoll_create1(0);
|
||||
epollfd = epoll_create1(EPOLL_CLOEXEC);
|
||||
assert(epollfd >= 0);
|
||||
|
||||
ev.events = EPOLLIN;
|
||||
|
||||
@@ -44,7 +44,7 @@ class LinuxInput : public Observable<const InputEvent *>, public concurrency::OS
|
||||
int fd = -1;
|
||||
int ret;
|
||||
uint8_t report[8];
|
||||
int epollfd;
|
||||
int epollfd = -1;
|
||||
struct epoll_event ev;
|
||||
uint8_t modifiers = 0;
|
||||
std::map<int, char> keymap{
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
#include "configuration.h"
|
||||
#if ARCH_PORTDUINO && defined(__linux__)
|
||||
#include "InputBroker.h"
|
||||
#include "LinuxJoystick.h"
|
||||
#include "platform/portduino/PortduinoGlue.h"
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
LinuxJoystick *aLinuxJoystick;
|
||||
|
||||
LinuxJoystick::LinuxJoystick(const char *name) : concurrency::OSThread(name)
|
||||
{
|
||||
this->_originName = name;
|
||||
}
|
||||
|
||||
// Translate a config.yaml action name into a broker event. Returns INPUT_BROKER_NONE
|
||||
// for unknown names so a typo simply leaves that button unmapped.
|
||||
static input_broker_event joystickActionToEvent(const std::string &action)
|
||||
{
|
||||
if (action == "select")
|
||||
return INPUT_BROKER_SELECT;
|
||||
if (action == "cancel")
|
||||
return INPUT_BROKER_CANCEL;
|
||||
if (action == "back")
|
||||
return INPUT_BROKER_BACK;
|
||||
if (action == "up")
|
||||
return INPUT_BROKER_UP;
|
||||
if (action == "down")
|
||||
return INPUT_BROKER_DOWN;
|
||||
if (action == "left")
|
||||
return INPUT_BROKER_LEFT;
|
||||
if (action == "right")
|
||||
return INPUT_BROKER_RIGHT;
|
||||
if (action == "user" || action == "userpress")
|
||||
return INPUT_BROKER_USER_PRESS;
|
||||
return INPUT_BROKER_NONE;
|
||||
}
|
||||
|
||||
void LinuxJoystick::init()
|
||||
{
|
||||
if (portduino_config.joystickButtons.empty()) {
|
||||
// No config override: fall back to the built-in defaults.
|
||||
buttonMap[JOY_BTN_A] = INPUT_BROKER_SELECT;
|
||||
buttonMap[JOY_BTN_B] = INPUT_BROKER_CANCEL;
|
||||
} else {
|
||||
for (const auto &button : portduino_config.joystickButtons) {
|
||||
input_broker_event event = joystickActionToEvent(button.second);
|
||||
if (event != INPUT_BROKER_NONE)
|
||||
buttonMap[button.first] = event;
|
||||
}
|
||||
}
|
||||
inputBroker->registerSource(this);
|
||||
}
|
||||
|
||||
void LinuxJoystick::deInit()
|
||||
{
|
||||
// reboot() on native does execv() in-place, which only closes O_CLOEXEC fds.
|
||||
// Close both descriptors here so we don't leak one per restart.
|
||||
if (fd >= 0)
|
||||
close(fd);
|
||||
if (epollfd >= 0)
|
||||
close(epollfd);
|
||||
fd = -1;
|
||||
epollfd = -1;
|
||||
}
|
||||
|
||||
// Bucket a raw axis value into a direction zone: -1 (min edge), 0 (center), +1 (max edge).
|
||||
static int axisZone(int value)
|
||||
{
|
||||
if (value <= JOY_AXIS_LOW)
|
||||
return -1;
|
||||
if (value >= JOY_AXIS_HIGH)
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void LinuxJoystick::emitEvent(input_broker_event event)
|
||||
{
|
||||
InputEvent e = {};
|
||||
e.inputEvent = event;
|
||||
e.source = this->_originName;
|
||||
e.kbchar = 0;
|
||||
// LOG_DEBUG("joystick: %s event %d", this->_originName, event);
|
||||
this->notifyObservers(&e);
|
||||
}
|
||||
|
||||
int32_t LinuxJoystick::runOnce()
|
||||
{
|
||||
if (firstTime) {
|
||||
if (portduino_config.joystickDevice == "")
|
||||
return disable();
|
||||
fd = open(portduino_config.joystickDevice.c_str(), O_RDWR | O_CLOEXEC);
|
||||
if (fd < 0)
|
||||
return disable();
|
||||
if (ioctl(fd, EVIOCGRAB, (void *)1) != 0)
|
||||
return disable();
|
||||
|
||||
epollfd = epoll_create1(EPOLL_CLOEXEC);
|
||||
assert(epollfd >= 0);
|
||||
|
||||
ev.events = EPOLLIN;
|
||||
ev.data.fd = fd;
|
||||
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev)) {
|
||||
perror("joystick: unable to epoll add");
|
||||
return disable();
|
||||
}
|
||||
// This is the first time the OSThread library has called this function, so do port setup
|
||||
firstTime = false;
|
||||
}
|
||||
|
||||
int nfds = epoll_wait(epollfd, events, JOY_MAX_EVENTS, 1);
|
||||
if (nfds < 0) {
|
||||
perror("joystick: epoll_wait failed");
|
||||
return disable();
|
||||
}
|
||||
|
||||
for (int i = 0; i < nfds; i++) {
|
||||
struct input_event evs[64];
|
||||
int rd = read(events[i].data.fd, evs, sizeof(evs));
|
||||
if (rd < (signed int)sizeof(struct input_event))
|
||||
continue;
|
||||
for (int j = 0; j < rd / ((signed int)sizeof(struct input_event)); j++) {
|
||||
unsigned int type = evs[j].type;
|
||||
unsigned int code = evs[j].code;
|
||||
int value = evs[j].value;
|
||||
|
||||
if (type == EV_ABS) {
|
||||
// D-pad reports as ABS_X / ABS_Y with digital 0 / 127 / 255 values. Emit on the
|
||||
// transition to an edge and arm auto-repeat; a held direction repeats below.
|
||||
if (code == ABS_X) {
|
||||
int zone = axisZone(value);
|
||||
if (zone != heldX) {
|
||||
heldX = zone;
|
||||
if (zone != 0) {
|
||||
emitEvent((zone < 0) ? INPUT_BROKER_LEFT : INPUT_BROKER_RIGHT);
|
||||
nextRepeatX = millis() + JOY_REPEAT_DELAY_MS;
|
||||
}
|
||||
}
|
||||
} else if (code == ABS_Y) {
|
||||
int zone = axisZone(value);
|
||||
if (zone != heldY) {
|
||||
heldY = zone;
|
||||
if (zone != 0) {
|
||||
emitEvent((zone < 0) ? INPUT_BROKER_UP : INPUT_BROKER_DOWN);
|
||||
nextRepeatY = millis() + JOY_REPEAT_DELAY_MS;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (type == EV_KEY && value == 1) {
|
||||
// Look up the configured action for this button; unmapped buttons are ignored.
|
||||
// Buttons fire once per press (no auto-repeat).
|
||||
auto mapped = buttonMap.find(code);
|
||||
if (mapped != buttonMap.end())
|
||||
emitEvent(mapped->second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-repeat held D-pad directions. Signed comparison is wraparound-safe.
|
||||
uint32_t now = millis();
|
||||
if (heldX != 0 && (int32_t)(now - nextRepeatX) >= 0) {
|
||||
emitEvent((heldX < 0) ? INPUT_BROKER_LEFT : INPUT_BROKER_RIGHT);
|
||||
nextRepeatX = now + JOY_REPEAT_INTERVAL_MS;
|
||||
}
|
||||
if (heldY != 0 && (int32_t)(now - nextRepeatY) >= 0) {
|
||||
emitEvent((heldY < 0) ? INPUT_BROKER_UP : INPUT_BROKER_DOWN);
|
||||
nextRepeatY = now + JOY_REPEAT_INTERVAL_MS;
|
||||
}
|
||||
|
||||
return 50; // Poll every 50msec
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,65 @@
|
||||
#pragma once
|
||||
// Linux evdev gamepad/joystick input. Only compiled on Linux portduino targets;
|
||||
// macOS / non-Linux builds have no <linux/input.h> or epoll. Unlike LinuxInput
|
||||
// (keyboard, EV_KEY only) this decodes the D-pad from EV_ABS axes as well.
|
||||
#if ARCH_PORTDUINO && defined(__linux__)
|
||||
#include "InputBroker.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
#include <linux/input.h>
|
||||
#include <map>
|
||||
#include <stdint.h>
|
||||
#include <sys/epoll.h>
|
||||
|
||||
#define JOY_MAX_EVENTS 10
|
||||
|
||||
// Default button map when config.yaml has no [Input] JoystickButtons override.
|
||||
// Codes confirmed by evdev capture on a 0079:0011 "USB Gamepad".
|
||||
#define JOY_BTN_A BTN_THUMB // 0x121 -> INPUT_BROKER_SELECT
|
||||
#define JOY_BTN_B BTN_THUMB2 // 0x122 -> INPUT_BROKER_CANCEL
|
||||
#define JOY_AXIS_CENTER 127 // D-pad resting value
|
||||
#define JOY_AXIS_LOW 64 // below this -> "min" edge (0)
|
||||
#define JOY_AXIS_HIGH 192 // above this -> "max" edge (255)
|
||||
|
||||
// D-pad auto-repeat while a direction is held (typematic).
|
||||
#define JOY_REPEAT_DELAY_MS 400 // hold this long before repeats start
|
||||
#define JOY_REPEAT_INTERVAL_MS 150 // then repeat this often
|
||||
|
||||
class LinuxJoystick : public Observable<const InputEvent *>, public concurrency::OSThread
|
||||
{
|
||||
public:
|
||||
explicit LinuxJoystick(const char *name);
|
||||
void init(); // Registers this source with the InputBroker
|
||||
void deInit(); // Strictly for cleanly "rebooting" the binary on native
|
||||
|
||||
// Current held D-pad zone, updated the instant the axis moves (independent of the typematic
|
||||
// auto-repeat). -1 = left/up edge, 0 = centered, +1 = right/down edge. Lets a game poll for
|
||||
// smooth continuous control instead of waiting for the slow repeat events.
|
||||
int heldXZone() const { return heldX; }
|
||||
int heldYZone() const { return heldY; }
|
||||
|
||||
protected:
|
||||
virtual int32_t runOnce() override;
|
||||
|
||||
private:
|
||||
void emitEvent(input_broker_event event);
|
||||
|
||||
const char *_originName;
|
||||
bool firstTime = true;
|
||||
|
||||
// evdev button code -> broker event, built from config (or defaults) in init().
|
||||
std::map<int, input_broker_event> buttonMap;
|
||||
|
||||
struct epoll_event events[JOY_MAX_EVENTS];
|
||||
struct epoll_event ev;
|
||||
int fd = -1;
|
||||
int epollfd = -1;
|
||||
|
||||
// D-pad auto-repeat state: currently held zone per axis (-1 / 0 / +1) and the
|
||||
// next millis() timestamp at which to re-emit while the direction is held.
|
||||
int heldX = 0;
|
||||
int heldY = 0;
|
||||
uint32_t nextRepeatX = 0;
|
||||
uint32_t nextRepeatY = 0;
|
||||
};
|
||||
extern LinuxJoystick *aLinuxJoystick;
|
||||
#endif
|
||||
+17
-1
@@ -22,13 +22,13 @@
|
||||
|
||||
#include "FSCommon.h"
|
||||
#include "Power.h"
|
||||
#include "RTC.h"
|
||||
#include "SPILock.h"
|
||||
#include "Throttle.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
#include "concurrency/Periodic.h"
|
||||
#include "detect/ScanI2C.h"
|
||||
#include "error.h"
|
||||
#include "gps/RTC.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_I2C
|
||||
#include "detect/ScanI2CConsumer.h"
|
||||
@@ -196,8 +196,16 @@ void setupNicheGraphics();
|
||||
#endif
|
||||
|
||||
#if defined(HW_SPI1_DEVICE) && defined(ARCH_ESP32)
|
||||
#if defined(HAS_SDCARD) && defined(SDCARD_USE_SPI1)
|
||||
// Reuse FSCommon's SPI_HSPI instance to avoid double-initializing SPI2_HOST in arduino-esp32 3.x.
|
||||
// Two SPIClass(HSPI) objects on the same bus cause the second spi_bus_initialize() to return
|
||||
// ESP_ERR_INVALID_STATE, leaving the LoRa device handle invalid and blocking SPI transfers.
|
||||
extern SPIClass SPI_HSPI;
|
||||
SPIClass &SPI1 = SPI_HSPI;
|
||||
#else
|
||||
SPIClass SPI1(HSPI);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
using namespace concurrency;
|
||||
|
||||
@@ -420,10 +428,14 @@ void setup()
|
||||
#if ARCH_PORTDUINO
|
||||
RTCQuality ourQuality = RTCQualityDevice;
|
||||
|
||||
#ifdef __linux__
|
||||
// timedatectl is systemd-only, so macOS, Windows and WASM stay at
|
||||
// RTCQualityDevice rather than claim NTP quality we have not verified.
|
||||
std::string timeCommandResult = exec("timedatectl status | grep synchronized | grep yes -c");
|
||||
if (timeCommandResult[0] == '1') {
|
||||
ourQuality = RTCQualityNTP;
|
||||
}
|
||||
#endif
|
||||
|
||||
struct timeval tv;
|
||||
tv.tv_sec = time(NULL);
|
||||
@@ -808,6 +820,10 @@ void setup()
|
||||
rp2040Setup();
|
||||
#endif
|
||||
|
||||
#ifdef ARCH_STM32WL
|
||||
stm32wlSetup();
|
||||
#endif
|
||||
|
||||
// We do this as early as possible because this loads preferences from flash
|
||||
// but we need to do this after main cpu init (esp32setup), because we need the random seed set
|
||||
nodeDB = new NodeDB;
|
||||
|
||||
+2
-1
@@ -113,7 +113,8 @@ extern bool runASAP;
|
||||
|
||||
extern bool pauseBluetoothLogging;
|
||||
|
||||
void nrf52Setup(), esp32Setup(), nrf52Loop(), esp32Loop(), rp2040Setup(), rp2040Loop(), clearBonds(), enterDfuMode();
|
||||
void nrf52Setup(), esp32Setup(), nrf52Loop(), esp32Loop(), rp2040Setup(), rp2040Loop(), clearBonds(), enterDfuMode(),
|
||||
stm32wlSetup();
|
||||
#ifdef ARCH_ESP32
|
||||
void esp32ReleaseBluetoothMemoryIfUnused();
|
||||
#endif
|
||||
|
||||
@@ -516,7 +516,7 @@ bool Channels::hasDefaultChannel()
|
||||
*/
|
||||
bool Channels::decryptForHash(ChannelIndex chIndex, ChannelHash channelHash)
|
||||
{
|
||||
if (chIndex > getNumChannels() || getHash(chIndex) != channelHash) {
|
||||
if (chIndex >= getNumChannels() || getHash(chIndex) != channelHash) {
|
||||
// LOG_DEBUG("Skip channel %d (hash %x) due to invalid hash/index, want=%x", chIndex, getHash(chIndex),
|
||||
// channelHash);
|
||||
return false;
|
||||
|
||||
@@ -224,7 +224,11 @@ bool CryptoEngine::encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtas
|
||||
uint64_t packetNum, size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut)
|
||||
{
|
||||
uint8_t *auth;
|
||||
long extraNonceTmp = random();
|
||||
// The extra nonce must be unpredictable: use the hardware RNG, falling back to the
|
||||
// seeded CSPRNG only when no hardware source is available.
|
||||
uint32_t extraNonceTmp;
|
||||
if (!HardwareRNG::fill((uint8_t *)&extraNonceTmp, sizeof(extraNonceTmp)))
|
||||
CryptRNG.rand((uint8_t *)&extraNonceTmp, sizeof(extraNonceTmp));
|
||||
auth = bytesOut + numBytes;
|
||||
memcpy((uint8_t *)(auth + 8), &extraNonceTmp,
|
||||
sizeof(uint32_t)); // do not use dereference on potential non aligned pointers : *extraNonce = extraNonceTmp;
|
||||
@@ -341,6 +345,32 @@ bool CryptoEngine::setDHPublicKey(uint8_t *pubKey)
|
||||
return true;
|
||||
}
|
||||
|
||||
void CryptoEngine::setPendingPublicKey(uint32_t node, const uint8_t *key)
|
||||
{
|
||||
concurrency::LockGuard g(&pendingKeyLock);
|
||||
pendingKeyVerificationNode = node;
|
||||
memcpy(pendingKeyVerificationPublicKey, key, 32);
|
||||
hasPendingKeyVerificationKey = true;
|
||||
}
|
||||
|
||||
void CryptoEngine::clearPendingPublicKey()
|
||||
{
|
||||
concurrency::LockGuard g(&pendingKeyLock);
|
||||
pendingKeyVerificationNode = 0;
|
||||
memset(pendingKeyVerificationPublicKey, 0, 32);
|
||||
hasPendingKeyVerificationKey = false;
|
||||
}
|
||||
|
||||
bool CryptoEngine::getPendingPublicKey(uint32_t node, meshtastic_NodeInfoLite_public_key_t &out)
|
||||
{
|
||||
concurrency::LockGuard g(&pendingKeyLock);
|
||||
if (!hasPendingKeyVerificationKey || node == 0 || node != pendingKeyVerificationNode)
|
||||
return false;
|
||||
out.size = 32;
|
||||
memcpy(out.bytes, pendingKeyVerificationPublicKey, 32);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
concurrency::Lock *cryptLock;
|
||||
|
||||
|
||||
@@ -59,6 +59,17 @@ class CryptoEngine
|
||||
virtual bool setDHPublicKey(uint8_t *publicKey);
|
||||
virtual void hash(uint8_t *bytes, size_t numBytes);
|
||||
|
||||
// Temporary holder for a peer's not-yet-verified public key, learned in-band during an
|
||||
// in-progress key-verification handshake before it is committed to NodeDB. Lets the Router
|
||||
// run the DH handshake to encode/decode the follow-on PKI packet. Single slot is enough:
|
||||
// only one verification runs at a time. Discarded when the handshake ends (resetToIdle).
|
||||
// Internally guarded by pendingKeyLock, not cryptLock: the Router calls the getter while
|
||||
// already holding the non-recursive cryptLock; KeyVerificationModule writes from elsewhere.
|
||||
void setPendingPublicKey(uint32_t node, const uint8_t *key);
|
||||
void clearPendingPublicKey();
|
||||
// Fills `out` (size set to 32) and returns true iff a pending key is held for `node`.
|
||||
bool getPendingPublicKey(uint32_t node, meshtastic_NodeInfoLite_public_key_t &out);
|
||||
|
||||
virtual void aesSetKey(const uint8_t *key, size_t key_len);
|
||||
|
||||
virtual void aesEncrypt(uint8_t *in, uint8_t *out);
|
||||
@@ -94,6 +105,10 @@ class CryptoEngine
|
||||
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||
uint8_t shared_key[32] = {0};
|
||||
uint8_t private_key[32] = {0};
|
||||
uint32_t pendingKeyVerificationNode = 0;
|
||||
uint8_t pendingKeyVerificationPublicKey[32] = {0};
|
||||
bool hasPendingKeyVerificationKey = false;
|
||||
concurrency::Lock pendingKeyLock;
|
||||
#if !(MESHTASTIC_EXCLUDE_XEDDSA)
|
||||
uint8_t xeddsa_public_key[32] = {0};
|
||||
uint8_t xeddsa_private_key[32] = {0};
|
||||
|
||||
@@ -22,6 +22,12 @@ extern Adafruit_nRFCrypto nRFCrypto;
|
||||
#include <unistd.h>
|
||||
#ifdef __linux__
|
||||
#include <sys/random.h> // getrandom()
|
||||
#elif defined(_WIN32)
|
||||
// Order is load-bearing, hence the blank line: bcrypt.h uses LONG/ULONG from
|
||||
// windows.h and does not include it itself.
|
||||
#include <windows.h>
|
||||
|
||||
#include <bcrypt.h> // BCryptGenRandom()
|
||||
#else
|
||||
#include <stdlib.h> // arc4random_buf() on Darwin/BSD
|
||||
#endif
|
||||
@@ -128,6 +134,12 @@ bool fill(uint8_t *buffer, size_t length, bool useRadioEntropy)
|
||||
if (generated == static_cast<ssize_t>(length)) {
|
||||
filled = true;
|
||||
}
|
||||
#elif defined(_WIN32)
|
||||
// No getrandom/arc4random on Windows; BCryptGenRandom is the documented CSPRNG.
|
||||
// Preferred over std::random_device, whose libstdc++ Windows backend reports entropy() == 0.
|
||||
if (BCryptGenRandom(NULL, buffer, static_cast<ULONG>(length), BCRYPT_USE_SYSTEM_PREFERRED_RNG) == 0) { // STATUS_SUCCESS
|
||||
filled = true;
|
||||
}
|
||||
#elif defined(__EMSCRIPTEN__)
|
||||
// Browser/wasm: no getrandom/arc4random - fall through to std::random_device,
|
||||
// which emscripten backs with crypto.getRandomValues().
|
||||
|
||||
@@ -229,6 +229,11 @@ static inline void modemPresetToParams(meshtastic_Config_LoRaConfig_ModemPreset
|
||||
cr = 5;
|
||||
sf = 10;
|
||||
break;
|
||||
case PRESET(MEDIUM_TURBO):
|
||||
bwKHz = wideLora ? 1625.0f : 500.0f;
|
||||
cr = 5;
|
||||
sf = 9;
|
||||
break;
|
||||
case PRESET(LONG_TURBO):
|
||||
bwKHz = wideLora ? 1625.0f : 500.0f;
|
||||
cr = 8;
|
||||
|
||||
@@ -11,12 +11,13 @@
|
||||
#include "NodeDB.h"
|
||||
#include "Power.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "RTC.h"
|
||||
#include "TypeConversions.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "graphics/draw/MessageRenderer.h"
|
||||
#include "main.h"
|
||||
#include "mesh-pb-constants.h"
|
||||
#include "meshUtils.h"
|
||||
#include "modules/AdminModule.h"
|
||||
#include "modules/NodeInfoModule.h"
|
||||
#include "modules/PositionModule.h"
|
||||
#include "modules/RoutingModule.h"
|
||||
@@ -63,6 +64,7 @@ Allocator<meshtastic_ClientNotification> &clientNotificationPool = staticClientN
|
||||
|
||||
Allocator<meshtastic_QueueStatus> &queueStatusPool = staticQueueStatusPool;
|
||||
|
||||
#include "PositionPrecision.h"
|
||||
#include "Router.h"
|
||||
|
||||
MeshService::MeshService()
|
||||
@@ -173,6 +175,51 @@ NodeNum MeshService::getNodenumFromRequestId(uint32_t request_id)
|
||||
return nodenum;
|
||||
}
|
||||
|
||||
#if MESHTASTIC_ENABLE_FRAME_INJECTION
|
||||
// Deliver a client-supplied frame into the receive pipeline as if it arrived off the LoRa chip. Mirrors
|
||||
// the portduino SimRadio SIMULATOR_APP unwrap so the same host wire format works on real hardware: the
|
||||
// frame rides inside a Compressed envelope wrapped in a MeshPacket that carries from/to/id/channel.
|
||||
// Compressed.portnum == UNKNOWN_APP -> Compressed.data is verbatim ciphertext, decrypted as if off-air
|
||||
// otherwise -> Compressed.data is the plaintext payload for Compressed.portnum
|
||||
void MeshService::injectAsReceived(meshtastic_MeshPacket &p)
|
||||
{
|
||||
meshtastic_Compressed scratch;
|
||||
if (p.which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
|
||||
memset(&scratch, 0, sizeof(scratch));
|
||||
if (pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &meshtastic_Compressed_msg, &scratch)) {
|
||||
if (scratch.portnum == meshtastic_PortNum_UNKNOWN_APP) {
|
||||
p.which_payload_variant = meshtastic_MeshPacket_encrypted_tag;
|
||||
memcpy(p.encrypted.bytes, scratch.data.bytes, scratch.data.size);
|
||||
p.encrypted.size = scratch.data.size;
|
||||
} else {
|
||||
memcpy(&p.decoded.payload, &scratch.data, sizeof(scratch.data));
|
||||
p.decoded.portnum = scratch.portnum;
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR("inject: could not decode Compressed envelope, dropping");
|
||||
return;
|
||||
}
|
||||
}
|
||||
// The real RX path (RadioLibInterface::handleReceiveInterrupt) drops sender==0; mirror it so injection
|
||||
// behaves identically to an over-the-air frame.
|
||||
if (p.from == 0) {
|
||||
LOG_WARN("inject: dropping frame with from==0 (matches real LoRa RX)");
|
||||
return;
|
||||
}
|
||||
meshtastic_MeshPacket *mp = packetPool.allocCopy(p);
|
||||
if (!mp)
|
||||
return;
|
||||
if (mp->rx_snr == 0) // plausible synthetic link metadata unless the caller set it
|
||||
mp->rx_snr = 8;
|
||||
if (mp->rx_rssi == 0)
|
||||
mp->rx_rssi = -40;
|
||||
mp->rx_time = getValidTime(RTCQualityFromNet);
|
||||
LOG_INFO("inject: RX from=0x%08x to=0x%08x id=0x%08x ch=%d %s", mp->from, mp->to, mp->id, mp->channel,
|
||||
mp->which_payload_variant == meshtastic_MeshPacket_encrypted_tag ? "encrypted" : "decoded");
|
||||
router->enqueueReceivedMessage(mp);
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Given a ToRadio buffer parse it and properly handle it (setup radio, owner or send packet into the mesh)
|
||||
* Called by PhoneAPI.handleToRadio. Note: p is a scratch buffer, this function is allowed to write to it but it can not keep a
|
||||
@@ -186,6 +233,16 @@ void MeshService::handleToRadio(meshtastic_MeshPacket &p)
|
||||
SimRadio::instance->unpackAndReceive(p);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
#if MESHTASTIC_ENABLE_FRAME_INJECTION
|
||||
// Real-hardware analog of the SimRadio path above: deliver a client-supplied frame into the RX
|
||||
// pipeline exactly as if it had arrived off the LoRa chip. Reached before the p.from=0 line below,
|
||||
// so an injected sender is preserved. Build-flag gated (off by default) - it lets anything with a
|
||||
// wired connection forge over-the-air traffic, so it must never ship enabled.
|
||||
if (p.which_payload_variant == meshtastic_MeshPacket_decoded_tag && p.decoded.portnum == meshtastic_PortNum_SIMULATOR_APP) {
|
||||
injectAsReceived(p);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
p.from = 0; // We don't let clients assign nodenums to their sent messages
|
||||
p.next_hop = NO_NEXT_HOP_PREFERENCE; // We don't let clients assign next_hop to their sent messages
|
||||
@@ -200,9 +257,17 @@ void MeshService::handleToRadio(meshtastic_MeshPacket &p)
|
||||
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
|
||||
if (const StoredMessage *sm = messageStore.tryAddFromPacket(p))
|
||||
graphics::MessageRenderer::handleNewMessage(nullptr, *sm, p); // notify UI
|
||||
})
|
||||
#if !MESHTASTIC_EXCLUDE_ADMIN
|
||||
// Note admin requests on their way out: AdminModule only accepts a response from a remote we
|
||||
// actually asked. Runs before encryption, while the payload is still readable.
|
||||
if (adminModule && p.which_payload_variant == meshtastic_MeshPacket_decoded_tag &&
|
||||
p.decoded.portnum == meshtastic_PortNum_ADMIN_APP)
|
||||
adminModule->noteOutgoingAdminRequest(p);
|
||||
#endif
|
||||
|
||||
// Send the packet into the mesh
|
||||
DEBUG_HEAP_BEFORE;
|
||||
auto a = packetPool.allocCopy(p);
|
||||
@@ -293,8 +358,31 @@ bool MeshService::trySendPosition(NodeNum dest, bool wantReplies)
|
||||
LOG_DEBUG("Skip position ping; no fresh position since boot");
|
||||
return false;
|
||||
}
|
||||
LOG_INFO("Send position ping to 0x%08x, wantReplies=%d, channel=%d", dest, wantReplies, node->channel);
|
||||
positionModule->sendOurPosition(dest, wantReplies, node->channel);
|
||||
// Prefer the node's current channel, but fall back to the first channel with
|
||||
// position enabled (matching PositionModule::sendOurPosition() behavior).
|
||||
uint8_t sendChan = node->channel;
|
||||
if (getPositionPrecisionForChannel(sendChan) == 0) {
|
||||
bool found = false;
|
||||
for (uint8_t ch = 0; ch < 8; ++ch) {
|
||||
if (getPositionPrecisionForChannel(ch) != 0) {
|
||||
sendChan = ch;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
// No channel with position enabled: fall back to sending nodeinfo, as before.
|
||||
if (nodeInfoModule) {
|
||||
LOG_INFO(
|
||||
"No channel with position enabled; sending nodeinfo instead to 0x%08x, wantReplies=%d, channel=%d",
|
||||
dest, wantReplies, node->channel);
|
||||
nodeInfoModule->sendOurNodeInfo(dest, wantReplies, node->channel);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
LOG_INFO("Send position ping to 0x%08x, wantReplies=%d, channel=%d", dest, wantReplies, sendChan);
|
||||
positionModule->sendOurPosition(dest, wantReplies, sendChan);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -159,6 +159,12 @@ class MeshService
|
||||
*/
|
||||
void handleToRadio(meshtastic_MeshPacket &p);
|
||||
|
||||
#if MESHTASTIC_ENABLE_FRAME_INJECTION
|
||||
/// Test/debug seam (build-flag gated, off by default): deliver a client-supplied frame into the
|
||||
/// receive pipeline as if it had arrived off the LoRa chip. See the definition for the wire format.
|
||||
void injectAsReceived(meshtastic_MeshPacket &p);
|
||||
#endif
|
||||
|
||||
/** The radioConfig object just changed, call this to force the hw to change to the new settings
|
||||
* @return true if client devices should be sent a new set of radio configs
|
||||
*/
|
||||
|
||||
+62
-71
@@ -13,7 +13,6 @@
|
||||
#include "NodeDB.h"
|
||||
#include "PacketHistory.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "RTC.h"
|
||||
#include "RadioInterface.h"
|
||||
#include "Router.h"
|
||||
#include "SPILock.h"
|
||||
@@ -21,12 +20,14 @@
|
||||
#include "TransmitHistory.h"
|
||||
#include "TypeConversions.h"
|
||||
#include "error.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "main.h"
|
||||
#include "memory/MemAudit.h"
|
||||
#include "mesh-pb-constants.h"
|
||||
#include "mesh/generated/meshtastic/deviceonly_legacy.pb.h"
|
||||
#include "meshUtils.h"
|
||||
#include "modules/NeighborInfoModule.h"
|
||||
#include "target_specific.h"
|
||||
#if HAS_VARIABLE_HOPS
|
||||
#include "modules/HopScalingModule.h"
|
||||
#endif
|
||||
@@ -50,11 +51,7 @@
|
||||
#include "SPILock.h"
|
||||
#include "modules/StoreForwardModule.h"
|
||||
#include <Preferences.h>
|
||||
#include <esp_efuse.h>
|
||||
#include <esp_efuse_table.h>
|
||||
#include <nvs_flash.h>
|
||||
#include <soc/efuse_reg.h>
|
||||
#include <soc/soc.h>
|
||||
#endif
|
||||
|
||||
#ifdef ARCH_PORTDUINO
|
||||
@@ -372,8 +369,7 @@ void NodeDB::disarmNodeDatabaseDecodeTargets()
|
||||
*/
|
||||
uint32_t radioGeneration;
|
||||
|
||||
// FIXME - move this somewhere else
|
||||
extern void getMacAddr(uint8_t *dmac);
|
||||
// getMacAddr() and getDeviceId() are the per-architecture hooks declared in target_specific.h.
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -410,53 +406,13 @@ NodeDB::NodeDB()
|
||||
uint32_t channelFileCRC = crc32Buffer(&channelFile, sizeof(channelFile));
|
||||
|
||||
int saveWhat = 0;
|
||||
// Get device unique id
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6)
|
||||
uint32_t unique_id[4];
|
||||
// ESP32 factory burns a unique id in efuse for S2+ series and evidently C3+ series
|
||||
// This is used for HMACs in the esp-rainmaker AIOT platform and seems to be a good choice for us
|
||||
esp_err_t err = esp_efuse_read_field_blob(ESP_EFUSE_OPTIONAL_UNIQUE_ID, unique_id, sizeof(unique_id) * 8);
|
||||
if (err == ESP_OK) {
|
||||
memcpy(myNodeInfo.device_id.bytes, unique_id, sizeof(unique_id));
|
||||
myNodeInfo.device_id.size = 16;
|
||||
} else {
|
||||
LOG_WARN("Failed to read unique id from efuse");
|
||||
// Re-read the device id from silicon each boot via the per-arch getDeviceId(); clear the
|
||||
// disk-loaded value first so a failed/empty derivation leaves it unset rather than stale.
|
||||
myNodeInfo.device_id.size = 0;
|
||||
memset(myNodeInfo.device_id.bytes, 0, sizeof(myNodeInfo.device_id.bytes));
|
||||
if (getDeviceId(myNodeInfo.device_id.bytes)) {
|
||||
myNodeInfo.device_id.size = sizeof(myNodeInfo.device_id.bytes);
|
||||
}
|
||||
#elif defined(ARCH_NRF54L15)
|
||||
// nRF54L15: DEVICEID is under FICR->INFO sub-struct (not top-level as on nRF52)
|
||||
uint64_t device_id_start = ((uint64_t)NRF_FICR->INFO.DEVICEID[1] << 32) | NRF_FICR->INFO.DEVICEID[0];
|
||||
uint64_t device_id_end = ((uint64_t)NRF_FICR->DEVICEADDR[1] << 32) | NRF_FICR->DEVICEADDR[0];
|
||||
memcpy(myNodeInfo.device_id.bytes, &device_id_start, sizeof(device_id_start));
|
||||
memcpy(myNodeInfo.device_id.bytes + sizeof(device_id_start), &device_id_end, sizeof(device_id_end));
|
||||
myNodeInfo.device_id.size = 16;
|
||||
#elif defined(ARCH_NRF52)
|
||||
// Nordic applies a FIPS compliant Random ID to each chip at the factory
|
||||
// We concatenate the device address to the Random ID to create a unique ID for now
|
||||
// This will likely utilize a crypto module in the future
|
||||
uint64_t device_id_start = ((uint64_t)NRF_FICR->DEVICEID[1] << 32) | NRF_FICR->DEVICEID[0];
|
||||
uint64_t device_id_end = ((uint64_t)NRF_FICR->DEVICEADDR[1] << 32) | NRF_FICR->DEVICEADDR[0];
|
||||
memcpy(myNodeInfo.device_id.bytes, &device_id_start, sizeof(device_id_start));
|
||||
memcpy(myNodeInfo.device_id.bytes + sizeof(device_id_start), &device_id_end, sizeof(device_id_end));
|
||||
myNodeInfo.device_id.size = 16;
|
||||
// Uncomment below to print the device id
|
||||
#elif ARCH_PORTDUINO
|
||||
if (portduino_config.has_device_id) {
|
||||
memcpy(myNodeInfo.device_id.bytes, portduino_config.device_id, 16);
|
||||
myNodeInfo.device_id.size = 16;
|
||||
}
|
||||
#else
|
||||
// FIXME - implement for other platforms
|
||||
#endif
|
||||
|
||||
// if (myNodeInfo.device_id.size == 16) {
|
||||
// std::string deviceIdHex;
|
||||
// for (size_t i = 0; i < myNodeInfo.device_id.size; ++i) {
|
||||
// char buf[3];
|
||||
// snprintf(buf, sizeof(buf), "%02X", myNodeInfo.device_id.bytes[i]);
|
||||
// deviceIdHex += buf;
|
||||
// }
|
||||
// LOG_DEBUG("Device ID (HEX): %s", deviceIdHex.c_str());
|
||||
// }
|
||||
|
||||
// likewise - we always want the app requirements to come from the running appload
|
||||
myNodeInfo.min_app_version = 30200; // format is Mmmss (where M is 1+the numeric major number. i.e. 30200 means 2.2.00
|
||||
@@ -522,6 +478,12 @@ NodeDB::NodeDB()
|
||||
LOG_DEBUG("Number of Device Reboots: %d", myNodeInfo.reboot_count);
|
||||
#endif
|
||||
|
||||
// UA_868 is obsolete; migrate to EU_868 before resetRadioConfig() below validates the region.
|
||||
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UA_868) {
|
||||
LOG_INFO("UA_868 region is obsolete, migrating saved config to EU_868");
|
||||
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
||||
}
|
||||
|
||||
resetRadioConfig(); // If bogus settings got saved, then fix them
|
||||
// nodeDB->LOG_DEBUG("region=%d, NODENUM=0x%x, dbsize=%d", config.lora.region, myNodeInfo.my_node_num, numMeshNodes);
|
||||
|
||||
@@ -1564,16 +1526,34 @@ void NodeDB::initModuleConfigIntervals()
|
||||
moduleConfig.telemetry.device_update_interval = MAX_INTERVAL;
|
||||
#endif
|
||||
|
||||
#ifdef USERPREFS_CONFIG_ENVIRONMENT_MEASUREMENT_ENABLED
|
||||
moduleConfig.telemetry.environment_measurement_enabled = USERPREFS_CONFIG_ENVIRONMENT_MEASUREMENT_ENABLED;
|
||||
#endif
|
||||
|
||||
#ifdef USERPREFS_CONFIG_ENV_TELEM_UPDATE_INTERVAL
|
||||
moduleConfig.telemetry.environment_update_interval = USERPREFS_CONFIG_ENV_TELEM_UPDATE_INTERVAL;
|
||||
#else
|
||||
moduleConfig.telemetry.environment_update_interval = 0;
|
||||
#endif
|
||||
|
||||
#ifdef USERPREFS_CONFIG_ENVIRONMENT_MEASUREMENT_ENABLED
|
||||
moduleConfig.telemetry.environment_measurement_enabled = USERPREFS_CONFIG_ENVIRONMENT_MEASUREMENT_ENABLED;
|
||||
#ifdef USERPREFS_CONFIG_ENV_SCREEN_SCREEN_ENABLED
|
||||
moduleConfig.telemetry.environment_screen_enabled = USERPREFS_CONFIG_ENV_SCREEN_SCREEN_ENABLED;
|
||||
#endif
|
||||
|
||||
#ifdef USERPREFS_CONFIG_AQ_TELEM_UPDATE_INTERVAL
|
||||
moduleConfig.telemetry.air_quality_interval = USERPREFS_CONFIG_AQ_TELEM_UPDATE_INTERVAL;
|
||||
#else
|
||||
moduleConfig.telemetry.air_quality_interval = 0;
|
||||
#endif
|
||||
|
||||
#ifdef USERPREFS_CONFIG_AQ_MEASUREMENT_ENABLED
|
||||
moduleConfig.telemetry.air_quality_enabled = USERPREFS_CONFIG_AQ_MEASUREMENT_ENABLED;
|
||||
#endif
|
||||
|
||||
#ifdef USERPREFS_CONFIG_AQ_SCREEN_ENABLED
|
||||
moduleConfig.telemetry.air_quality_screen_enabled = USERPREFS_CONFIG_AQ_SCREEN_ENABLED;
|
||||
#endif
|
||||
|
||||
moduleConfig.telemetry.power_update_interval = 0;
|
||||
moduleConfig.telemetry.health_update_interval = 0;
|
||||
moduleConfig.neighbor_info.update_interval = 0;
|
||||
@@ -1635,10 +1615,16 @@ void NodeDB::removeNodeByNum(NodeNum nodeNum)
|
||||
removed++;
|
||||
}
|
||||
numMeshNodes -= removed;
|
||||
std::fill(nodeDatabase.nodes.begin() + numMeshNodes, nodeDatabase.nodes.begin() + numMeshNodes + 1,
|
||||
meshtastic_NodeInfoLite());
|
||||
if (removed)
|
||||
eraseNodeSatellites(nodeNum);
|
||||
if (removed) {
|
||||
// Clear exactly the slots compaction vacated. Sizing this from `removed` (rather than a
|
||||
// fixed one) keeps it inside the vector when nothing matched and the store is full.
|
||||
const size_t first = numMeshNodes;
|
||||
const size_t last = std::min(first + removed, nodeDatabase.nodes.size());
|
||||
std::fill(nodeDatabase.nodes.begin() + first, nodeDatabase.nodes.begin() + last, meshtastic_NodeInfoLite());
|
||||
}
|
||||
// Drop the node's satellite stores and warm-tier copy regardless of which tier it lived in, so
|
||||
// an explicit removal fully forgets it.
|
||||
eraseNodeSatellites(nodeNum);
|
||||
#if WARM_NODE_COUNT > 0
|
||||
// Explicit user removal: don't let the warm tier resurrect the node
|
||||
warmStore.remove(nodeNum);
|
||||
@@ -1898,7 +1884,8 @@ void NodeDB::cleanupMeshDB()
|
||||
// Keep any key we learned (e.g. via a DM before the NodeInfo
|
||||
// exchange completed) rather than losing it with the purge.
|
||||
if (n.public_key.size == 32)
|
||||
warmStore.absorb(gone, n.last_heard, n.public_key.bytes, n.role, warmProtectedCategory(n));
|
||||
warmStore.absorb(gone, n.last_heard, n.public_key.bytes, n.role, warmProtectedCategory(n),
|
||||
nodeInfoLiteHasXeddsaSigned(&n));
|
||||
#endif
|
||||
|
||||
eraseNodeSatellites(gone);
|
||||
@@ -2082,7 +2069,7 @@ void NodeDB::demoteOldestHotNodesToWarm()
|
||||
// Keep the public key if we have one (40 B warm record); keyless nodes
|
||||
// still get a placeholder so re-admission restores last_heard.
|
||||
warmStore.absorb(n.num, n.last_heard, n.public_key.size > 0 ? n.public_key.bytes : nullptr, n.role,
|
||||
warmProtectedCategory(n));
|
||||
warmProtectedCategory(n), nodeInfoLiteHasXeddsaSigned(&n));
|
||||
// Demotion drops the node from the header table, so drop its satellites
|
||||
// too (the eviction chokepoint) - they'd otherwise orphan until the next
|
||||
// enforceSatelliteCaps pass.
|
||||
@@ -3084,14 +3071,12 @@ HopStartStatus classifyHopStart(const meshtastic_MeshPacket &p)
|
||||
return HopStartStatus::INVALID;
|
||||
|
||||
if (p.hop_start == 0) {
|
||||
// hop_start == hop_limit == 0: intentional zero-hop broadcast (e.g. beacon). Valid by definition -
|
||||
// the packet was never meant to travel any hops, so no hop_start ambiguity applies.
|
||||
if (p.hop_limit == 0)
|
||||
return HopStartStatus::VALID;
|
||||
// Firmware prior to 2.3.0 (585805c) lacked a hop_start field. Firmware version 2.5.0 (bf34329) introduced a
|
||||
// bitfield that is always present. Use the presence of the bitfield to determine if the origin's firmware
|
||||
// version is guaranteed to have hop_start populated. Note that this can only be done for decoded packets as
|
||||
// the bitfield is encrypted under the channel encryption key.
|
||||
// hop_start == 0 is either a modern zero-hop broadcast (e.g. beacon) or pre-2.3.0 firmware (585805c)
|
||||
// that never populated hop_start. Firmware 2.5.0 (bf34329) introduced a bitfield that is always
|
||||
// present; use it to tell the two apart. The bitfield is encrypted under the channel key, so this can
|
||||
// only be resolved for decoded packets - until then the status stays MISSING_OR_UNKNOWN. Callers
|
||||
// acting before decode must therefore not treat UNKNOWN as a drop (the bitfield isn't readable yet);
|
||||
// the verdict is re-checked post-decode in Router::handleReceived.
|
||||
if (p.which_payload_variant == meshtastic_MeshPacket_decoded_tag && p.decoded.has_bitfield)
|
||||
return HopStartStatus::VALID;
|
||||
return HopStartStatus::MISSING_OR_UNKNOWN;
|
||||
@@ -3285,6 +3270,9 @@ void NodeDB::addFromContact(meshtastic_SharedContact contact)
|
||||
LOG_WARN(PROTECTED_CAP_WARN_FMT, "ignore", contact.node_num, MAX_NUM_NODES - 2);
|
||||
nodeInfoLiteSetBit(info, NODEINFO_BITFIELD_IS_FAVORITE_MASK, false);
|
||||
eraseNodeSatellites(contact.node_num);
|
||||
#if HAS_SCREEN || defined(MESHTASTIC_INCLUDE_NICHE_GRAPHICS)
|
||||
messageStore.deleteAllMessagesFromNode(contact.node_num);
|
||||
#endif
|
||||
} else {
|
||||
/* Clients are sending add_contact before every text message DM (because clients may hold a larger node database with
|
||||
* public keys than the radio holds). However, we don't want to update last_heard just because we sent someone a DM!
|
||||
@@ -3788,7 +3776,7 @@ meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n)
|
||||
// Demote to the warm tier so the identity (and crucially the
|
||||
// PKI key) outlives the hot-store slot.
|
||||
warmStore.absorb(evicted.num, evicted.last_heard, evicted.public_key.size == 32 ? evicted.public_key.bytes : NULL,
|
||||
evicted.role, warmProtectedCategory(evicted));
|
||||
evicted.role, warmProtectedCategory(evicted), nodeInfoLiteHasXeddsaSigned(&evicted));
|
||||
#endif
|
||||
eraseNodeSatellites(evicted.num);
|
||||
// Shove the remaining nodes down the chain
|
||||
@@ -3817,10 +3805,13 @@ meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n)
|
||||
// Re-admission: restore what the warm tier kept for this node
|
||||
WarmNodeEntry warm;
|
||||
if (warmStore.take(n, warm)) {
|
||||
lite->last_heard = warmTimeOf(warm); // mask off the stolen role/protected metadata bits
|
||||
lite->last_heard = warmTimeOf(warm); // mask off the stolen metadata bits
|
||||
// Restore the role the warm tier cached, so re-admission isn't stuck at CLIENT
|
||||
// until the next NodeInfo arrives.
|
||||
lite->role = static_cast<meshtastic_Config_DeviceConfig_Role>(warmRoleOf(warm));
|
||||
// Restore the signer bit too: it is learned from verified traffic, not from
|
||||
// NodeInfo, so a round trip through the warm tier must not relearn it from zero.
|
||||
nodeInfoLiteSetBit(lite, NODEINFO_BITFIELD_HAS_XEDDSA_SIGNED_MASK, warmSignerOf(warm));
|
||||
if (!memfll(warm.public_key, 0, sizeof(warm.public_key))) {
|
||||
lite->public_key.size = 32;
|
||||
memcpy(lite->public_key.bytes, warm.public_key, 32);
|
||||
|
||||
+4
-1
@@ -156,7 +156,10 @@ inline bool shouldDropPacketForPreHop(const meshtastic_MeshPacket &p)
|
||||
if (isFromUs(&p)) {
|
||||
return false; // local-originated packets should never be dropped by pre-hop drop policy
|
||||
}
|
||||
return classifyHopStart(p) != HopStartStatus::VALID;
|
||||
// Pre-decode: the channel-encrypted bitfield isn't readable yet, so a missing/unknown hop_start can't be
|
||||
// distinguished from a modern packet. Only drop the provably-corrupt case here; the bitfield-dependent
|
||||
// verdict is re-checked post-decode in Router::handleReceived.
|
||||
return classifyHopStart(p) == HopStartStatus::INVALID;
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -1,253 +0,0 @@
|
||||
#include "PacketCache.h"
|
||||
#include "Router.h"
|
||||
|
||||
PacketCache packetCache{};
|
||||
|
||||
/**
|
||||
* Allocate a new cache entry and copy the packet header and payload into it
|
||||
*/
|
||||
PacketCacheEntry *PacketCache::cache(const meshtastic_MeshPacket *p, bool preserveMetadata)
|
||||
{
|
||||
size_t payload_size =
|
||||
(p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag) ? p->encrypted.size : p->decoded.payload.size;
|
||||
PacketCacheEntry *e = (PacketCacheEntry *)malloc(sizeof(PacketCacheEntry) + payload_size +
|
||||
(preserveMetadata ? sizeof(PacketCacheMetadata) : 0));
|
||||
if (!e) {
|
||||
LOG_ERROR("Unable to allocate memory for packet cache entry");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
*e = {};
|
||||
e->header.from = p->from;
|
||||
e->header.to = p->to;
|
||||
e->header.id = p->id;
|
||||
e->header.channel = p->channel;
|
||||
e->header.next_hop = p->next_hop;
|
||||
e->header.relay_node = p->relay_node;
|
||||
e->header.flags = (p->hop_limit & PACKET_FLAGS_HOP_LIMIT_MASK) | (p->want_ack ? PACKET_FLAGS_WANT_ACK_MASK : 0) |
|
||||
(p->via_mqtt ? PACKET_FLAGS_VIA_MQTT_MASK : 0) |
|
||||
((p->hop_start << PACKET_FLAGS_HOP_START_SHIFT) & PACKET_FLAGS_HOP_START_MASK);
|
||||
|
||||
PacketCacheMetadata m{};
|
||||
if (preserveMetadata) {
|
||||
e->has_metadata = true;
|
||||
m.rx_rssi = (uint8_t)(p->rx_rssi + 200);
|
||||
m.rx_snr = (uint8_t)((p->rx_snr + 30.0f) / 0.25f);
|
||||
m.rx_time = p->rx_time;
|
||||
m.transport_mechanism = p->transport_mechanism;
|
||||
m.priority = p->priority;
|
||||
}
|
||||
if (p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag) {
|
||||
e->encrypted = true;
|
||||
e->payload_len = p->encrypted.size;
|
||||
memcpy(((unsigned char *)e) + sizeof(PacketCacheEntry), p->encrypted.bytes, p->encrypted.size);
|
||||
} else if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
|
||||
e->encrypted = false;
|
||||
if (preserveMetadata) {
|
||||
m.portnum = p->decoded.portnum;
|
||||
m.want_response = p->decoded.want_response;
|
||||
m.emoji = p->decoded.emoji;
|
||||
m.bitfield = p->decoded.bitfield;
|
||||
if (p->decoded.reply_id)
|
||||
m.reply_id = p->decoded.reply_id;
|
||||
else if (p->decoded.request_id)
|
||||
m.request_id = p->decoded.request_id;
|
||||
}
|
||||
e->payload_len = p->decoded.payload.size;
|
||||
memcpy(((unsigned char *)e) + sizeof(PacketCacheEntry), p->decoded.payload.bytes, p->decoded.payload.size);
|
||||
} else {
|
||||
LOG_ERROR("Unable to cache packet with unknown payload type %d", p->which_payload_variant);
|
||||
free(e);
|
||||
return NULL;
|
||||
}
|
||||
if (preserveMetadata)
|
||||
memcpy(((unsigned char *)e) + sizeof(PacketCacheEntry) + e->payload_len, &m, sizeof(m));
|
||||
|
||||
size += sizeof(PacketCacheEntry) + e->payload_len + (e->has_metadata ? sizeof(PacketCacheMetadata) : 0);
|
||||
insert(e);
|
||||
return e;
|
||||
};
|
||||
|
||||
/**
|
||||
* Dump a list of packets into the provided buffer
|
||||
*/
|
||||
void PacketCache::dump(void *dest, const PacketCacheEntry **entries, size_t num_entries)
|
||||
{
|
||||
unsigned char *pos = (unsigned char *)dest;
|
||||
for (size_t i = 0; i < num_entries; i++) {
|
||||
size_t entry_len =
|
||||
sizeof(PacketCacheEntry) + entries[i]->payload_len + (entries[i]->has_metadata ? sizeof(PacketCacheMetadata) : 0);
|
||||
memcpy(pos, entries[i], entry_len);
|
||||
pos += entry_len;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the length of buffer needed to dump the specified entries
|
||||
*/
|
||||
size_t PacketCache::dumpSize(const PacketCacheEntry **entries, size_t num_entries)
|
||||
{
|
||||
size_t total_size = 0;
|
||||
for (size_t i = 0; i < num_entries; i++) {
|
||||
total_size += sizeof(PacketCacheEntry) + entries[i]->payload_len;
|
||||
if (entries[i]->has_metadata)
|
||||
total_size += sizeof(PacketCacheMetadata);
|
||||
}
|
||||
return total_size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a packet in the cache
|
||||
*/
|
||||
PacketCacheEntry *PacketCache::find(NodeNum from, PacketId id)
|
||||
{
|
||||
uint16_t h = PACKET_HASH(from, id);
|
||||
PacketCacheEntry *e = buckets[PACKET_CACHE_BUCKET(h)];
|
||||
while (e) {
|
||||
if (e->header.from == from && e->header.id == id)
|
||||
return e;
|
||||
e = e->next;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a packet in the cache by its hash
|
||||
*/
|
||||
PacketCacheEntry *PacketCache::find(PacketHash h)
|
||||
{
|
||||
PacketCacheEntry *e = buckets[PACKET_CACHE_BUCKET(h)];
|
||||
while (e) {
|
||||
if (PACKET_HASH(e->header.from, e->header.id) == h)
|
||||
return e;
|
||||
e = e->next;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a list of packets from the provided buffer
|
||||
*/
|
||||
bool PacketCache::load(void *src, PacketCacheEntry **entries, size_t num_entries)
|
||||
{
|
||||
memset(entries, 0, sizeof(PacketCacheEntry *) * num_entries);
|
||||
unsigned char *pos = (unsigned char *)src;
|
||||
for (size_t i = 0; i < num_entries; i++) {
|
||||
PacketCacheEntry e{};
|
||||
memcpy(&e, pos, sizeof(PacketCacheEntry));
|
||||
size_t entry_len = sizeof(PacketCacheEntry) + e.payload_len + (e.has_metadata ? sizeof(PacketCacheMetadata) : 0);
|
||||
entries[i] = (PacketCacheEntry *)malloc(entry_len);
|
||||
size += entry_len;
|
||||
if (!entries[i]) {
|
||||
LOG_ERROR("Unable to allocate memory for packet cache entry");
|
||||
for (size_t j = 0; j < i; j++) {
|
||||
size -= sizeof(PacketCacheEntry) + entries[j]->payload_len +
|
||||
(entries[j]->has_metadata ? sizeof(PacketCacheMetadata) : 0);
|
||||
free(entries[j]);
|
||||
entries[j] = NULL;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
memcpy(entries[i], pos, entry_len);
|
||||
pos += entry_len;
|
||||
}
|
||||
for (size_t i = 0; i < num_entries; i++)
|
||||
insert(entries[i]);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the cached packet into the provided MeshPacket structure
|
||||
*/
|
||||
void PacketCache::rehydrate(const PacketCacheEntry *e, meshtastic_MeshPacket *p)
|
||||
{
|
||||
if (!e || !p)
|
||||
return;
|
||||
|
||||
*p = {};
|
||||
p->from = e->header.from;
|
||||
p->to = e->header.to;
|
||||
p->id = e->header.id;
|
||||
p->channel = e->header.channel;
|
||||
p->next_hop = e->header.next_hop;
|
||||
p->relay_node = e->header.relay_node;
|
||||
p->hop_limit = e->header.flags & PACKET_FLAGS_HOP_LIMIT_MASK;
|
||||
p->want_ack = !!(e->header.flags & PACKET_FLAGS_WANT_ACK_MASK);
|
||||
p->via_mqtt = !!(e->header.flags & PACKET_FLAGS_VIA_MQTT_MASK);
|
||||
p->hop_start = (e->header.flags & PACKET_FLAGS_HOP_START_MASK) >> PACKET_FLAGS_HOP_START_SHIFT;
|
||||
p->which_payload_variant = e->encrypted ? meshtastic_MeshPacket_encrypted_tag : meshtastic_MeshPacket_decoded_tag;
|
||||
|
||||
unsigned char *payload = ((unsigned char *)e) + sizeof(PacketCacheEntry);
|
||||
PacketCacheMetadata m{};
|
||||
if (e->has_metadata) {
|
||||
memcpy(&m, (payload + e->payload_len), sizeof(m));
|
||||
p->rx_rssi = ((int)m.rx_rssi) - 200;
|
||||
p->rx_snr = ((float)m.rx_snr * 0.25f) - 30.0f;
|
||||
p->rx_time = m.rx_time;
|
||||
p->transport_mechanism = (meshtastic_MeshPacket_TransportMechanism)m.transport_mechanism;
|
||||
p->priority = (meshtastic_MeshPacket_Priority)m.priority;
|
||||
}
|
||||
if (e->encrypted) {
|
||||
memcpy(p->encrypted.bytes, payload, e->payload_len);
|
||||
p->encrypted.size = e->payload_len;
|
||||
} else {
|
||||
memcpy(p->decoded.payload.bytes, payload, e->payload_len);
|
||||
p->decoded.payload.size = e->payload_len;
|
||||
if (e->has_metadata) {
|
||||
// Decrypted-only metadata
|
||||
p->decoded.portnum = (meshtastic_PortNum)m.portnum;
|
||||
p->decoded.want_response = m.want_response;
|
||||
p->decoded.emoji = m.emoji;
|
||||
p->decoded.bitfield = m.bitfield;
|
||||
if (m.reply_id)
|
||||
p->decoded.reply_id = m.reply_id;
|
||||
else if (m.request_id)
|
||||
p->decoded.request_id = m.request_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Release a cache entry
|
||||
*/
|
||||
void PacketCache::release(PacketCacheEntry *e)
|
||||
{
|
||||
if (!e)
|
||||
return;
|
||||
remove(e);
|
||||
size -= sizeof(PacketCacheEntry) + e->payload_len + (e->has_metadata ? sizeof(PacketCacheMetadata) : 0);
|
||||
free(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a new entry into the hash table
|
||||
*/
|
||||
void PacketCache::insert(PacketCacheEntry *e)
|
||||
{
|
||||
assert(e);
|
||||
PacketHash h = PACKET_HASH(e->header.from, e->header.id);
|
||||
PacketCacheEntry **target = &buckets[PACKET_CACHE_BUCKET(h)];
|
||||
e->next = *target;
|
||||
*target = e;
|
||||
num_entries++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an entry from the hash table
|
||||
*/
|
||||
void PacketCache::remove(PacketCacheEntry *e)
|
||||
{
|
||||
assert(e);
|
||||
PacketHash h = PACKET_HASH(e->header.from, e->header.id);
|
||||
PacketCacheEntry **target = &buckets[PACKET_CACHE_BUCKET(h)];
|
||||
while (*target) {
|
||||
if (*target == e) {
|
||||
*target = e->next;
|
||||
e->next = NULL;
|
||||
num_entries--;
|
||||
break;
|
||||
} else {
|
||||
target = &(*target)->next;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
#pragma once
|
||||
#include "RadioInterface.h"
|
||||
|
||||
#define PACKET_HASH(a, b) ((((a ^ b) >> 16) ^ (a ^ b)) & 0xFFFF) // 16 bit fold of packet (from, id) tuple
|
||||
typedef uint16_t PacketHash;
|
||||
|
||||
#define PACKET_CACHE_BUCKETS 64 // Number of hash table buckets
|
||||
#define PACKET_CACHE_BUCKET(h) (((h >> 12) ^ (h >> 6) ^ h) & 0x3F) // Fold hash down to 6-bit bucket index
|
||||
|
||||
typedef struct PacketCacheEntry {
|
||||
PacketCacheEntry *next;
|
||||
PacketHeader header;
|
||||
uint16_t payload_len = 0;
|
||||
union {
|
||||
uint16_t bitfield;
|
||||
struct {
|
||||
uint8_t encrypted : 1; // Payload is encrypted
|
||||
uint8_t has_metadata : 1; // Payload includes PacketCacheMetadata
|
||||
uint8_t : 6; // Reserved for future use
|
||||
uint8_t : 8; // Reserved for future use
|
||||
};
|
||||
};
|
||||
} PacketCacheEntry;
|
||||
|
||||
typedef struct PacketCacheMetadata {
|
||||
PacketCacheMetadata() : _bitfield(0), reply_id(0), _bitfield2(0) {}
|
||||
union {
|
||||
uint32_t _bitfield;
|
||||
struct {
|
||||
uint16_t portnum : 9; // meshtastic_MeshPacket::decoded::portnum
|
||||
uint16_t want_response : 1; // meshtastic_MeshPacket::decoded::want_response
|
||||
uint16_t emoji : 1; // meshtastic_MeshPacket::decoded::emoji
|
||||
uint16_t bitfield : 5; // meshtastic_MeshPacket::decoded::bitfield (truncated)
|
||||
uint8_t rx_rssi : 8; // meshtastic_MeshPacket::rx_rssi (map via actual RSSI + 200)
|
||||
uint8_t rx_snr : 8; // meshtastic_MeshPacket::rx_snr (map via (p->rx_snr + 30.0f) / 0.25f)
|
||||
};
|
||||
};
|
||||
union {
|
||||
uint32_t reply_id; // meshtastic_MeshPacket::decoded.reply_id
|
||||
uint32_t request_id; // meshtastic_MeshPacket::decoded.request_id
|
||||
};
|
||||
uint32_t rx_time = 0; // meshtastic_MeshPacket::rx_time
|
||||
uint8_t transport_mechanism = 0; // meshtastic_MeshPacket::transport_mechanism
|
||||
struct {
|
||||
uint8_t _bitfield2;
|
||||
union {
|
||||
uint8_t priority : 7; // meshtastic_MeshPacket::priority
|
||||
uint8_t reserved : 1; // Reserved for future use
|
||||
};
|
||||
};
|
||||
} PacketCacheMetadata;
|
||||
|
||||
class PacketCache
|
||||
{
|
||||
public:
|
||||
PacketCacheEntry *cache(const meshtastic_MeshPacket *p, bool preserveMetadata);
|
||||
static void dump(void *dest, const PacketCacheEntry **entries, size_t num_entries);
|
||||
size_t dumpSize(const PacketCacheEntry **entries, size_t num_entries);
|
||||
PacketCacheEntry *find(NodeNum from, PacketId id);
|
||||
PacketCacheEntry *find(PacketHash h);
|
||||
bool load(void *src, PacketCacheEntry **entries, size_t num_entries);
|
||||
size_t getNumEntries() { return num_entries; }
|
||||
size_t getSize() { return size; }
|
||||
void rehydrate(const PacketCacheEntry *e, meshtastic_MeshPacket *p);
|
||||
void release(PacketCacheEntry *e);
|
||||
|
||||
private:
|
||||
PacketCacheEntry *buckets[PACKET_CACHE_BUCKETS]{};
|
||||
size_t num_entries = 0;
|
||||
size_t size = 0;
|
||||
void insert(PacketCacheEntry *e);
|
||||
void remove(PacketCacheEntry *e);
|
||||
};
|
||||
|
||||
extern PacketCache packetCache;
|
||||
+81
-31
@@ -38,7 +38,7 @@
|
||||
#include "mqtt/MQTT.h"
|
||||
#endif
|
||||
#include "Throttle.h"
|
||||
#include <RTC.h>
|
||||
#include "gps/RTC.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -1201,19 +1201,49 @@ void PhoneAPI::prefetchNodeInfos()
|
||||
onNowHasData(0);
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
/// Derive a stable id for a replayed satellite-DB record. Unchanged history replayed on
|
||||
/// every reconnect must not look like a new packet to the phone's history/dedup - the id
|
||||
/// only changes when the underlying data (its timestamp) actually changes.
|
||||
// `kind` only needs to distinguish the record types that can otherwise collide (e.g. device
|
||||
// vs. environment metrics both replay as TELEMETRY_APP with the same node/last_heard) - pass
|
||||
// the payload's own variant/port constant.
|
||||
uint32_t makeReplayPacketId(NodeNum num, uint32_t timestamp, uint32_t kind)
|
||||
{
|
||||
uint32_t h = num;
|
||||
h = h * 2654435761u + timestamp;
|
||||
h = h * 2654435761u + kind;
|
||||
return h ? h : 1; // some clients treat id 0 as "unset"
|
||||
}
|
||||
|
||||
/// Populate hop_start/hop_limit from the node's real last-known hop count (if any) so a
|
||||
/// replayed packet doesn't read as "heard directly" when it wasn't.
|
||||
void setReplayHopFields(meshtastic_MeshPacket &pkt, const meshtastic_NodeInfoLite *header)
|
||||
{
|
||||
uint8_t hopLimit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
||||
uint8_t hopsAway = (header && header->has_hops_away) ? header->hops_away : 0;
|
||||
pkt.hop_start = hopLimit;
|
||||
pkt.hop_limit = hopsAway < hopLimit ? (uint8_t)(hopLimit - hopsAway) : 0;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
meshtastic_MeshPacket PhoneAPI::makeReplayPositionPacket(NodeNum num, const meshtastic_PositionLite &pos)
|
||||
{
|
||||
// Shape this exactly like a fresh live broadcast Position from the peer so the
|
||||
// phone runs it through its normal "live position broadcast" handler path.
|
||||
// to=ourNum would read as a DM-from-peer and never lands in node detail UI.
|
||||
meshtastic_MeshPacket pkt = meshtastic_MeshPacket_init_default;
|
||||
const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
|
||||
pkt.from = num;
|
||||
pkt.to = NODENUM_BROADCAST;
|
||||
pkt.id = generatePacketId();
|
||||
pkt.rx_time = pos.time;
|
||||
// Stable per-node/per-fix id: replaying the same unchanged history on every
|
||||
// reconnect must not look like a brand new packet to the phone's history/dedup.
|
||||
pkt.id = makeReplayPacketId(num, pkt.rx_time, meshtastic_PortNum_POSITION_APP);
|
||||
pkt.channel = 0;
|
||||
pkt.hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
||||
pkt.hop_start = pkt.hop_limit;
|
||||
pkt.rx_snr = header ? header->snr : 0;
|
||||
setReplayHopFields(pkt, header);
|
||||
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||
// Mark as if heard over the air, not internally generated
|
||||
pkt.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA;
|
||||
@@ -1231,13 +1261,13 @@ meshtastic_MeshPacket PhoneAPI::makeReplayTelemetryPacket(NodeNum num, const mes
|
||||
meshtastic_MeshPacket pkt = meshtastic_MeshPacket_init_default;
|
||||
pkt.from = num;
|
||||
pkt.to = NODENUM_BROADCAST;
|
||||
pkt.id = generatePacketId();
|
||||
// No native timestamp on telemetry packets here; use last_heard.
|
||||
const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
|
||||
pkt.rx_time = header ? header->last_heard : 0;
|
||||
pkt.id = makeReplayPacketId(num, pkt.rx_time, meshtastic_Telemetry_device_metrics_tag);
|
||||
pkt.channel = 0;
|
||||
pkt.hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
||||
pkt.hop_start = pkt.hop_limit;
|
||||
pkt.rx_snr = header ? header->snr : 0;
|
||||
setReplayHopFields(pkt, header);
|
||||
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||
// Mark as if heard over the air, not internally generated - iOS client filters
|
||||
// TRANSPORT_INTERNAL packets out of broadcast peer state updates.
|
||||
@@ -1337,12 +1367,12 @@ meshtastic_MeshPacket PhoneAPI::makeReplayEnvironmentPacket(uint32_t num, const
|
||||
meshtastic_MeshPacket pkt = meshtastic_MeshPacket_init_default;
|
||||
pkt.from = num;
|
||||
pkt.to = NODENUM_BROADCAST;
|
||||
pkt.id = generatePacketId();
|
||||
const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
|
||||
pkt.rx_time = header ? header->last_heard : 0;
|
||||
pkt.id = makeReplayPacketId(num, pkt.rx_time, meshtastic_Telemetry_environment_metrics_tag);
|
||||
pkt.channel = 0;
|
||||
pkt.hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
||||
pkt.hop_start = pkt.hop_limit;
|
||||
pkt.rx_snr = header ? header->snr : 0;
|
||||
setReplayHopFields(pkt, header);
|
||||
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||
// Mark as if heard over the air, not internally generated - iOS client filters
|
||||
// TRANSPORT_INTERNAL packets out of broadcast peer state updates.
|
||||
@@ -1400,13 +1430,13 @@ meshtastic_MeshPacket PhoneAPI::makeReplayStatusPacket(uint32_t num, const mesht
|
||||
meshtastic_MeshPacket pkt = meshtastic_MeshPacket_init_default;
|
||||
pkt.from = num;
|
||||
pkt.to = NODENUM_BROADCAST;
|
||||
pkt.id = generatePacketId();
|
||||
// StatusMessage has no native timestamp; use last_heard.
|
||||
const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
|
||||
pkt.rx_time = header ? header->last_heard : 0;
|
||||
pkt.id = makeReplayPacketId(num, pkt.rx_time, meshtastic_PortNum_NODE_STATUS_APP);
|
||||
pkt.channel = 0;
|
||||
pkt.hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
||||
pkt.hop_start = pkt.hop_limit;
|
||||
pkt.rx_snr = header ? header->snr : 0;
|
||||
setReplayHopFields(pkt, header);
|
||||
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||
// Mark as if heard over the air, not internally generated - client filters
|
||||
pkt.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA;
|
||||
@@ -1667,6 +1697,19 @@ bool PhoneAPI::wasSeenRecently(uint32_t id)
|
||||
return false;
|
||||
}
|
||||
|
||||
PhoneAPI::LocalAdminGate PhoneAPI::classifyLocalAdminPacket(const meshtastic_MeshPacket &p, bool adminAuthorized,
|
||||
meshtastic_AdminMessage &outAdmin)
|
||||
{
|
||||
if (p.which_payload_variant != meshtastic_MeshPacket_decoded_tag || p.decoded.portnum != meshtastic_PortNum_ADMIN_APP)
|
||||
return LocalAdminGate::NotAdmin;
|
||||
outAdmin = meshtastic_AdminMessage_init_zero;
|
||||
if (!pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &meshtastic_AdminMessage_msg, &outAdmin))
|
||||
return LocalAdminGate::NotAdmin; // undecodable: let the normal reject path respond
|
||||
if (outAdmin.which_payload_variant == meshtastic_AdminMessage_lockdown_auth_tag)
|
||||
return LocalAdminGate::LockdownAuth; // the passphrase itself; authenticate regardless of prior state
|
||||
return adminAuthorized ? LocalAdminGate::AuthorizedPassThrough : LocalAdminGate::DropUnauthorized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a packet that the phone wants us to send. It is our responsibility to free the packet to the pool
|
||||
*/
|
||||
@@ -1691,26 +1734,33 @@ bool PhoneAPI::handleToRadioPacket(meshtastic_MeshPacket &p)
|
||||
// the gate here closes that race and covers H6/H7 from the
|
||||
// audit: get_config_request and set_config from unauthed
|
||||
// clients no longer reach AdminModule at all.
|
||||
if (p.from == 0 && p.which_payload_variant == meshtastic_MeshPacket_decoded_tag &&
|
||||
p.decoded.portnum == meshtastic_PortNum_ADMIN_APP) {
|
||||
// Gate on the connection, not the wire `from`, which a client can forge to a non-zero value to
|
||||
// bypass the check before MeshService normalizes it back to a local identity.
|
||||
// Scope the decoded admin message: it holds the plaintext passphrase, so bound its lifetime.
|
||||
{
|
||||
meshtastic_AdminMessage admin = meshtastic_AdminMessage_init_zero;
|
||||
if (pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &meshtastic_AdminMessage_msg, &admin)) {
|
||||
if (admin.which_payload_variant == meshtastic_AdminMessage_lockdown_auth_tag) {
|
||||
handleLockdownAuthInline(admin.lockdown_auth);
|
||||
// Wipe the decoded passphrase scratch - the byte array in
|
||||
// p.decoded.payload.bytes is wiped by handleLockdownAuthInline.
|
||||
volatile uint8_t *adminVol = const_cast<volatile uint8_t *>(admin.lockdown_auth.passphrase.bytes);
|
||||
for (size_t i = 0; i < sizeof(admin.lockdown_auth.passphrase.bytes); i++)
|
||||
adminVol[i] = 0;
|
||||
return true;
|
||||
}
|
||||
if (!getAdminAuthorized()) {
|
||||
LOG_WARN("Lockdown: dropping admin payload variant=%d from unauthorized connection", admin.which_payload_variant);
|
||||
return false;
|
||||
}
|
||||
switch (classifyLocalAdminPacket(p, getAdminAuthorized(), admin)) {
|
||||
case LocalAdminGate::LockdownAuth: {
|
||||
handleLockdownAuthInline(admin.lockdown_auth);
|
||||
// The encoded wipe is the security-critical one: nothing else clears the passphrase from
|
||||
// the packet buffer. handleLockdownAuthInline already zeroes the decoded copy up to its
|
||||
// size; re-zero the full scratch capacity here as defense in depth.
|
||||
volatile uint8_t *adminVol = const_cast<volatile uint8_t *>(admin.lockdown_auth.passphrase.bytes);
|
||||
for (size_t i = 0; i < sizeof(admin.lockdown_auth.passphrase.bytes); i++)
|
||||
adminVol[i] = 0;
|
||||
volatile uint8_t *encodedVol = const_cast<volatile uint8_t *>(p.decoded.payload.bytes);
|
||||
for (size_t i = 0; i < sizeof(p.decoded.payload.bytes); i++)
|
||||
encodedVol[i] = 0;
|
||||
p.decoded.payload.size = 0; // keep the length consistent with the wiped buffer
|
||||
return true;
|
||||
}
|
||||
case LocalAdminGate::DropUnauthorized:
|
||||
LOG_WARN("Lockdown: dropping admin payload variant=%d from unauthorized connection", admin.which_payload_variant);
|
||||
return false;
|
||||
case LocalAdminGate::NotAdmin:
|
||||
case LocalAdminGate::AuthorizedPassThrough:
|
||||
break; // normal handling
|
||||
}
|
||||
// pb_decode failure: fall through to normal handling so the
|
||||
// regular Router/AdminModule reject path can respond.
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -319,4 +319,18 @@ class PhoneAPI
|
||||
|
||||
/// If the mesh service tells us fromNum has changed, tell the phone
|
||||
virtual int onNotify(uint32_t newValue) override;
|
||||
|
||||
public:
|
||||
/// How the lockdown admin gate should treat a phone->radio packet.
|
||||
enum class LocalAdminGate {
|
||||
NotAdmin, ///< Not a decodable ADMIN_APP payload; normal handling.
|
||||
LockdownAuth, ///< A lockdown_auth payload; authenticate the connection inline.
|
||||
DropUnauthorized, ///< Admin payload from a connection that has not authenticated; drop.
|
||||
AuthorizedPassThrough, ///< Admin payload from an authorized connection; normal handling.
|
||||
};
|
||||
|
||||
/// Classify a phone->radio packet for the lockdown admin gate, ignoring the wire `from` (which a
|
||||
/// client can forge) and deciding on the connection's authorization. Fills outAdmin for lockdown.
|
||||
static LocalAdminGate classifyLocalAdminPacket(const meshtastic_MeshPacket &p, bool adminAuthorized,
|
||||
meshtastic_AdminMessage &outAdmin);
|
||||
};
|
||||
|
||||
+43
-21
@@ -39,8 +39,8 @@
|
||||
#endif
|
||||
|
||||
static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_STD[] = {
|
||||
PRESET(LONG_FAST), PRESET(LONG_SLOW), PRESET(MEDIUM_SLOW), PRESET(MEDIUM_FAST), PRESET(SHORT_SLOW),
|
||||
PRESET(SHORT_FAST), PRESET(LONG_MODERATE), PRESET(SHORT_TURBO), PRESET(LONG_TURBO), MODEM_PRESET_END};
|
||||
PRESET(LONG_FAST), PRESET(LONG_SLOW), PRESET(MEDIUM_SLOW), PRESET(MEDIUM_FAST), PRESET(SHORT_SLOW), PRESET(SHORT_FAST),
|
||||
PRESET(LONG_MODERATE), PRESET(SHORT_TURBO), PRESET(LONG_TURBO), PRESET(MEDIUM_TURBO), MODEM_PRESET_END};
|
||||
|
||||
static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_EU_868[] = {
|
||||
PRESET(LONG_FAST), PRESET(LONG_SLOW), PRESET(MEDIUM_SLOW), PRESET(MEDIUM_FAST),
|
||||
@@ -55,6 +55,34 @@ static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_NARROW[] = {PRESET
|
||||
|
||||
static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_TINY[] = {PRESET(TINY_FAST), PRESET(TINY_SLOW), MODEM_PRESET_END};
|
||||
|
||||
// The EU_868/EU_866/EU_N_868 trio share the 868 band but own mutually exclusive preset
|
||||
// profiles. Selecting a preset locked to a sibling swaps the region to that sibling (see
|
||||
// regionSwapForPreset), so from any region in the trio every one of these presets is
|
||||
// selectable. This union is what we advertise to clients as the trio's legal list. It is a
|
||||
// display-only superset: on-device enforcement still uses each region's own disjoint
|
||||
// profile->presets, so this must never be assigned to a RegionProfile (that would make
|
||||
// supportsPreset() accept the preset in place and defeat the swap). Keep in sync with the
|
||||
// EU_868/EU_866/EU_N_868 profile lists below. Sized to the 11-preset wire cap.
|
||||
static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_EU_SUPERSET[] = {
|
||||
PRESET(LONG_FAST), PRESET(LONG_SLOW), PRESET(MEDIUM_SLOW), PRESET(MEDIUM_FAST), PRESET(SHORT_SLOW), PRESET(SHORT_FAST),
|
||||
PRESET(LONG_MODERATE), PRESET(LITE_FAST), PRESET(LITE_SLOW), PRESET(NARROW_FAST), PRESET(NARROW_SLOW), MODEM_PRESET_END};
|
||||
|
||||
// The EU_868/EU_866/EU_N_868 trio own mutually exclusive preset lists. Selecting a preset
|
||||
// locked to a sibling means the user wants that sibling region, not the default preset.
|
||||
static const meshtastic_Config_LoRaConfig_RegionCode SWAPPABLE_EU_REGIONS[] = {
|
||||
meshtastic_Config_LoRaConfig_RegionCode_EU_868,
|
||||
meshtastic_Config_LoRaConfig_RegionCode_EU_866,
|
||||
meshtastic_Config_LoRaConfig_RegionCode_EU_N_868,
|
||||
};
|
||||
|
||||
static bool isSwappableEuRegion(meshtastic_Config_LoRaConfig_RegionCode code)
|
||||
{
|
||||
for (auto c : SWAPPABLE_EU_REGIONS)
|
||||
if (c == code)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Region profiles: bundle preset list + regulatory parameters shared across regions
|
||||
// presets, spacing, padding, audio, licensed, text throttle, position throttle, telemetry throttle
|
||||
const RegionProfile PROFILE_STD = {PRESETS_STD, 0, 0, true, false, 0, 1, 1};
|
||||
@@ -184,11 +212,9 @@ const RegionInfo regions[] = {
|
||||
|
||||
/*
|
||||
433,05-434,7 Mhz 10 mW
|
||||
868,0-868,6 Mhz 25 mW
|
||||
https://nkrzi.gov.ua/images/upload/256/5810/PDF_UUZ_19_01_2016.pdf
|
||||
https://zakon.rada.gov.ua/laws/show/262-2026-п
|
||||
*/
|
||||
RDEF(UA_433, 433.0f, 434.7f, 10, 10, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
|
||||
RDEF(UA_868, 868.0f, 868.6f, 1, 14, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
|
||||
|
||||
/*
|
||||
Malaysia
|
||||
@@ -337,8 +363,12 @@ LoRaRadioType radioType = NO_RADIO;
|
||||
|
||||
extern RadioLibHal *RadioLibHAL;
|
||||
#if defined(HW_SPI1_DEVICE) && defined(ARCH_ESP32)
|
||||
#if defined(HAS_SDCARD) && defined(SDCARD_USE_SPI1)
|
||||
extern SPIClass &SPI1; // alias for SPI_HSPI; both on SPI2_HOST
|
||||
#else
|
||||
extern SPIClass SPI1;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
std::unique_ptr<RadioInterface> initLoRa()
|
||||
{
|
||||
@@ -685,8 +715,13 @@ void getRegionPresetMap(meshtastic_LoRaRegionPresetMap &map)
|
||||
grp.default_preset = r->getDefaultPreset();
|
||||
grp.licensed_only = r->profile->licensedOnly;
|
||||
grp.presets_count = 0;
|
||||
for (size_t i = 0; r->profile->presets[i] != MODEM_PRESET_END && grp.presets_count < maxPresets; i++)
|
||||
grp.presets[grp.presets_count++] = r->profile->presets[i];
|
||||
// EU 86x siblings advertise the trio's superset - any of those presets is
|
||||
// reachable from here via an automatic region swap. Every other region
|
||||
// advertises exactly its own enforced profile list.
|
||||
const meshtastic_Config_LoRaConfig_ModemPreset *advertised =
|
||||
isSwappableEuRegion(r->code) ? PRESETS_EU_SUPERSET : r->profile->presets;
|
||||
for (size_t i = 0; advertised[i] != MODEM_PRESET_END && grp.presets_count < maxPresets; i++)
|
||||
grp.presets[grp.presets_count++] = advertised[i];
|
||||
}
|
||||
|
||||
// Map this region to its group (capacity checked at the top of the loop).
|
||||
@@ -961,14 +996,6 @@ static void sendErrorNotification(const char *msg, meshtastic_LogRecord_Level le
|
||||
service->sendClientNotification(cn);
|
||||
}
|
||||
|
||||
// The EU_868/EU_866/EU_N_868 trio own mutually exclusive preset lists. Selecting a preset
|
||||
// locked to a sibling means the user wants that sibling region, not the default preset.
|
||||
static const meshtastic_Config_LoRaConfig_RegionCode SWAPPABLE_EU_REGIONS[] = {
|
||||
meshtastic_Config_LoRaConfig_RegionCode_EU_868,
|
||||
meshtastic_Config_LoRaConfig_RegionCode_EU_866,
|
||||
meshtastic_Config_LoRaConfig_RegionCode_EU_N_868,
|
||||
};
|
||||
|
||||
/**
|
||||
* If currentRegion is one of the swappable EU regions and preset belongs to a sibling in
|
||||
* that trio, return the sibling region that owns the preset. Returns nullptr otherwise.
|
||||
@@ -976,12 +1003,7 @@ static const meshtastic_Config_LoRaConfig_RegionCode SWAPPABLE_EU_REGIONS[] = {
|
||||
const RegionInfo *RadioInterface::regionSwapForPreset(meshtastic_Config_LoRaConfig_RegionCode currentRegion,
|
||||
meshtastic_Config_LoRaConfig_ModemPreset preset)
|
||||
{
|
||||
bool currentIsSwappable = false;
|
||||
for (auto code : SWAPPABLE_EU_REGIONS) {
|
||||
if (code == currentRegion)
|
||||
currentIsSwappable = true;
|
||||
}
|
||||
if (!currentIsSwappable)
|
||||
if (!isSwappableEuRegion(currentRegion))
|
||||
return nullptr;
|
||||
|
||||
for (auto code : SWAPPABLE_EU_REGIONS) {
|
||||
|
||||
+74
-25
@@ -5,7 +5,7 @@
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PositionPrecision.h"
|
||||
#include "RTC.h"
|
||||
#include "gps/RTC.h"
|
||||
|
||||
#include "configuration.h"
|
||||
#include "main.h"
|
||||
@@ -711,37 +711,62 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
||||
bool matchedChannel = false;
|
||||
ChannelIndex chIndex = 0;
|
||||
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||
// Attempt PKI decryption first. The sender's key may come from the hot
|
||||
// store or the warm tier (nodes evicted from the hot store keep their key
|
||||
// there), so DMs from long-tail nodes still decrypt.
|
||||
meshtastic_NodeInfoLite_public_key_t fromKey = {0, {0}};
|
||||
if (p->channel == 0 && isToUs(p) && p->to > 0 && !isBroadcast(p->to) && nodeDB->copyPublicKey(p->from, fromKey) &&
|
||||
nodeDB->getMeshNode(p->to) != nullptr && nodeDB->getMeshNode(p->to)->public_key.size > 0 &&
|
||||
rawSize > MESHTASTIC_PKC_OVERHEAD) {
|
||||
// Resolve the sender's public key: prefer the one stored in NodeDB (hot store or warm tier), else
|
||||
// fall back to a not-yet-committed key held during an in-progress key-verification handshake.
|
||||
meshtastic_NodeInfoLite_public_key_t remotePublic = {0, {0}};
|
||||
bool haveRemoteKey = nodeDB->copyPublicKey(p->from, remotePublic) || crypto->getPendingPublicKey(p->from, remotePublic);
|
||||
|
||||
meshtastic_NodeInfoLite *ourNode = nullptr;
|
||||
if (p->channel == 0 && isToUs(p) && p->to > 0 && !isBroadcast(p->to) && rawSize > MESHTASTIC_PKC_OVERHEAD &&
|
||||
(ourNode = nodeDB->getMeshNode(p->to)) != nullptr && ourNode->public_key.size > 0) {
|
||||
pkiAttempted = true;
|
||||
LOG_DEBUG("Attempt PKI decryption");
|
||||
// Try the sender's known key first, then each configured admin key so an authorized admin can
|
||||
// reach a node that has not yet learned their key. AES-CCM AEAD rejects wrong candidates.
|
||||
bool viaAdminKey = false;
|
||||
if (haveRemoteKey && crypto->decryptCurve25519(p->from, remotePublic, p->id, rawSize, p->encrypted.bytes, bytes)) {
|
||||
decrypted = true;
|
||||
}
|
||||
for (int i = 0; i < 3 && !decrypted; i++) {
|
||||
if (config.security.admin_key[i].size != 32)
|
||||
continue;
|
||||
remotePublic.size = 32;
|
||||
memcpy(remotePublic.bytes, config.security.admin_key[i].bytes, 32);
|
||||
|
||||
if (crypto->decryptCurve25519(p->from, fromKey, p->id, rawSize, p->encrypted.bytes, bytes)) {
|
||||
if (crypto->decryptCurve25519(p->from, remotePublic, p->id, rawSize, p->encrypted.bytes, bytes)) {
|
||||
decrypted = true;
|
||||
viaAdminKey = true;
|
||||
break; // stop after first successful decryption
|
||||
}
|
||||
}
|
||||
if (decrypted) {
|
||||
LOG_INFO("PKI Decryption worked!");
|
||||
|
||||
meshtastic_Data decodedtmp;
|
||||
memset(&decodedtmp, 0, sizeof(decodedtmp));
|
||||
rawSize -= MESHTASTIC_PKC_OVERHEAD;
|
||||
if (pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &decodedtmp) &&
|
||||
size_t payloadSize = rawSize - MESHTASTIC_PKC_OVERHEAD;
|
||||
if (pb_decode_from_bytes(bytes, payloadSize, &meshtastic_Data_msg, &decodedtmp) &&
|
||||
decodedtmp.portnum != meshtastic_PortNum_UNKNOWN_APP) {
|
||||
decrypted = true;
|
||||
rawSize = payloadSize; // commit the overhead subtraction only on full success
|
||||
LOG_INFO("Packet decrypted using PKI!");
|
||||
p->pki_encrypted = true;
|
||||
memcpy(p->public_key.bytes, fromKey.bytes, 32);
|
||||
memcpy(p->public_key.bytes, remotePublic.bytes, 32);
|
||||
p->public_key.size = 32;
|
||||
p->decoded = decodedtmp;
|
||||
p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // change type to decoded
|
||||
if (viaAdminKey) {
|
||||
// Persist the admin key for the sender so future packets take the fast path and we can
|
||||
// PKI-reply; p->from is bound into the AEAD nonce, so the trusted admin authenticated it.
|
||||
meshtastic_NodeInfoLite *fromNode = nodeDB->getOrCreateMeshNode(p->from);
|
||||
if (fromNode != nullptr)
|
||||
fromNode->public_key = remotePublic;
|
||||
}
|
||||
} else {
|
||||
// AEAD already authenticated this ciphertext, so no other candidate could decode it -
|
||||
// the payload is simply malformed.
|
||||
LOG_ERROR("PKC Decrypted, but pb_decode failed!");
|
||||
return DecodeState::DECODE_FAILURE;
|
||||
}
|
||||
} else {
|
||||
LOG_WARN("PKC decrypt attempted but failed!");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -786,17 +811,16 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
||||
if (decrypted) {
|
||||
// parsing was successful
|
||||
p->channel = chIndex; // change to store the index instead of the hash
|
||||
if (p->decoded.has_bitfield)
|
||||
p->decoded.want_response |= p->decoded.bitfield & BITFIELD_WANT_RESPONSE_MASK;
|
||||
|
||||
#if !(MESHTASTIC_EXCLUDE_PKI) && !(MESHTASTIC_EXCLUDE_XEDDSA)
|
||||
// rawSize is the size of the encoded Data exactly as the sender built it (the PKI branch's
|
||||
// MESHTASTIC_PKC_OVERHEAD subtraction preserves that, and PKI packets are unicast so the
|
||||
// downgrade predicate ignores them anyway).
|
||||
// Use the encoded Data size before merging local-only bitfield state.
|
||||
if (!checkXeddsaReceivePolicy(p, rawSize))
|
||||
return DecodeState::DECODE_POLICY_REJECT;
|
||||
#endif
|
||||
|
||||
if (p->decoded.has_bitfield)
|
||||
p->decoded.want_response |= p->decoded.bitfield & BITFIELD_WANT_RESPONSE_MASK;
|
||||
|
||||
/* Not actually ever used.
|
||||
// Decompress if needed. jm
|
||||
if (p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_COMPRESSED_APP) {
|
||||
@@ -918,7 +942,7 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
|
||||
|
||||
LOG_DEBUG("Original length - %d ", p->decoded.payload.size);
|
||||
LOG_DEBUG("Compressed length - %d ", compressed_len);
|
||||
LOG_DEBUG("Original message - %s ", p->decoded.payload.bytes);
|
||||
LOG_DEBUG("Original message - %.*s ", (int)p->decoded.payload.size, p->decoded.payload.bytes);
|
||||
|
||||
// If the compressed length is greater than or equal to the original size, don't use the compressed form
|
||||
if (compressed_len >= p->decoded.payload.size) {
|
||||
@@ -947,10 +971,17 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
|
||||
ChannelIndex chIndex = p->channel; // keep as a local because we are about to change it
|
||||
|
||||
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||
// Destination key from the hot store or the warm tier (evicted
|
||||
// long-tail nodes keep their key there)
|
||||
// Resolve the destination's public key: prefer NodeDB (hot store or warm tier - evicted
|
||||
// long-tail nodes keep their key there), otherwise (for a key-verification follow-on packet
|
||||
// that explicitly requested PKI) fall back to the not-yet-verified key held during an
|
||||
// in-progress handshake. This lets us DH-encode the follow-on packet before the peer's key
|
||||
// has been committed to NodeDB.
|
||||
meshtastic_NodeInfoLite_public_key_t destKey = {0, {0}};
|
||||
bool haveDestKey = nodeDB->copyPublicKey(p->to, destKey);
|
||||
if (!haveDestKey && p->pki_encrypted && p->decoded.portnum == meshtastic_PortNum_KEY_VERIFICATION_APP &&
|
||||
crypto->getPendingPublicKey(p->to, destKey)) {
|
||||
haveDestKey = true;
|
||||
}
|
||||
// We may want to retool things so we can send a PKC packet when the client specifies a key and nodenum, even if the node
|
||||
// is not in the local nodedb
|
||||
// First, only PKC encrypt packets we are originating
|
||||
@@ -968,11 +999,17 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
|
||||
config.security.private_key.size == 32 && !isBroadcast(p->to) &&
|
||||
// Some portnums either make no sense to send with PKC
|
||||
p->decoded.portnum != meshtastic_PortNum_TRACEROUTE_APP && p->decoded.portnum != meshtastic_PortNum_NODEINFO_APP &&
|
||||
p->decoded.portnum != meshtastic_PortNum_ROUTING_APP && p->decoded.portnum != meshtastic_PortNum_POSITION_APP) {
|
||||
p->decoded.portnum != meshtastic_PortNum_ROUTING_APP && p->decoded.portnum != meshtastic_PortNum_POSITION_APP &&
|
||||
// We allow Key Verification messages to be sent without a known destination key, since the point of those messages is
|
||||
// to exchange keys. The first exchange (no usable key yet) falls through to channel encryption; the follow-on packet
|
||||
// uses the pending key resolved into haveDestKey/destKey above.
|
||||
// Though possible the first packet each direction should go non-pkc
|
||||
// to handle the case where the remote node has our key, but we don't have theirs.
|
||||
!(p->decoded.portnum == meshtastic_PortNum_KEY_VERIFICATION_APP && !haveDestKey)) {
|
||||
LOG_DEBUG("Use PKI!");
|
||||
if (numbytes + MESHTASTIC_HEADER_LENGTH + MESHTASTIC_PKC_OVERHEAD > MAX_LORA_PAYLOAD_LEN)
|
||||
return meshtastic_Routing_Error_TOO_LARGE;
|
||||
// Check for a known public key for the destination
|
||||
// Check for a usable public key for the destination (NodeDB or a pending key-verification key)
|
||||
if (!haveDestKey) {
|
||||
LOG_WARN("Unknown public key for destination node 0x%08x (portnum %d), refusing to send legacy DM", p->to,
|
||||
p->decoded.portnum);
|
||||
@@ -1085,6 +1122,18 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
|
||||
else
|
||||
printPacket("handleReceived(REMOTE)", p);
|
||||
|
||||
#if MESHTASTIC_PREHOP_DROP
|
||||
// Pre-hop firmware drop, post-decode half: the bitfield that proves the origin populated hop_start is
|
||||
// encrypted under the channel key, so it can only be evaluated now that the packet is decoded. A packet
|
||||
// whose hop_start is still missing/unknown comes from pre-hop firmware - keep it out of module
|
||||
// processing, admin handling, phone delivery, MQTT and rebroadcast. Local-origin packets are exempt.
|
||||
if (!isFromUs(p) && classifyHopStart(*p) != HopStartStatus::VALID) {
|
||||
logHopStartDrop(*p, "post-decode pre-hop drop");
|
||||
cancelSending(p->from, p->id);
|
||||
skipHandle = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Neighbor info module is disabled, ignore expensive neighbor info packets
|
||||
if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag &&
|
||||
p->decoded.portnum == meshtastic_PortNum_NEIGHBORINFO_APP &&
|
||||
|
||||
@@ -173,30 +173,6 @@ template <typename T> bool SX126xInterface<T>::init()
|
||||
LOG_WARN("Failed to apply SX1262 register 0x8B5 patch for RX improvement");
|
||||
}
|
||||
|
||||
#if 0
|
||||
// Read/write a register we are not using (only used for FSK mode) to test SPI comms
|
||||
uint8_t crcLSB = 0;
|
||||
int err = lora.readRegister(SX126X_REG_CRC_POLYNOMIAL_LSB, &crcLSB, 1);
|
||||
if(err != RADIOLIB_ERR_NONE)
|
||||
RECORD_CRITICALERROR(CriticalErrorCode_SX1262Failure);
|
||||
|
||||
//if(crcLSB != 0x0f)
|
||||
// RECORD_CRITICALERROR(CriticalErrorCode_SX1262Failure);
|
||||
|
||||
crcLSB = 0x5a;
|
||||
err = lora.writeRegister(SX126X_REG_CRC_POLYNOMIAL_LSB, &crcLSB, 1);
|
||||
if(err != RADIOLIB_ERR_NONE)
|
||||
RECORD_CRITICALERROR(CriticalErrorCode_SX1262Failure);
|
||||
|
||||
err = lora.readRegister(SX126X_REG_CRC_POLYNOMIAL_LSB, &crcLSB, 1);
|
||||
if(err != RADIOLIB_ERR_NONE)
|
||||
RECORD_CRITICALERROR(CriticalErrorCode_SX1262Failure);
|
||||
|
||||
if(crcLSB != 0x5a)
|
||||
RECORD_CRITICALERROR(CriticalErrorCode_SX1262Failure);
|
||||
// If we got this far register accesses (and therefore SPI comms) are good
|
||||
#endif
|
||||
|
||||
if (res == RADIOLIB_ERR_NONE)
|
||||
res = lora.setCRC(RADIOLIB_SX126X_LORA_CRC_ON);
|
||||
|
||||
|
||||
+32
-10
@@ -1,14 +1,15 @@
|
||||
#include "StreamAPI.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "RTC.h"
|
||||
#include "Throttle.h"
|
||||
#include "concurrency/LockGuard.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/RTC.h"
|
||||
|
||||
#define START1 0x94
|
||||
#define START2 0xc3
|
||||
#define HEADER_LEN 4
|
||||
|
||||
/// Poll the underlying stream, drain output, and update connection state.
|
||||
int32_t StreamAPI::runOncePart()
|
||||
{
|
||||
auto result = readStream();
|
||||
@@ -17,6 +18,7 @@ int32_t StreamAPI::runOncePart()
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Consume supplied input bytes, drain output, and update connection state.
|
||||
int32_t StreamAPI::runOncePart(char *buf, uint16_t bufLen)
|
||||
{
|
||||
auto result = readStream(buf, bufLen);
|
||||
@@ -48,6 +50,11 @@ int32_t StreamAPI::readStream(const char *buf, uint16_t bufLen)
|
||||
void StreamAPI::writeStream()
|
||||
{
|
||||
if (canWrite) {
|
||||
// A transport that retained a short frame must complete it before
|
||||
// getFromRadio() advances the PhoneAPI state to the next packet.
|
||||
if (!finishPendingFrame())
|
||||
return;
|
||||
|
||||
uint32_t len;
|
||||
do {
|
||||
// Send every packet we can
|
||||
@@ -58,6 +65,7 @@ void StreamAPI::writeStream()
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse supplied bytes through the framed ToRadio receive state machine.
|
||||
int32_t StreamAPI::handleRecStream(const char *buf, uint16_t bufLen)
|
||||
{
|
||||
uint16_t index = 0;
|
||||
@@ -167,20 +175,27 @@ int32_t StreamAPI::readStream()
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode the stream marker and big-endian payload length.
|
||||
size_t StreamAPI::buildFrameHeader(uint8_t *buf, size_t payloadLen)
|
||||
{
|
||||
buf[0] = START1;
|
||||
buf[1] = START2;
|
||||
buf[2] = (payloadLen >> 8) & 0xff;
|
||||
buf[3] = payloadLen & 0xff;
|
||||
return payloadLen + HEADER_LEN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the current txBuffer over our stream
|
||||
*/
|
||||
bool StreamAPI::writeFrame(uint8_t *buf, size_t len)
|
||||
/// Write one framed payload using the transport's failure semantics.
|
||||
bool StreamAPI::writeFrame(uint8_t *buf, size_t len, bool bestEffort)
|
||||
{
|
||||
(void)bestEffort;
|
||||
if (len == 0 || !canWrite)
|
||||
return false;
|
||||
|
||||
buf[0] = START1;
|
||||
buf[1] = START2;
|
||||
buf[2] = (len >> 8) & 0xff;
|
||||
buf[3] = len & 0xff;
|
||||
|
||||
auto totalLen = len + HEADER_LEN;
|
||||
const size_t totalLen = buildFrameHeader(buf, len);
|
||||
// Serialize write-readiness checks, writes and write-failure handling
|
||||
// against concurrent stream writes/close.
|
||||
concurrency::LockGuard guard(&streamLock);
|
||||
@@ -197,11 +212,13 @@ bool StreamAPI::writeFrame(uint8_t *buf, size_t len)
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Emit the prepared main PhoneAPI payload as required output.
|
||||
bool StreamAPI::emitTxBuffer(size_t len)
|
||||
{
|
||||
return writeFrame(txBuf, len);
|
||||
return writeFrame(txBuf, len, false);
|
||||
}
|
||||
|
||||
/// Emit the initial reboot notification as a framed FromRadio payload.
|
||||
void StreamAPI::emitRebooted()
|
||||
{
|
||||
// In case we send a FromRadio packet
|
||||
@@ -213,8 +230,13 @@ void StreamAPI::emitRebooted()
|
||||
emitTxBuffer(pb_encode_to_bytes(txBuf + HEADER_LEN, meshtastic_FromRadio_size, &meshtastic_FromRadio_msg, &fromRadioScratch));
|
||||
}
|
||||
|
||||
/// Encode and emit one protobuf LogRecord using the dedicated log buffers.
|
||||
void StreamAPI::emitLogRecord(meshtastic_LogRecord_Level level, const char *src, const char *format, va_list arg)
|
||||
{
|
||||
// A retained short log frame still points into txBufLog, so do not overwrite it.
|
||||
if (!canEncodeLogRecord())
|
||||
return;
|
||||
|
||||
// IMPORTANT: do NOT touch `fromRadioScratch` or `txBuf` here - those
|
||||
// belong to the main packet-emission path and a LOG_ firing during
|
||||
// `writeStream()` would corrupt an in-flight encode. We keep a
|
||||
@@ -237,7 +259,7 @@ void StreamAPI::emitLogRecord(meshtastic_LogRecord_Level level, const char *src,
|
||||
|
||||
size_t len =
|
||||
pb_encode_to_bytes(txBufLog + HEADER_LEN, meshtastic_FromRadio_size, &meshtastic_FromRadio_msg, &fromRadioScratchLog);
|
||||
writeFrame(txBufLog, len);
|
||||
writeFrame(txBufLog, len, true);
|
||||
}
|
||||
|
||||
/// Hookable to find out when connection changes
|
||||
|
||||
+14
-3
@@ -91,12 +91,24 @@ class StreamAPI : public PhoneAPI
|
||||
/// Low level function to emit a protobuf encapsulated log record
|
||||
void emitLogRecord(meshtastic_LogRecord_Level level, const char *src, const char *format, va_list arg);
|
||||
|
||||
/// Return whether the transport can accept a frame of the requested size.
|
||||
virtual bool canWriteFrame(size_t frameLen) { return true; }
|
||||
/// Let transports recover from or close after an incomplete write.
|
||||
virtual void onFrameWriteFailed(size_t frameLen, size_t writtenLen) {}
|
||||
|
||||
private:
|
||||
bool writeFrame(uint8_t *buf, size_t len);
|
||||
/// Fill in the 4-byte 0x94C3 length header; returns the total frame length.
|
||||
static size_t buildFrameHeader(uint8_t *buf, size_t payloadLen);
|
||||
|
||||
/// Complete retained transport output before dequeuing another PhoneAPI packet.
|
||||
virtual bool finishPendingFrame() { return true; }
|
||||
/// Return whether the dedicated log buffer is available for encoding.
|
||||
virtual bool canEncodeLogRecord() { return true; }
|
||||
/// Frame and write a payload, optionally using best-effort admission.
|
||||
virtual bool writeFrame(uint8_t *buf, size_t len, bool bestEffort);
|
||||
|
||||
concurrency::Lock streamLock;
|
||||
|
||||
private:
|
||||
/// Dedicated scratch + tx buffer for LogRecord emission.
|
||||
///
|
||||
/// The main packet emission path (`writeStream` -> `getFromRadio` ->
|
||||
@@ -117,5 +129,4 @@ class StreamAPI : public PhoneAPI
|
||||
/// interleave on the wire.
|
||||
meshtastic_FromRadio fromRadioScratchLog = {};
|
||||
uint8_t txBufLog[MAX_STREAM_BUF_SIZE] = {0};
|
||||
concurrency::Lock streamLock;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
#include "StreamFrameWriter.h"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
/// Start a frame or retain required output behind an incomplete frame.
|
||||
bool StreamFrameWriter::writeFrame(Stream &stream, uint8_t *frame, size_t frameLen, bool bestEffort)
|
||||
{
|
||||
if (!finishPendingFrame(stream)) {
|
||||
// Single required-frame producer invariant: writeStream() is gated on
|
||||
// finishPendingFrame(), so a second required frame can never arrive here.
|
||||
assert(bestEffort || !deferredFrame);
|
||||
|
||||
// Preserve required output that was encoded while another frame tail
|
||||
// was pending. Best-effort output is dropped instead.
|
||||
if (!bestEffort && !deferredFrame) {
|
||||
deferredFrame = frame;
|
||||
deferredFrameLen = frameLen;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Never start best-effort output unless its complete frame fits.
|
||||
if (bestEffort && stream.availableForWrite() < (int)frameLen)
|
||||
return false;
|
||||
|
||||
size_t written = stream.write(frame, frameLen);
|
||||
if (written == frameLen)
|
||||
return true;
|
||||
|
||||
pendingFrame = frame;
|
||||
pendingFrameLen = frameLen;
|
||||
pendingFrameOffset = written;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Continue the pending tail with at most one Stream::write() call.
|
||||
bool StreamFrameWriter::finishPendingFrame(Stream &stream)
|
||||
{
|
||||
if (!pendingFrame)
|
||||
return true;
|
||||
|
||||
size_t remaining = pendingFrameLen - pendingFrameOffset;
|
||||
size_t written = stream.write(pendingFrame + pendingFrameOffset, remaining);
|
||||
if (written > remaining)
|
||||
written = remaining;
|
||||
pendingFrameOffset += written;
|
||||
|
||||
if (pendingFrameOffset < pendingFrameLen)
|
||||
return false;
|
||||
|
||||
pendingFrame = nullptr;
|
||||
pendingFrameLen = 0;
|
||||
pendingFrameOffset = 0;
|
||||
|
||||
// Promote required output without starting another write in this call.
|
||||
if (deferredFrame) {
|
||||
pendingFrame = deferredFrame;
|
||||
pendingFrameLen = deferredFrameLen;
|
||||
deferredFrame = nullptr;
|
||||
deferredFrameLen = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include "Stream.h"
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
/** Caller-owned frame storage must remain valid and unchanged until isIdle(). */
|
||||
class StreamFrameWriter
|
||||
{
|
||||
public:
|
||||
/// Start a complete frame, or retain a required frame behind pending output.
|
||||
bool writeFrame(Stream &stream, uint8_t *frame, size_t frameLen, bool bestEffort);
|
||||
|
||||
/// Make one bounded attempt to finish pending output.
|
||||
bool finishPendingFrame(Stream &stream);
|
||||
|
||||
/// Return true when no frame buffer is retained.
|
||||
bool isIdle() const { return pendingFrame == nullptr && deferredFrame == nullptr; }
|
||||
|
||||
private:
|
||||
uint8_t *pendingFrame = nullptr;
|
||||
size_t pendingFrameLen = 0;
|
||||
size_t pendingFrameOffset = 0;
|
||||
uint8_t *deferredFrame = nullptr;
|
||||
size_t deferredFrameLen = 0;
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "TransmitHistory.h"
|
||||
#include "FSCommon.h"
|
||||
#include "RTC.h"
|
||||
#include "SPILock.h"
|
||||
#include "gps/RTC.h"
|
||||
#include <Throttle.h>
|
||||
|
||||
#ifdef FSCom
|
||||
|
||||
+57
-39
@@ -13,12 +13,11 @@
|
||||
|
||||
#if defined(NRF52840_XXAA)
|
||||
#include "flash/flash_nrf5x.h"
|
||||
#define WARM_RING_MAGIC 0x324E5257u // "WRN2" - v2: last_heard low bits carry role + protected category
|
||||
#define WARM_RING_MAGIC 0x334E5257u // "WRN3" - v3: last_heard low bits carry role + protected + signer
|
||||
#define WARM_RING_MAGIC_V2 0x324E5257u // "WRN2" - v2: role + protected only; bit 6 was still timestamp.
|
||||
#define WARM_RING_MAGIC_V1 0x474E5257u // "WRNG" - v1: last_heard was a plain timestamp.
|
||||
// v1 pages are still read on upgrade: we keep each record's identity + public key but
|
||||
// DISCARD its last_heard (the old timestamp would be misread as role/protected bits).
|
||||
// Records re-rank and re-learn their role on the next contact. Legacy pages convert to
|
||||
// v2 naturally as the ring rotates.
|
||||
// Older pages are still read on upgrade: v1 keeps identity + key but discards last_heard,
|
||||
// v2 keeps the word but clears bit 6, which was still part of the timestamp then.
|
||||
// A tombstone is an entry record whose last_heard is all-ones - getTime()
|
||||
// (unix seconds) cannot reach 0xFFFFFFFF until 2106, and erased flash is
|
||||
// detected via num == 0xFFFFFFFF before last_heard is ever inspected.
|
||||
@@ -34,10 +33,13 @@ struct WarmStoreHeader {
|
||||
};
|
||||
static_assert(sizeof(WarmStoreHeader) == 16, "header layout is part of the persistence format");
|
||||
|
||||
#define WARM_STORE_MAGIC 0x324D5257u // "WRM2" - v2: last_heard low bits carry role + protected category
|
||||
#define WARM_STORE_MAGIC 0x334D5257u // "WRM3" - v3: last_heard low bits carry role + protected + signer
|
||||
#define WARM_STORE_MAGIC_V2 \
|
||||
0x324D5257u // "WRM2" - v2: role + protected only; bit 6 was still timestamp. On upgrade
|
||||
// we clear the signer bit, then rewrite as v3.
|
||||
#define WARM_STORE_MAGIC_V1 \
|
||||
0x314D5257u // "WRM1" - v1: last_heard was a plain timestamp. On upgrade we keep
|
||||
// identity + key but discard last_heard, then rewrite as v2.
|
||||
// identity + key but discard last_heard, then rewrite as v3.
|
||||
|
||||
#ifdef FSCom
|
||||
static const char *warmFileName = "/prefs/warm.dat";
|
||||
@@ -134,18 +136,18 @@ WarmNodeEntry *WarmNodeStore::place(NodeNum num, uint32_t lastHeard, const uint8
|
||||
return slot;
|
||||
}
|
||||
|
||||
bool WarmNodeStore::absorb(NodeNum num, uint32_t lastHeard, const uint8_t *key32, uint8_t role, uint8_t protectedCat)
|
||||
bool WarmNodeStore::absorb(NodeNum num, uint32_t lastHeard, const uint8_t *key32, uint8_t role, uint8_t protectedCat, bool signer)
|
||||
{
|
||||
// Pack role + protected category into the low bits of last_heard. place() and ring
|
||||
// replay store the raw word verbatim, so the metadata round-trips through flash.
|
||||
const uint32_t packed = warmPackLastHeard(lastHeard, role, protectedCat);
|
||||
// Pack role + protected category + signer into the low bits of last_heard. place() and
|
||||
// ring replay store the raw word verbatim, so the metadata round-trips through flash.
|
||||
const uint32_t packed = warmPackLastHeard(lastHeard, role, protectedCat, signer);
|
||||
const WarmNodeEntry *slot = place(num, packed, key32);
|
||||
if (!slot)
|
||||
return false;
|
||||
persistEntry(*slot);
|
||||
LOG_MIGRATION("WarmStore absorb 0x%08x key=%d last_heard=%u role=%u prot=%u (now %u/%u)", (unsigned)num,
|
||||
LOG_MIGRATION("WarmStore absorb 0x%08x key=%d last_heard=%u role=%u prot=%u signer=%u (now %u/%u)", (unsigned)num,
|
||||
keyIsSet(slot->public_key) ? 1 : 0, (unsigned)warmTimeOf(*slot), (unsigned)role, (unsigned)protectedCat,
|
||||
(unsigned)count(), (unsigned)capacity());
|
||||
signer ? 1u : 0u, (unsigned)count(), (unsigned)capacity());
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -258,19 +260,24 @@ bool WarmNodeStore::saveIfDirty()
|
||||
// (stranded live entries re-appended, then erased). Flash access holds spiLock -
|
||||
// the page cache is shared with InternalFS/LittleFS.
|
||||
|
||||
bool WarmNodeStore::ringReadHeader(uint8_t page, WarmPageHeader &h, bool *legacy) const
|
||||
bool WarmNodeStore::ringReadHeader(uint8_t page, WarmPageHeader &h, WarmFormat *fmt) const
|
||||
{
|
||||
flash_nrf5x_read(&h, WARM_FLASH_PAGE_ADDR(page), sizeof(h));
|
||||
if (h.seq == 0xFFFFFFFFu)
|
||||
return false; // erased page
|
||||
if (h.magic == WARM_RING_MAGIC) {
|
||||
if (legacy)
|
||||
*legacy = false;
|
||||
if (fmt)
|
||||
*fmt = WarmFormat::Current;
|
||||
return true;
|
||||
}
|
||||
if (h.magic == WARM_RING_MAGIC_V2) {
|
||||
if (fmt)
|
||||
*fmt = WarmFormat::V2; // replay it, but clear the signer bit (see WARM_RING_MAGIC_V2)
|
||||
return true;
|
||||
}
|
||||
if (h.magic == WARM_RING_MAGIC_V1) {
|
||||
if (legacy)
|
||||
*legacy = true; // v1 page: replay it, but discard last_heard (see WARM_RING_MAGIC_V1)
|
||||
if (fmt)
|
||||
*fmt = WarmFormat::V1; // replay it, but discard last_heard (see WARM_RING_MAGIC_V1)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -386,13 +393,13 @@ void WarmNodeStore::load()
|
||||
// Order valid pages by ascending seq so replay applies oldest first
|
||||
uint8_t order[WARM_FLASH_PAGES] = {};
|
||||
uint32_t seqs[WARM_FLASH_PAGES] = {};
|
||||
bool legacyOf[WARM_FLASH_PAGES] = {}; // per-page: v1 (WRNG) → discard last_heard on replay
|
||||
WarmFormat fmtOf[WARM_FLASH_PAGES] = {}; // per-page on-flash format; older ones are normalised on replay
|
||||
uint8_t nValid = 0;
|
||||
uint8_t nCorrupt = 0;
|
||||
for (uint8_t p = 0; p < WARM_FLASH_PAGES; p++) {
|
||||
WarmPageHeader h;
|
||||
bool legacy = false;
|
||||
if (!ringReadHeader(p, h, &legacy)) {
|
||||
WarmFormat fmt = WarmFormat::Current;
|
||||
if (!ringReadHeader(p, h, &fmt)) {
|
||||
// An erased page reads back all-ones; any other magic is a
|
||||
// partially-written or bit-rotted header we're dropping, so flag it
|
||||
// rather than silently treating the loss as a clean empty ring.
|
||||
@@ -400,7 +407,7 @@ void WarmNodeStore::load()
|
||||
nCorrupt++;
|
||||
continue;
|
||||
}
|
||||
legacyOf[p] = legacy;
|
||||
fmtOf[p] = fmt;
|
||||
uint8_t pos = nValid;
|
||||
while (pos > 0 && static_cast<int32_t>(h.seq - seqs[pos - 1]) < 0) {
|
||||
order[pos] = order[pos - 1];
|
||||
@@ -427,7 +434,7 @@ void WarmNodeStore::load()
|
||||
uint32_t migrated = 0;
|
||||
for (uint8_t k = 0; k < nValid; k++) {
|
||||
const uint8_t p = order[k];
|
||||
const bool legacy = legacyOf[p];
|
||||
const WarmFormat fmt = fmtOf[p];
|
||||
uint16_t slot = 0;
|
||||
for (; slot < kRecordsPerPage; slot++) {
|
||||
WarmNodeEntry rec;
|
||||
@@ -444,12 +451,15 @@ void WarmNodeStore::load()
|
||||
memset(e, 0, sizeof(*e));
|
||||
}
|
||||
} else {
|
||||
// v1 (legacy) record: keep identity + key, but discard the old timestamp -
|
||||
// its low bits would otherwise be misread as role/protected metadata.
|
||||
// Normalise older records: v1's timestamp would be misread as role/protected,
|
||||
// and v2's bit 6 as a signer we never verified.
|
||||
uint32_t lh = rec.last_heard;
|
||||
if (legacy) {
|
||||
if (fmt == WarmFormat::V1) {
|
||||
lh = 0;
|
||||
migrated++;
|
||||
} else if (fmt == WarmFormat::V2) {
|
||||
lh &= ~(WARM_SIGNER_MASK << WARM_SIGNER_SHIFT);
|
||||
migrated++;
|
||||
}
|
||||
const WarmNodeEntry *e = place(rec.num, lh, rec.public_key);
|
||||
if (e)
|
||||
@@ -460,17 +470,16 @@ void WarmNodeStore::load()
|
||||
activePage = p;
|
||||
writeSlot = slot;
|
||||
nextSeq = seqs[k] + 1;
|
||||
// If the head is a v1 page, force the next append to rotate into a fresh v2 page,
|
||||
// so new (v2) records never land in a page whose header says v1 (which would make
|
||||
// a later load discard their last_heard - including the role/protected we just set).
|
||||
if (legacy)
|
||||
// Rotate rather than append into an older-format page: a later load would
|
||||
// normalise the new records to that page's format and drop metadata.
|
||||
if (fmt != WarmFormat::Current)
|
||||
writeSlot = kRecordsPerPage;
|
||||
}
|
||||
}
|
||||
if (nCorrupt)
|
||||
LOG_WARN("WarmStore: dropped %u corrupt ring page(s), some nodes lost", nCorrupt);
|
||||
if (migrated)
|
||||
LOG_INFO("WarmStore: migrated %u v1 record(s) (kept key, discarded last_heard)", (unsigned)migrated);
|
||||
LOG_INFO("WarmStore: migrated %u legacy record(s) to the current format", (unsigned)migrated);
|
||||
LOG_INFO("WarmStore: replayed %u ring records -> %u live nodes (page %u, slot %u)", (unsigned)replayed, (unsigned)count(),
|
||||
activePage, writeSlot);
|
||||
}
|
||||
@@ -537,10 +546,14 @@ void WarmNodeStore::load()
|
||||
LOG_WARN("WarmStore: %s header read failed, starting empty", warmFileName);
|
||||
return;
|
||||
}
|
||||
// v1 (WRM1) is still accepted: same record size, but its last_heard was a plain
|
||||
// timestamp. We keep identity + key and discard last_heard on load (see below).
|
||||
const bool legacy = (h.magic == WARM_STORE_MAGIC_V1);
|
||||
if ((h.magic != WARM_STORE_MAGIC && !legacy) || h.entrySize != sizeof(WarmNodeEntry) || h.count > WARM_NODE_COUNT) {
|
||||
// Older snapshots are still accepted: same record size, fewer metadata bits in
|
||||
// last_heard. Both are normalised to the current format below.
|
||||
const bool known = h.magic == WARM_STORE_MAGIC || h.magic == WARM_STORE_MAGIC_V2 || h.magic == WARM_STORE_MAGIC_V1;
|
||||
const WarmFormat fmt = h.magic == WARM_STORE_MAGIC_V1 ? WarmFormat::V1
|
||||
: h.magic == WARM_STORE_MAGIC_V2 ? WarmFormat::V2
|
||||
: WarmFormat::Current;
|
||||
const bool legacy = fmt != WarmFormat::Current;
|
||||
if (!known || h.entrySize != sizeof(WarmNodeEntry) || h.count > WARM_NODE_COUNT) {
|
||||
f.close();
|
||||
LOG_WARN("WarmStore: %s header invalid (magic=0x%08x entrySize=%u count=%u), starting empty", warmFileName, h.magic,
|
||||
h.entrySize, h.count);
|
||||
@@ -560,19 +573,24 @@ void WarmNodeStore::load()
|
||||
memset(entries, 0, WARM_NODE_COUNT * sizeof(WarmNodeEntry));
|
||||
return;
|
||||
}
|
||||
if (legacy) {
|
||||
// Migrate v1 → v2: discard the old last_heard (its low bits would be misread as
|
||||
// role/protected); keep num + public_key. Mark dirty so save() rewrites as v2.
|
||||
// Normalise older records, then mark dirty so save() rewrites in the current format:
|
||||
// v1's timestamp would be misread as role/protected, v2's bit 6 as an unverified signer.
|
||||
if (fmt == WarmFormat::V1) {
|
||||
for (size_t i = 0; i < WARM_NODE_COUNT; i++)
|
||||
if (entries[i].num)
|
||||
entries[i].last_heard = 0;
|
||||
dirty = true;
|
||||
} else if (fmt == WarmFormat::V2) {
|
||||
for (size_t i = 0; i < WARM_NODE_COUNT; i++)
|
||||
if (entries[i].num)
|
||||
entries[i].last_heard &= ~(WARM_SIGNER_MASK << WARM_SIGNER_SHIFT);
|
||||
dirty = true;
|
||||
}
|
||||
} else {
|
||||
f.close();
|
||||
}
|
||||
LOG_INFO("WarmStore: loaded %u warm nodes from %s%s", h.count, warmFileName,
|
||||
legacy ? " (v1 migrated: discarded last_heard)" : "");
|
||||
legacy ? " (migrated from an older format)" : "");
|
||||
}
|
||||
|
||||
bool WarmNodeStore::save()
|
||||
|
||||
+22
-10
@@ -43,27 +43,33 @@ static_assert(sizeof(WarmNodeEntry) == 40, "WarmNodeEntry must stay 40 B - persi
|
||||
//
|
||||
// The warm tier only uses last_heard to LRU-rank evicted (long-tail) nodes, so ~minute
|
||||
// recency resolution is plenty. We reclaim the low WARM_META_BITS of that field to carry
|
||||
// the evicted node's device role + a protected category, at zero cost to record size
|
||||
// (entry stays 40 B; no RAM/flash growth). The high bits remain a real unix-seconds
|
||||
// timestamp quantised to (1 << WARM_META_BITS) seconds.
|
||||
// the evicted node's device role, a protected category + a signer flag, at zero cost to
|
||||
// record size (entry stays 40 B; no RAM/flash growth). The high bits remain a real
|
||||
// unix-seconds timestamp quantised to (1 << WARM_META_BITS) seconds.
|
||||
//
|
||||
// Safe because: a real timestamp can never be all-ones (the tombstone sentinel) before
|
||||
// 2106, and tombstones/erased flash are detected via num before last_heard is read. Only
|
||||
// the LOW bits are stolen - the high (era) bits are untouched, so the time range is intact.
|
||||
static constexpr uint32_t WARM_META_BITS = 6; // role(4) + protected(2)
|
||||
static constexpr uint32_t WARM_META_MASK = (1u << WARM_META_BITS) - 1; // 0x3F → 64 s quantum
|
||||
static constexpr uint32_t WARM_TIME_MASK = ~WARM_META_MASK; // 0xFFFFFFC0
|
||||
static constexpr uint32_t WARM_META_BITS = 7; // role(4) + protected(2) + signer(1)
|
||||
static constexpr uint32_t WARM_META_MASK = (1u << WARM_META_BITS) - 1; // 0x7F → 128 s quantum
|
||||
static constexpr uint32_t WARM_TIME_MASK = ~WARM_META_MASK; // 0xFFFFFF80
|
||||
static constexpr uint32_t WARM_ROLE_MASK = 0x0Fu; // bits [3:0] device role (0..12)
|
||||
static constexpr uint32_t WARM_PROT_SHIFT = 4; // bits [5:4] protected category
|
||||
static constexpr uint32_t WARM_PROT_MASK = 0x03u;
|
||||
static constexpr uint32_t WARM_SIGNER_SHIFT = 6; // bit [6] we verified an XEdDSA signature from this node
|
||||
static constexpr uint32_t WARM_SIGNER_MASK = 0x01u;
|
||||
|
||||
// On-disk record format, from the page/file magic; older ones are normalised by load().
|
||||
enum class WarmFormat : uint8_t { Current, V2, V1 };
|
||||
|
||||
// Protected category cached alongside role so consumers needn't re-derive the mapping.
|
||||
enum class WarmProtected : uint8_t { None = 0, Role = 1, Flag = 2, XeddsaSigner = 3 };
|
||||
|
||||
inline uint32_t warmPackLastHeard(uint32_t lastHeard, uint8_t role, uint8_t prot)
|
||||
inline uint32_t warmPackLastHeard(uint32_t lastHeard, uint8_t role, uint8_t prot, bool signer)
|
||||
{
|
||||
return (lastHeard & WARM_TIME_MASK) | (static_cast<uint32_t>(role) & WARM_ROLE_MASK) |
|
||||
((static_cast<uint32_t>(prot) & WARM_PROT_MASK) << WARM_PROT_SHIFT);
|
||||
((static_cast<uint32_t>(prot) & WARM_PROT_MASK) << WARM_PROT_SHIFT) |
|
||||
((signer ? WARM_SIGNER_MASK : 0u) << WARM_SIGNER_SHIFT);
|
||||
}
|
||||
inline uint32_t warmTimeOf(const WarmNodeEntry &e)
|
||||
{
|
||||
@@ -77,6 +83,10 @@ inline uint8_t warmProtOf(const WarmNodeEntry &e)
|
||||
{
|
||||
return static_cast<uint8_t>((e.last_heard >> WARM_PROT_SHIFT) & WARM_PROT_MASK);
|
||||
}
|
||||
inline bool warmSignerOf(const WarmNodeEntry &e)
|
||||
{
|
||||
return ((e.last_heard >> WARM_SIGNER_SHIFT) & WARM_SIGNER_MASK) != 0;
|
||||
}
|
||||
|
||||
// Gated on NRF52840_XXAA: the ring sits at 0xEA000
|
||||
// valid only on the 1 MB-flash nRF52840.
|
||||
@@ -99,9 +109,11 @@ class WarmNodeStore
|
||||
/// entries; otherwise the oldest (keyless-first) entry is replaced.
|
||||
/// @param role the node's device role (meshtastic_Config_DeviceConfig_Role, 0..12)
|
||||
/// @param protectedCat WarmProtected category cached for the hop-trim path
|
||||
/// @param signer true if we ever verified an XEdDSA signature from this node, so
|
||||
/// re-admission restores the bit rather than relearning it
|
||||
/// @return true if the node was stored or updated
|
||||
bool absorb(NodeNum num, uint32_t lastHeard, const uint8_t *key32 /* may be NULL */, uint8_t role = 0,
|
||||
uint8_t protectedCat = 0);
|
||||
uint8_t protectedCat = 0, bool signer = false);
|
||||
|
||||
/// Look up the cached device role + protected category for a warm node.
|
||||
/// @return false if the node is not in the warm tier.
|
||||
@@ -167,7 +179,7 @@ class WarmNodeStore
|
||||
void ringAppend(const WarmNodeEntry &rec, int storeSlot /* -1 for tombstones */);
|
||||
void ringRotate(); // reclaim oldest page, compacting stranded live entries
|
||||
void ringOpenPage(uint8_t page); // erase + write header (seq = nextSeq++)
|
||||
bool ringReadHeader(uint8_t page, WarmPageHeader &h, bool *legacy = nullptr) const;
|
||||
bool ringReadHeader(uint8_t page, WarmPageHeader &h, WarmFormat *fmt = nullptr) const;
|
||||
#endif
|
||||
|
||||
bool save();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#include "mesh/eth/ethClient.h"
|
||||
#include "NodeDB.h"
|
||||
#include "RTC.h"
|
||||
#include "concurrency/Periodic.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "main.h"
|
||||
#include "mesh/api/ethServerAPI.h"
|
||||
#include "target_specific.h"
|
||||
|
||||
@@ -69,6 +69,8 @@ PB_BIND(meshtastic_Config_SessionkeyConfig, meshtastic_Config_SessionkeyConfig,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -374,7 +374,11 @@ typedef enum _meshtastic_Config_LoRaConfig_ModemPreset {
|
||||
Note: TCXO with tight tolerances (±5 ppm or better) is *absolutely required* at these narrow bandwidths.
|
||||
Only compatible with SX127x and SX126x chipsets.
|
||||
Comparable link budget and data rate to LONG_MODERATE. */
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_TINY_SLOW = 15
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_TINY_SLOW = 15,
|
||||
/* Medium Range - Turbo
|
||||
This preset performs similarly to MEDIUM_FAST, but with 500kHz bandwidth.
|
||||
It is not legal to use in all regions due to this wider bandwidth. */
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_TURBO = 16
|
||||
} meshtastic_Config_LoRaConfig_ModemPreset;
|
||||
|
||||
typedef enum _meshtastic_Config_LoRaConfig_FEM_LNA_Mode {
|
||||
@@ -395,6 +399,19 @@ typedef enum _meshtastic_Config_BluetoothConfig_PairingMode {
|
||||
meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN = 2
|
||||
} meshtastic_Config_BluetoothConfig_PairingMode;
|
||||
|
||||
/* Controls how the device authenticates remotely received mesh packets. */
|
||||
typedef enum _meshtastic_Config_SecurityConfig_PacketSignaturePolicy {
|
||||
/* Accept unsigned packets for maximum compatibility while still rejecting malformed or invalid signatures.
|
||||
This is the default to avoid legacy nodes dropping signed packets during rebroadcast. */
|
||||
meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_COMPATIBLE = 0,
|
||||
/* Prefer authenticated packets while retaining compatibility with unsigned packets from nodes not known to sign.
|
||||
Rejects unsigned, signable broadcasts from nodes that have previously signed. */
|
||||
meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_BALANCED = 1,
|
||||
/* Accept only packets authenticated by a verified XEdDSA signature or successful PKI decryption.
|
||||
Unsigned, malformed, invalid, or unverifiable packets are ignored. */
|
||||
meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_STRICT = 2
|
||||
} meshtastic_Config_SecurityConfig_PacketSignaturePolicy;
|
||||
|
||||
/* Struct definitions */
|
||||
/* Configuration */
|
||||
typedef struct _meshtastic_Config_DeviceConfig {
|
||||
@@ -685,6 +702,8 @@ typedef struct _meshtastic_Config_SecurityConfig {
|
||||
bool debug_log_api_enabled;
|
||||
/* Allow incoming device control over the insecure legacy admin channel. */
|
||||
bool admin_channel_enabled;
|
||||
/* Determines the packet signature policy applied to remotely received mesh packets. */
|
||||
meshtastic_Config_SecurityConfig_PacketSignaturePolicy packet_signature_policy;
|
||||
} meshtastic_Config_SecurityConfig;
|
||||
|
||||
/* Blank config request, strictly for getting the session key */
|
||||
@@ -767,8 +786,8 @@ extern "C" {
|
||||
#define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_ITU2_125CM+1))
|
||||
|
||||
#define _meshtastic_Config_LoRaConfig_ModemPreset_MIN meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST
|
||||
#define _meshtastic_Config_LoRaConfig_ModemPreset_MAX meshtastic_Config_LoRaConfig_ModemPreset_TINY_SLOW
|
||||
#define _meshtastic_Config_LoRaConfig_ModemPreset_ARRAYSIZE ((meshtastic_Config_LoRaConfig_ModemPreset)(meshtastic_Config_LoRaConfig_ModemPreset_TINY_SLOW+1))
|
||||
#define _meshtastic_Config_LoRaConfig_ModemPreset_MAX meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_TURBO
|
||||
#define _meshtastic_Config_LoRaConfig_ModemPreset_ARRAYSIZE ((meshtastic_Config_LoRaConfig_ModemPreset)(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_TURBO+1))
|
||||
|
||||
#define _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MIN meshtastic_Config_LoRaConfig_FEM_LNA_Mode_DISABLED
|
||||
#define _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MAX meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT
|
||||
@@ -778,6 +797,10 @@ extern "C" {
|
||||
#define _meshtastic_Config_BluetoothConfig_PairingMode_MAX meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN
|
||||
#define _meshtastic_Config_BluetoothConfig_PairingMode_ARRAYSIZE ((meshtastic_Config_BluetoothConfig_PairingMode)(meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN+1))
|
||||
|
||||
#define _meshtastic_Config_SecurityConfig_PacketSignaturePolicy_MIN meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_COMPATIBLE
|
||||
#define _meshtastic_Config_SecurityConfig_PacketSignaturePolicy_MAX meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_STRICT
|
||||
#define _meshtastic_Config_SecurityConfig_PacketSignaturePolicy_ARRAYSIZE ((meshtastic_Config_SecurityConfig_PacketSignaturePolicy)(meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_STRICT+1))
|
||||
|
||||
|
||||
#define meshtastic_Config_DeviceConfig_role_ENUMTYPE meshtastic_Config_DeviceConfig_Role
|
||||
#define meshtastic_Config_DeviceConfig_rebroadcast_mode_ENUMTYPE meshtastic_Config_DeviceConfig_RebroadcastMode
|
||||
@@ -801,6 +824,7 @@ extern "C" {
|
||||
|
||||
#define meshtastic_Config_BluetoothConfig_mode_ENUMTYPE meshtastic_Config_BluetoothConfig_PairingMode
|
||||
|
||||
#define meshtastic_Config_SecurityConfig_packet_signature_policy_ENUMTYPE meshtastic_Config_SecurityConfig_PacketSignaturePolicy
|
||||
|
||||
|
||||
|
||||
@@ -814,7 +838,7 @@ extern "C" {
|
||||
#define meshtastic_Config_DisplayConfig_init_default {0, _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0, 0, 0}
|
||||
#define meshtastic_Config_LoRaConfig_init_default {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0, _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MIN, 0}
|
||||
#define meshtastic_Config_BluetoothConfig_init_default {0, _meshtastic_Config_BluetoothConfig_PairingMode_MIN, 0}
|
||||
#define meshtastic_Config_SecurityConfig_init_default {{0, {0}}, {0, {0}}, 0, {{0, {0}}, {0, {0}}, {0, {0}}}, 0, 0, 0, 0}
|
||||
#define meshtastic_Config_SecurityConfig_init_default {{0, {0}}, {0, {0}}, 0, {{0, {0}}, {0, {0}}, {0, {0}}}, 0, 0, 0, 0, _meshtastic_Config_SecurityConfig_PacketSignaturePolicy_MIN}
|
||||
#define meshtastic_Config_SessionkeyConfig_init_default {0}
|
||||
#define meshtastic_Config_init_zero {0, {meshtastic_Config_DeviceConfig_init_zero}}
|
||||
#define meshtastic_Config_DeviceConfig_init_zero {_meshtastic_Config_DeviceConfig_Role_MIN, 0, 0, 0, _meshtastic_Config_DeviceConfig_RebroadcastMode_MIN, 0, 0, 0, 0, "", 0, _meshtastic_Config_DeviceConfig_BuzzerMode_MIN}
|
||||
@@ -825,7 +849,7 @@ extern "C" {
|
||||
#define meshtastic_Config_DisplayConfig_init_zero {0, _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0, 0, 0}
|
||||
#define meshtastic_Config_LoRaConfig_init_zero {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0, _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MIN, 0}
|
||||
#define meshtastic_Config_BluetoothConfig_init_zero {0, _meshtastic_Config_BluetoothConfig_PairingMode_MIN, 0}
|
||||
#define meshtastic_Config_SecurityConfig_init_zero {{0, {0}}, {0, {0}}, 0, {{0, {0}}, {0, {0}}, {0, {0}}}, 0, 0, 0, 0}
|
||||
#define meshtastic_Config_SecurityConfig_init_zero {{0, {0}}, {0, {0}}, 0, {{0, {0}}, {0, {0}}, {0, {0}}}, 0, 0, 0, 0, _meshtastic_Config_SecurityConfig_PacketSignaturePolicy_MIN}
|
||||
#define meshtastic_Config_SessionkeyConfig_init_zero {0}
|
||||
|
||||
/* Field tags (for use in manual encoding/decoding) */
|
||||
@@ -921,6 +945,7 @@ extern "C" {
|
||||
#define meshtastic_Config_SecurityConfig_serial_enabled_tag 5
|
||||
#define meshtastic_Config_SecurityConfig_debug_log_api_enabled_tag 6
|
||||
#define meshtastic_Config_SecurityConfig_admin_channel_enabled_tag 8
|
||||
#define meshtastic_Config_SecurityConfig_packet_signature_policy_tag 9
|
||||
#define meshtastic_Config_device_tag 1
|
||||
#define meshtastic_Config_position_tag 2
|
||||
#define meshtastic_Config_power_tag 3
|
||||
@@ -1082,7 +1107,8 @@ X(a, STATIC, REPEATED, BYTES, admin_key, 3) \
|
||||
X(a, STATIC, SINGULAR, BOOL, is_managed, 4) \
|
||||
X(a, STATIC, SINGULAR, BOOL, serial_enabled, 5) \
|
||||
X(a, STATIC, SINGULAR, BOOL, debug_log_api_enabled, 6) \
|
||||
X(a, STATIC, SINGULAR, BOOL, admin_channel_enabled, 8)
|
||||
X(a, STATIC, SINGULAR, BOOL, admin_channel_enabled, 8) \
|
||||
X(a, STATIC, SINGULAR, UENUM, packet_signature_policy, 9)
|
||||
#define meshtastic_Config_SecurityConfig_CALLBACK NULL
|
||||
#define meshtastic_Config_SecurityConfig_DEFAULT NULL
|
||||
|
||||
@@ -1126,7 +1152,7 @@ extern const pb_msgdesc_t meshtastic_Config_SessionkeyConfig_msg;
|
||||
#define meshtastic_Config_NetworkConfig_size 204
|
||||
#define meshtastic_Config_PositionConfig_size 62
|
||||
#define meshtastic_Config_PowerConfig_size 52
|
||||
#define meshtastic_Config_SecurityConfig_size 178
|
||||
#define meshtastic_Config_SecurityConfig_size 180
|
||||
#define meshtastic_Config_SessionkeyConfig_size 0
|
||||
#define meshtastic_Config_size 207
|
||||
|
||||
|
||||
@@ -452,7 +452,7 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg;
|
||||
/* Maximum encoded size of messages (where known) */
|
||||
/* meshtastic_NodeDatabase_size depends on runtime parameters */
|
||||
#define MESHTASTIC_MESHTASTIC_DEVICEONLY_PB_H_MAX_SIZE meshtastic_BackupPreferences_size
|
||||
#define meshtastic_BackupPreferences_size 2738
|
||||
#define meshtastic_BackupPreferences_size 2740
|
||||
#define meshtastic_ChannelFile_size 718
|
||||
#define meshtastic_DeviceState_size 1944
|
||||
#define meshtastic_NodeEnvironmentEntry_size 170
|
||||
|
||||
@@ -6,10 +6,34 @@
|
||||
#error Regenerate this file with the current version of nanopb generator.
|
||||
#endif
|
||||
|
||||
PB_BIND(meshtastic_SensorData, meshtastic_SensorData, AUTO)
|
||||
PB_BIND(meshtastic_FileTransfer, meshtastic_FileTransfer, 4)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_DirectoryListing, meshtastic_DirectoryListing, 4)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_I2CTransaction, meshtastic_I2CTransaction, 2)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_SdCardInfo, meshtastic_SdCardInfo, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_I2CResult, meshtastic_I2CResult, 2)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_InterdeviceMessage, meshtastic_InterdeviceMessage, 4)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
PB_BIND(meshtastic_InterdeviceMessage, meshtastic_InterdeviceMessage, 2)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -10,38 +10,194 @@
|
||||
#endif
|
||||
|
||||
/* Enum definitions */
|
||||
typedef enum _meshtastic_MessageType {
|
||||
meshtastic_MessageType_ACK = 0,
|
||||
meshtastic_MessageType_COLLECT_INTERVAL = 160, /* in ms */
|
||||
meshtastic_MessageType_BEEP_ON = 161, /* duration ms */
|
||||
meshtastic_MessageType_BEEP_OFF = 162, /* cancel prematurely */
|
||||
meshtastic_MessageType_SHUTDOWN = 163,
|
||||
meshtastic_MessageType_POWER_ON = 164,
|
||||
meshtastic_MessageType_SCD41_TEMP = 176,
|
||||
meshtastic_MessageType_SCD41_HUMIDITY = 177,
|
||||
meshtastic_MessageType_SCD41_CO2 = 178,
|
||||
meshtastic_MessageType_AHT20_TEMP = 179,
|
||||
meshtastic_MessageType_AHT20_HUMIDITY = 180,
|
||||
meshtastic_MessageType_TVOC_INDEX = 181
|
||||
} meshtastic_MessageType;
|
||||
/* Version of the interdevice protocol spoken on the link. Both sides send
|
||||
theirs in the ping/pong handshake; a peer reporting a different one runs
|
||||
firmware that does not match and is not talked to.
|
||||
|
||||
On a change that breaks the other side (renumbered fields, changed
|
||||
semantics, removed messages), raise the value of CURRENT. Do not add
|
||||
another entry: this enum carries a single constant, not a history. */
|
||||
typedef enum _meshtastic_InterdeviceVersion {
|
||||
meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_UNSPECIFIED = 0,
|
||||
/* Never use 1: ping/pong were bools before the handshake existed, and a
|
||||
bool true is the same varint on the wire as the number 1, so firmware
|
||||
predating the handshake would pass it. */
|
||||
meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_CURRENT = 2
|
||||
} meshtastic_InterdeviceVersion;
|
||||
|
||||
/* Defines the supported file operations */
|
||||
typedef enum _meshtastic_FileOperation {
|
||||
meshtastic_FileOperation_GET = 0,
|
||||
meshtastic_FileOperation_POST = 1,
|
||||
meshtastic_FileOperation_PUT = 2,
|
||||
meshtastic_FileOperation_DELETE = 3
|
||||
} meshtastic_FileOperation;
|
||||
|
||||
/* Outcome of a file or directory operation. The requester must be able to
|
||||
tell a transient condition from a definitive one: BUSY is worth another
|
||||
try, NOT_FOUND is not. */
|
||||
typedef enum _meshtastic_FileStatus {
|
||||
meshtastic_FileStatus_FILE_UNSPECIFIED = 0,
|
||||
meshtastic_FileStatus_FILE_OK = 1,
|
||||
/* Retry later: the co-processor is doing card maintenance (mount,
|
||||
free space scan) and cannot serve the request right now */
|
||||
meshtastic_FileStatus_FILE_BUSY = 2,
|
||||
meshtastic_FileStatus_FILE_NO_CARD = 3,
|
||||
meshtastic_FileStatus_FILE_NOT_FOUND = 4,
|
||||
/* PUT only: offset did not match the current end of the file. file_size
|
||||
carries the size the file actually has, so the writer can resync (or
|
||||
recognize its own chunk as already written after a lost response). */
|
||||
meshtastic_FileStatus_FILE_OFFSET_CONFLICT = 5,
|
||||
meshtastic_FileStatus_FILE_IO_ERROR = 6,
|
||||
meshtastic_FileStatus_FILE_NOT_A_FILE = 7 /* path is a directory (GET) or not one (listing) */
|
||||
} meshtastic_FileStatus;
|
||||
|
||||
/* What to do with the SD card of the co-processor */
|
||||
typedef enum _meshtastic_SdCommand {
|
||||
meshtastic_SdCommand_SD_COMMAND_UNSPECIFIED = 0,
|
||||
meshtastic_SdCommand_SD_MOUNT = 1, /* mount a card that is in the slot, also after an eject */
|
||||
meshtastic_SdCommand_SD_EJECT = 2, /* flush and release the card so it can be pulled safely */
|
||||
meshtastic_SdCommand_SD_FORMAT = 3 /* wipe the card and put a fresh FAT on it, then mount it */
|
||||
} meshtastic_SdCommand;
|
||||
|
||||
typedef enum _meshtastic_SdCardInfo_CardType {
|
||||
meshtastic_SdCardInfo_CardType_NONE = 0,
|
||||
meshtastic_SdCardInfo_CardType_MMC = 1,
|
||||
meshtastic_SdCardInfo_CardType_SD = 2,
|
||||
meshtastic_SdCardInfo_CardType_SDHC = 3,
|
||||
meshtastic_SdCardInfo_CardType_SDXC = 4,
|
||||
meshtastic_SdCardInfo_CardType_UNKNOWN_CARD = 5
|
||||
} meshtastic_SdCardInfo_CardType;
|
||||
|
||||
typedef enum _meshtastic_SdCardInfo_FatType {
|
||||
meshtastic_SdCardInfo_FatType_UNKNOWN_FAT = 0,
|
||||
meshtastic_SdCardInfo_FatType_FAT16 = 1,
|
||||
meshtastic_SdCardInfo_FatType_FAT32 = 2,
|
||||
meshtastic_SdCardInfo_FatType_EXFAT = 3
|
||||
} meshtastic_SdCardInfo_FatType;
|
||||
|
||||
typedef enum _meshtastic_I2CResult_Status {
|
||||
/* Never sent: an all-defaults (e.g. accidentally empty) message must
|
||||
not decode as a successful transaction */
|
||||
meshtastic_I2CResult_Status_UNSPECIFIED = 0,
|
||||
meshtastic_I2CResult_Status_OK = 1,
|
||||
meshtastic_I2CResult_Status_NACK_ADDRESS = 2,
|
||||
meshtastic_I2CResult_Status_NACK_DATA = 3,
|
||||
meshtastic_I2CResult_Status_ERROR = 4
|
||||
} meshtastic_I2CResult_Status;
|
||||
|
||||
/* Struct definitions */
|
||||
typedef struct _meshtastic_SensorData {
|
||||
/* The message type */
|
||||
meshtastic_MessageType type;
|
||||
pb_size_t which_data;
|
||||
union {
|
||||
float float_value;
|
||||
uint32_t uint32_value;
|
||||
} data;
|
||||
} meshtastic_SensorData;
|
||||
typedef PB_BYTES_ARRAY_T(4096) meshtastic_FileTransfer_filedata_t;
|
||||
/* Message for file operations */
|
||||
typedef struct _meshtastic_FileTransfer {
|
||||
meshtastic_FileOperation operation; /* File operation (GET, POST, PUT, DELETE) */
|
||||
char filepath[256]; /* Path of the file on the SD card */
|
||||
meshtastic_FileTransfer_filedata_t filedata; /* Chunk content (POST/PUT request, GET response) */
|
||||
meshtastic_FileStatus status; /* Response: outcome of the operation */
|
||||
char message[255]; /* Response: human readable detail, may be empty */
|
||||
uint64_t offset; /* Byte offset of this chunk within the file (ranged GET/PUT) */
|
||||
/* GET request: number of bytes to read, 0 = max chunk size. A response
|
||||
carries at most the filedata max_size (see interdevice.options) per
|
||||
chunk; larger requests are truncated, visible in the filedata length. */
|
||||
uint32_t length;
|
||||
uint64_t file_size; /* GET response: total size of the file */
|
||||
} meshtastic_FileTransfer;
|
||||
|
||||
/* Message for structured directory listing */
|
||||
typedef struct _meshtastic_DirectoryListing {
|
||||
char directory[256]; /* Path of the directory */
|
||||
/* One page of entry names, full FAT LFN length. Subdirectories carry a
|
||||
trailing slash. Note that a name whose directory prefix pushes the
|
||||
combined path past the FileTransfer.filepath limit cannot round-trip.
|
||||
Page size is the max_count in interdevice.options; page through with
|
||||
offset and total_count. */
|
||||
pb_size_t filenames_count;
|
||||
char filenames[16][256];
|
||||
meshtastic_FileStatus status; /* Response: outcome of the operation */
|
||||
char message[255]; /* Response: human readable detail, may be empty */
|
||||
uint32_t offset; /* Request: skip this many entries (paging) */
|
||||
uint32_t total_count; /* Response: total number of entries in the directory */
|
||||
} meshtastic_DirectoryListing;
|
||||
|
||||
typedef PB_BYTES_ARRAY_T(256) meshtastic_I2CTransaction_write_data_t;
|
||||
/* A single I2C transaction: an optional write followed by an optional
|
||||
read with repeated start, matching the TwoWire usage of sensor drivers
|
||||
(beginTransmission/write.../endTransmission(false)/requestFrom) */
|
||||
typedef struct _meshtastic_I2CTransaction {
|
||||
uint32_t address; /* 7-bit device address */
|
||||
meshtastic_I2CTransaction_write_data_t write_data; /* Bytes to write, may be empty */
|
||||
/* Number of bytes to read after the write, 0 = write-only. Bounded by
|
||||
the read_data max_size of I2CResult (see interdevice.options); larger
|
||||
requests are truncated, visible in the returned byte count. */
|
||||
uint32_t read_len;
|
||||
} meshtastic_I2CTransaction;
|
||||
|
||||
/* SD card statistics */
|
||||
typedef struct _meshtastic_SdCardInfo {
|
||||
/* Card initialized and usable. False while `busy` is set does not mean
|
||||
there is no card: the co-processor does not know yet. */
|
||||
bool present;
|
||||
meshtastic_SdCardInfo_CardType card_type;
|
||||
meshtastic_SdCardInfo_FatType fat_type;
|
||||
uint64_t card_size; /* Filesystem size in bytes */
|
||||
uint64_t used_bytes; /* Used bytes (may be expensive to compute on FAT32) */
|
||||
uint64_t free_bytes; /* Free bytes */
|
||||
/* used_bytes/free_bytes are only meaningful when true: the scan behind
|
||||
them runs in the background after mount and can take a while, and a
|
||||
full card is otherwise indistinguishable from a scan in progress */
|
||||
bool stats_valid;
|
||||
/* The co-processor is mounting a card right now, so whether one is
|
||||
present is not decided yet. Ask again rather than concluding the slot
|
||||
is empty. */
|
||||
bool busy;
|
||||
/* A card answers in the slot but carries no filesystem that could be
|
||||
mounted (present is false then). Formatting it makes it usable. */
|
||||
bool unformatted;
|
||||
} meshtastic_SdCardInfo;
|
||||
|
||||
typedef PB_BYTES_ARRAY_T(256) meshtastic_I2CResult_read_data_t;
|
||||
/* Result of an I2CTransaction */
|
||||
typedef struct _meshtastic_I2CResult {
|
||||
meshtastic_I2CResult_Status status;
|
||||
meshtastic_I2CResult_read_data_t read_data; /* Data read from the device, empty for write-only transactions */
|
||||
} meshtastic_I2CResult;
|
||||
|
||||
typedef PB_BYTES_ARRAY_T(128) meshtastic_InterdeviceMessage_i2c_scan_result_t;
|
||||
/* Main message for interdevice communication */
|
||||
typedef struct _meshtastic_InterdeviceMessage {
|
||||
pb_size_t which_data;
|
||||
union {
|
||||
char nmea[1024];
|
||||
meshtastic_SensorData sensor;
|
||||
uint16_t beep;
|
||||
meshtastic_I2CTransaction i2c_transaction;
|
||||
meshtastic_I2CResult i2c_result;
|
||||
bool i2c_scan; /* Request: scan the secondary I2C bus */
|
||||
meshtastic_InterdeviceMessage_i2c_scan_result_t i2c_scan_result; /* Response: 7-bit addresses of discovered devices */
|
||||
meshtastic_FileTransfer file_transfer;
|
||||
meshtastic_DirectoryListing directory_listing;
|
||||
bool get_sd_info; /* Request: SD card statistics */
|
||||
meshtastic_SdCardInfo sd_info; /* Response */
|
||||
/* Link liveness probe and version handshake. The receiver answers ping
|
||||
with pong, echoing the id. Touches no peripherals, so it works with
|
||||
nothing attached. Both carry the version the sender speaks; a peer
|
||||
that answers with a different one speaks another protocol and must
|
||||
not be used. */
|
||||
meshtastic_InterdeviceVersion ping;
|
||||
meshtastic_InterdeviceVersion pong;
|
||||
/* Response: the request could not be decoded or is of an unhandled
|
||||
type, so the requester fails fast instead of burning its timeout.
|
||||
Echoes the id when known, 0 when the frame was undecodable. Never
|
||||
sent in reaction to a nack. */
|
||||
bool nack;
|
||||
/* Request: mount the card, or release it so it can be pulled safely. The
|
||||
co-processor answers with sd_info. Without an eject the card is mounted
|
||||
on its own and kept mounted; after one it stays released until a mount
|
||||
is asked for. */
|
||||
meshtastic_SdCommand sd_command;
|
||||
} data;
|
||||
/* Correlates a response with its request: responses echo the id of the
|
||||
request they answer. 0 for unsolicited messages (e.g. the nmea stream). */
|
||||
uint32_t id;
|
||||
} meshtastic_InterdeviceMessage;
|
||||
|
||||
|
||||
@@ -50,53 +206,205 @@ extern "C" {
|
||||
#endif
|
||||
|
||||
/* Helper constants for enums */
|
||||
#define _meshtastic_MessageType_MIN meshtastic_MessageType_ACK
|
||||
#define _meshtastic_MessageType_MAX meshtastic_MessageType_TVOC_INDEX
|
||||
#define _meshtastic_MessageType_ARRAYSIZE ((meshtastic_MessageType)(meshtastic_MessageType_TVOC_INDEX+1))
|
||||
#define _meshtastic_InterdeviceVersion_MIN meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_UNSPECIFIED
|
||||
#define _meshtastic_InterdeviceVersion_MAX meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_CURRENT
|
||||
#define _meshtastic_InterdeviceVersion_ARRAYSIZE ((meshtastic_InterdeviceVersion)(meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_CURRENT+1))
|
||||
|
||||
#define meshtastic_SensorData_type_ENUMTYPE meshtastic_MessageType
|
||||
#define _meshtastic_FileOperation_MIN meshtastic_FileOperation_GET
|
||||
#define _meshtastic_FileOperation_MAX meshtastic_FileOperation_DELETE
|
||||
#define _meshtastic_FileOperation_ARRAYSIZE ((meshtastic_FileOperation)(meshtastic_FileOperation_DELETE+1))
|
||||
|
||||
#define _meshtastic_FileStatus_MIN meshtastic_FileStatus_FILE_UNSPECIFIED
|
||||
#define _meshtastic_FileStatus_MAX meshtastic_FileStatus_FILE_NOT_A_FILE
|
||||
#define _meshtastic_FileStatus_ARRAYSIZE ((meshtastic_FileStatus)(meshtastic_FileStatus_FILE_NOT_A_FILE+1))
|
||||
|
||||
#define _meshtastic_SdCommand_MIN meshtastic_SdCommand_SD_COMMAND_UNSPECIFIED
|
||||
#define _meshtastic_SdCommand_MAX meshtastic_SdCommand_SD_FORMAT
|
||||
#define _meshtastic_SdCommand_ARRAYSIZE ((meshtastic_SdCommand)(meshtastic_SdCommand_SD_FORMAT+1))
|
||||
|
||||
#define _meshtastic_SdCardInfo_CardType_MIN meshtastic_SdCardInfo_CardType_NONE
|
||||
#define _meshtastic_SdCardInfo_CardType_MAX meshtastic_SdCardInfo_CardType_UNKNOWN_CARD
|
||||
#define _meshtastic_SdCardInfo_CardType_ARRAYSIZE ((meshtastic_SdCardInfo_CardType)(meshtastic_SdCardInfo_CardType_UNKNOWN_CARD+1))
|
||||
|
||||
#define _meshtastic_SdCardInfo_FatType_MIN meshtastic_SdCardInfo_FatType_UNKNOWN_FAT
|
||||
#define _meshtastic_SdCardInfo_FatType_MAX meshtastic_SdCardInfo_FatType_EXFAT
|
||||
#define _meshtastic_SdCardInfo_FatType_ARRAYSIZE ((meshtastic_SdCardInfo_FatType)(meshtastic_SdCardInfo_FatType_EXFAT+1))
|
||||
|
||||
#define _meshtastic_I2CResult_Status_MIN meshtastic_I2CResult_Status_UNSPECIFIED
|
||||
#define _meshtastic_I2CResult_Status_MAX meshtastic_I2CResult_Status_ERROR
|
||||
#define _meshtastic_I2CResult_Status_ARRAYSIZE ((meshtastic_I2CResult_Status)(meshtastic_I2CResult_Status_ERROR+1))
|
||||
|
||||
#define meshtastic_FileTransfer_operation_ENUMTYPE meshtastic_FileOperation
|
||||
#define meshtastic_FileTransfer_status_ENUMTYPE meshtastic_FileStatus
|
||||
|
||||
#define meshtastic_DirectoryListing_status_ENUMTYPE meshtastic_FileStatus
|
||||
|
||||
|
||||
#define meshtastic_SdCardInfo_card_type_ENUMTYPE meshtastic_SdCardInfo_CardType
|
||||
#define meshtastic_SdCardInfo_fat_type_ENUMTYPE meshtastic_SdCardInfo_FatType
|
||||
|
||||
#define meshtastic_I2CResult_status_ENUMTYPE meshtastic_I2CResult_Status
|
||||
|
||||
#define meshtastic_InterdeviceMessage_data_ping_ENUMTYPE meshtastic_InterdeviceVersion
|
||||
#define meshtastic_InterdeviceMessage_data_pong_ENUMTYPE meshtastic_InterdeviceVersion
|
||||
#define meshtastic_InterdeviceMessage_data_sd_command_ENUMTYPE meshtastic_SdCommand
|
||||
|
||||
|
||||
/* Initializer values for message structs */
|
||||
#define meshtastic_SensorData_init_default {_meshtastic_MessageType_MIN, 0, {0}}
|
||||
#define meshtastic_InterdeviceMessage_init_default {0, {""}}
|
||||
#define meshtastic_SensorData_init_zero {_meshtastic_MessageType_MIN, 0, {0}}
|
||||
#define meshtastic_InterdeviceMessage_init_zero {0, {""}}
|
||||
#define meshtastic_FileTransfer_init_default {_meshtastic_FileOperation_MIN, "", {0, {0}}, _meshtastic_FileStatus_MIN, "", 0, 0, 0}
|
||||
#define meshtastic_DirectoryListing_init_default {"", 0, {"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""}, _meshtastic_FileStatus_MIN, "", 0, 0}
|
||||
#define meshtastic_I2CTransaction_init_default {0, {0, {0}}, 0}
|
||||
#define meshtastic_SdCardInfo_init_default {0, _meshtastic_SdCardInfo_CardType_MIN, _meshtastic_SdCardInfo_FatType_MIN, 0, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_I2CResult_init_default {_meshtastic_I2CResult_Status_MIN, {0, {0}}}
|
||||
#define meshtastic_InterdeviceMessage_init_default {0, {""}, 0}
|
||||
#define meshtastic_FileTransfer_init_zero {_meshtastic_FileOperation_MIN, "", {0, {0}}, _meshtastic_FileStatus_MIN, "", 0, 0, 0}
|
||||
#define meshtastic_DirectoryListing_init_zero {"", 0, {"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""}, _meshtastic_FileStatus_MIN, "", 0, 0}
|
||||
#define meshtastic_I2CTransaction_init_zero {0, {0, {0}}, 0}
|
||||
#define meshtastic_SdCardInfo_init_zero {0, _meshtastic_SdCardInfo_CardType_MIN, _meshtastic_SdCardInfo_FatType_MIN, 0, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_I2CResult_init_zero {_meshtastic_I2CResult_Status_MIN, {0, {0}}}
|
||||
#define meshtastic_InterdeviceMessage_init_zero {0, {""}, 0}
|
||||
|
||||
/* Field tags (for use in manual encoding/decoding) */
|
||||
#define meshtastic_SensorData_type_tag 1
|
||||
#define meshtastic_SensorData_float_value_tag 2
|
||||
#define meshtastic_SensorData_uint32_value_tag 3
|
||||
#define meshtastic_FileTransfer_operation_tag 1
|
||||
#define meshtastic_FileTransfer_filepath_tag 2
|
||||
#define meshtastic_FileTransfer_filedata_tag 3
|
||||
#define meshtastic_FileTransfer_status_tag 4
|
||||
#define meshtastic_FileTransfer_message_tag 5
|
||||
#define meshtastic_FileTransfer_offset_tag 6
|
||||
#define meshtastic_FileTransfer_length_tag 7
|
||||
#define meshtastic_FileTransfer_file_size_tag 8
|
||||
#define meshtastic_DirectoryListing_directory_tag 1
|
||||
#define meshtastic_DirectoryListing_filenames_tag 2
|
||||
#define meshtastic_DirectoryListing_status_tag 3
|
||||
#define meshtastic_DirectoryListing_message_tag 4
|
||||
#define meshtastic_DirectoryListing_offset_tag 5
|
||||
#define meshtastic_DirectoryListing_total_count_tag 6
|
||||
#define meshtastic_I2CTransaction_address_tag 1
|
||||
#define meshtastic_I2CTransaction_write_data_tag 2
|
||||
#define meshtastic_I2CTransaction_read_len_tag 3
|
||||
#define meshtastic_SdCardInfo_present_tag 1
|
||||
#define meshtastic_SdCardInfo_card_type_tag 2
|
||||
#define meshtastic_SdCardInfo_fat_type_tag 3
|
||||
#define meshtastic_SdCardInfo_card_size_tag 4
|
||||
#define meshtastic_SdCardInfo_used_bytes_tag 5
|
||||
#define meshtastic_SdCardInfo_free_bytes_tag 6
|
||||
#define meshtastic_SdCardInfo_stats_valid_tag 7
|
||||
#define meshtastic_SdCardInfo_busy_tag 8
|
||||
#define meshtastic_SdCardInfo_unformatted_tag 9
|
||||
#define meshtastic_I2CResult_status_tag 1
|
||||
#define meshtastic_I2CResult_read_data_tag 2
|
||||
#define meshtastic_InterdeviceMessage_nmea_tag 1
|
||||
#define meshtastic_InterdeviceMessage_sensor_tag 2
|
||||
#define meshtastic_InterdeviceMessage_beep_tag 2
|
||||
#define meshtastic_InterdeviceMessage_i2c_transaction_tag 3
|
||||
#define meshtastic_InterdeviceMessage_i2c_result_tag 4
|
||||
#define meshtastic_InterdeviceMessage_i2c_scan_tag 5
|
||||
#define meshtastic_InterdeviceMessage_i2c_scan_result_tag 6
|
||||
#define meshtastic_InterdeviceMessage_file_transfer_tag 7
|
||||
#define meshtastic_InterdeviceMessage_directory_listing_tag 8
|
||||
#define meshtastic_InterdeviceMessage_get_sd_info_tag 9
|
||||
#define meshtastic_InterdeviceMessage_sd_info_tag 10
|
||||
#define meshtastic_InterdeviceMessage_ping_tag 11
|
||||
#define meshtastic_InterdeviceMessage_pong_tag 12
|
||||
#define meshtastic_InterdeviceMessage_nack_tag 13
|
||||
#define meshtastic_InterdeviceMessage_sd_command_tag 14
|
||||
#define meshtastic_InterdeviceMessage_id_tag 15
|
||||
|
||||
/* Struct field encoding specification for nanopb */
|
||||
#define meshtastic_SensorData_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, UENUM, type, 1) \
|
||||
X(a, STATIC, ONEOF, FLOAT, (data,float_value,data.float_value), 2) \
|
||||
X(a, STATIC, ONEOF, UINT32, (data,uint32_value,data.uint32_value), 3)
|
||||
#define meshtastic_SensorData_CALLBACK NULL
|
||||
#define meshtastic_SensorData_DEFAULT NULL
|
||||
#define meshtastic_FileTransfer_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, UENUM, operation, 1) \
|
||||
X(a, STATIC, SINGULAR, STRING, filepath, 2) \
|
||||
X(a, STATIC, SINGULAR, BYTES, filedata, 3) \
|
||||
X(a, STATIC, SINGULAR, UENUM, status, 4) \
|
||||
X(a, STATIC, SINGULAR, STRING, message, 5) \
|
||||
X(a, STATIC, SINGULAR, UINT64, offset, 6) \
|
||||
X(a, STATIC, SINGULAR, UINT32, length, 7) \
|
||||
X(a, STATIC, SINGULAR, UINT64, file_size, 8)
|
||||
#define meshtastic_FileTransfer_CALLBACK NULL
|
||||
#define meshtastic_FileTransfer_DEFAULT NULL
|
||||
|
||||
#define meshtastic_DirectoryListing_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, STRING, directory, 1) \
|
||||
X(a, STATIC, REPEATED, STRING, filenames, 2) \
|
||||
X(a, STATIC, SINGULAR, UENUM, status, 3) \
|
||||
X(a, STATIC, SINGULAR, STRING, message, 4) \
|
||||
X(a, STATIC, SINGULAR, UINT32, offset, 5) \
|
||||
X(a, STATIC, SINGULAR, UINT32, total_count, 6)
|
||||
#define meshtastic_DirectoryListing_CALLBACK NULL
|
||||
#define meshtastic_DirectoryListing_DEFAULT NULL
|
||||
|
||||
#define meshtastic_I2CTransaction_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, UINT32, address, 1) \
|
||||
X(a, STATIC, SINGULAR, BYTES, write_data, 2) \
|
||||
X(a, STATIC, SINGULAR, UINT32, read_len, 3)
|
||||
#define meshtastic_I2CTransaction_CALLBACK NULL
|
||||
#define meshtastic_I2CTransaction_DEFAULT NULL
|
||||
|
||||
#define meshtastic_SdCardInfo_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, BOOL, present, 1) \
|
||||
X(a, STATIC, SINGULAR, UENUM, card_type, 2) \
|
||||
X(a, STATIC, SINGULAR, UENUM, fat_type, 3) \
|
||||
X(a, STATIC, SINGULAR, UINT64, card_size, 4) \
|
||||
X(a, STATIC, SINGULAR, UINT64, used_bytes, 5) \
|
||||
X(a, STATIC, SINGULAR, UINT64, free_bytes, 6) \
|
||||
X(a, STATIC, SINGULAR, BOOL, stats_valid, 7) \
|
||||
X(a, STATIC, SINGULAR, BOOL, busy, 8) \
|
||||
X(a, STATIC, SINGULAR, BOOL, unformatted, 9)
|
||||
#define meshtastic_SdCardInfo_CALLBACK NULL
|
||||
#define meshtastic_SdCardInfo_DEFAULT NULL
|
||||
|
||||
#define meshtastic_I2CResult_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, UENUM, status, 1) \
|
||||
X(a, STATIC, SINGULAR, BYTES, read_data, 2)
|
||||
#define meshtastic_I2CResult_CALLBACK NULL
|
||||
#define meshtastic_I2CResult_DEFAULT NULL
|
||||
|
||||
#define meshtastic_InterdeviceMessage_FIELDLIST(X, a) \
|
||||
X(a, STATIC, ONEOF, STRING, (data,nmea,data.nmea), 1) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (data,sensor,data.sensor), 2)
|
||||
X(a, STATIC, ONEOF, UINT32, (data,beep,data.beep), 2) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (data,i2c_transaction,data.i2c_transaction), 3) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (data,i2c_result,data.i2c_result), 4) \
|
||||
X(a, STATIC, ONEOF, BOOL, (data,i2c_scan,data.i2c_scan), 5) \
|
||||
X(a, STATIC, ONEOF, BYTES, (data,i2c_scan_result,data.i2c_scan_result), 6) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (data,file_transfer,data.file_transfer), 7) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (data,directory_listing,data.directory_listing), 8) \
|
||||
X(a, STATIC, ONEOF, BOOL, (data,get_sd_info,data.get_sd_info), 9) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (data,sd_info,data.sd_info), 10) \
|
||||
X(a, STATIC, ONEOF, UENUM, (data,ping,data.ping), 11) \
|
||||
X(a, STATIC, ONEOF, UENUM, (data,pong,data.pong), 12) \
|
||||
X(a, STATIC, ONEOF, BOOL, (data,nack,data.nack), 13) \
|
||||
X(a, STATIC, ONEOF, UENUM, (data,sd_command,data.sd_command), 14) \
|
||||
X(a, STATIC, SINGULAR, UINT32, id, 15)
|
||||
#define meshtastic_InterdeviceMessage_CALLBACK NULL
|
||||
#define meshtastic_InterdeviceMessage_DEFAULT NULL
|
||||
#define meshtastic_InterdeviceMessage_data_sensor_MSGTYPE meshtastic_SensorData
|
||||
#define meshtastic_InterdeviceMessage_data_i2c_transaction_MSGTYPE meshtastic_I2CTransaction
|
||||
#define meshtastic_InterdeviceMessage_data_i2c_result_MSGTYPE meshtastic_I2CResult
|
||||
#define meshtastic_InterdeviceMessage_data_file_transfer_MSGTYPE meshtastic_FileTransfer
|
||||
#define meshtastic_InterdeviceMessage_data_directory_listing_MSGTYPE meshtastic_DirectoryListing
|
||||
#define meshtastic_InterdeviceMessage_data_sd_info_MSGTYPE meshtastic_SdCardInfo
|
||||
|
||||
extern const pb_msgdesc_t meshtastic_SensorData_msg;
|
||||
extern const pb_msgdesc_t meshtastic_FileTransfer_msg;
|
||||
extern const pb_msgdesc_t meshtastic_DirectoryListing_msg;
|
||||
extern const pb_msgdesc_t meshtastic_I2CTransaction_msg;
|
||||
extern const pb_msgdesc_t meshtastic_SdCardInfo_msg;
|
||||
extern const pb_msgdesc_t meshtastic_I2CResult_msg;
|
||||
extern const pb_msgdesc_t meshtastic_InterdeviceMessage_msg;
|
||||
|
||||
/* Defines for backwards compatibility with code written before nanopb-0.4.0 */
|
||||
#define meshtastic_SensorData_fields &meshtastic_SensorData_msg
|
||||
#define meshtastic_FileTransfer_fields &meshtastic_FileTransfer_msg
|
||||
#define meshtastic_DirectoryListing_fields &meshtastic_DirectoryListing_msg
|
||||
#define meshtastic_I2CTransaction_fields &meshtastic_I2CTransaction_msg
|
||||
#define meshtastic_SdCardInfo_fields &meshtastic_SdCardInfo_msg
|
||||
#define meshtastic_I2CResult_fields &meshtastic_I2CResult_msg
|
||||
#define meshtastic_InterdeviceMessage_fields &meshtastic_InterdeviceMessage_msg
|
||||
|
||||
/* Maximum encoded size of messages (where known) */
|
||||
#define MESHTASTIC_MESHTASTIC_INTERDEVICE_PB_H_MAX_SIZE meshtastic_InterdeviceMessage_size
|
||||
#define meshtastic_InterdeviceMessage_size 1026
|
||||
#define meshtastic_SensorData_size 9
|
||||
#define meshtastic_DirectoryListing_size 4657
|
||||
#define meshtastic_FileTransfer_size 4646
|
||||
#define meshtastic_I2CResult_size 261
|
||||
#define meshtastic_I2CTransaction_size 271
|
||||
#define meshtastic_InterdeviceMessage_size 4666
|
||||
#define meshtastic_SdCardInfo_size 45
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* extern "C" */
|
||||
|
||||
@@ -211,7 +211,7 @@ extern const pb_msgdesc_t meshtastic_LocalModuleConfig_msg;
|
||||
|
||||
/* Maximum encoded size of messages (where known) */
|
||||
#define MESHTASTIC_MESHTASTIC_LOCALONLY_PB_H_MAX_SIZE meshtastic_LocalModuleConfig_size
|
||||
#define meshtastic_LocalConfig_size 757
|
||||
#define meshtastic_LocalConfig_size 759
|
||||
#define meshtastic_LocalModuleConfig_size 1126
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -412,6 +412,8 @@ typedef enum _meshtastic_FirmwareEdition {
|
||||
meshtastic_FirmwareEdition_BURNING_MAN = 18,
|
||||
/* Hamvention, the Dayton amateur radio convention */
|
||||
meshtastic_FirmwareEdition_HAMVENTION = 19,
|
||||
/* FAB, the international Fab Lab digital fabrication conference */
|
||||
meshtastic_FirmwareEdition_FAB = 20,
|
||||
/* Placeholder for DIY and unofficial events */
|
||||
meshtastic_FirmwareEdition_DIY_EDITION = 127
|
||||
} meshtastic_FirmwareEdition;
|
||||
@@ -1395,6 +1397,9 @@ typedef struct _meshtastic_DeviceMetadata {
|
||||
/* Bit field of boolean for excluded modules
|
||||
(bitwise OR of ExcludedModules) */
|
||||
uint32_t excluded_modules;
|
||||
/* Indicates whether this firmware build includes XEdDSA packet signature verification.
|
||||
This is a read-only capability and must be false when XEdDSA is not compiled in. */
|
||||
bool has_xeddsa;
|
||||
} meshtastic_DeviceMetadata;
|
||||
|
||||
/* A distinct set of legal modem presets shared by one or more LoRa regions.
|
||||
@@ -1743,7 +1748,7 @@ extern "C" {
|
||||
#define meshtastic_Compressed_init_default {_meshtastic_PortNum_MIN, {0, {0}}}
|
||||
#define meshtastic_NeighborInfo_init_default {0, 0, 0, 0, {meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default, meshtastic_Neighbor_init_default}}
|
||||
#define meshtastic_Neighbor_init_default {0, 0, 0, 0}
|
||||
#define meshtastic_DeviceMetadata_init_default {"", 0, 0, 0, 0, 0, _meshtastic_Config_DeviceConfig_Role_MIN, 0, _meshtastic_HardwareModel_MIN, 0, 0, 0}
|
||||
#define meshtastic_DeviceMetadata_init_default {"", 0, 0, 0, 0, 0, _meshtastic_Config_DeviceConfig_Role_MIN, 0, _meshtastic_HardwareModel_MIN, 0, 0, 0, 0}
|
||||
#define meshtastic_LoRaPresetGroup_init_default {0, {_meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN}, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0}
|
||||
#define meshtastic_LoRaRegionPresets_init_default {_meshtastic_Config_LoRaConfig_RegionCode_MIN, 0}
|
||||
#define meshtastic_LoRaRegionPresetMap_init_default {0, {meshtastic_LoRaPresetGroup_init_default, meshtastic_LoRaPresetGroup_init_default, meshtastic_LoRaPresetGroup_init_default, meshtastic_LoRaPresetGroup_init_default, meshtastic_LoRaPresetGroup_init_default, meshtastic_LoRaPresetGroup_init_default, meshtastic_LoRaPresetGroup_init_default, meshtastic_LoRaPresetGroup_init_default}, 0, {meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default, meshtastic_LoRaRegionPresets_init_default}}
|
||||
@@ -1782,7 +1787,7 @@ extern "C" {
|
||||
#define meshtastic_Compressed_init_zero {_meshtastic_PortNum_MIN, {0, {0}}}
|
||||
#define meshtastic_NeighborInfo_init_zero {0, 0, 0, 0, {meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero, meshtastic_Neighbor_init_zero}}
|
||||
#define meshtastic_Neighbor_init_zero {0, 0, 0, 0}
|
||||
#define meshtastic_DeviceMetadata_init_zero {"", 0, 0, 0, 0, 0, _meshtastic_Config_DeviceConfig_Role_MIN, 0, _meshtastic_HardwareModel_MIN, 0, 0, 0}
|
||||
#define meshtastic_DeviceMetadata_init_zero {"", 0, 0, 0, 0, 0, _meshtastic_Config_DeviceConfig_Role_MIN, 0, _meshtastic_HardwareModel_MIN, 0, 0, 0, 0}
|
||||
#define meshtastic_LoRaPresetGroup_init_zero {0, {_meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, _meshtastic_Config_LoRaConfig_ModemPreset_MIN}, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0}
|
||||
#define meshtastic_LoRaRegionPresets_init_zero {_meshtastic_Config_LoRaConfig_RegionCode_MIN, 0}
|
||||
#define meshtastic_LoRaRegionPresetMap_init_zero {0, {meshtastic_LoRaPresetGroup_init_zero, meshtastic_LoRaPresetGroup_init_zero, meshtastic_LoRaPresetGroup_init_zero, meshtastic_LoRaPresetGroup_init_zero, meshtastic_LoRaPresetGroup_init_zero, meshtastic_LoRaPresetGroup_init_zero, meshtastic_LoRaPresetGroup_init_zero, meshtastic_LoRaPresetGroup_init_zero}, 0, {meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero, meshtastic_LoRaRegionPresets_init_zero}}
|
||||
@@ -1985,6 +1990,7 @@ extern "C" {
|
||||
#define meshtastic_DeviceMetadata_hasRemoteHardware_tag 10
|
||||
#define meshtastic_DeviceMetadata_hasPKC_tag 11
|
||||
#define meshtastic_DeviceMetadata_excluded_modules_tag 12
|
||||
#define meshtastic_DeviceMetadata_has_xeddsa_tag 14
|
||||
#define meshtastic_LoRaPresetGroup_presets_tag 1
|
||||
#define meshtastic_LoRaPresetGroup_default_preset_tag 2
|
||||
#define meshtastic_LoRaPresetGroup_licensed_only_tag 3
|
||||
@@ -2403,7 +2409,8 @@ X(a, STATIC, SINGULAR, UINT32, position_flags, 8) \
|
||||
X(a, STATIC, SINGULAR, UENUM, hw_model, 9) \
|
||||
X(a, STATIC, SINGULAR, BOOL, hasRemoteHardware, 10) \
|
||||
X(a, STATIC, SINGULAR, BOOL, hasPKC, 11) \
|
||||
X(a, STATIC, SINGULAR, UINT32, excluded_modules, 12)
|
||||
X(a, STATIC, SINGULAR, UINT32, excluded_modules, 12) \
|
||||
X(a, STATIC, SINGULAR, BOOL, has_xeddsa, 14)
|
||||
#define meshtastic_DeviceMetadata_CALLBACK NULL
|
||||
#define meshtastic_DeviceMetadata_DEFAULT NULL
|
||||
|
||||
@@ -2552,7 +2559,7 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg;
|
||||
#define meshtastic_ClientNotification_size 482
|
||||
#define meshtastic_Compressed_size 239
|
||||
#define meshtastic_Data_size 335
|
||||
#define meshtastic_DeviceMetadata_size 54
|
||||
#define meshtastic_DeviceMetadata_size 56
|
||||
#define meshtastic_DuplicatedPublicKey_size 0
|
||||
#define meshtastic_FileInfo_size 236
|
||||
#define meshtastic_FromRadio_size 510
|
||||
|
||||
@@ -121,7 +121,9 @@ typedef enum _meshtastic_TelemetrySensorType {
|
||||
/* ICM-42607-P 6‑Axis IMU */
|
||||
meshtastic_TelemetrySensorType_ICM42607P = 53,
|
||||
/* SPA06 pressure and temperature */
|
||||
meshtastic_TelemetrySensorType_SPA06 = 54
|
||||
meshtastic_TelemetrySensorType_SPA06 = 54,
|
||||
/* HM330X PM SENSOR */
|
||||
meshtastic_TelemetrySensorType_HM330X = 55
|
||||
} meshtastic_TelemetrySensorType;
|
||||
|
||||
/* Struct definitions */
|
||||
@@ -502,8 +504,8 @@ extern "C" {
|
||||
|
||||
/* Helper constants for enums */
|
||||
#define _meshtastic_TelemetrySensorType_MIN meshtastic_TelemetrySensorType_SENSOR_UNSET
|
||||
#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_SPA06
|
||||
#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_SPA06+1))
|
||||
#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_HM330X
|
||||
#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_HM330X+1))
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -96,14 +96,6 @@ static void taskCreateCert(void *parameter)
|
||||
{
|
||||
prefs.begin("MeshtasticHTTPS", false);
|
||||
|
||||
#if 0
|
||||
// Delete the saved certs (used in debugging)
|
||||
LOG_DEBUG("Delete any saved SSL keys");
|
||||
// prefs.clear();
|
||||
prefs.remove("PK");
|
||||
prefs.remove("cert");
|
||||
#endif
|
||||
|
||||
LOG_INFO("Checking if we have a saved SSL Certificate");
|
||||
|
||||
size_t pkLen = prefs.getBytesLength("PK");
|
||||
|
||||
@@ -231,8 +231,6 @@ int callback_static_file(const struct _u_request *request, struct _u_response *r
|
||||
}
|
||||
}
|
||||
|
||||
static void handleWebResponse() {}
|
||||
|
||||
/*
|
||||
* Adapt the radioapi to the Webservice handleAPIv1ToRadio
|
||||
* Trigger : WebGui(SAVE)->WebServcice->phoneApi
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#include "configuration.h"
|
||||
#if HAS_WIFI
|
||||
#include "NodeDB.h"
|
||||
#include "RTC.h"
|
||||
#include "concurrency/Periodic.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "mesh/wifi/WiFiAPClient.h"
|
||||
|
||||
#include "main.h"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "meshUtils.h"
|
||||
#include "target_specific.h"
|
||||
#include <string.h>
|
||||
|
||||
/*
|
||||
@@ -79,6 +80,20 @@ bool memfll(const uint8_t *mem, uint8_t find, size_t numbytes)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool getMacAddrDeviceId(uint8_t *deviceId)
|
||||
{
|
||||
// Zero-initialized: getMacAddr() may return without writing (e.g. Portduino with no MAC
|
||||
// source), and we want that no-write case to hit the all-zeros guard below, not read garbage.
|
||||
uint8_t mac[6] = {0};
|
||||
getMacAddr(mac);
|
||||
if (memfll(mac, 0, sizeof(mac))) {
|
||||
LOG_WARN("MAC is all zeros, leaving device_id unset");
|
||||
return false;
|
||||
}
|
||||
memcpy(deviceId, mac, sizeof(mac));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool isOneOf(int item, int count, ...)
|
||||
{
|
||||
va_list args;
|
||||
|
||||
@@ -49,6 +49,10 @@ void printBytes(const char *label, const uint8_t *p, size_t numbytes);
|
||||
// is the memory region filled with a single character?
|
||||
bool memfll(const uint8_t *mem, uint8_t find, size_t numbytes);
|
||||
|
||||
// getDeviceId() fallback (see target_specific.h): copies the 6-byte factory MAC, or returns false
|
||||
// on an all-zero MAC so two blank devices don't collide on an all-zero id.
|
||||
bool getMacAddrDeviceId(uint8_t *deviceId);
|
||||
|
||||
bool isOneOf(int item, int count, ...);
|
||||
|
||||
const std::string vformat(const char *const zcFormat, ...);
|
||||
|
||||
+443
-31
@@ -1,14 +1,17 @@
|
||||
#include "AdminModule.h"
|
||||
#include "Channels.h"
|
||||
#include "DisplayFormatters.h"
|
||||
#include "HardwareRNG.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PositionPrecision.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "RTC.h"
|
||||
#include "SPILock.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "input/InputBroker.h"
|
||||
#include "meshUtils.h"
|
||||
#include <FSCommon.h>
|
||||
#include <Throttle.h>
|
||||
#include <ctype.h> // for better whitespace handling
|
||||
#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WIFI
|
||||
#include "MeshtasticOTA.h"
|
||||
@@ -26,6 +29,7 @@
|
||||
|
||||
#include "Default.h"
|
||||
#include "MeshRadio.h"
|
||||
#include "MessageStore.h"
|
||||
#include "RadioInterface.h"
|
||||
#include "TypeConversions.h"
|
||||
#include "mesh/RadioLibInterface.h"
|
||||
@@ -47,6 +51,9 @@
|
||||
#include "GPS.h"
|
||||
#endif
|
||||
|
||||
#include <RNG.h> // CryptRNG, the seeded CSPRNG used as fallback for the session passkey
|
||||
#include <Throttle.h> // rollover-safe elapsed-time checks for the session passkey
|
||||
|
||||
#if MESHTASTIC_EXCLUDE_GPS
|
||||
#include "modules/PositionModule.h"
|
||||
#endif
|
||||
@@ -124,9 +131,16 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
||||
}
|
||||
#endif
|
||||
meshtastic_Channel *ch = &channels.getByIndex(mp.channel);
|
||||
// Could tighten this up further by tracking the last public_key we went an AdminMessage request to
|
||||
// and only allowing responses from that remote.
|
||||
if (messageIsResponse(r)) {
|
||||
// Only accept a response from a remote we sent the matching request to. from == 0 is a
|
||||
// local client, which PhoneAPI has already gated.
|
||||
const pb_size_t moduleConfigTag = r->which_payload_variant == meshtastic_AdminMessage_get_module_config_response_tag
|
||||
? r->get_module_config_response.which_payload_variant
|
||||
: 0;
|
||||
if (mp.from != 0 && !responseIsSolicited(mp, r->which_payload_variant, moduleConfigTag)) {
|
||||
LOG_INFO("Ignore admin response from 0x%08x, no outstanding request", mp.from);
|
||||
return handled;
|
||||
}
|
||||
LOG_DEBUG("Allow admin response message");
|
||||
} else if (mp.from == 0) {
|
||||
// Local admin from a BLE/USB/TCP client. from == 0 cannot arrive from the
|
||||
@@ -461,6 +475,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
||||
LOG_INFO("Commit transaction for edited settings");
|
||||
hasOpenEditTransaction = false;
|
||||
saveChanges(SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS | SEGMENT_NODEDATABASE);
|
||||
flushChannelWarnings(); // one coalesced message for everything edited in this transaction
|
||||
break;
|
||||
}
|
||||
case meshtastic_AdminMessage_get_device_connection_status_request_tag: {
|
||||
@@ -470,8 +485,9 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
||||
}
|
||||
case meshtastic_AdminMessage_get_module_config_response_tag: {
|
||||
LOG_INFO("Client received a get_module_config response");
|
||||
if (fromOthers && r->get_module_config_response.which_payload_variant ==
|
||||
meshtastic_AdminMessage_ModuleConfigType_REMOTEHARDWARE_CONFIG) {
|
||||
// which_payload_variant is the ModuleConfig oneof tag, so compare against that tag, not the
|
||||
// AdminMessage ModuleConfigType enum (whose REMOTEHARDWARE value is a different number).
|
||||
if (fromOthers && r->get_module_config_response.which_payload_variant == meshtastic_ModuleConfig_remote_hardware_tag) {
|
||||
handleGetModuleConfigResponse(mp, r);
|
||||
}
|
||||
break;
|
||||
@@ -522,7 +538,14 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
||||
if (node != NULL) {
|
||||
if (nodeDB->setProtectedFlag(node, NODEINFO_BITFIELD_IS_IGNORED_MASK, true)) {
|
||||
nodeDB->eraseNodeSatellites(node->num);
|
||||
#if HAS_SCREEN || defined(MESHTASTIC_INCLUDE_NICHE_GRAPHICS)
|
||||
messageStore.deleteAllMessagesFromNode(node->num);
|
||||
#endif
|
||||
saveChanges(SEGMENT_NODEDATABASE, false);
|
||||
#if HAS_SCREEN
|
||||
if (screen)
|
||||
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
|
||||
#endif
|
||||
} else if (mp.from == 0) { // local request from the phone - tell the user why it didn't take
|
||||
sendWarning(NodeDB::PROTECTED_CAP_WARN_FMT, "ignore", r->set_ignored_node, MAX_NUM_NODES - 2);
|
||||
} else {
|
||||
@@ -755,7 +778,7 @@ void AdminModule::handleSetOwner(const meshtastic_User &o)
|
||||
changed = 1;
|
||||
owner.is_licensed = o.is_licensed;
|
||||
if (channels.ensureLicensedOperation()) {
|
||||
sendWarning(licensedModeMessage);
|
||||
warnLicensedMode();
|
||||
}
|
||||
}
|
||||
if (owner.has_is_unmessagable != o.has_is_unmessagable ||
|
||||
@@ -771,12 +794,35 @@ void AdminModule::handleSetOwner(const meshtastic_User &o)
|
||||
}
|
||||
}
|
||||
|
||||
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && \
|
||||
!MESHTASTIC_EXCLUDE_ACCELEROMETER
|
||||
// Shared by double-tap-as-button (device) and wake-on-motion (display). Start on either flag's
|
||||
// off->on edge; stop on the on->off edge once neither needs it (disable() clears `enabled` so a
|
||||
// later re-enable restarts). Skip the stop if the sensor also drives the compass, else the heading
|
||||
// freezes until reboot. wasOn/nowOn = old/new of the changed flag; otherFeatureOn = the other flag.
|
||||
static void reconcileAccelerometerThread(bool wasOn, bool nowOn, bool otherFeatureOn)
|
||||
{
|
||||
if (!accelerometerThread) // null unless a sensor was detected at boot
|
||||
return;
|
||||
|
||||
if (!wasOn && nowOn && accelerometerThread->enabled == false) {
|
||||
accelerometerThread->enabled = true;
|
||||
accelerometerThread->start();
|
||||
} else if (wasOn && !nowOn && !otherFeatureOn && accelerometerThread->enabled == true &&
|
||||
!accelerometerThread->providesHeading()) {
|
||||
accelerometerThread->disable();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers)
|
||||
{
|
||||
auto changes = SEGMENT_CONFIG;
|
||||
auto existingRole = config.device.role;
|
||||
bool isRegionUnset = (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET);
|
||||
bool requiresReboot = true;
|
||||
bool loraPresetWarnPending = false;
|
||||
meshtastic_Config_LoRaConfig pendingOldLora = {}, pendingNewLora = {};
|
||||
|
||||
switch (c.which_payload_variant) {
|
||||
case meshtastic_Config_device_tag: {
|
||||
@@ -784,12 +830,8 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers)
|
||||
config.has_device = true;
|
||||
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && \
|
||||
!MESHTASTIC_EXCLUDE_ACCELEROMETER
|
||||
if (config.device.double_tap_as_button_press == false && c.payload_variant.device.double_tap_as_button_press == true &&
|
||||
accelerometerThread->enabled == false) {
|
||||
config.device.double_tap_as_button_press = c.payload_variant.device.double_tap_as_button_press;
|
||||
accelerometerThread->enabled = true;
|
||||
accelerometerThread->start();
|
||||
}
|
||||
reconcileAccelerometerThread(config.device.double_tap_as_button_press,
|
||||
c.payload_variant.device.double_tap_as_button_press, config.display.wake_on_tap_or_motion);
|
||||
#endif
|
||||
if (config.device.button_gpio == c.payload_variant.device.button_gpio &&
|
||||
config.device.buzzer_gpio == c.payload_variant.device.buzzer_gpio &&
|
||||
@@ -888,12 +930,8 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers)
|
||||
}
|
||||
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && \
|
||||
!MESHTASTIC_EXCLUDE_ACCELEROMETER
|
||||
if (config.display.wake_on_tap_or_motion == false && c.payload_variant.display.wake_on_tap_or_motion == true &&
|
||||
accelerometerThread->enabled == false) {
|
||||
config.display.wake_on_tap_or_motion = c.payload_variant.display.wake_on_tap_or_motion;
|
||||
accelerometerThread->enabled = true;
|
||||
accelerometerThread->start();
|
||||
}
|
||||
reconcileAccelerometerThread(config.display.wake_on_tap_or_motion, c.payload_variant.display.wake_on_tap_or_motion,
|
||||
config.device.double_tap_as_button_press);
|
||||
#endif
|
||||
config.display = c.payload_variant.display;
|
||||
break;
|
||||
@@ -1041,6 +1079,11 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers)
|
||||
#endif
|
||||
|
||||
config.lora = validatedLora; // Finally, return the validated config back to the main config
|
||||
if (validatedLora.modem_preset != oldLoraConfig.modem_preset) {
|
||||
pendingOldLora = oldLoraConfig;
|
||||
pendingNewLora = validatedLora;
|
||||
loraPresetWarnPending = true;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -1103,6 +1146,11 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers)
|
||||
} // end of switch case which_payload_variant
|
||||
|
||||
saveChanges(changes, requiresReboot);
|
||||
if (loraPresetWarnPending)
|
||||
warnOnLoraPresetChange(pendingOldLora, pendingNewLora);
|
||||
// Inside an edit transaction the queued warnings are flushed once at commit; otherwise emit now.
|
||||
if (!hasOpenEditTransaction)
|
||||
flushChannelWarnings();
|
||||
} // end of handleSetConfig
|
||||
|
||||
bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c)
|
||||
@@ -1308,7 +1356,7 @@ void AdminModule::handleSetChannel(const meshtastic_Channel &cc)
|
||||
{
|
||||
channels.setChannel(cc);
|
||||
if (channels.ensureLicensedOperation()) {
|
||||
sendWarning(licensedModeMessage);
|
||||
warnLicensedMode();
|
||||
}
|
||||
// Refresh derived state (primaryIndex in particular) BEFORE the precision clamp below. usesPublicKey()
|
||||
// resolves a secondary channel's key against the primary, so it must see the post-update primaryIndex;
|
||||
@@ -1331,6 +1379,10 @@ void AdminModule::handleSetChannel(const meshtastic_Channel &cc)
|
||||
if (clamped)
|
||||
sendWarning(publicChannelPrecisionMessage);
|
||||
saveChanges(SEGMENT_CHANNELS, false);
|
||||
warnOnChannelSet(channels.getByIndex(cc.index)); // passes the saved channel
|
||||
// Inside an edit transaction the queued warnings are flushed once at commit; otherwise emit now.
|
||||
if (!hasOpenEditTransaction)
|
||||
flushChannelWarnings();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1400,6 +1452,15 @@ void AdminModule::handleGetConfig(const meshtastic_MeshPacket &req, const uint32
|
||||
LOG_INFO("Get config: Security");
|
||||
res.get_config_response.which_payload_variant = meshtastic_Config_security_tag;
|
||||
res.get_config_response.payload_variant.security = config.security;
|
||||
// The device identity private key is backup material for the local owner only. A local
|
||||
// admin client sets from == 0 (BLE/USB/TCP); never return the key to a remote requester,
|
||||
// even an authorized one, since it would travel over the air. public_key/admin_key are
|
||||
// public and stay put.
|
||||
if (req.from != 0) {
|
||||
auto &sec = res.get_config_response.payload_variant.security;
|
||||
memset(sec.private_key.bytes, 0, sizeof(sec.private_key.bytes));
|
||||
sec.private_key.size = 0;
|
||||
}
|
||||
break;
|
||||
case meshtastic_AdminMessage_ConfigType_SESSIONKEY_CONFIG:
|
||||
LOG_INFO("Get config: Sessionkey");
|
||||
@@ -1746,7 +1807,7 @@ void AdminModule::handleSetHamMode(const meshtastic_HamParameters &p)
|
||||
// Remove PSK of primary channel for plaintext amateur usage
|
||||
|
||||
if (channels.ensureLicensedOperation()) {
|
||||
sendWarning(licensedModeMessage);
|
||||
warnLicensedMode();
|
||||
}
|
||||
channels.onConfigChanged();
|
||||
|
||||
@@ -1766,11 +1827,15 @@ AdminModule::AdminModule() : ProtobufModule("Admin", meshtastic_PortNum_ADMIN_AP
|
||||
|
||||
void AdminModule::setPassKey(meshtastic_AdminMessage *res)
|
||||
{
|
||||
if (session_time == 0 || millis() / 1000 > session_time + 150) {
|
||||
for (int i = 0; i < 8; i++) {
|
||||
session_passkey[i] = random();
|
||||
}
|
||||
session_time = millis() / 1000;
|
||||
// Regenerate once there is no session yet or the current key is older than 150s. session_time
|
||||
// holds millis(); the Throttle check is rollover-safe, unlike the previous seconds comparison.
|
||||
if (!sessionPasskeyValid || !Throttle::isWithinTimespanMs(session_time, 150 * 1000UL)) {
|
||||
// Session passkey authenticates admin replies, so it must be unpredictable: prefer the
|
||||
// hardware RNG, falling back to the seeded CSPRNG only when no hardware source exists.
|
||||
if (!HardwareRNG::fill(session_passkey, sizeof(session_passkey)))
|
||||
CryptRNG.rand(session_passkey, sizeof(session_passkey));
|
||||
session_time = millis();
|
||||
sessionPasskeyValid = true;
|
||||
}
|
||||
memcpy(res->session_passkey.bytes, session_passkey, 8);
|
||||
res->session_passkey.size = 8;
|
||||
@@ -1783,7 +1848,8 @@ bool AdminModule::checkPassKey(meshtastic_AdminMessage *res)
|
||||
{ // check that the key in the packet is still valid
|
||||
printBytes("Incoming session key: ", res->session_passkey.bytes, 8);
|
||||
printBytes("Expected session key: ", session_passkey, 8);
|
||||
return (session_time + 300 > millis() / 1000 && res->session_passkey.size == 8 &&
|
||||
// Key is valid for 300s from issue; sessionPasskeyValid guards an unissued session.
|
||||
return (sessionPasskeyValid && Throttle::isWithinTimespanMs(session_time, 300 * 1000UL) && res->session_passkey.size == 8 &&
|
||||
memcmp(res->session_passkey.bytes, session_passkey, 8) == 0);
|
||||
}
|
||||
|
||||
@@ -1804,6 +1870,107 @@ bool AdminModule::messageIsResponse(const meshtastic_AdminMessage *r)
|
||||
return false;
|
||||
}
|
||||
|
||||
// The response variant that answers a getter request, or 0 if the request has none.
|
||||
static pb_size_t adminResponseForRequest(pb_size_t requestVariant)
|
||||
{
|
||||
switch (requestVariant) {
|
||||
case meshtastic_AdminMessage_get_channel_request_tag:
|
||||
return meshtastic_AdminMessage_get_channel_response_tag;
|
||||
case meshtastic_AdminMessage_get_owner_request_tag:
|
||||
return meshtastic_AdminMessage_get_owner_response_tag;
|
||||
case meshtastic_AdminMessage_get_config_request_tag:
|
||||
return meshtastic_AdminMessage_get_config_response_tag;
|
||||
case meshtastic_AdminMessage_get_module_config_request_tag:
|
||||
return meshtastic_AdminMessage_get_module_config_response_tag;
|
||||
case meshtastic_AdminMessage_get_canned_message_module_messages_request_tag:
|
||||
return meshtastic_AdminMessage_get_canned_message_module_messages_response_tag;
|
||||
case meshtastic_AdminMessage_get_device_metadata_request_tag:
|
||||
return meshtastic_AdminMessage_get_device_metadata_response_tag;
|
||||
case meshtastic_AdminMessage_get_ringtone_request_tag:
|
||||
return meshtastic_AdminMessage_get_ringtone_response_tag;
|
||||
case meshtastic_AdminMessage_get_device_connection_status_request_tag:
|
||||
return meshtastic_AdminMessage_get_device_connection_status_response_tag;
|
||||
case meshtastic_AdminMessage_get_node_remote_hardware_pins_request_tag:
|
||||
return meshtastic_AdminMessage_get_node_remote_hardware_pins_response_tag;
|
||||
case meshtastic_AdminMessage_get_ui_config_request_tag:
|
||||
return meshtastic_AdminMessage_get_ui_config_response_tag;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
void AdminModule::noteOutgoingAdminRequest(const meshtastic_MeshPacket &p)
|
||||
{
|
||||
if (p.which_payload_variant != meshtastic_MeshPacket_decoded_tag || p.decoded.portnum != meshtastic_PortNum_ADMIN_APP)
|
||||
return;
|
||||
// Local admin is answered in-process, and a broadcast is never a request to one remote.
|
||||
if (p.to == 0 || isBroadcast(p.to) || p.to == nodeDB->getNodeNum())
|
||||
return;
|
||||
|
||||
meshtastic_AdminMessage admin = meshtastic_AdminMessage_init_zero;
|
||||
if (!pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &meshtastic_AdminMessage_msg, &admin))
|
||||
return;
|
||||
const pb_size_t responseVariant = adminResponseForRequest(admin.which_payload_variant);
|
||||
if (!responseVariant)
|
||||
return; // not a getter whose response we can pair
|
||||
|
||||
const bool keyValid = p.pki_encrypted && p.public_key.size == 32;
|
||||
|
||||
// One entry per request (a client sends N indexed get_channel requests, each answered once, so
|
||||
// entries must not merge). Free slot, else evict the oldest by rollover-safe elapsed time.
|
||||
OutstandingAdminRequest *slot = nullptr;
|
||||
for (auto &o : outstandingAdminRequests)
|
||||
if (o.to == 0) {
|
||||
slot = &o;
|
||||
break;
|
||||
}
|
||||
if (!slot) {
|
||||
const uint32_t now = millis();
|
||||
slot = &outstandingAdminRequests[0];
|
||||
for (auto &o : outstandingAdminRequests)
|
||||
if ((uint32_t)(now - o.sentAtMs) > (uint32_t)(now - slot->sentAtMs))
|
||||
slot = &o;
|
||||
}
|
||||
|
||||
slot->to = p.to;
|
||||
slot->sentAtMs = millis();
|
||||
slot->expectedResponse = responseVariant;
|
||||
slot->moduleConfigType = admin.which_payload_variant == meshtastic_AdminMessage_get_module_config_request_tag
|
||||
? (uint8_t)admin.get_module_config_request
|
||||
: 0;
|
||||
slot->keyValid = keyValid;
|
||||
if (keyValid)
|
||||
memcpy(slot->key, p.public_key.bytes, 32);
|
||||
else
|
||||
memset(slot->key, 0, 32);
|
||||
LOG_DEBUG("Admin request sent to 0x%08x, expecting its response", p.to);
|
||||
}
|
||||
|
||||
bool AdminModule::responseIsSolicited(const meshtastic_MeshPacket &mp, pb_size_t responseVariant, pb_size_t moduleConfigTag)
|
||||
{
|
||||
// Scan every entry: several requests for the same variant may differ only in pinning, and an
|
||||
// unpinned one still authorizes the response even if a pinned one does not.
|
||||
for (auto &o : outstandingAdminRequests) {
|
||||
if (o.to != mp.from || o.expectedResponse != responseVariant)
|
||||
continue;
|
||||
if (!Throttle::isWithinTimespanMs(o.sentAtMs, kOutstandingAdminRequestMs)) {
|
||||
o.to = 0; // lapsed; free the slot and keep looking for another live match
|
||||
continue;
|
||||
}
|
||||
// A request pinned to a PKC key must be answered over PKC by that same key.
|
||||
if (o.keyValid && (!mp.pki_encrypted || mp.public_key.size != 32 || memcmp(mp.public_key.bytes, o.key, 32) != 0))
|
||||
continue;
|
||||
// remote_hardware is the only module config that mutates state (the pin table), so require
|
||||
// it to answer a request for that exact subtype, not just any get_module_config_request.
|
||||
if (moduleConfigTag == meshtastic_ModuleConfig_remote_hardware_tag &&
|
||||
o.moduleConfigType != meshtastic_AdminMessage_ModuleConfigType_REMOTEHARDWARE_CONFIG)
|
||||
continue;
|
||||
o.to = 0; // consume: one request authorizes one response, no replay within the window
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AdminModule::messageIsRequest(const meshtastic_AdminMessage *r)
|
||||
{
|
||||
if (r->which_payload_variant == meshtastic_AdminMessage_get_channel_request_tag ||
|
||||
@@ -1884,14 +2051,259 @@ void AdminModule::sendWarningAndLog(const char *format, ...)
|
||||
vsnprintf(buf, sizeof(buf), format, args);
|
||||
va_end(args);
|
||||
|
||||
LOG_WARN(buf);
|
||||
// 2. Call sendWarning
|
||||
// SECURITY NOTE: We pass "%s", buf instead of just 'buf'.
|
||||
// If 'buf' contained a % symbol (e.g. "Battery 50%"), passing it directly
|
||||
// would crash sendWarning. "%s" treats it purely as text.
|
||||
// SECURITY NOTE: Both LOG_WARN and sendWarning are printf-style, so we pass
|
||||
// "%s", buf rather than 'buf' directly. If 'buf' contained a % symbol (e.g. a
|
||||
// user-supplied channel name like "50%"), passing it as the format string would
|
||||
// read bogus varargs and could crash. "%s" treats it purely as text.
|
||||
LOG_WARN("%s", buf);
|
||||
sendWarning("%s", buf);
|
||||
}
|
||||
|
||||
// Strip spaces and fold to lowercase for loose preset-name comparison.
|
||||
static void normalizePresetName(const char *src, char *dst, size_t dstLen)
|
||||
{
|
||||
size_t j = 0;
|
||||
for (size_t i = 0; src[i] && j + 1 < dstLen; i++) {
|
||||
if (src[i] != ' ')
|
||||
dst[j++] = (char)tolower((unsigned char)src[i]);
|
||||
}
|
||||
dst[j] = '\0';
|
||||
}
|
||||
|
||||
// Record one channel-configuration warning. The first message is kept verbatim in case it
|
||||
// turns out to be the only one; nameIssue/pskIssue and the channel bitmask feed the catch-all
|
||||
// wording if more than one channel ends up flagged. flushChannelWarnings() emits the result.
|
||||
void AdminModule::queueChannelWarning(uint8_t channelIndex, bool nameIssue, bool pskIssue, const char *format, ...)
|
||||
{
|
||||
if (pendingWarningCount == 0) {
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
vsnprintf(pendingWarningText, sizeof(pendingWarningText), format, args);
|
||||
va_end(args);
|
||||
}
|
||||
if (channelIndex < MAX_NUM_CHANNELS)
|
||||
pendingWarningChannels |= (1u << channelIndex);
|
||||
pendingWarningNameIssue |= nameIssue;
|
||||
pendingWarningPskIssue |= pskIssue;
|
||||
pendingWarningCount++;
|
||||
}
|
||||
|
||||
// Queue the fixed "licensed mode activated" notice, deferring it to commit during an edit
|
||||
// transaction so repeated triggers collapse to a single message.
|
||||
void AdminModule::warnLicensedMode()
|
||||
{
|
||||
if (hasOpenEditTransaction)
|
||||
pendingLicenseWarning = true;
|
||||
else
|
||||
sendWarning(licensedModeMessage);
|
||||
}
|
||||
|
||||
// Emit the coalesced channel warning(s): nothing if none queued, the lone message verbatim if
|
||||
// exactly one, otherwise a single catch-all naming every flagged channel. The licensed-mode
|
||||
// notice, if queued, is emitted once alongside. Always resets state.
|
||||
void AdminModule::flushChannelWarnings()
|
||||
{
|
||||
if (pendingLicenseWarning)
|
||||
sendWarning(licensedModeMessage);
|
||||
|
||||
if (pendingWarningCount == 1) {
|
||||
sendWarningAndLog("%s", pendingWarningText);
|
||||
} else if (pendingWarningCount > 1) {
|
||||
char list[48] = {};
|
||||
for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) {
|
||||
if (!(pendingWarningChannels & (1u << i)))
|
||||
continue;
|
||||
char num[8];
|
||||
snprintf(num, sizeof(num), "%s%u", *list ? ", " : "", i);
|
||||
strncat(list, num, sizeof(list) - strlen(list) - 1);
|
||||
}
|
||||
if (pendingWarningNameIssue && pendingWarningPskIssue)
|
||||
sendWarningAndLog("There may be name and PSK issues on channels %s", list); // max 60 bytes
|
||||
else if (pendingWarningNameIssue)
|
||||
sendWarningAndLog("There may be name issues on channels %s", list); // max 52 bytes
|
||||
else
|
||||
sendWarningAndLog("There may be PSK issues on channels %s", list); // max 51 bytes
|
||||
}
|
||||
pendingWarningText[0] = '\0';
|
||||
pendingWarningChannels = 0;
|
||||
pendingWarningCount = 0;
|
||||
pendingWarningNameIssue = false;
|
||||
pendingWarningPskIssue = false;
|
||||
pendingLicenseWarning = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Emit client warnings for common misconfigurations on a newly committed channel.
|
||||
*
|
||||
* Called from handleSetChannel() after the channel has been saved. The following checks
|
||||
* are performed:
|
||||
*
|
||||
* - Blank PSK (size == 0) on a non-licensed device: the channel has no encryption.
|
||||
* - Blank name with a non-default key (not the default key, 0x01): the name will auto-resolve to
|
||||
* the current modem preset name, but the key mismatch means other preset nodes cannot
|
||||
* decode traffic on this channel.
|
||||
* - Named channel whose name is a case/space variant of the current modem preset: the
|
||||
* explicit name prevents auto-resolution; client should clear it.
|
||||
* - Same variant match but PSK is not the default key: looks like the preset channel but
|
||||
* is incompatible with nodes using the preset's default key.
|
||||
*
|
||||
* @param cc The channel that was written.
|
||||
*/
|
||||
void AdminModule::warnOnChannelSet(const meshtastic_Channel &cc)
|
||||
{
|
||||
if (cc.role == meshtastic_Channel_Role_DISABLED || !cc.has_settings) // don't check unused channels
|
||||
return;
|
||||
|
||||
bool blankPsk = (cc.settings.psk.size == 0 && !owner.is_licensed);
|
||||
|
||||
if (!config.lora.use_preset) { // custom or unset preset can mistype things too
|
||||
const char *mistypePreset = nullptr;
|
||||
if (*cc.settings.name) {
|
||||
char normChan[32];
|
||||
normalizePresetName(cc.settings.name, normChan, sizeof(normChan));
|
||||
for (auto preset = _meshtastic_Config_LoRaConfig_ModemPreset_MIN;
|
||||
preset <= _meshtastic_Config_LoRaConfig_ModemPreset_MAX;
|
||||
preset = (meshtastic_Config_LoRaConfig_ModemPreset)(preset + 1)) {
|
||||
const char *name = DisplayFormatters::getModemPresetDisplayName(preset, false, true);
|
||||
if (strcmp(name, "Invalid") == 0)
|
||||
continue; // skip preset slots without a real display name
|
||||
if (strcmp(cc.settings.name, name) == 0)
|
||||
break; // exact match - not a mistype, no warning
|
||||
char normPreset[32];
|
||||
normalizePresetName(name, normPreset, sizeof(normPreset));
|
||||
if (strcmp(normChan, normPreset) == 0) {
|
||||
mistypePreset = name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// At most one warning: collapse blank-PSK + mistype into a single catch-all.
|
||||
if (blankPsk && mistypePreset)
|
||||
queueChannelWarning(cc.index, true, true, "There may be name and PSK issues on channel %d", cc.index);
|
||||
else if (mistypePreset)
|
||||
queueChannelWarning(cc.index, true, false,
|
||||
"Channel %d name '%s' looks like a mistype of '%s' - "
|
||||
"make sure to type it exactly!", // max 90 bytes
|
||||
cc.index, cc.settings.name, mistypePreset);
|
||||
else if (blankPsk)
|
||||
queueChannelWarning(cc.index, false, true,
|
||||
"Channel %d '%s' has a blank PSK (no encryption) instead of default key", // max 100 bytes
|
||||
cc.index, cc.settings.name);
|
||||
return;
|
||||
}
|
||||
|
||||
const char *presetName = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, true);
|
||||
bool isDefaultKey = (cc.settings.psk.size == 1 && cc.settings.psk.bytes[0] == 0x01);
|
||||
char normPreset[32], normChan[32]; // max size is 11 plus nul, but allow for future expansion
|
||||
normalizePresetName(presetName, normPreset, sizeof(normPreset));
|
||||
normalizePresetName(cc.settings.name, normChan, sizeof(normChan));
|
||||
|
||||
if (!*cc.settings.name) {
|
||||
// Blank name resolves to the preset name - A and B are mutually exclusive (psk.size can't be both 0 and >0)
|
||||
if (blankPsk)
|
||||
queueChannelWarning(cc.index, false, true,
|
||||
"Channel %d '%s' has a blank PSK (no encryption) instead of default key", // max 100 bytes
|
||||
cc.index, cc.settings.name);
|
||||
else if (!isDefaultKey && cc.settings.psk.size > 0)
|
||||
queueChannelWarning(cc.index, false, true,
|
||||
"Channel %d will resolve to preset '%s' but uses a non-default key - "
|
||||
"default-key nodes can't decode it.", // max 102 bytes
|
||||
cc.index, presetName);
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcmp(normChan, normPreset) != 0) { // name unrelated to preset
|
||||
if (blankPsk)
|
||||
queueChannelWarning(cc.index, false, true,
|
||||
"Channel %d '%s' has a blank PSK (no encryption) instead of default key", // max 100 bytes
|
||||
cc.index, cc.settings.name);
|
||||
return;
|
||||
}
|
||||
|
||||
bool variantName = (strcmp(cc.settings.name, presetName) != 0);
|
||||
bool keyMismatch = (!isDefaultKey && cc.settings.psk.size > 0);
|
||||
int issues = (int)blankPsk + (int)variantName + (int)keyMismatch;
|
||||
|
||||
if (issues > 1) {
|
||||
bool hasNameIssue = variantName;
|
||||
bool hasPskIssue = blankPsk || keyMismatch;
|
||||
if (hasNameIssue && hasPskIssue)
|
||||
queueChannelWarning(cc.index, true, true, "There may be name and PSK issues on channel %d", cc.index);
|
||||
else if (hasNameIssue)
|
||||
queueChannelWarning(cc.index, true, false, "There may be name issues on channel %d", cc.index);
|
||||
else
|
||||
queueChannelWarning(cc.index, false, true, "There may be PSK issues on channel %d", cc.index);
|
||||
return;
|
||||
}
|
||||
|
||||
if (blankPsk)
|
||||
queueChannelWarning(cc.index, false, true,
|
||||
"Channel %d '%s' has a blank PSK (no encryption) instead of default key", // max 100 bytes
|
||||
cc.index, cc.settings.name);
|
||||
if (variantName)
|
||||
queueChannelWarning(cc.index, true, false,
|
||||
"Channel %d name '%s' looks like a mistype of '%s' - "
|
||||
"clear the name to use the preset name automatically.", // max 113 bytes
|
||||
cc.index, cc.settings.name, presetName);
|
||||
if (keyMismatch)
|
||||
queueChannelWarning(cc.index, false, true,
|
||||
"Channel %d '%s' matches preset '%s' but uses a non-default key - "
|
||||
"default-key nodes can't decode it.", // max 108 bytes
|
||||
cc.index, cc.settings.name, presetName);
|
||||
} // warnOnChannelSet
|
||||
|
||||
/**
|
||||
* @brief Scan all channels for preset-name conflicts after a modem preset change is committed.
|
||||
*
|
||||
* Called from handleSetConfig() after the LoRa config has been saved, and only when
|
||||
* modem_preset actually changed (rejected configs are never passed here). For every
|
||||
* named, non-disabled channel two checks are performed:
|
||||
*
|
||||
* - Name matches the *old* preset (case-insensitive, spaces stripped): the channel
|
||||
* was likely tracking the previous preset; the user should rename it if it should
|
||||
* follow the new one.
|
||||
* - Name matches the *new* preset: the channel name collides with the auto-generated
|
||||
* preset name but won't resolve automatically because the name is set explicitly.
|
||||
*
|
||||
* No-ops if the new config does not use a preset.
|
||||
*
|
||||
* @param oldLora LoRa config before the update.
|
||||
* @param newLora LoRa config after the update.
|
||||
*/
|
||||
void AdminModule::warnOnLoraPresetChange(const meshtastic_Config_LoRaConfig &oldLora, const meshtastic_Config_LoRaConfig &newLora)
|
||||
{
|
||||
if (!newLora.use_preset || newLora.modem_preset == oldLora.modem_preset)
|
||||
return;
|
||||
|
||||
char normOld[32] = {}, normNew[32];
|
||||
if (oldLora.use_preset) {
|
||||
const char *oldName = DisplayFormatters::getModemPresetDisplayName(oldLora.modem_preset, false, true);
|
||||
normalizePresetName(oldName, normOld, sizeof(normOld));
|
||||
}
|
||||
const char *newName = DisplayFormatters::getModemPresetDisplayName(newLora.modem_preset, false, true);
|
||||
normalizePresetName(newName, normNew, sizeof(normNew));
|
||||
|
||||
// Queue one (name) warning per affected channel; flushChannelWarnings() collapses them
|
||||
// into a single message - either the lone warning verbatim or a catch-all listing indices.
|
||||
for (int i = 0; i < channels.getNumChannels(); i++) {
|
||||
const meshtastic_Channel &ch = channels.getByIndex(i);
|
||||
if (ch.role == meshtastic_Channel_Role_DISABLED || !ch.has_settings || !*ch.settings.name)
|
||||
continue;
|
||||
char normChan[32];
|
||||
normalizePresetName(ch.settings.name, normChan, sizeof(normChan));
|
||||
if (*normOld && strcmp(normChan, normOld) == 0)
|
||||
queueChannelWarning(i, true, false,
|
||||
"Channel %d name '%s' matches the old preset. "
|
||||
"Rename it manually if it should track the new preset.", // max 98 bytes
|
||||
i, ch.settings.name);
|
||||
else if (strcmp(normChan, normNew) == 0)
|
||||
queueChannelWarning(i, true, false,
|
||||
"Channel %d '%s' looks like preset '%s' but won't auto-resolve - "
|
||||
"clear the name to fix it.", // max 98 bytes
|
||||
i, ch.settings.name, newName);
|
||||
}
|
||||
} // warnOnLoraPresetChange
|
||||
|
||||
void disableBluetooth()
|
||||
{
|
||||
#if HAS_BLUETOOTH
|
||||
|
||||
@@ -41,7 +41,8 @@ class AdminModule : public ProtobufModule<meshtastic_AdminMessage>, public Obser
|
||||
bool hasOpenEditTransaction = false;
|
||||
|
||||
uint8_t session_passkey[8] = {0};
|
||||
uint session_time = 0;
|
||||
uint32_t session_time = 0; // millis() when the current session passkey was issued
|
||||
bool sessionPasskeyValid = false; // separate flag: millis() 0 at boot is a valid issue time
|
||||
|
||||
void saveChanges(int saveWhat, bool shouldReboot = true);
|
||||
|
||||
@@ -77,7 +78,29 @@ class AdminModule : public ProtobufModule<meshtastic_AdminMessage>, public Obser
|
||||
public:
|
||||
void handleSetHamMode(const meshtastic_HamParameters &req);
|
||||
|
||||
/// Note an admin request leaving this node for a remote, so that remote's response is
|
||||
/// accepted. Called from the client-to-mesh path (MeshService::handleToRadio).
|
||||
void noteOutgoingAdminRequest(const meshtastic_MeshPacket &p);
|
||||
|
||||
private:
|
||||
// An admin response has no session passkey and its sender need not hold an admin key, so a
|
||||
// request we sent is the only thing vouching for it. Track each request independently (its own
|
||||
// expiry and pinned key) so a later one can't extend or relax an earlier one.
|
||||
static constexpr size_t kOutstandingAdminRequests = 8;
|
||||
static constexpr uint32_t kOutstandingAdminRequestMs = 300 * 1000; // same window as the session passkey
|
||||
struct OutstandingAdminRequest {
|
||||
NodeNum to; // 0 = free slot
|
||||
uint32_t sentAtMs; // millis() when this request went out
|
||||
pb_size_t expectedResponse; // the one response variant this request authorizes
|
||||
uint8_t moduleConfigType; // for get_module_config_request: which ModuleConfigType we asked
|
||||
uint8_t key[32]; // pinned destination key when the request went out over PKC
|
||||
bool keyValid;
|
||||
};
|
||||
OutstandingAdminRequest outstandingAdminRequests[kOutstandingAdminRequests] = {};
|
||||
|
||||
/// Whether a response (variant responseVariant, module-config subtype moduleConfigTag or 0)
|
||||
/// from mp.from answers a request we sent; consumes the matched request so it can't be replayed.
|
||||
bool responseIsSolicited(const meshtastic_MeshPacket &mp, pb_size_t responseVariant, pb_size_t moduleConfigTag);
|
||||
void handleStoreDeviceUIConfig(const meshtastic_DeviceUIConfig &uicfg);
|
||||
void handleSendInputEvent(const meshtastic_AdminMessage_InputEvent &inputEvent);
|
||||
void reboot(int32_t seconds);
|
||||
@@ -89,6 +112,27 @@ class AdminModule : public ProtobufModule<meshtastic_AdminMessage>, public Obser
|
||||
bool messageIsRequest(const meshtastic_AdminMessage *r);
|
||||
void sendWarning(const char *format, ...) __attribute__((format(printf, 2, 3)));
|
||||
void sendWarningAndLog(const char *format, ...) __attribute__((format(printf, 2, 3)));
|
||||
void warnOnLoraPresetChange(const meshtastic_Config_LoRaConfig &oldLora, const meshtastic_Config_LoRaConfig &newLora);
|
||||
void warnOnChannelSet(const meshtastic_Channel &cc);
|
||||
|
||||
// Channel-configuration warnings are coalesced into a single client notification.
|
||||
// queueChannelWarning() records one warning for a channel; while an edit transaction
|
||||
// is open (begin/commit_edit_settings) the warnings accumulate and flushChannelWarnings()
|
||||
// emits one combined message at commit. Outside a transaction the caller flushes
|
||||
// immediately, so a single channel/preset edit still produces a single message.
|
||||
void queueChannelWarning(uint8_t channelIndex, bool nameIssue, bool pskIssue, const char *format, ...)
|
||||
__attribute__((format(printf, 5, 6)));
|
||||
// Emit the "licensed mode activated" notice, deferring to commit during an edit transaction
|
||||
// so repeated triggers (e.g. owner + several channels) produce a single message.
|
||||
void warnLicensedMode();
|
||||
void flushChannelWarnings();
|
||||
|
||||
char pendingWarningText[250] = {}; // the lone queued message, used verbatim when only one fired
|
||||
uint32_t pendingWarningChannels = 0; // bitmask of channel indices with a queued warning
|
||||
uint8_t pendingWarningCount = 0; // number of queued warnings this transaction
|
||||
bool pendingWarningNameIssue = false; // any queued warning was about a channel name
|
||||
bool pendingWarningPskIssue = false; // any queued warning was about a PSK
|
||||
bool pendingLicenseWarning = false; // a licensed-mode notice is queued for this transaction
|
||||
};
|
||||
|
||||
static constexpr const char *licensedModeMessage =
|
||||
|
||||
@@ -289,10 +289,6 @@ void CannedMessageModule::updateDestinationSelectionList()
|
||||
}
|
||||
}
|
||||
|
||||
meshtastic_MeshPacket *p = allocDataPacket();
|
||||
p->pki_encrypted = true;
|
||||
p->channel = 0;
|
||||
|
||||
// Populate active channels
|
||||
std::vector<String> seenChannels;
|
||||
seenChannels.reserve(channels.getNumChannels());
|
||||
@@ -2118,6 +2114,7 @@ static float getSnrLimit(meshtastic_Config_LoRaConfig_ModemPreset preset)
|
||||
return -6.0f;
|
||||
case PRESET(MEDIUM_SLOW):
|
||||
case PRESET(MEDIUM_FAST):
|
||||
case PRESET(MEDIUM_TURBO):
|
||||
return -5.5f;
|
||||
case PRESET(SHORT_SLOW):
|
||||
case PRESET(SHORT_FAST):
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
#include "ExternalNotificationModule.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "RTC.h"
|
||||
#include "Router.h"
|
||||
#include "buzz/buzz.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "main.h"
|
||||
#include "mesh/generated/meshtastic/rtttl.pb.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
#if !MESHTASTIC_EXCLUDE_PKI
|
||||
#include "KeyVerificationModule.h"
|
||||
#include "CryptoEngine.h"
|
||||
#include "HardwareRNG.h"
|
||||
#include "MeshService.h"
|
||||
#include "RTC.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "graphics/draw/MenuHandler.h"
|
||||
#include "main.h"
|
||||
#include "meshUtils.h"
|
||||
#include "modules/AdminModule.h"
|
||||
#include "modules/NodeInfoModule.h"
|
||||
#include <RNG.h>
|
||||
#include <SHA256.h>
|
||||
|
||||
KeyVerificationModule *keyVerificationModule;
|
||||
@@ -34,7 +38,7 @@ AdminMessageHandleResult KeyVerificationModule::handleAdminMessageForModule(cons
|
||||
{
|
||||
updateState();
|
||||
if (request->which_payload_variant == meshtastic_AdminMessage_key_verification_tag && mp.from == 0) {
|
||||
LOG_WARN("Handling Key Verification Admin Message type %u", request->key_verification.message_type);
|
||||
LOG_DEBUG("Handling Key Verification Admin Message type %u", request->key_verification.message_type);
|
||||
|
||||
if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_INITIATE_VERIFICATION &&
|
||||
currentState == KEY_VERIFICATION_IDLE) {
|
||||
@@ -48,9 +52,7 @@ AdminMessageHandleResult KeyVerificationModule::handleAdminMessageForModule(cons
|
||||
|
||||
} else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_VERIFY &&
|
||||
request->key_verification.nonce == currentNonce) {
|
||||
auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
|
||||
if (remoteNodePtr)
|
||||
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
||||
commitVerifiedRemoteNode();
|
||||
resetToIdle();
|
||||
} else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_NOT_VERIFY) {
|
||||
resetToIdle();
|
||||
@@ -63,9 +65,8 @@ AdminMessageHandleResult KeyVerificationModule::handleAdminMessageForModule(cons
|
||||
bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_KeyVerification *r)
|
||||
{
|
||||
updateState();
|
||||
if (mp.pki_encrypted == false) {
|
||||
return false;
|
||||
}
|
||||
// Note: pki_encrypted is not required here. The first response (M2) may arrive channel-encrypted in
|
||||
// the bootstrap case; the follow-on hash1 packet (M3) is required to be PKI in its branch below.
|
||||
if (mp.from != currentRemoteNode) { // because the inital connection request is handled in allocReply()
|
||||
return false;
|
||||
}
|
||||
@@ -74,9 +75,14 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
|
||||
}
|
||||
|
||||
if (currentState == KEY_VERIFICATION_SENDER_HAS_INITIATED && r->nonce == currentNonce && r->hash2.size == 32 &&
|
||||
r->hash1.size == 0) {
|
||||
r->hash1.size == 32) {
|
||||
memcpy(hash2, r->hash2.bytes, 32);
|
||||
IF_SCREEN(screen->showNumberPicker("Enter Security Number", 60000, 6, [](int number_picked) -> void {
|
||||
// The response carries the responder's public key in hash1. If we don't already hold it, stash it
|
||||
// as a pending key so the Router can PKI-encrypt our follow-on packet (committed to NodeDB on accept).
|
||||
auto *responderNode = nodeDB->getMeshNode(currentRemoteNode);
|
||||
if (responderNode == nullptr || responderNode->public_key.size != 32)
|
||||
crypto->setPendingPublicKey(currentRemoteNode, r->hash1.bytes);
|
||||
IF_SCREEN(screen->showNumberPicker("Enter Security Number", 60000, 6, false, [](uint32_t number_picked) -> void {
|
||||
keyVerificationModule->processSecurityNumber(number_picked);
|
||||
});)
|
||||
|
||||
@@ -95,7 +101,8 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
|
||||
currentState = KEY_VERIFICATION_SENDER_AWAITING_NUMBER;
|
||||
return true;
|
||||
|
||||
} else if (currentState == KEY_VERIFICATION_RECEIVER_AWAITING_HASH1 && r->hash1.size == 32 && r->nonce == currentNonce) {
|
||||
} else if (currentState == KEY_VERIFICATION_RECEIVER_AWAITING_HASH1 && mp.pki_encrypted && r->hash1.size == 32 &&
|
||||
r->nonce == currentNonce) {
|
||||
if (memcmp(hash1, r->hash1.bytes, 32) == 0) {
|
||||
memset(message, 0, sizeof(message));
|
||||
sprintf(message, "Verification: \n");
|
||||
@@ -108,10 +115,9 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
|
||||
options.notificationType = graphics::notificationTypeEnum::selection_picker;
|
||||
options.bannerCallback =
|
||||
[=](int selected) {
|
||||
LOG_DEBUG("User selected %d for key verification", selected);
|
||||
if (selected == 1) {
|
||||
auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
|
||||
if (remoteNodePtr)
|
||||
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
||||
keyVerificationModule->commitVerifiedRemoteNode();
|
||||
}
|
||||
};
|
||||
screen->showOverlayBanner(options);)
|
||||
@@ -139,22 +145,29 @@ bool KeyVerificationModule::sendInitialRequest(NodeNum remoteNode)
|
||||
{
|
||||
LOG_DEBUG("keyVerification start");
|
||||
// generate nonce
|
||||
updateState();
|
||||
updateState(false);
|
||||
if (currentState != KEY_VERIFICATION_IDLE) {
|
||||
IF_SCREEN(graphics::menuHandler::menuQueue = graphics::menuHandler::ThrottleMessage;)
|
||||
return false;
|
||||
}
|
||||
updateState(true);
|
||||
currentNonce = random();
|
||||
currentNonceTimestamp = getTime();
|
||||
currentRemoteNode = remoteNode;
|
||||
meshtastic_KeyVerification KeyVerification = meshtastic_KeyVerification_init_zero;
|
||||
KeyVerification.nonce = currentNonce;
|
||||
KeyVerification.hash2.size = 0;
|
||||
KeyVerification.hash1.size = 0;
|
||||
// Carry our public key in the otherwise-unused hash1 field so a peer that does not yet hold our
|
||||
// key can learn it from this first message (bootstrap / onboarding).
|
||||
KeyVerification.hash1.size = 32;
|
||||
memcpy(KeyVerification.hash1.bytes, owner.public_key.bytes, 32);
|
||||
meshtastic_MeshPacket *p = allocDataProtobuf(KeyVerification);
|
||||
p->to = remoteNode;
|
||||
p->channel = 0;
|
||||
p->pki_encrypted = true;
|
||||
// Only request PKI when we already hold the destination's key. Otherwise this first message goes out
|
||||
// channel-encrypted (the Router falls back) so the peer can bootstrap from the key carried in hash1.
|
||||
auto *remoteNodePtr = nodeDB->getMeshNode(remoteNode);
|
||||
p->pki_encrypted = (remoteNodePtr != nullptr && remoteNodePtr->public_key.size == 32);
|
||||
p->decoded.want_response = true;
|
||||
p->priority = meshtastic_MeshPacket_Priority_HIGH;
|
||||
service->sendToMesh(p, RX_SRC_LOCAL, true);
|
||||
@@ -171,9 +184,6 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply()
|
||||
if (currentState != KEY_VERIFICATION_IDLE) { // TODO: cooldown period
|
||||
LOG_WARN("Key Verification requested, but already in a request");
|
||||
return nullptr;
|
||||
} else if (!currentRequest->pki_encrypted) {
|
||||
LOG_WARN("Key Verification requested, but not in a PKI packet");
|
||||
return nullptr;
|
||||
}
|
||||
currentState = KEY_VERIFICATION_RECEIVER_AWAITING_HASH1;
|
||||
|
||||
@@ -188,15 +198,43 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply()
|
||||
response.nonce = scratch.nonce;
|
||||
currentRemoteNode = req.from;
|
||||
currentNonceTimestamp = getTime();
|
||||
currentSecurityNumber = random(1, 999999);
|
||||
// The security number is the handshake's MitM-resistance entropy, so draw it from the hardware
|
||||
// RNG (falling back to the CSPRNG used for key material), never the predictable random().
|
||||
// Hold cryptLock like xeddsa_sign does: on nRF52 the fill toggles the CC310 that packet crypto
|
||||
// on the BLE task also uses, and the CryptRNG state is shared with the signing path.
|
||||
uint32_t securityEntropy = 0;
|
||||
{
|
||||
concurrency::LockGuard g(cryptLock);
|
||||
if (!HardwareRNG::fill((uint8_t *)&securityEntropy, sizeof(securityEntropy)))
|
||||
CryptRNG.rand((uint8_t *)&securityEntropy, sizeof(securityEntropy));
|
||||
}
|
||||
currentSecurityNumber = (securityEntropy % 999999) + 1;
|
||||
|
||||
// generate hash1
|
||||
// Resolve the requester's public key: from the PKI envelope, else carried in hash1 (bootstrap).
|
||||
// Stash unknown keys as pending (committed to NodeDB only once verification is accepted).
|
||||
const uint8_t *senderKey = nullptr;
|
||||
if (currentRequest->pki_encrypted && currentRequest->public_key.size == 32) {
|
||||
senderKey = currentRequest->public_key.bytes; // this is bizarre, fixme
|
||||
} else if (scratch.hash1.size == 32) {
|
||||
senderKey = scratch.hash1.bytes;
|
||||
}
|
||||
if (senderKey == nullptr) {
|
||||
LOG_WARN("Key Verification request without a usable public key");
|
||||
resetToIdle();
|
||||
return nullptr;
|
||||
}
|
||||
auto *senderNode = nodeDB->getMeshNode(currentRemoteNode);
|
||||
bool senderKeyInNodeDB = (senderNode != nullptr && senderNode->public_key.size == 32);
|
||||
if (!senderKeyInNodeDB)
|
||||
crypto->setPendingPublicKey(currentRemoteNode, senderKey);
|
||||
|
||||
// generate local hash1
|
||||
hash.reset();
|
||||
hash.update(¤tSecurityNumber, sizeof(currentSecurityNumber));
|
||||
hash.update(¤tNonce, sizeof(currentNonce));
|
||||
hash.update(¤tRemoteNode, sizeof(currentRemoteNode));
|
||||
hash.update(&ourNodeNum, sizeof(ourNodeNum));
|
||||
hash.update(currentRequest->public_key.bytes, currentRequest->public_key.size);
|
||||
hash.update(senderKey, 32);
|
||||
hash.update(owner.public_key.bytes, owner.public_key.size);
|
||||
hash.finalize(hash1, 32);
|
||||
|
||||
@@ -205,15 +243,19 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply()
|
||||
hash.update(¤tNonce, sizeof(currentNonce));
|
||||
hash.update(hash1, 32);
|
||||
hash.finalize(hash2, 32);
|
||||
response.hash1.size = 0;
|
||||
// Carry our public key in hash1 of the response so the requester can bootstrap our key as well.
|
||||
response.hash1.size = 32;
|
||||
memcpy(response.hash1.bytes, owner.public_key.bytes, 32);
|
||||
response.hash2.size = 32;
|
||||
memcpy(response.hash2.bytes, hash2, 32);
|
||||
|
||||
responsePacket = allocDataProtobuf(response);
|
||||
|
||||
responsePacket->pki_encrypted = true;
|
||||
// PKI-encrypt the response only if we already held the requester's key. In the bootstrap case it goes
|
||||
// out channel-encrypted so the requester (who lacks our key) can decode it and read hash1.
|
||||
responsePacket->pki_encrypted = senderKeyInNodeDB;
|
||||
IF_SCREEN(snprintf(message, 25, "Security Number \n%03u %03u", currentSecurityNumber / 1000, currentSecurityNumber % 1000);
|
||||
screen->showSimpleBanner(message, 30000); LOG_WARN("%s", message);)
|
||||
screen->showSimpleBanner(message, 30000); LOG_DEBUG("%s", message);)
|
||||
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
|
||||
if (cn) {
|
||||
cn->level = meshtastic_LogRecord_Level_WARNING;
|
||||
@@ -227,7 +269,7 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply()
|
||||
cn->payload_variant.key_verification_number_inform.security_number = currentSecurityNumber;
|
||||
service->sendClientNotification(cn);
|
||||
}
|
||||
LOG_WARN("Security Number %04u, nonce %llu", currentSecurityNumber, currentNonce);
|
||||
LOG_DEBUG("Security Number %04u, nonce %llu", currentSecurityNumber, currentNonce);
|
||||
return responsePacket;
|
||||
}
|
||||
|
||||
@@ -236,14 +278,18 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
|
||||
SHA256 hash;
|
||||
NodeNum ourNodeNum = nodeDB->getNodeNum();
|
||||
uint8_t scratch_hash[32] = {0};
|
||||
LOG_WARN("received security number: %u", incomingNumber);
|
||||
meshtastic_NodeInfoLite *remoteNodePtr = nullptr;
|
||||
remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
|
||||
if (!remoteNodePtr || !nodeInfoLiteHasUser(remoteNodePtr) || remoteNodePtr->public_key.size != 32) {
|
||||
currentState = KEY_VERIFICATION_IDLE;
|
||||
return; // should we throw an error here?
|
||||
LOG_DEBUG("received security number: %u", incomingNumber);
|
||||
meshtastic_NodeInfoLite *remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
|
||||
// Resolve the remote public key: NodeDB if known, otherwise the pending key learned during this
|
||||
// handshake (bootstrap case).
|
||||
meshtastic_NodeInfoLite_public_key_t remotePublic = {0, {0}};
|
||||
if (remoteNodePtr != nullptr && remoteNodePtr->public_key.size == 32) {
|
||||
remotePublic = remoteNodePtr->public_key;
|
||||
} else if (!crypto->getPendingPublicKey(currentRemoteNode, remotePublic)) {
|
||||
LOG_WARN("No public key available for remote node, aborting key verification");
|
||||
resetToIdle();
|
||||
return;
|
||||
}
|
||||
LOG_WARN("hashing ");
|
||||
// calculate hash1
|
||||
hash.reset();
|
||||
hash.update(&incomingNumber, sizeof(incomingNumber));
|
||||
@@ -252,7 +298,7 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
|
||||
hash.update(¤tRemoteNode, sizeof(currentRemoteNode));
|
||||
hash.update(owner.public_key.bytes, owner.public_key.size);
|
||||
|
||||
hash.update(remoteNodePtr->public_key.bytes, remoteNodePtr->public_key.size);
|
||||
hash.update(remotePublic.bytes, 32);
|
||||
hash.finalize(hash1, 32);
|
||||
|
||||
hash.reset();
|
||||
@@ -297,13 +343,13 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
|
||||
return;
|
||||
}
|
||||
|
||||
void KeyVerificationModule::updateState()
|
||||
void KeyVerificationModule::updateState(bool resetTimer)
|
||||
{
|
||||
if (currentState != KEY_VERIFICATION_IDLE) {
|
||||
// check for the 60 second timeout
|
||||
if (currentNonceTimestamp < getTime() - 60) {
|
||||
resetToIdle();
|
||||
} else {
|
||||
} else if (resetTimer) {
|
||||
currentNonceTimestamp = getTime();
|
||||
}
|
||||
}
|
||||
@@ -318,6 +364,32 @@ void KeyVerificationModule::resetToIdle()
|
||||
currentSecurityNumber = 0;
|
||||
currentRemoteNode = 0;
|
||||
currentState = KEY_VERIFICATION_IDLE;
|
||||
// Discard any not-yet-verified key learned during this handshake; on reject/timeout it is never trusted.
|
||||
crypto->clearPendingPublicKey();
|
||||
}
|
||||
|
||||
void KeyVerificationModule::commitVerifiedRemoteNode()
|
||||
{
|
||||
// The remote node already has a NodeDB entry by this point (packets were exchanged during the
|
||||
// handshake), so getMeshNode is sufficient; bail defensively if it is somehow absent.
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(currentRemoteNode);
|
||||
if (!node) {
|
||||
LOG_WARN("Attempted to commit key, but unknown node");
|
||||
return;
|
||||
}
|
||||
// If we only held the peer's key as a pending (unverified) key during the handshake, commit it to
|
||||
// NodeDB now that the user has confirmed the verification, so future PKI traffic can use it.
|
||||
meshtastic_NodeInfoLite_public_key_t pending = {0, {0}};
|
||||
if (node->public_key.size != 32 && crypto->getPendingPublicKey(currentRemoteNode, pending))
|
||||
node->public_key = pending;
|
||||
node->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
||||
LOG_INFO("Node 0x%08x manually verified with security number %u", currentRemoteNode, currentSecurityNumber);
|
||||
if (nodeInfoModule)
|
||||
nodeInfoModule->sendOurNodeInfo(currentRemoteNode, false, node->channel, true);
|
||||
crypto->clearPendingPublicKey();
|
||||
currentState = KEY_VERIFICATION_IDLE;
|
||||
// Persist the committed key and verified flag so manual verification survives a reboot.
|
||||
nodeDB->saveToDisk(SEGMENT_NODEDATABASE);
|
||||
}
|
||||
|
||||
void KeyVerificationModule::generateVerificationCode(char *readableCode)
|
||||
|
||||
@@ -12,6 +12,39 @@ enum KeyVerificationState {
|
||||
KEY_VERIFICATION_RECEIVER_AWAITING_HASH1,
|
||||
};
|
||||
|
||||
// KeyVerification Module overview
|
||||
// This module allows for two useful functions. First, it implements a 2sv process that can manually verify a trustworthy
|
||||
// connection with another node. It specifically verifies that the other node holds the correct private key for its public key, so
|
||||
// it is resistant to MitM attacks. Second, it can be used to bootstrap trust in a new node by carrying the public key in the
|
||||
// initial unencrypted message (in the hash1 field of the KeyVerification protobuf). This allows a user to manually verify a new
|
||||
// node even if they don't have that node in the local nodeDB at all.
|
||||
|
||||
// The handshake process is as follows (NodeA = initiator, NodeB = responder):
|
||||
// 1. NodeA sends a KeyVerification message containing a random nonce and its own public key (in the
|
||||
// hash1 field) to NodeB. Implemented in sendInitialRequest(). It is PKI-encrypted if NodeA already
|
||||
// holds NodeB's key, otherwise channel-encrypted (the bootstrap case).
|
||||
//
|
||||
// 2. NodeB replies (allocReply()) with its own public key (hash1 field) and hash2. NodeB generates a
|
||||
// random 6-digit security number and stashes NodeA's public key (as a pending key if not already in
|
||||
// the nodeDB). It computes hash1 = SHA256(securityNumber, nonce, NodeA_num, NodeB_num, PK_A, PK_B),
|
||||
// then hash2 = SHA256(nonce, hash1). The reply is PKI-encrypted only if NodeB already held NodeA's
|
||||
// key; in the bootstrap case it is channel-encrypted so NodeA can read NodeB's key from hash1.
|
||||
//
|
||||
// 3. NodeA receives the reply (handleReceivedProtobuf()), checks the nonce, stashes NodeB's public key,
|
||||
// and prompts the user to enter the security number. The security number is never sent over the mesh
|
||||
// and must be communicated over a secondary channel. processSecurityNumber() recomputes hash1 from
|
||||
// the entered number and verifies SHA256(nonce, hash1) matches the received hash2. NodeA then sends
|
||||
// its hash1 back to NodeB in a PKI-encrypted KeyVerification message (the follow-on PKI packet) and
|
||||
// shows the KeyVerificationFinalPrompt menu, displaying 8 characters derived from hash1.
|
||||
//
|
||||
// 4. NodeB receives NodeA's hash1 (handleReceivedProtobuf(); required to be PKI-encrypted), checks it
|
||||
// matches the hash1 NodeB generated, and shows the same 8-character code for final confirmation.
|
||||
//
|
||||
// The final on-screen code comparison is the actual manual verification: the user confirms the codes
|
||||
// match on both devices, proving the two nodes agree on the same public keys (no MitM substitution).
|
||||
// PKI-encrypting the follow-on packet additionally proves each node holds the private key for the
|
||||
// agreed public key.
|
||||
|
||||
class KeyVerificationModule : public ProtobufModule<meshtastic_KeyVerification> //, private concurrency::OSThread //
|
||||
{
|
||||
// CallbackObserver<KeyVerificationModule, const meshtastic::Status *> nodeStatusObserver =
|
||||
@@ -29,6 +62,7 @@ class KeyVerificationModule : public ProtobufModule<meshtastic_KeyVerification>
|
||||
bool sendInitialRequest(NodeNum remoteNode);
|
||||
void generateVerificationCode(char *); // fills char with the user readable verification code
|
||||
uint32_t getCurrentRemoteNode() { return currentRemoteNode; }
|
||||
void commitVerifiedRemoteNode(); // Commit a pending key to NodeDB and mark the node manually verified
|
||||
|
||||
protected:
|
||||
/* Called to handle a particular incoming message
|
||||
@@ -58,8 +92,8 @@ class KeyVerificationModule : public ProtobufModule<meshtastic_KeyVerification>
|
||||
char message[40] = {0};
|
||||
|
||||
void processSecurityNumber(uint32_t);
|
||||
void updateState(); // check the timeouts and maybe reset the state to idle
|
||||
void resetToIdle(); // Zero out module state
|
||||
void updateState(bool resetTimer = true); // check the timeouts and maybe reset the state to idle
|
||||
void resetToIdle(); // Zero out module state
|
||||
};
|
||||
|
||||
extern KeyVerificationModule *keyVerificationModule;
|
||||
@@ -2,11 +2,11 @@
|
||||
#include "Default.h"
|
||||
#include "DisplayFormatters.h"
|
||||
#include "NodeDB.h"
|
||||
#include "RTC.h"
|
||||
#include "RadioInterface.h"
|
||||
#include "Router.h"
|
||||
#include "TransmitHistory.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "main.h"
|
||||
#include <Throttle.h>
|
||||
#include <string.h>
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
#if !MESHTASTIC_EXCLUDE_CANNEDMESSAGES
|
||||
#include "modules/CannedMessageModule.h"
|
||||
#endif
|
||||
#if HAS_SCREEN && BASEUI_HAS_GAMES
|
||||
#include "modules/games/GamesModule.h"
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_DETECTIONSENSOR
|
||||
#include "modules/DetectionSensorModule.h"
|
||||
#endif
|
||||
@@ -206,6 +209,11 @@ void setupModules()
|
||||
cannedMessageModule = new CannedMessageModule();
|
||||
}
|
||||
#endif
|
||||
#if HAS_SCREEN && BASEUI_HAS_GAMES
|
||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||
gamesModule = new GamesModule();
|
||||
}
|
||||
#endif
|
||||
#if ARCH_PORTDUINO
|
||||
new HostMetricsModule();
|
||||
#endif
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#include "Default.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "RTC.h"
|
||||
#include "gps/RTC.h"
|
||||
#include <Throttle.h>
|
||||
|
||||
NeighborInfoModule *neighborInfoModule;
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "NodeStatus.h"
|
||||
#include "RTC.h"
|
||||
#include "Router.h"
|
||||
#include "TransmitHistory.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "main.h"
|
||||
#include <Throttle.h>
|
||||
#include <algorithm>
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PositionPrecision.h"
|
||||
#include "RTC.h"
|
||||
#include "Router.h"
|
||||
#include "TransmitHistory.h"
|
||||
#include "TypeConversions.h"
|
||||
#include "airtime.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/GeoCoord.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "main.h"
|
||||
#include "meshUtils.h"
|
||||
#include "meshtastic/atak.pb.h"
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PowerMon.h"
|
||||
#include "RTC.h"
|
||||
#include "Router.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "main.h"
|
||||
#include "sleep.h"
|
||||
#include "target_specific.h"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user