mv plugins/ to modules/

This commit is contained in:
Jm Casler
2022-02-26 23:56:26 -08:00
parent 9050fe7f90
commit 218a208ab7
48 changed files with 27 additions and 27 deletions

202
src/modules/AdminPlugin.cpp Normal file
View File

@@ -0,0 +1,202 @@
#include "configuration.h"
#include "AdminPlugin.h"
#include "Channels.h"
#include "MeshService.h"
#include "NodeDB.h"
#include "Router.h"
#include "main.h"
#ifdef PORTDUINO
#include "unistd.h"
#endif
AdminPlugin *adminPlugin;
/// A special reserved string to indicate strings we can not share with external nodes. We will use this 'reserved' word instead.
/// Also, to make setting work correctly, if someone tries to set a string to this reserved value we assume they don't really want a change.
static const char *secretReserved = "sekrit";
/// If buf is !empty, change it to secret
static void hideSecret(char *buf) {
if(*buf) {
strcpy(buf, secretReserved);
}
}
/// If buf is the reserved secret word, replace the buffer with currentVal
static void writeSecret(char *buf, const char *currentVal) {
if(strcmp(buf, secretReserved) == 0) {
strcpy(buf, currentVal);
}
}
void AdminPlugin::handleGetChannel(const MeshPacket &req, uint32_t channelIndex)
{
if (req.decoded.want_response) {
// We create the reply here
AdminMessage r = AdminMessage_init_default;
r.get_channel_response = channels.getByIndex(channelIndex);
r.which_variant = AdminMessage_get_channel_response_tag;
myReply = allocDataProtobuf(r);
}
}
void AdminPlugin::handleGetRadio(const MeshPacket &req)
{
if (req.decoded.want_response) {
// We create the reply here
AdminMessage r = AdminMessage_init_default;
r.get_radio_response = radioConfig;
// NOTE: The phone app needs to know the ls_secs & phone_timeout value so it can properly expect sleep behavior.
// So even if we internally use 0 to represent 'use default' we still need to send the value we are
// using to the app (so that even old phone apps work with new device loads).
r.get_radio_response.preferences.ls_secs = getPref_ls_secs();
r.get_radio_response.preferences.phone_timeout_secs = getPref_phone_timeout_secs();
// hideSecret(r.get_radio_response.preferences.wifi_ssid); // hmm - leave public for now, because only minimally private and useful for users to know current provisioning)
hideSecret(r.get_radio_response.preferences.wifi_password);
r.which_variant = AdminMessage_get_radio_response_tag;
myReply = allocDataProtobuf(r);
}
}
bool AdminPlugin::handleReceivedProtobuf(const MeshPacket &mp, AdminMessage *r)
{
// if handled == false, then let others look at this message also if they want
bool handled = false;
assert(r);
switch (r->which_variant) {
case AdminMessage_set_owner_tag:
DEBUG_MSG("Client is setting owner\n");
handleSetOwner(r->set_owner);
break;
case AdminMessage_set_radio_tag:
DEBUG_MSG("Client is setting radio\n");
handleSetRadio(r->set_radio);
break;
case AdminMessage_set_channel_tag:
DEBUG_MSG("Client is setting channel %d\n", r->set_channel.index);
if (r->set_channel.index < 0 || r->set_channel.index >= (int)MAX_NUM_CHANNELS)
myReply = allocErrorResponse(Routing_Error_BAD_REQUEST, &mp);
else
handleSetChannel(r->set_channel);
break;
case AdminMessage_get_channel_request_tag: {
uint32_t i = r->get_channel_request - 1;
DEBUG_MSG("Client is getting channel %u\n", i);
if (i >= MAX_NUM_CHANNELS)
myReply = allocErrorResponse(Routing_Error_BAD_REQUEST, &mp);
else
handleGetChannel(mp, i);
break;
}
case AdminMessage_get_radio_request_tag:
DEBUG_MSG("Client is getting radio\n");
handleGetRadio(mp);
break;
case AdminMessage_reboot_seconds_tag: {
int32_t s = r->reboot_seconds;
DEBUG_MSG("Rebooting in %d seconds\n", s);
rebootAtMsec = (s < 0) ? 0 : (millis() + s * 1000);
break;
}
case AdminMessage_shutdown_seconds_tag: {
int32_t s = r->shutdown_seconds;
DEBUG_MSG("Shutdown in %d seconds\n", s);
shutdownAtMsec = (s < 0) ? 0 : (millis() + s * 1000);
break;
}
#ifdef PORTDUINO
case AdminMessage_exit_simulator_tag:
DEBUG_MSG("Exiting simulator\n");
_exit(0);
break;
#endif
default:
AdminMessage response = AdminMessage_init_default;
AdminMessageHandleResult handleResult = MeshPlugin::handleAdminMessageForAllPlugins(mp, r, &response);
if (handleResult == AdminMessageHandleResult::HANDLED_WITH_RESPONSE)
{
myReply = allocDataProtobuf(response);
}
else if (mp.decoded.want_response)
{
DEBUG_MSG("We did not responded to a request that wanted a respond. req.variant=%d\n", r->which_variant);
}
else if (handleResult != AdminMessageHandleResult::HANDLED)
{
// Probably a message sent by us or sent to our local node. FIXME, we should avoid scanning these messages
DEBUG_MSG("Ignoring nonrelevant admin %d\n", r->which_variant);
}
break;
}
return handled;
}
void AdminPlugin::handleSetOwner(const User &o)
{
int changed = 0;
if (*o.long_name) {
changed |= strcmp(owner.long_name, o.long_name);
strcpy(owner.long_name, o.long_name);
}
if (*o.short_name) {
changed |= strcmp(owner.short_name, o.short_name);
strcpy(owner.short_name, o.short_name);
}
if (*o.id) {
changed |= strcmp(owner.id, o.id);
strcpy(owner.id, o.id);
}
if (owner.is_licensed != o.is_licensed) {
changed = 1;
owner.is_licensed = o.is_licensed;
}
if ((!changed || o.team) && (owner.team != o.team)) {
changed = 1;
owner.team = o.team;
}
if (changed) // If nothing really changed, don't broadcast on the network or write to flash
service.reloadOwner();
}
void AdminPlugin::handleSetChannel(const Channel &cc)
{
channels.setChannel(cc);
// Just update and save the channels - no need to update the radio for ! primary channel changes
if (cc.index == 0) {
// FIXME, this updates the user preferences also, which isn't needed - we really just want to notify on configChanged
service.reloadConfig();
} else {
channels.onConfigChanged(); // tell the radios about this change
nodeDB.saveChannelsToDisk();
}
}
void AdminPlugin::handleSetRadio(RadioConfig &r)
{
writeSecret(r.preferences.wifi_password, radioConfig.preferences.wifi_password);
radioConfig = r;
service.reloadConfig();
}
AdminPlugin::AdminPlugin() : ProtobufPlugin("Admin", PortNum_ADMIN_APP, AdminMessage_fields)
{
// restrict to the admin channel for rx
boundChannel = Channels::adminChannel;
}

31
src/modules/AdminPlugin.h Normal file
View File

@@ -0,0 +1,31 @@
#pragma once
#include "ProtobufPlugin.h"
/**
* Routing plugin for router control messages
*/
class AdminPlugin : public ProtobufPlugin<AdminMessage>
{
public:
/** Constructor
* name is for debugging output
*/
AdminPlugin();
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 MeshPacket &mp, AdminMessage *p) override;
private:
void handleSetOwner(const User &o);
void handleSetChannel(const Channel &cc);
void handleSetRadio(RadioConfig &r);
void handleGetChannel(const MeshPacket &req, uint32_t channelIndex);
void handleGetRadio(const MeshPacket &req);
};
extern AdminPlugin *adminPlugin;

View File

@@ -0,0 +1,538 @@
#include "configuration.h"
#include "CannedMessagePlugin.h"
#include "MeshService.h"
#include "FSCommon.h"
#include "mesh/generated/cannedmessages.pb.h"
// TODO: reuse defined from Screen.cpp
#define FONT_SMALL ArialMT_Plain_10
#define FONT_MEDIUM ArialMT_Plain_16
#define FONT_LARGE ArialMT_Plain_24
// Remove Canned message screen if no action is taken for some milliseconds
#define INACTIVATE_AFTER_MS 20000
static const char *cannedMessagesConfigFile = "/prefs/cannedConf.proto";
CannedMessagePluginConfig cannedMessagePluginConfig;
CannedMessagePlugin *cannedMessagePlugin;
// TODO: move it into NodeDB.h!
extern bool loadProto(const char *filename, size_t protoSize, size_t objSize, const pb_msgdesc_t *fields, void *dest_struct);
extern bool saveProto(const char *filename, size_t protoSize, size_t objSize, const pb_msgdesc_t *fields, const void *dest_struct);
CannedMessagePlugin::CannedMessagePlugin()
: SinglePortPlugin("canned", PortNum_TEXT_MESSAGE_APP),
concurrency::OSThread("CannedMessagePlugin")
{
if (radioConfig.preferences.canned_message_plugin_enabled)
{
this->loadProtoForPlugin();
if(this->splitConfiguredMessages() <= 0)
{
DEBUG_MSG("CannedMessagePlugin: No messages are configured. Plugin is disabled\n");
this->runState = CANNED_MESSAGE_RUN_STATE_DISABLED;
}
else
{
DEBUG_MSG("CannedMessagePlugin is enabled\n");
this->inputObserver.observe(inputBroker);
}
}
}
/**
* @brief Items in array this->messages will be set to be pointing on the right
* starting points of the string this->messageStore
*
* @return int Returns the number of messages found.
*/
int CannedMessagePlugin::splitConfiguredMessages()
{
int messageIndex = 0;
int i = 0;
// collect all the message parts
strcpy(
this->messageStore,
cannedMessagePluginConfig.messagesPart1);
strcat(
this->messageStore,
cannedMessagePluginConfig.messagesPart2);
strcat(
this->messageStore,
cannedMessagePluginConfig.messagesPart3);
strcat(
this->messageStore,
cannedMessagePluginConfig.messagesPart4);
// The first message points to the beginning of the store.
this->messages[messageIndex++] =
this->messageStore;
int upTo =
strlen(this->messageStore) - 1;
while (i < upTo)
{
if (this->messageStore[i] == '|')
{
// Message ending found, replace it with string-end character.
this->messageStore[i] = '\0';
DEBUG_MSG("CannedMessage %d is: '%s'\n",
messageIndex-1, this->messages[messageIndex-1]);
// hit our max messages, bail
if (messageIndex >= CANNED_MESSAGE_PLUGIN_MESSAGE_MAX_COUNT)
{
this->messagesCount = messageIndex;
return this->messagesCount;
}
// Next message starts after pipe (|) just found.
this->messages[messageIndex++] =
(this->messageStore + i + 1);
}
i += 1;
}
if (strlen(this->messages[messageIndex-1]) > 0)
{
// We have a last message.
DEBUG_MSG("CannedMessage %d is: '%s'\n",
messageIndex-1, this->messages[messageIndex-1]);
this->messagesCount = messageIndex;
}
else
{
this->messagesCount = messageIndex-1;
}
return this->messagesCount;
}
int CannedMessagePlugin::handleInputEvent(const InputEvent *event)
{
if (
(strlen(radioConfig.preferences.canned_message_plugin_allow_input_source) > 0) &&
(strcmp(radioConfig.preferences.canned_message_plugin_allow_input_source, event->source) != 0) &&
(strcmp(radioConfig.preferences.canned_message_plugin_allow_input_source, "_any") != 0))
{
// Event source is not accepted.
// Event only accepted if source matches the configured one, or
// the configured one is "_any" (or if there is no configured
// source at all)
return 0;
}
bool validEvent = false;
if (event->inputEvent == static_cast<char>(InputEventChar_KEY_UP))
{
DEBUG_MSG("Canned message event UP\n");
this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_UP;
validEvent = true;
}
if (event->inputEvent == static_cast<char>(InputEventChar_KEY_DOWN))
{
DEBUG_MSG("Canned message event DOWN\n");
this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_DOWN;
validEvent = true;
}
if (event->inputEvent == static_cast<char>(InputEventChar_KEY_SELECT))
{
DEBUG_MSG("Canned message event Select\n");
this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT;
validEvent = true;
}
if (validEvent)
{
// Let runOnce to be called immediately.
setIntervalFromNow(0);
}
return 0;
}
void CannedMessagePlugin::sendText(NodeNum dest,
const char* message,
bool wantReplies)
{
MeshPacket *p = allocDataPacket();
p->to = dest;
p->want_ack = true;
p->decoded.payload.size = strlen(message);
memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size);
if (radioConfig.preferences.canned_message_plugin_send_bell)
{
p->decoded.payload.bytes[p->decoded.payload.size-1] = 7; // Bell character
p->decoded.payload.bytes[p->decoded.payload.size] = '\0'; // Bell character
p->decoded.payload.size++;
}
DEBUG_MSG("Sending message id=%d, msg=%.*s\n",
p->id, p->decoded.payload.size, p->decoded.payload.bytes);
service.sendToMesh(p);
}
int32_t CannedMessagePlugin::runOnce()
{
if ((!radioConfig.preferences.canned_message_plugin_enabled)
|| (this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED)
|| (this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE))
{
return 30000; // TODO: should return MAX_VAL
}
DEBUG_MSG("Check status\n");
UIFrameEvent e = {false, true};
if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE)
{
// TODO: might have some feedback of sendig state
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
e.frameChanged = true;
this->currentMessageIndex = -1;
this->notifyObservers(&e);
}
else if (
(this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE)
&& (millis() - this->lastTouchMillis) > INACTIVATE_AFTER_MS)
{
// Reset plugin
DEBUG_MSG("Reset due the lack of activity.\n");
e.frameChanged = true;
this->currentMessageIndex = -1;
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
this->notifyObservers(&e);
}
else if (this->currentMessageIndex == -1)
{
this->currentMessageIndex = 0;
DEBUG_MSG("First touch (%d):%s\n", this->currentMessageIndex, this->getCurrentMessage());
e.frameChanged = true;
this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE;
}
else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_SELECT)
{
sendText(
NODENUM_BROADCAST,
this->messages[this->currentMessageIndex],
true);
this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE;
this->currentMessageIndex = -1;
this->notifyObservers(&e);
return 2000;
}
else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_UP)
{
this->currentMessageIndex = getPrevIndex();
this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE;
DEBUG_MSG("MOVE UP (%d):%s\n", this->currentMessageIndex, this->getCurrentMessage());
}
else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_DOWN)
{
this->currentMessageIndex = this->getNextIndex();
this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE;
DEBUG_MSG("MOVE DOWN (%d):%s\n", this->currentMessageIndex, this->getCurrentMessage());
}
if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE)
{
this->lastTouchMillis = millis();
this->notifyObservers(&e);
return INACTIVATE_AFTER_MS;
}
return 30000; // TODO: should return MAX_VAL
}
const char* CannedMessagePlugin::getCurrentMessage()
{
return this->messages[this->currentMessageIndex];
}
const char* CannedMessagePlugin::getPrevMessage()
{
return this->messages[this->getPrevIndex()];
}
const char* CannedMessagePlugin::getNextMessage()
{
return this->messages[this->getNextIndex()];
}
bool CannedMessagePlugin::shouldDraw()
{
if (!radioConfig.preferences.canned_message_plugin_enabled)
{
return false;
}
return (currentMessageIndex != -1) || (this->runState != CANNED_MESSAGE_RUN_STATE_INACTIVE);
}
int CannedMessagePlugin::getNextIndex()
{
if (this->currentMessageIndex >= (this->messagesCount -1))
{
return 0;
}
else
{
return this->currentMessageIndex + 1;
}
}
int CannedMessagePlugin::getPrevIndex()
{
if (this->currentMessageIndex <= 0)
{
return this->messagesCount - 1;
}
else
{
return this->currentMessageIndex - 1;
}
}
void CannedMessagePlugin::drawFrame(
OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
displayedNodeNum = 0; // Not currently showing a node pane
if (cannedMessagePlugin->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE)
{
display->setTextAlignment(TEXT_ALIGN_CENTER);
display->setFont(FONT_MEDIUM);
display->drawString(display->getWidth()/2 + x, 0 + y + 12, "Sending...");
}
else
{
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL);
display->drawString(0 + x, 0 + y, cannedMessagePlugin->getPrevMessage());
display->setFont(FONT_MEDIUM);
display->drawString(0 + x, 0 + y + 8, cannedMessagePlugin->getCurrentMessage());
display->setFont(FONT_SMALL);
display->drawString(0 + x, 0 + y + 24, cannedMessagePlugin->getNextMessage());
}
}
void CannedMessagePlugin::loadProtoForPlugin()
{
if (!loadProto(cannedMessagesConfigFile, CannedMessagePluginConfig_size, sizeof(cannedMessagesConfigFile), CannedMessagePluginConfig_fields, &cannedMessagePluginConfig)) {
installDefaultCannedMessagePluginConfig();
}
}
/**
* @brief Save the plugin config to file.
*
* @return true On success.
* @return false On error.
*/
bool CannedMessagePlugin::saveProtoForPlugin()
{
bool okay = true;
#ifdef FS
FS.mkdir("/prefs");
#endif
okay &= saveProto(cannedMessagesConfigFile, CannedMessagePluginConfig_size, sizeof(CannedMessagePluginConfig), CannedMessagePluginConfig_fields, &cannedMessagePluginConfig);
return okay;
}
/**
* @brief Fill configuration with default values.
*/
void CannedMessagePlugin::installDefaultCannedMessagePluginConfig()
{
memset(cannedMessagePluginConfig.messagesPart1, 0, sizeof(cannedMessagePluginConfig.messagesPart1));
memset(cannedMessagePluginConfig.messagesPart2, 0, sizeof(cannedMessagePluginConfig.messagesPart2));
memset(cannedMessagePluginConfig.messagesPart3, 0, sizeof(cannedMessagePluginConfig.messagesPart3));
memset(cannedMessagePluginConfig.messagesPart4, 0, sizeof(cannedMessagePluginConfig.messagesPart4));
}
/**
* @brief An admin message arrived to AdminPlugin. 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 CannedMessagePlugin::handleAdminMessageForPlugin(
const MeshPacket &mp, AdminMessage *request, AdminMessage *response)
{
AdminMessageHandleResult result;
switch (request->which_variant) {
case AdminMessage_get_canned_message_plugin_part1_request_tag:
DEBUG_MSG("Client is getting radio canned message part1\n");
this->handleGetCannedMessagePluginPart1(mp, response);
result = AdminMessageHandleResult::HANDLED_WITH_RESPONSE;
break;
case AdminMessage_get_canned_message_plugin_part2_request_tag:
DEBUG_MSG("Client is getting radio canned message part2\n");
this->handleGetCannedMessagePluginPart2(mp, response);
result = AdminMessageHandleResult::HANDLED_WITH_RESPONSE;
break;
case AdminMessage_get_canned_message_plugin_part3_request_tag:
DEBUG_MSG("Client is getting radio canned message part3\n");
this->handleGetCannedMessagePluginPart3(mp, response);
result = AdminMessageHandleResult::HANDLED_WITH_RESPONSE;
break;
case AdminMessage_get_canned_message_plugin_part4_request_tag:
DEBUG_MSG("Client is getting radio canned message part4\n");
this->handleGetCannedMessagePluginPart4(mp, response);
result = AdminMessageHandleResult::HANDLED_WITH_RESPONSE;
break;
case AdminMessage_set_canned_message_plugin_part1_tag:
DEBUG_MSG("Client is setting radio canned message part 1\n");
this->handleSetCannedMessagePluginPart1(
request->set_canned_message_plugin_part1);
result = AdminMessageHandleResult::HANDLED;
break;
case AdminMessage_set_canned_message_plugin_part2_tag:
DEBUG_MSG("Client is setting radio canned message part 2\n");
this->handleSetCannedMessagePluginPart2(
request->set_canned_message_plugin_part2);
result = AdminMessageHandleResult::HANDLED;
break;
case AdminMessage_set_canned_message_plugin_part3_tag:
DEBUG_MSG("Client is setting radio canned message part 3\n");
this->handleSetCannedMessagePluginPart3(
request->set_canned_message_plugin_part3);
result = AdminMessageHandleResult::HANDLED;
break;
case AdminMessage_set_canned_message_plugin_part4_tag:
DEBUG_MSG("Client is setting radio canned message part 4\n");
this->handleSetCannedMessagePluginPart4(
request->set_canned_message_plugin_part4);
result = AdminMessageHandleResult::HANDLED;
break;
default:
result = AdminMessageHandleResult::NOT_HANDLED;
}
return result;
}
void CannedMessagePlugin::handleGetCannedMessagePluginPart1(
const MeshPacket &req, AdminMessage *response)
{
DEBUG_MSG("*** handleGetCannedMessagePluginPart1\n");
assert(req.decoded.want_response);
response->which_variant = AdminMessage_get_canned_message_plugin_part1_response_tag;
strcpy(
response->get_canned_message_plugin_part1_response,
cannedMessagePluginConfig.messagesPart1);
}
void CannedMessagePlugin::handleGetCannedMessagePluginPart2(
const MeshPacket &req, AdminMessage *response)
{
DEBUG_MSG("*** handleGetCannedMessagePluginPart2\n");
assert(req.decoded.want_response);
response->which_variant = AdminMessage_get_canned_message_plugin_part2_response_tag;
strcpy(
response->get_canned_message_plugin_part2_response,
cannedMessagePluginConfig.messagesPart2);
}
void CannedMessagePlugin::handleGetCannedMessagePluginPart3(
const MeshPacket &req, AdminMessage *response)
{
DEBUG_MSG("*** handleGetCannedMessagePluginPart3\n");
assert(req.decoded.want_response);
response->which_variant = AdminMessage_get_canned_message_plugin_part3_response_tag;
strcpy(
response->get_canned_message_plugin_part3_response,
cannedMessagePluginConfig.messagesPart3);
}
void CannedMessagePlugin::handleGetCannedMessagePluginPart4(
const MeshPacket &req, AdminMessage *response)
{
DEBUG_MSG("*** handleGetCannedMessagePluginPart4\n");
assert(req.decoded.want_response);
response->which_variant = AdminMessage_get_canned_message_plugin_part4_response_tag;
strcpy(
response->get_canned_message_plugin_part4_response,
cannedMessagePluginConfig.messagesPart4);
}
void CannedMessagePlugin::handleSetCannedMessagePluginPart1(const char *from_msg)
{
int changed = 0;
if (*from_msg)
{
changed |= strcmp(cannedMessagePluginConfig.messagesPart1, from_msg);
strcpy(cannedMessagePluginConfig.messagesPart1, from_msg);
DEBUG_MSG("*** from_msg.text:%s\n", from_msg);
}
if (changed)
{
this->saveProtoForPlugin();
}
}
void CannedMessagePlugin::handleSetCannedMessagePluginPart2(const char *from_msg)
{
int changed = 0;
if (*from_msg)
{
changed |= strcmp(cannedMessagePluginConfig.messagesPart2, from_msg);
strcpy(cannedMessagePluginConfig.messagesPart2, from_msg);
}
if (changed)
{
this->saveProtoForPlugin();
}
}
void CannedMessagePlugin::handleSetCannedMessagePluginPart3(const char *from_msg)
{
int changed = 0;
if (*from_msg)
{
changed |= strcmp(cannedMessagePluginConfig.messagesPart3, from_msg);
strcpy(cannedMessagePluginConfig.messagesPart3, from_msg);
}
if (changed)
{
this->saveProtoForPlugin();
}
}
void CannedMessagePlugin::handleSetCannedMessagePluginPart4(const char *from_msg)
{
int changed = 0;
if (*from_msg)
{
changed |= strcmp(cannedMessagePluginConfig.messagesPart4, from_msg);
strcpy(cannedMessagePluginConfig.messagesPart4, from_msg);
}
if (changed)
{
this->saveProtoForPlugin();
}
}

View File

@@ -0,0 +1,86 @@
#pragma once
#include "ProtobufPlugin.h"
#include "input/InputBroker.h"
enum cannedMessagePluginRunState
{
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_ACTION_SELECT,
CANNED_MESSAGE_RUN_STATE_ACTION_UP,
CANNED_MESSAGE_RUN_STATE_ACTION_DOWN,
};
#define CANNED_MESSAGE_PLUGIN_MESSAGE_MAX_COUNT 50
/**
* Sum of CannedMessagePluginConfig part sizes.
*/
#define CANNED_MESSAGE_PLUGIN_MESSAGES_SIZE 800
class CannedMessagePlugin :
public SinglePortPlugin,
public Observable<const UIFrameEvent *>,
private concurrency::OSThread
{
CallbackObserver<CannedMessagePlugin, const InputEvent *> inputObserver =
CallbackObserver<CannedMessagePlugin, const InputEvent *>(
this, &CannedMessagePlugin::handleInputEvent);
public:
CannedMessagePlugin();
const char* getCurrentMessage();
const char* getPrevMessage();
const char* getNextMessage();
bool shouldDraw();
void eventUp();
void eventDown();
void eventSelect();
void handleGetCannedMessagePluginPart1(const MeshPacket &req, AdminMessage *response);
void handleGetCannedMessagePluginPart2(const MeshPacket &req, AdminMessage *response);
void handleGetCannedMessagePluginPart3(const MeshPacket &req, AdminMessage *response);
void handleGetCannedMessagePluginPart4(const MeshPacket &req, AdminMessage *response);
void handleSetCannedMessagePluginPart1(const char *from_msg);
void handleSetCannedMessagePluginPart2(const char *from_msg);
void handleSetCannedMessagePluginPart3(const char *from_msg);
void handleSetCannedMessagePluginPart4(const char *from_msg);
protected:
virtual int32_t runOnce() override;
void sendText(
NodeNum dest,
const char* message,
bool wantReplies);
int splitConfiguredMessages();
int getNextIndex();
int getPrevIndex();
int handleInputEvent(const InputEvent *event);
virtual bool wantUIFrame() override { return this->shouldDraw(); }
virtual Observable<const UIFrameEvent *>* getUIFrameObservable() override { return this; }
virtual void drawFrame(
OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override;
virtual AdminMessageHandleResult handleAdminMessageForPlugin(
const MeshPacket &mp, AdminMessage *request, AdminMessage *response) override;
void loadProtoForPlugin();
bool saveProtoForPlugin();
void installDefaultCannedMessagePluginConfig();
int currentMessageIndex = -1;
cannedMessagePluginRunState runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
char messageStore[CANNED_MESSAGE_PLUGIN_MESSAGES_SIZE+1];
char *messages[CANNED_MESSAGE_PLUGIN_MESSAGE_MAX_COUNT];
int messagesCount = 0;
unsigned long lastTouchMillis = 0;
};
extern CannedMessagePlugin *cannedMessagePlugin;

View File

@@ -0,0 +1,187 @@
#include "configuration.h"
#include "ExternalNotificationPlugin.h"
#include "MeshService.h"
#include "NodeDB.h"
#include "RTC.h"
#include "Router.h"
#include <Arduino.h>
//#include <assert.h>
/*
Documentation:
https://github.com/meshtastic/Meshtastic-device/blob/master/docs/software/modules/ExternalNotificationPlugin.md
This plugin supports:
https://github.com/meshtastic/Meshtastic-device/issues/654
Quick reference:
radioConfig.preferences.ext_notification_plugin_enabled
0 = Disabled (Default)
1 = Enabled
radioConfig.preferences.ext_notification_plugin_active
0 = Active Low (Default)
1 = Active High
radioConfig.preferences.ext_notification_plugin_alert_message
0 = Disabled (Default)
1 = Alert when a text message comes
radioConfig.preferences.ext_notification_plugin_alert_bell
0 = Disabled (Default)
1 = Alert when the bell character is received
radioConfig.preferences.ext_notification_plugin_output
GPIO of the output. (Default = 13)
radioConfig.preferences.ext_notification_plugin_output_ms
Amount of time in ms for the alert. Default is 1000.
*/
// Default configurations
#define EXT_NOTIFICATION_PLUGIN_OUTPUT EXT_NOTIFY_OUT
#define EXT_NOTIFICATION_PLUGIN_OUTPUT_MS 1000
#define ASCII_BELL 0x07
bool externalCurrentState = 0;
uint32_t externalTurnedOn = 0;
int32_t ExternalNotificationPlugin::runOnce()
{
/*
Uncomment the preferences below if you want to use the plugin
without having to configure it from the PythonAPI or WebUI.
*/
// radioConfig.preferences.ext_notification_plugin_enabled = 1;
// radioConfig.preferences.ext_notification_plugin_alert_message = 1;
// radioConfig.preferences.ext_notification_plugin_active = 1;
// radioConfig.preferences.ext_notification_plugin_alert_bell = 1;
// radioConfig.preferences.ext_notification_plugin_output_ms = 1000;
// radioConfig.preferences.ext_notification_plugin_output = 13;
if (externalCurrentState) {
// If the output is turned on, turn it back off after the given period of time.
if (externalTurnedOn + (radioConfig.preferences.ext_notification_plugin_output_ms
? radioConfig.preferences.ext_notification_plugin_output_ms
: EXT_NOTIFICATION_PLUGIN_OUTPUT_MS) <
millis()) {
DEBUG_MSG("Turning off external notification\n");
setExternalOff();
}
}
return (25);
}
void ExternalNotificationPlugin::setExternalOn()
{
#ifdef EXT_NOTIFY_OUT
externalCurrentState = 1;
externalTurnedOn = millis();
digitalWrite((radioConfig.preferences.ext_notification_plugin_output ? radioConfig.preferences.ext_notification_plugin_output
: EXT_NOTIFICATION_PLUGIN_OUTPUT),
(radioConfig.preferences.ext_notification_plugin_active ? true : false));
#endif
}
void ExternalNotificationPlugin::setExternalOff()
{
#ifdef EXT_NOTIFY_OUT
externalCurrentState = 0;
digitalWrite((radioConfig.preferences.ext_notification_plugin_output ? radioConfig.preferences.ext_notification_plugin_output
: EXT_NOTIFICATION_PLUGIN_OUTPUT),
(radioConfig.preferences.ext_notification_plugin_active ? false : true));
#endif
}
// --------
ExternalNotificationPlugin::ExternalNotificationPlugin()
: SinglePortPlugin("ExternalNotificationPlugin", PortNum_TEXT_MESSAGE_APP), concurrency::OSThread(
"ExternalNotificationPlugin")
{
// restrict to the admin channel for rx
boundChannel = Channels::gpioChannel;
#ifndef NO_ESP32
#ifdef EXT_NOTIFY_OUT
/*
Uncomment the preferences below if you want to use the plugin
without having to configure it from the PythonAPI or WebUI.
*/
// radioConfig.preferences.ext_notification_plugin_enabled = 1;
// radioConfig.preferences.ext_notification_plugin_alert_message = 1;
// radioConfig.preferences.ext_notification_plugin_active = 1;
// radioConfig.preferences.ext_notification_plugin_alert_bell = 1;
// radioConfig.preferences.ext_notification_plugin_output_ms = 1000;
// radioConfig.preferences.ext_notification_plugin_output = 13;
if (radioConfig.preferences.ext_notification_plugin_enabled) {
DEBUG_MSG("Initializing External Notification Plugin\n");
// Set the direction of a pin
pinMode((radioConfig.preferences.ext_notification_plugin_output ? radioConfig.preferences.ext_notification_plugin_output
: EXT_NOTIFICATION_PLUGIN_OUTPUT),
OUTPUT);
// Turn off the pin
setExternalOff();
} else {
DEBUG_MSG("External Notification Plugin Disabled\n");
enabled = false;
}
#endif
#endif
}
ProcessMessage ExternalNotificationPlugin::handleReceived(const MeshPacket &mp)
{
#ifndef NO_ESP32
#ifdef EXT_NOTIFY_OUT
if (radioConfig.preferences.ext_notification_plugin_enabled) {
if (getFrom(&mp) != nodeDB.getNodeNum()) {
// TODO: This may be a problem if messages are sent in unicide, but I'm not sure if it will.
// Need to know if and how this could be a problem.
if (radioConfig.preferences.ext_notification_plugin_alert_bell) {
auto &p = mp.decoded;
DEBUG_MSG("externalNotificationPlugin - Notification Bell\n");
for (int i = 0; i < p.payload.size; i++) {
if (p.payload.bytes[i] == ASCII_BELL) {
setExternalOn();
}
}
}
if (radioConfig.preferences.ext_notification_plugin_alert_message) {
DEBUG_MSG("externalNotificationPlugin - Notification Plugin\n");
setExternalOn();
}
}
} else {
DEBUG_MSG("External Notification Plugin Disabled\n");
}
#endif
#endif
return ProcessMessage::CONTINUE; // Let others look at this message also if they want
}

View File

@@ -0,0 +1,32 @@
#pragma once
#include "SinglePortPlugin.h"
#include "concurrency/OSThread.h"
#include "configuration.h"
#include <Arduino.h>
#include <functional>
/*
* Radio interface for ExternalNotificationPlugin
*
*/
class ExternalNotificationPlugin : public SinglePortPlugin, private concurrency::OSThread
{
public:
ExternalNotificationPlugin();
void setExternalOn();
void setExternalOff();
void getExternal();
protected:
// virtual MeshPacket *allocReply();
/** 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 MeshPacket &mp) override;
virtual int32_t runOnce() override;
};

View File

@@ -0,0 +1,74 @@
#include "configuration.h"
#include "NodeInfoPlugin.h"
#include "MeshService.h"
#include "NodeDB.h"
#include "RTC.h"
#include "Router.h"
#include "main.h"
NodeInfoPlugin *nodeInfoPlugin;
bool NodeInfoPlugin::handleReceivedProtobuf(const MeshPacket &mp, User *pptr)
{
auto p = *pptr;
nodeDB.updateUser(getFrom(&mp), p);
bool wasBroadcast = mp.to == NODENUM_BROADCAST;
// Show new nodes on LCD screen
if (wasBroadcast) {
String lcd = String("Joined: ") + p.long_name + "\n";
if(screen)
screen->print(lcd.c_str());
}
// DEBUG_MSG("did handleReceived\n");
return false; // Let others look at this message also if they want
}
void NodeInfoPlugin::sendOurNodeInfo(NodeNum dest, bool wantReplies)
{
// 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);
MeshPacket *p = allocReply();
p->to = dest;
p->decoded.want_response = wantReplies;
p->priority = MeshPacket_Priority_BACKGROUND;
prevPacketId = p->id;
service.sendToMesh(p);
}
MeshPacket *NodeInfoPlugin::allocReply()
{
User &u = owner;
DEBUG_MSG("sending owner %s/%s/%s\n", u.id, u.long_name, u.short_name);
return allocDataProtobuf(u);
}
NodeInfoPlugin::NodeInfoPlugin()
: ProtobufPlugin("nodeinfo", PortNum_NODEINFO_APP, User_fields), concurrency::OSThread("NodeInfoPlugin")
{
isPromiscuous = true; // We always want to update our nodedb, even if we are sniffing on others
setIntervalFromNow(30 *
1000); // Send our initial owner announcement 30 seconds after we start (to give network time to setup)
}
int32_t NodeInfoPlugin::runOnce()
{
static uint32_t currentGeneration;
// If we changed channels, ask everyone else for their latest info
bool requestReplies = currentGeneration != radioGeneration;
currentGeneration = radioGeneration;
DEBUG_MSG("Sending our nodeinfo to mesh (wantReplies=%d)\n", requestReplies);
sendOurNodeInfo(NODENUM_BROADCAST, requestReplies); // Send our info (don't request replies)
return getPref_position_broadcast_secs() * 1000;
}

View File

@@ -0,0 +1,39 @@
#pragma once
#include "ProtobufPlugin.h"
/**
* NodeInfo plugin for sending/receiving NodeInfos into the mesh
*/
class NodeInfoPlugin : public ProtobufPlugin<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
*/
NodeInfoPlugin();
/**
* Send our NodeInfo into the mesh
*/
void sendOurNodeInfo(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = 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 MeshPacket &mp, 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 MeshPacket *allocReply() override;
/** Does our periodic broadcast */
virtual int32_t runOnce() override;
};
extern NodeInfoPlugin *nodeInfoPlugin;

10
src/modules/PluginDev.h Normal file
View File

@@ -0,0 +1,10 @@
#pragma once
/*
* To developers:
* Use this to enable / disable features in your plugin that you don't want to risk checking into GitHub.
*
*/
// Enable development more for StoreForwardPlugin
bool StoreForward_Dev = false;

62
src/modules/Plugins.cpp Normal file
View File

@@ -0,0 +1,62 @@
#include "configuration.h"
#include "input/InputBroker.h"
#include "input/RotaryEncoderInterruptImpl1.h"
#include "modules/AdminPlugin.h"
#include "modules/CannedMessagePlugin.h"
#include "modules/ExternalNotificationPlugin.h"
#include "modules/NodeInfoPlugin.h"
#include "modules/PositionPlugin.h"
#include "modules/RemoteHardwarePlugin.h"
#include "modules/ReplyPlugin.h"
#include "modules/RoutingPlugin.h"
#include "modules/TextMessagePlugin.h"
#ifndef PORTDUINO
#include "modules/Telemetry/Telemetry.h"
#endif
#ifndef NO_ESP32
#include "modules/esp32/RangeTestPlugin.h"
#include "modules/esp32/SerialPlugin.h"
#include "modules/esp32/StoreForwardPlugin.h"
#endif
/**
* Create plugin instances here. If you are adding a new plugin, you must 'new' it here (or somewhere else)
*/
void setupPlugins()
{
inputBroker = new InputBroker();
adminPlugin = new AdminPlugin();
nodeInfoPlugin = new NodeInfoPlugin();
positionPlugin = new PositionPlugin();
textMessagePlugin = new TextMessagePlugin();
// Note: if the rest of meshtastic doesn't need to explicitly use your plugin, you do not need to assign the instance
// to a global variable.
new RemoteHardwarePlugin();
new ReplyPlugin();
rotaryEncoderInterruptImpl1 = new RotaryEncoderInterruptImpl1();
rotaryEncoderInterruptImpl1->init();
cannedMessagePlugin = new CannedMessagePlugin();
#ifndef PORTDUINO
new TelemetryPlugin();
#endif
#ifndef NO_ESP32
// Only run on an esp32 based device.
/*
Maintained by MC Hamster (Jm Casler) jm@casler.org
*/
new SerialPlugin();
new ExternalNotificationPlugin();
// rangeTestPlugin = new RangeTestPlugin();
storeForwardPlugin = new StoreForwardPlugin();
new RangeTestPlugin();
// new StoreForwardPlugin();
#endif
// NOTE! This plugin must be added LAST because it likes to check for replies from other plugins and avoid sending extra acks
routingPlugin = new RoutingPlugin();
}

6
src/modules/Plugins.h Normal file
View File

@@ -0,0 +1,6 @@
#pragma once
/**
* Create plugin instances here. If you are adding a new plugin, you must 'new' it here (or somewhere else)
*/
void setupPlugins();

View File

@@ -0,0 +1,206 @@
#include "PositionPlugin.h"
#include "MeshService.h"
#include "NodeDB.h"
#include "RTC.h"
#include "Router.h"
#include "airtime.h"
#include "configuration.h"
#include "gps/GeoCoord.h"
PositionPlugin *positionPlugin;
PositionPlugin::PositionPlugin()
: ProtobufPlugin("position", PortNum_POSITION_APP, Position_fields), concurrency::OSThread("PositionPlugin")
{
isPromiscuous = true; // We always want to update our nodedb, even if we are sniffing on others
setIntervalFromNow(60 * 1000); // Send our initial position 60 seconds after we start (to give GPS time to setup)
}
bool PositionPlugin::handleReceivedProtobuf(const MeshPacket &mp, 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)
if (nodeDB.getNodeNum() == getFrom(&mp)) {
DEBUG_MSG("Incoming update from MYSELF\n");
// DEBUG_MSG("Ignored an incoming update from MYSELF\n");
// return false;
}
// Log packet size and list of fields
DEBUG_MSG("POSITION node=%08x l=%d %s%s%s%s%s%s%s%s%s%s%s%s%s%s\n", getFrom(&mp), mp.decoded.payload.size,
p.latitude_i ? "LAT " : "", p.longitude_i ? "LON " : "", p.altitude ? "MSL " : "", p.altitude_hae ? "HAE " : "",
p.alt_geoid_sep ? "GEO " : "", p.PDOP ? "PDOP " : "", p.HDOP ? "HDOP " : "", p.VDOP ? "VDOP " : "",
p.sats_in_view ? "SIV " : "", p.fix_quality ? "FXQ " : "", p.fix_type ? "FXT " : "", p.pos_timestamp ? "PTS " : "",
p.time ? "TIME " : "", p.battery_level ? "BAT " : "");
if (p.time) {
struct timeval tv;
uint32_t secs = p.time;
tv.tv_sec = secs;
tv.tv_usec = 0;
perhapsSetRTC(RTCQualityFromNet, &tv);
}
nodeDB.updatePosition(getFrom(&mp), p);
return false; // Let others look at this message also if they want
}
MeshPacket *PositionPlugin::allocReply()
{
NodeInfo *node = service.refreshMyNodeInfo(); // 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 = radioConfig.preferences.position_flags;
// Populate a Position struct with ONLY the requested fields
Position p = Position_init_default; // Start with an empty structure
// lat/lon are unconditionally included - IF AVAILABLE!
p.latitude_i = node->position.latitude_i;
p.longitude_i = node->position.longitude_i;
p.time = node->position.time;
if (pos_flags & PositionFlags_POS_BATTERY)
p.battery_level = node->position.battery_level;
if (pos_flags & PositionFlags_POS_ALTITUDE) {
if (pos_flags & PositionFlags_POS_ALT_MSL)
p.altitude = node->position.altitude;
else
p.altitude_hae = node->position.altitude_hae;
if (pos_flags & PositionFlags_POS_GEO_SEP)
p.alt_geoid_sep = node->position.alt_geoid_sep;
}
if (pos_flags & PositionFlags_POS_DOP) {
if (pos_flags & PositionFlags_POS_HVDOP) {
p.HDOP = node->position.HDOP;
p.VDOP = node->position.VDOP;
} else
p.PDOP = node->position.PDOP;
}
if (pos_flags & PositionFlags_POS_SATINVIEW)
p.sats_in_view = node->position.sats_in_view;
if (pos_flags & PositionFlags_POS_TIMESTAMP)
p.pos_timestamp = node->position.pos_timestamp;
// Strip out any time information before sending packets to other nodes - to keep the wire size small (and because other
// nodes shouldn't trust it anyways) Note: we allow a device with a local GPS to include the time, so that gpsless
// devices can get time.
if (getRTCQuality() < RTCQualityGPS) {
DEBUG_MSG("Stripping time %u from position send\n", p.time);
p.time = 0;
} else
DEBUG_MSG("Providing time to mesh %u\n", p.time);
return allocDataProtobuf(p);
}
void PositionPlugin::sendOurPosition(NodeNum dest, bool wantReplies)
{
// 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);
MeshPacket *p = allocReply();
p->to = dest;
p->decoded.want_response = wantReplies;
p->priority = MeshPacket_Priority_BACKGROUND;
prevPacketId = p->id;
service.sendToMesh(p);
}
int32_t PositionPlugin::runOnce()
{
NodeInfo *node = nodeDB.getNode(nodeDB.getNodeNum());
// radioConfig.preferences.position_broadcast_smart = true;
// We limit our GPS broadcasts to a max rate
uint32_t now = millis();
if (lastGpsSend == 0 || now - lastGpsSend >= getPref_position_broadcast_secs() * 1000) {
// Only send packets if the channel is less than 40% utilized.
if (airTime->channelUtilizationPercent() < 40) {
lastGpsSend = now;
lastGpsLatitude = node->position.latitude_i;
lastGpsLongitude = node->position.longitude_i;
// If we changed channels, ask everyone else for their latest info
bool requestReplies = currentGeneration != radioGeneration;
currentGeneration = radioGeneration;
DEBUG_MSG("Sending pos@%x:6 to mesh (wantReplies=%d)\n", node->position.pos_timestamp, requestReplies);
sendOurPosition(NODENUM_BROADCAST, requestReplies);
} else {
DEBUG_MSG("Channel utilization is >50 percent. Skipping this opportunity to send.\n");
}
} else if (radioConfig.preferences.position_broadcast_smart == true) {
// Only send packets if the channel is less than 40% utilized.
if (airTime->channelUtilizationPercent() < 40) {
NodeInfo *node2 = service.refreshMyNodeInfo(); // should guarantee there is now a position
if (node2->has_position && (node2->position.latitude_i != 0 || node2->position.longitude_i != 0)) {
// The minimum distance to travel before we are able to send a new position packet.
const uint32_t distanceTravelMinimum = 30;
// The minimum time that would pass before we are able to send a new position packet.
const uint32_t timeTravelMinimum = 30;
// Determine the distance in meters between two points on the globe
float distance = GeoCoord::latLongToMeter(lastGpsLatitude * 1e-7, lastGpsLongitude * 1e-7,
node->position.latitude_i * 1e-7, node->position.longitude_i * 1e-7);
// Yes, this has a bunch of magic numbers. Sorry. This is to make the scale non-linear.
const float distanceTravelMath = 1203 / (sqrt(pow(myNodeInfo.bitrate, 1.5) / 1.1));
uint32_t distanceTravel =
(distanceTravelMath >= distanceTravelMinimum) ? distanceTravelMath : distanceTravelMinimum;
// Yes, this has a bunch of magic numbers. Sorry.
uint32_t timeTravel =
((1500 / myNodeInfo.bitrate) >= timeTravelMinimum) ? (1500 / myNodeInfo.bitrate) : timeTravelMinimum;
// If the distance traveled since the last update is greater than 100 meters
// and it's been at least 60 seconds since the last update
if ((abs(distance) >= distanceTravel) && (now - lastGpsSend >= timeTravel * 1000)) {
bool requestReplies = currentGeneration != radioGeneration;
currentGeneration = radioGeneration;
DEBUG_MSG("Sending smart pos@%x:6 to mesh (wantReplies=%d, dt=%d, tt=%d)\n", node2->position.pos_timestamp,
requestReplies, distanceTravel, timeTravel);
sendOurPosition(NODENUM_BROADCAST, requestReplies);
/* Update lastGpsSend to now. This means if the device is stationary, then
getPref_position_broadcast_secs will still apply.
*/
lastGpsSend = now;
}
}
} else {
DEBUG_MSG("Channel utilization is >40 percent. Skipping this opportunity to send.\n");
}
}
return 5000; // to save power only wake for our callback occasionally
}

View File

@@ -0,0 +1,50 @@
#pragma once
#include "ProtobufPlugin.h"
#include "concurrency/OSThread.h"
/**
* Position plugin for sending/receiving positions into the mesh
*/
class PositionPlugin : public ProtobufPlugin<Position>, 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;
/// 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
*/
PositionPlugin();
/**
* Send our position into the mesh
*/
void sendOurPosition(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = 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 MeshPacket &mp, 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 MeshPacket *allocReply() override;
/** Does our periodic broadcast */
virtual int32_t runOnce() override;
};
extern PositionPlugin *positionPlugin;

View File

@@ -0,0 +1,140 @@
#include "configuration.h"
#include "RemoteHardwarePlugin.h"
#include "MeshService.h"
#include "NodeDB.h"
#include "RTC.h"
#include "Router.h"
#include "main.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)
/// Set pin modes for every set bit in a mask
static void pinModes(uint64_t mask, uint8_t mode)
{
for (uint64_t i = 0; i < NUM_GPIOS; i++) {
if (mask & (1 << i)) {
pinMode(i, mode);
}
}
}
/// Read all the pins mentioned in a mask
static uint64_t digitalReads(uint64_t mask)
{
uint64_t res = 0;
// The Arduino docs show to run pinMode(). But, when testing, found it is best not to.
// If the line below is uncommented, read will flip the pin to the default of the second
// argument in pinModes(), which will make the read turn the PIN "on".
//pinModes(mask, INPUT_PULLUP);
for (uint64_t i = 0; i < NUM_GPIOS; i++) {
uint64_t m = 1 << i;
if (mask & m) {
if (digitalRead(i)) {
res |= m;
}
}
}
return res;
}
RemoteHardwarePlugin::RemoteHardwarePlugin()
: ProtobufPlugin("remotehardware", PortNum_REMOTE_HARDWARE_APP, HardwareMessage_fields), concurrency::OSThread(
"remotehardware")
{
}
bool RemoteHardwarePlugin::handleReceivedProtobuf(const MeshPacket &req, HardwareMessage *pptr)
{
auto p = *pptr;
DEBUG_MSG("Received RemoteHardware typ=%d\n", p.typ);
switch (p.typ) {
case HardwareMessage_Type_WRITE_GPIOS:
// Print notification to LCD screen
screen->print("Write GPIOs\n");
for (uint8_t i = 0; i < NUM_GPIOS; i++) {
uint64_t mask = 1 << i;
if (p.gpio_mask & mask) {
digitalWrite(i, (p.gpio_value & mask) ? 1 : 0);
}
}
pinModes(p.gpio_mask, OUTPUT);
break;
case HardwareMessage_Type_READ_GPIOS: {
// Print notification to LCD screen
if (screen)
screen->print("Read GPIOs\n");
uint64_t res = digitalReads(p.gpio_mask);
// Send the reply
HardwareMessage r = HardwareMessage_init_default;
r.typ = HardwareMessage_Type_READ_GPIOS_REPLY;
r.gpio_value = res;
r.gpio_mask = p.gpio_mask;
MeshPacket *p2 = allocDataProtobuf(r);
setReplyTo(p2, req);
myReply = p2;
break;
}
case 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
DEBUG_MSG("Now watching GPIOs 0x%llx\n", watchGpios);
break;
}
case HardwareMessage_Type_READ_GPIOS_REPLY:
case HardwareMessage_Type_GPIOS_CHANGED:
break; // Ignore - we might see our own replies
default:
DEBUG_MSG("Hardware operation %d not yet implemented! FIXME\n", p.typ);
break;
}
return false;
}
int32_t RemoteHardwarePlugin::runOnce()
{
if (watchGpios) {
uint32_t now = millis();
if (now - lastWatchMsec >= WATCH_INTERVAL_MSEC) {
uint64_t curVal = digitalReads(watchGpios);
if (curVal != previousWatch) {
previousWatch = curVal;
DEBUG_MSG("Broadcasting GPIOS 0x%llx changed!\n", curVal);
// Something changed! Tell the world with a broadcast message
HardwareMessage r = HardwareMessage_init_default;
r.typ = HardwareMessage_Type_GPIOS_CHANGED;
r.gpio_value = curVal;
MeshPacket *p = allocDataProtobuf(r);
service.sendToMesh(p);
}
}
} else {
// No longer watching anything - stop using CPU
enabled = false;
}
return 200; // Poll our GPIOs every 200ms (FIXME, make adjustable via protobuf arg)
}

View File

@@ -0,0 +1,43 @@
#pragma once
#include "ProtobufPlugin.h"
#include "mesh/generated/remote_hardware.pb.h"
#include "concurrency/OSThread.h"
/**
* A plugin that provides easy low-level remote access to device hardware.
*/
class RemoteHardwarePlugin : public ProtobufPlugin<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;
public:
/** Constructor
* name is for debugging output
*/
RemoteHardwarePlugin();
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 MeshPacket &mp, 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 RemoteHardwarePlugin remoteHardwarePlugin;

View File

@@ -0,0 +1,24 @@
#include "configuration.h"
#include "ReplyPlugin.h"
#include "MeshService.h"
#include "main.h"
#include <assert.h>
MeshPacket *ReplyPlugin::allocReply()
{
assert(currentRequest); // should always be !NULL
auto req = *currentRequest;
auto &p = req.decoded;
// The incoming message is in p.payload
DEBUG_MSG("Received message from=0x%0x, id=%d, msg=%.*s\n", req.from, req.id, p.payload.size, p.payload.bytes);
screen->print("Sending reply\n");
const char *replyStr = "Message Received";
auto reply = allocDataPacket(); // Allocate a packet for sending
reply->decoded.payload.size = strlen(replyStr); // You must specify how many bytes are in the reply
memcpy(reply->decoded.payload.bytes, replyStr, reply->decoded.payload.size);
return reply;
}

22
src/modules/ReplyPlugin.h Normal file
View File

@@ -0,0 +1,22 @@
#pragma once
#include "SinglePortPlugin.h"
/**
* A simple example plugin that just replies with "Message received" to any message it receives.
*/
class ReplyPlugin : public SinglePortPlugin
{
public:
/** Constructor
* name is for debugging output
*/
ReplyPlugin() : SinglePortPlugin("reply", PortNum_REPLY_APP) {}
protected:
/** For reply plugin we do all of our processing in the (normally optional)
* want_replies handling
*/
virtual MeshPacket *allocReply() override;
};

View File

@@ -0,0 +1,47 @@
#include "configuration.h"
#include "RoutingPlugin.h"
#include "MeshService.h"
#include "NodeDB.h"
#include "Router.h"
#include "main.h"
RoutingPlugin *routingPlugin;
bool RoutingPlugin::handleReceivedProtobuf(const MeshPacket &mp, Routing *r)
{
printPacket("Routing sniffing", &mp);
router->sniffReceived(&mp, r);
// FIXME - move this to a non promsicious PhoneAPI plugin?
// Note: we are careful not to send back packets that started with the phone back to the phone
if ((mp.to == NODENUM_BROADCAST || mp.to == nodeDB.getNodeNum()) && (mp.from != 0)) {
printPacket("Delivering rx packet", &mp);
service.handleFromRadio(&mp);
}
return false; // Let others look at this message also if they want
}
MeshPacket *RoutingPlugin::allocReply()
{
assert(currentRequest);
// We only consider making replies if the request was a legit routing packet (not just something we were sniffing)
if (currentRequest->decoded.portnum == PortNum_ROUTING_APP) {
assert(0); // 1.2 refactoring fixme, Not sure if anything needs this yet?
// return allocDataProtobuf(u);
}
return NULL;
}
void RoutingPlugin::sendAckNak(Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex)
{
auto p = allocAckNak(err, to, idFrom, chIndex);
router->sendLocal(p); // we sometimes send directly to the local node
}
RoutingPlugin::RoutingPlugin() : ProtobufPlugin("routing", PortNum_ROUTING_APP, Routing_fields)
{
isPromiscuous = true;
}

View File

@@ -0,0 +1,35 @@
#pragma once
#include "ProtobufPlugin.h"
#include "Channels.h"
/**
* Routing plugin for router control messages
*/
class RoutingPlugin : public ProtobufPlugin<Routing>
{
public:
/** Constructor
* name is for debugging output
*/
RoutingPlugin();
void sendAckNak(Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex);
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 MeshPacket &mp, 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 MeshPacket *allocReply() override;
/// Override wantPacket to say we want to see all packets, not just those for our port number
virtual bool wantPacket(const MeshPacket *p) override { return true; }
};
extern RoutingPlugin *routingPlugin;

View File

@@ -0,0 +1,29 @@
#include "../mesh/generated/telemetry.pb.h"
#include "configuration.h"
#include "TelemetrySensor.h"
#include "BME280Sensor.h"
#include <Adafruit_BME280.h>
BME280Sensor::BME280Sensor() : TelemetrySensor {} {
}
int32_t BME280Sensor::runOnce() {
unsigned bme280Status;
// Default i2c address for BME280
bme280Status = bme280.begin(0x76);
if (!bme280Status) {
DEBUG_MSG("Could not find a valid BME280 sensor, check wiring, address, sensor ID!");
// TODO more verbose diagnostics
} else {
DEBUG_MSG("Telemetry: Opened BME280 on default i2c bus");
}
return BME_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
}
bool BME280Sensor::getMeasurement(Telemetry *measurement) {
measurement->temperature = bme280.readTemperature();
measurement->relative_humidity = bme280.readHumidity();
measurement->barometric_pressure = bme280.readPressure() / 100.0F;
return true;
}

View File

@@ -0,0 +1,15 @@
#include "../mesh/generated/telemetry.pb.h"
#include "TelemetrySensor.h"
#include <Adafruit_BME280.h>
#define BME_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS 1000
class BME280Sensor : virtual public TelemetrySensor {
private:
Adafruit_BME280 bme280;
public:
BME280Sensor();
virtual int32_t runOnce() override;
virtual bool getMeasurement(Telemetry *measurement) override;
};

View File

@@ -0,0 +1,36 @@
#include "../mesh/generated/telemetry.pb.h"
#include "configuration.h"
#include "TelemetrySensor.h"
#include "BME680Sensor.h"
#include <Adafruit_BME680.h>
BME680Sensor::BME680Sensor() : TelemetrySensor {} {
}
int32_t BME680Sensor::runOnce() {
unsigned bme680Status;
// Default i2c address for BME680
bme680Status = bme680.begin(0x76);
if (!bme680Status) {
DEBUG_MSG("Could not find a valid BME680 sensor, check wiring, address, sensor ID!");
// TODO more verbose TelemetrySensor
} else {
DEBUG_MSG("Telemetry: Opened BME680 on default i2c bus");
// Set up oversampling and filter initialization
bme680.setTemperatureOversampling(BME680_OS_8X);
bme680.setHumidityOversampling(BME680_OS_2X);
bme680.setPressureOversampling(BME680_OS_4X);
bme680.setIIRFilterSize(BME680_FILTER_SIZE_3);
bme680.setGasHeater(320, 150); // 320*C for 150 ms
}
return (BME_680_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS);
}
bool BME680Sensor::getMeasurement(Telemetry *measurement) {
measurement->temperature = bme680.readTemperature();
measurement->relative_humidity = bme680.readHumidity();
measurement->barometric_pressure = bme680.readPressure() / 100.0F;
measurement->gas_resistance = bme680.readGas() / 1000.0;
return true;
}

View File

@@ -0,0 +1,15 @@
#include "../mesh/generated/telemetry.pb.h"
#include "TelemetrySensor.h"
#include <Adafruit_BME680.h>
#define BME_680_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS 1000
class BME680Sensor : virtual public TelemetrySensor {
private:
Adafruit_BME680 bme680;
public:
BME680Sensor();
virtual int32_t runOnce() override;
virtual bool getMeasurement(Telemetry *measurement) override;
};

View File

@@ -0,0 +1,36 @@
#include "../mesh/generated/telemetry.pb.h"
#include "configuration.h"
#include "MeshService.h"
#include "TelemetrySensor.h"
#include "DHTSensor.h"
#include <DHT.h>
DHTSensor::DHTSensor() : TelemetrySensor {} {
}
int32_t DHTSensor::runOnce() {
if (RadioConfig_UserPreferences_TelemetrySensorType_DHT11 ||
RadioConfig_UserPreferences_TelemetrySensorType_DHT12) {
dht = new DHT(radioConfig.preferences.telemetry_module_sensor_pin, DHT11);
}
else {
dht = new DHT(radioConfig.preferences.telemetry_module_sensor_pin, DHT22);
}
dht->begin();
dht->read();
DEBUG_MSG("Telemetry: Opened DHT11/DHT12 on pin: %d\n",
radioConfig.preferences.telemetry_module_sensor_pin);
return (DHT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS);
}
bool DHTSensor::getMeasurement(Telemetry *measurement) {
if (!dht->read(true)) {
DEBUG_MSG("Telemetry: FAILED TO READ DATA\n");
return false;
}
measurement->relative_humidity = dht->readHumidity();
measurement->temperature = dht->readTemperature();
return true;
}

View File

@@ -0,0 +1,15 @@
#include "../mesh/generated/telemetry.pb.h"
#include "TelemetrySensor.h"
#include <DHT.h>
#define DHT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS 1000
class DHTSensor : virtual public TelemetrySensor {
private:
DHT *dht = NULL;
public:
DHTSensor();
virtual int32_t runOnce() override;
virtual bool getMeasurement(Telemetry *measurement) override;
};

View File

@@ -0,0 +1,31 @@
#include "../mesh/generated/telemetry.pb.h"
#include "configuration.h"
#include "MeshService.h"
#include "TelemetrySensor.h"
#include "DallasSensor.h"
#include <DS18B20.h>
#include <OneWire.h>
DallasSensor::DallasSensor() : TelemetrySensor {} {
}
int32_t DallasSensor::runOnce() {
oneWire = new OneWire(radioConfig.preferences.telemetry_module_sensor_pin);
ds18b20 = new DS18B20(oneWire);
ds18b20->begin();
ds18b20->setResolution(12);
ds18b20->requestTemperatures();
DEBUG_MSG("Telemetry: Opened DS18B20 on pin: %d\n",
radioConfig.preferences.telemetry_module_sensor_pin);
return (DS18B20_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS);
}
bool DallasSensor::getMeasurement(Telemetry *measurement) {
if (ds18b20->isConversionComplete()) {
measurement->temperature = ds18b20->getTempC();
measurement->relative_humidity = 0;
ds18b20->requestTemperatures();
return true;
}
return false;
}

View File

@@ -0,0 +1,17 @@
#include "../mesh/generated/telemetry.pb.h"
#include "TelemetrySensor.h"
#include <DS18B20.h>
#include <OneWire.h>
#define DS18B20_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS 1000
class DallasSensor : virtual public TelemetrySensor {
private:
OneWire *oneWire = NULL;
DS18B20 *ds18b20 = NULL;
public:
DallasSensor();
virtual int32_t runOnce() override;
virtual bool getMeasurement(Telemetry *measurement) override;
};

View File

@@ -0,0 +1,28 @@
#include "../mesh/generated/telemetry.pb.h"
#include "configuration.h"
#include "TelemetrySensor.h"
#include "MCP9808Sensor.h"
#include <Adafruit_MCP9808.h>
MCP9808Sensor::MCP9808Sensor() : TelemetrySensor {} {
}
int32_t MCP9808Sensor::runOnce() {
unsigned mcp9808Status;
// Default i2c address for MCP9808
mcp9808Status = mcp9808.begin(0x18);
if (!mcp9808Status) {
DEBUG_MSG("Could not find a valid MCP9808 sensor, check wiring, address, sensor ID!");
} else {
DEBUG_MSG("TelemetrySensor: Opened MCP9808 on default i2c bus");
// Reduce resolution from 0.0625 degrees (precision) to 0.125 degrees (high).
mcp9808.setResolution(2);
}
return (MCP_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS);
}
bool MCP9808Sensor::getMeasurement(Telemetry *measurement) {
measurement->temperature = mcp9808.readTempC();
return true;
}

View File

@@ -0,0 +1,15 @@
#include "../mesh/generated/telemetry.pb.h"
#include "TelemetrySensor.h"
#include <Adafruit_MCP9808.h>
#define MCP_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS 1000
class MCP9808Sensor : virtual public TelemetrySensor {
private:
Adafruit_MCP9808 mcp9808;
public:
MCP9808Sensor();
virtual int32_t runOnce() override;
virtual bool getMeasurement(Telemetry *measurement) override;
};

View File

@@ -0,0 +1,12 @@
#pragma once
#include "../mesh/generated/telemetry.pb.h"
#define DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS 1000
class TelemetrySensor {
protected:
TelemetrySensor() { }
public:
virtual int32_t runOnce() = 0;
virtual bool getMeasurement(Telemetry *measurement) = 0;
};

View File

@@ -0,0 +1,299 @@
#include "Telemetry.h"
#include "../mesh/generated/telemetry.pb.h"
#include "MeshService.h"
#include "NodeDB.h"
#include "RTC.h"
#include "Router.h"
#include "configuration.h"
#include "main.h"
#include <OLEDDisplay.h>
#include <OLEDDisplayUi.h>
// Sensors
#include "Sensor/BME280Sensor.h"
#include "Sensor/BME680Sensor.h"
#include "Sensor/DHTSensor.h"
#include "Sensor/DallasSensor.h"
#include "Sensor/MCP9808Sensor.h"
BME280Sensor bme280Sensor;
BME680Sensor bme680Sensor;
DHTSensor dhtSensor;
DallasSensor dallasSensor;
MCP9808Sensor mcp9808Sensor;
#define FAILED_STATE_SENSOR_READ_MULTIPLIER 10
#define DISPLAY_RECEIVEID_MEASUREMENTS_ON_SCREEN true
#ifdef HAS_EINK
// The screen is bigger so use bigger fonts
#define FONT_SMALL ArialMT_Plain_16
#define FONT_MEDIUM ArialMT_Plain_24
#define FONT_LARGE ArialMT_Plain_24
#else
#define FONT_SMALL ArialMT_Plain_10
#define FONT_MEDIUM ArialMT_Plain_16
#define FONT_LARGE ArialMT_Plain_24
#endif
#define fontHeight(font) ((font)[1] + 1) // height is position 1
#define FONT_HEIGHT_SMALL fontHeight(FONT_SMALL)
#define FONT_HEIGHT_MEDIUM fontHeight(FONT_MEDIUM)
int32_t TelemetryPlugin::runOnce()
{
#ifndef PORTDUINO
/*
Uncomment the preferences below if you want to use the plugin
without having to configure it from the PythonAPI or WebUI.
*/
/*
radioConfig.preferences.telemetry_module_measurement_enabled = 1;
radioConfig.preferences.telemetry_module_screen_enabled = 1;
radioConfig.preferences.telemetry_module_read_error_count_threshold = 5;
radioConfig.preferences.telemetry_module_update_interval = 600;
radioConfig.preferences.telemetry_module_recovery_interval = 60;
radioConfig.preferences.telemetry_module_display_farenheit = false;
radioConfig.preferences.telemetry_module_sensor_pin = 13;
radioConfig.preferences.telemetry_module_sensor_type =
RadioConfig_UserPreferences_TelemetrySensorType::
RadioConfig_UserPreferences_TelemetrySensorType_BME280;
*/
if (!(radioConfig.preferences.telemetry_module_measurement_enabled ||
radioConfig.preferences.telemetry_module_screen_enabled)) {
// If this plugin is not enabled, and the user doesn't want the display screen don't waste any OSThread time on it
return (INT32_MAX);
}
if (firstTime) {
// This is the first time the OSThread library has called this function, so do some setup
firstTime = 0;
if (radioConfig.preferences.telemetry_module_measurement_enabled) {
DEBUG_MSG("Telemetry: Initializing\n");
// it's possible to have this plugin enabled, only for displaying values on the screen.
// therefore, we should only enable the sensor loop if measurement is also enabled
switch (radioConfig.preferences.telemetry_module_sensor_type) {
case RadioConfig_UserPreferences_TelemetrySensorType_DHT11:
case RadioConfig_UserPreferences_TelemetrySensorType_DHT12:
case RadioConfig_UserPreferences_TelemetrySensorType_DHT21:
case RadioConfig_UserPreferences_TelemetrySensorType_DHT22:
return dhtSensor.runOnce();
case RadioConfig_UserPreferences_TelemetrySensorType_DS18B20:
return dallasSensor.runOnce();
case RadioConfig_UserPreferences_TelemetrySensorType_BME280:
return bme280Sensor.runOnce();
case RadioConfig_UserPreferences_TelemetrySensorType_BME680:
return bme680Sensor.runOnce();
case RadioConfig_UserPreferences_TelemetrySensorType_MCP9808:
return mcp9808Sensor.runOnce();
default:
DEBUG_MSG("Telemetry: Invalid sensor type selected; Disabling plugin");
return (INT32_MAX);
break;
}
}
return (INT32_MAX);
} else {
// if we somehow got to a second run of this plugin with measurement disabled, then just wait forever
if (!radioConfig.preferences.telemetry_module_measurement_enabled)
return (INT32_MAX);
// this is not the first time OSThread library has called this function
// so just do what we intend to do on the interval
if (sensor_read_error_count > radioConfig.preferences.telemetry_module_read_error_count_threshold) {
if (radioConfig.preferences.telemetry_module_recovery_interval > 0) {
DEBUG_MSG("Telemetry: TEMPORARILY DISABLED; The "
"telemetry_module_read_error_count_threshold has been exceed: %d. Will retry reads in "
"%d seconds\n",
radioConfig.preferences.telemetry_module_read_error_count_threshold,
radioConfig.preferences.telemetry_module_recovery_interval);
sensor_read_error_count = 0;
return (radioConfig.preferences.telemetry_module_recovery_interval * 1000);
}
DEBUG_MSG("Telemetry: DISABLED; The telemetry_module_read_error_count_threshold has "
"been exceed: %d. Reads will not be retried until after device reset\n",
radioConfig.preferences.telemetry_module_read_error_count_threshold);
return (INT32_MAX);
} else if (sensor_read_error_count > 0) {
DEBUG_MSG("Telemetry: There have been %d sensor read failures. Will retry %d more times\n",
sensor_read_error_count, sensor_read_error_count, sensor_read_error_count,
radioConfig.preferences.telemetry_module_read_error_count_threshold -
sensor_read_error_count);
}
if (!sendOurTelemetry()) {
// if we failed to read the sensor, then try again
// as soon as we can according to the maximum polling frequency
switch (radioConfig.preferences.telemetry_module_sensor_type) {
case RadioConfig_UserPreferences_TelemetrySensorType_DHT11:
case RadioConfig_UserPreferences_TelemetrySensorType_DHT12:
case RadioConfig_UserPreferences_TelemetrySensorType_DHT21:
case RadioConfig_UserPreferences_TelemetrySensorType_DHT22:
return (DHT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS);
case RadioConfig_UserPreferences_TelemetrySensorType_DS18B20:
return (DS18B20_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS);
case RadioConfig_UserPreferences_TelemetrySensorType_BME280:
case RadioConfig_UserPreferences_TelemetrySensorType_BME680:
return (BME_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS);
case RadioConfig_UserPreferences_TelemetrySensorType_MCP9808:
return (MCP_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS);
default:
return (DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS);
}
}
}
// The return of runOnce is an int32 representing the desired number of
// miliseconds until the function should be called again by the
// OSThread library. Multiply the preference value by 1000 to convert seconds to miliseconds
return (radioConfig.preferences.telemetry_module_update_interval * 1000);
#endif
}
bool TelemetryPlugin::wantUIFrame()
{
return radioConfig.preferences.telemetry_module_screen_enabled;
}
String GetSenderName(const MeshPacket &mp)
{
String sender;
auto node = nodeDB.getNode(getFrom(&mp));
if (node) {
sender = node->user.short_name;
} else {
sender = "UNK";
}
return sender;
}
uint32_t GetTimeSinceMeshPacket(const MeshPacket *mp)
{
uint32_t now = getTime();
uint32_t last_seen = mp->rx_time;
int delta = (int)(now - last_seen);
if (delta < 0) // our clock must be slightly off still - not set from GPS yet
delta = 0;
return delta;
}
float TelemetryPlugin::CelsiusToFarenheit(float c)
{
return (c * 9) / 5 + 32;
}
void TelemetryPlugin::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_MEDIUM);
display->drawString(x, y, "Environment");
if (lastMeasurementPacket == nullptr) {
display->setFont(FONT_SMALL);
display->drawString(x, y += fontHeight(FONT_MEDIUM), "No measurement");
return;
}
Telemetry lastMeasurement;
uint32_t agoSecs = GetTimeSinceMeshPacket(lastMeasurementPacket);
String lastSender = GetSenderName(*lastMeasurementPacket);
auto &p = lastMeasurementPacket->decoded;
if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, Telemetry_fields, &lastMeasurement)) {
display->setFont(FONT_SMALL);
display->drawString(x, y += fontHeight(FONT_MEDIUM), "Measurement Error");
DEBUG_MSG("Telemetry: unable to decode last packet");
return;
}
display->setFont(FONT_SMALL);
String last_temp = String(lastMeasurement.temperature, 0) + "°C";
if (radioConfig.preferences.telemetry_module_display_farenheit) {
last_temp = String(CelsiusToFarenheit(lastMeasurement.temperature), 0) + "°F";
}
display->drawString(x, y += fontHeight(FONT_MEDIUM) - 2, "From: " + lastSender + "(" + String(agoSecs) + "s)");
display->drawString(x, y += fontHeight(FONT_SMALL) - 2,"Temp/Hum: " + last_temp + " / " + String(lastMeasurement.relative_humidity, 0) + "%");
if (lastMeasurement.barometric_pressure != 0)
display->drawString(x, y += fontHeight(FONT_SMALL),"Press: " + String(lastMeasurement.barometric_pressure, 0) + "hPA");
}
bool TelemetryPlugin::handleReceivedProtobuf(const MeshPacket &mp, Telemetry *p)
{
if (!(radioConfig.preferences.telemetry_module_measurement_enabled ||
radioConfig.preferences.telemetry_module_screen_enabled)) {
// If this plugin is not enabled in any capacity, don't handle the packet, and allow other plugins to consume
return false;
}
String sender = GetSenderName(mp);
DEBUG_MSG("Telemetry: Received data from %s\n", sender);
DEBUG_MSG("Telemetry->relative_humidity: %f\n", p->relative_humidity);
DEBUG_MSG("Telemetry->temperature: %f\n", p->temperature);
DEBUG_MSG("Telemetry->barometric_pressure: %f\n", p->barometric_pressure);
DEBUG_MSG("Telemetry->gas_resistance: %f\n", p->gas_resistance);
lastMeasurementPacket = packetPool.allocCopy(mp);
return false; // Let others look at this message also if they want
}
bool TelemetryPlugin::sendOurTelemetry(NodeNum dest, bool wantReplies)
{
Telemetry m;
m.barometric_pressure = 0;
m.gas_resistance = 0;
DEBUG_MSG("-----------------------------------------\n");
DEBUG_MSG("Telemetry: Read data\n");
switch (radioConfig.preferences.telemetry_module_sensor_type) {
case RadioConfig_UserPreferences_TelemetrySensorType_DS18B20:
if (!dallasSensor.getMeasurement(&m))
sensor_read_error_count++;
break;
case RadioConfig_UserPreferences_TelemetrySensorType_DHT11:
case RadioConfig_UserPreferences_TelemetrySensorType_DHT12:
case RadioConfig_UserPreferences_TelemetrySensorType_DHT21:
case RadioConfig_UserPreferences_TelemetrySensorType_DHT22:
if (!dhtSensor.getMeasurement(&m))
sensor_read_error_count++;
break;
case RadioConfig_UserPreferences_TelemetrySensorType_BME280:
bme280Sensor.getMeasurement(&m);
break;
case RadioConfig_UserPreferences_TelemetrySensorType_BME680:
bme680Sensor.getMeasurement(&m);
break;
case RadioConfig_UserPreferences_TelemetrySensorType_MCP9808:
mcp9808Sensor.getMeasurement(&m);
break;
default:
DEBUG_MSG("Telemetry: Invalid sensor type selected; Disabling plugin");
return false;
}
DEBUG_MSG("Telemetry->relative_humidity: %f\n", m.relative_humidity);
DEBUG_MSG("Telemetry->temperature: %f\n", m.temperature);
DEBUG_MSG("Telemetry->barometric_pressure: %f\n", m.barometric_pressure);
DEBUG_MSG("Telemetry->gas_resistance: %f\n", m.gas_resistance);
sensor_read_error_count = 0;
MeshPacket *p = allocDataProtobuf(m);
p->to = dest;
p->decoded.want_response = wantReplies;
lastMeasurementPacket = packetPool.allocCopy(*p);
DEBUG_MSG("Telemetry: Sending packet to mesh");
service.sendToMesh(p);
return true;
}

View File

@@ -0,0 +1,35 @@
#pragma once
#include "../mesh/generated/telemetry.pb.h"
#include "ProtobufPlugin.h"
#include <OLEDDisplay.h>
#include <OLEDDisplayUi.h>
class TelemetryPlugin : private concurrency::OSThread, public ProtobufPlugin<Telemetry>
{
public:
TelemetryPlugin()
: concurrency::OSThread("TelemetryPlugin"),
ProtobufPlugin("Telemetry", PortNum_TELEMETRY_APP, &Telemetry_msg)
{
lastMeasurementPacket = nullptr;
}
virtual bool wantUIFrame() override;
virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) 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 MeshPacket &mp, Telemetry *p) override;
virtual int32_t runOnce() override;
/**
* Send our Telemetry into the mesh
*/
bool sendOurTelemetry(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false);
private:
float CelsiusToFarenheit(float c);
bool firstTime = 1;
const MeshPacket *lastMeasurementPacket;
uint32_t sensor_read_error_count = 0;
};

View File

@@ -0,0 +1,22 @@
#include "configuration.h"
#include "TextMessagePlugin.h"
#include "NodeDB.h"
#include "PowerFSM.h"
TextMessagePlugin *textMessagePlugin;
ProcessMessage TextMessagePlugin::handleReceived(const MeshPacket &mp)
{
auto &p = mp.decoded;
DEBUG_MSG("Received text msg from=0x%0x, id=0x%x, msg=%.*s\n", mp.from, mp.id, p.payload.size, p.payload.bytes);
// We only store/display messages destined for us.
// Keep a copy of the most recent text message.
devicestate.rx_text_message = mp;
devicestate.has_rx_text_message = true;
powerFSM.trigger(EVENT_RECEIVED_TEXT_MSG);
notifyObservers(&mp);
return ProcessMessage::CONTINUE; // Let others look at this message also if they want
}

View File

@@ -0,0 +1,25 @@
#pragma once
#include "SinglePortPlugin.h"
#include "Observer.h"
/**
* Text message handling for meshtastic - draws on the OLED display the most recent received message
*/
class TextMessagePlugin : public SinglePortPlugin, public Observable<const MeshPacket *>
{
public:
/** Constructor
* name is for debugging output
*/
TextMessagePlugin() : SinglePortPlugin("text", PortNum_TEXT_MESSAGE_APP) {}
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 MeshPacket &mp) override;
};
extern TextMessagePlugin *textMessagePlugin;

View File

@@ -0,0 +1,295 @@
#include "RangeTestPlugin.h"
#include "MeshService.h"
#include "NodeDB.h"
#include "PowerFSM.h"
#include "RTC.h"
#include "Router.h"
#include "airtime.h"
#include "configuration.h"
#include "gps/GeoCoord.h"
#include <Arduino.h>
#include <FSCommon.h>
//#include <assert.h>
/*
As a sender, I can send packets every n-seonds. These packets include an incramented PacketID.
As a receiver, I can receive packets from multiple senders. These packets can be saved to the Filesystem.
*/
RangeTestPlugin *rangeTestPlugin;
RangeTestPluginRadio *rangeTestPluginRadio;
RangeTestPlugin::RangeTestPlugin() : concurrency::OSThread("RangeTestPlugin") {}
uint32_t packetSequence = 0;
#define SEC_PER_DAY 86400
#define SEC_PER_HOUR 3600
#define SEC_PER_MIN 60
int32_t RangeTestPlugin::runOnce()
{
#ifndef NO_ESP32
/*
Uncomment the preferences below if you want to use the plugin
without having to configure it from the PythonAPI or WebUI.
*/
// radioConfig.preferences.range_test_plugin_enabled = 1;
// radioConfig.preferences.range_test_plugin_sender = 45;
// radioConfig.preferences.range_test_plugin_save = 1;
// Fixed position is useful when testing indoors.
// radioConfig.preferences.fixed_position = 1;
uint32_t senderHeartbeat = radioConfig.preferences.range_test_plugin_sender * 1000;
if (radioConfig.preferences.range_test_plugin_enabled) {
if (firstTime) {
rangeTestPluginRadio = new RangeTestPluginRadio();
firstTime = 0;
if (radioConfig.preferences.range_test_plugin_sender) {
DEBUG_MSG("Initializing Range Test Plugin -- Sender\n");
return (5000); // Sending first message 5 seconds after initilization.
} else {
DEBUG_MSG("Initializing Range Test Plugin -- Receiver\n");
return (500);
}
} else {
if (radioConfig.preferences.range_test_plugin_sender) {
// If sender
DEBUG_MSG("Range Test Plugin - Sending heartbeat every %d ms\n", (senderHeartbeat));
DEBUG_MSG("gpsStatus->getLatitude() %d\n", gpsStatus->getLatitude());
DEBUG_MSG("gpsStatus->getLongitude() %d\n", gpsStatus->getLongitude());
DEBUG_MSG("gpsStatus->getHasLock() %d\n", gpsStatus->getHasLock());
DEBUG_MSG("gpsStatus->getDOP() %d\n", gpsStatus->getDOP());
DEBUG_MSG("gpsStatus->getHasLock() %d\n", gpsStatus->getHasLock());
DEBUG_MSG("pref.fixed_position() %d\n", radioConfig.preferences.fixed_position);
// Only send packets if the channel is less than 25% utilized.
if (airTime->channelUtilizationPercent() < 25) {
rangeTestPluginRadio->sendPayload();
} else {
DEBUG_MSG("rangeTest - Channel utilization is >25 percent. Skipping this opportunity to send.\n");
}
return (senderHeartbeat);
} else {
// Otherwise, we're a receiver.
return (500);
}
// TBD
}
} else {
DEBUG_MSG("Range Test Plugin - Disabled\n");
}
#endif
return (INT32_MAX);
}
MeshPacket *RangeTestPluginRadio::allocReply()
{
auto reply = allocDataPacket(); // Allocate a packet for sending
return reply;
}
void RangeTestPluginRadio::sendPayload(NodeNum dest, bool wantReplies)
{
MeshPacket *p = allocReply();
p->to = dest;
p->decoded.want_response = wantReplies;
p->want_ack = true;
packetSequence++;
static char heartbeatString[20];
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 RangeTestPluginRadio::handleReceived(const MeshPacket &mp)
{
#ifndef NO_ESP32
if (radioConfig.preferences.range_test_plugin_enabled) {
/*
auto &p = mp.decoded;
DEBUG_MSG("Received text msg self=0x%0x, from=0x%0x, to=0x%0x, id=%d, msg=%.*s\n",
nodeDB.getNodeNum(), mp.from, mp.to, mp.id, p.payload.size, p.payload.bytes);
*/
if (getFrom(&mp) != nodeDB.getNodeNum()) {
if (radioConfig.preferences.range_test_plugin_save) {
appendFile(mp);
}
/*
NodeInfo *n = nodeDB.getNode(getFrom(&mp));
DEBUG_MSG("-----------------------------------------\n");
DEBUG_MSG("p.payload.bytes \"%s\"\n", p.payload.bytes);
DEBUG_MSG("p.payload.size %d\n", p.payload.size);
DEBUG_MSG("---- Received Packet:\n");
DEBUG_MSG("mp.from %d\n", mp.from);
DEBUG_MSG("mp.rx_snr %f\n", mp.rx_snr);
DEBUG_MSG("mp.hop_limit %d\n", mp.hop_limit);
DEBUG_MSG("mp.decoded.position.latitude_i %d\n", mp.decoded.position.latitude_i); // Depricated
DEBUG_MSG("mp.decoded.position.longitude_i %d\n", mp.decoded.position.longitude_i); // Depricated
DEBUG_MSG("---- Node Information of Received Packet (mp.from):\n");
DEBUG_MSG("n->user.long_name %s\n", n->user.long_name);
DEBUG_MSG("n->user.short_name %s\n", n->user.short_name);
DEBUG_MSG("n->user.macaddr %X\n", n->user.macaddr);
DEBUG_MSG("n->has_position %d\n", n->has_position);
DEBUG_MSG("n->position.latitude_i %d\n", n->position.latitude_i);
DEBUG_MSG("n->position.longitude_i %d\n", n->position.longitude_i);
DEBUG_MSG("n->position.battery_level %d\n", n->position.battery_level);
DEBUG_MSG("---- Current device location information:\n");
DEBUG_MSG("gpsStatus->getLatitude() %d\n", gpsStatus->getLatitude());
DEBUG_MSG("gpsStatus->getLongitude() %d\n", gpsStatus->getLongitude());
DEBUG_MSG("gpsStatus->getHasLock() %d\n", gpsStatus->getHasLock());
DEBUG_MSG("gpsStatus->getDOP() %d\n", gpsStatus->getDOP());
DEBUG_MSG("-----------------------------------------\n");
*/
}
} else {
DEBUG_MSG("Range Test Plugin Disabled\n");
}
#endif
return ProcessMessage::CONTINUE; // Let others look at this message also if they want
}
bool RangeTestPluginRadio::appendFile(const MeshPacket &mp)
{
auto &p = mp.decoded;
NodeInfo *n = nodeDB.getNode(getFrom(&mp));
/*
DEBUG_MSG("-----------------------------------------\n");
DEBUG_MSG("p.payload.bytes \"%s\"\n", p.payload.bytes);
DEBUG_MSG("p.payload.size %d\n", p.payload.size);
DEBUG_MSG("---- Received Packet:\n");
DEBUG_MSG("mp.from %d\n", mp.from);
DEBUG_MSG("mp.rx_snr %f\n", mp.rx_snr);
DEBUG_MSG("mp.hop_limit %d\n", mp.hop_limit);
// DEBUG_MSG("mp.decoded.position.latitude_i %d\n", mp.decoded.position.latitude_i); // Depricated
// DEBUG_MSG("mp.decoded.position.longitude_i %d\n", mp.decoded.position.longitude_i); // Depricated
DEBUG_MSG("---- Node Information of Received Packet (mp.from):\n");
DEBUG_MSG("n->user.long_name %s\n", n->user.long_name);
DEBUG_MSG("n->user.short_name %s\n", n->user.short_name);
DEBUG_MSG("n->user.macaddr %X\n", n->user.macaddr);
DEBUG_MSG("n->has_position %d\n", n->has_position);
DEBUG_MSG("n->position.latitude_i %d\n", n->position.latitude_i);
DEBUG_MSG("n->position.longitude_i %d\n", n->position.longitude_i);
DEBUG_MSG("n->position.battery_level %d\n", n->position.battery_level);
DEBUG_MSG("---- Current device location information:\n");
DEBUG_MSG("gpsStatus->getLatitude() %d\n", gpsStatus->getLatitude());
DEBUG_MSG("gpsStatus->getLongitude() %d\n", gpsStatus->getLongitude());
DEBUG_MSG("gpsStatus->getHasLock() %d\n", gpsStatus->getHasLock());
DEBUG_MSG("gpsStatus->getDOP() %d\n", gpsStatus->getDOP());
DEBUG_MSG("-----------------------------------------\n");
*/
if (!FSBegin()) {
DEBUG_MSG("An Error has occurred while mounting the filesystem\n");
return 0;
}
if (FSCom.totalBytes() - FSCom.usedBytes() < 51200) {
DEBUG_MSG("Filesystem doesn't have enough free space. Aborting write.\n");
return 0;
}
// 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) {
DEBUG_MSG("There was an error opening the file for writing\n");
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")) {
DEBUG_MSG("File was written\n");
} else {
DEBUG_MSG("File write failed\n");
}
fileToWrite.close();
}
//--------- Append content to file
File fileToAppend = FSCom.open("/static/rangetest.csv", FILE_APPEND);
if (!fileToAppend) {
DEBUG_MSG("There was an error opening the file for appending\n");
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
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
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.close();
return 1;
}

View File

@@ -0,0 +1,58 @@
#pragma once
#include "SinglePortPlugin.h"
#include "concurrency/OSThread.h"
#include "configuration.h"
#include <Arduino.h>
#include <functional>
class RangeTestPlugin : private concurrency::OSThread
{
bool firstTime = 1;
public:
RangeTestPlugin();
protected:
virtual int32_t runOnce() override;
};
extern RangeTestPlugin *rangeTestPlugin;
/*
* Radio interface for RangeTestPlugin
*
*/
class RangeTestPluginRadio : public SinglePortPlugin
{
uint32_t lastRxID = 0;
public:
RangeTestPluginRadio() : SinglePortPlugin("RangeTestPluginRadio", PortNum_TEXT_MESSAGE_APP) {}
/**
* 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 MeshPacket &mp);
/**
* Kevin's magical calculation of two points to meters.
*/
float latLongToMeter(double lat_a, double lng_a, double lat_b, double lng_b);
protected:
virtual 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 MeshPacket &mp) override;
};
extern RangeTestPluginRadio *rangeTestPluginRadio;

View File

@@ -0,0 +1,211 @@
#include "configuration.h"
#include "SerialPlugin.h"
#include "MeshService.h"
#include "NodeDB.h"
#include "RTC.h"
#include "Router.h"
#include <Arduino.h>
#include <assert.h>
/*
SerialPlugin
A simple interface to send messages over the mesh network by sending strings
over a serial port.
Default is to use RX GPIO 16 and TX GPIO 17.
Need help with this plugin? Post your question on the Meshtastic Discourse:
https://meshtastic.discourse.group
Basic Usage:
1) Enable the plugin by setting serialplugin_enabled to 1.
2) Set the pins (serialplugin_rxd / serialplugin_rxd) for your preferred RX and TX GPIO pins.
On tbeam, recommend to use:
RXD 35
TXD 15
3) Set serialplugin_timeout to the amount of time to wait before we consider
your packet as "done".
4) (Optional) In SerialPlugin.h set the port to PortNum_TEXT_MESSAGE_APP if you want to
send messages to/from the general text message channel.
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 serialplugin_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 infomration.
- This won't happen any time soon.
KNOWN PROBLEMS
* Until the plugin is initilized 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 NRF and the Linux device targets.
*/
#define RXD2 16
#define TXD2 17
#define SERIALPLUGIN_RX_BUFFER 128
#define SERIALPLUGIN_STRING_MAX Constants_DATA_PAYLOAD_LEN
#define SERIALPLUGIN_TIMEOUT 250
#define SERIALPLUGIN_BAUD 38400
#define SERIALPLUGIN_ACK 1
SerialPlugin *serialPlugin;
SerialPluginRadio *serialPluginRadio;
SerialPlugin::SerialPlugin() : concurrency::OSThread("SerialPlugin") {}
char serialStringChar[Constants_DATA_PAYLOAD_LEN];
SerialPluginRadio::SerialPluginRadio() : SinglePortPlugin("SerialPluginRadio", PortNum_SERIAL_APP)
{
// restrict to the admin channel for rx
boundChannel = Channels::serialChannel;
}
int32_t SerialPlugin::runOnce()
{
#ifndef NO_ESP32
/*
Uncomment the preferences below if you want to use the plugin
without having to configure it from the PythonAPI or WebUI.
*/
// radioConfig.preferences.serialplugin_enabled = 1;
// radioConfig.preferences.serialplugin_rxd = 35;
// radioConfig.preferences.serialplugin_txd = 15;
// radioConfig.preferences.serialplugin_timeout = 1000;
// radioConfig.preferences.serialplugin_echo = 1;
if (radioConfig.preferences.serialplugin_enabled) {
if (firstTime) {
// Interface with the serial peripheral from in here.
DEBUG_MSG("Initializing serial peripheral interface\n");
if (radioConfig.preferences.serialplugin_rxd && radioConfig.preferences.serialplugin_txd) {
Serial2.begin(SERIALPLUGIN_BAUD, SERIAL_8N1, radioConfig.preferences.serialplugin_rxd,
radioConfig.preferences.serialplugin_txd);
} else {
Serial2.begin(SERIALPLUGIN_BAUD, SERIAL_8N1, RXD2, TXD2);
}
if (radioConfig.preferences.serialplugin_timeout) {
Serial2.setTimeout(
radioConfig.preferences.serialplugin_timeout); // Number of MS to wait to set the timeout for the string.
} else {
Serial2.setTimeout(SERIALPLUGIN_TIMEOUT); // Number of MS to wait to set the timeout for the string.
}
Serial2.setRxBufferSize(SERIALPLUGIN_RX_BUFFER);
serialPluginRadio = new SerialPluginRadio();
firstTime = 0;
} else {
String serialString;
while (Serial2.available()) {
serialString = Serial2.readString();
serialString.toCharArray(serialStringChar, Constants_DATA_PAYLOAD_LEN);
serialPluginRadio->sendPayload();
DEBUG_MSG("Received: %s\n", serialStringChar);
}
}
return (10);
} else {
DEBUG_MSG("Serial Plugin Disabled\n");
return (INT32_MAX);
}
#else
return INT32_MAX;
#endif
}
MeshPacket *SerialPluginRadio::allocReply()
{
auto reply = allocDataPacket(); // Allocate a packet for sending
return reply;
}
void SerialPluginRadio::sendPayload(NodeNum dest, bool wantReplies)
{
MeshPacket *p = allocReply();
p->to = dest;
p->decoded.want_response = wantReplies;
p->want_ack = SERIALPLUGIN_ACK;
p->decoded.payload.size = strlen(serialStringChar); // You must specify how many bytes are in the reply
memcpy(p->decoded.payload.bytes, serialStringChar, p->decoded.payload.size);
service.sendToMesh(p);
}
ProcessMessage SerialPluginRadio::handleReceived(const MeshPacket &mp)
{
#ifndef NO_ESP32
if (radioConfig.preferences.serialplugin_enabled) {
auto &p = mp.decoded;
// DEBUG_MSG("Received text msg self=0x%0x, from=0x%0x, to=0x%0x, id=%d, msg=%.*s\n",
// nodeDB.getNodeNum(), mp.from, mp.to, mp.id, p.payload.size, p.payload.bytes);
if (getFrom(&mp) == nodeDB.getNodeNum()) {
/*
* If radioConfig.preferences.serialplugin_echo is true, then echo the packets that are sent out back to the TX
* of the serial interface.
*/
if (radioConfig.preferences.serialplugin_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;
// DEBUG_MSG("* * Message came this device\n");
// Serial2.println("* * Message came this device");
Serial2.printf("%s", p.payload.bytes);
}
}
} else {
if (radioConfig.preferences.serialplugin_mode == 0 || radioConfig.preferences.serialplugin_mode == 1) {
// DEBUG_MSG("* * Message came from the mesh\n");
// Serial2.println("* * Message came from the mesh");
Serial2.printf("%s", p.payload.bytes);
} else if (radioConfig.preferences.serialplugin_mode == 10) {
/*
@jobionekabnoi
Add code here to handle what gets sent out to the serial interface.
Format it the way you want.
*/
}
}
} else {
DEBUG_MSG("Serial Plugin Disabled\n");
}
#endif
return ProcessMessage::CONTINUE; // Let others look at this message also if they want
}

View File

@@ -0,0 +1,53 @@
#pragma once
#include "SinglePortPlugin.h"
#include "concurrency/OSThread.h"
#include "configuration.h"
#include <Arduino.h>
#include <functional>
class SerialPlugin : private concurrency::OSThread
{
bool firstTime = 1;
public:
SerialPlugin();
protected:
virtual int32_t runOnce() override;
};
extern SerialPlugin *serialPlugin;
/*
* Radio interface for SerialPlugin
*
*/
class SerialPluginRadio : public SinglePortPlugin
{
uint32_t lastRxID = 0;
public:
/*
TODO: Switch this to PortNum_SERIAL_APP once the change is able to be merged back here
from the main code.
*/
SerialPluginRadio();
/**
* Send our payload into the mesh
*/
void sendPayload(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false);
protected:
virtual 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 MeshPacket &mp) override;
};
extern SerialPluginRadio *serialPluginRadio;

View File

@@ -0,0 +1,444 @@
#include "StoreForwardPlugin.h"
#include "MeshService.h"
#include "NodeDB.h"
#include "RTC.h"
#include "Router.h"
#include "airtime.h"
#include "configuration.h"
#include "mesh-pb-constants.h"
#include "mesh/generated/storeforward.pb.h"
#include "modules/PluginDev.h"
#include <Arduino.h>
#include <iterator>
#include <map>
StoreForwardPlugin *storeForwardPlugin;
int32_t StoreForwardPlugin::runOnce()
{
#ifndef NO_ESP32
if (radioConfig.preferences.store_forward_plugin_enabled) {
if (radioConfig.preferences.is_router) {
// Send out the message queue.
if (this->busy) {
// Only send packets if the channel is less than 25% utilized.
if (airTime->channelUtilizationPercent() < 25) {
// DEBUG_MSG("--- --- --- In busy loop 1 %d\n", this->packetHistoryTXQueue_index);
storeForwardPlugin->sendPayload(this->busyTo, this->packetHistoryTXQueue_index);
if (this->packetHistoryTXQueue_index == packetHistoryTXQueue_size) {
strcpy(this->routerMessage, "** S&F - Done");
storeForwardPlugin->sendMessage(this->busyTo, this->routerMessage);
// DEBUG_MSG("--- --- --- In busy loop - Done \n");
this->packetHistoryTXQueue_index = 0;
this->busy = false;
} else {
this->packetHistoryTXQueue_index++;
}
} else {
DEBUG_MSG("Channel utilization is too high. Skipping this opportunity to send and will retry later.\n");
}
}
DEBUG_MSG("SF myNodeInfo.bitrate = %f bytes / sec\n", myNodeInfo.bitrate);
return (this->packetTimeMax);
} else {
DEBUG_MSG("Store & Forward Plugin - Disabled (is_router = false)\n");
return (INT32_MAX);
}
} else {
DEBUG_MSG("Store & Forward Plugin - Disabled\n");
return (INT32_MAX);
}
#endif
return (INT32_MAX);
}
/*
Create our data structure in the PSRAM.
*/
void StoreForwardPlugin::populatePSRAM()
{
/*
For PSRAM usage, see:
https://learn.upesy.com/en/programmation/psram.html#psram-tab
*/
DEBUG_MSG("Before PSRAM initilization:\n");
DEBUG_MSG(" Total heap: %d\n", ESP.getHeapSize());
DEBUG_MSG(" Free heap: %d\n", ESP.getFreeHeap());
DEBUG_MSG(" Total PSRAM: %d\n", ESP.getPsramSize());
DEBUG_MSG(" Free PSRAM: %d\n", ESP.getFreePsram());
this->packetHistoryTXQueue =
static_cast<PacketHistoryStruct *>(ps_calloc(this->historyReturnMax, sizeof(PacketHistoryStruct)));
/* Use a maximum of 2/3 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 : (((ESP.getFreePsram() / 3) * 2) / sizeof(PacketHistoryStruct)));
this->packetHistory = static_cast<PacketHistoryStruct *>(ps_calloc(numberOfPackets, sizeof(PacketHistoryStruct)));
DEBUG_MSG("After PSRAM initilization:\n");
DEBUG_MSG(" Total heap: %d\n", ESP.getHeapSize());
DEBUG_MSG(" Free heap: %d\n", ESP.getFreeHeap());
DEBUG_MSG(" Total PSRAM: %d\n", ESP.getPsramSize());
DEBUG_MSG(" Free PSRAM: %d\n", ESP.getFreePsram());
DEBUG_MSG("Store and Forward Stats:\n");
DEBUG_MSG(" numberOfPackets for packetHistory - %u\n", numberOfPackets);
}
void StoreForwardPlugin::historyReport()
{
DEBUG_MSG("Iterating through the message history...\n");
DEBUG_MSG("Message history contains %u records\n", this->packetHistoryCurrent);
}
/*
*
*/
void StoreForwardPlugin::historySend(uint32_t msAgo, uint32_t to)
{
// uint32_t packetsSent = 0;
uint32_t queueSize = storeForwardPlugin->historyQueueCreate(msAgo, to);
if (queueSize) {
snprintf(this->routerMessage, 80, "** S&F - Sending %u message(s)", queueSize);
storeForwardPlugin->sendMessage(to, this->routerMessage);
this->busy = true; // runOnce() will pickup the next steps once busy = true.
this->busyTo = to;
} else {
strcpy(this->routerMessage, "** S&F - No history to send");
storeForwardPlugin->sendMessage(to, this->routerMessage);
}
}
uint32_t StoreForwardPlugin::historyQueueCreate(uint32_t msAgo, uint32_t to)
{
// uint32_t packetHistoryTXQueueIndex = 0;
this->packetHistoryTXQueue_size = 0;
for (int i = 0; i < this->packetHistoryCurrent; i++) {
/*
DEBUG_MSG("SF historyQueueCreate\n");
DEBUG_MSG("SF historyQueueCreate - time %d\n", this->packetHistory[i].time);
DEBUG_MSG("SF historyQueueCreate - millis %d\n", millis());
DEBUG_MSG("SF historyQueueCreate - math %d\n", (millis() - msAgo));
*/
if (this->packetHistory[i].time && (this->packetHistory[i].time < (millis() - msAgo))) {
DEBUG_MSG("SF historyQueueCreate - Time matches - ok\n");
/*
Copy the messages that were received by the router in the last msAgo
to the packetHistoryTXQueue structure.
TODO: The condition (this->packetHistory[i].to & NODENUM_BROADCAST) == to) is not tested since
I don't have an easy way to target a specific user. Will need to do this soon.
*/
if ((this->packetHistory[i].to & NODENUM_BROADCAST) == NODENUM_BROADCAST ||
((this->packetHistory[i].to & NODENUM_BROADCAST) == to)) {
this->packetHistoryTXQueue[this->packetHistoryTXQueue_size].time = this->packetHistory[i].time;
this->packetHistoryTXQueue[this->packetHistoryTXQueue_size].time = this->packetHistory[i].time;
this->packetHistoryTXQueue[this->packetHistoryTXQueue_size].to = this->packetHistory[i].to;
this->packetHistoryTXQueue[this->packetHistoryTXQueue_size].from = this->packetHistory[i].from;
this->packetHistoryTXQueue[this->packetHistoryTXQueue_size].channel = this->packetHistory[i].channel;
this->packetHistoryTXQueue[this->packetHistoryTXQueue_size].payload_size = this->packetHistory[i].payload_size;
memcpy(this->packetHistoryTXQueue[this->packetHistoryTXQueue_size].payload, this->packetHistory[i].payload,
Constants_DATA_PAYLOAD_LEN);
this->packetHistoryTXQueue_size++;
DEBUG_MSG("PacketHistoryStruct time=%d\n", this->packetHistory[i].time);
DEBUG_MSG("PacketHistoryStruct msg=%.*s\n", this->packetHistory[i].payload);
// DEBUG_MSG("PacketHistoryStruct msg=%.*s\n", this->packetHistoryTXQueue[packetHistoryTXQueueIndex].payload);
}
}
}
return this->packetHistoryTXQueue_size;
}
void StoreForwardPlugin::historyAdd(const MeshPacket &mp)
{
const auto &p = mp.decoded;
this->packetHistory[this->packetHistoryCurrent].time = millis();
this->packetHistory[this->packetHistoryCurrent].to = mp.to;
this->packetHistory[this->packetHistoryCurrent].channel = mp.channel;
this->packetHistory[this->packetHistoryCurrent].from = mp.from;
this->packetHistory[this->packetHistoryCurrent].payload_size = p.payload.size;
memcpy(this->packetHistory[this->packetHistoryCurrent].payload, p.payload.bytes, Constants_DATA_PAYLOAD_LEN);
this->packetHistoryCurrent++;
}
MeshPacket *StoreForwardPlugin::allocReply()
{
auto reply = allocDataPacket(); // Allocate a packet for sending
return reply;
}
void StoreForwardPlugin::sendPayload(NodeNum dest, uint32_t packetHistory_index)
{
DEBUG_MSG("Sending S&F Payload\n");
MeshPacket *p = allocReply();
p->to = dest;
p->from = this->packetHistoryTXQueue[packetHistory_index].from;
p->channel = this->packetHistoryTXQueue[packetHistory_index].channel;
// Let's assume that if the router received the S&F request that the client is in range.
// TODO: Make this configurable.
p->want_ack = false;
p->decoded.payload.size =
this->packetHistoryTXQueue[packetHistory_index].payload_size; // You must specify how many bytes are in the reply
memcpy(p->decoded.payload.bytes, this->packetHistoryTXQueue[packetHistory_index].payload,
this->packetHistoryTXQueue[packetHistory_index].payload_size);
service.sendToMesh(p);
}
void StoreForwardPlugin::sendMessage(NodeNum dest, char *str)
{
MeshPacket *p = allocReply();
p->to = dest;
// FIXME - Determine if the delayed packet is broadcast or delayed. For now, assume
// everything is broadcast.
p->delayed = MeshPacket_Delayed_DELAYED_BROADCAST;
// Let's assume that if the router received the S&F request that the client is in range.
// TODO: Make this configurable.
p->want_ack = false;
p->decoded.payload.size = strlen(str); // You must specify how many bytes are in the reply
memcpy(p->decoded.payload.bytes, str, strlen(str));
service.sendToMesh(p);
// HardwareMessage_init_default
}
ProcessMessage StoreForwardPlugin::handleReceived(const MeshPacket &mp)
{
#ifndef NO_ESP32
if (radioConfig.preferences.store_forward_plugin_enabled) {
DEBUG_MSG("--- S&F Received something\n");
// The router node should not be sending messages as a client.
if (getFrom(&mp) != nodeDB.getNodeNum()) {
if (mp.decoded.portnum == PortNum_TEXT_MESSAGE_APP) {
DEBUG_MSG("Packet came from - PortNum_TEXT_MESSAGE_APP\n");
auto &p = mp.decoded;
if ((p.payload.bytes[0] == 'S') && (p.payload.bytes[1] == 'F') && (p.payload.bytes[2] == 0x00)) {
DEBUG_MSG("--- --- --- Request to send\n");
// Send the last 60 minutes of messages.
if (this->busy) {
strcpy(this->routerMessage, "** S&F - Busy. Try again shortly.");
storeForwardPlugin->sendMessage(getFrom(&mp), this->routerMessage);
} else {
storeForwardPlugin->historySend(1000 * 60, getFrom(&mp));
}
} else if ((p.payload.bytes[0] == 'S') && (p.payload.bytes[1] == 'F') && (p.payload.bytes[2] == 'm') &&
(p.payload.bytes[3] == 0x00)) {
strlcpy(this->routerMessage, "01234567890123456789012345678901234567890123456789012345678901234567890123456789"
"01234567890123456789012345678901234567890123456789012345678901234567890123456789"
"01234567890123456789012345678901234567890123456789012345678901234567890123456",
sizeof(this->routerMessage));
storeForwardPlugin->sendMessage(getFrom(&mp), this->routerMessage);
} else {
storeForwardPlugin->historyAdd(mp);
}
} else if (mp.decoded.portnum == PortNum_STORE_FORWARD_APP) {
} else {
DEBUG_MSG("Packet came from an unknown port %u\n", mp.decoded.portnum);
}
}
} else {
DEBUG_MSG("Store & Forward Plugin - Disabled\n");
}
#endif
return ProcessMessage::CONTINUE; // Let others look at this message also if they want
}
ProcessMessage StoreForwardPlugin::handleReceivedProtobuf(const MeshPacket &mp, StoreAndForward *p)
{
if (!radioConfig.preferences.store_forward_plugin_enabled) {
// If this plugin is not enabled in any capacity, don't handle the packet, and allow other plugins to consume
return ProcessMessage::CONTINUE;
}
if (mp.decoded.portnum == PortNum_TEXT_MESSAGE_APP) {
DEBUG_MSG("Packet came from an PortNum_TEXT_MESSAGE_APP port %u\n", mp.decoded.portnum);
return ProcessMessage::CONTINUE;
} else if (mp.decoded.portnum == PortNum_STORE_FORWARD_APP) {
DEBUG_MSG("Packet came from an PortNum_STORE_FORWARD_APP port %u\n", mp.decoded.portnum);
} else {
DEBUG_MSG("Packet came from an UNKNOWN port %u\n", mp.decoded.portnum);
return ProcessMessage::CONTINUE;
}
switch (p->rr) {
case StoreAndForward_RequestResponse_CLIENT_ERROR:
// Do nothing
DEBUG_MSG("StoreAndForward_RequestResponse_CLIENT_ERROR\n");
break;
case StoreAndForward_RequestResponse_CLIENT_HISTORY:
DEBUG_MSG("StoreAndForward_RequestResponse_CLIENT_HISTORY\n");
// Send the last 60 minutes of messages.
if (this->busy) {
strcpy(this->routerMessage, "** S&F - Busy. Try again shortly.");
storeForwardPlugin->sendMessage(getFrom(&mp), this->routerMessage);
} else {
storeForwardPlugin->historySend(1000 * 60, getFrom(&mp));
}
break;
case StoreAndForward_RequestResponse_CLIENT_PING:
// Do nothing
DEBUG_MSG("StoreAndForward_RequestResponse_CLIENT_PING\n");
break;
case StoreAndForward_RequestResponse_CLIENT_PONG:
// Do nothing
DEBUG_MSG("StoreAndForward_RequestResponse_CLIENT_PONG\n");
break;
case StoreAndForward_RequestResponse_CLIENT_STATS:
// Do nothing
DEBUG_MSG("StoreAndForward_RequestResponse_CLIENT_STATS\n");
break;
case StoreAndForward_RequestResponse_ROUTER_BUSY:
// Do nothing
DEBUG_MSG("StoreAndForward_RequestResponse_ROUTER_BUSY\n");
break;
case StoreAndForward_RequestResponse_ROUTER_ERROR:
// Do nothing
DEBUG_MSG("StoreAndForward_RequestResponse_ROUTER_ERROR\n");
break;
case StoreAndForward_RequestResponse_ROUTER_HEARTBEAT:
// Do nothing
DEBUG_MSG("StoreAndForward_RequestResponse_ROUTER_HEARTBEAT\n");
break;
case StoreAndForward_RequestResponse_ROUTER_PING:
// Do nothing
DEBUG_MSG("StoreAndForward_RequestResponse_ROUTER_PING\n");
break;
case StoreAndForward_RequestResponse_ROUTER_PONG:
// Do nothing
DEBUG_MSG("StoreAndForward_RequestResponse_ROUTER_PONG\n");
break;
default:
assert(0); // unexpected state - FIXME, make an error code and reboot
}
return ProcessMessage::STOP; // There's no need for others to look at this message.
}
StoreForwardPlugin::StoreForwardPlugin()
: SinglePortPlugin("StoreForwardPlugin", PortNum_TEXT_MESSAGE_APP), concurrency::OSThread("StoreForwardPlugin")
{
#ifndef NO_ESP32
isPromiscuous = true; // Brown chicken brown cow
if (StoreForward_Dev) {
/*
Uncomment the preferences below if you want to use the plugin
without having to configure it from the PythonAPI or WebUI.
*/
radioConfig.preferences.store_forward_plugin_enabled = 1;
radioConfig.preferences.is_router = 1;
radioConfig.preferences.is_always_powered = 1;
}
if (radioConfig.preferences.store_forward_plugin_enabled) {
// Router
if (radioConfig.preferences.is_router) {
DEBUG_MSG("Initializing Store & Forward Plugin - Enabled as Router\n");
if (ESP.getPsramSize()) {
if (ESP.getFreePsram() >= 1024 * 1024) {
// Do the startup here
// Maximum number of records to return.
if (radioConfig.preferences.store_forward_plugin_history_return_max)
this->historyReturnMax = radioConfig.preferences.store_forward_plugin_history_return_max;
// Maximum time window for records to return (in minutes)
if (radioConfig.preferences.store_forward_plugin_history_return_window)
this->historyReturnWindow = radioConfig.preferences.store_forward_plugin_history_return_window;
// Maximum number of records to store in memory
if (radioConfig.preferences.store_forward_plugin_records)
this->records = radioConfig.preferences.store_forward_plugin_records;
// Maximum number of records to store in memory
if (radioConfig.preferences.store_forward_plugin_heartbeat)
this->heartbeat = radioConfig.preferences.store_forward_plugin_heartbeat;
// Popupate PSRAM with our data structures.
this->populatePSRAM();
} else {
DEBUG_MSG("Device has less than 1M of PSRAM free. Aborting startup.\n");
DEBUG_MSG("Store & Forward Plugin - Aborting Startup.\n");
}
} else {
DEBUG_MSG("Device doesn't have PSRAM.\n");
DEBUG_MSG("Store & Forward Plugin - Aborting Startup.\n");
}
// Client
} else {
DEBUG_MSG("Initializing Store & Forward Plugin - Enabled as Client\n");
}
}
#endif
}

View File

@@ -0,0 +1,85 @@
#pragma once
#include "SinglePortPlugin.h"
#include "concurrency/OSThread.h"
#include "mesh/generated/storeforward.pb.h"
#include "configuration.h"
#include <Arduino.h>
#include <functional>
struct PacketHistoryStruct {
uint32_t time;
uint32_t to;
uint32_t from;
uint8_t channel;
bool ack;
uint8_t payload[Constants_DATA_PAYLOAD_LEN];
pb_size_t payload_size;
};
class StoreForwardPlugin : public SinglePortPlugin, private concurrency::OSThread
{
// bool firstTime = 1;
bool busy = 0;
uint32_t busyTo = 0;
char routerMessage[Constants_DATA_PAYLOAD_LEN] = {0};
uint32_t receivedRecord[50][2] = {{0}};
PacketHistoryStruct *packetHistory = 0;
uint32_t packetHistoryCurrent = 0;
PacketHistoryStruct *packetHistoryTXQueue = 0;
uint32_t packetHistoryTXQueue_size = 0;
uint32_t packetHistoryTXQueue_index = 0;
uint32_t packetTimeMax = 2000;
public:
StoreForwardPlugin();
/**
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 MeshPacket &mp);
void historyReport();
void historySend(uint32_t msAgo, uint32_t to);
uint32_t historyQueueCreate(uint32_t msAgo, uint32_t to);
/**
* Send our payload into the mesh
*/
void sendPayload(NodeNum dest = NODENUM_BROADCAST, uint32_t packetHistory_index = 0);
void sendMessage(NodeNum dest, char *str);
virtual MeshPacket *allocReply() override;
/*
Override the wantPortnum method.
*/
virtual bool wantPortnum(PortNum p) { return true; };
private:
void populatePSRAM();
// S&F Defaults
uint32_t historyReturnMax = 250; // 250 records
uint32_t historyReturnWindow = 240; // 4 hours
uint32_t records = 0; // Calculated
bool heartbeat = false; // No heartbeat.
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 MeshPacket &mp) override;
virtual ProcessMessage handleReceivedProtobuf(const MeshPacket &mp, StoreAndForward *p);
};
extern StoreForwardPlugin *storeForwardPlugin;