32feet icon indicating copy to clipboard operation
32feet copied to clipboard

Unable to receive data on Android using BluetoothClient and BluetoothListener

Open Speedberg opened this issue 1 year ago • 14 comments

I've been unable to receive any data on my Android device, regardless if I use BluetoothClient or BluetoothListener.

When using BluetoothClient, the Android device is able to successfully connect and send data to my PC (which is using BluetoothListener) - however, any data sent from my PC to the Android device is not received.

When using BluetoothListener, my PC (using BluetoothClient) is able to successfully connect to the Android device - however, the Android device again doesn't receive any data, and also listener.Pending() always returns false.

Android version: 13 Desktop OS: Windows InTheHand.Net.Bluetooth: 4.0.36

Any help would be greatly appreciated :)

Speedberg avatar Aug 20 '23 16:08 Speedberg

Can you please share your code to replicate the issue?

peterfoot avatar Aug 20 '23 17:08 peterfoot

Here's the code for the client and the server:

using InTheHand.Net;
using System.Threading;
using System.Threading.Tasks;

namespace Pen.Net;

public sealed class BluetoothClient : BluetoothPeer, IClient
{
    public IConnection Connection => _connection;

    public bool IsConnected => _connection?.IsConnected ?? false;

    private BluetoothConnection _connection;

    public bool Connect(object address, object service)
    {
        _connection = new BluetoothConnection(this);
        var task = Task.Run(async () => await _connection.Connect((BluetoothAddress)address, Bluetooth.Service));
        task.Wait();
        return _connection.IsConnected;
    }

    public void Disconnect()
    {
        if (!IsConnected)
            return;
        _connection.Close();
    }

    public override void Poll()
    {
        //Receive new data from the server
        _connection.Receive();

        if(_connection.HasTimedOut)
        {
            Logging.Logger.Debug("Disconnecting!");
            OnDisconnected(_connection, DisconnectReason.TimedOut);
            Disconnect();
        }
    }

    public void ResetHearbeat()
    {
        _connection.ResetTimeOut();
    }
}
using System.Net;
using System.Net.Sockets;

using InTheHand.Net;
using InTheHand.Net.Bluetooth;
using InTheHand.Net.Sockets;

namespace Pen.Net;

public sealed class BluetoothServer : BluetoothPeer, IServer
{
    private bool _isRunning;
    private BluetoothListener _listener;
    private BluetoothConnection _connection;

    public bool IsRunning => _isRunning;

    public event EventHandler<IConnection> OnConnection;
    public event EventHandler<IConnection> OnDisconnection;

    public BluetoothServer()
    {
        //BluetoothService.SerialPort
        _listener = new BluetoothListener(Bluetooth.Service);
        _isRunning = false;
        _connection = null;
    }

    public void Start(object address, object service)
    {
        if(_isRunning)
            Stop();

        try
        {
            _listener.Start();
            _isRunning = true;
        } catch(System.Exception e)
        {
            Logging.Logger.Error("[Bluetooth] Failed to start server. Reason: {0}", e);
        }
    }

    public void Stop()
    {
        if(!_isRunning)
            return;

        _listener.Stop();
        _isRunning = false;
        _connection = null;
    }

    public override void Poll()
    {
        if(!_isRunning)
            return;

        //Accept new connections
        Accept();

        //Receive data
        if(_connection != null)
        {
            //_connection.Receive();

            if(_connection.HasTimedOut)
            {
                Logging.Logger.Debug("Timed out!");
                Close(_connection);
                OnDisconnected(_connection, DisconnectReason.TimedOut);
                Stop();
            }
        }
    }

    public void Close(IConnection connection)
    {
        _connection.Close();
    }

    private void Accept()
    {
        try
        {
            if(_listener.Pending())
            {
                Pen.Logging.Logger.Debug("[Bluetooth] Accepted connection!");

                if(_connection == null)
                {
                    _connection = new BluetoothConnection(_listener.AcceptBluetoothClient(), this);
                    OnConnection?.Invoke(this,_connection);
                }
            }
        } catch(System.Exception e)
        {
            Pen.Logging.Logger.Error("[Bluetooth] Server failed to accept connection: {0}", args: e);
        }
    }

    public void SendDataToAll(byte[] data)
    {
        if(_connection == null)
            return;
        _connection.Send(data);
    }

    public void ResetHeartbeat(IConnection connection)
    {
        if(connection == null)
            return;

        connection.ResetTimeOut();
    }
}
using System.Net;
using System.Net.Sockets;

using InTheHand.Net.Bluetooth;
using InTheHand.Net;
using InTheHand.Net.Sockets;

using BClient = InTheHand.Net.Sockets.BluetoothClient;
using System.Text;

namespace Pen.Net;

public sealed class BluetoothConnection : IConnection
{
    public IPEndPoint EndPoint => throw new NotImplementedException();

    public bool IsConnected => _client?.Connected ?? false;

    public bool HasTimedOut => (DateTime.UtcNow - _lastHearbeat).TotalMilliseconds > 10000;

    private BClient _client;
    private NetworkStream _stream;

    private ushort _packetLength;
    private byte[] _lengthBuffer = new byte[Packet.HeaderSize];

    private byte[] _receiveBuffer = new byte[100];
    private int _receiveBufferSize = 100;
    private byte[] _sendBuffer = new byte[Packet.MaximumSize + Packet.HeaderSize];

    private DateTime _lastHearbeat;
    private BluetoothPeer _peer;

    public BluetoothConnection(BluetoothPeer peer)
    {
        try
        {
            _client = new BClient();
            _peer = peer;
        }
        catch(System.Exception e)
        {
            Logging.Logger.Debug("BC Error: {0}", e);
        }
    }

    public BluetoothConnection(BClient client, BluetoothPeer peer)
    {
        _client = client;
        _stream = _client.GetStream();

        _peer = peer;

        _lastHearbeat = DateTime.UtcNow;

        _stream.BeginRead(_receiveBuffer, 0, _receiveBufferSize, ReceiveCallback, null);
    }

    public async Task<bool> Connect(BluetoothAddress address, Guid service)
    {
        await _client.ConnectAsync(address, service);
        await Task.Delay(1000);
        Logging.Logger.Debug("Machine name: {0}",_client.RemoteMachineName);
        _stream = _client.GetStream();

        //TODO - throw error if Stream or socket is null
        

        _lastHearbeat = DateTime.UtcNow;

        _stream.BeginRead(_receiveBuffer, 0, _receiveBufferSize, ReceiveCallback, null);

        return _client.Connected;
    }

    public void Close()
    {
        _client.Dispose();
        _client = null;
    }

    private void ReceiveCallback(IAsyncResult result)
    {
        Logging.Logger.Debug("[BT] Received callback!");
        if(!IsConnected)
        {
            try
            {
                int count = _stream.EndRead(result);
                Logging.Logger.Debug("[BT] Bytes: {0}", count);
            } catch(System.Exception e)
            {

            }
            return;
        }

        try
        {
            int byteCount = _stream.EndRead(result);
            if (byteCount <= 0)
            {
                _peer.OnDisconnected(this, DisconnectReason.Disconnected);
                return;
            }

            Logging.Logger.Debug("[BT] Received: {0}", byteCount);

            byte[] data = new byte[byteCount];
            Array.Copy(_receiveBuffer, data, byteCount);

            _peer.OnDataReceived(data, this);

            _stream.BeginRead(_receiveBuffer, 0, _receiveBufferSize, ReceiveCallback, null);
        }
        catch (SocketException ex)
        {
            CheckForDisconnect(ref ex);
            Pen.Logging.Logger.Error($"[Bluetooth] Stream error: {ex}");
        }
        catch(System.Exception e)
        {
            Pen.Logging.Logger.Error($"[Bluetooth] Error when receiving data: {e}");
        }
    }

    private void SendCallback(IAsyncResult result)
    {
        try
        {
            //Logging.Logger.Debug("Send callback!");
            _stream.EndWrite(result);
        }
        catch (SocketException ex)
        {
            //CheckForDisconnect(ref ex);
            Pen.Logging.Logger.Error($"[Bluetooth] Send error: {ex}");
        }
        catch(System.Exception e)
        {
            Pen.Logging.Logger.Error($"[Bluetooth] Error when sending data: {e}");
        }
    }


    public void Receive()
    {
#if __ANDROID__
        if(_stream.DataAvailable)
        {
            int bytes = _stream.Read(_receiveBuffer, 0, _receiveBufferSize);
            Logging.Logger.Debug("Bytes: {0}", bytes);
        }
        //return;
#endif
    }

    public void Send(byte[] data)
    {
        if(!IsConnected)
            return;
        try
        {
            //Array.Copy(BitConverter.GetBytes((ushort)data.Length),_sendBuffer,Packet.HeaderSize);
            //Array.Copy(data, 0, _sendBuffer, Packet.HeaderSize, data.Length);
            Logging.Logger.Debug("[BT] Sent: {0}", data.Length);
            _stream.BeginWrite(data, 0, data.Length, SendCallback, null);
        }catch(SocketException ex)
        {
            CheckForDisconnect(ref ex);
        }catch(System.Exception e)
        {
            Logging.Logger.Error($"[Bluetooth] Failed to send data. Reason: {e}");
        }
    }

    private void CheckForDisconnect(ref SocketException ex)
    {
        Logging.Logger.Warn("Disconnect error: {0}", ex);
        switch (ex.SocketErrorCode)
        {
            case SocketError.Interrupted:
            case SocketError.NotSocket:
                _peer.OnDisconnected(this, DisconnectReason.TransportError);
                break;
            case SocketError.ConnectionReset:
            case SocketError.ConnectionAborted:
            case SocketError.Disconnecting:
                _peer.OnDisconnected(this, DisconnectReason.Disconnected);
                break;
            case SocketError.TimedOut:
                _peer.OnDisconnected(this, DisconnectReason.TimedOut);
                break;
            default:
                _peer.OnDisconnected(this, DisconnectReason.TransportError);
                break;
        }
    }

    public void ResetTimeOut()
    {
        _lastHearbeat = DateTime.UtcNow;
    }
}

The full project is quite large - I'm happy to create a test repo if needed. Please let me know if you need any additional information.

Speedberg avatar Aug 20 '23 17:08 Speedberg

I've created a minimal test repository which can be found here. There are comments in this file which highlight where the issues occur.

Speedberg avatar Aug 21 '23 17:08 Speedberg

I can see a way to mitigate this in the source but currently if you call GetStream multiple times you'll get different instances of the wrapper AndroidNetworkStream this could cause issues with multiple reads/writes and also if the first goes out of scope and is collected it will dispose the underlying stream. Because the Android API doesn't use .NET Sockets the Client property returns null and this special class is used to return a NetworkStream to appear the same as the Sockets based implementations.

peterfoot avatar Sep 05 '23 08:09 peterfoot

also BluetoothListener.Pending always returns false on Android because there is no underlying API to determine if there is a pending connection. You have to just Accept and wait unfortunately

peterfoot avatar Sep 05 '23 09:09 peterfoot

I see, thank you for letting me know. Is there anything I can do to prevent this from happening? I've tried the following below, but am still unable to read data on my Android device.

using (Stream stream = client.GetStream())
{

    string message = "Hello, world!";
    byte[] buffer = new byte[100];

    while (_keepRunning)
    {
        if (client.Connected)
        {
            await stream.WriteAsync(System.Text.Encoding.UTF8.GetBytes(message));
            //Note: this hangs / blocks on Android
            int read = await stream.ReadAsync(buffer, 0, buffer.Length);

            if (read > 0)
            {
                Logger.Debug("Data read: {0}", read);
            }
        }
        await Task.Delay(10);
    }
}

Speedberg avatar Sep 06 '23 16:09 Speedberg

Have you tried calling Flush/FlushAsync after the WriteAsync?

peterfoot avatar Sep 06 '23 16:09 peterfoot

I tried your suggestion, but unfortunately the issue seems to persist. After calling stream.ReadAsync, the debug messages afterwards are never written to the console.

using (Stream stream = client.GetStream())
{

    string message = "Hello, world!";
    byte[] buffer = new byte[100];

    while (_keepRunning)
    {
        if (client.Connected)
        {
            await stream.WriteAsync(System.Text.Encoding.UTF8.GetBytes(message));
            await stream.FlushAsync();
            Logger.Debug("Data written!");

            //Note: this hangs / blocks on Android
            int read = await stream.ReadAsync(buffer, 0, buffer.Length);
            await stream.FlushAsync();

            if (read > 0)
            {
                Logger.Debug("Data read: {0}", read);
            }

            Logger.Debug("Data read called!");
        }
        await Task.Delay(10);
    }
}

Speedberg avatar Sep 06 '23 18:09 Speedberg

Have you tried reading the DataAvailable or Length property on the network stream before commencing the Read operation? If you request more bytes than are available on Android it will block unless the connection breaks and it will return partial data

peterfoot avatar Sep 07 '23 13:09 peterfoot

Unfortunately this doesn't seem to work either: IsDataAvailable() always returns false, and Length always returns 0. My PC can read any data sent by the Android device, but the Android device still seems to not be receiving anything.

using (Stream stream = client.GetStream())
{

    string message = "Hello, world!";
    byte[] buffer = new byte[100];

    while (_keepRunning)
    {
        if (client.Connected)
        {
            await stream.WriteAsync(System.Text.Encoding.UTF8.GetBytes(message));
            await stream.FlushAsync();
            Logger.Debug("Data written!");
#if __ANDROID__
            Logger.Debug("Android!");
            if(stream.IsDataAvailable() || stream.Length > 0)
            {
                int read = await stream.ReadAsync(buffer, 0, buffer.Length);
                await stream.FlushAsync();

                if (read > 0)
                {
                    Logger.Debug("Data read: {0}", read);
                }
                Logger.Debug("Data read called!");
            }
#endif
        }
        await Task.Delay(10);
    }
}

Speedberg avatar Sep 07 '23 17:09 Speedberg

If the Length returns zero then the data hasn't been sent. Did you add a flush after the write on the PC side too? I think there is some difference here in how Windows buffers the stream versus Android and so adding Flush to both will ensure the data is sent.

peterfoot avatar Sep 07 '23 18:09 peterfoot

I've added the call to Flush() on the PC side, however the issue still seems to persist and Length still returns 0.

PC

public async Task Host()
{
    if(_keepRunning)
        return;
    try
    {
        Logger.Debug("Starting listener...");
        BluetoothListener listener = new BluetoothListener(BluetoothService.SerialPort);
        BluetoothClient client = null;
        Stream stream = null;
        _keepRunning = true;
        listener.Start();

        string message = "Hello, world!";
        byte[] buffer = new byte[100];

        while(_keepRunning)
        {
            if(listener.Pending())
            {
                client = listener.AcceptBluetoothClient();
                stream = client.GetStream();
                Logger.Debug("Accepted client: {0}", client.RemoteMachineName);
            }

            if(client != null)
            {
                if(client.Connected)
                {
                    await stream.WriteAsync(System.Text.Encoding.UTF8.GetBytes(message));
                    await stream.FlushAsync();
                    
                    int read = await stream.ReadAsync(buffer,0,buffer.Length);
                    await stream.FlushAsync();
                    
                    if(read > 0)
                    {
                        Logger.Debug("Data read: {0}", read);
                    }
                }
            }
            await Task.Delay(10);
        }

        Logger.Debug("Stopping listener...");

        listener.Stop();
    } catch(System.Exception e)
    {
        Logger.Error("Server Error: {0}", e);
    }
}

Android

public async Task Client()
{
    if(_keepRunning)
        return;
    try
    {
        Logger.Debug("Picking device...");
        BluetoothDevicePicker picker = new BluetoothDevicePicker();
        var device = await picker.PickSingleDeviceAsync();
        if(device == null)
            return;
        Logger.Debug("Device chosen: {0}",device.DeviceAddress);

        BluetoothClient client = new BluetoothClient();
        Logger.Debug("Client attempting connection...");
        await client.ConnectAsync(device.DeviceAddress, BluetoothService.SerialPort);
        Logger.Debug("Client connection successful!");
        _keepRunning = true;

        using (Stream stream = client.GetStream())
        {

            string message = "Hello, world!";
            byte[] buffer = new byte[100];

            while (_keepRunning)
            {
                if (client.Connected)
                {
                    await stream.WriteAsync(System.Text.Encoding.UTF8.GetBytes(message));
                    await stream.FlushAsync();
                    Logger.Debug("Data written!");
#if __ANDROID__
                    Logger.Debug("Android: {0} {1}", stream.IsDataAvailable(), stream.Length);
                    if(stream.IsDataAvailable() || stream.Length > 0)
                    {
                        int read = await stream.ReadAsync(buffer, 0, buffer.Length);
                        await stream.FlushAsync();

                        if (read > 0)
                        {
                            Logger.Debug("Data read: {0}", read);
                        }
                        Logger.Debug("Data read called!");
                    }
#endif
                }
                await Task.Delay(10);
            }
        }
        Logger.Debug("Client disconnecting...");
        client.Dispose();
    } catch(System.Exception e)
    {
        Logger.Error("Client Error: {0}", e);
    }
}

Speedberg avatar Sep 07 '23 18:09 Speedberg

I'm working on a new sample to show the client and listener and the following works for me on both Windows and Android:-

BluetoothClient client = new BluetoothClient();
client.Connect(device.DeviceAddress, MyServiceUuid);
if(client.Connected)
{
    using (var str = client.GetStream())
    {
        StreamReader r = new StreamReader(str);
        var receivedString = r.ReadLine();
        ReceivedTextLabel.Text = receivedString;

        StreamWriter w = new StreamWriter(str);
        w.WriteLine("Hello Bluetooth server!");
        w.Flush();
        w.Close();
        r.Close();
    }
}
private async Task ListenerTask()
{
    var client = _listener.AcceptBluetoothClient();
    using(var stream = client.GetStream() )
    {
        var sw = new StreamWriter(stream);
        sw.WriteLine("Hello Bluetooth client!");
        sw.Flush();

        await Task.Delay(1000);

        var sr = new StreamReader(stream);
        var receivedString = sr.ReadToEnd();
        Dispatcher.Dispatch(() =>
        {
            ReceivedTextLabel.Text = receivedString;
        });
        

        sr.Close();
        sw.Close();
    }
}

I've used both StreamReader and StreamWriter to transfer text and always call Flush after writing. I'm building it into a more full featured sample a bit like the chat app from the original 32feet samples.

peterfoot avatar Sep 08 '23 13:09 peterfoot

Your code works on my devices too - it seems to be that there is an issue with directly reading bytes from the stream. I'm not quite sure why StreamWriter and StreamReader work but stream.Read doesn't.

Speedberg avatar Sep 08 '23 17:09 Speedberg