diff --git a/src/mesh/Channels.h b/src/mesh/Channels.h index 7873a306a..3bedf4476 100644 --- a/src/mesh/Channels.h +++ b/src/mesh/Channels.h @@ -94,6 +94,13 @@ class Channels bool ensureLicensedOperation(); + /** + * Validate a channel, fixing any errors as needed + */ + meshtastic_Channel &fixupChannel(ChannelIndex chIndex); + + int16_t getHash(ChannelIndex i) { return hashes[i]; } + private: /** Given a channel index, change to use the crypto key specified by that index * @@ -111,13 +118,6 @@ class Channels */ int16_t generateHash(ChannelIndex channelNum); - int16_t getHash(ChannelIndex i) { return hashes[i]; } - - /** - * Validate a channel, fixing any errors as needed - */ - meshtastic_Channel &fixupChannel(ChannelIndex chIndex); - /** * Writes the default lora config */ diff --git a/src/mesh/RadioLibInterface.cpp b/src/mesh/RadioLibInterface.cpp index e3ef58f14..17b5f070a 100644 --- a/src/mesh/RadioLibInterface.cpp +++ b/src/mesh/RadioLibInterface.cpp @@ -8,6 +8,9 @@ #include "error.h" #include "main.h" #include "mesh-pb-constants.h" +#if !MESHTASTIC_EXCLUDE_TIPS +#include "modules/MeshTipsModule.h" +#endif #include #include @@ -248,6 +251,9 @@ void RadioLibInterface::onNotify(uint32_t notification) switch (notification) { case ISR_TX: handleTransmitInterrupt(); +#if !MESHTASTIC_EXCLUDE_TIPS + MeshTipsModule::configureRadioForPacket(this, txQueue.getFront()); +#endif startReceive(); setTransmitDelay(); break; @@ -270,9 +276,16 @@ void RadioLibInterface::onNotify(uint32_t notification) if (delay_remaining > 0) { // There's still some delay pending on this packet, so resume waiting for it to elapse notifyLater(delay_remaining, TRANSMIT_DELAY_COMPLETED, false); +#if !MESHTASTIC_EXCLUDE_TIPS + } else if (MeshTipsModule::configureRadioForPacket(this, txp)) { + // We just switched radio config, so wait to ensure the new channel is available + setTransmitDelay(); +#endif } else { if (isChannelActive()) { // check if there is currently a LoRa packet on the channel - startReceive(); // try receiving this packet, afterwards we'll be trying to transmit again + if (!txp->nonstandard_radio_config) { + startReceive(); // try receiving this packet, afterwards we'll be trying to transmit again + } setTransmitDelay(); } else { // Send any outgoing packets we have ready as fast as possible to keep the time between channel scan and diff --git a/src/modules/MeshTipsModule.cpp b/src/modules/MeshTipsModule.cpp new file mode 100644 index 000000000..dfb5dbb6b --- /dev/null +++ b/src/modules/MeshTipsModule.cpp @@ -0,0 +1,242 @@ +#include "MeshTipsModule.h" +#include "Default.h" +#include "MeshService.h" +#include "RTC.h" +#include "RadioInterface.h" +#include "configuration.h" +#include "main.h" +#include + +#define NODENUM_TIPS 0x00000004 + +static meshtastic_Config_LoRaConfig_ModemPreset originalModemPreset; // original modem preset +static uint16_t originalLoraChannel; // original frequency slot +char originalChannelName[sizeof(MeshTipsModule_TXSettings)]; // original channel name + +MeshTipsModule::MeshTipsModule() +{ + originalModemPreset = config.lora.modem_preset; + originalLoraChannel = config.lora.channel_num; + strncpy(originalChannelName, channels.getPrimary().name, sizeof(originalChannelName)); +} + +bool MeshTipsModule::configureRadioForPacket(RadioInterface *iface, meshtastic_MeshPacket *p) +{ + meshtastic_ChannelSettings *c = (meshtastic_ChannelSettings *)&channels.getPrimary(); + if (p && p->from == NODENUM_TIPS && p->nonstandard_radio_config && + (p->modem_preset != config.lora.modem_preset || p->frequency_slot != config.lora.channel_num)) { + LOG_INFO("Reconfiguring for TX of packet %#08lx (from=%#08lx size=%lu)", p->id, p->from, p->decoded.payload.size); + + config.lora.modem_preset = (meshtastic_Config_LoRaConfig_ModemPreset)p->modem_preset; + config.lora.channel_num = p->frequency_slot; + memset(c->name, 0, sizeof(c->name)); + + switch (p->modem_preset) { + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO: + strncpy(c->name, "ShortTurbo", sizeof(c->name)); + break; + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST: + strncpy(c->name, "ShortFast", sizeof(c->name)); + break; + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW: + strncpy(c->name, "ShortSlow", sizeof(c->name)); + break; + case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST: + strncpy(c->name, "MediumFast", sizeof(c->name)); + break; + case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW: + strncpy(c->name, "MediumSlow", sizeof(c->name)); + break; + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST: + strncpy(c->name, "LongFast", sizeof(c->name)); + break; + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE: + strncpy(c->name, "LongMod", sizeof(c->name)); + break; + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW: + strncpy(c->name, "LongSlow", sizeof(c->name)); + break; + } + + channels.fixupChannel(channels.getPrimaryIndex()); + p->channel = channels.getHash(channels.getPrimaryIndex()); + iface->reconfigure(); + + return true; + } else if ((!p || !p->nonstandard_radio_config) && + (config.lora.modem_preset != originalModemPreset || config.lora.channel_num != originalLoraChannel)) { + LOG_INFO("Reconfiguring for TX of packet %#08lx (from=%#08lx size=%lu)", p->id, p->from, p->decoded.payload.size); + + config.lora.modem_preset = originalModemPreset; + config.lora.channel_num = originalLoraChannel; + memset(c->name, 0, sizeof(c->name)); + strncpy(c->name, originalChannelName, sizeof(c->name)); + + channels.fixupChannel(channels.getPrimaryIndex()); + iface->reconfigure(); + return true; + } + return false; +} + +MeshTipsModule_TXSettings MeshTipsModule::stripTargetRadioSettings(meshtastic_MeshPacket *p) +{ + MeshTipsModule_TXSettings s = { + .preset = originalModemPreset, + .slot = originalLoraChannel, + }; + + // clamp final byte of payload, just in case, because I don't know if this is taken care of elsewhere + p->decoded.payload.bytes[p->decoded.payload.size] = 0; + + if (!p || strlen((char *)p->decoded.payload.bytes) < 4 || p->decoded.payload.bytes[0] != '#') + return s; + + char *msg = strchr((char *)p->decoded.payload.bytes, ' '); + if (!*msg) + return s; + *msg++ = 0; + + const char presetString[3] = {p->decoded.payload.bytes[1], p->decoded.payload.bytes[2], 0}; + char *slotString = (char *)&p->decoded.payload.bytes[3]; + if (!*slotString) + return s; + + for (char *c = slotString; *c; c++) { + if (!strchr("1234567890", *c)) + return s; + } + + uint8_t preset = 0; + if (!strncmp(presetString, "ST", 2)) + s.preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO; + else if (!strncmp(presetString, "SF", 2)) + s.preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST; + else if (!strncmp(presetString, "SS", 2)) + s.preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW; + else if (!strncmp(presetString, "MF", 2)) + s.preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST; + else if (!strncmp(presetString, "MS", 2)) + s.preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW; + else if (!strncmp(presetString, "LF", 2)) + s.preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; + else if (!strncmp(presetString, "LM", 2)) + s.preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE; + else if (!strncmp(presetString, "LS", 2)) + s.preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW; + else + return s; + + s.slot = std::stoi(slotString); + + p->decoded.payload.size = 1; + // don't use strcpy, because strcpy has undefined behaviour if src & dst overlap + for (char *a = msg, *b = (char *)p->decoded.payload.bytes; (*b++ = *a++);) + p->decoded.payload.size++; + + return s; +} + +MeshTipsNodeInfoModule *meshTipsNodeInfoModule; + +bool MeshTipsNodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_User *pptr) +{ + // do nothing if we receive nodeinfo, because we only care about sending our own + return true; +} + +void MeshTipsNodeInfoModule::sendTipsNodeInfo() +{ + LOG_INFO("Send NodeInfo for mesh tips"); + static meshtastic_User u = { + .hw_model = meshtastic_HardwareModel_PRIVATE_HW, + .is_licensed = false, + .role = meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE, + .public_key = + { + .size = 32, + .bytes = {0x39, 0x37, 0x58, 0xe4, 0x05, 0x34, 0x7d, 0xe0, 0x49, 0x73, 0xec, 0xaf, 0xbc, 0x8e, 0x07, 0xe8, + 0x66, 0x57, 0xe4, 0xa1, 0x2d, 0x53, 0x0e, 0x26, 0x51, 0x1f, 0x1a, 0x6c, 0xbf, 0xe8, 0x5e, 0x04}, + }, + .has_is_unmessagable = true, + .is_unmessagable = true, + }; + strncpy(u.id, "!mesh_tips", sizeof(u.id)); + strncpy(u.long_name, "WLG Mesh Tips Robot", sizeof(u.long_name)); + strncpy(u.short_name, "TIPS", sizeof(u.short_name)); + meshtastic_MeshPacket *p = allocDataProtobuf(u); + p->to = NODENUM_BROADCAST; + p->from = NODENUM_TIPS; + p->hop_limit = 0; + p->decoded.want_response = false; + p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; + p->modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; + + meshtastic_MeshPacket *p_LF20 = packetPool.allocCopy(*p); + service->sendToMesh(p, RX_SRC_LOCAL, false); + + p_LF20->frequency_slot = 20; + p_LF20->nonstandard_radio_config = true; + service->sendToMesh(p_LF20, RX_SRC_LOCAL, false); +} + +MeshTipsNodeInfoModule::MeshTipsNodeInfoModule() + : ProtobufModule("nodeinfo_tips", meshtastic_PortNum_NODEINFO_APP, &meshtastic_User_msg), + concurrency::OSThread("MeshTipsNodeInfo") +{ + MeshTipsModule(); + + setIntervalFromNow(setStartDelay()); // Send our initial owner announcement 30 seconds + // after we start (to give network time to setup) +} + +int32_t MeshTipsNodeInfoModule::runOnce() +{ + if (airTime->isTxAllowedAirUtil() && config.device.role != meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN) { + sendTipsNodeInfo(); + } + return Default::getConfiguredOrDefaultMs(config.device.node_info_broadcast_secs, default_node_info_broadcast_secs); +} + +MeshTipsMessageModule *meshTipsMessageModule; + +ProcessMessage MeshTipsMessageModule::handleReceived(const meshtastic_MeshPacket &mp) +{ +#ifdef DEBUG_PORT + auto &d = mp.decoded; +#endif + + meshtastic_MeshPacket *p = packetPool.allocCopy(mp); + MeshTipsModule_TXSettings s = stripTargetRadioSettings(p); + if (!p->decoded.payload.size || p->decoded.payload.bytes[0] == '#') + return ProcessMessage::STOP; + p->to = NODENUM_BROADCAST; + p->decoded.source = p->from; + p->from = NODENUM_TIPS; + p->channel = channels.getPrimaryIndex(); + p->hop_limit = 0; + p->hop_start = 0; + p->rx_rssi = 0; + p->rx_snr = 0; + p->priority = meshtastic_MeshPacket_Priority_HIGH; + p->want_ack = false; + p->modem_preset = s.preset; + p->frequency_slot = s.slot; + if (s.preset != originalModemPreset || s.slot != originalLoraChannel) { + p->nonstandard_radio_config = true; + } + p->rx_time = getValidTime(RTCQualityFromNet); + service->sendToMesh(p, RX_SRC_LOCAL, false); + + powerFSM.trigger(EVENT_RECEIVED_MSG); + notifyObservers(&mp); + + return ProcessMessage::CONTINUE; // No other module should be caring about this message +} + +bool MeshTipsMessageModule::wantPacket(const meshtastic_MeshPacket *p) +{ + meshtastic_Channel *c = &channels.getByIndex(p->channel); + return c->role == meshtastic_Channel_Role_SECONDARY && strlen(c->settings.name) == strlen("Tips") && + !strcmp(c->settings.name, "Tips") && p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP; +} \ No newline at end of file diff --git a/src/modules/MeshTipsModule.h b/src/modules/MeshTipsModule.h new file mode 100644 index 000000000..7b3173dee --- /dev/null +++ b/src/modules/MeshTipsModule.h @@ -0,0 +1,80 @@ +#pragma once +#include "Observer.h" +#include "ProtobufModule.h" +#include "RadioInterface.h" +#include "SinglePortModule.h" + +typedef struct { + meshtastic_Config_LoRaConfig_ModemPreset preset; + uint16_t slot; +} MeshTipsModule_TXSettings; + +/** + * Base class for the tips robot + */ +class MeshTipsModule +{ + public: + /** + * Constructor + */ + MeshTipsModule(); + + /** + * Configure the radio to send the target packet, or return to default config if p is NULL + */ + static bool configureRadioForPacket(RadioInterface *iface, meshtastic_MeshPacket *p); + + /** + * Get target modem settings + */ + MeshTipsModule_TXSettings stripTargetRadioSettings(meshtastic_MeshPacket *p); +}; + +/** + * Tips module for sending tips into the mesh + */ +class MeshTipsNodeInfoModule : private MeshTipsModule, public ProtobufModule, private concurrency::OSThread +{ + public: + /** + * Constructor + */ + MeshTipsNodeInfoModule(); + + protected: + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_User *p) override; + + /** + * Send NodeInfo to the mesh + */ + void sendTipsNodeInfo(); + + /** Does our periodic broadcast */ + virtual int32_t runOnce() override; +}; +extern MeshTipsNodeInfoModule *meshTipsNodeInfoModule; + +/** + * Text message handling for the tips robot + */ +class MeshTipsMessageModule : private MeshTipsModule, public SinglePortModule, public Observable +{ + public: + /** + * Constructor + */ + MeshTipsMessageModule() : MeshTipsModule(), SinglePortModule("tips", meshtastic_PortNum_TEXT_MESSAGE_APP) {} + + protected: + /** + * Called to handle a particular incoming message + */ + virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; + + /** + * Indicate whether this module wants to process the packet + */ + virtual bool wantPacket(const meshtastic_MeshPacket *p) override; +}; +extern MeshTipsMessageModule *meshTipsMessageModule; \ No newline at end of file diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index 783c08b9f..60327047e 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -34,6 +34,9 @@ #if !MESHTASTIC_EXCLUDE_NODEINFO #include "modules/NodeInfoModule.h" #endif +#if !MESHTASTIC_EXCLUDE_TIPS +#include "modules/MeshTipsModule.h" +#endif #if !MESHTASTIC_EXCLUDE_GPS #include "modules/PositionModule.h" #endif @@ -121,6 +124,10 @@ void setupModules() #if !MESHTASTIC_EXCLUDE_NODEINFO nodeInfoModule = new NodeInfoModule(); #endif +#if !MESHTASTIC_EXCLUDE_TIPS + meshTipsNodeInfoModule = new MeshTipsNodeInfoModule(); + meshTipsMessageModule = new MeshTipsMessageModule(); +#endif #if !MESHTASTIC_EXCLUDE_GPS positionModule = new PositionModule(); #endif