mirror of
https://github.com/meshtastic/firmware.git
synced 2026-01-19 08:17:36 +00:00
Merge remote-tracking branch 'remotes/origin/master' into arduino-esp32-v3.2
# Conflicts: # arch/esp32/esp32.ini # src/graphics/draw/MenuHandler.cpp # src/modules/Modules.cpp # variants/esp32s3/seeed-sensecap-indicator/platformio.ini
This commit is contained in:
@@ -43,6 +43,10 @@
|
||||
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C
|
||||
#include "motion/AccelerometerThread.h"
|
||||
#endif
|
||||
#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \
|
||||
!defined(CONFIG_IDF_TARGET_ESP32C3)
|
||||
#include "SerialModule.h"
|
||||
#endif
|
||||
|
||||
AdminModule *adminModule;
|
||||
bool hasOpenEditTransaction;
|
||||
@@ -596,7 +600,6 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c)
|
||||
if (config.device.button_gpio == c.payload_variant.device.button_gpio &&
|
||||
config.device.buzzer_gpio == c.payload_variant.device.buzzer_gpio &&
|
||||
config.device.role == c.payload_variant.device.role &&
|
||||
config.device.disable_triple_click == c.payload_variant.device.disable_triple_click &&
|
||||
config.device.rebroadcast_mode == c.payload_variant.device.rebroadcast_mode) {
|
||||
requiresReboot = false;
|
||||
}
|
||||
@@ -630,6 +633,7 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c)
|
||||
#if USERPREFS_EVENT_MODE
|
||||
// If we're in event mode, nobody is a Router or Repeater
|
||||
if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER ||
|
||||
config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE ||
|
||||
config.device.role == meshtastic_Config_DeviceConfig_Role_REPEATER) {
|
||||
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT;
|
||||
}
|
||||
@@ -638,7 +642,16 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c)
|
||||
case meshtastic_Config_position_tag:
|
||||
LOG_INFO("Set config: Position");
|
||||
config.has_position = true;
|
||||
// If we have turned off the GPS (disabled or not present) and we're not using fixed position,
|
||||
// clear the stored position since it may not get updated
|
||||
if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED &&
|
||||
c.payload_variant.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED &&
|
||||
config.position.fixed_position == false && c.payload_variant.position.fixed_position == false) {
|
||||
nodeDB->clearLocalPosition();
|
||||
saveChanges(SEGMENT_NODEDATABASE | SEGMENT_CONFIG, false);
|
||||
}
|
||||
config.position = c.payload_variant.position;
|
||||
|
||||
// Save nodedb as well in case we got a fixed position packet
|
||||
break;
|
||||
case meshtastic_Config_power_tag:
|
||||
@@ -798,8 +811,13 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c)
|
||||
|
||||
bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c)
|
||||
{
|
||||
if (!hasOpenEditTransaction)
|
||||
// If we are in an open transaction or configuring MQTT or Serial (which have validation), defer disabling Bluetooth
|
||||
// Otherwise, disable Bluetooth to prevent the phone from interfering with the config
|
||||
if (!hasOpenEditTransaction &&
|
||||
!IS_ONE_OF(c.which_payload_variant, meshtastic_ModuleConfig_mqtt_tag, meshtastic_ModuleConfig_serial_tag)) {
|
||||
disableBluetooth();
|
||||
}
|
||||
|
||||
switch (c.which_payload_variant) {
|
||||
case meshtastic_ModuleConfig_mqtt_tag:
|
||||
#if MESHTASTIC_EXCLUDE_MQTT
|
||||
@@ -810,12 +828,22 @@ bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c)
|
||||
if (!MQTT::isValidConfig(c.payload_variant.mqtt)) {
|
||||
return false;
|
||||
}
|
||||
// Disable Bluetooth to prevent interference during MQTT configuration
|
||||
disableBluetooth();
|
||||
moduleConfig.has_mqtt = true;
|
||||
moduleConfig.mqtt = c.payload_variant.mqtt;
|
||||
#endif
|
||||
break;
|
||||
case meshtastic_ModuleConfig_serial_tag:
|
||||
LOG_INFO("Set module config: Serial");
|
||||
#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \
|
||||
!defined(CONFIG_IDF_TARGET_ESP32C3)
|
||||
if (!SerialModule::isValidConfig(c.payload_variant.serial)) {
|
||||
LOG_ERROR("Invalid serial config");
|
||||
return false;
|
||||
}
|
||||
disableBluetooth(); // Disable Bluetooth to prevent interference during Serial configuration
|
||||
#endif
|
||||
moduleConfig.has_serial = true;
|
||||
moduleConfig.serial = c.payload_variant.serial;
|
||||
break;
|
||||
@@ -971,9 +999,10 @@ void AdminModule::handleGetConfig(const meshtastic_MeshPacket &req, const uint32
|
||||
// So even if we internally use 0 to represent 'use default' we still need to send the value we are
|
||||
// using to the app (so that even old phone apps work with new device loads).
|
||||
// r.get_radio_response.preferences.ls_secs = getPref_ls_secs();
|
||||
// hideSecret(r.get_radio_response.preferences.wifi_ssid); // hmm - leave public for now, because only minimally private
|
||||
// and useful for users to know current provisioning) hideSecret(r.get_radio_response.preferences.wifi_password);
|
||||
// r.get_config_response.which_payloadVariant = Config_ModuleConfig_telemetry_tag;
|
||||
// hideSecret(r.get_radio_response.preferences.wifi_ssid); // hmm - leave public for now, because only minimally
|
||||
// private and useful for users to know current provisioning)
|
||||
// hideSecret(r.get_radio_response.preferences.wifi_password); r.get_config_response.which_payloadVariant =
|
||||
// Config_ModuleConfig_telemetry_tag;
|
||||
res.which_payload_variant = meshtastic_AdminMessage_get_config_response_tag;
|
||||
setPassKey(&res);
|
||||
myReply = allocDataProtobuf(res);
|
||||
@@ -1057,9 +1086,10 @@ void AdminModule::handleGetModuleConfig(const meshtastic_MeshPacket &req, const
|
||||
// So even if we internally use 0 to represent 'use default' we still need to send the value we are
|
||||
// using to the app (so that even old phone apps work with new device loads).
|
||||
// r.get_radio_response.preferences.ls_secs = getPref_ls_secs();
|
||||
// hideSecret(r.get_radio_response.preferences.wifi_ssid); // hmm - leave public for now, because only minimally private
|
||||
// and useful for users to know current provisioning) hideSecret(r.get_radio_response.preferences.wifi_password);
|
||||
// r.get_config_response.which_payloadVariant = Config_ModuleConfig_telemetry_tag;
|
||||
// hideSecret(r.get_radio_response.preferences.wifi_ssid); // hmm - leave public for now, because only minimally
|
||||
// private and useful for users to know current provisioning)
|
||||
// hideSecret(r.get_radio_response.preferences.wifi_password); r.get_config_response.which_payloadVariant =
|
||||
// Config_ModuleConfig_telemetry_tag;
|
||||
res.which_payload_variant = meshtastic_AdminMessage_get_module_config_response_tag;
|
||||
setPassKey(&res);
|
||||
myReply = allocDataProtobuf(res);
|
||||
@@ -1190,7 +1220,7 @@ void AdminModule::reboot(int32_t seconds)
|
||||
{
|
||||
LOG_INFO("Reboot in %d seconds", seconds);
|
||||
if (screen)
|
||||
screen->showOverlayBanner("Rebooting...", 0); // stays on screen
|
||||
screen->showSimpleBanner("Rebooting...", 0); // stays on screen
|
||||
rebootAtMsec = (seconds < 0) ? 0 : (millis() + seconds * 1000);
|
||||
}
|
||||
|
||||
@@ -1326,12 +1356,6 @@ void AdminModule::handleSendInputEvent(const meshtastic_AdminMessage_InputEvent
|
||||
LOG_DEBUG("Processing input event: event_code=%u, kb_char=%u, touch_x=%u, touch_y=%u", inputEvent.event_code,
|
||||
inputEvent.kb_char, inputEvent.touch_x, inputEvent.touch_y);
|
||||
|
||||
// Validate input parameters
|
||||
if (inputEvent.event_code > INPUT_BROKER_ANYKEY) {
|
||||
LOG_WARN("Invalid input event code: %u", inputEvent.event_code);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create InputEvent for injection
|
||||
InputEvent event = {.inputEvent = (input_broker_event)inputEvent.event_code,
|
||||
.kbchar = (unsigned char)inputEvent.kb_char,
|
||||
|
||||
@@ -56,6 +56,7 @@ CannedMessageModule::CannedMessageModule()
|
||||
disable();
|
||||
} else {
|
||||
LOG_INFO("CannedMessageModule is enabled");
|
||||
moduleConfig.canned_message.enabled = true;
|
||||
this->inputObserver.observe(inputBroker);
|
||||
}
|
||||
}
|
||||
@@ -442,25 +443,34 @@ int CannedMessageModule::handleDestinationSelectionInput(const InputEvent *event
|
||||
return 1;
|
||||
}
|
||||
|
||||
// UP
|
||||
if (isUp && destIndex > 0) {
|
||||
destIndex--;
|
||||
if (isUp) {
|
||||
if (destIndex > 0) {
|
||||
destIndex--;
|
||||
} else if (totalEntries > 0) {
|
||||
destIndex = totalEntries - 1;
|
||||
}
|
||||
|
||||
if ((destIndex / columns) < scrollIndex)
|
||||
scrollIndex = destIndex / columns;
|
||||
else if ((destIndex / columns) >= (scrollIndex + visibleRows))
|
||||
scrollIndex = (destIndex / columns) - visibleRows + 1;
|
||||
|
||||
screen->forceDisplay();
|
||||
screen->forceDisplay(true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// DOWN
|
||||
if (isDown && destIndex + 1 < totalEntries) {
|
||||
destIndex++;
|
||||
if (isDown) {
|
||||
if (destIndex + 1 < totalEntries) {
|
||||
destIndex++;
|
||||
} else if (totalEntries > 0) {
|
||||
destIndex = 0;
|
||||
scrollIndex = 0;
|
||||
}
|
||||
|
||||
if ((destIndex / columns) >= (scrollIndex + visibleRows))
|
||||
scrollIndex = (destIndex / columns) - visibleRows + 1;
|
||||
|
||||
screen->forceDisplay();
|
||||
screen->forceDisplay(true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -482,7 +492,7 @@ int CannedMessageModule::handleDestinationSelectionInput(const InputEvent *event
|
||||
|
||||
runState = returnToCannedList ? CANNED_MESSAGE_RUN_STATE_ACTIVE : CANNED_MESSAGE_RUN_STATE_FREETEXT;
|
||||
returnToCannedList = false;
|
||||
screen->forceDisplay();
|
||||
screen->forceDisplay(true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -495,7 +505,7 @@ int CannedMessageModule::handleDestinationSelectionInput(const InputEvent *event
|
||||
// UIFrameEvent e;
|
||||
// e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
||||
// notifyObservers(&e);
|
||||
screen->forceDisplay();
|
||||
screen->forceDisplay(true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -707,7 +717,7 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent *event)
|
||||
}
|
||||
|
||||
// Backspace
|
||||
if (event->inputEvent == INPUT_BROKER_BACK) {
|
||||
if (event->inputEvent == INPUT_BROKER_BACK && this->freetext.length() > 0) {
|
||||
payload = 0x08;
|
||||
lastTouchMillis = millis();
|
||||
runOnce();
|
||||
@@ -730,7 +740,8 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent *event)
|
||||
}
|
||||
|
||||
// Cancel (dismiss freetext screen)
|
||||
if (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG) {
|
||||
if (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG ||
|
||||
(event->inputEvent == INPUT_BROKER_BACK && this->freetext.length() == 0)) {
|
||||
runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
||||
freetext = "";
|
||||
cursor = 0;
|
||||
@@ -840,7 +851,13 @@ void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const cha
|
||||
this->waitingForAck = true;
|
||||
|
||||
// Log outgoing message
|
||||
LOG_INFO("Send message id=%d, dest=%x, msg=%.*s", p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes);
|
||||
LOG_INFO("Send message id=%u, dest=%x, msg=%.*s", p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes);
|
||||
|
||||
if (p->to != 0xffffffff) {
|
||||
LOG_INFO("Proactively adding %x as favorite node", p->to);
|
||||
nodeDB->set_favorite(true, p->to);
|
||||
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
|
||||
}
|
||||
|
||||
// Send to mesh and phone (even if no phone connected, to track ACKs)
|
||||
service->sendToMesh(p, RX_SRC_LOCAL, true);
|
||||
@@ -980,6 +997,7 @@ int32_t CannedMessageModule::runOnce()
|
||||
}
|
||||
this->cursor--;
|
||||
}
|
||||
} else {
|
||||
}
|
||||
break;
|
||||
case INPUT_BROKER_MSG_TAB: // Tab key: handled by input handler
|
||||
@@ -1430,7 +1448,7 @@ void CannedMessageModule::drawEmotePickerScreen(OLEDDisplay *display, OLEDDispla
|
||||
int headerY = y;
|
||||
int listTop = headerY + headerFontHeight + headerMargin;
|
||||
|
||||
int visibleRows = (display->getHeight() - listTop - 2) / rowHeight;
|
||||
int _visibleRows = (display->getHeight() - listTop - 2) / rowHeight;
|
||||
int numEmotes = graphics::numEmotes;
|
||||
|
||||
// Clamp highlight index
|
||||
@@ -1440,11 +1458,11 @@ void CannedMessageModule::drawEmotePickerScreen(OLEDDisplay *display, OLEDDispla
|
||||
emotePickerIndex = numEmotes - 1;
|
||||
|
||||
// Determine which emote is at the top
|
||||
int topIndex = emotePickerIndex - visibleRows / 2;
|
||||
int topIndex = emotePickerIndex - _visibleRows / 2;
|
||||
if (topIndex < 0)
|
||||
topIndex = 0;
|
||||
if (topIndex > numEmotes - visibleRows)
|
||||
topIndex = std::max(0, numEmotes - visibleRows);
|
||||
if (topIndex > numEmotes - _visibleRows)
|
||||
topIndex = std::max(0, numEmotes - _visibleRows);
|
||||
|
||||
// Draw header/title
|
||||
display->setFont(FONT_SMALL);
|
||||
@@ -1692,7 +1710,7 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
|
||||
} else {
|
||||
// Text: split by words and wrap inside word if needed
|
||||
String text = token.second;
|
||||
uint16_t pos = 0;
|
||||
pos = 0;
|
||||
while (pos < text.length()) {
|
||||
// Find next space (or end)
|
||||
int spacePos = text.indexOf(' ', pos);
|
||||
@@ -1736,7 +1754,7 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
|
||||
int yLine = inputY;
|
||||
for (auto &line : lines) {
|
||||
int nextX = x;
|
||||
for (auto &token : line) {
|
||||
for (const auto &token : line) {
|
||||
if (token.first) {
|
||||
const graphics::Emote *emote = nullptr;
|
||||
for (int j = 0; j < graphics::numEmotes; j++) {
|
||||
@@ -1772,19 +1790,20 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
|
||||
|
||||
int topMsg;
|
||||
std::vector<int> rowHeights;
|
||||
int visibleRows;
|
||||
int _visibleRows;
|
||||
|
||||
// Draw header (To: ...)
|
||||
drawHeader(display, x, y, buffer);
|
||||
|
||||
// Shift message list upward by 3 pixels to reduce spacing between header and first message
|
||||
const int listYOffset = y + FONT_HEIGHT_SMALL - 3;
|
||||
visibleRows = (display->getHeight() - listYOffset) / baseRowSpacing;
|
||||
_visibleRows = (display->getHeight() - listYOffset) / baseRowSpacing;
|
||||
|
||||
// Figure out which messages are visible and their needed heights
|
||||
topMsg =
|
||||
(messagesCount > visibleRows && currentMessageIndex >= visibleRows - 1) ? currentMessageIndex - visibleRows + 2 : 0;
|
||||
int countRows = std::min(messagesCount, visibleRows);
|
||||
topMsg = (messagesCount > _visibleRows && currentMessageIndex >= _visibleRows - 1)
|
||||
? currentMessageIndex - _visibleRows + 2
|
||||
: 0;
|
||||
int countRows = std::min(messagesCount, _visibleRows);
|
||||
|
||||
// --- Build per-row max height based on all emotes in line ---
|
||||
for (int i = 0; i < countRows; i++) {
|
||||
@@ -1811,7 +1830,7 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
|
||||
int lineY = yCursor;
|
||||
const char *msg = getMessageByIndex(msgIdx);
|
||||
int rowHeight = rowHeights[vis];
|
||||
bool highlight = (msgIdx == currentMessageIndex);
|
||||
bool _highlight = (msgIdx == currentMessageIndex);
|
||||
|
||||
// --- Multi-emote tokenization ---
|
||||
std::vector<std::pair<bool, String>> tokens; // (isEmote, token)
|
||||
@@ -1864,20 +1883,20 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
|
||||
int textYOffset = (rowHeight - FONT_HEIGHT_SMALL) / 2;
|
||||
|
||||
#ifdef USE_EINK
|
||||
int nextX = x + (highlight ? 12 : 0);
|
||||
if (highlight)
|
||||
int nextX = x + (_highlight ? 12 : 0);
|
||||
if (_highlight)
|
||||
display->drawString(x + 0, lineY + textYOffset, ">");
|
||||
#else
|
||||
int scrollPadding = 8;
|
||||
if (highlight) {
|
||||
if (_highlight) {
|
||||
display->fillRect(x + 0, lineY, display->getWidth() - scrollPadding, rowHeight);
|
||||
display->setColor(BLACK);
|
||||
}
|
||||
int nextX = x + (highlight ? 2 : 0);
|
||||
int nextX = x + (_highlight ? 2 : 0);
|
||||
#endif
|
||||
|
||||
// Draw all tokens left to right
|
||||
for (auto &token : tokens) {
|
||||
for (const auto &token : tokens) {
|
||||
if (token.first) {
|
||||
// Emote
|
||||
const graphics::Emote *emote = nullptr;
|
||||
@@ -1899,7 +1918,7 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
|
||||
}
|
||||
}
|
||||
#ifndef USE_EINK
|
||||
if (highlight)
|
||||
if (_highlight)
|
||||
display->setColor(WHITE);
|
||||
#endif
|
||||
|
||||
@@ -1907,11 +1926,11 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
|
||||
}
|
||||
|
||||
// Scrollbar
|
||||
if (messagesCount > visibleRows) {
|
||||
if (messagesCount > _visibleRows) {
|
||||
int scrollHeight = display->getHeight() - listYOffset;
|
||||
int scrollTrackX = display->getWidth() - 6;
|
||||
display->drawRect(scrollTrackX, listYOffset, 4, scrollHeight);
|
||||
int barHeight = (scrollHeight * visibleRows) / messagesCount;
|
||||
int barHeight = (scrollHeight * _visibleRows) / messagesCount;
|
||||
int scrollPos = listYOffset + (scrollHeight * topMsg) / messagesCount;
|
||||
display->fillRect(scrollTrackX, scrollPos, 4, barHeight);
|
||||
}
|
||||
@@ -2057,6 +2076,9 @@ void CannedMessageModule::handleSetCannedMessageModuleMessages(const char *from_
|
||||
|
||||
if (changed) {
|
||||
this->saveProtoForModule();
|
||||
if (splitConfiguredMessages()) {
|
||||
moduleConfig.canned_message.enabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2066,4 +2088,4 @@ String CannedMessageModule::drawWithCursor(String text, int cursor)
|
||||
return result;
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -126,9 +126,11 @@ int32_t ExternalNotificationModule::runOnce()
|
||||
millis()) {
|
||||
setExternalState(1, !getExternal(1));
|
||||
}
|
||||
if (externalTurnedOn[2] + (moduleConfig.external_notification.output_ms ? moduleConfig.external_notification.output_ms
|
||||
// Only toggle buzzer output if not using PWM mode (to avoid conflict with RTTTL)
|
||||
if (!moduleConfig.external_notification.use_pwm &&
|
||||
externalTurnedOn[2] + (moduleConfig.external_notification.output_ms ? moduleConfig.external_notification.output_ms
|
||||
: EXT_NOTIFICATION_MODULE_OUTPUT_MS) <
|
||||
millis()) {
|
||||
millis()) {
|
||||
LOG_DEBUG("EXTERNAL 2 %d compared to %d", externalTurnedOn[2] + moduleConfig.external_notification.output_ms,
|
||||
millis());
|
||||
setExternalState(2, !getExternal(2));
|
||||
@@ -247,7 +249,8 @@ void ExternalNotificationModule::setExternalState(uint8_t index, bool on)
|
||||
digitalWrite(moduleConfig.external_notification.output_vibra, on);
|
||||
break;
|
||||
case 2:
|
||||
if (moduleConfig.external_notification.output_buzzer)
|
||||
// Only control buzzer pin digitally if not using PWM mode
|
||||
if (moduleConfig.external_notification.output_buzzer && !moduleConfig.external_notification.use_pwm)
|
||||
digitalWrite(moduleConfig.external_notification.output_buzzer, on);
|
||||
break;
|
||||
default:
|
||||
@@ -320,6 +323,11 @@ void ExternalNotificationModule::stopNow()
|
||||
#endif
|
||||
nagCycleCutoff = 1; // small value
|
||||
isNagging = false;
|
||||
// Turn off all outputs
|
||||
for (int i = 0; i < 3; i++) {
|
||||
setExternalState(i, false);
|
||||
externalTurnedOn[i] = 0;
|
||||
}
|
||||
setIntervalFromNow(0);
|
||||
#ifdef T_WATCH_S3
|
||||
drv.stop();
|
||||
@@ -362,9 +370,8 @@ ExternalNotificationModule::ExternalNotificationModule()
|
||||
if (nodeDB->loadProto(rtttlConfigFile, meshtastic_RTTTLConfig_size, sizeof(meshtastic_RTTTLConfig),
|
||||
&meshtastic_RTTTLConfig_msg, &rtttlConfig) != LoadFileResult::LOAD_SUCCESS) {
|
||||
memset(rtttlConfig.ringtone, 0, sizeof(rtttlConfig.ringtone));
|
||||
strncpy(rtttlConfig.ringtone,
|
||||
"24:d=32,o=5,b=565:f6,p,f6,4p,p,f6,p,f6,2p,p,b6,p,b6,p,b6,p,b6,p,b,p,b,p,b,p,b,p,b,p,b,p,b,p,b,1p.,2p.,p",
|
||||
sizeof(rtttlConfig.ringtone));
|
||||
// The default ringtone is always loaded from userPrefs.jsonc
|
||||
strncpy(rtttlConfig.ringtone, USERPREFS_RINGTONE_RTTTL, sizeof(rtttlConfig.ringtone));
|
||||
}
|
||||
|
||||
LOG_INFO("Init External Notification Module");
|
||||
@@ -479,14 +486,17 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP
|
||||
if (containsBell) {
|
||||
LOG_INFO("externalNotificationModule - Notification Bell (Buzzer)");
|
||||
isNagging = true;
|
||||
if (!moduleConfig.external_notification.use_pwm) {
|
||||
if (!moduleConfig.external_notification.use_pwm && !moduleConfig.external_notification.use_i2s_as_buzzer) {
|
||||
setExternalState(2, true);
|
||||
} else {
|
||||
#ifdef HAS_I2S
|
||||
audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone));
|
||||
#else
|
||||
rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone);
|
||||
if (moduleConfig.external_notification.use_i2s_as_buzzer) {
|
||||
audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone));
|
||||
} else
|
||||
#endif
|
||||
if (moduleConfig.external_notification.use_pwm) {
|
||||
rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone);
|
||||
}
|
||||
}
|
||||
if (moduleConfig.external_notification.nag_timeout) {
|
||||
nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000;
|
||||
@@ -527,10 +537,11 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP
|
||||
#ifdef HAS_I2S
|
||||
if (moduleConfig.external_notification.use_i2s_as_buzzer) {
|
||||
audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone));
|
||||
}
|
||||
#else
|
||||
rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone);
|
||||
} else
|
||||
#endif
|
||||
if (moduleConfig.external_notification.use_pwm) {
|
||||
rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone);
|
||||
}
|
||||
}
|
||||
if (moduleConfig.external_notification.nag_timeout) {
|
||||
nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000;
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
#include "KeyVerificationModule.h"
|
||||
#include "MeshService.h"
|
||||
#include "RTC.h"
|
||||
#include "graphics/draw/MenuHandler.h"
|
||||
#include "main.h"
|
||||
#include "meshUtils.h"
|
||||
#include "modules/AdminModule.h"
|
||||
#include <SHA256.h>
|
||||
|
||||
@@ -48,18 +50,22 @@ AdminMessageHandleResult KeyVerificationModule::handleAdminMessageForModule(cons
|
||||
bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_KeyVerification *r)
|
||||
{
|
||||
updateState();
|
||||
if (mp.pki_encrypted == false)
|
||||
if (mp.pki_encrypted == false) {
|
||||
return false;
|
||||
if (mp.from != currentRemoteNode) // because the inital connection request is handled in allocReply()
|
||||
}
|
||||
if (mp.from != currentRemoteNode) { // because the inital connection request is handled in allocReply()
|
||||
return false;
|
||||
}
|
||||
if (currentState == KEY_VERIFICATION_IDLE) {
|
||||
return false; // if we're idle, the only acceptable message is an init, which should be handled by allocReply()
|
||||
}
|
||||
|
||||
} else if (currentState == KEY_VERIFICATION_SENDER_HAS_INITIATED && r->nonce == currentNonce && r->hash2.size == 32 &&
|
||||
r->hash1.size == 0) {
|
||||
if (currentState == KEY_VERIFICATION_SENDER_HAS_INITIATED && r->nonce == currentNonce && r->hash2.size == 32 &&
|
||||
r->hash1.size == 0) {
|
||||
memcpy(hash2, r->hash2.bytes, 32);
|
||||
if (screen)
|
||||
screen->showOverlayBanner("Enter Security Number", 30000);
|
||||
IF_SCREEN(screen->showNumberPicker("Enter Security Number", 60000, 6, [](int number_picked) -> void {
|
||||
keyVerificationModule->processSecurityNumber(number_picked);
|
||||
});)
|
||||
|
||||
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
|
||||
cn->level = meshtastic_LogRecord_Level_WARNING;
|
||||
@@ -79,16 +85,20 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
|
||||
memset(message, 0, sizeof(message));
|
||||
sprintf(message, "Verification: \n");
|
||||
generateVerificationCode(message + 15);
|
||||
static const char *optionsArray[] = {"ACCEPT", "REJECT"};
|
||||
LOG_INFO("Hash1 matches!");
|
||||
if (screen) {
|
||||
screen->showOverlayBanner(message, 30000, optionsArray, 2, [=](int selected) {
|
||||
if (selected == 0) {
|
||||
auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
|
||||
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
||||
}
|
||||
});
|
||||
}
|
||||
static const char *optionsArray[] = {"Reject", "Accept"};
|
||||
// Don't try to put the array definition in the macro. Does not work with curly braces.
|
||||
IF_SCREEN(graphics::BannerOverlayOptions options; options.message = message; options.durationMs = 30000;
|
||||
options.optionsArrayPtr = optionsArray; options.optionsCount = 2;
|
||||
options.notificationType = graphics::notificationTypeEnum::selection_picker;
|
||||
options.bannerCallback =
|
||||
[=](int selected) {
|
||||
if (selected == 1) {
|
||||
auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
|
||||
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
||||
}
|
||||
};
|
||||
screen->showOverlayBanner(options);)
|
||||
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
|
||||
cn->level = meshtastic_LogRecord_Level_WARNING;
|
||||
sprintf(cn->message, "Final confirmation for incoming manual key verification %s", message);
|
||||
@@ -113,6 +123,7 @@ bool KeyVerificationModule::sendInitialRequest(NodeNum remoteNode)
|
||||
// generate nonce
|
||||
updateState();
|
||||
if (currentState != KEY_VERIFICATION_IDLE) {
|
||||
IF_SCREEN(graphics::menuHandler::menuQueue = graphics::menuHandler::throttle_message;)
|
||||
return false;
|
||||
}
|
||||
currentNonce = random();
|
||||
@@ -183,11 +194,8 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply()
|
||||
responsePacket = allocDataProtobuf(response);
|
||||
|
||||
responsePacket->pki_encrypted = true;
|
||||
if (screen) {
|
||||
snprintf(message, 25, "Security Number \n%03u %03u", currentSecurityNumber / 1000, currentSecurityNumber % 1000);
|
||||
screen->showOverlayBanner(message, 30000);
|
||||
LOG_WARN("%s", message);
|
||||
}
|
||||
IF_SCREEN(snprintf(message, 25, "Security Number \n%03u %03u", currentSecurityNumber / 1000, currentSecurityNumber % 1000);
|
||||
screen->showSimpleBanner(message, 30000); LOG_WARN("%s", message);)
|
||||
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
|
||||
cn->level = meshtastic_LogRecord_Level_WARNING;
|
||||
sprintf(cn->message, "Incoming Key Verification.\nSecurity Number\n%03u %03u", currentSecurityNumber / 1000,
|
||||
@@ -251,12 +259,7 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
|
||||
p->priority = meshtastic_MeshPacket_Priority_HIGH;
|
||||
service->sendToMesh(p, RX_SRC_LOCAL, true);
|
||||
currentState = KEY_VERIFICATION_SENDER_AWAITING_USER;
|
||||
memset(message, 0, sizeof(message));
|
||||
sprintf(message, "Verification: \n");
|
||||
generateVerificationCode(message + 15); // send the toPhone packet
|
||||
if (screen) {
|
||||
screen->showOverlayBanner(message, 30000);
|
||||
}
|
||||
IF_SCREEN(screen->requestMenu(graphics::menuHandler::key_verification_final_prompt);)
|
||||
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
|
||||
cn->level = meshtastic_LogRecord_Level_WARNING;
|
||||
sprintf(cn->message, "Final confirmation for outgoing manual key verification %s", message);
|
||||
@@ -275,7 +278,7 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
|
||||
void KeyVerificationModule::updateState()
|
||||
{
|
||||
if (currentState != KEY_VERIFICATION_IDLE) {
|
||||
// check for the 30 second timeout
|
||||
// check for the 60 second timeout
|
||||
if (currentNonceTimestamp < getTime() - 60) {
|
||||
resetToIdle();
|
||||
} else {
|
||||
|
||||
@@ -27,6 +27,8 @@ class KeyVerificationModule : public ProtobufModule<meshtastic_KeyVerification>
|
||||
}*/
|
||||
virtual bool wantUIFrame() { return false; };
|
||||
bool sendInitialRequest(NodeNum remoteNode);
|
||||
void generateVerificationCode(char *); // fills char with the user readable verification code
|
||||
uint32_t getCurrentRemoteNode() { return currentRemoteNode; }
|
||||
|
||||
protected:
|
||||
/* Called to handle a particular incoming message
|
||||
@@ -56,9 +58,8 @@ class KeyVerificationModule : public ProtobufModule<meshtastic_KeyVerification>
|
||||
char message[40] = {0};
|
||||
|
||||
void processSecurityNumber(uint32_t);
|
||||
void updateState(); // check the timeouts and maybe reset the state to idle
|
||||
void resetToIdle(); // Zero out module state
|
||||
void generateVerificationCode(char *); // fills char with the user readable verification code
|
||||
void updateState(); // check the timeouts and maybe reset the state to idle
|
||||
void resetToIdle(); // Zero out module state
|
||||
};
|
||||
|
||||
extern KeyVerificationModule *keyVerificationModule;
|
||||
@@ -53,6 +53,7 @@
|
||||
#endif
|
||||
#if ARCH_PORTDUINO
|
||||
#include "input/LinuxInputImpl.h"
|
||||
#include "input/SeesawRotary.h"
|
||||
#include "modules/Telemetry/HostMetrics.h"
|
||||
#if !MESHTASTIC_EXCLUDE_STOREFORWARD
|
||||
#include "modules/StoreForwardModule.h"
|
||||
@@ -163,7 +164,6 @@ void setupModules()
|
||||
// Example: Put your module here
|
||||
// new ReplyModule();
|
||||
#if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER && !MESHTASTIC_EXCLUDE_I2C
|
||||
|
||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||
rotaryEncoderInterruptImpl1 = new RotaryEncoderInterruptImpl1();
|
||||
if (!rotaryEncoderInterruptImpl1->init()) {
|
||||
@@ -189,6 +189,11 @@ void setupModules()
|
||||
#endif // HAS_BUTTON
|
||||
#if ARCH_PORTDUINO
|
||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||
seesawRotary = new SeesawRotary("SeesawRotary");
|
||||
if (!seesawRotary->init()) {
|
||||
delete seesawRotary;
|
||||
seesawRotary = nullptr;
|
||||
}
|
||||
aLinuxInputImpl = new LinuxInputImpl();
|
||||
aLinuxInputImpl->init();
|
||||
}
|
||||
@@ -213,11 +218,14 @@ void setupModules()
|
||||
#if HAS_TELEMETRY
|
||||
new DeviceTelemetryModule();
|
||||
#endif
|
||||
// TODO: How to improve this?
|
||||
#if HAS_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
||||
new EnvironmentTelemetryModule();
|
||||
#if __has_include("Adafruit_PM25AQI.h")
|
||||
if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first > 0) {
|
||||
new AirQualityTelemetryModule();
|
||||
}
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY
|
||||
if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MAX30102].first > 0 ||
|
||||
nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MLX90614].first > 0) {
|
||||
|
||||
@@ -14,6 +14,11 @@ bool NodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mes
|
||||
{
|
||||
auto p = *pptr;
|
||||
|
||||
if (p.is_licensed != owner.is_licensed) {
|
||||
LOG_WARN("Invalid nodeInfo detected, is_licensed mismatch!");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Coerce user.id to be derived from the node number
|
||||
snprintf(p.id, sizeof(p.id), "!%08x", getFrom(&mp));
|
||||
|
||||
|
||||
@@ -266,9 +266,11 @@ meshtastic_MeshPacket *PositionModule::allocPositionPacket()
|
||||
|
||||
LOG_INFO("Position packet: time=%i lat=%i lon=%i", p.time, p.latitude_i, p.longitude_i);
|
||||
|
||||
#ifndef MESHTASTIC_EXCLUDE_ATAK
|
||||
// TAK Tracker devices should send their position in a TAK packet over the ATAK port
|
||||
if (config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER)
|
||||
return allocAtakPli();
|
||||
#endif
|
||||
|
||||
return allocDataProtobuf(p);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
meshtastic_MeshPacket *ReplyModule::allocReply()
|
||||
{
|
||||
assert(currentRequest); // should always be !NULL
|
||||
#ifdef DEBUG_PORT
|
||||
#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
|
||||
auto req = *currentRequest;
|
||||
auto &p = req.decoded;
|
||||
// The incoming message is in p.payload
|
||||
|
||||
@@ -74,6 +74,26 @@ static Print *serialPrint = &Serial2;
|
||||
char serialBytes[512];
|
||||
size_t serialPayloadSize;
|
||||
|
||||
bool SerialModule::isValidConfig(const meshtastic_ModuleConfig_SerialConfig &config)
|
||||
{
|
||||
if (config.override_console_serial_port && !IS_ONE_OF(config.mode, meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA,
|
||||
meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO)) {
|
||||
const char *warning =
|
||||
"Invalid Serial config: override console serial port is only supported in NMEA and CalTopo output-only modes.";
|
||||
LOG_ERROR(warning);
|
||||
#if !IS_RUNNING_TESTS
|
||||
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
|
||||
cn->level = meshtastic_LogRecord_Level_ERROR;
|
||||
cn->time = getValidTime(RTCQualityFromNet);
|
||||
snprintf(cn->message, sizeof(cn->message), "%s", warning);
|
||||
service->sendClientNotification(cn);
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
SerialModuleRadio::SerialModuleRadio() : MeshModule("SerialModuleRadio")
|
||||
{
|
||||
switch (moduleConfig.serial.mode) {
|
||||
|
||||
@@ -20,6 +20,8 @@ class SerialModule : public StreamAPI, private concurrency::OSThread
|
||||
public:
|
||||
SerialModule();
|
||||
|
||||
static bool isValidConfig(const meshtastic_ModuleConfig_SerialConfig &config);
|
||||
|
||||
protected:
|
||||
virtual int32_t runOnce() override;
|
||||
|
||||
|
||||
@@ -202,6 +202,8 @@ void StoreForwardModule::historyAdd(const meshtastic_MeshPacket &mp)
|
||||
this->packetHistory[this->packetHistoryTotalCount].reply_id = p.reply_id;
|
||||
this->packetHistory[this->packetHistoryTotalCount].emoji = (bool)p.emoji;
|
||||
this->packetHistory[this->packetHistoryTotalCount].payload_size = p.payload.size;
|
||||
this->packetHistory[this->packetHistoryTotalCount].rx_rssi = mp.rx_rssi;
|
||||
this->packetHistory[this->packetHistoryTotalCount].rx_snr = mp.rx_snr;
|
||||
memcpy(this->packetHistory[this->packetHistoryTotalCount].payload, p.payload.bytes, meshtastic_Constants_DATA_PAYLOAD_LEN);
|
||||
|
||||
this->packetHistoryTotalCount++;
|
||||
@@ -252,6 +254,8 @@ meshtastic_MeshPacket *StoreForwardModule::preparePayload(NodeNum dest, uint32_t
|
||||
p->decoded.reply_id = this->packetHistory[i].reply_id;
|
||||
p->rx_time = this->packetHistory[i].time;
|
||||
p->decoded.emoji = (uint32_t)this->packetHistory[i].emoji;
|
||||
p->rx_rssi = this->packetHistory[i].rx_rssi;
|
||||
p->rx_snr = this->packetHistory[i].rx_snr;
|
||||
|
||||
// Let's assume that if the server received the S&F request that the client is in range.
|
||||
// TODO: Make this configurable.
|
||||
@@ -623,4 +627,4 @@ StoreForwardModule::StoreForwardModule()
|
||||
disable();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ struct PacketHistoryStruct {
|
||||
bool emoji;
|
||||
uint8_t payload[meshtastic_Constants_DATA_PAYLOAD_LEN];
|
||||
pb_size_t payload_size;
|
||||
int32_t rx_rssi;
|
||||
float rx_snr;
|
||||
};
|
||||
|
||||
class StoreForwardModule : private concurrency::OSThread, public ProtobufModule<meshtastic_StoreAndForward>
|
||||
@@ -108,4 +110,4 @@ class StoreForwardModule : private concurrency::OSThread, public ProtobufModule<
|
||||
virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_StoreAndForward *p);
|
||||
};
|
||||
|
||||
extern StoreForwardModule *storeForwardModule;
|
||||
extern StoreForwardModule *storeForwardModule;
|
||||
|
||||
@@ -47,7 +47,7 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event)
|
||||
bool isMuted = externalNotificationModule->getMute();
|
||||
externalNotificationModule->setMute(!isMuted);
|
||||
IF_SCREEN(graphics::isMuted = !isMuted; if (!isMuted) externalNotificationModule->stopNow();
|
||||
screen->showOverlayBanner(isMuted ? "Notifications\nEnabled" : "Notifications\nDisabled", 3000);)
|
||||
screen->showSimpleBanner(isMuted ? "Notifications\nEnabled" : "Notifications\nDisabled", 3000);)
|
||||
}
|
||||
return 0;
|
||||
// Bluetooth
|
||||
@@ -58,24 +58,24 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event)
|
||||
#if defined(ARDUINO_ARCH_NRF52)
|
||||
if (!config.bluetooth.enabled) {
|
||||
disableBluetooth();
|
||||
IF_SCREEN(screen->showOverlayBanner("Bluetooth OFF\nRebooting", 3000));
|
||||
IF_SCREEN(screen->showSimpleBanner("Bluetooth OFF\nRebooting", 3000));
|
||||
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 2000;
|
||||
} else {
|
||||
IF_SCREEN(screen->showOverlayBanner("Bluetooth ON\nRebooting", 3000));
|
||||
IF_SCREEN(screen->showSimpleBanner("Bluetooth ON\nRebooting", 3000));
|
||||
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
|
||||
}
|
||||
#else
|
||||
if (!config.bluetooth.enabled) {
|
||||
disableBluetooth();
|
||||
IF_SCREEN(screen->showOverlayBanner("Bluetooth OFF", 3000));
|
||||
IF_SCREEN(screen->showSimpleBanner("Bluetooth OFF", 3000));
|
||||
} else {
|
||||
IF_SCREEN(screen->showOverlayBanner("Bluetooth ON\nRebooting", 3000));
|
||||
IF_SCREEN(screen->showSimpleBanner("Bluetooth ON\nRebooting", 3000));
|
||||
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
|
||||
}
|
||||
#endif
|
||||
return 0;
|
||||
case INPUT_BROKER_MSG_REBOOT:
|
||||
IF_SCREEN(screen->showOverlayBanner("Rebooting...", 0));
|
||||
IF_SCREEN(screen->showSimpleBanner("Rebooting...", 0));
|
||||
nodeDB->saveToDisk();
|
||||
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
|
||||
// runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
||||
@@ -89,10 +89,15 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event)
|
||||
#if !MESHTASTIC_EXCLUDE_GPS
|
||||
if (gps) {
|
||||
LOG_WARN("GPS Toggle2");
|
||||
if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED &&
|
||||
config.position.fixed_position == false) {
|
||||
nodeDB->clearLocalPosition();
|
||||
nodeDB->saveToDisk();
|
||||
}
|
||||
gps->toggleGpsMode();
|
||||
const char *msg =
|
||||
(config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) ? "GPS Enabled" : "GPS Disabled";
|
||||
IF_SCREEN(screen->forceDisplay(); screen->showOverlayBanner(msg, 3000);)
|
||||
IF_SCREEN(screen->forceDisplay(); screen->showSimpleBanner(msg, 3000);)
|
||||
}
|
||||
#endif
|
||||
return true;
|
||||
@@ -100,18 +105,14 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event)
|
||||
case INPUT_BROKER_SEND_PING:
|
||||
service->refreshLocalMeshNode();
|
||||
if (service->trySendPosition(NODENUM_BROADCAST, true)) {
|
||||
IF_SCREEN(screen->showOverlayBanner("Position\nSent", 3000));
|
||||
IF_SCREEN(screen->showSimpleBanner("Position\nSent", 3000));
|
||||
} else {
|
||||
IF_SCREEN(screen->showOverlayBanner("Node Info\nSent", 3000));
|
||||
IF_SCREEN(screen->showSimpleBanner("Node Info\nSent", 3000));
|
||||
}
|
||||
return true;
|
||||
// Power control
|
||||
case INPUT_BROKER_SHUTDOWN:
|
||||
LOG_ERROR("Shutting Down");
|
||||
IF_SCREEN(screen->showOverlayBanner("Shutting Down..."));
|
||||
nodeDB->saveToDisk();
|
||||
shutdownAtMsec = millis() + DEFAULT_SHUTDOWN_SECONDS * 1000;
|
||||
// runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
||||
shutdownAtMsec = millis();
|
||||
return true;
|
||||
|
||||
default:
|
||||
|
||||
@@ -121,7 +121,7 @@ int32_t AirQualityTelemetryModule::runOnce()
|
||||
bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t)
|
||||
{
|
||||
if (t->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) {
|
||||
#ifdef DEBUG_PORT
|
||||
#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
|
||||
const char *sender = getSenderShortName(mp);
|
||||
|
||||
LOG_INFO("(Received from %s): pm10_standard=%i, pm25_standard=%i, pm100_standard=%i", sender,
|
||||
|
||||
@@ -49,7 +49,7 @@ bool DeviceTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
|
||||
return false;
|
||||
|
||||
if (t->which_variant == meshtastic_Telemetry_device_metrics_tag) {
|
||||
#ifdef DEBUG_PORT
|
||||
#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
|
||||
const char *sender = getSenderShortName(mp);
|
||||
|
||||
LOG_INFO("(Received from %s): air_util_tx=%f, channel_utilization=%f, battery_level=%i, voltage=%f", sender,
|
||||
|
||||
@@ -450,7 +450,7 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt
|
||||
|
||||
if (isOwnTelemetry && bannerMsg && isCooldownOver) {
|
||||
LOG_INFO("drawFrame: IAQ %d (own) — showing banner: %s", m.iaq, bannerMsg);
|
||||
screen->showOverlayBanner(bannerMsg, 3000);
|
||||
screen->showSimpleBanner(bannerMsg, 3000);
|
||||
|
||||
// Only buzz if IAQ is over 200
|
||||
if (m.iaq > 200 && moduleConfig.external_notification.enabled && !externalNotificationModule->getMute()) {
|
||||
@@ -502,7 +502,7 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt
|
||||
bool EnvironmentTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t)
|
||||
{
|
||||
if (t->which_variant == meshtastic_Telemetry_environment_metrics_tag) {
|
||||
#ifdef DEBUG_PORT
|
||||
#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
|
||||
const char *sender = getSenderShortName(mp);
|
||||
|
||||
LOG_INFO("(Received from %s): barometric_pressure=%f, current=%f, gas_resistance=%f, relative_humidity=%f, "
|
||||
|
||||
@@ -149,7 +149,7 @@ void HealthTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *
|
||||
bool HealthTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t)
|
||||
{
|
||||
if (t->which_variant == meshtastic_Telemetry_health_metrics_tag) {
|
||||
#ifdef DEBUG_PORT
|
||||
#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
|
||||
const char *sender = getSenderShortName(mp);
|
||||
|
||||
LOG_INFO("(Received from %s): temperature=%f, heart_bpm=%d, spO2=%d,", sender, t->variant.health_metrics.temperature,
|
||||
|
||||
@@ -27,7 +27,7 @@ bool HostMetricsModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp,
|
||||
return false;
|
||||
|
||||
if (t->which_variant == meshtastic_Telemetry_host_metrics_tag) {
|
||||
#ifdef DEBUG_PORT
|
||||
#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
|
||||
const char *sender = getSenderShortName(mp);
|
||||
if (t->variant.host_metrics.has_user_string)
|
||||
t->variant.host_metrics.user_string[sizeof(t->variant.host_metrics.user_string) - 1] = '\0';
|
||||
|
||||
@@ -168,7 +168,7 @@ void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *s
|
||||
bool PowerTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t)
|
||||
{
|
||||
if (t->which_variant == meshtastic_Telemetry_power_metrics_tag) {
|
||||
#ifdef DEBUG_PORT
|
||||
#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
|
||||
const char *sender = getSenderShortName(mp);
|
||||
|
||||
LOG_INFO("(Received from %s): ch1_voltage=%.1f, ch1_current=%.1f, ch2_voltage=%.1f, ch2_current=%.1f, "
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "MAX17048Sensor.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_I2C && !defined(ARCH_STM32WL) && __has_include(<Adafruit_MAX1704X.h>)
|
||||
#if !MESHTASTIC_EXCLUDE_I2C && __has_include(<Adafruit_MAX1704X.h>)
|
||||
|
||||
MAX17048Singleton *MAX17048Singleton::GetInstance()
|
||||
{
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_I2C && !defined(ARCH_STM32WL) && __has_include(<Adafruit_MAX1704X.h>)
|
||||
#if !MESHTASTIC_EXCLUDE_I2C && __has_include(<Adafruit_MAX1704X.h>)
|
||||
|
||||
// Samples to store in a buffer to determine if the battery is charging or discharging
|
||||
#define MAX17048_CHARGING_SAMPLES 3
|
||||
|
||||
@@ -20,4 +20,15 @@ bool NullSensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t NullSensor::getBusVoltageMv()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int16_t NullSensor::getCurrentMa()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -4,9 +4,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "TelemetrySensor.h"
|
||||
|
||||
class NullSensor : public TelemetrySensor
|
||||
#include "CurrentSensor.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include "VoltageSensor.h"
|
||||
|
||||
class NullSensor : public TelemetrySensor, VoltageSensor, CurrentSensor
|
||||
{
|
||||
|
||||
protected:
|
||||
@@ -17,6 +20,9 @@ class NullSensor : public TelemetrySensor
|
||||
virtual int32_t runOnce() override;
|
||||
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
||||
int32_t runTrigger() { return 0; }
|
||||
|
||||
virtual uint16_t getBusVoltageMv() override;
|
||||
virtual int16_t getCurrentMa() override;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -4,11 +4,12 @@
|
||||
#include "PowerFSM.h"
|
||||
#include "buzz.h"
|
||||
#include "configuration.h"
|
||||
#include "graphics/Screen.h"
|
||||
TextMessageModule *textMessageModule;
|
||||
|
||||
ProcessMessage TextMessageModule::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
#ifdef DEBUG_PORT
|
||||
#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
|
||||
auto &p = mp.decoded;
|
||||
LOG_INFO("Received text msg from=0x%0x, id=0x%x, msg=%.*s", mp.from, mp.id, p.payload.size, p.payload.bytes);
|
||||
#endif
|
||||
@@ -17,7 +18,10 @@ ProcessMessage TextMessageModule::handleReceived(const meshtastic_MeshPacket &mp
|
||||
devicestate.rx_text_message = mp;
|
||||
devicestate.has_rx_text_message = true;
|
||||
|
||||
powerFSM.trigger(EVENT_RECEIVED_MSG);
|
||||
// Only trigger screen wake if configuration allows it
|
||||
if (shouldWakeOnReceivedMessage()) {
|
||||
powerFSM.trigger(EVENT_RECEIVED_MSG);
|
||||
}
|
||||
notifyObservers(&mp);
|
||||
|
||||
return ProcessMessage::CONTINUE; // Let others look at this message also if they want
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
#include "TraceRouteModule.h"
|
||||
#include "MeshService.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "mesh/Router.h"
|
||||
#include "meshUtils.h"
|
||||
#include <vector>
|
||||
|
||||
extern graphics::Screen *screen;
|
||||
|
||||
TraceRouteModule *traceRouteModule;
|
||||
|
||||
@@ -27,6 +34,123 @@ void TraceRouteModule::alterReceivedProtobuf(meshtastic_MeshPacket &p, meshtasti
|
||||
// Set updated route to the payload of the to be flooded packet
|
||||
p.decoded.payload.size =
|
||||
pb_encode_to_bytes(p.decoded.payload.bytes, sizeof(p.decoded.payload.bytes), &meshtastic_RouteDiscovery_msg, r);
|
||||
|
||||
if (tracingNode != 0) {
|
||||
// check isResponseFromTarget
|
||||
bool isResponseFromTarget = (incoming.request_id != 0 && p.from == tracingNode);
|
||||
bool isRequestToUs = (incoming.request_id == 0 && p.to == nodeDB->getNodeNum() && tracingNode != 0);
|
||||
|
||||
// Check if this is a trace route response containing our target node
|
||||
bool containsTargetNode = false;
|
||||
for (uint8_t i = 0; i < r->route_count; i++) {
|
||||
if (r->route[i] == tracingNode) {
|
||||
containsTargetNode = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (uint8_t i = 0; i < r->route_back_count; i++) {
|
||||
if (r->route_back[i] == tracingNode) {
|
||||
containsTargetNode = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this response contains a complete route to our target
|
||||
bool hasCompleteRoute = (r->route_count > 0 && r->route_back_count > 0) ||
|
||||
(containsTargetNode && (r->route_count > 0 || r->route_back_count > 0));
|
||||
|
||||
LOG_INFO("TracRoute packet analysis: tracingNode=0x%08x, p.from=0x%08x, p.to=0x%08x, request_id=0x%08x", tracingNode,
|
||||
p.from, p.to, incoming.request_id);
|
||||
LOG_INFO("TracRoute conditions: isResponseFromTarget=%d, isRequestToUs=%d, containsTargetNode=%d, hasCompleteRoute=%d",
|
||||
isResponseFromTarget, isRequestToUs, containsTargetNode, hasCompleteRoute);
|
||||
|
||||
if (isResponseFromTarget || isRequestToUs || (containsTargetNode && hasCompleteRoute)) {
|
||||
LOG_INFO("TracRoute result detected: isResponseFromTarget=%d, isRequestToUs=%d", isResponseFromTarget, isRequestToUs);
|
||||
|
||||
LOG_INFO("SNR arrays - towards_count=%d, back_count=%d", r->snr_towards_count, r->snr_back_count);
|
||||
for (int i = 0; i < r->snr_towards_count; i++) {
|
||||
LOG_INFO("SNR towards[%d] = %d (%.1fdB)", i, r->snr_towards[i], (float)r->snr_towards[i] / 4.0f);
|
||||
}
|
||||
for (int i = 0; i < r->snr_back_count; i++) {
|
||||
LOG_INFO("SNR back[%d] = %d (%.1fdB)", i, r->snr_back[i], (float)r->snr_back[i] / 4.0f);
|
||||
}
|
||||
|
||||
String result = "";
|
||||
|
||||
// Show request path (from initiator to target)
|
||||
if (r->route_count > 0) {
|
||||
result += getNodeName(nodeDB->getNodeNum());
|
||||
for (uint8_t i = 0; i < r->route_count; i++) {
|
||||
result += " > ";
|
||||
const char *name = getNodeName(r->route[i]);
|
||||
float snr =
|
||||
(i < r->snr_towards_count && r->snr_towards[i] != INT8_MIN) ? ((float)r->snr_towards[i] / 4.0f) : 0.0f;
|
||||
result += name;
|
||||
if (snr != 0.0f) {
|
||||
result += "(";
|
||||
result += String(snr, 1);
|
||||
result += "dB)";
|
||||
}
|
||||
}
|
||||
result += " > ";
|
||||
result += getNodeName(tracingNode);
|
||||
if (r->snr_towards_count > 0 && r->snr_towards[r->snr_towards_count - 1] != INT8_MIN) {
|
||||
result += "(";
|
||||
result += String((float)r->snr_towards[r->snr_towards_count - 1] / 4.0f, 1);
|
||||
result += "dB)";
|
||||
}
|
||||
result += "\n";
|
||||
} else {
|
||||
// Direct connection (no intermediate hops)
|
||||
result += getNodeName(nodeDB->getNodeNum());
|
||||
result += " > ";
|
||||
result += getNodeName(tracingNode);
|
||||
if (r->snr_towards_count > 0 && r->snr_towards[0] != INT8_MIN) {
|
||||
result += "(";
|
||||
result += String((float)r->snr_towards[0] / 4.0f, 1);
|
||||
result += "dB)";
|
||||
}
|
||||
result += "\n";
|
||||
}
|
||||
|
||||
// Show response path (from target back to initiator)
|
||||
if (r->route_back_count > 0) {
|
||||
result += getNodeName(tracingNode);
|
||||
for (int8_t i = r->route_back_count - 1; i >= 0; i--) {
|
||||
result += " > ";
|
||||
const char *name = getNodeName(r->route_back[i]);
|
||||
float snr = (i < r->snr_back_count && r->snr_back[i] != INT8_MIN) ? ((float)r->snr_back[i] / 4.0f) : 0.0f;
|
||||
result += name;
|
||||
if (snr != 0.0f) {
|
||||
result += "(";
|
||||
result += String(snr, 1);
|
||||
result += "dB)";
|
||||
}
|
||||
}
|
||||
// add initiator node
|
||||
result += " > ";
|
||||
result += getNodeName(nodeDB->getNodeNum());
|
||||
if (r->snr_back_count > 0 && r->snr_back[r->snr_back_count - 1] != INT8_MIN) {
|
||||
result += "(";
|
||||
result += String((float)r->snr_back[r->snr_back_count - 1] / 4.0f, 1);
|
||||
result += "dB)";
|
||||
}
|
||||
} else {
|
||||
// Direct return path (no intermediate hops)
|
||||
result += getNodeName(tracingNode);
|
||||
result += " > ";
|
||||
result += getNodeName(nodeDB->getNodeNum());
|
||||
if (r->snr_back_count > 0 && r->snr_back[0] != INT8_MIN) {
|
||||
result += "(";
|
||||
result += String((float)r->snr_back[0] / 4.0f, 1);
|
||||
result += "dB)";
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("Trace route result: %s", result.c_str());
|
||||
handleTraceRouteResult(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TraceRouteModule::insertUnknownHops(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r, bool isTowardsDestination)
|
||||
@@ -108,7 +232,7 @@ void TraceRouteModule::appendMyIDandSNR(meshtastic_RouteDiscovery *updated, floa
|
||||
|
||||
void TraceRouteModule::printRoute(meshtastic_RouteDiscovery *r, uint32_t origin, uint32_t dest, bool isTowardsDestination)
|
||||
{
|
||||
#ifdef DEBUG_PORT
|
||||
#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
|
||||
std::string route = "Route traced:\n";
|
||||
route += vformat("0x%x --> ", origin);
|
||||
for (uint8_t i = 0; i < r->route_count; i++) {
|
||||
@@ -173,8 +297,467 @@ meshtastic_MeshPacket *TraceRouteModule::allocReply()
|
||||
}
|
||||
|
||||
TraceRouteModule::TraceRouteModule()
|
||||
: ProtobufModule("traceroute", meshtastic_PortNum_TRACEROUTE_APP, &meshtastic_RouteDiscovery_msg)
|
||||
: ProtobufModule("traceroute", meshtastic_PortNum_TRACEROUTE_APP, &meshtastic_RouteDiscovery_msg), OSThread("TraceRoute")
|
||||
{
|
||||
ourPortNum = meshtastic_PortNum_TRACEROUTE_APP;
|
||||
isPromiscuous = true; // We need to update the route even if it is not destined to us
|
||||
}
|
||||
|
||||
const char *TraceRouteModule::getNodeName(NodeNum node)
|
||||
{
|
||||
meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node);
|
||||
if (info && info->has_user) {
|
||||
if (strlen(info->user.short_name) > 0) {
|
||||
return info->user.short_name;
|
||||
}
|
||||
if (strlen(info->user.long_name) > 0) {
|
||||
return info->user.long_name;
|
||||
}
|
||||
}
|
||||
|
||||
static char fallback[12];
|
||||
snprintf(fallback, sizeof(fallback), "0x%08x", node);
|
||||
return fallback;
|
||||
}
|
||||
|
||||
bool TraceRouteModule::startTraceRoute(NodeNum node)
|
||||
{
|
||||
LOG_INFO("=== TraceRoute startTraceRoute CALLED: node=0x%08x ===", node);
|
||||
unsigned long now = millis();
|
||||
|
||||
if (node == 0 || node == NODENUM_BROADCAST) {
|
||||
LOG_ERROR("Invalid node number for trace route: 0x%08x", node);
|
||||
runState = TRACEROUTE_STATE_RESULT;
|
||||
resultText = "Invalid node";
|
||||
resultShowTime = millis();
|
||||
tracingNode = 0;
|
||||
|
||||
requestFocus();
|
||||
UIFrameEvent e;
|
||||
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
||||
notifyObservers(&e);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (node == nodeDB->getNodeNum()) {
|
||||
LOG_ERROR("Cannot trace route to self: 0x%08x", node);
|
||||
runState = TRACEROUTE_STATE_RESULT;
|
||||
resultText = "Cannot trace self";
|
||||
resultShowTime = millis();
|
||||
tracingNode = 0;
|
||||
|
||||
requestFocus();
|
||||
UIFrameEvent e;
|
||||
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
||||
notifyObservers(&e);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!initialized) {
|
||||
lastTraceRouteTime = 0;
|
||||
initialized = true;
|
||||
LOG_INFO("TraceRoute initialized for first time");
|
||||
}
|
||||
|
||||
if (runState == TRACEROUTE_STATE_TRACKING) {
|
||||
LOG_INFO("TraceRoute already in progress");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (initialized && lastTraceRouteTime > 0 && now - lastTraceRouteTime < cooldownMs) {
|
||||
// Cooldown
|
||||
unsigned long wait = (cooldownMs - (now - lastTraceRouteTime)) / 1000;
|
||||
bannerText = String("Wait for ") + String(wait) + String("s");
|
||||
runState = TRACEROUTE_STATE_COOLDOWN;
|
||||
|
||||
requestFocus();
|
||||
UIFrameEvent e;
|
||||
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
||||
notifyObservers(&e);
|
||||
LOG_INFO("Cooldown active, please wait %lu seconds before starting a new trace route.", wait);
|
||||
return false;
|
||||
}
|
||||
|
||||
tracingNode = node;
|
||||
lastTraceRouteTime = now;
|
||||
runState = TRACEROUTE_STATE_TRACKING;
|
||||
bannerText = String("Tracing ") + getNodeName(node);
|
||||
|
||||
LOG_INFO("TraceRoute UI: Starting trace route to node 0x%08x, requesting focus", node);
|
||||
|
||||
// 请求焦点,然后触发UI更新事件
|
||||
requestFocus();
|
||||
UIFrameEvent e;
|
||||
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
||||
notifyObservers(&e);
|
||||
|
||||
// 设置定时器来处理超时检查
|
||||
setIntervalFromNow(1000); // 每秒检查一次状态
|
||||
|
||||
meshtastic_RouteDiscovery req = meshtastic_RouteDiscovery_init_zero;
|
||||
LOG_INFO("Creating RouteDiscovery protobuf...");
|
||||
|
||||
// Allocate a packet directly from router like the reference code
|
||||
meshtastic_MeshPacket *p = router->allocForSending();
|
||||
if (p) {
|
||||
// Set destination and port
|
||||
p->to = node;
|
||||
p->decoded.portnum = meshtastic_PortNum_TRACEROUTE_APP;
|
||||
p->decoded.want_response = true;
|
||||
|
||||
// Manually encode the RouteDiscovery payload
|
||||
p->decoded.payload.size =
|
||||
pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_RouteDiscovery_msg, &req);
|
||||
|
||||
LOG_INFO("Packet allocated successfully: to=0x%08x, portnum=%d, want_response=%d, payload_size=%d", p->to,
|
||||
p->decoded.portnum, p->decoded.want_response, p->decoded.payload.size);
|
||||
LOG_INFO("About to call service->sendToMesh...");
|
||||
|
||||
if (service) {
|
||||
LOG_INFO("MeshService is available, sending packet...");
|
||||
service->sendToMesh(p, RX_SRC_USER);
|
||||
LOG_INFO("sendToMesh called successfully for trace route to node 0x%08x", node);
|
||||
} else {
|
||||
LOG_ERROR("MeshService is NULL!");
|
||||
runState = TRACEROUTE_STATE_RESULT;
|
||||
resultText = "Service unavailable";
|
||||
resultShowTime = millis();
|
||||
tracingNode = 0;
|
||||
|
||||
requestFocus();
|
||||
UIFrameEvent e2;
|
||||
e2.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
||||
notifyObservers(&e2);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR("Failed to allocate TraceRoute packet from router");
|
||||
runState = TRACEROUTE_STATE_RESULT;
|
||||
resultText = "Failed to send";
|
||||
resultShowTime = millis();
|
||||
tracingNode = 0;
|
||||
|
||||
requestFocus();
|
||||
UIFrameEvent e2;
|
||||
e2.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
||||
notifyObservers(&e2);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void TraceRouteModule::launch(NodeNum node)
|
||||
{
|
||||
if (node == 0 || node == NODENUM_BROADCAST) {
|
||||
LOG_ERROR("Invalid node number for trace route: 0x%08x", node);
|
||||
runState = TRACEROUTE_STATE_RESULT;
|
||||
resultText = "Invalid node";
|
||||
resultShowTime = millis();
|
||||
tracingNode = 0;
|
||||
|
||||
requestFocus();
|
||||
UIFrameEvent e;
|
||||
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
||||
notifyObservers(&e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (node == nodeDB->getNodeNum()) {
|
||||
LOG_ERROR("Cannot trace route to self: 0x%08x", node);
|
||||
runState = TRACEROUTE_STATE_RESULT;
|
||||
resultText = "Cannot trace self";
|
||||
resultShowTime = millis();
|
||||
tracingNode = 0;
|
||||
|
||||
requestFocus();
|
||||
UIFrameEvent e;
|
||||
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
||||
notifyObservers(&e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!initialized) {
|
||||
lastTraceRouteTime = 0;
|
||||
initialized = true;
|
||||
LOG_INFO("TraceRoute initialized for first time");
|
||||
}
|
||||
|
||||
unsigned long now = millis();
|
||||
if (initialized && lastTraceRouteTime > 0 && now - lastTraceRouteTime < cooldownMs) {
|
||||
unsigned long wait = (cooldownMs - (now - lastTraceRouteTime)) / 1000;
|
||||
bannerText = String("Wait for ") + String(wait) + String("s");
|
||||
runState = TRACEROUTE_STATE_COOLDOWN;
|
||||
|
||||
requestFocus();
|
||||
UIFrameEvent e;
|
||||
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
||||
notifyObservers(&e);
|
||||
LOG_INFO("Cooldown active, please wait %lu seconds before starting a new trace route.", wait);
|
||||
return;
|
||||
}
|
||||
|
||||
runState = TRACEROUTE_STATE_TRACKING;
|
||||
tracingNode = node;
|
||||
lastTraceRouteTime = now;
|
||||
bannerText = String("Tracing ") + getNodeName(node);
|
||||
|
||||
requestFocus();
|
||||
|
||||
UIFrameEvent e;
|
||||
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
||||
notifyObservers(&e);
|
||||
|
||||
setIntervalFromNow(1000);
|
||||
|
||||
meshtastic_RouteDiscovery req = meshtastic_RouteDiscovery_init_zero;
|
||||
LOG_INFO("Creating RouteDiscovery protobuf...");
|
||||
|
||||
meshtastic_MeshPacket *p = router->allocForSending();
|
||||
if (p) {
|
||||
p->to = node;
|
||||
p->decoded.portnum = meshtastic_PortNum_TRACEROUTE_APP;
|
||||
p->decoded.want_response = true;
|
||||
|
||||
p->decoded.payload.size =
|
||||
pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_RouteDiscovery_msg, &req);
|
||||
|
||||
LOG_INFO("Packet allocated successfully: to=0x%08x, portnum=%d, want_response=%d, payload_size=%d", p->to,
|
||||
p->decoded.portnum, p->decoded.want_response, p->decoded.payload.size);
|
||||
|
||||
if (service) {
|
||||
service->sendToMesh(p, RX_SRC_USER);
|
||||
LOG_INFO("sendToMesh called successfully for trace route to node 0x%08x", node);
|
||||
} else {
|
||||
LOG_ERROR("MeshService is NULL!");
|
||||
runState = TRACEROUTE_STATE_RESULT;
|
||||
resultText = "Service unavailable";
|
||||
resultShowTime = millis();
|
||||
tracingNode = 0;
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR("Failed to allocate TraceRoute packet from router");
|
||||
runState = TRACEROUTE_STATE_RESULT;
|
||||
resultText = "Failed to send";
|
||||
resultShowTime = millis();
|
||||
tracingNode = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void TraceRouteModule::handleTraceRouteResult(const String &result)
|
||||
{
|
||||
resultText = result;
|
||||
runState = TRACEROUTE_STATE_RESULT;
|
||||
resultShowTime = millis();
|
||||
tracingNode = 0;
|
||||
|
||||
LOG_INFO("TraceRoute result ready, requesting focus. Result: %s", result.c_str());
|
||||
|
||||
setIntervalFromNow(1000);
|
||||
|
||||
requestFocus();
|
||||
UIFrameEvent e;
|
||||
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
||||
notifyObservers(&e);
|
||||
|
||||
LOG_INFO("=== TraceRoute handleTraceRouteResult END ===");
|
||||
}
|
||||
|
||||
bool TraceRouteModule::shouldDraw()
|
||||
{
|
||||
bool draw = (runState != TRACEROUTE_STATE_IDLE);
|
||||
static TraceRouteRunState lastLoggedState = TRACEROUTE_STATE_IDLE;
|
||||
if (runState != lastLoggedState) {
|
||||
LOG_INFO("TraceRoute shouldDraw: runState=%d, draw=%d", runState, draw);
|
||||
lastLoggedState = runState;
|
||||
}
|
||||
return draw;
|
||||
}
|
||||
#if HAS_SCREEN
|
||||
void TraceRouteModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
LOG_DEBUG("TraceRoute drawFrame called: runState=%d", runState);
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
|
||||
if (runState == TRACEROUTE_STATE_TRACKING) {
|
||||
display->setFont(FONT_MEDIUM);
|
||||
int centerY = y + (display->getHeight() / 2) - (FONT_HEIGHT_MEDIUM / 2);
|
||||
display->drawString(display->getWidth() / 2 + x, centerY, bannerText);
|
||||
|
||||
} else if (runState == TRACEROUTE_STATE_RESULT) {
|
||||
display->setFont(FONT_MEDIUM);
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
|
||||
display->drawString(x, y, "Route Result");
|
||||
|
||||
int contentStartY = y + FONT_HEIGHT_MEDIUM + 2; // Add more spacing after title
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
|
||||
if (resultText.length() > 0) {
|
||||
std::vector<String> lines;
|
||||
String currentLine = "";
|
||||
int maxWidth = display->getWidth() - 4;
|
||||
|
||||
int start = 0;
|
||||
int newlinePos = resultText.indexOf('\n', start);
|
||||
|
||||
while (newlinePos != -1 || start < resultText.length()) {
|
||||
String segment;
|
||||
if (newlinePos != -1) {
|
||||
segment = resultText.substring(start, newlinePos);
|
||||
start = newlinePos + 1;
|
||||
newlinePos = resultText.indexOf('\n', start);
|
||||
} else {
|
||||
segment = resultText.substring(start);
|
||||
start = resultText.length();
|
||||
}
|
||||
|
||||
if (display->getStringWidth(segment) <= maxWidth) {
|
||||
lines.push_back(segment);
|
||||
} else {
|
||||
// Try to break at better positions (space, >, <, -)
|
||||
String remaining = segment;
|
||||
|
||||
while (remaining.length() > 0) {
|
||||
String tempLine = "";
|
||||
int lastGoodBreak = -1;
|
||||
bool lineComplete = false;
|
||||
|
||||
for (int i = 0; i < remaining.length(); i++) {
|
||||
char ch = remaining.charAt(i);
|
||||
String testLine = tempLine + ch;
|
||||
|
||||
if (display->getStringWidth(testLine) > maxWidth) {
|
||||
if (lastGoodBreak >= 0) {
|
||||
// Break at the last good position
|
||||
lines.push_back(remaining.substring(0, lastGoodBreak + 1));
|
||||
remaining = remaining.substring(lastGoodBreak + 1);
|
||||
lineComplete = true;
|
||||
break;
|
||||
} else if (tempLine.length() > 0) {
|
||||
lines.push_back(tempLine);
|
||||
remaining = remaining.substring(i);
|
||||
lineComplete = true;
|
||||
break;
|
||||
} else {
|
||||
// Single character exceeds width
|
||||
lines.push_back(String(ch));
|
||||
remaining = remaining.substring(i + 1);
|
||||
lineComplete = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
tempLine = testLine;
|
||||
// Mark good break positions
|
||||
if (ch == ' ' || ch == '>' || ch == '<' || ch == '-' || ch == '(' || ch == ')') {
|
||||
lastGoodBreak = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!lineComplete) {
|
||||
// Reached end of remaining text
|
||||
if (tempLine.length() > 0) {
|
||||
lines.push_back(tempLine);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int lineHeight = FONT_HEIGHT_SMALL + 1; // Use proper font height with 1px spacing
|
||||
for (size_t i = 0; i < lines.size(); i++) {
|
||||
int lineY = contentStartY + (i * lineHeight);
|
||||
if (lineY + FONT_HEIGHT_SMALL <= display->getHeight()) {
|
||||
display->drawString(x + 2, lineY, lines[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else if (runState == TRACEROUTE_STATE_COOLDOWN) {
|
||||
display->setFont(FONT_MEDIUM);
|
||||
int centerY = y + (display->getHeight() / 2) - (FONT_HEIGHT_MEDIUM / 2);
|
||||
display->drawString(display->getWidth() / 2 + x, centerY, bannerText);
|
||||
}
|
||||
}
|
||||
#endif // HAS_SCREEN
|
||||
int32_t TraceRouteModule::runOnce()
|
||||
{
|
||||
unsigned long now = millis();
|
||||
|
||||
if (runState == TRACEROUTE_STATE_IDLE) {
|
||||
return INT32_MAX;
|
||||
}
|
||||
|
||||
// Check for tracking timeout
|
||||
if (runState == TRACEROUTE_STATE_TRACKING && now - lastTraceRouteTime > trackingTimeoutMs) {
|
||||
LOG_INFO("TraceRoute timeout, no response received");
|
||||
runState = TRACEROUTE_STATE_RESULT;
|
||||
resultText = "No response received";
|
||||
resultShowTime = now;
|
||||
tracingNode = 0;
|
||||
|
||||
requestFocus();
|
||||
UIFrameEvent e;
|
||||
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
||||
notifyObservers(&e);
|
||||
|
||||
setIntervalFromNow(resultDisplayMs);
|
||||
return resultDisplayMs;
|
||||
}
|
||||
|
||||
// Update cooldown display every second
|
||||
if (runState == TRACEROUTE_STATE_COOLDOWN) {
|
||||
unsigned long wait = (cooldownMs - (now - lastTraceRouteTime)) / 1000;
|
||||
if (wait > 0) {
|
||||
String newBannerText = String("Wait for ") + String(wait) + String("s");
|
||||
bannerText = newBannerText;
|
||||
LOG_INFO("TraceRoute cooldown: updating banner to %s", bannerText.c_str());
|
||||
|
||||
// Force flash UI
|
||||
requestFocus();
|
||||
UIFrameEvent e;
|
||||
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
||||
notifyObservers(&e);
|
||||
|
||||
if (screen) {
|
||||
screen->forceDisplay();
|
||||
}
|
||||
|
||||
return 1000;
|
||||
} else {
|
||||
// Cooldown finished
|
||||
LOG_INFO("TraceRoute cooldown finished, returning to IDLE");
|
||||
runState = TRACEROUTE_STATE_IDLE;
|
||||
bannerText = "";
|
||||
UIFrameEvent e;
|
||||
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
||||
notifyObservers(&e);
|
||||
return INT32_MAX;
|
||||
}
|
||||
}
|
||||
|
||||
if (runState == TRACEROUTE_STATE_RESULT) {
|
||||
if (now - resultShowTime >= resultDisplayMs) {
|
||||
LOG_INFO("TraceRoute result display timeout, returning to IDLE");
|
||||
runState = TRACEROUTE_STATE_IDLE;
|
||||
resultText = "";
|
||||
bannerText = "";
|
||||
tracingNode = 0;
|
||||
UIFrameEvent e;
|
||||
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
||||
notifyObservers(&e);
|
||||
return INT32_MAX;
|
||||
} else {
|
||||
return 1000;
|
||||
}
|
||||
}
|
||||
|
||||
if (runState == TRACEROUTE_STATE_TRACKING) {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
return INT32_MAX;
|
||||
}
|
||||
@@ -1,16 +1,40 @@
|
||||
#pragma once
|
||||
#include "ProtobufModule.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "input/InputBroker.h"
|
||||
#if HAS_SCREEN
|
||||
#include "OLEDDisplayUi.h"
|
||||
#endif
|
||||
|
||||
#define ROUTE_SIZE sizeof(((meshtastic_RouteDiscovery *)0)->route) / sizeof(((meshtastic_RouteDiscovery *)0)->route[0])
|
||||
|
||||
/**
|
||||
* A module that traces the route to a certain destination node
|
||||
*/
|
||||
class TraceRouteModule : public ProtobufModule<meshtastic_RouteDiscovery>
|
||||
enum TraceRouteRunState { TRACEROUTE_STATE_IDLE, TRACEROUTE_STATE_TRACKING, TRACEROUTE_STATE_RESULT, TRACEROUTE_STATE_COOLDOWN };
|
||||
|
||||
class TraceRouteModule : public ProtobufModule<meshtastic_RouteDiscovery>,
|
||||
public Observable<const UIFrameEvent *>,
|
||||
private concurrency::OSThread
|
||||
{
|
||||
public:
|
||||
TraceRouteModule();
|
||||
|
||||
bool startTraceRoute(NodeNum node);
|
||||
void launch(NodeNum node);
|
||||
void handleTraceRouteResult(const String &result);
|
||||
bool shouldDraw();
|
||||
#if HAS_SCREEN
|
||||
virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override;
|
||||
#endif
|
||||
|
||||
const char *getNodeName(NodeNum node);
|
||||
|
||||
virtual bool wantUIFrame() override { return shouldDraw(); }
|
||||
virtual Observable<const UIFrameEvent *> *getUIFrameObservable() override { return this; }
|
||||
|
||||
protected:
|
||||
bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_RouteDiscovery *r) override;
|
||||
|
||||
@@ -20,6 +44,8 @@ class TraceRouteModule : public ProtobufModule<meshtastic_RouteDiscovery>
|
||||
the route array containing the IDs of nodes this packet went through */
|
||||
void alterReceivedProtobuf(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r) override;
|
||||
|
||||
virtual int32_t runOnce() override;
|
||||
|
||||
private:
|
||||
// Call to add unknown hops (e.g. when a node couldn't decrypt it) to the route based on hopStart and current hopLimit
|
||||
void insertUnknownHops(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r, bool isTowardsDestination);
|
||||
@@ -31,6 +57,17 @@ class TraceRouteModule : public ProtobufModule<meshtastic_RouteDiscovery>
|
||||
Set origin to where the request came from.
|
||||
Set dest to the ID of its destination, or NODENUM_BROADCAST if it has not yet arrived there. */
|
||||
void printRoute(meshtastic_RouteDiscovery *r, uint32_t origin, uint32_t dest, bool isTowardsDestination);
|
||||
|
||||
TraceRouteRunState runState = TRACEROUTE_STATE_IDLE;
|
||||
unsigned long lastTraceRouteTime = 0;
|
||||
unsigned long resultShowTime = 0;
|
||||
unsigned long cooldownMs = 30000;
|
||||
unsigned long resultDisplayMs = 10000;
|
||||
unsigned long trackingTimeoutMs = 10000;
|
||||
String bannerText;
|
||||
String resultText;
|
||||
NodeNum tracingNode = 0;
|
||||
bool initialized = false;
|
||||
};
|
||||
|
||||
extern TraceRouteModule *traceRouteModule;
|
||||
@@ -16,7 +16,7 @@ WaypointModule *waypointModule;
|
||||
|
||||
ProcessMessage WaypointModule::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
#ifdef DEBUG_PORT
|
||||
#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
|
||||
auto &p = mp.decoded;
|
||||
LOG_INFO("Received waypoint msg from=0x%0x, id=0x%x, msg=%.*s", mp.from, mp.id, p.payload.size, p.payload.bytes);
|
||||
#endif
|
||||
@@ -137,7 +137,7 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state,
|
||||
if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading())) {
|
||||
const meshtastic_PositionLite &op = ourNode->position;
|
||||
float myHeading;
|
||||
if (screen->ignoreCompass) {
|
||||
if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) {
|
||||
myHeading = 0;
|
||||
} else {
|
||||
if (screen->hasHeading())
|
||||
@@ -152,7 +152,7 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state,
|
||||
GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(wp.latitude_i), DegD(wp.longitude_i));
|
||||
// If the top of the compass is a static north then bearingToOther can be drawn on the compass directly
|
||||
// If the top of the compass is not a static north we need adjust bearingToOther based on heading
|
||||
if (!screen->ignoreCompass)
|
||||
if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING)
|
||||
bearingToOther -= myHeading;
|
||||
graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user