Merge branch 'master' into nomad-gemini

This commit is contained in:
Thomas Göttgens
2025-06-25 20:12:00 +02:00
committed by GitHub
281 changed files with 13894 additions and 5536 deletions

View File

@@ -5,8 +5,10 @@
#include "PowerFSM.h"
#include "RTC.h"
#include "SPILock.h"
#include "input/InputBroker.h"
#include "meshUtils.h"
#include <FSCommon.h>
#include <ctype.h> // for better whitespace handling
#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_BLUETOOTH
#include "BleOta.h"
#endif
@@ -155,6 +157,28 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
*/
case meshtastic_AdminMessage_set_owner_tag:
LOG_DEBUG("Client set owner");
// Validate names
if (*r->set_owner.long_name) {
const char *start = r->set_owner.long_name;
// Skip all whitespace (space, tab, newline, etc)
while (*start && isspace((unsigned char)*start))
start++;
if (*start == '\0') {
LOG_WARN("Rejected long_name: must contain at least 1 non-whitespace character");
myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp);
break;
}
}
if (*r->set_owner.short_name) {
const char *start = r->set_owner.short_name;
while (*start && isspace((unsigned char)*start))
start++;
if (*start == '\0') {
LOG_WARN("Rejected short_name: must contain at least 1 non-whitespace character");
myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp);
break;
}
}
handleSetOwner(r->set_owner);
break;
@@ -200,14 +224,16 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
#if defined(ARCH_ESP32)
#if !MESHTASTIC_EXCLUDE_BLUETOOTH
if (!BleOta::getOtaAppVersion().isEmpty()) {
screen->startFirmwareUpdateScreen();
if (screen)
screen->startFirmwareUpdateScreen();
BleOta::switchToOtaApp();
LOG_INFO("Rebooting to BLE OTA");
}
#endif
#if !MESHTASTIC_EXCLUDE_WIFI
if (WiFiOTA::trySwitchToOTA()) {
screen->startFirmwareUpdateScreen();
if (screen)
screen->startFirmwareUpdateScreen();
WiFiOTA::saveConfig(&config.network);
LOG_INFO("Rebooting to WiFi OTA");
}
@@ -297,6 +323,8 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
if (node != NULL) {
node->is_favorite = true;
saveChanges(SEGMENT_NODEDATABASE, false);
if (screen)
screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens
}
break;
}
@@ -306,6 +334,8 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
if (node != NULL) {
node->is_favorite = false;
saveChanges(SEGMENT_NODEDATABASE, false);
if (screen)
screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens
}
break;
}
@@ -420,6 +450,11 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
#endif
break;
}
case meshtastic_AdminMessage_send_input_event_tag: {
LOG_INFO("Client requesting to send input event");
handleSendInputEvent(r->send_input_event);
break;
}
#ifdef ARCH_PORTDUINO
case meshtastic_AdminMessage_exit_simulator_tag:
LOG_INFO("Exiting simulator");
@@ -435,22 +470,38 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
setPassKey(&res);
myReply = allocDataProtobuf(res);
} else if (mp.decoded.want_response) {
LOG_DEBUG("Did not responded to a request that wanted a respond. req.variant=%d", r->which_payload_variant);
LOG_DEBUG("Module API did not respond to admin message. req.variant=%d", r->which_payload_variant);
} else if (handleResult != AdminMessageHandleResult::HANDLED) {
// Probably a message sent by us or sent to our local node. FIXME, we should avoid scanning these messages
LOG_DEBUG("Ignore irrelevant admin %d", r->which_payload_variant);
LOG_DEBUG("Module API did not handle admin message %d", r->which_payload_variant);
}
break;
}
// Allow any observers (e.g. the UI) to handle/respond
AdminMessageHandleResult observerResult = AdminMessageHandleResult::NOT_HANDLED;
meshtastic_AdminMessage observerResponse = meshtastic_AdminMessage_init_default;
AdminModule_ObserverData observerData = {
.request = r,
.response = &observerResponse,
.result = &observerResult,
};
notifyObservers(&observerData);
if (observerResult == AdminMessageHandleResult::HANDLED_WITH_RESPONSE) {
setPassKey(&observerResponse);
myReply = allocDataProtobuf(observerResponse);
LOG_DEBUG("Observer responded to admin message");
} else if (observerResult == AdminMessageHandleResult::HANDLED) {
LOG_DEBUG("Observer handled admin message");
}
// If asked for a response and it is not yet set, generate an 'ACK' response
if (mp.decoded.want_response && !myReply) {
myReply = allocErrorResponse(meshtastic_Routing_Error_NONE, &mp);
}
// Allow any observers (e.g. the UI) to respond to this event
notifyObservers(r);
return handled;
}
@@ -507,7 +558,7 @@ void AdminModule::handleSetOwner(const meshtastic_User &o)
if (owner.has_is_unmessagable != o.has_is_unmessagable ||
(o.has_is_unmessagable && owner.is_unmessagable != o.is_unmessagable)) {
changed = 1;
owner.has_is_unmessagable = o.has_is_unmessagable || o.has_is_unmessagable;
owner.has_is_unmessagable = owner.has_is_unmessagable || o.has_is_unmessagable;
owner.is_unmessagable = o.is_unmessagable;
}
@@ -620,8 +671,12 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c)
config.has_display = true;
if (config.display.screen_on_secs == c.payload_variant.display.screen_on_secs &&
config.display.flip_screen == c.payload_variant.display.flip_screen &&
config.display.oled == c.payload_variant.display.oled) {
config.display.oled == c.payload_variant.display.oled &&
config.display.displaymode == c.payload_variant.display.displaymode) {
requiresReboot = false;
} else if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR &&
c.payload_variant.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
config.bluetooth.enabled = false;
}
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
if (config.display.wake_on_tap_or_motion == false && c.payload_variant.display.wake_on_tap_or_motion == true &&
@@ -707,11 +762,16 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c)
LOG_INFO("Set config: Security");
config.security = c.payload_variant.security;
#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN) && !(MESHTASTIC_EXCLUDE_PKI)
// We check for a potentially valid private key, and a blank public key, and regen the public key if needed.
if (config.security.private_key.size == 32 && !memfll(config.security.private_key.bytes, 0, 32) &&
(config.security.public_key.size == 0 || memfll(config.security.public_key.bytes, 0, 32))) {
if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) {
config.security.public_key.size = 32;
// If the client set the key to blank, go ahead and regenerate so long as we're not in ham mode
if (!owner.is_licensed && config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
if (config.security.private_key.size != 32) {
crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes);
} else if (config.security.public_key.size != 32) {
// We check for a potentially valid private key, and a blank public key, and regen the public key if needed.
if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) {
config.security.public_key.size = 32;
}
}
}
#endif
@@ -1101,7 +1161,7 @@ void AdminModule::handleGetDeviceConnectionStatus(const meshtastic_MeshPacket &r
#endif
#endif
conn.has_serial = true; // No serial-less devices
#if !EXCLUDE_POWER_FSM
#if !MESHTASTIC_EXCLUDE_POWER_FSM
conn.serial.is_connected = powerFSM.getState() == &stateSERIAL;
#else
conn.serial.is_connected = powerFSM.getState();
@@ -1137,7 +1197,8 @@ void AdminModule::handleGetDeviceUIConfig(const meshtastic_MeshPacket &req)
void AdminModule::reboot(int32_t seconds)
{
LOG_INFO("Reboot in %d seconds", seconds);
screen->startAlert("Rebooting...");
if (screen)
screen->showOverlayBanner("Rebooting...", 0); // stays on screen
rebootAtMsec = (seconds < 0) ? 0 : (millis() + seconds * 1000);
}
@@ -1161,6 +1222,27 @@ void AdminModule::handleStoreDeviceUIConfig(const meshtastic_DeviceUIConfig &uic
void AdminModule::handleSetHamMode(const meshtastic_HamParameters &p)
{
// Validate ham parameters before setting since this would bypass validation in the owner struct
if (*p.call_sign) {
const char *start = p.call_sign;
// Skip all whitespace
while (*start && isspace((unsigned char)*start))
start++;
if (*start == '\0') {
LOG_WARN("Rejected ham call_sign: must contain at least 1 non-whitespace character");
return;
}
}
if (*p.short_name) {
const char *start = p.short_name;
while (*start && isspace((unsigned char)*start))
start++;
if (*start == '\0') {
LOG_WARN("Rejected ham short_name: must contain at least 1 non-whitespace character");
return;
}
}
// Set call sign and override lora limitations for licensed use
strncpy(owner.long_name, p.call_sign, sizeof(owner.long_name));
strncpy(owner.short_name, p.short_name, sizeof(owner.short_name));
@@ -1247,6 +1329,39 @@ bool AdminModule::messageIsRequest(const meshtastic_AdminMessage *r)
return false;
}
void AdminModule::handleSendInputEvent(const meshtastic_AdminMessage_InputEvent &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,
.touchX = inputEvent.touch_x,
.touchY = inputEvent.touch_y};
// Log the event being injected
LOG_INFO("Injecting input event from admin: source=%s, event=%u, char=%c(%u), touch=(%u,%u)", event.source, event.inputEvent,
(event.kbchar >= 32 && event.kbchar <= 126) ? event.kbchar : '?', event.kbchar, event.touchX, event.touchY);
// Wake the device if asleep
powerFSM.trigger(EVENT_INPUT);
#if !defined(MESHTASTIC_EXCLUDE_INPUTBROKER)
// Inject the event through InputBroker
if (inputBroker) {
inputBroker->injectInputEvent(&event);
} else {
LOG_ERROR("InputBroker not available for event injection");
}
#endif
}
void AdminModule::sendWarning(const char *message)
{
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();

View File

@@ -6,10 +6,19 @@
#include "mesh/wifi/WiFiAPClient.h"
#endif
/**
* Datatype passed to Observers by AdminModule, to allow external handling of admin messages
*/
struct AdminModule_ObserverData {
const meshtastic_AdminMessage *request;
meshtastic_AdminMessage *response;
AdminMessageHandleResult *result;
};
/**
* Admin module for admin messages
*/
class AdminModule : public ProtobufModule<meshtastic_AdminMessage>, public Observable<const meshtastic_AdminMessage *>
class AdminModule : public ProtobufModule<meshtastic_AdminMessage>, public Observable<AdminModule_ObserverData *>
{
public:
/** Constructor
@@ -54,6 +63,7 @@ class AdminModule : public ProtobufModule<meshtastic_AdminMessage>, public Obser
void handleSetChannel();
void handleSetHamMode(const meshtastic_HamParameters &req);
void handleStoreDeviceUIConfig(const meshtastic_DeviceUIConfig &uicfg);
void handleSendInputEvent(const meshtastic_AdminMessage_InputEvent &inputEvent);
void reboot(int32_t seconds);
void setPassKey(meshtastic_AdminMessage *res);

File diff suppressed because it is too large Load Diff

View File

@@ -3,27 +3,38 @@
#include "ProtobufModule.h"
#include "input/InputBroker.h"
// ============================
// Enums & Defines
// ============================
enum cannedMessageModuleRunState {
CANNED_MESSAGE_RUN_STATE_DISABLED,
CANNED_MESSAGE_RUN_STATE_INACTIVE,
CANNED_MESSAGE_RUN_STATE_ACTIVE,
CANNED_MESSAGE_RUN_STATE_FREETEXT,
CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE,
CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED,
CANNED_MESSAGE_RUN_STATE_MESSAGE,
CANNED_MESSAGE_RUN_STATE_ACTION_SELECT,
CANNED_MESSAGE_RUN_STATE_ACTION_UP,
CANNED_MESSAGE_RUN_STATE_ACTION_DOWN,
};
enum cannedMessageDestinationType {
CANNED_MESSAGE_DESTINATION_TYPE_NONE,
CANNED_MESSAGE_DESTINATION_TYPE_NODE,
CANNED_MESSAGE_DESTINATION_TYPE_CHANNEL
CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION,
CANNED_MESSAGE_RUN_STATE_FREETEXT,
CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION,
CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER
};
enum CannedMessageModuleIconType { shift, backspace, space, enter };
#define CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT 50
#define CANNED_MESSAGE_MODULE_MESSAGES_SIZE 800
#ifndef CANNED_MESSAGE_MODULE_ENABLE
#define CANNED_MESSAGE_MODULE_ENABLE 0
#endif
// ============================
// Data Structures
// ============================
struct Letter {
String character;
float width;
@@ -33,71 +44,72 @@ struct Letter {
int rectHeight;
};
#define CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT 50
/**
* Sum of CannedMessageModuleConfig part sizes.
*/
#define CANNED_MESSAGE_MODULE_MESSAGES_SIZE 800
struct NodeEntry {
meshtastic_NodeInfoLite *node;
uint32_t lastHeard;
};
#ifndef CANNED_MESSAGE_MODULE_ENABLE
#define CANNED_MESSAGE_MODULE_ENABLE 0
#endif
// ============================
// Main Class
// ============================
class CannedMessageModule : public SinglePortModule, public Observable<const UIFrameEvent *>, private concurrency::OSThread
{
CallbackObserver<CannedMessageModule, const InputEvent *> inputObserver =
CallbackObserver<CannedMessageModule, const InputEvent *>(this, &CannedMessageModule::handleInputEvent);
public:
CannedMessageModule();
void LaunchWithDestination(NodeNum, uint8_t newChannel = 0);
void LaunchFreetextWithDestination(NodeNum, uint8_t newChannel = 0);
// === Emote Picker navigation ===
int emotePickerIndex = 0; // Tracks currently selected emote in the picker
// === Message navigation ===
const char *getCurrentMessage();
const char *getPrevMessage();
const char *getNextMessage();
const char *getMessageByIndex(int index);
const char *getNodeName(NodeNum node);
// === State/UI ===
bool shouldDraw();
bool hasMessages();
// void eventUp();
// void eventDown();
// void eventSelect();
void showTemporaryMessage(const String &message);
void resetSearch();
void updateDestinationSelectionList();
void drawDestinationSelectionScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
bool isCharInputAllowed() const;
String drawWithCursor(String text, int cursor);
// === Emote Picker ===
int handleEmotePickerInput(const InputEvent *event);
void drawEmotePickerScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
// === Admin Handlers ===
void handleGetCannedMessageModuleMessages(const meshtastic_MeshPacket &req, meshtastic_AdminMessage *response);
void handleSetCannedMessageModuleMessages(const char *from_msg);
void showTemporaryMessage(const String &message);
String drawWithCursor(String text, int cursor);
#ifdef RAK14014
cannedMessageModuleRunState getRunState() const { return runState; }
#endif
/*
-Override the wantPacket method. We need the Routing Messages to look for ACKs.
*/
// === Packet Interest Filter ===
virtual bool wantPacket(const meshtastic_MeshPacket *p) override
{
if (p->rx_rssi != 0) {
this->lastRxRssi = p->rx_rssi;
}
if (p->rx_snr > 0) {
this->lastRxSnr = p->rx_snr;
}
switch (p->decoded.portnum) {
case meshtastic_PortNum_ROUTING_APP:
return waitingForAck;
default:
return false;
}
if (p->rx_rssi != 0)
lastRxRssi = p->rx_rssi;
if (p->rx_snr > 0)
lastRxSnr = p->rx_snr;
return (p->decoded.portnum == meshtastic_PortNum_ROUTING_APP) ? waitingForAck : false;
}
protected:
// === Thread Entry Point ===
virtual int32_t runOnce() override;
// === Transmission ===
void sendText(NodeNum dest, ChannelIndex channel, const char *message, bool wantReplies);
void drawHeader(OLEDDisplay *display, int16_t x, int16_t y, char *buffer);
int splitConfiguredMessages();
int getNextIndex();
int getPrevIndex();
@@ -105,58 +117,87 @@ class CannedMessageModule : public SinglePortModule, public Observable<const UIF
#if defined(USE_VIRTUAL_KEYBOARD)
void drawKeyboard(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
String keyForCoordinates(uint x, uint y);
bool shift = false;
int charSet = 0;
void drawShiftIcon(OLEDDisplay *display, int x, int y, float scale = 1);
void drawBackspaceIcon(OLEDDisplay *display, int x, int y, float scale = 1);
void drawEnterIcon(OLEDDisplay *display, int x, int y, float scale = 1);
#endif
char highlight = 0x00;
// === Input Handling ===
int handleInputEvent(const InputEvent *event);
virtual bool wantUIFrame() override { return this->shouldDraw(); }
virtual bool wantUIFrame() override { return shouldDraw(); }
virtual Observable<const UIFrameEvent *> *getUIFrameObservable() override { return this; }
virtual bool interceptingKeyboardInput() override;
#if !HAS_TFT
virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override;
#endif
virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp,
meshtastic_AdminMessage *request,
meshtastic_AdminMessage *response) override;
/** Called to handle a particular incoming message
* @return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be considered
* for it
*/
virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
void loadProtoForModule();
bool saveProtoForModule();
void installDefaultCannedMessageModuleConfig();
int currentMessageIndex = -1;
cannedMessageModuleRunState runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
char payload = 0x00;
unsigned int cursor = 0;
String freetext = ""; // Text Buffer for Freetext Editor
NodeNum dest = NODENUM_BROADCAST;
ChannelIndex channel = 0;
cannedMessageDestinationType destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE;
uint8_t numChannels = 0;
ChannelIndex indexChannels[MAX_NUM_CHANNELS] = {0};
NodeNum incoming = NODENUM_BROADCAST;
bool ack = false; // True means ACK, false means NAK (error_reason != NONE)
bool waitingForAck = false; // Are currently interested in routing packets?
float lastRxSnr = 0;
int32_t lastRxRssi = 0;
private:
// === Input Observers ===
CallbackObserver<CannedMessageModule, const InputEvent *> inputObserver =
CallbackObserver<CannedMessageModule, const InputEvent *>(this, &CannedMessageModule::handleInputEvent);
// === Display and UI ===
int displayHeight = 64;
int destIndex = 0;
int scrollIndex = 0;
int visibleRows = 0;
bool needsUpdate = true;
unsigned long lastUpdateMillis = 0;
String searchQuery;
String freetext;
String temporaryMessage;
// === Message Storage ===
char messageStore[CANNED_MESSAGE_MODULE_MESSAGES_SIZE + 1];
char *messages[CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT];
int messagesCount = 0;
int currentMessageIndex = -1;
// === Routing & Acknowledgment ===
NodeNum dest = NODENUM_BROADCAST; // Destination node for outgoing messages (default: broadcast)
NodeNum incoming = NODENUM_BROADCAST; // Source node from which last ACK/NACK was received
NodeNum lastSentNode = 0; // Tracks the most recent node we sent a message to (for UI display)
ChannelIndex channel = 0; // Channel index used when sending a message
bool ack = false; // True = ACK received, False = NACK or failed
bool waitingForAck = false; // True if we're expecting an ACK and should monitor routing packets
bool lastAckWasRelayed = false; // True if the ACK was relayed through intermediate nodes
uint8_t lastAckHopStart = 0; // Hop start value from the received ACK packet
uint8_t lastAckHopLimit = 0; // Hop limit value from the received ACK packet
float lastRxSnr = 0; // SNR from last received ACK (used for diagnostics/UI)
int32_t lastRxRssi = 0; // RSSI from last received ACK (used for diagnostics/UI)
// === State Tracking ===
cannedMessageModuleRunState runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
char highlight = 0x00;
char payload = 0x00;
unsigned int cursor = 0;
unsigned long lastTouchMillis = 0;
String temporaryMessage;
uint32_t lastFilterUpdate = 0;
static constexpr uint32_t filterDebounceMs = 30;
std::vector<uint8_t> activeChannelIndices;
std::vector<NodeEntry> filteredNodes;
#if defined(USE_VIRTUAL_KEYBOARD)
bool shift = false;
int charSet = 0; // 0=ABC, 1=123
#endif
bool isUpEvent(const InputEvent *event);
bool isDownEvent(const InputEvent *event);
bool isSelectEvent(const InputEvent *event);
bool handleTabSwitch(const InputEvent *event);
int handleDestinationSelectionInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect);
bool handleMessageSelectorInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect);
bool handleFreeTextInput(const InputEvent *event);
#if defined(USE_VIRTUAL_KEYBOARD)
Letter keyboard[2][4][10] = {{{{"Q", 20, 0, 0, 0, 0},

View File

@@ -188,7 +188,7 @@ int32_t ExternalNotificationModule::runOnce()
// Play RTTTL over i2s audio interface if enabled as buzzer
#ifdef HAS_I2S
if (moduleConfig.external_notification.use_i2s_as_buzzer) {
if (moduleConfig.external_notification.use_i2s_as_buzzer && canBuzz()) {
if (audioThread->isPlaying()) {
// Continue playing
} else if (isNagging && (nagCycleCutoff >= millis())) {
@@ -197,7 +197,7 @@ int32_t ExternalNotificationModule::runOnce()
}
#endif
// now let the PWM buzzer play
if (moduleConfig.external_notification.use_pwm && config.device.buzzer_gpio) {
if (moduleConfig.external_notification.use_pwm && config.device.buzzer_gpio && canBuzz()) {
if (rtttl::isPlaying()) {
rtttl::play();
} else if (isNagging && (nagCycleCutoff >= millis())) {
@@ -210,6 +210,18 @@ int32_t ExternalNotificationModule::runOnce()
}
}
/**
* Based on buzzer mode, return true if we can buzz.
*/
bool ExternalNotificationModule::canBuzz()
{
if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED &&
config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_SYSTEM_ONLY) {
return true;
}
return false;
}
bool ExternalNotificationModule::wantPacket(const meshtastic_MeshPacket *p)
{
return MeshService::isTextPayload(p);
@@ -293,6 +305,12 @@ bool ExternalNotificationModule::getExternal(uint8_t index)
return externalCurrentState[index];
}
// Allow other firmware components to determine whether a notification is ongoing
bool ExternalNotificationModule::nagging()
{
return isNagging;
}
void ExternalNotificationModule::stopNow()
{
rtttl::stop();
@@ -338,6 +356,9 @@ ExternalNotificationModule::ExternalNotificationModule()
// moduleConfig.external_notification.alert_message_buzzer = true;
if (moduleConfig.external_notification.enabled) {
if (inputBroker) // put our callback in the inputObserver list
inputObserver.observe(inputBroker);
if (nodeDB->loadProto(rtttlConfigFile, meshtastic_RTTTLConfig_size, sizeof(meshtastic_RTTTLConfig),
&meshtastic_RTTTLConfig_msg, &rtttlConfig) != LoadFileResult::LOAD_SUCCESS) {
memset(rtttlConfig.ringtone, 0, sizeof(rtttlConfig.ringtone));
@@ -364,7 +385,7 @@ ExternalNotificationModule::ExternalNotificationModule()
setExternalState(1, false);
externalTurnedOn[1] = 0;
}
if (moduleConfig.external_notification.output_buzzer) {
if (moduleConfig.external_notification.output_buzzer && canBuzz()) {
if (!moduleConfig.external_notification.use_pwm) {
LOG_INFO("Use Pin %i for buzzer", moduleConfig.external_notification.output_buzzer);
pinMode(moduleConfig.external_notification.output_buzzer, OUTPUT);
@@ -454,7 +475,7 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP
}
}
if (moduleConfig.external_notification.alert_bell_buzzer) {
if (moduleConfig.external_notification.alert_bell_buzzer && canBuzz()) {
if (containsBell) {
LOG_INFO("externalNotificationModule - Notification Bell (Buzzer)");
isNagging = true;
@@ -583,4 +604,13 @@ void ExternalNotificationModule::handleSetRingtone(const char *from_msg)
if (changed) {
nodeDB->saveProto(rtttlConfigFile, meshtastic_RTTTLConfig_size, &meshtastic_RTTTLConfig_msg, &rtttlConfig);
}
}
int ExternalNotificationModule::handleInputEvent(const InputEvent *event)
{
if (nagCycleCutoff != UINT32_MAX) {
stopNow();
return 1;
}
return 0;
}

View File

@@ -3,6 +3,8 @@
#include "SinglePortModule.h"
#include "concurrency/OSThread.h"
#include "configuration.h"
#include "input/InputBroker.h"
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !defined(CONFIG_IDF_TARGET_ESP32C6)
#include <NonBlockingRtttl.h>
#else
@@ -27,11 +29,15 @@ class rtttl
*/
class ExternalNotificationModule : public SinglePortModule, private concurrency::OSThread
{
CallbackObserver<ExternalNotificationModule, const InputEvent *> inputObserver =
CallbackObserver<ExternalNotificationModule, const InputEvent *>(this, &ExternalNotificationModule::handleInputEvent);
uint32_t output = 0;
public:
ExternalNotificationModule();
int handleInputEvent(const InputEvent *arg);
uint32_t nagCycleCutoff = 1;
void setExternalState(uint8_t index = 0, bool on = false);
@@ -40,6 +46,9 @@ class ExternalNotificationModule : public SinglePortModule, private concurrency:
void setMute(bool mute) { isMuted = mute; }
bool getMute() { return isMuted; }
bool canBuzz();
bool nagging();
void stopNow();
void handleGetRingtone(const meshtastic_MeshPacket &req, meshtastic_AdminMessage *response);

View File

@@ -0,0 +1,310 @@
#if !MESHTASTIC_EXCLUDE_PKI
#include "KeyVerificationModule.h"
#include "MeshService.h"
#include "RTC.h"
#include "main.h"
#include "modules/AdminModule.h"
#include <SHA256.h>
KeyVerificationModule *keyVerificationModule;
KeyVerificationModule::KeyVerificationModule()
: ProtobufModule("KeyVerification", meshtastic_PortNum_KEY_VERIFICATION_APP, &meshtastic_KeyVerification_msg)
{
ourPortNum = meshtastic_PortNum_KEY_VERIFICATION_APP;
}
AdminMessageHandleResult KeyVerificationModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp,
meshtastic_AdminMessage *request,
meshtastic_AdminMessage *response)
{
updateState();
if (request->which_payload_variant == meshtastic_AdminMessage_key_verification_tag && mp.from == 0) {
LOG_WARN("Handling Key Verification Admin Message type %u", request->key_verification.message_type);
if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_INITIATE_VERIFICATION &&
currentState == KEY_VERIFICATION_IDLE) {
sendInitialRequest(request->key_verification.remote_nodenum);
} else if (request->key_verification.message_type ==
meshtastic_KeyVerificationAdmin_MessageType_PROVIDE_SECURITY_NUMBER &&
request->key_verification.has_security_number && currentState == KEY_VERIFICATION_SENDER_AWAITING_NUMBER &&
request->key_verification.nonce == currentNonce) {
processSecurityNumber(request->key_verification.security_number);
} else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_VERIFY &&
request->key_verification.nonce == currentNonce) {
auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
resetToIdle();
} else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_NOT_VERIFY) {
resetToIdle();
}
return AdminMessageHandleResult::HANDLED;
}
return AdminMessageHandleResult::NOT_HANDLED;
}
bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_KeyVerification *r)
{
updateState();
if (mp.pki_encrypted == false)
return false;
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) {
memcpy(hash2, r->hash2.bytes, 32);
if (screen)
screen->showOverlayBanner("Enter Security Number", 30000);
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
cn->level = meshtastic_LogRecord_Level_WARNING;
sprintf(cn->message, "Enter Security Number for Key Verification");
cn->which_payload_variant = meshtastic_ClientNotification_key_verification_number_request_tag;
cn->payload_variant.key_verification_number_request.nonce = currentNonce;
strncpy(cn->payload_variant.key_verification_number_request.remote_longname, // should really check for nulls, etc
nodeDB->getMeshNode(currentRemoteNode)->user.long_name,
sizeof(cn->payload_variant.key_verification_number_request.remote_longname));
service->sendClientNotification(cn);
LOG_INFO("Received hash2");
currentState = KEY_VERIFICATION_SENDER_AWAITING_NUMBER;
return true;
} else if (currentState == KEY_VERIFICATION_RECEIVER_AWAITING_HASH1 && r->hash1.size == 32 && r->nonce == currentNonce) {
if (memcmp(hash1, r->hash1.bytes, 32) == 0) {
memset(message, 0, sizeof(message));
sprintf(message, "Verification: \n");
generateVerificationCode(message + 15);
sprintf(message + 24, "\nACCEPT\nREJECT");
LOG_INFO("Hash1 matches!");
if (screen) {
screen->showOverlayBanner(message, 30000, 2, [=](int selected) {
if (selected == 0) {
auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
}
});
}
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
cn->level = meshtastic_LogRecord_Level_WARNING;
sprintf(cn->message, "Final confirmation for incoming manual key verification %s", message);
cn->which_payload_variant = meshtastic_ClientNotification_key_verification_final_tag;
cn->payload_variant.key_verification_final.nonce = currentNonce;
strncpy(cn->payload_variant.key_verification_final.remote_longname, // should really check for nulls, etc
nodeDB->getMeshNode(currentRemoteNode)->user.long_name,
sizeof(cn->payload_variant.key_verification_final.remote_longname));
cn->payload_variant.key_verification_final.isSender = false;
service->sendClientNotification(cn);
currentState = KEY_VERIFICATION_RECEIVER_AWAITING_USER;
return true;
}
}
return false;
}
bool KeyVerificationModule::sendInitialRequest(NodeNum remoteNode)
{
LOG_DEBUG("keyVerification start");
// generate nonce
updateState();
if (currentState != KEY_VERIFICATION_IDLE) {
return false;
}
currentNonce = random();
currentNonceTimestamp = getTime();
currentRemoteNode = remoteNode;
meshtastic_KeyVerification KeyVerification = meshtastic_KeyVerification_init_zero;
KeyVerification.nonce = currentNonce;
KeyVerification.hash2.size = 0;
KeyVerification.hash1.size = 0;
meshtastic_MeshPacket *p = allocDataProtobuf(KeyVerification);
p->to = remoteNode;
p->channel = 0;
p->pki_encrypted = true;
p->decoded.want_response = true;
p->priority = meshtastic_MeshPacket_Priority_HIGH;
service->sendToMesh(p, RX_SRC_LOCAL, true);
currentState = KEY_VERIFICATION_SENDER_HAS_INITIATED;
return true;
}
meshtastic_MeshPacket *KeyVerificationModule::allocReply()
{
SHA256 hash;
NodeNum ourNodeNum = nodeDB->getNodeNum();
updateState();
if (currentState != KEY_VERIFICATION_IDLE) { // TODO: cooldown period
LOG_WARN("Key Verification requested, but already in a request");
return nullptr;
} else if (!currentRequest->pki_encrypted) {
LOG_WARN("Key Verification requested, but not in a PKI packet");
return nullptr;
}
currentState = KEY_VERIFICATION_RECEIVER_AWAITING_HASH1;
auto req = *currentRequest;
const auto &p = req.decoded;
meshtastic_KeyVerification scratch;
meshtastic_KeyVerification response;
meshtastic_MeshPacket *responsePacket = nullptr;
pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_KeyVerification_msg, &scratch);
currentNonce = scratch.nonce;
response.nonce = scratch.nonce;
currentRemoteNode = req.from;
currentNonceTimestamp = getTime();
currentSecurityNumber = random(1, 999999);
// generate hash1
hash.reset();
hash.update(&currentSecurityNumber, sizeof(currentSecurityNumber));
hash.update(&currentNonce, sizeof(currentNonce));
hash.update(&currentRemoteNode, sizeof(currentRemoteNode));
hash.update(&ourNodeNum, sizeof(ourNodeNum));
hash.update(currentRequest->public_key.bytes, currentRequest->public_key.size);
hash.update(owner.public_key.bytes, owner.public_key.size);
hash.finalize(hash1, 32);
// generate hash2
hash.reset();
hash.update(&currentNonce, sizeof(currentNonce));
hash.update(hash1, 32);
hash.finalize(hash2, 32);
response.hash1.size = 0;
response.hash2.size = 32;
memcpy(response.hash2.bytes, hash2, 32);
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);
}
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
cn->level = meshtastic_LogRecord_Level_WARNING;
sprintf(cn->message, "Incoming Key Verification.\nSecurity Number\n%03u %03u", currentSecurityNumber / 1000,
currentSecurityNumber % 1000);
cn->which_payload_variant = meshtastic_ClientNotification_key_verification_number_inform_tag;
cn->payload_variant.key_verification_number_inform.nonce = currentNonce;
strncpy(cn->payload_variant.key_verification_number_inform.remote_longname, // should really check for nulls, etc
nodeDB->getMeshNode(currentRemoteNode)->user.long_name,
sizeof(cn->payload_variant.key_verification_number_inform.remote_longname));
cn->payload_variant.key_verification_number_inform.security_number = currentSecurityNumber;
service->sendClientNotification(cn);
LOG_WARN("Security Number %04u, nonce %llu", currentSecurityNumber, currentNonce);
return responsePacket;
}
void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
{
SHA256 hash;
NodeNum ourNodeNum = nodeDB->getNodeNum();
uint8_t scratch_hash[32] = {0};
LOG_WARN("received security number: %u", incomingNumber);
meshtastic_NodeInfoLite *remoteNodePtr = nullptr;
remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
if (remoteNodePtr == nullptr || !remoteNodePtr->has_user || remoteNodePtr->user.public_key.size != 32) {
currentState = KEY_VERIFICATION_IDLE;
return; // should we throw an error here?
}
LOG_WARN("hashing ");
// calculate hash1
hash.reset();
hash.update(&incomingNumber, sizeof(incomingNumber));
hash.update(&currentNonce, sizeof(currentNonce));
hash.update(&ourNodeNum, sizeof(ourNodeNum));
hash.update(&currentRemoteNode, sizeof(currentRemoteNode));
hash.update(owner.public_key.bytes, owner.public_key.size);
hash.update(remoteNodePtr->user.public_key.bytes, remoteNodePtr->user.public_key.size);
hash.finalize(hash1, 32);
hash.reset();
hash.update(&currentNonce, sizeof(currentNonce));
hash.update(hash1, 32);
hash.finalize(scratch_hash, 32);
if (memcmp(scratch_hash, hash2, 32) != 0) {
LOG_WARN("Hash2 did not match");
return; // should probably throw an error of some sort
}
currentSecurityNumber = incomingNumber;
meshtastic_KeyVerification KeyVerification = meshtastic_KeyVerification_init_zero;
KeyVerification.nonce = currentNonce;
KeyVerification.hash2.size = 0;
KeyVerification.hash1.size = 32;
memcpy(KeyVerification.hash1.bytes, hash1, 32);
meshtastic_MeshPacket *p = allocDataProtobuf(KeyVerification);
p->to = currentRemoteNode;
p->channel = 0;
p->pki_encrypted = true;
p->decoded.want_response = true;
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);
}
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
cn->level = meshtastic_LogRecord_Level_WARNING;
sprintf(cn->message, "Final confirmation for outgoing manual key verification %s", message);
cn->which_payload_variant = meshtastic_ClientNotification_key_verification_final_tag;
cn->payload_variant.key_verification_final.nonce = currentNonce;
strncpy(cn->payload_variant.key_verification_final.remote_longname, // should really check for nulls, etc
nodeDB->getMeshNode(currentRemoteNode)->user.long_name,
sizeof(cn->payload_variant.key_verification_final.remote_longname));
cn->payload_variant.key_verification_final.isSender = true;
service->sendClientNotification(cn);
LOG_INFO(message);
return;
}
void KeyVerificationModule::updateState()
{
if (currentState != KEY_VERIFICATION_IDLE) {
// check for the 30 second timeout
if (currentNonceTimestamp < getTime() - 60) {
resetToIdle();
} else {
currentNonceTimestamp = getTime();
}
}
}
void KeyVerificationModule::resetToIdle()
{
memset(hash1, 0, 32);
memset(hash2, 0, 32);
currentNonce = 0;
currentNonceTimestamp = 0;
currentSecurityNumber = 0;
currentRemoteNode = 0;
currentState = KEY_VERIFICATION_IDLE;
}
void KeyVerificationModule::generateVerificationCode(char *readableCode)
{
for (int i = 0; i < 4; i++) {
// drop the two highest significance bits, then encode as a base64
readableCode[i] = (hash1[i] >> 2) + 48; // not a standardized base64, but workable and avoids having a dictionary.
}
readableCode[4] = ' ';
for (int i = 5; i < 9; i++) {
// drop the two highest significance bits, then encode as a base64
readableCode[i] = (hash1[i] >> 2) + 48; // not a standardized base64, but workable and avoids having a dictionary.
}
}
#endif

View File

@@ -0,0 +1,64 @@
#pragma once
#include "ProtobufModule.h"
#include "SinglePortModule.h"
enum KeyVerificationState {
KEY_VERIFICATION_IDLE,
KEY_VERIFICATION_SENDER_HAS_INITIATED,
KEY_VERIFICATION_SENDER_AWAITING_NUMBER,
KEY_VERIFICATION_SENDER_AWAITING_USER,
KEY_VERIFICATION_RECEIVER_AWAITING_USER,
KEY_VERIFICATION_RECEIVER_AWAITING_HASH1,
};
class KeyVerificationModule : public ProtobufModule<meshtastic_KeyVerification> //, private concurrency::OSThread //
{
// CallbackObserver<KeyVerificationModule, const meshtastic::Status *> nodeStatusObserver =
// CallbackObserver<KeyVerificationModule, const meshtastic::Status *>(this, &KeyVerificationModule::handleStatusUpdate);
public:
KeyVerificationModule();
/* : concurrency::OSThread("KeyVerification"),
ProtobufModule("KeyVerification", meshtastic_PortNum_KEY_VERIFICATION_APP, &meshtastic_KeyVerification_msg)
{
nodeStatusObserver.observe(&nodeStatus->onNewStatus);
setIntervalFromNow(setStartDelay()); // Wait until NodeInfo is sent
}*/
virtual bool wantUIFrame() { return false; };
bool sendInitialRequest(NodeNum remoteNode);
protected:
/* Called to handle a particular incoming message
@return true if you've guaranteed you've handled this message and no other handlers should be considered for it
*/
virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_KeyVerification *p);
// virtual meshtastic_MeshPacket *allocReply() override;
// rather than add to the craziness that is the admin module, just handle those requests here.
virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp,
meshtastic_AdminMessage *request,
meshtastic_AdminMessage *response) override;
/*
* Send our Telemetry into the mesh
*/
bool sendMetrics();
virtual meshtastic_MeshPacket *allocReply() override;
private:
uint64_t currentNonce = 0;
uint32_t currentNonceTimestamp = 0;
NodeNum currentRemoteNode = 0;
uint32_t currentSecurityNumber = 0;
KeyVerificationState currentState = KEY_VERIFICATION_IDLE;
uint8_t hash1[32] = {0}; //
uint8_t hash2[32] = {0}; //
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
};
extern KeyVerificationModule *keyVerificationModule;

View File

@@ -1,17 +1,21 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_INPUTBROKER
#include "buzz/BuzzerFeedbackThread.h"
#include "input/ExpressLRSFiveWay.h"
#include "input/InputBroker.h"
#include "input/RotaryEncoderInterruptImpl1.h"
#include "input/ScanAndSelect.h"
#include "input/SerialKeyboardImpl.h"
#include "input/TrackballInterruptImpl1.h"
#include "input/UpDownInterruptImpl1.h"
#include "modules/SystemCommandsModule.h"
#if !MESHTASTIC_EXCLUDE_I2C
#include "input/cardKbI2cImpl.h"
#endif
#include "input/kbMatrixImpl.h"
#endif
#if !MESHTASTIC_EXCLUDE_PKI
#include "KeyVerificationModule.h"
#endif
#if !MESHTASTIC_EXCLUDE_ADMIN
#include "modules/AdminModule.h"
#endif
@@ -62,6 +66,7 @@
#include "modules/Telemetry/AirQualityTelemetry.h"
#include "modules/Telemetry/EnvironmentTelemetry.h"
#include "modules/Telemetry/HealthTelemetry.h"
#include "modules/Telemetry/Sensor/TelemetrySensor.h"
#endif
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_POWER_TELEMETRY
#include "modules/Telemetry/PowerTelemetry.h"
@@ -104,7 +109,11 @@ void setupModules()
{
if (config.device.role != meshtastic_Config_DeviceConfig_Role_REPEATER) {
#if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER
inputBroker = new InputBroker();
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
inputBroker = new InputBroker();
systemCommandsModule = new SystemCommandsModule();
buzzerFeedbackThread = new BuzzerFeedbackThread();
}
#endif
#if !MESHTASTIC_EXCLUDE_ADMIN
adminModule = new AdminModule();
@@ -133,7 +142,9 @@ void setupModules()
#if !MESHTASTIC_EXCLUDE_ATAK
atakPluginModule = new AtakPluginModule();
#endif
#if !MESHTASTIC_EXCLUDE_PKI
keyVerificationModule = new KeyVerificationModule();
#endif
#if !MESHTASTIC_EXCLUDE_DROPZONE
dropzoneModule = new DropzoneModule();
#endif
@@ -152,50 +163,49 @@ void setupModules()
// Example: Put your module here
// new ReplyModule();
#if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER
rotaryEncoderInterruptImpl1 = new RotaryEncoderInterruptImpl1();
if (!rotaryEncoderInterruptImpl1->init()) {
delete rotaryEncoderInterruptImpl1;
rotaryEncoderInterruptImpl1 = nullptr;
}
upDownInterruptImpl1 = new UpDownInterruptImpl1();
if (!upDownInterruptImpl1->init()) {
delete upDownInterruptImpl1;
upDownInterruptImpl1 = nullptr;
}
#if HAS_SCREEN
// In order to have the user button dismiss the canned message frame, this class lightly interacts with the Screen class
scanAndSelectInput = new ScanAndSelectInput();
if (!scanAndSelectInput->init()) {
delete scanAndSelectInput;
scanAndSelectInput = nullptr;
}
#endif
cardKbI2cImpl = new CardKbI2cImpl();
cardKbI2cImpl->init();
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
rotaryEncoderInterruptImpl1 = new RotaryEncoderInterruptImpl1();
if (!rotaryEncoderInterruptImpl1->init()) {
delete rotaryEncoderInterruptImpl1;
rotaryEncoderInterruptImpl1 = nullptr;
}
upDownInterruptImpl1 = new UpDownInterruptImpl1();
if (!upDownInterruptImpl1->init()) {
delete upDownInterruptImpl1;
upDownInterruptImpl1 = nullptr;
}
cardKbI2cImpl = new CardKbI2cImpl();
cardKbI2cImpl->init();
#ifdef INPUTBROKER_MATRIX_TYPE
kbMatrixImpl = new KbMatrixImpl();
kbMatrixImpl->init();
kbMatrixImpl = new KbMatrixImpl();
kbMatrixImpl->init();
#endif // INPUTBROKER_MATRIX_TYPE
#ifdef INPUTBROKER_SERIAL_TYPE
aSerialKeyboardImpl = new SerialKeyboardImpl();
aSerialKeyboardImpl->init();
aSerialKeyboardImpl = new SerialKeyboardImpl();
aSerialKeyboardImpl->init();
#endif // INPUTBROKER_MATRIX_TYPE
}
#endif // HAS_BUTTON
#if ARCH_PORTDUINO && !HAS_TFT
aLinuxInputImpl = new LinuxInputImpl();
aLinuxInputImpl->init();
#if ARCH_PORTDUINO
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
aLinuxInputImpl = new LinuxInputImpl();
aLinuxInputImpl->init();
}
#endif
#if HAS_TRACKBALL && !MESHTASTIC_EXCLUDE_INPUTBROKER
trackballInterruptImpl1 = new TrackballInterruptImpl1();
trackballInterruptImpl1->init();
#if !MESHTASTIC_EXCLUDE_INPUTBROKER
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
trackballInterruptImpl1 = new TrackballInterruptImpl1();
trackballInterruptImpl1->init(TB_DOWN, TB_UP, TB_LEFT, TB_RIGHT, TB_PRESS);
}
#endif
#ifdef INPUTBROKER_EXPRESSLRSFIVEWAY_TYPE
expressLRSFiveWayInput = new ExpressLRSFiveWay();
#endif
#if HAS_SCREEN && !MESHTASTIC_EXCLUDE_CANNEDMESSAGES
cannedMessageModule = new CannedMessageModule();
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
cannedMessageModule = new CannedMessageModule();
}
#endif
#if ARCH_PORTDUINO
new HostMetricsModule();
@@ -221,7 +231,9 @@ void setupModules()
#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \
!defined(CONFIG_IDF_TARGET_ESP32C3)
#if !MESHTASTIC_EXCLUDE_SERIAL
new SerialModule();
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
new SerialModule();
}
#endif
#endif
#ifdef ARCH_ESP32

View File

@@ -21,13 +21,6 @@ bool NodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mes
bool wasBroadcast = isBroadcast(mp.to);
// Show new nodes on LCD screen
if (wasBroadcast) {
String lcd = String("Joined: ") + p.long_name + "\n";
if (screen)
screen->print(lcd.c_str());
}
// if user has changed while packet was not for us, inform phone
if (hasChanged && !wasBroadcast && !isToUs(&mp))
service->sendToPhone(packetPool.allocCopy(mp));

View File

@@ -265,7 +265,6 @@ meshtastic_MeshPacket *PositionModule::allocPositionPacket()
}
LOG_INFO("Position packet: time=%i lat=%i lon=%i", p.time, p.latitude_i, p.longitude_i);
lastSentToMesh = millis();
// 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)
@@ -276,13 +275,18 @@ meshtastic_MeshPacket *PositionModule::allocPositionPacket()
meshtastic_MeshPacket *PositionModule::allocReply()
{
if (config.device.role != meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND && lastSentToMesh &&
Throttle::isWithinTimespanMs(lastSentToMesh, 3 * 60 * 1000)) {
LOG_DEBUG("Skip Position reply since we sent it <3min ago");
if (config.device.role != meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND && lastSentReply &&
Throttle::isWithinTimespanMs(lastSentReply, 3 * 60 * 1000)) {
LOG_DEBUG("Skip Position reply since we sent a reply <3min ago");
ignoreRequest = true; // Mark it as ignored for MeshModule
return nullptr;
}
return allocPositionPacket();
meshtastic_MeshPacket *reply = allocPositionPacket();
if (reply) {
lastSentReply = millis(); // Track when we sent this reply
}
return reply;
}
meshtastic_MeshPacket *PositionModule::allocAtakPli()
@@ -328,7 +332,13 @@ 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);
sendOurPosition(NODENUM_BROADCAST, 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) {
sendOurPosition(NODENUM_BROADCAST, requestReplies, channelNum);
return;
}
}
}
void PositionModule::sendOurPosition(NodeNum dest, bool wantReplies, uint8_t channel)
@@ -340,11 +350,6 @@ void PositionModule::sendOurPosition(NodeNum dest, bool wantReplies, uint8_t cha
// 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;
} else if (channels.getByIndex(channel).role == meshtastic_Channel_Role_PRIMARY) {
// backwards compatibility for Primary channels created before position_precision was set by default
precision = 13;
} else {
precision = 0;
}
meshtastic_MeshPacket *p = allocPositionPacket();

View File

@@ -63,7 +63,7 @@ class PositionModule : public ProtobufModule<meshtastic_Position>, private concu
void sendLostAndFoundText();
bool hasQualityTimesource();
bool hasGPS();
uint32_t lastSentToMesh = 0; // Last time we sent our position to the mesh
uint32_t lastSentReply = 0; // Last time we sent a position reply (used for reply throttling only)
const uint32_t minimumTimeThreshold =
Default::getConfiguredOrDefaultMs(config.position.broadcast_smart_minimum_interval_secs, 30);

View File

@@ -83,9 +83,6 @@ bool RemoteHardwareModule::handleReceivedProtobuf(const meshtastic_MeshPacket &r
switch (p.type) {
case meshtastic_HardwareMessage_Type_WRITE_GPIOS: {
// Print notification to LCD screen
screen->print("Write GPIOs\n");
pinModes(p.gpio_mask, OUTPUT, availablePins);
for (uint8_t i = 0; i < NUM_GPIOS; i++) {
uint64_t mask = 1ULL << i;
@@ -98,10 +95,6 @@ bool RemoteHardwareModule::handleReceivedProtobuf(const meshtastic_MeshPacket &r
}
case meshtastic_HardwareMessage_Type_READ_GPIOS: {
// Print notification to LCD screen
if (screen)
screen->print("Read GPIOs\n");
uint64_t res = digitalReads(p.gpio_mask, availablePins);
// Send the reply

View File

@@ -15,8 +15,6 @@ meshtastic_MeshPacket *ReplyModule::allocReply()
LOG_INFO("Received message from=0x%0x, id=%d, msg=%.*s", req.from, req.id, p.payload.size, p.payload.bytes);
#endif
screen->print("Send reply\n");
const char *replyStr = "Message Received";
auto reply = allocDataPacket(); // Allocate a packet for sending
reply->decoded.payload.size = strlen(replyStr); // You must specify how many bytes are in the reply

View File

@@ -341,7 +341,7 @@ ProcessMessage SerialModuleRadio::handleReceived(const meshtastic_MeshPacket &mp
serialPrint->write(p.payload.bytes, p.payload.size);
} else if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp));
String sender = (node && node->has_user) ? node->user.short_name : "???";
const char *sender = (node && node->has_user) ? node->user.short_name : "???";
serialPrint->println();
serialPrint->printf("%s: %s", sender, p.payload.bytes);
serialPrint->println();
@@ -410,8 +410,8 @@ uint32_t SerialModule::getBaudRate()
// Add this structure to help with parsing WindGust = 24.4 serial lines.
struct ParsedLine {
String name;
String value;
char name[64];
char value[128];
};
/**
@@ -438,16 +438,30 @@ ParsedLine parseLine(const char *line)
strncpy(nameBuf, line, nameLen);
nameBuf[nameLen] = '\0';
// Create trimmed name string
String name = String(nameBuf);
name.trim();
// Trim whitespace from name
char *nameStart = nameBuf;
while (*nameStart && isspace(*nameStart))
nameStart++;
char *nameEnd = nameStart + strlen(nameStart) - 1;
while (nameEnd > nameStart && isspace(*nameEnd))
*nameEnd-- = '\0';
// Extract value after equals sign
String value = String(equals + 1);
value.trim();
// Copy trimmed name
strncpy(result.name, nameStart, sizeof(result.name) - 1);
result.name[sizeof(result.name) - 1] = '\0';
// Extract value part (after equals)
const char *valueStart = equals + 1;
while (*valueStart && isspace(*valueStart))
valueStart++;
strncpy(result.value, valueStart, sizeof(result.value) - 1);
result.value[sizeof(result.value) - 1] = '\0';
// Trim trailing whitespace from value
char *valueEnd = result.value + strlen(result.value) - 1;
while (valueEnd > result.value && isspace(*valueEnd))
*valueEnd-- = '\0';
result.name = name;
result.value = value;
return result;
}
@@ -517,16 +531,16 @@ void SerialModule::processWXSerial()
memcpy(line, &serialBytes[lineStart], lineEnd - lineStart);
ParsedLine parsed = parseLine(line);
if (parsed.name.length() > 0) {
if (parsed.name == "WindDir") {
strlcpy(windDir, parsed.value.c_str(), sizeof(windDir));
if (strlen(parsed.name) > 0) {
if (strcmp(parsed.name, "WindDir") == 0) {
strlcpy(windDir, parsed.value, sizeof(windDir));
double radians = GeoCoord::toRadians(strtof(windDir, nullptr));
dir_sum_sin += sin(radians);
dir_sum_cos += cos(radians);
dirCount++;
gotwind = true;
} else if (parsed.name == "WindSpeed") {
strlcpy(windVel, parsed.value.c_str(), sizeof(windVel));
} else if (strcmp(parsed.name, "WindSpeed") == 0) {
strlcpy(windVel, parsed.value, sizeof(windVel));
float newv = strtof(windVel, nullptr);
velSum += newv;
velCount++;
@@ -534,28 +548,28 @@ void SerialModule::processWXSerial()
lull = newv;
}
gotwind = true;
} else if (parsed.name == "WindGust") {
strlcpy(windGust, parsed.value.c_str(), sizeof(windGust));
} else if (strcmp(parsed.name, "WindGust") == 0) {
strlcpy(windGust, parsed.value, sizeof(windGust));
float newg = strtof(windGust, nullptr);
if (newg > gust) {
gust = newg;
}
gotwind = true;
} else if (parsed.name == "BatVoltage") {
strlcpy(batVoltage, parsed.value.c_str(), sizeof(batVoltage));
} else if (strcmp(parsed.name, "BatVoltage") == 0) {
strlcpy(batVoltage, parsed.value, sizeof(batVoltage));
batVoltageF = strtof(batVoltage, nullptr);
break; // last possible data we want so break
} else if (parsed.name == "CapVoltage") {
strlcpy(capVoltage, parsed.value.c_str(), sizeof(capVoltage));
} else if (strcmp(parsed.name, "CapVoltage") == 0) {
strlcpy(capVoltage, parsed.value, sizeof(capVoltage));
capVoltageF = strtof(capVoltage, nullptr);
} else if (parsed.name == "GXTS04Temp" || parsed.name == "Temperature") {
strlcpy(temperature, parsed.value.c_str(), sizeof(temperature));
} else if (strcmp(parsed.name, "GXTS04Temp") == 0 || strcmp(parsed.name, "Temperature") == 0) {
strlcpy(temperature, parsed.value, sizeof(temperature));
temperatureF = strtof(temperature, nullptr);
} else if (parsed.name == "RainIntSum") {
strlcpy(rainStr, parsed.value.c_str(), sizeof(rainStr));
} else if (strcmp(parsed.name, "RainIntSum") == 0) {
strlcpy(rainStr, parsed.value, sizeof(rainStr));
rainSum = int(strtof(rainStr, nullptr));
} else if (parsed.name == "Rain") {
strlcpy(rainStr, parsed.value.c_str(), sizeof(rainStr));
} else if (strcmp(parsed.name, "Rain") == 0) {
strlcpy(rainStr, parsed.value, sizeof(rainStr));
rain = strtof(rainStr, nullptr);
}
}

View File

@@ -0,0 +1,118 @@
#include "SystemCommandsModule.h"
#include "meshUtils.h"
#if HAS_SCREEN
#include "graphics/Screen.h"
#include "graphics/SharedUIDisplay.h"
#endif
#include "GPS.h"
#include "MeshService.h"
#include "Module.h"
#include "NodeDB.h"
#include "main.h"
#include "modules/AdminModule.h"
#include "modules/ExternalNotificationModule.h"
SystemCommandsModule *systemCommandsModule;
SystemCommandsModule::SystemCommandsModule()
{
if (inputBroker)
inputObserver.observe(inputBroker);
}
int SystemCommandsModule::handleInputEvent(const InputEvent *event)
{
LOG_INFO("Input event %u! kb %u", event->inputEvent, event->kbchar);
// System commands (all others fall through)
switch (event->kbchar) {
// Fn key symbols
case INPUT_BROKER_MSG_FN_SYMBOL_ON:
IF_SCREEN(screen->setFunctionSymbol("Fn"));
return 0;
case INPUT_BROKER_MSG_FN_SYMBOL_OFF:
IF_SCREEN(screen->removeFunctionSymbol("Fn"));
return 0;
// Brightness
case INPUT_BROKER_MSG_BRIGHTNESS_UP:
IF_SCREEN(screen->increaseBrightness());
LOG_DEBUG("Increase Screen Brightness");
return 0;
case INPUT_BROKER_MSG_BRIGHTNESS_DOWN:
IF_SCREEN(screen->decreaseBrightness());
LOG_DEBUG("Decrease Screen Brightness");
return 0;
// Mute
case INPUT_BROKER_MSG_MUTE_TOGGLE:
if (moduleConfig.external_notification.enabled && externalNotificationModule) {
bool isMuted = externalNotificationModule->getMute();
externalNotificationModule->setMute(!isMuted);
IF_SCREEN(graphics::isMuted = !isMuted; if (!isMuted) externalNotificationModule->stopNow();
screen->showOverlayBanner(isMuted ? "Notifications\nEnabled" : "Notifications\nDisabled", 3000);)
}
return 0;
// Bluetooth
case INPUT_BROKER_MSG_BLUETOOTH_TOGGLE:
config.bluetooth.enabled = !config.bluetooth.enabled;
LOG_INFO("User toggled Bluetooth");
nodeDB->saveToDisk();
#if defined(ARDUINO_ARCH_NRF52)
if (!config.bluetooth.enabled) {
disableBluetooth();
IF_SCREEN(screen->showOverlayBanner("Bluetooth OFF\nRebooting", 3000));
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 2000;
} else {
IF_SCREEN(screen->showOverlayBanner("Bluetooth ON\nRebooting", 3000));
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
}
#else
if (!config.bluetooth.enabled) {
disableBluetooth();
IF_SCREEN(screen->showOverlayBanner("Bluetooth OFF", 3000));
} else {
IF_SCREEN(screen->showOverlayBanner("Bluetooth ON\nRebooting", 3000));
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
}
#endif
return 0;
case INPUT_BROKER_MSG_REBOOT:
IF_SCREEN(screen->showOverlayBanner("Rebooting...", 0));
nodeDB->saveToDisk();
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
// runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
return true;
}
switch (event->inputEvent) {
// GPS
case INPUT_BROKER_GPS_TOGGLE:
LOG_WARN("GPS Toggle");
#if !MESHTASTIC_EXCLUDE_GPS
if (gps) {
LOG_WARN("GPS Toggle2");
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);)
}
#endif
return true;
// Mesh ping
case INPUT_BROKER_SEND_PING:
service->refreshLocalMeshNode();
if (service->trySendPosition(NODENUM_BROADCAST, true)) {
IF_SCREEN(screen->showOverlayBanner("Position\nUpdate Sent", 3000));
} else {
IF_SCREEN(screen->showOverlayBanner("Node Info\nUpdate Sent", 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;
return true;
}
return false;
}

View File

@@ -0,0 +1,19 @@
#pragma once
#include "MeshModule.h"
#include "configuration.h"
#include "input/InputBroker.h"
#include <Arduino.h>
#include <functional>
class SystemCommandsModule
{
CallbackObserver<SystemCommandsModule, const InputEvent *> inputObserver =
CallbackObserver<SystemCommandsModule, const InputEvent *>(this, &SystemCommandsModule::handleInputEvent);
public:
SystemCommandsModule();
int handleInputEvent(const InputEvent *event);
};
extern SystemCommandsModule *systemCommandsModule;

View File

@@ -1,6 +1,6 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include("Adafruit_PM25AQI.h")
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include("Adafruit_PM25AQI.h")
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "AirQualityTelemetry.h"

View File

@@ -1,6 +1,6 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "Default.h"
@@ -11,20 +11,27 @@
#include "RTC.h"
#include "Router.h"
#include "UnitConversions.h"
#include "buzz.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/images.h"
#include "main.h"
#include "modules/ExternalNotificationModule.h"
#include "power.h"
#include "sleep.h"
#include "target_specific.h"
#include <OLEDDisplay.h>
#include <OLEDDisplayUi.h>
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL
// Sensors
// Sensors
#include "Sensor/CGRadSensSensor.h"
#include "Sensor/RCWL9620Sensor.h"
#include "Sensor/nullSensor.h"
namespace graphics
{
extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr);
}
#if __has_include(<Adafruit_AHTX0.h>)
#include "Sensor/AHT10.h"
AHT10Sensor aht10Sensor;
@@ -101,6 +108,13 @@ SHTC3Sensor shtc3Sensor;
NullSensor shtc3Sensor;
#endif
#if __has_include("RAK12035_SoilMoisture.h") && defined(RAK_4631) && RAK_4631 == 1
#include "Sensor/RAK12035Sensor.h"
RAK12035Sensor rak12035Sensor;
#else
NullSensor rak12035Sensor;
#endif
#if __has_include(<Adafruit_VEML7700.h>)
#include "Sensor/VEML7700Sensor.h"
VEML7700Sensor veml7700Sensor;
@@ -173,6 +187,7 @@ NullSensor pct2075Sensor;
RCWL9620Sensor rcwl9620Sensor;
CGRadSensSensor cgRadSens;
#endif
#ifdef T1000X_SENSOR_EN
#include "Sensor/T1000xSensor.h"
@@ -182,6 +197,7 @@ T1000xSensor t1000xSensor;
#include "Sensor/IndicatorSensor.h"
IndicatorSensor indicatorSensor;
#endif
#define FAILED_STATE_SENSOR_READ_MULTIPLIER 10
#define DISPLAY_RECEIVEID_MEASUREMENTS_ON_SCREEN true
@@ -288,6 +304,11 @@ int32_t EnvironmentTelemetryModule::runOnce()
result = rak9154Sensor.runOnce();
#endif
#if __has_include("RAK12035_SoilMoisture.h") && defined(RAK_4631) && RAK_4631 == 1
if (rak12035Sensor.hasSensor()) {
result = rak12035Sensor.runOnce();
}
#endif
#endif
}
// it's possible to have this module enabled, only for displaying values on the screen.
@@ -330,120 +351,152 @@ bool EnvironmentTelemetryModule::wantUIFrame()
void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
display->setTextAlignment(TEXT_ALIGN_LEFT);
// === Setup display ===
display->clear();
display->setFont(FONT_SMALL);
display->setTextAlignment(TEXT_ALIGN_LEFT);
int line = 1;
if (lastMeasurementPacket == nullptr) {
// If there's no valid packet, display "Environment"
display->drawString(x, y, "Environment");
display->drawString(x, y += _fontHeight(FONT_SMALL), "No measurement");
// === Set Title
const char *titleStr = (SCREEN_WIDTH > 128) ? "Environment" : "Env.";
// === Header ===
graphics::drawCommonHeader(display, x, y, titleStr);
// === Row spacing setup ===
const int rowHeight = FONT_HEIGHT_SMALL - 4;
int currentY = graphics::getTextPositions(display)[line++];
// === Show "No Telemetry" if no data available ===
if (!lastMeasurementPacket) {
display->drawString(x, currentY, "No Telemetry");
return;
}
// Decode the last measurement packet
meshtastic_Telemetry lastMeasurement;
uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket);
const char *lastSender = getSenderShortName(*lastMeasurementPacket);
// Decode the telemetry message from the latest received packet
const meshtastic_Data &p = lastMeasurementPacket->decoded;
if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &lastMeasurement)) {
display->drawString(x, y, "Measurement Error");
LOG_ERROR("Unable to decode last packet");
meshtastic_Telemetry telemetry;
if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &telemetry)) {
display->drawString(x, currentY, "No Telemetry");
return;
}
// Display "Env. From: ..." on its own
display->drawString(x, y, "Env. From: " + String(lastSender) + " (" + String(agoSecs) + "s)");
const auto &m = telemetry.variant.environment_metrics;
// Prepare sensor data strings
String sensorData[10];
int sensorCount = 0;
// Check if any telemetry field has valid data
bool hasAny = m.has_temperature || m.has_relative_humidity || m.barometric_pressure != 0 || m.iaq != 0 || m.voltage != 0 ||
m.current != 0 || m.lux != 0 || m.white_lux != 0 || m.weight != 0 || m.distance != 0 || m.radiation != 0;
if (lastMeasurement.variant.environment_metrics.has_temperature ||
lastMeasurement.variant.environment_metrics.has_relative_humidity) {
String last_temp = String(lastMeasurement.variant.environment_metrics.temperature, 0) + "°C";
if (moduleConfig.telemetry.environment_display_fahrenheit) {
last_temp =
String(UnitConversions::CelsiusToFahrenheit(lastMeasurement.variant.environment_metrics.temperature), 0) + "°F";
if (!hasAny) {
display->drawString(x, currentY, "No Telemetry");
return;
}
// === First line: Show sender name + time since received (left), and first metric (right) ===
const char *sender = getSenderShortName(*lastMeasurementPacket);
uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket);
String agoStr = (agoSecs > 864000) ? "?"
: (agoSecs > 3600) ? String(agoSecs / 3600) + "h"
: (agoSecs > 60) ? String(agoSecs / 60) + "m"
: String(agoSecs) + "s";
String leftStr = String(sender) + " (" + agoStr + ")";
display->drawString(x, currentY, leftStr); // Left side: who and when
// === Collect sensor readings as label strings (no icons) ===
std::vector<String> entries;
if (m.has_temperature) {
String tempStr = moduleConfig.telemetry.environment_display_fahrenheit
? "Tmp: " + String(UnitConversions::CelsiusToFahrenheit(m.temperature), 1) + "°F"
: "Tmp: " + String(m.temperature, 1) + "°C";
entries.push_back(tempStr);
}
if (m.has_relative_humidity)
entries.push_back("Hum: " + String(m.relative_humidity, 0) + "%");
if (m.barometric_pressure != 0)
entries.push_back("Prss: " + String(m.barometric_pressure, 0) + " hPa");
if (m.iaq != 0) {
String aqi = "IAQ: " + String(m.iaq);
const char *bannerMsg = nullptr; // Default: no banner
if (m.iaq <= 25)
aqi += " (Excellent)";
else if (m.iaq <= 50)
aqi += " (Good)";
else if (m.iaq <= 100)
aqi += " (Moderate)";
else if (m.iaq <= 150)
aqi += " (Poor)";
else if (m.iaq <= 200) {
aqi += " (Unhealthy)";
bannerMsg = "Unhealthy IAQ";
} else if (m.iaq <= 300) {
aqi += " (Very Unhealthy)";
bannerMsg = "Very Unhealthy IAQ";
} else {
aqi += " (Hazardous)";
bannerMsg = "Hazardous IAQ";
}
sensorData[sensorCount++] =
"Temp/Hum: " + last_temp + " / " + String(lastMeasurement.variant.environment_metrics.relative_humidity, 0) + "%";
}
entries.push_back(aqi);
if (lastMeasurement.variant.environment_metrics.barometric_pressure != 0) {
sensorData[sensorCount++] =
"Press: " + String(lastMeasurement.variant.environment_metrics.barometric_pressure, 0) + "hPA";
}
// === IAQ alert logic ===
static uint32_t lastAlertTime = 0;
uint32_t now = millis();
if (lastMeasurement.variant.environment_metrics.voltage != 0) {
sensorData[sensorCount++] = "Volt/Cur: " + String(lastMeasurement.variant.environment_metrics.voltage, 0) + "V / " +
String(lastMeasurement.variant.environment_metrics.current, 0) + "mA";
}
bool isOwnTelemetry = lastMeasurementPacket->from == nodeDB->getNodeNum();
bool isCooldownOver = (now - lastAlertTime > 60000);
if (lastMeasurement.variant.environment_metrics.iaq != 0) {
sensorData[sensorCount++] = "IAQ: " + String(lastMeasurement.variant.environment_metrics.iaq);
}
if (isOwnTelemetry && bannerMsg && isCooldownOver) {
LOG_INFO("drawFrame: IAQ %d (own) — showing banner: %s", m.iaq, bannerMsg);
screen->showOverlayBanner(bannerMsg, 3000);
if (lastMeasurement.variant.environment_metrics.distance != 0) {
sensorData[sensorCount++] = "Water Level: " + String(lastMeasurement.variant.environment_metrics.distance, 0) + "mm";
}
if (lastMeasurement.variant.environment_metrics.weight != 0) {
sensorData[sensorCount++] = "Weight: " + String(lastMeasurement.variant.environment_metrics.weight, 0) + "kg";
}
if (lastMeasurement.variant.environment_metrics.radiation != 0) {
sensorData[sensorCount++] = "Rad: " + String(lastMeasurement.variant.environment_metrics.radiation, 2) + "µR/h";
}
if (lastMeasurement.variant.environment_metrics.lux != 0) {
sensorData[sensorCount++] = "Illuminance: " + String(lastMeasurement.variant.environment_metrics.lux, 2) + "lx";
}
if (lastMeasurement.variant.environment_metrics.white_lux != 0) {
sensorData[sensorCount++] = "W_Lux: " + String(lastMeasurement.variant.environment_metrics.white_lux, 2) + "lx";
}
static int scrollOffset = 0;
static bool scrollingDown = true;
static uint32_t lastScrollTime = millis();
// Determine how many lines we can fit on display
// Calculated once only: display dimensions don't change during runtime.
static int maxLines = 0;
if (!maxLines) {
const int16_t paddingTop = _fontHeight(FONT_SMALL); // Heading text
const int16_t paddingBottom = 8; // Indicator dots
maxLines = (display->getHeight() - paddingTop - paddingBottom) / _fontHeight(FONT_SMALL);
assert(maxLines > 0);
}
// Draw as many lines of data as we can fit
int linesToShow = min(maxLines, sensorCount);
for (int i = 0; i < linesToShow; i++) {
int index = (scrollOffset + i) % sensorCount;
display->drawString(x, y += _fontHeight(FONT_SMALL), sensorData[index]);
}
// Only scroll if there are more than 3 sensor data lines
if (sensorCount > 3) {
// Update scroll offset every 5 seconds
if (millis() - lastScrollTime > 5000) {
if (scrollingDown) {
scrollOffset++;
if (scrollOffset + linesToShow >= sensorCount) {
scrollingDown = false;
}
} else {
scrollOffset--;
if (scrollOffset <= 0) {
scrollingDown = true;
}
// Only buzz if IAQ is over 200
if (m.iaq > 200 && moduleConfig.external_notification.enabled && !externalNotificationModule->getMute()) {
playLongBeep();
}
lastScrollTime = millis();
lastAlertTime = now;
}
}
if (m.voltage != 0 || m.current != 0)
entries.push_back(String(m.voltage, 1) + "V / " + String(m.current, 0) + "mA");
if (m.lux != 0)
entries.push_back("Light: " + String(m.lux, 0) + "lx");
if (m.white_lux != 0)
entries.push_back("White: " + String(m.white_lux, 0) + "lx");
if (m.weight != 0)
entries.push_back("Weight: " + String(m.weight, 0) + "kg");
if (m.distance != 0)
entries.push_back("Level: " + String(m.distance, 0) + "mm");
if (m.radiation != 0)
entries.push_back("Rad: " + String(m.radiation, 2) + " µR/h");
// === Show first available metric on top-right of first line ===
if (!entries.empty()) {
String valueStr = entries.front();
int rightX = SCREEN_WIDTH - display->getStringWidth(valueStr);
display->drawString(rightX, currentY, valueStr);
entries.erase(entries.begin()); // Remove from queue
}
// === Advance to next line for remaining telemetry entries ===
currentY += rowHeight;
// === Draw remaining entries in 2-column format (left and right) ===
for (size_t i = 0; i < entries.size(); i += 2) {
// Left column
display->drawString(x, currentY, entries[i]);
// Right column if it exists
if (i + 1 < entries.size()) {
int rightX = SCREEN_WIDTH / 2;
display->drawString(rightX, currentY, entries[i + 1]);
}
currentY += rowHeight;
}
}
bool EnvironmentTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t)
@@ -625,6 +678,14 @@ bool EnvironmentTelemetryModule::getEnvironmentTelemetry(meshtastic_Telemetry *m
valid = valid && rak9154Sensor.getMetrics(m);
hasSensor = true;
#endif
#if __has_include("RAK12035_SoilMoisture.h") && defined(RAK_4631) && \
RAK_4631 == \
1 // Not really needed, but may as well just skip at a lower level it if no library or not a RAK_4631
if (rak12035Sensor.hasSensor()) {
valid = valid && rak12035Sensor.getMetrics(m);
hasSensor = true;
}
#endif
#endif
return valid && hasSensor;
}
@@ -679,6 +740,9 @@ bool EnvironmentTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
LOG_INFO("Send: radiation=%fµR/h", m.variant.environment_metrics.radiation);
LOG_INFO("Send: soil_temperature=%f, soil_moisture=%u", m.variant.environment_metrics.soil_temperature,
m.variant.environment_metrics.soil_moisture);
sensor_read_error_count = 0;
meshtastic_MeshPacket *p = allocDataProtobuf(m);
@@ -850,8 +914,17 @@ AdminMessageHandleResult EnvironmentTelemetryModule::handleAdminMessageForModule
if (result != AdminMessageHandleResult::NOT_HANDLED)
return result;
}
#if __has_include("RAK12035_SoilMoisture.h") && defined(RAK_4631) && \
RAK_4631 == \
1 // Not really needed, but may as well just skip it at a lower level if no library or not a RAK_4631
if (rak12035Sensor.hasSensor()) {
result = rak12035Sensor.handleAdminMessage(mp, request, response);
if (result != AdminMessageHandleResult::NOT_HANDLED)
return result;
}
#endif
#endif
return result;
}
#endif
#endif

View File

@@ -118,22 +118,31 @@ void HealthTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *
}
// Display "Health From: ..." on its own
display->drawString(x, y, "Health From: " + String(lastSender) + "(" + String(agoSecs) + "s)");
char headerStr[64];
snprintf(headerStr, sizeof(headerStr), "Health From: %s(%ds)", lastSender, (int)agoSecs);
display->drawString(x, y, headerStr);
String last_temp = String(lastMeasurement.variant.health_metrics.temperature, 0) + "°C";
char last_temp[16];
if (moduleConfig.telemetry.environment_display_fahrenheit) {
last_temp = String(UnitConversions::CelsiusToFahrenheit(lastMeasurement.variant.health_metrics.temperature), 0) + "°F";
snprintf(last_temp, sizeof(last_temp), "%.0f°F",
UnitConversions::CelsiusToFahrenheit(lastMeasurement.variant.health_metrics.temperature));
} else {
snprintf(last_temp, sizeof(last_temp), "%.0f°C", lastMeasurement.variant.health_metrics.temperature);
}
// Continue with the remaining details
display->drawString(x, y += _fontHeight(FONT_SMALL), "Temp: " + last_temp);
char tempStr[32];
snprintf(tempStr, sizeof(tempStr), "Temp: %s", last_temp);
display->drawString(x, y += _fontHeight(FONT_SMALL), tempStr);
if (lastMeasurement.variant.health_metrics.has_heart_bpm) {
display->drawString(x, y += _fontHeight(FONT_SMALL),
"Heart Rate: " + String(lastMeasurement.variant.health_metrics.heart_bpm, 0) + " bpm");
char heartStr[32];
snprintf(heartStr, sizeof(heartStr), "Heart Rate: %.0f bpm", lastMeasurement.variant.health_metrics.heart_bpm);
display->drawString(x, y += _fontHeight(FONT_SMALL), heartStr);
}
if (lastMeasurement.variant.health_metrics.has_spO2) {
display->drawString(x, y += _fontHeight(FONT_SMALL),
"spO2: " + String(lastMeasurement.variant.health_metrics.spO2, 0) + " %");
char spo2Str[32];
snprintf(spo2Str, sizeof(spo2Str), "spO2: %.0f %%", lastMeasurement.variant.health_metrics.spO2);
display->drawString(x, y += _fontHeight(FONT_SMALL), spo2Str);
}
}

View File

@@ -29,13 +29,15 @@ bool HostMetricsModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp,
if (t->which_variant == meshtastic_Telemetry_host_metrics_tag) {
#ifdef DEBUG_PORT
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';
LOG_INFO("(Received Host Metrics from %s): uptime=%u, diskfree=%lu, memory free=%lu, load=%04.2f, %04.2f, %04.2f, %s",
sender, t->variant.host_metrics.uptime_seconds, t->variant.host_metrics.diskfree1_bytes,
LOG_INFO("(Received Host Metrics from %s): uptime=%u, diskfree=%lu, memory free=%lu, load=%04.2f, %04.2f, %04.2f", sender,
t->variant.host_metrics.uptime_seconds, t->variant.host_metrics.diskfree1_bytes,
t->variant.host_metrics.freemem_bytes, static_cast<float>(t->variant.host_metrics.load1) / 100,
static_cast<float>(t->variant.host_metrics.load5) / 100,
static_cast<float>(t->variant.host_metrics.load15) / 100,
t->variant.host_metrics.has_user_string ? t->variant.host_metrics.user_string : "");
static_cast<float>(t->variant.host_metrics.load15) / 100);
// t->variant.host_metrics.has_user_string ? t->variant.host_metrics.user_string : "");
#endif
}
return false; // Let others look at this message also if they want
@@ -111,7 +113,8 @@ meshtastic_Telemetry HostMetricsModule::getHostMetrics()
if (settingsStrings[hostMetrics_user_command] != "") {
std::string userCommandResult = exec(settingsStrings[hostMetrics_user_command].c_str());
if (userCommandResult.length() > 1) {
strncpy(t.variant.host_metrics.user_string, userCommandResult.c_str(), 200);
strncpy(t.variant.host_metrics.user_string, userCommandResult.c_str(), sizeof(t.variant.host_metrics.user_string));
t.variant.host_metrics.user_string[sizeof(t.variant.host_metrics.user_string) - 1] = '\0';
t.variant.host_metrics.has_user_string = true;
}
}
@@ -121,12 +124,12 @@ meshtastic_Telemetry HostMetricsModule::getHostMetrics()
bool HostMetricsModule::sendMetrics()
{
meshtastic_Telemetry telemetry = getHostMetrics();
LOG_INFO("Send: uptime=%u, diskfree=%lu, memory free=%lu, load=%04.2f, %04.2f, %04.2f %s",
LOG_INFO("Send: uptime=%u, diskfree=%lu, memory free=%lu, load=%04.2f, %04.2f, %04.2f",
telemetry.variant.host_metrics.uptime_seconds, telemetry.variant.host_metrics.diskfree1_bytes,
telemetry.variant.host_metrics.freemem_bytes, static_cast<float>(telemetry.variant.host_metrics.load1) / 100,
static_cast<float>(telemetry.variant.host_metrics.load5) / 100,
static_cast<float>(telemetry.variant.host_metrics.load15) / 100,
telemetry.variant.host_metrics.has_user_string ? telemetry.variant.host_metrics.user_string : "");
static_cast<float>(telemetry.variant.host_metrics.load15) / 100);
// telemetry.variant.host_metrics.has_user_string ? telemetry.variant.host_metrics.user_string : "");
meshtastic_MeshPacket *p = allocDataProtobuf(telemetry);
p->to = NODENUM_BROADCAST;
@@ -137,4 +140,4 @@ bool HostMetricsModule::sendMetrics()
service->sendToMesh(p, RX_SRC_LOCAL, true);
return true;
}
#endif
#endif

View File

@@ -10,6 +10,7 @@
#include "PowerTelemetry.h"
#include "RTC.h"
#include "Router.h"
#include "graphics/SharedUIDisplay.h"
#include "main.h"
#include "power.h"
#include "sleep.h"
@@ -21,6 +22,11 @@
#include "graphics/ScreenFonts.h"
#include <Throttle.h>
namespace graphics
{
extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr);
}
int32_t PowerTelemetryModule::runOnce()
{
if (sleepOnNextExecution == true) {
@@ -103,13 +109,20 @@ bool PowerTelemetryModule::wantUIFrame()
void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
display->clear();
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL);
int line = 1;
// === Set Title
const char *titleStr = (SCREEN_WIDTH > 128) ? "Power Telem." : "Power";
// === Header ===
graphics::drawCommonHeader(display, x, y, titleStr);
if (lastMeasurementPacket == nullptr) {
// In case of no valid packet, display "Power Telemetry", "No measurement"
display->drawString(x, y, "Power Telemetry");
display->drawString(x, y += _fontHeight(FONT_SMALL), "No measurement");
// In case of no valid packet, display "Power Telemetry", "No measurement"
display->drawString(x, graphics::getTextPositions(display)[line++], "No measurement");
return;
}
@@ -120,29 +133,35 @@ void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *s
const meshtastic_Data &p = lastMeasurementPacket->decoded;
if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &lastMeasurement)) {
display->drawString(x, y, "Measurement Error");
display->drawString(x, graphics::getTextPositions(display)[line++], "Measurement Error");
LOG_ERROR("Unable to decode last packet");
return;
}
// Display "Pow. From: ..."
display->drawString(x, y, "Pow. From: " + String(lastSender) + "(" + String(agoSecs) + "s)");
char fromStr[64];
snprintf(fromStr, sizeof(fromStr), "Pow. From: %s (%us)", lastSender, agoSecs);
display->drawString(x, graphics::getTextPositions(display)[line++], fromStr);
// Display current and voltage based on ...power_metrics.has_[channel/voltage/current]... flags
if (lastMeasurement.variant.power_metrics.has_ch1_voltage || lastMeasurement.variant.power_metrics.has_ch1_current) {
display->drawString(x, y += _fontHeight(FONT_SMALL),
"Ch1: " + String(lastMeasurement.variant.power_metrics.ch1_voltage, 2) + "V " +
String(lastMeasurement.variant.power_metrics.ch1_current, 0) + "mA");
const auto &m = lastMeasurement.variant.power_metrics;
int lineY = textSecondLine;
auto drawLine = [&](const char *label, float voltage, float current) {
char lineStr[64];
snprintf(lineStr, sizeof(lineStr), "%s: %.2fV %.0fmA", label, voltage, current);
display->drawString(x, lineY, lineStr);
lineY += _fontHeight(FONT_SMALL);
};
if (m.has_ch1_voltage || m.has_ch1_current) {
drawLine("Ch1", m.ch1_voltage, m.ch1_current);
}
if (lastMeasurement.variant.power_metrics.has_ch2_voltage || lastMeasurement.variant.power_metrics.has_ch2_current) {
display->drawString(x, y += _fontHeight(FONT_SMALL),
"Ch2: " + String(lastMeasurement.variant.power_metrics.ch2_voltage, 2) + "V " +
String(lastMeasurement.variant.power_metrics.ch2_current, 0) + "mA");
if (m.has_ch2_voltage || m.has_ch2_current) {
drawLine("Ch2", m.ch2_voltage, m.ch2_current);
}
if (lastMeasurement.variant.power_metrics.has_ch3_voltage || lastMeasurement.variant.power_metrics.has_ch3_current) {
display->drawString(x, y += _fontHeight(FONT_SMALL),
"Ch3: " + String(lastMeasurement.variant.power_metrics.ch3_voltage, 2) + "V " +
String(lastMeasurement.variant.power_metrics.ch3_current, 0) + "mA");
if (m.has_ch3_voltage || m.has_ch3_current) {
drawLine("Ch3", m.ch3_voltage, m.ch3_current);
}
}

View File

@@ -137,17 +137,17 @@ void BME680Sensor::updateState()
#endif
}
void BME680Sensor::checkStatus(String functionName)
void BME680Sensor::checkStatus(const char *functionName)
{
if (bme680.status < BSEC_OK)
LOG_ERROR("%s BSEC2 code: %s", functionName.c_str(), String(bme680.status).c_str());
LOG_ERROR("%s BSEC2 code: %d", functionName, bme680.status);
else if (bme680.status > BSEC_OK)
LOG_WARN("%s BSEC2 code: %s", functionName.c_str(), String(bme680.status).c_str());
LOG_WARN("%s BSEC2 code: %d", functionName, bme680.status);
if (bme680.sensor.status < BME68X_OK)
LOG_ERROR("%s BME68X code: %s", functionName.c_str(), String(bme680.sensor.status).c_str());
LOG_ERROR("%s BME68X code: %d", functionName, bme680.sensor.status);
else if (bme680.sensor.status > BME68X_OK)
LOG_WARN("%s BME68X code: %s", functionName.c_str(), String(bme680.sensor.status).c_str());
LOG_WARN("%s BME68X code: %d", functionName, bme680.sensor.status);
}
#endif

View File

@@ -34,7 +34,7 @@ class BME680Sensor : public TelemetrySensor
BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY};
void loadState();
void updateState();
void checkStatus(String functionName);
void checkStatus(const char *functionName);
public:
BME680Sensor();

View File

@@ -8,16 +8,24 @@
#include "VoltageSensor.h"
#include <INA3221.h>
#ifndef INA3221_ENV_CH
#define INA3221_ENV_CH INA3221_CH1
#endif
#ifndef INA3221_BAT_CH
#define INA3221_BAT_CH INA3221_CH1
#endif
class INA3221Sensor : public TelemetrySensor, VoltageSensor, CurrentSensor
{
private:
INA3221 ina3221 = INA3221(INA3221_ADDR42_SDA);
// channel to report voltage/current for environment metrics
ina3221_ch_t ENV_CH = INA3221_CH1;
static const ina3221_ch_t ENV_CH = INA3221_ENV_CH;
// channel to report battery voltage for device_battery_ina_address
ina3221_ch_t BAT_CH = INA3221_CH1;
static const ina3221_ch_t BAT_CH = INA3221_BAT_CH;
// get a single measurement for a channel
struct _INA3221Measurement getMeasurement(ina3221_ch_t ch);

View File

@@ -0,0 +1,109 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include("RAK12035_SoilMoisture.h") && defined(RAK_4631) && RAK_4631 == 1
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "RAK12035Sensor.h"
RAK12035Sensor::RAK12035Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_RAK12035, "RAK12035") {}
int32_t RAK12035Sensor::runOnce()
{
if (!hasSensor()) {
return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
}
// TODO:: check for up to 2 additional sensors and start them if present.
sensor.set_sensor_addr(RAK120351_ADDR);
delay(100);
sensor.begin(nodeTelemetrySensorsMap[sensorType].first);
// Get sensor firmware version
uint8_t data = 0;
sensor.get_sensor_version(&data);
if (data != 0) {
LOG_INFO("Init sensor: %s", sensorName);
LOG_INFO("RAK12035Sensor Init Succeed \nSensor1 Firmware version: %i, Sensor Name: %s", data, sensorName);
status = true;
sensor.sensor_sleep();
} else {
// If we reach here, it means the sensor did not initialize correctly.
LOG_INFO("Init sensor: %s", sensorName);
LOG_ERROR("RAK12035Sensor Init Failed");
status = false;
}
return initI2CSensor();
}
void RAK12035Sensor::setup()
{
// Set the calibration values
// Reading the saved calibration values from the sensor.
// TODO:: Check for and run calibration check for up to 2 additional sensors if present.
uint16_t zero_val = 0;
uint16_t hundred_val = 0;
uint16_t default_zero_val = 550;
uint16_t default_hundred_val = 420;
sensor.sensor_on();
delay(200);
sensor.get_dry_cal(&zero_val);
sensor.get_wet_cal(&hundred_val);
delay(200);
if (zero_val == 0 || zero_val <= hundred_val) {
LOG_INFO("Dry calibration value is %d", zero_val);
LOG_INFO("Wet calibration value is %d", hundred_val);
LOG_INFO("This does not make sense. You can recalibrate this sensor using the calibration sketch included here: "
"https://github.com/RAKWireless/RAK12035_SoilMoisture.");
LOG_INFO("For now, setting default calibration value for Dry Calibration: %d", default_zero_val);
sensor.set_dry_cal(default_zero_val);
sensor.get_dry_cal(&zero_val);
LOG_INFO("Dry calibration reset complete. New value is %d", zero_val);
}
if (hundred_val == 0 || hundred_val >= zero_val) {
LOG_INFO("Dry calibration value is %d", zero_val);
LOG_INFO("Wet calibration value is %d", hundred_val);
LOG_INFO("This does not make sense. You can recalibrate this sensor using the calibration sketch included here: "
"https://github.com/RAKWireless/RAK12035_SoilMoisture.");
LOG_INFO("For now, setting default calibration value for Wet Calibration: %d", default_hundred_val);
sensor.set_wet_cal(default_hundred_val);
sensor.get_wet_cal(&hundred_val);
LOG_INFO("Wet calibration reset complete. New value is %d", hundred_val);
}
sensor.sensor_sleep();
delay(200);
LOG_INFO("Dry calibration value is %d", zero_val);
LOG_INFO("Wet calibration value is %d", hundred_val);
}
bool RAK12035Sensor::getMetrics(meshtastic_Telemetry *measurement)
{
// TODO:: read and send metrics for up to 2 additional soil monitors if present.
// -- how to do this.. this could get a little complex..
// ie - 1> we combine them into an average and send that, 2> we send them as separate metrics
// ^-- these scenarios would require different handling of the metrics in the receiving end and maybe a setting in the
// device ui and an additional proto for that?
measurement->variant.environment_metrics.has_soil_temperature = true;
measurement->variant.environment_metrics.has_soil_moisture = true;
uint8_t moisture = 0;
uint16_t temp = 0;
bool success = false;
sensor.sensor_on();
delay(200);
success = sensor.get_sensor_moisture(&moisture);
delay(200);
success &= sensor.get_sensor_temperature(&temp);
delay(200);
sensor.sensor_sleep();
if (success == false) {
LOG_ERROR("Failed to read sensor data");
return false;
}
measurement->variant.environment_metrics.soil_temperature = ((float)temp / 10.0f);
measurement->variant.environment_metrics.soil_moisture = moisture;
return true;
}
#endif

View File

@@ -0,0 +1,28 @@
#pragma once
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<RAK12035_SoilMoisture.h>) && defined(RAK_4631)
#ifndef _MT_RAK12035VBSENSOR_H
#define _MT_RAK12035VBSENSOR_H
#endif
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "RAK12035_SoilMoisture.h"
#include "TelemetrySensor.h"
#include <Arduino.h>
class RAK12035Sensor : public TelemetrySensor
{
private:
RAK12035 sensor;
protected:
virtual void setup() override;
public:
RAK12035Sensor();
virtual int32_t runOnce() override;
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
};
#endif

View File

@@ -41,21 +41,36 @@ void RCWL9620Sensor::begin(TwoWire *wire, uint8_t addr, uint8_t sda, uint8_t scl
float RCWL9620Sensor::getDistance()
{
uint32_t data;
_wire->beginTransmission(_addr); // Transfer data to addr.
_wire->write(0x01);
_wire->endTransmission(); // Stop data transmission with the Ultrasonic
// Unit.
uint32_t data = 0;
uint8_t b1 = 0, b2 = 0, b3 = 0;
_wire->requestFrom(_addr,
(uint8_t)3); // Request 3 bytes from Ultrasonic Unit.
LOG_DEBUG("[RCWL9620] Start measure command");
_wire->beginTransmission(_addr);
_wire->write(0x01); // À tester aussi sans cette ligne si besoin
uint8_t result = _wire->endTransmission();
LOG_DEBUG("[RCWL9620] endTransmission result = %d", result);
delay(100); // délai pour laisser le capteur répondre
LOG_DEBUG("[RCWL9620] Read i2c data:");
_wire->requestFrom(_addr, (uint8_t)3);
if (_wire->available() < 3) {
LOG_DEBUG("[RCWL9620] less than 3 octets !");
return 0.0;
}
b1 = _wire->read();
b2 = _wire->read();
b3 = _wire->read();
data = ((uint32_t)b1 << 16) | ((uint32_t)b2 << 8) | b3;
float Distance = float(data) / 1000.0;
LOG_DEBUG("[RCWL9620] Bytes readed = %02X %02X %02X", b1, b2, b3);
LOG_DEBUG("[RCWL9620] data=%.2f, level=%.2f", (double)data, (double)Distance);
data = _wire->read();
data <<= 8;
data |= _wire->read();
data <<= 8;
data |= _wire->read();
float Distance = float(data) / 1000;
if (Distance > 4500.00) {
return 4500.00;
} else {
@@ -63,4 +78,4 @@ float RCWL9620Sensor::getDistance()
}
}
#endif
#endif

View File

@@ -2,9 +2,13 @@
#include "NodeDB.h"
#include "PowerFSM.h"
#include "configuration.h"
#include "graphics/draw/CompassRenderer.h"
#if HAS_SCREEN
#include "gps/RTC.h"
#include "graphics/Screen.h"
#include "graphics/TimeFormatters.h"
#include "graphics/draw/NodeListRenderer.h"
#include "main.h"
#endif
@@ -48,6 +52,8 @@ ProcessMessage WaypointModule::handleReceived(const meshtastic_MeshPacket &mp)
bool WaypointModule::shouldDraw()
{
#if !MESHTASTIC_EXCLUDE_WAYPOINT
if (screen == nullptr)
return false;
// If no waypoint to show
if (!devicestate.has_rx_waypoint)
return false;
@@ -79,13 +85,15 @@ bool WaypointModule::shouldDraw()
/// Draw the last waypoint we received
void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
if (screen == nullptr)
return;
// Prepare to draw
display->setFont(FONT_SMALL);
display->setTextAlignment(TEXT_ALIGN_LEFT);
// Handle inverted display
// Unsure of expected behavior: for now, copy drawNodeInfo
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED)
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED)
display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL);
// Decode the waypoint
@@ -101,7 +109,7 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state,
// Get timestamp info. Will pass as a field to drawColumns
static char lastStr[20];
screen->getTimeAgoStr(sinceReceived(&mp), lastStr, sizeof(lastStr));
getTimeAgoStr(sinceReceived(&mp), lastStr, sizeof(lastStr));
// Will contain distance information, passed as a field to drawColumns
static char distStr[20];
@@ -115,7 +123,7 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state,
// Dimensions / co-ordinates for the compass/circle
int16_t compassX = 0, compassY = 0;
uint16_t compassDiam = graphics::Screen::getCompassDiam(display->getWidth(), display->getHeight());
uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(display->getWidth(), display->getHeight());
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) {
compassX = x + display->getWidth() - compassDiam / 2 - 5;
@@ -133,7 +141,7 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state,
myHeading = (screen->getHeading()) * PI / 180; // gotta convert compass degrees to Radians
else
myHeading = screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i));
screen->drawCompassNorth(display, compassX, compassY, myHeading);
graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, (compassDiam / 2));
// Compass bearing to waypoint
float bearingToOther =
@@ -142,7 +150,7 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state,
// If the top of the compass is not a static north we need adjust bearingToOther based on heading
if (!config.display.compass_north_top)
bearingToOther -= myHeading;
screen->drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther);
graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther);
float bearingToOtherDegrees = (bearingToOther < 0) ? bearingToOther + 2 * PI : bearingToOther;
bearingToOtherDegrees = bearingToOtherDegrees * 180 / PI;
@@ -180,11 +188,11 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state,
// Undo color-inversion, if set prior to drawing header
// Unsure of expected behavior? For now: copy drawNodeInfo
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) {
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) {
display->setColor(BLACK);
}
// Must be after distStr is populated
screen->drawColumns(display, x, y, fields);
graphics::NodeListRenderer::drawColumns(display, x, y, fields);
}
#endif

View File

@@ -3,6 +3,9 @@
#include "Default.h"
#include "MeshService.h"
#include "PaxcounterModule.h"
#include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/images.h"
#include <assert.h>
PaxcounterModule *paxcounterModule;
@@ -112,20 +115,32 @@ int32_t PaxcounterModule::runOnce()
#if HAS_SCREEN
#include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h"
void PaxcounterModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
display->clear();
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL);
int line = 1;
// === Set Title
const char *titleStr = "Pax";
// === Header ===
graphics::drawCommonHeader(display, x, y, titleStr);
char buffer[50];
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL);
display->drawString(x + 0, y + 0, "PAX");
libpax_counter_count(&count_from_libpax);
display->setTextAlignment(TEXT_ALIGN_CENTER);
display->setFont(FONT_SMALL);
display->drawStringf(display->getWidth() / 2 + x, 0 + y + 12, buffer, "WiFi: %d\nBLE: %d\nuptime: %ds",
count_from_libpax.wifi_count, count_from_libpax.ble_count, millis() / 1000);
display->drawStringf(display->getWidth() / 2 + x, graphics::getTextPositions(display)[line++], buffer,
"WiFi: %d\nBLE: %d\nUptime: %ds", count_from_libpax.wifi_count, count_from_libpax.ble_count,
millis() / 1000);
}
#endif // HAS_SCREEN