Repository navigation
Add reply bot module with DM-only responses and rate limiting #9456
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
306d324
Implement Meshtastic reply bot module with ping and status features
mattatat25 1c706a0
Merge branch 'develop' into replybot-buddy-apply
mattatat25 e9ba33c
Update ReplyBotModule.cpp
mattatat25 3381370
Update src/modules/ReplyBotModule.h
mattatat25 61846eb
Update src/modules/ReplyBotModule.cpp
mattatat25 932b551
Tidying up
mattatat25 7114702
Merge branch 'develop' into replybot-buddy-apply
mattatat25 50aa7af
Merge branch 'replybot-buddy-apply' of https://github.com/mattatat25/…
mattatat25 89d3178
Merge branch 'develop' into replybot-buddy-apply
thebentern 39759f9
Merge branch 'develop' into replybot-buddy-apply
mattatat25 de89f56
Merge branch 'develop' into replybot-buddy-apply
mattatat25 cd412f9
Merge branch 'develop' into replybot-buddy-apply
mattatat25 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,183 @@ | ||
| #if !MESHTASTIC_EXCLUDE_REPLYBOT | ||
| /* | ||
| * ReplyBotModule.cpp | ||
| * | ||
| * This module implements a simple reply bot for the Meshtastic firmware. It listens for | ||
| * specific text commands ("/ping", "/hello" and "/test") delivered either via a direct | ||
| * message (DM) or a broadcast on the primary channel. When a supported command is | ||
| * received the bot responds with a short status message that includes the hop count | ||
| * (minimum number of relays), RSSI and SNR of the received packet. To avoid spamming | ||
| * the network it enforces a per‑sender cooldown between responses. By default the | ||
| * module is enabled; define MESHTASTIC_EXCLUDE_REPLYBOT at build time to exclude it | ||
|
thebentern marked this conversation as resolved.
Resolved
|
||
| * entirely. See the official firmware documentation for guidance on adding modules. | ||
| */ | ||
|
mattatat25 marked this conversation as resolved.
Resolved
|
||
|
|
||
| #include "ReplyBotModule.h" | ||
| #include "Channels.h" | ||
| #include "MeshService.h" | ||
| #include "NodeDB.h" | ||
| #include "configuration.h" | ||
| #include "mesh/MeshTypes.h" | ||
|
|
||
| #include <Arduino.h> | ||
| #include <cctype> | ||
| #include <cstring> | ||
|
|
||
| // | ||
| // Rate limiting data structures | ||
| // | ||
| // Each sender is tracked in a small ring buffer. When a message arrives from a | ||
| // sender we check the last time we responded to them. If the difference is | ||
| // less than the configured cooldown (different values for DM vs broadcast) | ||
| // the message is ignored; otherwise we update the last response time and | ||
| // proceed with replying. | ||
|
|
||
| struct ReplyBotCooldownEntry { | ||
| uint32_t from = 0; | ||
| uint32_t lastMs = 0; | ||
| }; | ||
|
|
||
| static constexpr uint8_t REPLYBOT_COOLDOWN_SLOTS = 8; // ring buffer size | ||
| static constexpr uint32_t REPLYBOT_DM_COOLDOWN_MS = 15 * 1000; // 15 seconds for DMs | ||
| static constexpr uint32_t REPLYBOT_LF_COOLDOWN_MS = 60 * 1000; // 60 seconds for LongFast broadcasts | ||
|
|
||
| static ReplyBotCooldownEntry replybotCooldown[REPLYBOT_COOLDOWN_SLOTS]; | ||
| static uint8_t replybotCooldownIdx = 0; | ||
|
|
||
| // Return true if a reply should be rate‑limited for this sender, updating the | ||
| // entry table as needed. | ||
| static bool replybotRateLimited(uint32_t from, uint32_t cooldownMs) | ||
| { | ||
| const uint32_t now = millis(); | ||
| for (auto &e : replybotCooldown) { | ||
| if (e.from == from) { | ||
| // Found existing entry; check if cooldown expired | ||
| if ((uint32_t)(now - e.lastMs) < cooldownMs) { | ||
| return true; | ||
| } | ||
| e.lastMs = now; | ||
| return false; | ||
| } | ||
| } | ||
| // No entry found – insert new sender into the ring | ||
| replybotCooldown[replybotCooldownIdx].from = from; | ||
| replybotCooldown[replybotCooldownIdx].lastMs = now; | ||
| replybotCooldownIdx = (replybotCooldownIdx + 1) % REPLYBOT_COOLDOWN_SLOTS; | ||
| return false; | ||
| } | ||
|
|
||
| // Constructor – registers a single text port and marks the module promiscuous | ||
| // so that broadcast messages on the primary channel are visible. | ||
| ReplyBotModule::ReplyBotModule() : SinglePortModule("replybot", meshtastic_PortNum_TEXT_MESSAGE_APP) | ||
| { | ||
| isPromiscuous = true; | ||
| } | ||
|
mattatat25 marked this conversation as resolved.
Resolved
|
||
|
|
||
| void ReplyBotModule::setup() | ||
| { | ||
| // In future we may add a protobuf configuration; for now the module is | ||
| // always enabled when compiled in. | ||
| } | ||
|
|
||
| // Determine whether we want to process this packet. We only care about | ||
| // plain text messages addressed to our port. | ||
| bool ReplyBotModule::wantPacket(const meshtastic_MeshPacket *p) | ||
| { | ||
| return (p && p->decoded.portnum == ourPortNum); | ||
| } | ||
|
|
||
| ProcessMessage ReplyBotModule::handleReceived(const meshtastic_MeshPacket &mp) | ||
| { | ||
| // Accept only direct messages to us or broadcasts on the Primary channel | ||
| // (regardless of modem preset: LongFast, MediumFast, etc). | ||
|
|
||
| const uint32_t ourNode = nodeDB->getNodeNum(); | ||
| const bool isDM = (mp.to == ourNode); | ||
| const bool isPrimaryChannel = (mp.channel == channels.getPrimaryIndex()) && isBroadcast(mp.to); | ||
| if (!isDM && !isPrimaryChannel) { | ||
| return ProcessMessage::CONTINUE; | ||
| } | ||
|
|
||
| // Ignore empty payloads | ||
| if (mp.decoded.payload.size == 0) { | ||
| return ProcessMessage::CONTINUE; | ||
| } | ||
|
|
||
| // Copy payload into a null‑terminated buffer | ||
| char buf[260]; | ||
| memset(buf, 0, sizeof(buf)); | ||
| size_t n = mp.decoded.payload.size; | ||
| if (n > sizeof(buf) - 1) | ||
| n = sizeof(buf) - 1; | ||
| memcpy(buf, mp.decoded.payload.bytes, n); | ||
|
|
||
| // React only to supported slash commands | ||
| if (!isCommand(buf)) { | ||
| return ProcessMessage::CONTINUE; | ||
| } | ||
|
|
||
| // Apply rate limiting per sender depending on DM/broadcast | ||
| const uint32_t cooldownMs = isDM ? REPLYBOT_DM_COOLDOWN_MS : REPLYBOT_LF_COOLDOWN_MS; | ||
| if (replybotRateLimited(mp.from, cooldownMs)) { | ||
| return ProcessMessage::CONTINUE; | ||
| } | ||
|
|
||
| // Compute hop count indicator – if the relay_node is non‑zero we know | ||
| // there was at least one relay. Some firmware builds support a hop_start | ||
| // field which could be used for more accurate counts, but here we use | ||
| // the available relay_node flag only. | ||
| // int hopsAway = mp.hop_start - mp.hop_limit; | ||
| int hopsAway = getHopsAway(mp); | ||
|
|
||
| // Normalize RSSI: if positive adjust down by 200 to align with typical values | ||
| int rssi = mp.rx_rssi; | ||
| if (rssi > 0) { | ||
| rssi -= 200; | ||
| } | ||
| float snr = mp.rx_snr; | ||
|
|
||
| // Build the reply message and send it back via DM | ||
| char reply[96]; | ||
| snprintf(reply, sizeof(reply), "🎙️ Mic Check : %d Hops away | RSSI %d | SNR %.1f", hopsAway, rssi, snr); | ||
| sendDm(mp, reply); | ||
| return ProcessMessage::CONTINUE; | ||
| } | ||
|
|
||
| // Check if the message starts with one of the supported commands. Leading | ||
| // whitespace is skipped and commands must be followed by end‑of‑string or | ||
| // whitespace. | ||
| bool ReplyBotModule::isCommand(const char *msg) const | ||
| { | ||
| if (!msg) | ||
| return false; | ||
| while (*msg == ' ' || *msg == '\t') | ||
| msg++; | ||
| auto isEndOrSpace = [](char c) { return c == '\0' || std::isspace(static_cast<unsigned char>(c)); }; | ||
| if (strncmp(msg, "/ping", 5) == 0 && isEndOrSpace(msg[5])) | ||
| return true; | ||
| if (strncmp(msg, "/hello", 6) == 0 && isEndOrSpace(msg[6])) | ||
| return true; | ||
| if (strncmp(msg, "/test", 5) == 0 && isEndOrSpace(msg[5])) | ||
| return true; | ||
| return false; | ||
| } | ||
|
|
||
| // Send a direct message back to the originating node. | ||
| void ReplyBotModule::sendDm(const meshtastic_MeshPacket &rx, const char *text) | ||
| { | ||
| if (!text) | ||
| return; | ||
| meshtastic_MeshPacket *p = allocDataPacket(); | ||
| p->to = rx.from; | ||
| p->channel = rx.channel; | ||
| p->want_ack = false; | ||
| p->decoded.want_response = false; | ||
| size_t len = strlen(text); | ||
| if (len > sizeof(p->decoded.payload.bytes)) { | ||
| len = sizeof(p->decoded.payload.bytes); | ||
| } | ||
| p->decoded.payload.size = len; | ||
| memcpy(p->decoded.payload.bytes, text, len); | ||
| service->sendToMesh(p); | ||
| } | ||
| #endif // MESHTASTIC_EXCLUDE_REPLYBOT | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| #pragma once | ||
| #if !MESHTASTIC_EXCLUDE_REPLYBOT | ||
| #include "SinglePortModule.h" | ||
| #include "mesh/generated/meshtastic/mesh.pb.h" | ||
|
|
||
| class ReplyBotModule : public SinglePortModule | ||
| { | ||
| public: | ||
| ReplyBotModule(); | ||
| void setup() override; | ||
| bool wantPacket(const meshtastic_MeshPacket *p) override; | ||
| ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; | ||
|
|
||
| protected: | ||
| bool isCommand(const char *msg) const; | ||
| void sendDm(const meshtastic_MeshPacket &rx, const char *text); | ||
| }; | ||
| #endif // MESHTASTIC_EXCLUDE_REPLYBOT |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.