diff --git a/src/configuration.h b/src/configuration.h index 2c084174d00..83d27e43b46 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -426,6 +426,25 @@ along with this program. If not, see . #ifndef HAS_BLUETOOTH #define HAS_BLUETOOTH 0 #endif +#ifndef HAS_BLE_MESH_ADVERTISING +// Keep the experimental BLE advertisement bearer opt-in by platform capability: +// it currently needs a NimBLE2 build with a spare legacy advertising instance. +// Bluefruit/nRF52 builds compile the shared codec but leave the bearer disabled +// until there is a backend that can scan and advertise without disturbing the +// normal phone GATT advertising path. +#if HAS_BLUETOOTH && defined(ARCH_ESP32) && defined(NIMBLE_TWO) && defined(CONFIG_BT_NIMBLE_EXT_ADV) && \ + CONFIG_BT_NIMBLE_EXT_ADV && defined(CONFIG_BT_NIMBLE_MAX_EXT_ADV_INSTANCES) && CONFIG_BT_NIMBLE_MAX_EXT_ADV_INSTANCES > 1 +#define HAS_BLE_MESH_ADVERTISING 1 +#else +#define HAS_BLE_MESH_ADVERTISING 0 +#endif +#endif +#ifndef BLE_MESH_ADVERTISEMENT_COMPANY_ID +// Experimental company id used by the BLE advertisement mesh bearer. Builds +// can override this once Meshtastic chooses a final Bluetooth SIG/company-data +// allocation or switches the bearer to service data. +#define BLE_MESH_ADVERTISEMENT_COMPANY_ID 0x05e1 +#endif #ifndef USE_TFTDISPLAY #define USE_TFTDISPLAY 0 #endif diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index bb24b365e54..fdf0e078dad 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -15,6 +15,9 @@ #if !MESHTASTIC_EXCLUDE_MQTT #include "mqtt/MQTT.h" #endif +#if HAS_BLE_MESH_ADVERTISING +#include "mesh/ble/BleAdvertisementMesh.h" +#endif #include "Default.h" #if ARCH_PORTDUINO #include "Throttle.h" @@ -387,6 +390,13 @@ ErrorCode Router::send(meshtastic_MeshPacket *p) } #endif +#if HAS_BLE_MESH_ADVERTISING + if (bleAdvertisementMesh && + config.network.enabled_protocols & meshtastic_Config_NetworkConfig_ProtocolFlags_BLE_ADVERTISEMENT_BROADCAST) { + bleAdvertisementMesh->onSend(p); + } +#endif + assert(iface); // This should have been detected already in sendLocal (or we just received a packet from outside) return iface->send(p); } diff --git a/src/mesh/ble/BleAdvertisementMesh.cpp b/src/mesh/ble/BleAdvertisementMesh.cpp new file mode 100644 index 00000000000..223441d0f96 --- /dev/null +++ b/src/mesh/ble/BleAdvertisementMesh.cpp @@ -0,0 +1,337 @@ +#include "configuration.h" +#include "mesh/ble/BleAdvertisementMesh.h" +#include "mesh/mesh-pb-constants.h" + +#if HAS_BLE_MESH_ADVERTISING +#include "Router.h" +#include "main.h" +#endif +#include +#include +#include + +namespace +{ +uint16_t crc16ccitt(const uint8_t *data, size_t length) +{ + uint16_t crc = 0xffff; + for (size_t i = 0; i < length; i++) { + crc ^= static_cast(data[i]) << 8; + for (uint8_t bit = 0; bit < 8; bit++) { + crc = (crc & 0x8000) ? static_cast((crc << 1) ^ 0x1021) : static_cast(crc << 1); + } + } + return crc; +} + +void putLe16(uint8_t *p, uint16_t v) +{ + p[0] = static_cast(v); + p[1] = static_cast(v >> 8); +} + +void putLe32(uint8_t *p, uint32_t v) +{ + p[0] = static_cast(v); + p[1] = static_cast(v >> 8); + p[2] = static_cast(v >> 16); + p[3] = static_cast(v >> 24); +} + +uint16_t getLe16(const uint8_t *p) +{ + return static_cast(p[0]) | (static_cast(p[1]) << 8); +} + +uint32_t getLe32(const uint8_t *p) +{ + return static_cast(p[0]) | (static_cast(p[1]) << 8) | (static_cast(p[2]) << 16) | + (static_cast(p[3]) << 24); +} +} // namespace + +BleAdvertisementMeshCodec::BleAdvertisementMeshCodec() {} + +uint8_t BleAdvertisementMeshCodec::frameCount(const meshtastic_MeshPacket *mp) +{ + if (!mp || mp->which_payload_variant != meshtastic_MeshPacket_encrypted_tag) { + return 0; + } + + uint8_t encoded[meshtastic_MeshPacket_size] = {0}; + size_t encodedLength = pb_encode_to_bytes(encoded, sizeof(encoded), &meshtastic_MeshPacket_msg, mp); + if (encodedLength == 0) { + return 0; + } + + return (encodedLength + MAX_FRAGMENT_PAYLOAD_SIZE - 1) / MAX_FRAGMENT_PAYLOAD_SIZE; +} + +bool BleAdvertisementMeshCodec::forEachFrame(const meshtastic_MeshPacket *mp, EmitFrame emit, void *context) +{ + if (!mp || !emit || mp->which_payload_variant != meshtastic_MeshPacket_encrypted_tag) { + return false; + } + + uint8_t encoded[meshtastic_MeshPacket_size] = {0}; + size_t encodedLength = pb_encode_to_bytes(encoded, sizeof(encoded), &meshtastic_MeshPacket_msg, mp); + if (encodedLength == 0) { + return false; + } + + uint8_t fragmentCount = (encodedLength + MAX_FRAGMENT_PAYLOAD_SIZE - 1) / MAX_FRAGMENT_PAYLOAD_SIZE; + if (fragmentCount == 0 || fragmentCount > MAX_FRAGMENTS) { + return false; + } + + uint16_t crc = crc16ccitt(encoded, encodedLength); + for (uint8_t fragment = 0; fragment < fragmentCount; fragment++) { + size_t offset = fragment * MAX_FRAGMENT_PAYLOAD_SIZE; + size_t remaining = encodedLength - offset; + uint8_t payloadLength = + static_cast(remaining > MAX_FRAGMENT_PAYLOAD_SIZE ? MAX_FRAGMENT_PAYLOAD_SIZE : remaining); + + uint8_t frame[MAX_FRAME_SIZE] = {0}; + frame[0] = FRAME_MAGIC_0; + frame[1] = FRAME_MAGIC_1; + frame[2] = FRAME_VERSION; + frame[3] = fragment; + frame[4] = fragmentCount; + frame[5] = payloadLength; + putLe32(&frame[6], mp->from); + putLe32(&frame[10], mp->id); + putLe16(&frame[14], crc); + memcpy(&frame[FRAME_HEADER_SIZE], &encoded[offset], payloadLength); + + if (!emit(frame, FRAME_HEADER_SIZE + payloadLength, context)) { + return false; + } + } + + return true; +} + +BleAdvertisementMeshCodec::ReassemblyContext *BleAdvertisementMeshCodec::getContext(uint32_t from, uint32_t packetId, + uint16_t crc, uint8_t fragmentCount) +{ + for (auto &context : contexts) { + if (context.expectedFragments != 0 && context.from == from && context.packetId == packetId && context.crc == crc && + context.expectedFragments == fragmentCount) { + return &context; + } + } + + for (auto &context : contexts) { + if (context.expectedFragments == 0) { + reset(context, from, packetId, crc, fragmentCount); + return &context; + } + } + + ReassemblyContext &context = contexts[nextContext]; + nextContext = (nextContext + 1) % REASSEMBLY_CONTEXTS; + reset(context, from, packetId, crc, fragmentCount); + return &context; +} + +void BleAdvertisementMeshCodec::reset(ReassemblyContext &context, uint32_t from, uint32_t packetId, uint16_t crc, + uint8_t fragmentCount) +{ + context.from = from; + context.packetId = packetId; + context.crc = crc; + context.expectedFragments = fragmentCount; + context.lastFragmentLength = 0; + context.receivedMask = 0; + memset(context.encodedPacket, 0, sizeof(context.encodedPacket)); +} + +void BleAdvertisementMeshCodec::clear(ReassemblyContext &context) +{ + reset(context, 0, 0, 0, 0); +} + +bool BleAdvertisementMeshCodec::receiveFrame(const uint8_t *frame, size_t frameLength, meshtastic_MeshPacket *out) +{ + if (!frame || !out || frameLength < FRAME_HEADER_SIZE || frameLength > MAX_FRAME_SIZE || frame[0] != FRAME_MAGIC_0 || + frame[1] != FRAME_MAGIC_1 || frame[2] != FRAME_VERSION) { + return false; + } + + uint8_t fragment = frame[3]; + uint8_t fragmentCount = frame[4]; + uint8_t payloadLength = frame[5]; + uint32_t from = getLe32(&frame[6]); + uint32_t packetId = getLe32(&frame[10]); + uint16_t crc = getLe16(&frame[14]); + + if (fragmentCount == 0 || fragmentCount > MAX_FRAGMENTS || fragment >= fragmentCount || + payloadLength > MAX_FRAGMENT_PAYLOAD_SIZE || frameLength != FRAME_HEADER_SIZE + payloadLength) { + return false; + } + + size_t offset = fragment * MAX_FRAGMENT_PAYLOAD_SIZE; + if (offset + payloadLength > sizeof(contexts[0].encodedPacket)) { + return false; + } + + ReassemblyContext *context = getContext(from, packetId, crc, fragmentCount); + + memcpy(&context->encodedPacket[offset], &frame[FRAME_HEADER_SIZE], payloadLength); + context->receivedMask |= 1ULL << fragment; + if (fragment == fragmentCount - 1) { + context->lastFragmentLength = payloadLength; + } + + uint64_t completeMask = fragmentCount == 64 ? UINT64_MAX : ((1ULL << fragmentCount) - 1); + if (context->receivedMask != completeMask || context->lastFragmentLength == 0) { + return false; + } + + size_t encodedLength = (fragmentCount - 1) * MAX_FRAGMENT_PAYLOAD_SIZE + context->lastFragmentLength; + if (crc16ccitt(context->encodedPacket, encodedLength) != context->crc) { + clear(*context); + return false; + } + + memset(out, 0, sizeof(*out)); + bool decoded = pb_decode_from_bytes(context->encodedPacket, encodedLength, &meshtastic_MeshPacket_msg, out); + clear(*context); + return decoded; +} + +#if HAS_BLE_MESH_ADVERTISING + +BleAdvertisementMesh *bleAdvertisementMesh = nullptr; + +namespace +{ +struct PlatformEmitContext +{ + BleAdvertisementMesh::QueuedFrame *frames; + uint8_t maxFrames; + uint8_t count; +}; + +bool emitPlatformFrame(const uint8_t *frame, size_t frameLength, void *context) +{ + auto *platformContext = static_cast(context); + if (!platformContext || !frame || frameLength == 0 || frameLength > BleAdvertisementMeshCodec::MAX_FRAME_SIZE || + platformContext->count >= platformContext->maxFrames) { + return false; + } + + BleAdvertisementMesh::QueuedFrame &queued = platformContext->frames[platformContext->count++]; + memcpy(queued.data, frame, frameLength); + queued.length = frameLength; + return true; +} +} // namespace + +BleAdvertisementMesh::BleAdvertisementMesh(PlatformAdvertiseFrame advertiseFrame) + : concurrency::OSThread("BleAdvMesh"), advertiseFrame(advertiseFrame) +{ +} + +bool BleAdvertisementMesh::onSend(const meshtastic_MeshPacket *mp) +{ + if (!mp || !advertiseFrame || + mp->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_BLE_ADVERTISEMENT) { + return false; + } + + uint8_t neededFrames = BleAdvertisementMeshCodec::frameCount(mp); + uint16_t queuedCopies = static_cast(neededFrames) * FRAME_REPEAT_ROUNDS; + if (neededFrames == 0 || queuedCopies > freeSlots()) { + LOG_WARN("Drop BLE advertisement broadcast id=%u, fragments=%u free=%u", mp->id, neededFrames, freeSlots()); + return false; + } + + QueuedFrame frames[BleAdvertisementMeshCodec::MAX_FRAGMENTS]; + PlatformEmitContext context = {frames, BleAdvertisementMeshCodec::MAX_FRAGMENTS, 0}; + if (!BleAdvertisementMeshCodec::forEachFrame(mp, emitPlatformFrame, &context) || context.count != neededFrames) { + return false; + } + + LOG_DEBUG("Queue packet for BLE advertisements (id=%u fragments=%u rounds=%u)", mp->id, neededFrames, FRAME_REPEAT_ROUNDS); + for (uint8_t round = 0; round < FRAME_REPEAT_ROUNDS; round++) { + for (uint8_t i = 0; i < context.count; i++) { + const QueuedFrame &frame = context.frames[(round + i) % context.count]; + if (!enqueueFrame(frame.data, frame.length)) { + return false; + } + } + } + + return true; +} + +void BleAdvertisementMesh::onAdvertisement(const uint8_t *frame, size_t frameLength) +{ + meshtastic_MeshPacket mp = {}; + if (!codec.receiveFrame(frame, frameLength, &mp)) { + return; + } + + if (!rememberPacket(mp.from, mp.id)) { + LOG_DEBUG("Drop duplicate BLE advertisement packet (from=0x%x id=%u)", mp.from, mp.id); + return; + } + + if (router && mp.which_payload_variant == meshtastic_MeshPacket_encrypted_tag) { + mp.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_BLE_ADVERTISEMENT; + UniquePacketPoolPacket p = packetPool.allocUniqueCopy(mp); + p->rx_snr = 0; + p->rx_rssi = 0; + router->enqueueReceivedMessage(p.release()); + } +} + +bool BleAdvertisementMesh::enqueueFrame(const uint8_t *frame, size_t frameLength) +{ + if (!frame || frameLength == 0 || frameLength > BleAdvertisementMeshCodec::MAX_FRAME_SIZE || + queuedFrames >= FRAME_QUEUE_SIZE) { + return false; + } + + QueuedFrame &queued = queue[queueTail]; + memcpy(queued.data, frame, frameLength); + queued.length = frameLength; + queueTail = (queueTail + 1) % FRAME_QUEUE_SIZE; + queuedFrames++; + setIntervalFromNow(0); + return true; +} + +bool BleAdvertisementMesh::rememberPacket(uint32_t from, uint32_t packetId) +{ + for (uint8_t i = 0; i < RECENT_PACKET_COUNT; i++) { + if (recentFrom[i] == from && recentPacketId[i] == packetId) { + return false; + } + } + + recentFrom[recentPacketIndex] = from; + recentPacketId[recentPacketIndex] = packetId; + recentPacketIndex = (recentPacketIndex + 1) % RECENT_PACKET_COUNT; + return true; +} + +int32_t BleAdvertisementMesh::runOnce() +{ + if (!advertiseFrame || queuedFrames == 0) { + return 1000; + } + + QueuedFrame queued = queue[queueHead]; + queueHead = (queueHead + 1) % FRAME_QUEUE_SIZE; + queuedFrames--; + + if (!advertiseFrame(queued.data, queued.length)) { + LOG_WARN("BLE advertisement frame transmit failed"); + } + + return queuedFrames > 0 ? FRAME_INTERVAL_MS : 1000; +} + +#endif // HAS_BLE_MESH_ADVERTISING diff --git a/src/mesh/ble/BleAdvertisementMesh.h b/src/mesh/ble/BleAdvertisementMesh.h new file mode 100644 index 00000000000..bde3488da02 --- /dev/null +++ b/src/mesh/ble/BleAdvertisementMesh.h @@ -0,0 +1,114 @@ +#pragma once + +#include "mesh/generated/meshtastic/mesh.pb.h" +#include +#include + +/** + * BLE advertisement bearer for encrypted MeshPacket protobufs. + * + * The platform BLE layer is intentionally kept outside this codec. ESP32 and + * nRF52 implementations can carry these frames in service data, manufacturer + * data, or an extended advertising set without changing router semantics. + */ +class BleAdvertisementMeshCodec +{ + public: + static constexpr uint8_t FRAME_MAGIC_0 = 'M'; + static constexpr uint8_t FRAME_MAGIC_1 = 'T'; + static constexpr uint8_t FRAME_VERSION = 1; + // Legacy BLE advertisements are 31 bytes including AD type overhead. A + // manufacturer-data bearer leaves 27 bytes for our frame after the 16-bit + // company id. + static constexpr size_t MAX_FRAME_SIZE = 27; + static constexpr size_t FRAME_HEADER_SIZE = 16; + static constexpr size_t MAX_FRAGMENT_PAYLOAD_SIZE = MAX_FRAME_SIZE - FRAME_HEADER_SIZE; + static constexpr uint8_t MAX_FRAGMENTS = 64; + + typedef bool (*EmitFrame)(const uint8_t *frame, size_t frameLength, void *context); + + BleAdvertisementMeshCodec(); + + /** + * Encode an encrypted MeshPacket into one or more BLE ADV frames. + */ + static bool forEachFrame(const meshtastic_MeshPacket *mp, EmitFrame emit, void *context); + static uint8_t frameCount(const meshtastic_MeshPacket *mp); + + /** + * Accept one BLE ADV frame. Returns true when a complete MeshPacket has + * been reassembled into out. + */ + bool receiveFrame(const uint8_t *frame, size_t frameLength, meshtastic_MeshPacket *out); + + private: + static constexpr uint8_t REASSEMBLY_CONTEXTS = 4; + + struct ReassemblyContext + { + uint32_t from = 0; + uint32_t packetId = 0; + uint16_t crc = 0; + uint8_t expectedFragments = 0; + uint8_t lastFragmentLength = 0; + uint64_t receivedMask = 0; + uint8_t encodedPacket[meshtastic_MeshPacket_size] = {0}; + }; + + ReassemblyContext contexts[REASSEMBLY_CONTEXTS]; + uint8_t nextContext = 0; + + ReassemblyContext *getContext(uint32_t from, uint32_t packetId, uint16_t crc, uint8_t fragmentCount); + void reset(ReassemblyContext &context, uint32_t from, uint32_t packetId, uint16_t crc, uint8_t fragmentCount); + void clear(ReassemblyContext &context); +}; + +#if HAS_BLE_MESH_ADVERTISING + +#include "MeshTypes.h" +#include "concurrency/OSThread.h" + +class BleAdvertisementMesh : public concurrency::OSThread +{ + public: + typedef bool (*PlatformAdvertiseFrame)(const uint8_t *frame, size_t frameLength); + + struct QueuedFrame + { + uint8_t data[BleAdvertisementMeshCodec::MAX_FRAME_SIZE] = {0}; + uint8_t length = 0; + }; + + explicit BleAdvertisementMesh(PlatformAdvertiseFrame advertiseFrame); + + bool onSend(const meshtastic_MeshPacket *mp); + void onAdvertisement(const uint8_t *frame, size_t frameLength); + + private: + static constexpr uint8_t MAX_PACKET_FRAMES = + (meshtastic_MeshPacket_size + BleAdvertisementMeshCodec::MAX_FRAGMENT_PAYLOAD_SIZE - 1) / + BleAdvertisementMeshCodec::MAX_FRAGMENT_PAYLOAD_SIZE; + static constexpr uint8_t FRAME_REPEAT_ROUNDS = 8; + static constexpr uint16_t FRAME_QUEUE_SIZE = MAX_PACKET_FRAMES * FRAME_REPEAT_ROUNDS; + static constexpr uint32_t FRAME_INTERVAL_MS = 140; + static constexpr uint8_t RECENT_PACKET_COUNT = 8; + + PlatformAdvertiseFrame advertiseFrame = nullptr; + BleAdvertisementMeshCodec codec; + QueuedFrame queue[FRAME_QUEUE_SIZE]; + uint16_t queueHead = 0; + uint16_t queueTail = 0; + uint16_t queuedFrames = 0; + uint32_t recentFrom[RECENT_PACKET_COUNT] = {0}; + uint32_t recentPacketId[RECENT_PACKET_COUNT] = {0}; + uint8_t recentPacketIndex = 0; + + uint16_t freeSlots() const { return FRAME_QUEUE_SIZE - queuedFrames; } + bool enqueueFrame(const uint8_t *frame, size_t frameLength); + bool rememberPacket(uint32_t from, uint32_t packetId); + virtual int32_t runOnce() override; +}; + +extern BleAdvertisementMesh *bleAdvertisementMesh; + +#endif // HAS_BLE_MESH_ADVERTISING diff --git a/src/mesh/ble/BleAdvertisementMeshPlatform.cpp b/src/mesh/ble/BleAdvertisementMeshPlatform.cpp new file mode 100644 index 00000000000..9403f2636d5 --- /dev/null +++ b/src/mesh/ble/BleAdvertisementMeshPlatform.cpp @@ -0,0 +1,164 @@ +#include "configuration.h" +#include "mesh/ble/BleAdvertisementMeshPlatform.h" + +#if HAS_BLE_MESH_ADVERTISING + +#include "DebugConfiguration.h" +#include "mesh/ble/BleAdvertisementMesh.h" + +#if defined(ARCH_ESP32) && defined(NIMBLE_TWO) && defined(CONFIG_BT_NIMBLE_EXT_ADV) && CONFIG_BT_NIMBLE_EXT_ADV && \ + defined(CONFIG_BT_NIMBLE_MAX_EXT_ADV_INSTANCES) && CONFIG_BT_NIMBLE_MAX_EXT_ADV_INSTANCES > 1 +#define BLE_ADV_MESH_ESP32_NIMBLE_BACKEND 1 +#else +#define BLE_ADV_MESH_ESP32_NIMBLE_BACKEND 0 +#endif + +#if BLE_ADV_MESH_ESP32_NIMBLE_BACKEND +#include +#include +#if defined(CONFIG_BT_NIMBLE_ROLE_OBSERVER) && CONFIG_BT_NIMBLE_ROLE_OBSERVER +#include +#include +#endif +#endif +#include +#include + +namespace +{ +constexpr uint16_t BLE_ADV_MESH_COMPANY_ID = BLE_MESH_ADVERTISEMENT_COMPANY_ID; +constexpr uint8_t BLE_ADV_MESH_INSTANCE = 1; +constexpr uint32_t BLE_ADV_MESH_BURST_MS = 120; + +void putLe16(uint8_t *p, uint16_t v) +{ + p[0] = static_cast(v); + p[1] = static_cast(v >> 8); +} + +#if BLE_ADV_MESH_ESP32_NIMBLE_BACKEND + +#if defined(CONFIG_BT_NIMBLE_ROLE_OBSERVER) && CONFIG_BT_NIMBLE_ROLE_OBSERVER +class BleAdvMeshScanCallbacks : public NimBLEScanCallbacks +{ + public: + void onResult(const NimBLEAdvertisedDevice *advertisedDevice) override + { + handleAdvertisement(advertisedDevice); + } + + void onDiscovered(const NimBLEAdvertisedDevice *advertisedDevice) override + { + handleAdvertisement(advertisedDevice); + } + + private: + void handleAdvertisement(const NimBLEAdvertisedDevice *advertisedDevice) + { + if (!advertisedDevice || !advertisedDevice->haveManufacturerData() || !bleAdvertisementMesh) { + return; + } + + uint8_t dataCount = advertisedDevice->getManufacturerDataCount(); + for (uint8_t i = 0; i < dataCount; i++) { + std::string manufacturerData = advertisedDevice->getManufacturerData(i); + if (manufacturerData.size() < 2 + BleAdvertisementMeshCodec::FRAME_HEADER_SIZE) { + continue; + } + + const auto *data = reinterpret_cast(manufacturerData.data()); + uint16_t companyId = static_cast(data[0]) | (static_cast(data[1]) << 8); + if (companyId != BLE_ADV_MESH_COMPANY_ID) { + continue; + } + + bleAdvertisementMesh->onAdvertisement(data + 2, manufacturerData.size() - 2); + } + } +}; + +BleAdvMeshScanCallbacks scanCallbacks; +#endif + +bool advertiseBleAdvMeshFrame(const uint8_t *frame, size_t frameLength) +{ + if (!frame || frameLength == 0 || frameLength > BleAdvertisementMeshCodec::MAX_FRAME_SIZE) { + return false; + } + + uint8_t manufacturerData[2 + BleAdvertisementMeshCodec::MAX_FRAME_SIZE] = {0}; + putLe16(manufacturerData, BLE_ADV_MESH_COMPANY_ID); + memcpy(&manufacturerData[2], frame, frameLength); + + NimBLEExtAdvertisement advertisement; + advertisement.setLegacyAdvertising(true); + advertisement.setConnectable(false); + advertisement.setScannable(false); + advertisement.setMinInterval(160); + advertisement.setMaxInterval(240); + if (!advertisement.setManufacturerData(manufacturerData, frameLength + 2)) { + return false; + } + + NimBLEExtAdvertising *advertising = NimBLEDevice::getAdvertising(); + if (!advertising) { + return false; + } + + advertising->stop(BLE_ADV_MESH_INSTANCE); + advertising->removeInstance(BLE_ADV_MESH_INSTANCE); + return advertising->setInstanceData(BLE_ADV_MESH_INSTANCE, advertisement) && + advertising->start(BLE_ADV_MESH_INSTANCE, BLE_ADV_MESH_BURST_MS, 0); +} + +void startBleAdvMeshScan() +{ +#if defined(CONFIG_BT_NIMBLE_ROLE_OBSERVER) && CONFIG_BT_NIMBLE_ROLE_OBSERVER + NimBLEScan *scan = NimBLEDevice::getScan(); + if (!scan) { + LOG_WARN("BLE advertisement mesh scan unavailable"); + return; + } + + scan->setScanCallbacks(&scanCallbacks, true); + scan->setActiveScan(false); + scan->setInterval(500); + scan->setWindow(80); + scan->setDuplicateFilter(0); + scan->setMaxResults(0); + if (!scan->start(0, false, true)) { + LOG_WARN("BLE advertisement mesh scan start failed"); + } +#else + LOG_WARN("BLE advertisement mesh receive disabled: NimBLE observer role is not enabled"); +#endif +} + +#else + +bool advertiseBleAdvMeshFrame(const uint8_t *, size_t) +{ + LOG_WARN("BLE advertisement mesh transmit disabled on this platform"); + return false; +} + +void startBleAdvMeshScan() +{ + LOG_WARN("BLE advertisement mesh scan disabled on this platform"); +} + +#endif +} // namespace + +void initBleAdvertisementMeshPlatform() +{ + if (bleAdvertisementMesh) { + return; + } + + LOG_INFO("Init BLE advertisement mesh bearer"); + bleAdvertisementMesh = new BleAdvertisementMesh(advertiseBleAdvMeshFrame); + startBleAdvMeshScan(); +} + +#endif // HAS_BLE_MESH_ADVERTISING diff --git a/src/mesh/ble/BleAdvertisementMeshPlatform.h b/src/mesh/ble/BleAdvertisementMeshPlatform.h new file mode 100644 index 00000000000..6b70c386083 --- /dev/null +++ b/src/mesh/ble/BleAdvertisementMeshPlatform.h @@ -0,0 +1,11 @@ +#pragma once + +#ifndef HAS_BLE_MESH_ADVERTISING +#define HAS_BLE_MESH_ADVERTISING 0 +#endif + +#if HAS_BLE_MESH_ADVERTISING + +void initBleAdvertisementMeshPlatform(); + +#endif diff --git a/src/mesh/generated/meshtastic/config.pb.h b/src/mesh/generated/meshtastic/config.pb.h index 820bb276450..7f1b211e347 100644 --- a/src/mesh/generated/meshtastic/config.pb.h +++ b/src/mesh/generated/meshtastic/config.pb.h @@ -171,7 +171,9 @@ typedef enum _meshtastic_Config_NetworkConfig_ProtocolFlags { /* Do not broadcast packets over any network protocol */ meshtastic_Config_NetworkConfig_ProtocolFlags_NO_BROADCAST = 0, /* Enable broadcasting packets via UDP over the local network */ - meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST = 1 + meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST = 1, + /* Enable broadcasting packets over Bluetooth LE advertisements. */ + meshtastic_Config_NetworkConfig_ProtocolFlags_BLE_ADVERTISEMENT_BROADCAST = 2 } meshtastic_Config_NetworkConfig_ProtocolFlags; /* Deprecated in 2.7.4: Unused */ @@ -710,8 +712,8 @@ extern "C" { #define _meshtastic_Config_NetworkConfig_AddressMode_ARRAYSIZE ((meshtastic_Config_NetworkConfig_AddressMode)(meshtastic_Config_NetworkConfig_AddressMode_STATIC+1)) #define _meshtastic_Config_NetworkConfig_ProtocolFlags_MIN meshtastic_Config_NetworkConfig_ProtocolFlags_NO_BROADCAST -#define _meshtastic_Config_NetworkConfig_ProtocolFlags_MAX meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST -#define _meshtastic_Config_NetworkConfig_ProtocolFlags_ARRAYSIZE ((meshtastic_Config_NetworkConfig_ProtocolFlags)(meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST+1)) +#define _meshtastic_Config_NetworkConfig_ProtocolFlags_MAX meshtastic_Config_NetworkConfig_ProtocolFlags_BLE_ADVERTISEMENT_BROADCAST +#define _meshtastic_Config_NetworkConfig_ProtocolFlags_ARRAYSIZE ((meshtastic_Config_NetworkConfig_ProtocolFlags)(meshtastic_Config_NetworkConfig_ProtocolFlags_BLE_ADVERTISEMENT_BROADCAST+1)) #define _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_UNUSED #define _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MAX meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_UNUSED diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h index cb5f19df5a0..e7b1119fe43 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.h +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -621,7 +621,9 @@ typedef enum _meshtastic_MeshPacket_TransportMechanism { /* Arrived via Multicast UDP */ meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MULTICAST_UDP = 6, /* Arrived via API connection */ - meshtastic_MeshPacket_TransportMechanism_TRANSPORT_API = 7 + meshtastic_MeshPacket_TransportMechanism_TRANSPORT_API = 7, + /* Arrived via Bluetooth LE advertisements */ + meshtastic_MeshPacket_TransportMechanism_TRANSPORT_BLE_ADVERTISEMENT = 8 } meshtastic_MeshPacket_TransportMechanism; /* Log levels, chosen to match python logging conventions. */ @@ -1516,8 +1518,8 @@ extern "C" { #define _meshtastic_MeshPacket_Delayed_ARRAYSIZE ((meshtastic_MeshPacket_Delayed)(meshtastic_MeshPacket_Delayed_DELAYED_DIRECT+1)) #define _meshtastic_MeshPacket_TransportMechanism_MIN meshtastic_MeshPacket_TransportMechanism_TRANSPORT_INTERNAL -#define _meshtastic_MeshPacket_TransportMechanism_MAX meshtastic_MeshPacket_TransportMechanism_TRANSPORT_API -#define _meshtastic_MeshPacket_TransportMechanism_ARRAYSIZE ((meshtastic_MeshPacket_TransportMechanism)(meshtastic_MeshPacket_TransportMechanism_TRANSPORT_API+1)) +#define _meshtastic_MeshPacket_TransportMechanism_MAX meshtastic_MeshPacket_TransportMechanism_TRANSPORT_BLE_ADVERTISEMENT +#define _meshtastic_MeshPacket_TransportMechanism_ARRAYSIZE ((meshtastic_MeshPacket_TransportMechanism)(meshtastic_MeshPacket_TransportMechanism_TRANSPORT_BLE_ADVERTISEMENT+1)) #define _meshtastic_LogRecord_Level_MIN meshtastic_LogRecord_Level_UNSET #define _meshtastic_LogRecord_Level_MAX meshtastic_LogRecord_Level_CRITICAL diff --git a/src/nimble/NimbleBluetooth.cpp b/src/nimble/NimbleBluetooth.cpp index d4cb1d9effa..e8851dbf36a 100644 --- a/src/nimble/NimbleBluetooth.cpp +++ b/src/nimble/NimbleBluetooth.cpp @@ -8,6 +8,7 @@ #include "concurrency/OSThread.h" #include "main.h" #include "mesh/PhoneAPI.h" +#include "mesh/ble/BleAdvertisementMeshPlatform.h" #include "mesh/mesh-pb-constants.h" #include "sleep.h" #include @@ -867,6 +868,11 @@ void NimbleBluetooth::setup() bleServer->setCallbacks(serverCallbacks, true); setupService(); startAdvertising(); +#if HAS_BLE_MESH_ADVERTISING + if (config.network.enabled_protocols & meshtastic_Config_NetworkConfig_ProtocolFlags_BLE_ADVERTISEMENT_BROADCAST) { + initBleAdvertisementMeshPlatform(); + } +#endif } void NimbleBluetooth::setupService() diff --git a/src/platform/nrf52/NRF52Bluetooth.cpp b/src/platform/nrf52/NRF52Bluetooth.cpp index 52e45ccccc9..00147858fc7 100644 --- a/src/platform/nrf52/NRF52Bluetooth.cpp +++ b/src/platform/nrf52/NRF52Bluetooth.cpp @@ -5,6 +5,7 @@ #include "PowerFSM.h" #include "configuration.h" #include "main.h" +#include "mesh/ble/BleAdvertisementMeshPlatform.h" #include "mesh/PhoneAPI.h" #include "mesh/mesh-pb-constants.h" #include @@ -343,6 +344,11 @@ void NRF52Bluetooth::setup() // Setup the advertising packet(s) LOG_INFO("Set up the advertising payload(s)"); startAdv(); +#if HAS_BLE_MESH_ADVERTISING + if (config.network.enabled_protocols & meshtastic_Config_NetworkConfig_ProtocolFlags_BLE_ADVERTISEMENT_BROADCAST) { + initBleAdvertisementMeshPlatform(); + } +#endif LOG_INFO("Advertise"); } void NRF52Bluetooth::resumeAdvertising() diff --git a/test/test_ble_advertisement_mesh/test_main.cpp b/test/test_ble_advertisement_mesh/test_main.cpp new file mode 100644 index 00000000000..99f49f24539 --- /dev/null +++ b/test/test_ble_advertisement_mesh/test_main.cpp @@ -0,0 +1,169 @@ +#include "mesh/ble/BleAdvertisementMesh.h" +#include +#include +#include + +namespace +{ +struct Capture +{ + uint8_t frames[BleAdvertisementMeshCodec::MAX_FRAGMENTS][BleAdvertisementMeshCodec::MAX_FRAME_SIZE] = {}; + size_t lengths[BleAdvertisementMeshCodec::MAX_FRAGMENTS] = {}; + size_t count = 0; +}; + +bool captureFrame(const uint8_t *frame, size_t frameLength, void *context) +{ + auto *capture = static_cast(context); + if (capture->count >= BleAdvertisementMeshCodec::MAX_FRAGMENTS) { + return false; + } + memcpy(capture->frames[capture->count], frame, frameLength); + capture->lengths[capture->count] = frameLength; + capture->count++; + return true; +} + +meshtastic_MeshPacket makePacket() +{ + meshtastic_MeshPacket mp = meshtastic_MeshPacket_init_zero; + mp.from = 0x11223344; + mp.to = 0xffffffff; + mp.id = 0x55667788; + mp.channel = 8; + mp.hop_limit = 3; + mp.which_payload_variant = meshtastic_MeshPacket_encrypted_tag; + mp.encrypted.size = 32; + for (uint8_t i = 0; i < mp.encrypted.size; i++) { + mp.encrypted.bytes[i] = i; + } + return mp; +} + +meshtastic_MeshPacket makeMaxEncryptedPacket() +{ + meshtastic_MeshPacket mp = makePacket(); + mp.encrypted.size = sizeof(mp.encrypted.bytes); + for (uint16_t i = 0; i < mp.encrypted.size; i++) { + mp.encrypted.bytes[i] = i & 0xff; + } + return mp; +} +} // namespace + +void test_ble_adv_codec_round_trips_fragmented_mesh_packet() +{ + meshtastic_MeshPacket original = makePacket(); + Capture capture; + + TEST_ASSERT_TRUE(BleAdvertisementMeshCodec::forEachFrame(&original, captureFrame, &capture)); + TEST_ASSERT_GREATER_THAN(1, capture.count); + TEST_ASSERT_EQUAL_UINT8(capture.count, BleAdvertisementMeshCodec::frameCount(&original)); + + BleAdvertisementMeshCodec codec; + meshtastic_MeshPacket decoded = meshtastic_MeshPacket_init_zero; + bool complete = false; + for (size_t i = 0; i < capture.count; i++) { + complete = codec.receiveFrame(capture.frames[i], capture.lengths[i], &decoded); + } + + TEST_ASSERT_TRUE(complete); + TEST_ASSERT_EQUAL_UINT32(original.from, decoded.from); + TEST_ASSERT_EQUAL_UINT32(original.to, decoded.to); + TEST_ASSERT_EQUAL_UINT32(original.id, decoded.id); + TEST_ASSERT_EQUAL_UINT8(original.channel, decoded.channel); + TEST_ASSERT_EQUAL_UINT8(original.hop_limit, decoded.hop_limit); + TEST_ASSERT_EQUAL_UINT8(original.encrypted.size, decoded.encrypted.size); + TEST_ASSERT_EQUAL_UINT8_ARRAY(original.encrypted.bytes, decoded.encrypted.bytes, original.encrypted.size); +} + +void test_ble_adv_codec_round_trips_max_encrypted_mesh_packet() +{ + meshtastic_MeshPacket original = makeMaxEncryptedPacket(); + Capture capture; + + TEST_ASSERT_TRUE(BleAdvertisementMeshCodec::forEachFrame(&original, captureFrame, &capture)); + TEST_ASSERT_GREATER_THAN(8, capture.count); + TEST_ASSERT_LESS_OR_EQUAL(BleAdvertisementMeshCodec::MAX_FRAGMENTS, capture.count); + TEST_ASSERT_EQUAL_UINT8(capture.count, BleAdvertisementMeshCodec::frameCount(&original)); + + BleAdvertisementMeshCodec codec; + meshtastic_MeshPacket decoded = meshtastic_MeshPacket_init_zero; + bool complete = false; + for (size_t i = 0; i < capture.count; i++) { + complete = codec.receiveFrame(capture.frames[i], capture.lengths[i], &decoded); + } + + TEST_ASSERT_TRUE(complete); + TEST_ASSERT_EQUAL_UINT32(original.from, decoded.from); + TEST_ASSERT_EQUAL_UINT32(original.id, decoded.id); + TEST_ASSERT_EQUAL_UINT16(original.encrypted.size, decoded.encrypted.size); + TEST_ASSERT_EQUAL_UINT8_ARRAY(original.encrypted.bytes, decoded.encrypted.bytes, original.encrypted.size); +} + +void test_ble_adv_codec_rejects_crc_mismatch() +{ + meshtastic_MeshPacket original = makePacket(); + Capture capture; + TEST_ASSERT_TRUE(BleAdvertisementMeshCodec::forEachFrame(&original, captureFrame, &capture)); + + capture.frames[capture.count - 1][capture.lengths[capture.count - 1] - 1] ^= 0x55; + + BleAdvertisementMeshCodec codec; + meshtastic_MeshPacket decoded = meshtastic_MeshPacket_init_zero; + bool complete = false; + for (size_t i = 0; i < capture.count; i++) { + complete = codec.receiveFrame(capture.frames[i], capture.lengths[i], &decoded); + } + + TEST_ASSERT_FALSE(complete); +} + +void test_ble_adv_codec_handles_interleaved_packets() +{ + meshtastic_MeshPacket first = makePacket(); + meshtastic_MeshPacket second = makePacket(); + second.from = 0x99aabbcc; + second.id = 0x12345678; + for (uint8_t i = 0; i < second.encrypted.size; i++) { + second.encrypted.bytes[i] = 0xff - i; + } + + Capture firstCapture; + Capture secondCapture; + TEST_ASSERT_TRUE(BleAdvertisementMeshCodec::forEachFrame(&first, captureFrame, &firstCapture)); + TEST_ASSERT_TRUE(BleAdvertisementMeshCodec::forEachFrame(&second, captureFrame, &secondCapture)); + TEST_ASSERT_EQUAL(firstCapture.count, secondCapture.count); + + BleAdvertisementMeshCodec codec; + meshtastic_MeshPacket decoded = meshtastic_MeshPacket_init_zero; + bool sawFirst = false; + bool sawSecond = false; + + for (size_t i = 0; i < firstCapture.count; i++) { + if (codec.receiveFrame(firstCapture.frames[i], firstCapture.lengths[i], &decoded)) { + TEST_ASSERT_EQUAL_UINT32(first.id, decoded.id); + sawFirst = true; + } + if (codec.receiveFrame(secondCapture.frames[i], secondCapture.lengths[i], &decoded)) { + TEST_ASSERT_EQUAL_UINT32(second.id, decoded.id); + sawSecond = true; + } + } + + TEST_ASSERT_TRUE(sawFirst); + TEST_ASSERT_TRUE(sawSecond); +} + +void setup() +{ + delay(10); + UNITY_BEGIN(); + RUN_TEST(test_ble_adv_codec_round_trips_fragmented_mesh_packet); + RUN_TEST(test_ble_adv_codec_round_trips_max_encrypted_mesh_packet); + RUN_TEST(test_ble_adv_codec_rejects_crc_mismatch); + RUN_TEST(test_ble_adv_codec_handles_interleaved_packets); + exit(UNITY_END()); +} + +void loop() {} diff --git a/variants/esp32c6/m5stack_unitc6l/platformio.ini b/variants/esp32c6/m5stack_unitc6l/platformio.ini index bf605ca612d..61540d70337 100644 --- a/variants/esp32c6/m5stack_unitc6l/platformio.ini +++ b/variants/esp32c6/m5stack_unitc6l/platformio.ini @@ -33,8 +33,9 @@ build_flags = -DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_MODE=1 -D HAS_BLUETOOTH=1 - -DCONFIG_BT_NIMBLE_EXT_ADV=1 - -DCONFIG_BT_NIMBLE_MAX_EXT_ADV_INSTANCES=2 + -DCONFIG_BT_NIMBLE_EXT_ADV=1 + -DCONFIG_BT_NIMBLE_MAX_EXT_ADV_INSTANCES=2 + -DCONFIG_BT_NIMBLE_ROLE_OBSERVER=1 -D NIMBLE_TWO monitor_speed=115200 lib_ignore =