mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-19 18:01:30 +10:00
Merge branch 'develop' into t-display-s3-pro
This commit is contained in:
@@ -151,7 +151,7 @@ inline bool Syslog::_sendLog(uint16_t pri, const char *appName, const char *mess
|
||||
if (!this->_enabled)
|
||||
return false;
|
||||
|
||||
if ((this->_server == NULL && this->_ip == INADDR_NONE) || this->_port == 0)
|
||||
if ((this->_server == NULL && this->_ip == IPAddress(0, 0, 0, 0)) || this->_port == 0)
|
||||
return false;
|
||||
|
||||
// Check priority against priMask values.
|
||||
|
||||
@@ -13,6 +13,11 @@ extern MemGet memGet;
|
||||
#define LED_STATE_ON 1
|
||||
#endif
|
||||
|
||||
// WIFI LED
|
||||
#ifndef WIFI_STATE_ON
|
||||
#define WIFI_STATE_ON 1
|
||||
#endif
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// DEBUG
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -147,7 +152,9 @@ extern "C" void logLegacy(const char *level, const char *fmt, ...);
|
||||
// Default Bluetooth PIN
|
||||
#define defaultBLEPin 123456
|
||||
|
||||
#if HAS_ETHERNET && !defined(USE_WS5500)
|
||||
#if HAS_ETHERNET && defined(USE_CH390D)
|
||||
#include <ESP32_CH390.h>
|
||||
#elif HAS_ETHERNET && !defined(USE_WS5500)
|
||||
#include <RAK13800_W5100S.h>
|
||||
#endif // HAS_ETHERNET
|
||||
|
||||
|
||||
@@ -48,6 +48,14 @@ using namespace STM32_LittleFS_Namespace;
|
||||
using namespace Adafruit_LittleFS_Namespace;
|
||||
#endif
|
||||
|
||||
#if defined(ARCH_NRF54L15)
|
||||
// nRF54L15 — Zephyr LittleFS on 36 KB storage_partition (internal RRAM)
|
||||
#include "InternalFileSystem.h"
|
||||
#define FSCom InternalFS
|
||||
#define FSBegin() FSCom.begin()
|
||||
using namespace Adafruit_LittleFS_Namespace;
|
||||
#endif
|
||||
|
||||
void fsInit();
|
||||
void fsListFiles();
|
||||
bool copyFile(const char *from, const char *to);
|
||||
|
||||
+26
-31
@@ -6,7 +6,6 @@
|
||||
#include "SPILock.h"
|
||||
#include "SafeFile.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "graphics/draw/MessageRenderer.h"
|
||||
#include <cstring> // memcpy
|
||||
|
||||
#ifndef MESSAGE_TEXT_POOL_SIZE
|
||||
@@ -181,13 +180,8 @@ const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &pa
|
||||
|
||||
bool isDM = (sm.dest != 0 && sm.dest != NODENUM_BROADCAST);
|
||||
|
||||
if (packet.from == 0) {
|
||||
sm.type = isDM ? MessageType::DM_TO_US : MessageType::BROADCAST;
|
||||
sm.ackStatus = AckStatus::NONE;
|
||||
} else {
|
||||
sm.type = isDM ? MessageType::DM_TO_US : MessageType::BROADCAST;
|
||||
sm.ackStatus = AckStatus::ACKED;
|
||||
}
|
||||
sm.type = isDM ? MessageType::DM_TO_US : MessageType::BROADCAST;
|
||||
sm.ackStatus = (packet.from == 0) ? AckStatus::NONE : AckStatus::ACKED;
|
||||
|
||||
addLiveMessage(sm);
|
||||
|
||||
@@ -372,26 +366,25 @@ void MessageStore::clearAllMessages()
|
||||
#endif
|
||||
}
|
||||
|
||||
// Internal helper: erase first or last message matching a predicate
|
||||
template <typename Predicate> static void eraseIf(std::deque<StoredMessage> &deque, Predicate pred, bool fromBack = false)
|
||||
// Internal helpers for targeted erasure.
|
||||
template <typename Predicate> static bool eraseFirstMatch(std::deque<StoredMessage> &deque, Predicate pred)
|
||||
{
|
||||
if (fromBack) {
|
||||
// Iterate from the back and erase all matches from the end
|
||||
for (auto it = deque.rbegin(); it != deque.rend();) {
|
||||
if (pred(*it)) {
|
||||
it = std::deque<StoredMessage>::reverse_iterator(deque.erase(std::next(it).base()));
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
for (auto it = deque.begin(); it != deque.end(); ++it) {
|
||||
if (pred(*it)) {
|
||||
deque.erase(it);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
// Manual forward search to erase all matches
|
||||
for (auto it = deque.begin(); it != deque.end();) {
|
||||
if (pred(*it)) {
|
||||
it = deque.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
template <typename Predicate> static void eraseAllMatches(std::deque<StoredMessage> &deque, Predicate pred)
|
||||
{
|
||||
for (auto it = deque.begin(); it != deque.end();) {
|
||||
if (pred(*it)) {
|
||||
it = deque.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -399,7 +392,9 @@ template <typename Predicate> static void eraseIf(std::deque<StoredMessage> &deq
|
||||
// Delete oldest message (RAM + persisted queue)
|
||||
void MessageStore::deleteOldestMessage()
|
||||
{
|
||||
eraseIf(liveMessages, [](StoredMessage &) { return true; });
|
||||
if (!liveMessages.empty()) {
|
||||
liveMessages.pop_front();
|
||||
}
|
||||
saveToFlash();
|
||||
}
|
||||
|
||||
@@ -407,14 +402,14 @@ void MessageStore::deleteOldestMessage()
|
||||
void MessageStore::deleteOldestMessageInChannel(uint8_t channel)
|
||||
{
|
||||
auto pred = [channel](const StoredMessage &m) { return m.type == MessageType::BROADCAST && m.channelIndex == channel; };
|
||||
eraseIf(liveMessages, pred);
|
||||
eraseFirstMatch(liveMessages, pred);
|
||||
saveToFlash();
|
||||
}
|
||||
|
||||
void MessageStore::deleteAllMessagesInChannel(uint8_t channel)
|
||||
{
|
||||
auto pred = [channel](const StoredMessage &m) { return m.type == MessageType::BROADCAST && m.channelIndex == channel; };
|
||||
eraseIf(liveMessages, pred, false /* delete ALL, not just first */);
|
||||
eraseAllMatches(liveMessages, pred);
|
||||
saveToFlash();
|
||||
}
|
||||
|
||||
@@ -427,7 +422,7 @@ void MessageStore::deleteAllMessagesWithPeer(uint32_t peer)
|
||||
uint32_t other = (m.sender == local) ? m.dest : m.sender;
|
||||
return other == peer;
|
||||
};
|
||||
eraseIf(liveMessages, pred, false);
|
||||
eraseAllMatches(liveMessages, pred);
|
||||
saveToFlash();
|
||||
}
|
||||
|
||||
@@ -440,7 +435,7 @@ void MessageStore::deleteOldestMessageWithPeer(uint32_t peer)
|
||||
uint32_t other = (m.sender == nodeDB->getNodeNum()) ? m.dest : m.sender;
|
||||
return other == peer;
|
||||
};
|
||||
eraseIf(liveMessages, pred);
|
||||
eraseFirstMatch(liveMessages, pred);
|
||||
saveToFlash();
|
||||
}
|
||||
|
||||
|
||||
@@ -124,9 +124,6 @@ class MessageStore
|
||||
// Allocate text into pool (used by sender-side code)
|
||||
static uint16_t storeText(const char *src, size_t len);
|
||||
|
||||
// Used when loading from flash to rebuild the text pool
|
||||
static uint16_t rebuildTextFromFlash(const char *src, size_t len);
|
||||
|
||||
private:
|
||||
std::deque<StoredMessage> liveMessages; // Single in-RAM message buffer (also used for persistence)
|
||||
std::string filename; // Flash filename for persistence
|
||||
|
||||
+10
-1
@@ -910,7 +910,16 @@ void Power::readPowerStatus()
|
||||
|
||||
// Notify any status instances that are observing us
|
||||
const PowerStatus powerStatus2 = PowerStatus(hasBattery, usbPowered, isChargingNow, batteryVoltageMv, batteryChargePercent);
|
||||
if (millis() > lastLogTime + 50 * 1000) {
|
||||
|
||||
// Log battery-presence transitions once; skip OptUnknown so we don't lie before the first probe.
|
||||
static OptionalBool prevHasBattery = OptUnknown;
|
||||
if (hasBattery != OptUnknown && hasBattery != prevHasBattery) {
|
||||
LOG_INFO("Power: battery hardware %s", hasBattery == OptTrue ? "detected" : "absent (USB-only)");
|
||||
prevHasBattery = hasBattery;
|
||||
}
|
||||
|
||||
// Periodic telemetry only emits when a battery is actually present (otherwise values are constant -1/0).
|
||||
if (hasBattery == OptTrue && !Throttle::isWithinTimespanMs(lastLogTime, 50 * 1000)) {
|
||||
LOG_DEBUG("Battery: usbPower=%d, isCharging=%d, batMv=%d, batPct=%d", powerStatus2.getHasUSB(),
|
||||
powerStatus2.getIsCharging(), powerStatus2.getBatteryVoltageMv(), powerStatus2.getBatteryChargePercent());
|
||||
lastLogTime = millis();
|
||||
|
||||
@@ -225,14 +225,16 @@ void RedirectablePrint::log_to_ble(const char *logLevel, const char *format, va_
|
||||
isBleConnected = nimbleBluetooth && nimbleBluetooth->isActive() && nimbleBluetooth->isConnected();
|
||||
#elif defined(ARCH_NRF52)
|
||||
isBleConnected = nrf52Bluetooth != nullptr && nrf52Bluetooth->isConnected();
|
||||
#elif defined(ARCH_NRF54L15)
|
||||
isBleConnected = nrf54l15Bluetooth != nullptr && nrf54l15Bluetooth->isConnected();
|
||||
#endif
|
||||
if (isBleConnected) {
|
||||
auto thread = concurrency::OSThread::currentThread;
|
||||
meshtastic_LogRecord logRecord = meshtastic_LogRecord_init_zero;
|
||||
logRecord.level = getLogLevel(logLevel);
|
||||
vsprintf(logRecord.message, format, arg);
|
||||
vsnprintf(logRecord.message, sizeof(logRecord.message), format, arg);
|
||||
if (thread)
|
||||
strcpy(logRecord.source, thread->ThreadName.c_str());
|
||||
strlcpy(logRecord.source, thread->ThreadName.c_str(), sizeof(logRecord.source));
|
||||
logRecord.time = getValidTime(RTCQuality::RTCQualityDevice, true);
|
||||
|
||||
auto buffer = std::unique_ptr<uint8_t[]>(new uint8_t[meshtastic_LogRecord_size]);
|
||||
@@ -241,6 +243,8 @@ void RedirectablePrint::log_to_ble(const char *logLevel, const char *format, va_
|
||||
nimbleBluetooth->sendLog(buffer.get(), size);
|
||||
#elif defined(ARCH_NRF52)
|
||||
nrf52Bluetooth->sendLog(buffer.get(), size);
|
||||
#elif defined(ARCH_NRF54L15)
|
||||
nrf54l15Bluetooth->sendLog(buffer.get(), size);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
+10
-142
@@ -65,7 +65,6 @@ 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"
|
||||
#include "modules/TextMessageModule.h"
|
||||
#include "modules/WaypointModule.h"
|
||||
#include "sleep.h"
|
||||
#include "target_specific.h"
|
||||
@@ -231,6 +230,7 @@ void Screen::showOverlayBanner(BannerOverlayOptions banner_overlay_options)
|
||||
#endif
|
||||
// Store the message and set the expiration timestamp
|
||||
strncpy(NotificationRenderer::alertBannerMessage, banner_overlay_options.message, 255);
|
||||
NotificationRenderer::parseBannerMessageWithFonts(NotificationRenderer::alertBannerMessage);
|
||||
NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination
|
||||
NotificationRenderer::alertBannerUntil =
|
||||
(banner_overlay_options.durationMs == 0) ? 0 : millis() + banner_overlay_options.durationMs;
|
||||
@@ -240,9 +240,9 @@ void Screen::showOverlayBanner(BannerOverlayOptions banner_overlay_options)
|
||||
NotificationRenderer::alertBannerCallback = banner_overlay_options.bannerCallback;
|
||||
NotificationRenderer::curSelected = banner_overlay_options.InitialSelected;
|
||||
NotificationRenderer::pauseBanner = false;
|
||||
NotificationRenderer::current_notification_type = notificationTypeEnum::selection_picker;
|
||||
NotificationRenderer::current_notification_type = banner_overlay_options.notificationType;
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
ui->setOverlays(overlays, 2);
|
||||
ui->setTargetFPS(60);
|
||||
updateUiFrame(ui);
|
||||
}
|
||||
@@ -264,7 +264,7 @@ void Screen::showNodePicker(const char *message, uint32_t durationMs, std::funct
|
||||
NotificationRenderer::current_notification_type = notificationTypeEnum::node_picker;
|
||||
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
ui->setOverlays(overlays, 2);
|
||||
ui->setTargetFPS(60);
|
||||
updateUiFrame(ui);
|
||||
}
|
||||
@@ -288,7 +288,7 @@ void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t
|
||||
NotificationRenderer::currentNumber = 0;
|
||||
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
ui->setOverlays(overlays, 2);
|
||||
ui->setTargetFPS(60);
|
||||
updateUiFrame(ui);
|
||||
}
|
||||
@@ -311,7 +311,7 @@ void Screen::showTextInput(const char *header, const char *initialText, uint32_t
|
||||
|
||||
// Set the overlay using the same pattern as other notification types
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
ui->setOverlays(overlays, 2);
|
||||
ui->setTargetFPS(60);
|
||||
updateUiFrame(ui);
|
||||
}
|
||||
@@ -696,7 +696,7 @@ void Screen::setup()
|
||||
static OverlayCallback overlays[] = {
|
||||
graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame
|
||||
};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
ui->setOverlays(overlays, 1);
|
||||
|
||||
// Enable UTF-8 to display mapping
|
||||
dispdev->setFontTableLookupFunction(customFontTableLookup);
|
||||
@@ -1313,7 +1313,7 @@ void Screen::setFrames(FrameFocus focus)
|
||||
|
||||
// Add overlays: frame icons and alert banner)
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
ui->setOverlays(overlays, 2);
|
||||
|
||||
prevFrame = -1; // Force drawFavoriteNode to pick a new node (because our list just changed)
|
||||
|
||||
@@ -1643,138 +1643,6 @@ int Screen::handleStatusUpdate(const meshtastic::Status *arg)
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Handles when message is received; will jump to text message frame.
|
||||
int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
|
||||
{
|
||||
if (showingNormalScreen) {
|
||||
if (packet->from == 0) {
|
||||
// Outgoing message (likely sent from phone)
|
||||
devicestate.has_rx_text_message = false;
|
||||
memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message));
|
||||
hiddenFrames.textMessage = true;
|
||||
hasUnreadMessage = false; // Clear unread state when user replies
|
||||
|
||||
setFrames(FOCUS_PRESERVE); // Stay on same frame, silently update frame list
|
||||
} else {
|
||||
// Incoming message
|
||||
devicestate.has_rx_text_message = true; // Needed to include the message frame
|
||||
hasUnreadMessage = true; // Enables mail icon in the header
|
||||
setFrames(FOCUS_PRESERVE); // Refresh frame list without switching view (no-op during text_input)
|
||||
|
||||
// Only wake/force display if the configuration allows it
|
||||
if (shouldWakeOnReceivedMessage()) {
|
||||
setOn(true); // Wake up the screen first
|
||||
forceDisplay(); // Forces screen redraw
|
||||
}
|
||||
// === Prepare banner/popup content ===
|
||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from);
|
||||
const meshtastic_Channel channel =
|
||||
channels.getByIndex(packet->channel ? packet->channel : channels.getPrimaryIndex());
|
||||
const char *longName = nodeInfoLiteHasUser(node) ? node->long_name : nullptr;
|
||||
|
||||
const char *msgRaw = reinterpret_cast<const char *>(packet->decoded.payload.bytes);
|
||||
|
||||
char banner[256];
|
||||
|
||||
bool isAlert = false;
|
||||
|
||||
if (moduleConfig.external_notification.alert_bell || moduleConfig.external_notification.alert_bell_vibra ||
|
||||
moduleConfig.external_notification.alert_bell_buzzer)
|
||||
// Check for bell character to determine if this message is an alert
|
||||
for (size_t i = 0; i < packet->decoded.payload.size && i < 100; i++) {
|
||||
if (msgRaw[i] == ASCII_BELL) {
|
||||
isAlert = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Unlike generic messages, alerts (when enabled via the ext notif module) ignore any
|
||||
// 'mute' preferences set to any specific node or channel.
|
||||
// If on-screen keyboard is active, show a transient popup over keyboard instead of interrupting it
|
||||
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
|
||||
// Wake and force redraw so popup is visible immediately
|
||||
if (shouldWakeOnReceivedMessage()) {
|
||||
setOn(true);
|
||||
forceDisplay();
|
||||
}
|
||||
|
||||
// Build popup: title = message source name, content = message text (sanitized)
|
||||
// Title
|
||||
char titleBuf[64] = {0};
|
||||
if (longName && longName[0]) {
|
||||
// Sanitize sender name
|
||||
std::string t = sanitizeString(longName);
|
||||
strncpy(titleBuf, t.c_str(), sizeof(titleBuf) - 1);
|
||||
} else {
|
||||
strncpy(titleBuf, "Message", sizeof(titleBuf) - 1);
|
||||
}
|
||||
|
||||
// Content: payload bytes may not be null-terminated, remove ASCII_BELL and sanitize
|
||||
char content[256] = {0};
|
||||
{
|
||||
std::string raw;
|
||||
raw.reserve(packet->decoded.payload.size);
|
||||
for (size_t i = 0; i < packet->decoded.payload.size; ++i) {
|
||||
char c = msgRaw[i];
|
||||
if (c == ASCII_BELL)
|
||||
continue; // strip bell
|
||||
raw.push_back(c);
|
||||
}
|
||||
std::string sanitized = sanitizeString(raw);
|
||||
strncpy(content, sanitized.c_str(), sizeof(content) - 1);
|
||||
}
|
||||
|
||||
NotificationRenderer::showKeyboardMessagePopupWithTitle(titleBuf, content, 3000);
|
||||
|
||||
// Maintain existing buzzer behavior on M5 if applicable
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY ||
|
||||
(isAlert && moduleConfig.external_notification.alert_bell_buzzer) ||
|
||||
(!isBroadcast(packet->to) && isToUs(packet))) {
|
||||
playLongBeep();
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
// No keyboard active: use regular banner flow, respecting mute settings
|
||||
if (isAlert) {
|
||||
if (longName && longName[0]) {
|
||||
snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName);
|
||||
} else {
|
||||
strcpy(banner, "Alert Received");
|
||||
}
|
||||
screen->showSimpleBanner(banner, 3000);
|
||||
} else if (!channel.settings.has_module_settings || !channel.settings.module_settings.is_muted) {
|
||||
if (longName && longName[0]) {
|
||||
if (currentResolution == ScreenResolution::UltraLow) {
|
||||
strcpy(banner, "New Message");
|
||||
} else {
|
||||
snprintf(banner, sizeof(banner), "New Message from\n%s", longName);
|
||||
}
|
||||
} else {
|
||||
strcpy(banner, "New Message");
|
||||
}
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
screen->setOn(true);
|
||||
screen->showSimpleBanner(banner, 1500);
|
||||
if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY ||
|
||||
(isAlert && moduleConfig.external_notification.alert_bell_buzzer) ||
|
||||
(!isBroadcast(packet->to) && isToUs(packet))) {
|
||||
// Beep if not in DIRECT_MSG_ONLY mode or if in DIRECT_MSG_ONLY mode and either
|
||||
// - packet contains an alert and alert bell buzzer is enabled
|
||||
// - packet is a non-broadcast that is addressed to this node
|
||||
playLongBeep();
|
||||
}
|
||||
#else
|
||||
screen->showSimpleBanner(banner, 3000);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Triggered by MeshModules
|
||||
int Screen::handleUIFrameEvent(const UIFrameEvent *event)
|
||||
{
|
||||
@@ -1820,7 +1688,7 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
|
||||
NotificationRenderer::inEvent = *event;
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
ui->setOverlays(overlays, 2);
|
||||
setFastFramerate(); // Draw ASAP
|
||||
updateUiFrame(ui);
|
||||
return 0;
|
||||
@@ -1835,7 +1703,7 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
if (NotificationRenderer::isOverlayBannerShowing()) {
|
||||
NotificationRenderer::inEvent = *event;
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
ui->setOverlays(overlays, 2);
|
||||
setFastFramerate(); // Draw ASAP
|
||||
updateUiFrame(ui);
|
||||
|
||||
|
||||
@@ -609,7 +609,6 @@ class Screen : public concurrency::OSThread
|
||||
|
||||
// Handle observer events
|
||||
int handleStatusUpdate(const meshtastic::Status *arg);
|
||||
int handleTextMessage(const meshtastic_MeshPacket *packet);
|
||||
int handleUIFrameEvent(const UIFrameEvent *arg);
|
||||
int handleInputEvent(const InputEvent *arg);
|
||||
int handleAdminMessage(AdminModule_ObserverData *arg);
|
||||
|
||||
@@ -88,6 +88,16 @@
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// nRF52 flash optimization: re-route FONT_LARGE_LOCAL to the 16pt glyph so
|
||||
// the display-tier dispatch below picks up 16pt everywhere it would have used
|
||||
// 24pt. Drops the ~9.6 KB ArialMT_Plain_24 table from the linked binary.
|
||||
// Set MESHTASTIC_LARGE_FONT_24PT=1 in build_flags to opt out per variant
|
||||
// (undefined or 0 keeps the optimization on).
|
||||
#if defined(ARCH_NRF52) && (!defined(MESHTASTIC_LARGE_FONT_24PT) || MESHTASTIC_LARGE_FONT_24PT == 0)
|
||||
#undef FONT_LARGE_LOCAL
|
||||
#define FONT_LARGE_LOCAL FONT_MEDIUM_LOCAL
|
||||
#endif
|
||||
|
||||
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
|
||||
defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || \
|
||||
defined(USE_ST7796) || defined(HACKADAY_COMMUNICATOR)) && \
|
||||
|
||||
@@ -57,6 +57,70 @@ BannerOverlayOptions createStaticBannerOptions(const char *message, const MenuOp
|
||||
return bannerOptions;
|
||||
}
|
||||
|
||||
const StoredMessage *getNewestMessageForActiveThread()
|
||||
{
|
||||
const auto &messages = messageStore.getMessages();
|
||||
if (messages.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto mode = graphics::MessageRenderer::getThreadMode();
|
||||
const int channel = graphics::MessageRenderer::getThreadChannel();
|
||||
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 (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
|
||||
if (m.type == MessageType::BROADCAST && static_cast<int>(m.channelIndex) == channel) {
|
||||
return &m;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
|
||||
if (m.type != MessageType::DM_TO_US) {
|
||||
continue;
|
||||
}
|
||||
const uint32_t other = (m.sender == localNode) ? m.dest : m.sender;
|
||||
if (other == peer) {
|
||||
return &m;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void launchReplyForMessage(const StoredMessage &message, bool freetext)
|
||||
{
|
||||
if (message.type == MessageType::BROADCAST || message.dest == NODENUM_BROADCAST) {
|
||||
if (freetext) {
|
||||
cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, message.channelIndex);
|
||||
} else {
|
||||
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, message.channelIndex);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const uint32_t localNode = nodeDB->getNodeNum();
|
||||
const uint32_t peer = (message.sender == localNode) ? message.dest : message.sender;
|
||||
if (peer == 0 || peer == NODENUM_BROADCAST) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (freetext) {
|
||||
cannedMessageModule->LaunchFreetextWithDestination(peer);
|
||||
} else {
|
||||
cannedMessageModule->LaunchWithDestination(peer);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
menuHandler::screenMenus menuHandler::menuQueue = MenuNone;
|
||||
@@ -594,9 +658,12 @@ void menuHandler::messageResponseMenu()
|
||||
|
||||
#ifdef HAS_I2S
|
||||
} else if (selected == Aloud) {
|
||||
const meshtastic_MeshPacket &mp = devicestate.rx_text_message;
|
||||
const char *msg = reinterpret_cast<const char *>(mp.decoded.payload.bytes);
|
||||
audioThread->readAloud(msg);
|
||||
if (const StoredMessage *latest = getNewestMessageForActiveThread()) {
|
||||
const char *msg = MessageStore::getText(*latest);
|
||||
if (msg && msg[0]) {
|
||||
audioThread->readAloud(msg);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
};
|
||||
@@ -656,20 +723,12 @@ void menuHandler::replyMenu()
|
||||
|
||||
// Preset reply
|
||||
if (selected == ReplyPreset) {
|
||||
|
||||
if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
|
||||
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, ch);
|
||||
|
||||
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
|
||||
cannedMessageModule->LaunchWithDestination(peer);
|
||||
|
||||
} else {
|
||||
// Fallback for last received message
|
||||
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
|
||||
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
|
||||
} else {
|
||||
cannedMessageModule->LaunchWithDestination(devicestate.rx_text_message.from);
|
||||
}
|
||||
} else if (const StoredMessage *latest = getNewestMessageForActiveThread()) {
|
||||
launchReplyForMessage(*latest, false);
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -677,20 +736,12 @@ void menuHandler::replyMenu()
|
||||
|
||||
// Freetext reply
|
||||
if (selected == ReplyFreetext) {
|
||||
|
||||
if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
|
||||
cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, ch);
|
||||
|
||||
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
|
||||
cannedMessageModule->LaunchFreetextWithDestination(peer);
|
||||
|
||||
} else {
|
||||
// Fallback for last received message
|
||||
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
|
||||
cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
|
||||
} else {
|
||||
cannedMessageModule->LaunchFreetextWithDestination(devicestate.rx_text_message.from);
|
||||
}
|
||||
} else if (const StoredMessage *latest = getNewestMessageForActiveThread()) {
|
||||
launchReplyForMessage(*latest, true);
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
@@ -66,6 +66,110 @@ uint32_t pow_of_10(uint32_t n)
|
||||
return ret;
|
||||
}
|
||||
|
||||
char graphics::NotificationRenderer::alertBannerLines[MAX_LINES + 1][64] = {};
|
||||
uint8_t graphics::NotificationRenderer::alertBannerLineCount = 0;
|
||||
graphics::NotificationRenderer::BannerFont graphics::NotificationRenderer::alertBannerLineFonts[MAX_LINES + 1] = {};
|
||||
|
||||
static inline graphics::NotificationRenderer::BannerFont parseFontTagPrefix(const char *&p)
|
||||
{
|
||||
// Tags must be at the start of the line:
|
||||
// [S] small, [M] medium, [L] large
|
||||
if (p && p[0] == '[' && p[2] == ']' && p[1] != '\0') {
|
||||
char t = p[1];
|
||||
if (t == 'S') {
|
||||
p += 3;
|
||||
return graphics::NotificationRenderer::BANNER_FONT_SMALL;
|
||||
}
|
||||
if (t == 'M') {
|
||||
p += 3;
|
||||
return graphics::NotificationRenderer::BANNER_FONT_MEDIUM;
|
||||
}
|
||||
if (t == 'L') {
|
||||
p += 3;
|
||||
return graphics::NotificationRenderer::BANNER_FONT_LARGE;
|
||||
}
|
||||
}
|
||||
return graphics::NotificationRenderer::BANNER_FONT_DEFAULT;
|
||||
}
|
||||
|
||||
static inline const uint8_t *fontForBannerLine(graphics::NotificationRenderer::BannerFont f)
|
||||
{
|
||||
switch (f) {
|
||||
case graphics::NotificationRenderer::BANNER_FONT_SMALL:
|
||||
return FONT_SMALL;
|
||||
case graphics::NotificationRenderer::BANNER_FONT_MEDIUM:
|
||||
return FONT_MEDIUM;
|
||||
case graphics::NotificationRenderer::BANNER_FONT_LARGE:
|
||||
return FONT_LARGE;
|
||||
case graphics::NotificationRenderer::BANNER_FONT_DEFAULT:
|
||||
default:
|
||||
return FONT_SMALL;
|
||||
}
|
||||
}
|
||||
|
||||
static inline uint8_t effectiveLineHeightForBannerLine(graphics::NotificationRenderer::BannerFont f)
|
||||
{
|
||||
uint8_t height = FONT_HEIGHT_SMALL;
|
||||
switch (f) {
|
||||
case graphics::NotificationRenderer::BANNER_FONT_MEDIUM:
|
||||
height = FONT_HEIGHT_MEDIUM;
|
||||
break;
|
||||
case graphics::NotificationRenderer::BANNER_FONT_LARGE:
|
||||
height = FONT_HEIGHT_LARGE;
|
||||
break;
|
||||
case graphics::NotificationRenderer::BANNER_FONT_SMALL:
|
||||
case graphics::NotificationRenderer::BANNER_FONT_DEFAULT:
|
||||
default:
|
||||
height = FONT_HEIGHT_SMALL;
|
||||
break;
|
||||
}
|
||||
return (height > 3) ? (height - 3) : height;
|
||||
}
|
||||
|
||||
void graphics::NotificationRenderer::parseBannerMessageWithFonts(const char *message)
|
||||
{
|
||||
alertBannerLineCount = 0;
|
||||
for (uint8_t i = 0; i < (MAX_LINES + 1); i++) {
|
||||
alertBannerLines[i][0] = '\0';
|
||||
alertBannerLineFonts[i] = BANNER_FONT_DEFAULT;
|
||||
}
|
||||
|
||||
if (!message || !message[0]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const char *p = message;
|
||||
|
||||
while (*p && alertBannerLineCount < (MAX_LINES + 1)) {
|
||||
const char *lineStart = p;
|
||||
while (*p && *p != '\n') {
|
||||
p++;
|
||||
}
|
||||
|
||||
char tmp[64] = {0};
|
||||
size_t len = (size_t)(p - lineStart);
|
||||
if (len > (sizeof(tmp) - 1)) {
|
||||
len = sizeof(tmp) - 1;
|
||||
}
|
||||
memcpy(tmp, lineStart, len);
|
||||
tmp[len] = '\0';
|
||||
|
||||
// Tag at start
|
||||
const char *tp = tmp;
|
||||
BannerFont f = parseFontTagPrefix(tp);
|
||||
alertBannerLineFonts[alertBannerLineCount] = f;
|
||||
|
||||
// Store stripped text
|
||||
strncpy(alertBannerLines[alertBannerLineCount], tp, sizeof(alertBannerLines[0]) - 1);
|
||||
alertBannerLines[alertBannerLineCount][sizeof(alertBannerLines[0]) - 1] = '\0';
|
||||
alertBannerLineCount++;
|
||||
|
||||
if (*p == '\n') {
|
||||
p++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Used on boot when a certificate is being created
|
||||
void NotificationRenderer::drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
@@ -375,23 +479,37 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp
|
||||
const char *lineStarts[MAX_LINES + 1] = {0};
|
||||
uint16_t lineCount = 0;
|
||||
char lineBuffer[40] = {0};
|
||||
bool useTaggedTextBanner =
|
||||
(current_notification_type == notificationTypeEnum::text_banner && alertBannerOptions == 0 && alertBannerLineCount > 0);
|
||||
|
||||
// Parse lines
|
||||
char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage));
|
||||
lineStarts[lineCount] = alertBannerMessage;
|
||||
if (useTaggedTextBanner) {
|
||||
lineCount = std::min<uint8_t>(alertBannerLineCount, MAX_LINES);
|
||||
for (uint16_t i = 0; i < lineCount; i++) {
|
||||
lineStarts[i] = alertBannerLines[i];
|
||||
lineLengths[i] = strlen(lineStarts[i]);
|
||||
display->setFont(fontForBannerLine(alertBannerLineFonts[i]));
|
||||
lineWidths[i] = display->getStringWidth(lineStarts[i], lineLengths[i], true);
|
||||
if (lineWidths[i] > maxWidth)
|
||||
maxWidth = lineWidths[i];
|
||||
}
|
||||
} else {
|
||||
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');
|
||||
lineLengths[lineCount] = lineStarts[lineCount + 1] - lineStarts[lineCount];
|
||||
if (lineStarts[lineCount + 1][0] == '\n')
|
||||
lineStarts[lineCount + 1] += 1;
|
||||
lineWidths[lineCount] = display->getStringWidth(lineStarts[lineCount], lineLengths[lineCount], true);
|
||||
if (lineWidths[lineCount] > maxWidth)
|
||||
maxWidth = lineWidths[lineCount];
|
||||
lineCount++;
|
||||
while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) {
|
||||
lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n');
|
||||
lineLengths[lineCount] = lineStarts[lineCount + 1] - lineStarts[lineCount];
|
||||
if (lineStarts[lineCount + 1][0] == '\n')
|
||||
lineStarts[lineCount + 1] += 1;
|
||||
lineWidths[lineCount] = display->getStringWidth(lineStarts[lineCount], lineLengths[lineCount], true);
|
||||
if (lineWidths[lineCount] > maxWidth)
|
||||
maxWidth = lineWidths[lineCount];
|
||||
lineCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Measure option widths
|
||||
display->setFont(FONT_SMALL);
|
||||
for (int i = 0; i < alertBannerOptions; i++) {
|
||||
optionWidths[i] = display->getStringWidth(optionsArrayPtr[i], strlen(optionsArrayPtr[i]), true);
|
||||
if (optionWidths[i] > maxWidth)
|
||||
@@ -505,6 +623,10 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
bool needs_bell = false;
|
||||
uint16_t lineWidths[totalLines] = {0};
|
||||
uint16_t lineLengths[totalLines] = {0};
|
||||
BannerFont lineFonts[totalLines] = {};
|
||||
uint8_t lineEffectiveHeights[totalLines] = {0};
|
||||
const char *renderLines[totalLines] = {0};
|
||||
bool useTaggedBannerFonts = (current_notification_type == notificationTypeEnum::text_banner && alertBannerOptions == 0);
|
||||
|
||||
if (maxWidth != 0)
|
||||
is_picker = true;
|
||||
@@ -517,11 +639,22 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
uint16_t widestLineWithBars = 0;
|
||||
|
||||
while (lines[lineCount] != nullptr) {
|
||||
auto newlinePointer = strchr(lines[lineCount], '\n');
|
||||
const char *renderText = lines[lineCount];
|
||||
BannerFont lineFont = BANNER_FONT_DEFAULT;
|
||||
if (useTaggedBannerFonts && lineCount < alertBannerLineCount) {
|
||||
renderText = alertBannerLines[lineCount];
|
||||
lineFont = alertBannerLineFonts[lineCount];
|
||||
}
|
||||
renderLines[lineCount] = renderText;
|
||||
lineFonts[lineCount] = lineFont;
|
||||
lineEffectiveHeights[lineCount] = effectiveLineHeightForBannerLine(lineFont);
|
||||
display->setFont(fontForBannerLine(lineFont));
|
||||
|
||||
auto newlinePointer = strchr(renderText, '\n');
|
||||
if (newlinePointer)
|
||||
lineLengths[lineCount] = (newlinePointer - lines[lineCount]); // Check for newlines first
|
||||
else // if the newline wasn't found, then pull string length from strlen
|
||||
lineLengths[lineCount] = strlen(lines[lineCount]);
|
||||
lineLengths[lineCount] = (newlinePointer - renderText);
|
||||
else
|
||||
lineLengths[lineCount] = strlen(renderText);
|
||||
|
||||
if (current_notification_type == notificationTypeEnum::node_picker) {
|
||||
char measureBuffer[64] = {0};
|
||||
@@ -533,7 +666,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
|
||||
// Consider extra width for signal bars on lines that contain "Signal:"
|
||||
uint16_t potentialWidth = lineWidths[lineCount];
|
||||
if (graphics::bannerSignalBars >= 0 && strncmp(lines[lineCount], "Signal:", 7) == 0) {
|
||||
if (graphics::bannerSignalBars >= 0 && strncmp(renderText, "Signal:", 7) == 0) {
|
||||
const int totalBars = 5;
|
||||
const int barWidth = 3;
|
||||
const int barSpacing = 2;
|
||||
@@ -569,8 +702,21 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
|
||||
uint16_t screenHeight = display->height();
|
||||
uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3;
|
||||
uint8_t visibleTotalLines = std::min<uint8_t>(lineCount, (screenHeight - vPadding * 2) / effectiveLineHeight);
|
||||
uint16_t contentHeight = visibleTotalLines * effectiveLineHeight;
|
||||
uint8_t visibleTotalLines = 0;
|
||||
uint16_t contentHeight = 0;
|
||||
const uint16_t availableHeight = (screenHeight > (vPadding * 2)) ? (screenHeight - vPadding * 2) : 0;
|
||||
for (uint8_t i = 0; i < lineCount; i++) {
|
||||
uint8_t thisLineHeight = lineEffectiveHeights[i] ? lineEffectiveHeights[i] : effectiveLineHeight;
|
||||
if (contentHeight + thisLineHeight > availableHeight) {
|
||||
break;
|
||||
}
|
||||
contentHeight += thisLineHeight;
|
||||
visibleTotalLines++;
|
||||
}
|
||||
if (visibleTotalLines == 0 && lineCount > 0) {
|
||||
visibleTotalLines = 1;
|
||||
contentHeight = lineEffectiveHeights[0] ? lineEffectiveHeights[0] : effectiveLineHeight;
|
||||
}
|
||||
uint16_t boxHeight = contentHeight + vPadding * 2;
|
||||
if (visibleTotalLines == 1) {
|
||||
boxHeight += (currentResolution == ScreenResolution::High) ? 4 : 3;
|
||||
@@ -616,15 +762,18 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
|
||||
// Draw Content
|
||||
int16_t lineY = boxTop + vPadding;
|
||||
for (int i = 0; i < lineCount; i++) {
|
||||
for (int i = 0; i < visibleTotalLines; i++) {
|
||||
display->setFont(fontForBannerLine(lineFonts[i]));
|
||||
int16_t thisLineHeight = lineEffectiveHeights[i] ? lineEffectiveHeights[i] : effectiveLineHeight;
|
||||
int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2;
|
||||
if (needs_bell && i == 0) {
|
||||
int bellY = lineY + (FONT_HEIGHT_SMALL - 8) / 2;
|
||||
int fontHeight = thisLineHeight + 3;
|
||||
int bellY = lineY + (fontHeight - 8) / 2;
|
||||
display->drawXbm(textX - 10, bellY, 8, 8, bell_alert);
|
||||
display->drawXbm(textX + lineWidths[i] + 2, bellY, 8, 8, bell_alert);
|
||||
}
|
||||
char lineBuffer[lineLengths[i] + 1];
|
||||
strncpy(lineBuffer, lines[i], lineLengths[i]);
|
||||
strncpy(lineBuffer, renderLines[i], lineLengths[i]);
|
||||
lineBuffer[lineLengths[i]] = '\0';
|
||||
// Determine if this is a pop-up or a pick list
|
||||
if (alertBannerOptions > 0 && i == 0) {
|
||||
@@ -658,7 +807,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
display->drawString(textX, lineY - yOffset, lineBuffer);
|
||||
}
|
||||
display->setColor(WHITE);
|
||||
lineY += (effectiveLineHeight - 2 - background_yOffset);
|
||||
lineY += (thisLineHeight - 2 - background_yOffset);
|
||||
} else {
|
||||
// Pop-up
|
||||
// If this is the Signal line, center text + bars as one group
|
||||
@@ -716,7 +865,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
display->drawString(textX, lineY, lineBuffer);
|
||||
}
|
||||
}
|
||||
lineY += (effectiveLineHeight);
|
||||
lineY += thisLineHeight;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,12 @@ class NotificationRenderer
|
||||
|
||||
static bool pauseBanner;
|
||||
|
||||
enum BannerFont : uint8_t { BANNER_FONT_DEFAULT = 0, BANNER_FONT_SMALL, BANNER_FONT_MEDIUM, BANNER_FONT_LARGE };
|
||||
|
||||
static char alertBannerLines[MAX_LINES + 1][64]; // parsed text per line
|
||||
static uint8_t alertBannerLineCount;
|
||||
static BannerFont alertBannerLineFonts[MAX_LINES + 1];
|
||||
static void parseBannerMessageWithFonts(const char *message);
|
||||
static void resetBanner();
|
||||
static void showKeyboardMessagePopupWithTitle(const char *title, const char *content, uint32_t durationMs);
|
||||
static void drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
|
||||
@@ -5,9 +5,8 @@
|
||||
Shows the latest incoming text message, as well as sender.
|
||||
Both broadcast and direct messages will be shown here, from all channels.
|
||||
|
||||
This module doesn't doesn't use the devicestate.rx_text_message,' as this is overwritten to contain outgoing messages
|
||||
This module doesn't collect its own text message. Instead, the WindowManager stores the most recent incoming text message.
|
||||
This is available to any interested modules (SingeMessageApplet, NotificationApplet etc.) via InkHUD::latestMessage
|
||||
This is available to any interested modules (SingleMessageApplet, NotificationApplet etc.) via InkHUD::latestMessage
|
||||
|
||||
We do still receive notifications from the text message module though,
|
||||
to know when a new message has arrived, and trigger the update.
|
||||
@@ -46,4 +45,4 @@ class AllMessageApplet : public Applet
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -3,11 +3,10 @@
|
||||
/*
|
||||
|
||||
Shows the latest incoming *Direct Message* (DM), as well as sender.
|
||||
This compliments the threaded message applets
|
||||
This complements the threaded message applets
|
||||
|
||||
This module doesn't doesn't use the devicestate.rx_text_message,' as this is overwritten to contain outgoing messages
|
||||
This module doesn't collect its own text message. Instead, the WindowManager stores the most recent incoming text message.
|
||||
This is available to any interested modules (SingeMessageApplet, NotificationApplet etc.) via InkHUD::latestMessage
|
||||
This is available to any interested modules (SingleMessageApplet, NotificationApplet etc.) via InkHUD::latestMessage
|
||||
|
||||
We do still receive notifications from the text message module though,
|
||||
to know when a new message has arrived, and trigger the update.
|
||||
@@ -46,4 +45,4 @@ class DMApplet : public Applet
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -525,7 +525,7 @@ int InkHUD::Events::beforeReboot(void *unused)
|
||||
// Callback when a new text message is received
|
||||
// Caches the most recently received message, for use by applets
|
||||
// Rx does not trigger a save to flash, however the data *will* be saved alongside other during shutdown, etc.
|
||||
// Note: this is different from devicestate.rx_text_message, which may contain an *outgoing* message
|
||||
// Note: this is intentionally separate from device-state message fields.
|
||||
int InkHUD::Events::onReceiveTextMessage(const meshtastic_MeshPacket *packet)
|
||||
{
|
||||
// Short circuit: don't store outgoing messages
|
||||
|
||||
@@ -121,8 +121,7 @@ class Persistence
|
||||
|
||||
// Most recently received text message
|
||||
// Value is updated by InkHUD::WindowManager, as a courtesy to applets
|
||||
// Note: different from devicestate.rx_text_message,
|
||||
// which may contain an *outgoing message* to broadcast
|
||||
// InkHUD keeps its own latest-message cache for applets.
|
||||
struct LatestMessage {
|
||||
MessageStore::Message broadcast; // Most recent message received broadcast
|
||||
MessageStore::Message dm; // Most recent received DM
|
||||
|
||||
@@ -464,7 +464,7 @@ Most recently received text message
|
||||
|
||||
Collected here, so various user applets don't all have to store their own copy of this info.
|
||||
|
||||
We are unable to use `devicestate.rx_text_message` for this purpose, because:
|
||||
We keep this separate latest-message cache for this purpose, because:
|
||||
|
||||
- it is cleared by an outgoing text message
|
||||
- we want to store both a recent broadcast and a recent DM
|
||||
|
||||
@@ -333,6 +333,12 @@ void InputBroker::Init()
|
||||
BaseType_t higherWake = 0;
|
||||
concurrency::mainDelay.interruptFromISR(&higherWake);
|
||||
};
|
||||
#if defined(ELECROW_ThinkNode_M7)
|
||||
userConfigNoScreen.longLongPressTime = 15 * 1000;
|
||||
userConfigNoScreen.longLongPress = INPUT_BROKER_FACTORY_RST;
|
||||
#else
|
||||
userConfigNoScreen.longLongPress = INPUT_BROKER_SHUTDOWN;
|
||||
#endif
|
||||
userConfigNoScreen.singlePress = INPUT_BROKER_USER_PRESS;
|
||||
userConfigNoScreen.longPress = INPUT_BROKER_NONE;
|
||||
userConfigNoScreen.longPressTime = 500;
|
||||
|
||||
@@ -25,6 +25,7 @@ enum input_broker_event {
|
||||
INPUT_BROKER_USER_PRESS,
|
||||
INPUT_BROKER_ALT_PRESS,
|
||||
INPUT_BROKER_ALT_LONG,
|
||||
INPUT_BROKER_FACTORY_RST = 0x9a,
|
||||
INPUT_BROKER_SHUTDOWN = 0x9b,
|
||||
INPUT_BROKER_GPS_TOGGLE = 0x9e,
|
||||
INPUT_BROKER_SEND_PING = 0xaf,
|
||||
|
||||
+19
-4
@@ -2,6 +2,9 @@
|
||||
#if !MESHTASTIC_EXCLUDE_GPS
|
||||
#include "GPS.h"
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_INPUTBROKER
|
||||
#include "input/InputBroker.h"
|
||||
#endif
|
||||
#include "MeshRadio.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
@@ -59,12 +62,18 @@ NimbleBluetooth *nimbleBluetooth = nullptr;
|
||||
NRF52Bluetooth *nrf52Bluetooth = nullptr;
|
||||
#endif
|
||||
|
||||
#if HAS_WIFI || defined(USE_WS5500)
|
||||
#ifdef ARCH_NRF54L15
|
||||
void nrf54l15Setup();
|
||||
void nrf54l15Loop();
|
||||
NRF54L15Bluetooth *nrf54l15Bluetooth = nullptr;
|
||||
#endif
|
||||
|
||||
#if HAS_WIFI || defined(USE_WS5500) || defined(USE_CH390D)
|
||||
#include "mesh/api/WiFiServerAPI.h"
|
||||
#include "mesh/wifi/WiFiAPClient.h"
|
||||
#endif
|
||||
|
||||
#if HAS_ETHERNET && !defined(USE_WS5500)
|
||||
#if HAS_ETHERNET && !defined(USE_WS5500) && !defined(USE_CH390D)
|
||||
#include "mesh/api/ethServerAPI.h"
|
||||
#include "mesh/eth/ethClient.h"
|
||||
#endif
|
||||
@@ -245,7 +254,7 @@ const char *getDeviceName()
|
||||
uint32_t timeLastPowered = 0;
|
||||
|
||||
static OSThread *powerFSMthread;
|
||||
OSThread *ambientLightingThread;
|
||||
AmbientLightingThread *ambientLightingThread;
|
||||
|
||||
RadioLibHal *RadioLibHAL = NULL;
|
||||
|
||||
@@ -335,7 +344,7 @@ void setup()
|
||||
|
||||
#ifdef WIFI_LED
|
||||
pinMode(WIFI_LED, OUTPUT);
|
||||
digitalWrite(WIFI_LED, LOW);
|
||||
digitalWrite(WIFI_LED, HIGH ^ WIFI_STATE_ON);
|
||||
#endif
|
||||
|
||||
#ifdef BLE_LED
|
||||
@@ -696,6 +705,9 @@ void setup()
|
||||
#ifdef ARCH_NRF52
|
||||
nrf52Setup();
|
||||
#endif
|
||||
#ifdef ARCH_NRF54L15
|
||||
nrf54l15Setup();
|
||||
#endif
|
||||
|
||||
#ifdef ARCH_RP2040
|
||||
rp2040Setup();
|
||||
@@ -1126,6 +1138,9 @@ void loop()
|
||||
#endif
|
||||
#ifdef ARCH_NRF52
|
||||
nrf52Loop();
|
||||
#endif
|
||||
#ifdef ARCH_NRF54L15
|
||||
nrf54l15Loop();
|
||||
#endif
|
||||
power->powerCommandsCheck();
|
||||
|
||||
|
||||
@@ -19,6 +19,10 @@ extern NimbleBluetooth *nimbleBluetooth;
|
||||
#include "NRF52Bluetooth.h"
|
||||
extern NRF52Bluetooth *nrf52Bluetooth;
|
||||
#endif
|
||||
#ifdef ARCH_NRF54L15
|
||||
#include "NRF54L15Bluetooth.h"
|
||||
extern NRF54L15Bluetooth *nrf54l15Bluetooth;
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_I2C
|
||||
#include "detect/ScanI2CTwoWire.h"
|
||||
#endif
|
||||
|
||||
@@ -93,6 +93,23 @@ void Channels::initDefaultLoraConfig()
|
||||
#ifdef USERPREFS_LORACONFIG_CHANNEL_NUM
|
||||
loraConfig.channel_num = USERPREFS_LORACONFIG_CHANNEL_NUM;
|
||||
#endif
|
||||
|
||||
// Apply any hardcoded USERPREFS overrides for custom modem config (e.g. region-locked boards)
|
||||
#ifdef USERPREFS_LORACONFIG_USE_PRESET
|
||||
loraConfig.use_preset = USERPREFS_LORACONFIG_USE_PRESET;
|
||||
#endif
|
||||
#ifdef USERPREFS_LORACONFIG_BANDWIDTH
|
||||
loraConfig.bandwidth = USERPREFS_LORACONFIG_BANDWIDTH;
|
||||
#endif
|
||||
#ifdef USERPREFS_LORACONFIG_SPREAD_FACTOR
|
||||
loraConfig.spread_factor = USERPREFS_LORACONFIG_SPREAD_FACTOR;
|
||||
#endif
|
||||
#ifdef USERPREFS_LORACONFIG_CODING_RATE
|
||||
loraConfig.coding_rate = USERPREFS_LORACONFIG_CODING_RATE;
|
||||
#endif
|
||||
#ifdef USERPREFS_LORACONFIG_OVERRIDE_FREQUENCY
|
||||
loraConfig.override_frequency = USERPREFS_LORACONFIG_OVERRIDE_FREQUENCY;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Channels::ensureLicensedOperation()
|
||||
|
||||
@@ -32,7 +32,7 @@ template class LR20x0Interface<LR2021>;
|
||||
template class SX126xInterface<STM32WLx>;
|
||||
#endif
|
||||
|
||||
#if HAS_ETHERNET && !defined(USE_WS5500)
|
||||
#if HAS_ETHERNET && !defined(USE_WS5500) && !defined(USE_CH390D)
|
||||
#include "api/ethServerAPI.h"
|
||||
template class ServerAPI<EthernetClient>;
|
||||
template class APIServerPort<ethServerAPI, EthernetServer>;
|
||||
|
||||
@@ -334,7 +334,9 @@ void MeshService::sendToPhone(meshtastic_MeshPacket *p)
|
||||
|
||||
if (toPhoneQueue.enqueue(p, 0) == false) {
|
||||
LOG_CRIT("Failed to queue a packet into toPhoneQueue!");
|
||||
abort();
|
||||
releaseToPool(p);
|
||||
fromNum++; // notify observers so phone can resync
|
||||
return;
|
||||
}
|
||||
fromNum++;
|
||||
}
|
||||
@@ -351,7 +353,8 @@ void MeshService::sendMqttMessageToClientProxy(meshtastic_MqttClientProxyMessage
|
||||
|
||||
if (toPhoneMqttProxyQueue.enqueue(m, 0) == false) {
|
||||
LOG_CRIT("Failed to queue a packet into toPhoneMqttProxyQueue!");
|
||||
abort();
|
||||
releaseMqttClientProxyMessageToPool(m);
|
||||
return;
|
||||
}
|
||||
fromNum++;
|
||||
}
|
||||
@@ -383,7 +386,8 @@ void MeshService::sendClientNotification(meshtastic_ClientNotification *n)
|
||||
|
||||
if (toPhoneClientNotificationQueue.enqueue(n, 0) == false) {
|
||||
LOG_CRIT("Failed to queue a notification into toPhoneClientNotificationQueue!");
|
||||
abort();
|
||||
releaseClientNotificationToPool(n);
|
||||
return;
|
||||
}
|
||||
fromNum++;
|
||||
}
|
||||
|
||||
+211
-57
@@ -25,6 +25,7 @@
|
||||
#include "mesh/generated/meshtastic/deviceonly_legacy.pb.h"
|
||||
#include "meshUtils.h"
|
||||
#include "modules/NeighborInfoModule.h"
|
||||
#include "xmodem.h"
|
||||
#include <ErriezCRC32.h>
|
||||
#include <algorithm>
|
||||
#include <pb_decode.h>
|
||||
@@ -81,6 +82,14 @@ static unsigned char userprefs_admin_key_1[] = USERPREFS_USE_ADMIN_KEY_1;
|
||||
static unsigned char userprefs_admin_key_2[] = USERPREFS_USE_ADMIN_KEY_2;
|
||||
#endif
|
||||
|
||||
// Weak empty variant initialization function.
|
||||
// May be redefined by variant files.
|
||||
void variantDefaultConfig() __attribute__((weak));
|
||||
void variantDefaultConfig() {}
|
||||
|
||||
void variantDefaultModuleConfig() __attribute__((weak));
|
||||
void variantDefaultModuleConfig() {}
|
||||
|
||||
#ifdef HELTEC_MESH_NODE_T114
|
||||
|
||||
uint32_t read8(uint8_t bits, uint8_t dummy, uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_t rst)
|
||||
@@ -152,6 +161,25 @@ uint32_t get_st7789_id(uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_
|
||||
|
||||
#endif
|
||||
|
||||
// When armed by loadFromDisk, the decode callback writes satellite entries
|
||||
// straight into these maps instead of the temp vectors. Nullptr = legacy
|
||||
// push_back-to-vector path for backup/restore and other decoders.
|
||||
namespace
|
||||
{
|
||||
#if !MESHTASTIC_EXCLUDE_POSITIONDB
|
||||
std::map<NodeNum, meshtastic_PositionLite> *s_decodePositionsTarget = nullptr;
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
|
||||
std::map<NodeNum, meshtastic_DeviceMetrics> *s_decodeTelemetryTarget = nullptr;
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB
|
||||
std::map<NodeNum, meshtastic_EnvironmentMetrics> *s_decodeEnvironmentTarget = nullptr;
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_STATUSDB
|
||||
std::map<NodeNum, meshtastic_StatusMessage> *s_decodeStatusTarget = nullptr;
|
||||
#endif
|
||||
} // namespace
|
||||
|
||||
bool meshtastic_NodeDatabase_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_t *field)
|
||||
{
|
||||
const auto *iter = reinterpret_cast<const pb_field_iter_t *>(field);
|
||||
@@ -166,10 +194,10 @@ bool meshtastic_NodeDatabase_callback(pb_istream_t *istream, pb_ostream_t *ostre
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (istream) {
|
||||
meshtastic_NodeInfoLite node;
|
||||
if (istream && istream->bytes_left) {
|
||||
meshtastic_NodeInfoLite node = meshtastic_NodeInfoLite_init_zero;
|
||||
auto *vec = static_cast<std::vector<meshtastic_NodeInfoLite> *>(iter->pData);
|
||||
if (istream->bytes_left && pb_decode(istream, meshtastic_NodeInfoLite_fields, &node))
|
||||
if (pb_decode(istream, meshtastic_NodeInfoLite_fields, &node))
|
||||
vec->push_back(node);
|
||||
}
|
||||
return true;
|
||||
@@ -184,11 +212,19 @@ bool meshtastic_NodeDatabase_callback(pb_istream_t *istream, pb_ostream_t *ostre
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (istream) {
|
||||
meshtastic_NodePositionEntry entry;
|
||||
auto *vec = static_cast<std::vector<meshtastic_NodePositionEntry> *>(iter->pData);
|
||||
if (istream->bytes_left && pb_decode(istream, meshtastic_NodePositionEntry_fields, &entry))
|
||||
if (istream && istream->bytes_left) {
|
||||
meshtastic_NodePositionEntry entry = meshtastic_NodePositionEntry_init_zero;
|
||||
if (pb_decode(istream, meshtastic_NodePositionEntry_fields, &entry)) {
|
||||
#if !MESHTASTIC_EXCLUDE_POSITIONDB
|
||||
if (s_decodePositionsTarget) {
|
||||
if (entry.has_position)
|
||||
(*s_decodePositionsTarget)[entry.num] = entry.position;
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
auto *vec = static_cast<std::vector<meshtastic_NodePositionEntry> *>(iter->pData);
|
||||
vec->push_back(entry);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -202,11 +238,19 @@ bool meshtastic_NodeDatabase_callback(pb_istream_t *istream, pb_ostream_t *ostre
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (istream) {
|
||||
meshtastic_NodeTelemetryEntry entry;
|
||||
auto *vec = static_cast<std::vector<meshtastic_NodeTelemetryEntry> *>(iter->pData);
|
||||
if (istream->bytes_left && pb_decode(istream, meshtastic_NodeTelemetryEntry_fields, &entry))
|
||||
if (istream && istream->bytes_left) {
|
||||
meshtastic_NodeTelemetryEntry entry = meshtastic_NodeTelemetryEntry_init_zero;
|
||||
if (pb_decode(istream, meshtastic_NodeTelemetryEntry_fields, &entry)) {
|
||||
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
|
||||
if (s_decodeTelemetryTarget) {
|
||||
if (entry.has_device_metrics)
|
||||
(*s_decodeTelemetryTarget)[entry.num] = entry.device_metrics;
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
auto *vec = static_cast<std::vector<meshtastic_NodeTelemetryEntry> *>(iter->pData);
|
||||
vec->push_back(entry);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -220,11 +264,19 @@ bool meshtastic_NodeDatabase_callback(pb_istream_t *istream, pb_ostream_t *ostre
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (istream) {
|
||||
meshtastic_NodeStatusEntry entry;
|
||||
auto *vec = static_cast<std::vector<meshtastic_NodeStatusEntry> *>(iter->pData);
|
||||
if (istream->bytes_left && pb_decode(istream, meshtastic_NodeStatusEntry_fields, &entry))
|
||||
if (istream && istream->bytes_left) {
|
||||
meshtastic_NodeStatusEntry entry = meshtastic_NodeStatusEntry_init_zero;
|
||||
if (pb_decode(istream, meshtastic_NodeStatusEntry_fields, &entry)) {
|
||||
#if !MESHTASTIC_EXCLUDE_STATUSDB
|
||||
if (s_decodeStatusTarget) {
|
||||
if (entry.has_status)
|
||||
(*s_decodeStatusTarget)[entry.num] = entry.status;
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
auto *vec = static_cast<std::vector<meshtastic_NodeStatusEntry> *>(iter->pData);
|
||||
vec->push_back(entry);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -238,11 +290,19 @@ bool meshtastic_NodeDatabase_callback(pb_istream_t *istream, pb_ostream_t *ostre
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (istream) {
|
||||
meshtastic_NodeEnvironmentEntry entry;
|
||||
auto *vec = static_cast<std::vector<meshtastic_NodeEnvironmentEntry> *>(iter->pData);
|
||||
if (istream->bytes_left && pb_decode(istream, meshtastic_NodeEnvironmentEntry_fields, &entry))
|
||||
if (istream && istream->bytes_left) {
|
||||
meshtastic_NodeEnvironmentEntry entry = meshtastic_NodeEnvironmentEntry_init_zero;
|
||||
if (pb_decode(istream, meshtastic_NodeEnvironmentEntry_fields, &entry)) {
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB
|
||||
if (s_decodeEnvironmentTarget) {
|
||||
if (entry.has_environment_metrics)
|
||||
(*s_decodeEnvironmentTarget)[entry.num] = entry.environment_metrics;
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
auto *vec = static_cast<std::vector<meshtastic_NodeEnvironmentEntry> *>(iter->pData);
|
||||
vec->push_back(entry);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -251,6 +311,42 @@ bool meshtastic_NodeDatabase_callback(pb_istream_t *istream, pb_ostream_t *ostre
|
||||
}
|
||||
}
|
||||
|
||||
void NodeDB::armNodeDatabaseDecodeTargets()
|
||||
{
|
||||
#if !MESHTASTIC_EXCLUDE_POSITIONDB
|
||||
nodePositions.clear();
|
||||
s_decodePositionsTarget = &nodePositions;
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
|
||||
nodeTelemetry.clear();
|
||||
s_decodeTelemetryTarget = &nodeTelemetry;
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB
|
||||
nodeEnvironment.clear();
|
||||
s_decodeEnvironmentTarget = &nodeEnvironment;
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_STATUSDB
|
||||
nodeStatus.clear();
|
||||
s_decodeStatusTarget = &nodeStatus;
|
||||
#endif
|
||||
}
|
||||
|
||||
void NodeDB::disarmNodeDatabaseDecodeTargets()
|
||||
{
|
||||
#if !MESHTASTIC_EXCLUDE_POSITIONDB
|
||||
s_decodePositionsTarget = nullptr;
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
|
||||
s_decodeTelemetryTarget = nullptr;
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB
|
||||
s_decodeEnvironmentTarget = nullptr;
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_STATUSDB
|
||||
s_decodeStatusTarget = nullptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
/** The current change # for radio settings. Starts at 0 on boot and any time the radio settings
|
||||
* might have changed is incremented. Allows others to detect they might now be on a new channel.
|
||||
*/
|
||||
@@ -297,6 +393,13 @@ NodeDB::NodeDB()
|
||||
} else {
|
||||
LOG_WARN("Failed to read unique id from efuse");
|
||||
}
|
||||
#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
|
||||
@@ -730,6 +833,23 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
|
||||
config.lora.modem_preset = USERPREFS_LORACONFIG_MODEM_PRESET;
|
||||
#else
|
||||
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
||||
#endif
|
||||
#ifdef USERPREFS_LORACONFIG_USE_PRESET
|
||||
config.lora.use_preset = USERPREFS_LORACONFIG_USE_PRESET;
|
||||
#else
|
||||
config.lora.use_preset = true;
|
||||
#endif
|
||||
#ifdef USERPREFS_LORACONFIG_BANDWIDTH
|
||||
config.lora.bandwidth = USERPREFS_LORACONFIG_BANDWIDTH;
|
||||
#endif
|
||||
#ifdef USERPREFS_LORACONFIG_SPREAD_FACTOR
|
||||
config.lora.spread_factor = USERPREFS_LORACONFIG_SPREAD_FACTOR;
|
||||
#endif
|
||||
#ifdef USERPREFS_LORACONFIG_CODING_RATE
|
||||
config.lora.coding_rate = USERPREFS_LORACONFIG_CODING_RATE;
|
||||
#endif
|
||||
#ifdef USERPREFS_LORACONFIG_OVERRIDE_FREQUENCY
|
||||
config.lora.override_frequency = USERPREFS_LORACONFIG_OVERRIDE_FREQUENCY;
|
||||
#endif
|
||||
config.lora.hop_limit = HOP_RELIABLE;
|
||||
#ifdef USERPREFS_CONFIG_LORA_IGNORE_MQTT
|
||||
@@ -904,6 +1024,8 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
|
||||
#endif
|
||||
|
||||
initConfigIntervals();
|
||||
variantDefaultConfig();
|
||||
variantDefaultModuleConfig();
|
||||
}
|
||||
|
||||
void NodeDB::initConfigIntervals()
|
||||
@@ -1119,7 +1241,14 @@ void NodeDB::initModuleConfigIntervals()
|
||||
#else
|
||||
moduleConfig.telemetry.device_update_interval = MAX_INTERVAL;
|
||||
#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;
|
||||
#endif
|
||||
moduleConfig.telemetry.air_quality_interval = 0;
|
||||
moduleConfig.telemetry.power_update_interval = 0;
|
||||
moduleConfig.telemetry.health_update_interval = 0;
|
||||
@@ -1161,7 +1290,6 @@ void NodeDB::resetNodes(bool keepFavorites)
|
||||
std::fill(nodeDatabase.nodes.begin() + 1, nodeDatabase.nodes.end(), meshtastic_NodeInfoLite());
|
||||
}
|
||||
(void)ourNum;
|
||||
devicestate.has_rx_text_message = false;
|
||||
devicestate.has_rx_waypoint = false;
|
||||
saveNodeDatabaseToDisk();
|
||||
saveDeviceStateToDisk();
|
||||
@@ -1368,7 +1496,6 @@ void NodeDB::installDefaultDeviceState()
|
||||
devicestate.version = DEVICESTATE_CUR_VER;
|
||||
devicestate.receive_queue_count = 0; // Not yet implemented FIXME
|
||||
devicestate.has_rx_waypoint = false;
|
||||
devicestate.has_rx_text_message = false;
|
||||
|
||||
generatePacketId(); // FIXME - ugly way to init current_packet_id;
|
||||
|
||||
@@ -1510,6 +1637,19 @@ void NodeDB::loadFromDisk()
|
||||
}
|
||||
|
||||
#endif
|
||||
// Arm the direct-into-map decode so satellite entries skip the temp vectors.
|
||||
{
|
||||
concurrency::LockGuard guard(&satelliteMutex);
|
||||
armNodeDatabaseDecodeTargets();
|
||||
}
|
||||
struct Disarm {
|
||||
NodeDB &self;
|
||||
~Disarm() { self.disarmNodeDatabaseDecodeTargets(); }
|
||||
} disarm{*this};
|
||||
|
||||
// Avoid push_back's power-of-2 capacity growth wasting RAM at small N.
|
||||
nodeDatabase.nodes.reserve(MAX_NUM_NODES);
|
||||
|
||||
auto state = loadProto(nodeDatabaseFileName, getMaxNodesAllocatedSize(), sizeof(meshtastic_NodeDatabase),
|
||||
&meshtastic_NodeDatabase_msg, &nodeDatabase);
|
||||
if (nodeDatabase.version < DEVICESTATE_MIN_VER) {
|
||||
@@ -1523,50 +1663,33 @@ void NodeDB::loadFromDisk()
|
||||
} else {
|
||||
meshNodes = &nodeDatabase.nodes;
|
||||
numMeshNodes = nodeDatabase.nodes.size();
|
||||
// Hydrate the satellite maps; the on-disk vectors stay empty in steady
|
||||
// state and are repopulated only at save time.
|
||||
concurrency::LockGuard guard(&satelliteMutex);
|
||||
// Counts computed outside LOG_INFO() so cppcheck doesn't choke on #if in macro args.
|
||||
const unsigned posCount =
|
||||
#if !MESHTASTIC_EXCLUDE_POSITIONDB
|
||||
nodePositions.clear();
|
||||
nodePositions.reserve(nodeDatabase.positions.size());
|
||||
for (const auto &entry : nodeDatabase.positions) {
|
||||
if (entry.has_position)
|
||||
nodePositions[entry.num] = entry.position;
|
||||
}
|
||||
nodeDatabase.positions.clear();
|
||||
nodeDatabase.positions.shrink_to_fit();
|
||||
(unsigned)nodePositions.size();
|
||||
#else
|
||||
0u;
|
||||
#endif
|
||||
const unsigned telCount =
|
||||
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
|
||||
nodeTelemetry.clear();
|
||||
nodeTelemetry.reserve(nodeDatabase.telemetry.size());
|
||||
for (const auto &entry : nodeDatabase.telemetry) {
|
||||
if (entry.has_device_metrics)
|
||||
nodeTelemetry[entry.num] = entry.device_metrics;
|
||||
}
|
||||
nodeDatabase.telemetry.clear();
|
||||
nodeDatabase.telemetry.shrink_to_fit();
|
||||
(unsigned)nodeTelemetry.size();
|
||||
#else
|
||||
0u;
|
||||
#endif
|
||||
const unsigned envCount =
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB
|
||||
nodeEnvironment.clear();
|
||||
nodeEnvironment.reserve(nodeDatabase.environment.size());
|
||||
for (const auto &entry : nodeDatabase.environment) {
|
||||
if (entry.has_environment_metrics)
|
||||
nodeEnvironment[entry.num] = entry.environment_metrics;
|
||||
}
|
||||
nodeDatabase.environment.clear();
|
||||
nodeDatabase.environment.shrink_to_fit();
|
||||
(unsigned)nodeEnvironment.size();
|
||||
#else
|
||||
0u;
|
||||
#endif
|
||||
const unsigned statusCount =
|
||||
#if !MESHTASTIC_EXCLUDE_STATUSDB
|
||||
nodeStatus.clear();
|
||||
nodeStatus.reserve(nodeDatabase.status.size());
|
||||
for (const auto &entry : nodeDatabase.status) {
|
||||
if (entry.has_status)
|
||||
nodeStatus[entry.num] = entry.status;
|
||||
}
|
||||
nodeDatabase.status.clear();
|
||||
nodeDatabase.status.shrink_to_fit();
|
||||
(unsigned)nodeStatus.size();
|
||||
#else
|
||||
0u;
|
||||
#endif
|
||||
LOG_INFO("Loaded saved nodedatabase version %d, with nodes count: %d", nodeDatabase.version, nodeDatabase.nodes.size());
|
||||
LOG_INFO("Loaded saved nodedatabase v%d: %d nodes, %u pos, %u tel, %u env, %u status", nodeDatabase.version,
|
||||
nodeDatabase.nodes.size(), posCount, telCount, envCount, statusCount);
|
||||
}
|
||||
|
||||
if (numMeshNodes > MAX_NUM_NODES) {
|
||||
@@ -1634,6 +1757,28 @@ void NodeDB::loadFromDisk()
|
||||
config.lora.tx_enabled = false;
|
||||
#endif
|
||||
|
||||
// Always-apply LoRa overrides: applied after loading saved config so they
|
||||
// take effect even when NVS already has a valid config (e.g. region-locked
|
||||
// dev boards with no BLE/serial to set the region at runtime).
|
||||
#ifdef USERPREFS_CONFIG_LORA_REGION
|
||||
config.lora.region = USERPREFS_CONFIG_LORA_REGION;
|
||||
#endif
|
||||
#ifdef USERPREFS_LORACONFIG_USE_PRESET
|
||||
config.lora.use_preset = USERPREFS_LORACONFIG_USE_PRESET;
|
||||
#endif
|
||||
#ifdef USERPREFS_LORACONFIG_BANDWIDTH
|
||||
config.lora.bandwidth = USERPREFS_LORACONFIG_BANDWIDTH;
|
||||
#endif
|
||||
#ifdef USERPREFS_LORACONFIG_SPREAD_FACTOR
|
||||
config.lora.spread_factor = USERPREFS_LORACONFIG_SPREAD_FACTOR;
|
||||
#endif
|
||||
#ifdef USERPREFS_LORACONFIG_CODING_RATE
|
||||
config.lora.coding_rate = USERPREFS_LORACONFIG_CODING_RATE;
|
||||
#endif
|
||||
#ifdef USERPREFS_LORACONFIG_OVERRIDE_FREQUENCY
|
||||
config.lora.override_frequency = USERPREFS_LORACONFIG_OVERRIDE_FREQUENCY;
|
||||
#endif
|
||||
|
||||
if (backupSecurity.private_key.size > 0) {
|
||||
LOG_DEBUG("Restoring backup of security config");
|
||||
config.security = backupSecurity;
|
||||
@@ -1844,6 +1989,15 @@ bool NodeDB::saveNodeDatabaseToDisk()
|
||||
return false;
|
||||
}
|
||||
|
||||
// Defer (don't fail) while xmodem holds the prefs file handle. Returning false
|
||||
// would propagate through saveToDisk() and trigger fsFormat() mid-transfer.
|
||||
#ifdef FSCom
|
||||
if (xModem.isBusy()) {
|
||||
LOG_DEBUG("Deferring NodeDB save: xmodem transfer in progress");
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef FSCom
|
||||
spiLock->lock();
|
||||
FSCom.mkdir("/prefs");
|
||||
|
||||
+12
-7
@@ -4,9 +4,9 @@
|
||||
#include <Arduino.h>
|
||||
#include <algorithm>
|
||||
#include <assert.h>
|
||||
#include <map>
|
||||
#include <pb_encode.h>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "MeshTypes.h"
|
||||
@@ -170,19 +170,19 @@ class NodeDB
|
||||
Observable<const meshtastic::NodeStatus *> newStatus;
|
||||
pb_size_t numMeshNodes;
|
||||
|
||||
// Satellite per-NodeNum maps for data we used to inline into NodeInfoLite,
|
||||
// gated by MESHTASTIC_EXCLUDE_*DB so STM32WL can omit them.
|
||||
// Satellite per-NodeNum maps. std::map avoids unordered_map's bucket-array
|
||||
// preallocation; O(log N) lookup is fine at these sizes.
|
||||
#if !MESHTASTIC_EXCLUDE_POSITIONDB
|
||||
std::unordered_map<NodeNum, meshtastic_PositionLite> nodePositions;
|
||||
std::map<NodeNum, meshtastic_PositionLite> nodePositions;
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
|
||||
std::unordered_map<NodeNum, meshtastic_DeviceMetrics> nodeTelemetry;
|
||||
std::map<NodeNum, meshtastic_DeviceMetrics> nodeTelemetry;
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB
|
||||
std::unordered_map<NodeNum, meshtastic_EnvironmentMetrics> nodeEnvironment;
|
||||
std::map<NodeNum, meshtastic_EnvironmentMetrics> nodeEnvironment;
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_STATUSDB
|
||||
std::unordered_map<NodeNum, meshtastic_StatusMessage> nodeStatus;
|
||||
std::map<NodeNum, meshtastic_StatusMessage> nodeStatus;
|
||||
#endif
|
||||
|
||||
bool keyIsLowEntropy = false;
|
||||
@@ -429,6 +429,11 @@ class NodeDB
|
||||
// the legacy descriptor and copies entries into the v25 layout. Caller
|
||||
// is responsible for save / install-default on the result.
|
||||
bool migrateLegacyNodeDatabase();
|
||||
|
||||
// Route satellite-store decode entries straight into our maps instead of
|
||||
// temp vectors. Must be paired — disarm before any other NodeDatabase decode.
|
||||
void armNodeDatabaseDecodeTargets();
|
||||
void disarmNodeDatabaseDecodeTargets();
|
||||
};
|
||||
|
||||
extern NodeDB *nodeDB;
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
#include "Throttle.h"
|
||||
|
||||
#define PACKETHISTORY_MAX \
|
||||
max((u_int32_t)(MAX_NUM_NODES * 2.0), \
|
||||
(u_int32_t)100) // x2..3 Should suffice. Empirical setup. 16B per record malloc'ed, but no less than 100
|
||||
max((uint32_t)(MAX_NUM_NODES * 2.0), \
|
||||
(uint32_t)100) // x2..3 Should suffice. Empirical setup. 16B per record malloc'ed, but no less than 100
|
||||
|
||||
#define RECENT_WARN_AGE (10 * 60 * 1000L) // Warn if the packet that gets removed was more recent than 10 min
|
||||
|
||||
|
||||
+166
-189
@@ -62,7 +62,7 @@ void PhoneAPI::handleStartConfig()
|
||||
onConfigStart();
|
||||
|
||||
// even if we were already connected - restart our state machine
|
||||
if (config_nonce == SPECIAL_NONCE_ONLY_NODES || config_nonce == SPECIAL_NONCE_GRADIENT_ONLY_NODES) {
|
||||
if (config_nonce == SPECIAL_NONCE_ONLY_NODES) {
|
||||
// If client only wants node info, jump directly to sending nodes
|
||||
state = STATE_SEND_OWN_NODEINFO;
|
||||
LOG_INFO("Client only wants node info, skipping other config");
|
||||
@@ -71,7 +71,15 @@ void PhoneAPI::handleStartConfig()
|
||||
}
|
||||
pauseBluetoothLogging = true;
|
||||
spiLock->lock();
|
||||
#if defined(MESHTASTIC_EXCLUDE_FILES_MANIFEST)
|
||||
// Skip the recursive FS walk. Used by platforms whose Zephyr LittleFS
|
||||
// backend can't safely traverse a deep tree (e.g. nRF54L15) and platforms
|
||||
// that don't support OTA browsing — the manifest is only consumed by
|
||||
// companion apps for those flows.
|
||||
filesManifest.clear();
|
||||
#else
|
||||
filesManifest = getFiles("/", 10);
|
||||
#endif
|
||||
spiLock->unlock();
|
||||
LOG_DEBUG("Got %d files in manifest", filesManifest.size());
|
||||
|
||||
@@ -138,6 +146,7 @@ void PhoneAPI::close()
|
||||
replayTelemetryIndex = 0;
|
||||
replayEnvironmentIndex = 0;
|
||||
replayStatusIndex = 0;
|
||||
replayPhase = REPLAY_PHASE_IDLE;
|
||||
}
|
||||
packetForPhone = NULL;
|
||||
filesManifest.clear();
|
||||
@@ -320,7 +329,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
nodeInfoForPhone.num = 0;
|
||||
}
|
||||
}
|
||||
if (config_nonce == SPECIAL_NONCE_ONLY_NODES || config_nonce == SPECIAL_NONCE_GRADIENT_ONLY_NODES) {
|
||||
if (config_nonce == SPECIAL_NONCE_ONLY_NODES) {
|
||||
// If client only wants node info, jump directly to sending nodes
|
||||
state = STATE_SEND_OTHER_NODEINFOS;
|
||||
onNowHasData(0);
|
||||
@@ -535,135 +544,16 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
// Just in case we stored a different user.id in the past, but should never happen going forward
|
||||
sprintf(infoToSend.user.id, "!%08x", infoToSend.num);
|
||||
|
||||
// Logging this really slows down sending nodes on initial connection because the serial console is so slow, so only
|
||||
// uncomment if you really need to:
|
||||
// LOG_INFO("nodeinfo: num=0x%x, lastseen=%u, id=%s, name=%s", nodeInfoForPhone.num, nodeInfoForPhone.last_heard,
|
||||
// nodeInfoForPhone.user.id, nodeInfoForPhone.user.long_name);
|
||||
|
||||
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_node_info_tag;
|
||||
fromRadioScratch.node_info = infoToSend;
|
||||
prefetchNodeInfos();
|
||||
} else {
|
||||
LOG_DEBUG("Done sending %d of %d nodeinfos millis=%u", readIndex, nodeDB->getNumMeshNodes(), millis());
|
||||
concurrency::LockGuard guard(&nodeInfoMutex);
|
||||
nodeInfoMutex.lock();
|
||||
nodeInfoQueue.clear();
|
||||
// Replay states no-op for legacy clients / excluded DBs.
|
||||
state = STATE_REPLAY_POSITIONS;
|
||||
return getFromRadio(buf);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case STATE_REPLAY_POSITIONS: {
|
||||
if (replayPositionOrder.empty() && replayPositionIndex == 0)
|
||||
beginReplayPositions();
|
||||
prefetchReplayPositions();
|
||||
|
||||
meshtastic_MeshPacket pkt = {};
|
||||
bool havePkt = false;
|
||||
{
|
||||
concurrency::LockGuard guard(&nodeInfoMutex);
|
||||
if (!replayQueue.empty()) {
|
||||
pkt = replayQueue.front();
|
||||
replayQueue.pop_front();
|
||||
havePkt = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (havePkt) {
|
||||
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_packet_tag;
|
||||
fromRadioScratch.packet = pkt;
|
||||
} else {
|
||||
LOG_DEBUG("Done replaying positions count=%u millis=%u", (unsigned)replayPositionIndex, millis());
|
||||
state = STATE_REPLAY_TELEMETRY;
|
||||
return getFromRadio(buf);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case STATE_REPLAY_TELEMETRY: {
|
||||
if (replayTelemetryOrder.empty() && replayTelemetryIndex == 0)
|
||||
beginReplayTelemetry();
|
||||
prefetchReplayTelemetry();
|
||||
|
||||
meshtastic_MeshPacket pkt = {};
|
||||
bool havePkt = false;
|
||||
{
|
||||
concurrency::LockGuard guard(&nodeInfoMutex);
|
||||
if (!replayQueue.empty()) {
|
||||
pkt = replayQueue.front();
|
||||
replayQueue.pop_front();
|
||||
havePkt = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (havePkt) {
|
||||
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_packet_tag;
|
||||
fromRadioScratch.packet = pkt;
|
||||
} else {
|
||||
LOG_DEBUG("Done replaying telemetry count=%u millis=%u", (unsigned)replayTelemetryIndex, millis());
|
||||
state = STATE_REPLAY_ENVIRONMENT;
|
||||
return getFromRadio(buf);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case STATE_REPLAY_ENVIRONMENT: {
|
||||
if (replayEnvironmentOrder.empty() && replayEnvironmentIndex == 0)
|
||||
beginReplayEnvironment();
|
||||
prefetchReplayEnvironment();
|
||||
|
||||
meshtastic_MeshPacket pkt = {};
|
||||
bool havePkt = false;
|
||||
{
|
||||
concurrency::LockGuard guard(&nodeInfoMutex);
|
||||
if (!replayQueue.empty()) {
|
||||
pkt = replayQueue.front();
|
||||
replayQueue.pop_front();
|
||||
havePkt = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (havePkt) {
|
||||
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_packet_tag;
|
||||
fromRadioScratch.packet = pkt;
|
||||
} else {
|
||||
LOG_DEBUG("Done replaying environment count=%u millis=%u", (unsigned)replayEnvironmentIndex, millis());
|
||||
state = STATE_REPLAY_STATUS;
|
||||
return getFromRadio(buf);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case STATE_REPLAY_STATUS: {
|
||||
if (replayStatusOrder.empty() && replayStatusIndex == 0)
|
||||
beginReplayStatus();
|
||||
prefetchReplayStatus();
|
||||
|
||||
meshtastic_MeshPacket pkt = {};
|
||||
bool havePkt = false;
|
||||
{
|
||||
concurrency::LockGuard guard(&nodeInfoMutex);
|
||||
if (!replayQueue.empty()) {
|
||||
pkt = replayQueue.front();
|
||||
replayQueue.pop_front();
|
||||
havePkt = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (havePkt) {
|
||||
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_packet_tag;
|
||||
fromRadioScratch.packet = pkt;
|
||||
} else {
|
||||
LOG_DEBUG("Done replaying status count=%u millis=%u", (unsigned)replayStatusIndex, millis());
|
||||
replayPositionOrder.clear();
|
||||
replayPositionOrder.shrink_to_fit();
|
||||
replayTelemetryOrder.clear();
|
||||
replayTelemetryOrder.shrink_to_fit();
|
||||
replayEnvironmentOrder.clear();
|
||||
replayEnvironmentOrder.shrink_to_fit();
|
||||
replayStatusOrder.clear();
|
||||
replayStatusOrder.shrink_to_fit();
|
||||
nodeInfoMutex.unlock();
|
||||
// Satellite-DB replay (positions/telemetry/environment/status) now happens
|
||||
// *after* config_complete_id, interleaved with live traffic in STATE_SEND_PACKETS.
|
||||
state = STATE_SEND_FILEMANIFEST;
|
||||
return getFromRadio(buf);
|
||||
}
|
||||
@@ -673,8 +563,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
case STATE_SEND_FILEMANIFEST: {
|
||||
LOG_DEBUG("FromRadio=STATE_SEND_FILEMANIFEST");
|
||||
// ONLY_NODES variants skip the manifest.
|
||||
if (config_state == filesManifest.size() || config_nonce == SPECIAL_NONCE_ONLY_NODES ||
|
||||
config_nonce == SPECIAL_NONCE_GRADIENT_ONLY_NODES) {
|
||||
if (config_state == filesManifest.size() || config_nonce == SPECIAL_NONCE_ONLY_NODES) {
|
||||
config_state = 0;
|
||||
filesManifest.clear();
|
||||
// Skip to complete packet
|
||||
@@ -719,6 +608,16 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_packet_tag;
|
||||
fromRadioScratch.packet = *packetForPhone;
|
||||
releasePhonePacket();
|
||||
} else if (replayPending()) {
|
||||
// No live packet pending — feed the phone one cached satellite-DB packet.
|
||||
// popReplayPacket advances through positions->telemetry->environment->status,
|
||||
// and flips replayPhase back to IDLE when everything has been drained.
|
||||
meshtastic_MeshPacket replayPkt;
|
||||
if (popReplayPacket(replayPkt)) {
|
||||
printPacket("replay packet to phone", &replayPkt);
|
||||
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_packet_tag;
|
||||
fromRadioScratch.packet = replayPkt;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -743,10 +642,19 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
void PhoneAPI::sendConfigComplete()
|
||||
{
|
||||
LOG_INFO("Config Send Complete millis=%u", millis());
|
||||
const bool shouldReplaySatellites = (config_nonce != SPECIAL_NONCE_ONLY_CONFIG);
|
||||
// The phone sees config_complete_id first (treats sync as done), then the cached
|
||||
// satellite-DB packets (positions / telemetry / environment / status) trickle in
|
||||
// afterward as ordinary mesh packets (except SPECIAL_NONCE_ONLY_CONFIG, which
|
||||
// skips node/satellite sync entirely). Any client that handles live POSITION_APP /
|
||||
// TELEMETRY_APP / NODE_STATUS_APP packets handles these identically. STM32WL and
|
||||
// other builds that compile the satellite DBs out produce no replay packets and
|
||||
// the phase advances to IDLE in microseconds.
|
||||
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_config_complete_id_tag;
|
||||
fromRadioScratch.config_complete_id = config_nonce;
|
||||
config_nonce = 0;
|
||||
state = STATE_SEND_PACKETS;
|
||||
replayPhase = shouldReplaySatellites ? REPLAY_PHASE_POSITIONS : REPLAY_PHASE_IDLE;
|
||||
if (api_type == TYPE_BLE) {
|
||||
service->api_state = service->STATE_BLE;
|
||||
} else if (api_type == TYPE_WIFI) {
|
||||
@@ -787,7 +695,8 @@ void PhoneAPI::prefetchNodeInfos()
|
||||
{
|
||||
bool added = false;
|
||||
bool wasEmpty = false;
|
||||
const bool gradient = clientWantsGradientSync();
|
||||
// Other-node NodeInfos always go out thin (no bundled position/device_metrics).
|
||||
// The post-config_complete_id replay drain delivers those as ordinary mesh packets.
|
||||
// Keep the queue topped up so BLE reads stay responsive even if DB fetches take a moment.
|
||||
{
|
||||
concurrency::LockGuard guard(&nodeInfoMutex);
|
||||
@@ -797,8 +706,7 @@ void PhoneAPI::prefetchNodeInfos()
|
||||
if (!nextNode)
|
||||
break;
|
||||
|
||||
auto info =
|
||||
gradient ? TypeConversions::ConvertToNodeInfoThin(nextNode) : TypeConversions::ConvertToNodeInfo(nextNode);
|
||||
auto info = TypeConversions::ConvertToNodeInfoThin(nextNode);
|
||||
bool isUs = info.num == nodeDB->getNodeNum();
|
||||
info.hops_away = isUs ? 0 : info.hops_away;
|
||||
info.last_heard = isUs ? getValidTime(RTCQualityFromNet) : info.last_heard;
|
||||
@@ -820,11 +728,20 @@ void PhoneAPI::prefetchNodeInfos()
|
||||
|
||||
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;
|
||||
pkt.from = num;
|
||||
pkt.to = nodeDB->getNodeNum();
|
||||
pkt.to = NODENUM_BROADCAST;
|
||||
pkt.id = generatePacketId();
|
||||
pkt.rx_time = pos.time;
|
||||
pkt.channel = 0;
|
||||
pkt.hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
||||
pkt.hop_start = pkt.hop_limit;
|
||||
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||
// Mark as if heard over the air, not internally generated
|
||||
pkt.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA;
|
||||
pkt.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
||||
pkt.decoded.portnum = meshtastic_PortNum_POSITION_APP;
|
||||
meshtastic_Position fullPos = TypeConversions::ConvertToPosition(pos);
|
||||
@@ -838,11 +755,18 @@ meshtastic_MeshPacket PhoneAPI::makeReplayTelemetryPacket(NodeNum num, const mes
|
||||
{
|
||||
meshtastic_MeshPacket pkt = meshtastic_MeshPacket_init_default;
|
||||
pkt.from = num;
|
||||
pkt.to = nodeDB->getNodeNum();
|
||||
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.channel = 0;
|
||||
pkt.hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
||||
pkt.hop_start = pkt.hop_limit;
|
||||
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.
|
||||
pkt.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA;
|
||||
pkt.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
||||
pkt.decoded.portnum = meshtastic_PortNum_TELEMETRY_APP;
|
||||
meshtastic_Telemetry fullTel = meshtastic_Telemetry_init_default;
|
||||
@@ -858,16 +782,12 @@ meshtastic_MeshPacket PhoneAPI::makeReplayTelemetryPacket(NodeNum num, const mes
|
||||
void PhoneAPI::beginReplayPositions()
|
||||
{
|
||||
#if MESHTASTIC_EXCLUDE_POSITIONDB
|
||||
// Build excluded entirely - leave the order list empty so the state arm
|
||||
// Build excluded entirely - leave the order list empty so the phase
|
||||
// immediately drains and advances.
|
||||
replayPositionOrder.clear();
|
||||
replayPositionIndex = 0;
|
||||
#else
|
||||
if (!clientWantsGradientSync()) {
|
||||
replayPositionOrder.clear();
|
||||
replayPositionIndex = 0;
|
||||
return;
|
||||
}
|
||||
// Caller (popReplayPacket) only invokes us when replayPhase is armed.
|
||||
// Snapshot the keyset at phase start so concurrent inserts/erases on the
|
||||
// map don't invalidate iteration. Skip our own node - the phone already
|
||||
// got our position bundled in STATE_SEND_OWN_NODEINFO.
|
||||
@@ -882,8 +802,6 @@ void PhoneAPI::prefetchReplayPositions()
|
||||
#if MESHTASTIC_EXCLUDE_POSITIONDB
|
||||
return;
|
||||
#else
|
||||
if (!clientWantsGradientSync())
|
||||
return;
|
||||
bool added = false;
|
||||
bool wasEmpty = false;
|
||||
{
|
||||
@@ -909,11 +827,6 @@ void PhoneAPI::beginReplayTelemetry()
|
||||
replayTelemetryOrder.clear();
|
||||
replayTelemetryIndex = 0;
|
||||
#else
|
||||
if (!clientWantsGradientSync()) {
|
||||
replayTelemetryOrder.clear();
|
||||
replayTelemetryIndex = 0;
|
||||
return;
|
||||
}
|
||||
replayTelemetryOrder = nodeDB->snapshotTelemetryNodeNums(nodeDB->getNodeNum());
|
||||
replayTelemetryIndex = 0;
|
||||
LOG_INFO("Begin telemetry replay: %u entries millis=%u", (unsigned)replayTelemetryOrder.size(), millis());
|
||||
@@ -925,8 +838,6 @@ void PhoneAPI::prefetchReplayTelemetry()
|
||||
#if MESHTASTIC_EXCLUDE_TELEMETRYDB
|
||||
return;
|
||||
#else
|
||||
if (!clientWantsGradientSync())
|
||||
return;
|
||||
bool added = false;
|
||||
bool wasEmpty = false;
|
||||
{
|
||||
@@ -950,10 +861,17 @@ meshtastic_MeshPacket PhoneAPI::makeReplayEnvironmentPacket(uint32_t num, const
|
||||
{
|
||||
meshtastic_MeshPacket pkt = meshtastic_MeshPacket_init_default;
|
||||
pkt.from = num;
|
||||
pkt.to = nodeDB->getNodeNum();
|
||||
pkt.to = NODENUM_BROADCAST;
|
||||
pkt.id = generatePacketId();
|
||||
const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
|
||||
pkt.rx_time = header ? header->last_heard : 0;
|
||||
pkt.channel = 0;
|
||||
pkt.hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
||||
pkt.hop_start = pkt.hop_limit;
|
||||
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.
|
||||
pkt.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA;
|
||||
pkt.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
||||
pkt.decoded.portnum = meshtastic_PortNum_TELEMETRY_APP;
|
||||
meshtastic_Telemetry fullTel = meshtastic_Telemetry_init_default;
|
||||
@@ -972,11 +890,6 @@ void PhoneAPI::beginReplayEnvironment()
|
||||
replayEnvironmentOrder.clear();
|
||||
replayEnvironmentIndex = 0;
|
||||
#else
|
||||
if (!clientWantsGradientSync()) {
|
||||
replayEnvironmentOrder.clear();
|
||||
replayEnvironmentIndex = 0;
|
||||
return;
|
||||
}
|
||||
replayEnvironmentOrder = nodeDB->snapshotEnvironmentNodeNums(nodeDB->getNodeNum());
|
||||
replayEnvironmentIndex = 0;
|
||||
LOG_INFO("Begin environment replay: %u entries millis=%u", (unsigned)replayEnvironmentOrder.size(), millis());
|
||||
@@ -988,8 +901,6 @@ void PhoneAPI::prefetchReplayEnvironment()
|
||||
#if MESHTASTIC_EXCLUDE_ENVIRONMENTDB
|
||||
return;
|
||||
#else
|
||||
if (!clientWantsGradientSync())
|
||||
return;
|
||||
bool added = false;
|
||||
bool wasEmpty = false;
|
||||
{
|
||||
@@ -1013,11 +924,17 @@ meshtastic_MeshPacket PhoneAPI::makeReplayStatusPacket(uint32_t num, const mesht
|
||||
{
|
||||
meshtastic_MeshPacket pkt = meshtastic_MeshPacket_init_default;
|
||||
pkt.from = num;
|
||||
pkt.to = nodeDB->getNodeNum();
|
||||
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.channel = 0;
|
||||
pkt.hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
||||
pkt.hop_start = pkt.hop_limit;
|
||||
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;
|
||||
pkt.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
||||
pkt.decoded.portnum = meshtastic_PortNum_NODE_STATUS_APP;
|
||||
size_t len =
|
||||
@@ -1032,11 +949,6 @@ void PhoneAPI::beginReplayStatus()
|
||||
replayStatusOrder.clear();
|
||||
replayStatusIndex = 0;
|
||||
#else
|
||||
if (!clientWantsGradientSync()) {
|
||||
replayStatusOrder.clear();
|
||||
replayStatusIndex = 0;
|
||||
return;
|
||||
}
|
||||
replayStatusOrder = nodeDB->snapshotStatusNodeNums(nodeDB->getNodeNum());
|
||||
replayStatusIndex = 0;
|
||||
LOG_INFO("Begin status replay: %u entries millis=%u", (unsigned)replayStatusOrder.size(), millis());
|
||||
@@ -1048,8 +960,6 @@ void PhoneAPI::prefetchReplayStatus()
|
||||
#if MESHTASTIC_EXCLUDE_STATUSDB
|
||||
return;
|
||||
#else
|
||||
if (!clientWantsGradientSync())
|
||||
return;
|
||||
bool added = false;
|
||||
bool wasEmpty = false;
|
||||
{
|
||||
@@ -1069,6 +979,94 @@ void PhoneAPI::prefetchReplayStatus()
|
||||
#endif
|
||||
}
|
||||
|
||||
// Pop one cached satellite-DB packet from the active replay phase.
|
||||
// Phases drain in order: positions -> telemetry -> environment -> status.
|
||||
// When the current phase's cursor is exhausted (queue empty AND no more entries
|
||||
// to snapshot), advance to the next phase. When all four phases are done,
|
||||
// flip replayPhase back to IDLE and release the snapshot vectors.
|
||||
//
|
||||
// Returns true if a packet was placed in `out`; false if everything is drained.
|
||||
bool PhoneAPI::popReplayPacket(meshtastic_MeshPacket &out)
|
||||
{
|
||||
while (replayPhase != REPLAY_PHASE_IDLE) {
|
||||
// Prime the active phase: seed the snapshot vector on first entry,
|
||||
// top up replayQueue from the snapshot up to kReplayPrefetchDepth.
|
||||
switch (replayPhase) {
|
||||
case REPLAY_PHASE_POSITIONS:
|
||||
if (replayPositionOrder.empty() && replayPositionIndex == 0)
|
||||
beginReplayPositions();
|
||||
prefetchReplayPositions();
|
||||
break;
|
||||
case REPLAY_PHASE_TELEMETRY:
|
||||
if (replayTelemetryOrder.empty() && replayTelemetryIndex == 0)
|
||||
beginReplayTelemetry();
|
||||
prefetchReplayTelemetry();
|
||||
break;
|
||||
case REPLAY_PHASE_ENVIRONMENT:
|
||||
if (replayEnvironmentOrder.empty() && replayEnvironmentIndex == 0)
|
||||
beginReplayEnvironment();
|
||||
prefetchReplayEnvironment();
|
||||
break;
|
||||
case REPLAY_PHASE_STATUS:
|
||||
if (replayStatusOrder.empty() && replayStatusIndex == 0)
|
||||
beginReplayStatus();
|
||||
prefetchReplayStatus();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
{
|
||||
concurrency::LockGuard guard(&nodeInfoMutex);
|
||||
if (!replayQueue.empty()) {
|
||||
out = replayQueue.front();
|
||||
replayQueue.pop_front();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Queue empty AND no more entries to feed it — phase is exhausted.
|
||||
advanceReplayPhase();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void PhoneAPI::advanceReplayPhase()
|
||||
{
|
||||
switch (replayPhase) {
|
||||
case REPLAY_PHASE_POSITIONS:
|
||||
LOG_DEBUG("Replay drain: positions done (count=%u) millis=%u", (unsigned)replayPositionIndex, millis());
|
||||
replayPhase = REPLAY_PHASE_TELEMETRY;
|
||||
break;
|
||||
case REPLAY_PHASE_TELEMETRY:
|
||||
LOG_DEBUG("Replay drain: telemetry done (count=%u) millis=%u", (unsigned)replayTelemetryIndex, millis());
|
||||
replayPhase = REPLAY_PHASE_ENVIRONMENT;
|
||||
break;
|
||||
case REPLAY_PHASE_ENVIRONMENT:
|
||||
LOG_DEBUG("Replay drain: environment done (count=%u) millis=%u", (unsigned)replayEnvironmentIndex, millis());
|
||||
replayPhase = REPLAY_PHASE_STATUS;
|
||||
break;
|
||||
case REPLAY_PHASE_STATUS:
|
||||
LOG_INFO("Replay drain complete (status count=%u) millis=%u", (unsigned)replayStatusIndex, millis());
|
||||
replayPositionOrder.clear();
|
||||
replayPositionOrder.shrink_to_fit();
|
||||
replayTelemetryOrder.clear();
|
||||
replayTelemetryOrder.shrink_to_fit();
|
||||
replayEnvironmentOrder.clear();
|
||||
replayEnvironmentOrder.shrink_to_fit();
|
||||
replayStatusOrder.clear();
|
||||
replayStatusOrder.shrink_to_fit();
|
||||
replayPositionIndex = 0;
|
||||
replayTelemetryIndex = 0;
|
||||
replayEnvironmentIndex = 0;
|
||||
replayStatusIndex = 0;
|
||||
replayPhase = REPLAY_PHASE_IDLE;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void PhoneAPI::releaseMqttClientProxyPhonePacket()
|
||||
{
|
||||
if (mqttClientProxyMessageForPhone) {
|
||||
@@ -1115,31 +1113,6 @@ bool PhoneAPI::available()
|
||||
PREFETCH_NODEINFO:
|
||||
prefetchNodeInfos();
|
||||
return true;
|
||||
case STATE_REPLAY_POSITIONS: {
|
||||
// Prime the iterator if we haven't yet, then top up the queue.
|
||||
if (replayPositionOrder.empty() && replayPositionIndex == 0)
|
||||
beginReplayPositions();
|
||||
prefetchReplayPositions();
|
||||
return true; // Always advance state machine; arm itself transitions when drained
|
||||
}
|
||||
case STATE_REPLAY_TELEMETRY: {
|
||||
if (replayTelemetryOrder.empty() && replayTelemetryIndex == 0)
|
||||
beginReplayTelemetry();
|
||||
prefetchReplayTelemetry();
|
||||
return true;
|
||||
}
|
||||
case STATE_REPLAY_ENVIRONMENT: {
|
||||
if (replayEnvironmentOrder.empty() && replayEnvironmentIndex == 0)
|
||||
beginReplayEnvironment();
|
||||
prefetchReplayEnvironment();
|
||||
return true;
|
||||
}
|
||||
case STATE_REPLAY_STATUS: {
|
||||
if (replayStatusOrder.empty() && replayStatusIndex == 0)
|
||||
beginReplayStatus();
|
||||
prefetchReplayStatus();
|
||||
return true;
|
||||
}
|
||||
case STATE_SEND_PACKETS: {
|
||||
if (!queueStatusPacketForPhone)
|
||||
queueStatusPacketForPhone = service->getQueueStatusForPhone();
|
||||
@@ -1171,7 +1144,11 @@ bool PhoneAPI::available()
|
||||
if (!packetForPhone)
|
||||
packetForPhone = service->getForPhone();
|
||||
hasPacket = !!packetForPhone;
|
||||
return hasPacket;
|
||||
if (hasPacket)
|
||||
return true;
|
||||
// Trailing replay drain — feeds cached satellite-DB packets alongside
|
||||
// (lower priority than) live traffic.
|
||||
return replayPending();
|
||||
}
|
||||
default:
|
||||
LOG_ERROR("PhoneAPI::available unexpected state %d", state);
|
||||
|
||||
+22
-15
@@ -26,9 +26,6 @@
|
||||
|
||||
#define SPECIAL_NONCE_ONLY_CONFIG 69420
|
||||
#define SPECIAL_NONCE_ONLY_NODES 69421 // ( ͡° ͜ʖ ͡°)
|
||||
// Gradient sync: phone sends one of these to opt into thin-header + replay.
|
||||
#define SPECIAL_NONCE_GRADIENT_SYNC 69422
|
||||
#define SPECIAL_NONCE_GRADIENT_ONLY_NODES 69423
|
||||
|
||||
/**
|
||||
* Provides our protobuf based API which phone/PC clients can use to talk to our device
|
||||
@@ -52,15 +49,22 @@ class PhoneAPI
|
||||
STATE_SEND_CONFIG, // Replacement for the old Radioconfig
|
||||
STATE_SEND_MODULECONFIG, // Send Module specific config
|
||||
STATE_SEND_OTHER_NODEINFOS, // states progress in this order as the device sends to to the client
|
||||
// Drain satellite DBs as synthetic POSITION_APP / TELEMETRY_APP /
|
||||
// NODE_STATUS_APP packets when the phone opted into gradient sync.
|
||||
STATE_REPLAY_POSITIONS,
|
||||
STATE_REPLAY_TELEMETRY,
|
||||
STATE_REPLAY_ENVIRONMENT,
|
||||
STATE_REPLAY_STATUS,
|
||||
STATE_SEND_FILEMANIFEST, // Send file manifest
|
||||
STATE_SEND_FILEMANIFEST, // Send file manifest
|
||||
STATE_SEND_COMPLETE_ID,
|
||||
STATE_SEND_PACKETS // send packets or debug strings
|
||||
STATE_SEND_PACKETS // live mesh packets + any cached satellite-DB replay that trails sync completion
|
||||
};
|
||||
|
||||
// Satellite-DB replay (positions / telemetry / environment / status) used to live
|
||||
// as four top-level states between STATE_SEND_OTHER_NODEINFOS and STATE_SEND_FILEMANIFEST.
|
||||
// It now drains *after* config_complete_id has been emitted: the phone considers the
|
||||
// initial sync done as soon as headers + manifest are delivered, and the cached
|
||||
// position/telemetry/etc. trickle in alongside live mesh traffic inside STATE_SEND_PACKETS.
|
||||
enum ReplayPhase : uint8_t {
|
||||
REPLAY_PHASE_IDLE = 0, // not replaying (legacy clients, no-op DBs, or replay finished)
|
||||
REPLAY_PHASE_POSITIONS,
|
||||
REPLAY_PHASE_TELEMETRY,
|
||||
REPLAY_PHASE_ENVIRONMENT,
|
||||
REPLAY_PHASE_STATUS,
|
||||
};
|
||||
|
||||
State state = STATE_SEND_NOTHING;
|
||||
@@ -114,6 +118,7 @@ class PhoneAPI
|
||||
size_t replayTelemetryIndex = 0;
|
||||
size_t replayEnvironmentIndex = 0;
|
||||
size_t replayStatusIndex = 0;
|
||||
ReplayPhase replayPhase = REPLAY_PHASE_IDLE; // armed by sendConfigComplete() for full/default sync
|
||||
|
||||
meshtastic_ToRadio toRadioScratch = {
|
||||
0}; // this is a static scratch object, any data must be copied elsewhere before returning
|
||||
@@ -164,10 +169,6 @@ class PhoneAPI
|
||||
|
||||
bool isConnected() { return state != STATE_SEND_NOTHING; }
|
||||
bool isSendingPackets() { return state == STATE_SEND_PACKETS; }
|
||||
bool clientWantsGradientSync() const
|
||||
{
|
||||
return config_nonce == SPECIAL_NONCE_GRADIENT_SYNC || config_nonce == SPECIAL_NONCE_GRADIENT_ONLY_NODES;
|
||||
}
|
||||
|
||||
protected:
|
||||
/// Our fromradio packet while it is being assembled
|
||||
@@ -229,6 +230,12 @@ class PhoneAPI
|
||||
meshtastic_MeshPacket makeReplayEnvironmentPacket(uint32_t num, const meshtastic_EnvironmentMetrics &env);
|
||||
meshtastic_MeshPacket makeReplayStatusPacket(uint32_t num, const meshtastic_StatusMessage &status);
|
||||
|
||||
// Post-sync replay drain: pop one cached packet from the active phase, advancing
|
||||
// through positions -> telemetry -> environment -> status until everything is drained.
|
||||
bool popReplayPacket(meshtastic_MeshPacket &out);
|
||||
void advanceReplayPhase();
|
||||
bool replayPending() const { return replayPhase != REPLAY_PHASE_IDLE; }
|
||||
|
||||
void releaseMqttClientProxyPhonePacket();
|
||||
|
||||
void releaseClientNotification();
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
#include "PositionPrecision.h"
|
||||
#include "Channels.h"
|
||||
#include "mesh-pb-constants.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
uint32_t getPositionPrecisionForChannel(uint8_t channelIndex)
|
||||
{
|
||||
const meshtastic_Channel &channel = channels.getByIndex(channelIndex);
|
||||
|
||||
if (channel.settings.has_module_settings) {
|
||||
return channel.settings.module_settings.position_precision;
|
||||
} else if (channel.role == meshtastic_Channel_Role_PRIMARY) {
|
||||
return 32;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static int32_t truncateCoordinate(int32_t coordinate, uint32_t precision)
|
||||
{
|
||||
uint32_t coordinateBits = static_cast<uint32_t>(coordinate);
|
||||
uint32_t truncated = coordinateBits & (UINT32_MAX << (32 - precision));
|
||||
|
||||
// Use the middle of the possible location, not the low edge of the bucket.
|
||||
truncated += (1UL << (31 - precision));
|
||||
|
||||
return static_cast<int32_t>(truncated);
|
||||
}
|
||||
|
||||
void applyPositionPrecision(meshtastic_Position &position, uint32_t precision)
|
||||
{
|
||||
if (precision == 0) {
|
||||
uint32_t time = position.time;
|
||||
position = meshtastic_Position_init_default;
|
||||
position.time = time;
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t effectivePrecision = precision > 32 ? 32 : precision;
|
||||
position.precision_bits = effectivePrecision;
|
||||
|
||||
if (effectivePrecision < 32) {
|
||||
position.latitude_i = truncateCoordinate(position.latitude_i, effectivePrecision);
|
||||
position.longitude_i = truncateCoordinate(position.longitude_i, effectivePrecision);
|
||||
}
|
||||
}
|
||||
|
||||
bool applyPositionPrecision(meshtastic_MeshPacket &packet, uint32_t precision)
|
||||
{
|
||||
if (packet.which_payload_variant != meshtastic_MeshPacket_decoded_tag ||
|
||||
packet.decoded.portnum != meshtastic_PortNum_POSITION_APP) {
|
||||
return true;
|
||||
}
|
||||
|
||||
meshtastic_Position position = meshtastic_Position_init_default;
|
||||
if (!pb_decode_from_bytes(packet.decoded.payload.bytes, packet.decoded.payload.size, &meshtastic_Position_msg, &position)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
applyPositionPrecision(position, precision);
|
||||
packet.decoded.payload.size = pb_encode_to_bytes(packet.decoded.payload.bytes, sizeof(packet.decoded.payload.bytes),
|
||||
&meshtastic_Position_msg, &position);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool applyPositionPrecisionForChannel(meshtastic_MeshPacket &packet, uint8_t channelIndex)
|
||||
{
|
||||
if (packet.which_payload_variant != meshtastic_MeshPacket_decoded_tag ||
|
||||
packet.decoded.portnum != meshtastic_PortNum_POSITION_APP) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return applyPositionPrecision(packet, getPositionPrecisionForChannel(channelIndex));
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "meshtastic/mesh.pb.h"
|
||||
#include <stdint.h>
|
||||
|
||||
uint32_t getPositionPrecisionForChannel(uint8_t channelIndex);
|
||||
void applyPositionPrecision(meshtastic_Position &position, uint32_t precision);
|
||||
bool applyPositionPrecision(meshtastic_MeshPacket &packet, uint32_t precision);
|
||||
bool applyPositionPrecisionForChannel(meshtastic_MeshPacket &packet, uint8_t channelIndex);
|
||||
@@ -46,16 +46,6 @@ RadioLibInterface::RadioLibInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE c
|
||||
#endif
|
||||
}
|
||||
|
||||
RadioLibInterface::~RadioLibInterface()
|
||||
{
|
||||
// If the static `instance` pointer still references us, clear it.
|
||||
// A later successful init() may have replaced `instance` with a newer
|
||||
// interface — don't clobber that case.
|
||||
if (instance == this) {
|
||||
instance = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
// ESP32 doesn't use that flag
|
||||
#define YIELD_FROM_ISR(x) portYIELD_FROM_ISR()
|
||||
@@ -434,6 +424,9 @@ void RadioLibInterface::completeSending()
|
||||
// that can take a long time
|
||||
auto p = sendingPacket;
|
||||
sendingPacket = NULL;
|
||||
#ifdef LED_LORA
|
||||
digitalWrite(LED_LORA, LED_STATE_OFF);
|
||||
#endif
|
||||
|
||||
if (p) {
|
||||
// Packet has been sent, count it toward our TX airtime utilization.
|
||||
@@ -555,6 +548,9 @@ void RadioLibInterface::pollMissedIrqs()
|
||||
if (isReceiving) {
|
||||
checkRxDoneIrqFlag();
|
||||
}
|
||||
if (sendingPacket) {
|
||||
checkTxDoneIrqFlag();
|
||||
}
|
||||
}
|
||||
|
||||
void RadioLibInterface::resetAGC()
|
||||
@@ -570,6 +566,14 @@ void RadioLibInterface::checkRxDoneIrqFlag()
|
||||
}
|
||||
}
|
||||
|
||||
void RadioLibInterface::checkTxDoneIrqFlag()
|
||||
{
|
||||
if (iface->checkIrq(RADIOLIB_IRQ_TX_DONE)) {
|
||||
LOG_WARN("caught missed TX_DONE");
|
||||
notify(ISR_TX, true);
|
||||
}
|
||||
}
|
||||
|
||||
void RadioLibInterface::configHardwareForSend()
|
||||
{
|
||||
powerMon->setState(meshtastic_PowerMon_State_Lora_TXOn);
|
||||
@@ -611,6 +615,9 @@ bool RadioLibInterface::startSend(meshtastic_MeshPacket *txp)
|
||||
enableInterrupt(isrTxLevel0);
|
||||
lastTxStart = millis();
|
||||
printPacket("Started Tx", txp);
|
||||
#ifdef LED_LORA
|
||||
digitalWrite(LED_LORA, LED_STATE_ON);
|
||||
#endif
|
||||
}
|
||||
|
||||
return res == RADIOLIB_ERR_NONE;
|
||||
|
||||
@@ -104,6 +104,13 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
|
||||
*/
|
||||
static RadioLibInterface *instance;
|
||||
|
||||
/** Clear instance on destruction so stale pointer checks in loop() are safe */
|
||||
virtual ~RadioLibInterface()
|
||||
{
|
||||
if (instance == this)
|
||||
instance = nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Glue functions called from ISR land
|
||||
*/
|
||||
@@ -136,13 +143,6 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
|
||||
RadioLibInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst,
|
||||
RADIOLIB_PIN_TYPE busy, PhysicalLayer *iface = NULL);
|
||||
|
||||
/**
|
||||
* Clear the static `instance` pointer if it still points at us, so callers
|
||||
* that check `RadioLibInterface::instance != nullptr` don't dereference a
|
||||
* freed object after a failed init() + unique_ptr reset.
|
||||
*/
|
||||
virtual ~RadioLibInterface();
|
||||
|
||||
virtual ErrorCode send(meshtastic_MeshPacket *p) override;
|
||||
|
||||
/**
|
||||
@@ -293,4 +293,5 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
|
||||
bool removePendingTXPacket(NodeNum from, PacketId id, uint32_t hop_limit_lt) override;
|
||||
|
||||
void checkRxDoneIrqFlag();
|
||||
void checkTxDoneIrqFlag();
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "MeshRadio.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PositionPrecision.h"
|
||||
#include "RTC.h"
|
||||
|
||||
#include "configuration.h"
|
||||
@@ -367,6 +368,11 @@ ErrorCode Router::send(meshtastic_MeshPacket *p)
|
||||
}
|
||||
|
||||
fixPriority(p); // Before encryption, fix the priority if it's unset
|
||||
if (!applyPositionPrecisionForChannel(*p, p->channel)) {
|
||||
LOG_ERROR("Dropping malformed position packet before send");
|
||||
packetPool.release(p);
|
||||
return meshtastic_Routing_Error_BAD_REQUEST;
|
||||
}
|
||||
|
||||
// If the packet is not yet encrypted, do so now
|
||||
if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
|
||||
|
||||
@@ -37,6 +37,7 @@ meshtastic_NodeInfo TypeConversions::ConvertToNodeInfo(const meshtastic_NodeInfo
|
||||
info.position.altitude = position->altitude;
|
||||
info.position.location_source = position->location_source;
|
||||
info.position.time = position->time;
|
||||
info.position.precision_bits = position->precision_bits;
|
||||
}
|
||||
if (nodeInfoLiteHasUser(lite)) {
|
||||
info.has_user = true;
|
||||
@@ -71,6 +72,7 @@ meshtastic_PositionLite TypeConversions::ConvertToPositionLite(meshtastic_Positi
|
||||
lite.altitude = position.altitude;
|
||||
lite.location_source = position.location_source;
|
||||
lite.time = position.time;
|
||||
lite.precision_bits = position.precision_bits;
|
||||
|
||||
return lite;
|
||||
}
|
||||
@@ -89,6 +91,11 @@ meshtastic_Position TypeConversions::ConvertToPosition(meshtastic_PositionLite l
|
||||
position.altitude = lite.altitude;
|
||||
position.location_source = lite.location_source;
|
||||
position.time = lite.time;
|
||||
// Preserve the peer's broadcast precision; falls back to 0 for entries cached
|
||||
// before the precision_bits field existed in PositionLite (pre-migration data).
|
||||
// iOS treats 0 as "unspecified precision" and won't render the pin — so for
|
||||
// unset values, declare full precision so the stored lat/lon renders as a point.
|
||||
position.precision_bits = lite.precision_bits == 0 ? 32 : lite.precision_bits;
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "configuration.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
#if HAS_ETHERNET && !defined(USE_WS5500)
|
||||
#if HAS_ETHERNET && !defined(USE_WS5500) && !defined(USE_CH390D)
|
||||
|
||||
#include "ethServerAPI.h"
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "ServerAPI.h"
|
||||
#ifndef USE_WS5500
|
||||
#if !defined(USE_WS5500) && !defined(USE_CH390D)
|
||||
#include <RAK13800_W5100S.h>
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#include <RAK13800_W5100S.h>
|
||||
#include <SPI.h>
|
||||
|
||||
#if HAS_NETWORKING
|
||||
#if HAS_NETWORKING && !defined(USE_WS5500) && !defined(USE_CH390D)
|
||||
|
||||
#ifndef DISABLE_NTP
|
||||
#include <NTPClient.h>
|
||||
|
||||
@@ -15,6 +15,9 @@ PB_BIND(meshtastic_AdminMessage_InputEvent, meshtastic_AdminMessage_InputEvent,
|
||||
PB_BIND(meshtastic_AdminMessage_OTAEvent, meshtastic_AdminMessage_OTAEvent, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_LockdownAuth, meshtastic_LockdownAuth, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_HamParameters, meshtastic_HamParameters, AUTO)
|
||||
|
||||
|
||||
|
||||
@@ -130,6 +130,76 @@ typedef struct _meshtastic_AdminMessage_OTAEvent {
|
||||
meshtastic_AdminMessage_OTAEvent_ota_hash_t ota_hash;
|
||||
} meshtastic_AdminMessage_OTAEvent;
|
||||
|
||||
typedef PB_BYTES_ARRAY_T(32) meshtastic_LockdownAuth_passphrase_t;
|
||||
/* Lockdown passphrase delivery payload.
|
||||
|
||||
One message handles three operations distinguished by content:
|
||||
- Provision (first-time): passphrase set, lock_now=false. Firmware
|
||||
generates DEK, wraps with passphrase-derived KEK, persists.
|
||||
- Unlock: passphrase set, lock_now=false. Firmware verifies
|
||||
passphrase against stored DEK, unlocks storage, authorizes the
|
||||
connection that delivered this packet.
|
||||
- Lock now: lock_now=true, passphrase ignored. Firmware revokes
|
||||
all client auth and reboots into the locked state.
|
||||
|
||||
Firmware decides between provision and unlock based on its own state
|
||||
(whether a DEK file already exists). Clients do not need to track
|
||||
which case applies. */
|
||||
typedef struct _meshtastic_LockdownAuth {
|
||||
/* Passphrase bytes (1-32). Empty when lock_now is true.
|
||||
Capped to 32 to match the proto cap on related security fields. */
|
||||
meshtastic_LockdownAuth_passphrase_t passphrase;
|
||||
/* Optional override of the boot-count token TTL granted on success.
|
||||
0 = use firmware default (TOKEN_DEFAULT_BOOTS).
|
||||
On reboot the firmware decrements this; when it reaches 0 the
|
||||
device boots fully locked and requires a fresh passphrase. */
|
||||
uint32_t boots_remaining;
|
||||
/* Optional wall-clock expiry for the unlock token, as absolute
|
||||
Unix-epoch seconds. 0 = no time limit (only the boot-count TTL
|
||||
applies). On boot, if the device RTC is set and now > this value,
|
||||
the token is treated as expired. */
|
||||
uint32_t valid_until_epoch;
|
||||
/* If true, ignore passphrase fields, immediately revoke all
|
||||
connection-level admin authorization, and reboot the device into
|
||||
the locked state. Always honoured regardless of current lock state. */
|
||||
bool lock_now;
|
||||
/* Optional per-boot uptime cap on the unlocked session, in seconds.
|
||||
0 = unlimited (token-only enforcement, suitable for unattended
|
||||
tower / infrastructure nodes).
|
||||
|
||||
When non-zero, the firmware arms an uptime timer at unlock. On
|
||||
each expiry, while there is still boot-count budget, the firmware
|
||||
decrements the on-flash boot count in place, revokes per-
|
||||
connection admin auth (clients must re-authenticate to see
|
||||
content), re-engages the screen lock, and re-arms the timer
|
||||
without rebooting. Mesh routing keeps running across session
|
||||
boundaries; only when the boot-count budget reaches zero does
|
||||
the device hard-lock and reboot.
|
||||
|
||||
Total exposure ceiling = ((resolved boot count) + 1) * max_session_seconds.
|
||||
The +1 accounts for the initial passphrase-unlocked session
|
||||
itself, since boots_remaining is the number of subsequent
|
||||
session rolls (each consuming one boot from the rollback ledger).
|
||||
The resolved boot count is the value the firmware writes into the
|
||||
token at unlock time: the client-supplied boots_remaining when
|
||||
non-zero, otherwise the firmware default (TOKEN_DEFAULT_BOOTS).
|
||||
Note that boots_remaining == 0 in this message means "use firmware
|
||||
default", NOT "zero boots" — a client computing the ceiling for
|
||||
display should mirror that resolution rather than multiplying the
|
||||
raw request value.
|
||||
|
||||
The cap is persisted in the token, so it survives token-based
|
||||
auto-unlock across reboots. Explicit operator Lock Now still
|
||||
deletes the token and forces passphrase re-entry.
|
||||
|
||||
Uses millis() (CPU uptime), not wall-clock time, so the cap is
|
||||
immune to GPS spoofing, RTC backup-battery removal, and Faraday
|
||||
cage isolation — none of those move the uptime counter. The only
|
||||
way to reset the session clock is a reboot, which costs a boot
|
||||
from the on-flash, HMAC-bound counter. */
|
||||
uint32_t max_session_seconds;
|
||||
} meshtastic_LockdownAuth;
|
||||
|
||||
/* Parameters for setting up Meshtastic for ameteur radio usage */
|
||||
typedef struct _meshtastic_HamParameters {
|
||||
/* Amateur radio call sign, eg. KD2ABC */
|
||||
@@ -384,6 +454,15 @@ typedef struct _meshtastic_AdminMessage {
|
||||
meshtastic_AdminMessage_OTAEvent ota_request;
|
||||
/* Parameters and sensor configuration */
|
||||
meshtastic_SensorConfig sensor_config;
|
||||
/* Lockdown passphrase delivery / unlock / lock-now command for hardened
|
||||
firmware builds (see MESHTASTIC_LOCKDOWN). Used to provision the
|
||||
passphrase on first boot, unlock encrypted storage on subsequent
|
||||
reboots, re-verify on already-unlocked devices to authorize a new
|
||||
client connection, or immediately re-lock the device.
|
||||
|
||||
Replaces the earlier scheme that repurposed SecurityConfig.private_key
|
||||
to carry passphrase bytes; that hack is retired. */
|
||||
meshtastic_LockdownAuth lockdown_auth;
|
||||
};
|
||||
/* The node generates this key and sends it with any get_x_response packets.
|
||||
The client MUST include the same key with any set_x commands. Key expires after 300 seconds.
|
||||
@@ -429,6 +508,7 @@ extern "C" {
|
||||
|
||||
|
||||
|
||||
|
||||
#define meshtastic_KeyVerificationAdmin_message_type_ENUMTYPE meshtastic_KeyVerificationAdmin_MessageType
|
||||
|
||||
|
||||
@@ -441,6 +521,7 @@ extern "C" {
|
||||
#define meshtastic_AdminMessage_init_default {0, {0}, {0, {0}}}
|
||||
#define meshtastic_AdminMessage_InputEvent_init_default {0, 0, 0, 0}
|
||||
#define meshtastic_AdminMessage_OTAEvent_init_default {_meshtastic_OTAMode_MIN, {0, {0}}}
|
||||
#define meshtastic_LockdownAuth_init_default {{0, {0}}, 0, 0, 0, 0}
|
||||
#define meshtastic_HamParameters_init_default {"", 0, 0, ""}
|
||||
#define meshtastic_NodeRemoteHardwarePinsResponse_init_default {0, {meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default}}
|
||||
#define meshtastic_SharedContact_init_default {0, false, meshtastic_User_init_default, 0, 0}
|
||||
@@ -453,6 +534,7 @@ extern "C" {
|
||||
#define meshtastic_AdminMessage_init_zero {0, {0}, {0, {0}}}
|
||||
#define meshtastic_AdminMessage_InputEvent_init_zero {0, 0, 0, 0}
|
||||
#define meshtastic_AdminMessage_OTAEvent_init_zero {_meshtastic_OTAMode_MIN, {0, {0}}}
|
||||
#define meshtastic_LockdownAuth_init_zero {{0, {0}}, 0, 0, 0, 0}
|
||||
#define meshtastic_HamParameters_init_zero {"", 0, 0, ""}
|
||||
#define meshtastic_NodeRemoteHardwarePinsResponse_init_zero {0, {meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero}}
|
||||
#define meshtastic_SharedContact_init_zero {0, false, meshtastic_User_init_zero, 0, 0}
|
||||
@@ -470,6 +552,11 @@ extern "C" {
|
||||
#define meshtastic_AdminMessage_InputEvent_touch_y_tag 4
|
||||
#define meshtastic_AdminMessage_OTAEvent_reboot_ota_mode_tag 1
|
||||
#define meshtastic_AdminMessage_OTAEvent_ota_hash_tag 2
|
||||
#define meshtastic_LockdownAuth_passphrase_tag 1
|
||||
#define meshtastic_LockdownAuth_boots_remaining_tag 2
|
||||
#define meshtastic_LockdownAuth_valid_until_epoch_tag 3
|
||||
#define meshtastic_LockdownAuth_lock_now_tag 4
|
||||
#define meshtastic_LockdownAuth_max_session_seconds_tag 5
|
||||
#define meshtastic_HamParameters_call_sign_tag 1
|
||||
#define meshtastic_HamParameters_tx_power_tag 2
|
||||
#define meshtastic_HamParameters_frequency_tag 3
|
||||
@@ -560,6 +647,7 @@ extern "C" {
|
||||
#define meshtastic_AdminMessage_nodedb_reset_tag 100
|
||||
#define meshtastic_AdminMessage_ota_request_tag 102
|
||||
#define meshtastic_AdminMessage_sensor_config_tag 103
|
||||
#define meshtastic_AdminMessage_lockdown_auth_tag 104
|
||||
#define meshtastic_AdminMessage_session_passkey_tag 101
|
||||
|
||||
/* Struct field encoding specification for nanopb */
|
||||
@@ -621,7 +709,8 @@ X(a, STATIC, ONEOF, INT32, (payload_variant,factory_reset_config,factory
|
||||
X(a, STATIC, ONEOF, BOOL, (payload_variant,nodedb_reset,nodedb_reset), 100) \
|
||||
X(a, STATIC, SINGULAR, BYTES, session_passkey, 101) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,ota_request,ota_request), 102) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,sensor_config,sensor_config), 103)
|
||||
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,sensor_config,sensor_config), 103) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,lockdown_auth,lockdown_auth), 104)
|
||||
#define meshtastic_AdminMessage_CALLBACK NULL
|
||||
#define meshtastic_AdminMessage_DEFAULT NULL
|
||||
#define meshtastic_AdminMessage_payload_variant_get_channel_response_MSGTYPE meshtastic_Channel
|
||||
@@ -644,6 +733,7 @@ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,sensor_config,sensor_config)
|
||||
#define meshtastic_AdminMessage_payload_variant_key_verification_MSGTYPE meshtastic_KeyVerificationAdmin
|
||||
#define meshtastic_AdminMessage_payload_variant_ota_request_MSGTYPE meshtastic_AdminMessage_OTAEvent
|
||||
#define meshtastic_AdminMessage_payload_variant_sensor_config_MSGTYPE meshtastic_SensorConfig
|
||||
#define meshtastic_AdminMessage_payload_variant_lockdown_auth_MSGTYPE meshtastic_LockdownAuth
|
||||
|
||||
#define meshtastic_AdminMessage_InputEvent_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, UINT32, event_code, 1) \
|
||||
@@ -659,6 +749,15 @@ X(a, STATIC, SINGULAR, BYTES, ota_hash, 2)
|
||||
#define meshtastic_AdminMessage_OTAEvent_CALLBACK NULL
|
||||
#define meshtastic_AdminMessage_OTAEvent_DEFAULT NULL
|
||||
|
||||
#define meshtastic_LockdownAuth_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, BYTES, passphrase, 1) \
|
||||
X(a, STATIC, SINGULAR, UINT32, boots_remaining, 2) \
|
||||
X(a, STATIC, SINGULAR, UINT32, valid_until_epoch, 3) \
|
||||
X(a, STATIC, SINGULAR, BOOL, lock_now, 4) \
|
||||
X(a, STATIC, SINGULAR, UINT32, max_session_seconds, 5)
|
||||
#define meshtastic_LockdownAuth_CALLBACK NULL
|
||||
#define meshtastic_LockdownAuth_DEFAULT NULL
|
||||
|
||||
#define meshtastic_HamParameters_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, STRING, call_sign, 1) \
|
||||
X(a, STATIC, SINGULAR, INT32, tx_power, 2) \
|
||||
@@ -737,6 +836,7 @@ X(a, STATIC, OPTIONAL, UINT32, set_accuracy, 1)
|
||||
extern const pb_msgdesc_t meshtastic_AdminMessage_msg;
|
||||
extern const pb_msgdesc_t meshtastic_AdminMessage_InputEvent_msg;
|
||||
extern const pb_msgdesc_t meshtastic_AdminMessage_OTAEvent_msg;
|
||||
extern const pb_msgdesc_t meshtastic_LockdownAuth_msg;
|
||||
extern const pb_msgdesc_t meshtastic_HamParameters_msg;
|
||||
extern const pb_msgdesc_t meshtastic_NodeRemoteHardwarePinsResponse_msg;
|
||||
extern const pb_msgdesc_t meshtastic_SharedContact_msg;
|
||||
@@ -751,6 +851,7 @@ extern const pb_msgdesc_t meshtastic_SHTXX_config_msg;
|
||||
#define meshtastic_AdminMessage_fields &meshtastic_AdminMessage_msg
|
||||
#define meshtastic_AdminMessage_InputEvent_fields &meshtastic_AdminMessage_InputEvent_msg
|
||||
#define meshtastic_AdminMessage_OTAEvent_fields &meshtastic_AdminMessage_OTAEvent_msg
|
||||
#define meshtastic_LockdownAuth_fields &meshtastic_LockdownAuth_msg
|
||||
#define meshtastic_HamParameters_fields &meshtastic_HamParameters_msg
|
||||
#define meshtastic_NodeRemoteHardwarePinsResponse_fields &meshtastic_NodeRemoteHardwarePinsResponse_msg
|
||||
#define meshtastic_SharedContact_fields &meshtastic_SharedContact_msg
|
||||
@@ -768,6 +869,7 @@ extern const pb_msgdesc_t meshtastic_SHTXX_config_msg;
|
||||
#define meshtastic_AdminMessage_size 511
|
||||
#define meshtastic_HamParameters_size 31
|
||||
#define meshtastic_KeyVerificationAdmin_size 25
|
||||
#define meshtastic_LockdownAuth_size 54
|
||||
#define meshtastic_NodeRemoteHardwarePinsResponse_size 496
|
||||
#define meshtastic_SCD30_config_size 27
|
||||
#define meshtastic_SCD4X_config_size 29
|
||||
|
||||
@@ -33,6 +33,8 @@ typedef struct _meshtastic_PositionLite {
|
||||
uint32_t time;
|
||||
/* TODO: REPLACE */
|
||||
meshtastic_Position_LocSource location_source;
|
||||
/* Indicates the bits of precision set by the sending node */
|
||||
uint32_t precision_bits;
|
||||
} meshtastic_PositionLite;
|
||||
|
||||
typedef PB_BYTES_ARRAY_T(32) meshtastic_UserLite_public_key_t;
|
||||
@@ -211,7 +213,7 @@ extern "C" {
|
||||
#endif
|
||||
|
||||
/* Initializer values for message structs */
|
||||
#define meshtastic_PositionLite_init_default {0, 0, 0, 0, _meshtastic_Position_LocSource_MIN}
|
||||
#define meshtastic_PositionLite_init_default {0, 0, 0, 0, _meshtastic_Position_LocSource_MIN, 0}
|
||||
#define meshtastic_UserLite_init_default {{0}, "", "", _meshtastic_HardwareModel_MIN, 0, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}, false, 0}
|
||||
#define meshtastic_NodeInfoLite_init_default {0, 0, 0, 0, false, 0, 0, 0, "", "", _meshtastic_HardwareModel_MIN, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}}
|
||||
#define meshtastic_DeviceState_init_default {false, meshtastic_MyNodeInfo_init_default, false, meshtastic_User_init_default, 0, {meshtastic_MeshPacket_init_default}, false, meshtastic_MeshPacket_init_default, 0, 0, 0, false, meshtastic_MeshPacket_init_default, 0, {meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default}}
|
||||
@@ -222,7 +224,7 @@ extern "C" {
|
||||
#define meshtastic_NodeDatabase_init_default {0, {0}, {0}, {0}, {0}, {0}}
|
||||
#define meshtastic_ChannelFile_init_default {0, {meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default}, 0}
|
||||
#define meshtastic_BackupPreferences_init_default {0, 0, false, meshtastic_LocalConfig_init_default, false, meshtastic_LocalModuleConfig_init_default, false, meshtastic_ChannelFile_init_default, false, meshtastic_User_init_default}
|
||||
#define meshtastic_PositionLite_init_zero {0, 0, 0, 0, _meshtastic_Position_LocSource_MIN}
|
||||
#define meshtastic_PositionLite_init_zero {0, 0, 0, 0, _meshtastic_Position_LocSource_MIN, 0}
|
||||
#define meshtastic_UserLite_init_zero {{0}, "", "", _meshtastic_HardwareModel_MIN, 0, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}, false, 0}
|
||||
#define meshtastic_NodeInfoLite_init_zero {0, 0, 0, 0, false, 0, 0, 0, "", "", _meshtastic_HardwareModel_MIN, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}}
|
||||
#define meshtastic_DeviceState_init_zero {false, meshtastic_MyNodeInfo_init_zero, false, meshtastic_User_init_zero, 0, {meshtastic_MeshPacket_init_zero}, false, meshtastic_MeshPacket_init_zero, 0, 0, 0, false, meshtastic_MeshPacket_init_zero, 0, {meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero}}
|
||||
@@ -240,6 +242,7 @@ extern "C" {
|
||||
#define meshtastic_PositionLite_altitude_tag 3
|
||||
#define meshtastic_PositionLite_time_tag 4
|
||||
#define meshtastic_PositionLite_location_source_tag 5
|
||||
#define meshtastic_PositionLite_precision_bits_tag 6
|
||||
#define meshtastic_UserLite_macaddr_tag 1
|
||||
#define meshtastic_UserLite_long_name_tag 2
|
||||
#define meshtastic_UserLite_short_name_tag 3
|
||||
@@ -298,7 +301,8 @@ X(a, STATIC, SINGULAR, SFIXED32, latitude_i, 1) \
|
||||
X(a, STATIC, SINGULAR, SFIXED32, longitude_i, 2) \
|
||||
X(a, STATIC, SINGULAR, INT32, altitude, 3) \
|
||||
X(a, STATIC, SINGULAR, FIXED32, time, 4) \
|
||||
X(a, STATIC, SINGULAR, UENUM, location_source, 5)
|
||||
X(a, STATIC, SINGULAR, UENUM, location_source, 5) \
|
||||
X(a, STATIC, SINGULAR, UINT32, precision_bits, 6)
|
||||
#define meshtastic_PositionLite_CALLBACK NULL
|
||||
#define meshtastic_PositionLite_DEFAULT NULL
|
||||
|
||||
@@ -444,13 +448,13 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg;
|
||||
#define MESHTASTIC_MESHTASTIC_DEVICEONLY_PB_H_MAX_SIZE meshtastic_BackupPreferences_size
|
||||
#define meshtastic_BackupPreferences_size 2432
|
||||
#define meshtastic_ChannelFile_size 718
|
||||
#define meshtastic_DeviceState_size 1737
|
||||
#define meshtastic_DeviceState_size 1944
|
||||
#define meshtastic_NodeEnvironmentEntry_size 170
|
||||
#define meshtastic_NodeInfoLite_size 105
|
||||
#define meshtastic_NodePositionEntry_size 36
|
||||
#define meshtastic_NodePositionEntry_size 42
|
||||
#define meshtastic_NodeStatusEntry_size 89
|
||||
#define meshtastic_NodeTelemetryEntry_size 35
|
||||
#define meshtastic_PositionLite_size 28
|
||||
#define meshtastic_PositionLite_size 34
|
||||
#define meshtastic_UserLite_size 98
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -115,7 +115,7 @@ extern const pb_msgdesc_t meshtastic_NodeDatabase_Legacy_msg;
|
||||
/* Maximum encoded size of messages (where known) */
|
||||
/* meshtastic_NodeDatabase_Legacy_size depends on runtime parameters */
|
||||
#define MESHTASTIC_MESHTASTIC_DEVICEONLY_LEGACY_PB_H_MAX_SIZE meshtastic_NodeInfoLite_Legacy_size
|
||||
#define meshtastic_NodeInfoLite_Legacy_size 196
|
||||
#define meshtastic_NodeInfoLite_Legacy_size 202
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* extern "C" */
|
||||
|
||||
@@ -57,6 +57,9 @@ PB_BIND(meshtastic_QueueStatus, meshtastic_QueueStatus, AUTO)
|
||||
PB_BIND(meshtastic_FromRadio, meshtastic_FromRadio, 2)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_LockdownStatus, meshtastic_LockdownStatus, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_ClientNotification, meshtastic_ClientNotification, 2)
|
||||
|
||||
|
||||
@@ -134,6 +137,8 @@ PB_BIND(meshtastic_ChunkedPayloadResponse, meshtastic_ChunkedPayloadResponse, AU
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -317,6 +317,10 @@ typedef enum _meshtastic_HardwareModel {
|
||||
meshtastic_HardwareModel_THINKNODE_M9 = 131,
|
||||
/* The Heltec-V4-R8 uses an ESP32S3R8 chip, plus an SX1262. */
|
||||
meshtastic_HardwareModel_HELTEC_V4_R8 = 132,
|
||||
/* The HELTEC_MESH_NODE_T1 uses an NRF52840 chip, plus an SX1262. */
|
||||
meshtastic_HardwareModel_HELTEC_MESH_NODE_T1 = 133,
|
||||
/* B&Q Consulting Station G3: TBD */
|
||||
meshtastic_HardwareModel_STATION_G3 = 134,
|
||||
/* ------------------------------------------------------------------------------------------------------------------------------------------
|
||||
Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits.
|
||||
------------------------------------------------------------------------------------------------------------------------------------------ */
|
||||
@@ -634,6 +638,25 @@ typedef enum _meshtastic_LogRecord_Level {
|
||||
meshtastic_LogRecord_Level_TRACE = 5
|
||||
} meshtastic_LogRecord_Level;
|
||||
|
||||
typedef enum _meshtastic_LockdownStatus_State {
|
||||
/* Default; should not be sent. */
|
||||
meshtastic_LockdownStatus_State_STATE_UNSPECIFIED = 0,
|
||||
/* No passphrase has ever been provisioned on this device.
|
||||
Client should prompt the operator to set one. */
|
||||
meshtastic_LockdownStatus_State_NEEDS_PROVISION = 1,
|
||||
/* Storage is locked or this client has not authenticated yet.
|
||||
lock_reason carries a machine-readable detail string.
|
||||
Client should present (or auto-replay) a passphrase via
|
||||
AdminMessage.lockdown_auth. */
|
||||
meshtastic_LockdownStatus_State_LOCKED = 2,
|
||||
/* Passphrase accepted; client is now authorized for this connection.
|
||||
boots_remaining and valid_until_epoch describe the active session
|
||||
token's TTL. */
|
||||
meshtastic_LockdownStatus_State_UNLOCKED = 3,
|
||||
/* Passphrase rejected. backoff_seconds is non-zero when rate-limited. */
|
||||
meshtastic_LockdownStatus_State_UNLOCK_FAILED = 4
|
||||
} meshtastic_LockdownStatus_State;
|
||||
|
||||
/* Struct definitions */
|
||||
/* A GPS Position */
|
||||
typedef struct _meshtastic_Position {
|
||||
@@ -797,6 +820,7 @@ typedef struct _meshtastic_Routing {
|
||||
} meshtastic_Routing;
|
||||
|
||||
typedef PB_BYTES_ARRAY_T(233) meshtastic_Data_payload_t;
|
||||
typedef PB_BYTES_ARRAY_T(64) meshtastic_Data_xeddsa_signature_t;
|
||||
/* (Formerly called SubPacket)
|
||||
The payload portion fo a packet, this is the actual bytes that are sent
|
||||
inside a radio packet (because from/to are broken out by the comms library) */
|
||||
@@ -830,6 +854,8 @@ typedef struct _meshtastic_Data {
|
||||
/* Bitfield for extra flags. First use is to indicate that user approves the packet being uploaded to MQTT. */
|
||||
bool has_bitfield;
|
||||
uint8_t bitfield;
|
||||
/* XEdDSA signature for the payload */
|
||||
meshtastic_Data_xeddsa_signature_t xeddsa_signature;
|
||||
} meshtastic_Data;
|
||||
|
||||
typedef PB_BYTES_ARRAY_T(32) meshtastic_KeyVerification_hash1_t;
|
||||
@@ -1034,6 +1060,8 @@ typedef struct _meshtastic_MeshPacket {
|
||||
uint32_t tx_after;
|
||||
/* Indicates which transport mechanism this packet arrived over */
|
||||
meshtastic_MeshPacket_TransportMechanism transport_mechanism;
|
||||
/* Indicates whether the packet has a valid signature */
|
||||
bool xeddsa_signed;
|
||||
} meshtastic_MeshPacket;
|
||||
|
||||
/* The bluetooth to device link:
|
||||
@@ -1090,6 +1118,10 @@ typedef struct _meshtastic_NodeInfo {
|
||||
/* True if node has been muted
|
||||
Persistes between NodeDB internal clean ups */
|
||||
bool is_muted;
|
||||
/* True if node is signing its packets via XEdDSA
|
||||
Persists between NodeDB internal clean ups
|
||||
LSB 1 of the bitfield */
|
||||
bool has_xeddsa_signed;
|
||||
} meshtastic_NodeInfo;
|
||||
|
||||
typedef PB_BYTES_ARRAY_T(16) meshtastic_MyNodeInfo_device_id_t;
|
||||
@@ -1144,6 +1176,38 @@ typedef struct _meshtastic_QueueStatus {
|
||||
uint32_t mesh_packet_id;
|
||||
} meshtastic_QueueStatus;
|
||||
|
||||
/* Lockdown state report from firmware to client (for hardened builds
|
||||
with MESHTASTIC_LOCKDOWN). Sent immediately after config_complete_id
|
||||
to inform a freshly-connected unauthorized client what it must do,
|
||||
and again in response to each LockdownAuth admin command. */
|
||||
typedef struct _meshtastic_LockdownStatus {
|
||||
/* Current lockdown state being reported. */
|
||||
meshtastic_LockdownStatus_State state;
|
||||
/* For LOCKED: machine-readable reason. Known values:
|
||||
"needs_auth" — storage already unlocked, client must auth
|
||||
"token_missing" — no boot token on flash
|
||||
"token_expired" — boot token wall-clock TTL elapsed
|
||||
"token_boots_zero" — boot token boot-count TTL exhausted
|
||||
"token_hmac_fail" — token tampered or wrong device
|
||||
"token_dek_fail" — token DEK decrypt failed
|
||||
"token_wrong_size" — token file corrupted
|
||||
"token_bad_magic" — token file corrupted
|
||||
"not_provisioned" — should generally use NEEDS_PROVISION state instead
|
||||
Other values may be added; clients should treat unknown values as
|
||||
"locked, ask for passphrase". */
|
||||
char lock_reason[32];
|
||||
/* For UNLOCKED: remaining boots on the issued session token.
|
||||
Decrements by 1 on each subsequent boot. */
|
||||
uint32_t boots_remaining;
|
||||
/* For UNLOCKED: wall-clock expiry of the issued session token,
|
||||
absolute Unix-epoch seconds. 0 = no time limit. */
|
||||
uint32_t valid_until_epoch;
|
||||
/* For UNLOCK_FAILED: seconds the client must wait before another
|
||||
passphrase attempt will be accepted. 0 = wrong passphrase, no
|
||||
backoff (immediate retry allowed but advisable to prompt user). */
|
||||
uint32_t backoff_seconds;
|
||||
} meshtastic_LockdownStatus;
|
||||
|
||||
typedef struct _meshtastic_KeyVerificationNumberInform {
|
||||
uint64_t nonce;
|
||||
char remote_longname[40];
|
||||
@@ -1317,6 +1381,12 @@ typedef struct _meshtastic_FromRadio {
|
||||
meshtastic_ClientNotification clientNotification;
|
||||
/* Persistent data for device-ui */
|
||||
meshtastic_DeviceUIConfig deviceuiConfig;
|
||||
/* Lockdown state notification for hardened firmware builds.
|
||||
Sent post-config (so unauthorized clients learn they must
|
||||
provision/unlock) and after each LockdownAuth admin command
|
||||
to report success or failure. Replaces the earlier scheme of
|
||||
encoding state as magic-string prefixes inside ClientNotification. */
|
||||
meshtastic_LockdownStatus lockdown_status;
|
||||
};
|
||||
} meshtastic_FromRadio;
|
||||
|
||||
@@ -1458,6 +1528,10 @@ extern "C" {
|
||||
#define _meshtastic_LogRecord_Level_MAX meshtastic_LogRecord_Level_CRITICAL
|
||||
#define _meshtastic_LogRecord_Level_ARRAYSIZE ((meshtastic_LogRecord_Level)(meshtastic_LogRecord_Level_CRITICAL+1))
|
||||
|
||||
#define _meshtastic_LockdownStatus_State_MIN meshtastic_LockdownStatus_State_STATE_UNSPECIFIED
|
||||
#define _meshtastic_LockdownStatus_State_MAX meshtastic_LockdownStatus_State_UNLOCK_FAILED
|
||||
#define _meshtastic_LockdownStatus_State_ARRAYSIZE ((meshtastic_LockdownStatus_State)(meshtastic_LockdownStatus_State_UNLOCK_FAILED+1))
|
||||
|
||||
#define meshtastic_Position_location_source_ENUMTYPE meshtastic_Position_LocSource
|
||||
#define meshtastic_Position_altitude_source_ENUMTYPE meshtastic_Position_AltSource
|
||||
|
||||
@@ -1488,6 +1562,8 @@ extern "C" {
|
||||
|
||||
|
||||
|
||||
#define meshtastic_LockdownStatus_state_ENUMTYPE meshtastic_LockdownStatus_State
|
||||
|
||||
#define meshtastic_ClientNotification_level_ENUMTYPE meshtastic_LogRecord_Level
|
||||
|
||||
|
||||
@@ -1515,19 +1591,20 @@ extern "C" {
|
||||
#define meshtastic_User_init_default {"", "", "", {0}, _meshtastic_HardwareModel_MIN, 0, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}, false, 0}
|
||||
#define meshtastic_RouteDiscovery_init_default {0, {0, 0, 0, 0, 0, 0, 0, 0}, 0, {0, 0, 0, 0, 0, 0, 0, 0}, 0, {0, 0, 0, 0, 0, 0, 0, 0}, 0, {0, 0, 0, 0, 0, 0, 0, 0}}
|
||||
#define meshtastic_Routing_init_default {0, {meshtastic_RouteDiscovery_init_default}}
|
||||
#define meshtastic_Data_init_default {_meshtastic_PortNum_MIN, {0, {0}}, 0, 0, 0, 0, 0, 0, false, 0}
|
||||
#define meshtastic_Data_init_default {_meshtastic_PortNum_MIN, {0, {0}}, 0, 0, 0, 0, 0, 0, false, 0, {0, {0}}}
|
||||
#define meshtastic_KeyVerification_init_default {0, {0, {0}}, {0, {0}}}
|
||||
#define meshtastic_StoreForwardPlusPlus_init_default {_meshtastic_StoreForwardPlusPlus_SFPP_message_type_MIN, {0, {0}}, {0, {0}}, {0, {0}}, {0, {0}}, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_RemoteShell_init_default {_meshtastic_RemoteShell_OpCode_MIN, 0, 0, 0, {0, {0}}, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_Waypoint_init_default {0, false, 0, false, 0, 0, 0, "", "", 0}
|
||||
#define meshtastic_StatusMessage_init_default {""}
|
||||
#define meshtastic_MqttClientProxyMessage_init_default {"", 0, {{0, {0}}}, 0}
|
||||
#define meshtastic_MeshPacket_init_default {0, 0, 0, 0, {meshtastic_Data_init_default}, 0, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, 0, _meshtastic_MeshPacket_Delayed_MIN, 0, 0, {0, {0}}, 0, 0, 0, 0, _meshtastic_MeshPacket_TransportMechanism_MIN}
|
||||
#define meshtastic_NodeInfo_init_default {0, false, meshtastic_User_init_default, false, meshtastic_Position_init_default, 0, 0, false, meshtastic_DeviceMetrics_init_default, 0, 0, false, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_MeshPacket_init_default {0, 0, 0, 0, {meshtastic_Data_init_default}, 0, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, 0, _meshtastic_MeshPacket_Delayed_MIN, 0, 0, {0, {0}}, 0, 0, 0, 0, _meshtastic_MeshPacket_TransportMechanism_MIN, 0}
|
||||
#define meshtastic_NodeInfo_init_default {0, false, meshtastic_User_init_default, false, meshtastic_Position_init_default, 0, 0, false, meshtastic_DeviceMetrics_init_default, 0, 0, false, 0, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_MyNodeInfo_init_default {0, 0, 0, {0, {0}}, "", _meshtastic_FirmwareEdition_MIN, 0}
|
||||
#define meshtastic_LogRecord_init_default {"", 0, "", _meshtastic_LogRecord_Level_MIN}
|
||||
#define meshtastic_QueueStatus_init_default {0, 0, 0, 0}
|
||||
#define meshtastic_FromRadio_init_default {0, 0, {meshtastic_MeshPacket_init_default}}
|
||||
#define meshtastic_LockdownStatus_init_default {_meshtastic_LockdownStatus_State_MIN, "", 0, 0, 0}
|
||||
#define meshtastic_ClientNotification_init_default {false, 0, 0, _meshtastic_LogRecord_Level_MIN, "", 0, {meshtastic_KeyVerificationNumberInform_init_default}}
|
||||
#define meshtastic_KeyVerificationNumberInform_init_default {0, "", 0}
|
||||
#define meshtastic_KeyVerificationNumberRequest_init_default {0, ""}
|
||||
@@ -1549,19 +1626,20 @@ extern "C" {
|
||||
#define meshtastic_User_init_zero {"", "", "", {0}, _meshtastic_HardwareModel_MIN, 0, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}, false, 0}
|
||||
#define meshtastic_RouteDiscovery_init_zero {0, {0, 0, 0, 0, 0, 0, 0, 0}, 0, {0, 0, 0, 0, 0, 0, 0, 0}, 0, {0, 0, 0, 0, 0, 0, 0, 0}, 0, {0, 0, 0, 0, 0, 0, 0, 0}}
|
||||
#define meshtastic_Routing_init_zero {0, {meshtastic_RouteDiscovery_init_zero}}
|
||||
#define meshtastic_Data_init_zero {_meshtastic_PortNum_MIN, {0, {0}}, 0, 0, 0, 0, 0, 0, false, 0}
|
||||
#define meshtastic_Data_init_zero {_meshtastic_PortNum_MIN, {0, {0}}, 0, 0, 0, 0, 0, 0, false, 0, {0, {0}}}
|
||||
#define meshtastic_KeyVerification_init_zero {0, {0, {0}}, {0, {0}}}
|
||||
#define meshtastic_StoreForwardPlusPlus_init_zero {_meshtastic_StoreForwardPlusPlus_SFPP_message_type_MIN, {0, {0}}, {0, {0}}, {0, {0}}, {0, {0}}, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_RemoteShell_init_zero {_meshtastic_RemoteShell_OpCode_MIN, 0, 0, 0, {0, {0}}, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_Waypoint_init_zero {0, false, 0, false, 0, 0, 0, "", "", 0}
|
||||
#define meshtastic_StatusMessage_init_zero {""}
|
||||
#define meshtastic_MqttClientProxyMessage_init_zero {"", 0, {{0, {0}}}, 0}
|
||||
#define meshtastic_MeshPacket_init_zero {0, 0, 0, 0, {meshtastic_Data_init_zero}, 0, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, 0, _meshtastic_MeshPacket_Delayed_MIN, 0, 0, {0, {0}}, 0, 0, 0, 0, _meshtastic_MeshPacket_TransportMechanism_MIN}
|
||||
#define meshtastic_NodeInfo_init_zero {0, false, meshtastic_User_init_zero, false, meshtastic_Position_init_zero, 0, 0, false, meshtastic_DeviceMetrics_init_zero, 0, 0, false, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_MeshPacket_init_zero {0, 0, 0, 0, {meshtastic_Data_init_zero}, 0, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, 0, _meshtastic_MeshPacket_Delayed_MIN, 0, 0, {0, {0}}, 0, 0, 0, 0, _meshtastic_MeshPacket_TransportMechanism_MIN, 0}
|
||||
#define meshtastic_NodeInfo_init_zero {0, false, meshtastic_User_init_zero, false, meshtastic_Position_init_zero, 0, 0, false, meshtastic_DeviceMetrics_init_zero, 0, 0, false, 0, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_MyNodeInfo_init_zero {0, 0, 0, {0, {0}}, "", _meshtastic_FirmwareEdition_MIN, 0}
|
||||
#define meshtastic_LogRecord_init_zero {"", 0, "", _meshtastic_LogRecord_Level_MIN}
|
||||
#define meshtastic_QueueStatus_init_zero {0, 0, 0, 0}
|
||||
#define meshtastic_FromRadio_init_zero {0, 0, {meshtastic_MeshPacket_init_zero}}
|
||||
#define meshtastic_LockdownStatus_init_zero {_meshtastic_LockdownStatus_State_MIN, "", 0, 0, 0}
|
||||
#define meshtastic_ClientNotification_init_zero {false, 0, 0, _meshtastic_LogRecord_Level_MIN, "", 0, {meshtastic_KeyVerificationNumberInform_init_zero}}
|
||||
#define meshtastic_KeyVerificationNumberInform_init_zero {0, "", 0}
|
||||
#define meshtastic_KeyVerificationNumberRequest_init_zero {0, ""}
|
||||
@@ -1629,6 +1707,7 @@ extern "C" {
|
||||
#define meshtastic_Data_reply_id_tag 7
|
||||
#define meshtastic_Data_emoji_tag 8
|
||||
#define meshtastic_Data_bitfield_tag 9
|
||||
#define meshtastic_Data_xeddsa_signature_tag 10
|
||||
#define meshtastic_KeyVerification_nonce_tag 1
|
||||
#define meshtastic_KeyVerification_hash1_tag 2
|
||||
#define meshtastic_KeyVerification_hash2_tag 3
|
||||
@@ -1686,6 +1765,7 @@ extern "C" {
|
||||
#define meshtastic_MeshPacket_relay_node_tag 19
|
||||
#define meshtastic_MeshPacket_tx_after_tag 20
|
||||
#define meshtastic_MeshPacket_transport_mechanism_tag 21
|
||||
#define meshtastic_MeshPacket_xeddsa_signed_tag 22
|
||||
#define meshtastic_NodeInfo_num_tag 1
|
||||
#define meshtastic_NodeInfo_user_tag 2
|
||||
#define meshtastic_NodeInfo_position_tag 3
|
||||
@@ -1699,6 +1779,7 @@ extern "C" {
|
||||
#define meshtastic_NodeInfo_is_ignored_tag 11
|
||||
#define meshtastic_NodeInfo_is_key_manually_verified_tag 12
|
||||
#define meshtastic_NodeInfo_is_muted_tag 13
|
||||
#define meshtastic_NodeInfo_has_xeddsa_signed_tag 14
|
||||
#define meshtastic_MyNodeInfo_my_node_num_tag 1
|
||||
#define meshtastic_MyNodeInfo_reboot_count_tag 8
|
||||
#define meshtastic_MyNodeInfo_min_app_version_tag 11
|
||||
@@ -1714,6 +1795,11 @@ extern "C" {
|
||||
#define meshtastic_QueueStatus_free_tag 2
|
||||
#define meshtastic_QueueStatus_maxlen_tag 3
|
||||
#define meshtastic_QueueStatus_mesh_packet_id_tag 4
|
||||
#define meshtastic_LockdownStatus_state_tag 1
|
||||
#define meshtastic_LockdownStatus_lock_reason_tag 2
|
||||
#define meshtastic_LockdownStatus_boots_remaining_tag 3
|
||||
#define meshtastic_LockdownStatus_valid_until_epoch_tag 4
|
||||
#define meshtastic_LockdownStatus_backoff_seconds_tag 5
|
||||
#define meshtastic_KeyVerificationNumberInform_nonce_tag 1
|
||||
#define meshtastic_KeyVerificationNumberInform_remote_longname_tag 2
|
||||
#define meshtastic_KeyVerificationNumberInform_security_number_tag 3
|
||||
@@ -1773,6 +1859,7 @@ extern "C" {
|
||||
#define meshtastic_FromRadio_fileInfo_tag 15
|
||||
#define meshtastic_FromRadio_clientNotification_tag 16
|
||||
#define meshtastic_FromRadio_deviceuiConfig_tag 17
|
||||
#define meshtastic_FromRadio_lockdown_status_tag 18
|
||||
#define meshtastic_Heartbeat_nonce_tag 1
|
||||
#define meshtastic_ToRadio_packet_tag 1
|
||||
#define meshtastic_ToRadio_want_config_id_tag 3
|
||||
@@ -1859,7 +1946,8 @@ X(a, STATIC, SINGULAR, FIXED32, source, 5) \
|
||||
X(a, STATIC, SINGULAR, FIXED32, request_id, 6) \
|
||||
X(a, STATIC, SINGULAR, FIXED32, reply_id, 7) \
|
||||
X(a, STATIC, SINGULAR, FIXED32, emoji, 8) \
|
||||
X(a, STATIC, OPTIONAL, UINT32, bitfield, 9)
|
||||
X(a, STATIC, OPTIONAL, UINT32, bitfield, 9) \
|
||||
X(a, STATIC, SINGULAR, BYTES, xeddsa_signature, 10)
|
||||
#define meshtastic_Data_CALLBACK NULL
|
||||
#define meshtastic_Data_DEFAULT NULL
|
||||
|
||||
@@ -1944,7 +2032,8 @@ X(a, STATIC, SINGULAR, BOOL, pki_encrypted, 17) \
|
||||
X(a, STATIC, SINGULAR, UINT32, next_hop, 18) \
|
||||
X(a, STATIC, SINGULAR, UINT32, relay_node, 19) \
|
||||
X(a, STATIC, SINGULAR, UINT32, tx_after, 20) \
|
||||
X(a, STATIC, SINGULAR, UENUM, transport_mechanism, 21)
|
||||
X(a, STATIC, SINGULAR, UENUM, transport_mechanism, 21) \
|
||||
X(a, STATIC, SINGULAR, BOOL, xeddsa_signed, 22)
|
||||
#define meshtastic_MeshPacket_CALLBACK NULL
|
||||
#define meshtastic_MeshPacket_DEFAULT NULL
|
||||
#define meshtastic_MeshPacket_payload_variant_decoded_MSGTYPE meshtastic_Data
|
||||
@@ -1962,7 +2051,8 @@ X(a, STATIC, OPTIONAL, UINT32, hops_away, 9) \
|
||||
X(a, STATIC, SINGULAR, BOOL, is_favorite, 10) \
|
||||
X(a, STATIC, SINGULAR, BOOL, is_ignored, 11) \
|
||||
X(a, STATIC, SINGULAR, BOOL, is_key_manually_verified, 12) \
|
||||
X(a, STATIC, SINGULAR, BOOL, is_muted, 13)
|
||||
X(a, STATIC, SINGULAR, BOOL, is_muted, 13) \
|
||||
X(a, STATIC, SINGULAR, BOOL, has_xeddsa_signed, 14)
|
||||
#define meshtastic_NodeInfo_CALLBACK NULL
|
||||
#define meshtastic_NodeInfo_DEFAULT NULL
|
||||
#define meshtastic_NodeInfo_user_MSGTYPE meshtastic_User
|
||||
@@ -2013,7 +2103,8 @@ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,metadata,metadata), 13) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,mqttClientProxyMessage,mqttClientProxyMessage), 14) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,fileInfo,fileInfo), 15) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,clientNotification,clientNotification), 16) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,deviceuiConfig,deviceuiConfig), 17)
|
||||
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,deviceuiConfig,deviceuiConfig), 17) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,lockdown_status,lockdown_status), 18)
|
||||
#define meshtastic_FromRadio_CALLBACK NULL
|
||||
#define meshtastic_FromRadio_DEFAULT NULL
|
||||
#define meshtastic_FromRadio_payload_variant_packet_MSGTYPE meshtastic_MeshPacket
|
||||
@@ -2030,6 +2121,16 @@ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,deviceuiConfig,deviceuiConfi
|
||||
#define meshtastic_FromRadio_payload_variant_fileInfo_MSGTYPE meshtastic_FileInfo
|
||||
#define meshtastic_FromRadio_payload_variant_clientNotification_MSGTYPE meshtastic_ClientNotification
|
||||
#define meshtastic_FromRadio_payload_variant_deviceuiConfig_MSGTYPE meshtastic_DeviceUIConfig
|
||||
#define meshtastic_FromRadio_payload_variant_lockdown_status_MSGTYPE meshtastic_LockdownStatus
|
||||
|
||||
#define meshtastic_LockdownStatus_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, UENUM, state, 1) \
|
||||
X(a, STATIC, SINGULAR, STRING, lock_reason, 2) \
|
||||
X(a, STATIC, SINGULAR, UINT32, boots_remaining, 3) \
|
||||
X(a, STATIC, SINGULAR, UINT32, valid_until_epoch, 4) \
|
||||
X(a, STATIC, SINGULAR, UINT32, backoff_seconds, 5)
|
||||
#define meshtastic_LockdownStatus_CALLBACK NULL
|
||||
#define meshtastic_LockdownStatus_DEFAULT NULL
|
||||
|
||||
#define meshtastic_ClientNotification_FIELDLIST(X, a) \
|
||||
X(a, STATIC, OPTIONAL, UINT32, reply_id, 1) \
|
||||
@@ -2190,6 +2291,7 @@ extern const pb_msgdesc_t meshtastic_MyNodeInfo_msg;
|
||||
extern const pb_msgdesc_t meshtastic_LogRecord_msg;
|
||||
extern const pb_msgdesc_t meshtastic_QueueStatus_msg;
|
||||
extern const pb_msgdesc_t meshtastic_FromRadio_msg;
|
||||
extern const pb_msgdesc_t meshtastic_LockdownStatus_msg;
|
||||
extern const pb_msgdesc_t meshtastic_ClientNotification_msg;
|
||||
extern const pb_msgdesc_t meshtastic_KeyVerificationNumberInform_msg;
|
||||
extern const pb_msgdesc_t meshtastic_KeyVerificationNumberRequest_msg;
|
||||
@@ -2226,6 +2328,7 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg;
|
||||
#define meshtastic_LogRecord_fields &meshtastic_LogRecord_msg
|
||||
#define meshtastic_QueueStatus_fields &meshtastic_QueueStatus_msg
|
||||
#define meshtastic_FromRadio_fields &meshtastic_FromRadio_msg
|
||||
#define meshtastic_LockdownStatus_fields &meshtastic_LockdownStatus_msg
|
||||
#define meshtastic_ClientNotification_fields &meshtastic_ClientNotification_msg
|
||||
#define meshtastic_KeyVerificationNumberInform_fields &meshtastic_KeyVerificationNumberInform_msg
|
||||
#define meshtastic_KeyVerificationNumberRequest_fields &meshtastic_KeyVerificationNumberRequest_msg
|
||||
@@ -2251,7 +2354,7 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg;
|
||||
#define meshtastic_ChunkedPayload_size 245
|
||||
#define meshtastic_ClientNotification_size 482
|
||||
#define meshtastic_Compressed_size 239
|
||||
#define meshtastic_Data_size 269
|
||||
#define meshtastic_Data_size 335
|
||||
#define meshtastic_DeviceMetadata_size 54
|
||||
#define meshtastic_DuplicatedPublicKey_size 0
|
||||
#define meshtastic_FileInfo_size 236
|
||||
@@ -2261,14 +2364,15 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg;
|
||||
#define meshtastic_KeyVerificationNumberInform_size 58
|
||||
#define meshtastic_KeyVerificationNumberRequest_size 52
|
||||
#define meshtastic_KeyVerification_size 79
|
||||
#define meshtastic_LockdownStatus_size 53
|
||||
#define meshtastic_LogRecord_size 426
|
||||
#define meshtastic_LowEntropyKey_size 0
|
||||
#define meshtastic_MeshPacket_size 381
|
||||
#define meshtastic_MeshPacket_size 450
|
||||
#define meshtastic_MqttClientProxyMessage_size 501
|
||||
#define meshtastic_MyNodeInfo_size 83
|
||||
#define meshtastic_NeighborInfo_size 258
|
||||
#define meshtastic_Neighbor_size 22
|
||||
#define meshtastic_NodeInfo_size 325
|
||||
#define meshtastic_NodeInfo_size 327
|
||||
#define meshtastic_NodeRemoteHardwarePin_size 29
|
||||
#define meshtastic_Position_size 144
|
||||
#define meshtastic_QueueStatus_size 23
|
||||
|
||||
@@ -115,7 +115,11 @@ typedef enum _meshtastic_TelemetrySensorType {
|
||||
/* SHT family of sensors for temperature and humidity */
|
||||
meshtastic_TelemetrySensorType_SHTXX = 50,
|
||||
/* DS248X Bridge for one-wire temperature sensors */
|
||||
meshtastic_TelemetrySensorType_DS248X = 51
|
||||
meshtastic_TelemetrySensorType_DS248X = 51,
|
||||
/* MMC5983MA 3-Axis Digital Magnetic Sensor */
|
||||
meshtastic_TelemetrySensorType_MMC5983MA = 52,
|
||||
/* ICM-42607-P 6‑Axis IMU */
|
||||
meshtastic_TelemetrySensorType_ICM42607P = 53
|
||||
} meshtastic_TelemetrySensorType;
|
||||
|
||||
/* Struct definitions */
|
||||
@@ -496,8 +500,8 @@ extern "C" {
|
||||
|
||||
/* Helper constants for enums */
|
||||
#define _meshtastic_TelemetrySensorType_MIN meshtastic_TelemetrySensorType_SENSOR_UNSET
|
||||
#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_DS248X
|
||||
#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_DS248X+1))
|
||||
#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_ICM42607P
|
||||
#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_ICM42607P+1))
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ static_assert(sizeof(meshtastic_NodeInfoLite) <= 130, "NodeInfoLite size increas
|
||||
#if defined(ARCH_STM32WL)
|
||||
#define MAX_NUM_NODES 10
|
||||
#elif defined(ARCH_NRF52)
|
||||
#define MAX_NUM_NODES 80
|
||||
#define MAX_NUM_NODES 150
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32S3)
|
||||
#include "Esp.h"
|
||||
static inline int get_max_num_nodes()
|
||||
|
||||
@@ -15,6 +15,12 @@
|
||||
#define ETH ETH2
|
||||
#endif // HAS_ETHERNET
|
||||
|
||||
#if HAS_ETHERNET && defined(USE_CH390D)
|
||||
#include "ESP32_CH390.h"
|
||||
#include "hal/spi_types.h"
|
||||
#define ETH CH390
|
||||
#endif // HAS_ETHERNET
|
||||
|
||||
#include <WiFiUdp.h>
|
||||
#ifdef ARCH_ESP32
|
||||
#if !MESHTASTIC_EXCLUDE_WEBSERVER
|
||||
@@ -56,12 +62,43 @@ unsigned long lastrun_ntp = 0;
|
||||
|
||||
bool needReconnect = true; // If we create our reconnector, run it once at the beginning
|
||||
bool isReconnecting = false; // If we are currently reconnecting
|
||||
#if defined(USE_WS5500) || defined(USE_CH390D)
|
||||
static volatile bool ethNetworkConnectedPending = false;
|
||||
#endif
|
||||
|
||||
WiFiUDP syslogClient;
|
||||
meshtastic::Syslog syslog(syslogClient);
|
||||
|
||||
Periodic *wifiReconnect;
|
||||
|
||||
#if defined(USE_WS5500) || defined(USE_CH390D)
|
||||
static void onNetworkConnected();
|
||||
static uint32_t lastEthIP = 0;
|
||||
static int32_t ethNetworkConnectedPoll()
|
||||
{
|
||||
if (ethNetworkConnectedPending) {
|
||||
ethNetworkConnectedPending = false;
|
||||
uint32_t ip = (uint32_t)ETH.localIP();
|
||||
bool ipChanged = APStartupComplete && ip != 0 && ip != lastEthIP;
|
||||
onNetworkConnected();
|
||||
if (ipChanged) {
|
||||
LOG_INFO("Ethernet IP changed (%u.%u.%u.%u), restarting mDNS", ip & 0xff, (ip >> 8) & 0xff, (ip >> 16) & 0xff,
|
||||
(ip >> 24) & 0xff);
|
||||
MDNS.end();
|
||||
if (MDNS.begin("Meshtastic")) {
|
||||
MDNS.addService("meshtastic", "tcp", SERVER_API_DEFAULT_PORT);
|
||||
MDNS.addServiceTxt("meshtastic", "tcp", "shortname", String(owner.short_name));
|
||||
MDNS.addServiceTxt("meshtastic", "tcp", "id", String(nodeDB->getNodeId().c_str()));
|
||||
MDNS.addServiceTxt("meshtastic", "tcp", "pio_env", optstr(APP_ENV));
|
||||
}
|
||||
}
|
||||
if (ip != 0)
|
||||
lastEthIP = ip;
|
||||
}
|
||||
return 500;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_WS5500
|
||||
// Startup Ethernet
|
||||
bool initEthernet()
|
||||
@@ -72,6 +109,38 @@ bool initEthernet()
|
||||
#if !MESHTASTIC_EXCLUDE_WEBSERVER
|
||||
createSSLCert(); // For WebServer
|
||||
#endif
|
||||
new concurrency::Periodic("EthConnect", ethNetworkConnectedPoll);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_CH390D
|
||||
// Startup Ethernet
|
||||
bool initEthernet()
|
||||
{
|
||||
// Configure CH390
|
||||
ch390_config_t ch390_conf = CH390_DEFAULT_CONFIG();
|
||||
ch390_conf.spi_host = SPI3_HOST;
|
||||
ch390_conf.spi_cs_gpio = ETH_CS_PIN;
|
||||
ch390_conf.spi_sck_gpio = ETH_SCLK_PIN;
|
||||
ch390_conf.spi_mosi_gpio = ETH_MOSI_PIN;
|
||||
ch390_conf.spi_miso_gpio = ETH_MISO_PIN;
|
||||
ch390_conf.int_gpio = ETH_INT_PIN;
|
||||
#ifdef ETH_RST_PIN
|
||||
ch390_conf.reset_gpio = ETH_RST_PIN;
|
||||
#else
|
||||
ch390_conf.reset_gpio = -1;
|
||||
#endif
|
||||
ch390_conf.spi_clock_mhz = 20;
|
||||
if ((config.network.eth_enabled) && (ETH.begin(ch390_conf))) {
|
||||
WiFi.onEvent(WiFiEvent);
|
||||
#if !MESHTASTIC_EXCLUDE_WEBSERVER
|
||||
createSSLCert(); // For WebServer
|
||||
#endif
|
||||
new concurrency::Periodic("EthConnect", ethNetworkConnectedPoll);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -222,10 +291,8 @@ static int32_t reconnectWiFi()
|
||||
#endif
|
||||
return 1000; // check once per second
|
||||
} else {
|
||||
#ifdef ARCH_RP2040
|
||||
onNetworkConnected(); // will only do anything once
|
||||
#endif
|
||||
return 300000; // every 5 minutes
|
||||
onNetworkConnected(); // will only do anything once (guarded by APStartupComplete)
|
||||
return 300000; // every 5 minutes
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +301,7 @@ bool isWifiAvailable()
|
||||
|
||||
if (config.network.wifi_enabled && (config.network.wifi_ssid[0])) {
|
||||
return true;
|
||||
#ifdef USE_WS5500
|
||||
#if defined(USE_WS5500) || defined(USE_CH390D)
|
||||
} else if (config.network.eth_enabled) {
|
||||
return true;
|
||||
#endif
|
||||
@@ -274,9 +341,6 @@ bool initWifi()
|
||||
const char *wifiPsw = config.network.wifi_psk;
|
||||
|
||||
#ifndef ARCH_RP2040
|
||||
#if !MESHTASTIC_EXCLUDE_WEBSERVER
|
||||
createSSLCert(); // For WebServer
|
||||
#endif
|
||||
WiFi.persistent(false); // Disable flash storage for WiFi credentials
|
||||
#endif
|
||||
if (!*wifiPsw) // Treat empty password as no password
|
||||
@@ -301,6 +365,9 @@ bool initWifi()
|
||||
#endif
|
||||
}
|
||||
#ifdef ARCH_ESP32
|
||||
// Register WiFi event handler BEFORE createSSLCert() to prevent race condition:
|
||||
// Without this, WiFi can auto-reconnect during cert generation and fire GOT_IP
|
||||
// before the handler is registered, causing onNetworkConnected() to never run.
|
||||
WiFi.onEvent(WiFiEvent);
|
||||
WiFi.setAutoReconnect(true);
|
||||
WiFi.setSleep(false);
|
||||
@@ -322,6 +389,12 @@ bool initWifi()
|
||||
wifiDisconnectReason = info.wifi_sta_disconnected.reason;
|
||||
},
|
||||
WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_DISCONNECTED);
|
||||
#endif
|
||||
|
||||
#ifndef ARCH_RP2040
|
||||
#if !MESHTASTIC_EXCLUDE_WEBSERVER
|
||||
createSSLCert(); // For WebServer - called after WiFi.onEvent() to avoid race condition
|
||||
#endif
|
||||
#endif
|
||||
LOG_DEBUG("JOINING WIFI soon: ssid=%s", wifiName);
|
||||
wifiReconnect = new Periodic("WifiConnect", reconnectWiFi);
|
||||
@@ -384,13 +457,13 @@ static void WiFiEvent(WiFiEvent_t event)
|
||||
#endif
|
||||
}
|
||||
#ifdef WIFI_LED
|
||||
digitalWrite(WIFI_LED, HIGH);
|
||||
digitalWrite(WIFI_LED, LOW ^ WIFI_STATE_ON);
|
||||
#endif
|
||||
break;
|
||||
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
|
||||
LOG_INFO("Disconnected from WiFi access point");
|
||||
#ifdef WIFI_LED
|
||||
digitalWrite(WIFI_LED, LOW);
|
||||
digitalWrite(WIFI_LED, HIGH ^ WIFI_STATE_ON);
|
||||
#endif
|
||||
#if HAS_UDP_MULTICAST
|
||||
if (udpHandler) {
|
||||
@@ -452,13 +525,13 @@ static void WiFiEvent(WiFiEvent_t event)
|
||||
case ARDUINO_EVENT_WIFI_AP_START:
|
||||
LOG_INFO("WiFi access point started");
|
||||
#ifdef WIFI_LED
|
||||
digitalWrite(WIFI_LED, HIGH);
|
||||
digitalWrite(WIFI_LED, LOW ^ WIFI_STATE_ON);
|
||||
#endif
|
||||
break;
|
||||
case ARDUINO_EVENT_WIFI_AP_STOP:
|
||||
LOG_INFO("WiFi access point stopped");
|
||||
#ifdef WIFI_LED
|
||||
digitalWrite(WIFI_LED, LOW);
|
||||
digitalWrite(WIFI_LED, HIGH ^ WIFI_STATE_ON);
|
||||
#endif
|
||||
break;
|
||||
case ARDUINO_EVENT_WIFI_AP_STACONNECTED:
|
||||
@@ -494,18 +567,18 @@ static void WiFiEvent(WiFiEvent_t event)
|
||||
LOG_INFO("Ethernet disconnected");
|
||||
break;
|
||||
case ARDUINO_EVENT_ETH_GOT_IP:
|
||||
#ifdef USE_WS5500
|
||||
#if defined(USE_WS5500) || defined(USE_CH390D)
|
||||
LOG_INFO("Obtained IP address: %s, %u Mbps, %s", ETH.localIP().toString().c_str(), ETH.linkSpeed(),
|
||||
ETH.fullDuplex() ? "FULL_DUPLEX" : "HALF_DUPLEX");
|
||||
onNetworkConnected();
|
||||
ethNetworkConnectedPending = true;
|
||||
#endif
|
||||
break;
|
||||
case ARDUINO_EVENT_ETH_GOT_IP6:
|
||||
#ifdef USE_WS5500
|
||||
#if defined(USE_WS5500) || defined(USE_CH390D)
|
||||
#if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0)
|
||||
LOG_INFO("Obtained Local IP6 address: %s", ETH.linkLocalIPv6().toString().c_str());
|
||||
LOG_INFO("Obtained GlobalIP6 address: %s", ETH.globalIPv6().toString().c_str());
|
||||
#else
|
||||
#elif defined(USE_WS5500)
|
||||
LOG_INFO("Obtained IP6 address: %s", ETH.localIPv6().toString().c_str());
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -26,7 +26,7 @@ bool isWifiAvailable();
|
||||
|
||||
uint8_t getWifiDisconnectReason();
|
||||
|
||||
#ifdef USE_WS5500
|
||||
#if defined(USE_WS5500) || defined(USE_CH390D)
|
||||
// Startup Ethernet
|
||||
bool initEthernet();
|
||||
#endif
|
||||
@@ -666,7 +666,7 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers)
|
||||
bool requiresReboot = true;
|
||||
|
||||
switch (c.which_payload_variant) {
|
||||
case meshtastic_Config_device_tag:
|
||||
case meshtastic_Config_device_tag: {
|
||||
LOG_INFO("Set config: Device");
|
||||
config.has_device = true;
|
||||
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && \
|
||||
@@ -720,6 +720,7 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers)
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
} // case meshtastic_Config_device_tag
|
||||
case meshtastic_Config_position_tag:
|
||||
LOG_INFO("Set config: Position");
|
||||
config.has_position = true;
|
||||
@@ -1318,7 +1319,7 @@ void AdminModule::handleGetDeviceConnectionStatus(const meshtastic_MeshPacket &r
|
||||
}
|
||||
#endif
|
||||
|
||||
#if HAS_ETHERNET && !defined(USE_WS5500)
|
||||
#if HAS_ETHERNET && !defined(USE_WS5500) && !defined(USE_CH390D)
|
||||
conn.has_ethernet = true;
|
||||
conn.ethernet.has_status = true;
|
||||
if (Ethernet.linkStatus() == LinkON) {
|
||||
@@ -1345,6 +1346,10 @@ void AdminModule::handleGetDeviceConnectionStatus(const meshtastic_MeshPacket &r
|
||||
if (config.bluetooth.enabled && nrf52Bluetooth) {
|
||||
conn.bluetooth.is_connected = nrf52Bluetooth->isConnected();
|
||||
}
|
||||
#elif defined(ARCH_NRF54L15)
|
||||
if (config.bluetooth.enabled && nrf54l15Bluetooth) {
|
||||
conn.bluetooth.is_connected = nrf54l15Bluetooth->isConnected();
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
conn.has_serial = true; // No serial-less devices
|
||||
@@ -1605,6 +1610,9 @@ void disableBluetooth()
|
||||
#elif defined(ARCH_NRF52)
|
||||
if (nrf52Bluetooth)
|
||||
nrf52Bluetooth->shutdown();
|
||||
#elif defined(ARCH_NRF54L15)
|
||||
if (nrf54l15Bluetooth)
|
||||
nrf54l15Bluetooth->shutdown();
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "GPS.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PositionPrecision.h"
|
||||
#include "RTC.h"
|
||||
#include "Router.h"
|
||||
#include "TransmitHistory.h"
|
||||
@@ -107,13 +108,7 @@ bool PositionModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mes
|
||||
}
|
||||
|
||||
nodeDB->updatePosition(getFrom(&mp), p);
|
||||
if (channels.getByIndex(mp.channel).settings.has_module_settings) {
|
||||
precision = channels.getByIndex(mp.channel).settings.module_settings.position_precision;
|
||||
} else if (channels.getByIndex(mp.channel).role == meshtastic_Channel_Role_PRIMARY) {
|
||||
precision = 32;
|
||||
} else {
|
||||
precision = 0;
|
||||
}
|
||||
precision = getPositionPrecisionForChannel(mp.channel);
|
||||
|
||||
return false; // Let others look at this message also if they want
|
||||
}
|
||||
@@ -121,15 +116,12 @@ bool PositionModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mes
|
||||
void PositionModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_Position *p)
|
||||
{
|
||||
// Phone position packets need to be truncated to the channel precision
|
||||
if (isFromUs(&mp) && (precision < 32 && precision > 0)) {
|
||||
LOG_DEBUG("Truncate phone position to channel precision %i", precision);
|
||||
p->latitude_i = p->latitude_i & (UINT32_MAX << (32 - precision));
|
||||
p->longitude_i = p->longitude_i & (UINT32_MAX << (32 - precision));
|
||||
|
||||
// We want the imprecise position to be the middle of the possible location, not
|
||||
p->latitude_i += (1 << (31 - precision));
|
||||
p->longitude_i += (1 << (31 - precision));
|
||||
|
||||
if (isFromUs(&mp)) {
|
||||
if (precision == 0)
|
||||
LOG_DEBUG("Strip phone position due to channel precision 0");
|
||||
else if (precision < 32)
|
||||
LOG_DEBUG("Truncate phone position to channel precision %i", precision);
|
||||
applyPositionPrecision(*p, precision);
|
||||
mp.decoded.payload.size =
|
||||
pb_encode_to_bytes(mp.decoded.payload.bytes, sizeof(mp.decoded.payload.bytes), &meshtastic_Position_msg, p);
|
||||
}
|
||||
@@ -206,20 +198,11 @@ meshtastic_MeshPacket *PositionModule::allocPositionPacket()
|
||||
|
||||
// lat/lon are unconditionally included - IF AVAILABLE!
|
||||
LOG_DEBUG("Send location with precision %i", precision);
|
||||
if (precision < 32 && precision > 0) {
|
||||
p.latitude_i = localPosition.latitude_i & (UINT32_MAX << (32 - precision));
|
||||
p.longitude_i = localPosition.longitude_i & (UINT32_MAX << (32 - precision));
|
||||
|
||||
// We want the imprecise position to be the middle of the possible location, not
|
||||
p.latitude_i += (1 << (31 - precision));
|
||||
p.longitude_i += (1 << (31 - precision));
|
||||
} else {
|
||||
p.latitude_i = localPosition.latitude_i;
|
||||
p.longitude_i = localPosition.longitude_i;
|
||||
}
|
||||
p.precision_bits = precision;
|
||||
p.latitude_i = localPosition.latitude_i;
|
||||
p.longitude_i = localPosition.longitude_i;
|
||||
p.has_latitude_i = true;
|
||||
p.has_longitude_i = true;
|
||||
applyPositionPrecision(p, precision);
|
||||
// Always use NTP / GPS time if available
|
||||
if (getValidTime(RTCQualityNTP) > 0) {
|
||||
p.time = getValidTime(RTCQualityNTP);
|
||||
@@ -350,8 +333,7 @@ void PositionModule::sendOurPosition()
|
||||
// If we changed channels, ask everyone else for their latest info
|
||||
LOG_INFO("Send pos@%x:6 to mesh (wantReplies=%d)", localPosition.timestamp, requestReplies);
|
||||
for (uint8_t channelNum = 0; channelNum < 8; channelNum++) {
|
||||
if (channels.getByIndex(channelNum).settings.has_module_settings &&
|
||||
channels.getByIndex(channelNum).settings.module_settings.position_precision != 0) {
|
||||
if (getPositionPrecisionForChannel(channelNum) != 0) {
|
||||
sendOurPosition(NODENUM_BROADCAST, requestReplies, channelNum);
|
||||
return;
|
||||
}
|
||||
@@ -369,10 +351,8 @@ void PositionModule::sendOurPosition(NodeNum dest, bool wantReplies, uint8_t cha
|
||||
if (prevPacketId) // if we wrap around to zero, we'll simply fail to cancel in that rare case (no big deal)
|
||||
service->cancelSending(prevPacketId);
|
||||
|
||||
// Set's the class precision value for this particular packet
|
||||
if (channels.getByIndex(channel).settings.has_module_settings) {
|
||||
precision = channels.getByIndex(channel).settings.module_settings.position_precision;
|
||||
}
|
||||
// Set the class precision value for this particular packet.
|
||||
precision = getPositionPrecisionForChannel(channel);
|
||||
|
||||
meshtastic_MeshPacket *p = allocPositionPacket();
|
||||
if (p == nullptr) {
|
||||
|
||||
@@ -95,8 +95,23 @@ int32_t StatusLEDModule::runOnce()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (power_state != charging && power_state != charged && !doing_fast_blink) {
|
||||
// If we want a LED to be dedicated to the simple hearbeat, we can use that instead of the charge LED
|
||||
#if defined(LED_HEARTBEAT)
|
||||
if (power_state != charging && power_state != charged && !doing_fast_blink && !config.device.led_heartbeat_disabled) {
|
||||
if (HEARTBEAT_LED_state == LED_STATE_ON) {
|
||||
HEARTBEAT_LED_state = LED_STATE_OFF;
|
||||
my_interval = 999;
|
||||
} else {
|
||||
HEARTBEAT_LED_state = LED_STATE_ON;
|
||||
my_interval = 1;
|
||||
}
|
||||
digitalWrite(LED_HEARTBEAT, HEARTBEAT_LED_state);
|
||||
} else {
|
||||
HEARTBEAT_LED_state = LED_STATE_OFF;
|
||||
digitalWrite(LED_HEARTBEAT, HEARTBEAT_LED_state);
|
||||
}
|
||||
#else
|
||||
if (power_state != charging && power_state != charged && !doing_fast_blink && !config.device.led_heartbeat_disabled) {
|
||||
if (CHARGE_LED_state == LED_STATE_ON) {
|
||||
CHARGE_LED_state = LED_STATE_OFF;
|
||||
my_interval = 999;
|
||||
@@ -105,7 +120,7 @@ int32_t StatusLEDModule::runOnce()
|
||||
my_interval = 1;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
if (!config.bluetooth.enabled || PAIRING_LED_starttime + 30 * 1000 < millis() || doing_fast_blink) {
|
||||
PAIRING_LED_state = LED_STATE_OFF;
|
||||
} else if (ble_state == unpaired) {
|
||||
|
||||
@@ -43,6 +43,9 @@ class StatusLEDModule : private concurrency::OSThread
|
||||
private:
|
||||
bool CHARGE_LED_state = LED_STATE_OFF;
|
||||
bool PAIRING_LED_state = LED_STATE_OFF;
|
||||
#if defined(LED_HEARTBEAT)
|
||||
bool HEARTBEAT_LED_state = LED_STATE_OFF;
|
||||
#endif
|
||||
|
||||
uint32_t PAIRING_LED_starttime = 0;
|
||||
uint32_t lastUserbuttonTime = 0;
|
||||
|
||||
@@ -115,6 +115,17 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event)
|
||||
case INPUT_BROKER_SHUTDOWN:
|
||||
shutdownAtMsec = millis();
|
||||
return true;
|
||||
// factory reset
|
||||
case INPUT_BROKER_FACTORY_RST:
|
||||
disableBluetooth();
|
||||
LOG_INFO("Initiate full factory reset");
|
||||
nodeDB->factoryReset(true);
|
||||
// reboot(DEFAULT_REBOOT_SECONDS);
|
||||
LOG_INFO("Reboot in %d seconds", DEFAULT_REBOOT_SECONDS);
|
||||
if (screen)
|
||||
screen->showSimpleBanner("Rebooting...", 0); // stays on screen
|
||||
rebootAtMsec = (DEFAULT_REBOOT_SECONDS < 0) ? 0 : (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
||||
return true;
|
||||
|
||||
default:
|
||||
// No other input events handled here
|
||||
|
||||
@@ -21,9 +21,6 @@ ProcessMessage TextMessageModule::handleReceived(const meshtastic_MeshPacket &mp
|
||||
textPacketList[textPacketListIndex] = mp.id;
|
||||
textPacketListIndex = (textPacketListIndex + 1) % TEXT_PACKET_LIST_SIZE;
|
||||
|
||||
// We only store/display messages destined for us.
|
||||
devicestate.rx_text_message = mp;
|
||||
devicestate.has_rx_text_message = true;
|
||||
IF_SCREEN(
|
||||
// Guard against running in MeshtasticUI or with no screen
|
||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||
@@ -59,4 +56,4 @@ bool TextMessageModule::recentlySeen(uint32_t id)
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
* Text message handling for Meshtastic.
|
||||
*
|
||||
* This module is responsible for receiving and storing incoming text messages
|
||||
* from the mesh. It updates device state and notifies observers so that other
|
||||
* components (such as the MessageRenderer) can later display or process them.
|
||||
* from the mesh. It notifies observers so that other components (such as the
|
||||
* MessageRenderer) can later display or process them.
|
||||
*
|
||||
* Rendering of messages on screen is no longer done here.
|
||||
*/
|
||||
@@ -36,4 +36,4 @@ class TextMessageModule : public SinglePortModule, public Observable<const mesht
|
||||
size_t textPacketListIndex = 0;
|
||||
};
|
||||
|
||||
extern TextMessageModule *textMessageModule;
|
||||
extern TextMessageModule *textMessageModule;
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
#if HAS_ETHERNET && defined(USE_WS5500)
|
||||
#include <ETHClass2.h>
|
||||
#define ETH ETH2
|
||||
#elif HAS_ETHERNET && defined(USE_CH390D)
|
||||
#include "ESP32_CH390.h"
|
||||
#define ETH CH390
|
||||
#endif // HAS_ETHERNET
|
||||
#include "Default.h"
|
||||
#if !defined(ARCH_NRF52) || NRF52_USE_JSON
|
||||
@@ -344,6 +347,9 @@ inline bool isConnectedToNetwork()
|
||||
#ifdef USE_WS5500
|
||||
if (ETH.connected())
|
||||
return true;
|
||||
#elif defined(USE_CH390D)
|
||||
if (ETH.isConnected())
|
||||
return true;
|
||||
#endif
|
||||
|
||||
#if HAS_WIFI
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
#include <WiFiClientSecure.h>
|
||||
#endif
|
||||
#endif
|
||||
#if HAS_ETHERNET && !defined(USE_WS5500)
|
||||
#if HAS_ETHERNET && !defined(USE_WS5500) && !defined(USE_CH390D)
|
||||
#include <EthernetClient.h>
|
||||
#endif
|
||||
|
||||
|
||||
@@ -587,51 +587,30 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
|
||||
virtual uint32_t onPassKeyRequest() override
|
||||
#endif
|
||||
{
|
||||
uint32_t passkey = config.bluetooth.fixed_pin;
|
||||
uint32_t configuredPasskey = config.bluetooth.fixed_pin;
|
||||
|
||||
if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN) {
|
||||
LOG_INFO("Use random passkey");
|
||||
// This is the passkey to be entered on peer - we pick a number >100,000 to ensure 6 digits
|
||||
passkey = random(100000, 999999);
|
||||
configuredPasskey = random(100000, 999999);
|
||||
}
|
||||
LOG_INFO("*** Enter passkey %d on the peer side ***", passkey);
|
||||
LOG_INFO("*** Enter passkey %d on the peer side ***", configuredPasskey);
|
||||
|
||||
powerFSM.trigger(EVENT_BLUETOOTH_PAIR);
|
||||
meshtastic::BluetoothStatus newStatus(std::to_string(passkey));
|
||||
std::string passkey = std::to_string(configuredPasskey);
|
||||
meshtastic::BluetoothStatus newStatus(passkey);
|
||||
bluetoothStatus->updateStatus(&newStatus);
|
||||
|
||||
#if HAS_SCREEN // Todo: migrate this display code back into Screen class, and observe bluetoothStatus
|
||||
if (screen) {
|
||||
screen->startAlert([passkey](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void {
|
||||
char btPIN[16] = "888888";
|
||||
snprintf(btPIN, sizeof(btPIN), "%06u", passkey);
|
||||
int x_offset = display->width() / 2;
|
||||
int y_offset = display->height() <= 80 ? 0 : 12;
|
||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
display->setFont(FONT_MEDIUM);
|
||||
display->drawString(x_offset + x, y_offset + y, "Bluetooth");
|
||||
#if !defined(M5STACK_UNITC6L)
|
||||
display->setFont(FONT_SMALL);
|
||||
y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_MEDIUM - 4 : y_offset + FONT_HEIGHT_MEDIUM + 5;
|
||||
display->drawString(x_offset + x, y_offset + y, "Enter this code");
|
||||
#endif
|
||||
display->setFont(FONT_LARGE);
|
||||
char pin[8];
|
||||
snprintf(pin, sizeof(pin), "%.3s %.3s", btPIN, btPIN + 3);
|
||||
y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_SMALL - 5 : y_offset + FONT_HEIGHT_SMALL + 5;
|
||||
display->drawString(x_offset + x, y_offset + y, pin);
|
||||
|
||||
display->setFont(FONT_SMALL);
|
||||
char deviceName[64];
|
||||
snprintf(deviceName, sizeof(deviceName), "Name: %s", getDeviceName());
|
||||
y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_LARGE - 6 : y_offset + FONT_HEIGHT_LARGE + 5;
|
||||
display->drawString(x_offset + x, y_offset + y, deviceName);
|
||||
});
|
||||
std::string ble_message = "Bluetooth\nPIN\n[M]" + passkey.substr(0, 3) + " " + passkey.substr(3, 6);
|
||||
screen->showSimpleBanner(ble_message.c_str(), 30000);
|
||||
}
|
||||
#endif
|
||||
passkeyShowing = true;
|
||||
|
||||
return passkey;
|
||||
return configuredPasskey;
|
||||
}
|
||||
|
||||
#ifdef NIMBLE_TWO
|
||||
|
||||
@@ -146,6 +146,8 @@
|
||||
#define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M2
|
||||
#elif defined(ELECROW_ThinkNode_M5)
|
||||
#define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M5
|
||||
#elif defined(ELECROW_ThinkNode_M7)
|
||||
#define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M7
|
||||
#elif defined(ESP32_S3_PICO)
|
||||
#define HW_VENDOR meshtastic_HardwareModel_ESP32_S3_PICO
|
||||
#elif defined(SENSELORA_S3)
|
||||
@@ -156,6 +158,8 @@
|
||||
#define HW_VENDOR meshtastic_HardwareModel_CHATTER_2
|
||||
#elif defined(STATION_G2)
|
||||
#define HW_VENDOR meshtastic_HardwareModel_STATION_G2
|
||||
#elif defined(STATION_G3)
|
||||
#define HW_VENDOR meshtastic_HardwareModel_STATION_G3
|
||||
#elif defined(UNPHONE)
|
||||
#define HW_VENDOR meshtastic_HardwareModel_UNPHONE
|
||||
#elif defined(WIPHONE)
|
||||
|
||||
@@ -32,7 +32,7 @@ void variant_shutdown() {}
|
||||
#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !MESHTASTIC_EXCLUDE_BLUETOOTH
|
||||
void setBluetoothEnable(bool enable)
|
||||
{
|
||||
#ifdef USE_WS5500
|
||||
#if defined(USE_WS5500) || defined(USE_CH390D)
|
||||
if ((config.bluetooth.enabled == true) && (config.network.wifi_enabled == false))
|
||||
#elif HAS_WIFI
|
||||
if (!isWifiAvailable() && config.bluetooth.enabled == true)
|
||||
|
||||
@@ -386,34 +386,12 @@ bool NRF52Bluetooth::onPairingPasskey(uint16_t conn_handle, uint8_t const passke
|
||||
meshtastic::BluetoothStatus newStatus(textkey);
|
||||
bluetoothStatus->updateStatus(&newStatus);
|
||||
|
||||
#if HAS_SCREEN && \
|
||||
!defined(MESHTASTIC_EXCLUDE_SCREEN) // Todo: migrate this display code back into Screen class, and observe bluetoothStatus
|
||||
#if HAS_SCREEN && !defined(MESHTASTIC_EXCLUDE_SCREEN)
|
||||
if (screen) {
|
||||
screen->startAlert([](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void {
|
||||
char btPIN[16] = "888888";
|
||||
snprintf(btPIN, sizeof(btPIN), "%06u", configuredPasskey);
|
||||
int x_offset = display->width() / 2;
|
||||
int y_offset = display->height() <= 80 ? 0 : 12;
|
||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
display->setFont(FONT_MEDIUM);
|
||||
display->drawString(x_offset + x, y_offset + y, "Bluetooth");
|
||||
|
||||
display->setFont(FONT_SMALL);
|
||||
y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_MEDIUM - 4 : y_offset + FONT_HEIGHT_MEDIUM + 5;
|
||||
display->drawString(x_offset + x, y_offset + y, "Enter this code");
|
||||
|
||||
display->setFont(FONT_LARGE);
|
||||
String displayPin(btPIN);
|
||||
String pin = displayPin.substring(0, 3) + " " + displayPin.substring(3, 6);
|
||||
y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_SMALL - 5 : y_offset + FONT_HEIGHT_SMALL + 5;
|
||||
display->drawString(x_offset + x, y_offset + y, pin);
|
||||
|
||||
display->setFont(FONT_SMALL);
|
||||
String deviceName = "Name: ";
|
||||
deviceName.concat(getDeviceName());
|
||||
y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_LARGE - 6 : y_offset + FONT_HEIGHT_LARGE + 5;
|
||||
display->drawString(x_offset + x, y_offset + y, deviceName);
|
||||
});
|
||||
std::string configuredPasskeyText = std::to_string(configuredPasskey);
|
||||
std::string ble_message =
|
||||
"Bluetooth\nPIN\n[M]" + configuredPasskeyText.substr(0, 3) + " " + configuredPasskeyText.substr(3, 6);
|
||||
screen->showSimpleBanner(ble_message.c_str(), 30000);
|
||||
}
|
||||
#endif
|
||||
passkeyShowing = true;
|
||||
|
||||
@@ -0,0 +1,824 @@
|
||||
/**
|
||||
* Arduino.h — Zephyr compatibility shim for nRF54L15
|
||||
*
|
||||
* Provides the Arduino API surface expected by Meshtastic, backed by
|
||||
* Zephyr primitives. Only the subset actually used by Meshtastic is
|
||||
* implemented; the rest compiles as no-ops / stubs for now.
|
||||
*
|
||||
* Phase 2: compile only. Real GPIO / SPI / Wire implementations follow
|
||||
* in Phase 3 once the build is clean.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#ifndef Arduino_h
|
||||
#define Arduino_h
|
||||
|
||||
// ── C standard headers ───────────────────────────────────────────────────────
|
||||
#include <math.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h> /* strcasecmp, strncasecmp */
|
||||
|
||||
// ── Zephyr kernel ────────────────────────────────────────────────────────────
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/sys/reboot.h>
|
||||
|
||||
// ── Basic Arduino types ──────────────────────────────────────────────────────
|
||||
typedef bool boolean;
|
||||
typedef uint8_t byte;
|
||||
typedef uint16_t word;
|
||||
|
||||
// ── Pin / digital constants ──────────────────────────────────────────────────
|
||||
#define INPUT 0u
|
||||
#define OUTPUT 1u
|
||||
#define INPUT_PULLUP 2u
|
||||
#define INPUT_PULLDOWN 3u
|
||||
#define OUTPUT_OPENDRAIN 4u
|
||||
|
||||
#define HIGH 1u
|
||||
#define LOW 0u
|
||||
|
||||
#define CHANGE 1
|
||||
#define FALLING 2
|
||||
#define RISING 3
|
||||
|
||||
#ifndef LED_BUILTIN
|
||||
#define LED_BUILTIN -1
|
||||
#endif
|
||||
|
||||
// ── Math / trig constants ────────────────────────────────────────────────────
|
||||
#ifndef PI
|
||||
#define PI 3.14159265358979323846
|
||||
#endif
|
||||
#define HALF_PI 1.57079632679489661923
|
||||
#define TWO_PI 6.28318530717958647693
|
||||
#define DEG_TO_RAD 0.01745329251994329576
|
||||
#define RAD_TO_DEG 57.2957795130823208767
|
||||
#define EULER 2.71828182845904523536
|
||||
|
||||
// ── Bit utilities ────────────────────────────────────────────────────────────
|
||||
#define bitRead(v, b) (((v) >> (b)) & 1)
|
||||
#define bitSet(v, b) ((v) |= (1UL << (b)))
|
||||
#define bitClear(v, b) ((v) &= ~(1UL << (b)))
|
||||
#define bitToggle(v, b) ((v) ^= (1UL << (b)))
|
||||
#define bitWrite(v, b, x) ((x) ? bitSet(v, b) : bitClear(v, b))
|
||||
#define bit(b) (1UL << (b))
|
||||
#define lowByte(w) ((uint8_t)((w)&0xff))
|
||||
#define highByte(w) ((uint8_t)((w) >> 8))
|
||||
// word(h,l) — only define if not already defined (conflicts with typedef above)
|
||||
#undef word
|
||||
#define word(h, l) ((uint16_t)(((h) << 8) | (l)))
|
||||
|
||||
// ── UART config constants ─────────────────────────────────────────────────────
|
||||
#define SERIAL_8N1 0x800001cu
|
||||
#define SERIAL_8N2 0x8000001eu
|
||||
#define SERIAL_8E1 0x8000001eu
|
||||
#define SERIAL_7E1 0x8000001cu
|
||||
|
||||
// ── Integer order ────────────────────────────────────────────────────────────
|
||||
// Adafruit BusIO's SPIDevice.h has `typedef BitOrder BusIOBitOrder;` which
|
||||
// requires BitOrder to be a *type*, not a macro. Mirror the ArduinoCore-API
|
||||
// enum definition rather than #defines.
|
||||
enum BitOrder : uint8_t {
|
||||
LSBFIRST = 0,
|
||||
MSBFIRST = 1,
|
||||
};
|
||||
|
||||
// ── pgmspace compatibility (no-ops on Cortex-M) ──────────────────────────────
|
||||
#define PROGMEM
|
||||
#define PSTR(s) (s)
|
||||
#define F(s) (s)
|
||||
#define pgm_read_byte(addr) (*((const uint8_t *)(addr)))
|
||||
#define pgm_read_word(addr) (*((const uint16_t *)(addr)))
|
||||
#define pgm_read_dword(addr) (*((const uint32_t *)(addr)))
|
||||
#define pgm_read_float(addr) (*((const float *)(addr)))
|
||||
#define pgm_read_ptr(addr) (*((const void **)(addr)))
|
||||
#define strlen_P(s) strlen(s)
|
||||
#define strcpy_P(d, s) strcpy(d, s)
|
||||
#define strncpy_P(d, s, n) strncpy(d, s, n)
|
||||
#define strcmp_P(a, b) strcmp(a, b)
|
||||
#define memcpy_P(d, s, n) memcpy(d, s, n)
|
||||
#define sprintf_P sprintf
|
||||
typedef const char *PGM_P;
|
||||
typedef const char *PGM_VOID_P;
|
||||
|
||||
// ── Arduino numeric base constants (used by Print, RadioLib, etc.) ───────────
|
||||
#define DEC 10
|
||||
#define HEX 16
|
||||
#define OCT 8
|
||||
#define BIN 2
|
||||
|
||||
// ── ulong / uint typedef (used by RadioLibInterface, etc.) ───────────────────
|
||||
typedef unsigned long ulong;
|
||||
typedef unsigned int uint;
|
||||
|
||||
// ── Interrupt stubs ──────────────────────────────────────────────────────────
|
||||
static inline void interrupts() {}
|
||||
static inline void noInterrupts() {}
|
||||
#define digitalPinToInterrupt(p) (p)
|
||||
|
||||
// ── portMAX_DELAY — freertosinc.h also defines this; let it win ──────────────
|
||||
// We intentionally do NOT define portMAX_DELAY here. freertosinc.h defines
|
||||
// it for the FreeRTOS / Meshtastic threading layer and must not be overridden.
|
||||
|
||||
// ── Timing & system functions — declared with C linkage ──────────────────────
|
||||
// buzz.cpp and others forward-declare delay() as extern "C"; keep linkage
|
||||
// consistent by wrapping in extern "C" here.
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
void NVIC_SystemReset(void);
|
||||
uint32_t millis(void);
|
||||
uint32_t micros(void);
|
||||
void delay(uint32_t ms);
|
||||
void delayMicroseconds(uint32_t us);
|
||||
void yield(void);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
#include <cctype>
|
||||
#include <cstdarg>
|
||||
|
||||
// ── C++ STL — include BEFORE defining any min/max helpers ───────────────────
|
||||
// Include algorithm first so its min/max templates are in scope.
|
||||
// We must NOT define min/max as function-like macros: the C++ STL uses
|
||||
// 3-argument versions (min(a,b,comp)) that the preprocessor would treat as
|
||||
// calling a 2-arg macro with 3 args.
|
||||
#include <algorithm>
|
||||
// Bring 2-arg std::min / std::max into the global namespace as unqualified
|
||||
// names so that Arduino code calling min(a,b) continues to compile.
|
||||
// (Arduino convention; kept minimal to avoid surprises.)
|
||||
#undef min
|
||||
#undef max
|
||||
using std::max;
|
||||
using std::min;
|
||||
|
||||
// ── Arduino math helpers (macros safe for mixed-type / C calls) ──────────────
|
||||
#ifndef abs
|
||||
#define abs(x) ((x) >= 0 ? (x) : -(x))
|
||||
#endif
|
||||
#define constrain(x, l, h) ((x) < (l) ? (l) : ((x) > (h) ? (h) : (x)))
|
||||
#define round(x) ((x) >= 0 ? (long)((x) + 0.5) : (long)((x)-0.5))
|
||||
#define radians(d) ((d)*DEG_TO_RAD)
|
||||
#define degrees(r) ((r)*RAD_TO_DEG)
|
||||
#define sq(x) ((x) * (x))
|
||||
|
||||
// ── Random ───────────────────────────────────────────────────────────────────
|
||||
static inline void randomSeed(unsigned long seed)
|
||||
{
|
||||
srand((unsigned int)seed);
|
||||
}
|
||||
static inline long random(void)
|
||||
{
|
||||
return (long)rand();
|
||||
}
|
||||
static inline long random(long bound)
|
||||
{
|
||||
return bound > 0 ? (rand() % bound) : 0;
|
||||
}
|
||||
static inline long random(long lo, long hi)
|
||||
{
|
||||
return hi > lo ? lo + rand() % (hi - lo) : lo;
|
||||
}
|
||||
|
||||
// ── GPIO — real Zephyr implementation (Phase 3) ──────────────────────────────
|
||||
// Implemented in nrf54l15_arduino.cpp using Zephyr GPIO/SPI APIs.
|
||||
// Pin numbering: P0.n = n, P1.n = 16+n, P2.n = 32+n
|
||||
void pinMode(uint32_t pin, uint32_t mode);
|
||||
void digitalWrite(uint32_t pin, uint32_t value);
|
||||
int digitalRead(uint32_t pin);
|
||||
static inline void digitalToggle(uint32_t pin)
|
||||
{
|
||||
digitalWrite(pin, !digitalRead(pin));
|
||||
}
|
||||
static inline uint32_t analogRead(uint32_t)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
static inline void analogWrite(uint32_t, uint32_t) {}
|
||||
static inline void analogReadResolution(int) {}
|
||||
static inline void analogWriteResolution(int) {}
|
||||
|
||||
// ── __WFI — provided by CMSIS core_cm33.h; do NOT redefine here ─────────────
|
||||
|
||||
// ── __FlashStringHelper — Arduino PROGMEM string class (no-op on Cortex-M) ──
|
||||
class __FlashStringHelper;
|
||||
|
||||
// ── attachInterrupt / detachInterrupt — real Zephyr GPIO interrupt impl ──────
|
||||
typedef void (*voidFuncPtr)(void);
|
||||
void attachInterrupt(uint32_t pin, voidFuncPtr cb, int mode);
|
||||
void detachInterrupt(uint32_t pin);
|
||||
|
||||
// ── Forward declaration of String (needed by Print / Stream) ─────────────────
|
||||
class String;
|
||||
|
||||
// ── Print base class ─────────────────────────────────────────────────────────
|
||||
class Print
|
||||
{
|
||||
public:
|
||||
virtual size_t write(uint8_t c) = 0;
|
||||
virtual size_t write(const uint8_t *buf, size_t n)
|
||||
{
|
||||
size_t written = 0;
|
||||
while (n--)
|
||||
written += write(*buf++);
|
||||
return written;
|
||||
}
|
||||
size_t write(const char *s) { return s ? write((const uint8_t *)s, strlen(s)) : 0; }
|
||||
size_t write(const char *s, size_t n) { return write((const uint8_t *)s, n); }
|
||||
|
||||
size_t print(const char *s) { return s ? write((const uint8_t *)s, strlen(s)) : 0; }
|
||||
int printf(const char *fmt, ...) __attribute__((format(printf, 2, 3)));
|
||||
|
||||
size_t print(char c) { return write((uint8_t)c); }
|
||||
size_t print(const String &s);
|
||||
size_t print(unsigned char n, int base = 10);
|
||||
size_t print(int n, int base = 10);
|
||||
size_t print(long n, int base = 10);
|
||||
size_t print(unsigned int n, int base = 10);
|
||||
size_t print(unsigned long n, int base = 10);
|
||||
size_t print(float n, int digits = 2);
|
||||
size_t print(double n, int digits = 2);
|
||||
size_t print(bool b) { return print(b ? "true" : "false"); }
|
||||
|
||||
size_t println() { return write((uint8_t)'\n'); }
|
||||
size_t println(const char *s)
|
||||
{
|
||||
size_t r = print(s);
|
||||
return r + println();
|
||||
}
|
||||
size_t println(char c)
|
||||
{
|
||||
size_t r = print(c);
|
||||
return r + println();
|
||||
}
|
||||
size_t println(const String &s);
|
||||
size_t println(int n, int base = 10)
|
||||
{
|
||||
size_t r = print(n, base);
|
||||
return r + println();
|
||||
}
|
||||
size_t println(long n, int base = 10)
|
||||
{
|
||||
size_t r = print(n, base);
|
||||
return r + println();
|
||||
}
|
||||
size_t println(unsigned long n, int base = 10)
|
||||
{
|
||||
size_t r = print(n, base);
|
||||
return r + println();
|
||||
}
|
||||
size_t println(unsigned int n, int base = 10)
|
||||
{
|
||||
size_t r = print(n, base);
|
||||
return r + println();
|
||||
}
|
||||
size_t println(float n, int d = 2)
|
||||
{
|
||||
size_t r = print(n, d);
|
||||
return r + println();
|
||||
}
|
||||
size_t println(double n, int d = 2)
|
||||
{
|
||||
size_t r = print(n, d);
|
||||
return r + println();
|
||||
}
|
||||
size_t println(bool b)
|
||||
{
|
||||
size_t r = print(b);
|
||||
return r + println();
|
||||
}
|
||||
|
||||
virtual void flush() {}
|
||||
};
|
||||
|
||||
// ── Stream base class ────────────────────────────────────────────────────────
|
||||
class Stream : public Print
|
||||
{
|
||||
public:
|
||||
virtual int available() = 0;
|
||||
virtual int read() = 0;
|
||||
virtual int peek() = 0;
|
||||
virtual void setTimeout(unsigned long) {}
|
||||
virtual bool find(const char *) { return false; }
|
||||
String readString();
|
||||
String readStringUntil(char terminator);
|
||||
};
|
||||
|
||||
// ── Minimal Arduino String class (backed by a char buffer) ───────────────────
|
||||
class String
|
||||
{
|
||||
public:
|
||||
String() : _buf(nullptr), _len(0), _cap(0) {}
|
||||
// Implicit conversion is part of the Arduino String contract, used pervasively across the codebase.
|
||||
// cppcheck-suppress noExplicitConstructor
|
||||
String(const char *cstr) : _buf(nullptr), _len(0), _cap(0)
|
||||
{
|
||||
if (cstr)
|
||||
assign(cstr, strlen(cstr));
|
||||
}
|
||||
// cppcheck-suppress noExplicitConstructor
|
||||
String(const String &s) : _buf(nullptr), _len(0), _cap(0) { assign(s._buf ? s._buf : "", s._len); }
|
||||
// cppcheck-suppress noExplicitConstructor
|
||||
String(char c) : _buf(nullptr), _len(0), _cap(0)
|
||||
{
|
||||
const char tmp[2] = {c, 0};
|
||||
assign(tmp, 1);
|
||||
}
|
||||
// cppcheck-suppress noExplicitConstructor
|
||||
String(int n) : _buf(nullptr), _len(0), _cap(0)
|
||||
{
|
||||
char tmp[16];
|
||||
snprintf(tmp, 16, "%d", n);
|
||||
assign(tmp, strlen(tmp));
|
||||
}
|
||||
// cppcheck-suppress noExplicitConstructor
|
||||
String(unsigned int n) : _buf(nullptr), _len(0), _cap(0)
|
||||
{
|
||||
char tmp[16];
|
||||
snprintf(tmp, 16, "%u", n);
|
||||
assign(tmp, strlen(tmp));
|
||||
}
|
||||
// cppcheck-suppress noExplicitConstructor
|
||||
String(long n) : _buf(nullptr), _len(0), _cap(0)
|
||||
{
|
||||
char tmp[24];
|
||||
snprintf(tmp, 24, "%ld", n);
|
||||
assign(tmp, strlen(tmp));
|
||||
}
|
||||
// cppcheck-suppress noExplicitConstructor
|
||||
String(unsigned long n) : _buf(nullptr), _len(0), _cap(0)
|
||||
{
|
||||
char tmp[24];
|
||||
snprintf(tmp, 24, "%lu", n);
|
||||
assign(tmp, strlen(tmp));
|
||||
}
|
||||
// cppcheck-suppress noExplicitConstructor
|
||||
String(float n, int d = 2) : _buf(nullptr), _len(0), _cap(0)
|
||||
{
|
||||
char tmp[32];
|
||||
snprintf(tmp, 32, "%.*f", d, n);
|
||||
assign(tmp, strlen(tmp));
|
||||
}
|
||||
// cppcheck-suppress noExplicitConstructor
|
||||
String(double n, int d = 2) : _buf(nullptr), _len(0), _cap(0)
|
||||
{
|
||||
char tmp[32];
|
||||
snprintf(tmp, 32, "%.*f", d, (double)n);
|
||||
assign(tmp, strlen(tmp));
|
||||
}
|
||||
~String() { free(_buf); }
|
||||
|
||||
String &operator=(const String &s)
|
||||
{
|
||||
assign(s._buf ? s._buf : "", s._len);
|
||||
return *this;
|
||||
}
|
||||
String &operator=(const char *s)
|
||||
{
|
||||
assign(s ? s : "", s ? strlen(s) : 0);
|
||||
return *this;
|
||||
}
|
||||
String &operator=(char c)
|
||||
{
|
||||
const char tmp[2] = {c, 0};
|
||||
assign(tmp, 1);
|
||||
return *this;
|
||||
}
|
||||
|
||||
String &operator+=(const String &s)
|
||||
{
|
||||
concat(s._buf ? s._buf : "", s._len);
|
||||
return *this;
|
||||
}
|
||||
String &operator+=(const char *s)
|
||||
{
|
||||
if (s)
|
||||
concat(s, strlen(s));
|
||||
return *this;
|
||||
}
|
||||
String &operator+=(char c)
|
||||
{
|
||||
concat(&c, 1);
|
||||
return *this;
|
||||
}
|
||||
String &operator+=(int n) { return *this += String(n); }
|
||||
String &operator+=(unsigned long n) { return *this += String(n); }
|
||||
|
||||
String operator+(const String &rhs) const
|
||||
{
|
||||
String r(*this);
|
||||
r += rhs;
|
||||
return r;
|
||||
}
|
||||
String operator+(const char *rhs) const
|
||||
{
|
||||
String r(*this);
|
||||
r += rhs;
|
||||
return r;
|
||||
}
|
||||
String operator+(char rhs) const
|
||||
{
|
||||
String r(*this);
|
||||
r += rhs;
|
||||
return r;
|
||||
}
|
||||
|
||||
bool operator==(const String &s) const { return _len == s._len && (_len == 0 || strcmp(_buf, s._buf) == 0); }
|
||||
bool operator==(const char *s) const { return s && strcmp(c_str(), s) == 0; }
|
||||
bool operator!=(const String &s) const { return !(*this == s); }
|
||||
bool operator!=(const char *s) const { return !(*this == s); }
|
||||
bool operator<(const String &s) const { return strcmp(c_str(), s.c_str()) < 0; }
|
||||
bool operator>(const String &s) const { return strcmp(c_str(), s.c_str()) > 0; }
|
||||
|
||||
char operator[](unsigned int i) const { return (_buf && i < _len) ? _buf[i] : 0; }
|
||||
char &operator[](unsigned int i)
|
||||
{
|
||||
static char dummy = 0;
|
||||
return (_buf && i < _len) ? _buf[i] : dummy;
|
||||
}
|
||||
|
||||
const char *c_str() const { return _buf ? _buf : ""; }
|
||||
unsigned int length() const { return _len; }
|
||||
bool isEmpty() const { return _len == 0; }
|
||||
bool equals(const String &s) const { return *this == s; }
|
||||
bool equals(const char *s) const { return *this == s; }
|
||||
bool equalsIgnoreCase(const String &s) const
|
||||
{
|
||||
if (_len != s._len)
|
||||
return false;
|
||||
for (unsigned i = 0; i < _len; i++)
|
||||
if (std::tolower(_buf[i]) != std::tolower(s._buf[i]))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
bool startsWith(const String &pfx) const
|
||||
{
|
||||
if (pfx._len > _len)
|
||||
return false;
|
||||
return strncmp(c_str(), pfx.c_str(), pfx._len) == 0;
|
||||
}
|
||||
bool startsWith(const char *pfx) const
|
||||
{
|
||||
if (!pfx)
|
||||
return false;
|
||||
size_t pl = strlen(pfx);
|
||||
return pl <= _len && strncmp(c_str(), pfx, pl) == 0;
|
||||
}
|
||||
bool endsWith(const String &sfx) const
|
||||
{
|
||||
if (sfx._len > _len)
|
||||
return false;
|
||||
return strcmp(c_str() + _len - sfx._len, sfx.c_str()) == 0;
|
||||
}
|
||||
int indexOf(char c, unsigned from = 0) const
|
||||
{
|
||||
if (!_buf)
|
||||
return -1;
|
||||
const char *p = strchr(_buf + from, c);
|
||||
return p ? (int)(p - _buf) : -1;
|
||||
}
|
||||
int indexOf(const String &s, unsigned from = 0) const
|
||||
{
|
||||
if (!_buf)
|
||||
return -1;
|
||||
const char *p = strstr(_buf + from, s.c_str());
|
||||
return p ? (int)(p - _buf) : -1;
|
||||
}
|
||||
int lastIndexOf(char c) const
|
||||
{
|
||||
if (!_buf)
|
||||
return -1;
|
||||
const char *p = strrchr(_buf, c);
|
||||
return p ? (int)(p - _buf) : -1;
|
||||
}
|
||||
String substring(unsigned beginIndex) const
|
||||
{
|
||||
if (!_buf || beginIndex >= _len)
|
||||
return String();
|
||||
return String(_buf + beginIndex);
|
||||
}
|
||||
String substring(unsigned beginIndex, unsigned endIndex) const
|
||||
{
|
||||
if (!_buf || beginIndex >= _len)
|
||||
return String();
|
||||
if (endIndex > _len)
|
||||
endIndex = _len;
|
||||
if (endIndex <= beginIndex)
|
||||
return String();
|
||||
String r;
|
||||
r.assign(_buf + beginIndex, endIndex - beginIndex);
|
||||
return r;
|
||||
}
|
||||
void toUpperCase()
|
||||
{
|
||||
if (_buf)
|
||||
for (unsigned i = 0; i < _len; i++)
|
||||
_buf[i] = (char)std::toupper(_buf[i]);
|
||||
}
|
||||
void toLowerCase()
|
||||
{
|
||||
if (_buf)
|
||||
for (unsigned i = 0; i < _len; i++)
|
||||
_buf[i] = (char)std::tolower(_buf[i]);
|
||||
}
|
||||
void trim()
|
||||
{
|
||||
if (!_buf || _len == 0)
|
||||
return;
|
||||
unsigned s = 0;
|
||||
while (s < _len && std::isspace(_buf[s]))
|
||||
s++;
|
||||
unsigned e = _len;
|
||||
while (e > s && std::isspace(_buf[e - 1]))
|
||||
e--;
|
||||
if (s > 0 || e < _len) {
|
||||
memmove(_buf, _buf + s, e - s);
|
||||
_len = e - s;
|
||||
_buf[_len] = 0;
|
||||
}
|
||||
}
|
||||
void replace(char from, char to)
|
||||
{
|
||||
if (_buf)
|
||||
for (unsigned i = 0; i < _len; i++)
|
||||
if (_buf[i] == from)
|
||||
_buf[i] = to;
|
||||
}
|
||||
void replace(const String &from, const String &to);
|
||||
bool remove(unsigned index, unsigned count = 1)
|
||||
{
|
||||
if (!_buf || index >= _len)
|
||||
return false;
|
||||
if (index + count > _len)
|
||||
count = _len - index;
|
||||
memmove(_buf + index, _buf + index + count, _len - index - count + 1);
|
||||
_len -= count;
|
||||
return true;
|
||||
}
|
||||
void clear()
|
||||
{
|
||||
_len = 0;
|
||||
if (_buf)
|
||||
_buf[0] = 0;
|
||||
}
|
||||
char charAt(unsigned i) const { return (*this)[i]; }
|
||||
void setCharAt(unsigned i, char c)
|
||||
{
|
||||
if (_buf && i < _len)
|
||||
_buf[i] = c;
|
||||
}
|
||||
void toCharArray(char *buf, unsigned int bufsize, unsigned int index = 0) const
|
||||
{
|
||||
if (!buf || bufsize == 0)
|
||||
return;
|
||||
unsigned int avail = (_buf && _len > index) ? (_len - index) : 0;
|
||||
unsigned int copy = avail < bufsize - 1 ? avail : bufsize - 1;
|
||||
if (copy > 0)
|
||||
memcpy(buf, _buf + index, copy);
|
||||
buf[copy] = '\0';
|
||||
}
|
||||
void concat(const String &s) { *this += s; }
|
||||
void concat(const char *s) { *this += s; }
|
||||
long toInt() const { return _buf ? atol(_buf) : 0; }
|
||||
float toFloat() const { return _buf ? (float)atof(_buf) : 0.0f; }
|
||||
double toDouble() const { return _buf ? atof(_buf) : 0.0; }
|
||||
|
||||
private:
|
||||
char *_buf;
|
||||
unsigned int _len;
|
||||
unsigned int _cap;
|
||||
|
||||
void assign(const char *s, unsigned int n)
|
||||
{
|
||||
if (n >= _cap)
|
||||
reserve(n + 1);
|
||||
if (_buf) {
|
||||
memcpy(_buf, s, n);
|
||||
_buf[n] = 0;
|
||||
_len = n;
|
||||
}
|
||||
}
|
||||
void concat(const char *s, unsigned int n)
|
||||
{
|
||||
if (!s || n == 0)
|
||||
return;
|
||||
unsigned newlen = _len + n;
|
||||
if (newlen >= _cap)
|
||||
reserve(newlen + 1);
|
||||
if (_buf) {
|
||||
memcpy(_buf + _len, s, n);
|
||||
_len = newlen;
|
||||
_buf[_len] = 0;
|
||||
}
|
||||
}
|
||||
void reserve(unsigned int n)
|
||||
{
|
||||
char *b = (char *)realloc(_buf, n);
|
||||
if (b) {
|
||||
_buf = b;
|
||||
_cap = n;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
inline String operator+(const char *lhs, const String &rhs)
|
||||
{
|
||||
return String(lhs) + rhs;
|
||||
}
|
||||
inline String operator+(char lhs, const String &rhs)
|
||||
{
|
||||
return String(lhs) + rhs;
|
||||
}
|
||||
|
||||
// ── Print inline definitions that need String ────────────────────────────────
|
||||
inline size_t Print::print(const String &s)
|
||||
{
|
||||
return write((const uint8_t *)s.c_str(), s.length());
|
||||
}
|
||||
inline size_t Print::println(const String &s)
|
||||
{
|
||||
size_t r = print(s);
|
||||
return r + println();
|
||||
}
|
||||
|
||||
// ── Stream inline definitions that need String ───────────────────────────────
|
||||
inline String Stream::readString()
|
||||
{
|
||||
return String();
|
||||
}
|
||||
inline String Stream::readStringUntil(char)
|
||||
{
|
||||
return String();
|
||||
}
|
||||
|
||||
// ── HardwareSerial ───────────────────────────────────────────────────────────
|
||||
class HardwareSerial : public Stream
|
||||
{
|
||||
public:
|
||||
void begin(unsigned long) {}
|
||||
void begin(unsigned long, uint16_t) {}
|
||||
void end() {}
|
||||
void setPins(int rx, int tx) {}
|
||||
void setPinout(int tx, int rx) {}
|
||||
void setFIFOSize(size_t) {}
|
||||
void setRxBufferSize(size_t) {}
|
||||
void begin(unsigned long baud, uint32_t config, int8_t rx = -1, int8_t tx = -1, bool invert = false) {}
|
||||
int available() override { return 0; }
|
||||
int read() override { return -1; }
|
||||
int peek() override { return -1; }
|
||||
size_t write(uint8_t c) override;
|
||||
size_t write(const uint8_t *buf, size_t n) override;
|
||||
using Print::write; // un-hide base class write(const char*)
|
||||
size_t readBytes(uint8_t *buf, size_t len) { return 0; }
|
||||
size_t readBytes(char *buf, size_t len) { return 0; }
|
||||
operator bool() const { return true; }
|
||||
void flush() override {}
|
||||
String readString() { return String(); }
|
||||
String readStringUntil(char) { return String(); }
|
||||
};
|
||||
|
||||
// Uart — nRF52 BSP alias for HardwareSerial (used by GPS.h when ARCH_NRF52)
|
||||
typedef HardwareSerial Uart;
|
||||
|
||||
extern HardwareSerial Serial;
|
||||
extern HardwareSerial Serial1;
|
||||
extern HardwareSerial Serial2;
|
||||
|
||||
// ── map() utility ────────────────────────────────────────────────────────────
|
||||
static inline long map(long x, long in_min, long in_max, long out_min, long out_max)
|
||||
{
|
||||
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
|
||||
}
|
||||
|
||||
// ── shiftIn / shiftOut stubs ─────────────────────────────────────────────────
|
||||
static inline uint8_t shiftIn(uint8_t, uint8_t, uint8_t)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
static inline void shiftOut(uint8_t, uint8_t, uint8_t, uint8_t) {}
|
||||
|
||||
// ── tone / noTone stubs ──────────────────────────────────────────────────────
|
||||
static inline void tone(uint8_t, unsigned int, unsigned long = 0) {}
|
||||
static inline void noTone(uint8_t) {}
|
||||
|
||||
// ── pulseIn stub ─────────────────────────────────────────────────────────────
|
||||
static inline unsigned long pulseIn(uint8_t, uint8_t, unsigned long = 1000000UL)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ── strdup / stpcpy — POSIX extensions not in Zephyr newlib ─────────────────
|
||||
#ifndef strdup
|
||||
static inline char *strdup(const char *s)
|
||||
{
|
||||
size_t n = strlen(s) + 1;
|
||||
char *d = (char *)malloc(n);
|
||||
if (d)
|
||||
memcpy(d, s, n);
|
||||
return d;
|
||||
}
|
||||
#endif
|
||||
#ifndef stpcpy
|
||||
static inline char *stpcpy(char *dst, const char *src)
|
||||
{
|
||||
while ((*dst++ = *src++) != '\0') {
|
||||
}
|
||||
return dst - 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
// ── strnstr — BSD extension not in Zephyr libc; defined in meshUtils.cpp ─────
|
||||
// Declare here so callers (GPS.cpp etc.) don't need ARCH_PORTDUINO.
|
||||
#ifndef STRNSTR
|
||||
#define STRNSTR
|
||||
char *strnstr(const char *s, const char *find, size_t slen);
|
||||
#endif
|
||||
|
||||
// ── strlcpy — BSD extension; implementation in nrf54l15_arduino.cpp ──────────
|
||||
#ifndef HAVE_STRLCPY
|
||||
#define HAVE_STRLCPY
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
size_t strlcpy(char *dst, const char *src, size_t size);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// ── setenv / getenv / tzset — Zephyr stubs for timezone support ──────────────
|
||||
#include <stdlib.h>
|
||||
static inline int setenv(const char *, const char *, int)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
static inline void tzset(void) {}
|
||||
|
||||
// ── dbgHeapFree / dbgHeapTotal — nRF52 BSP heap diagnostics ─────────────────
|
||||
// Used by memGet.cpp when ARCH_NRF52 is defined. Return 0 for Phase 2.
|
||||
static inline uint32_t dbgHeapFree(void)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
static inline uint32_t dbgHeapTotal(void)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ── WCharacter helpers ───────────────────────────────────────────────────────
|
||||
static inline bool isAlpha(char c)
|
||||
{
|
||||
return std::isalpha((unsigned char)c) != 0;
|
||||
}
|
||||
static inline bool isAlphaNumeric(char c)
|
||||
{
|
||||
return std::isalnum((unsigned char)c) != 0;
|
||||
}
|
||||
static inline bool isDigit(char c)
|
||||
{
|
||||
return std::isdigit((unsigned char)c) != 0;
|
||||
}
|
||||
static inline bool isSpace(char c)
|
||||
{
|
||||
return std::isspace((unsigned char)c) != 0;
|
||||
}
|
||||
static inline bool isUpperCase(char c)
|
||||
{
|
||||
return std::isupper((unsigned char)c) != 0;
|
||||
}
|
||||
static inline bool isLowerCase(char c)
|
||||
{
|
||||
return std::islower((unsigned char)c) != 0;
|
||||
}
|
||||
static inline char toUpperCase(char c)
|
||||
{
|
||||
return (char)std::toupper((unsigned char)c);
|
||||
}
|
||||
static inline char toLowerCase(char c)
|
||||
{
|
||||
return (char)std::tolower((unsigned char)c);
|
||||
}
|
||||
|
||||
#else /* C only */
|
||||
#ifndef min
|
||||
#define min(a, b) ((a) < (b) ? (a) : (b))
|
||||
#endif
|
||||
#ifndef max
|
||||
#define max(a, b) ((a) > (b) ? (a) : (b))
|
||||
#endif
|
||||
#ifndef abs
|
||||
#define abs(x) ((x) >= 0 ? (x) : -(x))
|
||||
#endif
|
||||
#define constrain(x, l, h) ((x) < (l) ? (l) : ((x) > (h) ? (h) : (x)))
|
||||
#define round(x) ((x) >= 0 ? (long)((x) + 0.5) : (long)((x)-0.5))
|
||||
#endif /* __cplusplus */
|
||||
|
||||
#endif /* Arduino_h */
|
||||
@@ -0,0 +1,34 @@
|
||||
// IPAddress.h — stub for nRF54L15/Zephyr
|
||||
// MQTT.cpp includes this for IPv4 address representation.
|
||||
// Phase 2: compile-only stub.
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
class IPAddress
|
||||
{
|
||||
public:
|
||||
IPAddress() : _addr(0) {}
|
||||
explicit IPAddress(uint32_t addr) : _addr(addr) {}
|
||||
IPAddress(uint8_t a, uint8_t b, uint8_t c, uint8_t d)
|
||||
: _addr(((uint32_t)a) | ((uint32_t)b << 8) | ((uint32_t)c << 16) | ((uint32_t)d << 24))
|
||||
{
|
||||
}
|
||||
|
||||
uint8_t operator[](int i) const { return reinterpret_cast<const uint8_t *>(&_addr)[i]; }
|
||||
operator uint32_t() const { return _addr; }
|
||||
bool operator==(const IPAddress &o) const { return _addr == o._addr; }
|
||||
bool operator!=(const IPAddress &o) const { return _addr != o._addr; }
|
||||
|
||||
bool fromString(const char *addr)
|
||||
{
|
||||
unsigned a, b, c, d;
|
||||
if (sscanf(addr, "%u.%u.%u.%u", &a, &b, &c, &d) == 4 && a <= 255 && b <= 255 && c <= 255 && d <= 255) {
|
||||
_addr = a | (b << 8) | (c << 16) | (d << 24);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
uint32_t _addr;
|
||||
};
|
||||
@@ -0,0 +1,274 @@
|
||||
// InternalFileSystem.cpp — Zephyr LittleFS backend for nRF54L15
|
||||
//
|
||||
// Implements Adafruit_LittleFS_Namespace used by FSCommon.h/cpp.
|
||||
// Storage: 36 KB storage_partition in nRF54L15 internal RRAM (defined in
|
||||
// zephyr/dts/nordic/nrf54l15_partition.dtsi, included by the board DTS).
|
||||
|
||||
#include "InternalFileSystem.h"
|
||||
#include "configuration.h"
|
||||
|
||||
#include <zephyr/fs/fs.h>
|
||||
#include <zephyr/fs/littlefs.h>
|
||||
#include <zephyr/storage/flash_map.h>
|
||||
|
||||
using namespace Adafruit_LittleFS_Namespace;
|
||||
|
||||
// ── LittleFS mount ────────────────────────────────────────────────────────
|
||||
|
||||
FS_LITTLEFS_DECLARE_DEFAULT_CONFIG(nrf54l15_lfs_data);
|
||||
|
||||
static struct fs_mount_t _lfs_mnt = {
|
||||
.type = FS_LITTLEFS,
|
||||
.mnt_point = NRF54L15_FS_MOUNT,
|
||||
.fs_data = &nrf54l15_lfs_data,
|
||||
.storage_dev = (void *)(uintptr_t)FIXED_PARTITION_ID(storage_partition),
|
||||
.flags = 0,
|
||||
};
|
||||
|
||||
// ── Global singleton ──────────────────────────────────────────────────────
|
||||
|
||||
Adafruit_LittleFS_Namespace::InternalFileSystem InternalFS;
|
||||
|
||||
// ── Path helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
void InternalFileSystem::toabs(const char *rel, char *abs, size_t abssz)
|
||||
{
|
||||
// Root "/" maps to the mount point itself (no trailing slash)
|
||||
if (rel[0] == '/' && rel[1] == '\0') {
|
||||
strncpy(abs, NRF54L15_FS_MOUNT, abssz - 1);
|
||||
abs[abssz - 1] = '\0';
|
||||
} else if (rel[0] == '/') {
|
||||
snprintf(abs, abssz, "%s%s", NRF54L15_FS_MOUNT, rel);
|
||||
} else {
|
||||
snprintf(abs, abssz, "%s/%s", NRF54L15_FS_MOUNT, rel);
|
||||
}
|
||||
}
|
||||
|
||||
// Strip mount-point prefix to get the FS-root-relative path ("/prefs/...").
|
||||
static void torel(const char *abs, char *rel, size_t relsz)
|
||||
{
|
||||
const char *mp = NRF54L15_FS_MOUNT;
|
||||
size_t mplen = strlen(mp);
|
||||
if (strncmp(abs, mp, mplen) == 0) {
|
||||
const char *suffix = abs + mplen;
|
||||
if (suffix[0] == '\0') {
|
||||
strncpy(rel, "/", relsz);
|
||||
} else {
|
||||
strncpy(rel, suffix, relsz - 1);
|
||||
rel[relsz - 1] = '\0';
|
||||
}
|
||||
} else {
|
||||
strncpy(rel, abs, relsz - 1);
|
||||
rel[relsz - 1] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
// ── InternalFileSystem methods ────────────────────────────────────────────
|
||||
|
||||
bool InternalFileSystem::begin()
|
||||
{
|
||||
if (_mounted)
|
||||
return true;
|
||||
|
||||
int rc = fs_mount(&_lfs_mnt);
|
||||
if (rc == 0) {
|
||||
_mounted = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Mount failed: attempt to format (creates a fresh LittleFS)
|
||||
LOG_WARN("LittleFS mount failed (%d), formatting storage partition...", rc);
|
||||
int fmt_rc = fs_mkfs(FS_LITTLEFS, (uintptr_t)FIXED_PARTITION_ID(storage_partition), NULL, 0);
|
||||
if (fmt_rc != 0) {
|
||||
LOG_ERROR("LittleFS format failed (%d)", fmt_rc);
|
||||
return false;
|
||||
}
|
||||
|
||||
rc = fs_mount(&_lfs_mnt);
|
||||
if (rc == 0) {
|
||||
_mounted = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
LOG_ERROR("LittleFS mount failed after format (%d)", rc);
|
||||
return false;
|
||||
}
|
||||
|
||||
File InternalFileSystem::open(const char *path, const char *mode)
|
||||
{
|
||||
if (!_mounted)
|
||||
return File();
|
||||
|
||||
char abs[NRF54L15_FS_PATHLEN];
|
||||
toabs(path, abs, sizeof(abs));
|
||||
|
||||
auto s = std::make_shared<NRF54L15FileState>();
|
||||
if (!s)
|
||||
return File();
|
||||
|
||||
strncpy(s->fullpath, abs, sizeof(s->fullpath) - 1);
|
||||
torel(abs, s->relpath, sizeof(s->relpath));
|
||||
|
||||
// Check whether the path is a directory
|
||||
struct fs_dirent entry;
|
||||
int stat_rc = fs_stat(abs, &entry);
|
||||
if (stat_rc == 0 && entry.type == FS_DIR_ENTRY_DIR) {
|
||||
s->is_dir = true;
|
||||
if (fs_opendir(&s->dir, abs) == 0) {
|
||||
s->valid = true;
|
||||
return File(s);
|
||||
}
|
||||
return File();
|
||||
}
|
||||
|
||||
// Open as a regular file
|
||||
fs_mode_t flags;
|
||||
if (strcmp(mode, FILE_O_WRITE) == 0) {
|
||||
// Truncate on write — unlink first to ensure a clean start
|
||||
fs_unlink(abs);
|
||||
flags = FS_O_WRITE | FS_O_CREATE;
|
||||
} else {
|
||||
flags = FS_O_READ;
|
||||
}
|
||||
|
||||
if (fs_open(&s->file, abs, flags) == 0) {
|
||||
s->is_dir = false;
|
||||
s->valid = true;
|
||||
return File(s);
|
||||
}
|
||||
|
||||
return File();
|
||||
}
|
||||
|
||||
bool InternalFileSystem::exists(const char *path)
|
||||
{
|
||||
if (!_mounted)
|
||||
return false;
|
||||
char abs[NRF54L15_FS_PATHLEN];
|
||||
toabs(path, abs, sizeof(abs));
|
||||
struct fs_dirent entry;
|
||||
return fs_stat(abs, &entry) == 0;
|
||||
}
|
||||
|
||||
bool InternalFileSystem::remove(const char *path)
|
||||
{
|
||||
if (!_mounted)
|
||||
return false;
|
||||
char abs[NRF54L15_FS_PATHLEN];
|
||||
toabs(path, abs, sizeof(abs));
|
||||
return fs_unlink(abs) == 0;
|
||||
}
|
||||
|
||||
bool InternalFileSystem::rename(const char *from, const char *to)
|
||||
{
|
||||
if (!_mounted)
|
||||
return false;
|
||||
char absfrom[NRF54L15_FS_PATHLEN], absto[NRF54L15_FS_PATHLEN];
|
||||
toabs(from, absfrom, sizeof(absfrom));
|
||||
toabs(to, absto, sizeof(absto));
|
||||
return fs_rename(absfrom, absto) == 0;
|
||||
}
|
||||
|
||||
bool InternalFileSystem::mkdir(const char *path)
|
||||
{
|
||||
if (!_mounted)
|
||||
return false;
|
||||
char abs[NRF54L15_FS_PATHLEN];
|
||||
toabs(path, abs, sizeof(abs));
|
||||
int rc = fs_mkdir(abs);
|
||||
return rc == 0 || rc == -EEXIST;
|
||||
}
|
||||
|
||||
bool InternalFileSystem::rmdir(const char *path)
|
||||
{
|
||||
if (!_mounted)
|
||||
return false;
|
||||
char abs[NRF54L15_FS_PATHLEN];
|
||||
toabs(path, abs, sizeof(abs));
|
||||
return fs_unlink(abs) == 0;
|
||||
}
|
||||
|
||||
bool InternalFileSystem::rmdir_r(const char *path)
|
||||
{
|
||||
if (!_mounted)
|
||||
return false;
|
||||
char abs[NRF54L15_FS_PATHLEN];
|
||||
toabs(path, abs, sizeof(abs));
|
||||
|
||||
struct fs_dir_t dir;
|
||||
fs_dir_t_init(&dir);
|
||||
if (fs_opendir(&dir, abs) != 0) {
|
||||
// Not a directory — try to delete as file
|
||||
return fs_unlink(abs) == 0;
|
||||
}
|
||||
|
||||
struct fs_dirent entry;
|
||||
char child[NRF54L15_FS_PATHLEN];
|
||||
while (fs_readdir(&dir, &entry) == 0 && entry.name[0] != '\0') {
|
||||
snprintf(child, sizeof(child), "%s/%s", abs, entry.name);
|
||||
if (entry.type == FS_DIR_ENTRY_DIR) {
|
||||
// Recurse: pass the absolute path stripped of mount prefix
|
||||
char childrel[NRF54L15_FS_PATHLEN];
|
||||
torel(child, childrel, sizeof(childrel));
|
||||
rmdir_r(childrel);
|
||||
} else {
|
||||
fs_unlink(child);
|
||||
}
|
||||
}
|
||||
fs_closedir(&dir);
|
||||
return fs_unlink(abs) == 0;
|
||||
}
|
||||
|
||||
bool InternalFileSystem::format()
|
||||
{
|
||||
if (_mounted) {
|
||||
fs_unmount(&_lfs_mnt);
|
||||
_mounted = false;
|
||||
}
|
||||
int rc = fs_mkfs(FS_LITTLEFS, (uintptr_t)FIXED_PARTITION_ID(storage_partition), NULL, 0);
|
||||
if (rc != 0) {
|
||||
LOG_ERROR("LittleFS format failed (%d)", rc);
|
||||
return false;
|
||||
}
|
||||
return begin();
|
||||
}
|
||||
|
||||
// ── File::openNextFile ────────────────────────────────────────────────────
|
||||
// Defined here because it accesses Zephyr fs_readdir/fs_open APIs.
|
||||
|
||||
File File::openNextFile()
|
||||
{
|
||||
if (!_s || !_s->valid || !_s->is_dir)
|
||||
return File();
|
||||
|
||||
struct fs_dirent entry;
|
||||
if (fs_readdir(&_s->dir, &entry) != 0)
|
||||
return File();
|
||||
if (entry.name[0] == '\0')
|
||||
return File(); // end of directory
|
||||
|
||||
char childabs[NRF54L15_FS_PATHLEN];
|
||||
snprintf(childabs, sizeof(childabs), "%s/%s", _s->fullpath, entry.name);
|
||||
|
||||
auto s = std::make_shared<NRF54L15FileState>();
|
||||
if (!s)
|
||||
return File();
|
||||
|
||||
strncpy(s->fullpath, childabs, sizeof(s->fullpath) - 1);
|
||||
torel(childabs, s->relpath, sizeof(s->relpath));
|
||||
|
||||
if (entry.type == FS_DIR_ENTRY_DIR) {
|
||||
s->is_dir = true;
|
||||
if (fs_opendir(&s->dir, childabs) == 0) {
|
||||
s->valid = true;
|
||||
return File(s);
|
||||
}
|
||||
} else {
|
||||
s->is_dir = false;
|
||||
if (fs_open(&s->file, childabs, FS_O_READ) == 0) {
|
||||
s->valid = true;
|
||||
return File(s);
|
||||
}
|
||||
}
|
||||
return File();
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
// InternalFileSystem.h — Zephyr LittleFS backend for nRF54L15
|
||||
//
|
||||
// Implements the Adafruit InternalFileSystem API subset used by Meshtastic,
|
||||
// backed by Zephyr's fs/littlefs on the storage_partition of the nRF54L15's
|
||||
// internal RRAM. Partition size is taken from the DTS at compile time via
|
||||
// FIXED_PARTITION_SIZE(storage_partition) — the DK overlay currently maps
|
||||
// ~700 KB into slot1 (see zephyr/boards/nrf54l15dk_nrf54l15_cpuapp.overlay).
|
||||
//
|
||||
// Mount point: /lfs
|
||||
// All paths passed to open/exists/mkdir etc. are relative to the FS root
|
||||
// (e.g. "/prefs/config.proto") and are prepended with "/lfs" internally.
|
||||
//
|
||||
// File objects are copyable via std::shared_ptr<NRF54L15FileState>.
|
||||
// The underlying Zephyr handle is closed when the last copy is destroyed.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <zephyr/fs/fs.h>
|
||||
#include <zephyr/storage/flash_map.h>
|
||||
|
||||
#ifndef FILE_O_READ
|
||||
#define FILE_O_READ "r"
|
||||
#define FILE_O_WRITE "w"
|
||||
#endif
|
||||
|
||||
#define NRF54L15_FS_MOUNT "/lfs"
|
||||
#define NRF54L15_FS_PATHLEN 256
|
||||
|
||||
namespace Adafruit_LittleFS_Namespace
|
||||
{
|
||||
|
||||
class InternalFileSystem; // forward
|
||||
|
||||
// ── Internal file/dir state ───────────────────────────────────────────────
|
||||
|
||||
struct NRF54L15FileState {
|
||||
bool valid = false;
|
||||
bool is_dir = false;
|
||||
|
||||
// Absolute Zephyr path, e.g. "/lfs/prefs/config.proto"
|
||||
char fullpath[NRF54L15_FS_PATHLEN] = {0};
|
||||
// Path from FS root, e.g. "/prefs/config.proto" (returned by name())
|
||||
char relpath[NRF54L15_FS_PATHLEN] = {0};
|
||||
|
||||
struct fs_file_t file;
|
||||
struct fs_dir_t dir;
|
||||
|
||||
NRF54L15FileState()
|
||||
{
|
||||
fs_file_t_init(&file);
|
||||
fs_dir_t_init(&dir);
|
||||
}
|
||||
|
||||
~NRF54L15FileState()
|
||||
{
|
||||
if (valid) {
|
||||
if (is_dir)
|
||||
fs_closedir(&dir);
|
||||
else
|
||||
fs_close(&file);
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ── File ─────────────────────────────────────────────────────────────────
|
||||
|
||||
class File
|
||||
{
|
||||
public:
|
||||
File() = default;
|
||||
explicit File(InternalFileSystem &) {} // nRF52 compat constructor
|
||||
|
||||
explicit operator bool() const { return _s && _s->valid; }
|
||||
|
||||
int read(void *buf, uint16_t nbyte)
|
||||
{
|
||||
if (!_s || !_s->valid || _s->is_dir)
|
||||
return -1;
|
||||
ssize_t n = fs_read(&_s->file, buf, nbyte);
|
||||
return n < 0 ? -1 : (int)n;
|
||||
}
|
||||
|
||||
int read()
|
||||
{
|
||||
uint8_t b;
|
||||
return read(&b, 1) == 1 ? (int)b : -1;
|
||||
}
|
||||
|
||||
size_t write(const uint8_t *buf, size_t len)
|
||||
{
|
||||
if (!_s || !_s->valid || _s->is_dir)
|
||||
return 0;
|
||||
ssize_t n = fs_write(&_s->file, buf, len);
|
||||
return n < 0 ? 0 : (size_t)n;
|
||||
}
|
||||
|
||||
size_t write(uint8_t b) { return write(&b, 1); }
|
||||
|
||||
void flush()
|
||||
{
|
||||
if (_s && _s->valid && !_s->is_dir)
|
||||
fs_sync(&_s->file);
|
||||
}
|
||||
|
||||
void close() { _s.reset(); }
|
||||
|
||||
size_t size()
|
||||
{
|
||||
if (!_s || !_s->valid || _s->is_dir)
|
||||
return 0;
|
||||
struct fs_dirent entry;
|
||||
if (fs_stat(_s->fullpath, &entry) == 0)
|
||||
return (size_t)entry.size;
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool isDirectory() { return _s && _s->valid && _s->is_dir; }
|
||||
|
||||
// Returns path from FS root, e.g. "/prefs/config.proto"
|
||||
const char *name() { return _s ? _s->relpath : ""; }
|
||||
|
||||
// Returns the next entry in a directory. Modifies the dir stream in _s.
|
||||
File openNextFile();
|
||||
|
||||
void rewindDirectory()
|
||||
{
|
||||
if (!_s || !_s->valid || !_s->is_dir)
|
||||
return;
|
||||
// Zephyr has no rewinddir(); close + reopen the same handle. Skipping
|
||||
// the close would leak the LittleFS dir state and the next openNextFile
|
||||
// could return stale entries on some Zephyr versions.
|
||||
fs_closedir(&_s->dir);
|
||||
fs_dir_t_init(&_s->dir);
|
||||
if (fs_opendir(&_s->dir, _s->fullpath) != 0) {
|
||||
_s->valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool seek(uint32_t pos)
|
||||
{
|
||||
if (!_s || !_s->valid || _s->is_dir)
|
||||
return false;
|
||||
return fs_seek(&_s->file, (off_t)pos, FS_SEEK_SET) == 0;
|
||||
}
|
||||
|
||||
int available()
|
||||
{
|
||||
if (!_s || !_s->valid || _s->is_dir)
|
||||
return 0;
|
||||
off_t pos = fs_tell(&_s->file);
|
||||
if (pos < 0)
|
||||
return 0;
|
||||
struct fs_dirent entry;
|
||||
if (fs_stat(_s->fullpath, &entry) != 0)
|
||||
return 0;
|
||||
long rem = (long)entry.size - (long)pos;
|
||||
return rem > 0 ? (int)rem : 0;
|
||||
}
|
||||
|
||||
int peek() { return -1; }
|
||||
|
||||
// Internal: constructed by InternalFileSystem and openNextFile()
|
||||
explicit File(std::shared_ptr<NRF54L15FileState> s) : _s(std::move(s)) {}
|
||||
|
||||
private:
|
||||
std::shared_ptr<NRF54L15FileState> _s;
|
||||
};
|
||||
|
||||
// ── InternalFileSystem ────────────────────────────────────────────────────
|
||||
|
||||
class InternalFileSystem
|
||||
{
|
||||
public:
|
||||
bool begin();
|
||||
File open(const char *path, const char *mode);
|
||||
bool exists(const char *path);
|
||||
bool remove(const char *path);
|
||||
bool rename(const char *from, const char *to);
|
||||
bool mkdir(const char *path);
|
||||
bool rmdir(const char *path);
|
||||
bool rmdir_r(const char *path); // recursive delete (used by FSCommon rmDir)
|
||||
uint32_t usedBytes()
|
||||
{
|
||||
struct fs_statvfs st = {};
|
||||
if (fs_statvfs(NRF54L15_FS_MOUNT, &st) != 0)
|
||||
return 0;
|
||||
// Zephyr returns block counts; convert to bytes. f_frsize is the
|
||||
// fundamental fragment size (LittleFS reports it equal to the block
|
||||
// size). used = (total - free) * frag_size.
|
||||
if (st.f_blocks <= st.f_bfree)
|
||||
return 0;
|
||||
return (uint32_t)((st.f_blocks - st.f_bfree) * st.f_frsize);
|
||||
}
|
||||
uint32_t totalBytes() { return (uint32_t)FIXED_PARTITION_SIZE(storage_partition); }
|
||||
bool format();
|
||||
|
||||
// Convert a FS-root-relative path to an absolute Zephyr path.
|
||||
static void toabs(const char *rel, char *abs, size_t abssz);
|
||||
|
||||
private:
|
||||
bool _mounted = false;
|
||||
};
|
||||
|
||||
} // namespace Adafruit_LittleFS_Namespace
|
||||
|
||||
extern Adafruit_LittleFS_Namespace::InternalFileSystem InternalFS;
|
||||
@@ -0,0 +1,18 @@
|
||||
// NRF52Bluetooth.h — stub for nRF54L15/Zephyr
|
||||
// main.h includes this when ARCH_NRF52 is defined.
|
||||
// Bluetooth is excluded (MESHTASTIC_EXCLUDE_BLUETOOTH=1); this satisfies the
|
||||
// include chain without pulling in the nRF52 Bluefruit SDK.
|
||||
#pragma once
|
||||
|
||||
class NRF52Bluetooth
|
||||
{
|
||||
public:
|
||||
void setup() {}
|
||||
void shutdown() {}
|
||||
void startDisabled() {}
|
||||
void resumeAdvertising() {}
|
||||
void clearBonds() {}
|
||||
bool isConnected() { return false; }
|
||||
int getRssi() { return 0; }
|
||||
void sendLog(const uint8_t *, size_t) {}
|
||||
};
|
||||
@@ -0,0 +1,805 @@
|
||||
// NRF54L15Bluetooth.cpp — Zephyr BLE GATT peripheral for Meshtastic nRF54L15
|
||||
//
|
||||
// GATT profile (identical UUIDs to the nRF52 / NimBLE implementations):
|
||||
// Service: 6ba1b218-15a8-461f-9fa8-5dcae273eafd
|
||||
// fromNum: ed9da18c-a800-4f66-a670-aa7547e34453 READ | NOTIFY
|
||||
// fromRadio: 2c55e69e-4993-11ed-b878-0242ac120002 READ
|
||||
// toRadio: f75c76d2-129e-4dad-a1dd-7866124401e7 WRITE
|
||||
// logRadio: 5a3d6e49-06e6-4423-9944-e9de8cdf9547 READ | NOTIFY | INDICATE
|
||||
//
|
||||
// Threading model:
|
||||
// - BT RX thread: connected_cb / disconnected_cb / GATT read_/write_
|
||||
// callbacks
|
||||
// - Meshtastic OSThread scheduler (cooperative, main thread):
|
||||
// BleDeferredThread
|
||||
// polls pendingToRadio and runs the zombie-connection watchdog every 100 ms
|
||||
// - PhoneAPI::onNowHasData: sends fromNum notify synchronously from whichever
|
||||
// thread pushed the packet (bt_gatt_notify is thread-safe in Zephyr)
|
||||
// - active_conn protected by ble_mutex where needed
|
||||
|
||||
#include "NRF54L15Bluetooth.h"
|
||||
#include "BluetoothCommon.h"
|
||||
#include "BluetoothStatus.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
#include "configuration.h"
|
||||
#include "main.h"
|
||||
#include "mesh/PhoneAPI.h"
|
||||
#include "mesh/mesh-pb-constants.h"
|
||||
|
||||
#include <zephyr/bluetooth/bluetooth.h>
|
||||
#include <zephyr/bluetooth/conn.h>
|
||||
#include <zephyr/bluetooth/gatt.h>
|
||||
#include <zephyr/bluetooth/hci.h>
|
||||
#include <zephyr/bluetooth/uuid.h>
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/settings/settings.h>
|
||||
#include <zephyr/sys/reboot.h>
|
||||
|
||||
// ── UUID definitions (little-endian per Bluetooth spec)
|
||||
// ─────────────────────── Syntax: replace hyphens with commas, prefix 0x —
|
||||
// matches BT_UUID_128_ENCODE doc.
|
||||
|
||||
#define MESH_SVC_UUID_VAL BT_UUID_128_ENCODE(0x6ba1b218, 0x15a8, 0x461f, 0x9fa8, 0x5dcae273eafd)
|
||||
#define FROMNUM_UUID_VAL BT_UUID_128_ENCODE(0xed9da18c, 0xa800, 0x4f66, 0xa670, 0xaa7547e34453)
|
||||
#define FROMRADIO_UUID_VAL BT_UUID_128_ENCODE(0x2c55e69e, 0x4993, 0x11ed, 0xb878, 0x0242ac120002)
|
||||
#define TORADIO_UUID_VAL BT_UUID_128_ENCODE(0xf75c76d2, 0x129e, 0x4dad, 0xa1dd, 0x7866124401e7)
|
||||
#define LOGRADIO_UUID_VAL BT_UUID_128_ENCODE(0x5a3d6e49, 0x06e6, 0x4423, 0x9944, 0xe9de8cdf9547)
|
||||
|
||||
static const struct bt_uuid_128 mesh_svc_uuid = BT_UUID_INIT_128(MESH_SVC_UUID_VAL);
|
||||
static const struct bt_uuid_128 fromnum_uuid = BT_UUID_INIT_128(FROMNUM_UUID_VAL);
|
||||
static const struct bt_uuid_128 fromradio_uuid = BT_UUID_INIT_128(FROMRADIO_UUID_VAL);
|
||||
static const struct bt_uuid_128 toradio_uuid = BT_UUID_INIT_128(TORADIO_UUID_VAL);
|
||||
static const struct bt_uuid_128 logradio_uuid = BT_UUID_INIT_128(LOGRADIO_UUID_VAL);
|
||||
|
||||
// ── Module state ─────────────────────────────────────────────────────────────
|
||||
|
||||
static struct bt_conn *active_conn = nullptr;
|
||||
static K_MUTEX_DEFINE(ble_mutex);
|
||||
|
||||
// Take a reference to active_conn under ble_mutex. Returns nullptr if there is
|
||||
// no active connection. Caller MUST bt_conn_unref() when done.
|
||||
//
|
||||
// Reading `active_conn` outside this lock races with disconnected_cb which can
|
||||
// unref + null it on the BT RX thread — touching the freed pointer (even just
|
||||
// to bt_conn_ref it) is a use-after-free.
|
||||
static struct bt_conn *acquire_active_conn()
|
||||
{
|
||||
struct bt_conn *conn = nullptr;
|
||||
k_mutex_lock(&ble_mutex, K_FOREVER);
|
||||
if (active_conn) {
|
||||
conn = bt_conn_ref(active_conn);
|
||||
}
|
||||
k_mutex_unlock(&ble_mutex);
|
||||
return conn;
|
||||
}
|
||||
|
||||
static bool bt_initialized = false; // bt_enable() called at most once
|
||||
static bool ble_enabled = false; // set by setup(), cleared by shutdown()
|
||||
|
||||
// Forward declarations — BT_GATT_SERVICE_DEFINE(mesh_svc, ...) is below, but
|
||||
// read_fromradio() (defined earlier) needs to reference the service to notify
|
||||
// on fromNum after each non-empty read.
|
||||
#define FROMNUM_ATTR_IDX 2
|
||||
#define LOGRADIO_ATTR_IDX 9
|
||||
extern const struct bt_gatt_service_static mesh_svc;
|
||||
|
||||
static void start_advertising(); // forward declaration (defined in advertising
|
||||
// section below)
|
||||
|
||||
// Work item for advertising restart after disconnect.
|
||||
//
|
||||
// disconnected_cb runs on the BT RX thread (the same thread that processes
|
||||
// HCI Command Complete events). Calling bt_le_adv_start() →
|
||||
// bt_hci_cmd_send_sync() directly from that thread deadlocks: the thread blocks
|
||||
// on k_sem_take waiting for Command Complete, but it is the very thread that
|
||||
// would process it. After 10 s the host panics with "Controller unresponsive,
|
||||
// opcode 0x2006 timeout".
|
||||
//
|
||||
// Fix: submit a k_work item. The system workqueue runs bt_adv_restart_work_fn
|
||||
// on its own thread → no deadlock.
|
||||
static struct k_work adv_restart_work;
|
||||
|
||||
static void adv_restart_work_fn(struct k_work *work)
|
||||
{
|
||||
if (ble_enabled) {
|
||||
start_advertising();
|
||||
}
|
||||
}
|
||||
|
||||
// CCC state: 0=off, BT_GATT_CCC_NOTIFY=notify, BT_GATT_CCC_INDICATE=indicate
|
||||
static uint16_t fromnum_ccc_val = 0;
|
||||
static uint16_t logradio_ccc_val = 0;
|
||||
|
||||
// Scratch buffers — only one BLE operation at a time
|
||||
static uint8_t fromRadioBytes[meshtastic_FromRadio_size];
|
||||
static size_t fromRadioLen = 0;
|
||||
static uint8_t toRadioBytes[meshtastic_ToRadio_size];
|
||||
static uint8_t lastToRadio[MAX_TO_FROM_RADIO_SIZE];
|
||||
static uint32_t fromNumValue = 0;
|
||||
|
||||
// Deferred ToRadio processing
|
||||
//
|
||||
// write_toradio() runs on the BT RX workqueue thread (6 KB stack). Calling
|
||||
// phoneAPI->handleToRadio() directly triggers handleStartConfig →
|
||||
// getFiles("/", 10) → nanopb encode, which overflows the stack on the exact
|
||||
// "Client wants config" write. Instead we copy the payload into a pending
|
||||
// buffer under a mutex and let BleDeferredThread (running on the Meshtastic
|
||||
// OSThread scheduler, 24 KB stack) do the actual call outside the lock.
|
||||
//
|
||||
// The mutex makes the producer/consumer handoff race-free — producer may
|
||||
// overwrite a pending buffer the consumer hasn't read yet (dropped packet),
|
||||
// but partial reads / torn writes are impossible.
|
||||
K_MUTEX_DEFINE(pendingToRadioMutex);
|
||||
static uint8_t pendingToRadioBuf[MAX_TO_FROM_RADIO_SIZE];
|
||||
static size_t pendingToRadioLen = 0;
|
||||
static bool pendingToRadio = false;
|
||||
|
||||
// Zombie-connection watchdog state.
|
||||
//
|
||||
// The nRF54L15 Zephyr 4.2.1 SW-LL occasionally fails to forward an
|
||||
// LE Disconnection Complete event to the host: when iOS tears down the link
|
||||
// (either explicitly by the user or via supervision timeout), the LL layer
|
||||
// drops the connection but disconnected_cb never fires, active_conn stays
|
||||
// non-null and advertising never restarts — the device vanishes from scans
|
||||
// until power cycle. Track the connected timestamp and the last time we
|
||||
// observed ATT traffic; a long ATT idle on an "active" connection means we
|
||||
// are zombied. A cold reboot is the only path that reliably recovers (any
|
||||
// bt_hci_cmd_send_sync after this state, e.g. bt_le_adv_start or
|
||||
// bt_conn_disconnect, hangs in k_sem_take and later panics with "Controller
|
||||
// unresponsive, opcode 0x2006 timeout").
|
||||
static uint32_t connect_time_ms = 0;
|
||||
static uint32_t last_att_time_ms = 0;
|
||||
|
||||
// ── BluetoothPhoneAPI
|
||||
// ─────────────────────────────────────────────────────────
|
||||
|
||||
class BluetoothPhoneAPI : public PhoneAPI
|
||||
{
|
||||
virtual void onNowHasData(uint32_t fromRadioNum) override;
|
||||
virtual bool checkIsConnected() override;
|
||||
|
||||
public:
|
||||
BluetoothPhoneAPI() { api_type = TYPE_BLE; }
|
||||
};
|
||||
|
||||
static BluetoothPhoneAPI *phoneAPI = nullptr;
|
||||
|
||||
// ── CCC change callbacks
|
||||
// ──────────────────────────────────────────────────────
|
||||
|
||||
static void fromnum_ccc_changed(const struct bt_gatt_attr *attr, uint16_t value)
|
||||
{
|
||||
fromnum_ccc_val = value;
|
||||
LOG_INFO("BLE fromNum CCC: %u", value);
|
||||
}
|
||||
|
||||
static void logradio_ccc_changed(const struct bt_gatt_attr *attr, uint16_t value)
|
||||
{
|
||||
logradio_ccc_val = value;
|
||||
LOG_INFO("BLE logRadio CCC: %u", value);
|
||||
}
|
||||
|
||||
// ── GATT attribute callbacks
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
static ssize_t read_fromnum(struct bt_conn *conn, const struct bt_gatt_attr *attr, void *buf, uint16_t len, uint16_t offset)
|
||||
{
|
||||
LOG_INFO("GATT read_fromnum: fromNum=%u offset=%u", fromNumValue, offset);
|
||||
return bt_gatt_attr_read(conn, attr, buf, len, offset, &fromNumValue, sizeof(fromNumValue));
|
||||
}
|
||||
|
||||
static ssize_t read_fromradio(struct bt_conn *conn, const struct bt_gatt_attr *attr, void *buf, uint16_t len, uint16_t offset)
|
||||
{
|
||||
if (offset == 0) {
|
||||
// First chunk: pull the next packet from the queue.
|
||||
// Subsequent chunks (offset > 0) are ATT_READ_BLOB continuations of the
|
||||
// same value and must reuse fromRadioBytes untouched.
|
||||
fromRadioLen = phoneAPI ? phoneAPI->getFromRadio(fromRadioBytes) : 0;
|
||||
LOG_DEBUG("GATT read_fromradio len=%u", (unsigned)fromRadioLen);
|
||||
}
|
||||
last_att_time_ms = k_uptime_get_32();
|
||||
return bt_gatt_attr_read(conn, attr, buf, len, offset, fromRadioBytes, fromRadioLen);
|
||||
}
|
||||
|
||||
static ssize_t read_logradio(struct bt_conn *conn, const struct bt_gatt_attr *attr, void *buf, uint16_t len, uint16_t offset)
|
||||
{
|
||||
// logRadio is write-only from the device side (notify/indicate).
|
||||
// Return an empty read so GATT discovery doesn't fail with NOT_PERMITTED.
|
||||
return bt_gatt_attr_read(conn, attr, buf, len, offset, NULL, 0);
|
||||
}
|
||||
|
||||
static ssize_t write_toradio(struct bt_conn *conn, const struct bt_gatt_attr *attr, const void *buf, uint16_t len,
|
||||
uint16_t offset, uint8_t flags)
|
||||
{
|
||||
// Writes >MTU-3 arrive here with offset=0 and
|
||||
// flags=BT_GATT_WRITE_FLAG_EXECUTE after Zephyr reassembles the ATT Prepare
|
||||
// Write fragments (CONFIG_BT_ATT_PREPARE_COUNT>0). Single writes arrive with
|
||||
// flags=0.
|
||||
LOG_DEBUG("GATT write_toradio len=%u flags=0x%x", len, flags);
|
||||
if (offset != 0) {
|
||||
return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET);
|
||||
}
|
||||
// Reject any write that won't fit in the dedup buffer (lastToRadio) or the
|
||||
// pending handoff buffer (pendingToRadioBuf), both sized
|
||||
// MAX_TO_FROM_RADIO_SIZE. Returning success while silently dropping a
|
||||
// payload would let the phone believe a config write was applied.
|
||||
if (len > sizeof(toRadioBytes) || len > MAX_TO_FROM_RADIO_SIZE) {
|
||||
return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
|
||||
}
|
||||
|
||||
// Deduplicate — drop packet if identical to the last one we processed
|
||||
if (memcmp(lastToRadio, buf, len) != 0) {
|
||||
memcpy(lastToRadio, buf, len);
|
||||
if (len < MAX_TO_FROM_RADIO_SIZE) {
|
||||
memset(lastToRadio + len, 0, MAX_TO_FROM_RADIO_SIZE - len);
|
||||
}
|
||||
// Defer handleToRadio() to BleDeferredThread (24 KB stack).
|
||||
// Running it here on bt_workq (6 KB) overflows during handleStartConfig.
|
||||
// Always overwrite pending — we already dedup'd above via lastToRadio,
|
||||
// so any new write here is genuinely new data that must be delivered.
|
||||
k_mutex_lock(&pendingToRadioMutex, K_FOREVER);
|
||||
memcpy(pendingToRadioBuf, buf, len);
|
||||
pendingToRadioLen = len;
|
||||
pendingToRadio = true;
|
||||
k_mutex_unlock(&pendingToRadioMutex);
|
||||
}
|
||||
last_att_time_ms = k_uptime_get_32();
|
||||
return (ssize_t)len;
|
||||
}
|
||||
|
||||
// ── GATT service definition (static, linked at compile time)
|
||||
// ──────────────────
|
||||
//
|
||||
// Attribute indices (0-based):
|
||||
// [0] Primary Service declaration
|
||||
// [1] fromNum characteristic declaration
|
||||
// [2] fromNum value ← notify target (FROMNUM_ATTR_IDX)
|
||||
// [3] fromNum CCC descriptor
|
||||
// [4] fromRadio characteristic declaration
|
||||
// [5] fromRadio value
|
||||
// [6] toRadio characteristic declaration
|
||||
// [7] toRadio value
|
||||
// [8] logRadio characteristic declaration
|
||||
// [9] logRadio value ← notify target (LOGRADIO_ATTR_IDX)
|
||||
// [10] logRadio CCC descriptor
|
||||
|
||||
// All user characteristics require authenticated encryption (MITM passkey)
|
||||
// before the client can read/write. This mirrors the nrf52
|
||||
// SECMODE_ENC_WITH_MITM service permission. The stack returns "Insufficient
|
||||
// Authentication" on the first access attempt, prompting the client to pair
|
||||
// with the configured PIN.
|
||||
#define MESH_PERM_READ (BT_GATT_PERM_READ | BT_GATT_PERM_READ_AUTHEN)
|
||||
#define MESH_PERM_WRITE (BT_GATT_PERM_WRITE | BT_GATT_PERM_WRITE_AUTHEN)
|
||||
|
||||
BT_GATT_SERVICE_DEFINE(mesh_svc, BT_GATT_PRIMARY_SERVICE(&mesh_svc_uuid.uuid),
|
||||
|
||||
// fromNum: READ | NOTIFY — packet-counter triggers phone to read fromRadio
|
||||
BT_GATT_CHARACTERISTIC(&fromnum_uuid.uuid, BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, MESH_PERM_READ,
|
||||
read_fromnum, NULL, &fromNumValue),
|
||||
BT_GATT_CCC(fromnum_ccc_changed, MESH_PERM_READ | MESH_PERM_WRITE),
|
||||
|
||||
// fromRadio: READ — phone polls this after receiving a fromNum notification
|
||||
BT_GATT_CHARACTERISTIC(&fromradio_uuid.uuid, BT_GATT_CHRC_READ, MESH_PERM_READ, read_fromradio, NULL,
|
||||
NULL),
|
||||
|
||||
// toRadio: WRITE — phone sends protobuf packets to the device
|
||||
BT_GATT_CHARACTERISTIC(&toradio_uuid.uuid, BT_GATT_CHRC_WRITE, MESH_PERM_WRITE, NULL, write_toradio, NULL),
|
||||
|
||||
// logRadio: READ | NOTIFY | INDICATE — log stream to phone when connected
|
||||
BT_GATT_CHARACTERISTIC(&logradio_uuid.uuid,
|
||||
BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY | BT_GATT_CHRC_INDICATE, MESH_PERM_READ,
|
||||
read_logradio, NULL, NULL),
|
||||
BT_GATT_CCC(logradio_ccc_changed, MESH_PERM_READ | MESH_PERM_WRITE), );
|
||||
|
||||
// ── Advertising
|
||||
// ───────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Use legacy advertising (bt_le_adv_start / HCI 0x2006 path).
|
||||
//
|
||||
// History: we previously used bt_le_ext_adv_create (true extended advertising)
|
||||
// because bt_le_adv_start() with CONFIG_BT_EXT_ADV=y was translated internally
|
||||
// to the extended HCI path with LEGACY-bit (0x2036), which produced
|
||||
// non-connectable PDUs on the nRF54L15 SW-LL. The true extended path
|
||||
// (0x203x, AUX_ADV_IND) was connectable but caused two problems:
|
||||
// 1. iOS CoreBluetooth does not reliably complete GATT after connecting via
|
||||
// extended advertising (zero ATT PDUs observed in all test sessions).
|
||||
// 2. After each connection the controller auto-stops the advertising set, and
|
||||
// the subsequent bt_le_ext_adv_delete() sends LE Remove Advertising Set
|
||||
// (0x203c) which times out → kernel oops at hci_core.c:506.
|
||||
//
|
||||
// With CONFIG_BT_EXT_ADV=n the host uses pure legacy HCI commands — the same
|
||||
// path Nordic NCS uses in all nRF54L15 examples (peripheral_uart,
|
||||
// peripheral_lbs) and which is universally iOS-compatible. The legacy data
|
||||
// payload is 31 bytes:
|
||||
// FLAGS (3B) + UUID128 (18B) = 21B in adv; NAME in scan-response (17B).
|
||||
|
||||
static void start_advertising()
|
||||
{
|
||||
// IMPORTANT: BT_DATA_BYTES() uses C99 compound literals that GCC C++ treats
|
||||
// as temporaries; with -Os the compiler may elide writes, leaving stack
|
||||
// uninitialized. Use static const arrays for stable data (flags, UUID)
|
||||
// and a runtime pointer for the dynamic device name.
|
||||
static const uint8_t adv_flags_val[] = {BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR};
|
||||
static const uint8_t adv_uuid128_val[] = {MESH_SVC_UUID_VAL};
|
||||
|
||||
const char *name = bt_get_name();
|
||||
size_t full_name_len = strlen(name);
|
||||
|
||||
// Legacy scan-response payload is 31 bytes total. Each AD entry costs 2
|
||||
// bytes (length + type), leaving 29 bytes for the name. With
|
||||
// CONFIG_BT_DEVICE_NAME_MAX=32 the name can exceed that — truncate and
|
||||
// mark as SHORTENED so bt_le_adv_start() doesn't reject the payload.
|
||||
constexpr size_t LEGACY_SCAN_RSP_NAME_MAX = 31 - 2;
|
||||
bool name_shortened = full_name_len > LEGACY_SCAN_RSP_NAME_MAX;
|
||||
uint8_t name_len = (uint8_t)(name_shortened ? LEGACY_SCAN_RSP_NAME_MAX : full_name_len);
|
||||
|
||||
// Primary advertising data: FLAGS + Meshtastic service UUID128 (21 bytes
|
||||
// total)
|
||||
struct bt_data ad[] = {
|
||||
{BT_DATA_FLAGS, sizeof(adv_flags_val), adv_flags_val},
|
||||
{BT_DATA_UUID128_ALL, sizeof(adv_uuid128_val), adv_uuid128_val},
|
||||
};
|
||||
// Scan response: device name (discovered after scan request)
|
||||
struct bt_data sd[] = {
|
||||
{(uint8_t)(name_shortened ? BT_DATA_NAME_SHORTENED : BT_DATA_NAME_COMPLETE), name_len, (const uint8_t *)name},
|
||||
};
|
||||
|
||||
// BT_LE_ADV_OPT_CONN = connectable legacy ADV_IND + stops after first
|
||||
// connection (replaces deprecated
|
||||
// CONNECTABLE|ONE_TIME in Zephyr 4.2.1;
|
||||
// BT_LE_ADV_OPT_CONN = BIT(0)|BIT(1))
|
||||
// BT_LE_ADV_OPT_USE_IDENTITY = use static random identity address (stable
|
||||
// across reboots) Advertising restart after disconnect is via
|
||||
// adv_restart_work (system workqueue) so calling bt_le_adv_start() from the
|
||||
// BT RX thread context is avoided.
|
||||
int err = bt_le_adv_start(BT_LE_ADV_PARAM(BT_LE_ADV_OPT_CONN | BT_LE_ADV_OPT_USE_IDENTITY, BT_GAP_ADV_FAST_INT_MIN_2,
|
||||
BT_GAP_ADV_FAST_INT_MAX_2, NULL),
|
||||
ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd));
|
||||
|
||||
if (err == -EALREADY) {
|
||||
return;
|
||||
}
|
||||
if (err) {
|
||||
LOG_WARN("BLE adv start failed: %d", err);
|
||||
} else {
|
||||
LOG_INFO("BLE advertising as '%s'", bt_get_name());
|
||||
}
|
||||
}
|
||||
|
||||
static void stop_advertising()
|
||||
{
|
||||
bt_le_adv_stop();
|
||||
}
|
||||
|
||||
// ── Connection callbacks
|
||||
// ──────────────────────────────────────────────────────
|
||||
|
||||
static void connected_cb(struct bt_conn *conn, uint8_t err)
|
||||
{
|
||||
if (err) {
|
||||
LOG_WARN("BLE connection failed, err=0x%02x", err);
|
||||
return;
|
||||
}
|
||||
|
||||
k_mutex_lock(&ble_mutex, K_FOREVER);
|
||||
active_conn = bt_conn_ref(conn);
|
||||
k_mutex_unlock(&ble_mutex);
|
||||
|
||||
memset(lastToRadio, 0, sizeof(lastToRadio));
|
||||
connect_time_ms = k_uptime_get_32();
|
||||
last_att_time_ms = connect_time_ms;
|
||||
|
||||
char addr[BT_ADDR_LE_STR_LEN];
|
||||
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
|
||||
LOG_INFO("BLE connected: %s", addr);
|
||||
|
||||
meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED);
|
||||
bluetoothStatus->updateStatus(&newStatus);
|
||||
|
||||
// nRF54L15-DK has no screen — cannot display a PIN to the user.
|
||||
// Requesting BT_SECURITY_L2 causes the OS to show a pairing dialog that
|
||||
// the user dismisses, triggering disconnect + advertising restart failure.
|
||||
// Skip security negotiation; the Meshtastic app works over plain GATT.
|
||||
// (Security can be re-enabled once a display or NFC OOB path is available.)
|
||||
}
|
||||
|
||||
static void disconnected_cb(struct bt_conn *conn, uint8_t reason)
|
||||
{
|
||||
LOG_INFO("BLE disconnected, reason=0x%02x", reason);
|
||||
|
||||
k_mutex_lock(&ble_mutex, K_FOREVER);
|
||||
if (active_conn) {
|
||||
bt_conn_unref(active_conn);
|
||||
active_conn = nullptr;
|
||||
}
|
||||
k_mutex_unlock(&ble_mutex);
|
||||
|
||||
fromnum_ccc_val = 0;
|
||||
logradio_ccc_val = 0;
|
||||
connect_time_ms = 0;
|
||||
last_att_time_ms = 0;
|
||||
|
||||
if (phoneAPI) {
|
||||
phoneAPI->close();
|
||||
}
|
||||
memset(lastToRadio, 0, sizeof(lastToRadio));
|
||||
|
||||
meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED);
|
||||
bluetoothStatus->updateStatus(&newStatus);
|
||||
|
||||
// Schedule advertising restart via work queue — NOT from this callback
|
||||
// directly. disconnected_cb runs on the BT RX thread; calling
|
||||
// bt_le_adv_start() here would deadlock (see adv_restart_work comment above).
|
||||
if (ble_enabled) {
|
||||
k_work_submit(&adv_restart_work);
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(CONFIG_BT_SMP)
|
||||
static void security_changed_cb(struct bt_conn *conn, bt_security_t level, enum bt_security_err err)
|
||||
{
|
||||
if (err == BT_SECURITY_ERR_PIN_OR_KEY_MISSING) {
|
||||
// Phone has a stale bond (device was wiped/reflashed). Unpair the stale
|
||||
// entry so the phone re-pairs cleanly on the next connection attempt.
|
||||
LOG_WARN("BLE stale bond detected (key missing) — unpairing");
|
||||
bt_unpair(BT_ID_DEFAULT, bt_conn_get_dst(conn));
|
||||
bt_conn_disconnect(conn, BT_HCI_ERR_AUTH_FAIL);
|
||||
} else if (err) {
|
||||
LOG_WARN("BLE security change failed: level=%d err=%d", (int)level, (int)err);
|
||||
} else {
|
||||
LOG_INFO("BLE security level %d established", (int)level);
|
||||
}
|
||||
}
|
||||
#endif /* CONFIG_BT_SMP */
|
||||
|
||||
BT_CONN_CB_DEFINE(conn_callbacks) = {
|
||||
.connected = connected_cb,
|
||||
.disconnected = disconnected_cb,
|
||||
#if defined(CONFIG_BT_SMP)
|
||||
.security_changed = security_changed_cb,
|
||||
#endif
|
||||
};
|
||||
|
||||
// ── Pairing / auth callbacks
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
#if defined(CONFIG_BT_SMP)
|
||||
static uint32_t configuredPasskey;
|
||||
|
||||
static void auth_passkey_display(struct bt_conn *conn, unsigned int passkey)
|
||||
{
|
||||
char passkey_str[7];
|
||||
snprintf(passkey_str, sizeof(passkey_str), "%06u", passkey);
|
||||
configuredPasskey = passkey;
|
||||
LOG_INFO("BLE pairing PIN: %s", passkey_str);
|
||||
powerFSM.trigger(EVENT_BLUETOOTH_PAIR);
|
||||
|
||||
std::string textkey(passkey_str);
|
||||
meshtastic::BluetoothStatus pairingStatus(textkey);
|
||||
bluetoothStatus->updateStatus(&pairingStatus);
|
||||
}
|
||||
|
||||
static void auth_cancel(struct bt_conn *conn)
|
||||
{
|
||||
LOG_WARN("BLE pairing cancelled");
|
||||
}
|
||||
|
||||
static struct bt_conn_auth_cb auth_cb = {
|
||||
.passkey_display = auth_passkey_display,
|
||||
.passkey_entry = NULL,
|
||||
.cancel = auth_cancel,
|
||||
};
|
||||
|
||||
static void pairing_complete_cb(struct bt_conn *conn, bool bonded)
|
||||
{
|
||||
LOG_INFO("BLE pairing complete, bonded=%d", (int)bonded);
|
||||
meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED);
|
||||
bluetoothStatus->updateStatus(&newStatus);
|
||||
}
|
||||
|
||||
static void pairing_failed_cb(struct bt_conn *conn, enum bt_security_err reason)
|
||||
{
|
||||
LOG_WARN("BLE pairing failed, reason=%d", (int)reason);
|
||||
meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED);
|
||||
bluetoothStatus->updateStatus(&newStatus);
|
||||
}
|
||||
|
||||
static struct bt_conn_auth_info_cb auth_info_cb = {
|
||||
.pairing_complete = pairing_complete_cb,
|
||||
.pairing_failed = pairing_failed_cb,
|
||||
};
|
||||
#endif /* CONFIG_BT_SMP */
|
||||
|
||||
// ── BluetoothPhoneAPI methods
|
||||
// ─────────────────────────────────────────────────
|
||||
|
||||
void BluetoothPhoneAPI::onNowHasData(uint32_t fromRadioNum)
|
||||
{
|
||||
PhoneAPI::onNowHasData(fromRadioNum);
|
||||
fromNumValue = fromRadioNum;
|
||||
|
||||
if (!(fromnum_ccc_val & BT_GATT_CCC_NOTIFY))
|
||||
return;
|
||||
|
||||
// active_conn may be torn down on another thread while we're dispatching
|
||||
// this notify — acquire under ble_mutex so disconnected_cb can't free the
|
||||
// conn between the null check and bt_conn_ref.
|
||||
struct bt_conn *conn = acquire_active_conn();
|
||||
if (!conn)
|
||||
return;
|
||||
bt_gatt_notify(conn, &mesh_svc.attrs[FROMNUM_ATTR_IDX], &fromNumValue, sizeof(fromNumValue));
|
||||
bt_conn_unref(conn);
|
||||
}
|
||||
|
||||
bool BluetoothPhoneAPI::checkIsConnected()
|
||||
{
|
||||
return active_conn != nullptr;
|
||||
}
|
||||
|
||||
// ── Deferred ToRadio processor + zombie-connection watchdog ──────────────────
|
||||
//
|
||||
// write_toradio() runs on the BT RX workqueue thread (CONFIG_BT_RX_STACK_SIZE)
|
||||
// and cannot execute phoneAPI->handleToRadio() directly: handleStartConfig
|
||||
// recurses through nanopb encode + state machine init and overflows the RX
|
||||
// stack. This thread runs on the Meshtastic OSThread scheduler (24 KB stack),
|
||||
// picks up the pending ToRadio buffer flagged by write_toradio(), and calls
|
||||
// handleToRadio() with plenty of headroom.
|
||||
//
|
||||
// Real-time fromNum notifications are sent synchronously from
|
||||
// BluetoothPhoneAPI::onNowHasData() (called by PhoneAPI when new data is
|
||||
// queued).
|
||||
//
|
||||
// Zombie-connection detection has two tiers:
|
||||
//
|
||||
// (1) Liveness probe. After IDLE_BEFORE_PROBE_MS without ATT traffic, send
|
||||
// a bt_gatt_notify to fromNum every PROBE_INTERVAL_MS. If the
|
||||
// controller replies -ENOTCONN the LL link is definitely dead but the
|
||||
// host didn't forward LE Disconnection Complete → reboot. We avoid
|
||||
// probing during normal activity so iOS isn't woken up unnecessarily
|
||||
// (each probe wakes iOS → triggers a zero-byte FromRadio drain).
|
||||
//
|
||||
// (2) Hard watchdog. Absolute HARD_WATCHDOG_MS ceiling on ATT idle as a
|
||||
// fallback if probes somehow don't detect the zombie.
|
||||
class BleDeferredThread : public concurrency::OSThread
|
||||
{
|
||||
static constexpr uint32_t IDLE_BEFORE_PROBE_MS = 30000; // 30 s: start probing
|
||||
static constexpr uint32_t PROBE_INTERVAL_MS = 5000; // 5 s: between probes
|
||||
static constexpr uint32_t HARD_WATCHDOG_MS = 60000; // 1 min: last resort
|
||||
|
||||
uint32_t last_probe_ms = 0;
|
||||
|
||||
public:
|
||||
BleDeferredThread() : concurrency::OSThread("BleDeferred") {}
|
||||
|
||||
protected:
|
||||
int32_t runOnce() override
|
||||
{
|
||||
// Snapshot the pending ToRadio buffer under the mutex, then release
|
||||
// the lock before calling into handleToRadio (which can be slow and
|
||||
// must not block the BT RX thread producer).
|
||||
uint8_t buf[MAX_TO_FROM_RADIO_SIZE];
|
||||
size_t n = 0;
|
||||
bool have_pending = false;
|
||||
k_mutex_lock(&pendingToRadioMutex, K_FOREVER);
|
||||
if (pendingToRadio) {
|
||||
memcpy(buf, pendingToRadioBuf, pendingToRadioLen);
|
||||
n = pendingToRadioLen;
|
||||
pendingToRadio = false;
|
||||
have_pending = true;
|
||||
}
|
||||
k_mutex_unlock(&pendingToRadioMutex);
|
||||
if (have_pending && phoneAPI) {
|
||||
phoneAPI->handleToRadio(buf, n);
|
||||
}
|
||||
|
||||
// Take a reference to active_conn so it can't be freed underneath us
|
||||
// if disconnected_cb fires on another thread while we're dispatching.
|
||||
struct bt_conn *conn = acquire_active_conn();
|
||||
if (!conn || connect_time_ms == 0) {
|
||||
if (conn)
|
||||
bt_conn_unref(conn);
|
||||
last_probe_ms = 0;
|
||||
return 100;
|
||||
}
|
||||
|
||||
uint32_t now = k_uptime_get_32();
|
||||
uint32_t att_idle = now - last_att_time_ms;
|
||||
|
||||
// Liveness probe — only when ATT has been quiet for a while.
|
||||
if (att_idle > IDLE_BEFORE_PROBE_MS && (now - last_probe_ms) >= PROBE_INTERVAL_MS &&
|
||||
(fromnum_ccc_val & BT_GATT_CCC_NOTIFY)) {
|
||||
last_probe_ms = now;
|
||||
int err = bt_gatt_notify(conn, &mesh_svc.attrs[FROMNUM_ATTR_IDX], &fromNumValue, sizeof(fromNumValue));
|
||||
if (err == -ENOTCONN) {
|
||||
LOG_WARN("BLE zombie (probe ENOTCONN); rebooting");
|
||||
bt_conn_unref(conn);
|
||||
k_sleep(K_MSEC(50)); // flush log
|
||||
sys_reboot(SYS_REBOOT_COLD);
|
||||
}
|
||||
}
|
||||
bt_conn_unref(conn);
|
||||
|
||||
// Hard ceiling — last-resort reboot if probes miss the zombie.
|
||||
if (att_idle > HARD_WATCHDOG_MS && (now - connect_time_ms) > HARD_WATCHDOG_MS) {
|
||||
LOG_WARN("BLE zombie (hard watchdog %us); rebooting", HARD_WATCHDOG_MS / 1000);
|
||||
k_sleep(K_MSEC(50));
|
||||
sys_reboot(SYS_REBOOT_COLD);
|
||||
}
|
||||
return 100;
|
||||
}
|
||||
};
|
||||
|
||||
static BleDeferredThread *bleDeferredThread = nullptr;
|
||||
|
||||
// ── BT stack pre-initializer (call from main thread before OSThreads start) ──
|
||||
//
|
||||
// bt_enable() requires substantially more stack than a Meshtastic OSThread
|
||||
// (PowerFSMThread) provides — calling it there causes a stack overflow.
|
||||
// Call this from nrf54l15Setup() (main Zephyr thread, CONFIG_MAIN_STACK_SIZE)
|
||||
// so that by the time NRF54L15Bluetooth::setup() runs from PowerFSMThread,
|
||||
// bt_initialized is already true and bt_enable() is skipped.
|
||||
|
||||
void nrf54l15_bt_preinit()
|
||||
{
|
||||
if (!bt_initialized) {
|
||||
int err = bt_enable(NULL);
|
||||
if (err) {
|
||||
LOG_ERROR("BLE pre-init failed: %d", err);
|
||||
return;
|
||||
}
|
||||
bt_initialized = true;
|
||||
LOG_INFO("BLE stack pre-initialized on main thread");
|
||||
|
||||
// Phase 7: load bonding keys from LittleFS (/lfs/bt_settings).
|
||||
// LittleFS is already mounted by fsInit() before nrf54l15Setup() runs.
|
||||
// On first boot the file doesn't exist — settings_load() returns 0 (OK).
|
||||
// On subsequent boots, previously bonded peers are restored so the
|
||||
// phone can reconnect without re-pairing.
|
||||
err = settings_load();
|
||||
if (err) {
|
||||
LOG_WARN("settings_load failed: %d (OK on first boot)", err);
|
||||
} else {
|
||||
LOG_INFO("BT settings loaded from /lfs/bt_settings");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── NRF54L15Bluetooth public methods ─────────────────────────────────────────
|
||||
|
||||
// Shared init: idempotent setup of work item, OSThread, auth callbacks,
|
||||
// bt_enable, and device name. Leaves advertising control to the caller.
|
||||
static bool nrf54l15_bt_init_common()
|
||||
{
|
||||
k_work_init(&adv_restart_work, adv_restart_work_fn);
|
||||
|
||||
if (!bleDeferredThread) {
|
||||
bleDeferredThread = new BleDeferredThread();
|
||||
}
|
||||
|
||||
if (!phoneAPI) {
|
||||
phoneAPI = new BluetoothPhoneAPI();
|
||||
}
|
||||
|
||||
#if defined(CONFIG_BT_SMP)
|
||||
// NO_PIN is unsupported on this platform: the mesh GATT permissions are
|
||||
// declared with BT_GATT_PERM_*_AUTHEN, prj.conf sets
|
||||
// CONFIG_BT_SMP_ENFORCE_MITM=y, and the build pulls in the SMP/passkey path.
|
||||
// If a user requested NO_PIN we'd register no auth callbacks → no display
|
||||
// path for the passkey → every GATT access returns BT_ATT_ERR_AUTHENTICATION
|
||||
// and the link is unusable. Fall back to RANDOM_PIN behavior with a warning
|
||||
// instead of leaving BLE silently broken.
|
||||
if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) {
|
||||
LOG_WARN("BLE: NO_PIN not supported on nRF54L15-DK (MITM-only build); "
|
||||
"treating as RANDOM_PIN");
|
||||
}
|
||||
|
||||
bt_conn_auth_cb_register(&auth_cb);
|
||||
bt_conn_auth_info_cb_register(&auth_info_cb);
|
||||
|
||||
// FIXED_PIN — register the configured passkey so the mobile app prompts
|
||||
// the user for that specific number instead of a random display-only PIN.
|
||||
// RANDOM_PIN (and clamped NO_PIN) keeps the default behavior: Zephyr
|
||||
// generates a fresh passkey on each pairing attempt and fires
|
||||
// auth_passkey_display with it.
|
||||
if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_FIXED_PIN) {
|
||||
configuredPasskey = config.bluetooth.fixed_pin;
|
||||
int rc = bt_passkey_set(configuredPasskey);
|
||||
if (rc) {
|
||||
LOG_WARN("bt_passkey_set(%u) failed: %d", configuredPasskey, rc);
|
||||
} else {
|
||||
LOG_INFO("BLE fixed PIN: %06u", configuredPasskey);
|
||||
}
|
||||
} else {
|
||||
bt_passkey_set(BT_PASSKEY_INVALID); // random per-pair
|
||||
}
|
||||
#endif /* CONFIG_BT_SMP */
|
||||
|
||||
if (!bt_initialized) {
|
||||
int err = bt_enable(NULL);
|
||||
if (err) {
|
||||
LOG_ERROR("BLE enable failed: %d", err);
|
||||
return false;
|
||||
}
|
||||
bt_initialized = true;
|
||||
LOG_INFO("BLE stack enabled");
|
||||
}
|
||||
|
||||
bt_set_name(getDeviceName());
|
||||
return true;
|
||||
}
|
||||
|
||||
void NRF54L15Bluetooth::setup()
|
||||
{
|
||||
LOG_INFO("NRF54L15Bluetooth::setup()");
|
||||
if (!nrf54l15_bt_init_common()) {
|
||||
return;
|
||||
}
|
||||
ble_enabled = true;
|
||||
start_advertising();
|
||||
}
|
||||
|
||||
void NRF54L15Bluetooth::shutdown()
|
||||
{
|
||||
LOG_INFO("NRF54L15Bluetooth::shutdown()");
|
||||
ble_enabled = false;
|
||||
stop_advertising();
|
||||
|
||||
struct bt_conn *conn = acquire_active_conn();
|
||||
if (conn) {
|
||||
bt_conn_disconnect(conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN);
|
||||
bt_conn_unref(conn);
|
||||
}
|
||||
}
|
||||
|
||||
void NRF54L15Bluetooth::startDisabled()
|
||||
{
|
||||
// Initialize BT stack but leave advertising off until resumeAdvertising().
|
||||
if (!nrf54l15_bt_init_common()) {
|
||||
return;
|
||||
}
|
||||
ble_enabled = false;
|
||||
LOG_INFO("BLE initialized, advertising stopped (startDisabled)");
|
||||
}
|
||||
|
||||
void NRF54L15Bluetooth::resumeAdvertising()
|
||||
{
|
||||
ble_enabled = true;
|
||||
start_advertising();
|
||||
}
|
||||
|
||||
void NRF54L15Bluetooth::clearBonds()
|
||||
{
|
||||
LOG_INFO("BLE clear bonds");
|
||||
bt_unpair(BT_ID_DEFAULT, BT_ADDR_LE_ANY);
|
||||
}
|
||||
|
||||
bool NRF54L15Bluetooth::isConnected()
|
||||
{
|
||||
return active_conn != nullptr;
|
||||
}
|
||||
|
||||
int NRF54L15Bluetooth::getRssi()
|
||||
{
|
||||
return 0; // TODO: Zephyr has no direct bt_conn_get_rssi; use HCI RSSI read
|
||||
// command
|
||||
}
|
||||
|
||||
void NRF54L15Bluetooth::sendLog(const uint8_t *logMessage, size_t length)
|
||||
{
|
||||
if (length > 512 || logradio_ccc_val == 0) {
|
||||
return;
|
||||
}
|
||||
// Acquire a reference under ble_mutex so disconnected_cb can't free the
|
||||
// connection between the null check and bt_gatt_notify.
|
||||
struct bt_conn *conn = acquire_active_conn();
|
||||
if (!conn) {
|
||||
return;
|
||||
}
|
||||
// Send as notify regardless of whether client subscribed to NOTIFY or
|
||||
// INDICATE — bt_gatt_indicate() requires a params struct with a callback;
|
||||
// notify is simpler and the app accepts both. Change to indicate if
|
||||
// compatibility issues arise.
|
||||
bt_gatt_notify(conn, &mesh_svc.attrs[LOGRADIO_ATTR_IDX], logMessage, (uint16_t)length);
|
||||
bt_conn_unref(conn);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// NRF54L15Bluetooth.h — Zephyr BLE backend for nRF54L15
|
||||
//
|
||||
// Implements the same interface as NRF52Bluetooth (same method names and
|
||||
// signatures) so main.cpp and AdminModule can use nrf52Bluetooth pointer
|
||||
// without knowing the underlying implementation.
|
||||
//
|
||||
// GATT profile is identical to the nRF52 implementation:
|
||||
// Service: MESH_SERVICE_UUID
|
||||
// toRadio: TORADIO_UUID (WRITE)
|
||||
// fromRadio: FROMRADIO_UUID (READ)
|
||||
// fromNum: FROMNUM_UUID (READ | NOTIFY)
|
||||
// logRadio: LOGRADIO_UUID (READ | NOTIFY | INDICATE)
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "BluetoothCommon.h"
|
||||
|
||||
class NRF54L15Bluetooth : public BluetoothApi
|
||||
{
|
||||
public:
|
||||
void setup();
|
||||
void shutdown();
|
||||
void startDisabled();
|
||||
void resumeAdvertising();
|
||||
void clearBonds();
|
||||
bool isConnected();
|
||||
int getRssi();
|
||||
void sendLog(const uint8_t *logMessage, size_t length);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
// Nrf52SaadcLock.h — stub for nRF54L15/Zephyr
|
||||
// Power.cpp includes this when ARCH_NRF52 is defined.
|
||||
// Phase 2: compile-only stub.
|
||||
#pragma once
|
||||
|
||||
#ifdef ARCH_NRF52
|
||||
|
||||
#include "concurrency/Lock.h"
|
||||
|
||||
namespace concurrency
|
||||
{
|
||||
/** Shared mutex for SAADC configuration and reads (VDD + battery analog path).
|
||||
* On nRF54L15 ADC is handled differently; this is a compile-only stub. */
|
||||
extern Lock *nrf52SaadcLock;
|
||||
} // namespace concurrency
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,4 @@
|
||||
// Print.h — shim for nRF54L15/Zephyr
|
||||
// Meshtastic includes <Print.h> separately; redirect to our Arduino.h shim.
|
||||
#pragma once
|
||||
#include "Arduino.h"
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* SPI.h — Arduino SPI shim for Zephyr/nRF54L15
|
||||
*
|
||||
* Provides the Arduino SPIClass interface backed by Zephyr's SPI API. The
|
||||
* backing controller is SPIM00 (HP domain, 3.0 V); the implementation in
|
||||
* nrf54l15_arduino.cpp binds to DEVICE_DT_GET(DT_NODELABEL(spi00)) and the
|
||||
* bus is configured in zephyr/boards/nrf54l15dk_nrf54l15_cpuapp.overlay.
|
||||
* RadioLib uses ArduinoHal which calls transfer() byte-by-byte.
|
||||
*
|
||||
* CS pin is handled by RadioLib via digitalWrite() — hardware CS is not used.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Arduino.h"
|
||||
#include <stdint.h>
|
||||
|
||||
#define SPI_MODE0 0
|
||||
#define SPI_MODE1 1
|
||||
#define SPI_MODE2 2
|
||||
#define SPI_MODE3 3
|
||||
|
||||
struct SPISettings {
|
||||
uint32_t clock;
|
||||
uint8_t bitOrder;
|
||||
uint8_t dataMode;
|
||||
|
||||
// Arduino API allows `SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0))` — implicit form is intentional.
|
||||
// cppcheck-suppress noExplicitConstructor
|
||||
SPISettings(uint32_t clock = 4000000, uint8_t bitOrder = MSBFIRST, uint8_t dataMode = SPI_MODE0)
|
||||
: clock(clock), bitOrder(bitOrder), dataMode(dataMode)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
class SPIClass
|
||||
{
|
||||
public:
|
||||
void begin() {}
|
||||
void begin(uint8_t sck, uint8_t miso, uint8_t mosi, uint8_t ss = 0xFF) {}
|
||||
void end() {}
|
||||
void beginTransaction(SPISettings) {}
|
||||
void endTransaction() {}
|
||||
void setBitOrder(uint8_t order) {}
|
||||
void setDataMode(uint8_t mode) {}
|
||||
void setClockDivider(uint8_t div) {}
|
||||
void setFrequency(uint32_t freq) {}
|
||||
|
||||
// Real Zephyr SPI implementation — defined in nrf54l15_arduino.cpp
|
||||
uint8_t transfer(uint8_t data);
|
||||
uint16_t transfer16(uint16_t data);
|
||||
void transfer(void *buf, size_t count);
|
||||
void transferBytes(const uint8_t *tx, uint8_t *rx, uint32_t count);
|
||||
uint8_t transfer(uint8_t tx, uint8_t *rx, uint32_t count)
|
||||
{
|
||||
transferBytes(&tx, rx, count);
|
||||
return rx ? rx[0] : 0;
|
||||
}
|
||||
};
|
||||
|
||||
extern SPIClass SPI;
|
||||
extern SPIClass SPI1;
|
||||
@@ -0,0 +1,5 @@
|
||||
// Stream.h — shim for nRF54L15/Zephyr
|
||||
// StreamAPI.h and other Meshtastic headers include <Stream.h>.
|
||||
// Redirect to our Arduino.h shim which defines the Stream base class.
|
||||
#pragma once
|
||||
#include "Arduino.h"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Tone.h — shim for nRF54L15/Zephyr
|
||||
// Tone functions are stubbed in Arduino.h; this header satisfies direct includes.
|
||||
#pragma once
|
||||
#include "Arduino.h"
|
||||
@@ -0,0 +1,5 @@
|
||||
// WProgram.h — shim for nRF54L15/Zephyr
|
||||
// ArduinoThread (and other legacy Arduino libs) include <WProgram.h>.
|
||||
// Redirect to our Arduino.h shim.
|
||||
#pragma once
|
||||
#include "Arduino.h"
|
||||
@@ -0,0 +1,219 @@
|
||||
// Wire.cpp — Arduino TwoWire backed by Zephyr i2c30 (TWIM30 hardware).
|
||||
//
|
||||
// The pinctrl + clock-frequency are configured in
|
||||
// zephyr/boards/nrf54l15dk_nrf54l15_cpuapp.overlay. Runtime begin()/setClock()
|
||||
// are best-effort: setClock() goes through i2c_configure() to actually change
|
||||
// the bus speed; begin() just verifies the device is ready.
|
||||
|
||||
#include "Wire.h"
|
||||
#include "configuration.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <zephyr/device.h>
|
||||
#include <zephyr/drivers/i2c.h>
|
||||
#include <zephyr/kernel.h>
|
||||
|
||||
// Resolve the i2c30 node at compile time. If the overlay has not enabled
|
||||
// i2c30, this evaluates to a NULL device pointer and every call short-circuits
|
||||
// to a NACK return code — matching the prior compile-only stub behavior.
|
||||
#define I2C_NODE DT_NODELABEL(i2c30)
|
||||
|
||||
static const struct device *getI2CDevice()
|
||||
{
|
||||
#if DT_NODE_HAS_STATUS(I2C_NODE, okay)
|
||||
static const struct device *const dev = DEVICE_DT_GET(I2C_NODE);
|
||||
return dev;
|
||||
#else
|
||||
return nullptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Wire/Wire1 instances are defined in nrf54l15_arduino.cpp alongside the
|
||||
// other Arduino singletons (Serial, SPI, …).
|
||||
|
||||
TwoWire::TwoWire() : txAddr(0), txLen(0), txBuf{}, rxLen(0), rxPos(0), rxBuf{} {}
|
||||
|
||||
void TwoWire::begin()
|
||||
{
|
||||
const struct device *dev = getI2CDevice();
|
||||
if (dev == nullptr) {
|
||||
LOG_WARN("Wire.begin(): i2c30 not enabled in DT overlay");
|
||||
return;
|
||||
}
|
||||
if (!device_is_ready(dev)) {
|
||||
LOG_WARN("Wire.begin(): i2c30 device not ready");
|
||||
return;
|
||||
}
|
||||
LOG_INFO("Wire.begin(): i2c30 ready");
|
||||
}
|
||||
|
||||
void TwoWire::begin(uint8_t /*sda*/, uint8_t /*scl*/)
|
||||
{
|
||||
// SDA/SCL fixed by overlay pinctrl — pin args ignored.
|
||||
begin();
|
||||
}
|
||||
|
||||
void TwoWire::begin(int /*sda*/, int /*scl*/, uint32_t freq)
|
||||
{
|
||||
begin();
|
||||
if (freq) {
|
||||
setClock(freq);
|
||||
}
|
||||
}
|
||||
|
||||
void TwoWire::end()
|
||||
{
|
||||
// No-op: Zephyr i2c devices stay initialized for the lifetime of the
|
||||
// application. Runtime PM (zephyr,pm-device-runtime-auto in the DT)
|
||||
// handles low-power transitions when idle.
|
||||
}
|
||||
|
||||
void TwoWire::setClock(uint32_t freq)
|
||||
{
|
||||
const struct device *dev = getI2CDevice();
|
||||
if (dev == nullptr) {
|
||||
return;
|
||||
}
|
||||
uint32_t speed;
|
||||
if (freq >= 1000000U) {
|
||||
speed = I2C_SPEED_FAST_PLUS; // 1 MHz
|
||||
} else if (freq >= 400000U) {
|
||||
speed = I2C_SPEED_FAST; // 400 kHz
|
||||
} else {
|
||||
speed = I2C_SPEED_STANDARD; // 100 kHz
|
||||
}
|
||||
uint32_t cfg = I2C_MODE_CONTROLLER | I2C_SPEED_SET(speed);
|
||||
int rc = i2c_configure(dev, cfg);
|
||||
if (rc) {
|
||||
LOG_WARN("Wire.setClock(%u) failed: %d", (unsigned)freq, rc);
|
||||
}
|
||||
}
|
||||
|
||||
void TwoWire::beginTransmission(uint8_t addr)
|
||||
{
|
||||
txAddr = addr;
|
||||
txLen = 0;
|
||||
}
|
||||
|
||||
size_t TwoWire::write(uint8_t data)
|
||||
{
|
||||
if (txLen >= WIRE_BUFFER_LENGTH) {
|
||||
return 0; // overflow — endTransmission() will return 1
|
||||
}
|
||||
txBuf[txLen++] = data;
|
||||
return 1;
|
||||
}
|
||||
|
||||
size_t TwoWire::write(const uint8_t *data, size_t n)
|
||||
{
|
||||
size_t written = 0;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
if (write(data[i]) == 0) {
|
||||
break;
|
||||
}
|
||||
written++;
|
||||
}
|
||||
return written;
|
||||
}
|
||||
|
||||
uint8_t TwoWire::endTransmission(bool /*stop*/)
|
||||
{
|
||||
// Arduino return codes:
|
||||
// 0 = success
|
||||
// 1 = data-too-long (overflow caught in write())
|
||||
// 2 = NACK on address
|
||||
// 3 = NACK on data
|
||||
// 4 = other error
|
||||
// 5 = timeout
|
||||
if (txLen > WIRE_BUFFER_LENGTH) {
|
||||
return 1;
|
||||
}
|
||||
const struct device *dev = getI2CDevice();
|
||||
if (dev == nullptr || !device_is_ready(dev)) {
|
||||
return 4;
|
||||
}
|
||||
int rc = i2c_write(dev, txBuf, txLen, txAddr);
|
||||
txLen = 0;
|
||||
if (rc == 0) {
|
||||
return 0;
|
||||
}
|
||||
if (rc == -EIO) {
|
||||
return 2; // address NACK is the most common -EIO source on nrf-twim
|
||||
}
|
||||
if (rc == -ETIMEDOUT) {
|
||||
return 5;
|
||||
}
|
||||
return 4;
|
||||
}
|
||||
|
||||
uint8_t TwoWire::requestFrom(uint8_t addr, uint8_t quantity, bool /*stop*/)
|
||||
{
|
||||
rxLen = 0;
|
||||
rxPos = 0;
|
||||
if (quantity == 0) {
|
||||
return 0;
|
||||
}
|
||||
if (quantity > WIRE_BUFFER_LENGTH) {
|
||||
quantity = WIRE_BUFFER_LENGTH;
|
||||
}
|
||||
const struct device *dev = getI2CDevice();
|
||||
if (dev == nullptr || !device_is_ready(dev)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// If there is a pending TX (driver wrote register address via write()
|
||||
// without an explicit endTransmission()), use i2c_write_read so the
|
||||
// repeated-start path matches the typical "set register pointer then
|
||||
// read N bytes" sensor protocol.
|
||||
int rc;
|
||||
if (txLen > 0) {
|
||||
rc = i2c_write_read(dev, addr, txBuf, txLen, rxBuf, quantity);
|
||||
txLen = 0;
|
||||
} else {
|
||||
rc = i2c_read(dev, rxBuf, quantity, addr);
|
||||
}
|
||||
if (rc) {
|
||||
return 0;
|
||||
}
|
||||
rxLen = quantity;
|
||||
return quantity;
|
||||
}
|
||||
|
||||
int TwoWire::available()
|
||||
{
|
||||
return rxLen - rxPos;
|
||||
}
|
||||
|
||||
int TwoWire::read()
|
||||
{
|
||||
if (rxPos >= rxLen) {
|
||||
return -1;
|
||||
}
|
||||
return rxBuf[rxPos++];
|
||||
}
|
||||
|
||||
int TwoWire::peek()
|
||||
{
|
||||
if (rxPos >= rxLen) {
|
||||
return -1;
|
||||
}
|
||||
return rxBuf[rxPos];
|
||||
}
|
||||
|
||||
size_t TwoWire::readBytes(uint8_t *buf, size_t len)
|
||||
{
|
||||
size_t n = 0;
|
||||
while (n < len) {
|
||||
int b = read();
|
||||
if (b < 0) {
|
||||
break;
|
||||
}
|
||||
buf[n++] = (uint8_t)b;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
TwoWire::operator bool() const
|
||||
{
|
||||
return getI2CDevice() != nullptr;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Wire.h — Arduino TwoWire (I2C) shim for Zephyr / nRF54L15.
|
||||
*
|
||||
* Bus binding: the Zephyr device tree alias `i2c30` (TWIM30 hardware
|
||||
* peripheral, HP domain, 3.0 V) is resolved at compile time via
|
||||
* DEVICE_DT_GET in Wire.cpp. SDA/SCL pins are configured in the board
|
||||
* overlay via pinctrl — `begin(sda, scl)` overloads are accepted for
|
||||
* Arduino API compatibility but the pin arguments are ignored.
|
||||
*
|
||||
* Buffer sizes are sized for the worst-case I2C consumer we plan to use
|
||||
* (NXP SE050 secure element, ~256-byte T=1 frames). BMP280 / INA228 /
|
||||
* SHT40 / INA3221 read in single-digit bytes and fit trivially.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Arduino.h"
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifndef WIRE_BUFFER_LENGTH
|
||||
#define WIRE_BUFFER_LENGTH 256
|
||||
#endif
|
||||
|
||||
class TwoWire
|
||||
{
|
||||
public:
|
||||
TwoWire();
|
||||
|
||||
// ── Bus lifecycle ─────────────────────────────────────────────────
|
||||
// begin() variants — pin arguments are accepted for API compatibility
|
||||
// but ignored: SDA/SCL are fixed by the Zephyr overlay pinctrl. freq
|
||||
// is also fixed by overlay clock-frequency (use setClock() at runtime).
|
||||
void begin();
|
||||
void begin(uint8_t sda, uint8_t scl);
|
||||
void begin(int sda, int scl, uint32_t freq);
|
||||
void end();
|
||||
|
||||
void setClock(uint32_t freq);
|
||||
void setClockStretchLimit(uint32_t) {} // no-op on TWIM hardware
|
||||
|
||||
// ── Master write ─────────────────────────────────────────────────
|
||||
void beginTransmission(uint8_t addr);
|
||||
void beginTransmission(int addr) { beginTransmission((uint8_t)addr); }
|
||||
// Return codes (Arduino convention):
|
||||
// 0 = success, 1 = data-too-long, 2 = NACK on addr, 3 = NACK on data,
|
||||
// 4 = other error, 5 = timeout.
|
||||
uint8_t endTransmission(bool stop = true);
|
||||
uint8_t endTransmission(uint8_t stop) { return endTransmission(stop != 0); }
|
||||
|
||||
size_t write(uint8_t data);
|
||||
size_t write(const uint8_t *data, size_t n);
|
||||
|
||||
// ── Master read ──────────────────────────────────────────────────
|
||||
uint8_t requestFrom(uint8_t addr, uint8_t quantity, bool stop = true);
|
||||
uint8_t requestFrom(uint8_t addr, uint8_t quantity, uint8_t stop) { return requestFrom(addr, quantity, stop != 0); }
|
||||
uint8_t requestFrom(int addr, int quantity, int stop = 1) { return requestFrom((uint8_t)addr, (uint8_t)quantity, stop != 0); }
|
||||
|
||||
int available();
|
||||
int read();
|
||||
int peek();
|
||||
size_t readBytes(uint8_t *buf, size_t len);
|
||||
size_t readBytes(char *buf, size_t len) { return readBytes((uint8_t *)buf, len); }
|
||||
void flush() {}
|
||||
|
||||
// Slave callbacks unsupported — peripheral-only stub.
|
||||
void onReceive(void (*)(int)) {}
|
||||
void onRequest(void (*)(void)) {}
|
||||
|
||||
operator bool() const;
|
||||
|
||||
private:
|
||||
uint8_t txAddr;
|
||||
uint16_t txLen;
|
||||
uint8_t txBuf[WIRE_BUFFER_LENGTH];
|
||||
|
||||
uint16_t rxLen;
|
||||
uint16_t rxPos;
|
||||
uint8_t rxBuf[WIRE_BUFFER_LENGTH];
|
||||
};
|
||||
|
||||
extern TwoWire Wire;
|
||||
extern TwoWire Wire1; // alias to Wire — only one I2C bus on this board
|
||||
@@ -0,0 +1,79 @@
|
||||
#pragma once
|
||||
|
||||
#define ARCH_NRF54L15
|
||||
|
||||
//
|
||||
// Feature flags for nRF54L15.
|
||||
//
|
||||
// The HAS_* macros below are Meshtastic's compile-time feature gate: every
|
||||
// optional subsystem (BLE, screen, I2C, GPS, buttons, telemetry, sensors,
|
||||
// radio, CPU shutdown, ...) is wrapped in `#if HAS_FOO` so a given board
|
||||
// only pays for the features it actually ships. On memory-tight MCUs this
|
||||
// is not cosmetic — it's the difference between a binary that fits in
|
||||
// flash and one that doesn't, and between a build that links and one that
|
||||
// drags in drivers for hardware the board doesn't have. Defaulting to 0
|
||||
// here (rather than inheriting nRF52 defaults) is deliberate: the
|
||||
// nRF54L15-DK is a bare dev kit with no screen, no I2C sensors, no GPS,
|
||||
// no user buttons — so every HAS_* flag starts off and gets flipped on
|
||||
// explicitly by variants that add that hardware.
|
||||
//
|
||||
// Feature flags are also the cleanest way to absorb platform divergence
|
||||
// without sprinkling `#ifdef ARCH_NRF54L15` across shared code. Anywhere
|
||||
// a subsystem can be conditionally compiled via HAS_*, prefer that over
|
||||
// per-arch guards: it keeps the core code arch-agnostic, makes it trivial
|
||||
// to bring up the next board (flip the flags, don't patch call sites),
|
||||
// and keeps the "does this platform support X?" question answerable by
|
||||
// reading one file instead of grepping the tree. BLE in particular is
|
||||
// deferred to Phase 2 on this port — the nRF54L15 uses MPSL/Zephyr BLE
|
||||
// APIs rather than the Adafruit SoftDevice stack used by nRF52840 — so
|
||||
// while HAS_BLUETOOTH defaults to 1, the actual implementation lives in
|
||||
// NRF54L15Bluetooth.cpp behind its own Zephyr Kconfig gates.
|
||||
//
|
||||
|
||||
#ifndef HAS_BLUETOOTH
|
||||
#define HAS_BLUETOOTH 1
|
||||
#endif
|
||||
#ifndef HAS_SCREEN
|
||||
#define HAS_SCREEN 0
|
||||
#endif
|
||||
#ifndef HAS_WIRE
|
||||
#define HAS_WIRE 0
|
||||
#endif
|
||||
#ifndef HAS_GPS
|
||||
#define HAS_GPS 0
|
||||
#endif
|
||||
#ifndef HAS_BUTTON
|
||||
#define HAS_BUTTON 0
|
||||
#endif
|
||||
#ifndef HAS_TELEMETRY
|
||||
#define HAS_TELEMETRY 0
|
||||
#endif
|
||||
#ifndef HAS_SENSOR
|
||||
#define HAS_SENSOR 0
|
||||
#endif
|
||||
#ifndef HAS_RADIO
|
||||
#define HAS_RADIO 1
|
||||
#endif
|
||||
#ifndef HAS_CPU_SHUTDOWN
|
||||
#define HAS_CPU_SHUTDOWN 0
|
||||
#endif
|
||||
|
||||
// ADC reference — nRF54L15 SAADC uses VDD/4 internal ref by default
|
||||
#ifndef AREF_VOLTAGE
|
||||
#define AREF_VOLTAGE 3.6
|
||||
#endif
|
||||
#ifndef BATTERY_SENSE_RESOLUTION_BITS
|
||||
#define BATTERY_SENSE_RESOLUTION_BITS 12
|
||||
#endif
|
||||
|
||||
//
|
||||
// HW_VENDOR — maps build-time define to HardwareModel enum.
|
||||
// PRIVATE_HW (255): the protobuf HardwareModel enum reserves DK / DIY boards
|
||||
// without an SKU under this value; the nRF54L15-DK doesn't get a dedicated
|
||||
// enum number. Variant manifest matches via custom_meshtastic_hw_model = 255.
|
||||
//
|
||||
#ifdef NRF54L15_DK
|
||||
#define HW_VENDOR meshtastic_HardwareModel_PRIVATE_HW
|
||||
#else
|
||||
#define HW_VENDOR meshtastic_HardwareModel_UNSET
|
||||
#endif
|
||||
@@ -0,0 +1,19 @@
|
||||
// bluefruit.h — stub for nRF54L15/Zephyr
|
||||
// NodeDB.cpp includes this when ARCH_NRF52 is defined.
|
||||
// Bluetooth is excluded (MESHTASTIC_EXCLUDE_BLUETOOTH=1); this satisfies
|
||||
// the include chain without pulling in the Adafruit Bluefruit SDK.
|
||||
#pragma once
|
||||
|
||||
struct BLEPeripheral {
|
||||
void clearBonds() {}
|
||||
};
|
||||
struct BLECentral {
|
||||
void clearBonds() {}
|
||||
};
|
||||
|
||||
struct BlueFruitClass {
|
||||
BLEPeripheral Periph;
|
||||
BLECentral Central;
|
||||
};
|
||||
|
||||
extern BlueFruitClass Bluefruit;
|
||||
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
* main-nrf54l15.cpp — Platform entry points for Nordic nRF54L15
|
||||
*
|
||||
* Adapted from src/platform/nrf52/main-nrf52.cpp.
|
||||
* SoftDevice, Adafruit BLE, and nRFCrypto are NOT available on nRF54L15.
|
||||
* Phase 2 will add proper BLE via Zephyr MPSL APIs.
|
||||
*
|
||||
* TODO items are marked with "TODO(nrf54l15):"
|
||||
*/
|
||||
|
||||
#include "configuration.h"
|
||||
#include <SPI.h>
|
||||
#include <Wire.h>
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "NodeDB.h"
|
||||
#include "PowerMon.h"
|
||||
#include "Router.h"
|
||||
#include "error.h"
|
||||
#include "main.h"
|
||||
#include "mesh/MeshService.h"
|
||||
#include "meshUtils.h"
|
||||
#include "power.h"
|
||||
#include <power/PowerHAL.h>
|
||||
|
||||
// ── Watchdog ──────────────────────────────────────────────────────────────
|
||||
// TODO(nrf54l15): nRF54L15 has a WDT peripheral but nrfx_wdt driver support
|
||||
// may differ depending on the Zephyr SDK version. Enable once confirmed.
|
||||
#define APP_WATCHDOG_SECS 90
|
||||
static bool watchdog_running = false;
|
||||
|
||||
static inline void watchdog_feed() {} // TODO(nrf54l15): replace with real WDT feed
|
||||
|
||||
// ── Weak variant hooks ────────────────────────────────────────────────────
|
||||
void variant_shutdown() __attribute__((weak));
|
||||
void variant_shutdown() {}
|
||||
|
||||
void variant_nrf54l15LoopHook(void) __attribute__((weak));
|
||||
void variant_nrf54l15LoopHook(void) {}
|
||||
|
||||
// ── PowerHAL ─────────────────────────────────────────────────────────────
|
||||
bool powerHAL_isVBUSConnected()
|
||||
{
|
||||
// TODO(nrf54l15): nRF54L15 has a USB POWER peripheral — read USBREGSTATUS
|
||||
return false;
|
||||
}
|
||||
|
||||
bool powerHAL_isPowerLevelSafe()
|
||||
{
|
||||
// TODO(nrf54l15): implement SAADC VDD measurement similar to nRF52
|
||||
return true;
|
||||
}
|
||||
|
||||
void powerHAL_platformInit()
|
||||
{
|
||||
// TODO(nrf54l15): configure POF comparator and analog reference if needed
|
||||
}
|
||||
|
||||
// ── Utilities ─────────────────────────────────────────────────────────────
|
||||
bool loopCanSleep()
|
||||
{
|
||||
return !Serial;
|
||||
}
|
||||
|
||||
void updateBatteryLevel(uint8_t level)
|
||||
{
|
||||
(void)level;
|
||||
}
|
||||
|
||||
void __attribute__((noreturn)) __assert_func(const char *file, int line, const char *func, const char *failedexpr)
|
||||
{
|
||||
LOG_ERROR("assert failed %s: %d, %s, test=%s", file, line, func, failedexpr);
|
||||
NVIC_SystemReset();
|
||||
}
|
||||
|
||||
void getMacAddr(uint8_t *dmac)
|
||||
{
|
||||
// TODO(nrf54l15): verify FICR register layout for nRF54L15.
|
||||
// nRF52840 uses NRF_FICR->DEVICEADDR[0/1]; nRF54L15 Zephyr HAL may differ.
|
||||
#if defined(NRF_FICR)
|
||||
const uint8_t *src = (const uint8_t *)NRF_FICR->DEVICEADDR;
|
||||
dmac[5] = src[0];
|
||||
dmac[4] = src[1];
|
||||
dmac[3] = src[2];
|
||||
dmac[2] = src[3];
|
||||
dmac[1] = src[4];
|
||||
dmac[0] = src[5] | 0xc0;
|
||||
#else
|
||||
// Fallback: fixed placeholder until Zephyr FICR path is confirmed
|
||||
dmac[0] = 0xC2;
|
||||
dmac[1] = 0xA7;
|
||||
dmac[2] = 0x54;
|
||||
dmac[3] = 0x15;
|
||||
dmac[4] = 0x00;
|
||||
dmac[5] = 0x01;
|
||||
#endif
|
||||
}
|
||||
|
||||
// ── Bluetooth ─────────────────────────────────────────────────────────────────
|
||||
|
||||
void setBluetoothEnable(bool enable)
|
||||
{
|
||||
if (enable) {
|
||||
static bool initialized = false;
|
||||
if (!initialized) {
|
||||
nrf54l15Bluetooth = new NRF54L15Bluetooth();
|
||||
nrf54l15Bluetooth->startDisabled();
|
||||
initialized = true;
|
||||
}
|
||||
if (nrf54l15Bluetooth) {
|
||||
nrf54l15Bluetooth->resumeAdvertising();
|
||||
}
|
||||
} else {
|
||||
if (nrf54l15Bluetooth) {
|
||||
nrf54l15Bluetooth->shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void clearBonds()
|
||||
{
|
||||
if (!nrf54l15Bluetooth) {
|
||||
nrf54l15Bluetooth = new NRF54L15Bluetooth();
|
||||
nrf54l15Bluetooth->setup();
|
||||
}
|
||||
nrf54l15Bluetooth->clearBonds();
|
||||
}
|
||||
|
||||
void enterDfuMode()
|
||||
{
|
||||
// TODO(nrf54l15): nRF54L15 uses nRF Connect DFU (MCUboot/SUIT).
|
||||
// Trigger via Zephyr boot_request_upgrade() or similar.
|
||||
NVIC_SystemReset();
|
||||
}
|
||||
|
||||
// ── printf via RTT ────────────────────────────────────────────────────────
|
||||
// TODO(nrf54l15): SEGGER_RTT may not be available with Zephyr; use printk()
|
||||
// or a USB CDC console instead. Remove this override if it conflicts.
|
||||
#ifdef SEGGER_RTT_PRINTF
|
||||
int printf(const char *fmt, ...)
|
||||
{
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
auto res = SEGGER_RTT_vprintf(0, fmt, &args);
|
||||
va_end(args);
|
||||
return res;
|
||||
}
|
||||
#endif
|
||||
|
||||
// ── Deep sleep ────────────────────────────────────────────────────────────
|
||||
void cpuDeepSleep(uint32_t msecToWake)
|
||||
{
|
||||
#if HAS_WIRE
|
||||
Wire.end();
|
||||
#endif
|
||||
SPI.end();
|
||||
if (Serial)
|
||||
Serial.end();
|
||||
|
||||
variant_shutdown();
|
||||
|
||||
// TODO(nrf54l15): use Zephyr pm_system_suspend() or WFI for proper low-power
|
||||
if (msecToWake != portMAX_DELAY) {
|
||||
delay(msecToWake);
|
||||
NVIC_SystemReset();
|
||||
} else {
|
||||
// System off equivalent — halt
|
||||
while (1) {
|
||||
__WFI();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Setup / Loop ──────────────────────────────────────────────────────────
|
||||
// Forward declaration — defined in NRF54L15Bluetooth.cpp
|
||||
void nrf54l15_bt_preinit();
|
||||
|
||||
void nrf54l15Setup()
|
||||
{
|
||||
// nRF54L15 power peripheral layout differs from nRF52; RESETREAS not present here.
|
||||
// TODO(Phase 3): use zephyr/drivers/hwinfo.h hwinfo_get_reset_cause()
|
||||
LOG_DEBUG("Reset reason: (nRF54L15 power peripheral differs from nRF52, skipped)");
|
||||
|
||||
// TODO(nrf54l15): init SAADC, watchdog, and random seed via nrfx or Zephyr
|
||||
// For now seed with a fixed value; replace with hardware entropy source.
|
||||
#if defined(NRF_FICR)
|
||||
randomSeed(analogRead(0) ^ (uint32_t)NRF_FICR->DEVICEADDR[0]);
|
||||
#else
|
||||
randomSeed(analogRead(0));
|
||||
#endif
|
||||
|
||||
// Pre-initialize BT stack here on the main thread (CONFIG_MAIN_STACK_SIZE=8192).
|
||||
// bt_enable() overflows the smaller PowerFSMThread stack when called later.
|
||||
// NRF54L15Bluetooth::setup() checks bt_initialized and skips bt_enable() if true.
|
||||
nrf54l15_bt_preinit();
|
||||
}
|
||||
|
||||
void nrf54l15Loop()
|
||||
{
|
||||
// First-call gate for the future WDT init — body will hold real init code, not just the bookkeeping flag.
|
||||
// cppcheck-suppress duplicateConditionalAssign
|
||||
if (!watchdog_running) {
|
||||
// TODO(nrf54l15): enable WDT here
|
||||
watchdog_running = true;
|
||||
}
|
||||
watchdog_feed();
|
||||
|
||||
variant_nrf54l15LoopHook();
|
||||
}
|
||||
@@ -0,0 +1,557 @@
|
||||
/**
|
||||
* nrf54l15_arduino.cpp — Arduino shim implementations for Zephyr/nRF54L15
|
||||
*
|
||||
* Provides concrete implementations for Print, HardwareSerial, GPIO, SPI,
|
||||
* and String methods declared in Arduino.h / SPI.h.
|
||||
*
|
||||
* Phase 3: real GPIO via Zephyr GPIO API and real SPI via Zephyr SPI API.
|
||||
* Pin numbering convention: P0.n = n, P1.n = 16+n, P2.n = 32+n.
|
||||
*/
|
||||
|
||||
#include "Arduino.h"
|
||||
#include "SPI.h"
|
||||
#include "Wire.h"
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <zephyr/drivers/gpio.h>
|
||||
#include <zephyr/drivers/spi.h>
|
||||
#include <zephyr/kernel.h>
|
||||
// ── Bluefruit singleton stub (satisfies NodeDB.cpp ARCH_NRF52 path) ──────────
|
||||
#include "bluefruit.h"
|
||||
BlueFruitClass Bluefruit;
|
||||
|
||||
// ── _fini stub — ARM newlib's __libc_fini_array references _fini, but ────────
|
||||
// Zephyr startup doesn't provide it. Provide a weak no-op so the linker
|
||||
// is satisfied when C++ global dtors or atexit() pull in __libc_fini_array.
|
||||
extern "C" void __attribute__((weak)) _fini(void) {}
|
||||
|
||||
// ── SPI / Wire singletons ─────────────────────────────────────────────────────
|
||||
SPIClass SPI;
|
||||
SPIClass SPI1;
|
||||
TwoWire Wire;
|
||||
TwoWire Wire1;
|
||||
|
||||
// ── HardwareSerial singletons ────────────────────────────────────────────────
|
||||
HardwareSerial Serial;
|
||||
HardwareSerial Serial1;
|
||||
HardwareSerial Serial2;
|
||||
|
||||
// ── Timing functions — C linkage to match extern "C" declarations ────────────
|
||||
extern "C" uint32_t millis(void)
|
||||
{
|
||||
return (uint32_t)k_uptime_get_32();
|
||||
}
|
||||
extern "C" uint32_t micros(void)
|
||||
{
|
||||
return (uint32_t)(k_uptime_get() * 1000ULL);
|
||||
}
|
||||
extern "C" void delay(uint32_t ms)
|
||||
{
|
||||
k_sleep(K_MSEC(ms));
|
||||
}
|
||||
extern "C" void delayMicroseconds(uint32_t us)
|
||||
{
|
||||
k_sleep(K_USEC(us));
|
||||
}
|
||||
extern "C" void yield(void)
|
||||
{
|
||||
k_yield();
|
||||
}
|
||||
|
||||
// ── NVIC_SystemReset — wraps __NVIC_SystemReset from CMSIS core_cm33.h ───────
|
||||
// core_cm33.h has #define NVIC_SystemReset __NVIC_SystemReset, so undef it
|
||||
// before defining our own implementation to prevent macro expansion collision.
|
||||
#pragma push_macro("NVIC_SystemReset")
|
||||
#undef NVIC_SystemReset
|
||||
extern "C" void NVIC_SystemReset(void)
|
||||
{
|
||||
sys_reboot(SYS_REBOOT_COLD);
|
||||
}
|
||||
#pragma pop_macro("NVIC_SystemReset")
|
||||
|
||||
// ── HardwareSerial::write ─────────────────────────────────────────────────────
|
||||
size_t HardwareSerial::write(uint8_t c)
|
||||
{
|
||||
// TODO(nrf54l15 Phase 3): route through Zephyr UART / USB-CDC console
|
||||
// For now use printk so we at least get something over RTT/UART0
|
||||
printk("%c", (char)c);
|
||||
return 1;
|
||||
}
|
||||
|
||||
size_t HardwareSerial::write(const uint8_t *buf, size_t n)
|
||||
{
|
||||
for (size_t i = 0; i < n; i++)
|
||||
printk("%c", (char)buf[i]);
|
||||
return n;
|
||||
}
|
||||
|
||||
// ── Print::printf ─────────────────────────────────────────────────────────────
|
||||
int Print::printf(const char *fmt, ...)
|
||||
{
|
||||
char buf[256];
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
int n = vsnprintf(buf, sizeof(buf), fmt, args);
|
||||
va_end(args);
|
||||
if (n > 0)
|
||||
write((const uint8_t *)buf, (size_t)(n < (int)sizeof(buf) ? n : (int)sizeof(buf) - 1));
|
||||
return n;
|
||||
}
|
||||
|
||||
// ── strlcpy — BSD extension not in Zephyr newlib ────────────────────────────
|
||||
extern "C" size_t strlcpy(char *dst, const char *src, size_t size)
|
||||
{
|
||||
size_t len = strlen(src);
|
||||
if (size > 0) {
|
||||
size_t copy = len < size - 1 ? len : size - 1;
|
||||
memcpy(dst, src, copy);
|
||||
dst[copy] = '\0';
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
// ── Print numeric helpers ─────────────────────────────────────────────────────
|
||||
static size_t printNumber(Print &p, unsigned long n, uint8_t base)
|
||||
{
|
||||
if (base == 0)
|
||||
return p.write((uint8_t)n);
|
||||
|
||||
char buf[8 * sizeof(long) + 1];
|
||||
char *end = buf + sizeof(buf) - 1;
|
||||
*end = '\0';
|
||||
if (n == 0) {
|
||||
*--end = '0';
|
||||
} else {
|
||||
while (n > 0) {
|
||||
unsigned long remainder = n % base;
|
||||
*--end = (char)(remainder < 10 ? '0' + remainder : 'A' + remainder - 10);
|
||||
n /= base;
|
||||
}
|
||||
}
|
||||
return p.write((const uint8_t *)end, strlen(end));
|
||||
}
|
||||
|
||||
static size_t printFloat(Print &p, double number, uint8_t digits)
|
||||
{
|
||||
if (isnan(number))
|
||||
return p.print("nan");
|
||||
if (isinf(number))
|
||||
return p.print("inf");
|
||||
if (number > 4294967040.0 || number < -4294967040.0)
|
||||
return p.print("ovf");
|
||||
|
||||
size_t n = 0;
|
||||
if (number < 0.0) {
|
||||
n += p.write('-');
|
||||
number = -number;
|
||||
}
|
||||
|
||||
// Round
|
||||
double rounding = 0.5;
|
||||
for (uint8_t i = 0; i < digits; i++)
|
||||
rounding /= 10.0;
|
||||
number += rounding;
|
||||
|
||||
unsigned long int_part = (unsigned long)number;
|
||||
double remainder = number - (double)int_part;
|
||||
n += printNumber(p, int_part, 10);
|
||||
if (digits > 0) {
|
||||
n += p.write('.');
|
||||
for (uint8_t i = 0; i < digits; i++) {
|
||||
remainder *= 10.0;
|
||||
unsigned int d = (unsigned int)remainder;
|
||||
n += p.write('0' + d);
|
||||
remainder -= d;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
size_t Print::print(unsigned char n, int base)
|
||||
{
|
||||
return printNumber(*this, n, base);
|
||||
}
|
||||
size_t Print::print(int n, int base)
|
||||
{
|
||||
if (base == 10 && n < 0) {
|
||||
size_t r = write('-');
|
||||
return r + printNumber(*this, (unsigned long)(-n), base);
|
||||
}
|
||||
return printNumber(*this, (unsigned long)n, base);
|
||||
}
|
||||
size_t Print::print(long n, int base)
|
||||
{
|
||||
if (base == 10 && n < 0) {
|
||||
size_t r = write('-');
|
||||
return r + printNumber(*this, (unsigned long)(-n), base);
|
||||
}
|
||||
return printNumber(*this, (unsigned long)n, base);
|
||||
}
|
||||
size_t Print::print(unsigned int n, int base)
|
||||
{
|
||||
return printNumber(*this, n, base);
|
||||
}
|
||||
size_t Print::print(unsigned long n, int base)
|
||||
{
|
||||
return printNumber(*this, n, base);
|
||||
}
|
||||
size_t Print::print(float n, int d)
|
||||
{
|
||||
return printFloat(*this, n, d);
|
||||
}
|
||||
size_t Print::print(double n, int d)
|
||||
{
|
||||
return printFloat(*this, n, d);
|
||||
}
|
||||
|
||||
// ── String::replace(String, String) ─────────────────────────────────────────
|
||||
void String::replace(const String &from, const String &to)
|
||||
{
|
||||
if (from.isEmpty() || !_buf)
|
||||
return;
|
||||
// Simple O(n²) replace — fine for typical Meshtastic string lengths
|
||||
String result;
|
||||
const char *p = _buf;
|
||||
while (*p) {
|
||||
if (strncmp(p, from.c_str(), from.length()) == 0) {
|
||||
result += to;
|
||||
p += from.length();
|
||||
} else {
|
||||
result += *p++;
|
||||
}
|
||||
}
|
||||
*this = result;
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════════
|
||||
// GPIO — Real Zephyr implementation (Phase 3)
|
||||
// Pin mapping: P0.n = n (0-15), P1.n = 16+n (16-31), P2.n = 32+n (32-47)
|
||||
// ═════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
static const struct device *_gpio_dev_for_pin(uint32_t pin, gpio_pin_t *zpin)
|
||||
{
|
||||
if (pin < 16) {
|
||||
*zpin = (gpio_pin_t)pin;
|
||||
return DEVICE_DT_GET(DT_NODELABEL(gpio0));
|
||||
} else if (pin < 32) {
|
||||
*zpin = (gpio_pin_t)(pin - 16);
|
||||
return DEVICE_DT_GET(DT_NODELABEL(gpio1));
|
||||
} else {
|
||||
*zpin = (gpio_pin_t)(pin - 32);
|
||||
return DEVICE_DT_GET(DT_NODELABEL(gpio2));
|
||||
}
|
||||
}
|
||||
|
||||
void pinMode(uint32_t pin, uint32_t mode)
|
||||
{
|
||||
gpio_pin_t zpin;
|
||||
const struct device *dev = _gpio_dev_for_pin(pin, &zpin);
|
||||
if (!device_is_ready(dev))
|
||||
return;
|
||||
|
||||
gpio_flags_t flags;
|
||||
switch (mode) {
|
||||
case OUTPUT:
|
||||
flags = GPIO_OUTPUT_INACTIVE;
|
||||
break;
|
||||
case INPUT_PULLUP:
|
||||
flags = GPIO_INPUT | GPIO_PULL_UP;
|
||||
break;
|
||||
case INPUT_PULLDOWN:
|
||||
flags = GPIO_INPUT | GPIO_PULL_DOWN;
|
||||
break;
|
||||
default:
|
||||
flags = GPIO_INPUT;
|
||||
break;
|
||||
}
|
||||
gpio_pin_configure(dev, zpin, flags);
|
||||
}
|
||||
|
||||
// Bring-up diagnostics for the SX1262 wiring path. Off by default — enable by
|
||||
// adding `-DNRF54L15_GPIO_DEBUG` to platformio.ini build_flags. Useful when
|
||||
// validating CS/NRESET toggles after a wiring change, diagnosing a "stuck HIGH"
|
||||
// BUSY before the first NRESET pulse, or tracing BUSY transitions during early
|
||||
// boot. In normal operation these traces are noise (they bypass LOG level
|
||||
// controls and print on every GPIO touch), so they are gated at compile time.
|
||||
#ifdef NRF54L15_GPIO_DEBUG
|
||||
#define GPIO_LOG_MAX 20
|
||||
static uint32_t _gpio_log_count = 0;
|
||||
#endif
|
||||
|
||||
void digitalWrite(uint32_t pin, uint32_t value)
|
||||
{
|
||||
#ifdef NRF54L15_GPIO_DEBUG
|
||||
// Before the very first NRESET pulse, snapshot BUSY state.
|
||||
// If BUSY is already HIGH here, the chip never completed power-on calibration.
|
||||
if (pin == 32 && value == 0) {
|
||||
static bool _first_nreset = true;
|
||||
if (_first_nreset) {
|
||||
_first_nreset = false;
|
||||
const struct device *bdev = DEVICE_DT_GET(DT_NODELABEL(gpio2));
|
||||
if (device_is_ready(bdev)) {
|
||||
gpio_pin_configure(bdev, 3, GPIO_INPUT); // P2.03 = BUSY
|
||||
int busy_before = gpio_pin_get(bdev, 3);
|
||||
printk("[nrf54l15] BUSY before first NRESET = %d%s\n", busy_before,
|
||||
busy_before ? " ← STUCK HIGH (chip damaged?)" : " ← LOW (chip OK)");
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
gpio_pin_t zpin;
|
||||
const struct device *dev = _gpio_dev_for_pin(pin, &zpin);
|
||||
if (!device_is_ready(dev)) {
|
||||
// Genuine hardware/DTS misconfiguration — keep this regardless of the
|
||||
// GPIO_DEBUG gate so it surfaces in production builds too.
|
||||
printk("[GPIO] pin%u dev NOT READY\n", (unsigned)pin);
|
||||
return;
|
||||
}
|
||||
gpio_pin_set(dev, zpin, (int)value);
|
||||
#ifdef NRF54L15_GPIO_DEBUG
|
||||
if ((pin == 37 || pin == 32) && _gpio_log_count < GPIO_LOG_MAX) {
|
||||
// Read back the pin state to confirm it actually changed
|
||||
int actual = gpio_pin_get(dev, zpin);
|
||||
printk("[GPIO] pin%u → %u (read-back=%d)\n", (unsigned)pin, (unsigned)value, actual);
|
||||
_gpio_log_count++;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
int digitalRead(uint32_t pin)
|
||||
{
|
||||
gpio_pin_t zpin;
|
||||
const struct device *dev = _gpio_dev_for_pin(pin, &zpin);
|
||||
if (!device_is_ready(dev))
|
||||
return 0;
|
||||
int v = gpio_pin_get(dev, zpin);
|
||||
#ifdef NRF54L15_GPIO_DEBUG
|
||||
// Log BUSY pin (35=P2.03) state changes + periodic updates for 10 seconds
|
||||
if (pin == 35) {
|
||||
static uint32_t busy_log_count = 0;
|
||||
static int last_busy = -1;
|
||||
static uint32_t first_read_ms = 0;
|
||||
if (first_read_ms == 0)
|
||||
first_read_ms = k_uptime_get_32();
|
||||
uint32_t elapsed_ms = k_uptime_get_32() - first_read_ms;
|
||||
|
||||
// Always log state changes
|
||||
if (v != last_busy) {
|
||||
printk("[BUSY] %ums: state changed %d → %d\n", (unsigned)elapsed_ms, last_busy, v);
|
||||
last_busy = v;
|
||||
}
|
||||
// Also log every 500ms for first 10 seconds so we can see timeline
|
||||
if (elapsed_ms < 10000 && busy_log_count < 20 && (elapsed_ms / 500) > (busy_log_count)) {
|
||||
printk("[BUSY] %ums: pin=%d (periodic)\n", (unsigned)elapsed_ms, v);
|
||||
busy_log_count = (elapsed_ms / 500) + 1;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return v;
|
||||
}
|
||||
|
||||
// ─── attachInterrupt — supports up to NRF54L15_MAX_IRQS pins ────────────────
|
||||
#define NRF54L15_MAX_IRQS 8
|
||||
|
||||
struct _PinIrq {
|
||||
struct gpio_callback cb;
|
||||
voidFuncPtr user_cb;
|
||||
const struct device *dev;
|
||||
gpio_pin_t zpin;
|
||||
bool used;
|
||||
};
|
||||
|
||||
static _PinIrq _irq_table[NRF54L15_MAX_IRQS];
|
||||
|
||||
static void _gpio_irq_dispatch(const struct device *dev, struct gpio_callback *cb, uint32_t pins)
|
||||
{
|
||||
_PinIrq *irq = CONTAINER_OF(cb, _PinIrq, cb);
|
||||
if (irq->user_cb)
|
||||
irq->user_cb();
|
||||
}
|
||||
|
||||
void attachInterrupt(uint32_t pin, voidFuncPtr cb, int mode)
|
||||
{
|
||||
gpio_pin_t zpin;
|
||||
const struct device *dev = _gpio_dev_for_pin(pin, &zpin);
|
||||
if (!device_is_ready(dev))
|
||||
return;
|
||||
|
||||
// Find a free slot (or reuse existing registration for same pin)
|
||||
_PinIrq *slot = nullptr;
|
||||
for (int i = 0; i < NRF54L15_MAX_IRQS; i++) {
|
||||
if (_irq_table[i].used && _irq_table[i].dev == dev && _irq_table[i].zpin == zpin) {
|
||||
// Re-register: remove old callback first
|
||||
gpio_remove_callback(dev, &_irq_table[i].cb);
|
||||
slot = &_irq_table[i];
|
||||
break;
|
||||
}
|
||||
if (!slot && !_irq_table[i].used)
|
||||
slot = &_irq_table[i];
|
||||
}
|
||||
if (!slot)
|
||||
return; // table full
|
||||
|
||||
gpio_flags_t irq_flags;
|
||||
switch (mode) {
|
||||
case RISING:
|
||||
irq_flags = GPIO_INT_EDGE_RISING;
|
||||
break;
|
||||
case FALLING:
|
||||
irq_flags = GPIO_INT_EDGE_FALLING;
|
||||
break;
|
||||
default:
|
||||
irq_flags = GPIO_INT_EDGE_BOTH;
|
||||
break;
|
||||
}
|
||||
|
||||
slot->user_cb = cb;
|
||||
slot->dev = dev;
|
||||
slot->zpin = zpin;
|
||||
slot->used = true;
|
||||
|
||||
gpio_pin_configure(dev, zpin, GPIO_INPUT);
|
||||
gpio_init_callback(&slot->cb, _gpio_irq_dispatch, BIT(zpin));
|
||||
gpio_add_callback(dev, &slot->cb);
|
||||
gpio_pin_interrupt_configure(dev, zpin, irq_flags);
|
||||
}
|
||||
|
||||
void detachInterrupt(uint32_t pin)
|
||||
{
|
||||
gpio_pin_t zpin;
|
||||
const struct device *dev = _gpio_dev_for_pin(pin, &zpin);
|
||||
for (int i = 0; i < NRF54L15_MAX_IRQS; i++) {
|
||||
if (_irq_table[i].used && _irq_table[i].dev == dev && _irq_table[i].zpin == zpin) {
|
||||
gpio_pin_interrupt_configure(dev, zpin, GPIO_INT_DISABLE);
|
||||
gpio_remove_callback(dev, &_irq_table[i].cb);
|
||||
_irq_table[i].used = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════════
|
||||
// SPI — Real Zephyr implementation using SPIM00 (HP domain, 3.0V)
|
||||
// CS is handled by RadioLib via digitalWrite() — hardware CS not used.
|
||||
// Mode 0 (CPOL=0, CPHA=0), MSB first.
|
||||
// ═════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
// Use SPIM00 (HP domain, 3.0V) — SPIM20 is 1.8V LP domain, incompatible with SX1262.
|
||||
// Lazy-init: DEVICE_DT_GET in global scope fails when the extern symbol is
|
||||
// not visible in this translation unit. Use a function-local static instead.
|
||||
static const struct device *_spi00(void)
|
||||
{
|
||||
static const struct device *dev = nullptr;
|
||||
if (!dev) {
|
||||
dev = DEVICE_DT_GET(DT_NODELABEL(spi00));
|
||||
if (!device_is_ready(dev)) {
|
||||
printk("[nrf54l15] spi00 NOT READY\n");
|
||||
dev = nullptr;
|
||||
} else {
|
||||
printk("[nrf54l15] spi00 ready\n");
|
||||
}
|
||||
}
|
||||
return dev;
|
||||
}
|
||||
|
||||
// SPI config: Mode 0, MSB first, no hardware CS (RadioLib does it manually)
|
||||
//
|
||||
// SPIM00 base clock = 128 MHz (nRF54L15 default when NRF_CONFIG_CPU_FREQ_MHZ
|
||||
// is not set; SystemInit() applies 128 MHz). Hardware prescaler must be EVEN
|
||||
// and in [4, 126] (SPIM00_PRESCALER_DIVISOR_RANGE_MIN/MAX from MDK).
|
||||
// 1 MHz → prescaler = 128 > 126 → NRFX_ERROR_INVALID_PARAM → -EIO on every
|
||||
// transfer. Minimum valid frequency is 2 MHz (prescaler = 64).
|
||||
static const struct spi_config _spi00_cfg = {
|
||||
.frequency = 2000000U, // 2 MHz — minimum valid for SPIM00 at 128 MHz base
|
||||
.operation = SPI_OP_MODE_MASTER | SPI_WORD_SET(8) | SPI_TRANSFER_MSB,
|
||||
.slave = 0,
|
||||
.cs = {}, // CS = NULL → RadioLib handles CS via GPIO
|
||||
};
|
||||
|
||||
// Static DMA buffers — stack-allocated bufs on nRF54L15 may not be reachable
|
||||
// by SPIM20 EasyDMA. Static placement in .bss/.data is always in Global SRAM.
|
||||
// rx_byte is pre-filled with 0xAA before every transfer so we can distinguish:
|
||||
// 0xAA → DMA never wrote (EasyDMA can't reach the buffer)
|
||||
// 0x00 → MISO actively driven LOW (chip in reset / bus fight)
|
||||
// 0xFF → MISO floating HIGH
|
||||
// other → real chip response
|
||||
static uint8_t _spi_tx_byte __attribute__((aligned(4)));
|
||||
static uint8_t _spi_rx_byte __attribute__((aligned(4)));
|
||||
|
||||
// Dump the first SPI_DUMP_N byte exchanges so we can see what MISO returns.
|
||||
#define SPI_DUMP_N 30
|
||||
static uint32_t _spi_dump_count = 0;
|
||||
|
||||
uint8_t SPIClass::transfer(uint8_t data)
|
||||
{
|
||||
const struct device *dev = _spi00();
|
||||
if (!dev)
|
||||
return 0xFF;
|
||||
|
||||
_spi_tx_byte = data;
|
||||
_spi_rx_byte = 0xAA; // sentinel: if DMA doesn't write, we return 0xAA
|
||||
|
||||
struct spi_buf tx_buf = {.buf = &_spi_tx_byte, .len = 1};
|
||||
struct spi_buf rx_buf = {.buf = &_spi_rx_byte, .len = 1};
|
||||
struct spi_buf_set tx_set = {.buffers = &tx_buf, .count = 1};
|
||||
struct spi_buf_set rx_set = {.buffers = &rx_buf, .count = 1};
|
||||
|
||||
static uint32_t spi_err_count = 0;
|
||||
int ret = spi_transceive(dev, &_spi00_cfg, &tx_set, &rx_set);
|
||||
if (ret != 0 && spi_err_count++ < 3)
|
||||
printk("[SPI] err=%d tx=0x%02x\n", ret, data);
|
||||
|
||||
if (_spi_dump_count < SPI_DUMP_N) {
|
||||
printk("[SPI] #%u tx=0x%02x rx=0x%02x\n", (unsigned)_spi_dump_count, data, _spi_rx_byte);
|
||||
_spi_dump_count++;
|
||||
}
|
||||
|
||||
return _spi_rx_byte;
|
||||
}
|
||||
|
||||
// Static DMA-safe buffers for transfer16 — same EasyDMA reachability concern
|
||||
// applies as for the byte path: stack buffers from a caller thread may sit in
|
||||
// per-thread RAM regions that EasyDMA cannot reach.
|
||||
static uint8_t _spi_tx16[2] __attribute__((aligned(4)));
|
||||
static uint8_t _spi_rx16[2] __attribute__((aligned(4)));
|
||||
|
||||
uint16_t SPIClass::transfer16(uint16_t data)
|
||||
{
|
||||
const struct device *dev = _spi00();
|
||||
if (!dev)
|
||||
return 0xFFFF;
|
||||
|
||||
_spi_tx16[0] = (uint8_t)(data >> 8);
|
||||
_spi_tx16[1] = (uint8_t)(data & 0xFF);
|
||||
_spi_rx16[0] = 0xAA;
|
||||
_spi_rx16[1] = 0xAA;
|
||||
struct spi_buf tx_buf = {.buf = _spi_tx16, .len = 2};
|
||||
struct spi_buf rx_buf = {.buf = _spi_rx16, .len = 2};
|
||||
struct spi_buf_set tx_set = {.buffers = &tx_buf, .count = 1};
|
||||
struct spi_buf_set rx_set = {.buffers = &rx_buf, .count = 1};
|
||||
spi_transceive(dev, &_spi00_cfg, &tx_set, &rx_set);
|
||||
return ((uint16_t)_spi_rx16[0] << 8) | _spi_rx16[1];
|
||||
}
|
||||
|
||||
void SPIClass::transferBytes(const uint8_t *tx, uint8_t *rx, uint32_t count)
|
||||
{
|
||||
if (!count)
|
||||
return;
|
||||
const struct device *dev = _spi00();
|
||||
if (!dev)
|
||||
return;
|
||||
// Zephyr requires non-const buf pointer; cast is safe for tx-only direction
|
||||
struct spi_buf tx_buf = {.buf = const_cast<uint8_t *>(tx), .len = count};
|
||||
struct spi_buf rx_buf = {.buf = rx, .len = count};
|
||||
struct spi_buf_set tx_set = {.buffers = &tx_buf, .count = 1};
|
||||
struct spi_buf_set rx_set = {.buffers = rx_buf.buf ? &rx_buf : nullptr, .count = rx_buf.buf ? 1U : 0U};
|
||||
spi_transceive(dev, &_spi00_cfg, &tx_set, rx ? &rx_set : nullptr);
|
||||
}
|
||||
|
||||
void SPIClass::transfer(void *buf, size_t count)
|
||||
{
|
||||
if (!count || !buf)
|
||||
return;
|
||||
transferBytes(reinterpret_cast<const uint8_t *>(buf), reinterpret_cast<uint8_t *>(buf), (uint32_t)count);
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* nrf54l15_main.cpp — Zephyr entry point for Meshtastic nRF54L15 port
|
||||
*
|
||||
* Zephyr calls main() instead of Arduino's setup()/loop().
|
||||
* This file provides the main() that bootstraps the Arduino-style
|
||||
* Meshtastic application loop.
|
||||
*/
|
||||
|
||||
#include <zephyr/drivers/hwinfo.h>
|
||||
#include <zephyr/fatal.h>
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/sys/reboot.h>
|
||||
|
||||
// Forward declarations from src/main.cpp
|
||||
void setup();
|
||||
void loop();
|
||||
|
||||
// ── Crash info saved to noinit RAM (survives soft reset) ─────────────────────
|
||||
// Zephyr's arch_esf does not expose the faulting SP directly; we capture PSP
|
||||
// at entry to the fatal handler (the exception-basic frame lives there) and
|
||||
// store xPSR alongside PC/LR for context.
|
||||
struct crash_info {
|
||||
uint32_t magic;
|
||||
uint32_t reason;
|
||||
uint32_t pc;
|
||||
uint32_t psp; // stack pointer captured at fault entry
|
||||
uint32_t xpsr; // saved program status (flags + exception number)
|
||||
uint32_t lr;
|
||||
uint32_t cfsr; // Configurable Fault Status Register
|
||||
};
|
||||
static struct crash_info saved_crash __attribute__((section(".noinit")));
|
||||
#define CRASH_MAGIC 0xDEADBEEF
|
||||
|
||||
// Override Zephyr's weak fatal handler to save crash info, then cold-reboot so
|
||||
// main() can report the saved record on the next boot. We don't rely on
|
||||
// CONFIG_RESET_ON_FATAL_ERROR (default off → k_fatal_halt would spin forever)
|
||||
// — we issue sys_reboot() ourselves after flushing logs.
|
||||
extern "C" void k_sys_fatal_error_handler(unsigned int reason, const struct arch_esf *esf)
|
||||
{
|
||||
saved_crash.magic = CRASH_MAGIC;
|
||||
saved_crash.reason = reason;
|
||||
// Capture the faulting thread's stack pointer before we start using the
|
||||
// handler's own stack for logging.
|
||||
uint32_t psp_at_entry;
|
||||
__asm__ volatile("mrs %0, psp" : "=r"(psp_at_entry));
|
||||
saved_crash.psp = psp_at_entry;
|
||||
if (esf) {
|
||||
saved_crash.pc = esf->basic.pc;
|
||||
saved_crash.xpsr = esf->basic.xpsr;
|
||||
saved_crash.lr = esf->basic.lr;
|
||||
}
|
||||
// Read Cortex-M33 SCB CFSR
|
||||
saved_crash.cfsr = *((volatile uint32_t *)0xE000ED28U);
|
||||
printk("[nrf54l15] FATAL reason=%u pc=0x%08x lr=0x%08x cfsr=0x%08x\n", reason, saved_crash.pc, saved_crash.lr,
|
||||
saved_crash.cfsr);
|
||||
|
||||
// Walk the failing thread's stack and print any word that looks like a
|
||||
// Thumb code address (0x1000 — flash end, with the Thumb-mode low bit set).
|
||||
// The Cortex-M exception frame at PSP holds r0,r1,r2,r3,r12,lr,pc,xpsr
|
||||
// (8 words); deeper words are the caller's saved frame, which gives a
|
||||
// crude but useful poor-man's backtrace when CONFIG_DEBUG_COREDUMP is off.
|
||||
// Found the BLE-init bad_alloc → abort() chain (heap exhaustion under
|
||||
// CONFIG_BT_BUF_ACL_RX_SIZE=251) when the fault dump alone showed only
|
||||
// abort itself. Cheap (~150 B of code) and silent until a fault.
|
||||
uint32_t psp;
|
||||
__asm__ volatile("mrs %0, psp" : "=r"(psp));
|
||||
printk("[nrf54l15] PSP=0x%08x — stack walk:\n", psp);
|
||||
// Validate PSP before dereferencing. Real faults frequently leave PSP
|
||||
// pointing at corrupted/unmapped memory, and walking it blindly triggers a
|
||||
// second fault inside this handler. Restrict to nRF54L15 SRAM (256 KB at
|
||||
// 0x20000000) with 4-byte alignment, and clamp the walk so we never read
|
||||
// past the end of RAM.
|
||||
const uintptr_t SRAM_START = 0x20000000UL;
|
||||
const uintptr_t SRAM_END = 0x20040000UL;
|
||||
if (psp < SRAM_START || psp >= SRAM_END || (psp & 0x3U) != 0) {
|
||||
printk("[nrf54l15] PSP out of SRAM range or unaligned, skipping walk\n");
|
||||
} else {
|
||||
const uint32_t *sp = (const uint32_t *)psp;
|
||||
int max_words = (int)((SRAM_END - psp) / sizeof(uint32_t));
|
||||
if (max_words > 96)
|
||||
max_words = 96;
|
||||
for (int i = 0; i < max_words; i++) {
|
||||
uint32_t v = sp[i];
|
||||
if (v >= 0x00001000 && v < 0x00080000 && (v & 1)) {
|
||||
printk("[nrf54l15] sp[%d]=0x%08x (code)\n", i, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Give the RTT/printk backend a chance to drain before we reset, otherwise
|
||||
// the crash log line above is lost and the next boot's "Prev crash" line is
|
||||
// the only forensic evidence we get.
|
||||
k_busy_wait(50000); // 50 ms
|
||||
sys_reboot(SYS_REBOOT_COLD);
|
||||
// Unreachable; k_fatal_halt as a defensive backstop in case sys_reboot
|
||||
// returns (it shouldn't).
|
||||
k_fatal_halt(reason);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
uint32_t reset_cause = 0;
|
||||
hwinfo_get_reset_cause(&reset_cause);
|
||||
hwinfo_clear_reset_cause();
|
||||
printk("[nrf54l15] Reset cause: 0x%08x\n", reset_cause);
|
||||
|
||||
if (saved_crash.magic == CRASH_MAGIC) {
|
||||
printk("[nrf54l15] Prev crash: reason=%u pc=0x%08x lr=0x%08x psp=0x%08x xpsr=0x%08x cfsr=0x%08x\n", saved_crash.reason,
|
||||
saved_crash.pc, saved_crash.lr, saved_crash.psp, saved_crash.xpsr, saved_crash.cfsr);
|
||||
saved_crash.magic = 0;
|
||||
}
|
||||
|
||||
printk("[nrf54l15] A: main() entry\n");
|
||||
printk("[nrf54l15] B: calling setup()\n");
|
||||
setup();
|
||||
printk("[nrf54l15] C: setup() returned\n");
|
||||
while (true) {
|
||||
loop();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// utility/bonding.h — stub for nRF54L15/Zephyr
|
||||
// NodeDB.cpp includes this when ARCH_NRF52 is defined.
|
||||
// Bluetooth is excluded; this stub satisfies the include chain.
|
||||
#pragma once
|
||||
|
||||
// BLE role constants (from Bluefruit SDK)
|
||||
#define BLE_GAP_ROLE_PERIPH 0x01
|
||||
#define BLE_GAP_ROLE_CENTRAL 0x02
|
||||
|
||||
// Stub for bond_print_list()
|
||||
static inline void bond_print_list(uint8_t) {}
|
||||
@@ -291,7 +291,7 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
|
||||
|
||||
auto addToRoute = [](JsonArray *route, NodeNum num) {
|
||||
char long_name[40] = "Unknown";
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(num);
|
||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(num);
|
||||
bool name_known = nodeInfoLiteHasUser(node);
|
||||
if (name_known) {
|
||||
const size_t copy_len =
|
||||
|
||||
+12
-2
@@ -119,18 +119,24 @@ void XModemAdapter::handlePacket(meshtastic_XModem xmodemPacket)
|
||||
case meshtastic_XModem_Control_STX:
|
||||
if ((xmodemPacket.seq == 0) && !isReceiving && !isTransmitting) {
|
||||
// NULL packet has the destination filename
|
||||
memcpy(filename, &xmodemPacket.buffer.bytes, xmodemPacket.buffer.size);
|
||||
strncpy(filename, (const char *)xmodemPacket.buffer.bytes, sizeof(filename) - 1);
|
||||
filename[sizeof(filename) - 1] = '\0';
|
||||
|
||||
if (xmodemPacket.control == meshtastic_XModem_Control_SOH) { // Receive this file and put to Flash
|
||||
// FILE_O_WRITE on Adafruit_LittleFS is append, not truncate — remove first.
|
||||
spiLock->lock();
|
||||
if (FSCom.exists(filename))
|
||||
FSCom.remove(filename);
|
||||
file = FSCom.open(filename, FILE_O_WRITE);
|
||||
spiLock->unlock();
|
||||
if (file) {
|
||||
LOG_INFO("XModem: receiving %s", filename);
|
||||
sendControl(meshtastic_XModem_Control_ACK);
|
||||
isReceiving = true;
|
||||
packetno = 1;
|
||||
break;
|
||||
}
|
||||
LOG_WARN("XModem: open(%s, WRITE) failed", filename);
|
||||
sendControl(meshtastic_XModem_Control_NAK);
|
||||
isReceiving = false;
|
||||
break;
|
||||
@@ -168,8 +174,12 @@ void XModemAdapter::handlePacket(meshtastic_XModem xmodemPacket)
|
||||
check(xmodemPacket.buffer.bytes, xmodemPacket.buffer.size, xmodemPacket.crc16)) {
|
||||
// valid packet
|
||||
spiLock->lock();
|
||||
file.write(xmodemPacket.buffer.bytes, xmodemPacket.buffer.size);
|
||||
size_t written = file.write(xmodemPacket.buffer.bytes, xmodemPacket.buffer.size);
|
||||
spiLock->unlock();
|
||||
if (written != xmodemPacket.buffer.size) {
|
||||
LOG_WARN("XModem: short write seq=%d expected=%d wrote=%d (LittleFS partition full?)",
|
||||
(int)xmodemPacket.seq, (int)xmodemPacket.buffer.size, (int)written);
|
||||
}
|
||||
sendControl(meshtastic_XModem_Control_ACK);
|
||||
packetno++;
|
||||
break;
|
||||
|
||||
@@ -52,6 +52,9 @@ class XModemAdapter
|
||||
meshtastic_XModem getForPhone();
|
||||
void resetForPhone();
|
||||
|
||||
// True while a file transfer is in flight; lets callers avoid racing our `file` handle.
|
||||
bool isBusy() const { return isReceiving || isTransmitting; }
|
||||
|
||||
private:
|
||||
bool isReceiving = false;
|
||||
bool isTransmitting = false;
|
||||
|
||||
Reference in New Issue
Block a user