ESP32: BT Serial, how to do handshaking from the receiver side?

RetepV
Posts: 2
Joined: Fri Oct 31, 2025 4:48 pm

ESP32: BT Serial, how to do handshaking from the receiver side?

Postby RetepV » Tue Nov 18, 2025 7:14 pm

I have made a device with on an ESP32 which has an RS232 serial port that's connected to a device (an old computer in this case). Serial connection with the old computer works great.

In the ESP32, I also offer a Bluetooth Serial (SPP) service, to which I can connect with a PC. In my ESP32, I then proxy the data received from SPP connection to the RS232 port, and vice versa.

This works fine in general, except for one thing. The old computer is very slow (4800 baud max), and does not have handshaking (not even xon/xoff).

So I am trying to pace the transfer by introducing a short pause after each received character. For every normal character, I pause 60ms, for every carriage return (CR, hex 0x0D), I pause 500ms. The old computer is programmed in BASIC, and needs extra time to process what it received, after it receives a CR.

However, on the Bluetooth side, the sender just keeps sending at its maximum rate. And after a while internal buffers start to overflow and the ESP32 starts to send garbage to the old computer.

I don't see any way in the SPP API to tell the Bluetooth sender to wait a while until my internal buffers are cleared again (i.e. handshaking with the sender). I have implemented queues with low water/high water marks, and try to send the Bluetooth sender the XOFF character when the high water mark is reached (80% of buffer is full), to have it pause until the low water mark is reached (20% buffer is full) and I send XON again. But it doesn't seem to work, the sender just continues sending. My queues are currently 1024 bytes

I might be doing something wrong, overcomplicating it. I do see the ESP_SPP_CONG_EVT, so I assume that the stack actually handles congestion itself. Am I using it wrong?

My code is actually using the BluetoothSerial library and registered for the callback. So basically my code always reads what is being sent by the sender, as it reads it on the ESP_SPP_DATA_IND_EVT event.

Maybe I should simply not register for that event and just read the data in a loop and stop reading when my internal buffers are full? I just need a bit of tips and pointers.

RetepV
Posts: 2
Joined: Fri Oct 31, 2025 4:48 pm

Re: ESP32: BT Serial, how to do handshaking from the receiver side?

Postby RetepV » Wed Nov 19, 2025 9:51 am

I forgot to say something important: I am developing in the Arduino environment with arduino-esp32 version 2.0.17.

This is because I am using the FabGL library, which cannot run with the latest arduino-esp32 version 3.3.4 because that uses too much memory.

After some searching, it looks like that's using esp-idf version 4.4.7.
Last edited by RetepV on Wed Nov 19, 2025 10:06 am, edited 1 time in total.

RetepV
Posts: 2
Joined: Fri Oct 31, 2025 4:48 pm

Re: ESP32: BT Serial, how to do handshaking from the receiver side?

Postby RetepV » Wed Nov 19, 2025 10:33 am

A followup after digging through the Programming Guide for ideas. I basically want to try to not pull data from Bluedroid's queue, so that the queues fill up and Bluedroid might pause the sender automatically. For that, I don't want to read the data through the ESP_SPP_DATA_IND_EVT callback event, but call some read function on a timer.

So, in the documentation, it states that I should be able to pass modes SPP, ESP_SPP_MODE_CB or ESP_SPP_MODE_VFS to esp_spp_init(). However, enum esp_spp_mode_t only contains ESP_SPP_MODE_CB and ESP_SPP_MODE_VFS.

So there's an issue in the documentation. I check v5.5.1 documentation, and it has the same issue. I think that should be fixed.

I'm using ESP_SPP_MODE_CB, and wanted to use mode SPP in the hopes it would give me more control as a data consumer.

There's also no esp_spp_read() function exposed by the API (although I guess it exists somewhere), so that way seems to be blocked. How can I pause the sender in a nice way?

I am now thinking to stream received data to flash memory, and send it through the RS232 at its own pace. That way I have temporary storage for at least 9MB of data.

But I'd rather just pause the sender when necessary. It saves me a bunch of coding. And also, flash memory has a limited lifetime and it would make my device have a limited lifetime.

RetepV
Posts: 2
Joined: Fri Oct 31, 2025 4:48 pm

Re: ESP32: BT Serial, how to do handshaking from the receiver side?

Postby RetepV » Fri Dec 12, 2025 1:43 pm

This post is not very popular. :'( I am still coming up empty handed. One thing that makes this issue extremely hard to debug, is that my application used up all of the GPIO pins on the ESP32. So I cannot connect a JTAG and use a debugger.

Doing some deeper digging, I found out that the rfcomm protocol is credit based. The sender will keep sending as long as the receiver gives it credits. In btc_spp.c, I found that where BTA_JV_RFCOMM_DATA_IND_EVT: is handled (btc_spp_cb_handler), credits are being handed out to the sender (BTA_JvRfcommFlowControl).

If I understand that code right, then there IS some handshaking going on. The amount of credits given is related to the amount of slots free in the receive buffers. So if the receive buffers are full, the sender should not receive any new credits and should pause sending. But this does not seem to properly work in my application.

I am trying to make sense of this in my head...

1)

In the function where 'BTA_JV_RFCOMM_DATA_IND_EVT:' is handled, received data is being consumed from slot->rx.queue. If there is data, then it is sent to the api caller via the callback. A count is being kept, that counts the number of queue items consumed, and which corresponds to the number of times the callback was called. Consuming and calling callbacks is done in a loop (while(1)).

If all the current queue items have been consumed, the loop exits. And as the 'count' represents the number of items consumed from the queue, that count is sent as credits to the sender, so it will send the next batch.

This basically means that btc_spp will happily consume everything it can, as fast as it can, and force it down the throat of the API caller, which has no chance at all of saying 'stop!'. That could have been done a bit better...

In case of mode ESP_SPP_MODE_CB, the API caller cannot do *anything* to control the sender. Even if it does not implement the callback, the event handler in btc_spp will still happily consume everything, will just not call the callback, and the API caller will simply not have any data.

2)

There's also the consideration the of ESP_SPP_MODE_VFS case. For this mode I would have expected that it would actually work (but I tried, and it doesn't). If I wouldn't call spp_vfs_read(), the queue should fill up to the maximum, and the number of credits would go down to 0, pausing the sender. This would give the API caller some kind of control: if it can't read fast enough, the sender will be paused.

But looking into the code that calls the callback, the BTA_JV_RFCOMM_DATA_IND_EVT event is still being handled for ESP_SPP_MODE_VFS: the queue is still being consumed. Is this maybe a bug in the API, a missing check for ESP_SPP_MODE_CB?
Last edited by RetepV on Fri Dec 12, 2025 2:26 pm, edited 1 time in total.

Who is online

Users browsing this forum: ChatGPT-User, Google [Bot] and 3 guests