Signed-off-by: 吴文峰 <kevin@lmve.net>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,82 @@
|
||||
#include <sys/types.h>
|
||||
|
||||
#pragma once
|
||||
#include "ProtobufModule.h"
|
||||
#if HAS_WIFI
|
||||
#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<AdminModule_ObserverData *>
|
||||
{
|
||||
public:
|
||||
/** Constructor
|
||||
* name is for debugging output
|
||||
*/
|
||||
AdminModule();
|
||||
|
||||
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_AdminMessage *p) override;
|
||||
|
||||
private:
|
||||
bool hasOpenEditTransaction = false;
|
||||
|
||||
uint8_t session_passkey[8] = {0};
|
||||
uint session_time = 0;
|
||||
|
||||
void saveChanges(int saveWhat, bool shouldReboot = true);
|
||||
|
||||
/**
|
||||
* Getters
|
||||
*/
|
||||
void handleGetModuleConfigResponse(const meshtastic_MeshPacket &req, meshtastic_AdminMessage *p);
|
||||
void handleGetOwner(const meshtastic_MeshPacket &req);
|
||||
void handleGetConfig(const meshtastic_MeshPacket &req, uint32_t configType);
|
||||
void handleGetModuleConfig(const meshtastic_MeshPacket &req, uint32_t configType);
|
||||
void handleGetChannel(const meshtastic_MeshPacket &req, uint32_t channelIndex);
|
||||
void handleGetDeviceMetadata(const meshtastic_MeshPacket &req);
|
||||
void handleGetDeviceConnectionStatus(const meshtastic_MeshPacket &req);
|
||||
void handleGetNodeRemoteHardwarePins(const meshtastic_MeshPacket &req);
|
||||
void handleGetDeviceUIConfig(const meshtastic_MeshPacket &req);
|
||||
/**
|
||||
* Setters
|
||||
*/
|
||||
void handleSetOwner(const meshtastic_User &o);
|
||||
void handleSetChannel(const meshtastic_Channel &cc);
|
||||
void handleSetConfig(const meshtastic_Config &c);
|
||||
bool handleSetModuleConfig(const meshtastic_ModuleConfig &c);
|
||||
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);
|
||||
bool checkPassKey(meshtastic_AdminMessage *res);
|
||||
|
||||
bool messageIsResponse(const meshtastic_AdminMessage *r);
|
||||
bool messageIsRequest(const meshtastic_AdminMessage *r);
|
||||
void sendWarning(const char *message);
|
||||
};
|
||||
|
||||
static constexpr const char *licensedModeMessage =
|
||||
"Licensed mode activated, removing admin channel and encryption from all channels";
|
||||
|
||||
extern AdminModule *adminModule;
|
||||
|
||||
void disableBluetooth();
|
||||
@@ -0,0 +1,198 @@
|
||||
#include "AtakPluginModule.h"
|
||||
#include "Default.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "configuration.h"
|
||||
#include "main.h"
|
||||
#include "mesh/compression/unishox2.h"
|
||||
#include "meshtastic/atak.pb.h"
|
||||
|
||||
AtakPluginModule *atakPluginModule;
|
||||
|
||||
AtakPluginModule::AtakPluginModule()
|
||||
: ProtobufModule("atak", meshtastic_PortNum_ATAK_PLUGIN, &meshtastic_TAKPacket_msg), concurrency::OSThread("AtakPlugin")
|
||||
{
|
||||
ourPortNum = meshtastic_PortNum_ATAK_PLUGIN;
|
||||
}
|
||||
|
||||
/*
|
||||
Encompasses the full construction and sending packet to mesh
|
||||
Will be used for broadcast.
|
||||
*/
|
||||
int32_t AtakPluginModule::runOnce()
|
||||
{
|
||||
return default_broadcast_interval_secs;
|
||||
}
|
||||
|
||||
bool AtakPluginModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_TAKPacket *r)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
meshtastic_TAKPacket AtakPluginModule::cloneTAKPacketData(meshtastic_TAKPacket *t)
|
||||
{
|
||||
meshtastic_TAKPacket clone = meshtastic_TAKPacket_init_zero;
|
||||
if (t->has_group) {
|
||||
clone.has_group = true;
|
||||
clone.group = t->group;
|
||||
}
|
||||
if (t->has_status) {
|
||||
clone.has_status = true;
|
||||
clone.status = t->status;
|
||||
}
|
||||
if (t->has_contact) {
|
||||
clone.has_contact = true;
|
||||
clone.contact = {0};
|
||||
}
|
||||
|
||||
if (t->which_payload_variant == meshtastic_TAKPacket_pli_tag) {
|
||||
clone.which_payload_variant = meshtastic_TAKPacket_pli_tag;
|
||||
clone.payload_variant.pli = t->payload_variant.pli;
|
||||
} else if (t->which_payload_variant == meshtastic_TAKPacket_chat_tag) {
|
||||
clone.which_payload_variant = meshtastic_TAKPacket_chat_tag;
|
||||
clone.payload_variant.chat = {0};
|
||||
} else if (t->which_payload_variant == meshtastic_TAKPacket_detail_tag) {
|
||||
clone.which_payload_variant = meshtastic_TAKPacket_detail_tag;
|
||||
clone.payload_variant.detail.size = t->payload_variant.detail.size;
|
||||
memcpy(clone.payload_variant.detail.bytes, t->payload_variant.detail.bytes, t->payload_variant.detail.size);
|
||||
}
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_TAKPacket *t)
|
||||
{
|
||||
// From Phone (EUD)
|
||||
if (mp.from == 0) {
|
||||
LOG_DEBUG("Received uncompressed TAK payload from phone: %d bytes", mp.decoded.payload.size);
|
||||
// Compress for LoRA transport
|
||||
auto compressed = cloneTAKPacketData(t);
|
||||
compressed.is_compressed = true;
|
||||
if (t->has_contact) {
|
||||
auto length = unishox2_compress_lines(t->contact.callsign, strlen(t->contact.callsign), compressed.contact.callsign,
|
||||
sizeof(compressed.contact.callsign) - 1, USX_PSET_DFLT, NULL);
|
||||
if (length < 0) {
|
||||
LOG_WARN("Compress overflow contact.callsign. Revert to uncompressed packet");
|
||||
return;
|
||||
}
|
||||
LOG_DEBUG("Compressed callsign: %d bytes", length);
|
||||
length = unishox2_compress_lines(t->contact.device_callsign, strlen(t->contact.device_callsign),
|
||||
compressed.contact.device_callsign, sizeof(compressed.contact.device_callsign) - 1,
|
||||
USX_PSET_DFLT, NULL);
|
||||
if (length < 0) {
|
||||
LOG_WARN("Compress overflow contact.device_callsign. Revert to uncompressed packet");
|
||||
return;
|
||||
}
|
||||
LOG_DEBUG("Compressed device_callsign: %d bytes", length);
|
||||
}
|
||||
if (t->which_payload_variant == meshtastic_TAKPacket_chat_tag) {
|
||||
auto length = unishox2_compress_lines(t->payload_variant.chat.message, strlen(t->payload_variant.chat.message),
|
||||
compressed.payload_variant.chat.message,
|
||||
sizeof(compressed.payload_variant.chat.message) - 1, USX_PSET_DFLT, NULL);
|
||||
if (length < 0) {
|
||||
LOG_WARN("Compress overflow chat.message. Revert to uncompressed packet");
|
||||
return;
|
||||
}
|
||||
LOG_DEBUG("Compressed chat message: %d bytes", length);
|
||||
|
||||
if (t->payload_variant.chat.has_to) {
|
||||
compressed.payload_variant.chat.has_to = true;
|
||||
length = unishox2_compress_lines(t->payload_variant.chat.to, strlen(t->payload_variant.chat.to),
|
||||
compressed.payload_variant.chat.to,
|
||||
sizeof(compressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL);
|
||||
if (length < 0) {
|
||||
LOG_WARN("Compress overflow chat.to. Revert to uncompressed packet");
|
||||
return;
|
||||
}
|
||||
LOG_DEBUG("Compressed chat to: %d bytes", length);
|
||||
}
|
||||
|
||||
if (t->payload_variant.chat.has_to_callsign) {
|
||||
compressed.payload_variant.chat.has_to_callsign = true;
|
||||
length = unishox2_compress_lines(t->payload_variant.chat.to_callsign, strlen(t->payload_variant.chat.to_callsign),
|
||||
compressed.payload_variant.chat.to_callsign,
|
||||
sizeof(compressed.payload_variant.chat.to_callsign) - 1, USX_PSET_DFLT, NULL);
|
||||
if (length < 0) {
|
||||
LOG_WARN("Compress overflow chat.to_callsign. Revert to uncompressed packet");
|
||||
return;
|
||||
}
|
||||
LOG_DEBUG("Compressed chat to_callsign: %d bytes", length);
|
||||
}
|
||||
}
|
||||
mp.decoded.payload.size = pb_encode_to_bytes(mp.decoded.payload.bytes, sizeof(mp.decoded.payload.bytes),
|
||||
meshtastic_TAKPacket_fields, &compressed);
|
||||
LOG_DEBUG("Final payload: %d bytes", mp.decoded.payload.size);
|
||||
} else {
|
||||
if (!t->is_compressed) {
|
||||
// Not compressed. Something is wrong
|
||||
LOG_WARN("Received uncompressed TAKPacket over radio! Skip");
|
||||
return;
|
||||
}
|
||||
|
||||
// Decompress for Phone (EUD)
|
||||
auto uncompressed = cloneTAKPacketData(t);
|
||||
uncompressed.is_compressed = false;
|
||||
if (t->has_contact) {
|
||||
auto length =
|
||||
unishox2_decompress_lines(t->contact.callsign, strlen(t->contact.callsign), uncompressed.contact.callsign,
|
||||
sizeof(uncompressed.contact.callsign) - 1, USX_PSET_DFLT, NULL);
|
||||
if (length < 0) {
|
||||
LOG_WARN("Decompress overflow contact.callsign. Bailing out");
|
||||
return;
|
||||
}
|
||||
LOG_DEBUG("Decompressed callsign: %d bytes", length);
|
||||
|
||||
length = unishox2_decompress_lines(t->contact.device_callsign, strlen(t->contact.device_callsign),
|
||||
uncompressed.contact.device_callsign,
|
||||
sizeof(uncompressed.contact.device_callsign) - 1, USX_PSET_DFLT, NULL);
|
||||
if (length < 0) {
|
||||
LOG_WARN("Decompress overflow contact.device_callsign. Bailing out");
|
||||
return;
|
||||
}
|
||||
LOG_DEBUG("Decompressed device_callsign: %d bytes", length);
|
||||
}
|
||||
if (uncompressed.which_payload_variant == meshtastic_TAKPacket_chat_tag) {
|
||||
auto length = unishox2_decompress_lines(t->payload_variant.chat.message, strlen(t->payload_variant.chat.message),
|
||||
uncompressed.payload_variant.chat.message,
|
||||
sizeof(uncompressed.payload_variant.chat.message) - 1, USX_PSET_DFLT, NULL);
|
||||
if (length < 0) {
|
||||
LOG_WARN("Decompress overflow chat.message. Bailing out");
|
||||
return;
|
||||
}
|
||||
LOG_DEBUG("Decompressed chat message: %d bytes", length);
|
||||
|
||||
if (t->payload_variant.chat.has_to) {
|
||||
uncompressed.payload_variant.chat.has_to = true;
|
||||
length = unishox2_decompress_lines(t->payload_variant.chat.to, strlen(t->payload_variant.chat.to),
|
||||
uncompressed.payload_variant.chat.to,
|
||||
sizeof(uncompressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL);
|
||||
if (length < 0) {
|
||||
LOG_WARN("Decompress overflow chat.to. Bailing out");
|
||||
return;
|
||||
}
|
||||
LOG_DEBUG("Decompressed chat to: %d bytes", length);
|
||||
}
|
||||
|
||||
if (t->payload_variant.chat.has_to_callsign) {
|
||||
uncompressed.payload_variant.chat.has_to_callsign = true;
|
||||
length =
|
||||
unishox2_decompress_lines(t->payload_variant.chat.to_callsign, strlen(t->payload_variant.chat.to_callsign),
|
||||
uncompressed.payload_variant.chat.to_callsign,
|
||||
sizeof(uncompressed.payload_variant.chat.to_callsign) - 1, USX_PSET_DFLT, NULL);
|
||||
if (length < 0) {
|
||||
LOG_WARN("Decompress overflow chat.to_callsign. Bailing out");
|
||||
return;
|
||||
}
|
||||
LOG_DEBUG("Decompressed chat to_callsign: %d bytes", length);
|
||||
}
|
||||
}
|
||||
auto decompressedCopy = packetPool.allocCopy(mp);
|
||||
decompressedCopy->decoded.payload.size =
|
||||
pb_encode_to_bytes(decompressedCopy->decoded.payload.bytes, sizeof(decompressedCopy->decoded.payload),
|
||||
meshtastic_TAKPacket_fields, &uncompressed);
|
||||
|
||||
service->sendToPhone(decompressedCopy);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
#include "ProtobufModule.h"
|
||||
#include "meshtastic/atak.pb.h"
|
||||
|
||||
/**
|
||||
* Waypoint message handling for meshtastic
|
||||
*/
|
||||
class AtakPluginModule : public ProtobufModule<meshtastic_TAKPacket>, private concurrency::OSThread
|
||||
{
|
||||
public:
|
||||
/** Constructor
|
||||
* name is for debugging output
|
||||
*/
|
||||
AtakPluginModule();
|
||||
|
||||
protected:
|
||||
virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_TAKPacket *t) override;
|
||||
virtual void alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_TAKPacket *t) override;
|
||||
/* Does our periodic broadcast */
|
||||
int32_t runOnce() override;
|
||||
|
||||
private:
|
||||
meshtastic_TAKPacket cloneTAKPacketData(meshtastic_TAKPacket *t);
|
||||
};
|
||||
|
||||
extern AtakPluginModule *atakPluginModule;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,275 @@
|
||||
#pragma once
|
||||
#if HAS_SCREEN
|
||||
#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_SENDING_ACTIVE,
|
||||
CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED,
|
||||
CANNED_MESSAGE_RUN_STATE_ACTION_SELECT,
|
||||
CANNED_MESSAGE_RUN_STATE_ACTION_UP,
|
||||
CANNED_MESSAGE_RUN_STATE_ACTION_DOWN,
|
||||
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;
|
||||
int rectX;
|
||||
int rectY;
|
||||
int rectWidth;
|
||||
int rectHeight;
|
||||
};
|
||||
|
||||
struct NodeEntry {
|
||||
meshtastic_NodeInfoLite *node;
|
||||
uint32_t lastHeard;
|
||||
};
|
||||
|
||||
// ============================
|
||||
// Main Class
|
||||
// ============================
|
||||
|
||||
class CannedMessageModule : public SinglePortModule, public Observable<const UIFrameEvent *>, private concurrency::OSThread
|
||||
{
|
||||
public:
|
||||
CannedMessageModule();
|
||||
|
||||
void LaunchWithDestination(NodeNum, uint8_t newChannel = 0);
|
||||
void LaunchRepeatDestination();
|
||||
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 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);
|
||||
|
||||
#ifdef RAK14014
|
||||
cannedMessageModuleRunState getRunState() const { return runState; }
|
||||
#endif
|
||||
|
||||
// === Packet Interest Filter ===
|
||||
virtual bool wantPacket(const meshtastic_MeshPacket *p) override
|
||||
{
|
||||
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();
|
||||
|
||||
#if defined(USE_VIRTUAL_KEYBOARD)
|
||||
void drawKeyboard(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
String keyForCoordinates(uint x, uint y);
|
||||
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
|
||||
|
||||
// === Input Handling ===
|
||||
int handleInputEvent(const InputEvent *event);
|
||||
virtual bool wantUIFrame() override { return shouldDraw(); }
|
||||
virtual Observable<const UIFrameEvent *> *getUIFrameObservable() override { return this; }
|
||||
virtual bool interceptingKeyboardInput() override;
|
||||
virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override;
|
||||
virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp,
|
||||
meshtastic_AdminMessage *request,
|
||||
meshtastic_AdminMessage *response) override;
|
||||
|
||||
virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||
|
||||
void loadProtoForModule();
|
||||
bool saveProtoForModule();
|
||||
void installDefaultCannedMessageModuleConfig();
|
||||
|
||||
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;
|
||||
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},
|
||||
{"W", 22, 0, 0, 0, 0},
|
||||
{"E", 17, 0, 0, 0, 0},
|
||||
{"R", 16.5, 0, 0, 0, 0},
|
||||
{"T", 14, 0, 0, 0, 0},
|
||||
{"Y", 15, 0, 0, 0, 0},
|
||||
{"U", 16.5, 0, 0, 0, 0},
|
||||
{"I", 5, 0, 0, 0, 0},
|
||||
{"O", 19.5, 0, 0, 0, 0},
|
||||
{"P", 15.5, 0, 0, 0, 0}},
|
||||
{{"A", 14, 0, 0, 0, 0},
|
||||
{"S", 15, 0, 0, 0, 0},
|
||||
{"D", 16.5, 0, 0, 0, 0},
|
||||
{"F", 15, 0, 0, 0, 0},
|
||||
{"G", 17, 0, 0, 0, 0},
|
||||
{"H", 15.5, 0, 0, 0, 0},
|
||||
{"J", 12, 0, 0, 0, 0},
|
||||
{"K", 15.5, 0, 0, 0, 0},
|
||||
{"L", 14, 0, 0, 0, 0},
|
||||
{"", 0, 0, 0, 0, 0}},
|
||||
{{"⇧", 20, 0, 0, 0, 0},
|
||||
{"Z", 14, 0, 0, 0, 0},
|
||||
{"X", 14.5, 0, 0, 0, 0},
|
||||
{"C", 15.5, 0, 0, 0, 0},
|
||||
{"V", 13.5, 0, 0, 0, 0},
|
||||
{"B", 15, 0, 0, 0, 0},
|
||||
{"N", 15, 0, 0, 0, 0},
|
||||
{"M", 17, 0, 0, 0, 0},
|
||||
{"⌫", 20, 0, 0, 0, 0},
|
||||
{"", 0, 0, 0, 0, 0}},
|
||||
{{"123", 42, 0, 0, 0, 0},
|
||||
{" ", 64, 0, 0, 0, 0},
|
||||
{"↵", 36, 0, 0, 0, 0},
|
||||
{"", 0, 0, 0, 0, 0},
|
||||
{"", 0, 0, 0, 0, 0},
|
||||
{"", 0, 0, 0, 0, 0},
|
||||
{"", 0, 0, 0, 0, 0},
|
||||
{"", 0, 0, 0, 0, 0},
|
||||
{"", 0, 0, 0, 0, 0},
|
||||
{"", 0, 0, 0, 0, 0}}},
|
||||
{{{"1", 12, 0, 0, 0, 0},
|
||||
{"2", 13.5, 0, 0, 0, 0},
|
||||
{"3", 12.5, 0, 0, 0, 0},
|
||||
{"4", 14, 0, 0, 0, 0},
|
||||
{"5", 14, 0, 0, 0, 0},
|
||||
{"6", 14, 0, 0, 0, 0},
|
||||
{"7", 13.5, 0, 0, 0, 0},
|
||||
{"8", 14, 0, 0, 0, 0},
|
||||
{"9", 14, 0, 0, 0, 0},
|
||||
{"0", 14, 0, 0, 0, 0}},
|
||||
{{"-", 8, 0, 0, 0, 0},
|
||||
{"/", 8, 0, 0, 0, 0},
|
||||
{":", 4.5, 0, 0, 0, 0},
|
||||
{";", 4.5, 0, 0, 0, 0},
|
||||
{"(", 7, 0, 0, 0, 0},
|
||||
{")", 6.5, 0, 0, 0, 0},
|
||||
{"$", 12.5, 0, 0, 0, 0},
|
||||
{"&", 15, 0, 0, 0, 0},
|
||||
{"@", 21.5, 0, 0, 0, 0},
|
||||
{"\"", 8, 0, 0, 0, 0}},
|
||||
{{".", 8, 0, 0, 0, 0},
|
||||
{",", 8, 0, 0, 0, 0},
|
||||
{"?", 10, 0, 0, 0, 0},
|
||||
{"!", 10, 0, 0, 0, 0},
|
||||
{"'", 10, 0, 0, 0, 0},
|
||||
{"⌫", 20, 0, 0, 0, 0}},
|
||||
{{"ABC", 50, 0, 0, 0, 0}, {" ", 64, 0, 0, 0, 0}, {"↵", 36, 0, 0, 0, 0}}}};
|
||||
#endif
|
||||
};
|
||||
|
||||
extern CannedMessageModule *cannedMessageModule;
|
||||
#endif
|
||||
@@ -0,0 +1,164 @@
|
||||
#include "DetectionSensorModule.h"
|
||||
#include "Default.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "configuration.h"
|
||||
#include "main.h"
|
||||
#include <Throttle.h>
|
||||
DetectionSensorModule *detectionSensorModule;
|
||||
|
||||
#define GPIO_POLLING_INTERVAL 100
|
||||
#define DELAYED_INTERVAL 1000
|
||||
|
||||
typedef enum {
|
||||
DetectionSensorVerdictDetected,
|
||||
DetectionSensorVerdictSendState,
|
||||
DetectionSensorVerdictNoop,
|
||||
} DetectionSensorTriggerVerdict;
|
||||
|
||||
typedef DetectionSensorTriggerVerdict (*DetectionSensorTriggerHandler)(bool prev, bool current);
|
||||
|
||||
static DetectionSensorTriggerVerdict detection_trigger_logic_level(bool prev, bool current)
|
||||
{
|
||||
return current ? DetectionSensorVerdictDetected : DetectionSensorVerdictNoop;
|
||||
}
|
||||
|
||||
static DetectionSensorTriggerVerdict detection_trigger_single_edge(bool prev, bool current)
|
||||
{
|
||||
return (!prev && current) ? DetectionSensorVerdictDetected : DetectionSensorVerdictNoop;
|
||||
}
|
||||
|
||||
static DetectionSensorTriggerVerdict detection_trigger_either_edge(bool prev, bool current)
|
||||
{
|
||||
if (prev == current) {
|
||||
return DetectionSensorVerdictNoop;
|
||||
}
|
||||
return current ? DetectionSensorVerdictDetected : DetectionSensorVerdictSendState;
|
||||
}
|
||||
|
||||
const static DetectionSensorTriggerHandler handlers[_meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_MAX + 1] = {
|
||||
[meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_LOGIC_LOW] = detection_trigger_logic_level,
|
||||
[meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_LOGIC_HIGH] = detection_trigger_logic_level,
|
||||
[meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_FALLING_EDGE] = detection_trigger_single_edge,
|
||||
[meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_RISING_EDGE] = detection_trigger_single_edge,
|
||||
[meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_EITHER_EDGE_ACTIVE_LOW] = detection_trigger_either_edge,
|
||||
[meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_EITHER_EDGE_ACTIVE_HIGH] = detection_trigger_either_edge,
|
||||
};
|
||||
|
||||
int32_t DetectionSensorModule::runOnce()
|
||||
{
|
||||
/*
|
||||
Uncomment the preferences below if you want to use the module
|
||||
without having to configure it from the PythonAPI or WebUI.
|
||||
*/
|
||||
// moduleConfig.detection_sensor.enabled = true;
|
||||
// moduleConfig.detection_sensor.monitor_pin = 10; // WisBlock PIR IO6
|
||||
// moduleConfig.detection_sensor.monitor_pin = 21; // WisBlock RAK12013 Radar IO6
|
||||
// moduleConfig.detection_sensor.minimum_broadcast_secs = 30;
|
||||
// moduleConfig.detection_sensor.state_broadcast_secs = 120;
|
||||
// moduleConfig.detection_sensor.detection_trigger_type =
|
||||
// meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_LOGIC_HIGH;
|
||||
// strcpy(moduleConfig.detection_sensor.name, "Motion");
|
||||
|
||||
if (moduleConfig.detection_sensor.enabled == false)
|
||||
return disable();
|
||||
|
||||
if (firstTime) {
|
||||
|
||||
#ifdef DETECTION_SENSOR_EN
|
||||
pinMode(DETECTION_SENSOR_EN, OUTPUT);
|
||||
digitalWrite(DETECTION_SENSOR_EN, HIGH);
|
||||
#endif
|
||||
|
||||
// This is the first time the OSThread library has called this function, so do some setup
|
||||
firstTime = false;
|
||||
if (moduleConfig.detection_sensor.monitor_pin > 0) {
|
||||
pinMode(moduleConfig.detection_sensor.monitor_pin, moduleConfig.detection_sensor.use_pullup ? INPUT_PULLUP : INPUT);
|
||||
} else {
|
||||
LOG_WARN("Detection Sensor Module: Set to enabled but no monitor pin is set. Disable module");
|
||||
return disable();
|
||||
}
|
||||
LOG_INFO("Detection Sensor Module: init");
|
||||
|
||||
return setStartDelay();
|
||||
}
|
||||
|
||||
// LOG_DEBUG("Detection Sensor Module: Current pin state: %i", digitalRead(moduleConfig.detection_sensor.monitor_pin));
|
||||
|
||||
if (!Throttle::isWithinTimespanMs(lastSentToMesh,
|
||||
Default::getConfiguredOrDefaultMs(moduleConfig.detection_sensor.minimum_broadcast_secs))) {
|
||||
bool isDetected = hasDetectionEvent();
|
||||
DetectionSensorTriggerVerdict verdict =
|
||||
handlers[moduleConfig.detection_sensor.detection_trigger_type](wasDetected, isDetected);
|
||||
wasDetected = isDetected;
|
||||
switch (verdict) {
|
||||
case DetectionSensorVerdictDetected:
|
||||
sendDetectionMessage();
|
||||
return DELAYED_INTERVAL;
|
||||
case DetectionSensorVerdictSendState:
|
||||
sendCurrentStateMessage(isDetected);
|
||||
return DELAYED_INTERVAL;
|
||||
case DetectionSensorVerdictNoop:
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Even if we haven't detected an event, broadcast our current state to the mesh on the scheduled interval as a sort
|
||||
// of heartbeat. We only do this if the minimum broadcast interval is greater than zero, otherwise we'll only broadcast state
|
||||
// change detections.
|
||||
if (moduleConfig.detection_sensor.state_broadcast_secs > 0 &&
|
||||
!Throttle::isWithinTimespanMs(lastSentToMesh,
|
||||
Default::getConfiguredOrDefaultMs(moduleConfig.detection_sensor.state_broadcast_secs,
|
||||
default_telemetry_broadcast_interval_secs))) {
|
||||
sendCurrentStateMessage(hasDetectionEvent());
|
||||
return DELAYED_INTERVAL;
|
||||
}
|
||||
return GPIO_POLLING_INTERVAL;
|
||||
}
|
||||
|
||||
void DetectionSensorModule::sendDetectionMessage()
|
||||
{
|
||||
LOG_DEBUG("Detected event observed. Send message");
|
||||
char *message = new char[40];
|
||||
sprintf(message, "%s detected", moduleConfig.detection_sensor.name);
|
||||
meshtastic_MeshPacket *p = allocDataPacket();
|
||||
p->want_ack = false;
|
||||
p->decoded.payload.size = strlen(message);
|
||||
memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size);
|
||||
if (moduleConfig.detection_sensor.send_bell && p->decoded.payload.size < meshtastic_Constants_DATA_PAYLOAD_LEN) {
|
||||
p->decoded.payload.bytes[p->decoded.payload.size] = 7; // Bell character
|
||||
p->decoded.payload.bytes[p->decoded.payload.size + 1] = '\0'; // Bell character
|
||||
p->decoded.payload.size++;
|
||||
}
|
||||
lastSentToMesh = millis();
|
||||
if (!channels.isDefaultChannel(0)) {
|
||||
LOG_INFO("Send message id=%d, dest=%x, msg=%.*s", p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes);
|
||||
service->sendToMesh(p);
|
||||
} else
|
||||
LOG_ERROR("Message not allow on Public channel");
|
||||
delete[] message;
|
||||
}
|
||||
|
||||
void DetectionSensorModule::sendCurrentStateMessage(bool state)
|
||||
{
|
||||
char *message = new char[40];
|
||||
sprintf(message, "%s state: %i", moduleConfig.detection_sensor.name, state);
|
||||
meshtastic_MeshPacket *p = allocDataPacket();
|
||||
p->want_ack = false;
|
||||
p->decoded.payload.size = strlen(message);
|
||||
memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size);
|
||||
lastSentToMesh = millis();
|
||||
if (!channels.isDefaultChannel(0)) {
|
||||
LOG_INFO("Send message id=%d, dest=%x, msg=%.*s", p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes);
|
||||
service->sendToMesh(p);
|
||||
} else
|
||||
LOG_ERROR("Message not allow on Public channel");
|
||||
delete[] message;
|
||||
}
|
||||
|
||||
bool DetectionSensorModule::hasDetectionEvent()
|
||||
{
|
||||
bool currentState = digitalRead(moduleConfig.detection_sensor.monitor_pin);
|
||||
// LOG_DEBUG("Detection Sensor Module: Current state: %i", currentState);
|
||||
return (moduleConfig.detection_sensor.detection_trigger_type & 1) ? currentState : !currentState;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
#include "SinglePortModule.h"
|
||||
|
||||
class DetectionSensorModule : public SinglePortModule, private concurrency::OSThread
|
||||
{
|
||||
public:
|
||||
DetectionSensorModule() : SinglePortModule("detection", meshtastic_PortNum_DETECTION_SENSOR_APP), OSThread("DetectionSensor")
|
||||
{
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual int32_t runOnce() override;
|
||||
|
||||
private:
|
||||
bool firstTime = true;
|
||||
uint32_t lastSentToMesh = 0;
|
||||
bool wasDetected = false;
|
||||
void sendDetectionMessage();
|
||||
void sendCurrentStateMessage(bool state);
|
||||
bool hasDetectionEvent();
|
||||
};
|
||||
|
||||
extern DetectionSensorModule *detectionSensorModule;
|
||||
@@ -0,0 +1,95 @@
|
||||
#if !MESHTASTIC_EXCLUDE_DROPZONE
|
||||
|
||||
#include "DropzoneModule.h"
|
||||
#include "Meshservice->h"
|
||||
#include "configuration.h"
|
||||
#include "gps/GeoCoord.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "main.h"
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
#include "modules/Telemetry/Sensor/DFRobotLarkSensor.h"
|
||||
#include "modules/Telemetry/UnitConversions.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
DropzoneModule *dropzoneModule;
|
||||
|
||||
int32_t DropzoneModule::runOnce()
|
||||
{
|
||||
// Send on a 5 second delay from receiving the matching request
|
||||
if (startSendConditions != 0 && (startSendConditions + 5000U) < millis()) {
|
||||
service->sendToMesh(sendConditions(), RX_SRC_LOCAL);
|
||||
startSendConditions = 0;
|
||||
}
|
||||
// Run every second to check if we need to send conditions
|
||||
return 1000;
|
||||
}
|
||||
|
||||
ProcessMessage DropzoneModule::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
auto &p = mp.decoded;
|
||||
char matchCompare[54];
|
||||
auto incomingMessage = reinterpret_cast<const char *>(p.payload.bytes);
|
||||
sprintf(matchCompare, "%s conditions", owner.short_name);
|
||||
if (strncasecmp(incomingMessage, matchCompare, strlen(matchCompare)) == 0) {
|
||||
LOG_DEBUG("Received dropzone conditions request");
|
||||
startSendConditions = millis();
|
||||
}
|
||||
|
||||
sprintf(matchCompare, "%s conditions", owner.long_name);
|
||||
if (strncasecmp(incomingMessage, matchCompare, strlen(matchCompare)) == 0) {
|
||||
LOG_DEBUG("Received dropzone conditions request");
|
||||
startSendConditions = millis();
|
||||
}
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
meshtastic_MeshPacket *DropzoneModule::sendConditions()
|
||||
{
|
||||
char replyStr[200];
|
||||
/*
|
||||
CLOSED @ {HH:MM:SS}z
|
||||
Wind 2 kts @ 125°
|
||||
29.25 inHg 72°C
|
||||
*/
|
||||
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true);
|
||||
int hour = 0, min = 0, sec = 0;
|
||||
if (rtc_sec > 0) {
|
||||
long hms = rtc_sec % SEC_PER_DAY;
|
||||
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
|
||||
|
||||
hour = hms / SEC_PER_HOUR;
|
||||
min = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||||
sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN;
|
||||
}
|
||||
|
||||
// Check if the dropzone is open or closed by reading the analog pin
|
||||
// If pin is connected to GND (below 100 should be lower than floating voltage),
|
||||
// the dropzone is open
|
||||
auto dropzoneStatus = analogRead(A1) < 100 ? "OPEN" : "CLOSED";
|
||||
auto reply = allocDataPacket();
|
||||
|
||||
auto node = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
if (sensor.hasSensor()) {
|
||||
meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero;
|
||||
sensor.getMetrics(&telemetry);
|
||||
auto windSpeed = UnitConversions::MetersPerSecondToKnots(telemetry.variant.environment_metrics.wind_speed);
|
||||
auto windDirection = telemetry.variant.environment_metrics.wind_direction;
|
||||
auto temp = telemetry.variant.environment_metrics.temperature;
|
||||
auto baro = UnitConversions::HectoPascalToInchesOfMercury(telemetry.variant.environment_metrics.barometric_pressure);
|
||||
sprintf(replyStr, "%s @ %02d:%02d:%02dz\nWind %.2f kts @ %d°\nBaro %.2f inHg %.2f°C", dropzoneStatus, hour, min, sec,
|
||||
windSpeed, windDirection, baro, temp);
|
||||
} else {
|
||||
LOG_ERROR("No sensor found");
|
||||
sprintf(replyStr, "%s @ %02d:%02d:%02d\nNo sensor found", dropzoneStatus, hour, min, sec);
|
||||
}
|
||||
LOG_DEBUG("Conditions reply: %s", replyStr);
|
||||
reply->decoded.payload.size = strlen(replyStr); // You must specify how many bytes are in the reply
|
||||
memcpy(reply->decoded.payload.bytes, replyStr, reply->decoded.payload.size);
|
||||
|
||||
return reply;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
#if !MESHTASTIC_EXCLUDE_DROPZONE
|
||||
#include "SinglePortModule.h"
|
||||
#include "modules/Telemetry/Sensor/DFRobotLarkSensor.h"
|
||||
|
||||
/**
|
||||
* An example module that replies to a message with the current conditions
|
||||
* and status at the dropzone when it receives a text message mentioning it's name followed by "conditions"
|
||||
*/
|
||||
class DropzoneModule : public SinglePortModule, private concurrency::OSThread
|
||||
{
|
||||
DFRobotLarkSensor sensor;
|
||||
|
||||
public:
|
||||
/** Constructor
|
||||
* name is for debugging output
|
||||
*/
|
||||
DropzoneModule() : SinglePortModule("dropzone", meshtastic_PortNum_TEXT_MESSAGE_APP), concurrency::OSThread("Dropzone")
|
||||
{
|
||||
// Set up the analog pin for reading the dropzone status
|
||||
pinMode(PIN_A1, INPUT);
|
||||
}
|
||||
|
||||
virtual int32_t runOnce() override;
|
||||
|
||||
protected:
|
||||
/** Called to handle a particular incoming message
|
||||
*/
|
||||
virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||
|
||||
private:
|
||||
meshtastic_MeshPacket *sendConditions();
|
||||
uint32_t startSendConditions = 0;
|
||||
};
|
||||
|
||||
extern DropzoneModule *dropzoneModule;
|
||||
#endif
|
||||
@@ -0,0 +1,643 @@
|
||||
/**
|
||||
* @file ExternalNotificationModule.cpp
|
||||
* @brief Implementation of the ExternalNotificationModule class.
|
||||
*
|
||||
* This file contains the implementation of the ExternalNotificationModule class, which is responsible for handling external
|
||||
* notifications such as vibration, buzzer, and LED lights. The class provides methods to turn on and off the external
|
||||
* notification outputs and to play ringtones using PWM buzzer. It also includes default configurations and a runOnce() method to
|
||||
* handle the module's behavior.
|
||||
*
|
||||
* Documentation:
|
||||
* https://meshtastic.org/docs/configuration/module/external-notification
|
||||
*
|
||||
* @author Jm Casler & Meshtastic Team
|
||||
* @date [Insert Date]
|
||||
*/
|
||||
#include "ExternalNotificationModule.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "RTC.h"
|
||||
#include "Router.h"
|
||||
#include "buzz/buzz.h"
|
||||
#include "configuration.h"
|
||||
#include "main.h"
|
||||
#include "mesh/generated/meshtastic/rtttl.pb.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
#ifdef HAS_NCP5623
|
||||
#include <graphics/RAKled.h>
|
||||
#endif
|
||||
|
||||
#ifdef HAS_LP5562
|
||||
#include <graphics/NomadStarLED.h>
|
||||
#endif
|
||||
|
||||
#ifdef HAS_NEOPIXEL
|
||||
#include <graphics/NeoPixel.h>
|
||||
#endif
|
||||
|
||||
#ifdef UNPHONE
|
||||
#include "unPhone.h"
|
||||
extern unPhone unphone;
|
||||
#endif
|
||||
|
||||
#if defined(HAS_RGB_LED)
|
||||
uint8_t red = 0;
|
||||
uint8_t green = 0;
|
||||
uint8_t blue = 0;
|
||||
uint8_t white = 0;
|
||||
uint8_t colorState = 1;
|
||||
uint8_t brightnessIndex = 0;
|
||||
uint8_t brightnessValues[] = {0, 10, 20, 30, 50, 90, 160, 170}; // blue gets multiplied by 1.5
|
||||
bool ascending = true;
|
||||
#endif
|
||||
|
||||
#ifndef PIN_BUZZER
|
||||
#define PIN_BUZZER false
|
||||
#endif
|
||||
|
||||
/*
|
||||
Documentation:
|
||||
https://meshtastic.org/docs/configuration/module/external-notification
|
||||
*/
|
||||
|
||||
// Default configurations
|
||||
#ifdef EXT_NOTIFY_OUT
|
||||
#define EXT_NOTIFICATION_MODULE_OUTPUT EXT_NOTIFY_OUT
|
||||
#else
|
||||
#define EXT_NOTIFICATION_MODULE_OUTPUT 0
|
||||
#endif
|
||||
#define EXT_NOTIFICATION_MODULE_OUTPUT_MS 1000
|
||||
|
||||
#define EXT_NOTIFICATION_FAST_THREAD_MS 25
|
||||
|
||||
#define ASCII_BELL 0x07
|
||||
|
||||
meshtastic_RTTTLConfig rtttlConfig;
|
||||
|
||||
ExternalNotificationModule *externalNotificationModule;
|
||||
|
||||
bool externalCurrentState[3] = {};
|
||||
|
||||
uint32_t externalTurnedOn[3] = {};
|
||||
|
||||
static const char *rtttlConfigFile = "/prefs/ringtone.proto";
|
||||
|
||||
int32_t ExternalNotificationModule::runOnce()
|
||||
{
|
||||
if (!moduleConfig.external_notification.enabled) {
|
||||
return INT32_MAX; // we don't need this thread here...
|
||||
} else {
|
||||
uint32_t delay = EXT_NOTIFICATION_MODULE_OUTPUT_MS;
|
||||
bool isRtttlPlaying = rtttl::isPlaying();
|
||||
#ifdef HAS_I2S
|
||||
// audioThread->isPlaying() also handles actually playing the RTTTL, needs to be called in loop
|
||||
isRtttlPlaying = isRtttlPlaying || audioThread->isPlaying();
|
||||
#endif
|
||||
if ((nagCycleCutoff < millis()) && !isRtttlPlaying) {
|
||||
// Turn off external notification immediately when timeout is reached, regardless of song state
|
||||
nagCycleCutoff = UINT32_MAX;
|
||||
ExternalNotificationModule::stopNow();
|
||||
isNagging = false;
|
||||
return INT32_MAX; // save cycles till we're needed again
|
||||
}
|
||||
|
||||
// If the output is turned on, turn it back off after the given period of time.
|
||||
if (isNagging) {
|
||||
delay = (moduleConfig.external_notification.output_ms ? moduleConfig.external_notification.output_ms
|
||||
: EXT_NOTIFICATION_MODULE_OUTPUT_MS);
|
||||
if (externalTurnedOn[0] + delay < millis()) {
|
||||
setExternalState(0, !getExternal(0));
|
||||
}
|
||||
if (externalTurnedOn[1] + delay < millis()) {
|
||||
setExternalState(1, !getExternal(1));
|
||||
}
|
||||
// Only toggle buzzer output if not using PWM mode (to avoid conflict with RTTTL)
|
||||
if (!moduleConfig.external_notification.use_pwm && externalTurnedOn[2] + delay < millis()) {
|
||||
LOG_DEBUG("EXTERNAL 2 %d compared to %d", externalTurnedOn[2] + moduleConfig.external_notification.output_ms,
|
||||
millis());
|
||||
setExternalState(2, !getExternal(2));
|
||||
}
|
||||
#if defined(HAS_RGB_LED)
|
||||
red = (colorState & 4) ? brightnessValues[brightnessIndex] : 0; // Red enabled on colorState = 4,5,6,7
|
||||
green = (colorState & 2) ? brightnessValues[brightnessIndex] : 0; // Green enabled on colorState = 2,3,6,7
|
||||
blue = (colorState & 1) ? (brightnessValues[brightnessIndex] * 1.5) : 0; // Blue enabled on colorState = 1,3,5,7
|
||||
white = (colorState & 12) ? brightnessValues[brightnessIndex] : 0;
|
||||
#ifdef HAS_NCP5623
|
||||
if (rgb_found.type == ScanI2C::NCP5623) {
|
||||
rgb.setColor(red, green, blue);
|
||||
}
|
||||
#endif
|
||||
#ifdef HAS_LP5562
|
||||
if (rgb_found.type == ScanI2C::LP5562) {
|
||||
rgbw.setColor(red, green, blue, white);
|
||||
}
|
||||
#endif
|
||||
#ifdef RGBLED_CA
|
||||
analogWrite(RGBLED_RED, 255 - red); // CA type needs reverse logic
|
||||
analogWrite(RGBLED_GREEN, 255 - green);
|
||||
analogWrite(RGBLED_BLUE, 255 - blue);
|
||||
#elif defined(RGBLED_RED)
|
||||
analogWrite(RGBLED_RED, red);
|
||||
analogWrite(RGBLED_GREEN, green);
|
||||
analogWrite(RGBLED_BLUE, blue);
|
||||
#endif
|
||||
#ifdef HAS_NEOPIXEL
|
||||
pixels.fill(pixels.Color(red, green, blue), 0, NEOPIXEL_COUNT);
|
||||
pixels.show();
|
||||
#endif
|
||||
#ifdef UNPHONE
|
||||
unphone.rgb(red, green, blue);
|
||||
#endif
|
||||
if (ascending) { // fade in
|
||||
brightnessIndex++;
|
||||
if (brightnessIndex == (sizeof(brightnessValues) - 1)) {
|
||||
ascending = false;
|
||||
}
|
||||
} else {
|
||||
brightnessIndex--; // fade out
|
||||
}
|
||||
if (brightnessIndex == 0) {
|
||||
ascending = true;
|
||||
colorState++; // next color
|
||||
if (colorState > 7) {
|
||||
colorState = 1;
|
||||
}
|
||||
}
|
||||
// we need fast updates for the color change
|
||||
delay = EXT_NOTIFICATION_FAST_THREAD_MS;
|
||||
#endif
|
||||
|
||||
#ifdef T_WATCH_S3
|
||||
drv.go();
|
||||
#endif
|
||||
}
|
||||
|
||||
// Play RTTTL over i2s audio interface if enabled as buzzer
|
||||
#ifdef HAS_I2S
|
||||
if (moduleConfig.external_notification.use_i2s_as_buzzer) {
|
||||
if (audioThread->isPlaying()) {
|
||||
// Continue playing
|
||||
} else if (isNagging && (nagCycleCutoff >= millis())) {
|
||||
audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone));
|
||||
}
|
||||
// we need fast updates to play the RTTTL
|
||||
delay = EXT_NOTIFICATION_FAST_THREAD_MS;
|
||||
}
|
||||
#endif
|
||||
// now let the PWM buzzer play
|
||||
if (moduleConfig.external_notification.use_pwm && config.device.buzzer_gpio && canBuzz()) {
|
||||
if (rtttl::isPlaying()) {
|
||||
rtttl::play();
|
||||
} else if (isNagging && (nagCycleCutoff >= millis())) {
|
||||
// start the song again if we have time left
|
||||
rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone);
|
||||
}
|
||||
// we need fast updates to play the RTTTL
|
||||
delay = EXT_NOTIFICATION_FAST_THREAD_MS;
|
||||
}
|
||||
|
||||
return delay;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the external notification for the specified index.
|
||||
*
|
||||
* @param index The index of the external notification to change state.
|
||||
* @param on Whether we are turning things on (true) or off (false).
|
||||
*/
|
||||
void ExternalNotificationModule::setExternalState(uint8_t index, bool on)
|
||||
{
|
||||
externalCurrentState[index] = on;
|
||||
externalTurnedOn[index] = millis();
|
||||
|
||||
switch (index) {
|
||||
case 1:
|
||||
#ifdef UNPHONE
|
||||
unphone.vibe(on); // the unPhone's vibration motor is on a i2c GPIO expander
|
||||
#endif
|
||||
if (moduleConfig.external_notification.output_vibra)
|
||||
digitalWrite(moduleConfig.external_notification.output_vibra, on);
|
||||
break;
|
||||
case 2:
|
||||
// Only control buzzer pin digitally if not using PWM mode
|
||||
if (moduleConfig.external_notification.output_buzzer && !moduleConfig.external_notification.use_pwm)
|
||||
digitalWrite(moduleConfig.external_notification.output_buzzer, on);
|
||||
break;
|
||||
default:
|
||||
if (output > 0)
|
||||
digitalWrite(output, (moduleConfig.external_notification.active ? on : !on));
|
||||
break;
|
||||
}
|
||||
|
||||
#if defined(HAS_RGB_LED)
|
||||
if (!on) {
|
||||
red = 0;
|
||||
green = 0;
|
||||
blue = 0;
|
||||
white = 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef HAS_NCP5623
|
||||
if (rgb_found.type == ScanI2C::NCP5623) {
|
||||
rgb.setColor(red, green, blue);
|
||||
}
|
||||
#endif
|
||||
#ifdef HAS_LP5562
|
||||
if (rgb_found.type == ScanI2C::LP5562) {
|
||||
rgbw.setColor(red, green, blue, white);
|
||||
}
|
||||
#endif
|
||||
#ifdef RGBLED_CA
|
||||
analogWrite(RGBLED_RED, 255 - red); // CA type needs reverse logic
|
||||
analogWrite(RGBLED_GREEN, 255 - green);
|
||||
analogWrite(RGBLED_BLUE, 255 - blue);
|
||||
#elif defined(RGBLED_RED)
|
||||
analogWrite(RGBLED_RED, red);
|
||||
analogWrite(RGBLED_GREEN, green);
|
||||
analogWrite(RGBLED_BLUE, blue);
|
||||
#endif
|
||||
#ifdef HAS_NEOPIXEL
|
||||
pixels.fill(pixels.Color(red, green, blue), 0, NEOPIXEL_COUNT);
|
||||
pixels.show();
|
||||
#endif
|
||||
#ifdef UNPHONE
|
||||
unphone.rgb(red, green, blue);
|
||||
#endif
|
||||
#ifdef T_WATCH_S3
|
||||
if (on) {
|
||||
drv.go();
|
||||
} else {
|
||||
drv.stop();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
LOG_INFO("Turning off external notification: ");
|
||||
LOG_INFO("Stop RTTTL playback");
|
||||
rtttl::stop();
|
||||
#ifdef HAS_I2S
|
||||
LOG_INFO("Stop audioThread playback");
|
||||
if (audioThread->isPlaying())
|
||||
audioThread->stop();
|
||||
#endif
|
||||
// Turn off all outputs
|
||||
LOG_INFO("Turning off setExternalStates: ");
|
||||
for (int i = 0; i < 3; i++) {
|
||||
setExternalState(i, false);
|
||||
externalTurnedOn[i] = 0;
|
||||
LOG_INFO("%d ", i);
|
||||
}
|
||||
setIntervalFromNow(0);
|
||||
#ifdef T_WATCH_S3
|
||||
drv.stop();
|
||||
#endif
|
||||
|
||||
// Prevent the state machine from immediately re-triggering outputs after a manual stop.
|
||||
isNagging = false;
|
||||
nagCycleCutoff = UINT32_MAX;
|
||||
|
||||
#ifdef HAS_I2S
|
||||
// GPIO0 is used as mclk for I2S audio and set to OUTPUT by the sound library
|
||||
// T-Deck uses GPIO0 as trackball button, so restore the mode
|
||||
#if defined(T_DECK) || (defined(BUTTON_PIN) && BUTTON_PIN == 0)
|
||||
pinMode(0, INPUT);
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
ExternalNotificationModule::ExternalNotificationModule()
|
||||
: SinglePortModule("ExternalNotificationModule", meshtastic_PortNum_TEXT_MESSAGE_APP),
|
||||
concurrency::OSThread("ExternalNotification")
|
||||
{
|
||||
/*
|
||||
Uncomment the preferences below if you want to use the module
|
||||
without having to configure it from the PythonAPI or WebUI.
|
||||
*/
|
||||
|
||||
// moduleConfig.external_notification.alert_message = true;
|
||||
// moduleConfig.external_notification.alert_message_buzzer = true;
|
||||
// moduleConfig.external_notification.alert_message_vibra = true;
|
||||
// moduleConfig.external_notification.use_i2s_as_buzzer = true;
|
||||
|
||||
// moduleConfig.external_notification.active = true;
|
||||
// moduleConfig.external_notification.alert_bell = 1;
|
||||
// moduleConfig.external_notification.output_ms = 1000;
|
||||
// moduleConfig.external_notification.output = 4; // RAK4631 IO4
|
||||
// moduleConfig.external_notification.output_buzzer = 10; // RAK4631 IO6
|
||||
// moduleConfig.external_notification.output_vibra = 28; // RAK4631 IO7
|
||||
// moduleConfig.external_notification.nag_timeout = 300;
|
||||
|
||||
// T-Watch / T-Deck i2s audio as buzzer:
|
||||
// moduleConfig.external_notification.enabled = true;
|
||||
// moduleConfig.external_notification.nag_timeout = 300;
|
||||
// moduleConfig.external_notification.output_ms = 1000;
|
||||
// moduleConfig.external_notification.use_i2s_as_buzzer = true;
|
||||
// moduleConfig.external_notification.alert_message_buzzer = true;
|
||||
|
||||
if (moduleConfig.external_notification.enabled) {
|
||||
#if !defined(MESHTASTIC_EXCLUDE_INPUTBROKER)
|
||||
if (inputBroker) // put our callback in the inputObserver list
|
||||
inputObserver.observe(inputBroker);
|
||||
#endif
|
||||
if (nodeDB->loadProto(rtttlConfigFile, meshtastic_RTTTLConfig_size, sizeof(meshtastic_RTTTLConfig),
|
||||
&meshtastic_RTTTLConfig_msg, &rtttlConfig) != LoadFileResult::LOAD_SUCCESS) {
|
||||
memset(rtttlConfig.ringtone, 0, sizeof(rtttlConfig.ringtone));
|
||||
// The default ringtone is always loaded from userPrefs.jsonc
|
||||
strncpy(rtttlConfig.ringtone, USERPREFS_RINGTONE_RTTTL, sizeof(rtttlConfig.ringtone));
|
||||
}
|
||||
|
||||
LOG_INFO("Init External Notification Module");
|
||||
|
||||
output = moduleConfig.external_notification.output ? moduleConfig.external_notification.output
|
||||
: EXT_NOTIFICATION_MODULE_OUTPUT;
|
||||
|
||||
// Set the direction of a pin
|
||||
if (output > 0) {
|
||||
LOG_INFO("Use Pin %i in digital mode", output);
|
||||
pinMode(output, OUTPUT);
|
||||
}
|
||||
setExternalState(0, false);
|
||||
externalTurnedOn[0] = 0;
|
||||
if (moduleConfig.external_notification.output_vibra) {
|
||||
LOG_INFO("Use Pin %i for vibra motor", moduleConfig.external_notification.output_vibra);
|
||||
pinMode(moduleConfig.external_notification.output_vibra, OUTPUT);
|
||||
setExternalState(1, false);
|
||||
externalTurnedOn[1] = 0;
|
||||
}
|
||||
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);
|
||||
setExternalState(2, false);
|
||||
externalTurnedOn[2] = 0;
|
||||
} else {
|
||||
config.device.buzzer_gpio = config.device.buzzer_gpio ? config.device.buzzer_gpio : PIN_BUZZER;
|
||||
// in PWM Mode we force the buzzer pin if it is set
|
||||
LOG_INFO("Use Pin %i in PWM mode", config.device.buzzer_gpio);
|
||||
}
|
||||
}
|
||||
#ifdef HAS_NCP5623
|
||||
if (rgb_found.type == ScanI2C::NCP5623) {
|
||||
rgb.begin();
|
||||
rgb.setCurrent(10);
|
||||
}
|
||||
#endif
|
||||
#ifdef HAS_LP5562
|
||||
if (rgb_found.type == ScanI2C::LP5562) {
|
||||
rgbw.begin();
|
||||
rgbw.setCurrent(20);
|
||||
}
|
||||
#endif
|
||||
#ifdef RGBLED_RED
|
||||
pinMode(RGBLED_RED, OUTPUT); // set up the RGB led pins
|
||||
pinMode(RGBLED_GREEN, OUTPUT);
|
||||
pinMode(RGBLED_BLUE, OUTPUT);
|
||||
#endif
|
||||
#ifdef RGBLED_CA
|
||||
analogWrite(RGBLED_RED, 255); // with a common anode type, logic is reversed
|
||||
analogWrite(RGBLED_GREEN, 255); // so we want to initialise with lights off
|
||||
analogWrite(RGBLED_BLUE, 255);
|
||||
#endif
|
||||
#ifdef HAS_NEOPIXEL
|
||||
pixels.begin(); // Initialise the pixel(s)
|
||||
pixels.clear(); // Set all pixel colors to 'off'
|
||||
pixels.setBrightness(moduleConfig.ambient_lighting.current);
|
||||
#endif
|
||||
} else {
|
||||
LOG_INFO("External Notification Module Disabled");
|
||||
disable();
|
||||
}
|
||||
}
|
||||
|
||||
ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
if (moduleConfig.external_notification.enabled && !isSilenced) {
|
||||
#ifdef T_WATCH_S3
|
||||
drv.setWaveform(0, 75);
|
||||
drv.setWaveform(1, 56);
|
||||
drv.setWaveform(2, 0);
|
||||
drv.go();
|
||||
#endif
|
||||
if (!isFromUs(&mp)) {
|
||||
// Check if the message contains a bell character. Don't do this loop for every pin, just once.
|
||||
auto &p = mp.decoded;
|
||||
bool containsBell = false;
|
||||
for (size_t i = 0; i < p.payload.size; i++) {
|
||||
if (p.payload.bytes[i] == ASCII_BELL) {
|
||||
containsBell = true;
|
||||
}
|
||||
}
|
||||
|
||||
meshtastic_Channel ch = channels.getByIndex(mp.channel ? mp.channel : channels.getPrimaryIndex());
|
||||
if (moduleConfig.external_notification.alert_bell) {
|
||||
if (containsBell) {
|
||||
LOG_INFO("externalNotificationModule - Notification Bell");
|
||||
isNagging = true;
|
||||
setExternalState(0, true);
|
||||
if (moduleConfig.external_notification.nag_timeout) {
|
||||
nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000;
|
||||
} else {
|
||||
nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (moduleConfig.external_notification.alert_bell_vibra) {
|
||||
if (containsBell) {
|
||||
LOG_INFO("externalNotificationModule - Notification Bell (Vibra)");
|
||||
isNagging = true;
|
||||
setExternalState(1, true);
|
||||
if (moduleConfig.external_notification.nag_timeout) {
|
||||
nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000;
|
||||
} else {
|
||||
nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (moduleConfig.external_notification.alert_bell_buzzer && canBuzz()) {
|
||||
if (containsBell) {
|
||||
LOG_INFO("externalNotificationModule - Notification Bell (Buzzer)");
|
||||
isNagging = true;
|
||||
if (!moduleConfig.external_notification.use_pwm && !moduleConfig.external_notification.use_i2s_as_buzzer) {
|
||||
setExternalState(2, true);
|
||||
} else {
|
||||
#ifdef HAS_I2S
|
||||
if (moduleConfig.external_notification.use_i2s_as_buzzer) {
|
||||
audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone));
|
||||
} else
|
||||
#endif
|
||||
if (moduleConfig.external_notification.use_pwm) {
|
||||
rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone);
|
||||
}
|
||||
}
|
||||
if (moduleConfig.external_notification.nag_timeout) {
|
||||
nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000;
|
||||
} else {
|
||||
nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (moduleConfig.external_notification.alert_message &&
|
||||
(!ch.settings.has_module_settings || !ch.settings.module_settings.is_muted)) {
|
||||
LOG_INFO("externalNotificationModule - Notification Module");
|
||||
isNagging = true;
|
||||
setExternalState(0, true);
|
||||
if (moduleConfig.external_notification.nag_timeout) {
|
||||
nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000;
|
||||
} else {
|
||||
nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms;
|
||||
}
|
||||
}
|
||||
|
||||
if (moduleConfig.external_notification.alert_message_vibra &&
|
||||
(!ch.settings.has_module_settings || !ch.settings.module_settings.is_muted)) {
|
||||
LOG_INFO("externalNotificationModule - Notification Module (Vibra)");
|
||||
isNagging = true;
|
||||
setExternalState(1, true);
|
||||
if (moduleConfig.external_notification.nag_timeout) {
|
||||
nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000;
|
||||
} else {
|
||||
nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms;
|
||||
}
|
||||
}
|
||||
|
||||
if (moduleConfig.external_notification.alert_message_buzzer &&
|
||||
(!ch.settings.has_module_settings || !ch.settings.module_settings.is_muted)) {
|
||||
LOG_INFO("externalNotificationModule - Notification Module (Buzzer)");
|
||||
if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY ||
|
||||
(!isBroadcast(mp.to) && isToUs(&mp))) {
|
||||
// Buzz if buzzer mode is not in DIRECT_MSG_ONLY or is DM to us
|
||||
isNagging = true;
|
||||
if (!moduleConfig.external_notification.use_pwm && !moduleConfig.external_notification.use_i2s_as_buzzer) {
|
||||
setExternalState(2, true);
|
||||
} else {
|
||||
#ifdef HAS_I2S
|
||||
if (moduleConfig.external_notification.use_i2s_as_buzzer) {
|
||||
audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone));
|
||||
} else
|
||||
#endif
|
||||
if (moduleConfig.external_notification.use_pwm) {
|
||||
rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone);
|
||||
}
|
||||
}
|
||||
if (moduleConfig.external_notification.nag_timeout) {
|
||||
nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000;
|
||||
} else {
|
||||
nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms;
|
||||
}
|
||||
} else {
|
||||
// Don't beep if buzzer mode is "direct messages only" and it is no direct message
|
||||
LOG_INFO("Message buzzer was suppressed because buzzer mode DIRECT_MSG_ONLY");
|
||||
}
|
||||
}
|
||||
setIntervalFromNow(0); // run once so we know if we should do something
|
||||
}
|
||||
} else {
|
||||
LOG_INFO("External Notification Module Disabled or muted");
|
||||
}
|
||||
|
||||
return ProcessMessage::CONTINUE; // Let others look at this message also if they want
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief An admin message arrived to AdminModule. We are asked whether we want to handle that.
|
||||
*
|
||||
* @param mp The mesh packet arrived.
|
||||
* @param request The AdminMessage request extracted from the packet.
|
||||
* @param response The prepared response
|
||||
* @return AdminMessageHandleResult HANDLED if message was handled
|
||||
* HANDLED_WITH_RESULT if a result is also prepared.
|
||||
*/
|
||||
AdminMessageHandleResult ExternalNotificationModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp,
|
||||
meshtastic_AdminMessage *request,
|
||||
meshtastic_AdminMessage *response)
|
||||
{
|
||||
AdminMessageHandleResult result;
|
||||
|
||||
switch (request->which_payload_variant) {
|
||||
case meshtastic_AdminMessage_get_ringtone_request_tag:
|
||||
LOG_INFO("Client getting ringtone");
|
||||
this->handleGetRingtone(mp, response);
|
||||
result = AdminMessageHandleResult::HANDLED_WITH_RESPONSE;
|
||||
break;
|
||||
|
||||
case meshtastic_AdminMessage_set_ringtone_message_tag:
|
||||
LOG_INFO("Client setting ringtone");
|
||||
this->handleSetRingtone(request->set_canned_message_module_messages);
|
||||
result = AdminMessageHandleResult::HANDLED;
|
||||
break;
|
||||
|
||||
default:
|
||||
result = AdminMessageHandleResult::NOT_HANDLED;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void ExternalNotificationModule::handleGetRingtone(const meshtastic_MeshPacket &req, meshtastic_AdminMessage *response)
|
||||
{
|
||||
LOG_INFO("*** handleGetRingtone");
|
||||
if (req.decoded.want_response) {
|
||||
response->which_payload_variant = meshtastic_AdminMessage_get_ringtone_response_tag;
|
||||
strncpy(response->get_ringtone_response, rtttlConfig.ringtone, sizeof(response->get_ringtone_response));
|
||||
} // Don't send anything if not instructed to. Better than asserting.
|
||||
}
|
||||
|
||||
void ExternalNotificationModule::handleSetRingtone(const char *from_msg)
|
||||
{
|
||||
int changed = 0;
|
||||
|
||||
if (*from_msg) {
|
||||
changed |= strcmp(rtttlConfig.ringtone, from_msg);
|
||||
strncpy(rtttlConfig.ringtone, from_msg, sizeof(rtttlConfig.ringtone));
|
||||
LOG_INFO("*** from_msg.text:%s", 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;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
#pragma once
|
||||
|
||||
#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
|
||||
// Noop class for portduino.
|
||||
class rtttl
|
||||
{
|
||||
public:
|
||||
explicit rtttl() {}
|
||||
static bool isPlaying() { return false; }
|
||||
static void play() {}
|
||||
static void begin(byte a, const char *b){};
|
||||
static void stop() {}
|
||||
static bool done() { return true; }
|
||||
};
|
||||
#endif
|
||||
#include <Arduino.h>
|
||||
#include <functional>
|
||||
|
||||
/*
|
||||
* Radio interface for ExternalNotificationModule
|
||||
*
|
||||
*/
|
||||
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);
|
||||
bool getExternal(uint8_t index = 0);
|
||||
|
||||
void setMute(bool mute) { isSilenced = mute; }
|
||||
bool getMute() { return isSilenced; }
|
||||
|
||||
bool canBuzz();
|
||||
bool nagging();
|
||||
|
||||
void stopNow();
|
||||
|
||||
void handleGetRingtone(const meshtastic_MeshPacket &req, meshtastic_AdminMessage *response);
|
||||
void handleSetRingtone(const char *from_msg);
|
||||
|
||||
protected:
|
||||
/** 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;
|
||||
|
||||
virtual int32_t runOnce() override;
|
||||
|
||||
virtual bool wantPacket(const meshtastic_MeshPacket *p) override;
|
||||
|
||||
bool isNagging = false;
|
||||
|
||||
bool isSilenced = false;
|
||||
|
||||
virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp,
|
||||
meshtastic_AdminMessage *request,
|
||||
meshtastic_AdminMessage *response) override;
|
||||
};
|
||||
|
||||
extern ExternalNotificationModule *externalNotificationModule;
|
||||
@@ -0,0 +1,28 @@
|
||||
#include "GenericThreadModule.h"
|
||||
#include "MeshService.h"
|
||||
#include "configuration.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
/*
|
||||
Generic Thread Module allows for the execution of custom code at a set interval.
|
||||
*/
|
||||
GenericThreadModule *genericThreadModule;
|
||||
|
||||
GenericThreadModule::GenericThreadModule() : concurrency::OSThread("GenericThreadModule") {}
|
||||
|
||||
int32_t GenericThreadModule::runOnce()
|
||||
{
|
||||
|
||||
bool enabled = true;
|
||||
if (!enabled)
|
||||
return disable();
|
||||
|
||||
if (firstTime) {
|
||||
// do something the first time we run
|
||||
firstTime = 0;
|
||||
LOG_INFO("first time GenericThread running");
|
||||
}
|
||||
|
||||
LOG_INFO("GenericThread executing");
|
||||
return (my_interval);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include "MeshModule.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
#include "configuration.h"
|
||||
#include <Arduino.h>
|
||||
#include <functional>
|
||||
|
||||
class GenericThreadModule : private concurrency::OSThread
|
||||
{
|
||||
bool firstTime = 1;
|
||||
|
||||
public:
|
||||
GenericThreadModule();
|
||||
|
||||
protected:
|
||||
unsigned int my_interval = 10000; // interval in millisconds
|
||||
virtual int32_t runOnce() override;
|
||||
};
|
||||
|
||||
extern GenericThreadModule *genericThreadModule;
|
||||
@@ -0,0 +1,313 @@
|
||||
#if !MESHTASTIC_EXCLUDE_PKI
|
||||
#include "KeyVerificationModule.h"
|
||||
#include "MeshService.h"
|
||||
#include "RTC.h"
|
||||
#include "graphics/draw/MenuHandler.h"
|
||||
#include "main.h"
|
||||
#include "meshUtils.h"
|
||||
#include "modules/AdminModule.h"
|
||||
#include <SHA256.h>
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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->showNumberPicker("Enter Security Number", 60000, 6, [](int number_picked) -> void {
|
||||
keyVerificationModule->processSecurityNumber(number_picked);
|
||||
});)
|
||||
|
||||
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
|
||||
cn->level = meshtastic_LogRecord_Level_WARNING;
|
||||
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);
|
||||
LOG_INFO("Hash1 matches!");
|
||||
static const char *optionsArray[] = {"Reject", "Accept"};
|
||||
// Don't try to put the array definition in the macro. Does not work with curly braces.
|
||||
IF_SCREEN(graphics::BannerOverlayOptions options; options.message = message; options.durationMs = 30000;
|
||||
options.optionsArrayPtr = optionsArray; options.optionsCount = 2;
|
||||
options.notificationType = graphics::notificationTypeEnum::selection_picker;
|
||||
options.bannerCallback =
|
||||
[=](int selected) {
|
||||
if (selected == 1) {
|
||||
auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
|
||||
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
||||
}
|
||||
};
|
||||
screen->showOverlayBanner(options);)
|
||||
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
|
||||
cn->level = meshtastic_LogRecord_Level_WARNING;
|
||||
sprintf(cn->message, "Final confirmation for incoming manual key verification %s", message);
|
||||
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) {
|
||||
IF_SCREEN(graphics::menuHandler::menuQueue = graphics::menuHandler::throttle_message;)
|
||||
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->showSimpleBanner(message, 30000); LOG_WARN("%s", message);)
|
||||
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
|
||||
cn->level = meshtastic_LogRecord_Level_WARNING;
|
||||
sprintf(cn->message, "Incoming Key Verification.\nSecurity Number\n%03u %03u", currentSecurityNumber / 1000,
|
||||
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;
|
||||
IF_SCREEN(screen->requestMenu(graphics::menuHandler::key_verification_final_prompt);)
|
||||
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
|
||||
cn->level = meshtastic_LogRecord_Level_WARNING;
|
||||
sprintf(cn->message, "Final confirmation for outgoing manual key verification %s", message);
|
||||
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 60 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
|
||||
@@ -0,0 +1,65 @@
|
||||
#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);
|
||||
void generateVerificationCode(char *); // fills char with the user readable verification code
|
||||
uint32_t getCurrentRemoteNode() { return currentRemoteNode; }
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
extern KeyVerificationModule *keyVerificationModule;
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
/*
|
||||
* To developers:
|
||||
* Use this to enable / disable features in your module that you don't want to risk checking into GitHub.
|
||||
*
|
||||
*/
|
||||
|
||||
// Enable development more for StoreForwardModule
|
||||
bool StoreForward_Dev = false;
|
||||
@@ -0,0 +1,304 @@
|
||||
#include "configuration.h"
|
||||
#if !MESHTASTIC_EXCLUDE_INPUTBROKER
|
||||
#include "buzz/BuzzerFeedbackThread.h"
|
||||
#include "input/ExpressLRSFiveWay.h"
|
||||
#include "input/InputBroker.h"
|
||||
#include "input/RotaryEncoderImpl.h"
|
||||
#include "input/RotaryEncoderInterruptImpl1.h"
|
||||
#include "input/SerialKeyboardImpl.h"
|
||||
#include "input/UpDownInterruptImpl1.h"
|
||||
#include "input/i2cButton.h"
|
||||
#include "modules/SystemCommandsModule.h"
|
||||
#if HAS_TRACKBALL
|
||||
#include "input/TrackballInterruptImpl1.h"
|
||||
#endif
|
||||
|
||||
#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
|
||||
#if !MESHTASTIC_EXCLUDE_ATAK
|
||||
#include "modules/AtakPluginModule.h"
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_CANNEDMESSAGES
|
||||
#include "modules/CannedMessageModule.h"
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_DETECTIONSENSOR
|
||||
#include "modules/DetectionSensorModule.h"
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_NEIGHBORINFO
|
||||
#include "modules/NeighborInfoModule.h"
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_NODEINFO
|
||||
#include "modules/NodeInfoModule.h"
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_GPS
|
||||
#include "modules/PositionModule.h"
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_REMOTEHARDWARE
|
||||
#include "modules/RemoteHardwareModule.h"
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_POWERSTRESS
|
||||
#include "modules/PowerStressModule.h"
|
||||
#endif
|
||||
#include "modules/RoutingModule.h"
|
||||
#include "modules/TextMessageModule.h"
|
||||
#if !MESHTASTIC_EXCLUDE_TRACEROUTE
|
||||
#include "modules/TraceRouteModule.h"
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_WAYPOINT
|
||||
#include "modules/WaypointModule.h"
|
||||
#endif
|
||||
#if ARCH_PORTDUINO
|
||||
#include "input/LinuxInputImpl.h"
|
||||
#include "input/SeesawRotary.h"
|
||||
#include "modules/Telemetry/HostMetrics.h"
|
||||
#if !MESHTASTIC_EXCLUDE_STOREFORWARD
|
||||
#include "modules/StoreForwardModule.h"
|
||||
#endif
|
||||
#endif
|
||||
#if HAS_TELEMETRY
|
||||
#include "modules/Telemetry/DeviceTelemetry.h"
|
||||
#endif
|
||||
#if HAS_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
||||
#include "main.h"
|
||||
#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"
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE
|
||||
#include "modules/GenericThreadModule.h"
|
||||
#endif
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
#if defined(USE_SX1280) && !MESHTASTIC_EXCLUDE_AUDIO
|
||||
#include "modules/esp32/AudioModule.h"
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_PAXCOUNTER
|
||||
#include "modules/esp32/PaxcounterModule.h"
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_STOREFORWARD
|
||||
#include "modules/StoreForwardModule.h"
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION
|
||||
#include "modules/ExternalNotificationModule.h"
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_RANGETEST && !MESHTASTIC_EXCLUDE_GPS
|
||||
#include "modules/RangeTestModule.h"
|
||||
#endif
|
||||
#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !MESHTASTIC_EXCLUDE_SERIAL
|
||||
#include "modules/SerialModule.h"
|
||||
#endif
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_DROPZONE
|
||||
#include "modules/DropzoneModule.h"
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Create module instances here. If you are adding a new module, you must 'new' it here (or somewhere else)
|
||||
*/
|
||||
void setupModules()
|
||||
{
|
||||
#if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_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();
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_NODEINFO
|
||||
nodeInfoModule = new NodeInfoModule();
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_GPS
|
||||
positionModule = new PositionModule();
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_WAYPOINT
|
||||
waypointModule = new WaypointModule();
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_TEXTMESSAGE
|
||||
textMessageModule = new TextMessageModule();
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_TRACEROUTE
|
||||
traceRouteModule = new TraceRouteModule();
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_NEIGHBORINFO
|
||||
if (moduleConfig.has_neighbor_info && moduleConfig.neighbor_info.enabled) {
|
||||
neighborInfoModule = new NeighborInfoModule();
|
||||
}
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_DETECTIONSENSOR
|
||||
if (moduleConfig.has_detection_sensor && moduleConfig.detection_sensor.enabled) {
|
||||
detectionSensorModule = new DetectionSensorModule();
|
||||
}
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_ATAK
|
||||
if (config.device.role == meshtastic_Config_DeviceConfig_Role_TAK ||
|
||||
config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) {
|
||||
atakPluginModule = new AtakPluginModule();
|
||||
}
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_PKI
|
||||
keyVerificationModule = new KeyVerificationModule();
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_DROPZONE
|
||||
dropzoneModule = new DropzoneModule();
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE
|
||||
new GenericThreadModule();
|
||||
#endif
|
||||
// Note: if the rest of meshtastic doesn't need to explicitly use your module, you do not need to assign the instance
|
||||
// to a global variable.
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_REMOTEHARDWARE
|
||||
new RemoteHardwareModule();
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_POWERSTRESS
|
||||
new PowerStressModule();
|
||||
#endif
|
||||
// Example: Put your module here
|
||||
// new ReplyModule();
|
||||
#if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER
|
||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||
rotaryEncoderInterruptImpl1 = new RotaryEncoderInterruptImpl1();
|
||||
if (!rotaryEncoderInterruptImpl1->init()) {
|
||||
delete rotaryEncoderInterruptImpl1;
|
||||
rotaryEncoderInterruptImpl1 = nullptr;
|
||||
}
|
||||
#ifdef T_LORA_PAGER
|
||||
// use a special FSM based rotary encoder version for T-LoRa Pager
|
||||
rotaryEncoderImpl = new RotaryEncoderImpl();
|
||||
if (!rotaryEncoderImpl->init()) {
|
||||
delete rotaryEncoderImpl;
|
||||
rotaryEncoderImpl = nullptr;
|
||||
}
|
||||
#else
|
||||
upDownInterruptImpl1 = new UpDownInterruptImpl1();
|
||||
if (!upDownInterruptImpl1->init()) {
|
||||
delete upDownInterruptImpl1;
|
||||
upDownInterruptImpl1 = nullptr;
|
||||
}
|
||||
#endif
|
||||
cardKbI2cImpl = new CardKbI2cImpl();
|
||||
cardKbI2cImpl->init();
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
i2cButton = new i2cButtonThread("i2cButtonThread");
|
||||
#endif
|
||||
#ifdef INPUTBROKER_MATRIX_TYPE
|
||||
kbMatrixImpl = new KbMatrixImpl();
|
||||
kbMatrixImpl->init();
|
||||
#endif // INPUTBROKER_MATRIX_TYPE
|
||||
#ifdef INPUTBROKER_SERIAL_TYPE
|
||||
aSerialKeyboardImpl = new SerialKeyboardImpl();
|
||||
aSerialKeyboardImpl->init();
|
||||
#endif // INPUTBROKER_MATRIX_TYPE
|
||||
}
|
||||
#endif // HAS_BUTTON
|
||||
#if ARCH_PORTDUINO
|
||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||
seesawRotary = new SeesawRotary("SeesawRotary");
|
||||
if (!seesawRotary->init()) {
|
||||
delete seesawRotary;
|
||||
seesawRotary = nullptr;
|
||||
}
|
||||
aLinuxInputImpl = new LinuxInputImpl();
|
||||
aLinuxInputImpl->init();
|
||||
}
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_INPUTBROKER && HAS_TRACKBALL
|
||||
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
|
||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||
cannedMessageModule = new CannedMessageModule();
|
||||
}
|
||||
#endif
|
||||
#if ARCH_PORTDUINO
|
||||
new HostMetricsModule();
|
||||
#endif
|
||||
#if HAS_TELEMETRY
|
||||
new DeviceTelemetryModule();
|
||||
#endif
|
||||
#if HAS_TELEMETRY && HAS_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
||||
if (moduleConfig.has_telemetry &&
|
||||
(moduleConfig.telemetry.environment_measurement_enabled || moduleConfig.telemetry.environment_screen_enabled)) {
|
||||
new EnvironmentTelemetryModule();
|
||||
}
|
||||
#if __has_include("Adafruit_PM25AQI.h")
|
||||
if (moduleConfig.has_telemetry && moduleConfig.telemetry.air_quality_enabled &&
|
||||
nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first > 0) {
|
||||
new AirQualityTelemetryModule();
|
||||
}
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY
|
||||
if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MAX30102].first > 0 ||
|
||||
nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MLX90614].first > 0) {
|
||||
new HealthTelemetryModule();
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_POWER_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
||||
if (moduleConfig.has_telemetry &&
|
||||
(moduleConfig.telemetry.power_measurement_enabled || moduleConfig.telemetry.power_screen_enabled)) {
|
||||
new PowerTelemetryModule();
|
||||
}
|
||||
#endif
|
||||
#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_STM32WL)) && \
|
||||
!defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3)
|
||||
#if !MESHTASTIC_EXCLUDE_SERIAL
|
||||
if (moduleConfig.has_serial && moduleConfig.serial.enabled &&
|
||||
config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||
new SerialModule();
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
#ifdef ARCH_ESP32
|
||||
// Only run on an esp32 based device.
|
||||
#if defined(USE_SX1280) && !MESHTASTIC_EXCLUDE_AUDIO
|
||||
audioModule = new AudioModule();
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_PAXCOUNTER
|
||||
if (moduleConfig.has_paxcounter && moduleConfig.paxcounter.enabled) {
|
||||
paxcounterModule = new PaxcounterModule();
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
#if defined(ARCH_ESP32) || defined(ARCH_PORTDUINO)
|
||||
#if !MESHTASTIC_EXCLUDE_STOREFORWARD
|
||||
if (moduleConfig.has_store_forward && moduleConfig.store_forward.enabled) {
|
||||
storeForwardModule = new StoreForwardModule();
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION
|
||||
externalNotificationModule = new ExternalNotificationModule();
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_RANGETEST && !MESHTASTIC_EXCLUDE_GPS
|
||||
if (moduleConfig.has_range_test && moduleConfig.range_test.enabled)
|
||||
new RangeTestModule();
|
||||
#endif
|
||||
// NOTE! This module must be added LAST because it likes to check for replies from other modules and avoid sending extra
|
||||
// acks
|
||||
routingModule = new RoutingModule();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* Create module instances here. If you are adding a new module, you must 'new' it here (or somewhere else)
|
||||
*/
|
||||
void setupModules();
|
||||
@@ -0,0 +1,255 @@
|
||||
#include "NeighborInfoModule.h"
|
||||
#include "Default.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "RTC.h"
|
||||
#include <Throttle.h>
|
||||
|
||||
NeighborInfoModule *neighborInfoModule;
|
||||
|
||||
/*
|
||||
Prints a single neighbor info packet and associated neighbors
|
||||
Uses LOG_DEBUG, which equates to Console.log
|
||||
NOTE: For debugging only
|
||||
*/
|
||||
void NeighborInfoModule::printNeighborInfo(const char *header, const meshtastic_NeighborInfo *np)
|
||||
{
|
||||
LOG_DEBUG("%s NEIGHBORINFO PACKET from Node 0x%x to Node 0x%x (last sent by 0x%x)", header, np->node_id, nodeDB->getNodeNum(),
|
||||
np->last_sent_by_id);
|
||||
LOG_DEBUG("Packet contains %d neighbors", np->neighbors_count);
|
||||
for (int i = 0; i < np->neighbors_count; i++) {
|
||||
LOG_DEBUG("Neighbor %d: node_id=0x%x, snr=%.2f", i, np->neighbors[i].node_id, np->neighbors[i].snr);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Prints the nodeDB neighbors
|
||||
NOTE: for debugging only
|
||||
*/
|
||||
void NeighborInfoModule::printNodeDBNeighbors()
|
||||
{
|
||||
LOG_DEBUG("Our NodeDB contains %d neighbors", neighbors.size());
|
||||
for (size_t i = 0; i < neighbors.size(); i++) {
|
||||
LOG_DEBUG("Node %d: node_id=0x%x, snr=%.2f", i, neighbors[i].node_id, neighbors[i].snr);
|
||||
}
|
||||
}
|
||||
|
||||
/* Send our initial owner announcement 35 seconds after we start (to give
|
||||
* network time to setup) */
|
||||
NeighborInfoModule::NeighborInfoModule()
|
||||
: ProtobufModule("neighborinfo", meshtastic_PortNum_NEIGHBORINFO_APP, &meshtastic_NeighborInfo_msg),
|
||||
concurrency::OSThread("NeighborInfo")
|
||||
{
|
||||
ourPortNum = meshtastic_PortNum_NEIGHBORINFO_APP;
|
||||
nodeStatusObserver.observe(&nodeStatus->onNewStatus);
|
||||
|
||||
if (moduleConfig.neighbor_info.enabled) {
|
||||
isPromiscuous = true; // Update neighbors from all packets
|
||||
setIntervalFromNow(Default::getConfiguredOrDefaultMs(moduleConfig.neighbor_info.update_interval,
|
||||
default_telemetry_broadcast_interval_secs));
|
||||
} else {
|
||||
LOG_DEBUG("NeighborInfoModule is disabled");
|
||||
disable();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Collect neighbor info from the nodeDB's history, capping at a maximum number of
|
||||
entries and max time Assumes that the neighborInfo packet has been allocated
|
||||
@returns the number of entries collected
|
||||
*/
|
||||
uint32_t NeighborInfoModule::collectNeighborInfo(meshtastic_NeighborInfo *neighborInfo)
|
||||
{
|
||||
NodeNum my_node_id = nodeDB->getNodeNum();
|
||||
neighborInfo->node_id = my_node_id;
|
||||
neighborInfo->last_sent_by_id = my_node_id;
|
||||
neighborInfo->node_broadcast_interval_secs =
|
||||
Default::getConfiguredOrDefault(moduleConfig.neighbor_info.update_interval, default_telemetry_broadcast_interval_secs);
|
||||
|
||||
cleanUpNeighbors();
|
||||
|
||||
for (auto nbr : neighbors) {
|
||||
if ((neighborInfo->neighbors_count < MAX_NUM_NEIGHBORS) && (nbr.node_id != my_node_id)) {
|
||||
neighborInfo->neighbors[neighborInfo->neighbors_count].node_id = nbr.node_id;
|
||||
neighborInfo->neighbors[neighborInfo->neighbors_count].snr = nbr.snr;
|
||||
// Note: we don't set the last_rx_time and node_broadcast_intervals_secs
|
||||
// here, because we don't want to send this over the mesh
|
||||
neighborInfo->neighbors_count++;
|
||||
}
|
||||
}
|
||||
printNodeDBNeighbors();
|
||||
return neighborInfo->neighbors_count;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove neighbors from the database that we haven't heard from in a while
|
||||
*/
|
||||
void NeighborInfoModule::cleanUpNeighbors()
|
||||
{
|
||||
uint32_t now = getTime();
|
||||
NodeNum my_node_id = nodeDB->getNodeNum();
|
||||
for (auto it = neighbors.rbegin(); it != neighbors.rend();) {
|
||||
// We will remove a neighbor if we haven't heard from them in twice the
|
||||
// broadcast interval cannot use isWithinTimespanMs() as it->last_rx_time is
|
||||
// seconds since 1970
|
||||
if ((now - it->last_rx_time > it->node_broadcast_interval_secs * 2) && (it->node_id != my_node_id)) {
|
||||
LOG_DEBUG("Remove neighbor with node ID 0x%x", it->node_id);
|
||||
it = std::vector<meshtastic_Neighbor>::reverse_iterator(
|
||||
neighbors.erase(std::next(it).base())); // Erase the element and update the iterator
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Send neighbor info to the mesh */
|
||||
void NeighborInfoModule::sendNeighborInfo(NodeNum dest, bool wantReplies)
|
||||
{
|
||||
meshtastic_NeighborInfo neighborInfo = meshtastic_NeighborInfo_init_zero;
|
||||
collectNeighborInfo(&neighborInfo);
|
||||
// only send neighbours if we have some to send
|
||||
if (neighborInfo.neighbors_count > 0) {
|
||||
meshtastic_MeshPacket *p = allocDataProtobuf(neighborInfo);
|
||||
p->to = dest;
|
||||
p->decoded.want_response = wantReplies;
|
||||
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||
printNeighborInfo("SENDING", &neighborInfo);
|
||||
service->sendToMesh(p, RX_SRC_LOCAL, true);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Encompasses the full construction and sending packet to mesh
|
||||
Will be used for broadcast.
|
||||
*/
|
||||
int32_t NeighborInfoModule::runOnce()
|
||||
{
|
||||
if (moduleConfig.neighbor_info.transmit_over_lora &&
|
||||
(!channels.isDefaultChannel(channels.getPrimaryIndex()) || !RadioInterface::uses_default_frequency_slot) &&
|
||||
airTime->isTxAllowedChannelUtil(true) && airTime->isTxAllowedAirUtil()) {
|
||||
sendNeighborInfo(NODENUM_BROADCAST, false);
|
||||
} else {
|
||||
sendNeighborInfo(NODENUM_BROADCAST_NO_LORA, false);
|
||||
}
|
||||
return Default::getConfiguredOrDefaultMs(moduleConfig.neighbor_info.update_interval, default_neighbor_info_broadcast_secs);
|
||||
}
|
||||
|
||||
meshtastic_MeshPacket *NeighborInfoModule::allocReply()
|
||||
{
|
||||
LOG_INFO("NeighborInfoRequested.");
|
||||
if (lastSentReply && Throttle::isWithinTimespanMs(lastSentReply, 3 * 60 * 1000)) {
|
||||
LOG_DEBUG("Skip Neighbors reply since we sent a reply <3min ago");
|
||||
ignoreRequest = true; // Mark it as ignored for MeshModule
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
meshtastic_NeighborInfo neighborInfo = meshtastic_NeighborInfo_init_zero;
|
||||
collectNeighborInfo(&neighborInfo);
|
||||
|
||||
meshtastic_MeshPacket *reply = allocDataProtobuf(neighborInfo);
|
||||
|
||||
if (reply) {
|
||||
lastSentReply = millis(); // Track when we sent this reply
|
||||
}
|
||||
return reply;
|
||||
}
|
||||
|
||||
/*
|
||||
Collect a received neighbor info packet from another node
|
||||
Pass it to an upper client; do not persist this data on the mesh
|
||||
*/
|
||||
bool NeighborInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_NeighborInfo *np)
|
||||
{
|
||||
LOG_DEBUG("NeighborInfo: handleReceivedProtobuf");
|
||||
if (np) {
|
||||
printNeighborInfo("RECEIVED", np);
|
||||
// Ignore dummy/interceptable packets: single neighbor with nodeId 0 and snr 0
|
||||
if (np->neighbors_count != 1 || np->neighbors[0].node_id != 0 || np->neighbors[0].snr != 0.0f) {
|
||||
LOG_DEBUG(" Updating neighbours");
|
||||
updateNeighbors(mp, np);
|
||||
} else {
|
||||
LOG_DEBUG(" Ignoring dummy neighbor info packet (single neighbor with nodeId 0, snr 0)");
|
||||
}
|
||||
} else if (mp.hop_start != 0 && mp.hop_start == mp.hop_limit) {
|
||||
LOG_DEBUG("Get or create neighbor: %u with snr %f", mp.from, mp.rx_snr);
|
||||
// If the hopLimit is the same as hopStart, then it is a neighbor
|
||||
getOrCreateNeighbor(mp.from, mp.from, 0,
|
||||
mp.rx_snr); // Set the broadcast interval to 0, as we don't know it
|
||||
}
|
||||
// Allow others to handle this packet
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
Copy the content of a current NeighborInfo packet into a new one and update the
|
||||
last_sent_by_id to our NodeNum
|
||||
*/
|
||||
void NeighborInfoModule::alterReceivedProtobuf(meshtastic_MeshPacket &p, meshtastic_NeighborInfo *n)
|
||||
{
|
||||
n->last_sent_by_id = nodeDB->getNodeNum();
|
||||
|
||||
// Set updated last_sent_by_id to the payload of the to be flooded packet
|
||||
p.decoded.payload.size =
|
||||
pb_encode_to_bytes(p.decoded.payload.bytes, sizeof(p.decoded.payload.bytes), &meshtastic_NeighborInfo_msg, n);
|
||||
}
|
||||
|
||||
void NeighborInfoModule::resetNeighbors()
|
||||
{
|
||||
neighbors.clear();
|
||||
}
|
||||
|
||||
void NeighborInfoModule::updateNeighbors(const meshtastic_MeshPacket &mp, const meshtastic_NeighborInfo *np)
|
||||
{
|
||||
LOG_DEBUG("updateNeighbors");
|
||||
// The last sent ID will be 0 if the packet is from the phone, which we don't
|
||||
// count as an edge. So we assume that if it's zero, then this packet is from
|
||||
// our node.
|
||||
if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.from) {
|
||||
getOrCreateNeighbor(mp.from, np->last_sent_by_id, np->node_broadcast_interval_secs, mp.rx_snr);
|
||||
}
|
||||
}
|
||||
|
||||
meshtastic_Neighbor *NeighborInfoModule::getOrCreateNeighbor(NodeNum originalSender, NodeNum n,
|
||||
uint32_t node_broadcast_interval_secs, float snr)
|
||||
{
|
||||
// our node and the phone are the same node (not neighbors)
|
||||
if (n == 0) {
|
||||
n = nodeDB->getNodeNum();
|
||||
}
|
||||
// look for one in the existing list
|
||||
for (size_t i = 0; i < neighbors.size(); i++) {
|
||||
if (neighbors[i].node_id == n) {
|
||||
// if found, update it
|
||||
neighbors[i].snr = snr;
|
||||
neighbors[i].last_rx_time = getTime();
|
||||
// Only if this is the original sender, the broadcast interval corresponds
|
||||
// to it
|
||||
if (originalSender == n && node_broadcast_interval_secs != 0)
|
||||
neighbors[i].node_broadcast_interval_secs = node_broadcast_interval_secs;
|
||||
return &neighbors[i];
|
||||
}
|
||||
}
|
||||
// otherwise, allocate one and assign data to it
|
||||
|
||||
meshtastic_Neighbor new_nbr = meshtastic_Neighbor_init_zero;
|
||||
new_nbr.node_id = n;
|
||||
new_nbr.snr = snr;
|
||||
new_nbr.last_rx_time = getTime();
|
||||
// Only if this is the original sender, the broadcast interval corresponds to
|
||||
// it
|
||||
if (originalSender == n && node_broadcast_interval_secs != 0)
|
||||
new_nbr.node_broadcast_interval_secs = node_broadcast_interval_secs;
|
||||
else // Assume the same broadcast interval as us for the neighbor if we don't
|
||||
// know it
|
||||
new_nbr.node_broadcast_interval_secs = moduleConfig.neighbor_info.update_interval;
|
||||
|
||||
if (neighbors.size() < MAX_NUM_NEIGHBORS) {
|
||||
neighbors.push_back(new_nbr);
|
||||
} else {
|
||||
// If we have too many neighbors, replace the oldest one
|
||||
LOG_WARN("Neighbor DB is full, replace oldest neighbor");
|
||||
neighbors.erase(neighbors.begin());
|
||||
neighbors.push_back(new_nbr);
|
||||
}
|
||||
return &neighbors.back();
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
#pragma once
|
||||
#include "ProtobufModule.h"
|
||||
#define MAX_NUM_NEIGHBORS 10 // also defined in NeighborInfo protobuf options
|
||||
|
||||
/*
|
||||
* Neighborinfo module for sending info on each node's 0-hop neighbors to the mesh
|
||||
*/
|
||||
class NeighborInfoModule : public ProtobufModule<meshtastic_NeighborInfo>, private concurrency::OSThread
|
||||
{
|
||||
CallbackObserver<NeighborInfoModule, const meshtastic::Status *> nodeStatusObserver =
|
||||
CallbackObserver<NeighborInfoModule, const meshtastic::Status *>(this, &NeighborInfoModule::handleStatusUpdate);
|
||||
|
||||
std::vector<meshtastic_Neighbor> neighbors;
|
||||
|
||||
public:
|
||||
/*
|
||||
* Expose the constructor
|
||||
*/
|
||||
NeighborInfoModule();
|
||||
|
||||
/* Reset neighbor info after clearing nodeDB*/
|
||||
void resetNeighbors();
|
||||
|
||||
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_NeighborInfo *nb) override;
|
||||
|
||||
/* Messages can be received that have the want_response bit set. If set, this callback will be invoked
|
||||
* so that subclasses can (optionally) send a response back to the original sender. */
|
||||
virtual meshtastic_MeshPacket *allocReply() override;
|
||||
|
||||
/*
|
||||
* Collect neighbor info from the nodeDB's history, capping at a maximum number of entries and max time
|
||||
* @return the number of entries collected
|
||||
*/
|
||||
uint32_t collectNeighborInfo(meshtastic_NeighborInfo *neighborInfo);
|
||||
|
||||
/*
|
||||
Remove neighbors from the database that we haven't heard from in a while
|
||||
*/
|
||||
void cleanUpNeighbors();
|
||||
|
||||
/* Allocate a new NeighborInfo packet */
|
||||
meshtastic_NeighborInfo *allocateNeighborInfoPacket();
|
||||
|
||||
// Find a neighbor in our DB, create an empty neighbor if missing
|
||||
meshtastic_Neighbor *getOrCreateNeighbor(NodeNum originalSender, NodeNum n, uint32_t node_broadcast_interval_secs, float snr);
|
||||
|
||||
/*
|
||||
* Send info on our node's neighbors into the mesh
|
||||
*/
|
||||
void sendNeighborInfo(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false);
|
||||
|
||||
/* update neighbors with subpacket sniffed from network */
|
||||
void updateNeighbors(const meshtastic_MeshPacket &mp, const meshtastic_NeighborInfo *np);
|
||||
|
||||
/* update a NeighborInfo packet with our NodeNum as last_sent_by_id */
|
||||
void alterReceivedProtobuf(meshtastic_MeshPacket &p, meshtastic_NeighborInfo *n) override;
|
||||
|
||||
/* Does our periodic broadcast */
|
||||
int32_t runOnce() override;
|
||||
|
||||
/* Override wantPacket to say we want to see all packets when enabled, not just those for our port number.
|
||||
Exception is when the packet came via MQTT */
|
||||
virtual bool wantPacket(const meshtastic_MeshPacket *p) override { return enabled && !p->via_mqtt; }
|
||||
|
||||
/* These are for debugging only */
|
||||
void printNeighborInfo(const char *header, const meshtastic_NeighborInfo *np);
|
||||
void printNodeDBNeighbors();
|
||||
|
||||
private:
|
||||
uint32_t lastSentReply = 0; // Last time we sent a position reply (used for reply throttling only)
|
||||
};
|
||||
extern NeighborInfoModule *neighborInfoModule;
|
||||
@@ -0,0 +1,148 @@
|
||||
#include "NodeInfoModule.h"
|
||||
#include "Default.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "RTC.h"
|
||||
#include "Router.h"
|
||||
#include "configuration.h"
|
||||
#include "main.h"
|
||||
#include <Throttle.h>
|
||||
|
||||
NodeInfoModule *nodeInfoModule;
|
||||
|
||||
bool NodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_User *pptr)
|
||||
{
|
||||
if (mp.from == nodeDB->getNodeNum()) {
|
||||
LOG_WARN("Ignoring packet supposed to be from our own node: %08x", mp.from);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto p = *pptr;
|
||||
if (p.is_licensed != owner.is_licensed) {
|
||||
LOG_WARN("Invalid nodeInfo detected, is_licensed mismatch!");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Coerce user.id to be derived from the node number
|
||||
snprintf(p.id, sizeof(p.id), "!%08x", getFrom(&mp));
|
||||
|
||||
bool hasChanged = nodeDB->updateUser(getFrom(&mp), p, mp.channel);
|
||||
|
||||
bool wasBroadcast = isBroadcast(mp.to);
|
||||
|
||||
// LOG_DEBUG("did encode");
|
||||
// if user has changed while packet was not for us, inform phone
|
||||
if (hasChanged && !wasBroadcast && !isToUs(&mp)) {
|
||||
auto packetCopy = packetPool.allocCopy(mp); // Keep a copy of the packet for later analysis
|
||||
|
||||
// Re-encode the user protobuf, as we have stripped out the user.id
|
||||
packetCopy->decoded.payload.size = pb_encode_to_bytes(
|
||||
packetCopy->decoded.payload.bytes, sizeof(packetCopy->decoded.payload.bytes), &meshtastic_User_msg, &p);
|
||||
|
||||
service->sendToPhone(packetCopy);
|
||||
}
|
||||
|
||||
// LOG_DEBUG("did handleReceived");
|
||||
return false; // Let others look at this message also if they want
|
||||
}
|
||||
|
||||
void NodeInfoModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_User *p)
|
||||
{
|
||||
// Coerce user.id to be derived from the node number
|
||||
snprintf(p->id, sizeof(p->id), "!%08x", getFrom(&mp));
|
||||
|
||||
// Re-encode the altered protobuf back into the packet
|
||||
mp.decoded.payload.size =
|
||||
pb_encode_to_bytes(mp.decoded.payload.bytes, sizeof(mp.decoded.payload.bytes), &meshtastic_User_msg, p);
|
||||
}
|
||||
|
||||
void NodeInfoModule::sendOurNodeInfo(NodeNum dest, bool wantReplies, uint8_t channel, bool _shorterTimeout)
|
||||
{
|
||||
// cancel any not yet sent (now stale) position packets
|
||||
if (prevPacketId) // if we wrap around to zero, we'll simply fail to cancel in that rare case (no big deal)
|
||||
service->cancelSending(prevPacketId);
|
||||
shorterTimeout = _shorterTimeout;
|
||||
DEBUG_HEAP_BEFORE;
|
||||
meshtastic_MeshPacket *p = allocReply();
|
||||
DEBUG_HEAP_AFTER("NodeInfoModule::sendOurNodeInfo", p);
|
||||
|
||||
if (p) { // Check whether we didn't ignore it
|
||||
p->to = dest;
|
||||
p->decoded.want_response = (config.device.role != meshtastic_Config_DeviceConfig_Role_TRACKER &&
|
||||
config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) &&
|
||||
wantReplies;
|
||||
if (_shorterTimeout)
|
||||
p->priority = meshtastic_MeshPacket_Priority_DEFAULT;
|
||||
else
|
||||
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||
if (channel > 0) {
|
||||
LOG_DEBUG("Send ourNodeInfo to channel %d", channel);
|
||||
p->channel = channel;
|
||||
}
|
||||
|
||||
prevPacketId = p->id;
|
||||
|
||||
service->sendToMesh(p);
|
||||
shorterTimeout = false;
|
||||
}
|
||||
}
|
||||
|
||||
meshtastic_MeshPacket *NodeInfoModule::allocReply()
|
||||
{
|
||||
if (!airTime->isTxAllowedChannelUtil(false)) {
|
||||
ignoreRequest = true; // Mark it as ignored for MeshModule
|
||||
LOG_DEBUG("Skip send NodeInfo > 40%% ch. util");
|
||||
return NULL;
|
||||
}
|
||||
// If we sent our NodeInfo less than 5 min. ago, don't send it again as it may be still underway.
|
||||
if (!shorterTimeout && lastSentToMesh && Throttle::isWithinTimespanMs(lastSentToMesh, 5 * 60 * 1000)) {
|
||||
LOG_DEBUG("Skip send NodeInfo since we sent it <5min ago");
|
||||
ignoreRequest = true; // Mark it as ignored for MeshModule
|
||||
return NULL;
|
||||
} else if (shorterTimeout && lastSentToMesh && Throttle::isWithinTimespanMs(lastSentToMesh, 60 * 1000)) {
|
||||
LOG_DEBUG("Skip send NodeInfo since we sent it <60s ago");
|
||||
ignoreRequest = true; // Mark it as ignored for MeshModule
|
||||
return NULL;
|
||||
} else {
|
||||
ignoreRequest = false; // Don't ignore requests anymore
|
||||
meshtastic_User &u = owner;
|
||||
|
||||
// Strip the public key if the user is licensed
|
||||
if (u.is_licensed && u.public_key.size > 0) {
|
||||
u.public_key.bytes[0] = 0;
|
||||
u.public_key.size = 0;
|
||||
}
|
||||
|
||||
// FIXME: Clear the user.id field since it should be derived from node number on the receiving end
|
||||
// u.id[0] = '\0';
|
||||
|
||||
// Ensure our user.id is derived correctly
|
||||
strcpy(u.id, nodeDB->getNodeId().c_str());
|
||||
|
||||
LOG_INFO("Send owner %s/%s/%s", u.id, u.long_name, u.short_name);
|
||||
lastSentToMesh = millis();
|
||||
return allocDataProtobuf(u);
|
||||
}
|
||||
}
|
||||
|
||||
NodeInfoModule::NodeInfoModule()
|
||||
: ProtobufModule("nodeinfo", meshtastic_PortNum_NODEINFO_APP, &meshtastic_User_msg), concurrency::OSThread("NodeInfo")
|
||||
{
|
||||
isPromiscuous = true; // We always want to update our nodedb, even if we are sniffing on others
|
||||
|
||||
setIntervalFromNow(setStartDelay()); // Send our initial owner announcement 30 seconds
|
||||
// after we start (to give network time to setup)
|
||||
}
|
||||
|
||||
int32_t NodeInfoModule::runOnce()
|
||||
{
|
||||
// If we changed channels, ask everyone else for their latest info
|
||||
bool requestReplies = currentGeneration != radioGeneration;
|
||||
currentGeneration = radioGeneration;
|
||||
|
||||
if (airTime->isTxAllowedAirUtil() && config.device.role != meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN) {
|
||||
LOG_INFO("Send our nodeinfo to mesh (wantReplies=%d)", requestReplies);
|
||||
sendOurNodeInfo(NODENUM_BROADCAST, requestReplies); // Send our info (don't request replies)
|
||||
}
|
||||
return Default::getConfiguredOrDefaultMs(config.device.node_info_broadcast_secs, default_node_info_broadcast_secs);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
#include "ProtobufModule.h"
|
||||
|
||||
/**
|
||||
* NodeInfo module for sending/receiving NodeInfos into the mesh
|
||||
*/
|
||||
class NodeInfoModule : public ProtobufModule<meshtastic_User>, private concurrency::OSThread
|
||||
{
|
||||
/// The id of the last packet we sent, to allow us to cancel it if we make something fresher
|
||||
PacketId prevPacketId = 0;
|
||||
|
||||
uint32_t currentGeneration = 0;
|
||||
|
||||
public:
|
||||
/** Constructor
|
||||
* name is for debugging output
|
||||
*/
|
||||
NodeInfoModule();
|
||||
|
||||
/**
|
||||
* Send our NodeInfo into the mesh
|
||||
*/
|
||||
void sendOurNodeInfo(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false, uint8_t channel = 0,
|
||||
bool _shorterTimeout = false);
|
||||
|
||||
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_User *p) override;
|
||||
|
||||
/** Called to alter received User protobuf */
|
||||
virtual void alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_User *p) override;
|
||||
|
||||
/** Messages can be received that have the want_response bit set. If set, this callback will be invoked
|
||||
* so that subclasses can (optionally) send a response back to the original sender. */
|
||||
virtual meshtastic_MeshPacket *allocReply() override;
|
||||
|
||||
/** Does our periodic broadcast */
|
||||
virtual int32_t runOnce() override;
|
||||
|
||||
private:
|
||||
uint32_t lastSentToMesh = 0; // Last time we sent our NodeInfo to the mesh
|
||||
bool shorterTimeout = false;
|
||||
};
|
||||
|
||||
extern NodeInfoModule *nodeInfoModule;
|
||||
@@ -0,0 +1,514 @@
|
||||
#if !MESHTASTIC_EXCLUDE_GPS
|
||||
#include "PositionModule.h"
|
||||
#include "Default.h"
|
||||
#include "GPS.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "RTC.h"
|
||||
#include "Router.h"
|
||||
#include "TypeConversions.h"
|
||||
#include "airtime.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/GeoCoord.h"
|
||||
#include "main.h"
|
||||
#include "mesh/compression/unishox2.h"
|
||||
#include "meshUtils.h"
|
||||
#include "meshtastic/atak.pb.h"
|
||||
#include "sleep.h"
|
||||
#include "target_specific.h"
|
||||
#include <Throttle.h>
|
||||
|
||||
PositionModule *positionModule;
|
||||
|
||||
PositionModule::PositionModule()
|
||||
: ProtobufModule("position", meshtastic_PortNum_POSITION_APP, &meshtastic_Position_msg), concurrency::OSThread("Position")
|
||||
{
|
||||
precision = 0; // safe starting value
|
||||
isPromiscuous = true; // We always want to update our nodedb, even if we are sniffing on others
|
||||
nodeStatusObserver.observe(&nodeStatus->onNewStatus);
|
||||
|
||||
if (config.device.role != meshtastic_Config_DeviceConfig_Role_TRACKER &&
|
||||
config.device.role != meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) {
|
||||
setIntervalFromNow(setStartDelay());
|
||||
}
|
||||
|
||||
// Power saving trackers should clear their position on startup to avoid waking up and sending a stale position
|
||||
if ((config.device.role == meshtastic_Config_DeviceConfig_Role_TRACKER ||
|
||||
config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) &&
|
||||
config.power.is_power_saving) {
|
||||
LOG_DEBUG("Clear position on startup for sleepy tracker (ー。ー) zzz");
|
||||
nodeDB->clearLocalPosition();
|
||||
}
|
||||
}
|
||||
|
||||
bool PositionModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Position *pptr)
|
||||
{
|
||||
auto p = *pptr;
|
||||
|
||||
// If inbound message is a replay (or spoof!) of our own messages, we shouldn't process
|
||||
// (why use second-hand sources for our own data?)
|
||||
|
||||
// FIXME this can in fact happen with packets sent from EUD (src=RX_SRC_USER)
|
||||
// to set fixed location, EUD-GPS location or just the time (see also issue #900)
|
||||
bool isLocal = false;
|
||||
if (isFromUs(&mp)) {
|
||||
isLocal = true;
|
||||
if (config.position.fixed_position) {
|
||||
LOG_DEBUG("Ignore incoming position update from myself except for time, because position.fixed_position is true");
|
||||
|
||||
#ifdef T_WATCH_S3
|
||||
// Since we return early if position.fixed_position is true, set the T-Watch's RTC to the time received from the
|
||||
// client device here
|
||||
if (p.time && channels.getByIndex(mp.channel).role == meshtastic_Channel_Role_PRIMARY) {
|
||||
trySetRtc(p, isLocal, true);
|
||||
}
|
||||
#endif
|
||||
|
||||
nodeDB->setLocalPosition(p, true);
|
||||
return false;
|
||||
} else {
|
||||
LOG_DEBUG("Incoming update from MYSELF");
|
||||
nodeDB->setLocalPosition(p);
|
||||
}
|
||||
}
|
||||
|
||||
// Log packet size and data fields
|
||||
LOG_DEBUG("POSITION node=%08x l=%d lat=%d lon=%d msl=%d hae=%d geo=%d pdop=%d hdop=%d vdop=%d siv=%d fxq=%d fxt=%d pts=%d "
|
||||
"time=%d",
|
||||
getFrom(&mp), mp.decoded.payload.size, p.latitude_i, p.longitude_i, p.altitude, p.altitude_hae,
|
||||
p.altitude_geoidal_separation, p.PDOP, p.HDOP, p.VDOP, p.sats_in_view, p.fix_quality, p.fix_type, p.timestamp,
|
||||
p.time);
|
||||
|
||||
if (p.time && channels.getByIndex(mp.channel).role == meshtastic_Channel_Role_PRIMARY) {
|
||||
bool force = false;
|
||||
|
||||
#ifdef T_WATCH_S3
|
||||
// The T-Watch appears to "pause" its RTC when shut down, such that the time it reads upon powering on is the same as when
|
||||
// it was shut down. So we need to force the update here, since otherwise RTC::perhapsSetRTC will ignore it because it
|
||||
// will always be an equivalent or lesser RTCQuality (RTCQualityNTP or RTCQualityNet).
|
||||
force = true;
|
||||
#endif
|
||||
// Set from phone RTC Quality to RTCQualityNTP since it should be approximately so
|
||||
trySetRtc(p, isLocal, force);
|
||||
}
|
||||
|
||||
nodeDB->updatePosition(getFrom(&mp), p);
|
||||
if (channels.getByIndex(mp.channel).settings.has_module_settings) {
|
||||
precision = channels.getByIndex(mp.channel).settings.module_settings.position_precision;
|
||||
} else if (channels.getByIndex(mp.channel).role == meshtastic_Channel_Role_PRIMARY) {
|
||||
precision = 32;
|
||||
} else {
|
||||
precision = 0;
|
||||
}
|
||||
|
||||
return false; // Let others look at this message also if they want
|
||||
}
|
||||
|
||||
void PositionModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_Position *p)
|
||||
{
|
||||
// Phone position packets need to be truncated to the channel precision
|
||||
if (isFromUs(&mp) && (precision < 32 && precision > 0)) {
|
||||
LOG_DEBUG("Truncate phone position to channel precision %i", precision);
|
||||
p->latitude_i = p->latitude_i & (UINT32_MAX << (32 - precision));
|
||||
p->longitude_i = p->longitude_i & (UINT32_MAX << (32 - precision));
|
||||
|
||||
// We want the imprecise position to be the middle of the possible location, not
|
||||
p->latitude_i += (1 << (31 - precision));
|
||||
p->longitude_i += (1 << (31 - precision));
|
||||
|
||||
mp.decoded.payload.size =
|
||||
pb_encode_to_bytes(mp.decoded.payload.bytes, sizeof(mp.decoded.payload.bytes), &meshtastic_Position_msg, p);
|
||||
}
|
||||
}
|
||||
|
||||
void PositionModule::trySetRtc(meshtastic_Position p, bool isLocal, bool forceUpdate)
|
||||
{
|
||||
if (hasQualityTimesource() && !isLocal) {
|
||||
LOG_DEBUG("Ignore time from mesh because we have a GPS, RTC, or Phone/NTP time source in the past day");
|
||||
return;
|
||||
}
|
||||
if (!isLocal && p.location_source < meshtastic_Position_LocSource_LOC_INTERNAL) {
|
||||
LOG_DEBUG("Ignore time from mesh because it has a unknown or manual source");
|
||||
return;
|
||||
}
|
||||
struct timeval tv;
|
||||
uint32_t secs = p.time;
|
||||
|
||||
tv.tv_sec = secs;
|
||||
tv.tv_usec = 0;
|
||||
|
||||
perhapsSetRTC(isLocal ? RTCQualityNTP : RTCQualityFromNet, &tv, forceUpdate);
|
||||
}
|
||||
|
||||
bool PositionModule::hasQualityTimesource()
|
||||
{
|
||||
bool setFromPhoneOrNtpToday =
|
||||
lastSetFromPhoneNtpOrGps == 0 ? false : Throttle::isWithinTimespanMs(lastSetFromPhoneNtpOrGps, SEC_PER_DAY * 1000UL);
|
||||
#if MESHTASTIC_EXCLUDE_GPS
|
||||
bool hasGpsOrRtc = (rtc_found.address != ScanI2C::ADDRESS_NONE.address);
|
||||
#else
|
||||
bool hasGpsOrRtc = hasGPS() || (rtc_found.address != ScanI2C::ADDRESS_NONE.address);
|
||||
#endif
|
||||
return hasGpsOrRtc || setFromPhoneOrNtpToday;
|
||||
}
|
||||
|
||||
bool PositionModule::hasGPS()
|
||||
{
|
||||
#if MESHTASTIC_EXCLUDE_GPS
|
||||
return false;
|
||||
#else
|
||||
return gps && gps->isConnected();
|
||||
#endif
|
||||
}
|
||||
|
||||
// Allocate a packet with our position data if we have one
|
||||
meshtastic_MeshPacket *PositionModule::allocPositionPacket()
|
||||
{
|
||||
if (precision == 0) {
|
||||
LOG_DEBUG("Skip location send because precision is set to 0!");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
meshtastic_NodeInfoLite *node = service->refreshLocalMeshNode(); // should guarantee there is now a position
|
||||
assert(node->has_position);
|
||||
|
||||
// configuration of POSITION packet
|
||||
// consider making this a function argument?
|
||||
uint32_t pos_flags = config.position.position_flags;
|
||||
|
||||
// Populate a Position struct with ONLY the requested fields
|
||||
meshtastic_Position p = meshtastic_Position_init_default; // Start with an empty structure
|
||||
// if localPosition is totally empty, put our last saved position (lite) in there
|
||||
if (localPosition.latitude_i == 0 && localPosition.longitude_i == 0) {
|
||||
nodeDB->setLocalPosition(TypeConversions::ConvertToPosition(node->position));
|
||||
}
|
||||
localPosition.seq_number++;
|
||||
|
||||
if (localPosition.latitude_i == 0 && localPosition.longitude_i == 0) {
|
||||
LOG_WARN("Skip position send because lat/lon are zero!");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// lat/lon are unconditionally included - IF AVAILABLE!
|
||||
LOG_DEBUG("Send location with precision %i", precision);
|
||||
if (precision < 32 && precision > 0) {
|
||||
p.latitude_i = localPosition.latitude_i & (UINT32_MAX << (32 - precision));
|
||||
p.longitude_i = localPosition.longitude_i & (UINT32_MAX << (32 - precision));
|
||||
|
||||
// We want the imprecise position to be the middle of the possible location, not
|
||||
p.latitude_i += (1 << (31 - precision));
|
||||
p.longitude_i += (1 << (31 - precision));
|
||||
} else {
|
||||
p.latitude_i = localPosition.latitude_i;
|
||||
p.longitude_i = localPosition.longitude_i;
|
||||
}
|
||||
p.precision_bits = precision;
|
||||
p.has_latitude_i = true;
|
||||
p.has_longitude_i = true;
|
||||
// Always use NTP / GPS time if available
|
||||
if (getValidTime(RTCQualityNTP) > 0) {
|
||||
p.time = getValidTime(RTCQualityNTP);
|
||||
} else if (rtc_found.address != ScanI2C::ADDRESS_NONE.address) {
|
||||
LOG_INFO("Use RTC time for position");
|
||||
p.time = getValidTime(RTCQualityDevice);
|
||||
} else if (getRTCQuality() < RTCQualityNTP) {
|
||||
LOG_INFO("Strip low RTCQuality (%d) time from position", getRTCQuality());
|
||||
p.time = 0;
|
||||
}
|
||||
|
||||
if (config.position.fixed_position) {
|
||||
p.location_source = meshtastic_Position_LocSource_LOC_MANUAL;
|
||||
} else {
|
||||
p.location_source = localPosition.location_source;
|
||||
}
|
||||
|
||||
if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_ALTITUDE) {
|
||||
if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_ALTITUDE_MSL) {
|
||||
p.altitude = localPosition.altitude;
|
||||
p.has_altitude = true;
|
||||
} else {
|
||||
p.altitude_hae = localPosition.altitude_hae;
|
||||
p.has_altitude_hae = true;
|
||||
}
|
||||
|
||||
if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_GEOIDAL_SEPARATION) {
|
||||
p.altitude_geoidal_separation = localPosition.altitude_geoidal_separation;
|
||||
p.has_altitude_geoidal_separation = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_DOP) {
|
||||
if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_HVDOP) {
|
||||
p.HDOP = localPosition.HDOP;
|
||||
p.VDOP = localPosition.VDOP;
|
||||
} else
|
||||
p.PDOP = localPosition.PDOP;
|
||||
}
|
||||
|
||||
if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_SATINVIEW)
|
||||
p.sats_in_view = localPosition.sats_in_view;
|
||||
|
||||
if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_TIMESTAMP)
|
||||
p.timestamp = localPosition.timestamp;
|
||||
|
||||
if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_SEQ_NO)
|
||||
p.seq_number = localPosition.seq_number;
|
||||
|
||||
if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_HEADING) {
|
||||
p.ground_track = localPosition.ground_track;
|
||||
p.has_ground_track = true;
|
||||
}
|
||||
|
||||
if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_SPEED) {
|
||||
p.ground_speed = localPosition.ground_speed;
|
||||
p.has_ground_speed = true;
|
||||
}
|
||||
|
||||
LOG_INFO("Position packet: time=%i lat=%i lon=%i", p.time, p.latitude_i, p.longitude_i);
|
||||
|
||||
#ifndef MESHTASTIC_EXCLUDE_ATAK
|
||||
// TAK Tracker devices should send their position in a TAK packet over the ATAK port
|
||||
if (config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER)
|
||||
return allocAtakPli();
|
||||
#endif
|
||||
|
||||
return allocDataProtobuf(p);
|
||||
}
|
||||
|
||||
meshtastic_MeshPacket *PositionModule::allocReply()
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
meshtastic_MeshPacket *reply = allocPositionPacket();
|
||||
if (reply) {
|
||||
lastSentReply = millis(); // Track when we sent this reply
|
||||
}
|
||||
return reply;
|
||||
}
|
||||
|
||||
meshtastic_MeshPacket *PositionModule::allocAtakPli()
|
||||
{
|
||||
LOG_INFO("Send TAK PLI packet");
|
||||
meshtastic_MeshPacket *mp = allocDataPacket();
|
||||
mp->decoded.portnum = meshtastic_PortNum_ATAK_PLUGIN;
|
||||
|
||||
meshtastic_TAKPacket takPacket = {.is_compressed = true,
|
||||
.has_contact = true,
|
||||
.contact = meshtastic_Contact_init_default,
|
||||
.has_group = true,
|
||||
.group = {meshtastic_MemberRole_TeamMember, meshtastic_Team_Cyan},
|
||||
.has_status = true,
|
||||
.status =
|
||||
{
|
||||
.battery = powerStatus->getBatteryChargePercent(),
|
||||
},
|
||||
.which_payload_variant = meshtastic_TAKPacket_pli_tag,
|
||||
.payload_variant = {.pli = {
|
||||
.latitude_i = localPosition.latitude_i,
|
||||
.longitude_i = localPosition.longitude_i,
|
||||
.altitude = localPosition.altitude_hae,
|
||||
.speed = localPosition.ground_speed,
|
||||
.course = static_cast<uint16_t>(localPosition.ground_track),
|
||||
}}};
|
||||
|
||||
auto length = unishox2_compress_lines(owner.long_name, strlen(owner.long_name), takPacket.contact.device_callsign,
|
||||
sizeof(takPacket.contact.device_callsign) - 1, USX_PSET_DFLT, NULL);
|
||||
LOG_DEBUG("Uncompressed device_callsign '%s' - %d bytes", owner.long_name, strlen(owner.long_name));
|
||||
LOG_DEBUG("Compressed device_callsign '%s' - %d bytes", takPacket.contact.device_callsign, length);
|
||||
length = unishox2_compress_lines(owner.long_name, strlen(owner.long_name), takPacket.contact.callsign,
|
||||
sizeof(takPacket.contact.callsign) - 1, USX_PSET_DFLT, NULL);
|
||||
mp->decoded.payload.size =
|
||||
pb_encode_to_bytes(mp->decoded.payload.bytes, sizeof(mp->decoded.payload.bytes), &meshtastic_TAKPacket_msg, &takPacket);
|
||||
return mp;
|
||||
}
|
||||
|
||||
void PositionModule::sendOurPosition()
|
||||
{
|
||||
bool requestReplies = currentGeneration != radioGeneration;
|
||||
currentGeneration = radioGeneration;
|
||||
|
||||
// If we changed channels, ask everyone else for their latest info
|
||||
LOG_INFO("Send pos@%x:6 to mesh (wantReplies=%d)", localPosition.timestamp, requestReplies);
|
||||
for (uint8_t channelNum = 0; channelNum < 8; channelNum++) {
|
||||
if (channels.getByIndex(channelNum).settings.has_module_settings &&
|
||||
channels.getByIndex(channelNum).settings.module_settings.position_precision != 0) {
|
||||
sendOurPosition(NODENUM_BROADCAST, requestReplies, channelNum);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PositionModule::sendOurPosition(NodeNum dest, bool wantReplies, uint8_t channel)
|
||||
{
|
||||
// cancel any not yet sent (now stale) position packets
|
||||
if (prevPacketId) // if we wrap around to zero, we'll simply fail to cancel in that rare case (no big deal)
|
||||
service->cancelSending(prevPacketId);
|
||||
|
||||
// Set's the class precision value for this particular packet
|
||||
if (channels.getByIndex(channel).settings.has_module_settings) {
|
||||
precision = channels.getByIndex(channel).settings.module_settings.position_precision;
|
||||
}
|
||||
|
||||
meshtastic_MeshPacket *p = allocPositionPacket();
|
||||
if (p == nullptr) {
|
||||
LOG_DEBUG("allocPositionPacket returned a nullptr");
|
||||
return;
|
||||
}
|
||||
|
||||
p->to = dest;
|
||||
p->decoded.want_response = config.device.role == meshtastic_Config_DeviceConfig_Role_TRACKER ? false : wantReplies;
|
||||
if (config.device.role == meshtastic_Config_DeviceConfig_Role_TRACKER ||
|
||||
config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER)
|
||||
p->priority = meshtastic_MeshPacket_Priority_RELIABLE;
|
||||
else
|
||||
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||
prevPacketId = p->id;
|
||||
|
||||
if (channel > 0)
|
||||
p->channel = channel;
|
||||
|
||||
service->sendToMesh(p, RX_SRC_LOCAL, true);
|
||||
|
||||
if (IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_TRACKER,
|
||||
meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) &&
|
||||
config.power.is_power_saving) {
|
||||
meshtastic_ClientNotification *notification = clientNotificationPool.allocZeroed();
|
||||
notification->level = meshtastic_LogRecord_Level_INFO;
|
||||
notification->time = getValidTime(RTCQualityFromNet);
|
||||
sprintf(notification->message, "Sending position and sleeping for %us interval in a moment",
|
||||
Default::getConfiguredOrDefaultMs(config.position.position_broadcast_secs, default_broadcast_interval_secs) /
|
||||
1000U);
|
||||
service->sendClientNotification(notification);
|
||||
sleepOnNextExecution = true;
|
||||
LOG_DEBUG("Start next execution in 5s, then sleep");
|
||||
setIntervalFromNow(FIVE_SECONDS_MS);
|
||||
}
|
||||
}
|
||||
|
||||
#define RUNONCE_INTERVAL 5000;
|
||||
|
||||
int32_t PositionModule::runOnce()
|
||||
{
|
||||
if (sleepOnNextExecution == true) {
|
||||
sleepOnNextExecution = false;
|
||||
uint32_t nightyNightMs = Default::getConfiguredOrDefaultMs(config.position.position_broadcast_secs);
|
||||
LOG_DEBUG("Sleep for %ims, then awaking to send position again", nightyNightMs);
|
||||
doDeepSleep(nightyNightMs, false, false);
|
||||
}
|
||||
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
if (node == nullptr)
|
||||
return RUNONCE_INTERVAL;
|
||||
|
||||
// We limit our GPS broadcasts to a max rate
|
||||
uint32_t now = millis();
|
||||
uint32_t intervalMs = Default::getConfiguredOrDefaultMsScaled(config.position.position_broadcast_secs,
|
||||
default_broadcast_interval_secs, numOnlineNodes);
|
||||
uint32_t msSinceLastSend = now - lastGpsSend;
|
||||
// Only send packets if the channel util. is less than 25% utilized or we're a tracker with less than 40% utilized.
|
||||
if (!airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_TRACKER &&
|
||||
config.device.role != meshtastic_Config_DeviceConfig_Role_TAK_TRACKER)) {
|
||||
return RUNONCE_INTERVAL;
|
||||
}
|
||||
|
||||
if (lastGpsSend == 0 || msSinceLastSend >= intervalMs) {
|
||||
if (nodeDB->hasValidPosition(node)) {
|
||||
lastGpsSend = now;
|
||||
|
||||
lastGpsLatitude = node->position.latitude_i;
|
||||
lastGpsLongitude = node->position.longitude_i;
|
||||
|
||||
sendOurPosition();
|
||||
if (config.device.role == meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND) {
|
||||
sendLostAndFoundText();
|
||||
}
|
||||
}
|
||||
} else if (config.position.position_broadcast_smart_enabled) {
|
||||
const meshtastic_NodeInfoLite *node2 = service->refreshLocalMeshNode(); // should guarantee there is now a position
|
||||
|
||||
if (nodeDB->hasValidPosition(node2)) {
|
||||
// The minimum time (in seconds) that would pass before we are able to send a new position packet.
|
||||
|
||||
auto smartPosition = getDistanceTraveledSinceLastSend(node->position);
|
||||
msSinceLastSend = now - lastGpsSend;
|
||||
|
||||
if (smartPosition.hasTraveledOverThreshold &&
|
||||
Throttle::execute(
|
||||
&lastGpsSend, minimumTimeThreshold, []() { positionModule->sendOurPosition(); },
|
||||
[]() { LOG_DEBUG("Skip send smart broadcast due to time throttling"); })) {
|
||||
|
||||
LOG_DEBUG("Sent smart pos@%x:6 to mesh (distanceTraveled=%fm, minDistanceThreshold=%im, timeElapsed=%ims, "
|
||||
"minTimeInterval=%ims)",
|
||||
localPosition.timestamp, smartPosition.distanceTraveled, smartPosition.distanceThreshold,
|
||||
msSinceLastSend, minimumTimeThreshold);
|
||||
|
||||
// Set the current coords as our last ones, after we've compared distance with current and decided to send
|
||||
lastGpsLatitude = node->position.latitude_i;
|
||||
lastGpsLongitude = node->position.longitude_i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return RUNONCE_INTERVAL; // to save power only wake for our callback occasionally
|
||||
}
|
||||
|
||||
void PositionModule::sendLostAndFoundText()
|
||||
{
|
||||
meshtastic_MeshPacket *p = allocDataPacket();
|
||||
p->to = NODENUM_BROADCAST;
|
||||
char *message = new char[60];
|
||||
sprintf(message, "🚨I'm lost! Lat / Lon: %f, %f\a", (lastGpsLatitude * 1e-7), (lastGpsLongitude * 1e-7));
|
||||
p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP;
|
||||
p->want_ack = false;
|
||||
p->decoded.payload.size = strlen(message);
|
||||
memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size);
|
||||
|
||||
service->sendToMesh(p, RX_SRC_LOCAL, true);
|
||||
delete[] message;
|
||||
}
|
||||
|
||||
struct SmartPosition PositionModule::getDistanceTraveledSinceLastSend(meshtastic_PositionLite currentPosition)
|
||||
{
|
||||
// The minimum distance to travel before we are able to send a new position packet.
|
||||
const uint32_t distanceTravelThreshold =
|
||||
Default::getConfiguredOrDefault(config.position.broadcast_smart_minimum_distance, 100);
|
||||
|
||||
// Determine the distance in meters between two points on the globe
|
||||
float distanceTraveledSinceLastSend = GeoCoord::latLongToMeter(
|
||||
lastGpsLatitude * 1e-7, lastGpsLongitude * 1e-7, currentPosition.latitude_i * 1e-7, currentPosition.longitude_i * 1e-7);
|
||||
|
||||
return SmartPosition{.distanceTraveled = abs(distanceTraveledSinceLastSend),
|
||||
.distanceThreshold = distanceTravelThreshold,
|
||||
.hasTraveledOverThreshold = abs(distanceTraveledSinceLastSend) >= distanceTravelThreshold};
|
||||
}
|
||||
|
||||
void PositionModule::handleNewPosition()
|
||||
{
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
const meshtastic_NodeInfoLite *node2 = service->refreshLocalMeshNode(); // should guarantee there is now a position
|
||||
// We limit our GPS broadcasts to a max rate
|
||||
if (nodeDB->hasValidPosition(node2)) {
|
||||
auto smartPosition = getDistanceTraveledSinceLastSend(node->position);
|
||||
uint32_t msSinceLastSend = millis() - lastGpsSend;
|
||||
if (smartPosition.hasTraveledOverThreshold &&
|
||||
Throttle::execute(
|
||||
&lastGpsSend, minimumTimeThreshold, []() { positionModule->sendOurPosition(); },
|
||||
[]() { LOG_DEBUG("Skip send smart broadcast due to time throttling"); })) {
|
||||
LOG_DEBUG("Sent smart pos@%x:6 to mesh (distanceTraveled=%fm, minDistanceThreshold=%im, timeElapsed=%ims, "
|
||||
"minTimeInterval=%ims)",
|
||||
localPosition.timestamp, smartPosition.distanceTraveled, smartPosition.distanceThreshold, msSinceLastSend,
|
||||
minimumTimeThreshold);
|
||||
|
||||
// Set the current coords as our last ones, after we've compared distance with current and decided to send
|
||||
lastGpsLatitude = node->position.latitude_i;
|
||||
lastGpsLongitude = node->position.longitude_i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,85 @@
|
||||
#pragma once
|
||||
#include "Default.h"
|
||||
#include "ProtobufModule.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
|
||||
/**
|
||||
* Position module for sending/receiving positions into the mesh
|
||||
*/
|
||||
class PositionModule : public ProtobufModule<meshtastic_Position>, private concurrency::OSThread
|
||||
{
|
||||
CallbackObserver<PositionModule, const meshtastic::Status *> nodeStatusObserver =
|
||||
CallbackObserver<PositionModule, const meshtastic::Status *>(this, &PositionModule::handleStatusUpdate);
|
||||
|
||||
/// The id of the last packet we sent, to allow us to cancel it if we make something fresher
|
||||
PacketId prevPacketId = 0;
|
||||
|
||||
/// We limit our GPS broadcasts to a max rate
|
||||
uint32_t lastGpsSend = 0;
|
||||
|
||||
// Store the latest good lat / long
|
||||
int32_t lastGpsLatitude = 0;
|
||||
int32_t lastGpsLongitude = 0;
|
||||
|
||||
/// We force a rebroadcast if the radio settings change
|
||||
uint32_t currentGeneration = 0;
|
||||
|
||||
public:
|
||||
/** Constructor
|
||||
* name is for debugging output
|
||||
*/
|
||||
PositionModule();
|
||||
|
||||
/**
|
||||
* Send our position into the mesh
|
||||
*/
|
||||
void sendOurPosition(NodeNum dest, bool wantReplies = false, uint8_t channel = 0);
|
||||
void sendOurPosition();
|
||||
|
||||
void handleNewPosition();
|
||||
|
||||
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_Position *p) override;
|
||||
|
||||
virtual void alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_Position *p) override;
|
||||
|
||||
/** Messages can be received that have the want_response bit set. If set, this callback will be invoked
|
||||
* so that subclasses can (optionally) send a response back to the original sender. */
|
||||
virtual meshtastic_MeshPacket *allocReply() override;
|
||||
|
||||
/** Does our periodic broadcast */
|
||||
virtual int32_t runOnce() override;
|
||||
|
||||
private:
|
||||
meshtastic_MeshPacket *allocPositionPacket();
|
||||
struct SmartPosition getDistanceTraveledSinceLastSend(meshtastic_PositionLite currentPosition);
|
||||
meshtastic_MeshPacket *allocAtakPli();
|
||||
void trySetRtc(meshtastic_Position p, bool isLocal, bool forceUpdate = false);
|
||||
uint32_t precision;
|
||||
void sendLostAndFoundText();
|
||||
bool hasQualityTimesource();
|
||||
bool hasGPS();
|
||||
uint32_t lastSentReply = 0; // Last time we sent a position reply (used for reply throttling only)
|
||||
|
||||
#if USERPREFS_EVENT_MODE
|
||||
// In event mode we want to prevent excessive position broadcasts
|
||||
// we set the minimum interval to 5m
|
||||
const uint32_t minimumTimeThreshold =
|
||||
max(300000, Default::getConfiguredOrDefaultMs(config.position.broadcast_smart_minimum_interval_secs, 30));
|
||||
#else
|
||||
const uint32_t minimumTimeThreshold =
|
||||
Default::getConfiguredOrDefaultMs(config.position.broadcast_smart_minimum_interval_secs, 30);
|
||||
#endif
|
||||
};
|
||||
|
||||
struct SmartPosition {
|
||||
float distanceTraveled;
|
||||
uint32_t distanceThreshold;
|
||||
bool hasTraveledOverThreshold;
|
||||
};
|
||||
|
||||
extern PositionModule *positionModule;
|
||||
@@ -0,0 +1,134 @@
|
||||
#include "PowerStressModule.h"
|
||||
#include "Led.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PowerMon.h"
|
||||
#include "RTC.h"
|
||||
#include "Router.h"
|
||||
#include "configuration.h"
|
||||
#include "main.h"
|
||||
#include "sleep.h"
|
||||
#include "target_specific.h"
|
||||
#include <Throttle.h>
|
||||
|
||||
extern void printInfo();
|
||||
|
||||
PowerStressModule::PowerStressModule()
|
||||
: ProtobufModule("powerstress", meshtastic_PortNum_POWERSTRESS_APP, &meshtastic_PowerStressMessage_msg),
|
||||
concurrency::OSThread("PowerStress")
|
||||
{
|
||||
}
|
||||
|
||||
bool PowerStressModule::handleReceivedProtobuf(const meshtastic_MeshPacket &req, meshtastic_PowerStressMessage *pptr)
|
||||
{
|
||||
// We only respond to messages if powermon debugging is already on
|
||||
if (config.power.powermon_enables) {
|
||||
auto p = *pptr;
|
||||
LOG_INFO("Received PowerStress cmd=%d", p.cmd);
|
||||
|
||||
// Some commands we can handle immediately, anything else gets deferred to be handled by our thread
|
||||
switch (p.cmd) {
|
||||
case meshtastic_PowerStressMessage_Opcode_UNSET:
|
||||
LOG_ERROR("PowerStress operation unset");
|
||||
break;
|
||||
|
||||
case meshtastic_PowerStressMessage_Opcode_PRINT_INFO:
|
||||
printInfo();
|
||||
|
||||
// Now that we know we are actually doing power stress testing, go ahead and turn on all enables (so the log is fully
|
||||
// detailed)
|
||||
powerMon->force_enabled = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
if (currentMessage.cmd != meshtastic_PowerStressMessage_Opcode_UNSET)
|
||||
LOG_ERROR("PowerStress operation %d already in progress! Can't start new command", currentMessage.cmd);
|
||||
else
|
||||
currentMessage = p; // copy for use by thread (the message provided to us will be getting freed)
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int32_t PowerStressModule::runOnce()
|
||||
{
|
||||
if (!config.power.powermon_enables) {
|
||||
// Powermon not enabled - stop using CPU/stop this thread
|
||||
return disable();
|
||||
}
|
||||
|
||||
int32_t sleep_msec = 10; // when not active check for new messages every 10ms
|
||||
|
||||
auto &p = currentMessage;
|
||||
|
||||
if (isRunningCommand) {
|
||||
// Done with the previous command - our sleep must have finished
|
||||
p.cmd = meshtastic_PowerStressMessage_Opcode_UNSET;
|
||||
p.num_seconds = 0;
|
||||
isRunningCommand = false;
|
||||
LOG_INFO("S:PS:%u", p.cmd);
|
||||
} else {
|
||||
if (p.cmd != meshtastic_PowerStressMessage_Opcode_UNSET) {
|
||||
sleep_msec = (int32_t)(p.num_seconds * 1000);
|
||||
isRunningCommand = !!sleep_msec; // if the command wants us to sleep, make sure to mark that we have something running
|
||||
LOG_INFO(
|
||||
"S:PS:%u",
|
||||
p.cmd); // Emit a structured log saying we are starting a powerstress state (to make it easier to parse later)
|
||||
|
||||
switch (p.cmd) {
|
||||
case meshtastic_PowerStressMessage_Opcode_LED_ON:
|
||||
ledForceOn.set(true);
|
||||
break;
|
||||
case meshtastic_PowerStressMessage_Opcode_LED_OFF:
|
||||
ledForceOn.set(false);
|
||||
break;
|
||||
case meshtastic_PowerStressMessage_Opcode_GPS_ON:
|
||||
// FIXME - implement
|
||||
break;
|
||||
case meshtastic_PowerStressMessage_Opcode_GPS_OFF:
|
||||
// FIXME - implement
|
||||
break;
|
||||
case meshtastic_PowerStressMessage_Opcode_LORA_OFF:
|
||||
// FIXME - implement
|
||||
break;
|
||||
case meshtastic_PowerStressMessage_Opcode_LORA_RX:
|
||||
// FIXME - implement
|
||||
break;
|
||||
case meshtastic_PowerStressMessage_Opcode_LORA_TX:
|
||||
// FIXME - implement
|
||||
break;
|
||||
case meshtastic_PowerStressMessage_Opcode_SCREEN_OFF:
|
||||
// FIXME - implement
|
||||
break;
|
||||
case meshtastic_PowerStressMessage_Opcode_SCREEN_ON:
|
||||
// FIXME - implement
|
||||
break;
|
||||
case meshtastic_PowerStressMessage_Opcode_BT_OFF:
|
||||
setBluetoothEnable(false);
|
||||
break;
|
||||
case meshtastic_PowerStressMessage_Opcode_BT_ON:
|
||||
setBluetoothEnable(true);
|
||||
break;
|
||||
case meshtastic_PowerStressMessage_Opcode_CPU_DEEPSLEEP:
|
||||
doDeepSleep(sleep_msec, true, true);
|
||||
break;
|
||||
case meshtastic_PowerStressMessage_Opcode_CPU_FULLON: {
|
||||
uint32_t start_msec = millis();
|
||||
while (Throttle::isWithinTimespanMs(start_msec, sleep_msec))
|
||||
; // Don't let CPU idle at all
|
||||
sleep_msec = 0; // we already slept
|
||||
break;
|
||||
}
|
||||
case meshtastic_PowerStressMessage_Opcode_CPU_IDLE:
|
||||
// FIXME - implement
|
||||
break;
|
||||
default:
|
||||
LOG_ERROR("PowerStress operation %d not yet implemented!", p.cmd);
|
||||
sleep_msec = 0; // Don't do whatever sleep was requested...
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return sleep_msec;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
#include "ProtobufModule.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
#include "mesh/generated/meshtastic/powermon.pb.h"
|
||||
|
||||
/**
|
||||
* A module that provides easy low-level remote access to device hardware.
|
||||
*/
|
||||
class PowerStressModule : public ProtobufModule<meshtastic_PowerStressMessage>, private concurrency::OSThread
|
||||
{
|
||||
meshtastic_PowerStressMessage currentMessage = meshtastic_PowerStressMessage_init_default;
|
||||
bool isRunningCommand = false;
|
||||
|
||||
public:
|
||||
/** Constructor
|
||||
* name is for debugging output
|
||||
*/
|
||||
PowerStressModule();
|
||||
|
||||
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_PowerStressMessage *p) override;
|
||||
|
||||
/**
|
||||
* Periodically read the gpios we have been asked to WATCH, if they have changed,
|
||||
* broadcast a message with the change information.
|
||||
*
|
||||
* The method that will be called each time our thread gets a chance to run
|
||||
*
|
||||
* Returns desired period for next invocation (or RUN_SAME for no change)
|
||||
*/
|
||||
virtual int32_t runOnce() override;
|
||||
};
|
||||
|
||||
extern PowerStressModule powerStressModule;
|
||||
@@ -0,0 +1,343 @@
|
||||
/**
|
||||
* @file RangeTestModule.cpp
|
||||
* @brief Implementation of the RangeTestModule class and RangeTestModuleRadio class.
|
||||
*
|
||||
* As a sender, this module sends packets every n seconds with an incremented PacketID.
|
||||
* As a receiver, this module receives packets from multiple senders and saves them to the Filesystem.
|
||||
*
|
||||
* The RangeTestModule class is an OSThread that runs the module.
|
||||
* The RangeTestModuleRadio class handles sending and receiving packets.
|
||||
*/
|
||||
#include "RangeTestModule.h"
|
||||
#include "FSCommon.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "RTC.h"
|
||||
#include "Router.h"
|
||||
#include "SPILock.h"
|
||||
#include "airtime.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/GeoCoord.h"
|
||||
#include <Arduino.h>
|
||||
#include <Throttle.h>
|
||||
|
||||
RangeTestModule *rangeTestModule;
|
||||
RangeTestModuleRadio *rangeTestModuleRadio;
|
||||
|
||||
RangeTestModule::RangeTestModule() : concurrency::OSThread("RangeTest") {}
|
||||
|
||||
uint32_t packetSequence = 0;
|
||||
|
||||
int32_t RangeTestModule::runOnce()
|
||||
{
|
||||
#if defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_STM32WL) || defined(ARCH_PORTDUINO)
|
||||
|
||||
/*
|
||||
Uncomment the preferences below if you want to use the module
|
||||
without having to configure it from the PythonAPI or WebUI.
|
||||
*/
|
||||
|
||||
// moduleConfig.range_test.enabled = 1;
|
||||
// moduleConfig.range_test.sender = 30;
|
||||
// moduleConfig.range_test.save = 1;
|
||||
// moduleConfig.range_test.clear_on_reboot = 1;
|
||||
|
||||
// Fixed position is useful when testing indoors.
|
||||
// config.position.fixed_position = 1;
|
||||
|
||||
uint32_t senderHeartbeat = moduleConfig.range_test.sender * 1000;
|
||||
if (moduleConfig.range_test.enabled) {
|
||||
|
||||
if (firstTime) {
|
||||
rangeTestModuleRadio = new RangeTestModuleRadio();
|
||||
|
||||
firstTime = 0;
|
||||
|
||||
if (moduleConfig.range_test.clear_on_reboot) {
|
||||
// User wants to delete previous range test(s)
|
||||
LOG_INFO("Range Test Module - Clearing out previous test file");
|
||||
rangeTestModuleRadio->removeFile();
|
||||
}
|
||||
if (moduleConfig.range_test.sender) {
|
||||
LOG_INFO("Init Range Test Module -- Sender");
|
||||
started = millis(); // make a note of when we started
|
||||
return (5000); // Sending first message 5 seconds after initialization.
|
||||
} else {
|
||||
LOG_INFO("Init Range Test Module -- Receiver");
|
||||
return disable();
|
||||
// This thread does not need to run as a receiver
|
||||
}
|
||||
} else {
|
||||
|
||||
if (moduleConfig.range_test.sender) {
|
||||
// If sender
|
||||
LOG_INFO("Range Test Module - Sending heartbeat every %d ms", (senderHeartbeat));
|
||||
|
||||
LOG_INFO("gpsStatus->getLatitude() %d", gpsStatus->getLatitude());
|
||||
LOG_INFO("gpsStatus->getLongitude() %d", gpsStatus->getLongitude());
|
||||
LOG_INFO("gpsStatus->getHasLock() %d", gpsStatus->getHasLock());
|
||||
LOG_INFO("gpsStatus->getDOP() %d", gpsStatus->getDOP());
|
||||
LOG_INFO("fixed_position() %d", config.position.fixed_position);
|
||||
|
||||
// Only send packets if the channel is less than 25% utilized.
|
||||
if (airTime->isTxAllowedChannelUtil(true)) {
|
||||
rangeTestModuleRadio->sendPayload();
|
||||
}
|
||||
|
||||
// If we have been running for more than 8 hours, turn module back off
|
||||
if (!Throttle::isWithinTimespanMs(started, 28800000)) {
|
||||
LOG_INFO("Range Test Module - Disable after 8 hours");
|
||||
return disable();
|
||||
} else {
|
||||
return (senderHeartbeat);
|
||||
}
|
||||
} else {
|
||||
return disable();
|
||||
// This thread does not need to run as a receiver
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LOG_INFO("Range Test Module - Disabled");
|
||||
}
|
||||
|
||||
#endif
|
||||
return disable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a payload to a specified destination node.
|
||||
*
|
||||
* @param dest The destination node number.
|
||||
* @param wantReplies Whether or not to request replies from the destination node.
|
||||
*/
|
||||
void RangeTestModuleRadio::sendPayload(NodeNum dest, bool wantReplies)
|
||||
{
|
||||
meshtastic_MeshPacket *p = allocDataPacket();
|
||||
p->to = dest;
|
||||
p->decoded.want_response = wantReplies;
|
||||
p->hop_limit = 0;
|
||||
p->want_ack = false;
|
||||
|
||||
packetSequence++;
|
||||
|
||||
static char heartbeatString[MAX_LORA_PAYLOAD_LEN + 1];
|
||||
snprintf(heartbeatString, sizeof(heartbeatString), "seq %u", packetSequence);
|
||||
|
||||
p->decoded.payload.size = strlen(heartbeatString); // You must specify how many bytes are in the reply
|
||||
memcpy(p->decoded.payload.bytes, heartbeatString, p->decoded.payload.size);
|
||||
|
||||
service->sendToMesh(p);
|
||||
|
||||
// TODO: Handle this better. We want to keep the phone awake otherwise it stops sending.
|
||||
powerFSM.trigger(EVENT_CONTACT_FROM_PHONE);
|
||||
}
|
||||
|
||||
ProcessMessage RangeTestModuleRadio::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
#if defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_STM32WL) || defined(ARCH_PORTDUINO)
|
||||
|
||||
if (moduleConfig.range_test.enabled) {
|
||||
|
||||
/*
|
||||
auto &p = mp.decoded;
|
||||
LOG_DEBUG("Received text msg self=0x%0x, from=0x%0x, to=0x%0x, id=%d, msg=%.*s",
|
||||
LOG_INFO.getNodeNum(), mp.from, mp.to, mp.id, p.payload.size, p.payload.bytes);
|
||||
*/
|
||||
|
||||
if (!isFromUs(&mp)) {
|
||||
if (moduleConfig.range_test.save) {
|
||||
appendFile(mp);
|
||||
}
|
||||
|
||||
/*
|
||||
NodeInfoLite *n = nodeDB->getMeshNode(getFrom(&mp));
|
||||
|
||||
LOG_DEBUG("-----------------------------------------");
|
||||
LOG_DEBUG("p.payload.bytes \"%s\"", p.payload.bytes);
|
||||
LOG_DEBUG("p.payload.size %d", p.payload.size);
|
||||
LOG_DEBUG("---- Received Packet:");
|
||||
LOG_DEBUG("mp.from %d", mp.from);
|
||||
LOG_DEBUG("mp.rx_snr %f", mp.rx_snr);
|
||||
LOG_DEBUG("mp.rx_rssi %f", mp.rx_rssi);
|
||||
LOG_DEBUG("mp.hop_limit %d", mp.hop_limit);
|
||||
LOG_DEBUG("---- Node Information of Received Packet (mp.from):");
|
||||
LOG_DEBUG("n->user.long_name %s", n->user.long_name);
|
||||
LOG_DEBUG("n->user.short_name %s", n->user.short_name);
|
||||
LOG_DEBUG("n->has_position %d", n->has_position);
|
||||
LOG_DEBUG("n->position.latitude_i %d", n->position.latitude_i);
|
||||
LOG_DEBUG("n->position.longitude_i %d", n->position.longitude_i);
|
||||
LOG_DEBUG("---- Current device location information:");
|
||||
LOG_DEBUG("gpsStatus->getLatitude() %d", gpsStatus->getLatitude());
|
||||
LOG_DEBUG("gpsStatus->getLongitude() %d", gpsStatus->getLongitude());
|
||||
LOG_DEBUG("gpsStatus->getHasLock() %d", gpsStatus->getHasLock());
|
||||
LOG_DEBUG("gpsStatus->getDOP() %d", gpsStatus->getDOP());
|
||||
LOG_DEBUG("-----------------------------------------");
|
||||
*/
|
||||
}
|
||||
} else {
|
||||
LOG_INFO("Range Test Module Disabled");
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
return ProcessMessage::CONTINUE; // Let others look at this message also if they want
|
||||
}
|
||||
|
||||
bool RangeTestModuleRadio::appendFile(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
#ifdef ARCH_ESP32
|
||||
auto &p = mp.decoded;
|
||||
|
||||
meshtastic_NodeInfoLite *n = nodeDB->getMeshNode(getFrom(&mp));
|
||||
/*
|
||||
LOG_DEBUG("-----------------------------------------");
|
||||
LOG_DEBUG("p.payload.bytes \"%s\"", p.payload.bytes);
|
||||
LOG_DEBUG("p.payload.size %d", p.payload.size);
|
||||
LOG_DEBUG("---- Received Packet:");
|
||||
LOG_DEBUG("mp.from %d", mp.from);
|
||||
LOG_DEBUG("mp.rx_snr %f", mp.rx_snr);
|
||||
LOG_DEBUG("mp.hop_limit %d", mp.hop_limit);
|
||||
LOG_DEBUG("---- Node Information of Received Packet (mp.from):");
|
||||
LOG_DEBUG("n->user.long_name %s", n->user.long_name);
|
||||
LOG_DEBUG("n->user.short_name %s", n->user.short_name);
|
||||
LOG_DEBUG("n->has_position %d", n->has_position);
|
||||
LOG_DEBUG("n->position.latitude_i %d", n->position.latitude_i);
|
||||
LOG_DEBUG("n->position.longitude_i %d", n->position.longitude_i);
|
||||
LOG_DEBUG("---- Current device location information:");
|
||||
LOG_DEBUG("gpsStatus->getLatitude() %d", gpsStatus->getLatitude());
|
||||
LOG_DEBUG("gpsStatus->getLongitude() %d", gpsStatus->getLongitude());
|
||||
LOG_DEBUG("gpsStatus->getHasLock() %d", gpsStatus->getHasLock());
|
||||
LOG_DEBUG("gpsStatus->getDOP() %d", gpsStatus->getDOP());
|
||||
LOG_DEBUG("-----------------------------------------");
|
||||
*/
|
||||
concurrency::LockGuard g(spiLock);
|
||||
if (!FSBegin()) {
|
||||
LOG_DEBUG("An Error has occurred while mounting the filesystem");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (FSCom.totalBytes() - FSCom.usedBytes() < 51200) {
|
||||
LOG_DEBUG("Filesystem doesn't have enough free space. Aborting write");
|
||||
return 0;
|
||||
}
|
||||
|
||||
FSCom.mkdir("/static");
|
||||
|
||||
// If the file doesn't exist, write the header.
|
||||
if (!FSCom.exists("/static/rangetest.csv")) {
|
||||
//--------- Write to file
|
||||
File fileToWrite = FSCom.open("/static/rangetest.csv", FILE_WRITE);
|
||||
|
||||
if (!fileToWrite) {
|
||||
LOG_ERROR("There was an error opening the file for writing");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Print the CSV header
|
||||
if (fileToWrite.println("time,from,sender name,sender lat,sender long,rx lat,rx long,rx elevation,rx "
|
||||
"snr,distance,hop limit,payload,rx rssi")) {
|
||||
LOG_INFO("File was written");
|
||||
} else {
|
||||
LOG_ERROR("File write failed");
|
||||
}
|
||||
fileToWrite.flush();
|
||||
fileToWrite.close();
|
||||
}
|
||||
|
||||
//--------- Append content to file
|
||||
File fileToAppend = FSCom.open("/static/rangetest.csv", FILE_APPEND);
|
||||
|
||||
if (!fileToAppend) {
|
||||
LOG_ERROR("There was an error opening the file for appending");
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct timeval tv;
|
||||
if (!gettimeofday(&tv, NULL)) {
|
||||
long hms = tv.tv_sec % SEC_PER_DAY;
|
||||
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
|
||||
|
||||
// Tear apart hms into h:m:s
|
||||
int hour = hms / SEC_PER_HOUR;
|
||||
int min = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||||
int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN
|
||||
|
||||
fileToAppend.printf("%02d:%02d:%02d,", hour, min, sec); // Time
|
||||
} else {
|
||||
fileToAppend.printf("??:??:??,"); // Time
|
||||
}
|
||||
|
||||
fileToAppend.printf("%d,", getFrom(&mp)); // From
|
||||
fileToAppend.printf("%s,", n->user.long_name); // Long Name
|
||||
fileToAppend.printf("%f,", n->position.latitude_i * 1e-7); // Sender Lat
|
||||
fileToAppend.printf("%f,", n->position.longitude_i * 1e-7); // Sender Long
|
||||
if (gpsStatus->getIsConnected() || config.position.fixed_position) {
|
||||
fileToAppend.printf("%f,", gpsStatus->getLatitude() * 1e-7); // RX Lat
|
||||
fileToAppend.printf("%f,", gpsStatus->getLongitude() * 1e-7); // RX Long
|
||||
fileToAppend.printf("%d,", gpsStatus->getAltitude()); // RX Altitude
|
||||
} else {
|
||||
// When the phone API is in use, the node info will be updated with position
|
||||
meshtastic_NodeInfoLite *us = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
fileToAppend.printf("%f,", us->position.latitude_i * 1e-7); // RX Lat
|
||||
fileToAppend.printf("%f,", us->position.longitude_i * 1e-7); // RX Long
|
||||
fileToAppend.printf("%d,", us->position.altitude); // RX Altitude
|
||||
}
|
||||
|
||||
fileToAppend.printf("%f,", mp.rx_snr); // RX SNR
|
||||
|
||||
if (n->position.latitude_i && n->position.longitude_i && gpsStatus->getLatitude() && gpsStatus->getLongitude()) {
|
||||
float distance = GeoCoord::latLongToMeter(n->position.latitude_i * 1e-7, n->position.longitude_i * 1e-7,
|
||||
gpsStatus->getLatitude() * 1e-7, gpsStatus->getLongitude() * 1e-7);
|
||||
fileToAppend.printf("%f,", distance); // Distance in meters
|
||||
} else {
|
||||
fileToAppend.printf("0,");
|
||||
}
|
||||
|
||||
fileToAppend.printf("%d,", mp.hop_limit); // Packet Hop Limit
|
||||
|
||||
// TODO: If quotes are found in the payload, it has to be escaped.
|
||||
fileToAppend.printf("\"%s\"\n", p.payload.bytes);
|
||||
fileToAppend.printf("%i,", mp.rx_rssi); // RX RSSI
|
||||
|
||||
fileToAppend.flush();
|
||||
fileToAppend.close();
|
||||
|
||||
return 1;
|
||||
|
||||
#else
|
||||
LOG_ERROR("Failed to store range test results - feature only available for ESP32");
|
||||
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool RangeTestModuleRadio::removeFile()
|
||||
{
|
||||
#ifdef ARCH_ESP32
|
||||
if (!FSBegin()) {
|
||||
LOG_DEBUG("An Error has occurred while mounting the filesystem");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!FSCom.exists("/static/rangetest.csv")) {
|
||||
LOG_DEBUG("No range tests found.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
LOG_INFO("Deleting previous range test.");
|
||||
bool result = FSCom.remove("/static/rangetest.csv");
|
||||
|
||||
if (!result) {
|
||||
LOG_ERROR("Failed to delete range test.");
|
||||
return 0;
|
||||
}
|
||||
LOG_INFO("Range test removed.");
|
||||
|
||||
return 1;
|
||||
#else
|
||||
LOG_ERROR("Failed to remove range test results - feature only available for ESP32");
|
||||
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
|
||||
#include "SinglePortModule.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
#include "configuration.h"
|
||||
#include <Arduino.h>
|
||||
#include <functional>
|
||||
|
||||
class RangeTestModule : private concurrency::OSThread
|
||||
{
|
||||
bool firstTime = 1;
|
||||
unsigned long started = 0;
|
||||
|
||||
public:
|
||||
RangeTestModule();
|
||||
|
||||
protected:
|
||||
virtual int32_t runOnce() override;
|
||||
};
|
||||
|
||||
extern RangeTestModule *rangeTestModule;
|
||||
|
||||
/*
|
||||
* Radio interface for RangeTestModule
|
||||
*
|
||||
*/
|
||||
class RangeTestModuleRadio : public SinglePortModule
|
||||
{
|
||||
uint32_t lastRxID = 0;
|
||||
|
||||
public:
|
||||
RangeTestModuleRadio() : SinglePortModule("RangeTestModuleRadio", meshtastic_PortNum_RANGE_TEST_APP)
|
||||
{
|
||||
loopbackOk = true; // Allow locally generated messages to loop back to the client
|
||||
}
|
||||
|
||||
/**
|
||||
* Send our payload into the mesh
|
||||
*/
|
||||
void sendPayload(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false);
|
||||
|
||||
/**
|
||||
* Append range test data to the file on the Filesystem
|
||||
*/
|
||||
bool appendFile(const meshtastic_MeshPacket &mp);
|
||||
|
||||
/**
|
||||
* Cleanup range test data from filesystem
|
||||
*/
|
||||
bool removeFile();
|
||||
|
||||
protected:
|
||||
/** 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;
|
||||
};
|
||||
|
||||
extern RangeTestModuleRadio *rangeTestModuleRadio;
|
||||
@@ -0,0 +1,161 @@
|
||||
#include "RemoteHardwareModule.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "RTC.h"
|
||||
#include "Router.h"
|
||||
#include "configuration.h"
|
||||
#include "main.h"
|
||||
#include <Throttle.h>
|
||||
|
||||
#define NUM_GPIOS 64
|
||||
|
||||
// Because (FIXME) we currently don't tell API clients status on sent messages
|
||||
// we need to throttle our sending, so that if a gpio is bouncing up and down we
|
||||
// don't generate more messages than the net can send. So we limit watch messages to
|
||||
// a max of one change per 30 seconds
|
||||
#define WATCH_INTERVAL_MSEC (30 * 1000)
|
||||
|
||||
// Tests for access to read from or write to a specified GPIO pin
|
||||
static bool pinAccessAllowed(uint64_t mask, uint8_t pin)
|
||||
{
|
||||
// If undefined pin access is allowed, don't check the pin and just return true
|
||||
if (moduleConfig.remote_hardware.allow_undefined_pin_access) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Test to see if the pin is in the list of allowed pins and return true if found
|
||||
if (mask & (1ULL << pin)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Set pin modes for every set bit in a mask
|
||||
static void pinModes(uint64_t mask, uint8_t mode, uint64_t maskAvailable)
|
||||
{
|
||||
for (uint64_t i = 0; i < NUM_GPIOS; i++) {
|
||||
if (mask & (1ULL << i)) {
|
||||
if (pinAccessAllowed(maskAvailable, i)) {
|
||||
pinMode(i, mode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Read all the pins mentioned in a mask
|
||||
static uint64_t digitalReads(uint64_t mask, uint64_t maskAvailable)
|
||||
{
|
||||
uint64_t res = 0;
|
||||
|
||||
pinModes(mask, INPUT_PULLUP, maskAvailable);
|
||||
|
||||
for (uint64_t i = 0; i < NUM_GPIOS; i++) {
|
||||
uint64_t m = 1ULL << i;
|
||||
if (mask & m && pinAccessAllowed(maskAvailable, i)) {
|
||||
if (digitalRead(i)) {
|
||||
res |= m;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
RemoteHardwareModule::RemoteHardwareModule()
|
||||
: ProtobufModule("remotehardware", meshtastic_PortNum_REMOTE_HARDWARE_APP, &meshtastic_HardwareMessage_msg),
|
||||
concurrency::OSThread("RemoteHardware")
|
||||
{
|
||||
// restrict to the gpio channel for rx
|
||||
boundChannel = Channels::gpioChannel;
|
||||
|
||||
// Pull available pin allowlist from config and build a bitmask out of it for fast comparisons later
|
||||
for (uint8_t i = 0; i < 4; i++) {
|
||||
availablePins += 1ULL << moduleConfig.remote_hardware.available_pins[i].gpio_pin;
|
||||
}
|
||||
}
|
||||
|
||||
bool RemoteHardwareModule::handleReceivedProtobuf(const meshtastic_MeshPacket &req, meshtastic_HardwareMessage *pptr)
|
||||
{
|
||||
if (moduleConfig.remote_hardware.enabled) {
|
||||
auto p = *pptr;
|
||||
LOG_INFO("Received RemoteHardware type=%d", p.type);
|
||||
|
||||
switch (p.type) {
|
||||
case meshtastic_HardwareMessage_Type_WRITE_GPIOS: {
|
||||
pinModes(p.gpio_mask, OUTPUT, availablePins);
|
||||
for (uint8_t i = 0; i < NUM_GPIOS; i++) {
|
||||
uint64_t mask = 1ULL << i;
|
||||
if (p.gpio_mask & mask && pinAccessAllowed(availablePins, i)) {
|
||||
digitalWrite(i, (p.gpio_value & mask) ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case meshtastic_HardwareMessage_Type_READ_GPIOS: {
|
||||
uint64_t res = digitalReads(p.gpio_mask, availablePins);
|
||||
|
||||
// Send the reply
|
||||
meshtastic_HardwareMessage r = meshtastic_HardwareMessage_init_default;
|
||||
r.type = meshtastic_HardwareMessage_Type_READ_GPIOS_REPLY;
|
||||
r.gpio_value = res;
|
||||
r.gpio_mask = p.gpio_mask;
|
||||
meshtastic_MeshPacket *p2 = allocDataProtobuf(r);
|
||||
setReplyTo(p2, req);
|
||||
myReply = p2;
|
||||
break;
|
||||
}
|
||||
|
||||
case meshtastic_HardwareMessage_Type_WATCH_GPIOS: {
|
||||
watchGpios = p.gpio_mask;
|
||||
lastWatchMsec = 0; // Force a new publish soon
|
||||
previousWatch =
|
||||
~watchGpios; // generate a 'previous' value which is guaranteed to not match (to force an initial publish)
|
||||
enabled = true; // Let our thread run at least once
|
||||
setInterval(2000); // Set a new interval so we'll run soon
|
||||
LOG_INFO("Now watching GPIOs 0x%llx", watchGpios);
|
||||
break;
|
||||
}
|
||||
|
||||
case meshtastic_HardwareMessage_Type_READ_GPIOS_REPLY:
|
||||
case meshtastic_HardwareMessage_Type_GPIOS_CHANGED:
|
||||
break; // Ignore - we might see our own replies
|
||||
|
||||
default:
|
||||
LOG_ERROR("Hardware operation %d not yet implemented! FIXME", p.type);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
int32_t RemoteHardwareModule::runOnce()
|
||||
{
|
||||
if (moduleConfig.remote_hardware.enabled && watchGpios) {
|
||||
|
||||
if (!Throttle::isWithinTimespanMs(lastWatchMsec, WATCH_INTERVAL_MSEC)) {
|
||||
uint64_t curVal = digitalReads(watchGpios, availablePins);
|
||||
lastWatchMsec = millis();
|
||||
|
||||
if (curVal != previousWatch) {
|
||||
previousWatch = curVal;
|
||||
LOG_INFO("Broadcast GPIOS 0x%llx changed!", curVal);
|
||||
|
||||
// Something changed! Tell the world with a broadcast message
|
||||
meshtastic_HardwareMessage r = meshtastic_HardwareMessage_init_default;
|
||||
r.type = meshtastic_HardwareMessage_Type_GPIOS_CHANGED;
|
||||
r.gpio_value = curVal;
|
||||
meshtastic_MeshPacket *p = allocDataProtobuf(r);
|
||||
service->sendToMesh(p);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No longer watching anything - stop using CPU
|
||||
return disable();
|
||||
}
|
||||
|
||||
return 2000; // Poll our GPIOs every 2000ms
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
#include "ProtobufModule.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
#include "mesh/generated/meshtastic/remote_hardware.pb.h"
|
||||
|
||||
/**
|
||||
* A module that provides easy low-level remote access to device hardware.
|
||||
*/
|
||||
class RemoteHardwareModule : public ProtobufModule<meshtastic_HardwareMessage>, private concurrency::OSThread
|
||||
{
|
||||
/// The current set of GPIOs we've been asked to watch for changes
|
||||
uint64_t watchGpios = 0;
|
||||
|
||||
/// The previously read value of watched pins
|
||||
uint64_t previousWatch = 0;
|
||||
|
||||
/// The timestamp of our last watch event (we throttle watches to 1 change every 30 seconds)
|
||||
uint32_t lastWatchMsec = 0;
|
||||
|
||||
/// A bitmask of GPIOs that are exposed to the mesh if undefined access is not enabled
|
||||
uint64_t availablePins = 0;
|
||||
|
||||
public:
|
||||
/** Constructor
|
||||
* name is for debugging output
|
||||
*/
|
||||
RemoteHardwareModule();
|
||||
|
||||
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_HardwareMessage *p) override;
|
||||
|
||||
/**
|
||||
* Periodically read the gpios we have been asked to WATCH, if they have changed,
|
||||
* broadcast a message with the change information.
|
||||
*
|
||||
* The method that will be called each time our thread gets a chance to run
|
||||
*
|
||||
* Returns desired period for next invocation (or RUN_SAME for no change)
|
||||
*/
|
||||
virtual int32_t runOnce() override;
|
||||
};
|
||||
|
||||
extern RemoteHardwareModule remoteHardwareModule;
|
||||
@@ -0,0 +1,24 @@
|
||||
#include "ReplyModule.h"
|
||||
#include "MeshService.h"
|
||||
#include "configuration.h"
|
||||
#include "main.h"
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
meshtastic_MeshPacket *ReplyModule::allocReply()
|
||||
{
|
||||
assert(currentRequest); // should always be !NULL
|
||||
#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
|
||||
auto req = *currentRequest;
|
||||
auto &p = req.decoded;
|
||||
// The incoming message is in p.payload
|
||||
LOG_INFO("Received message from=0x%0x, id=%d, msg=%.*s", req.from, req.id, p.payload.size, p.payload.bytes);
|
||||
#endif
|
||||
|
||||
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
|
||||
memcpy(reply->decoded.payload.bytes, replyStr, reply->decoded.payload.size);
|
||||
|
||||
return reply;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
#include "SinglePortModule.h"
|
||||
|
||||
/**
|
||||
* A simple example module that just replies with "Message received" to any message it receives.
|
||||
*/
|
||||
class ReplyModule : public SinglePortModule
|
||||
{
|
||||
public:
|
||||
/** Constructor
|
||||
* name is for debugging output
|
||||
*/
|
||||
ReplyModule() : SinglePortModule("reply", meshtastic_PortNum_REPLY_APP) {}
|
||||
|
||||
protected:
|
||||
/** For reply module we do all of our processing in the (normally optional)
|
||||
* want_replies handling
|
||||
*/
|
||||
virtual meshtastic_MeshPacket *allocReply() override;
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
#include "RoutingModule.h"
|
||||
#include "Default.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "Router.h"
|
||||
#include "configuration.h"
|
||||
#include "main.h"
|
||||
|
||||
RoutingModule *routingModule;
|
||||
|
||||
bool RoutingModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Routing *r)
|
||||
{
|
||||
bool maybePKI = mp.which_payload_variant == meshtastic_MeshPacket_encrypted_tag && mp.channel == 0 && !isBroadcast(mp.to);
|
||||
// Beginning of logic whether to drop the packet based on Rebroadcast mode
|
||||
if (mp.which_payload_variant == meshtastic_MeshPacket_encrypted_tag &&
|
||||
(config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_LOCAL_ONLY ||
|
||||
config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_KNOWN_ONLY)) {
|
||||
if (!maybePKI)
|
||||
return false;
|
||||
if ((nodeDB->getMeshNode(mp.from) == NULL || !nodeDB->getMeshNode(mp.from)->has_user) &&
|
||||
(nodeDB->getMeshNode(mp.to) == NULL || !nodeDB->getMeshNode(mp.to)->has_user))
|
||||
return false;
|
||||
} else if (owner.is_licensed && nodeDB->getLicenseStatus(mp.from) == UserLicenseStatus::NotLicensed) {
|
||||
// Don't let licensed users to rebroadcast packets from unlicensed users
|
||||
// If we know they are in-fact unlicensed
|
||||
LOG_DEBUG("Packet from unlicensed user, ignoring packet");
|
||||
return false;
|
||||
}
|
||||
|
||||
printPacket("Routing sniffing", &mp);
|
||||
router->sniffReceived(&mp, r);
|
||||
|
||||
// FIXME - move this to a non promsicious PhoneAPI module?
|
||||
// Note: we are careful not to send back packets that started with the phone back to the phone
|
||||
if ((isBroadcast(mp.to) || isToUs(&mp)) && (mp.from != 0)) {
|
||||
printPacket("Delivering rx packet", &mp);
|
||||
service->handleFromRadio(&mp);
|
||||
}
|
||||
|
||||
return false; // Let others look at this message also if they want
|
||||
}
|
||||
|
||||
meshtastic_MeshPacket *RoutingModule::allocReply()
|
||||
{
|
||||
assert(currentRequest);
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void RoutingModule::sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit,
|
||||
bool ackWantsAck)
|
||||
{
|
||||
auto p = allocAckNak(err, to, idFrom, chIndex, hopLimit);
|
||||
|
||||
// Allow the caller to set want_ack on this ACK packet if it's important that the ACK be delivered reliably
|
||||
p->want_ack = ackWantsAck;
|
||||
|
||||
router->sendLocal(p); // we sometimes send directly to the local node
|
||||
}
|
||||
|
||||
uint8_t RoutingModule::getHopLimitForResponse(uint8_t hopStart, uint8_t hopLimit)
|
||||
{
|
||||
if (hopStart != 0) {
|
||||
// Hops used by the request. If somebody in between running modified firmware modified it, ignore it
|
||||
uint8_t hopsUsed = hopStart < hopLimit ? config.lora.hop_limit : hopStart - hopLimit;
|
||||
if (hopsUsed > config.lora.hop_limit) {
|
||||
// In event mode, we never want to send packets with more than our default 3 hops.
|
||||
#if !(EVENTMODE) // This falls through to the default.
|
||||
return hopsUsed; // If the request used more hops than the limit, use the same amount of hops
|
||||
#endif
|
||||
} else if ((uint8_t)(hopsUsed + 2) < config.lora.hop_limit) {
|
||||
return hopsUsed + 2; // Use only the amount of hops needed with some margin as the way back may be different
|
||||
}
|
||||
}
|
||||
return Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); // Use the default hop limit
|
||||
}
|
||||
|
||||
RoutingModule::RoutingModule() : ProtobufModule("routing", meshtastic_PortNum_ROUTING_APP, &meshtastic_Routing_msg)
|
||||
{
|
||||
isPromiscuous = true;
|
||||
|
||||
// moved the RebroadcastMode logic into handleReceivedProtobuf
|
||||
// LocalOnly requires either the from or to to be a known node
|
||||
// knownOnly specifically requires the from to be a known node.
|
||||
encryptedOk = true;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
#include "Channels.h"
|
||||
#include "ProtobufModule.h"
|
||||
|
||||
/**
|
||||
* Routing module for router control messages
|
||||
*/
|
||||
class RoutingModule : public ProtobufModule<meshtastic_Routing>
|
||||
{
|
||||
public:
|
||||
/** Constructor
|
||||
* name is for debugging output
|
||||
*/
|
||||
RoutingModule();
|
||||
|
||||
virtual void sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit = 0,
|
||||
bool ackWantsAck = false);
|
||||
|
||||
// Given the hopStart and hopLimit upon reception of a request, return the hop limit to use for the response
|
||||
uint8_t getHopLimitForResponse(uint8_t hopStart, uint8_t hopLimit);
|
||||
|
||||
protected:
|
||||
friend class Router;
|
||||
|
||||
/** 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_Routing *p) override;
|
||||
|
||||
/** Messages can be received that have the want_response bit set. If set, this callback will be invoked
|
||||
* so that subclasses can (optionally) send a response back to the original sender. */
|
||||
virtual meshtastic_MeshPacket *allocReply() override;
|
||||
|
||||
/// Override wantPacket to say we want to see all packets, not just those for our port number
|
||||
virtual bool wantPacket(const meshtastic_MeshPacket *p) override { return true; }
|
||||
};
|
||||
|
||||
extern RoutingModule *routingModule;
|
||||
@@ -0,0 +1,719 @@
|
||||
#include "SerialModule.h"
|
||||
#include "GeoCoord.h"
|
||||
#include "MeshService.h"
|
||||
#include "NMEAWPL.h"
|
||||
#include "NodeDB.h"
|
||||
#include "RTC.h"
|
||||
#include "Router.h"
|
||||
#include "configuration.h"
|
||||
#include <Arduino.h>
|
||||
#include <Throttle.h>
|
||||
|
||||
/*
|
||||
SerialModule
|
||||
A simple interface to send messages over the mesh network by sending strings
|
||||
over a serial port.
|
||||
|
||||
There are no PIN defaults, you have to enable the second serial port yourself.
|
||||
|
||||
Need help with this module? Post your question on the Meshtastic Discourse:
|
||||
https://meshtastic.discourse.group
|
||||
|
||||
Basic Usage:
|
||||
|
||||
1) Enable the module by setting enabled to 1.
|
||||
2) Set the pins (rxd / rxd) for your preferred RX and TX GPIO pins.
|
||||
On tbeam, recommend to use:
|
||||
RXD 35
|
||||
TXD 15
|
||||
3) Set timeout to the amount of time to wait before we consider
|
||||
your packet as "done".
|
||||
4) not applicable any more
|
||||
5) Connect to your device over the serial interface at 38400 8N1.
|
||||
6) Send a packet up to 240 bytes in length. This will get relayed over the mesh network.
|
||||
7) (Optional) Set echo to 1 and any message you send out will be echoed back
|
||||
to your device.
|
||||
|
||||
TODO (in this order):
|
||||
* Define a verbose RX mode to report on mesh and packet information.
|
||||
- This won't happen any time soon.
|
||||
|
||||
KNOWN PROBLEMS
|
||||
* Until the module is initialized by the startup sequence, the TX pin is in a floating
|
||||
state. Device connected to that pin may see this as "noise".
|
||||
* Will not work on Linux device targets.
|
||||
|
||||
|
||||
*/
|
||||
#ifdef HELTEC_MESH_SOLAR
|
||||
#include "meshSolarApp.h"
|
||||
#endif
|
||||
|
||||
#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_STM32WL)) && \
|
||||
!defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3)
|
||||
|
||||
#define RX_BUFFER 256
|
||||
#define TIMEOUT 250
|
||||
#define BAUD 38400
|
||||
#define ACK 1
|
||||
|
||||
// API: Defaulting to the formerly removed phone_timeout_secs value of 15 minutes
|
||||
#define SERIAL_CONNECTION_TIMEOUT (15 * 60) * 1000UL
|
||||
|
||||
SerialModule *serialModule;
|
||||
SerialModuleRadio *serialModuleRadio;
|
||||
|
||||
#if defined(TTGO_T_ECHO) || defined(CANARYONE) || defined(MESHLINK) || defined(ELECROW_ThinkNode_M1) || \
|
||||
defined(ELECROW_ThinkNode_M5) || defined(HELTEC_MESH_SOLAR) || defined(T_ECHO_LITE)
|
||||
SerialModule::SerialModule() : StreamAPI(&Serial), concurrency::OSThread("Serial")
|
||||
{
|
||||
api_type = TYPE_SERIAL;
|
||||
}
|
||||
static Print *serialPrint = &Serial;
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32C6) || defined(RAK3172) || defined(EBYTE_E77_MBL)
|
||||
SerialModule::SerialModule() : StreamAPI(&Serial1), concurrency::OSThread("Serial")
|
||||
{
|
||||
api_type = TYPE_SERIAL;
|
||||
}
|
||||
static Print *serialPrint = &Serial1;
|
||||
#else
|
||||
SerialModule::SerialModule() : StreamAPI(&Serial2), concurrency::OSThread("Serial")
|
||||
{
|
||||
api_type = TYPE_SERIAL;
|
||||
}
|
||||
static Print *serialPrint = &Serial2;
|
||||
#endif
|
||||
|
||||
char serialBytes[512];
|
||||
size_t serialPayloadSize;
|
||||
|
||||
bool SerialModule::isValidConfig(const meshtastic_ModuleConfig_SerialConfig &config)
|
||||
{
|
||||
if (config.override_console_serial_port && !IS_ONE_OF(config.mode, meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA,
|
||||
meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO,
|
||||
meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG)) {
|
||||
const char *warning =
|
||||
"Invalid Serial config: override console serial port is only supported in NMEA and CalTopo output-only modes.";
|
||||
LOG_ERROR(warning);
|
||||
#if !IS_RUNNING_TESTS
|
||||
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
|
||||
cn->level = meshtastic_LogRecord_Level_ERROR;
|
||||
cn->time = getValidTime(RTCQualityFromNet);
|
||||
snprintf(cn->message, sizeof(cn->message), "%s", warning);
|
||||
service->sendClientNotification(cn);
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
SerialModuleRadio::SerialModuleRadio() : MeshModule("SerialModuleRadio")
|
||||
{
|
||||
switch (moduleConfig.serial.mode) {
|
||||
case meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG:
|
||||
ourPortNum = meshtastic_PortNum_TEXT_MESSAGE_APP;
|
||||
break;
|
||||
case meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA:
|
||||
case meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO:
|
||||
ourPortNum = meshtastic_PortNum_POSITION_APP;
|
||||
break;
|
||||
default:
|
||||
ourPortNum = meshtastic_PortNum_SERIAL_APP;
|
||||
// restrict to the serial channel for rx
|
||||
boundChannel = Channels::serialChannel;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Checks if the serial connection is established.
|
||||
*
|
||||
* @return true if the serial connection is established, false otherwise.
|
||||
*
|
||||
* For the serial2 port we can't really detect if any client is on the other side, so instead just look for recent messages
|
||||
*/
|
||||
bool SerialModule::checkIsConnected()
|
||||
{
|
||||
return Throttle::isWithinTimespanMs(lastContactMsec, SERIAL_CONNECTION_TIMEOUT);
|
||||
}
|
||||
|
||||
int32_t SerialModule::runOnce()
|
||||
{
|
||||
/*
|
||||
Uncomment the preferences below if you want to use the module
|
||||
without having to configure it from the PythonAPI or WebUI.
|
||||
*/
|
||||
|
||||
// moduleConfig.serial.enabled = true;
|
||||
// moduleConfig.serial.rxd = 35;
|
||||
// moduleConfig.serial.txd = 15;
|
||||
// moduleConfig.serial.override_console_serial_port = true;
|
||||
// moduleConfig.serial.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO;
|
||||
// moduleConfig.serial.timeout = 1000;
|
||||
// moduleConfig.serial.echo = 1;
|
||||
|
||||
if (!moduleConfig.serial.enabled)
|
||||
return disable();
|
||||
|
||||
if (moduleConfig.serial.override_console_serial_port || (moduleConfig.serial.rxd && moduleConfig.serial.txd)) {
|
||||
if (firstTime) {
|
||||
// Interface with the serial peripheral from in here.
|
||||
LOG_INFO("Init serial peripheral interface");
|
||||
|
||||
uint32_t baud = getBaudRate();
|
||||
|
||||
if (moduleConfig.serial.override_console_serial_port) {
|
||||
#ifdef RP2040_SLOW_CLOCK
|
||||
Serial2.flush();
|
||||
serialPrint = &Serial2;
|
||||
#else
|
||||
Serial.flush();
|
||||
serialPrint = &Serial;
|
||||
#endif
|
||||
// Give it a chance to flush out 💩
|
||||
delay(10);
|
||||
}
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32C6)
|
||||
if (moduleConfig.serial.rxd && moduleConfig.serial.txd) {
|
||||
Serial1.setRxBufferSize(RX_BUFFER);
|
||||
Serial1.begin(baud, SERIAL_8N1, moduleConfig.serial.rxd, moduleConfig.serial.txd);
|
||||
} else {
|
||||
Serial.begin(baud);
|
||||
Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT);
|
||||
}
|
||||
#elif defined(ARCH_STM32WL)
|
||||
#ifndef RAK3172
|
||||
HardwareSerial *serialInstance = &Serial2;
|
||||
#else
|
||||
HardwareSerial *serialInstance = &Serial1;
|
||||
#endif
|
||||
if (moduleConfig.serial.rxd && moduleConfig.serial.txd) {
|
||||
serialInstance->setTx(moduleConfig.serial.txd);
|
||||
serialInstance->setRx(moduleConfig.serial.rxd);
|
||||
}
|
||||
serialInstance->begin(baud);
|
||||
serialInstance->setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT);
|
||||
#elif defined(ARCH_ESP32)
|
||||
|
||||
if (moduleConfig.serial.rxd && moduleConfig.serial.txd) {
|
||||
Serial2.setRxBufferSize(RX_BUFFER);
|
||||
Serial2.begin(baud, SERIAL_8N1, moduleConfig.serial.rxd, moduleConfig.serial.txd);
|
||||
} else {
|
||||
Serial.begin(baud);
|
||||
Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT);
|
||||
}
|
||||
#elif !defined(TTGO_T_ECHO) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && !defined(MESHLINK) && \
|
||||
!defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M5)
|
||||
if (moduleConfig.serial.rxd && moduleConfig.serial.txd) {
|
||||
#ifdef ARCH_RP2040
|
||||
Serial2.setFIFOSize(RX_BUFFER);
|
||||
Serial2.setPinout(moduleConfig.serial.txd, moduleConfig.serial.rxd);
|
||||
#else
|
||||
Serial2.setPins(moduleConfig.serial.rxd, moduleConfig.serial.txd);
|
||||
#endif
|
||||
Serial2.begin(baud, SERIAL_8N1);
|
||||
Serial2.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT);
|
||||
} else {
|
||||
#ifdef RP2040_SLOW_CLOCK
|
||||
Serial2.begin(baud, SERIAL_8N1);
|
||||
Serial2.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT);
|
||||
#else
|
||||
Serial.begin(baud, SERIAL_8N1);
|
||||
Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT);
|
||||
#endif
|
||||
}
|
||||
#else
|
||||
Serial.begin(baud, SERIAL_8N1);
|
||||
Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT);
|
||||
#endif
|
||||
serialModuleRadio = new SerialModuleRadio();
|
||||
|
||||
firstTime = 0;
|
||||
|
||||
// in API mode send rebooted sequence
|
||||
if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_PROTO) {
|
||||
emitRebooted();
|
||||
}
|
||||
} else {
|
||||
if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_PROTO) {
|
||||
return runOncePart();
|
||||
} else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA) && HAS_GPS) {
|
||||
// in NMEA mode send out GGA every 2 seconds, Don't read from Port
|
||||
if (!Throttle::isWithinTimespanMs(lastNmeaTime, 2000)) {
|
||||
lastNmeaTime = millis();
|
||||
printGGA(outbuf, sizeof(outbuf), localPosition);
|
||||
serialPrint->printf("%s", outbuf);
|
||||
}
|
||||
} else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO) && HAS_GPS) {
|
||||
if (!Throttle::isWithinTimespanMs(lastNmeaTime, 10000)) {
|
||||
lastNmeaTime = millis();
|
||||
uint32_t readIndex = 0;
|
||||
const meshtastic_NodeInfoLite *tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
|
||||
while (tempNodeInfo != NULL) {
|
||||
if (tempNodeInfo->has_user && nodeDB->hasValidPosition(tempNodeInfo)) {
|
||||
printWPL(outbuf, sizeof(outbuf), tempNodeInfo->position, tempNodeInfo->user.long_name, true);
|
||||
serialPrint->printf("%s", outbuf);
|
||||
}
|
||||
tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if !defined(TTGO_T_ECHO) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && !defined(MESHLINK) && \
|
||||
!defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M5)
|
||||
else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_WS85)) {
|
||||
processWXSerial();
|
||||
|
||||
}
|
||||
#if defined(HELTEC_MESH_SOLAR)
|
||||
else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG)) {
|
||||
serialPayloadSize = Serial.readBytes(serialBytes, sizeof(serialBytes) - 1);
|
||||
// If the parsing fails, the following parsing will be performed.
|
||||
if ((serialPayloadSize > 0) && (meshSolarCmdHandle(serialBytes) != 0)) {
|
||||
return runOncePart(serialBytes, serialPayloadSize);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
else {
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32C6)
|
||||
while (Serial1.available()) {
|
||||
serialPayloadSize = Serial1.readBytes(serialBytes, meshtastic_Constants_DATA_PAYLOAD_LEN);
|
||||
#else
|
||||
#ifndef RAK3172
|
||||
HardwareSerial *serialInstance = &Serial2;
|
||||
#else
|
||||
HardwareSerial *serialInstance = &Serial1;
|
||||
#endif
|
||||
while (serialInstance->available()) {
|
||||
serialPayloadSize = serialInstance->readBytes(serialBytes, meshtastic_Constants_DATA_PAYLOAD_LEN);
|
||||
#endif
|
||||
serialModuleRadio->sendPayload();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
return (10);
|
||||
} else {
|
||||
return disable();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends telemetry packet over the mesh network.
|
||||
*
|
||||
* @param m The telemetry data to be sent
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws None
|
||||
*/
|
||||
void SerialModule::sendTelemetry(meshtastic_Telemetry m)
|
||||
{
|
||||
meshtastic_MeshPacket *p = router->allocForSending();
|
||||
p->decoded.portnum = meshtastic_PortNum_TELEMETRY_APP;
|
||||
p->decoded.payload.size =
|
||||
pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_Telemetry_msg, &m);
|
||||
p->to = NODENUM_BROADCAST;
|
||||
p->decoded.want_response = false;
|
||||
if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR) {
|
||||
p->want_ack = true;
|
||||
p->priority = meshtastic_MeshPacket_Priority_HIGH;
|
||||
} else {
|
||||
p->priority = meshtastic_MeshPacket_Priority_RELIABLE;
|
||||
}
|
||||
service->sendToMesh(p, RX_SRC_LOCAL, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocates a new mesh packet for use as a reply to a received packet.
|
||||
*
|
||||
* @return A pointer to the newly allocated mesh packet.
|
||||
*/
|
||||
meshtastic_MeshPacket *SerialModuleRadio::allocReply()
|
||||
{
|
||||
auto reply = allocDataPacket(); // Allocate a packet for sending
|
||||
|
||||
return reply;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a payload to a specified destination node.
|
||||
*
|
||||
* @param dest The destination node number.
|
||||
* @param wantReplies Whether or not to request replies from the destination node.
|
||||
*/
|
||||
void SerialModuleRadio::sendPayload(NodeNum dest, bool wantReplies)
|
||||
{
|
||||
const meshtastic_Channel *ch = (boundChannel != NULL) ? &channels.getByName(boundChannel) : NULL;
|
||||
meshtastic_MeshPacket *p = allocReply();
|
||||
p->to = dest;
|
||||
if (ch != NULL) {
|
||||
p->channel = ch->index;
|
||||
}
|
||||
p->decoded.want_response = wantReplies;
|
||||
|
||||
p->want_ack = ACK;
|
||||
|
||||
p->decoded.payload.size = serialPayloadSize; // You must specify how many bytes are in the reply
|
||||
memcpy(p->decoded.payload.bytes, serialBytes, p->decoded.payload.size);
|
||||
|
||||
service->sendToMesh(p);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a received mesh packet.
|
||||
*
|
||||
* @param mp The received mesh packet.
|
||||
* @return The processed message.
|
||||
*/
|
||||
ProcessMessage SerialModuleRadio::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
if (moduleConfig.serial.enabled) {
|
||||
if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_PROTO) {
|
||||
// in API mode we don't care about stuff from radio.
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
auto &p = mp.decoded;
|
||||
// LOG_DEBUG("Received text msg self=0x%0x, from=0x%0x, to=0x%0x, id=%d, msg=%.*s",
|
||||
// nodeDB->getNodeNum(), mp.from, mp.to, mp.id, p.payload.size, p.payload.bytes);
|
||||
|
||||
if (isFromUs(&mp)) {
|
||||
|
||||
/*
|
||||
* If moduleConfig.serial.echo is true, then echo the packets that are sent out
|
||||
* back to the TX of the serial interface.
|
||||
*/
|
||||
if (moduleConfig.serial.echo) {
|
||||
|
||||
// For some reason, we get the packet back twice when we send out of the radio.
|
||||
// TODO: need to find out why.
|
||||
if (lastRxID != mp.id) {
|
||||
lastRxID = mp.id;
|
||||
// LOG_DEBUG("* * Message came this device");
|
||||
// serialPrint->println("* * Message came this device");
|
||||
serialPrint->printf("%s", p.payload.bytes);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_DEFAULT ||
|
||||
moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_SIMPLE) {
|
||||
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));
|
||||
const char *sender = (node && node->has_user) ? node->user.short_name : "???";
|
||||
serialPrint->println();
|
||||
serialPrint->printf("%s: %s", sender, p.payload.bytes);
|
||||
serialPrint->println();
|
||||
} else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA ||
|
||||
moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO) &&
|
||||
HAS_GPS) {
|
||||
// Decode the Payload some more
|
||||
meshtastic_Position scratch;
|
||||
meshtastic_Position *decoded = NULL;
|
||||
if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.decoded.portnum == ourPortNum) {
|
||||
memset(&scratch, 0, sizeof(scratch));
|
||||
if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Position_msg, &scratch)) {
|
||||
decoded = &scratch;
|
||||
}
|
||||
// send position packet as WPL to the serial port
|
||||
printWPL(outbuf, sizeof(outbuf), *decoded, nodeDB->getMeshNode(getFrom(&mp))->user.long_name,
|
||||
moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO);
|
||||
serialPrint->printf("%s", outbuf);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ProcessMessage::CONTINUE; // Let others look at this message also if they want
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns the baud rate of the serial module from the module configuration.
|
||||
*
|
||||
* @return uint32_t The baud rate of the serial module.
|
||||
*/
|
||||
uint32_t SerialModule::getBaudRate()
|
||||
{
|
||||
if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_110) {
|
||||
return 110;
|
||||
} else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_300) {
|
||||
return 300;
|
||||
} else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_600) {
|
||||
return 600;
|
||||
} else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_1200) {
|
||||
return 1200;
|
||||
} else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_2400) {
|
||||
return 2400;
|
||||
} else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_4800) {
|
||||
return 4800;
|
||||
} else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_9600) {
|
||||
return 9600;
|
||||
} else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_19200) {
|
||||
return 19200;
|
||||
} else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_38400) {
|
||||
return 38400;
|
||||
} else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_57600) {
|
||||
return 57600;
|
||||
} else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_115200) {
|
||||
return 115200;
|
||||
} else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_230400) {
|
||||
return 230400;
|
||||
} else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_460800) {
|
||||
return 460800;
|
||||
} else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_576000) {
|
||||
return 576000;
|
||||
} else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_921600) {
|
||||
return 921600;
|
||||
}
|
||||
return BAUD;
|
||||
}
|
||||
|
||||
// Add this structure to help with parsing WindGust = 24.4 serial lines.
|
||||
struct ParsedLine {
|
||||
char name[64];
|
||||
char value[128];
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse a line of format "Name = Value" into name/value pair
|
||||
* @param line Input line to parse
|
||||
* @return ParsedLine containing name and value, or empty strings if parse failed
|
||||
*/
|
||||
ParsedLine parseLine(const char *line)
|
||||
{
|
||||
ParsedLine result = {"", ""};
|
||||
|
||||
// Find equals sign
|
||||
const char *equals = strchr(line, '=');
|
||||
if (!equals) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Extract name by copying substring
|
||||
char nameBuf[64]; // Temporary buffer
|
||||
size_t nameLen = equals - line;
|
||||
if (nameLen >= sizeof(nameBuf)) {
|
||||
nameLen = sizeof(nameBuf) - 1;
|
||||
}
|
||||
strncpy(nameBuf, line, nameLen);
|
||||
nameBuf[nameLen] = '\0';
|
||||
|
||||
// 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';
|
||||
|
||||
// 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';
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the received weather station serial data, extract wind, voltage, and temperature information,
|
||||
* calculate averages and send telemetry data over the mesh network.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
void SerialModule::processWXSerial()
|
||||
{
|
||||
#if !defined(TTGO_T_ECHO) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && !defined(CONFIG_IDF_TARGET_ESP32C6) && \
|
||||
!defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M5) && !defined(ARCH_STM32WL)
|
||||
static unsigned int lastAveraged = 0;
|
||||
static unsigned int averageIntervalMillis = 300000; // 5 minutes hard coded.
|
||||
static double dir_sum_sin = 0;
|
||||
static double dir_sum_cos = 0;
|
||||
static float velSum = 0;
|
||||
static float gust = 0;
|
||||
static float lull = -1;
|
||||
static int velCount = 0;
|
||||
static int dirCount = 0;
|
||||
static char windDir[4] = "xxx"; // Assuming windDir is 3 characters long + null terminator
|
||||
static char windVel[5] = "xx.x"; // Assuming windVel is 4 characters long + null terminator
|
||||
static char windGust[5] = "xx.x"; // Assuming windGust is 4 characters long + null terminator
|
||||
static char batVoltage[5] = "0.0V";
|
||||
static char capVoltage[5] = "0.0V";
|
||||
static char temperature[5] = "00.0";
|
||||
static float batVoltageF = 0;
|
||||
static float capVoltageF = 0;
|
||||
static float temperatureF = 0;
|
||||
|
||||
static char rainStr[] = "5780860000";
|
||||
static int rainSum = 0;
|
||||
static float rain = 0;
|
||||
bool gotwind = false;
|
||||
|
||||
while (Serial2.available()) {
|
||||
// clear serialBytes buffer
|
||||
memset(serialBytes, '\0', sizeof(serialBytes));
|
||||
// memset(formattedString, '\0', sizeof(formattedString));
|
||||
serialPayloadSize = Serial2.readBytes(serialBytes, 512);
|
||||
// check for a strings we care about
|
||||
// example output of serial data fields from the WS85
|
||||
// WindDir = 79
|
||||
// WindSpeed = 0.5
|
||||
// WindGust = 0.6
|
||||
// GXTS04Temp = 24.4
|
||||
// Temperature = 23.4 // WS80
|
||||
|
||||
// RainIntSum = 0
|
||||
// Rain = 0.0
|
||||
if (serialPayloadSize > 0) {
|
||||
// Define variables for line processing
|
||||
int lineStart = 0;
|
||||
int lineEnd = -1;
|
||||
|
||||
// Process each byte in the received data
|
||||
for (size_t i = 0; i < serialPayloadSize; i++) {
|
||||
// go until we hit the end of line and then process the line
|
||||
if (serialBytes[i] == '\n') {
|
||||
lineEnd = i;
|
||||
// Extract the current line
|
||||
char line[meshtastic_Constants_DATA_PAYLOAD_LEN];
|
||||
memset(line, '\0', sizeof(line));
|
||||
if ((size_t)(lineEnd - lineStart) < sizeof(line) - 1) {
|
||||
memcpy(line, &serialBytes[lineStart], lineEnd - lineStart);
|
||||
|
||||
ParsedLine parsed = parseLine(line);
|
||||
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 (strcmp(parsed.name, "WindSpeed") == 0) {
|
||||
strlcpy(windVel, parsed.value, sizeof(windVel));
|
||||
float newv = strtof(windVel, nullptr);
|
||||
velSum += newv;
|
||||
velCount++;
|
||||
if (newv < lull || lull == -1) {
|
||||
lull = newv;
|
||||
}
|
||||
gotwind = true;
|
||||
} 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 (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 (strcmp(parsed.name, "CapVoltage") == 0) {
|
||||
strlcpy(capVoltage, parsed.value, sizeof(capVoltage));
|
||||
capVoltageF = strtof(capVoltage, nullptr);
|
||||
} else if (strcmp(parsed.name, "GXTS04Temp") == 0 || strcmp(parsed.name, "Temperature") == 0) {
|
||||
strlcpy(temperature, parsed.value, sizeof(temperature));
|
||||
temperatureF = strtof(temperature, nullptr);
|
||||
} else if (strcmp(parsed.name, "RainIntSum") == 0) {
|
||||
strlcpy(rainStr, parsed.value, sizeof(rainStr));
|
||||
rainSum = int(strtof(rainStr, nullptr));
|
||||
} else if (strcmp(parsed.name, "Rain") == 0) {
|
||||
strlcpy(rainStr, parsed.value, sizeof(rainStr));
|
||||
rain = strtof(rainStr, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
// Update lineStart for the next line
|
||||
lineStart = lineEnd + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
// clear the input buffer
|
||||
while (Serial2.available() > 0) {
|
||||
Serial2.read(); // Read and discard the bytes in the input buffer
|
||||
}
|
||||
}
|
||||
}
|
||||
if (gotwind) {
|
||||
|
||||
LOG_INFO("WS8X : %i %.1fg%.1f %.1fv %.1fv %.1fC rain: %.1f, %i sum", atoi(windDir), strtof(windVel, nullptr),
|
||||
strtof(windGust, nullptr), batVoltageF, capVoltageF, temperatureF, rain, rainSum);
|
||||
}
|
||||
if (gotwind && !Throttle::isWithinTimespanMs(lastAveraged, averageIntervalMillis)) {
|
||||
// calculate averages and send to the mesh
|
||||
float velAvg = 1.0 * velSum / velCount;
|
||||
|
||||
double avgSin = dir_sum_sin / dirCount;
|
||||
double avgCos = dir_sum_cos / dirCount;
|
||||
|
||||
double avgRadians = atan2(avgSin, avgCos);
|
||||
float dirAvg = GeoCoord::toDegrees(avgRadians);
|
||||
|
||||
if (dirAvg < 0) {
|
||||
dirAvg += 360.0;
|
||||
}
|
||||
lastAveraged = millis();
|
||||
|
||||
// make a telemetry packet with the data
|
||||
meshtastic_Telemetry m = meshtastic_Telemetry_init_zero;
|
||||
m.which_variant = meshtastic_Telemetry_environment_metrics_tag;
|
||||
|
||||
m.variant.environment_metrics.wind_speed = velAvg;
|
||||
m.variant.environment_metrics.has_wind_speed = true;
|
||||
|
||||
m.variant.environment_metrics.wind_direction = dirAvg;
|
||||
m.variant.environment_metrics.has_wind_direction = true;
|
||||
|
||||
m.variant.environment_metrics.temperature = temperatureF;
|
||||
m.variant.environment_metrics.has_temperature = true;
|
||||
|
||||
m.variant.environment_metrics.voltage =
|
||||
capVoltageF > batVoltageF ? capVoltageF : batVoltageF; // send the larger of the two voltage values.
|
||||
m.variant.environment_metrics.has_voltage = true;
|
||||
|
||||
m.variant.environment_metrics.wind_gust = gust;
|
||||
m.variant.environment_metrics.has_wind_gust = true;
|
||||
|
||||
m.variant.environment_metrics.rainfall_24h = rainSum;
|
||||
m.variant.environment_metrics.has_rainfall_24h = true;
|
||||
|
||||
// not sure if this value is actually the 1hr sum so needs to do some testing
|
||||
m.variant.environment_metrics.rainfall_1h = rain;
|
||||
m.variant.environment_metrics.has_rainfall_1h = true;
|
||||
|
||||
if (lull == -1)
|
||||
lull = 0;
|
||||
m.variant.environment_metrics.wind_lull = lull;
|
||||
m.variant.environment_metrics.has_wind_lull = true;
|
||||
|
||||
LOG_INFO("WS8X Transmit speed=%fm/s, direction=%d , lull=%f, gust=%f, voltage=%f temperature=%f",
|
||||
m.variant.environment_metrics.wind_speed, m.variant.environment_metrics.wind_direction,
|
||||
m.variant.environment_metrics.wind_lull, m.variant.environment_metrics.wind_gust,
|
||||
m.variant.environment_metrics.voltage, m.variant.environment_metrics.temperature);
|
||||
|
||||
sendTelemetry(m);
|
||||
|
||||
// reset counters and gust/lull
|
||||
velSum = velCount = dirCount = 0;
|
||||
dir_sum_sin = dir_sum_cos = 0;
|
||||
gust = 0;
|
||||
lull = -1;
|
||||
}
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,82 @@
|
||||
#pragma once
|
||||
|
||||
#include "MeshModule.h"
|
||||
#include "Router.h"
|
||||
#include "SinglePortModule.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
#include "configuration.h"
|
||||
#include <Arduino.h>
|
||||
#include <functional>
|
||||
|
||||
#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_STM32WL)) && \
|
||||
!defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3)
|
||||
|
||||
class SerialModule : public StreamAPI, private concurrency::OSThread
|
||||
{
|
||||
bool firstTime = 1;
|
||||
unsigned long lastNmeaTime = millis();
|
||||
char outbuf[90] = "";
|
||||
|
||||
public:
|
||||
SerialModule();
|
||||
|
||||
static bool isValidConfig(const meshtastic_ModuleConfig_SerialConfig &config);
|
||||
|
||||
protected:
|
||||
virtual int32_t runOnce() override;
|
||||
|
||||
/// Check the current underlying physical link to see if the client is currently connected
|
||||
virtual bool checkIsConnected() override;
|
||||
|
||||
private:
|
||||
uint32_t getBaudRate();
|
||||
void sendTelemetry(meshtastic_Telemetry m);
|
||||
void processWXSerial();
|
||||
};
|
||||
|
||||
extern SerialModule *serialModule;
|
||||
|
||||
/*
|
||||
* Radio interface for SerialModule
|
||||
*
|
||||
*/
|
||||
class SerialModuleRadio : public MeshModule
|
||||
{
|
||||
uint32_t lastRxID = 0;
|
||||
char outbuf[90] = "";
|
||||
|
||||
public:
|
||||
SerialModuleRadio();
|
||||
|
||||
/**
|
||||
* Send our payload into the mesh
|
||||
*/
|
||||
void sendPayload(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false);
|
||||
|
||||
protected:
|
||||
virtual meshtastic_MeshPacket *allocReply() 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;
|
||||
|
||||
meshtastic_PortNum ourPortNum;
|
||||
|
||||
virtual bool wantPacket(const meshtastic_MeshPacket *p) override { return p->decoded.portnum == ourPortNum; }
|
||||
|
||||
meshtastic_MeshPacket *allocDataPacket()
|
||||
{
|
||||
// Update our local node info with our position (even if we don't decide to update anyone else)
|
||||
meshtastic_MeshPacket *p = router->allocForSending();
|
||||
p->decoded.portnum = ourPortNum;
|
||||
|
||||
return p;
|
||||
}
|
||||
};
|
||||
|
||||
extern SerialModuleRadio *serialModuleRadio;
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,638 @@
|
||||
/**
|
||||
* @file StoreForwardModule.cpp
|
||||
* @brief Implementation of the StoreForwardModule class.
|
||||
*
|
||||
* This file contains the implementation of the StoreForwardModule class, which is responsible for managing the store and forward
|
||||
* functionality of the Meshtastic device. The class provides methods for sending and receiving messages, as well as managing the
|
||||
* message history queue. It also initializes and manages the data structures used for storing the message history.
|
||||
*
|
||||
* The StoreForwardModule class is used by the MeshService class to provide store and forward functionality to the Meshtastic
|
||||
* device.
|
||||
*
|
||||
* @author Jm Casler
|
||||
* @date [Insert Date]
|
||||
*/
|
||||
#include "StoreForwardModule.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "RTC.h"
|
||||
#include "Router.h"
|
||||
#include "Throttle.h"
|
||||
#include "airtime.h"
|
||||
#include "configuration.h"
|
||||
#include "memGet.h"
|
||||
#include "mesh-pb-constants.h"
|
||||
#include "mesh/generated/meshtastic/storeforward.pb.h"
|
||||
#include "modules/ModuleDev.h"
|
||||
#include <Arduino.h>
|
||||
#include <iterator>
|
||||
#include <map>
|
||||
|
||||
StoreForwardModule *storeForwardModule;
|
||||
|
||||
int32_t StoreForwardModule::runOnce()
|
||||
{
|
||||
#if defined(ARCH_ESP32) || defined(ARCH_PORTDUINO)
|
||||
if (moduleConfig.store_forward.enabled && is_server) {
|
||||
// Send out the message queue.
|
||||
if (this->busy) {
|
||||
// Only send packets if the channel is less than 25% utilized and until historyReturnMax
|
||||
if (airTime->isTxAllowedChannelUtil(true) && this->requestCount < this->historyReturnMax) {
|
||||
if (!storeForwardModule->sendPayload(this->busyTo, this->last_time)) {
|
||||
this->requestCount = 0;
|
||||
this->busy = false;
|
||||
}
|
||||
}
|
||||
} else if (this->heartbeat && (!Throttle::isWithinTimespanMs(lastHeartbeat, heartbeatInterval * 1000)) &&
|
||||
airTime->isTxAllowedChannelUtil(true)) {
|
||||
lastHeartbeat = millis();
|
||||
LOG_INFO("Send heartbeat");
|
||||
meshtastic_StoreAndForward sf = meshtastic_StoreAndForward_init_zero;
|
||||
sf.rr = meshtastic_StoreAndForward_RequestResponse_ROUTER_HEARTBEAT;
|
||||
sf.which_variant = meshtastic_StoreAndForward_heartbeat_tag;
|
||||
sf.variant.heartbeat.period = heartbeatInterval;
|
||||
sf.variant.heartbeat.secondary = 0; // TODO we always have one primary router for now
|
||||
storeForwardModule->sendMessage(NODENUM_BROADCAST, sf);
|
||||
}
|
||||
return (this->packetTimeMax);
|
||||
}
|
||||
#endif
|
||||
return disable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the PSRAM with data to be sent later when a device is out of range.
|
||||
*/
|
||||
void StoreForwardModule::populatePSRAM()
|
||||
{
|
||||
/*
|
||||
For PSRAM usage, see:
|
||||
https://learn.upesy.com/en/programmation/psram.html#psram-tab
|
||||
*/
|
||||
|
||||
LOG_DEBUG("Before PSRAM init: heap %d/%d PSRAM %d/%d", memGet.getFreeHeap(), memGet.getHeapSize(), memGet.getFreePsram(),
|
||||
memGet.getPsramSize());
|
||||
|
||||
/* Use a maximum of 3/4 the available PSRAM unless otherwise specified.
|
||||
Note: This needs to be done after every thing that would use PSRAM
|
||||
*/
|
||||
uint32_t numberOfPackets =
|
||||
(this->records ? this->records : (((memGet.getFreePsram() / 4) * 3) / sizeof(PacketHistoryStruct)));
|
||||
this->records = numberOfPackets;
|
||||
#if defined(ARCH_ESP32)
|
||||
this->packetHistory = static_cast<PacketHistoryStruct *>(ps_calloc(numberOfPackets, sizeof(PacketHistoryStruct)));
|
||||
#elif defined(ARCH_PORTDUINO)
|
||||
this->packetHistory = static_cast<PacketHistoryStruct *>(calloc(numberOfPackets, sizeof(PacketHistoryStruct)));
|
||||
|
||||
#endif
|
||||
|
||||
LOG_DEBUG("After PSRAM init: heap %d/%d PSRAM %d/%d", memGet.getFreeHeap(), memGet.getHeapSize(), memGet.getFreePsram(),
|
||||
memGet.getPsramSize());
|
||||
LOG_DEBUG("numberOfPackets for packetHistory - %u", numberOfPackets);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends messages from the message history to the specified recipient.
|
||||
*
|
||||
* @param sAgo The number of seconds ago from which to start sending messages.
|
||||
* @param to The recipient ID to send the messages to.
|
||||
*/
|
||||
void StoreForwardModule::historySend(uint32_t secAgo, uint32_t to)
|
||||
{
|
||||
this->last_time = getTime() < secAgo ? 0 : getTime() - secAgo;
|
||||
uint32_t queueSize = getNumAvailablePackets(to, last_time);
|
||||
if (queueSize > this->historyReturnMax)
|
||||
queueSize = this->historyReturnMax;
|
||||
|
||||
if (queueSize) {
|
||||
LOG_INFO("S&F - Send %u message(s)", queueSize);
|
||||
this->busy = true; // runOnce() will pickup the next steps once busy = true.
|
||||
this->busyTo = to;
|
||||
} else {
|
||||
LOG_INFO("S&F - No history");
|
||||
}
|
||||
meshtastic_StoreAndForward sf = meshtastic_StoreAndForward_init_zero;
|
||||
sf.rr = meshtastic_StoreAndForward_RequestResponse_ROUTER_HISTORY;
|
||||
sf.which_variant = meshtastic_StoreAndForward_history_tag;
|
||||
sf.variant.history.history_messages = queueSize;
|
||||
sf.variant.history.window = secAgo * 1000;
|
||||
sf.variant.history.last_request = lastRequest[to];
|
||||
storeForwardModule->sendMessage(to, sf);
|
||||
setIntervalFromNow(this->packetTimeMax); // Delay start of sending payloads
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of available packets in the message history for a specified destination node.
|
||||
*
|
||||
* @param dest The destination node number.
|
||||
* @param last_time The relative time to start counting messages from.
|
||||
* @return The number of available packets in the message history.
|
||||
*/
|
||||
uint32_t StoreForwardModule::getNumAvailablePackets(NodeNum dest, uint32_t last_time)
|
||||
{
|
||||
uint32_t count = 0;
|
||||
if (lastRequest.find(dest) == lastRequest.end()) {
|
||||
lastRequest.emplace(dest, 0);
|
||||
}
|
||||
for (uint32_t i = lastRequest[dest]; i < this->packetHistoryTotalCount; i++) {
|
||||
if (this->packetHistory[i].time && (this->packetHistory[i].time > last_time)) {
|
||||
// Client is only interested in packets not from itself and only in broadcast packets or packets towards it.
|
||||
if (this->packetHistory[i].from != dest &&
|
||||
(this->packetHistory[i].to == NODENUM_BROADCAST || this->packetHistory[i].to == dest)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocates a mesh packet for sending to the phone.
|
||||
*
|
||||
* @return A pointer to the allocated mesh packet or nullptr if none is available.
|
||||
*/
|
||||
meshtastic_MeshPacket *StoreForwardModule::getForPhone()
|
||||
{
|
||||
if (moduleConfig.store_forward.enabled && is_server) {
|
||||
NodeNum to = nodeDB->getNodeNum();
|
||||
if (!this->busy) {
|
||||
// Get number of packets we're going to send in this loop
|
||||
uint32_t histSize = getNumAvailablePackets(to, 0); // No time limit
|
||||
if (histSize) {
|
||||
this->busy = true;
|
||||
this->busyTo = to;
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// We're busy with sending to us until no payload is available anymore
|
||||
if (this->busy && this->busyTo == to) {
|
||||
meshtastic_MeshPacket *p = preparePayload(to, 0, true); // No time limit
|
||||
if (!p) // No more messages to send
|
||||
this->busy = false;
|
||||
return p;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a mesh packet to the history buffer for store-and-forward functionality.
|
||||
*
|
||||
* @param mp The mesh packet to add to the history buffer.
|
||||
*/
|
||||
void StoreForwardModule::historyAdd(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
const auto &p = mp.decoded;
|
||||
|
||||
if (this->packetHistoryTotalCount == this->records) {
|
||||
LOG_WARN("S&F - PSRAM Full. Starting overwrite");
|
||||
this->packetHistoryTotalCount = 0;
|
||||
for (auto &i : lastRequest) {
|
||||
i.second = 0; // Clear the last request index for each client device
|
||||
}
|
||||
}
|
||||
|
||||
this->packetHistory[this->packetHistoryTotalCount].time = getTime();
|
||||
this->packetHistory[this->packetHistoryTotalCount].to = mp.to;
|
||||
this->packetHistory[this->packetHistoryTotalCount].channel = mp.channel;
|
||||
this->packetHistory[this->packetHistoryTotalCount].from = getFrom(&mp);
|
||||
this->packetHistory[this->packetHistoryTotalCount].id = mp.id;
|
||||
this->packetHistory[this->packetHistoryTotalCount].reply_id = p.reply_id;
|
||||
this->packetHistory[this->packetHistoryTotalCount].emoji = (bool)p.emoji;
|
||||
this->packetHistory[this->packetHistoryTotalCount].payload_size = p.payload.size;
|
||||
this->packetHistory[this->packetHistoryTotalCount].rx_rssi = mp.rx_rssi;
|
||||
this->packetHistory[this->packetHistoryTotalCount].rx_snr = mp.rx_snr;
|
||||
this->packetHistory[this->packetHistoryTotalCount].hop_start = mp.hop_start;
|
||||
this->packetHistory[this->packetHistoryTotalCount].hop_limit = mp.hop_limit;
|
||||
this->packetHistory[this->packetHistoryTotalCount].via_mqtt = mp.via_mqtt;
|
||||
this->packetHistory[this->packetHistoryTotalCount].transport_mechanism = mp.transport_mechanism;
|
||||
memcpy(this->packetHistory[this->packetHistoryTotalCount].payload, p.payload.bytes, meshtastic_Constants_DATA_PAYLOAD_LEN);
|
||||
|
||||
this->packetHistoryTotalCount++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a payload to a specified destination node using the store and forward mechanism.
|
||||
*
|
||||
* @param dest The destination node number.
|
||||
* @param last_time The relative time to start sending messages from.
|
||||
* @return True if a packet was successfully sent, false otherwise.
|
||||
*/
|
||||
bool StoreForwardModule::sendPayload(NodeNum dest, uint32_t last_time)
|
||||
{
|
||||
meshtastic_MeshPacket *p = preparePayload(dest, last_time);
|
||||
if (p) {
|
||||
LOG_INFO("Send S&F Payload");
|
||||
service->sendToMesh(p);
|
||||
this->requestCount++;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a payload to be sent to a specified destination node from the S&F packet history.
|
||||
*
|
||||
* @param dest The destination node number.
|
||||
* @param last_time The relative time to start sending messages from.
|
||||
* @return A pointer to the prepared mesh packet or nullptr if none is available.
|
||||
*/
|
||||
meshtastic_MeshPacket *StoreForwardModule::preparePayload(NodeNum dest, uint32_t last_time, bool local)
|
||||
{
|
||||
for (uint32_t i = lastRequest[dest]; i < this->packetHistoryTotalCount; i++) {
|
||||
if (this->packetHistory[i].time && (this->packetHistory[i].time > last_time)) {
|
||||
/* Copy the messages that were received by the server in the last msAgo
|
||||
to the packetHistoryTXQueue structure.
|
||||
Client not interested in packets from itself and only in broadcast packets or packets towards it. */
|
||||
if (this->packetHistory[i].from != dest &&
|
||||
(this->packetHistory[i].to == NODENUM_BROADCAST || this->packetHistory[i].to == dest)) {
|
||||
|
||||
meshtastic_MeshPacket *p = allocDataPacket();
|
||||
|
||||
p->to = local ? this->packetHistory[i].to : dest; // PhoneAPI can handle original `to`
|
||||
p->from = this->packetHistory[i].from;
|
||||
p->id = this->packetHistory[i].id;
|
||||
p->channel = this->packetHistory[i].channel;
|
||||
p->decoded.reply_id = this->packetHistory[i].reply_id;
|
||||
p->rx_time = this->packetHistory[i].time;
|
||||
p->decoded.emoji = (uint32_t)this->packetHistory[i].emoji;
|
||||
p->rx_rssi = this->packetHistory[i].rx_rssi;
|
||||
p->rx_snr = this->packetHistory[i].rx_snr;
|
||||
p->hop_start = this->packetHistory[i].hop_start;
|
||||
p->hop_limit = this->packetHistory[i].hop_limit;
|
||||
p->via_mqtt = this->packetHistory[i].via_mqtt;
|
||||
p->transport_mechanism = (meshtastic_MeshPacket_TransportMechanism)this->packetHistory[i].transport_mechanism;
|
||||
|
||||
// Let's assume that if the server received the S&F request that the client is in range.
|
||||
// TODO: Make this configurable.
|
||||
p->want_ack = false;
|
||||
|
||||
if (local) { // PhoneAPI gets normal TEXT_MESSAGE_APP
|
||||
p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP;
|
||||
memcpy(p->decoded.payload.bytes, this->packetHistory[i].payload, this->packetHistory[i].payload_size);
|
||||
p->decoded.payload.size = this->packetHistory[i].payload_size;
|
||||
} else {
|
||||
meshtastic_StoreAndForward sf = meshtastic_StoreAndForward_init_zero;
|
||||
sf.which_variant = meshtastic_StoreAndForward_text_tag;
|
||||
sf.variant.text.size = this->packetHistory[i].payload_size;
|
||||
memcpy(sf.variant.text.bytes, this->packetHistory[i].payload, this->packetHistory[i].payload_size);
|
||||
if (this->packetHistory[i].to == NODENUM_BROADCAST) {
|
||||
sf.rr = meshtastic_StoreAndForward_RequestResponse_ROUTER_TEXT_BROADCAST;
|
||||
} else {
|
||||
sf.rr = meshtastic_StoreAndForward_RequestResponse_ROUTER_TEXT_DIRECT;
|
||||
}
|
||||
|
||||
p->decoded.payload.size = pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes),
|
||||
&meshtastic_StoreAndForward_msg, &sf);
|
||||
}
|
||||
|
||||
lastRequest[dest] = i + 1; // Update the last request index for the client device
|
||||
|
||||
return p;
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to a specified destination node using the store and forward protocol.
|
||||
*
|
||||
* @param dest The destination node number.
|
||||
* @param payload The message payload to be sent.
|
||||
*/
|
||||
void StoreForwardModule::sendMessage(NodeNum dest, const meshtastic_StoreAndForward &payload)
|
||||
{
|
||||
meshtastic_MeshPacket *p = allocDataProtobuf(payload);
|
||||
|
||||
p->to = dest;
|
||||
|
||||
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||
|
||||
// Let's assume that if the server received the S&F request that the client is in range.
|
||||
// TODO: Make this configurable.
|
||||
p->want_ack = false;
|
||||
p->decoded.want_response = false;
|
||||
|
||||
service->sendToMesh(p);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a store-and-forward message to the specified destination node.
|
||||
*
|
||||
* @param dest The destination node number.
|
||||
* @param rr The store-and-forward request/response message to send.
|
||||
*/
|
||||
void StoreForwardModule::sendMessage(NodeNum dest, meshtastic_StoreAndForward_RequestResponse rr)
|
||||
{
|
||||
// Craft an empty response, save some bytes in flash
|
||||
meshtastic_StoreAndForward sf = meshtastic_StoreAndForward_init_zero;
|
||||
sf.rr = rr;
|
||||
storeForwardModule->sendMessage(dest, sf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a text message with an error (busy or channel not available) to the specified destination node.
|
||||
*
|
||||
* @param dest The destination node number.
|
||||
* @param want_response True if the original message requested a response, false otherwise.
|
||||
*/
|
||||
void StoreForwardModule::sendErrorTextMessage(NodeNum dest, bool want_response)
|
||||
{
|
||||
meshtastic_MeshPacket *pr = allocDataPacket();
|
||||
pr->to = dest;
|
||||
pr->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||
pr->want_ack = false;
|
||||
pr->decoded.want_response = false;
|
||||
pr->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP;
|
||||
const char *str;
|
||||
if (this->busy) {
|
||||
str = "S&F - Busy. Try again shortly.";
|
||||
} else {
|
||||
str = "S&F not permitted on the public channel.";
|
||||
}
|
||||
LOG_WARN("%s", str);
|
||||
memcpy(pr->decoded.payload.bytes, str, strlen(str));
|
||||
pr->decoded.payload.size = strlen(str);
|
||||
if (want_response) {
|
||||
ignoreRequest = true; // This text message counts as response.
|
||||
}
|
||||
service->sendToMesh(pr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends statistics about the store and forward module to the specified node.
|
||||
*
|
||||
* @param to The node ID to send the statistics to.
|
||||
*/
|
||||
void StoreForwardModule::statsSend(uint32_t to)
|
||||
{
|
||||
meshtastic_StoreAndForward sf = meshtastic_StoreAndForward_init_zero;
|
||||
|
||||
sf.rr = meshtastic_StoreAndForward_RequestResponse_ROUTER_STATS;
|
||||
sf.which_variant = meshtastic_StoreAndForward_stats_tag;
|
||||
sf.variant.stats.messages_total = this->records;
|
||||
sf.variant.stats.messages_saved = this->packetHistoryTotalCount;
|
||||
sf.variant.stats.messages_max = this->records;
|
||||
sf.variant.stats.up_time = millis() / 1000;
|
||||
sf.variant.stats.requests = this->requests;
|
||||
sf.variant.stats.requests_history = this->requests_history;
|
||||
sf.variant.stats.heartbeat = this->heartbeat;
|
||||
sf.variant.stats.return_max = this->historyReturnMax;
|
||||
sf.variant.stats.return_window = this->historyReturnWindow;
|
||||
|
||||
LOG_DEBUG("Send S&F Stats");
|
||||
storeForwardModule->sendMessage(to, sf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a received mesh packet, potentially storing it for later forwarding.
|
||||
*
|
||||
* @param mp The received mesh packet.
|
||||
* @return A `ProcessMessage` indicating whether the packet was successfully handled.
|
||||
*/
|
||||
ProcessMessage StoreForwardModule::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
#if defined(ARCH_ESP32) || defined(ARCH_PORTDUINO)
|
||||
if (moduleConfig.store_forward.enabled) {
|
||||
|
||||
if ((mp.decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP) && is_server) {
|
||||
auto &p = mp.decoded;
|
||||
if (isToUs(&mp) && (p.payload.bytes[0] == 'S') && (p.payload.bytes[1] == 'F') && (p.payload.bytes[2] == 0x00)) {
|
||||
LOG_DEBUG("Legacy Request to send");
|
||||
|
||||
// Send the last 60 minutes of messages.
|
||||
if (this->busy || channels.isDefaultChannel(mp.channel)) {
|
||||
sendErrorTextMessage(getFrom(&mp), mp.decoded.want_response);
|
||||
} else {
|
||||
storeForwardModule->historySend(historyReturnWindow * 60, getFrom(&mp));
|
||||
}
|
||||
} else {
|
||||
storeForwardModule->historyAdd(mp);
|
||||
LOG_INFO("S&F stored. Message history contains %u records now", this->packetHistoryTotalCount);
|
||||
}
|
||||
} else if (!isFromUs(&mp) && mp.decoded.portnum == meshtastic_PortNum_STORE_FORWARD_APP) {
|
||||
auto &p = mp.decoded;
|
||||
meshtastic_StoreAndForward scratch;
|
||||
meshtastic_StoreAndForward *decoded = NULL;
|
||||
if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
|
||||
if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_StoreAndForward_msg, &scratch)) {
|
||||
decoded = &scratch;
|
||||
} else {
|
||||
LOG_ERROR("Error decoding proto module!");
|
||||
// if we can't decode it, nobody can process it!
|
||||
return ProcessMessage::STOP;
|
||||
}
|
||||
return handleReceivedProtobuf(mp, decoded) ? ProcessMessage::STOP : ProcessMessage::CONTINUE;
|
||||
}
|
||||
} // all others are irrelevant
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
return ProcessMessage::CONTINUE; // Let others look at this message also if they want
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a received protobuf message for the Store and Forward module.
|
||||
*
|
||||
* @param mp The received MeshPacket to handle.
|
||||
* @param p A pointer to the StoreAndForward object.
|
||||
* @return True if the message was successfully handled, false otherwise.
|
||||
*/
|
||||
bool StoreForwardModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_StoreAndForward *p)
|
||||
{
|
||||
if (!moduleConfig.store_forward.enabled) {
|
||||
// If this module is not enabled in any capacity, don't handle the packet, and allow other modules to consume
|
||||
return false;
|
||||
}
|
||||
|
||||
requests++;
|
||||
|
||||
switch (p->rr) {
|
||||
case meshtastic_StoreAndForward_RequestResponse_CLIENT_ERROR:
|
||||
case meshtastic_StoreAndForward_RequestResponse_CLIENT_ABORT:
|
||||
if (is_server) {
|
||||
// stop sending stuff, the client wants to abort or has another error
|
||||
if ((this->busy) && (this->busyTo == getFrom(&mp))) {
|
||||
LOG_ERROR("Client in ERROR or ABORT requested");
|
||||
this->requestCount = 0;
|
||||
this->busy = false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case meshtastic_StoreAndForward_RequestResponse_CLIENT_HISTORY:
|
||||
if (is_server) {
|
||||
requests_history++;
|
||||
LOG_INFO("Client Request to send HISTORY");
|
||||
// Send the last 60 minutes of messages.
|
||||
if (this->busy || channels.isDefaultChannel(mp.channel)) {
|
||||
sendErrorTextMessage(getFrom(&mp), mp.decoded.want_response);
|
||||
} else {
|
||||
if ((p->which_variant == meshtastic_StoreAndForward_history_tag) && (p->variant.history.window > 0)) {
|
||||
// window is in minutes
|
||||
storeForwardModule->historySend(p->variant.history.window * 60, getFrom(&mp));
|
||||
} else {
|
||||
storeForwardModule->historySend(historyReturnWindow * 60, getFrom(&mp)); // defaults to 4 hours
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case meshtastic_StoreAndForward_RequestResponse_CLIENT_PING:
|
||||
if (is_server) {
|
||||
// respond with a ROUTER PONG
|
||||
storeForwardModule->sendMessage(getFrom(&mp), meshtastic_StoreAndForward_RequestResponse_ROUTER_PONG);
|
||||
}
|
||||
break;
|
||||
|
||||
case meshtastic_StoreAndForward_RequestResponse_CLIENT_PONG:
|
||||
if (is_server) {
|
||||
// NodeDB is already updated
|
||||
}
|
||||
break;
|
||||
|
||||
case meshtastic_StoreAndForward_RequestResponse_CLIENT_STATS:
|
||||
if (is_server) {
|
||||
LOG_INFO("Client Request to send STATS");
|
||||
if (this->busy) {
|
||||
storeForwardModule->sendMessage(getFrom(&mp), meshtastic_StoreAndForward_RequestResponse_ROUTER_BUSY);
|
||||
LOG_INFO("S&F - Busy. Try again shortly");
|
||||
} else {
|
||||
storeForwardModule->statsSend(getFrom(&mp));
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case meshtastic_StoreAndForward_RequestResponse_ROUTER_ERROR:
|
||||
case meshtastic_StoreAndForward_RequestResponse_ROUTER_BUSY:
|
||||
if (is_client) {
|
||||
LOG_DEBUG("StoreAndForward_RequestResponse_ROUTER_BUSY");
|
||||
// retry in messages_saved * packetTimeMax ms
|
||||
retry_delay = millis() + getNumAvailablePackets(this->busyTo, this->last_time) * packetTimeMax *
|
||||
(meshtastic_StoreAndForward_RequestResponse_ROUTER_ERROR ? 2 : 1);
|
||||
}
|
||||
break;
|
||||
|
||||
case meshtastic_StoreAndForward_RequestResponse_ROUTER_PONG:
|
||||
// A router responded, this is equal to receiving a heartbeat
|
||||
case meshtastic_StoreAndForward_RequestResponse_ROUTER_HEARTBEAT:
|
||||
if (is_client) {
|
||||
// register heartbeat and interval
|
||||
if (p->which_variant == meshtastic_StoreAndForward_heartbeat_tag) {
|
||||
heartbeatInterval = p->variant.heartbeat.period;
|
||||
}
|
||||
lastHeartbeat = millis();
|
||||
LOG_INFO("StoreAndForward Heartbeat received");
|
||||
}
|
||||
break;
|
||||
|
||||
case meshtastic_StoreAndForward_RequestResponse_ROUTER_PING:
|
||||
if (is_client) {
|
||||
// respond with a CLIENT PONG
|
||||
storeForwardModule->sendMessage(getFrom(&mp), meshtastic_StoreAndForward_RequestResponse_CLIENT_PONG);
|
||||
}
|
||||
break;
|
||||
|
||||
case meshtastic_StoreAndForward_RequestResponse_ROUTER_STATS:
|
||||
if (is_client) {
|
||||
LOG_DEBUG("Router Response STATS");
|
||||
// These fields only have informational purpose on a client. Fill them to consume later.
|
||||
if (p->which_variant == meshtastic_StoreAndForward_stats_tag) {
|
||||
this->records = p->variant.stats.messages_max;
|
||||
this->requests = p->variant.stats.requests;
|
||||
this->requests_history = p->variant.stats.requests_history;
|
||||
this->heartbeat = p->variant.stats.heartbeat;
|
||||
this->historyReturnMax = p->variant.stats.return_max;
|
||||
this->historyReturnWindow = p->variant.stats.return_window;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case meshtastic_StoreAndForward_RequestResponse_ROUTER_HISTORY:
|
||||
if (is_client) {
|
||||
// These fields only have informational purpose on a client. Fill them to consume later.
|
||||
if (p->which_variant == meshtastic_StoreAndForward_history_tag) {
|
||||
this->historyReturnWindow = p->variant.history.window / 60000;
|
||||
LOG_INFO("Router Response HISTORY - Sending %d messages from last %d minutes",
|
||||
p->variant.history.history_messages, this->historyReturnWindow);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break; // no need to do anything
|
||||
}
|
||||
return false; // RoutingModule sends it to the phone
|
||||
}
|
||||
|
||||
StoreForwardModule::StoreForwardModule()
|
||||
: concurrency::OSThread("StoreForward"),
|
||||
ProtobufModule("StoreForward", meshtastic_PortNum_STORE_FORWARD_APP, &meshtastic_StoreAndForward_msg)
|
||||
{
|
||||
|
||||
#if defined(ARCH_ESP32) || defined(ARCH_PORTDUINO)
|
||||
|
||||
isPromiscuous = true; // Brown chicken brown cow
|
||||
|
||||
if (StoreForward_Dev) {
|
||||
/*
|
||||
Uncomment the preferences below if you want to use the module
|
||||
without having to configure it from the PythonAPI or WebUI.
|
||||
*/
|
||||
|
||||
moduleConfig.store_forward.enabled = 1;
|
||||
}
|
||||
|
||||
if (moduleConfig.store_forward.enabled) {
|
||||
|
||||
// Router
|
||||
if ((config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || moduleConfig.store_forward.is_server)) {
|
||||
LOG_INFO("Init Store & Forward Module in Server mode");
|
||||
if (memGet.getPsramSize() > 0) {
|
||||
if (memGet.getFreePsram() >= 1024 * 1024) {
|
||||
|
||||
// Do the startup here
|
||||
|
||||
// Maximum number of records to return.
|
||||
if (moduleConfig.store_forward.history_return_max)
|
||||
this->historyReturnMax = moduleConfig.store_forward.history_return_max;
|
||||
|
||||
// Maximum time window for records to return (in minutes)
|
||||
if (moduleConfig.store_forward.history_return_window)
|
||||
this->historyReturnWindow = moduleConfig.store_forward.history_return_window;
|
||||
|
||||
// Maximum number of records to store in memory
|
||||
if (moduleConfig.store_forward.records)
|
||||
this->records = moduleConfig.store_forward.records;
|
||||
|
||||
// send heartbeat advertising?
|
||||
if (moduleConfig.store_forward.heartbeat)
|
||||
this->heartbeat = moduleConfig.store_forward.heartbeat;
|
||||
else
|
||||
this->heartbeat = false;
|
||||
|
||||
// Popupate PSRAM with our data structures.
|
||||
this->populatePSRAM();
|
||||
is_server = true;
|
||||
} else {
|
||||
LOG_INFO(".");
|
||||
LOG_INFO("S&F: not enough PSRAM free, Disable");
|
||||
}
|
||||
} else {
|
||||
LOG_INFO("S&F: device doesn't have PSRAM, Disable");
|
||||
}
|
||||
|
||||
// Client
|
||||
} else {
|
||||
is_client = true;
|
||||
LOG_INFO("Init Store & Forward Module in Client mode");
|
||||
}
|
||||
} else {
|
||||
disable();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
#pragma once
|
||||
|
||||
#include "ProtobufModule.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
#include "mesh/generated/meshtastic/storeforward.pb.h"
|
||||
|
||||
#include "configuration.h"
|
||||
#include <Arduino.h>
|
||||
#include <functional>
|
||||
#include <unordered_map>
|
||||
|
||||
struct PacketHistoryStruct {
|
||||
uint32_t time;
|
||||
uint32_t to;
|
||||
uint32_t from;
|
||||
uint32_t id;
|
||||
uint8_t channel;
|
||||
uint32_t reply_id;
|
||||
bool emoji;
|
||||
uint8_t payload[meshtastic_Constants_DATA_PAYLOAD_LEN];
|
||||
pb_size_t payload_size;
|
||||
int32_t rx_rssi;
|
||||
float rx_snr;
|
||||
uint8_t hop_start;
|
||||
uint8_t hop_limit;
|
||||
bool via_mqtt;
|
||||
uint8_t transport_mechanism;
|
||||
};
|
||||
|
||||
class StoreForwardModule : private concurrency::OSThread, public ProtobufModule<meshtastic_StoreAndForward>
|
||||
{
|
||||
bool busy = 0;
|
||||
uint32_t busyTo = 0;
|
||||
char routerMessage[meshtastic_Constants_DATA_PAYLOAD_LEN] = {0};
|
||||
|
||||
PacketHistoryStruct *packetHistory = 0;
|
||||
uint32_t packetHistoryTotalCount = 0;
|
||||
uint32_t last_time = 0;
|
||||
uint32_t requestCount = 0;
|
||||
|
||||
uint32_t packetTimeMax = 5000; // Interval between sending history packets as a server.
|
||||
|
||||
bool is_client = false;
|
||||
bool is_server = false;
|
||||
|
||||
// Unordered_map stores the last request for each nodeNum (`to` field)
|
||||
std::unordered_map<NodeNum, uint32_t> lastRequest;
|
||||
|
||||
public:
|
||||
StoreForwardModule();
|
||||
|
||||
unsigned long lastHeartbeat = 0;
|
||||
uint32_t heartbeatInterval = 900;
|
||||
|
||||
/**
|
||||
Update our local reference of when we last saw that node.
|
||||
@return 0 if we have never seen that node before otherwise return the last time we saw the node.
|
||||
*/
|
||||
void historyAdd(const meshtastic_MeshPacket &mp);
|
||||
void statsSend(uint32_t to);
|
||||
void historySend(uint32_t secAgo, uint32_t to);
|
||||
uint32_t getNumAvailablePackets(NodeNum dest, uint32_t last_time);
|
||||
|
||||
/**
|
||||
* Send our payload into the mesh
|
||||
*/
|
||||
bool sendPayload(NodeNum dest = NODENUM_BROADCAST, uint32_t packetHistory_index = 0);
|
||||
meshtastic_MeshPacket *preparePayload(NodeNum dest, uint32_t packetHistory_index, bool local = false);
|
||||
void sendMessage(NodeNum dest, const meshtastic_StoreAndForward &payload);
|
||||
void sendMessage(NodeNum dest, meshtastic_StoreAndForward_RequestResponse rr);
|
||||
void sendErrorTextMessage(NodeNum dest, bool want_response);
|
||||
meshtastic_MeshPacket *getForPhone();
|
||||
// Returns true if we are configured as server AND we could allocate PSRAM.
|
||||
bool isServer() { return is_server; }
|
||||
|
||||
/*
|
||||
-Override the wantPacket method.
|
||||
*/
|
||||
virtual bool wantPacket(const meshtastic_MeshPacket *p) override
|
||||
{
|
||||
switch (p->decoded.portnum) {
|
||||
case meshtastic_PortNum_TEXT_MESSAGE_APP:
|
||||
case meshtastic_PortNum_STORE_FORWARD_APP:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void populatePSRAM();
|
||||
|
||||
// S&F Defaults
|
||||
uint32_t historyReturnMax = 25; // Return maximum of 25 records by default.
|
||||
uint32_t historyReturnWindow = 240; // Return history of last 4 hours by default.
|
||||
uint32_t records = 0; // Calculated
|
||||
bool heartbeat = false; // No heartbeat.
|
||||
|
||||
// stats
|
||||
uint32_t requests = 0; // Number of times any client sent a request to the S&F.
|
||||
uint32_t requests_history = 0; // Number of times the history was requested.
|
||||
|
||||
uint32_t retry_delay = 0; // If server is busy, retry after this delay (in ms).
|
||||
|
||||
protected:
|
||||
virtual int32_t runOnce() 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;
|
||||
virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_StoreAndForward *p);
|
||||
};
|
||||
|
||||
extern StoreForwardModule *storeForwardModule;
|
||||
@@ -0,0 +1,123 @@
|
||||
#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->showSimpleBanner(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->showSimpleBanner("Bluetooth OFF\nRebooting", 3000));
|
||||
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 2000;
|
||||
} else {
|
||||
IF_SCREEN(screen->showSimpleBanner("Bluetooth ON\nRebooting", 3000));
|
||||
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
|
||||
}
|
||||
#else
|
||||
if (!config.bluetooth.enabled) {
|
||||
disableBluetooth();
|
||||
IF_SCREEN(screen->showSimpleBanner("Bluetooth OFF", 3000));
|
||||
} else {
|
||||
IF_SCREEN(screen->showSimpleBanner("Bluetooth ON\nRebooting", 3000));
|
||||
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
|
||||
}
|
||||
#endif
|
||||
return 0;
|
||||
case INPUT_BROKER_MSG_REBOOT:
|
||||
IF_SCREEN(screen->showSimpleBanner("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");
|
||||
if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED &&
|
||||
config.position.fixed_position == false) {
|
||||
nodeDB->clearLocalPosition();
|
||||
nodeDB->saveToDisk();
|
||||
}
|
||||
gps->toggleGpsMode();
|
||||
const char *msg =
|
||||
(config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) ? "GPS Enabled" : "GPS Disabled";
|
||||
IF_SCREEN(screen->forceDisplay(); screen->showSimpleBanner(msg, 3000);)
|
||||
}
|
||||
#endif
|
||||
return true;
|
||||
// Mesh ping
|
||||
case INPUT_BROKER_SEND_PING:
|
||||
service->refreshLocalMeshNode();
|
||||
if (service->trySendPosition(NODENUM_BROADCAST, true)) {
|
||||
IF_SCREEN(screen->showSimpleBanner("Position\nSent", 3000));
|
||||
} else {
|
||||
IF_SCREEN(screen->showSimpleBanner("Node Info\nSent", 3000));
|
||||
}
|
||||
return true;
|
||||
// Power control
|
||||
case INPUT_BROKER_SHUTDOWN:
|
||||
shutdownAtMsec = millis();
|
||||
return true;
|
||||
|
||||
default:
|
||||
// No other input events handled here
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,236 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include("Adafruit_PM25AQI.h")
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "AirQualityTelemetry.h"
|
||||
#include "Default.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "RTC.h"
|
||||
#include "Router.h"
|
||||
#include "detect/ScanI2CTwoWire.h"
|
||||
#include "main.h"
|
||||
#include <Throttle.h>
|
||||
|
||||
#ifndef PMSA003I_WARMUP_MS
|
||||
// from the PMSA003I datasheet:
|
||||
// "Stable data should be got at least 30 seconds after the sensor wakeup
|
||||
// from the sleep mode because of the fan’s performance."
|
||||
#define PMSA003I_WARMUP_MS 30000
|
||||
#endif
|
||||
|
||||
int32_t AirQualityTelemetryModule::runOnce()
|
||||
{
|
||||
/*
|
||||
Uncomment the preferences below if you want to use the module
|
||||
without having to configure it from the PythonAPI or WebUI.
|
||||
*/
|
||||
|
||||
// moduleConfig.telemetry.air_quality_enabled = 1;
|
||||
|
||||
if (!(moduleConfig.telemetry.air_quality_enabled)) {
|
||||
// If this module is not enabled, and the user doesn't want the display screen don't waste any OSThread time on it
|
||||
return disable();
|
||||
}
|
||||
|
||||
if (firstTime) {
|
||||
// This is the first time the OSThread library has called this function, so do some setup
|
||||
firstTime = false;
|
||||
|
||||
if (moduleConfig.telemetry.air_quality_enabled) {
|
||||
LOG_INFO("Air quality Telemetry: init");
|
||||
|
||||
#ifdef PMSA003I_ENABLE_PIN
|
||||
// put the sensor to sleep on startup
|
||||
pinMode(PMSA003I_ENABLE_PIN, OUTPUT);
|
||||
digitalWrite(PMSA003I_ENABLE_PIN, LOW);
|
||||
#endif /* PMSA003I_ENABLE_PIN */
|
||||
|
||||
if (!aqi.begin_I2C()) {
|
||||
#ifndef I2C_NO_RESCAN
|
||||
LOG_WARN("Could not establish i2c connection to AQI sensor. Rescan");
|
||||
// rescan for late arriving sensors. AQI Module starts about 10 seconds into the boot so this is plenty.
|
||||
uint8_t i2caddr_scan[] = {PMSA0031_ADDR};
|
||||
uint8_t i2caddr_asize = 1;
|
||||
auto i2cScanner = std::unique_ptr<ScanI2CTwoWire>(new ScanI2CTwoWire());
|
||||
#if defined(I2C_SDA1)
|
||||
i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1, i2caddr_scan, i2caddr_asize);
|
||||
#endif
|
||||
i2cScanner->scanPort(ScanI2C::I2CPort::WIRE, i2caddr_scan, i2caddr_asize);
|
||||
auto found = i2cScanner->find(ScanI2C::DeviceType::PMSA0031);
|
||||
if (found.type != ScanI2C::DeviceType::NONE) {
|
||||
nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first = found.address.address;
|
||||
nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].second =
|
||||
i2cScanner->fetchI2CBus(found.address);
|
||||
return setStartDelay();
|
||||
}
|
||||
#endif
|
||||
return disable();
|
||||
}
|
||||
return setStartDelay();
|
||||
}
|
||||
return disable();
|
||||
} else {
|
||||
// if we somehow got to a second run of this module with measurement disabled, then just wait forever
|
||||
if (!moduleConfig.telemetry.air_quality_enabled)
|
||||
return disable();
|
||||
|
||||
switch (state) {
|
||||
#ifdef PMSA003I_ENABLE_PIN
|
||||
case State::IDLE:
|
||||
// sensor is in standby; fire it up and sleep
|
||||
LOG_DEBUG("runOnce(): state = idle");
|
||||
digitalWrite(PMSA003I_ENABLE_PIN, HIGH);
|
||||
state = State::ACTIVE;
|
||||
|
||||
return PMSA003I_WARMUP_MS;
|
||||
#endif /* PMSA003I_ENABLE_PIN */
|
||||
case State::ACTIVE:
|
||||
// sensor is already warmed up; grab telemetry and send it
|
||||
LOG_DEBUG("runOnce(): state = active");
|
||||
|
||||
if (((lastSentToMesh == 0) ||
|
||||
!Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled(
|
||||
moduleConfig.telemetry.air_quality_interval,
|
||||
default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
|
||||
airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) &&
|
||||
airTime->isTxAllowedAirUtil()) {
|
||||
sendTelemetry();
|
||||
lastSentToMesh = millis();
|
||||
} else if (service->isToPhoneQueueEmpty()) {
|
||||
// Just send to phone when it's not our time to send to mesh yet
|
||||
// Only send while queue is empty (phone assumed connected)
|
||||
sendTelemetry(NODENUM_BROADCAST, true);
|
||||
}
|
||||
|
||||
#ifdef PMSA003I_ENABLE_PIN
|
||||
// put sensor back to sleep
|
||||
digitalWrite(PMSA003I_ENABLE_PIN, LOW);
|
||||
state = State::IDLE;
|
||||
#endif /* PMSA003I_ENABLE_PIN */
|
||||
|
||||
return sendToPhoneIntervalMs;
|
||||
default:
|
||||
return disable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t)
|
||||
{
|
||||
if (t->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) {
|
||||
#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
|
||||
const char *sender = getSenderShortName(mp);
|
||||
|
||||
LOG_INFO("(Received from %s): pm10_standard=%i, pm25_standard=%i, pm100_standard=%i", sender,
|
||||
t->variant.air_quality_metrics.pm10_standard, t->variant.air_quality_metrics.pm25_standard,
|
||||
t->variant.air_quality_metrics.pm100_standard);
|
||||
|
||||
LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i",
|
||||
t->variant.air_quality_metrics.pm10_environmental, t->variant.air_quality_metrics.pm25_environmental,
|
||||
t->variant.air_quality_metrics.pm100_environmental);
|
||||
#endif
|
||||
// release previous packet before occupying a new spot
|
||||
if (lastMeasurementPacket != nullptr)
|
||||
packetPool.release(lastMeasurementPacket);
|
||||
|
||||
lastMeasurementPacket = packetPool.allocCopy(mp);
|
||||
}
|
||||
|
||||
return false; // Let others look at this message also if they want
|
||||
}
|
||||
|
||||
bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m)
|
||||
{
|
||||
if (!aqi.read(&data)) {
|
||||
LOG_WARN("Skip send measurements. Could not read AQIn");
|
||||
return false;
|
||||
}
|
||||
|
||||
m->time = getTime();
|
||||
m->which_variant = meshtastic_Telemetry_air_quality_metrics_tag;
|
||||
m->variant.air_quality_metrics.has_pm10_standard = true;
|
||||
m->variant.air_quality_metrics.pm10_standard = data.pm10_standard;
|
||||
m->variant.air_quality_metrics.has_pm25_standard = true;
|
||||
m->variant.air_quality_metrics.pm25_standard = data.pm25_standard;
|
||||
m->variant.air_quality_metrics.has_pm100_standard = true;
|
||||
m->variant.air_quality_metrics.pm100_standard = data.pm100_standard;
|
||||
|
||||
m->variant.air_quality_metrics.has_pm10_environmental = true;
|
||||
m->variant.air_quality_metrics.pm10_environmental = data.pm10_env;
|
||||
m->variant.air_quality_metrics.has_pm25_environmental = true;
|
||||
m->variant.air_quality_metrics.pm25_environmental = data.pm25_env;
|
||||
m->variant.air_quality_metrics.has_pm100_environmental = true;
|
||||
m->variant.air_quality_metrics.pm100_environmental = data.pm100_env;
|
||||
|
||||
LOG_INFO("Send: PM1.0(Standard)=%i, PM2.5(Standard)=%i, PM10.0(Standard)=%i", m->variant.air_quality_metrics.pm10_standard,
|
||||
m->variant.air_quality_metrics.pm25_standard, m->variant.air_quality_metrics.pm100_standard);
|
||||
|
||||
LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i",
|
||||
m->variant.air_quality_metrics.pm10_environmental, m->variant.air_quality_metrics.pm25_environmental,
|
||||
m->variant.air_quality_metrics.pm100_environmental);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
meshtastic_MeshPacket *AirQualityTelemetryModule::allocReply()
|
||||
{
|
||||
if (currentRequest) {
|
||||
auto req = *currentRequest;
|
||||
const auto &p = req.decoded;
|
||||
meshtastic_Telemetry scratch;
|
||||
meshtastic_Telemetry *decoded = NULL;
|
||||
memset(&scratch, 0, sizeof(scratch));
|
||||
if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &scratch)) {
|
||||
decoded = &scratch;
|
||||
} else {
|
||||
LOG_ERROR("Error decoding AirQualityTelemetry module!");
|
||||
return NULL;
|
||||
}
|
||||
// Check for a request for air quality metrics
|
||||
if (decoded->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) {
|
||||
meshtastic_Telemetry m = meshtastic_Telemetry_init_zero;
|
||||
if (getAirQualityTelemetry(&m)) {
|
||||
LOG_INFO("Air quality telemetry reply to request");
|
||||
return allocDataProtobuf(m);
|
||||
} else {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
|
||||
{
|
||||
meshtastic_Telemetry m = meshtastic_Telemetry_init_zero;
|
||||
if (getAirQualityTelemetry(&m)) {
|
||||
meshtastic_MeshPacket *p = allocDataProtobuf(m);
|
||||
p->to = dest;
|
||||
p->decoded.want_response = false;
|
||||
if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR)
|
||||
p->priority = meshtastic_MeshPacket_Priority_RELIABLE;
|
||||
else
|
||||
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||
|
||||
// release previous packet before occupying a new spot
|
||||
if (lastMeasurementPacket != nullptr)
|
||||
packetPool.release(lastMeasurementPacket);
|
||||
|
||||
lastMeasurementPacket = packetPool.allocCopy(*p);
|
||||
if (phoneOnly) {
|
||||
LOG_INFO("Send packet to phone");
|
||||
service->sendToPhone(p);
|
||||
} else {
|
||||
LOG_INFO("Send packet to mesh");
|
||||
service->sendToMesh(p, RX_SRC_LOCAL, true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,67 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include("Adafruit_PM25AQI.h")
|
||||
|
||||
#pragma once
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "Adafruit_PM25AQI.h"
|
||||
#include "NodeDB.h"
|
||||
#include "ProtobufModule.h"
|
||||
|
||||
class AirQualityTelemetryModule : private concurrency::OSThread, public ProtobufModule<meshtastic_Telemetry>
|
||||
{
|
||||
CallbackObserver<AirQualityTelemetryModule, const meshtastic::Status *> nodeStatusObserver =
|
||||
CallbackObserver<AirQualityTelemetryModule, const meshtastic::Status *>(this,
|
||||
&AirQualityTelemetryModule::handleStatusUpdate);
|
||||
|
||||
public:
|
||||
AirQualityTelemetryModule()
|
||||
: concurrency::OSThread("AirQualityTelemetry"),
|
||||
ProtobufModule("AirQualityTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg)
|
||||
{
|
||||
lastMeasurementPacket = nullptr;
|
||||
setIntervalFromNow(10 * 1000);
|
||||
aqi = Adafruit_PM25AQI();
|
||||
nodeStatusObserver.observe(&nodeStatus->onNewStatus);
|
||||
|
||||
#ifdef PMSA003I_ENABLE_PIN
|
||||
// the PMSA003I sensor uses about 300mW on its own; support powering it off when it's not actively taking
|
||||
// a reading
|
||||
state = State::IDLE;
|
||||
#else
|
||||
state = State::ACTIVE;
|
||||
#endif
|
||||
}
|
||||
|
||||
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_Telemetry *p) override;
|
||||
virtual int32_t runOnce() override;
|
||||
/** Called to get current Air Quality data
|
||||
@return true if it contains valid data
|
||||
*/
|
||||
bool getAirQualityTelemetry(meshtastic_Telemetry *m);
|
||||
virtual meshtastic_MeshPacket *allocReply() override;
|
||||
/**
|
||||
* Send our Telemetry into the mesh
|
||||
*/
|
||||
bool sendTelemetry(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false);
|
||||
|
||||
private:
|
||||
enum State {
|
||||
IDLE = 0,
|
||||
ACTIVE = 1,
|
||||
};
|
||||
|
||||
State state;
|
||||
Adafruit_PM25AQI aqi;
|
||||
PM25_AQI_Data data = {0};
|
||||
bool firstTime = true;
|
||||
meshtastic_MeshPacket *lastMeasurementPacket;
|
||||
uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute
|
||||
uint32_t lastSentToMesh = 0;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,190 @@
|
||||
#include "DeviceTelemetry.h"
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "Default.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "RTC.h"
|
||||
#include "RadioLibInterface.h"
|
||||
#include "Router.h"
|
||||
#include "configuration.h"
|
||||
#include "main.h"
|
||||
#include "memGet.h"
|
||||
#include <OLEDDisplay.h>
|
||||
#include <OLEDDisplayUi.h>
|
||||
#include <meshUtils.h>
|
||||
|
||||
#define MAGIC_USB_BATTERY_LEVEL 101
|
||||
|
||||
int32_t DeviceTelemetryModule::runOnce()
|
||||
{
|
||||
refreshUptime();
|
||||
bool isImpoliteRole =
|
||||
IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_SENSOR, meshtastic_Config_DeviceConfig_Role_ROUTER);
|
||||
if (((lastSentToMesh == 0) ||
|
||||
((uptimeLastMs - lastSentToMesh) >=
|
||||
Default::getConfiguredOrDefaultMsScaled(moduleConfig.telemetry.device_update_interval,
|
||||
default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
|
||||
airTime->isTxAllowedChannelUtil(!isImpoliteRole) && airTime->isTxAllowedAirUtil() &&
|
||||
config.device.role != meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN &&
|
||||
moduleConfig.telemetry.device_telemetry_enabled) {
|
||||
sendTelemetry();
|
||||
lastSentToMesh = uptimeLastMs;
|
||||
} else if (service->isToPhoneQueueEmpty()) {
|
||||
// Just send to phone when it's not our time to send to mesh yet
|
||||
// Only send while queue is empty (phone assumed connected)
|
||||
sendTelemetry(NODENUM_BROADCAST, true);
|
||||
if (lastSentStatsToPhone == 0 || (uptimeLastMs - lastSentStatsToPhone) >= sendStatsToPhoneIntervalMs) {
|
||||
sendLocalStatsToPhone();
|
||||
lastSentStatsToPhone = uptimeLastMs;
|
||||
}
|
||||
}
|
||||
return sendToPhoneIntervalMs;
|
||||
}
|
||||
|
||||
bool DeviceTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t)
|
||||
{
|
||||
if (t->which_variant == meshtastic_Telemetry_device_metrics_tag) {
|
||||
#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
|
||||
const char *sender = getSenderShortName(mp);
|
||||
|
||||
LOG_INFO("(Received from %s): air_util_tx=%f, channel_utilization=%f, battery_level=%i, voltage=%f", sender,
|
||||
t->variant.device_metrics.air_util_tx, t->variant.device_metrics.channel_utilization,
|
||||
t->variant.device_metrics.battery_level, t->variant.device_metrics.voltage);
|
||||
#endif
|
||||
nodeDB->updateTelemetry(getFrom(&mp), *t, RX_SRC_RADIO);
|
||||
}
|
||||
return false; // Let others look at this message also if they want
|
||||
}
|
||||
|
||||
meshtastic_MeshPacket *DeviceTelemetryModule::allocReply()
|
||||
{
|
||||
if (currentRequest) {
|
||||
auto req = *currentRequest;
|
||||
const auto &p = req.decoded;
|
||||
meshtastic_Telemetry scratch;
|
||||
meshtastic_Telemetry *decoded = NULL;
|
||||
memset(&scratch, 0, sizeof(scratch));
|
||||
if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &scratch)) {
|
||||
decoded = &scratch;
|
||||
} else {
|
||||
LOG_ERROR("Error decoding DeviceTelemetry module!");
|
||||
return NULL;
|
||||
}
|
||||
// Check for a request for device metrics
|
||||
if (decoded->which_variant == meshtastic_Telemetry_device_metrics_tag) {
|
||||
LOG_INFO("Device telemetry reply to request");
|
||||
return allocDataProtobuf(getDeviceTelemetry());
|
||||
} else if (decoded->which_variant == meshtastic_Telemetry_local_stats_tag) {
|
||||
LOG_INFO("Device telemetry reply w/ LocalStats to request");
|
||||
return allocDataProtobuf(getLocalStatsTelemetry());
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
meshtastic_Telemetry DeviceTelemetryModule::getDeviceTelemetry()
|
||||
{
|
||||
meshtastic_Telemetry t = meshtastic_Telemetry_init_zero;
|
||||
t.which_variant = meshtastic_Telemetry_device_metrics_tag;
|
||||
t.time = getTime();
|
||||
t.variant.device_metrics = meshtastic_DeviceMetrics_init_zero;
|
||||
t.variant.device_metrics.has_air_util_tx = true;
|
||||
t.variant.device_metrics.has_battery_level = true;
|
||||
t.variant.device_metrics.has_channel_utilization = true;
|
||||
t.variant.device_metrics.has_voltage = true;
|
||||
t.variant.device_metrics.has_uptime_seconds = true;
|
||||
|
||||
t.variant.device_metrics.air_util_tx = airTime->utilizationTXPercent();
|
||||
t.variant.device_metrics.battery_level = (!powerStatus->getHasBattery() || powerStatus->getIsCharging())
|
||||
? MAGIC_USB_BATTERY_LEVEL
|
||||
: powerStatus->getBatteryChargePercent();
|
||||
t.variant.device_metrics.channel_utilization = airTime->channelUtilizationPercent();
|
||||
t.variant.device_metrics.voltage = powerStatus->getBatteryVoltageMv() / 1000.0;
|
||||
t.variant.device_metrics.uptime_seconds = getUptimeSeconds();
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
meshtastic_Telemetry DeviceTelemetryModule::getLocalStatsTelemetry()
|
||||
{
|
||||
meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero;
|
||||
telemetry.which_variant = meshtastic_Telemetry_local_stats_tag;
|
||||
telemetry.variant.local_stats = meshtastic_LocalStats_init_zero;
|
||||
telemetry.time = getTime();
|
||||
telemetry.variant.local_stats.uptime_seconds = getUptimeSeconds();
|
||||
telemetry.variant.local_stats.channel_utilization = airTime->channelUtilizationPercent();
|
||||
telemetry.variant.local_stats.air_util_tx = airTime->utilizationTXPercent();
|
||||
telemetry.variant.local_stats.num_online_nodes = numOnlineNodes;
|
||||
telemetry.variant.local_stats.num_total_nodes = nodeDB->getNumMeshNodes();
|
||||
if (RadioLibInterface::instance) {
|
||||
telemetry.variant.local_stats.num_packets_tx = RadioLibInterface::instance->txGood;
|
||||
telemetry.variant.local_stats.num_packets_rx = RadioLibInterface::instance->rxGood + RadioLibInterface::instance->rxBad;
|
||||
telemetry.variant.local_stats.num_packets_rx_bad = RadioLibInterface::instance->rxBad;
|
||||
telemetry.variant.local_stats.num_tx_relay = RadioLibInterface::instance->txRelay;
|
||||
telemetry.variant.local_stats.num_tx_dropped = RadioLibInterface::instance->txDrop;
|
||||
}
|
||||
#ifdef ARCH_PORTDUINO
|
||||
if (SimRadio::instance) {
|
||||
telemetry.variant.local_stats.num_packets_tx = SimRadio::instance->txGood;
|
||||
telemetry.variant.local_stats.num_packets_rx = SimRadio::instance->rxGood + SimRadio::instance->rxBad;
|
||||
telemetry.variant.local_stats.num_packets_rx_bad = SimRadio::instance->rxBad;
|
||||
telemetry.variant.local_stats.num_tx_relay = SimRadio::instance->txRelay;
|
||||
telemetry.variant.local_stats.num_tx_dropped = SimRadio::instance->txDrop;
|
||||
}
|
||||
#else
|
||||
telemetry.variant.local_stats.heap_total_bytes = memGet.getHeapSize();
|
||||
telemetry.variant.local_stats.heap_free_bytes = memGet.getFreeHeap();
|
||||
#endif
|
||||
if (router) {
|
||||
telemetry.variant.local_stats.num_rx_dupe = router->rxDupe;
|
||||
telemetry.variant.local_stats.num_tx_relay_canceled = router->txRelayCanceled;
|
||||
}
|
||||
|
||||
LOG_INFO("Sending local stats: uptime=%i, channel_utilization=%f, air_util_tx=%f, num_online_nodes=%i, num_total_nodes=%i",
|
||||
telemetry.variant.local_stats.uptime_seconds, telemetry.variant.local_stats.channel_utilization,
|
||||
telemetry.variant.local_stats.air_util_tx, telemetry.variant.local_stats.num_online_nodes,
|
||||
telemetry.variant.local_stats.num_total_nodes);
|
||||
|
||||
LOG_INFO("num_packets_tx=%i, num_packets_rx=%i, num_packets_rx_bad=%i", telemetry.variant.local_stats.num_packets_tx,
|
||||
telemetry.variant.local_stats.num_packets_rx, telemetry.variant.local_stats.num_packets_rx_bad);
|
||||
|
||||
return telemetry;
|
||||
}
|
||||
|
||||
void DeviceTelemetryModule::sendLocalStatsToPhone()
|
||||
{
|
||||
meshtastic_MeshPacket *p = allocDataProtobuf(getLocalStatsTelemetry());
|
||||
p->to = NODENUM_BROADCAST;
|
||||
p->decoded.want_response = false;
|
||||
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||
|
||||
service->sendToPhone(p);
|
||||
}
|
||||
|
||||
bool DeviceTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
|
||||
{
|
||||
meshtastic_Telemetry telemetry = getDeviceTelemetry();
|
||||
LOG_INFO("Send: air_util_tx=%f, channel_utilization=%f, battery_level=%i, voltage=%f, uptime=%i",
|
||||
telemetry.variant.device_metrics.air_util_tx, telemetry.variant.device_metrics.channel_utilization,
|
||||
telemetry.variant.device_metrics.battery_level, telemetry.variant.device_metrics.voltage,
|
||||
telemetry.variant.device_metrics.uptime_seconds);
|
||||
|
||||
DEBUG_HEAP_BEFORE;
|
||||
meshtastic_MeshPacket *p = allocDataProtobuf(telemetry);
|
||||
DEBUG_HEAP_AFTER("DeviceTelemetryModule::sendTelemetry", p);
|
||||
|
||||
p->to = dest;
|
||||
p->decoded.want_response = false;
|
||||
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||
|
||||
nodeDB->updateTelemetry(nodeDB->getNodeNum(), telemetry, RX_SRC_LOCAL);
|
||||
if (phoneOnly) {
|
||||
LOG_INFO("Send packet to phone");
|
||||
service->sendToPhone(p);
|
||||
} else {
|
||||
LOG_INFO("Send packet to mesh");
|
||||
service->sendToMesh(p, RX_SRC_LOCAL, true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
#pragma once
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "NodeDB.h"
|
||||
#include "ProtobufModule.h"
|
||||
#include <OLEDDisplay.h>
|
||||
#include <OLEDDisplayUi.h>
|
||||
|
||||
class DeviceTelemetryModule : private concurrency::OSThread, public ProtobufModule<meshtastic_Telemetry>
|
||||
{
|
||||
CallbackObserver<DeviceTelemetryModule, const meshtastic::Status *> nodeStatusObserver =
|
||||
CallbackObserver<DeviceTelemetryModule, const meshtastic::Status *>(this, &DeviceTelemetryModule::handleStatusUpdate);
|
||||
|
||||
public:
|
||||
DeviceTelemetryModule()
|
||||
: concurrency::OSThread("DeviceTelemetry"),
|
||||
ProtobufModule("DeviceTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg)
|
||||
{
|
||||
uptimeWrapCount = 0;
|
||||
uptimeLastMs = millis();
|
||||
nodeStatusObserver.observe(&nodeStatus->onNewStatus);
|
||||
setIntervalFromNow(setStartDelay()); // Wait until NodeInfo is sent
|
||||
}
|
||||
virtual bool wantUIFrame() { return false; }
|
||||
|
||||
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_Telemetry *p) override;
|
||||
virtual meshtastic_MeshPacket *allocReply() override;
|
||||
virtual int32_t runOnce() override;
|
||||
/**
|
||||
* Send our Telemetry into the mesh
|
||||
*/
|
||||
bool sendTelemetry(NodeNum dest = NODENUM_BROADCAST, bool phoneOnly = false);
|
||||
|
||||
/**
|
||||
* Get the uptime in seconds
|
||||
* Loses some accuracy after 49 days, but that's fine
|
||||
*/
|
||||
uint32_t getUptimeSeconds() { return (0xFFFFFFFF / 1000) * uptimeWrapCount + (uptimeLastMs / 1000); }
|
||||
|
||||
private:
|
||||
meshtastic_Telemetry getDeviceTelemetry();
|
||||
meshtastic_Telemetry getLocalStatsTelemetry();
|
||||
|
||||
void sendLocalStatsToPhone();
|
||||
uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute
|
||||
uint32_t sendStatsToPhoneIntervalMs = 15 * SECONDS_IN_MINUTE * 1000; // Send stats to phone every 15 minutes
|
||||
uint32_t lastSentStatsToPhone = 0;
|
||||
uint32_t lastSentToMesh = 0;
|
||||
|
||||
void refreshUptime()
|
||||
{
|
||||
auto now = millis();
|
||||
// If we wrapped around (~49 days), increment the wrap count
|
||||
if (now < uptimeLastMs)
|
||||
uptimeWrapCount++;
|
||||
|
||||
uptimeLastMs = now;
|
||||
}
|
||||
|
||||
uint32_t uptimeWrapCount;
|
||||
uint32_t uptimeLastMs;
|
||||
};
|
||||
@@ -0,0 +1,722 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "Default.h"
|
||||
#include "EnvironmentTelemetry.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PowerFSM.h"
|
||||
#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>
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL
|
||||
|
||||
// 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, bool force_no_invert,
|
||||
bool show_date);
|
||||
}
|
||||
#if __has_include(<Adafruit_AHTX0.h>)
|
||||
#include "Sensor/AHT10.h"
|
||||
#endif
|
||||
|
||||
#if __has_include(<Adafruit_BME280.h>)
|
||||
#include "Sensor/BME280Sensor.h"
|
||||
#endif
|
||||
|
||||
#if __has_include(<Adafruit_BMP085.h>)
|
||||
#include "Sensor/BMP085Sensor.h"
|
||||
#endif
|
||||
|
||||
#if __has_include(<Adafruit_BMP280.h>)
|
||||
#include "Sensor/BMP280Sensor.h"
|
||||
#endif
|
||||
|
||||
#if __has_include(<Adafruit_LTR390.h>)
|
||||
#include "Sensor/LTR390UVSensor.h"
|
||||
#endif
|
||||
|
||||
#if __has_include(<bsec2.h>)
|
||||
#include "Sensor/BME680Sensor.h"
|
||||
#endif
|
||||
|
||||
#if __has_include(<Adafruit_DPS310.h>)
|
||||
#include "Sensor/DPS310Sensor.h"
|
||||
#endif
|
||||
|
||||
#if __has_include(<Adafruit_MCP9808.h>)
|
||||
#include "Sensor/MCP9808Sensor.h"
|
||||
#endif
|
||||
|
||||
#if __has_include(<Adafruit_SHT31.h>)
|
||||
#include "Sensor/SHT31Sensor.h"
|
||||
#endif
|
||||
|
||||
#if __has_include(<Adafruit_LPS2X.h>)
|
||||
#include "Sensor/LPS22HBSensor.h"
|
||||
#endif
|
||||
|
||||
#if __has_include(<Adafruit_SHTC3.h>)
|
||||
#include "Sensor/SHTC3Sensor.h"
|
||||
#endif
|
||||
|
||||
#if __has_include("RAK12035_SoilMoisture.h") && defined(RAK_4631) && RAK_4631 == 1
|
||||
#include "Sensor/RAK12035Sensor.h"
|
||||
#endif
|
||||
|
||||
#if __has_include(<Adafruit_VEML7700.h>)
|
||||
#include "Sensor/VEML7700Sensor.h"
|
||||
#endif
|
||||
|
||||
#if __has_include(<Adafruit_TSL2591.h>)
|
||||
#include "Sensor/TSL2591Sensor.h"
|
||||
#endif
|
||||
|
||||
#if __has_include(<ClosedCube_OPT3001.h>)
|
||||
#include "Sensor/OPT3001Sensor.h"
|
||||
#endif
|
||||
|
||||
#if __has_include(<Adafruit_SHT4x.h>)
|
||||
#include "Sensor/SHT4XSensor.h"
|
||||
#endif
|
||||
|
||||
#if __has_include(<SparkFun_MLX90632_Arduino_Library.h>)
|
||||
#include "Sensor/MLX90632Sensor.h"
|
||||
#endif
|
||||
|
||||
#if __has_include(<DFRobot_LarkWeatherStation.h>)
|
||||
#include "Sensor/DFRobotLarkSensor.h"
|
||||
#endif
|
||||
|
||||
#if __has_include(<DFRobot_RainfallSensor.h>)
|
||||
#include "Sensor/DFRobotGravitySensor.h"
|
||||
#endif
|
||||
|
||||
#if __has_include(<SparkFun_Qwiic_Scale_NAU7802_Arduino_Library.h>)
|
||||
#include "Sensor/NAU7802Sensor.h"
|
||||
#endif
|
||||
|
||||
#if __has_include(<Adafruit_BMP3XX.h>)
|
||||
#include "Sensor/BMP3XXSensor.h"
|
||||
#endif
|
||||
|
||||
#if __has_include(<Adafruit_PCT2075.h>)
|
||||
#include "Sensor/PCT2075Sensor.h"
|
||||
#endif
|
||||
|
||||
#endif
|
||||
#ifdef T1000X_SENSOR_EN
|
||||
#include "Sensor/T1000xSensor.h"
|
||||
#endif
|
||||
|
||||
#ifdef SENSECAP_INDICATOR
|
||||
#include "Sensor/IndicatorSensor.h"
|
||||
#endif
|
||||
|
||||
#if __has_include(<Adafruit_TSL2561_U.h>)
|
||||
#include "Sensor/TSL2561Sensor.h"
|
||||
#endif
|
||||
|
||||
#if __has_include(<BH1750_WE.h>)
|
||||
#include "Sensor/BH1750Sensor.h"
|
||||
#endif
|
||||
|
||||
#define FAILED_STATE_SENSOR_READ_MULTIPLIER 10
|
||||
#define DISPLAY_RECEIVEID_MEASUREMENTS_ON_SCREEN true
|
||||
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include <Throttle.h>
|
||||
|
||||
#include <forward_list>
|
||||
|
||||
static std::forward_list<TelemetrySensor *> sensors;
|
||||
|
||||
template <typename T> void addSensor(ScanI2C *i2cScanner, ScanI2C::DeviceType type)
|
||||
{
|
||||
ScanI2C::FoundDevice dev = i2cScanner->find(type);
|
||||
if (dev.type != ScanI2C::DeviceType::NONE || type == ScanI2C::DeviceType::NONE) {
|
||||
TelemetrySensor *sensor = new T();
|
||||
#if WIRE_INTERFACES_COUNT > 1
|
||||
TwoWire *bus = ScanI2CTwoWire::fetchI2CBus(dev.address);
|
||||
if (dev.address.port != ScanI2C::I2CPort::WIRE1 && sensor->onlyWire1()) {
|
||||
// This sensor only works on Wire (Wire1 is not supported)
|
||||
delete sensor;
|
||||
return;
|
||||
}
|
||||
#else
|
||||
TwoWire *bus = &Wire;
|
||||
#endif
|
||||
if (sensor->initDevice(bus, &dev)) {
|
||||
sensors.push_front(sensor);
|
||||
return;
|
||||
}
|
||||
// destroy sensor
|
||||
delete sensor;
|
||||
}
|
||||
}
|
||||
|
||||
void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
|
||||
{
|
||||
if (!moduleConfig.telemetry.environment_measurement_enabled && !ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE) {
|
||||
return;
|
||||
}
|
||||
LOG_INFO("Environment Telemetry adding I2C devices...");
|
||||
|
||||
// order by priority of metrics/values (low top, high bottom)
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
||||
#ifdef T1000X_SENSOR_EN
|
||||
// Not a real I2C device
|
||||
addSensor<T1000xSensor>(i2cScanner, ScanI2C::DeviceType::NONE);
|
||||
#else
|
||||
#ifdef SENSECAP_INDICATOR
|
||||
// Not a real I2C device, uses UART
|
||||
addSensor<IndicatorSensor>(i2cScanner, ScanI2C::DeviceType::NONE);
|
||||
#endif
|
||||
addSensor<RCWL9620Sensor>(i2cScanner, ScanI2C::DeviceType::RCWL9620);
|
||||
addSensor<CGRadSensSensor>(i2cScanner, ScanI2C::DeviceType::CGRADSENS);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL
|
||||
#if __has_include(<DFRobot_LarkWeatherStation.h>)
|
||||
addSensor<DFRobotLarkSensor>(i2cScanner, ScanI2C::DeviceType::DFROBOT_LARK);
|
||||
#endif
|
||||
#if __has_include(<DFRobot_RainfallSensor.h>)
|
||||
addSensor<DFRobotGravitySensor>(i2cScanner, ScanI2C::DeviceType::DFROBOT_RAIN);
|
||||
#endif
|
||||
#if __has_include(<Adafruit_AHTX0.h>)
|
||||
addSensor<AHT10Sensor>(i2cScanner, ScanI2C::DeviceType::AHT10);
|
||||
#endif
|
||||
#if __has_include(<Adafruit_BMP085.h>)
|
||||
addSensor<BMP085Sensor>(i2cScanner, ScanI2C::DeviceType::BMP_085);
|
||||
#endif
|
||||
#if __has_include(<Adafruit_BME280.h>)
|
||||
addSensor<BME280Sensor>(i2cScanner, ScanI2C::DeviceType::BME_280);
|
||||
#endif
|
||||
#if __has_include(<Adafruit_LTR390.h>)
|
||||
addSensor<LTR390UVSensor>(i2cScanner, ScanI2C::DeviceType::LTR390UV);
|
||||
#endif
|
||||
#if __has_include(<bsec2.h>)
|
||||
addSensor<BME680Sensor>(i2cScanner, ScanI2C::DeviceType::BME_680);
|
||||
#endif
|
||||
#if __has_include(<Adafruit_BMP280.h>)
|
||||
addSensor<BMP280Sensor>(i2cScanner, ScanI2C::DeviceType::BMP_280);
|
||||
#endif
|
||||
#if __has_include(<Adafruit_DPS310.h>)
|
||||
addSensor<DPS310Sensor>(i2cScanner, ScanI2C::DeviceType::DPS310);
|
||||
#endif
|
||||
#if __has_include(<Adafruit_MCP9808.h>)
|
||||
addSensor<MCP9808Sensor>(i2cScanner, ScanI2C::DeviceType::MCP9808);
|
||||
#endif
|
||||
#if __has_include(<Adafruit_SHT31.h>)
|
||||
addSensor<SHT31Sensor>(i2cScanner, ScanI2C::DeviceType::SHT31);
|
||||
#endif
|
||||
#if __has_include(<Adafruit_LPS2X.h>)
|
||||
addSensor<LPS22HBSensor>(i2cScanner, ScanI2C::DeviceType::LPS22HB);
|
||||
#endif
|
||||
#if __has_include(<Adafruit_SHTC3.h>)
|
||||
addSensor<SHTC3Sensor>(i2cScanner, ScanI2C::DeviceType::SHTC3);
|
||||
#endif
|
||||
#if __has_include("RAK12035_SoilMoisture.h") && defined(RAK_4631) && RAK_4631 == 1
|
||||
addSensor<RAK12035Sensor>(i2cScanner, ScanI2C::DeviceType::RAK12035);
|
||||
#endif
|
||||
#if __has_include(<Adafruit_VEML7700.h>)
|
||||
addSensor<VEML7700Sensor>(i2cScanner, ScanI2C::DeviceType::VEML7700);
|
||||
#endif
|
||||
#if __has_include(<Adafruit_TSL2591.h>)
|
||||
addSensor<TSL2591Sensor>(i2cScanner, ScanI2C::DeviceType::TSL2591);
|
||||
#endif
|
||||
#if __has_include(<ClosedCube_OPT3001.h>)
|
||||
addSensor<OPT3001Sensor>(i2cScanner, ScanI2C::DeviceType::OPT3001);
|
||||
#endif
|
||||
#if __has_include(<Adafruit_SHT4x.h>)
|
||||
addSensor<SHT4XSensor>(i2cScanner, ScanI2C::DeviceType::SHT4X);
|
||||
#endif
|
||||
#if __has_include(<SparkFun_MLX90632_Arduino_Library.h>)
|
||||
addSensor<MLX90632Sensor>(i2cScanner, ScanI2C::DeviceType::MLX90632);
|
||||
#endif
|
||||
|
||||
#if __has_include(<Adafruit_BMP3XX.h>)
|
||||
addSensor<BMP3XXSensor>(i2cScanner, ScanI2C::DeviceType::BMP_3XX);
|
||||
#endif
|
||||
#if __has_include(<Adafruit_PCT2075.h>)
|
||||
addSensor<PCT2075Sensor>(i2cScanner, ScanI2C::DeviceType::PCT2075);
|
||||
#endif
|
||||
#if __has_include(<Adafruit_TSL2561_U.h>)
|
||||
addSensor<TSL2561Sensor>(i2cScanner, ScanI2C::DeviceType::TSL2561);
|
||||
#endif
|
||||
#if __has_include(<SparkFun_Qwiic_Scale_NAU7802_Arduino_Library.h>)
|
||||
addSensor<NAU7802Sensor>(i2cScanner, ScanI2C::DeviceType::NAU7802);
|
||||
#endif
|
||||
#if __has_include(<BH1750_WE.h>)
|
||||
addSensor<BH1750Sensor>(i2cScanner, ScanI2C::DeviceType::BH1750);
|
||||
#endif
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
int32_t EnvironmentTelemetryModule::runOnce()
|
||||
{
|
||||
if (sleepOnNextExecution == true) {
|
||||
sleepOnNextExecution = false;
|
||||
uint32_t nightyNightMs = Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.environment_update_interval,
|
||||
default_telemetry_broadcast_interval_secs);
|
||||
LOG_DEBUG("Sleep for %ims, then awake to send metrics again", nightyNightMs);
|
||||
doDeepSleep(nightyNightMs, true, false);
|
||||
}
|
||||
|
||||
uint32_t result = UINT32_MAX;
|
||||
/*
|
||||
Uncomment the preferences below if you want to use the module
|
||||
without having to configure it from the PythonAPI or WebUI.
|
||||
*/
|
||||
|
||||
// moduleConfig.telemetry.environment_measurement_enabled = 1;
|
||||
// moduleConfig.telemetry.environment_screen_enabled = 1;
|
||||
// moduleConfig.telemetry.environment_update_interval = 15;
|
||||
|
||||
if (!(moduleConfig.telemetry.environment_measurement_enabled || moduleConfig.telemetry.environment_screen_enabled ||
|
||||
ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE)) {
|
||||
// If this module is not enabled, and the user doesn't want the display screen don't waste any OSThread time on it
|
||||
return disable();
|
||||
}
|
||||
|
||||
if (firstTime) {
|
||||
// This is the first time the OSThread library has called this function, so do some setup
|
||||
firstTime = 0;
|
||||
|
||||
if (moduleConfig.telemetry.environment_measurement_enabled || ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE) {
|
||||
LOG_INFO("Environment Telemetry: init");
|
||||
|
||||
// check if we have at least one sensor
|
||||
if (!sensors.empty()) {
|
||||
result = DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
|
||||
}
|
||||
|
||||
#ifdef T1000X_SENSOR_EN
|
||||
#elif !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL
|
||||
if (ina219Sensor.hasSensor())
|
||||
result = ina219Sensor.runOnce();
|
||||
if (ina260Sensor.hasSensor())
|
||||
result = ina260Sensor.runOnce();
|
||||
if (ina3221Sensor.hasSensor())
|
||||
result = ina3221Sensor.runOnce();
|
||||
if (max17048Sensor.hasSensor())
|
||||
result = max17048Sensor.runOnce();
|
||||
// this only works on the wismesh hub with the solar option. This is not an I2C sensor, so we don't need the
|
||||
// sensormap here.
|
||||
#ifdef HAS_RAKPROT
|
||||
result = rak9154Sensor.runOnce();
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
// it's possible to have this module enabled, only for displaying values on the screen.
|
||||
// therefore, we should only enable the sensor loop if measurement is also enabled
|
||||
return result == UINT32_MAX ? disable() : setStartDelay();
|
||||
} else {
|
||||
// if we somehow got to a second run of this module with measurement disabled, then just wait forever
|
||||
if (!moduleConfig.telemetry.environment_measurement_enabled && !ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE) {
|
||||
return disable();
|
||||
}
|
||||
|
||||
for (TelemetrySensor *sensor : sensors) {
|
||||
uint32_t delay = sensor->runOnce();
|
||||
if (delay < result) {
|
||||
result = delay;
|
||||
}
|
||||
}
|
||||
|
||||
if (((lastSentToMesh == 0) ||
|
||||
!Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled(
|
||||
moduleConfig.telemetry.environment_update_interval,
|
||||
default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
|
||||
airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) &&
|
||||
airTime->isTxAllowedAirUtil()) {
|
||||
sendTelemetry();
|
||||
lastSentToMesh = millis();
|
||||
} else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) &&
|
||||
(service->isToPhoneQueueEmpty())) {
|
||||
// Just send to phone when it's not our time to send to mesh yet
|
||||
// Only send while queue is empty (phone assumed connected)
|
||||
sendTelemetry(NODENUM_BROADCAST, true);
|
||||
lastSentToPhone = millis();
|
||||
}
|
||||
}
|
||||
return min(sendToPhoneIntervalMs, result);
|
||||
}
|
||||
|
||||
bool EnvironmentTelemetryModule::wantUIFrame()
|
||||
{
|
||||
return moduleConfig.telemetry.environment_screen_enabled;
|
||||
}
|
||||
|
||||
#if HAS_SCREEN
|
||||
void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
// === Setup display ===
|
||||
display->clear();
|
||||
display->setFont(FONT_SMALL);
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
int line = 1;
|
||||
|
||||
// === Set Title
|
||||
const char *titleStr = (graphics::isHighResolution) ? "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 telemetry message from the latest received packet
|
||||
const meshtastic_Data &p = lastMeasurementPacket->decoded;
|
||||
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;
|
||||
}
|
||||
|
||||
const auto &m = telemetry.variant.environment_metrics;
|
||||
|
||||
// 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 (!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";
|
||||
}
|
||||
|
||||
entries.push_back(aqi);
|
||||
|
||||
// === IAQ alert logic ===
|
||||
static uint32_t lastAlertTime = 0;
|
||||
uint32_t now = millis();
|
||||
|
||||
bool isOwnTelemetry = lastMeasurementPacket->from == nodeDB->getNodeNum();
|
||||
bool isCooldownOver = (now - lastAlertTime > 60000);
|
||||
|
||||
if (isOwnTelemetry && bannerMsg && isCooldownOver) {
|
||||
LOG_INFO("drawFrame: IAQ %d (own) — showing banner: %s", m.iaq, bannerMsg);
|
||||
screen->showSimpleBanner(bannerMsg, 3000);
|
||||
|
||||
// Only buzz if IAQ is over 200
|
||||
if (m.iaq > 200 && moduleConfig.external_notification.enabled && !externalNotificationModule->getMute()) {
|
||||
playLongBeep();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
graphics::drawCommonFooter(display, x, y);
|
||||
}
|
||||
#endif
|
||||
|
||||
bool EnvironmentTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t)
|
||||
{
|
||||
if (t->which_variant == meshtastic_Telemetry_environment_metrics_tag) {
|
||||
#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
|
||||
const char *sender = getSenderShortName(mp);
|
||||
|
||||
LOG_INFO("(Received from %s): barometric_pressure=%f, current=%f, gas_resistance=%f, relative_humidity=%f, "
|
||||
"temperature=%f",
|
||||
sender, t->variant.environment_metrics.barometric_pressure, t->variant.environment_metrics.current,
|
||||
t->variant.environment_metrics.gas_resistance, t->variant.environment_metrics.relative_humidity,
|
||||
t->variant.environment_metrics.temperature);
|
||||
LOG_INFO("(Received from %s): voltage=%f, IAQ=%d, distance=%f, lux=%f, white_lux=%f", sender,
|
||||
t->variant.environment_metrics.voltage, t->variant.environment_metrics.iaq,
|
||||
t->variant.environment_metrics.distance, t->variant.environment_metrics.lux,
|
||||
t->variant.environment_metrics.white_lux);
|
||||
|
||||
LOG_INFO("(Received from %s): wind speed=%fm/s, direction=%d degrees, weight=%fkg", sender,
|
||||
t->variant.environment_metrics.wind_speed, t->variant.environment_metrics.wind_direction,
|
||||
t->variant.environment_metrics.weight);
|
||||
|
||||
LOG_INFO("(Received from %s): radiation=%fµR/h", sender, t->variant.environment_metrics.radiation);
|
||||
|
||||
#endif
|
||||
// release previous packet before occupying a new spot
|
||||
if (lastMeasurementPacket != nullptr)
|
||||
packetPool.release(lastMeasurementPacket);
|
||||
|
||||
lastMeasurementPacket = packetPool.allocCopy(mp);
|
||||
}
|
||||
|
||||
return false; // Let others look at this message also if they want
|
||||
}
|
||||
|
||||
bool EnvironmentTelemetryModule::getEnvironmentTelemetry(meshtastic_Telemetry *m)
|
||||
{
|
||||
bool valid = true;
|
||||
bool hasSensor = false;
|
||||
m->time = getTime();
|
||||
m->which_variant = meshtastic_Telemetry_environment_metrics_tag;
|
||||
m->variant.environment_metrics = meshtastic_EnvironmentMetrics_init_zero;
|
||||
|
||||
for (TelemetrySensor *sensor : sensors) {
|
||||
valid = valid && sensor->getMetrics(m);
|
||||
hasSensor = true;
|
||||
}
|
||||
|
||||
#ifndef T1000X_SENSOR_EN
|
||||
if (ina219Sensor.hasSensor()) {
|
||||
valid = valid && ina219Sensor.getMetrics(m);
|
||||
hasSensor = true;
|
||||
}
|
||||
if (ina260Sensor.hasSensor()) {
|
||||
valid = valid && ina260Sensor.getMetrics(m);
|
||||
hasSensor = true;
|
||||
}
|
||||
if (ina3221Sensor.hasSensor()) {
|
||||
valid = valid && ina3221Sensor.getMetrics(m);
|
||||
hasSensor = true;
|
||||
}
|
||||
if (max17048Sensor.hasSensor()) {
|
||||
valid = valid && max17048Sensor.getMetrics(m);
|
||||
hasSensor = true;
|
||||
}
|
||||
#endif
|
||||
#ifdef HAS_RAKPROT
|
||||
valid = valid && rak9154Sensor.getMetrics(m);
|
||||
hasSensor = true;
|
||||
#endif
|
||||
return valid && hasSensor;
|
||||
}
|
||||
|
||||
meshtastic_MeshPacket *EnvironmentTelemetryModule::allocReply()
|
||||
{
|
||||
if (currentRequest) {
|
||||
auto req = *currentRequest;
|
||||
const auto &p = req.decoded;
|
||||
meshtastic_Telemetry scratch;
|
||||
meshtastic_Telemetry *decoded = NULL;
|
||||
memset(&scratch, 0, sizeof(scratch));
|
||||
if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &scratch)) {
|
||||
decoded = &scratch;
|
||||
} else {
|
||||
LOG_ERROR("Error decoding EnvironmentTelemetry module!");
|
||||
return NULL;
|
||||
}
|
||||
// Check for a request for environment metrics
|
||||
if (decoded->which_variant == meshtastic_Telemetry_environment_metrics_tag) {
|
||||
meshtastic_Telemetry m = meshtastic_Telemetry_init_zero;
|
||||
if (getEnvironmentTelemetry(&m)) {
|
||||
LOG_INFO("Environment telemetry reply to request");
|
||||
return allocDataProtobuf(m);
|
||||
} else {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool EnvironmentTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
|
||||
{
|
||||
meshtastic_Telemetry m = meshtastic_Telemetry_init_zero;
|
||||
m.which_variant = meshtastic_Telemetry_environment_metrics_tag;
|
||||
m.time = getTime();
|
||||
|
||||
if (getEnvironmentTelemetry(&m)) {
|
||||
LOG_INFO("Send: barometric_pressure=%f, current=%f, gas_resistance=%f, relative_humidity=%f, temperature=%f",
|
||||
m.variant.environment_metrics.barometric_pressure, m.variant.environment_metrics.current,
|
||||
m.variant.environment_metrics.gas_resistance, m.variant.environment_metrics.relative_humidity,
|
||||
m.variant.environment_metrics.temperature);
|
||||
LOG_INFO("Send: voltage=%f, IAQ=%d, distance=%f, lux=%f", m.variant.environment_metrics.voltage,
|
||||
m.variant.environment_metrics.iaq, m.variant.environment_metrics.distance, m.variant.environment_metrics.lux);
|
||||
|
||||
LOG_INFO("Send: wind speed=%fm/s, direction=%d degrees, weight=%fkg", m.variant.environment_metrics.wind_speed,
|
||||
m.variant.environment_metrics.wind_direction, m.variant.environment_metrics.weight);
|
||||
|
||||
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);
|
||||
p->to = dest;
|
||||
p->decoded.want_response = false;
|
||||
if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR)
|
||||
p->priority = meshtastic_MeshPacket_Priority_RELIABLE;
|
||||
else
|
||||
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||
// release previous packet before occupying a new spot
|
||||
if (lastMeasurementPacket != nullptr)
|
||||
packetPool.release(lastMeasurementPacket);
|
||||
|
||||
lastMeasurementPacket = packetPool.allocCopy(*p);
|
||||
if (phoneOnly) {
|
||||
LOG_INFO("Send packet to phone");
|
||||
service->sendToPhone(p);
|
||||
} else {
|
||||
LOG_INFO("Send packet to mesh");
|
||||
service->sendToMesh(p, RX_SRC_LOCAL, true);
|
||||
|
||||
if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR && config.power.is_power_saving) {
|
||||
meshtastic_ClientNotification *notification = clientNotificationPool.allocZeroed();
|
||||
notification->level = meshtastic_LogRecord_Level_INFO;
|
||||
notification->time = getValidTime(RTCQualityFromNet);
|
||||
sprintf(notification->message, "Sending telemetry and sleeping for %us interval in a moment",
|
||||
Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.environment_update_interval,
|
||||
default_telemetry_broadcast_interval_secs) /
|
||||
1000U);
|
||||
service->sendClientNotification(notification);
|
||||
sleepOnNextExecution = true;
|
||||
LOG_DEBUG("Start next execution in 5s, then sleep");
|
||||
setIntervalFromNow(FIVE_SECONDS_MS);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
AdminMessageHandleResult EnvironmentTelemetryModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp,
|
||||
meshtastic_AdminMessage *request,
|
||||
meshtastic_AdminMessage *response)
|
||||
{
|
||||
AdminMessageHandleResult result = AdminMessageHandleResult::NOT_HANDLED;
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL
|
||||
|
||||
for (TelemetrySensor *sensor : sensors) {
|
||||
result = sensor->handleAdminMessage(mp, request, response);
|
||||
if (result != AdminMessageHandleResult::NOT_HANDLED)
|
||||
return result;
|
||||
}
|
||||
|
||||
if (ina219Sensor.hasSensor()) {
|
||||
result = ina219Sensor.handleAdminMessage(mp, request, response);
|
||||
if (result != AdminMessageHandleResult::NOT_HANDLED)
|
||||
return result;
|
||||
}
|
||||
if (ina260Sensor.hasSensor()) {
|
||||
result = ina260Sensor.handleAdminMessage(mp, request, response);
|
||||
if (result != AdminMessageHandleResult::NOT_HANDLED)
|
||||
return result;
|
||||
}
|
||||
if (ina3221Sensor.hasSensor()) {
|
||||
result = ina3221Sensor.handleAdminMessage(mp, request, response);
|
||||
if (result != AdminMessageHandleResult::NOT_HANDLED)
|
||||
return result;
|
||||
}
|
||||
if (max17048Sensor.hasSensor()) {
|
||||
result = max17048Sensor.handleAdminMessage(mp, request, response);
|
||||
if (result != AdminMessageHandleResult::NOT_HANDLED)
|
||||
return result;
|
||||
}
|
||||
#endif
|
||||
return result;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,73 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifndef ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE
|
||||
#define ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE 0
|
||||
#endif
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "NodeDB.h"
|
||||
#include "ProtobufModule.h"
|
||||
#include "detect/ScanI2CConsumer.h"
|
||||
#include <OLEDDisplay.h>
|
||||
#include <OLEDDisplayUi.h>
|
||||
|
||||
class EnvironmentTelemetryModule : private concurrency::OSThread,
|
||||
public ScanI2CConsumer,
|
||||
public ProtobufModule<meshtastic_Telemetry>
|
||||
{
|
||||
CallbackObserver<EnvironmentTelemetryModule, const meshtastic::Status *> nodeStatusObserver =
|
||||
CallbackObserver<EnvironmentTelemetryModule, const meshtastic::Status *>(this,
|
||||
&EnvironmentTelemetryModule::handleStatusUpdate);
|
||||
|
||||
public:
|
||||
EnvironmentTelemetryModule()
|
||||
: concurrency::OSThread("EnvironmentTelemetry"), ScanI2CConsumer(),
|
||||
ProtobufModule("EnvironmentTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg)
|
||||
{
|
||||
lastMeasurementPacket = nullptr;
|
||||
nodeStatusObserver.observe(&nodeStatus->onNewStatus);
|
||||
setIntervalFromNow(10 * 1000);
|
||||
}
|
||||
virtual bool wantUIFrame() override;
|
||||
#if !HAS_SCREEN
|
||||
void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
#else
|
||||
virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override;
|
||||
#endif
|
||||
|
||||
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_Telemetry *p) override;
|
||||
virtual int32_t runOnce() override;
|
||||
/** Called to get current Environment telemetry data
|
||||
@return true if it contains valid data
|
||||
*/
|
||||
bool getEnvironmentTelemetry(meshtastic_Telemetry *m);
|
||||
virtual meshtastic_MeshPacket *allocReply() override;
|
||||
/**
|
||||
* Send our Telemetry into the mesh
|
||||
*/
|
||||
bool sendTelemetry(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false);
|
||||
|
||||
virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp,
|
||||
meshtastic_AdminMessage *request,
|
||||
meshtastic_AdminMessage *response) override;
|
||||
|
||||
void i2cScanFinished(ScanI2C *i2cScanner);
|
||||
|
||||
private:
|
||||
bool firstTime = 1;
|
||||
meshtastic_MeshPacket *lastMeasurementPacket;
|
||||
uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute
|
||||
uint32_t lastSentToMesh = 0;
|
||||
uint32_t lastSentToPhone = 0;
|
||||
uint32_t sensor_read_error_count = 0;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,258 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY && !defined(ARCH_PORTDUINO)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "Default.h"
|
||||
#include "HealthTelemetry.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "RTC.h"
|
||||
#include "Router.h"
|
||||
#include "UnitConversions.h"
|
||||
#include "main.h"
|
||||
#include "power.h"
|
||||
#include "sleep.h"
|
||||
#include "target_specific.h"
|
||||
#include <OLEDDisplay.h>
|
||||
#include <OLEDDisplayUi.h>
|
||||
|
||||
// Sensors
|
||||
#include "Sensor/MAX30102Sensor.h"
|
||||
#include "Sensor/MLX90614Sensor.h"
|
||||
|
||||
MAX30102Sensor max30102Sensor;
|
||||
MLX90614Sensor mlx90614Sensor;
|
||||
|
||||
#define FAILED_STATE_SENSOR_READ_MULTIPLIER 10
|
||||
#define DISPLAY_RECEIVEID_MEASUREMENTS_ON_SCREEN true
|
||||
|
||||
#if (HAS_SCREEN)
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#endif
|
||||
#include <Throttle.h>
|
||||
|
||||
int32_t HealthTelemetryModule::runOnce()
|
||||
{
|
||||
if (sleepOnNextExecution == true) {
|
||||
sleepOnNextExecution = false;
|
||||
uint32_t nightyNightMs = Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.health_update_interval,
|
||||
default_telemetry_broadcast_interval_secs);
|
||||
LOG_DEBUG("Sleep for %ims, then awake to send metrics again", nightyNightMs);
|
||||
doDeepSleep(nightyNightMs, true, false);
|
||||
}
|
||||
|
||||
uint32_t result = UINT32_MAX;
|
||||
|
||||
if (!(moduleConfig.telemetry.health_measurement_enabled || moduleConfig.telemetry.health_screen_enabled)) {
|
||||
// If this module is not enabled, and the user doesn't want the display screen don't waste any OSThread time on it
|
||||
return disable();
|
||||
}
|
||||
|
||||
if (firstTime) {
|
||||
// This is the first time the OSThread library has called this function, so do some setup
|
||||
firstTime = false;
|
||||
|
||||
if (moduleConfig.telemetry.health_measurement_enabled) {
|
||||
LOG_INFO("Health Telemetry: init");
|
||||
// Initialize sensors
|
||||
if (mlx90614Sensor.hasSensor())
|
||||
result = mlx90614Sensor.runOnce();
|
||||
if (max30102Sensor.hasSensor())
|
||||
result = max30102Sensor.runOnce();
|
||||
}
|
||||
return result == UINT32_MAX ? disable() : setStartDelay();
|
||||
} else {
|
||||
// if we somehow got to a second run of this module with measurement disabled, then just wait forever
|
||||
if (!moduleConfig.telemetry.health_measurement_enabled) {
|
||||
return disable();
|
||||
}
|
||||
|
||||
if (((lastSentToMesh == 0) ||
|
||||
!Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled(
|
||||
moduleConfig.telemetry.health_update_interval,
|
||||
default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
|
||||
airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) &&
|
||||
airTime->isTxAllowedAirUtil()) {
|
||||
sendTelemetry();
|
||||
lastSentToMesh = millis();
|
||||
} else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) &&
|
||||
(service->isToPhoneQueueEmpty())) {
|
||||
// Just send to phone when it's not our time to send to mesh yet
|
||||
// Only send while queue is empty (phone assumed connected)
|
||||
sendTelemetry(NODENUM_BROADCAST, true);
|
||||
lastSentToPhone = millis();
|
||||
}
|
||||
}
|
||||
return min(sendToPhoneIntervalMs, result);
|
||||
}
|
||||
|
||||
bool HealthTelemetryModule::wantUIFrame()
|
||||
{
|
||||
return moduleConfig.telemetry.health_screen_enabled;
|
||||
}
|
||||
|
||||
void HealthTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
|
||||
if (lastMeasurementPacket == nullptr) {
|
||||
// If there's no valid packet, display "Health"
|
||||
display->drawString(x, y, "Health");
|
||||
display->drawString(x, y += _fontHeight(FONT_SMALL), "No measurement");
|
||||
return;
|
||||
}
|
||||
|
||||
// Decode the last measurement packet
|
||||
meshtastic_Telemetry lastMeasurement;
|
||||
uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket);
|
||||
const char *lastSender = getSenderShortName(*lastMeasurementPacket);
|
||||
|
||||
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");
|
||||
return;
|
||||
}
|
||||
|
||||
// Display "Health From: ..." on its own
|
||||
char headerStr[64];
|
||||
snprintf(headerStr, sizeof(headerStr), "Health From: %s(%ds)", lastSender, (int)agoSecs);
|
||||
display->drawString(x, y, headerStr);
|
||||
|
||||
char last_temp[16];
|
||||
if (moduleConfig.telemetry.environment_display_fahrenheit) {
|
||||
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
|
||||
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) {
|
||||
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) {
|
||||
char spo2Str[32];
|
||||
snprintf(spo2Str, sizeof(spo2Str), "spO2: %.0f %%", lastMeasurement.variant.health_metrics.spO2);
|
||||
display->drawString(x, y += _fontHeight(FONT_SMALL), spo2Str);
|
||||
}
|
||||
}
|
||||
|
||||
bool HealthTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t)
|
||||
{
|
||||
if (t->which_variant == meshtastic_Telemetry_health_metrics_tag) {
|
||||
#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
|
||||
const char *sender = getSenderShortName(mp);
|
||||
|
||||
LOG_INFO("(Received from %s): temperature=%f, heart_bpm=%d, spO2=%d,", sender, t->variant.health_metrics.temperature,
|
||||
t->variant.health_metrics.heart_bpm, t->variant.health_metrics.spO2);
|
||||
|
||||
#endif
|
||||
// release previous packet before occupying a new spot
|
||||
if (lastMeasurementPacket != nullptr)
|
||||
packetPool.release(lastMeasurementPacket);
|
||||
|
||||
lastMeasurementPacket = packetPool.allocCopy(mp);
|
||||
}
|
||||
|
||||
return false; // Let others look at this message also if they want
|
||||
}
|
||||
|
||||
bool HealthTelemetryModule::getHealthTelemetry(meshtastic_Telemetry *m)
|
||||
{
|
||||
bool valid = true;
|
||||
bool hasSensor = false;
|
||||
m->time = getTime();
|
||||
m->which_variant = meshtastic_Telemetry_health_metrics_tag;
|
||||
m->variant.health_metrics = meshtastic_HealthMetrics_init_zero;
|
||||
|
||||
if (max30102Sensor.hasSensor()) {
|
||||
valid = valid && max30102Sensor.getMetrics(m);
|
||||
hasSensor = true;
|
||||
}
|
||||
if (mlx90614Sensor.hasSensor()) {
|
||||
valid = valid && mlx90614Sensor.getMetrics(m);
|
||||
hasSensor = true;
|
||||
}
|
||||
|
||||
return valid && hasSensor;
|
||||
}
|
||||
|
||||
meshtastic_MeshPacket *HealthTelemetryModule::allocReply()
|
||||
{
|
||||
if (currentRequest) {
|
||||
auto req = *currentRequest;
|
||||
const auto &p = req.decoded;
|
||||
meshtastic_Telemetry scratch;
|
||||
meshtastic_Telemetry *decoded = NULL;
|
||||
memset(&scratch, 0, sizeof(scratch));
|
||||
if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &scratch)) {
|
||||
decoded = &scratch;
|
||||
} else {
|
||||
LOG_ERROR("Error decoding HealthTelemetry module!");
|
||||
return NULL;
|
||||
}
|
||||
// Check for a request for health metrics
|
||||
if (decoded->which_variant == meshtastic_Telemetry_health_metrics_tag) {
|
||||
meshtastic_Telemetry m = meshtastic_Telemetry_init_zero;
|
||||
if (getHealthTelemetry(&m)) {
|
||||
LOG_INFO("Health telemetry reply to request");
|
||||
return allocDataProtobuf(m);
|
||||
} else {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool HealthTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
|
||||
{
|
||||
meshtastic_Telemetry m = meshtastic_Telemetry_init_zero;
|
||||
m.which_variant = meshtastic_Telemetry_health_metrics_tag;
|
||||
m.time = getTime();
|
||||
if (getHealthTelemetry(&m)) {
|
||||
LOG_INFO("Send: temperature=%f, heart_bpm=%d, spO2=%d", m.variant.health_metrics.temperature,
|
||||
m.variant.health_metrics.heart_bpm, m.variant.health_metrics.spO2);
|
||||
|
||||
sensor_read_error_count = 0;
|
||||
|
||||
meshtastic_MeshPacket *p = allocDataProtobuf(m);
|
||||
p->to = dest;
|
||||
p->decoded.want_response = false;
|
||||
if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR)
|
||||
p->priority = meshtastic_MeshPacket_Priority_RELIABLE;
|
||||
else
|
||||
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||
// release previous packet before occupying a new spot
|
||||
if (lastMeasurementPacket != nullptr)
|
||||
packetPool.release(lastMeasurementPacket);
|
||||
|
||||
lastMeasurementPacket = packetPool.allocCopy(*p);
|
||||
if (phoneOnly) {
|
||||
LOG_INFO("Send packet to phone");
|
||||
service->sendToPhone(p);
|
||||
} else {
|
||||
LOG_INFO("Send packet to mesh");
|
||||
service->sendToMesh(p, RX_SRC_LOCAL, true);
|
||||
|
||||
if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR && config.power.is_power_saving) {
|
||||
LOG_DEBUG("Start next execution in 5s, then sleep");
|
||||
sleepOnNextExecution = true;
|
||||
setIntervalFromNow(5000);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,60 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY && !defined(ARCH_PORTDUINO)
|
||||
|
||||
#pragma once
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "NodeDB.h"
|
||||
#include "ProtobufModule.h"
|
||||
#include <OLEDDisplay.h>
|
||||
#include <OLEDDisplayUi.h>
|
||||
|
||||
class HealthTelemetryModule : private concurrency::OSThread, public ProtobufModule<meshtastic_Telemetry>
|
||||
{
|
||||
CallbackObserver<HealthTelemetryModule, const meshtastic::Status *> nodeStatusObserver =
|
||||
CallbackObserver<HealthTelemetryModule, const meshtastic::Status *>(this, &HealthTelemetryModule::handleStatusUpdate);
|
||||
|
||||
public:
|
||||
HealthTelemetryModule()
|
||||
: concurrency::OSThread("HealthTelemetry"),
|
||||
ProtobufModule("HealthTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg)
|
||||
{
|
||||
lastMeasurementPacket = nullptr;
|
||||
nodeStatusObserver.observe(&nodeStatus->onNewStatus);
|
||||
setIntervalFromNow(10 * 1000);
|
||||
}
|
||||
|
||||
#if !HAS_SCREEN
|
||||
void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
#else
|
||||
virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override;
|
||||
#endif
|
||||
|
||||
virtual bool wantUIFrame() override;
|
||||
|
||||
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_Telemetry *p) override;
|
||||
virtual int32_t runOnce() override;
|
||||
/** Called to get current Health telemetry data
|
||||
@return true if it contains valid data
|
||||
*/
|
||||
bool getHealthTelemetry(meshtastic_Telemetry *m);
|
||||
virtual meshtastic_MeshPacket *allocReply() override;
|
||||
/**
|
||||
* Send our Telemetry into the mesh
|
||||
*/
|
||||
bool sendTelemetry(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false);
|
||||
|
||||
private:
|
||||
bool firstTime = 1;
|
||||
meshtastic_MeshPacket *lastMeasurementPacket;
|
||||
uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute
|
||||
uint32_t lastSentToMesh = 0;
|
||||
uint32_t lastSentToPhone = 0;
|
||||
uint32_t sensor_read_error_count = 0;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,139 @@
|
||||
#include "HostMetrics.h"
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "MeshService.h"
|
||||
#if ARCH_PORTDUINO
|
||||
#include "PortduinoGlue.h"
|
||||
#include <filesystem>
|
||||
#endif
|
||||
|
||||
int32_t HostMetricsModule::runOnce()
|
||||
{
|
||||
#if ARCH_PORTDUINO
|
||||
if (portduino_config.hostMetrics_interval == 0) {
|
||||
return disable();
|
||||
} else {
|
||||
sendMetrics();
|
||||
return 60 * 1000 * portduino_config.hostMetrics_interval;
|
||||
}
|
||||
#else
|
||||
return disable();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool HostMetricsModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t)
|
||||
{
|
||||
if (t->which_variant == meshtastic_Telemetry_host_metrics_tag) {
|
||||
#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
|
||||
const char *sender = getSenderShortName(mp);
|
||||
if (t->variant.host_metrics.has_user_string)
|
||||
t->variant.host_metrics.user_string[sizeof(t->variant.host_metrics.user_string) - 1] = '\0';
|
||||
|
||||
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 : "");
|
||||
#endif
|
||||
}
|
||||
return false; // Let others look at this message also if they want
|
||||
}
|
||||
|
||||
/*
|
||||
meshtastic_MeshPacket *HostMetricsModule::allocReply()
|
||||
{
|
||||
if (currentRequest) {
|
||||
auto req = *currentRequest;
|
||||
const auto &p = req.decoded;
|
||||
meshtastic_Telemetry scratch;
|
||||
meshtastic_Telemetry *decoded = NULL;
|
||||
memset(&scratch, 0, sizeof(scratch));
|
||||
if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_HostMetrics_msg, &scratch)) {
|
||||
decoded = &scratch;
|
||||
} else {
|
||||
LOG_ERROR("Error decoding HostMetrics module!");
|
||||
return NULL;
|
||||
}
|
||||
// Check for a request for device metrics
|
||||
if (decoded->which_variant == meshtastic_Telemetry_host_metrics_tag) {
|
||||
LOG_INFO("Device telemetry reply to request");
|
||||
return allocDataProtobuf(getHostMetrics());
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
*/
|
||||
|
||||
#if ARCH_PORTDUINO
|
||||
meshtastic_Telemetry HostMetricsModule::getHostMetrics()
|
||||
{
|
||||
std::string file_line;
|
||||
meshtastic_Telemetry t = meshtastic_Telemetry_init_zero;
|
||||
t.which_variant = meshtastic_Telemetry_host_metrics_tag;
|
||||
t.variant.host_metrics = meshtastic_HostMetrics_init_zero;
|
||||
|
||||
if (access("/proc/uptime", R_OK) == 0) {
|
||||
std::ifstream proc_uptime("/proc/uptime");
|
||||
if (proc_uptime.is_open()) {
|
||||
std::getline(proc_uptime, file_line, ' ');
|
||||
proc_uptime.close();
|
||||
t.variant.host_metrics.uptime_seconds = stoul(file_line);
|
||||
}
|
||||
}
|
||||
|
||||
std::filesystem::space_info root = std::filesystem::space("/");
|
||||
t.variant.host_metrics.diskfree1_bytes = root.available;
|
||||
|
||||
if (access("/proc/meminfo", R_OK) == 0) {
|
||||
std::ifstream proc_meminfo("/proc/meminfo");
|
||||
if (proc_meminfo.is_open()) {
|
||||
do {
|
||||
std::getline(proc_meminfo, file_line);
|
||||
} while (file_line.find("MemAvailable") == std::string::npos);
|
||||
proc_meminfo.close();
|
||||
t.variant.host_metrics.freemem_bytes = stoull(file_line.substr(file_line.find_first_of("0123456789"))) * 1024;
|
||||
}
|
||||
}
|
||||
if (access("/proc/loadavg", R_OK) == 0) {
|
||||
std::ifstream proc_loadavg("/proc/loadavg");
|
||||
if (proc_loadavg.is_open()) {
|
||||
std::getline(proc_loadavg, file_line, ' ');
|
||||
t.variant.host_metrics.load1 = stof(file_line) * 100;
|
||||
std::getline(proc_loadavg, file_line, ' ');
|
||||
t.variant.host_metrics.load5 = stof(file_line) * 100;
|
||||
std::getline(proc_loadavg, file_line, ' ');
|
||||
t.variant.host_metrics.load15 = stof(file_line) * 100;
|
||||
proc_loadavg.close();
|
||||
}
|
||||
}
|
||||
if (portduino_config.hostMetrics_user_command != "") {
|
||||
std::string userCommandResult = exec(portduino_config.hostMetrics_user_command.c_str());
|
||||
if (userCommandResult.length() > 1) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
bool HostMetricsModule::sendMetrics()
|
||||
{
|
||||
meshtastic_Telemetry telemetry = getHostMetrics();
|
||||
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 : "");
|
||||
|
||||
meshtastic_MeshPacket *p = allocDataProtobuf(telemetry);
|
||||
p->to = NODENUM_BROADCAST;
|
||||
p->decoded.want_response = false;
|
||||
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||
p->channel = portduino_config.hostMetrics_channel;
|
||||
LOG_INFO("Send packet to mesh");
|
||||
service->sendToMesh(p, RX_SRC_LOCAL, true);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "ProtobufModule.h"
|
||||
|
||||
class HostMetricsModule : private concurrency::OSThread, public ProtobufModule<meshtastic_Telemetry>
|
||||
{
|
||||
CallbackObserver<HostMetricsModule, const meshtastic::Status *> nodeStatusObserver =
|
||||
CallbackObserver<HostMetricsModule, const meshtastic::Status *>(this, &HostMetricsModule::handleStatusUpdate);
|
||||
|
||||
public:
|
||||
HostMetricsModule()
|
||||
: concurrency::OSThread("HostMetrics"),
|
||||
ProtobufModule("HostMetrics", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg)
|
||||
{
|
||||
uptimeWrapCount = 0;
|
||||
uptimeLastMs = millis();
|
||||
nodeStatusObserver.observe(&nodeStatus->onNewStatus);
|
||||
setIntervalFromNow(setStartDelay()); // Wait until NodeInfo is sent
|
||||
}
|
||||
virtual bool wantUIFrame() { return false; }
|
||||
|
||||
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_Telemetry *p) override;
|
||||
// virtual meshtastic_MeshPacket *allocReply() override;
|
||||
virtual int32_t runOnce() override;
|
||||
/**
|
||||
* Send our Telemetry into the mesh
|
||||
*/
|
||||
bool sendMetrics();
|
||||
|
||||
private:
|
||||
meshtastic_Telemetry getHostMetrics();
|
||||
|
||||
uint32_t lastSentToMesh = 0;
|
||||
uint32_t uptimeWrapCount;
|
||||
uint32_t uptimeLastMs;
|
||||
};
|
||||
@@ -0,0 +1,289 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "Default.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "PowerTelemetry.h"
|
||||
#include "RTC.h"
|
||||
#include "Router.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "main.h"
|
||||
#include "power.h"
|
||||
#include "sleep.h"
|
||||
#include "target_specific.h"
|
||||
|
||||
#define FAILED_STATE_SENSOR_READ_MULTIPLIER 10
|
||||
#define DISPLAY_RECEIVEID_MEASUREMENTS_ON_SCREEN true
|
||||
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include <Throttle.h>
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert,
|
||||
bool show_date);
|
||||
}
|
||||
|
||||
int32_t PowerTelemetryModule::runOnce()
|
||||
{
|
||||
if (sleepOnNextExecution == true) {
|
||||
sleepOnNextExecution = false;
|
||||
uint32_t nightyNightMs = Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.power_update_interval,
|
||||
default_telemetry_broadcast_interval_secs);
|
||||
LOG_DEBUG("Sleep for %ims, then awake to send metrics again", nightyNightMs);
|
||||
doDeepSleep(nightyNightMs, true, false);
|
||||
}
|
||||
|
||||
/*
|
||||
Uncomment the preferences below if you want to use the module
|
||||
without having to configure it from the PythonAPI or WebUI.
|
||||
*/
|
||||
|
||||
// moduleConfig.telemetry.power_measurement_enabled = 1;
|
||||
// moduleConfig.telemetry.power_screen_enabled = 1;
|
||||
// moduleConfig.telemetry.power_update_interval = 45;
|
||||
|
||||
if (!(moduleConfig.telemetry.power_measurement_enabled)) {
|
||||
// If this module is not enabled, and the user doesn't want the display screen don't waste any OSThread time on it
|
||||
return disable();
|
||||
}
|
||||
|
||||
uint32_t sendToMeshIntervalMs = Default::getConfiguredOrDefaultMsScaled(
|
||||
moduleConfig.telemetry.power_update_interval, default_telemetry_broadcast_interval_secs, numOnlineNodes);
|
||||
|
||||
if (firstTime) {
|
||||
// This is the first time the OSThread library has called this function, so do some setup
|
||||
firstTime = 0;
|
||||
uint32_t result = UINT32_MAX;
|
||||
|
||||
#if HAS_TELEMETRY
|
||||
if (moduleConfig.telemetry.power_measurement_enabled) {
|
||||
LOG_INFO("Power Telemetry: init");
|
||||
// If sensor is already initialized by EnvironmentTelemetryModule, then we don't need to initialize it again,
|
||||
// but we need to set the result to != UINT32_MAX to avoid it being disabled
|
||||
if (ina219Sensor.hasSensor())
|
||||
result = ina219Sensor.isInitialized() ? 0 : ina219Sensor.runOnce();
|
||||
if (ina226Sensor.hasSensor())
|
||||
result = ina226Sensor.isInitialized() ? 0 : ina226Sensor.runOnce();
|
||||
if (ina260Sensor.hasSensor())
|
||||
result = ina260Sensor.isInitialized() ? 0 : ina260Sensor.runOnce();
|
||||
if (ina3221Sensor.hasSensor())
|
||||
result = ina3221Sensor.isInitialized() ? 0 : ina3221Sensor.runOnce();
|
||||
if (max17048Sensor.hasSensor())
|
||||
result = max17048Sensor.isInitialized() ? 0 : max17048Sensor.runOnce();
|
||||
}
|
||||
|
||||
// it's possible to have this module enabled, only for displaying values on the screen.
|
||||
// therefore, we should only enable the sensor loop if measurement is also enabled
|
||||
return result == UINT32_MAX ? disable() : setStartDelay();
|
||||
#else
|
||||
return disable();
|
||||
#endif
|
||||
} else {
|
||||
// if we somehow got to a second run of this module with measurement disabled, then just wait forever
|
||||
if (!moduleConfig.telemetry.power_measurement_enabled)
|
||||
return disable();
|
||||
|
||||
if (((lastSentToMesh == 0) || !Throttle::isWithinTimespanMs(lastSentToMesh, sendToMeshIntervalMs)) &&
|
||||
airTime->isTxAllowedAirUtil()) {
|
||||
sendTelemetry();
|
||||
lastSentToMesh = millis();
|
||||
} else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) &&
|
||||
(service->isToPhoneQueueEmpty())) {
|
||||
// Just send to phone when it's not our time to send to mesh yet
|
||||
// Only send while queue is empty (phone assumed connected)
|
||||
sendTelemetry(NODENUM_BROADCAST, true);
|
||||
lastSentToPhone = millis();
|
||||
}
|
||||
}
|
||||
return min(sendToPhoneIntervalMs, sendToMeshIntervalMs);
|
||||
}
|
||||
|
||||
bool PowerTelemetryModule::wantUIFrame()
|
||||
{
|
||||
return moduleConfig.telemetry.power_screen_enabled;
|
||||
}
|
||||
|
||||
#if HAS_SCREEN
|
||||
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 = (graphics::isHighResolution) ? "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, graphics::getTextPositions(display)[line++], "No measurement");
|
||||
return;
|
||||
}
|
||||
|
||||
// Decode the last power packet
|
||||
meshtastic_Telemetry lastMeasurement;
|
||||
uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket);
|
||||
const char *lastSender = getSenderShortName(*lastMeasurementPacket);
|
||||
|
||||
const meshtastic_Data &p = lastMeasurementPacket->decoded;
|
||||
if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &lastMeasurement)) {
|
||||
display->drawString(x, graphics::getTextPositions(display)[line++], "Measurement Error");
|
||||
LOG_ERROR("Unable to decode last packet");
|
||||
return;
|
||||
}
|
||||
|
||||
// Display "Pow. From: ..."
|
||||
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
|
||||
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 (m.has_ch2_voltage || m.has_ch2_current) {
|
||||
drawLine("Ch2", m.ch2_voltage, m.ch2_current);
|
||||
}
|
||||
if (m.has_ch3_voltage || m.has_ch3_current) {
|
||||
drawLine("Ch3", m.ch3_voltage, m.ch3_current);
|
||||
}
|
||||
graphics::drawCommonFooter(display, x, y);
|
||||
}
|
||||
#endif
|
||||
|
||||
bool PowerTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t)
|
||||
{
|
||||
if (t->which_variant == meshtastic_Telemetry_power_metrics_tag) {
|
||||
#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
|
||||
const char *sender = getSenderShortName(mp);
|
||||
|
||||
LOG_INFO("(Received from %s): ch1_voltage=%.1f, ch1_current=%.1f, ch2_voltage=%.1f, ch2_current=%.1f, "
|
||||
"ch3_voltage=%.1f, ch3_current=%.1f",
|
||||
sender, t->variant.power_metrics.ch1_voltage, t->variant.power_metrics.ch1_current,
|
||||
t->variant.power_metrics.ch2_voltage, t->variant.power_metrics.ch2_current, t->variant.power_metrics.ch3_voltage,
|
||||
t->variant.power_metrics.ch3_current);
|
||||
#endif
|
||||
// release previous packet before occupying a new spot
|
||||
if (lastMeasurementPacket != nullptr)
|
||||
packetPool.release(lastMeasurementPacket);
|
||||
|
||||
lastMeasurementPacket = packetPool.allocCopy(mp);
|
||||
}
|
||||
|
||||
return false; // Let others look at this message also if they want
|
||||
}
|
||||
|
||||
bool PowerTelemetryModule::getPowerTelemetry(meshtastic_Telemetry *m)
|
||||
{
|
||||
bool valid = false;
|
||||
m->time = getTime();
|
||||
m->which_variant = meshtastic_Telemetry_power_metrics_tag;
|
||||
|
||||
m->variant.power_metrics = meshtastic_PowerMetrics_init_zero;
|
||||
#if HAS_TELEMETRY
|
||||
if (ina219Sensor.hasSensor())
|
||||
valid = ina219Sensor.getMetrics(m);
|
||||
if (ina226Sensor.hasSensor())
|
||||
valid = ina226Sensor.getMetrics(m);
|
||||
if (ina260Sensor.hasSensor())
|
||||
valid = ina260Sensor.getMetrics(m);
|
||||
if (ina3221Sensor.hasSensor())
|
||||
valid = ina3221Sensor.getMetrics(m);
|
||||
if (max17048Sensor.hasSensor())
|
||||
valid = max17048Sensor.getMetrics(m);
|
||||
#endif
|
||||
|
||||
return valid;
|
||||
}
|
||||
|
||||
meshtastic_MeshPacket *PowerTelemetryModule::allocReply()
|
||||
{
|
||||
if (currentRequest) {
|
||||
auto req = *currentRequest;
|
||||
const auto &p = req.decoded;
|
||||
meshtastic_Telemetry scratch;
|
||||
meshtastic_Telemetry *decoded = NULL;
|
||||
memset(&scratch, 0, sizeof(scratch));
|
||||
if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &scratch)) {
|
||||
decoded = &scratch;
|
||||
} else {
|
||||
LOG_ERROR("Error decoding PowerTelemetry module!");
|
||||
return NULL;
|
||||
}
|
||||
// Check for a request for power metrics
|
||||
if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) {
|
||||
meshtastic_Telemetry m = meshtastic_Telemetry_init_zero;
|
||||
if (getPowerTelemetry(&m)) {
|
||||
LOG_INFO("Power telemetry reply to request");
|
||||
return allocDataProtobuf(m);
|
||||
} else {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool PowerTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
|
||||
{
|
||||
meshtastic_Telemetry m = meshtastic_Telemetry_init_zero;
|
||||
m.which_variant = meshtastic_Telemetry_power_metrics_tag;
|
||||
m.time = getTime();
|
||||
if (getPowerTelemetry(&m)) {
|
||||
LOG_INFO("Send: ch1_voltage=%f, ch1_current=%f, ch2_voltage=%f, ch2_current=%f, "
|
||||
"ch3_voltage=%f, ch3_current=%f",
|
||||
m.variant.power_metrics.ch1_voltage, m.variant.power_metrics.ch1_current, m.variant.power_metrics.ch2_voltage,
|
||||
m.variant.power_metrics.ch2_current, m.variant.power_metrics.ch3_voltage, m.variant.power_metrics.ch3_current);
|
||||
|
||||
sensor_read_error_count = 0;
|
||||
|
||||
meshtastic_MeshPacket *p = allocDataProtobuf(m);
|
||||
p->to = dest;
|
||||
p->decoded.want_response = false;
|
||||
if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR)
|
||||
p->priority = meshtastic_MeshPacket_Priority_RELIABLE;
|
||||
else
|
||||
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||
// release previous packet before occupying a new spot
|
||||
if (lastMeasurementPacket != nullptr)
|
||||
packetPool.release(lastMeasurementPacket);
|
||||
|
||||
lastMeasurementPacket = packetPool.allocCopy(*p);
|
||||
if (phoneOnly) {
|
||||
LOG_INFO("Send packet to phone");
|
||||
service->sendToPhone(p);
|
||||
} else {
|
||||
LOG_INFO("Send packet to mesh");
|
||||
service->sendToMesh(p, RX_SRC_LOCAL, true);
|
||||
|
||||
if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR && config.power.is_power_saving) {
|
||||
LOG_DEBUG("Start next execution in 5s then sleep");
|
||||
sleepOnNextExecution = true;
|
||||
setIntervalFromNow(5000);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "NodeDB.h"
|
||||
#include "ProtobufModule.h"
|
||||
#include <OLEDDisplay.h>
|
||||
#include <OLEDDisplayUi.h>
|
||||
|
||||
class PowerTelemetryModule : private concurrency::OSThread, public ProtobufModule<meshtastic_Telemetry>
|
||||
{
|
||||
CallbackObserver<PowerTelemetryModule, const meshtastic::Status *> nodeStatusObserver =
|
||||
CallbackObserver<PowerTelemetryModule, const meshtastic::Status *>(this, &PowerTelemetryModule::handleStatusUpdate);
|
||||
|
||||
public:
|
||||
PowerTelemetryModule()
|
||||
: concurrency::OSThread("PowerTelemetry"),
|
||||
ProtobufModule("PowerTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg)
|
||||
{
|
||||
lastMeasurementPacket = nullptr;
|
||||
nodeStatusObserver.observe(&nodeStatus->onNewStatus);
|
||||
setIntervalFromNow(10 * 1000);
|
||||
}
|
||||
virtual bool wantUIFrame() override;
|
||||
#if !HAS_SCREEN
|
||||
void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
#else
|
||||
virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override;
|
||||
#endif
|
||||
|
||||
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_Telemetry *p) override;
|
||||
virtual int32_t runOnce() override;
|
||||
/** Called to get current Power telemetry data
|
||||
@return true if it contains valid data
|
||||
*/
|
||||
bool getPowerTelemetry(meshtastic_Telemetry *m);
|
||||
virtual meshtastic_MeshPacket *allocReply() override;
|
||||
/**
|
||||
* Send our Telemetry into the mesh
|
||||
*/
|
||||
bool sendTelemetry(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false);
|
||||
|
||||
private:
|
||||
bool firstTime = 1;
|
||||
meshtastic_MeshPacket *lastMeasurementPacket;
|
||||
uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute
|
||||
uint32_t lastSentToMesh = 0;
|
||||
uint32_t lastSentToPhone = 0;
|
||||
uint32_t sensor_read_error_count = 0;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Worth noting that both the AHT10 and AHT20 are supported without alteration.
|
||||
*/
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<Adafruit_AHTX0.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "AHT10.h"
|
||||
#include "TelemetrySensor.h"
|
||||
|
||||
#include <Adafruit_AHTX0.h>
|
||||
#include <typeinfo>
|
||||
|
||||
AHT10Sensor::AHT10Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_AHT10, "AHT10") {}
|
||||
|
||||
bool AHT10Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
|
||||
{
|
||||
LOG_INFO("Init sensor: %s", sensorName);
|
||||
aht10 = Adafruit_AHTX0();
|
||||
status = aht10.begin(bus, 0, dev->address.address);
|
||||
|
||||
initI2CSensor();
|
||||
return status;
|
||||
}
|
||||
|
||||
bool AHT10Sensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
{
|
||||
LOG_DEBUG("AHT10 getMetrics");
|
||||
|
||||
sensors_event_t humidity, temp;
|
||||
aht10.getEvent(&humidity, &temp);
|
||||
|
||||
// prefer other sensors like bmp280, bmp3xx
|
||||
if (!measurement->variant.environment_metrics.has_temperature) {
|
||||
measurement->variant.environment_metrics.has_temperature = true;
|
||||
measurement->variant.environment_metrics.temperature = temp.temperature;
|
||||
}
|
||||
|
||||
if (!measurement->variant.environment_metrics.has_relative_humidity) {
|
||||
measurement->variant.environment_metrics.has_relative_humidity = true;
|
||||
measurement->variant.environment_metrics.relative_humidity = humidity.relative_humidity;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Worth noting that both the AHT10 and AHT20 are supported without alteration.
|
||||
*/
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<Adafruit_AHTX0.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include <Adafruit_AHTX0.h>
|
||||
|
||||
class AHT10Sensor : public TelemetrySensor
|
||||
{
|
||||
private:
|
||||
Adafruit_AHTX0 aht10;
|
||||
|
||||
public:
|
||||
AHT10Sensor();
|
||||
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
||||
virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,54 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<BH1750_WE.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "BH1750Sensor.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include <BH1750_WE.h>
|
||||
|
||||
#ifndef BH1750_SENSOR_MODE
|
||||
#define BH1750_SENSOR_MODE BH1750Mode::CHM
|
||||
#endif
|
||||
|
||||
BH1750Sensor::BH1750Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_BH1750, "BH1750") {}
|
||||
|
||||
bool BH1750Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
|
||||
{
|
||||
LOG_INFO("Init sensor: %s with mode %d", sensorName, BH1750_SENSOR_MODE);
|
||||
|
||||
bh1750 = BH1750_WE(bus, dev->address.address);
|
||||
status = bh1750.init();
|
||||
if (!status) {
|
||||
return status;
|
||||
}
|
||||
|
||||
bh1750.setMode(BH1750_SENSOR_MODE);
|
||||
|
||||
initI2CSensor();
|
||||
return status;
|
||||
}
|
||||
|
||||
bool BH1750Sensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
{
|
||||
|
||||
/* An OTH and OTH_2 measurement takes ~120 ms. I suggest to wait
|
||||
140 ms to be on the safe side.
|
||||
An OTL measurement takes about 16 ms. I suggest to wait 20 ms
|
||||
to be on the safe side. */
|
||||
if (BH1750_SENSOR_MODE == BH1750Mode::OTH || BH1750_SENSOR_MODE == BH1750Mode::OTH_2) {
|
||||
bh1750.setMode(BH1750_SENSOR_MODE);
|
||||
delay(140); // wait for measurement to be completed
|
||||
} else if (BH1750_SENSOR_MODE == BH1750Mode::OTL) {
|
||||
bh1750.setMode(BH1750_SENSOR_MODE);
|
||||
delay(20);
|
||||
}
|
||||
|
||||
measurement->variant.environment_metrics.has_lux = true;
|
||||
float lightIntensity = bh1750.getLux();
|
||||
|
||||
measurement->variant.environment_metrics.lux = lightIntensity;
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<BH1750_WE.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include <BH1750_WE.h>
|
||||
|
||||
class BH1750Sensor : public TelemetrySensor
|
||||
{
|
||||
private:
|
||||
BH1750_WE bh1750;
|
||||
|
||||
public:
|
||||
BH1750Sensor();
|
||||
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
||||
virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,45 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<Adafruit_BME280.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "BME280Sensor.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include <Adafruit_BME280.h>
|
||||
#include <typeinfo>
|
||||
|
||||
BME280Sensor::BME280Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_BME280, "BME280") {}
|
||||
|
||||
bool BME280Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
|
||||
{
|
||||
LOG_INFO("Init sensor: %s", sensorName);
|
||||
status = bme280.begin(dev->address.address, bus);
|
||||
if (!status) {
|
||||
return status;
|
||||
}
|
||||
|
||||
bme280.setSampling(Adafruit_BME280::MODE_FORCED,
|
||||
Adafruit_BME280::SAMPLING_X1, // Temp. oversampling
|
||||
Adafruit_BME280::SAMPLING_X1, // Pressure oversampling
|
||||
Adafruit_BME280::SAMPLING_X1, // Humidity oversampling
|
||||
Adafruit_BME280::FILTER_OFF, Adafruit_BME280::STANDBY_MS_1000);
|
||||
|
||||
initI2CSensor();
|
||||
return status;
|
||||
}
|
||||
|
||||
bool BME280Sensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
{
|
||||
measurement->variant.environment_metrics.has_temperature = true;
|
||||
measurement->variant.environment_metrics.has_relative_humidity = true;
|
||||
measurement->variant.environment_metrics.has_barometric_pressure = true;
|
||||
|
||||
LOG_DEBUG("BME280 getMetrics");
|
||||
bme280.takeForcedMeasurement();
|
||||
measurement->variant.environment_metrics.temperature = bme280.readTemperature();
|
||||
measurement->variant.environment_metrics.relative_humidity = bme280.readHumidity();
|
||||
measurement->variant.environment_metrics.barometric_pressure = bme280.readPressure() / 100.0F;
|
||||
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,20 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<Adafruit_BME280.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include <Adafruit_BME280.h>
|
||||
|
||||
class BME280Sensor : public TelemetrySensor
|
||||
{
|
||||
private:
|
||||
Adafruit_BME280 bme280;
|
||||
|
||||
public:
|
||||
BME280Sensor();
|
||||
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
||||
virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,148 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<bsec2.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "BME680Sensor.h"
|
||||
#include "FSCommon.h"
|
||||
#include "SPILock.h"
|
||||
#include "TelemetrySensor.h"
|
||||
|
||||
BME680Sensor::BME680Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_BME680, "BME680") {}
|
||||
|
||||
int32_t BME680Sensor::runOnce()
|
||||
{
|
||||
if (!bme680.run()) {
|
||||
checkStatus("runTrigger");
|
||||
}
|
||||
return 35;
|
||||
}
|
||||
|
||||
bool BME680Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
|
||||
{
|
||||
status = 0;
|
||||
if (!bme680.begin(dev->address.address, *bus))
|
||||
checkStatus("begin");
|
||||
|
||||
if (bme680.status == BSEC_OK) {
|
||||
status = 1;
|
||||
if (!bme680.setConfig(bsec_config)) {
|
||||
checkStatus("setConfig");
|
||||
status = 0;
|
||||
}
|
||||
loadState();
|
||||
if (!bme680.updateSubscription(sensorList, ARRAY_LEN(sensorList), BSEC_SAMPLE_RATE_LP)) {
|
||||
checkStatus("updateSubscription");
|
||||
status = 0;
|
||||
}
|
||||
LOG_INFO("Init sensor: %s with the BSEC Library version %d.%d.%d.%d ", sensorName, bme680.version.major,
|
||||
bme680.version.minor, bme680.version.major_bugfix, bme680.version.minor_bugfix);
|
||||
}
|
||||
|
||||
if (status == 0)
|
||||
LOG_DEBUG("BME680Sensor::runOnce: bme680.status %d", bme680.status);
|
||||
|
||||
initI2CSensor();
|
||||
return status;
|
||||
}
|
||||
|
||||
bool BME680Sensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
{
|
||||
if (bme680.getData(BSEC_OUTPUT_RAW_PRESSURE).signal == 0)
|
||||
return false;
|
||||
|
||||
measurement->variant.environment_metrics.has_temperature = true;
|
||||
measurement->variant.environment_metrics.has_relative_humidity = true;
|
||||
measurement->variant.environment_metrics.has_barometric_pressure = true;
|
||||
measurement->variant.environment_metrics.has_gas_resistance = true;
|
||||
measurement->variant.environment_metrics.has_iaq = true;
|
||||
|
||||
measurement->variant.environment_metrics.temperature = bme680.getData(BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE).signal;
|
||||
measurement->variant.environment_metrics.relative_humidity =
|
||||
bme680.getData(BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY).signal;
|
||||
measurement->variant.environment_metrics.barometric_pressure = bme680.getData(BSEC_OUTPUT_RAW_PRESSURE).signal;
|
||||
measurement->variant.environment_metrics.gas_resistance = bme680.getData(BSEC_OUTPUT_RAW_GAS).signal / 1000.0;
|
||||
// Check if we need to save state to filesystem (every STATE_SAVE_PERIOD ms)
|
||||
measurement->variant.environment_metrics.iaq = bme680.getData(BSEC_OUTPUT_IAQ).signal;
|
||||
updateState();
|
||||
return true;
|
||||
}
|
||||
|
||||
void BME680Sensor::loadState()
|
||||
{
|
||||
#ifdef FSCom
|
||||
spiLock->lock();
|
||||
auto file = FSCom.open(bsecConfigFileName, FILE_O_READ);
|
||||
if (file) {
|
||||
file.read((uint8_t *)&bsecState, BSEC_MAX_STATE_BLOB_SIZE);
|
||||
file.close();
|
||||
bme680.setState(bsecState);
|
||||
LOG_INFO("%s state read from %s", sensorName, bsecConfigFileName);
|
||||
} else {
|
||||
LOG_INFO("No %s state found (File: %s)", sensorName, bsecConfigFileName);
|
||||
}
|
||||
spiLock->unlock();
|
||||
#else
|
||||
LOG_ERROR("ERROR: Filesystem not implemented");
|
||||
#endif
|
||||
}
|
||||
|
||||
void BME680Sensor::updateState()
|
||||
{
|
||||
#ifdef FSCom
|
||||
spiLock->lock();
|
||||
bool update = false;
|
||||
if (stateUpdateCounter == 0) {
|
||||
/* First state update when IAQ accuracy is >= 3 */
|
||||
accuracy = bme680.getData(BSEC_OUTPUT_IAQ).accuracy;
|
||||
if (accuracy >= 2) {
|
||||
LOG_DEBUG("%s state update IAQ accuracy %u >= 2", sensorName, accuracy);
|
||||
update = true;
|
||||
stateUpdateCounter++;
|
||||
} else {
|
||||
LOG_DEBUG("%s not updated, IAQ accuracy is %u < 2", sensorName, accuracy);
|
||||
}
|
||||
} else {
|
||||
/* Update every STATE_SAVE_PERIOD minutes */
|
||||
if ((stateUpdateCounter * STATE_SAVE_PERIOD) < millis()) {
|
||||
LOG_DEBUG("%s state update every %d minutes", sensorName, STATE_SAVE_PERIOD / 60000);
|
||||
update = true;
|
||||
stateUpdateCounter++;
|
||||
}
|
||||
}
|
||||
|
||||
if (update) {
|
||||
bme680.getState(bsecState);
|
||||
if (FSCom.exists(bsecConfigFileName) && !FSCom.remove(bsecConfigFileName)) {
|
||||
LOG_WARN("Can't remove old state file");
|
||||
}
|
||||
auto file = FSCom.open(bsecConfigFileName, FILE_O_WRITE);
|
||||
if (file) {
|
||||
LOG_INFO("%s state write to %s", sensorName, bsecConfigFileName);
|
||||
file.write((uint8_t *)&bsecState, BSEC_MAX_STATE_BLOB_SIZE);
|
||||
file.flush();
|
||||
file.close();
|
||||
} else {
|
||||
LOG_INFO("Can't write %s state (File: %s)", sensorName, bsecConfigFileName);
|
||||
}
|
||||
}
|
||||
spiLock->unlock();
|
||||
#else
|
||||
LOG_ERROR("ERROR: Filesystem not implemented");
|
||||
#endif
|
||||
}
|
||||
|
||||
void BME680Sensor::checkStatus(const char *functionName)
|
||||
{
|
||||
if (bme680.status < BSEC_OK)
|
||||
LOG_ERROR("%s BSEC2 code: %d", functionName, bme680.status);
|
||||
else if (bme680.status > BSEC_OK)
|
||||
LOG_WARN("%s BSEC2 code: %d", functionName, bme680.status);
|
||||
|
||||
if (bme680.sensor.status < BME68X_OK)
|
||||
LOG_ERROR("%s BME68X code: %d", functionName, bme680.sensor.status);
|
||||
else if (bme680.sensor.status > BME68X_OK)
|
||||
LOG_WARN("%s BME68X code: %d", functionName, bme680.sensor.status);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,45 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<bsec2.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include <bsec2.h>
|
||||
|
||||
#define STATE_SAVE_PERIOD UINT32_C(360 * 60 * 1000) // That's 6 hours worth of millis()
|
||||
|
||||
const uint8_t bsec_config[] = {
|
||||
#include "config/bme680/bme680_iaq_33v_3s_4d/bsec_iaq.txt"
|
||||
};
|
||||
|
||||
class BME680Sensor : public TelemetrySensor
|
||||
{
|
||||
private:
|
||||
Bsec2 bme680;
|
||||
|
||||
protected:
|
||||
const char *bsecConfigFileName = "/prefs/bsec.dat";
|
||||
uint8_t bsecState[BSEC_MAX_STATE_BLOB_SIZE] = {0};
|
||||
uint8_t accuracy = 0;
|
||||
uint16_t stateUpdateCounter = 0;
|
||||
bsecSensor sensorList[9] = {BSEC_OUTPUT_IAQ,
|
||||
BSEC_OUTPUT_RAW_TEMPERATURE,
|
||||
BSEC_OUTPUT_RAW_PRESSURE,
|
||||
BSEC_OUTPUT_RAW_HUMIDITY,
|
||||
BSEC_OUTPUT_RAW_GAS,
|
||||
BSEC_OUTPUT_STABILIZATION_STATUS,
|
||||
BSEC_OUTPUT_RUN_IN_STATUS,
|
||||
BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE,
|
||||
BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY};
|
||||
void loadState();
|
||||
void updateState();
|
||||
void checkStatus(const char *functionName);
|
||||
|
||||
public:
|
||||
BME680Sensor();
|
||||
virtual int32_t runOnce() override;
|
||||
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
||||
virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,36 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<Adafruit_BMP085.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "BMP085Sensor.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include <Adafruit_BMP085.h>
|
||||
#include <typeinfo>
|
||||
|
||||
BMP085Sensor::BMP085Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_BMP085, "BMP085") {}
|
||||
|
||||
bool BMP085Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
|
||||
{
|
||||
LOG_INFO("Init sensor: %s", sensorName);
|
||||
|
||||
bmp085 = Adafruit_BMP085();
|
||||
status = bmp085.begin(dev->address.address, bus);
|
||||
|
||||
initI2CSensor();
|
||||
return status;
|
||||
}
|
||||
|
||||
bool BMP085Sensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
{
|
||||
measurement->variant.environment_metrics.has_temperature = true;
|
||||
measurement->variant.environment_metrics.has_barometric_pressure = true;
|
||||
|
||||
LOG_DEBUG("BMP085 getMetrics");
|
||||
measurement->variant.environment_metrics.temperature = bmp085.readTemperature();
|
||||
measurement->variant.environment_metrics.barometric_pressure = bmp085.readPressure() / 100.0F;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,20 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<Adafruit_BMP085.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include <Adafruit_BMP085.h>
|
||||
|
||||
class BMP085Sensor : public TelemetrySensor
|
||||
{
|
||||
private:
|
||||
Adafruit_BMP085 bmp085;
|
||||
|
||||
public:
|
||||
BMP085Sensor();
|
||||
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
||||
virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,45 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<Adafruit_BMP280.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "BMP280Sensor.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include <Adafruit_BMP280.h>
|
||||
#include <typeinfo>
|
||||
|
||||
BMP280Sensor::BMP280Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_BMP280, "BMP280") {}
|
||||
|
||||
bool BMP280Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
|
||||
{
|
||||
LOG_INFO("Init sensor: %s", sensorName);
|
||||
|
||||
bmp280 = Adafruit_BMP280(bus);
|
||||
status = bmp280.begin(dev->address.address);
|
||||
if (!status) {
|
||||
return status;
|
||||
}
|
||||
|
||||
bmp280.setSampling(Adafruit_BMP280::MODE_FORCED,
|
||||
Adafruit_BMP280::SAMPLING_X1, // Temp. oversampling
|
||||
Adafruit_BMP280::SAMPLING_X1, // Pressure oversampling
|
||||
Adafruit_BMP280::FILTER_OFF, Adafruit_BMP280::STANDBY_MS_1000);
|
||||
|
||||
initI2CSensor();
|
||||
return status;
|
||||
}
|
||||
|
||||
bool BMP280Sensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
{
|
||||
measurement->variant.environment_metrics.has_temperature = true;
|
||||
measurement->variant.environment_metrics.has_barometric_pressure = true;
|
||||
|
||||
LOG_DEBUG("BMP280 getMetrics");
|
||||
bmp280.takeForcedMeasurement();
|
||||
measurement->variant.environment_metrics.temperature = bmp280.readTemperature();
|
||||
measurement->variant.environment_metrics.barometric_pressure = bmp280.readPressure() / 100.0F;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,20 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<Adafruit_BMP280.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include <Adafruit_BMP280.h>
|
||||
|
||||
class BMP280Sensor : public TelemetrySensor
|
||||
{
|
||||
private:
|
||||
Adafruit_BMP280 bmp280;
|
||||
|
||||
public:
|
||||
BMP280Sensor();
|
||||
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
||||
virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,88 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<Adafruit_BMP3XX.h>)
|
||||
|
||||
#include "BMP3XXSensor.h"
|
||||
|
||||
BMP3XXSensor::BMP3XXSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_BMP3XX, "BMP3XX") {}
|
||||
|
||||
bool BMP3XXSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
|
||||
{
|
||||
LOG_INFO("Init sensor: %s", sensorName);
|
||||
|
||||
// Get a singleton instance and initialise the bmp3xx
|
||||
if (bmp3xx == nullptr) {
|
||||
bmp3xx = BMP3XXSingleton::GetInstance();
|
||||
}
|
||||
status = bmp3xx->begin_I2C(dev->address.address, bus);
|
||||
if (!status) {
|
||||
return status;
|
||||
}
|
||||
|
||||
// set up oversampling and filter initialization
|
||||
bmp3xx->setTemperatureOversampling(BMP3_OVERSAMPLING_4X);
|
||||
bmp3xx->setPressureOversampling(BMP3_OVERSAMPLING_8X);
|
||||
bmp3xx->setIIRFilterCoeff(BMP3_IIR_FILTER_COEFF_3);
|
||||
bmp3xx->setOutputDataRate(BMP3_ODR_25_HZ);
|
||||
|
||||
// take a couple of initial readings to settle the sensor filters
|
||||
for (int i = 0; i < 3; i++) {
|
||||
bmp3xx->performReading();
|
||||
}
|
||||
initI2CSensor();
|
||||
return status;
|
||||
}
|
||||
|
||||
bool BMP3XXSensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
{
|
||||
if (bmp3xx == nullptr) {
|
||||
bmp3xx = BMP3XXSingleton::GetInstance();
|
||||
}
|
||||
if ((int)measurement->which_variant == meshtastic_Telemetry_environment_metrics_tag) {
|
||||
bmp3xx->performReading();
|
||||
|
||||
measurement->variant.environment_metrics.has_temperature = true;
|
||||
measurement->variant.environment_metrics.has_barometric_pressure = true;
|
||||
measurement->variant.environment_metrics.has_relative_humidity = false;
|
||||
|
||||
measurement->variant.environment_metrics.temperature = static_cast<float>(bmp3xx->temperature);
|
||||
measurement->variant.environment_metrics.barometric_pressure = static_cast<float>(bmp3xx->pressure) / 100.0F;
|
||||
measurement->variant.environment_metrics.relative_humidity = 0.0f;
|
||||
|
||||
LOG_DEBUG("BMP3XX getMetrics id: %i temp: %.1f press %.1f", measurement->which_variant,
|
||||
measurement->variant.environment_metrics.temperature,
|
||||
measurement->variant.environment_metrics.barometric_pressure);
|
||||
} else {
|
||||
LOG_DEBUG("BMP3XX getMetrics id: %i", measurement->which_variant);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get a singleton wrapper for an Adafruit_bmp3xx
|
||||
BMP3XXSingleton *BMP3XXSingleton::GetInstance()
|
||||
{
|
||||
if (pinstance == nullptr) {
|
||||
pinstance = new BMP3XXSingleton();
|
||||
}
|
||||
return pinstance;
|
||||
}
|
||||
|
||||
BMP3XXSingleton::BMP3XXSingleton() {}
|
||||
|
||||
BMP3XXSingleton::~BMP3XXSingleton() {}
|
||||
|
||||
BMP3XXSingleton *BMP3XXSingleton::pinstance{nullptr};
|
||||
|
||||
bool BMP3XXSingleton::performReading()
|
||||
{
|
||||
bool result = Adafruit_BMP3XX::performReading();
|
||||
if (result) {
|
||||
double atmospheric = this->pressure / 100.0;
|
||||
altitudeAmslMetres = 44330.0 * (1.0 - pow(atmospheric / SEAL_LEVEL_HPA, 0.1903));
|
||||
} else {
|
||||
altitudeAmslMetres = 0.0;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,55 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<Adafruit_BMP3XX.h>)
|
||||
|
||||
#ifndef _BMP3XX_SENSOR_H
|
||||
#define _BMP3XX_SENSOR_H
|
||||
|
||||
#define SEAL_LEVEL_HPA 1013.2f
|
||||
|
||||
#include "TelemetrySensor.h"
|
||||
#include <Adafruit_BMP3XX.h>
|
||||
#include <typeinfo>
|
||||
|
||||
// Singleton wrapper for the Adafruit_BMP3XX class
|
||||
class BMP3XXSingleton : public Adafruit_BMP3XX
|
||||
{
|
||||
private:
|
||||
static BMP3XXSingleton *pinstance;
|
||||
|
||||
protected:
|
||||
BMP3XXSingleton();
|
||||
~BMP3XXSingleton();
|
||||
|
||||
public:
|
||||
// Create a singleton instance (not thread safe)
|
||||
static BMP3XXSingleton *GetInstance();
|
||||
|
||||
// Singletons should not be cloneable.
|
||||
BMP3XXSingleton(BMP3XXSingleton &other) = delete;
|
||||
|
||||
// Singletons should not be assignable.
|
||||
void operator=(const BMP3XXSingleton &) = delete;
|
||||
|
||||
// Performs a full reading of all sensors in the BMP3XX. Assigns
|
||||
// the internal temperature, pressure and altitudeAmsl variables
|
||||
bool performReading();
|
||||
|
||||
// Altitude in metres above mean sea level, assigned after calling performReading()
|
||||
double altitudeAmslMetres = 0.0f;
|
||||
};
|
||||
|
||||
class BMP3XXSensor : public TelemetrySensor
|
||||
{
|
||||
protected:
|
||||
BMP3XXSingleton *bmp3xx = nullptr;
|
||||
|
||||
public:
|
||||
BMP3XXSensor();
|
||||
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
||||
virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Support for the ClimateGuard RadSens Dosimeter
|
||||
* A fun and educational sensor for Meshtastic; not for safety critical applications.
|
||||
*/
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "CGRadSensSensor.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include <Wire.h>
|
||||
#include <typeinfo>
|
||||
|
||||
CGRadSensSensor::CGRadSensSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_RADSENS, "RadSens") {}
|
||||
|
||||
bool CGRadSensSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
|
||||
{
|
||||
// Initialize the sensor following the same pattern as RCWL9620Sensor
|
||||
LOG_INFO("Init sensor: %s", sensorName);
|
||||
status = true;
|
||||
begin(bus, dev->address.address);
|
||||
initI2CSensor();
|
||||
return status;
|
||||
}
|
||||
|
||||
void CGRadSensSensor::begin(TwoWire *wire, uint8_t addr)
|
||||
{
|
||||
// Store the Wire and address to the sensor following the same pattern as RCWL9620Sensor
|
||||
_wire = wire;
|
||||
_addr = addr;
|
||||
_wire->begin();
|
||||
}
|
||||
|
||||
float CGRadSensSensor::getStaticRadiation()
|
||||
{
|
||||
// Read a register, following the same pattern as the RCWL9620Sensor
|
||||
_wire->beginTransmission(_addr); // Transfer data to addr.
|
||||
_wire->write(0x06); // Radiation intensity (static period T = 500 sec)
|
||||
if (_wire->endTransmission() == 0) {
|
||||
if (_wire->requestFrom(_addr, (uint8_t)3)) {
|
||||
; // Request 3 bytes
|
||||
uint32_t data = _wire->read();
|
||||
data <<= 8;
|
||||
data |= _wire->read();
|
||||
data <<= 8;
|
||||
data |= _wire->read();
|
||||
|
||||
// As per the data sheet for the RadSens
|
||||
// Register 0x06 contains the reading in 0.1 * μR / h
|
||||
float microRadPerHr = float(data) / 10.0;
|
||||
return microRadPerHr;
|
||||
}
|
||||
}
|
||||
return -1.0;
|
||||
}
|
||||
|
||||
bool CGRadSensSensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
{
|
||||
// Store the meansurement in the the appropriate fields of the protobuf
|
||||
measurement->variant.environment_metrics.has_radiation = true;
|
||||
|
||||
LOG_DEBUG("CGRADSENS getMetrics");
|
||||
measurement->variant.environment_metrics.radiation = getStaticRadiation();
|
||||
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Support for the ClimateGuard RadSens Dosimeter
|
||||
* A fun and educational sensor for Meshtastic; not for safety critical applications.
|
||||
*/
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include <Wire.h>
|
||||
|
||||
class CGRadSensSensor : public TelemetrySensor
|
||||
{
|
||||
private:
|
||||
uint8_t _addr = 0x66;
|
||||
TwoWire *_wire = &Wire;
|
||||
|
||||
protected:
|
||||
void begin(TwoWire *wire = &Wire, uint8_t addr = 0x66);
|
||||
float getStaticRadiation();
|
||||
|
||||
public:
|
||||
CGRadSensSensor();
|
||||
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
||||
virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,13 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
||||
|
||||
#pragma once
|
||||
|
||||
class CurrentSensor
|
||||
{
|
||||
public:
|
||||
virtual int16_t getCurrentMa() = 0;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,52 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<DFRobot_RainfallSensor.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "DFRobotGravitySensor.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include <DFRobot_RainfallSensor.h>
|
||||
#include <string>
|
||||
|
||||
DFRobotGravitySensor::DFRobotGravitySensor() : TelemetrySensor(meshtastic_TelemetrySensorType_DFROBOT_RAIN, "DFROBOT_RAIN") {}
|
||||
|
||||
DFRobotGravitySensor::~DFRobotGravitySensor()
|
||||
{
|
||||
if (gravity) {
|
||||
delete gravity;
|
||||
gravity = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool DFRobotGravitySensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
|
||||
{
|
||||
LOG_INFO("Init sensor: %s", sensorName);
|
||||
|
||||
gravity = new DFRobot_RainfallSensor_I2C(bus);
|
||||
status = gravity->begin();
|
||||
|
||||
LOG_DEBUG("%s VID: %x, PID: %x, Version: %s", sensorName, gravity->vid, gravity->pid, gravity->getFirmwareVersion().c_str());
|
||||
|
||||
initI2CSensor();
|
||||
return status;
|
||||
}
|
||||
|
||||
bool DFRobotGravitySensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
{
|
||||
if (!gravity) {
|
||||
LOG_ERROR("DFRobotGravitySensor not initialized");
|
||||
return false;
|
||||
}
|
||||
|
||||
measurement->variant.environment_metrics.has_rainfall_1h = true;
|
||||
measurement->variant.environment_metrics.has_rainfall_24h = true;
|
||||
|
||||
measurement->variant.environment_metrics.rainfall_1h = gravity->getRainfall(1);
|
||||
measurement->variant.environment_metrics.rainfall_24h = gravity->getRainfall(24);
|
||||
|
||||
LOG_INFO("Rain 1h: %f mm", measurement->variant.environment_metrics.rainfall_1h);
|
||||
LOG_INFO("Rain 24h: %f mm", measurement->variant.environment_metrics.rainfall_24h);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#ifndef _MT_DFROBOTGRAVITYSENSOR_H
|
||||
#define _MT_DFROBOTGRAVITYSENSOR_H
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<DFRobot_RainfallSensor.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include <DFRobot_RainfallSensor.h>
|
||||
#include <string>
|
||||
|
||||
class DFRobotGravitySensor : public TelemetrySensor
|
||||
{
|
||||
private:
|
||||
DFRobot_RainfallSensor_I2C *gravity = nullptr;
|
||||
|
||||
public:
|
||||
DFRobotGravitySensor();
|
||||
~DFRobotGravitySensor();
|
||||
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
||||
virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
|
||||
};
|
||||
|
||||
#endif
|
||||
#endif
|
||||
@@ -0,0 +1,54 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<DFRobot_LarkWeatherStation.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "DFRobotLarkSensor.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include "gps/GeoCoord.h"
|
||||
#include <DFRobot_LarkWeatherStation.h>
|
||||
#include <string>
|
||||
|
||||
DFRobotLarkSensor::DFRobotLarkSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_DFROBOT_LARK, "DFROBOT_LARK") {}
|
||||
|
||||
bool DFRobotLarkSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
|
||||
{
|
||||
LOG_INFO("Init sensor: %s", sensorName);
|
||||
lark = DFRobot_LarkWeatherStation_I2C(dev->address.address, bus);
|
||||
|
||||
if (lark.begin() == 0) // DFRobotLarkSensor init
|
||||
{
|
||||
LOG_DEBUG("DFRobotLarkSensor Init Succeed");
|
||||
status = true;
|
||||
} else {
|
||||
LOG_ERROR("DFRobotLarkSensor Init Failed");
|
||||
status = false;
|
||||
}
|
||||
initI2CSensor();
|
||||
return status;
|
||||
}
|
||||
|
||||
bool DFRobotLarkSensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
{
|
||||
measurement->variant.environment_metrics.has_temperature = true;
|
||||
measurement->variant.environment_metrics.has_relative_humidity = true;
|
||||
measurement->variant.environment_metrics.has_wind_speed = true;
|
||||
measurement->variant.environment_metrics.has_wind_direction = true;
|
||||
measurement->variant.environment_metrics.has_barometric_pressure = true;
|
||||
|
||||
measurement->variant.environment_metrics.temperature = lark.getValue("Temp").toFloat();
|
||||
measurement->variant.environment_metrics.relative_humidity = lark.getValue("Humi").toFloat();
|
||||
measurement->variant.environment_metrics.wind_speed = lark.getValue("Speed").toFloat();
|
||||
measurement->variant.environment_metrics.wind_direction = GeoCoord::bearingToDegrees(lark.getValue("Dir").c_str());
|
||||
measurement->variant.environment_metrics.barometric_pressure = lark.getValue("Pressure").toFloat();
|
||||
|
||||
LOG_INFO("Temperature: %f", measurement->variant.environment_metrics.temperature);
|
||||
LOG_INFO("Humidity: %f", measurement->variant.environment_metrics.relative_humidity);
|
||||
LOG_INFO("Wind Speed: %f", measurement->variant.environment_metrics.wind_speed);
|
||||
LOG_INFO("Wind Direction: %d", measurement->variant.environment_metrics.wind_direction);
|
||||
LOG_INFO("Barometric Pressure: %f", measurement->variant.environment_metrics.barometric_pressure);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#ifndef _MT_DFROBOTLARKSENSOR_H
|
||||
#define _MT_DFROBOTLARKSENSOR_H
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<DFRobot_LarkWeatherStation.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include <DFRobot_LarkWeatherStation.h>
|
||||
#include <string>
|
||||
|
||||
class DFRobotLarkSensor : public TelemetrySensor
|
||||
{
|
||||
private:
|
||||
DFRobot_LarkWeatherStation_I2C lark = DFRobot_LarkWeatherStation_I2C();
|
||||
|
||||
public:
|
||||
DFRobotLarkSensor();
|
||||
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
||||
virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
|
||||
};
|
||||
|
||||
#endif
|
||||
#endif
|
||||
@@ -0,0 +1,44 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<Adafruit_DPS310.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "DPS310Sensor.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include <Adafruit_DPS310.h>
|
||||
|
||||
DPS310Sensor::DPS310Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_DPS310, "DPS310") {}
|
||||
|
||||
bool DPS310Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
|
||||
{
|
||||
LOG_INFO("Init sensor: %s", sensorName);
|
||||
status = dps310.begin_I2C(dev->address.address, bus);
|
||||
if (!status) {
|
||||
return status;
|
||||
}
|
||||
|
||||
dps310.configurePressure(DPS310_1HZ, DPS310_4SAMPLES);
|
||||
dps310.configureTemperature(DPS310_1HZ, DPS310_4SAMPLES);
|
||||
dps310.setMode(DPS310_CONT_PRESTEMP);
|
||||
|
||||
initI2CSensor();
|
||||
return status;
|
||||
}
|
||||
|
||||
bool DPS310Sensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
{
|
||||
sensors_event_t temp, press;
|
||||
|
||||
if (!dps310.getEvents(&temp, &press)) {
|
||||
LOG_DEBUG("DPS310 getEvents no data");
|
||||
return false;
|
||||
}
|
||||
|
||||
measurement->variant.environment_metrics.has_temperature = true;
|
||||
measurement->variant.environment_metrics.has_barometric_pressure = true;
|
||||
measurement->variant.environment_metrics.temperature = temp.temperature;
|
||||
measurement->variant.environment_metrics.barometric_pressure = press.pressure;
|
||||
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,20 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<Adafruit_DPS310.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include <Adafruit_DPS310.h>
|
||||
|
||||
class DPS310Sensor : public TelemetrySensor
|
||||
{
|
||||
private:
|
||||
Adafruit_DPS310 dps310;
|
||||
|
||||
public:
|
||||
DPS310Sensor();
|
||||
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
||||
virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,53 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<Adafruit_INA219.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "INA219Sensor.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include <Adafruit_INA219.h>
|
||||
|
||||
#ifndef INA219_MULTIPLIER
|
||||
#define INA219_MULTIPLIER 1.0f
|
||||
#endif
|
||||
|
||||
INA219Sensor::INA219Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_INA219, "INA219") {}
|
||||
|
||||
int32_t INA219Sensor::runOnce()
|
||||
{
|
||||
LOG_INFO("Init sensor: %s", sensorName);
|
||||
if (!hasSensor()) {
|
||||
return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
|
||||
}
|
||||
if (!ina219.success()) {
|
||||
ina219 = Adafruit_INA219(nodeTelemetrySensorsMap[sensorType].first);
|
||||
status = ina219.begin(nodeTelemetrySensorsMap[sensorType].second);
|
||||
} else {
|
||||
status = ina219.success();
|
||||
}
|
||||
return initI2CSensor();
|
||||
}
|
||||
|
||||
void INA219Sensor::setup() {}
|
||||
|
||||
bool INA219Sensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
{
|
||||
measurement->variant.environment_metrics.has_voltage = true;
|
||||
measurement->variant.environment_metrics.has_current = true;
|
||||
|
||||
measurement->variant.environment_metrics.voltage = ina219.getBusVoltage_V();
|
||||
measurement->variant.environment_metrics.current = ina219.getCurrent_mA() * INA219_MULTIPLIER;
|
||||
return true;
|
||||
}
|
||||
|
||||
uint16_t INA219Sensor::getBusVoltageMv()
|
||||
{
|
||||
return lround(ina219.getBusVoltage_V() * 1000);
|
||||
}
|
||||
|
||||
int16_t INA219Sensor::getCurrentMa()
|
||||
{
|
||||
return lround(ina219.getCurrent_mA());
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,27 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<Adafruit_INA219.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "CurrentSensor.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include "VoltageSensor.h"
|
||||
#include <Adafruit_INA219.h>
|
||||
|
||||
class INA219Sensor : public TelemetrySensor, VoltageSensor, CurrentSensor
|
||||
{
|
||||
private:
|
||||
Adafruit_INA219 ina219;
|
||||
|
||||
protected:
|
||||
virtual void setup() override;
|
||||
|
||||
public:
|
||||
INA219Sensor();
|
||||
virtual int32_t runOnce() override;
|
||||
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
||||
virtual uint16_t getBusVoltageMv() override;
|
||||
virtual int16_t getCurrentMa() override;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,84 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include("INA226.h")
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "INA226.h"
|
||||
#include "INA226Sensor.h"
|
||||
#include "TelemetrySensor.h"
|
||||
|
||||
INA226Sensor::INA226Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_INA226, "INA226") {}
|
||||
|
||||
int32_t INA226Sensor::runOnce()
|
||||
{
|
||||
LOG_INFO("Init sensor: %s", sensorName);
|
||||
if (!hasSensor()) {
|
||||
return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
|
||||
}
|
||||
|
||||
begin(nodeTelemetrySensorsMap[sensorType].second, nodeTelemetrySensorsMap[sensorType].first);
|
||||
|
||||
if (!status) {
|
||||
status = ina226.begin();
|
||||
}
|
||||
return initI2CSensor();
|
||||
}
|
||||
|
||||
void INA226Sensor::setup() {}
|
||||
|
||||
void INA226Sensor::begin(TwoWire *wire, uint8_t addr)
|
||||
{
|
||||
_wire = wire;
|
||||
_addr = addr;
|
||||
ina226 = INA226(_addr, _wire);
|
||||
_wire->begin();
|
||||
ina226.setMaxCurrentShunt(0.8, 0.100);
|
||||
}
|
||||
|
||||
bool INA226Sensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
{
|
||||
switch (measurement->which_variant) {
|
||||
case meshtastic_Telemetry_environment_metrics_tag:
|
||||
return getEnvironmentMetrics(measurement);
|
||||
|
||||
case meshtastic_Telemetry_power_metrics_tag:
|
||||
return getPowerMetrics(measurement);
|
||||
}
|
||||
|
||||
// unsupported metric
|
||||
return false;
|
||||
}
|
||||
|
||||
bool INA226Sensor::getEnvironmentMetrics(meshtastic_Telemetry *measurement)
|
||||
{
|
||||
measurement->variant.environment_metrics.has_voltage = true;
|
||||
measurement->variant.environment_metrics.has_current = true;
|
||||
|
||||
measurement->variant.environment_metrics.voltage = ina226.getBusVoltage();
|
||||
measurement->variant.environment_metrics.current = ina226.getCurrent_mA();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool INA226Sensor::getPowerMetrics(meshtastic_Telemetry *measurement)
|
||||
{
|
||||
measurement->variant.power_metrics.has_ch1_voltage = true;
|
||||
measurement->variant.power_metrics.has_ch1_current = true;
|
||||
|
||||
measurement->variant.power_metrics.ch1_voltage = ina226.getBusVoltage();
|
||||
measurement->variant.power_metrics.ch1_current = ina226.getCurrent_mA();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
uint16_t INA226Sensor::getBusVoltageMv()
|
||||
{
|
||||
return lround(ina226.getBusVoltage() * 1000);
|
||||
}
|
||||
|
||||
int16_t INA226Sensor::getCurrentMa()
|
||||
{
|
||||
return lround(ina226.getCurrent_mA());
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,33 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "CurrentSensor.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include "VoltageSensor.h"
|
||||
#include <INA226.h>
|
||||
|
||||
class INA226Sensor : public TelemetrySensor, VoltageSensor, CurrentSensor
|
||||
{
|
||||
private:
|
||||
uint8_t _addr = INA_ADDR;
|
||||
TwoWire *_wire = &Wire;
|
||||
INA226 ina226 = INA226(_addr, _wire);
|
||||
|
||||
bool getEnvironmentMetrics(meshtastic_Telemetry *measurement);
|
||||
bool getPowerMetrics(meshtastic_Telemetry *measurement);
|
||||
|
||||
protected:
|
||||
virtual void setup() override;
|
||||
void begin(TwoWire *wire = &Wire, uint8_t addr = INA_ADDR);
|
||||
|
||||
public:
|
||||
INA226Sensor();
|
||||
virtual int32_t runOnce() override;
|
||||
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
||||
virtual uint16_t getBusVoltageMv() override;
|
||||
virtual int16_t getCurrentMa() override;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,43 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<Adafruit_INA260.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "INA260Sensor.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include <Adafruit_INA260.h>
|
||||
|
||||
INA260Sensor::INA260Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_INA260, "INA260") {}
|
||||
|
||||
int32_t INA260Sensor::runOnce()
|
||||
{
|
||||
LOG_INFO("Init sensor: %s", sensorName);
|
||||
if (!hasSensor()) {
|
||||
return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
|
||||
}
|
||||
|
||||
if (!status) {
|
||||
status = ina260.begin(nodeTelemetrySensorsMap[sensorType].first, nodeTelemetrySensorsMap[sensorType].second);
|
||||
}
|
||||
return initI2CSensor();
|
||||
}
|
||||
|
||||
void INA260Sensor::setup() {}
|
||||
|
||||
bool INA260Sensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
{
|
||||
measurement->variant.environment_metrics.has_voltage = true;
|
||||
measurement->variant.environment_metrics.has_current = true;
|
||||
|
||||
// mV conversion to V
|
||||
measurement->variant.environment_metrics.voltage = ina260.readBusVoltage() / 1000;
|
||||
measurement->variant.environment_metrics.current = ina260.readCurrent();
|
||||
return true;
|
||||
}
|
||||
|
||||
uint16_t INA260Sensor::getBusVoltageMv()
|
||||
{
|
||||
return lround(ina260.readBusVoltage());
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,25 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<Adafruit_INA260.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include "VoltageSensor.h"
|
||||
#include <Adafruit_INA260.h>
|
||||
|
||||
class INA260Sensor : public TelemetrySensor, VoltageSensor
|
||||
{
|
||||
private:
|
||||
Adafruit_INA260 ina260 = Adafruit_INA260();
|
||||
|
||||
protected:
|
||||
virtual void setup() override;
|
||||
|
||||
public:
|
||||
INA260Sensor();
|
||||
virtual int32_t runOnce() override;
|
||||
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
||||
virtual uint16_t getBusVoltageMv() override;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,110 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<INA3221.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "INA3221Sensor.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include <INA3221.h>
|
||||
|
||||
INA3221Sensor::INA3221Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_INA3221, "INA3221"){};
|
||||
|
||||
int32_t INA3221Sensor::runOnce()
|
||||
{
|
||||
LOG_INFO("Init sensor: %s", sensorName);
|
||||
if (!hasSensor()) {
|
||||
return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
|
||||
}
|
||||
if (!status) {
|
||||
ina3221.begin(nodeTelemetrySensorsMap[sensorType].second);
|
||||
ina3221.setShuntRes(100, 100, 100); // 0.1 Ohm shunt resistors
|
||||
status = true;
|
||||
} else {
|
||||
status = true;
|
||||
}
|
||||
return initI2CSensor();
|
||||
};
|
||||
|
||||
void INA3221Sensor::setup() {}
|
||||
|
||||
struct _INA3221Measurement INA3221Sensor::getMeasurement(ina3221_ch_t ch)
|
||||
{
|
||||
struct _INA3221Measurement measurement;
|
||||
|
||||
measurement.voltage = ina3221.getVoltage(ch);
|
||||
measurement.current = ina3221.getCurrent(ch);
|
||||
|
||||
return measurement;
|
||||
}
|
||||
|
||||
struct _INA3221Measurements INA3221Sensor::getMeasurements()
|
||||
{
|
||||
struct _INA3221Measurements measurements;
|
||||
|
||||
// INA3221 has 3 channels starting from 0
|
||||
for (int i = 0; i < 3; i++) {
|
||||
measurements.measurements[i] = getMeasurement((ina3221_ch_t)i);
|
||||
}
|
||||
|
||||
return measurements;
|
||||
}
|
||||
|
||||
bool INA3221Sensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
{
|
||||
switch (measurement->which_variant) {
|
||||
case meshtastic_Telemetry_environment_metrics_tag:
|
||||
return getEnvironmentMetrics(measurement);
|
||||
|
||||
case meshtastic_Telemetry_power_metrics_tag:
|
||||
return getPowerMetrics(measurement);
|
||||
}
|
||||
|
||||
// unsupported metric
|
||||
return false;
|
||||
}
|
||||
|
||||
bool INA3221Sensor::getEnvironmentMetrics(meshtastic_Telemetry *measurement)
|
||||
{
|
||||
struct _INA3221Measurement m = getMeasurement(ENV_CH);
|
||||
|
||||
measurement->variant.environment_metrics.has_voltage = true;
|
||||
measurement->variant.environment_metrics.has_current = true;
|
||||
|
||||
measurement->variant.environment_metrics.voltage = m.voltage;
|
||||
measurement->variant.environment_metrics.current = m.current;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool INA3221Sensor::getPowerMetrics(meshtastic_Telemetry *measurement)
|
||||
{
|
||||
struct _INA3221Measurements m = getMeasurements();
|
||||
|
||||
measurement->variant.power_metrics.has_ch1_voltage = true;
|
||||
measurement->variant.power_metrics.has_ch1_current = true;
|
||||
measurement->variant.power_metrics.has_ch2_voltage = true;
|
||||
measurement->variant.power_metrics.has_ch2_current = true;
|
||||
measurement->variant.power_metrics.has_ch3_voltage = true;
|
||||
measurement->variant.power_metrics.has_ch3_current = true;
|
||||
|
||||
measurement->variant.power_metrics.ch1_voltage = m.measurements[INA3221_CH1].voltage;
|
||||
measurement->variant.power_metrics.ch1_current = m.measurements[INA3221_CH1].current;
|
||||
measurement->variant.power_metrics.ch2_voltage = m.measurements[INA3221_CH2].voltage;
|
||||
measurement->variant.power_metrics.ch2_current = m.measurements[INA3221_CH2].current;
|
||||
measurement->variant.power_metrics.ch3_voltage = m.measurements[INA3221_CH3].voltage;
|
||||
measurement->variant.power_metrics.ch3_current = m.measurements[INA3221_CH3].current;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
uint16_t INA3221Sensor::getBusVoltageMv()
|
||||
{
|
||||
return lround(ina3221.getVoltage(BAT_CH) * 1000);
|
||||
}
|
||||
|
||||
int16_t INA3221Sensor::getCurrentMa()
|
||||
{
|
||||
return lround(ina3221.getCurrent(BAT_CH));
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,60 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<INA3221.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "CurrentSensor.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#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
|
||||
static const ina3221_ch_t ENV_CH = INA3221_ENV_CH;
|
||||
|
||||
// channel to report battery voltage for device_battery_ina_address
|
||||
static const ina3221_ch_t BAT_CH = INA3221_BAT_CH;
|
||||
|
||||
// get a single measurement for a channel
|
||||
struct _INA3221Measurement getMeasurement(ina3221_ch_t ch);
|
||||
|
||||
// get all measurements for all channels
|
||||
struct _INA3221Measurements getMeasurements();
|
||||
|
||||
bool getEnvironmentMetrics(meshtastic_Telemetry *measurement);
|
||||
bool getPowerMetrics(meshtastic_Telemetry *measurement);
|
||||
|
||||
protected:
|
||||
void setup() override;
|
||||
|
||||
public:
|
||||
INA3221Sensor();
|
||||
int32_t runOnce() override;
|
||||
bool getMetrics(meshtastic_Telemetry *measurement) override;
|
||||
virtual uint16_t getBusVoltageMv() override;
|
||||
virtual int16_t getCurrentMa() override;
|
||||
};
|
||||
|
||||
struct _INA3221Measurement {
|
||||
float voltage;
|
||||
float current;
|
||||
};
|
||||
|
||||
struct _INA3221Measurements {
|
||||
// INA3221 has 3 channels
|
||||
struct _INA3221Measurement measurements[3];
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,166 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && defined(SENSECAP_INDICATOR)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "IndicatorSensor.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include "serialization/cobs.h"
|
||||
#include <Adafruit_Sensor.h>
|
||||
#include <driver/uart.h>
|
||||
|
||||
IndicatorSensor::IndicatorSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SENSOR_UNSET, "Indicator") {}
|
||||
|
||||
#define SENSOR_BUF_SIZE (512)
|
||||
|
||||
uint8_t buf[SENSOR_BUF_SIZE]; // recv
|
||||
uint8_t data[SENSOR_BUF_SIZE]; // decode
|
||||
|
||||
#define ACK_PKT_PARA "ACK"
|
||||
|
||||
enum sensor_pkt_type {
|
||||
PKT_TYPE_ACK = 0x00, // uin32_t
|
||||
PKT_TYPE_CMD_COLLECT_INTERVAL = 0xA0, // uin32_t
|
||||
PKT_TYPE_CMD_BEEP_ON = 0xA1, // uin32_t ms: on time
|
||||
PKT_TYPE_CMD_BEEP_OFF = 0xA2,
|
||||
PKT_TYPE_CMD_SHUTDOWN = 0xA3, // uin32_t
|
||||
PKT_TYPE_CMD_POWER_ON = 0xA4,
|
||||
PKT_TYPE_SENSOR_SCD41_TEMP = 0xB0, // float
|
||||
PKT_TYPE_SENSOR_SCD41_HUMIDITY = 0xB1, // float
|
||||
PKT_TYPE_SENSOR_SCD41_CO2 = 0xB2, // float
|
||||
PKT_TYPE_SENSOR_AHT20_TEMP = 0xB3, // float
|
||||
PKT_TYPE_SENSOR_AHT20_HUMIDITY = 0xB4, // float
|
||||
PKT_TYPE_SENSOR_TVOC_INDEX = 0xB5, // float
|
||||
};
|
||||
|
||||
static int cmd_send(uint8_t cmd, const char *p_data, uint8_t len)
|
||||
{
|
||||
uint8_t send_buf[32] = {0};
|
||||
uint8_t send_data[32] = {0};
|
||||
|
||||
if (len > 31) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint8_t index = 1;
|
||||
|
||||
send_data[0] = cmd;
|
||||
|
||||
if (len > 0 && p_data != NULL) {
|
||||
memcpy(&send_data[1], p_data, len);
|
||||
index += len;
|
||||
}
|
||||
cobs_encode_result ret = cobs_encode(send_buf, sizeof(send_buf), send_data, index);
|
||||
|
||||
// LOG_DEBUG("cobs TX status:%d, len:%d, type 0x%x", ret.status, ret.out_len, cmd);
|
||||
|
||||
if (ret.status == COBS_ENCODE_OK) {
|
||||
return uart_write_bytes(SENSOR_PORT_NUM, send_buf, ret.out_len + 1);
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool IndicatorSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
|
||||
{
|
||||
LOG_INFO("%s: init", sensorName);
|
||||
setup();
|
||||
return true;
|
||||
}
|
||||
|
||||
void IndicatorSensor::setup()
|
||||
{
|
||||
uart_config_t uart_config = {
|
||||
.baud_rate = SENSOR_BAUD_RATE,
|
||||
.data_bits = UART_DATA_8_BITS,
|
||||
.parity = UART_PARITY_DISABLE,
|
||||
.stop_bits = UART_STOP_BITS_1,
|
||||
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
|
||||
.source_clk = UART_SCLK_APB,
|
||||
};
|
||||
int intr_alloc_flags = 0;
|
||||
char buffer[11];
|
||||
|
||||
uart_driver_install(SENSOR_PORT_NUM, SENSOR_BUF_SIZE * 2, 0, 0, NULL, intr_alloc_flags);
|
||||
uart_param_config(SENSOR_PORT_NUM, &uart_config);
|
||||
uart_set_pin(SENSOR_PORT_NUM, SENSOR_RP2040_TXD, SENSOR_RP2040_RXD, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
|
||||
cmd_send(PKT_TYPE_CMD_POWER_ON, NULL, 0);
|
||||
// measure and send only once every minute, for the phone API
|
||||
const char *interval = ultoa(60000, buffer, 10);
|
||||
cmd_send(PKT_TYPE_CMD_COLLECT_INTERVAL, interval, strlen(interval) + 1);
|
||||
}
|
||||
|
||||
bool IndicatorSensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
{
|
||||
cobs_decode_result ret;
|
||||
int len = uart_read_bytes(SENSOR_PORT_NUM, buf, (SENSOR_BUF_SIZE - 1), 100 / portTICK_PERIOD_MS);
|
||||
|
||||
float value = 0.0;
|
||||
uint8_t *p_buf_start = buf;
|
||||
uint8_t *p_buf_end = buf;
|
||||
if (len > 0) {
|
||||
while (p_buf_start < (buf + len)) {
|
||||
p_buf_end = p_buf_start;
|
||||
while (p_buf_end < (buf + len)) {
|
||||
if (*p_buf_end == 0x00) {
|
||||
break;
|
||||
}
|
||||
p_buf_end++;
|
||||
}
|
||||
// decode buf
|
||||
memset(data, 0, sizeof(data));
|
||||
ret = cobs_decode(data, sizeof(data), p_buf_start, p_buf_end - p_buf_start);
|
||||
|
||||
// LOG_DEBUG("cobs RX status:%d, len:%d, type:0x%x ", ret.status, ret.out_len, data[0]);
|
||||
|
||||
if (ret.out_len > 1 && ret.status == COBS_DECODE_OK) {
|
||||
|
||||
value = 0.0;
|
||||
uint8_t pkt_type = data[0];
|
||||
switch (pkt_type) {
|
||||
case PKT_TYPE_SENSOR_SCD41_CO2: {
|
||||
memcpy(&value, &data[1], sizeof(value));
|
||||
// LOG_DEBUG("CO2: %.1f", value);
|
||||
cmd_send(PKT_TYPE_ACK, ACK_PKT_PARA, 4);
|
||||
break;
|
||||
}
|
||||
|
||||
case PKT_TYPE_SENSOR_AHT20_TEMP: {
|
||||
memcpy(&value, &data[1], sizeof(value));
|
||||
// LOG_DEBUG("Temp: %.1f", value);
|
||||
cmd_send(PKT_TYPE_ACK, ACK_PKT_PARA, 4);
|
||||
measurement->variant.environment_metrics.has_temperature = true;
|
||||
measurement->variant.environment_metrics.temperature = value;
|
||||
break;
|
||||
}
|
||||
|
||||
case PKT_TYPE_SENSOR_AHT20_HUMIDITY: {
|
||||
memcpy(&value, &data[1], sizeof(value));
|
||||
// LOG_DEBUG("Humidity: %.1f", value);
|
||||
cmd_send(PKT_TYPE_ACK, ACK_PKT_PARA, 4);
|
||||
measurement->variant.environment_metrics.has_relative_humidity = true;
|
||||
measurement->variant.environment_metrics.relative_humidity = value;
|
||||
break;
|
||||
}
|
||||
|
||||
case PKT_TYPE_SENSOR_TVOC_INDEX: {
|
||||
memcpy(&value, &data[1], sizeof(value));
|
||||
// LOG_DEBUG("Tvoc: %.1f", value);
|
||||
cmd_send(PKT_TYPE_ACK, ACK_PKT_PARA, 4);
|
||||
measurement->variant.environment_metrics.has_iaq = true;
|
||||
measurement->variant.environment_metrics.iaq = value;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
p_buf_start = p_buf_end + 1; // next message
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,19 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "TelemetrySensor.h"
|
||||
|
||||
class IndicatorSensor : public TelemetrySensor
|
||||
{
|
||||
public:
|
||||
IndicatorSensor();
|
||||
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
||||
virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
|
||||
|
||||
private:
|
||||
void setup();
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,41 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<Adafruit_LPS2X.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "LPS22HBSensor.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include <Adafruit_LPS2X.h>
|
||||
#include <Adafruit_Sensor.h>
|
||||
|
||||
LPS22HBSensor::LPS22HBSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_LPS22, "LPS22HB") {}
|
||||
|
||||
bool LPS22HBSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
|
||||
{
|
||||
LOG_INFO("Init sensor: %s", sensorName);
|
||||
status = lps22hb.begin_I2C(dev->address.address, bus);
|
||||
if (!status) {
|
||||
return status;
|
||||
}
|
||||
lps22hb.setDataRate(LPS22_RATE_10_HZ);
|
||||
|
||||
initI2CSensor();
|
||||
return status;
|
||||
}
|
||||
|
||||
bool LPS22HBSensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
{
|
||||
measurement->variant.environment_metrics.has_temperature = true;
|
||||
measurement->variant.environment_metrics.has_barometric_pressure = true;
|
||||
|
||||
sensors_event_t temp;
|
||||
sensors_event_t pressure;
|
||||
lps22hb.getEvent(&pressure, &temp);
|
||||
|
||||
measurement->variant.environment_metrics.temperature = temp.temperature;
|
||||
measurement->variant.environment_metrics.barometric_pressure = pressure.pressure;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,21 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<Adafruit_LPS2X.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include <Adafruit_LPS2X.h>
|
||||
#include <Adafruit_Sensor.h>
|
||||
|
||||
class LPS22HBSensor : public TelemetrySensor
|
||||
{
|
||||
private:
|
||||
Adafruit_LPS22 lps22hb;
|
||||
|
||||
public:
|
||||
LPS22HBSensor();
|
||||
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
||||
virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,73 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<Adafruit_LTR390.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "LTR390UVSensor.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include <Adafruit_LTR390.h>
|
||||
|
||||
LTR390UVSensor::LTR390UVSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_LTR390UV, "LTR390UV") {}
|
||||
|
||||
bool LTR390UVSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
|
||||
{
|
||||
LOG_INFO("Init sensor: %s", sensorName);
|
||||
|
||||
status = ltr390uv.begin(bus);
|
||||
if (!status) {
|
||||
return status;
|
||||
}
|
||||
|
||||
ltr390uv.setMode(LTR390_MODE_UVS);
|
||||
ltr390uv.setGain(LTR390_GAIN_18); // Datasheet default
|
||||
ltr390uv.setResolution(LTR390_RESOLUTION_20BIT); // Datasheet default
|
||||
|
||||
initI2CSensor();
|
||||
return status;
|
||||
}
|
||||
|
||||
bool LTR390UVSensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
{
|
||||
LOG_DEBUG("LTR390UV getMetrics");
|
||||
|
||||
// Because the sensor does not measure Lux and UV at the same time, we need to read them in two passes.
|
||||
if (ltr390uv.newDataAvailable()) {
|
||||
measurement->variant.environment_metrics.has_lux = true;
|
||||
measurement->variant.environment_metrics.has_uv_lux = true;
|
||||
|
||||
if (ltr390uv.getMode() == LTR390_MODE_ALS) {
|
||||
lastLuxReading = 0.6 * ltr390uv.readALS() / (1 * 4); // Datasheet page 23 for gain x1 and 20bit resolution
|
||||
LOG_DEBUG("LTR390UV Lux reading: %f", lastLuxReading);
|
||||
|
||||
measurement->variant.environment_metrics.lux = lastLuxReading;
|
||||
measurement->variant.environment_metrics.uv_lux = lastUVReading;
|
||||
|
||||
ltr390uv.setGain(
|
||||
LTR390_GAIN_18); // Recommended for UVI - x18. Do not change, 2300 UV Sensitivity only specified for x18 gain
|
||||
ltr390uv.setMode(LTR390_MODE_UVS);
|
||||
|
||||
return true;
|
||||
|
||||
} else if (ltr390uv.getMode() == LTR390_MODE_UVS) {
|
||||
lastUVReading = ltr390uv.readUVS() /
|
||||
2300.f; // Datasheet page 23 and page 6, only characterisation for gain x18 and 20bit resolution
|
||||
LOG_DEBUG("LTR390UV UV reading: %f", lastUVReading);
|
||||
|
||||
measurement->variant.environment_metrics.lux = lastLuxReading;
|
||||
measurement->variant.environment_metrics.uv_lux = lastUVReading;
|
||||
|
||||
ltr390uv.setGain(
|
||||
LTR390_GAIN_1); // x1 gain will already max out the sensor at direct sunlight, so no need to increase it
|
||||
ltr390uv.setMode(LTR390_MODE_ALS);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// In case we fail to read the sensor mode, set the has_lux and has_uv_lux back to false
|
||||
measurement->variant.environment_metrics.has_lux = false;
|
||||
measurement->variant.environment_metrics.has_uv_lux = false;
|
||||
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,22 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<Adafruit_LTR390.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include <Adafruit_LTR390.h>
|
||||
|
||||
class LTR390UVSensor : public TelemetrySensor
|
||||
{
|
||||
private:
|
||||
Adafruit_LTR390 ltr390uv = Adafruit_LTR390();
|
||||
float lastLuxReading = 0;
|
||||
float lastUVReading = 0;
|
||||
|
||||
public:
|
||||
LTR390UVSensor();
|
||||
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
||||
virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,176 @@
|
||||
#include "MAX17048Sensor.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_I2C && __has_include(<Adafruit_MAX1704X.h>)
|
||||
|
||||
MAX17048Singleton *MAX17048Singleton::GetInstance()
|
||||
{
|
||||
if (pinstance == nullptr) {
|
||||
pinstance = new MAX17048Singleton();
|
||||
}
|
||||
return pinstance;
|
||||
}
|
||||
|
||||
MAX17048Singleton::MAX17048Singleton() {}
|
||||
|
||||
MAX17048Singleton::~MAX17048Singleton() {}
|
||||
|
||||
MAX17048Singleton *MAX17048Singleton::pinstance{nullptr};
|
||||
|
||||
bool MAX17048Singleton::runOnce(TwoWire *theWire)
|
||||
{
|
||||
initialized = begin(theWire);
|
||||
LOG_DEBUG("%s::runOnce %s", sensorStr, initialized ? "began ok" : "begin failed");
|
||||
return initialized;
|
||||
}
|
||||
|
||||
bool MAX17048Singleton::isBatteryCharging()
|
||||
{
|
||||
float volts = cellVoltage();
|
||||
if (isnan(volts)) {
|
||||
LOG_DEBUG("%s::isBatteryCharging not connected", sensorStr);
|
||||
return 0;
|
||||
}
|
||||
|
||||
MAX17048ChargeSample sample;
|
||||
sample.chargeRate = chargeRate(); // charge/discharge rate in percent/hr
|
||||
sample.cellPercent = cellPercent(); // state of charge in percent 0 to 100
|
||||
chargeSamples.push(sample); // save a sample into a fifo buffer
|
||||
|
||||
// Keep the fifo buffer trimmed
|
||||
while (chargeSamples.size() > MAX17048_CHARGING_SAMPLES)
|
||||
chargeSamples.pop();
|
||||
|
||||
// Based on the past n samples, is the lipo charging, discharging or idle
|
||||
if (chargeSamples.front().chargeRate > MAX17048_CHARGING_MINIMUM_RATE &&
|
||||
chargeSamples.back().chargeRate > MAX17048_CHARGING_MINIMUM_RATE) {
|
||||
if (chargeSamples.front().cellPercent > chargeSamples.back().cellPercent)
|
||||
chargeState = MAX17048ChargeState::EXPORT;
|
||||
else if (chargeSamples.front().cellPercent < chargeSamples.back().cellPercent)
|
||||
chargeState = MAX17048ChargeState::IMPORT;
|
||||
else
|
||||
chargeState = MAX17048ChargeState::IDLE;
|
||||
} else {
|
||||
chargeState = MAX17048ChargeState::IDLE;
|
||||
}
|
||||
|
||||
LOG_DEBUG("%s::isBatteryCharging %s volts: %.3f soc: %.3f rate: %.3f", sensorStr, chargeLabels[chargeState], volts,
|
||||
sample.cellPercent, sample.chargeRate);
|
||||
return chargeState == MAX17048ChargeState::IMPORT;
|
||||
}
|
||||
|
||||
uint16_t MAX17048Singleton::getBusVoltageMv()
|
||||
{
|
||||
float volts = cellVoltage();
|
||||
if (isnan(volts)) {
|
||||
LOG_DEBUG("%s::getBusVoltageMv is not connected", sensorStr);
|
||||
return 0;
|
||||
}
|
||||
LOG_DEBUG("%s::getBusVoltageMv %.3fmV", sensorStr, volts);
|
||||
return (uint16_t)(volts * 1000.0f);
|
||||
}
|
||||
|
||||
uint8_t MAX17048Singleton::getBusBatteryPercent()
|
||||
{
|
||||
float soc = cellPercent();
|
||||
LOG_DEBUG("%s::getBusBatteryPercent %.1f%%", sensorStr, soc);
|
||||
return clamp(static_cast<uint8_t>(round(soc)), static_cast<uint8_t>(0), static_cast<uint8_t>(100));
|
||||
}
|
||||
|
||||
uint16_t MAX17048Singleton::getTimeToGoSecs()
|
||||
{
|
||||
float rate = chargeRate(); // charge/discharge rate in percent/hr
|
||||
float soc = cellPercent(); // state of charge in percent 0 to 100
|
||||
soc = clamp(soc, 0.0f, 100.0f); // clamp soc between 0 and 100%
|
||||
float ttg = ((100.0f - soc) / rate) * 3600.0f; // calculate seconds to charge/discharge
|
||||
LOG_DEBUG("%s::getTimeToGoSecs %.0f seconds", sensorStr, ttg);
|
||||
return (uint16_t)ttg;
|
||||
}
|
||||
|
||||
bool MAX17048Singleton::isBatteryConnected()
|
||||
{
|
||||
float volts = cellVoltage();
|
||||
if (isnan(volts)) {
|
||||
LOG_DEBUG("%s::isBatteryConnected is not connected", sensorStr);
|
||||
return false;
|
||||
}
|
||||
|
||||
// if a valid voltage is returned, then battery must be connected
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MAX17048Singleton::isExternallyPowered()
|
||||
{
|
||||
float volts = cellVoltage();
|
||||
if (isnan(volts)) {
|
||||
// if the battery is not connected then there must be external power
|
||||
LOG_DEBUG("%s::isExternallyPowered battery is", sensorStr);
|
||||
return true;
|
||||
}
|
||||
// if the bus voltage is over MAX17048_BUS_POWER_VOLTS, then the external power
|
||||
// is assumed to be connected
|
||||
LOG_DEBUG("%s::isExternallyPowered %s connected", sensorStr, volts >= MAX17048_BUS_POWER_VOLTS ? "is" : "is not");
|
||||
return volts >= MAX17048_BUS_POWER_VOLTS;
|
||||
}
|
||||
|
||||
#if (HAS_TELEMETRY && (!MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR || !MESHTASTIC_EXCLUDE_POWER_TELEMETRY))
|
||||
|
||||
MAX17048Sensor::MAX17048Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_MAX17048, "MAX17048") {}
|
||||
|
||||
int32_t MAX17048Sensor::runOnce()
|
||||
{
|
||||
if (isInitialized()) {
|
||||
LOG_INFO("Init sensor: %s is already initialised", sensorName);
|
||||
return true;
|
||||
}
|
||||
|
||||
LOG_INFO("Init sensor: %s", sensorName);
|
||||
if (!hasSensor()) {
|
||||
return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
|
||||
}
|
||||
|
||||
// Get a singleton instance and initialise the max17048
|
||||
if (max17048 == nullptr) {
|
||||
max17048 = MAX17048Singleton::GetInstance();
|
||||
}
|
||||
status = max17048->runOnce(nodeTelemetrySensorsMap[sensorType].second);
|
||||
return initI2CSensor();
|
||||
}
|
||||
|
||||
void MAX17048Sensor::setup() {}
|
||||
|
||||
bool MAX17048Sensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
{
|
||||
LOG_DEBUG("MAX17048 getMetrics id: %i", measurement->which_variant);
|
||||
|
||||
float volts = max17048->cellVoltage();
|
||||
if (isnan(volts)) {
|
||||
LOG_DEBUG("MAX17048 getMetrics battery is not connected");
|
||||
return false;
|
||||
}
|
||||
|
||||
float rate = max17048->chargeRate(); // charge/discharge rate in percent/hr
|
||||
float soc = max17048->cellPercent(); // state of charge in percent 0 to 100
|
||||
soc = clamp(soc, 0.0f, 100.0f); // clamp soc between 0 and 100%
|
||||
float ttg = (100.0f - soc) / rate; // calculate hours to charge/discharge
|
||||
|
||||
LOG_DEBUG("MAX17048 getMetrics volts: %.3fV soc: %.1f%% ttg: %.1f hours", volts, soc, ttg);
|
||||
if ((int)measurement->which_variant == meshtastic_Telemetry_power_metrics_tag) {
|
||||
measurement->variant.power_metrics.has_ch1_voltage = true;
|
||||
measurement->variant.power_metrics.ch1_voltage = volts;
|
||||
} else if ((int)measurement->which_variant == meshtastic_Telemetry_device_metrics_tag) {
|
||||
measurement->variant.device_metrics.has_battery_level = true;
|
||||
measurement->variant.device_metrics.has_voltage = true;
|
||||
measurement->variant.device_metrics.battery_level = static_cast<uint32_t>(round(soc));
|
||||
measurement->variant.device_metrics.voltage = volts;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
uint16_t MAX17048Sensor::getBusVoltageMv()
|
||||
{
|
||||
return max17048->getBusVoltageMv();
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,111 @@
|
||||
#pragma once
|
||||
|
||||
#ifndef MAX17048_SENSOR_H
|
||||
#define MAX17048_SENSOR_H
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_I2C && __has_include(<Adafruit_MAX1704X.h>)
|
||||
|
||||
// Samples to store in a buffer to determine if the battery is charging or discharging
|
||||
#define MAX17048_CHARGING_SAMPLES 3
|
||||
|
||||
// Threshold to determine if the battery is on charge, in percent/hour
|
||||
#define MAX17048_CHARGING_MINIMUM_RATE 1.0f
|
||||
|
||||
// Threshold to determine if the board has bus power
|
||||
#define MAX17048_BUS_POWER_VOLTS 4.195f
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include "VoltageSensor.h"
|
||||
|
||||
#include "meshUtils.h"
|
||||
#include <Adafruit_MAX1704X.h>
|
||||
#include <queue>
|
||||
|
||||
struct MAX17048ChargeSample {
|
||||
float cellPercent;
|
||||
float chargeRate;
|
||||
};
|
||||
|
||||
enum MAX17048ChargeState { IDLE, EXPORT, IMPORT };
|
||||
|
||||
// Singleton wrapper for the Adafruit_MAX17048 class
|
||||
class MAX17048Singleton : public Adafruit_MAX17048
|
||||
{
|
||||
private:
|
||||
static MAX17048Singleton *pinstance;
|
||||
bool initialized = false;
|
||||
std::queue<MAX17048ChargeSample> chargeSamples;
|
||||
MAX17048ChargeState chargeState = IDLE;
|
||||
const String chargeLabels[3] = {F("idle"), F("export"), F("import")};
|
||||
const char *sensorStr = "MAX17048Sensor";
|
||||
|
||||
protected:
|
||||
MAX17048Singleton();
|
||||
~MAX17048Singleton();
|
||||
|
||||
public:
|
||||
// Create a singleton instance (not thread safe)
|
||||
static MAX17048Singleton *GetInstance();
|
||||
|
||||
// Singletons should not be cloneable.
|
||||
MAX17048Singleton(MAX17048Singleton &other) = delete;
|
||||
|
||||
// Singletons should not be assignable.
|
||||
void operator=(const MAX17048Singleton &) = delete;
|
||||
|
||||
// Initialise the sensor (not thread safe)
|
||||
virtual bool runOnce(TwoWire *theWire = &Wire);
|
||||
|
||||
// Get the current bus voltage
|
||||
uint16_t getBusVoltageMv();
|
||||
|
||||
// Get the state of charge in percent 0 to 100
|
||||
uint8_t getBusBatteryPercent();
|
||||
|
||||
// Calculate the seconds to charge/discharge
|
||||
uint16_t getTimeToGoSecs();
|
||||
|
||||
// Returns true if the battery sensor has started
|
||||
inline virtual bool isInitialised() { return initialized; };
|
||||
|
||||
// Returns true if the battery is currently on charge (not thread safe)
|
||||
bool isBatteryCharging();
|
||||
|
||||
// Returns true if a battery is actually connected
|
||||
bool isBatteryConnected();
|
||||
|
||||
// Returns true if there is bus or external power connected
|
||||
bool isExternallyPowered();
|
||||
};
|
||||
|
||||
#if (HAS_TELEMETRY && (!MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR || !MESHTASTIC_EXCLUDE_POWER_TELEMETRY))
|
||||
|
||||
class MAX17048Sensor : public TelemetrySensor, VoltageSensor
|
||||
{
|
||||
private:
|
||||
MAX17048Singleton *max17048 = nullptr;
|
||||
|
||||
protected:
|
||||
virtual void setup() override;
|
||||
|
||||
public:
|
||||
MAX17048Sensor();
|
||||
|
||||
// Initialise the sensor
|
||||
virtual int32_t runOnce() override;
|
||||
|
||||
// Get the current bus voltage and state of charge
|
||||
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
||||
|
||||
// Get the current bus voltage
|
||||
virtual uint16_t getBusVoltageMv() override;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,83 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY && __has_include(<MAX30105.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "MAX30102Sensor.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include <spo2_algorithm.h>
|
||||
|
||||
MAX30102Sensor::MAX30102Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_MAX30102, "MAX30102") {}
|
||||
|
||||
int32_t MAX30102Sensor::runOnce()
|
||||
{
|
||||
LOG_INFO("Init sensor: %s", sensorName);
|
||||
if (!hasSensor()) {
|
||||
return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
|
||||
}
|
||||
|
||||
if (max30102.begin(*nodeTelemetrySensorsMap[sensorType].second, _speed, nodeTelemetrySensorsMap[sensorType].first) ==
|
||||
true) // MAX30102 init
|
||||
{
|
||||
byte brightness = 60; // 0=Off to 255=50mA
|
||||
byte sampleAverage = 4; // 1, 2, 4, 8, 16, 32
|
||||
byte leds = 2; // 1 = Red only, 2 = Red + IR
|
||||
byte sampleRate = 100; // 50, 100, 200, 400, 800, 1000, 1600, 3200
|
||||
int pulseWidth = 411; // 69, 118, 215, 411
|
||||
int adcRange = 4096; // 2048, 4096, 8192, 16384
|
||||
|
||||
max30102.enableDIETEMPRDY(); // Enable the temperature ready interrupt
|
||||
max30102.setup(brightness, sampleAverage, leds, sampleRate, pulseWidth, adcRange);
|
||||
LOG_DEBUG("MAX30102 Init Succeed");
|
||||
status = true;
|
||||
} else {
|
||||
LOG_ERROR("MAX30102 Init Failed");
|
||||
status = false;
|
||||
}
|
||||
return initI2CSensor();
|
||||
}
|
||||
|
||||
void MAX30102Sensor::setup() {}
|
||||
|
||||
bool MAX30102Sensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
{
|
||||
uint32_t ir_buff[MAX30102_BUFFER_LEN];
|
||||
uint32_t red_buff[MAX30102_BUFFER_LEN];
|
||||
int32_t spo2;
|
||||
int8_t spo2_valid;
|
||||
int32_t heart_rate;
|
||||
int8_t heart_rate_valid;
|
||||
float temp = max30102.readTemperature();
|
||||
|
||||
measurement->variant.environment_metrics.temperature = temp;
|
||||
measurement->variant.environment_metrics.has_temperature = true;
|
||||
measurement->variant.health_metrics.temperature = temp;
|
||||
measurement->variant.health_metrics.has_temperature = true;
|
||||
for (byte i = 0; i < MAX30102_BUFFER_LEN; i++) {
|
||||
while (max30102.available() == false)
|
||||
max30102.check();
|
||||
|
||||
red_buff[i] = max30102.getRed();
|
||||
ir_buff[i] = max30102.getIR();
|
||||
max30102.nextSample();
|
||||
}
|
||||
|
||||
maxim_heart_rate_and_oxygen_saturation(ir_buff, MAX30102_BUFFER_LEN, red_buff, &spo2, &spo2_valid, &heart_rate,
|
||||
&heart_rate_valid);
|
||||
LOG_DEBUG("heart_rate=%d(%d), sp02=%d(%d)", heart_rate, heart_rate_valid, spo2, spo2_valid);
|
||||
if (heart_rate_valid) {
|
||||
measurement->variant.health_metrics.has_heart_bpm = true;
|
||||
measurement->variant.health_metrics.heart_bpm = heart_rate;
|
||||
} else {
|
||||
measurement->variant.health_metrics.has_heart_bpm = false;
|
||||
}
|
||||
if (spo2_valid) {
|
||||
measurement->variant.health_metrics.has_spO2 = true;
|
||||
measurement->variant.health_metrics.spO2 = spo2;
|
||||
} else {
|
||||
measurement->variant.health_metrics.has_spO2 = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,26 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY && __has_include(<MAX30105.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include <MAX30105.h>
|
||||
|
||||
#define MAX30102_BUFFER_LEN 100
|
||||
|
||||
class MAX30102Sensor : public TelemetrySensor
|
||||
{
|
||||
private:
|
||||
MAX30105 max30102 = MAX30105();
|
||||
uint32_t _speed = 200000UL;
|
||||
|
||||
protected:
|
||||
virtual void setup() override;
|
||||
|
||||
public:
|
||||
MAX30102Sensor();
|
||||
virtual int32_t runOnce() override;
|
||||
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,34 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<Adafruit_MCP9808.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "MCP9808Sensor.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include <Adafruit_MCP9808.h>
|
||||
|
||||
MCP9808Sensor::MCP9808Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_MCP9808, "MCP9808") {}
|
||||
|
||||
bool MCP9808Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
|
||||
{
|
||||
LOG_INFO("Init sensor: %s", sensorName);
|
||||
status = mcp9808.begin(dev->address.address, bus);
|
||||
if (!status) {
|
||||
return status;
|
||||
}
|
||||
mcp9808.setResolution(2);
|
||||
|
||||
initI2CSensor();
|
||||
return status;
|
||||
}
|
||||
|
||||
bool MCP9808Sensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
{
|
||||
measurement->variant.environment_metrics.has_temperature = true;
|
||||
|
||||
LOG_DEBUG("MCP9808 getMetrics");
|
||||
measurement->variant.environment_metrics.temperature = mcp9808.readTempC();
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,20 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<Adafruit_MCP9808.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include <Adafruit_MCP9808.h>
|
||||
|
||||
class MCP9808Sensor : public TelemetrySensor
|
||||
{
|
||||
private:
|
||||
Adafruit_MCP9808 mcp9808;
|
||||
|
||||
public:
|
||||
MCP9808Sensor();
|
||||
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
||||
virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,44 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<Adafruit_MLX90614.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "MLX90614Sensor.h"
|
||||
#include "TelemetrySensor.h"
|
||||
MLX90614Sensor::MLX90614Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_MLX90614, "MLX90614") {}
|
||||
|
||||
int32_t MLX90614Sensor::runOnce()
|
||||
{
|
||||
LOG_INFO("Init sensor: %s", sensorName);
|
||||
if (!hasSensor()) {
|
||||
return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
|
||||
}
|
||||
|
||||
if (mlx.begin(nodeTelemetrySensorsMap[sensorType].first, nodeTelemetrySensorsMap[sensorType].second) == true) // MLX90614 init
|
||||
{
|
||||
LOG_DEBUG("MLX90614 emissivity: %f", mlx.readEmissivity());
|
||||
if (fabs(MLX90614_EMISSIVITY - mlx.readEmissivity()) > 0.001) {
|
||||
mlx.writeEmissivity(MLX90614_EMISSIVITY);
|
||||
LOG_INFO("MLX90614 emissivity updated. In case of weird data, power cycle");
|
||||
}
|
||||
LOG_DEBUG("MLX90614 Init Succeed");
|
||||
status = true;
|
||||
} else {
|
||||
LOG_ERROR("MLX90614 Init Failed");
|
||||
status = false;
|
||||
}
|
||||
return initI2CSensor();
|
||||
}
|
||||
|
||||
void MLX90614Sensor::setup() {}
|
||||
|
||||
bool MLX90614Sensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
{
|
||||
measurement->variant.environment_metrics.temperature = mlx.readAmbientTempC();
|
||||
measurement->variant.environment_metrics.has_temperature = true;
|
||||
measurement->variant.health_metrics.temperature = mlx.readObjectTempC();
|
||||
measurement->variant.health_metrics.has_temperature = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,24 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<Adafruit_MLX90614.h>)
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include <Adafruit_MLX90614.h>
|
||||
|
||||
#define MLX90614_EMISSIVITY 0.98 // human skin
|
||||
|
||||
class MLX90614Sensor : public TelemetrySensor
|
||||
{
|
||||
private:
|
||||
Adafruit_MLX90614 mlx = Adafruit_MLX90614();
|
||||
|
||||
protected:
|
||||
virtual void setup() override;
|
||||
|
||||
public:
|
||||
MLX90614Sensor();
|
||||
virtual int32_t runOnce() override;
|
||||
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,36 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<SparkFun_MLX90632_Arduino_Library.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "MLX90632Sensor.h"
|
||||
#include "TelemetrySensor.h"
|
||||
|
||||
MLX90632Sensor::MLX90632Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_MLX90632, "MLX90632") {}
|
||||
|
||||
bool MLX90632Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
|
||||
{
|
||||
LOG_INFO("Init sensor: %s", sensorName);
|
||||
|
||||
MLX90632::status returnError;
|
||||
if (mlx.begin(dev->address.address, *bus, returnError) == true) // MLX90632 init
|
||||
{
|
||||
LOG_DEBUG("MLX90632 Init Succeed");
|
||||
status = true;
|
||||
} else {
|
||||
LOG_ERROR("MLX90632 Init Failed");
|
||||
status = false;
|
||||
}
|
||||
initI2CSensor();
|
||||
return status;
|
||||
}
|
||||
|
||||
bool MLX90632Sensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
{
|
||||
measurement->variant.environment_metrics.has_temperature = true;
|
||||
measurement->variant.environment_metrics.temperature = mlx.getObjectTemp(); // Get the object temperature in Fahrenheit
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,20 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<SparkFun_MLX90632_Arduino_Library.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include <SparkFun_MLX90632_Arduino_Library.h>
|
||||
|
||||
class MLX90632Sensor : public TelemetrySensor
|
||||
{
|
||||
private:
|
||||
MLX90632 mlx = MLX90632();
|
||||
|
||||
public:
|
||||
MLX90632Sensor();
|
||||
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
||||
virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
|
||||
};
|
||||
|
||||
#endif
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user