diff --git a/bin/version.sh b/bin/version.sh index 1a7f96efd..8a7fb3ca3 100644 --- a/bin/version.sh +++ b/bin/version.sh @@ -1,3 +1,3 @@ -export VERSION=1.1.7 \ No newline at end of file +export VERSION=1.1.8 \ No newline at end of file diff --git a/boards/nrf52840_dk_modified.json b/boards/nrf52840_dk_modified.json index a11ea02c5..cbd246480 100644 --- a/boards/nrf52840_dk_modified.json +++ b/boards/nrf52840_dk_modified.json @@ -1,7 +1,7 @@ { "build": { "arduino": { - "ldscript": "nrf52840_s140_v6.ld" + "ldscript": "nrf52840_s113_v7.ld" }, "core": "nRF5", "cpu": "cortex-m4", @@ -16,9 +16,9 @@ "name": "adafruit" }, "softdevice": { - "sd_flags": "-DS140", - "sd_name": "s140", - "sd_version": "6.1.1", + "sd_flags": "-DS113", + "sd_name": "s113", + "sd_version": "7.2.0", "sd_fwid": "0x00B6" }, "bootloader": { diff --git a/docs/hardware/pinetab/SX1302/DS_SX1302_V1.0.pdf b/docs/hardware/pinetab/SX1302/DS_SX1302_V1.0.pdf new file mode 100644 index 000000000..875008792 Binary files /dev/null and b/docs/hardware/pinetab/SX1302/DS_SX1302_V1.0.pdf differ diff --git a/docs/hardware/pinetab/SX1302/M-GW1302S 用户手册(2)(1)(1).pdf b/docs/hardware/pinetab/SX1302/M-GW1302S 用户手册(2)(1)(1).pdf new file mode 100644 index 000000000..9ad7bbb79 Binary files /dev/null and b/docs/hardware/pinetab/SX1302/M-GW1302S 用户手册(2)(1)(1).pdf differ diff --git a/docs/hardware/pinetab/SX1302/M-GW1302S(射频版)硬件设计手册_V1.1.pdf b/docs/hardware/pinetab/SX1302/M-GW1302S(射频版)硬件设计手册_V1.1.pdf new file mode 100644 index 000000000..bdc8bb250 Binary files /dev/null and b/docs/hardware/pinetab/SX1302/M-GW1302S(射频版)硬件设计手册_V1.1.pdf differ diff --git a/docs/hardware/pinetab/SX1302/M-GW1302(透传版)_硬件设计手册.pdf b/docs/hardware/pinetab/SX1302/M-GW1302(透传版)_硬件设计手册.pdf new file mode 100644 index 000000000..40c23e36c Binary files /dev/null and b/docs/hardware/pinetab/SX1302/M-GW1302(透传版)_硬件设计手册.pdf differ diff --git a/docs/software/TODO.md b/docs/software/TODO.md index 514fda9cf..4173296e1 100644 --- a/docs/software/TODO.md +++ b/docs/software/TODO.md @@ -2,15 +2,11 @@ You probably don't care about this section - skip to the next one. -Threading tasks: +For high speed/lots of devices/short range tasks: -- Use https://github.com/ivanseidel/ArduinoThread? rather than full coroutines -- clean up main loop() -- check that we are mostly asleep, show which thread is causing us to wake -- -- use tickless idle on nrf52, and sleep X msec or until an interrupt occurs or the cooperative scheduling changes. https://devzone.nordicsemi.com/f/nordic-q-a/12363/nrf52-freertos-power-consumption-tickless-idle -- BAD IDEA: use vTaskDelay and https://www.freertos.org/xTaskAbortDelay.html if scheduling changes. (define INCLUDE_xTaskAbortDelay on ESP32 and NRF52 - seems impossible to find?) -- GOOD IDEA: use xSemaphoreTake to take a semaphore using a timeout. Expect semaphore to not be set, but set it to indicate scheduling has changed. +- When guessing numhops for sending: if I've heard from many local (0 hop neighbors) decrease hopcount by 2 rather than 1. +This should nicely help 'router' nodes do the right thing when long range, or if there are many local nodes for short range. +- fix timeouts/delays to be based on packet length at current radio settings Nimble tasks: diff --git a/docs/software/pinetab.md b/docs/software/pinetab.md index ce5d3e753..21600716a 100644 --- a/docs/software/pinetab.md +++ b/docs/software/pinetab.md @@ -14,6 +14,13 @@ Notes here on using that driver: https://www.linuxquestions.org/questions/linux- Or if **absolutely** necessary could bitbang: https://www.cnx-software.com/2018/02/16/wch-ch341-usb-to-serial-chip-gets-linux-driver-to-control-gpios-over-usb/ +## Portduino tasks + +* How to access spi devices via ioctl (spidev): https://www.raspberrypi.org/documentation/hardware/raspberrypi/spi/README.md#:~:text=Troubleshooting-,Overview,bus)%2C%20UARTs%2C%20etc. +* access gpio via libgpiod? +* Use dkms to distribute driver? +* echo 100 > /sys/module/spi_ch341_usb/parameters/poll_period + ## Task list * Port meshtastic to build (under platformio) for a poxix target. spec: no screen, no gpios, sim network interface, posix threads, posix semaphores & queues, IO to the console only diff --git a/src/PowerFSM.cpp b/src/PowerFSM.cpp index ccc2f54ac..4b8b96368 100644 --- a/src/PowerFSM.cpp +++ b/src/PowerFSM.cpp @@ -251,13 +251,11 @@ void PowerFSM_setup() #ifndef NRF52_SERIES // We never enter light-sleep or NB states on NRF52 (because the CPU uses so little power normally) - lowPowerState = &stateDARK; - powerFSM.add_timed_transition(&stateDARK, &stateNB, getPref_phone_timeout_secs() * 1000, NULL, "Phone timeout"); - powerFSM.add_timed_transition(&stateNB, &stateLS, getPref_min_wake_secs() * 1000, NULL, "Min wake timeout"); - powerFSM.add_timed_transition(&stateDARK, &stateLS, getPref_wait_bluetooth_secs() * 1000, NULL, "Bluetooth timeout"); +#else + lowPowerState = &stateDARK; #endif auto meshSds = getPref_mesh_sds_timeout_secs(); diff --git a/src/RedirectablePrint.cpp b/src/RedirectablePrint.cpp index 04205f498..0205aa66d 100644 --- a/src/RedirectablePrint.cpp +++ b/src/RedirectablePrint.cpp @@ -1,4 +1,5 @@ #include "RedirectablePrint.h" +#include "configuration.h" #include /** @@ -10,4 +11,14 @@ void RedirectablePrint::setDestination(Print *_dest) { assert(_dest); dest = _dest; +} + +size_t RedirectablePrint::write(uint8_t c) +{ +#ifdef SEGGER_STDOUT_CH + SEGGER_RTT_PutCharSkip(SEGGER_STDOUT_CH, c); +#endif + + dest->write(c); + return 1; // We always claim one was written, rather than trusting what the serial port said (which could be zero) } \ No newline at end of file diff --git a/src/RedirectablePrint.h b/src/RedirectablePrint.h index f75aea010..2d525118d 100644 --- a/src/RedirectablePrint.h +++ b/src/RedirectablePrint.h @@ -19,7 +19,7 @@ class RedirectablePrint : public Print */ void setDestination(Print *dest); - virtual size_t write(uint8_t c) { return dest->write(c); } + virtual size_t write(uint8_t c); }; class NoopPrint : public Print diff --git a/src/configuration.h b/src/configuration.h index 5e8964d22..ed851ed8f 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -404,8 +404,11 @@ along with this program. If not, see . // Always include the SEGGER code on NRF52 - because useful for debugging #include "SEGGER_RTT.h" +// The channel we send stdout data to +#define SEGGER_STDOUT_CH 0 + // Debug printing to segger console -#define SEGGER_MSG(...) SEGGER_RTT_printf(0, __VA_ARGS__) +#define SEGGER_MSG(...) SEGGER_RTT_printf(SEGGER_STDOUT_CH, __VA_ARGS__) // If we are not on a NRF52840 (which has built in USB-ACM serial support) and we don't have serial pins hooked up, then we MUST // use SEGGER for debug output diff --git a/src/esp32/main-esp32.cpp b/src/esp32/main-esp32.cpp index aa85f9524..25e7e1a30 100644 --- a/src/esp32/main-esp32.cpp +++ b/src/esp32/main-esp32.cpp @@ -9,6 +9,7 @@ #include "utils.h" #include #include +#include void getMacAddr(uint8_t *dmac) { @@ -86,4 +87,58 @@ void esp32Loop() // for debug printing // radio.radioIf.canSleep(); +} + +void cpuDeepSleep(uint64_t msecToWake) +{ + /* + Some ESP32 IOs have internal pullups or pulldowns, which are enabled by default. + If an external circuit drives this pin in deep sleep mode, current consumption may + increase due to current flowing through these pullups and pulldowns. + + To isolate a pin, preventing extra current draw, call rtc_gpio_isolate() function. + For example, on ESP32-WROVER module, GPIO12 is pulled up externally. + GPIO12 also has an internal pulldown in the ESP32 chip. This means that in deep sleep, + some current will flow through these external and internal resistors, increasing deep + sleep current above the minimal possible value. + + Note: we don't isolate pins that are used for the LORA, LED, i2c, spi or the wake button + */ + static const uint8_t rtcGpios[] = {/* 0, */ 2, + /* 4, */ +#ifndef USE_JTAG + 13, + /* 14, */ /* 15, */ +#endif + /* 25, */ 26, /* 27, */ + 32, 33, 34, 35, + 36, 37 + /* 38, 39 */}; + + for (int i = 0; i < sizeof(rtcGpios); i++) + rtc_gpio_isolate((gpio_num_t)rtcGpios[i]); + + // FIXME, disable internal rtc pullups/pulldowns on the non isolated pins. for inputs that we aren't using + // to detect wake and in normal operation the external part drives them hard. + + // We want RTC peripherals to stay on + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + +#ifdef BUTTON_PIN + // Only GPIOs which are have RTC functionality can be used in this bit map: 0,2,4,12-15,25-27,32-39. + uint64_t gpioMask = (1ULL << BUTTON_PIN); + +#ifdef BUTTON_NEED_PULLUP + gpio_pullup_en((gpio_num_t)BUTTON_PIN); +#endif + + // Not needed because both of the current boards have external pullups + // FIXME change polarity in hw so we can wake on ANY_HIGH instead - that would allow us to use all three buttons (instead of + // just the first) gpio_pullup_en((gpio_num_t)BUTTON_PIN); + + esp_sleep_enable_ext1_wakeup(gpioMask, ESP_EXT1_WAKEUP_ALL_LOW); +#endif + + esp_sleep_enable_timer_wakeup(msecToWake * 1000ULL); // call expects usecs + esp_deep_sleep_start(); // TBD mA sleep current (battery) } \ No newline at end of file diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index d72ff70f5..314b4c313 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -25,9 +25,15 @@ uint8_t GPS::i2cAddress = 0; GPS *gps; +/// Multiple GPS instances might use the same serial port (in sequence), but we can +/// only init that port once. +static bool didSerialInit; + bool GPS::setupGPS() { - if (_serial_gps) { + if (_serial_gps && !didSerialInit) { + didSerialInit = true; + #ifdef GPS_RX_PIN _serial_gps->begin(GPS_BAUDRATE, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN); #else @@ -59,8 +65,10 @@ bool GPS::setup() setAwake(true); // Wake GPS power before doing any init bool ok = setupGPS(); - if (ok) + if (ok) { notifySleepObserver.observe(¬ifySleep); + notifyDeepSleepObserver.observe(¬ifyDeepSleep); + } return ok; } @@ -275,7 +283,19 @@ void GPS::forceWake(bool on) /// Prepare the GPS for the cpu entering deep or light sleep, expect to be gone for at least 100s of msecs int GPS::prepareSleep(void *unused) { + DEBUG_MSG("GPS prepare sleep!\n"); forceWake(false); return 0; } + +/// Prepare the GPS for the cpu entering deep or light sleep, expect to be gone for at least 100s of msecs +int GPS::prepareDeepSleep(void *unused) +{ + DEBUG_MSG("GPS deep sleep!\n"); + + // For deep sleep we also want abandon any lock attempts (because we want minimum power) + setAwake(false); + + return 0; +} diff --git a/src/gps/GPS.h b/src/gps/GPS.h index 98ad185f2..eca2962d2 100644 --- a/src/gps/GPS.h +++ b/src/gps/GPS.h @@ -30,6 +30,7 @@ class GPS : private concurrency::OSThread uint8_t numSatellites = 0; CallbackObserver notifySleepObserver = CallbackObserver(this, &GPS::prepareSleep); + CallbackObserver notifyDeepSleepObserver = CallbackObserver(this, &GPS::prepareDeepSleep); public: /** If !NULL we will use this serial port to construct our GPS */ @@ -115,6 +116,10 @@ class GPS : private concurrency::OSThread /// always returns 0 to indicate okay to sleep int prepareSleep(void *unused); + /// Prepare the GPS for the cpu entering deep sleep, expect to be gone for at least 100s of msecs + /// always returns 0 to indicate okay to sleep + int prepareDeepSleep(void *unused); + /** * Switch the GPS into a mode where we are actively looking for a lock, or alternatively switch GPS into a low power mode * diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 5e75e3244..0cd5c1bbb 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -85,13 +85,16 @@ static uint16_t displayWidth, displayHeight; #define SCREEN_TRANSITION_MSECS 300 #endif -static void drawBootScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +/** + * Draw the icon with extra info printed around the corners + */ +static void drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { // draw an xbm image. // Please note that everything that should be transitioned // needs to be drawn relative to x and y - // draw centered left to right and centered above the one line of app text + // draw centered icon left to right and centered above the one line of app text display->drawXbm(x + (SCREEN_WIDTH - icon_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - icon_height) / 2 + 2, icon_width, icon_height, (const uint8_t *)icon_bits); @@ -101,15 +104,31 @@ static void drawBootScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int1 display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title); display->setFont(FONT_SMALL); - const char *region = myRegion ? myRegion->name : NULL; - if (region) - display->drawString(x + 0, y + 0, region); + // Draw region in upper left + if (upperMsg) + display->drawString(x + 0, y + 0, upperMsg); + // Draw version in upper right char buf[16]; snprintf(buf, sizeof(buf), "%s", xstr(APP_VERSION)); // Note: we don't bother printing region or now, it makes the string too long display->drawString(x + SCREEN_WIDTH - display->getStringWidth(buf), y + 0, buf); screen->forceDisplay(); + + // FIXME - draw serial # somewhere? +} + +static void drawBootScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + // Draw region in upper left + const char *region = myRegion ? myRegion->name : NULL; + drawIconScreen(region, display, state, x, y); +} + +/// Used on eink displays while in deep sleep +static void drawSleepScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + drawIconScreen("Sleeping...", display, state, x, y); } static void drawFrameBluetooth(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) @@ -600,6 +619,21 @@ Screen::Screen(uint8_t address, int sda, int scl) : OSThread("Screen"), cmdQueue cmdQueue.setReader(this); } +/** + * Prepare the display for the unit going to the lowest power mode possible. Most screens will just + * poweroff, but eink screens will show a "I'm sleeping" graphic, possibly with a QR code + */ +void Screen::doDeepSleep() +{ +#ifdef HAS_EINK + static FrameCallback sleepFrames[] = {drawSleepScreen}; + static const int sleepFrameCount = sizeof(sleepFrames) / sizeof(sleepFrames[0]); + ui.setFrames(sleepFrames, sleepFrameCount); + ui.update(); +#endif + setOn(false); +} + void Screen::handleSetOn(bool on) { if (!useDisplay) @@ -636,7 +670,7 @@ void Screen::setup() displayWidth = dispdev.width(); displayHeight = dispdev.height(); - ui.setTimePerTransition(SCREEN_TRANSITION_MSECS); + ui.setTimePerTransition(SCREEN_TRANSITION_MSECS); ui.setIndicatorPosition(BOTTOM); // Defines where the first frame is located in the bar. diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index 5539dcc76..d0bf62286 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -99,6 +99,12 @@ class Screen : public concurrency::OSThread enqueueCmd(ScreenCmd{.cmd = on ? Cmd::SET_ON : Cmd::SET_OFF}); } + /** + * Prepare the display for the unit going to the lowest power mode possible. Most screens will just + * poweroff, but eink screens will show a "I'm sleeping" graphic, possibly with a QR code + */ + void doDeepSleep(); + /// Handles a button press. void onPress() { enqueueCmd(ScreenCmd{.cmd = Cmd::ON_PRESS}); } diff --git a/src/main.cpp b/src/main.cpp index 0f24e1bc4..61bb2be14 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -172,6 +172,10 @@ class ButtonThread : public OSThread { #ifdef BUTTON_PIN userButton = OneButton(BUTTON_PIN, true, true); +#ifdef INPUT_PULLUP_SENSE + // Some platforms (nrf52) have a SENSE variant which allows wake from sleep - override what OneButton did + pinMode(BUTTON_PIN, INPUT_PULLUP_SENSE); +#endif userButton.attachClick(userButtonPressed); userButton.attachDuringLongPress(userButtonPressedLong); userButton.attachDoubleClick(userButtonDoublePressed); @@ -179,6 +183,10 @@ class ButtonThread : public OSThread #endif #ifdef BUTTON_PIN_ALT userButtonAlt = OneButton(BUTTON_PIN_ALT, true, true); +#ifdef INPUT_PULLUP_SENSE + // Some platforms (nrf52) have a SENSE variant which allows wake from sleep - override what OneButton did + pinMode(BUTTON_PIN_ALT, INPUT_PULLUP_SENSE); +#endif userButtonAlt.attachClick(userButtonPressed); userButtonAlt.attachDuringLongPress(userButtonPressedLong); userButtonAlt.attachDoubleClick(userButtonDoublePressed); @@ -233,8 +241,8 @@ RadioInterface *rIf = NULL; void setup() { -#ifdef USE_SEGGER - SEGGER_RTT_ConfigUpBuffer(0, NULL, NULL, 0, SEGGER_RTT_MODE_NO_BLOCK_TRIM); +#ifdef SEGGER_STDOUT_CH + SEGGER_RTT_ConfigUpBuffer(SEGGER_STDOUT_CH, NULL, NULL, 1024, SEGGER_RTT_MODE_NO_BLOCK_TRIM); #endif // Debug diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 3714f4033..61be815ec 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -143,11 +143,15 @@ bool NodeDB::resetRadioConfig() DEBUG_MSG("***** DEVELOPMENT MODE - DO NOT RELEASE *****\n"); // Sleep quite frequently to stress test the BLE comms, broadcast position every 6 mins - radioConfig.preferences.screen_on_secs = 30; - radioConfig.preferences.wait_bluetooth_secs = 30; + radioConfig.preferences.screen_on_secs = 10; + radioConfig.preferences.wait_bluetooth_secs = 10; radioConfig.preferences.position_broadcast_secs = 6 * 60; radioConfig.preferences.ls_secs = 60; radioConfig.preferences.region = RegionCode_TW; + + // Enter super deep sleep soon and stay there not very long + //radioConfig.preferences.mesh_sds_timeout_secs = 10; + //radioConfig.preferences.sds_secs = 60; } // Update the global myRegion diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index 83bf5aa37..a1bbc5c4d 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -53,6 +53,68 @@ separated by 2.16 MHz with respect to the adjacent channels. Channel zero starts // 1kb was too small #define RADIO_STACK_SIZE 4096 +/** + * Calculate airtime per + * https://www.rs-online.com/designspark/rel-assets/ds-assets/uploads/knowledge-items/application-notes-for-the-internet-of-things/LoRa%20Design%20Guide.pdf + * section 4 + * + * @return num msecs for the packet + */ +uint32_t RadioInterface::getPacketTime(uint32_t pl) +{ + float bandwidthHz = bw * 1000.0f; + bool headDisable = false; // we currently always use the header + float tSym = (1 << sf) / bandwidthHz; + + bool lowDataOptEn = tSym > 16e-3 ? true : false; // Needed if symbol time is >16ms + + float tPreamble = (preambleLength + 4.25f) * tSym; + float numPayloadSym = + 8 + max(ceilf(((8.0f * pl - 4 * sf + 28 + 16 - 20 * headDisable) / (4 * (sf - 2 * lowDataOptEn))) * cr), 0.0f); + float tPayload = numPayloadSym * tSym; + float tPacket = tPreamble + tPayload; + + uint32_t msecs = tPacket * 1000; + + DEBUG_MSG("(bw=%d, sf=%d, cr=4/%d) packet symLen=%d ms, payloadSize=%u, time %d ms\n", (int)bw, sf, cr, (int)(tSym * 1000), + pl, msecs); + return msecs; +} + +uint32_t RadioInterface::getPacketTime(MeshPacket *p) +{ + assert(p->which_payload == MeshPacket_encrypted_tag); // It should have already been encoded by now + uint32_t pl = p->encrypted.size + sizeof(PacketHeader); + + return getPacketTime(pl); +} + +/** The delay to use for retransmitting dropped packets */ +uint32_t RadioInterface::getRetransmissionMsec(const MeshPacket *p) +{ + // was 20 and 22 secs respectively, but now with shortPacketMsec as 2269, this should give the same range + return random(9 * shortPacketMsec, 10 * shortPacketMsec); +} + +/** The delay to use when we want to send something but the ether is busy */ +uint32_t RadioInterface::getTxDelayMsec() +{ + /** At the low end we want to pick a delay large enough that anyone who just completed sending (some other node) + * has had enough time to switch their radio back into receive mode. + */ + const uint32_t MIN_TX_WAIT_MSEC = 100; + + /** + * At the high end, this value is used to spread node attempts across time so when they are replying to a packet + * they don't both check that the airwaves are clear at the same moment. As long as they are off by some amount + * one of the two will be first to start transmitting and the other will see that. I bet 500ms is more than enough + * to guarantee this. + */ + // const uint32_t MAX_TX_WAIT_MSEC = 2000; // stress test would still fail occasionally with 1000 + + return random(MIN_TX_WAIT_MSEC, shortPacketMsec); +} + void printPacket(const char *prefix, const MeshPacket *p) { DEBUG_MSG("%s (id=0x%08x Fr0x%02x To0x%02x, WantAck%d, HopLim%d", prefix, p->id, p->from & 0xff, p->to & 0xff, p->want_ack, @@ -125,6 +187,12 @@ bool RadioInterface::init() return true; } +int RadioInterface::notifyDeepSleepCb(void *unused) +{ + sleep(); + return 0; +} + /** hash a string into an integer * * djb2 by Dan Bernstein. @@ -149,8 +217,47 @@ void RadioInterface::applyModemConfig() // Set up default configuration // No Sync Words in LORA mode + if (channelSettings.spread_factor == 0) { + switch (channelSettings.modem_config) { + case ChannelSettings_ModemConfig_Bw125Cr45Sf128: ///< Bw = 125 kHz, Cr = 4/5, Sf = 128chips/symbol, CRC on. Default medium + ///< range + bw = 125; + cr = 5; + sf = 7; + break; + case ChannelSettings_ModemConfig_Bw500Cr45Sf128: ///< Bw = 500 kHz, Cr = 4/5, Sf = 128chips/symbol, CRC on. Fast+short + ///< range + bw = 500; + cr = 5; + sf = 7; + break; + case ChannelSettings_ModemConfig_Bw31_25Cr48Sf512: ///< Bw = 31.25 kHz, Cr = 4/8, Sf = 512chips/symbol, CRC on. Slow+long + ///< range + bw = 31.25; + cr = 8; + sf = 9; + break; + case ChannelSettings_ModemConfig_Bw125Cr48Sf4096: + bw = 125; + cr = 8; + sf = 12; + break; + default: + assert(0); // Unknown enum + } + } else { + sf = channelSettings.spread_factor; + cr = channelSettings.coding_rate; + bw = channelSettings.bandwidth; + + if (bw == 31) // This parameter is not an integer + bw = 31.25; + } + power = channelSettings.tx_power; + shortPacketMsec = getPacketTime(sizeof(PacketHeader)); + assert(myRegion); // Should have been found in init // If user has manually specified a channel num, then use that, otherwise generate one by hashing the name @@ -165,6 +272,7 @@ void RadioInterface::applyModemConfig() DEBUG_MSG("Radio myRegion->numChannels: %d\n", myRegion->numChannels); DEBUG_MSG("Radio channel_num: %d\n", channel_num); DEBUG_MSG("Radio frequency: %f\n", freq); + DEBUG_MSG("Short packet time: %u msec\n", shortPacketMsec); } /** diff --git a/src/mesh/RadioInterface.h b/src/mesh/RadioInterface.h index 1d2e4b750..62774a37c 100644 --- a/src/mesh/RadioInterface.h +++ b/src/mesh/RadioInterface.h @@ -48,9 +48,18 @@ class RadioInterface CallbackObserver(this, &RadioInterface::preflightSleepCb); CallbackObserver notifyDeepSleepObserver = - CallbackObserver(this, &RadioInterface::notifyDeepSleepDb); + CallbackObserver(this, &RadioInterface::notifyDeepSleepCb); + + /// Number of msecs we expect our shortest actual packet to be over the wire (used in retry timeout calcs) + uint32_t shortPacketMsec; protected: + float bw = 125; + uint8_t sf = 9; + uint8_t cr = 7; + + uint16_t preambleLength = 32; // 8 is default, but FIXME use longer to increase the amount of sleep time when receiving + MeshPacket *sendingPacket = NULL; // The packet we are currently sending uint32_t lastTxStart = 0L; @@ -108,6 +117,22 @@ class RadioInterface /// \return true if initialisation succeeded. virtual bool reconfigure() = 0; + /** The delay to use for retransmitting dropped packets */ + uint32_t getRetransmissionMsec(const MeshPacket *p); + + /** The delay to use when we want to send something but the ether is busy */ + uint32_t getTxDelayMsec(); + + /** + * Calculate airtime per + * https://www.rs-online.com/designspark/rel-assets/ds-assets/uploads/knowledge-items/application-notes-for-the-internet-of-things/LoRa%20Design%20Guide.pdf + * section 4 + * + * @return num msecs for the packet + */ + uint32_t getPacketTime(MeshPacket *p); + uint32_t getPacketTime(uint32_t totalPacketLen); + protected: int8_t power = 17; // Set by applyModemConfig() @@ -136,11 +161,7 @@ class RadioInterface /// Return 0 if sleep is okay int preflightSleepCb(void *unused = NULL) { return canSleep() ? 0 : 1; } - int notifyDeepSleepDb(void *unused = NULL) - { - sleep(); - return 0; - } + int notifyDeepSleepCb(void *unused = NULL); int reloadConfig(void *unused) { diff --git a/src/mesh/RadioLibInterface.cpp b/src/mesh/RadioLibInterface.cpp index 41d000d69..0d8fbc007 100644 --- a/src/mesh/RadioLibInterface.cpp +++ b/src/mesh/RadioLibInterface.cpp @@ -58,50 +58,6 @@ void INTERRUPT_ATTR RadioLibInterface::isrTxLevel0() */ RadioLibInterface *RadioLibInterface::instance; -/** - * Convert our modemConfig enum into wf, sf, etc... - */ -void RadioLibInterface::applyModemConfig() -{ - RadioInterface::applyModemConfig(); - - if (channelSettings.spread_factor == 0) { - switch (channelSettings.modem_config) { - case ChannelSettings_ModemConfig_Bw125Cr45Sf128: ///< Bw = 125 kHz, Cr = 4/5, Sf = 128chips/symbol, CRC on. Default medium - ///< range - bw = 125; - cr = 5; - sf = 7; - break; - case ChannelSettings_ModemConfig_Bw500Cr45Sf128: ///< Bw = 500 kHz, Cr = 4/5, Sf = 128chips/symbol, CRC on. Fast+short - ///< range - bw = 500; - cr = 5; - sf = 7; - break; - case ChannelSettings_ModemConfig_Bw31_25Cr48Sf512: ///< Bw = 31.25 kHz, Cr = 4/8, Sf = 512chips/symbol, CRC on. Slow+long - ///< range - bw = 31.25; - cr = 8; - sf = 9; - break; - case ChannelSettings_ModemConfig_Bw125Cr48Sf4096: - bw = 125; - cr = 8; - sf = 12; - break; - default: - assert(0); // Unknown enum - } - } else { - sf = channelSettings.spread_factor; - cr = channelSettings.coding_rate; - bw = channelSettings.bandwidth; - - if (bw == 31) // This parameter is not an integer - bw = 31.25; - } -} /** Could we send right now (i.e. either not actively receving or transmitting)? */ bool RadioLibInterface::canSendImmediately() @@ -130,6 +86,8 @@ ErrorCode RadioLibInterface::send(MeshPacket *p) // Sometimes when testing it is useful to be able to never turn on the xmitter #ifndef LORA_DISABLE_SENDING printPacket("enqueuing for send", p); + uint32_t xmitMsec = getPacketTime(p); + DEBUG_MSG("txGood=%d,rxGood=%d,rxBad=%d\n", txGood, rxGood, rxBad); ErrorCode res = txQueue.enqueue(p, 0) ? ERRNO_OK : ERRNO_UNKNOWN; @@ -158,19 +116,6 @@ bool RadioLibInterface::canSleep() return res; } -/** At the low end we want to pick a delay large enough that anyone who just completed sending (some other node) - * has had enough time to switch their radio back into receive mode. - */ -#define MIN_TX_WAIT_MSEC 100 - -/** - * At the high end, this value is used to spread node attempts across time so when they are replying to a packet - * they don't both check that the airwaves are clear at the same moment. As long as they are off by some amount - * one of the two will be first to start transmitting and the other will see that. I bet 500ms is more than enough - * to guarantee this. - */ -#define MAX_TX_WAIT_MSEC 2000 // stress test would still fail occasionally with 1000 - /** radio helper thread callback. We never immediately transmit after any operation (either rx or tx). Instead we should start receiving and @@ -226,8 +171,7 @@ void RadioLibInterface::startTransmitTimer(bool withDelay) { // If we have work to do and the timer wasn't already scheduled, schedule it now if (!txQueue.isEmpty()) { - uint32_t delay = - !withDelay ? 1 : random(MIN_TX_WAIT_MSEC, MAX_TX_WAIT_MSEC); // See documentation for loop() wrt these values + uint32_t delay = !withDelay ? 1 : getTxDelayMsec(); // DEBUG_MSG("xmit timer %d\n", delay); notifyLater(delay, TRANSMIT_DELAY_COMPLETED, false); // This will implicitly enable } @@ -236,20 +180,25 @@ void RadioLibInterface::startTransmitTimer(bool withDelay) void RadioLibInterface::handleTransmitInterrupt() { // DEBUG_MSG("handling lora TX interrupt\n"); - assert(sendingPacket); // Were we sending? - FIXME, this was null coming out of light sleep due to RF95 ISR! - - completeSending(); + // This can be null if we forced the device to enter standby mode. In that case + // ignore the transmit interrupt + if (sendingPacket) + completeSending(); } void RadioLibInterface::completeSending() { - if (sendingPacket) { + // 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", sendingPacket); + printPacket("Completed sending", p); // We are done sending that packet, release it - packetPool.release(sendingPacket); - sendingPacket = NULL; + packetPool.release(p); // DEBUG_MSG("Done with send\n"); } } @@ -295,7 +244,7 @@ void RadioLibInterface::handleReceiveInterrupt() addReceiveMetadata(mp); mp->which_payload = MeshPacket_encrypted_tag; // Mark that the payload is still encrypted at this point - assert(payloadLen <= sizeof(mp->encrypted.bytes)); + assert(((uint32_t) payloadLen) <= sizeof(mp->encrypted.bytes)); memcpy(mp->encrypted.bytes, payload, payloadLen); mp->encrypted.size = payloadLen; diff --git a/src/mesh/RadioLibInterface.h b/src/mesh/RadioLibInterface.h index 86dcb9701..e762fdcdc 100644 --- a/src/mesh/RadioLibInterface.h +++ b/src/mesh/RadioLibInterface.h @@ -77,9 +77,6 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified PointerQueue txQueue = PointerQueue(MAX_TX_QUEUE); protected: - float bw = 125; - uint8_t sf = 9; - uint8_t cr = 7; /** * FIXME, use a meshtastic sync word, but hashed with the Channel name. Currently picking the same default @@ -88,7 +85,6 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified uint8_t syncWord = SX126X_SYNC_WORD_PRIVATE; float currentLimit = 100; // FIXME - uint16_t preambleLength = 32; // 8 is default, but FIXME use longer to increase the amount of sleep time when receiving LockingModule module; // The HW interface to the radio @@ -165,13 +161,6 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified /** Do any hardware setup needed on entry into send configuration for the radio. Subclasses can customize */ virtual void configHardwareForSend() {} - /** - * Convert our modemConfig enum into wf, sf, etc... - * - * These paramaters will be pull from the channelSettings global - */ - virtual void applyModemConfig(); - /** Could we send right now (i.e. either not actively receiving or transmitting)? */ virtual bool canSendImmediately(); diff --git a/src/mesh/ReliableRouter.cpp b/src/mesh/ReliableRouter.cpp index f3bf886b4..bbbb52386 100644 --- a/src/mesh/ReliableRouter.cpp +++ b/src/mesh/ReliableRouter.cpp @@ -111,7 +111,6 @@ PendingPacket::PendingPacket(MeshPacket *p) { packet = p; numRetransmissions = NUM_RETRANSMISSIONS - 1; // We subtract one, because we assume the user just did the first send - setNextTx(); } PendingPacket *ReliableRouter::findPendingPacket(GlobalPacketId key) @@ -151,6 +150,7 @@ PendingPacket *ReliableRouter::startRetransmission(MeshPacket *p) auto id = GlobalPacketId(p); auto rec = PendingPacket(p); + setNextTx(&rec); stopRetransmission(p->from, p->id); pending[id] = rec; @@ -190,10 +190,9 @@ int32_t ReliableRouter::doRetransmissions() // Queue again --p.numRetransmissions; - p.setNextTx(); + setNextTx(&p); } - } - else { + } else { // Not yet time int32_t t = p.nextTxMsec - now; diff --git a/src/mesh/ReliableRouter.h b/src/mesh/ReliableRouter.h index caa5f1a55..91dd248a8 100644 --- a/src/mesh/ReliableRouter.h +++ b/src/mesh/ReliableRouter.h @@ -46,8 +46,6 @@ struct PendingPacket { PendingPacket() {} PendingPacket(MeshPacket *p); - - void setNextTx() { nextTxMsec = millis() + random(20 * 1000L, 22 * 1000L); } }; class GlobalPacketIdHashFunction @@ -130,4 +128,8 @@ class ReliableRouter : public FloodingRouter * @return the number of msecs until our next retransmission or MAXINT if none scheduled */ int32_t doRetransmissions(); + + void setNextTx(PendingPacket *pending) { + assert(iface); + pending->nextTxMsec = millis() + iface->getRetransmissionMsec(pending->packet); } }; diff --git a/src/mesh/Router.h b/src/mesh/Router.h index 2bdb7231f..4642a4daf 100644 --- a/src/mesh/Router.h +++ b/src/mesh/Router.h @@ -14,12 +14,13 @@ class Router : protected concurrency::OSThread { private: - RadioInterface *iface; - /// Packets which have just arrived from the radio, ready to be processed by this service and possibly /// forwarded to the phone. PointerQueue fromRadioQueue; + protected: + RadioInterface *iface = NULL; + public: /// Local services that want to see _every_ packet this node receives can observe this. /// Observers should always return 0 and _copy_ any packets they want to keep for use later (this packet will be getting diff --git a/src/mesh/SX1262Interface.cpp b/src/mesh/SX1262Interface.cpp index 75f60128d..0a23261b5 100644 --- a/src/mesh/SX1262Interface.cpp +++ b/src/mesh/SX1262Interface.cpp @@ -201,9 +201,21 @@ bool SX1262Interface::isActivelyReceiving() bool SX1262Interface::sleep() { - // put chipset into sleep mode - disableInterrupt(); - lora.sleep(); + // Not keeping config is busted - next time nrf52 board boots lora sending fails tcxo related? - see datasheet + DEBUG_MSG("sx1262 entering sleep mode (FIXME, don't keep config)\n"); + setStandby(); // Stop any pending operations + + // turn off TCXO if it was powered + // FIXME - this isn't correct + // lora.setTCXO(0); + + // put chipset into sleep mode (we've already disabled interrupts by now) + bool keepConfig = true; + lora.sleep(keepConfig); // Note: we do not keep the config, full reinit will be needed + +#ifdef SX1262_POWER_EN + digitalWrite(SX1262_POWER_EN, LOW); +#endif return true; } \ No newline at end of file diff --git a/src/nrf52/NRF52Bluetooth.cpp b/src/nrf52/NRF52Bluetooth.cpp index 9309e645e..35093ff17 100644 --- a/src/nrf52/NRF52Bluetooth.cpp +++ b/src/nrf52/NRF52Bluetooth.cpp @@ -202,6 +202,13 @@ void setupMeshService(void) // FIXME, turn off soft device access for debugging static bool isSoftDeviceAllowed = true; +void NRF52Bluetooth::shutdown() +{ + // Shutdown bluetooth for minimum power draw + DEBUG_MSG("Disable NRF52 bluetooth\n"); + Bluefruit.Advertising.stop(); +} + void NRF52Bluetooth::setup() { // Initialise the Bluefruit module diff --git a/src/nrf52/NRF52Bluetooth.h b/src/nrf52/NRF52Bluetooth.h index 40f13d8bc..a10e07aba 100644 --- a/src/nrf52/NRF52Bluetooth.h +++ b/src/nrf52/NRF52Bluetooth.h @@ -4,5 +4,6 @@ class NRF52Bluetooth { public: void setup(); + void shutdown(); }; diff --git a/src/nrf52/main-nrf52.cpp b/src/nrf52/main-nrf52.cpp index 6b4b1adc1..8b7bb65b9 100644 --- a/src/nrf52/main-nrf52.cpp +++ b/src/nrf52/main-nrf52.cpp @@ -1,6 +1,8 @@ #include "NRF52Bluetooth.h" #include "configuration.h" #include "graphics/TFTDisplay.h" +#include +#include #include #include #include @@ -49,7 +51,7 @@ void getMacAddr(uint8_t *dmac) NRF52Bluetooth *nrf52Bluetooth; static bool bleOn = false; -static const bool enableBle = true; // Set to false for easier debugging +static const bool enableBle = false; // Set to false for easier debugging void setBluetoothEnable(bool on) { @@ -64,7 +66,8 @@ void setBluetoothEnable(bool on) } } } else { - DEBUG_MSG("FIXME: implement BLE disable\n"); + if (nrf52Bluetooth) + nrf52Bluetooth->shutdown(); } bleOn = on; } @@ -97,7 +100,7 @@ void nrf52Setup() #ifdef BQ25703A_ADDR auto *bq = new BQ25713(); - if(!bq->setup()) + if (!bq->setup()) DEBUG_MSG("ERROR! Charge controller init failed\n"); #endif @@ -109,4 +112,30 @@ void nrf52Setup() // randomSeed(r); DEBUG_MSG("FIXME, call randomSeed\n"); // ::printf("TESTING PRINTF\n"); +} + +void cpuDeepSleep(uint64_t msecToWake) +{ + // FIXME, configure RTC or button press to wake us + // FIXME, power down SPI, I2C, RAMs + Wire.end(); + SPI.end(); + Serial.end(); + Serial1.end(); + + // FIXME, use system off mode with ram retention for key state? + // FIXME, use non-init RAM per + // https://devzone.nordicsemi.com/f/nordic-q-a/48919/ram-retention-settings-with-softdevice-enabled + + auto ok = sd_power_system_off(); + if(ok != NRF_SUCCESS) { + DEBUG_MSG("FIXME: Ignoring soft device (EasyDMA pending?) and forcing system-off!\n"); + NRF_POWER->SYSTEMOFF = 1; + } + + // The following code should not be run, because we are off + while (1) { + delay(5000); + DEBUG_MSG("."); + } } \ No newline at end of file diff --git a/src/portduino/PortduinoGlue.cpp b/src/portduino/PortduinoGlue.cpp index 690af6071..0862e33f9 100644 --- a/src/portduino/PortduinoGlue.cpp +++ b/src/portduino/PortduinoGlue.cpp @@ -1,6 +1,7 @@ #include "CryptoEngine.h" #include "target_specific.h" #include +#include "sleep.h" // FIXME - move getMacAddr/setBluetoothEnable into a HALPlatform class @@ -26,6 +27,10 @@ void setBluetoothEnable(bool on) notImplemented("setBluetoothEnable"); } +void cpuDeepSleep(uint64_t msecs) { + notImplemented("cpuDeepSleep"); +} + // FIXME - implement real crypto for linux CryptoEngine *crypto = new CryptoEngine(); diff --git a/src/sleep.cpp b/src/sleep.cpp index ee90c149c..60bf911f2 100644 --- a/src/sleep.cpp +++ b/src/sleep.cpp @@ -142,19 +142,22 @@ static void waitEnterSleep() void doDeepSleep(uint64_t msecToWake) { - DEBUG_MSG("Entering deep sleep for %llu seconds\n", msecToWake / 1000); + DEBUG_MSG("Entering deep sleep for %lu seconds\n", msecToWake / 1000); -#ifndef NO_ESP32 // not using wifi yet, but once we are this is needed to shutoff the radio hw // esp_wifi_stop(); waitEnterSleep(); - notifySleep.notifyObservers(NULL); // also tell the regular sleep handlers notifyDeepSleep.notifyObservers(NULL); - screen->setOn(false); // datasheet says this will draw only 10ua + screen->doDeepSleep(); // datasheet says this will draw only 10ua nodeDB.saveToDisk(); + // Kill GPS power completely (even if previously we just had it in sleep mode) + setGPSPower(false); + + setLed(false); + #ifdef RESET_OLED digitalWrite(RESET_OLED, 1); // put the display in reset before killing its power #endif @@ -163,11 +166,6 @@ void doDeepSleep(uint64_t msecToWake) digitalWrite(VEXT_ENABLE, 1); // turn off the display power #endif - // Kill GPS power completely (even if previously we just had it in sleep mode) - setGPSPower(false); - - setLed(false); - #ifdef TBEAM_V10 if (axp192_found) { // Obsolete comment: from back when we we used to receive lora packets while CPU was in deep sleep. @@ -185,57 +183,7 @@ void doDeepSleep(uint64_t msecToWake) } #endif - /* - Some ESP32 IOs have internal pullups or pulldowns, which are enabled by default. - If an external circuit drives this pin in deep sleep mode, current consumption may - increase due to current flowing through these pullups and pulldowns. - - To isolate a pin, preventing extra current draw, call rtc_gpio_isolate() function. - For example, on ESP32-WROVER module, GPIO12 is pulled up externally. - GPIO12 also has an internal pulldown in the ESP32 chip. This means that in deep sleep, - some current will flow through these external and internal resistors, increasing deep - sleep current above the minimal possible value. - - Note: we don't isolate pins that are used for the LORA, LED, i2c, spi or the wake button - */ - static const uint8_t rtcGpios[] = {/* 0, */ 2, - /* 4, */ -#ifndef USE_JTAG - 13, - /* 14, */ /* 15, */ -#endif - /* 25, */ 26, /* 27, */ - 32, 33, 34, 35, - 36, 37 - /* 38, 39 */}; - - for (int i = 0; i < sizeof(rtcGpios); i++) - rtc_gpio_isolate((gpio_num_t)rtcGpios[i]); - - // FIXME, disable internal rtc pullups/pulldowns on the non isolated pins. for inputs that we aren't using - // to detect wake and in normal operation the external part drives them hard. - - // We want RTC peripherals to stay on - esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); - -#ifdef BUTTON_PIN - // Only GPIOs which are have RTC functionality can be used in this bit map: 0,2,4,12-15,25-27,32-39. - uint64_t gpioMask = (1ULL << BUTTON_PIN); - -#ifdef BUTTON_NEED_PULLUP - gpio_pullup_en((gpio_num_t)BUTTON_PIN); -#endif - - // Not needed because both of the current boards have external pullups - // FIXME change polarity in hw so we can wake on ANY_HIGH instead - that would allow us to use all three buttons (instead of - // just the first) gpio_pullup_en((gpio_num_t)BUTTON_PIN); - - esp_sleep_enable_ext1_wakeup(gpioMask, ESP_EXT1_WAKEUP_ALL_LOW); -#endif - - esp_sleep_enable_timer_wakeup(msecToWake * 1000ULL); // call expects usecs - esp_deep_sleep_start(); // TBD mA sleep current (battery) -#endif + cpuDeepSleep(msecToWake); } #ifndef NO_ESP32 diff --git a/src/sleep.h b/src/sleep.h index 800100119..6bcb23352 100644 --- a/src/sleep.h +++ b/src/sleep.h @@ -4,7 +4,8 @@ #include "Observer.h" #include "configuration.h" -void doDeepSleep(uint64_t msecToWake); +void doDeepSleep(uint64_t msecToWake), cpuDeepSleep(uint64_t msecToWake); + #ifndef NO_ESP32 #include "esp_sleep.h" esp_sleep_wakeup_cause_t doLightSleep(uint64_t msecToWake); diff --git a/variants/eink/variant.h b/variants/eink/variant.h index a37c50aba..6e3fedd67 100644 --- a/variants/eink/variant.h +++ b/variants/eink/variant.h @@ -205,6 +205,7 @@ External serial flash WP25R1635FZUIL0 #define PIN_EINK_MOSI (0 + 29) // also called SDI // Controls power for the eink display - Board power is enabled either by VBUS from USB or the CPU asserting PWR_ON +// FIXME - I think this is actually just the board power enable - it enables power to the CPU also #define PIN_EINK_PWR_ON (0 + 12) #define HAS_EINK diff --git a/variants/pca10056-rc-clock/variant.h b/variants/pca10056-rc-clock/variant.h index 48dc72df2..9d35325c8 100644 --- a/variants/pca10056-rc-clock/variant.h +++ b/variants/pca10056-rc-clock/variant.h @@ -147,7 +147,7 @@ static const uint8_t SCK = PIN_SPI_SCK; #define SX1262_ANT_SW (32 + 10) // P1.10 // To debug via the segger JLINK console rather than the CDC-ACM serial device -#define USE_SEGGER +// #define USE_SEGGER #ifdef __cplusplus } diff --git a/variants/ppr1/variant.h b/variants/ppr1/variant.h index cfefa091b..e6bfd8a03 100644 --- a/variants/ppr1/variant.h +++ b/variants/ppr1/variant.h @@ -168,7 +168,7 @@ static const uint8_t SCK = PIN_SPI_SCK; #define LORA_DISABLE_SENDING // Define this to disable transmission for testing (power testing etc...) // To debug via the segger JLINK console rather than the CDC-ACM serial device -#define USE_SEGGER +// #define USE_SEGGER #ifdef __cplusplus }