mirror of
https://github.com/meshtastic/firmware.git
synced 2025-12-30 22:50:57 +00:00
Merge branch 'master' into nomad-gemini
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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
@@ -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},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
310
src/modules/KeyVerificationModule.cpp
Normal file
310
src/modules/KeyVerificationModule.cpp
Normal 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(¤tSecurityNumber, sizeof(currentSecurityNumber));
|
||||
hash.update(¤tNonce, sizeof(currentNonce));
|
||||
hash.update(¤tRemoteNode, sizeof(currentRemoteNode));
|
||||
hash.update(&ourNodeNum, sizeof(ourNodeNum));
|
||||
hash.update(currentRequest->public_key.bytes, currentRequest->public_key.size);
|
||||
hash.update(owner.public_key.bytes, owner.public_key.size);
|
||||
hash.finalize(hash1, 32);
|
||||
|
||||
// generate hash2
|
||||
hash.reset();
|
||||
hash.update(¤tNonce, 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(¤tNonce, sizeof(currentNonce));
|
||||
hash.update(&ourNodeNum, sizeof(ourNodeNum));
|
||||
hash.update(¤tRemoteNode, 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(¤tNonce, 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
|
||||
64
src/modules/KeyVerificationModule.h
Normal file
64
src/modules/KeyVerificationModule.h
Normal 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;
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
118
src/modules/SystemCommandsModule.cpp
Normal file
118
src/modules/SystemCommandsModule.cpp
Normal 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;
|
||||
}
|
||||
19
src/modules/SystemCommandsModule.h
Normal file
19
src/modules/SystemCommandsModule.h
Normal 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;
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
109
src/modules/Telemetry/Sensor/RAK12035Sensor.cpp
Normal file
109
src/modules/Telemetry/Sensor/RAK12035Sensor.cpp
Normal 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
|
||||
28
src/modules/Telemetry/Sensor/RAK12035Sensor.h
Normal file
28
src/modules/Telemetry/Sensor/RAK12035Sensor.h
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user