diff --git a/src/main.cpp b/src/main.cpp index c5e6e8b56..3430cb129 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -36,7 +36,7 @@ #include "nimble/NimbleBluetooth.h" #endif -#if HAS_WIFI +#if HAS_WIFI || defined(ARCH_PORTDUINO) #include "mesh/wifi/WiFiServerAPI.h" #include "mqtt/MQTT.h" #endif @@ -45,6 +45,9 @@ #include "RF95Interface.h" #include "SX1262Interface.h" #include "SX1268Interface.h" +#if !HAS_RADIO && defined(ARCH_PORTDUINO) +#include "platform/portduino/SimRadio.h" +#endif #if HAS_BUTTON #include "ButtonThread.h" @@ -385,7 +388,7 @@ void setup() } #endif -#if !HAS_RADIO +#ifdef ARCH_PORTDUINO if (!rIf) { rIf = new SimRadio; if (!rIf->init()) { @@ -411,7 +414,7 @@ void setup() #endif #ifdef ARCH_PORTDUINO - initApiServer(); + initApiServer(TCPPort); #endif // Start airtime logger thread. diff --git a/src/main.h b/src/main.h index e1142d36c..c92379274 100644 --- a/src/main.h +++ b/src/main.h @@ -20,6 +20,8 @@ extern bool isUSBPowered; extern uint8_t nodeTelemetrySensorsMap[TelemetrySensorType_LPS22+1]; +extern int TCPPort; // set by Portduino + // Global Screen singleton. extern graphics::Screen *screen; // extern Observable newPowerStatus; //TODO: move this to main-esp32.cpp somehow or a helper class diff --git a/src/mesh/MeshService.cpp b/src/mesh/MeshService.cpp index 825252402..99411f91d 100644 --- a/src/mesh/MeshService.cpp +++ b/src/mesh/MeshService.cpp @@ -123,6 +123,29 @@ void MeshService::reloadOwner() */ void MeshService::handleToRadio(MeshPacket &p) { + #ifdef ARCH_PORTDUINO + // Simulates device is receiving a packet via the LoRa chip + if (p.decoded.portnum == PortNum_SIMULATOR_APP) { + // Simulator packet (=Compressed packet) is encapsulated in a MeshPacket, so need to unwrap first + Compressed scratch; + Compressed *decoded = NULL; + if (p.which_payload_variant == MeshPacket_decoded_tag) { + memset(&scratch, 0, sizeof(scratch)); + p.decoded.payload.size = pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &Compressed_msg, &scratch); + if (p.decoded.payload.size) { + decoded = &scratch; + // Extract the original payload and replace + memcpy(&p.decoded.payload, &decoded->data, sizeof(decoded->data)); + // Switch the port from PortNum_SIMULATOR_APP back to the original PortNum + p.decoded.portnum = decoded->portnum; + } else + DEBUG_MSG("Error decoding protobuf for simulator message!\n"); + } + // Let SimRadio receive as if it did via its LoRa chip + SimRadio::instance->startReceive(&p); + return; + } + #endif if (p.from != 0) { // We don't let phones assign nodenums to their sent messages DEBUG_MSG("Warning: phone tried to pick a nodenum, we don't allow that.\n"); p.from = 0; diff --git a/src/mesh/MeshService.h b/src/mesh/MeshService.h index 45559d3a2..d886a57a9 100644 --- a/src/mesh/MeshService.h +++ b/src/mesh/MeshService.h @@ -10,6 +10,9 @@ #include "MeshTypes.h" #include "Observer.h" #include "PointerQueue.h" +#ifdef ARCH_PORTDUINO +#include "../platform/portduino/SimRadio.h" +#endif /** * Top level app for this service. keeps the mesh, the radio config and the queue of received packets. diff --git a/src/mesh/PhoneAPI.cpp b/src/mesh/PhoneAPI.cpp index b3f500f01..a725e6283 100644 --- a/src/mesh/PhoneAPI.cpp +++ b/src/mesh/PhoneAPI.cpp @@ -117,7 +117,7 @@ bool PhoneAPI::handleToRadio(const uint8_t *buf, size_t bufLength) size_t PhoneAPI::getFromRadio(uint8_t *buf) { if (!available()) { - DEBUG_MSG("getFromRadio=not available\n"); + // DEBUG_MSG("getFromRadio=not available\n"); return 0; } // In case we send a FromRadio packet @@ -319,7 +319,7 @@ bool PhoneAPI::available() if (!packetForPhone) packetForPhone = service.getForPhone(); bool hasPacket = !!packetForPhone; - DEBUG_MSG("available hasPacket=%d\n", hasPacket); + // DEBUG_MSG("available hasPacket=%d\n", hasPacket); return hasPacket; } default: diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index 864a9726c..59c8b49d6 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -461,12 +461,6 @@ void RadioInterface::limitPower() DEBUG_MSG("Set radio: final power level=%d\n", power); } -ErrorCode SimRadio::send(MeshPacket *p) -{ - DEBUG_MSG("SimRadio.send\n"); - packetPool.release(p); - return ERRNO_OK; -} void RadioInterface::deliverToReceiver(MeshPacket *p) { diff --git a/src/mesh/RadioInterface.h b/src/mesh/RadioInterface.h index 898ef20e4..e9f725c89 100644 --- a/src/mesh/RadioInterface.h +++ b/src/mesh/RadioInterface.h @@ -212,11 +212,6 @@ class RadioInterface } }; -class SimRadio : public RadioInterface -{ - public: - virtual ErrorCode send(MeshPacket *p) override; -}; /// Debug printing for packets void printPacket(const char *prefix, const MeshPacket *p); diff --git a/src/mesh/wifi/WiFiServerAPI.cpp b/src/mesh/wifi/WiFiServerAPI.cpp index 6014f8324..34a15f71b 100644 --- a/src/mesh/wifi/WiFiServerAPI.cpp +++ b/src/mesh/wifi/WiFiServerAPI.cpp @@ -4,11 +4,12 @@ static WiFiServerPort *apiPort; -void initApiServer() +void initApiServer(int port) { // Start API server on port 4403 if (!apiPort) { - apiPort = new WiFiServerPort(); + apiPort = new WiFiServerPort(port); + DEBUG_MSG("API server listening on TCP port %d\n", port); apiPort->init(); } } @@ -56,13 +57,11 @@ void WiFiServerPort::debugOut(char c) apiPort->openAPI->debugOut(c); } -#define MESHTASTIC_PORTNUM 4403 -WiFiServerPort::WiFiServerPort() : WiFiServer(MESHTASTIC_PORTNUM), concurrency::OSThread("ApiServer") {} +WiFiServerPort::WiFiServerPort(int port) : WiFiServer(port), concurrency::OSThread("ApiServer") {} void WiFiServerPort::init() { - DEBUG_MSG("API server listening on TCP port %d\n", MESHTASTIC_PORTNUM); begin(); } @@ -80,4 +79,4 @@ int32_t WiFiServerPort::runOnce() } return 100; // only check occasionally for incoming connections -} \ No newline at end of file +} diff --git a/src/mesh/wifi/WiFiServerAPI.h b/src/mesh/wifi/WiFiServerAPI.h index d3750e8c0..84a23be06 100644 --- a/src/mesh/wifi/WiFiServerAPI.h +++ b/src/mesh/wifi/WiFiServerAPI.h @@ -44,7 +44,7 @@ class WiFiServerPort : public WiFiServer, private concurrency::OSThread WiFiServerAPI *openAPI = NULL; public: - WiFiServerPort(); + explicit WiFiServerPort(int port); void init(); @@ -55,4 +55,4 @@ class WiFiServerPort : public WiFiServer, private concurrency::OSThread int32_t runOnce() override; }; -void initApiServer(); +void initApiServer(int port=4403); diff --git a/src/platform/portduino/PortduinoGlue.cpp b/src/platform/portduino/PortduinoGlue.cpp index 46abd6b85..b041391f8 100644 --- a/src/platform/portduino/PortduinoGlue.cpp +++ b/src/platform/portduino/PortduinoGlue.cpp @@ -51,13 +51,42 @@ class PolledIrqPin : public GPIOPin static GPIOPin *loraIrq; +int TCPPort = 4403; + +static error_t parse_opt(int key, char *arg, struct argp_state *state) { + switch (key) { + case 'p': + if (sscanf(arg, "%d", &TCPPort) < 1) + return ARGP_ERR_UNKNOWN; + else + printf("Using TCP port %d\n", TCPPort); + break; + case ARGP_KEY_ARG: + return 0; + default: + return ARGP_ERR_UNKNOWN; + } + return 0; +} + +void portduinoCustomInit() { + static struct argp_option options[] = {{"port", 'p', "PORT", 0, "The TCP port to use."}, {0}}; + static void *childArguments; + static char doc[] = "Meshtastic native build."; + static char args_doc[] = "..."; + static struct argp argp = {options, parse_opt, args_doc, doc, 0, 0, 0}; + const struct argp_child child = {&argp, OPTION_ARG_OPTIONAL, 0, 0}; + portduinoAddArguments(child, childArguments); +} + + /** apps run under portduino can optionally define a portduinoSetup() to * use portduino specific init code (such as gpioBind) to setup portduino on their host machine, * before running 'arduino' code. */ void portduinoSetup() { - printf("Setting up Meshtastic on Porduino...\n"); + printf("Setting up Meshtastic on Portduino...\n"); #ifdef PORTDUINO_LINUX_HARDWARE SPI.begin(); // We need to create SPI @@ -86,6 +115,9 @@ void portduinoSetup() #endif { + // Set the random seed equal to TCPPort to have a different seed per instance + randomSeed(TCPPort); + auto fakeBusy = new SimGPIOPin(SX126X_BUSY, "fakeBusy"); fakeBusy->writePin(LOW); fakeBusy->setSilent(true); diff --git a/src/platform/portduino/SimRadio.cpp b/src/platform/portduino/SimRadio.cpp new file mode 100644 index 000000000..ccfaa3b03 --- /dev/null +++ b/src/platform/portduino/SimRadio.cpp @@ -0,0 +1,250 @@ +#include "SimRadio.h" +#include "MeshService.h" +#include "Router.h" + +SimRadio::SimRadio() +{ + instance = this; +} + +SimRadio *SimRadio::instance; + +ErrorCode SimRadio::send(MeshPacket *p) +{ + printPacket("enqueuing for send", p); + + ErrorCode res = txQueue.enqueue(p) ? ERRNO_OK : ERRNO_UNKNOWN; + + if (res != ERRNO_OK) { // we weren't able to queue it, so we must drop it to prevent leaks + packetPool.release(p); + return res; + } + + // set (random) transmit delay to let others reconfigure their radio, + // to avoid collisions and implement timing-based flooding + DEBUG_MSG("Set random delay before transmitting.\n"); + setTransmitDelay(); + return res; +} + +void SimRadio::setTransmitDelay() +{ + MeshPacket *p = txQueue.getFront(); + // We want all sending/receiving to be done by our daemon thread. + // We use a delay here because this packet might have been sent in response to a packet we just received. + // So we want to make sure the other side has had a chance to reconfigure its radio. + + /* We assume if rx_snr = 0 and rx_rssi = 0, the packet was generated locally. + * This assumption is valid because of the offset generated by the radio to account for the noise + * floor. + */ + if (p->rx_snr == 0 && p->rx_rssi == 0) { + startTransmitTimer(true); + } else { + // If there is a SNR, start a timer scaled based on that SNR. + DEBUG_MSG("rx_snr found. hop_limit:%d rx_snr:%f\n", p->hop_limit, p->rx_snr); + startTransmitTimerSNR(p->rx_snr); + } +} + +void SimRadio::startTransmitTimer(bool withDelay) +{ + // If we have work to do and the timer wasn't already scheduled, schedule it now + if (!txQueue.empty()) { + uint32_t delayMsec = !withDelay ? 1 : getTxDelayMsec(); + // DEBUG_MSG("xmit timer %d\n", delay); + delay(delayMsec); + onNotify(TRANSMIT_DELAY_COMPLETED); + } else { + DEBUG_MSG("TX QUEUE EMPTY!\n"); + } +} + +void SimRadio::startTransmitTimerSNR(float snr) +{ + // If we have work to do and the timer wasn't already scheduled, schedule it now + if (!txQueue.empty()) { + uint32_t delayMsec = getTxDelayMsecWeighted(snr); + // DEBUG_MSG("xmit timer %d\n", delay); + delay(delayMsec); + onNotify(TRANSMIT_DELAY_COMPLETED); + } +} + +void SimRadio::handleTransmitInterrupt() +{ + // This can be null if we forced the device to enter standby mode. In that case + // ignore the transmit interrupt + if (sendingPacket) + completeSending(); +} + +void SimRadio::completeSending() +{ + // We are careful to clear sending packet before calling printPacket because + // that can take a long time + auto p = sendingPacket; + sendingPacket = NULL; + + if (p) { + txGood++; + printPacket("Completed sending", p); + + // We are done sending that packet, release it + packetPool.release(p); + // DEBUG_MSG("Done with send\n"); + } +} + + +/** Could we send right now (i.e. either not actively receving or transmitting)? */ +bool SimRadio::canSendImmediately() +{ + // We wait _if_ we are partially though receiving a packet (rather than just merely waiting for one). + // To do otherwise would be doubly bad because not only would we drop the packet that was on the way in, + // we almost certainly guarantee no one outside will like the packet we are sending. + bool busyTx = sendingPacket != NULL; + bool busyRx = isReceiving && isActivelyReceiving(); + + if (busyTx || busyRx) { + if (busyTx) + DEBUG_MSG("Can not send yet, busyTx\n"); + if (busyRx) + DEBUG_MSG("Can not send yet, busyRx\n"); + return false; + } else + return true; +} + +bool SimRadio::isActivelyReceiving() +{ + return false; // TODO check how this should be simulated +} + +bool SimRadio::isChannelActive() +{ + return false; // TODO ask simulator +} + +/** Attempt to cancel a previously sent packet. Returns true if a packet was found we could cancel */ +bool SimRadio::cancelSending(NodeNum from, PacketId id) +{ + auto p = txQueue.remove(from, id); + if (p) + packetPool.release(p); // free the packet we just removed + + bool result = (p != NULL); + DEBUG_MSG("cancelSending id=0x%x, removed=%d\n", id, result); + return result; +} + + +void SimRadio::onNotify(uint32_t notification) +{ + switch (notification) { + case ISR_TX: + handleTransmitInterrupt(); + DEBUG_MSG("tx complete - starting timer\n"); + startTransmitTimer(); + break; + case ISR_RX: + DEBUG_MSG("rx complete - starting timer\n"); + break; + case TRANSMIT_DELAY_COMPLETED: + DEBUG_MSG("delay done\n"); + + // If we are not currently in receive mode, then restart the random delay (this can happen if the main thread + // has placed the unit into standby) FIXME, how will this work if the chipset is in sleep mode? + if (!txQueue.empty()) { + if (!canSendImmediately()) { + // DEBUG_MSG("Currently Rx/Tx-ing: set random delay\n"); + setTransmitDelay(); // currently Rx/Tx-ing: reset random delay + } else { + if (isChannelActive()) { // check if there is currently a LoRa packet on the channel + // DEBUG_MSG("Channel is active: set random delay\n"); + setTransmitDelay(); // reset random delay + } else { + // Send any outgoing packets we have ready + MeshPacket *txp = txQueue.dequeue(); + assert(txp); + startSend(txp); + // Packet has been sent, count it toward our TX airtime utilization. + uint32_t xmitMsec = getPacketTime(txp); + airTime->logAirtime(TX_LOG, xmitMsec); + completeSending(); + } + } + } else { + // DEBUG_MSG("done with txqueue\n"); + } + break; + default: + assert(0); // We expected to receive a valid notification from the ISR + } +} + +/** start an immediate transmit */ +void SimRadio::startSend(MeshPacket * txp) +{ + printPacket("Starting low level send", txp); + size_t numbytes = beginSending(txp); + MeshPacket* p = packetPool.allocCopy(*txp); + perhapsDecode(p); + Compressed c = Compressed_init_default; + c.portnum = p->decoded.portnum; + // DEBUG_MSG("Sending back to simulator with portNum %d\n", p->decoded.portnum); + if (p->decoded.payload.size <= sizeof(c.data.bytes)) { + memcpy(&c.data.bytes, p->decoded.payload.bytes, p->decoded.payload.size); + c.data.size = p->decoded.payload.size; + } else { + DEBUG_MSG("Payload size is larger than compressed message allows! Sending empty payload.\n"); + } + p->decoded.payload.size = pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), Compressed_fields, &c); + p->decoded.portnum = PortNum_SIMULATOR_APP; + service.sendToPhone(p); // Sending back to simulator +} + + +void SimRadio::startReceive(MeshPacket *p) { + isReceiving = true; + handleReceiveInterrupt(p); +} + + +void SimRadio::handleReceiveInterrupt(MeshPacket *p) +{ + DEBUG_MSG("HANDLE RECEIVE INTERRUPT\n"); + uint32_t xmitMsec; + assert(isReceiving); + isReceiving = false; + + // read the number of actually received bytes + size_t length = getPacketLength(p); + xmitMsec = getPacketTime(length); + // DEBUG_MSG("Payload size %d vs length (includes header) %d\n", p->decoded.payload.size, length); + + MeshPacket *mp = packetPool.allocCopy(*p); // keep a copy in packtPool + mp->which_payload_variant = MeshPacket_decoded_tag; // Mark that the payload is already decoded + + printPacket("Lora RX", mp); + + airTime->logAirtime(RX_LOG, xmitMsec); + + deliverToReceiver(mp); +} + +size_t SimRadio::getPacketLength(MeshPacket *mp) { + auto &p = mp->decoded; + return (size_t)p.payload.size+sizeof(PacketHeader); +} + +int16_t SimRadio::readData(uint8_t* data, size_t len) { + int16_t state = RADIOLIB_ERR_NONE; + + if(state == RADIOLIB_ERR_NONE) { + // add null terminator + data[len] = 0; + } + + return state; +} \ No newline at end of file diff --git a/src/platform/portduino/SimRadio.h b/src/platform/portduino/SimRadio.h new file mode 100644 index 000000000..dad419c62 --- /dev/null +++ b/src/platform/portduino/SimRadio.h @@ -0,0 +1,87 @@ +#pragma once + +#include "RadioInterface.h" +#include "MeshPacketQueue.h" +#include "wifi/WiFiServerAPI.h" + +#define RADIOLIB_EXCLUDE_HTTP +#include + +class SimRadio : public RadioInterface +{ + enum PendingISR { ISR_NONE = 0, ISR_RX, ISR_TX, TRANSMIT_DELAY_COMPLETED }; + + /** + * Debugging counts + */ + uint32_t rxBad = 0, rxGood = 0, txGood = 0; + + MeshPacketQueue txQueue = MeshPacketQueue(MAX_TX_QUEUE); + + public: + SimRadio(); + + /** MeshService needs this to find our active instance + */ + static SimRadio *instance; + + + virtual ErrorCode send(MeshPacket *p) override; + + /** can we detect a LoRa preamble on the current channel? */ + virtual bool isChannelActive(); + + /** are we actively receiving a packet (only called during receiving state) + * This method is only public to facilitate debugging. Do not call. + */ + virtual bool isActivelyReceiving(); + + /** Attempt to cancel a previously sent packet. Returns true if a packet was found we could cancel */ + virtual bool cancelSending(NodeNum from, PacketId id) override; + + /** + * Start waiting to receive a message + * + * External functions can call this method to wake the device from sleep. + */ + virtual void startReceive(MeshPacket *p); + + protected: + /// are _trying_ to receive a packet currently (note - we might just be waiting for one) + bool isReceiving = false; + + private: + + void setTransmitDelay(); + + /** random timer with certain min. and max. settings */ + void startTransmitTimer(bool withDelay = true); + + /** timer scaled to SNR of to be flooded packet */ + void startTransmitTimerSNR(float snr); + + void handleTransmitInterrupt(); + void handleReceiveInterrupt(MeshPacket *p); + + void onNotify(uint32_t notification); + + // start an immediate transmit + virtual void startSend(MeshPacket *txp); + + // derive packet length + size_t getPacketLength(MeshPacket *p); + + int16_t readData(uint8_t* str, size_t len); + + protected: + /** Could we send right now (i.e. either not actively receiving or transmitting)? */ + virtual bool canSendImmediately(); + + + /** + * If a send was in progress finish it and return the buffer to the pool */ + void completeSending(); + +}; + +extern SimRadio *simRadio; \ No newline at end of file