diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index 30af24bd2..a2c56fad9 100644
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -1,7 +1,7 @@
# trunk-ignore-all(terrascan/AC_DOCKER_0002): Known terrascan issue
# trunk-ignore-all(hadolint/DL3008): Do not pin apt package versions
# trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions
-FROM mcr.microsoft.com/devcontainers/cpp:1-debian-12
+FROM mcr.microsoft.com/devcontainers/cpp:2-debian-12
USER root
diff --git a/bin/web.version b/bin/web.version
index 952f449f1..ba5c9fca6 100644
--- a/bin/web.version
+++ b/bin/web.version
@@ -1 +1 @@
-2.6.6
\ No newline at end of file
+2.6.7
\ No newline at end of file
diff --git a/platformio.ini b/platformio.ini
index 376f6e5a8..7c63ad7ad 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -164,7 +164,7 @@ lib_deps =
# renovate: datasource=custom.pio depName=QMC5883L Compass packageName=mprograms/library/QMC5883LCompass
mprograms/QMC5883LCompass@1.2.3
# renovate: datasource=custom.pio depName=DFRobot_RTU packageName=dfrobot/library/DFRobot_RTU
- dfrobot/DFRobot_RTU@1.0.3
+ dfrobot/DFRobot_RTU@1.0.6
# renovate: datasource=git-refs depName=DFRobot_RainfallSensor packageName=https://github.com/DFRobot/DFRobot_RainfallSensor gitBranch=master
https://github.com/DFRobot/DFRobot_RainfallSensor/archive/38fea5e02b40a5430be6dab39a99a6f6347d667e.zip
# renovate: datasource=custom.pio depName=INA226 packageName=robtillaart/library/INA226
diff --git a/protobufs b/protobufs
index 38638f19f..bf149bbdc 160000
--- a/protobufs
+++ b/protobufs
@@ -1 +1 @@
-Subproject commit 38638f19f84ad886222b484d6bf5a8459aed8c7e
+Subproject commit bf149bbdcce45ba7cd8643db7cb25e5c8815072b
diff --git a/src/configuration.h b/src/configuration.h
index c6c8d673c..baf24a636 100644
--- a/src/configuration.h
+++ b/src/configuration.h
@@ -126,6 +126,11 @@ along with this program. If not, see .
#define TX_GAIN_LORA 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 9, 8, 7
#endif
+#ifdef RAK13302
+#define NUM_PA_POINTS 22
+#define TX_GAIN_LORA 7, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 8
+#endif
+
// Default system gain to 0 if not defined
#ifndef TX_GAIN_LORA
#define TX_GAIN_LORA 0
diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp
index dcaa5d69b..8e1299f51 100644
--- a/src/graphics/SharedUIDisplay.cpp
+++ b/src/graphics/SharedUIDisplay.cpp
@@ -1,6 +1,8 @@
-#include "graphics/SharedUIDisplay.h"
+#include "configuration.h"
+#if HAS_SCREEN
#include "RTC.h"
#include "graphics/ScreenFonts.h"
+#include "graphics/SharedUIDisplay.h"
#include "graphics/draw/UIRenderer.h"
#include "main.h"
#include "meshtastic/config.pb.h"
@@ -423,3 +425,4 @@ std::string sanitizeString(const std::string &input)
}
} // namespace graphics
+#endif
\ No newline at end of file
diff --git a/src/graphics/VirtualKeyboard.cpp b/src/graphics/VirtualKeyboard.cpp
index 84d5551cb..8062a0338 100644
--- a/src/graphics/VirtualKeyboard.cpp
+++ b/src/graphics/VirtualKeyboard.cpp
@@ -1,5 +1,6 @@
-#include "VirtualKeyboard.h"
#include "configuration.h"
+#if HAS_SCREEN
+#include "VirtualKeyboard.h"
#include "graphics/Screen.h"
#include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h"
@@ -736,3 +737,4 @@ bool VirtualKeyboard::isTimedOut() const
}
} // namespace graphics
+#endif
\ No newline at end of file
diff --git a/src/graphics/draw/CompassRenderer.cpp b/src/graphics/draw/CompassRenderer.cpp
index 0e5a1d727..629949ffd 100644
--- a/src/graphics/draw/CompassRenderer.cpp
+++ b/src/graphics/draw/CompassRenderer.cpp
@@ -1,3 +1,5 @@
+#include "configuration.h"
+#if HAS_SCREEN
#include "CompassRenderer.h"
#include "NodeDB.h"
#include "UIRenderer.h"
@@ -135,3 +137,4 @@ uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight)
} // namespace CompassRenderer
} // namespace graphics
+#endif
\ No newline at end of file
diff --git a/src/graphics/emotes.cpp b/src/graphics/emotes.cpp
index e1a105d20..a0227d365 100644
--- a/src/graphics/emotes.cpp
+++ b/src/graphics/emotes.cpp
@@ -1,3 +1,5 @@
+#include "configuration.h"
+#if HAS_SCREEN
#include "emotes.h"
namespace graphics
@@ -275,3 +277,4 @@ const unsigned char bell_icon[] PROGMEM = {
#endif
} // namespace graphics
+#endif
\ No newline at end of file
diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp
index 7876276a8..09f76ed46 100644
--- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp
+++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp
@@ -709,7 +709,7 @@ void InkHUD::MenuApplet::drawSystemInfoPanel(int16_t left, int16_t top, uint16_t
// Voltage
float voltage = powerStatus->getBatteryVoltageMv() / 1000.0;
char voltageStr[6]; // "XX.XV"
- sprintf(voltageStr, "%.1fV", voltage);
+ sprintf(voltageStr, "%.2fV", voltage);
printAt(colC[0], labelT, "Bat", CENTER, TOP);
printAt(colC[0], valT, voltageStr, CENTER, TOP);
diff --git a/src/main.cpp b/src/main.cpp
index bb97a1aa6..3801f6f9f 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -841,7 +841,14 @@ void setup()
SPI.begin();
}
#elif !defined(ARCH_ESP32) // ARCH_RP2040
+#if defined(RAK3401) || defined(RAK13302)
+ pinMode(WB_IO2, OUTPUT);
+ digitalWrite(WB_IO2, HIGH);
+ SPI1.setPins(LORA_MISO, LORA_SCK, LORA_MOSI);
+ SPI1.begin();
+#else
SPI.begin();
+#endif
#else
// ESP32
#if defined(HW_SPI1_DEVICE)
diff --git a/src/mesh/PhoneAPI.cpp b/src/mesh/PhoneAPI.cpp
index 51a2bc148..d1e342c80 100644
--- a/src/mesh/PhoneAPI.cpp
+++ b/src/mesh/PhoneAPI.cpp
@@ -57,6 +57,9 @@ void PhoneAPI::handleStartConfig()
#endif
}
+ // Allow subclasses to prepare for high-throughput config traffic
+ onConfigStart();
+
// even if we were already connected - restart our state machine
if (config_nonce == SPECIAL_NONCE_ONLY_NODES) {
// If client only wants node info, jump directly to sending nodes
@@ -71,7 +74,7 @@ void PhoneAPI::handleStartConfig()
spiLock->unlock();
LOG_DEBUG("Got %d files in manifest", filesManifest.size());
- LOG_INFO("Start API client config");
+ LOG_INFO("Start API client config millis=%u", millis());
// Protect against concurrent BLE callbacks: they run in NimBLE's FreeRTOS task and also touch nodeInfoQueue.
{
concurrency::LockGuard guard(&nodeInfoMutex);
@@ -453,7 +456,10 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
break;
case STATE_SEND_OTHER_NODEINFOS: {
- LOG_DEBUG("Send known nodes");
+ if (readIndex == 2) { // readIndex==2 will be true for the first non-us node
+ LOG_INFO("Start sending nodeinfos millis=%u", millis());
+ }
+
meshtastic_NodeInfo infoToSend = {};
{
concurrency::LockGuard guard(&nodeInfoMutex);
@@ -470,13 +476,22 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
if (infoToSend.num != 0) {
// Just in case we stored a different user.id in the past, but should never happen going forward
sprintf(infoToSend.user.id, "!%08x", infoToSend.num);
- LOG_DEBUG("nodeinfo: num=0x%x, lastseen=%u, id=%s, name=%s", infoToSend.num, infoToSend.last_heard,
- infoToSend.user.id, infoToSend.user.long_name);
+
+ // Logging this really slows down sending nodes on initial connection because the serial console is so slow, so only
+ // uncomment if you really need to:
+ // LOG_INFO("nodeinfo: num=0x%x, lastseen=%u, id=%s, name=%s", nodeInfoForPhone.num, nodeInfoForPhone.last_heard,
+ // nodeInfoForPhone.user.id, nodeInfoForPhone.user.long_name);
+
+ // Occasional progress logging. (readIndex==2 will be true for the first non-us node)
+ if (readIndex == 2 || readIndex % 20 == 0) {
+ LOG_DEBUG("nodeinfo: %d/%d", readIndex, nodeDB->getNumMeshNodes());
+ }
+
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_node_info_tag;
fromRadioScratch.node_info = infoToSend;
prefetchNodeInfos();
} else {
- LOG_DEBUG("Done sending nodeinfo");
+ LOG_DEBUG("Done sending %d of %d nodeinfos millis=%u", readIndex, nodeDB->getNumMeshNodes(), millis());
concurrency::LockGuard guard(&nodeInfoMutex);
nodeInfoQueue.clear();
state = STATE_SEND_FILEMANIFEST;
@@ -558,11 +573,15 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
void PhoneAPI::sendConfigComplete()
{
- LOG_INFO("Config Send Complete");
+ LOG_INFO("Config Send Complete millis=%u", millis());
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_config_complete_id_tag;
fromRadioScratch.config_complete_id = config_nonce;
config_nonce = 0;
state = STATE_SEND_PACKETS;
+
+ // Allow subclasses to know we've entered steady-state so they can lower power consumption
+ onConfigComplete();
+
pauseBluetoothLogging = false;
}
diff --git a/src/mesh/PhoneAPI.h b/src/mesh/PhoneAPI.h
index a8d0faa28..d6682684f 100644
--- a/src/mesh/PhoneAPI.h
+++ b/src/mesh/PhoneAPI.h
@@ -136,6 +136,7 @@ class PhoneAPI
bool available();
bool isConnected() { return state != STATE_SEND_NOTHING; }
+ bool isSendingPackets() { return state == STATE_SEND_PACKETS; }
protected:
/// Our fromradio packet while it is being assembled
@@ -158,6 +159,11 @@ class PhoneAPI
*/
virtual void onNowHasData(uint32_t fromRadioNum) {}
+ /// Subclasses can use these lifecycle hooks for transport-specific behavior around config/steady-state
+ /// (i.e. BLE connection params)
+ virtual void onConfigStart() {}
+ virtual void onConfigComplete() {}
+
/// begin a new connection
void handleStartConfig();
diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h
index d8d2f2e8a..059af57ae 100644
--- a/src/mesh/generated/meshtastic/mesh.pb.h
+++ b/src/mesh/generated/meshtastic/mesh.pb.h
@@ -282,6 +282,8 @@ typedef enum _meshtastic_HardwareModel {
meshtastic_HardwareModel_HELTEC_WIRELESS_TRACKER_V2 = 113,
/* LilyGo T-Watch Ultra */
meshtastic_HardwareModel_T_WATCH_ULTRA = 114,
+ /* Elecrow ThinkNode M3 */
+ meshtastic_HardwareModel_THINKNODE_M3 = 115,
/* ------------------------------------------------------------------------------------------------------------------------------------------
Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits.
------------------------------------------------------------------------------------------------------------------------------------------ */
diff --git a/src/mesh/generated/meshtastic/telemetry.pb.h b/src/mesh/generated/meshtastic/telemetry.pb.h
index a8ca96e95..dec89ba15 100644
--- a/src/mesh/generated/meshtastic/telemetry.pb.h
+++ b/src/mesh/generated/meshtastic/telemetry.pb.h
@@ -101,7 +101,9 @@ typedef enum _meshtastic_TelemetrySensorType {
/* SEN5X PM SENSORS */
meshtastic_TelemetrySensorType_SEN5X = 43,
/* TSL2561 light sensor */
- meshtastic_TelemetrySensorType_TSL2561 = 44
+ meshtastic_TelemetrySensorType_TSL2561 = 44,
+ /* BH1750 light sensor */
+ meshtastic_TelemetrySensorType_BH1750 = 45
} meshtastic_TelemetrySensorType;
/* Struct definitions */
@@ -438,8 +440,8 @@ extern "C" {
/* Helper constants for enums */
#define _meshtastic_TelemetrySensorType_MIN meshtastic_TelemetrySensorType_SENSOR_UNSET
-#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_TSL2561
-#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_TSL2561+1))
+#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_BH1750
+#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_BH1750+1))
diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp
index 95947560d..2337af808 100644
--- a/src/modules/Telemetry/EnvironmentTelemetry.cpp
+++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp
@@ -361,6 +361,7 @@ bool EnvironmentTelemetryModule::wantUIFrame()
return moduleConfig.telemetry.environment_screen_enabled;
}
+#if HAS_SCREEN
void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
// === Setup display ===
@@ -510,6 +511,7 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt
currentY += rowHeight;
}
}
+#endif
bool EnvironmentTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t)
{
diff --git a/src/modules/Telemetry/PowerTelemetry.cpp b/src/modules/Telemetry/PowerTelemetry.cpp
index 479861a2e..e69ee3931 100644
--- a/src/modules/Telemetry/PowerTelemetry.cpp
+++ b/src/modules/Telemetry/PowerTelemetry.cpp
@@ -108,6 +108,7 @@ bool PowerTelemetryModule::wantUIFrame()
return moduleConfig.telemetry.power_screen_enabled;
}
+#if HAS_SCREEN
void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
display->clear();
@@ -165,6 +166,7 @@ void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *s
drawLine("Ch3", m.ch3_voltage, m.ch3_current);
}
}
+#endif
bool PowerTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t)
{
diff --git a/src/nimble/NimbleBluetooth.cpp b/src/nimble/NimbleBluetooth.cpp
index 471661fa7..b41751ee8 100644
--- a/src/nimble/NimbleBluetooth.cpp
+++ b/src/nimble/NimbleBluetooth.cpp
@@ -3,12 +3,15 @@
#include "BluetoothCommon.h"
#include "NimbleBluetooth.h"
#include "PowerFSM.h"
+#include "StaticPointerQueue.h"
+#include "concurrency/OSThread.h"
#include "main.h"
#include "mesh/PhoneAPI.h"
#include "mesh/mesh-pb-constants.h"
#include "sleep.h"
#include
+#include
#include
#ifdef NIMBLE_TWO
@@ -34,45 +37,288 @@ constexpr uint16_t kPreferredBleTxTimeUs = (kPreferredBleTxOctets + 14) * 8;
} // namespace
#endif
+// Debugging options: careful, they slow things down quite a bit!
+// #define DEBUG_NIMBLE_ON_READ_TIMING // uncomment to time onRead duration
+// #define DEBUG_NIMBLE_ON_WRITE_TIMING // uncomment to time onWrite duration
+// #define DEBUG_NIMBLE_NOTIFY // uncomment to enable notify logging
+
+#define NIMBLE_BLUETOOTH_TO_PHONE_QUEUE_SIZE 3
+#define NIMBLE_BLUETOOTH_FROM_PHONE_QUEUE_SIZE 3
+
NimBLECharacteristic *fromNumCharacteristic;
NimBLECharacteristic *BatteryCharacteristic;
NimBLECharacteristic *logRadioCharacteristic;
NimBLEServer *bleServer;
static bool passkeyShowing;
+static std::atomic nimbleBluetoothConnHandle{-1}; // actual handles are uint16_t, so -1 means "no connection"
class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
{
+ /*
+ CAUTION: There's a lot going on here and lots of room to break things.
+
+ This NimbleBluetooth.cpp file does some tricky synchronization between the NimBLE FreeRTOS task (which runs the onRead and
+ onWrite callbacks) and the main task (which runs runOnce and the rest of PhoneAPI).
+
+ The main idea is to add a little bit of synchronization here to make it so that the rest of the codebase doesn't have to
+ know about concurrency and mutexes, and can just run happily ever after as a cooperative multitasking OSThread system, where
+ locking isn't something that anyone has to worry about too much! :)
+
+ We achieve this by having some queues and mutexes in this file only, and ensuring that all calls to getFromRadio and
+ handleToRadio are only made from the main FreeRTOS task. This way, the rest of the codebase doesn't have to worry about
+ being run concurrently, which would make everything else much much much more complicated.
+
+ PHONE -> RADIO:
+ - [NimBLE FreeRTOS task:] onWrite callback holds fromPhoneMutex and pushes received packets into fromPhoneQueue.
+ - [Main task:] runOnceHandleFromPhoneQueue in main task holds fromPhoneMutex, pulls packets from fromPhoneQueue, and calls
+ handleToRadio **in main task**.
+
+ RADIO -> PHONE:
+ - [NimBLE FreeRTOS task:] onRead callback sets onReadCallbackIsWaitingForData flag and polls in a busy loop. (unless
+ there's already a packet waiting in toPhoneQueue)
+ - [Main task:] runOnceHandleToPhoneQueue sees onReadCallbackIsWaitingForData flag, calls getFromRadio **in main task** to
+ get packets from radio, holds toPhoneMutex, pushes the packet into toPhoneQueue, and clears the
+ onReadCallbackIsWaitingForData flag.
+ - [NimBLE FreeRTOS task:] onRead callback sees that the onReadCallbackIsWaitingForData flag cleared, holds toPhoneMutex,
+ pops the packet from toPhoneQueue, and returns it to NimBLE.
+
+ MUTEXES:
+ - fromPhoneMutex protects fromPhoneQueue and fromPhoneQueueSize
+ - toPhoneMutex protects toPhoneQueue, toPhoneQueueByteSizes, and toPhoneQueueSize
+
+ ATOMICS:
+ - fromPhoneQueueSize is only increased by onWrite, and only decreased by runOnceHandleFromPhoneQueue (or onDisconnect).
+ - toPhoneQueueSize is only increased by runOnceHandleToPhoneQueue, and only decreased by onRead (or onDisconnect).
+ - onReadCallbackIsWaitingForData is a flag. It's only set by onRead, and only cleared by runOnceHandleToPhoneQueue (or
+ onDisconnect).
+
+ PRELOADING: see comments in runOnceToPhoneCanPreloadNextPacket about when it's safe to preload packets from getFromRadio.
+
+ BLE CONNECTION PARAMS:
+ - During config, we request a high-throughput, low-latency BLE connection for speed.
+ - After config, we switch to a lower-power BLE connection for steady-state use to extend battery life.
+
+ MEMORY MANAGEMENT:
+ - We keep packets on the stack and do not allocate heap.
+ - We use std::array for fromPhoneQueue and toPhoneQueue to avoid mallocs and frees across FreeRTOS tasks.
+ - Yes, we have to do some copy operations on pop because of this, but it's worth it to avoid cross-task memory management.
+
+ NOTIFY IS BROKEN:
+ - Adding NIMBLE_PROPERTY::NOTIFY to FromRadioCharacteristic appears to break things. It is NOT backwards compatible.
+
+ ZERO-SIZE READS:
+ - Returning a zero-size read from onRead breaks some clients during the config phase. So we have to block onRead until we
+ have data.
+ - During the STATE_SEND_PACKETS phase, it's totally OK to return zero-size reads, as clients are expected to do reads
+ until they get a 0-byte response.
+
+ CROSS-TASK WAKEUP:
+ - If you call: bluetoothPhoneAPI->setIntervalFromNow(0); to schedule immediate processing of new data,
+ - Then you should also call: concurrency::mainDelay.interrupt(); to wake up the main loop if it's sleeping.
+ - Otherwise, you're going to wait ~100ms or so until the main loop wakes up from some other cause.
+ */
+
public:
- BluetoothPhoneAPI() : concurrency::OSThread("NimbleBluetooth") { nimble_queue.resize(3); }
- std::vector nimble_queue;
- std::mutex nimble_mutex;
- uint8_t queue_size = 0;
- uint8_t fromRadioBytes[meshtastic_FromRadio_size] = {0};
- size_t numBytes = 0;
- bool hasChecked = false;
- bool phoneWants = false;
+ BluetoothPhoneAPI() : concurrency::OSThread("NimbleBluetooth") {}
+
+ /* Packets from phone (BLE onWrite callback) */
+ std::mutex fromPhoneMutex;
+ std::atomic fromPhoneQueueSize{0};
+ // We use array here (and pay the cost of memcpy) to avoid dynamic memory allocations and frees across FreeRTOS tasks.
+ std::array fromPhoneQueue{};
+
+ /* Packets to phone (BLE onRead callback) */
+ std::mutex toPhoneMutex;
+ std::atomic toPhoneQueueSize{0};
+ // We use array here (and pay the cost of memcpy) to avoid dynamic memory allocations and frees across FreeRTOS tasks.
+ std::array, NIMBLE_BLUETOOTH_TO_PHONE_QUEUE_SIZE> toPhoneQueue{};
+ std::array toPhoneQueueByteSizes{};
+ // The onReadCallbackIsWaitingForData flag provides synchronization between the NimBLE task's onRead callback and our main
+ // task's runOnce. It's only set by onRead, and only cleared by runOnce.
+ std::atomic onReadCallbackIsWaitingForData{false};
+
+ /* Statistics/logging helpers */
+ std::atomic readCount{0};
+ std::atomic notifyCount{0};
+ std::atomic writeCount{0};
protected:
virtual int32_t runOnce() override
{
- std::lock_guard guard(nimble_mutex);
- if (queue_size > 0) {
- for (uint8_t i = 0; i < queue_size; i++) {
- handleToRadio(nimble_queue.at(i).data(), nimble_queue.at(i).length());
+ bool shouldBreakAndRetryLater = false;
+
+ while (runOnceHasWorkToDo()) {
+ // Important that we service onRead first, because the onRead callback blocks NimBLE until we clear
+ // onReadCallbackIsWaitingForData.
+ shouldBreakAndRetryLater = runOnceHandleToPhoneQueue(); // push data from getFromRadio to onRead
+ runOnceHandleFromPhoneQueue(); // pull data from onWrite to handleToRadio
+
+ if (shouldBreakAndRetryLater) {
+ // onRead still wants data, but it's not available yet. Return so we can try again when a packet may be ready.
+#ifdef DEBUG_NIMBLE_ON_READ_TIMING
+ LOG_INFO("BLE runOnce breaking to retry later (leaving onRead waiting)");
+#endif
+ return 100; // try again in 100ms
}
- LOG_DEBUG("Queue_size %u", queue_size);
- queue_size = 0;
- }
- if (!hasChecked && phoneWants) {
- // Pull fresh data while we're outside of the NimBLE callback context.
- numBytes = getFromRadio(fromRadioBytes);
- hasChecked = true;
}
// the run is triggered via NimbleBluetoothToRadioCallback and NimbleBluetoothFromRadioCallback
return INT32_MAX;
}
+
+ virtual void onConfigStart() override
+ {
+ LOG_INFO("BLE onConfigStart");
+
+ // Prefer high throughput during config/setup, at the cost of high power consumption (for a few seconds)
+ if (bleServer && isConnected()) {
+ int32_t conn_handle = nimbleBluetoothConnHandle.load();
+ if (conn_handle != -1) {
+ requestHighThroughputConnection(static_cast(conn_handle));
+ }
+ }
+ }
+
+ virtual void onConfigComplete() override
+ {
+ LOG_INFO("BLE onConfigComplete");
+
+ // Switch to lower power consumption BLE connection params for steady-state use after config/setup is complete
+ if (bleServer && isConnected()) {
+ int32_t conn_handle = nimbleBluetoothConnHandle.load();
+ if (conn_handle != -1) {
+ requestLowerPowerConnection(static_cast(conn_handle));
+ }
+ }
+ }
+
+ bool runOnceHasWorkToDo() { return runOnceHasWorkToPhone() || runOnceHasWorkFromPhone(); }
+
+ bool runOnceHasWorkToPhone() { return onReadCallbackIsWaitingForData || runOnceToPhoneCanPreloadNextPacket(); }
+
+ bool runOnceToPhoneCanPreloadNextPacket()
+ {
+ /*
+ * PRELOADING getFromRadio RESPONSES:
+ *
+ * It's not safe to preload packets if we're in STATE_SEND_PACKETS, because there may be a while between the time we call
+ * getFromRadio and when the client actually reads it. If the connection drops in that time, we might lose that packet
+ * forever. In STATE_SEND_PACKETS, if we wait for onRead before we call getFromRadio, we minimize the time window where
+ * the client might disconnect before completing the read.
+ *
+ * However, if we're in the setup states (sending config, nodeinfo, etc), it's safe and beneficial to preload packets into
+ * toPhoneQueue because the client will just reconnect after a disconnect, losing nothing.
+ */
+
+ if (!isConnected()) {
+ return false;
+ } else if (isSendingPackets()) {
+ // If we're in STATE_SEND_PACKETS, we must wait for onRead before calling getFromRadio.
+ return false;
+ } else {
+ // In other states, we can preload as long as there's space in the toPhoneQueue.
+ return toPhoneQueueSize < NIMBLE_BLUETOOTH_TO_PHONE_QUEUE_SIZE;
+ }
+ }
+
+ bool runOnceHandleToPhoneQueue()
+ {
+ // Returns false normally.
+ // Returns true if we should break out of runOnce and retry later, such as setup states where getFromRadio returns 0
+ // bytes.
+
+ // Stack buffer for getFromRadio packet
+ uint8_t fromRadioBytes[meshtastic_FromRadio_size] = {0};
+ size_t numBytes = 0;
+
+ if (onReadCallbackIsWaitingForData || runOnceToPhoneCanPreloadNextPacket()) {
+ numBytes = getFromRadio(fromRadioBytes);
+
+ if (numBytes == 0) {
+ // Client expected a read, but we have nothing to send.
+ // Returning a 0-byte packet breaks clients during the config phase, so we have to block onRead until there's a
+ // packet ready.
+ if (isSendingPackets()) {
+ // In STATE_SEND_PACKETS, it is 100% OK to return a 0-byte response, as we expect clients to do read beyond
+ // notifies regularly, to make sure they have nothing else to read.
+#ifdef DEBUG_NIMBLE_ON_READ_TIMING
+ LOG_DEBUG("BLE getFromRadio returned numBytes=0, but in STATE_SEND_PACKETS, so clearing "
+ "onReadCallbackIsWaitingForData flag");
+#endif
+ } else {
+ // In other states, this breaks clients.
+ // Return early, leaving onReadCallbackIsWaitingForData==true so onRead knows to try again.
+ // This gives runOnce a chance to handleToRadio and produce a response.
+#ifdef DEBUG_NIMBLE_ON_READ_TIMING
+ LOG_DEBUG("BLE getFromRadio returned numBytes=0. Blocking onRead until we have data");
+#endif
+
+ // Return true to tell runOnce to shouldBreakAndRetryLater, so we don't busy-loop in runOnce even though
+ // onRead is still waiting!
+ return true;
+ }
+ } else {
+ // Push to toPhoneQueue, protected by toPhoneMutex. Hold the mutex as briefly as possible.
+ if (toPhoneQueueSize < NIMBLE_BLUETOOTH_TO_PHONE_QUEUE_SIZE) {
+ // Note: the comparison above is safe without a mutex because we are the only method that *increases*
+ // toPhoneQueueSize. (It's okay if toPhoneQueueSize *decreases* in the NimBLE task meanwhile.)
+
+ { // scope for toPhoneMutex mutex
+ std::lock_guard guard(toPhoneMutex);
+ size_t storeAtIndex = toPhoneQueueSize.load();
+ memcpy(toPhoneQueue[storeAtIndex].data(), fromRadioBytes, numBytes);
+ toPhoneQueueByteSizes[storeAtIndex] = numBytes;
+ toPhoneQueueSize++;
+ }
+#ifdef DEBUG_NIMBLE_ON_READ_TIMING
+ LOG_DEBUG("BLE getFromRadio returned numBytes=%u, pushed toPhoneQueueSize=%u", numBytes,
+ toPhoneQueueSize.load());
+#endif
+ } else {
+ // Shouldn't happen because the onRead callback shouldn't be waiting if the queue is full!
+ LOG_ERROR("Shouldn't happen! Drop FromRadio packet, toPhoneQueue full (%u bytes)", numBytes);
+ }
+ }
+
+ // Clear the onReadCallbackIsWaitingForData flag so onRead knows it can proceed.
+ onReadCallbackIsWaitingForData = false; // only clear this flag AFTER the push
+ }
+
+ return false;
+ }
+
+ bool runOnceHasWorkFromPhone() { return fromPhoneQueueSize > 0; }
+
+ void runOnceHandleFromPhoneQueue()
+ {
+ // Handle packets we received from onWrite from the phone.
+ if (fromPhoneQueueSize > 0) {
+ // Note: the comparison above is safe without a mutex because we are the only method that *decreases*
+ // fromPhoneQueueSize. (It's okay if fromPhoneQueueSize *increases* in the NimBLE task meanwhile.)
+
+ LOG_DEBUG("NimbleBluetooth: handling ToRadio packet, fromPhoneQueueSize=%u", fromPhoneQueueSize.load());
+
+ // Pop the front of fromPhoneQueue, holding the mutex only briefly while we pop.
+ NimBLEAttValue val;
+ { // scope for fromPhoneMutex mutex
+ std::lock_guard guard(fromPhoneMutex);
+ val = fromPhoneQueue[0];
+
+ // Shift the rest of the queue down
+ for (uint8_t i = 1; i < fromPhoneQueueSize; i++) {
+ fromPhoneQueue[i - 1] = fromPhoneQueue[i];
+ }
+
+ // Safe decrement due to onDisconnect
+ if (fromPhoneQueueSize > 0)
+ fromPhoneQueueSize--;
+ }
+
+ handleToRadio(val.data(), val.length());
+ }
+ }
+
/**
* Subclasses can use this as a hook to provide custom notifications for their transport (i.e. bluetooth notifies)
*/
@@ -80,14 +326,22 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
{
PhoneAPI::onNowHasData(fromRadioNum);
+ int currentNotifyCount = notifyCount.fetch_add(1);
+
uint8_t cc = bleServer->getConnectedCount();
- LOG_DEBUG("BLE notify fromNum: %d connections: %d", fromRadioNum, cc);
+
+#ifdef DEBUG_NIMBLE_NOTIFY
+ // This logging slows things down when there are lots of packets going to the phone, like initial connection:
+ LOG_DEBUG("BLE notify(%d) fromNum: %d connections: %d", currentNotifyCount, fromRadioNum, cc);
+#endif
uint8_t val[4];
put_le32(val, fromRadioNum);
fromNumCharacteristic->setValue(val, sizeof(val));
#ifdef NIMBLE_TWO
+ // NOTE: I don't have any NIMBLE_TWO devices, but this line makes me suspicious, and I suspect it needs to just be
+ // notify().
fromNumCharacteristic->notify(val, sizeof(val), BLE_HS_CONN_HANDLE_NONE);
#else
fromNumCharacteristic->notify();
@@ -96,6 +350,54 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
/// Check the current underlying physical link to see if the client is currently connected
virtual bool checkIsConnected() { return bleServer && bleServer->getConnectedCount() > 0; }
+
+ void requestHighThroughputConnection(uint16_t conn_handle)
+ {
+ /* Request a lower-latency, higher-throughput BLE connection.
+
+ This comes at the cost of higher power consumption, so we may want to only use this for initial setup, and then switch to
+ a slower mode.
+
+ See https://developer.apple.com/library/archive/qa/qa1931/_index.html for formulas to calculate values, iOS/macOS
+ constraints, and recommendations. (Android doesn't have specific constraints, but seems to be compatible with the Apple
+ recommendations.)
+
+ Selected settings:
+ minInterval (units of 1.25ms): 7.5ms = 6 (lower than the Apple recommended minimum, but allows faster when the client
+ supports it.)
+ maxInterval (units of 1.25ms): 15ms = 12
+ latency: 0 (don't allow peripheral to skip any connection events)
+ timeout (units of 10ms): 6 seconds = 600 (supervision timeout)
+
+ These are intentionally aggressive to prioritize speed over power consumption, but are only used for a few seconds at
+ setup. Not worth adjusting much.
+ */
+ LOG_INFO("BLE requestHighThroughputConnection");
+ bleServer->updateConnParams(conn_handle, 6, 12, 0, 600);
+ }
+
+ void requestLowerPowerConnection(uint16_t conn_handle)
+ {
+ /* Request a lower power consumption (but higher latency, lower throughput) BLE connection.
+
+ This is suitable for steady-state operation after initial setup is complete.
+
+ See https://developer.apple.com/library/archive/qa/qa1931/_index.html for formulas to calculate values, iOS/macOS
+ constraints, and recommendations. (Android doesn't have specific constraints, but seems to be compatible with the Apple
+ recommendations.)
+
+ Selected settings:
+ minInterval (units of 1.25ms): 30ms = 24
+ maxInterval (units of 1.25ms): 50ms = 40
+ latency: 2 (allow peripheral to skip up to 2 consecutive connection events to save power)
+ timeout (units of 10ms): 6 seconds = 600 (supervision timeout)
+
+ There's an opportunity for tuning here if anyone wants to do some power measurements, but these should allow 10-20 packets
+ per second.
+ */
+ LOG_INFO("BLE requestLowerPowerConnection");
+ bleServer->updateConnParams(conn_handle, 24, 40, 2, 600);
+ }
};
static BluetoothPhoneAPI *bluetoothPhoneAPI;
@@ -114,18 +416,45 @@ class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks
virtual void onWrite(NimBLECharacteristic *pCharacteristic)
#endif
{
+ // CAUTION: This callback runs in the NimBLE task!!! Don't do anything except communicate with the main task's runOnce.
+ // Assumption: onWrite is serialized by NimBLE, so we don't need to lock here against multiple concurrent onWrite calls.
+
+ int currentWriteCount = bluetoothPhoneAPI->writeCount.fetch_add(1);
+
+#ifdef DEBUG_NIMBLE_ON_WRITE_TIMING
+ int startMillis = millis();
+ LOG_DEBUG("BLE onWrite(%d): start millis=%d", currentWriteCount, startMillis);
+#endif
+
auto val = pCharacteristic->getValue();
if (memcmp(lastToRadio, val.data(), val.length()) != 0) {
- if (bluetoothPhoneAPI->queue_size < 3) {
+ if (bluetoothPhoneAPI->fromPhoneQueueSize < NIMBLE_BLUETOOTH_FROM_PHONE_QUEUE_SIZE) {
+ // Note: the comparison above is safe without a mutex because we are the only method that *increases*
+ // fromPhoneQueueSize. (It's okay if fromPhoneQueueSize *decreases* in the main task meanwhile.)
memcpy(lastToRadio, val.data(), val.length());
- std::lock_guard guard(bluetoothPhoneAPI->nimble_mutex);
- bluetoothPhoneAPI->nimble_queue.at(bluetoothPhoneAPI->queue_size) = val;
- bluetoothPhoneAPI->queue_size++;
+
+ { // scope for fromPhoneMutex mutex
+ // Append to fromPhoneQueue, protected by fromPhoneMutex. Hold the mutex as briefly as possible.
+ std::lock_guard guard(bluetoothPhoneAPI->fromPhoneMutex);
+ bluetoothPhoneAPI->fromPhoneQueue.at(bluetoothPhoneAPI->fromPhoneQueueSize) = val;
+ bluetoothPhoneAPI->fromPhoneQueueSize++;
+ }
+
+ // After releasing the mutex, schedule immediate processing of the new packet.
bluetoothPhoneAPI->setIntervalFromNow(0);
+ concurrency::mainDelay.interrupt(); // wake up main loop if sleeping
+
+#ifdef DEBUG_NIMBLE_ON_WRITE_TIMING
+ int finishMillis = millis();
+ LOG_DEBUG("BLE onWrite(%d): append to fromPhoneQueue took %u ms. numBytes=%d", currentWriteCount,
+ finishMillis - startMillis, val.length());
+#endif
+ } else {
+ LOG_WARN("BLE onWrite(%d): Drop ToRadio packet, fromPhoneQueue full (%u bytes)", currentWriteCount, val.length());
}
} else {
- LOG_DEBUG("Drop duplicate ToRadio packet (%u bytes)", val.length());
+ LOG_DEBUG("BLE onWrite(%d): Drop duplicate ToRadio packet (%u bytes)", currentWriteCount, val.length());
}
}
};
@@ -138,32 +467,111 @@ class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks
virtual void onRead(NimBLECharacteristic *pCharacteristic)
#endif
{
- bluetoothPhoneAPI->phoneWants = true;
- bluetoothPhoneAPI->setIntervalFromNow(0);
- std::lock_guard guard(bluetoothPhoneAPI->nimble_mutex); // BLE callbacks run in NimBLE task
+ // In some cases, it seems a new connection starts with a read.
+ // The API has no bytes to send, leading to a timeout. This short-circuits this problem.
+ if (!bluetoothPhoneAPI->isConnected())
+ return;
+ // CAUTION: This callback runs in the NimBLE task!!! Don't do anything except communicate with the main task's runOnce.
- if (!bluetoothPhoneAPI->hasChecked) {
- // Fetch payload on demand; prefetch keeps this fast for the first read.
- bluetoothPhoneAPI->numBytes = bluetoothPhoneAPI->getFromRadio(bluetoothPhoneAPI->fromRadioBytes);
- bluetoothPhoneAPI->hasChecked = true;
- }
+ int currentReadCount = bluetoothPhoneAPI->readCount.fetch_add(1);
+ int tries = 0;
+ int startMillis = millis();
- pCharacteristic->setValue(bluetoothPhoneAPI->fromRadioBytes, bluetoothPhoneAPI->numBytes);
-
- if (bluetoothPhoneAPI->numBytes != 0) {
-#ifdef NIMBLE_TWO
- // Notify immediately so subscribed clients see the packet without an extra read.
- pCharacteristic->notify(bluetoothPhoneAPI->fromRadioBytes, bluetoothPhoneAPI->numBytes, BLE_HS_CONN_HANDLE_NONE);
-#else
- pCharacteristic->notify();
+#ifdef DEBUG_NIMBLE_ON_READ_TIMING
+ LOG_DEBUG("BLE onRead(%d): start millis=%d", currentReadCount, startMillis);
#endif
+
+ // Is there a packet ready to go, or do we have to ask the main task to get one for us?
+ if (bluetoothPhoneAPI->toPhoneQueueSize > 0) {
+ // Note: the comparison above is safe without a mutex because we are the only method that *decreases*
+ // toPhoneQueueSize. (It's okay if toPhoneQueueSize *increases* in the main task meanwhile.)
+
+ // There's already a packet queued. Great! We don't need to wait for onReadCallbackIsWaitingForData.
+#ifdef DEBUG_NIMBLE_ON_READ_TIMING
+ LOG_DEBUG("BLE onRead(%d): packet already waiting, no need to set onReadCallbackIsWaitingForData", currentReadCount);
+#endif
+ } else {
+ // Tell the main task that we'd like a packet.
+ bluetoothPhoneAPI->onReadCallbackIsWaitingForData = true;
+
+ // Wait for the main task to produce a packet for us, up to about 20 seconds.
+ // It normally takes just a few milliseconds, but at initial startup, etc, the main task can get blocked for longer
+ // doing various setup tasks.
+ while (bluetoothPhoneAPI->onReadCallbackIsWaitingForData && tries < 4000) {
+ // Schedule the main task runOnce to run ASAP.
+ bluetoothPhoneAPI->setIntervalFromNow(0);
+ concurrency::mainDelay.interrupt(); // wake up main loop if sleeping
+
+ if (!bluetoothPhoneAPI->onReadCallbackIsWaitingForData) {
+ // we may be able to break even before a delay, if the call to interrupt woke up the main loop and it ran
+ // already
+#ifdef DEBUG_NIMBLE_ON_READ_TIMING
+ LOG_DEBUG("BLE onRead(%d): broke before delay after %u ms, %d tries", currentReadCount,
+ millis() - startMillis, tries);
+#endif
+ break;
+ }
+
+ // This delay happens in the NimBLE FreeRTOS task, which really can't do anything until we get a value back.
+ // No harm in polling pretty frequently.
+ delay(tries < 20 ? 1 : 5);
+ tries++;
+
+ if (tries == 4000) {
+ LOG_WARN(
+ "BLE onRead(%d): timeout waiting for data after %u ms, %d tries, giving up and returning 0-size response",
+ currentReadCount, millis() - startMillis, tries);
+ }
+ }
}
- if (bluetoothPhoneAPI->numBytes != 0) // if we did send something, queue it up right away to reload
+ // Pop from toPhoneQueue, protected by toPhoneMutex. Hold the mutex as briefly as possible.
+ uint8_t fromRadioBytes[meshtastic_FromRadio_size] = {0}; // Stack buffer for getFromRadio packet
+ size_t numBytes = 0;
+ { // scope for toPhoneMutex mutex
+ std::lock_guard guard(bluetoothPhoneAPI->toPhoneMutex);
+ size_t toPhoneQueueSize = bluetoothPhoneAPI->toPhoneQueueSize.load();
+ if (toPhoneQueueSize > 0) {
+ // Copy from the front of the toPhoneQueue
+ memcpy(fromRadioBytes, bluetoothPhoneAPI->toPhoneQueue[0].data(), bluetoothPhoneAPI->toPhoneQueueByteSizes[0]);
+ numBytes = bluetoothPhoneAPI->toPhoneQueueByteSizes[0];
+
+ // Shift the rest of the queue down
+ for (uint8_t i = 1; i < toPhoneQueueSize; i++) {
+ memcpy(bluetoothPhoneAPI->toPhoneQueue[i - 1].data(), bluetoothPhoneAPI->toPhoneQueue[i].data(),
+ bluetoothPhoneAPI->toPhoneQueueByteSizes[i]);
+ // The above line is similar to:
+ // bluetoothPhoneAPI->toPhoneQueue[i - 1] = bluetoothPhoneAPI->toPhoneQueue[i]
+ // but is usually faster because it doesn't have to copy all the trailing bytes beyond
+ // toPhoneQueueByteSizes[i].
+ //
+ // We deliberately use an array here (and pay the CPU cost of some memcpy) to avoid synchronizing dynamic
+ // memory allocations and frees across FreeRTOS tasks.
+
+ bluetoothPhoneAPI->toPhoneQueueByteSizes[i - 1] = bluetoothPhoneAPI->toPhoneQueueByteSizes[i];
+ }
+
+ // Safe decrement due to onDisconnect
+ if (bluetoothPhoneAPI->toPhoneQueueSize > 0)
+ bluetoothPhoneAPI->toPhoneQueueSize--;
+ } else {
+ // nothing in the toPhoneQueue; that's fine, and we'll just have numBytes=0.
+ }
+ }
+
+#ifdef DEBUG_NIMBLE_ON_READ_TIMING
+ int finishMillis = millis();
+ LOG_DEBUG("BLE onRead(%d): onReadCallbackIsWaitingForData took %u ms, %d tries. numBytes=%d", currentReadCount,
+ finishMillis - startMillis, tries, numBytes);
+#endif
+
+ pCharacteristic->setValue(fromRadioBytes, numBytes);
+
+ // If we sent something, wake up the main loop if it's sleeping in case there are more packets ready to enqueue.
+ if (numBytes != 0) {
bluetoothPhoneAPI->setIntervalFromNow(0);
- bluetoothPhoneAPI->numBytes = 0;
- bluetoothPhoneAPI->hasChecked = false;
- bluetoothPhoneAPI->phoneWants = false;
+ concurrency::mainDelay.interrupt(); // wake up main loop if sleeping
+ }
}
};
@@ -245,6 +653,13 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
if (screen)
screen->endAlert();
}
+
+ // Store the connection handle for future use
+#ifdef NIMBLE_TWO
+ nimbleBluetoothConnHandle = connInfo.getConnHandle();
+#else
+ nimbleBluetoothConnHandle = desc->conn_handle;
+#endif
}
#ifdef NIMBLE_TWO
@@ -291,16 +706,29 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
bluetoothStatus->updateStatus(&newStatus);
if (bluetoothPhoneAPI) {
- std::lock_guard guard(bluetoothPhoneAPI->nimble_mutex);
bluetoothPhoneAPI->close();
- bluetoothPhoneAPI->numBytes = 0;
- bluetoothPhoneAPI->queue_size = 0;
- bluetoothPhoneAPI->hasChecked = false;
- bluetoothPhoneAPI->phoneWants = false;
+
+ { // scope for fromPhoneMutex mutex
+ std::lock_guard guard(bluetoothPhoneAPI->fromPhoneMutex);
+ bluetoothPhoneAPI->fromPhoneQueueSize = 0;
+ }
+
+ bluetoothPhoneAPI->onReadCallbackIsWaitingForData = false;
+ { // scope for toPhoneMutex mutex
+ std::lock_guard guard(bluetoothPhoneAPI->toPhoneMutex);
+ bluetoothPhoneAPI->toPhoneQueueSize = 0;
+ }
+
+ bluetoothPhoneAPI->readCount = 0;
+ bluetoothPhoneAPI->notifyCount = 0;
+ bluetoothPhoneAPI->writeCount = 0;
}
// Clear the last ToRadio packet buffer to avoid rejecting first packet from new connection
memset(lastToRadio, 0, sizeof(lastToRadio));
+
+ nimbleBluetoothConnHandle = -1; // -1 means "no connection"
+
#ifdef NIMBLE_TWO
// Restart Advertising
ble->startAdvertising();
@@ -455,8 +883,7 @@ void NimbleBluetooth::setupService()
if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) {
ToRadioCharacteristic = bleService->createCharacteristic(TORADIO_UUID, NIMBLE_PROPERTY::WRITE);
// Allow notifications so phones can stream FromRadio without polling.
- FromRadioCharacteristic =
- bleService->createCharacteristic(FROMRADIO_UUID, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY);
+ FromRadioCharacteristic = bleService->createCharacteristic(FROMRADIO_UUID, NIMBLE_PROPERTY::READ);
fromNumCharacteristic =
bleService->createCharacteristic(FROMNUM_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ, 4);
logRadioCharacteristic =
@@ -464,9 +891,8 @@ void NimbleBluetooth::setupService()
} else {
ToRadioCharacteristic = bleService->createCharacteristic(
TORADIO_UUID, NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_AUTHEN | NIMBLE_PROPERTY::WRITE_ENC);
- FromRadioCharacteristic =
- bleService->createCharacteristic(FROMRADIO_UUID, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_AUTHEN |
- NIMBLE_PROPERTY::READ_ENC | NIMBLE_PROPERTY::NOTIFY);
+ FromRadioCharacteristic = bleService->createCharacteristic(
+ FROMRADIO_UUID, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_AUTHEN | NIMBLE_PROPERTY::READ_ENC);
fromNumCharacteristic = bleService->createCharacteristic(
FROMNUM_UUID,
NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_AUTHEN | NIMBLE_PROPERTY::READ_ENC, 4);
diff --git a/variants/esp32/tlora_v2_1_16/platformio.ini b/variants/esp32/tlora_v2_1_16/platformio.ini
index 6967bb480..8d5bdab9e 100644
--- a/variants/esp32/tlora_v2_1_16/platformio.ini
+++ b/variants/esp32/tlora_v2_1_16/platformio.ini
@@ -5,3 +5,12 @@ board_check = true
build_flags =
${esp32_base.build_flags} -D TLORA_V2_1_16 -I variants/esp32/tlora_v2_1_16
upload_speed = 115200
+
+[env:sugarcube]
+extends = env:tlora-v2-1-1_6
+board_level = extra
+build_flags =
+ ${env:tlora-v2-1-1_6.build_flags}
+ -DBUTTON_PIN=0
+ -DPIN_BUZZER=25
+ -DLED_PIN=-1
\ No newline at end of file
diff --git a/variants/esp32/tlora_v2_1_16/variant.h b/variants/esp32/tlora_v2_1_16/variant.h
index 48c069ab7..9584dd68b 100644
--- a/variants/esp32/tlora_v2_1_16/variant.h
+++ b/variants/esp32/tlora_v2_1_16/variant.h
@@ -8,7 +8,11 @@
#define I2C_SDA 21 // I2C pins for this board
#define I2C_SCL 22
+#if defined(LED_PIN) && LED_PIN == -1
+#undef LED_PIN
+#else
#define LED_PIN 25 // If defined we will blink this LED
+#endif
#define USE_RF95
#define LORA_DIO0 26 // a No connect on the SX1262 module
diff --git a/variants/nrf52840/rak3401_1watt/platformio.ini b/variants/nrf52840/rak3401_1watt/platformio.ini
new file mode 100644
index 000000000..1a915a6b3
--- /dev/null
+++ b/variants/nrf52840/rak3401_1watt/platformio.ini
@@ -0,0 +1,31 @@
+; The very slick RAK wireless RAK 4631 / 4630 board - Unified firmware for 5005/19003, with or without OLED RAK 1921
+[env:rak3401-1watt]
+extends = nrf52840_base
+board = wiscore_rak4631
+board_check = true
+build_flags = ${nrf52840_base.build_flags}
+ -Ivariants/nrf52840/rak3401_1watt
+ -D RAK_4631
+; -DGPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely.
+ -D RAK3401
+ -D RAK13302 ; RAK 1Watt Power Amplifier
+ -DRADIOLIB_EXCLUDE_SX128X=1
+ -DRADIOLIB_EXCLUDE_SX127X=1
+ -DRADIOLIB_EXCLUDE_LR11X0=1
+build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak3401_1watt> +
+lib_deps =
+ ${nrf52840_base.lib_deps}
+ ${networking_base.lib_deps}
+ melopero/Melopero RV3028@^1.1.0
+ rakwireless/RAKwireless NCP5623 RGB LED library@^1.0.2
+ beegee-tokyo/RAK12035_SoilMoisture@^1.0.4
+ https://github.com/RAKWireless/RAK12034-BMX160/archive/dcead07ffa267d3c906e9ca4a1330ab989e957e2.zip
+
+; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm)
+; Note: as of 6/2013 the serial/bootloader based programming takes approximately 30 seconds
+;upload_protocol = jlink
+
+; Allows programming and debug via the RAK NanoDAP as the default debugger tool for the RAK4631 (it is only $10!)
+; programming time is about the same as the bootloader version.
+; For information on this see the meshtastic developers documentation for "Development on the NRF52"
+
diff --git a/variants/nrf52840/rak3401_1watt/variant.cpp b/variants/nrf52840/rak3401_1watt/variant.cpp
new file mode 100644
index 000000000..e84b60b3b
--- /dev/null
+++ b/variants/nrf52840/rak3401_1watt/variant.cpp
@@ -0,0 +1,45 @@
+/*
+ Copyright (c) 2014-2015 Arduino LLC. All right reserved.
+ Copyright (c) 2016 Sandeep Mistry All right reserved.
+ Copyright (c) 2018, Adafruit Industries (adafruit.com)
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ See the GNU Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+*/
+
+#include "variant.h"
+#include "nrf.h"
+#include "wiring_constants.h"
+#include "wiring_digital.h"
+
+const uint32_t g_ADigitalPinMap[] = {
+ // P0
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
+
+ // P1
+ 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47};
+
+void initVariant()
+{
+ // LED1 & LED2
+ pinMode(PIN_LED1, OUTPUT);
+ ledOff(PIN_LED1);
+
+ pinMode(PIN_LED2, OUTPUT);
+ ledOff(PIN_LED2);
+
+ // 3V3 Power Rail
+ pinMode(PIN_3V3_EN, OUTPUT);
+ digitalWrite(PIN_3V3_EN, HIGH);
+}
diff --git a/variants/nrf52840/rak3401_1watt/variant.h b/variants/nrf52840/rak3401_1watt/variant.h
new file mode 100644
index 000000000..d4bb1a175
--- /dev/null
+++ b/variants/nrf52840/rak3401_1watt/variant.h
@@ -0,0 +1,226 @@
+/*
+ Copyright (c) 2014-2015 Arduino LLC. All right reserved.
+ Copyright (c) 2016 Sandeep Mistry All right reserved.
+ Copyright (c) 2018, Adafruit Industries (adafruit.com)
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ See the GNU Lesser General Public License for more details.
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+*/
+
+#ifndef _VARIANT_RAK3401_
+#define _VARIANT_RAK3401_
+
+#define RAK4630
+
+/** Master clock frequency */
+#define VARIANT_MCK (64000000ul)
+
+#define USE_LFXO // Board uses 32khz crystal for LF
+// define USE_LFRC // Board uses RC for LF
+
+/*----------------------------------------------------------------------------
+ * Headers
+ *----------------------------------------------------------------------------*/
+
+#include "WVariant.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif // __cplusplus
+
+// Number of pins defined in PinDescription array
+#define PINS_COUNT (48)
+#define NUM_DIGITAL_PINS (48)
+#define NUM_ANALOG_INPUTS (6)
+#define NUM_ANALOG_OUTPUTS (0)
+
+// LEDs
+#define PIN_LED1 (35)
+#define PIN_LED2 (36)
+
+#define LED_BUILTIN PIN_LED1
+#define LED_CONN PIN_LED2
+
+#define LED_GREEN PIN_LED1
+#define LED_BLUE PIN_LED2
+
+#define LED_STATE_ON 1 // State when LED is litted
+
+/*
+ * Analog pins
+ */
+#define PIN_A0 (5)
+#define PIN_A1 (31)
+#define PIN_A2 (28)
+#define PIN_A3 (29)
+#define PIN_A4 (30)
+#define PIN_A5 (31)
+#define PIN_A6 (0xff)
+#define PIN_A7 (0xff)
+
+static const uint8_t A0 = PIN_A0;
+static const uint8_t A1 = PIN_A1;
+static const uint8_t A2 = PIN_A2;
+static const uint8_t A3 = PIN_A3;
+static const uint8_t A4 = PIN_A4;
+static const uint8_t A5 = PIN_A5;
+static const uint8_t A6 = PIN_A6;
+static const uint8_t A7 = PIN_A7;
+#define ADC_RESOLUTION 14
+
+// Other pins
+#define WB_I2C1_SDA (13) // SENSOR_SLOT IO_SLOT
+#define WB_I2C1_SCL (14) // SENSOR_SLOT IO_SLOT
+
+#define PIN_AREF (2)
+#define PIN_NFC1 (9)
+#define WB_IO5 PIN_NFC1
+#define WB_IO4 (4)
+#define PIN_NFC2 (10)
+
+static const uint8_t AREF = PIN_AREF;
+
+/*
+ * Serial interfaces
+ */
+#define PIN_SERIAL1_RX (15)
+#define PIN_SERIAL1_TX (16)
+
+// Connected to Jlink CDC
+#define PIN_SERIAL2_RX (8)
+#define PIN_SERIAL2_TX (6)
+
+/*
+ * SPI Interfaces
+ */
+#define SPI_INTERFACES_COUNT 2
+
+#define PIN_SPI_MISO (45)
+#define PIN_SPI_MOSI (44)
+#define PIN_SPI_SCK (43)
+
+#define PIN_SPI1_MISO (29) // (0 + 29)
+#define PIN_SPI1_MOSI (30) // (0 + 30)
+#define PIN_SPI1_SCK (3) // (0 + 3)
+
+static const uint8_t SS = 42;
+static const uint8_t MOSI = PIN_SPI_MOSI;
+static const uint8_t MISO = PIN_SPI_MISO;
+static const uint8_t SCK = PIN_SPI_SCK;
+
+/*
+ * eink display pins
+ */
+#define PIN_EINK_CS (0 + 26)
+#define PIN_EINK_BUSY (0 + 4)
+#define PIN_EINK_DC (0 + 17)
+#define PIN_EINK_RES (-1)
+#define PIN_EINK_SCLK (0 + 3)
+#define PIN_EINK_MOSI (0 + 30) // also called SDI
+
+/*
+ * Wire Interfaces
+ */
+#define WIRE_INTERFACES_COUNT 1
+
+#define PIN_WIRE_SDA (WB_I2C1_SDA)
+#define PIN_WIRE_SCL (WB_I2C1_SCL)
+
+// QSPI Pins
+#define PIN_QSPI_SCK 3
+#define PIN_QSPI_CS 26
+#define PIN_QSPI_IO0 30
+#define PIN_QSPI_IO1 29
+#define PIN_QSPI_IO2 28
+#define PIN_QSPI_IO3 2
+
+// On-board QSPI Flash
+#define EXTERNAL_FLASH_DEVICES IS25LP080D
+#define EXTERNAL_FLASH_USE_QSPI
+
+// 1watt sx1262 RAK13302
+#define HW_SPI1_DEVICE 1
+
+#define LORA_SCK PIN_SPI1_SCK
+#define LORA_MISO PIN_SPI1_MISO
+#define LORA_MOSI PIN_SPI1_MOSI
+#define LORA_CS 26
+
+#define USE_SX1262
+#define SX126X_CS (26)
+#define SX126X_DIO1 (10)
+#define SX126X_BUSY (9)
+#define SX126X_RESET (4)
+
+#define SX126X_POWER_EN (21)
+// DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3
+#define SX126X_DIO2_AS_RF_SWITCH
+#define SX126X_DIO3_TCXO_VOLTAGE 1.8
+
+// Testing USB detection
+#define NRF_APM
+// If using a power chip like the INA3221 you can override the default battery voltage channel below
+// and comment out NRF_APM to use the INA3221 instead of the USB detection for charging
+// #define INA3221_BAT_CH INA3221_CH2
+// #define INA3221_ENV_CH INA3221_CH1
+
+// enables 3.3V periphery like GPS or IO Module
+// Do not toggle this for GPS power savings
+#define PIN_3V3_EN (34)
+#define WB_IO2 PIN_3V3_EN
+
+// RAK1910 GPS module
+// If using the wisblock GPS module and pluged into Port A on WisBlock base
+// IO1 is hooked to PPS (pin 12 on header) = gpio 17
+// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power (1 is on).
+// Therefore must be 1 to keep peripherals powered
+// Power is on the controllable 3V3_S rail
+// #define PIN_GPS_RESET (34)
+// #define PIN_GPS_EN PIN_3V3_EN
+#define PIN_GPS_PPS (17) // Pulse per second input from the GPS
+
+#define GPS_RX_PIN PIN_SERIAL1_RX
+#define GPS_TX_PIN PIN_SERIAL1_TX
+
+// Define pin to enable GPS toggle (set GPIO to LOW) via user button triple press
+
+// RAK12002 RTC Module
+#define RV3028_RTC (uint8_t)0b1010010
+
+// RAK18001 Buzzer in Slot C
+// #define PIN_BUZZER 21 // IO3 is PWM2
+// NEW: set this via protobuf instead!
+
+// Battery
+// The battery sense is hooked to pin A0 (5)
+#define BATTERY_PIN PIN_A0
+// and has 12 bit resolution
+#define BATTERY_SENSE_RESOLUTION_BITS 12
+#define BATTERY_SENSE_RESOLUTION 4096.0
+#undef AREF_VOLTAGE
+#define AREF_VOLTAGE 3.0
+#define VBAT_AR_INTERNAL AR_INTERNAL_3_0
+#define ADC_MULTIPLIER 1.73
+
+#define HAS_RTC 1
+
+#define RAK_4631 1
+
+#ifdef __cplusplus
+}
+#endif
+
+/*----------------------------------------------------------------------------
+ * Arduino objects - C++ only
+ *----------------------------------------------------------------------------*/
+
+#endif