Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 96 additions & 69 deletions src/modules/BitChatBLEBridge.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,24 @@ static BitChatBLEBridge* activeBridge = nullptr;
*/
class BitChatBLECharacteristicCallbacks : public NimBLECharacteristicCallbacks {
public:
#ifdef NIMBLE_TWO
void onWrite(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo) override {
if (activeBridge) {
auto value = pCharacteristic->getValue();
activeBridge->onBitChatWrite(reinterpret_cast<const uint8_t*>(value.data()), value.length(), connInfo.getConnHandle());
}
}
#else
void onWrite(NimBLECharacteristic* pCharacteristic) override {
if (activeBridge) {
auto value = pCharacteristic->getValue();
activeBridge->onBitChatWrite(reinterpret_cast<const uint8_t*>(value.data()), value.length());
// On older NimBLE, connection handle isn't provided directly in onWrite.
// Workaround: Use the tracked handle if there is exactly one connection.
uint16_t handle = activeBridge->getSingleConnectionHandle();
activeBridge->onBitChatWrite(reinterpret_cast<const uint8_t*>(value.data()), value.length(), handle);
}
}
#endif

void onRead(NimBLECharacteristic* pCharacteristic) override {
LOG_INFO("BitChat BLE: Characteristic READ by client");
Expand All @@ -37,13 +49,17 @@ class BitChatBLECharacteristicCallbacks : public NimBLECharacteristicCallbacks {

void onSubscribe(NimBLECharacteristic* pCharacteristic, ble_gap_conn_desc* desc, uint16_t subValue) override {
if (subValue == 1) {
LOG_INFO("BitChat BLE: Client SUBSCRIBED to notifications");
LOG_INFO("BitChat BLE: Client SUBSCRIBED to notifications (handle=%d)", desc->conn_handle);
if (activeBridge) {
// Track this connection
activeBridge->addConnection(desc->conn_handle);
activeBridge->onBitChatConnect();
}
} else if (subValue == 0) {
LOG_INFO("BitChat BLE: Client UNSUBSCRIBED from notifications");
LOG_INFO("BitChat BLE: Client UNSUBSCRIBED from notifications (handle=%d)", desc->conn_handle);
if (activeBridge) {
// Remove tracked connection
activeBridge->removeConnection(desc->conn_handle);
activeBridge->onBitChatDisconnect();
}
}
Expand All @@ -65,6 +81,10 @@ class BitChatBLEServerCallbacks : public NimBLEServerCallbacks {
void onDisconnect(NimBLEServer* pServer) override {
LOG_INFO("BitChat BLE: Client DISCONNECTED from BitChat service");
if (activeBridge) {
// Check connected count to clean up
if (pServer->getConnectedCount() == 0) {
activeBridge->clearConnections();
}
activeBridge->onBitChatDisconnect();
}
}
Expand All @@ -89,7 +109,7 @@ static BitChatBLEBridge* activeBridge = nullptr;
void bitchat_characteristic_write_callback(uint16_t conn_hdl, BLECharacteristic* chr, uint8_t* data, uint16_t len)
{
if (activeBridge) {
activeBridge->onBitChatWrite(data, len);
activeBridge->onBitChatWrite(data, len, conn_hdl);
}
}

Expand Down Expand Up @@ -366,18 +386,24 @@ void BitChatBLEBridge::stopAdvertising()
}

void BitChatBLEBridge::broadcastMessage(const BitChatMessage& msg)
{
// Broadcast sends to ALL connected devices (no handle filtering)
unicastMessage(0xFFFF, msg);
}

void BitChatBLEBridge::unicastMessage(uint16_t connHandle, const BitChatMessage& msg)
{
if (!serviceActive) {
LOG_WARN("BitChat BLE: Cannot broadcast - service not active");
LOG_WARN("BitChat BLE: Cannot send - service not active");
return;
}

// Serialize BitChat message once
uint8_t buffer[BITCHAT_HEADER_SIZE + BITCHAT_MAX_PAYLOAD_SIZE];
uint8_t buffer[BITCHAT_MAX_MESSAGE_SIZE];
size_t messageSize = BitChatProtocolHandler::serializeMessage(msg, buffer, sizeof(buffer));

if (messageSize == 0) {
LOG_ERROR("BitChat BLE: Failed to serialize message for broadcast");
LOG_ERROR("BitChat BLE: Failed to serialize message for send");
return;
}

Expand All @@ -386,61 +412,72 @@ void BitChatBLEBridge::broadcastMessage(const BitChatMessage& msg)

try {
if (!bitchatCharacteristic) {
LOG_WARN("BitChat BLE: Cannot broadcast - characteristic not initialized");
LOG_WARN("BitChat BLE: Cannot send - characteristic not initialized");
return;
}
// Set characteristic value and notify (Peripheral role - to connected centrals)
// This ensures the announcement is available even if the client doesn't subscribe to notifications

// Set characteristic value (for Read requests)
bitchatCharacteristic->setValue(buffer, messageSize);
bitchatCharacteristic->notify();

LOG_DEBUG("BitChat BLE: ESP32 broadcasted message type=0x%02x, %d bytes (set value + notify)", msg.type, messageSize);
// Send Notification
if (connHandle == 0xFFFF) {
// Broadcast to all subscribed clients
bitchatCharacteristic->notify();
LOG_DEBUG("BitChat BLE: ESP32 broadcasted message type=0x%02x, %d bytes", msg.type, messageSize);
} else {
// Unicast to specific connection
#ifdef NIMBLE_TWO
bitchatCharacteristic->notify(buffer, messageSize, true, connHandle);
LOG_DEBUG("BitChat BLE: ESP32 unicast to handle %d, %d bytes", connHandle, messageSize);
#else
// Old API doesn't support targeted notify easily?
// Fallback to broadcast or check if we can filter
// NimBLECharacteristic::notify() usually sends to all subscribed.
// If we can't target, we must broadcast.
bitchatCharacteristic->notify();
LOG_WARN("BitChat BLE: ESP32 unicast fallback to broadcast (old API)");
#endif
}

} catch (const std::exception& e) {
LOG_ERROR("BitChat BLE: Exception during message broadcast: %s", e.what());
LOG_ERROR("BitChat BLE: Exception during message send: %s", e.what());
}

#elif defined(ARCH_NRF52)
// Broadcast on ALL links (like iOS and Android do):
// 1. Peripheral role: notify subscribed centrals (devices connected to us)
// 2. Central role: write to connected peripherals (devices we're connected to)

// 1. Send via Peripheral role (notify subscribed centrals)
if (!bitchatCharacteristic) {
LOG_WARN("BitChat BLE: Cannot broadcast - characteristic not initialized");
LOG_WARN("BitChat BLE: Cannot send - characteristic not initialized");
return;
}

// BLE notifications are limited by MTU (MTU - 3 bytes for ATT header)
// BLE notifications are limited by MTU
size_t peripheralLimit = getPeripheralNotificationLimit();

if (messageSize > peripheralLimit) {
// Message is too large for a single notification
// Skip notification and send via Central write instead
// Android should buffer Central writes, allowing large messages to be received
LOG_WARN("BitChat BLE: Message %d bytes > %zu (peripheral MTU payload limit), skipping notify",
messageSize, peripheralLimit);
LOG_WARN("BitChat BLE: Android should receive this via Central write (if connected)");
// Don't send via notify - let Central write handle it below
// We can't send this large message via notify.
// Android/iOS might read it if we set value, but notify is how push works.
// Fragmentation should have handled this?
// No, BitChat fragmentation is for Mesh. BLE MTU is separate.
// But we can't easily fragment at BLE level without a protocol.
// We just drop/warn for now if it exceeds MTU.
} else {
// Message fits in single notification - send directly
bitchatCharacteristic->write(buffer, messageSize);
bool notifyResult = bitchatCharacteristic->notify(buffer, messageSize);
if (notifyResult) {
LOG_DEBUG("BitChat BLE: Sent via Peripheral notify, %d bytes", messageSize);

if (connHandle == 0xFFFF) {
// Broadcast
bool notifyResult = bitchatCharacteristic->notify(buffer, messageSize);
LOG_DEBUG("BitChat BLE: nRF52 broadcast result=%d", notifyResult);
} else {
LOG_DEBUG("BitChat BLE: notify() returned false (client may not have enabled notifications yet), %d bytes", messageSize);
// Unicast using notify(conn_hdl, ...)
bool notifyResult = bitchatCharacteristic->notify(connHandle, buffer, messageSize);
LOG_DEBUG("BitChat BLE: nRF52 unicast to %d result=%d", connHandle, notifyResult);
}
}

#endif

#ifdef ARCH_NRF52
LOG_DEBUG("BitChat BLE: nRF52 broadcasted message type=0x%02x, %d bytes", msg.type, messageSize);
#endif
}

void BitChatBLEBridge::onBitChatWrite(const uint8_t* data, size_t length)
void BitChatBLEBridge::onBitChatWrite(const uint8_t* data, size_t length, uint16_t connHandle)
{
uint32_t currentTime = millis();

Expand All @@ -451,17 +488,23 @@ void BitChatBLEBridge::onBitChatWrite(const uint8_t* data, size_t length)
}

// Check if this looks like the START of a new message (has valid header)
// If the incoming data has a valid BitChat header (length >= 12 bytes),
// and we already have buffered data, this is a NEW message
if (length >= BITCHAT_HEADER_SIZE && writeBufferOffset > 0) {
// We check V1 (13) and V2 (16) minimums.
size_t minHeader = BITCHAT_HEADER_SIZE_V1;

if (length >= minHeader && writeBufferOffset > 0) {
// This looks like a new message header while we have buffered data
// Discard the old buffer and start fresh with this new message
LOG_WARN("BitChat BLE: New message header detected, discarding %d buffered bytes", writeBufferOffset);
writeBufferOffset = 0;
}

// Try to parse directly first
LOG_DEBUG("BitChat BLE: Received data %d bytes", length);
LOG_INFO("BitChat BLE: Received data %d bytes from handle %d", length, connHandle);
if (length >= 16) {
LOG_INFO("Raw Header: %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x",
data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
data[8], data[9], data[10], data[11], data[12], data[13], data[14], data[15]);
}
BitChatMessage msg;
if (BitChatProtocolHandler::parseMessage(data, length, msg)) {
// Success! Complete message received in one write
Expand All @@ -470,13 +513,13 @@ void BitChatBLEBridge::onBitChatWrite(const uint8_t* data, size_t length)
// Validate message
if (!BitChatProtocolHandler::validateMessage(msg)) {
LOG_WARN("BitChat BLE: Invalid message received via BLE (type=0x%02x)", msg.type);
// Message was parsed but invalid - just drop it
return;
}

// Forward to bridge module for processing
if (bitchatBridgeModule) {
bitchatBridgeModule->processBitChatMessage(msg, true); // fromBLE = true
// Queue for processing in main thread
bitchatBridgeModule->queueMessageForProcessing(msg, true, connHandle);
}
return;
}
Expand All @@ -486,13 +529,11 @@ void BitChatBLEBridge::onBitChatWrite(const uint8_t* data, size_t length)

// Check if adding this would overflow
if (writeBufferOffset + length > sizeof(writeBuffer)) {
LOG_ERROR("BitChat BLE: Buffer would overflow (%d + %d > %d), discarding buffer and starting fresh",
LOG_ERROR("BitChat BLE: Buffer would overflow (%d + %d > %d), discarding buffer",
writeBufferOffset, length, sizeof(writeBuffer));
writeBufferOffset = 0;

// If this single write is too large, just drop it
if (length > sizeof(writeBuffer)) {
LOG_ERROR("BitChat BLE: Single write too large (%d bytes), dropping", length);
return;
}
}
Expand All @@ -507,41 +548,26 @@ void BitChatBLEBridge::onBitChatWrite(const uint8_t* data, size_t length)
// Success!
LOG_INFO("BitChat BLE: Reassembled message from %d bytes", writeBufferOffset);

// Calculate actual message size for buffer cleanup
bool hasRecipient = (msg.flags & BITCHAT_FLAG_HAS_RECIPIENT) != 0;
bool hasSignature = (msg.flags & BITCHAT_FLAG_HAS_SIGNATURE) != 0;
size_t messageSize = BITCHAT_HEADER_SIZE + 8 + (hasRecipient ? 8 : 0) + msg.payloadLength + (hasSignature ? BITCHAT_SIGNATURE_SIZE : 0);
// Calculate message size
// We can't rely on fixed calculation since header is variable.
// But parseMessage succeeded, so we know it's valid.
// We should clear the buffer.

// Validate message
if (!BitChatProtocolHandler::validateMessage(msg)) {
LOG_WARN("BitChat BLE: Invalid message received via BLE (type=0x%02x)", msg.type);
// Skip past this invalid message if there's more data
if (messageSize < writeBufferOffset) {
size_t remainingBytes = writeBufferOffset - messageSize;
memmove(writeBuffer, writeBuffer + messageSize, remainingBytes);
writeBufferOffset = remainingBytes;
LOG_DEBUG("BitChat BLE: Skipped invalid message, %d bytes remaining", writeBufferOffset);
// Try parsing again
if (BitChatProtocolHandler::parseMessage(writeBuffer, writeBufferOffset, msg)) {
if (BitChatProtocolHandler::validateMessage(msg) && bitchatBridgeModule) {
bitchatBridgeModule->queueMessageForProcessing(msg, true);
}
writeBufferOffset = 0;
}
} else {
writeBufferOffset = 0;
}
// Reset buffer
writeBufferOffset = 0;
return;
}

writeBufferOffset = 0;

// Queue message for deferred processing in main loop (avoid stack overflow in callback)
if (bitchatBridgeModule) {
bitchatBridgeModule->queueMessageForProcessing(msg, true); // fromBLE = true
bitchatBridgeModule->queueMessageForProcessing(msg, true, connHandle);
}
} else {
// Still incomplete, wait for more data
// Still incomplete
LOG_DEBUG("BitChat BLE: Waiting for more data (buffered %d bytes)", writeBufferOffset);
}
}
Expand All @@ -556,7 +582,8 @@ void BitChatBLEBridge::onBitChatConnect()
{
// Send an immediate announcement via Peripheral notify when Android/iOS connects
if (bridgeModule) {
bridgeModule->sendPeerAnnouncement();
// Request announcement from main thread to avoid stack overflow in BLE callback
bridgeModule->requestPeerAnnouncement();
}
}

Expand Down
Loading