diff --git a/boards/t-echo.json b/boards/t-echo.json index 957ba01e3..c53132fda 100644 --- a/boards/t-echo.json +++ b/boards/t-echo.json @@ -9,6 +9,7 @@ "f_cpu": "64000000L", "hwids": [ ["0x239A", "0x4405"], + ["0x239A", "0x0029"], ["0x239A", "0x002A"] ], "usb_product": "TTGO_eink", diff --git a/protobufs b/protobufs index bdf9d6a81..99bd42baf 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit bdf9d6a81b06b919f4d01455a2eb766e30f1141c +Subproject commit 99bd42baf8dd2e8ca0eec70f05e1cf7f1a40a283 diff --git a/src/ButtonThread.h b/src/ButtonThread.h index 66efd6004..20dc14cc4 100644 --- a/src/ButtonThread.h +++ b/src/ButtonThread.h @@ -54,15 +54,18 @@ class ButtonThread : public concurrency::OSThread if (settingsMap.count(user) != 0 && settingsMap[user] != RADIOLIB_NC) userButton = OneButton(settingsMap[user], true, true); #elif defined(BUTTON_PIN) - - userButton = OneButton(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, true, true); + int pin = config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN; + userButton = OneButton(pin, true, true); #endif + #ifdef INPUT_PULLUP_SENSE // Some platforms (nrf52) have a SENSE variant which allows wake from sleep - override what OneButton did - pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT_PULLUP_SENSE); + pinMode(pin, INPUT_PULLUP_SENSE); #endif userButton.attachClick(userButtonPressed); - userButton.setClickMs(300); + userButton.setClickMs(400); + userButton.setPressMs(1000); + userButton.setDebounceMs(10); userButton.attachDuringLongPress(userButtonPressedLong); userButton.attachDoubleClick(userButtonDoublePressed); userButton.attachMultiClick(userButtonMultiPressed); @@ -72,7 +75,15 @@ class ButtonThread : public concurrency::OSThread if (settingsMap.count(user) != 0 && settingsMap[user] != RADIOLIB_NC) wakeOnIrq(settingsMap[user], FALLING); #else - wakeOnIrq(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, FALLING); + static OneButton *pBtn = &userButton; // only one instance of ButtonThread is created, so static is safe + attachInterrupt( + pin, + []() { + BaseType_t higherWake = 0; + mainDelay.interruptFromISR(&higherWake); + pBtn->tick(); + }, + CHANGE); #endif #endif #ifdef BUTTON_PIN_ALT @@ -194,6 +205,7 @@ class ButtonThread : public concurrency::OSThread { if (!config.device.disable_triple_click && (gps != nullptr)) { gps->toggleGpsMode(); + screen->forceDisplay(); } } diff --git a/src/mesh/MeshModule.cpp b/src/mesh/MeshModule.cpp index a1e719721..9c6ca78ee 100644 --- a/src/mesh/MeshModule.cpp +++ b/src/mesh/MeshModule.cpp @@ -67,7 +67,7 @@ meshtastic_MeshPacket *MeshModule::allocErrorResponse(meshtastic_Routing_Error e return r; } -void MeshModule::callPlugins(const meshtastic_MeshPacket &mp, RxSource src) +void MeshModule::callPlugins(meshtastic_MeshPacket &mp, RxSource src) { // LOG_DEBUG("In call modules\n"); bool moduleFound = false; @@ -124,9 +124,10 @@ void MeshModule::callPlugins(const meshtastic_MeshPacket &mp, RxSource src) } else printPacket("packet on wrong channel, but can't respond", &mp); } else { - ProcessMessage handled = pi.handleReceived(mp); + pi.alterReceived(mp); + // Possibly send replies (but only if the message was directed to us specifically, i.e. not for promiscious // sniffing) also: we only let the one module send a reply, once that happens, remaining modules are not // considered diff --git a/src/mesh/MeshModule.h b/src/mesh/MeshModule.h index 323cc8595..ebe3af1a0 100644 --- a/src/mesh/MeshModule.h +++ b/src/mesh/MeshModule.h @@ -64,7 +64,7 @@ class MeshModule /** For use only by MeshService */ - static void callPlugins(const meshtastic_MeshPacket &mp, RxSource src = RX_SRC_RADIO); + static void callPlugins(meshtastic_MeshPacket &mp, RxSource src = RX_SRC_RADIO); static std::vector GetMeshModulesWithUIFrames(); static void observeUIEvents(Observer *observer); @@ -72,10 +72,7 @@ class MeshModule meshtastic_AdminMessage *request, meshtastic_AdminMessage *response); #if HAS_SCREEN - virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) - { - return; - } + virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { return; } #endif protected: const char *name; @@ -135,10 +132,12 @@ class MeshModule @return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be considered for it */ - virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) - { - return ProcessMessage::CONTINUE; - } + virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) { return ProcessMessage::CONTINUE; } + + /** Called to change a particular incoming message + This allows the module to change the message before it is passed through the rest of the call-chain. + */ + virtual void alterReceived(meshtastic_MeshPacket &mp) {} /** 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. @@ -151,14 +150,8 @@ class MeshModule /*** * @return true if you want to be alloced a UI screen frame */ - virtual bool wantUIFrame() - { - return false; - } - virtual Observable *getUIFrameObservable() - { - return NULL; - } + virtual bool wantUIFrame() { return false; } + virtual Observable *getUIFrameObservable() { return NULL; } meshtastic_MeshPacket *allocAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex); diff --git a/src/mesh/ProtobufModule.h b/src/mesh/ProtobufModule.h index 1771f4322..d87bb47c3 100644 --- a/src/mesh/ProtobufModule.h +++ b/src/mesh/ProtobufModule.h @@ -30,6 +30,10 @@ template class ProtobufModule : protected SinglePortModule */ virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, T *decoded) = 0; + /** Called to make changes to a particular incoming message + */ + virtual void alterReceivedProtobuf(meshtastic_MeshPacket &mp, T *decoded){}; + /** * Return a mesh packet which has been preinited with a particular protobuf data payload and port number. * You can then send this packet (after customizing any of the payload fields you might need) with @@ -86,4 +90,26 @@ template class ProtobufModule : protected SinglePortModule return handleReceivedProtobuf(mp, decoded) ? ProcessMessage::STOP : ProcessMessage::CONTINUE; } + + /** Called to alter a particular incoming message + */ + virtual void alterReceived(meshtastic_MeshPacket &mp) override + { + auto &p = mp.decoded; + + T scratch; + T *decoded = NULL; + if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.decoded.portnum == ourPortNum) { + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, fields, &scratch)) { + decoded = &scratch; + } else { + LOG_ERROR("Error decoding protobuf module!\n"); + // if we can't decode it, nobody can process it! + return; + } + } + + return alterReceivedProtobuf(mp, decoded); + } }; \ No newline at end of file diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index fe39f9b55..cea3968ce 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -123,6 +123,13 @@ const RegionInfo regions[] = { */ RDEF(MY_919, 919.0f, 924.0f, 100, 0, 27, true, true, false), + /* + Singapore + SG_923 Band 30d: 917 - 925 MHz at 100mW, no restrictions. + https://www.imda.gov.sg/-/media/imda/files/regulation-licensing-and-consultations/ict-standards/telecommunication-standards/radio-comms/imdatssrd.pdf + */ + RDEF(SG_923, 917.0f, 925.0f, 100, 0, 20, true, false, false), + /* 2.4 GHZ WLAN Band equivalent. Only for SX128x chips. */ diff --git a/src/mesh/ReliableRouter.cpp b/src/mesh/ReliableRouter.cpp index 946a669cc..a1e9f281d 100644 --- a/src/mesh/ReliableRouter.cpp +++ b/src/mesh/ReliableRouter.cpp @@ -168,10 +168,14 @@ bool ReliableRouter::stopRetransmission(GlobalPacketId key) auto p = old->packet; auto numErased = pending.erase(key); assert(numErased == 1); - // remove the 'original' (identified by originator and packet->id) from the txqueue and free it - cancelSending(getFrom(p), p->id); - // now free the pooled copy for retransmission too - packetPool.release(p); + /* Only when we already transmitted a packet via LoRa, we will cancel the packet in the Tx queue + to avoid canceling a transmission if it was ACKed super fast via MQTT */ + if (old->numRetransmissions < NUM_RETRANSMISSIONS - 1) { + // remove the 'original' (identified by originator and packet->id) from the txqueue and free it + cancelSending(getFrom(p), p->id); + // now free the pooled copy for retransmission too + packetPool.release(p); + } return true; } else return false; diff --git a/src/mesh/generated/meshtastic/config.pb.h b/src/mesh/generated/meshtastic/config.pb.h index b06e9a707..4047f7367 100644 --- a/src/mesh/generated/meshtastic/config.pb.h +++ b/src/mesh/generated/meshtastic/config.pb.h @@ -214,7 +214,9 @@ typedef enum _meshtastic_Config_LoRaConfig_RegionCode { /* Malaysia 433mhz */ meshtastic_Config_LoRaConfig_RegionCode_MY_433 = 16, /* Malaysia 919mhz */ - meshtastic_Config_LoRaConfig_RegionCode_MY_919 = 17 + meshtastic_Config_LoRaConfig_RegionCode_MY_919 = 17, + /* Singapore 923mhz */ + meshtastic_Config_LoRaConfig_RegionCode_SG_923 = 18 } meshtastic_Config_LoRaConfig_RegionCode; /* Standard predefined channel settings @@ -543,8 +545,8 @@ extern "C" { #define _meshtastic_Config_DisplayConfig_DisplayMode_ARRAYSIZE ((meshtastic_Config_DisplayConfig_DisplayMode)(meshtastic_Config_DisplayConfig_DisplayMode_COLOR+1)) #define _meshtastic_Config_LoRaConfig_RegionCode_MIN meshtastic_Config_LoRaConfig_RegionCode_UNSET -#define _meshtastic_Config_LoRaConfig_RegionCode_MAX meshtastic_Config_LoRaConfig_RegionCode_MY_919 -#define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_MY_919+1)) +#define _meshtastic_Config_LoRaConfig_RegionCode_MAX meshtastic_Config_LoRaConfig_RegionCode_SG_923 +#define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_SG_923+1)) #define _meshtastic_Config_LoRaConfig_ModemPreset_MIN meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST #define _meshtastic_Config_LoRaConfig_ModemPreset_MAX meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE diff --git a/src/modules/AtakPluginModule.cpp b/src/modules/AtakPluginModule.cpp new file mode 100644 index 000000000..86e143811 --- /dev/null +++ b/src/modules/AtakPluginModule.cpp @@ -0,0 +1,128 @@ +#include "AtakPluginModule.h" +#include "MeshService.h" +#include "NodeDB.h" +#include "PowerFSM.h" +#include "configuration.h" +#include "main.h" +#include "meshtastic/atak.pb.h" + +extern "C" { +#include "mesh/compression/unishox2.h" +} + +AtakPluginModule *atakPluginModule; + +AtakPluginModule::AtakPluginModule() + : ProtobufModule("atak", meshtastic_PortNum_ATAK_PLUGIN, &meshtastic_TAKPacket_msg), concurrency::OSThread("AtakPluginModule") +{ + ourPortNum = meshtastic_PortNum_ATAK_PLUGIN; +} + +/* +Encompasses the full construction and sending packet to mesh +Will be used for broadcast. +*/ +int32_t AtakPluginModule::runOnce() +{ + return default_broadcast_interval_secs; +} + +bool AtakPluginModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_TAKPacket *r) +{ + return false; +} + +meshtastic_TAKPacket AtakPluginModule::cloneTAKPacketData(meshtastic_TAKPacket *t) +{ + meshtastic_TAKPacket clone = meshtastic_TAKPacket_init_zero; + if (t->has_group) { + clone.has_group = true; + clone.group = t->group; + } + if (t->has_status) { + clone.has_status = true; + clone.status = t->status; + } + if (t->has_contact) { + clone.has_contact = true; + clone.contact = {0}; + } + + if (t->which_payload_variant == meshtastic_TAKPacket_pli_tag) { + clone.which_payload_variant = meshtastic_TAKPacket_pli_tag; + clone.payload_variant.pli = t->payload_variant.pli; + } else if (t->which_payload_variant == meshtastic_TAKPacket_chat_tag) { + clone.which_payload_variant = meshtastic_TAKPacket_chat_tag; + clone.payload_variant.chat = {0}; + } + + return clone; +} + +void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_TAKPacket *t) +{ + // From Phone (EUD) + if (mp.from == 0) { + LOG_DEBUG("Received uncompressed TAK payload from phone with %d bytes\n", mp.decoded.payload.size); + // Compress for LoRA transport + auto compressed = cloneTAKPacketData(t); + compressed.is_compressed = true; + if (t->has_contact) { + auto length = unishox2_compress_simple(t->contact.callsign, strlen(t->contact.callsign), compressed.contact.callsign); + LOG_DEBUG("Uncompressed callsign '%s' - %d bytes\n", t->contact.callsign, strlen(t->contact.callsign)); + LOG_DEBUG("Compressed callsign '%s' - %d bytes\n", t->contact.callsign, length); + + length = unishox2_compress_simple(t->contact.device_callsign, strlen(t->contact.device_callsign), + compressed.contact.device_callsign); + LOG_DEBUG("Uncompressed device_callsign '%s' - %d bytes\n", t->contact.device_callsign, + strlen(t->contact.device_callsign)); + LOG_DEBUG("Compressed device_callsign '%s' - %d bytes\n", compressed.contact.device_callsign, length); + } + if (t->which_payload_variant == meshtastic_TAKPacket_chat_tag) { + auto length = unishox2_compress_simple(t->payload_variant.chat.message, strlen(t->payload_variant.chat.message), + compressed.payload_variant.chat.message); + LOG_DEBUG("Uncompressed chat message '%s' - %d bytes\n", t->payload_variant.chat.message, + strlen(t->payload_variant.chat.message)); + LOG_DEBUG("Compressed chat message '%s' - %d bytes\n", compressed.payload_variant.chat.message, length); + } + mp.decoded.payload.size = pb_encode_to_bytes(mp.decoded.payload.bytes, sizeof(mp.decoded.payload.bytes), + meshtastic_TAKPacket_fields, &compressed); + LOG_DEBUG("Final payload size of %d bytes\n", mp.decoded.payload.size); + } else { + if (!t->is_compressed) { + // Not compressed. Something is wrong + LOG_ERROR("Received uncompressed TAKPacket over radio!\n"); + return; + } + + // Decompress for Phone (EUD) + auto decompressedCopy = packetPool.allocCopy(mp); + auto uncompressed = cloneTAKPacketData(t); + uncompressed.is_compressed = false; + if (t->has_contact) { + auto length = + unishox2_decompress_simple(t->contact.callsign, strlen(t->contact.callsign), uncompressed.contact.callsign); + + LOG_DEBUG("Compressed callsign: %d bytes\n", strlen(t->contact.callsign)); + LOG_DEBUG("Decompressed callsign: '%s' @ %d bytes\n", uncompressed.contact.callsign, length); + + length = unishox2_decompress_simple(t->contact.device_callsign, strlen(t->contact.device_callsign), + uncompressed.contact.device_callsign); + + LOG_DEBUG("Compressed device_callsign: %d bytes\n", strlen(t->contact.device_callsign)); + LOG_DEBUG("Decompressed device_callsign: '%s' @ %d bytes\n", uncompressed.contact.device_callsign, length); + } + if (uncompressed.which_payload_variant == meshtastic_TAKPacket_chat_tag) { + auto length = unishox2_decompress_simple(t->payload_variant.chat.message, strlen(t->payload_variant.chat.message), + uncompressed.payload_variant.chat.message); + LOG_DEBUG("Compressed chat message: %d bytes\n", strlen(t->payload_variant.chat.message)); + LOG_DEBUG("Decompressed chat message: '%s' @ %d bytes\n", uncompressed.payload_variant.chat.message, length); + } + decompressedCopy->decoded.payload.size = + pb_encode_to_bytes(decompressedCopy->decoded.payload.bytes, sizeof(decompressedCopy->decoded.payload), + meshtastic_TAKPacket_fields, &uncompressed); + + service.sendToPhone(decompressedCopy); + } + return; +} \ No newline at end of file diff --git a/src/modules/AtakPluginModule.h b/src/modules/AtakPluginModule.h new file mode 100644 index 000000000..0470a3b32 --- /dev/null +++ b/src/modules/AtakPluginModule.h @@ -0,0 +1,26 @@ +#pragma once +#include "ProtobufModule.h" +#include "meshtastic/atak.pb.h" + +/** + * Waypoint message handling for meshtastic + */ +class AtakPluginModule : public ProtobufModule, private concurrency::OSThread +{ + public: + /** Constructor + * name is for debugging output + */ + AtakPluginModule(); + + protected: + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_TAKPacket *t) override; + virtual void alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_TAKPacket *t) override; + /* Does our periodic broadcast */ + int32_t runOnce() override; + + private: + meshtastic_TAKPacket cloneTAKPacketData(meshtastic_TAKPacket *t); +}; + +extern AtakPluginModule *atakPluginModule; \ No newline at end of file diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index 37c7576f6..fd109b765 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -6,6 +6,7 @@ #include "input/cardKbI2cImpl.h" #include "input/kbMatrixImpl.h" #include "modules/AdminModule.h" +#include "modules/AtakPluginModule.h" #include "modules/CannedMessageModule.h" #include "modules/DetectionSensorModule.h" #include "modules/NeighborInfoModule.h" @@ -61,7 +62,7 @@ void setupModules() traceRouteModule = new TraceRouteModule(); neighborInfoModule = new NeighborInfoModule(); detectionSensorModule = new DetectionSensorModule(); - + atakPluginModule = new AtakPluginModule(); // Note: if the rest of meshtastic doesn't need to explicitly use your module, you do not need to assign the instance // to a global variable. diff --git a/variants/t-echo/platformio.ini b/variants/t-echo/platformio.ini index 2555032df..843bd88ff 100644 --- a/variants/t-echo/platformio.ini +++ b/variants/t-echo/platformio.ini @@ -6,6 +6,7 @@ debug_tool = jlink # add -DCFG_SYSVIEW if you want to use the Segger systemview tool for OS profiling. build_flags = ${nrf52840_base.build_flags} -Ivariants/t-echo + -DGPS_POWER_TOGGLE -L "${platformio.libdeps_dir}/${this.__env__}/BSEC2 Software Library/src/cortex-m4/fpv4-sp-d16-hard" build_src_filter = ${nrf52_base.build_src_filter} +<../variants/t-echo> lib_deps =