I have an Arduino Uno (atmega328p) and this sketch which sends data every 100ms
#include <SoftwareSerial.h>
const int BT_RX_PIN = 15; // SoftwareSerial RX
const int BT_TX_PIN = 17; // SoftwareSerial TX
SoftwareSerial BTserial(BT_RX_PIN, BT_TX_PIN);
const unsigned long btSendIntervalMs = 100;
unsigned long lastBtSendTime = 0;
void setup() {
BTserial.begin(9600);
}
void loop() {
unsigned long currentTime = millis();
if (currentTime - lastBtSendTime >= btSendIntervalMs) {
sendBluetoothData();
lastBtSendTime = currentTime;
}
}
void sendBluetoothData() {
int Rand1 = random(10000);
int Rand2 = random(10000);
BTserial.print('$');
BTserial.print(String(Rand1));
BTserial.print('#');
BTserial.print(String(Rand2));
BTserial.print('&');
BTserial.println(); // Newline at the end
}
Sending two random numbers, not a whole lot of data, right? I'm purposely breaking them up for every print call instead of putting everything into one string.
On my PC, using a Bluetooth 5.0 USB Dongle (RFCOMM), I use PuTTy to connect to the device and be able to actually see what and how fast does the Arduino Sketch send the data. https://youtu.be/Hgb8PcwW7iQ
Looks like it's sending a fairly small amount of bytes without any latency (100ms in this instance, as user-defined)
However, I have my Android application that reads from the Bluetooth BufferedReader
, line by line, and log them into the Debug window. Basically, PuTTy is the emulation for Android here, and on PuTTy, it works just like expected.
This is how I set my Bluetooth connection up:
private static BluetoothAdapter btAdapter;
private static BluetoothDevice btDevice;
private static BluetoothSocket btSocket;
private static InputStream mmInputStream;
private static BufferedReader bufferedReader;
static boolean connect(BluetoothDevice device) {
try{
if(btAdapter == null) {
Toast.makeText(CurrentContext.get(), "Bluetooth Adapter not found...", Toast.LENGTH_LONG).show();
return false;
}
else if(!btAdapter.isEnabled()) {
Toast.makeText(CurrentContext.get(), "Bluetooth Adapter is not enabled...", Toast.LENGTH_LONG).show();
return false;
}
if(btSocket != null && btSocket.isConnected()) {
if(btDevice == null) {
Toast.makeText(CurrentContext.get(), "Bluetooth Device could not be found...", Toast.LENGTH_LONG).show();
return false;
}
else {
Toast.makeText(CurrentContext.get(), "Already connected to: " + btDevice.getName(), Toast.LENGTH_LONG).show();
return false;
}
}
Toast.makeText(CurrentContext.get(), "Connecting. Please wait...", Toast.LENGTH_LONG);
btDevice = device;
if(btDevice == null) {
Toast.makeText(CurrentContext.get(), "Bluetooth Device could not be found...", Toast.LENGTH_LONG).show();
return false;
}
btSocket = btDevice.createRfcommSocketToServiceRecord(CONNECTION_UUID);
if(btSocket == null) {
Toast.makeText(CurrentContext.get(), "Bluetooth Socket could not be found...", Toast.LENGTH_LONG).show();
return false;
}
btAdapter.cancelDiscovery();
btSocket.connect();
mmInputStream = new DataInputStream(btSocket.getInputStream());
bufferedReader = new BufferedReader(new InputStreamReader(mmInputStream, StandardCharsets.UTF_8)); // Ignore error
beginInputStreamListener(); // Start the thread for reading data
}catch(Exception e){
e.printStackTrace();
Toast.makeText(CurrentContext.get(), "Could not connect to: " + device.getName(), Toast.LENGTH_LONG).show();
return false;
}
Toast.makeText(CurrentContext.get(), "Connected to: " + btDevice.getName(), Toast.LENGTH_LONG).show();
return true;
}
And here is the separate thread I call via beginInputStreamListener
, to read data continuously.
I want to avoid using it in the ui thread at all costs.
private static void beginInputStreamListener(){
dataListenerThread = new Thread(new Runnable() {
@Override
public void run() {
dataListenerThreadActive = true; // Just a boolean I can switch to terminate the loop below
while(!Thread.currentThread().isInterrupted() && dataListenerThreadActive) {
try {
if(bufferedReader.ready()) {
String line = bufferedReader.readLine();
if(line != null) {
Log.d("RECEIVED_DATA", line);
}
}
Thread.sleep(10); // Some small delay
} catch (IOException | InterruptedException e) {
Handler mainHandler = new Handler(Looper.getMainLooper());
mainHandler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(CurrentContext.get(), "An unresolved problem occured...", Toast.LENGTH_LONG).show();
}
});
e.printStackTrace();
dataListenerThreadActive = false;
reset();
}
}
}
});
dataListenerThread.start();
}
I've tried different approaches with reading byte-by-byte with .read(), .available(), etc. and those are supposed to work instantaneously either way, but in the end figured since I'm sending line by line, that this might fix the issue still. But it doesn't! For some reason, when I connect the app to my HC-05 (Arduino device), it starts printing pretty fast just like in PuTTy (how I want it to work!), but after like 5 seconds of it clearly working properly, it has some sort of delay, then prints 10 lines in one go, then delays a second or two, then prints 10 or N more instead of continuous fluid logging (Just like in the PuTTy video). This is extremely frustrating, as I'm not streaming 4k videos through this, but sending absolutely petty data that works well on PuTTy.
https://www.youtube.com/watch?v=o7PkR9xZw60 Compare with first video
What is the catch?
UPDATE: I've realized something. First, there was culprit to the first couple of seconds when the Android app reads everything smoothly. That's because I haven't flushed the data for the time Arduino spent sending it through the buffer before being connected to. Either way, when receiving live data, it breaks up into chunks. So, flushing just eliminates the illusion of the first couple of seconds and removes the unwanted data.
Now, to the main point, I've realized that I could make my Android app read the lines in intervals as well. Why? Because when it receives a chunk of say 5 lines, then waits a second or two for another chunk, instead of immediately reading the next line, I can make it read the lines every 100ms and when it finishes, it will receive the next 5 or so lines, and so on.
This works far better, but is still not perfect, because there is still some latency going on.
Setting the interval of reading lines in my Android app equal to the interval of sending each line in Arduino ensures the most stable logging, but there's an issue. After like 5 minutes, the Android app cannot keep up, and is stuck on reading older data, while the newer one is way ahead. This is confirmed by letting it read for 5 minutes, then abruptly shutting off my Arduino, where it will keep reading the filled buffer for a few seconds.
To counter that, I've sacrificed stability and made the interval on the Arduino like 230ms, and the reading interval on Android 200ms, and it always keeps up, but sometimes blocks.
I've introduced a second thread that does that, while the input listener just buffers the queued data.
private static final ConcurrentLinkedQueue<String> dataQueue = new ConcurrentLinkedQueue<>();
private static void beginInputStreamListener(){
dataListenerThread = new Thread(new Runnable() {
@Override
public void run() {
try {
while (mmInputStream.available() > 0) {
mmInputStream.read();
}
} catch (IOException e) {
e.printStackTrace();
}
dataListenerActive.set(true);
byte[] buffer = new byte[1]; // Read ONE byte at a time
StringBuilder lineBuffer = new StringBuilder();
while (!Thread.currentThread().isInterrupted() && dataListenerActive.get()) {
try {
int bytesRead = mmInputStream.read(buffer);
if (bytesRead > 0) {
char c = (char) buffer[0];
if (c == '\n') {
// Complete line received
String line = lineBuffer.toString().trim();
if (!line.isEmpty()) {
String data = line;
dataQueue.offer(data);
}
lineBuffer.setLength(0);
} else if (c != '\r') { // Ignore carriage returns
lineBuffer.append(c);
}
}
// NO Thread.sleep() - hammer the input stream for maximum responsiveness
} catch (IOException e) {
e.printStackTrace();
dataListenerActive.set(false);
dataProcessorActive.set(false);
DisconnectedAbruptly = true;
reset();
}
}
}
});
dataListenerThread.setPriority(Thread.MAX_PRIORITY); // Set to maximum priority!
dataListenerThread.start();
}
private static void beginDataProcessor() {
dataProcessorThread = new Thread(new Runnable() {
@Override
public void run() {
dataProcessorActive.set(true);
while (!Thread.currentThread().isInterrupted() && dataProcessorActive.get()) {
try {
// Get ONE line from queue
String data = dataQueue.poll();
if (data != null) {
Log.d("RD", data);
}
// Wait exactly 100ms
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
}
});
dataProcessorThread.start();
}
https://www.youtube.com/watch?v=s0449nDMctY
This looks better, but in no way can I sell stories how a modern smartphone cannot handle small data in fair intervals. This is very frustrating, and I'm sure that I'm not the only one who came across such. There is still some latency. In the video example the intervals are 200ms for reading, and 230ms for writing. Doing 100/130 gives the same results, up to 500ms of loss. I need it to be as fast as it could be. 500ms is too slow for me if I were to conclude my solution as 500ms.
I am open to more solutions anyone may come up with. This is a temporary workaround, but I do not accept it as the final solution, at least not yet.
UPDATE: Reproducible example (Android Studio required): https://www.dropbox.com/scl/fi/bgwwnNIffuafvjttzc/androidtest.zip?rlkey=S4heegugsyffjGBHe7fqyhl8&st=couw1evw&dl=0
Through extremely rough research, HC-05 works with SPP and Android cannot work with it like a PC software (PuTTy) would. I've tried doing it raw through NDK as well, but the lag keeps appearing due to some internal buffer.
I've tried optimizing my sketch and android code, even like I said used NDK for native bluetooth reading, to no avail. It acts the same, even on 300-500 ms, it stacks up and doesn't do well.
The solution is changing hardware from HC-05 to one that supports BLE and use it from now on. I've tested a BLE device and it works flawlessly, even sends a whole line instead of fragmented, making data parsing much easier.
In short: To fluidly transfer small data as fast as possible, BLE is needed instead of old outdated hardware.