Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BluetoothEndPoint does not contain a constructor that takes 2 arguments #75

Closed
soyxan opened this issue Jul 7, 2020 · 34 comments
Closed

Comments

@soyxan
Copy link

soyxan commented Jul 7, 2020

On version 4.0.4 when I code the following line in a Visual Studio 2017 .NET Framework 4.6.1 Windows Forms application:

BluetoothEndPoint blep = new BluetoothEndPoint(BluetoothAddress.Parse("001122334455"), BluetoothService.SerialPort);

I get the error "BluetoothEndPoint does not contain a constructor that takes 2 arguments"

As per the documentation BluetoothEndPoint has constructor that takes 2 or 3 arguments.

Note: I am using 4.0.4 version because 4.0.5 nuget package fails to install as per #74

Thanks!

@peterfoot
Copy link
Member

In the current release the BluetoothEndPoint is not emphasised as the BluetoothClient.Connect(address, service) provides a simpler approach for cross platform use. I'm looking at reintroducing BluetoothEndPoint as a supported alternative but will have to assess how it will be implemented on platform APIs which are not Socket oriented.

@soyxan
Copy link
Author

soyxan commented Jul 9, 2020

What I am missing from BluetoothClient.Connect(address, service) that BluetoothEndPoint has is the possibility of specifying a port number for the connection BluetoothEndPoint(BluetoothAddress, Guid, Int32).
I am having some issues connecting to a HeaadSet (HFP) for getting the battery level and I want to see if specifying the port numbre will help me.

@peterfoot
Copy link
Member

Specifying a port number may be useful, especially for reconnecting to a previously used device but using the service uuid will ensure you're using the correct port for an exposed service. If you can't connect using the Headset profile it may be because the device is already connected or that it uses the Handsfree profile instead.

@soyxan
Copy link
Author

soyxan commented Jul 10, 2020

Ok, so using the guid exposed by the device on BluetoothDeviceInfo.InstalledServices is equivalent to using a port number.

In my case I am able to connect to the device with BluetoothClient.Connect(address, service) but once connected I try to send and/or receive but there is no data on the socket stream, I thought that specifying a port number would help, but now I see that it is pointless.

This is my code:

BluetoothDevicePicker picker = new BluetoothDevicePicker();
BluetoothDeviceInfo device = await picker.PickSingleDeviceAsync();

BluetoothClient cli = new BluetoothClient();
cli.Connect(device.DeviceAddress, /*BluetoothService.SerialPort*/ device.InstalledServices.ElementAt(0));
if (cli.Connected) MessageBox.Show("Connected");

//cli.Authenticate = true;
//cli.Encrypt = true;

NetworkStream stream = cli.GetStream();
stream.Write(System.Text.Encoding.ASCII.GetBytes("\r\n\r\n"), 0, 4);

if (stream.CanRead)
{
    byte[] buff = new byte[1024];
    int n = 0;
    StringBuilder str = new StringBuilder();
    while (stream.DataAvailable)
    {
        n = stream.Read(buff, 0, buff.Length);
        str.AppendFormat("{0}", Encoding.ASCII.GetString(buff, 0, n));
    }
    MessageBox.Show(str.ToString());
}

I am trying to use the RFCOMM service in order to get the battery information from the device (Headset with Bluetooth 4.1) by sending AT commands, similar to what they do in the following python code:

https://github.com/TheWeirdDev/Bluetooth_Headset_Battery_Level/blob/master/bluetooth_battery.py

@soyxan
Copy link
Author

soyxan commented Jul 16, 2020

I have logged the bluetooth communications between and Android device (that can get the battery value from the headset) and the headset. I have opened with WIreShark and it seems that AT commands that inform about the battery are not sent trough RFCOMM protocol, they are sent trough HFP protocol. How can I use the HFP protocol to send/get AT commands with this library?

@peterfoot
Copy link
Member

Handsfree Profile runs over an RFComm connection. All of it's control commands/responses are text based AT commands. To connect to a handsfree device you open an RFComm socket to the BluetoothService.Handsfree service uuid. There is a set of required commands which the handsfree device will send to the phone (Audio Gateway) and you must respond to before you can get indicator values. Both Handsfree and Headset are RFComm based but Handsfree has a lot more functionality and has been updated more recently. You may find however that if the handsfree device is connected and active on the Android device you can't open another connection as the handsfree device may only support a single device at once.

@soyxan
Copy link
Author

soyxan commented Jul 17, 2020

Thanks @peterfoot for your support!

I have extracted the bluetooth log from my Android phone (which is able to get the battery level) to be sure that the headset is using HFP to send AT command with the battery information (AT+IPHONEACCEV), this is one of the packets extracted from the log (with wireshark):

Frame 1122: 38 bytes on wire (304 bits), 38 bytes captured (304 bits)
Encapsulation type: Bluetooth H4 with linux header (99)
Arrival Time: Jul 16, 2020 12:18:16.449232000 Romance Daylight Time
[Time shift for this packet: 0.000000000 seconds]
Epoch Time: 1594894696.449232000 seconds
[Time delta from previous captured frame: 0.001885000 seconds]
[Time delta from previous displayed frame: 0.018263000 seconds]
[Time since reference or first frame: 21.363811000 seconds]
Frame Number: 1122
Frame Length: 38 bytes (304 bits)
Capture Length: 38 bytes (304 bits)
[Frame is marked: False]
[Frame is ignored: False]
Point-to-Point Direction: Received (1)
[Protocols in frame: bluetooth:hci_h4:bthci_acl:btl2cap:btrfcomm:bthfp]
Bluetooth
    [Source: GNAudio_xxxxx (AA:BB:CC:DD:EE:FF)]
    [Destination: SamsungE_xxxxx (FF:EE:DD:CC:BB:AA)]
Bluetooth HCI H4
    [Direction: Rcvd (0x01)]
    HCI Packet Type: ACL Data (0x02)
Bluetooth HCI ACL Packet
Bluetooth L2CAP Protocol
Bluetooth RFCOMM Protocol
    Address: E/A flag: 1, C/R flag: 0, Direction: 0, Channel: 2
    Control: Frame type: Unnumbered Information with Header check (UIH) (0xef), P/F flag: 0
    Payload length: 25
    Frame Check Sequence: 0xbf
Bluetooth HFP Profile
    [Role: HS - Headset (2)]
    AT Stream: AT+IPHONEACCEV=2,1,5,2,0\r
    Command 0: +IPHONEACCEV
        Command Line Prefix: AT
        Command: +IPHONEACCEV (Apple Bluetooth Headset Battery Level Indication)
        Type: Action Command (0x003d)
        Parameters
            Count: 2
            Key: Battery Level (1)
            Value: 5
            Key: Dock State (2)
            Value: 0

And this is the code I am using to get this AT commands from the headset in my Windows C# application using the 32feet (InTheHand.Net.Bluetooth) library. To do so I try to connect to the headset using BluetoothService.Handsfree:

BluetoothDevicePicker picker = new BluetoothDevicePicker();
BluetoothDeviceInfo device = await picker.PickSingleDeviceAsync();
BluetoothClient cli = new BluetoothClient();

//device.SetServiceState(BluetoothService.Handsfree, true);

cli.Connect(device.DeviceAddress, BluetoothService.Handsfree);
if (cli.Connected) MessageBox.Show("OK");

NetworkStream stream;

stream = cli.GetStream();
if (stream.CanRead)
{
    byte[] buff = new byte[1024];
    int n = 0;
    StringBuilder str = new StringBuilder();
    while (stream.DataAvailable)
    {
        n = await stream.ReadAsync(buff, 0, buff.Length);
        str.AppendFormat("{0}", Encoding.ASCII.GetString(buff, 0, n));
    }
    MessageBox.Show(str.ToString());
}

When executing the code it gives the following error in cli.Connect(device.DeviceAddress, BluetoothService.Handsfree):

System.Net.Sockets.SocketException: 'A socket operation failed because the destination host was down'

With cli.Connect(device.DeviceAddress, BluetoothService.SerialPort) it connects (so the host it is not down) but i do not get any reply from the stream.ReadAsync call. I have checked that my PC is the only one connected to the headset.

@peterfoot
Copy link
Member

The headset is definitely paired to the PC? You could try checking the BluetoothDeviceInfo InstalledServices array for the Handsfree uuid but this only returns services which Windows knows about and so on older versions of Windows this won't include Handsfree.

Secondly the command you are using is an Apple specific extension to the Handsfree service so while it will work on many newer devices it's not universally supported. Also you should not simply connect and send this command - there is an expected sequence of commands and responses when the handsfree device and audio gateway (your PC in this case) go through when first connected. This allows both sides to negotiate supported features. Then not only can you get indicator values but also receive notifications when these change. You'll have to refer to the HFP spec which has a flowchart describing this.

@soyxan
Copy link
Author

soyxan commented Jul 17, 2020

The headset is definitely paired to the PC? You could try checking the BluetoothDeviceInfo InstalledServices array for the Handsfree uuid but this only returns services which Windows knows about and so on older versions of Windows this won't include Handsfree.

I am testing on Windows 10 and the Headset is definitely paired and connected before executing my code (i do not know if this is the right way to proceed, as the headset is already connected and I connect again from my code, anyway I tried also to disconnect it from Windows and then execute my code, but I get the same results)

I tried to connect using each one of the 5 InstalledServices I can get from device:

InstalledServices[0] {00001101-0000-1000-8000-00805f9b34fb}
InstalledServices[1] {0000110b-0000-1000-8000-00805f9b34fb}
InstalledServices[2] {0000110c-0000-1000-8000-00805f9b34fb}
InstalledServices[3] {0000110e-0000-1000-8000-00805f9b34fb}
InstalledServices[4] {0000111e-0000-1000-8000-00805f9b34fb}

The only one that is connecting is the first one:
cli.Connect(device.DeviceAddress, device.InstalledServices.ElementAt(0))

But still I cannot get any commands from the device when I execute:

NetworkStream stream;

stream = cli.GetStream();
if (stream.CanRead)
{
    byte[] buff = new byte[1024];
    int n = 0;
    StringBuilder str = new StringBuilder();
    n = await stream.ReadAsync(buff, 0, buff.Length);
    str.AppendFormat("{0}", Encoding.ASCII.GetString(buff, 0, n));

    MessageBox.Show(str.ToString());
}

The execution gets stuck in ReadAsync call.

Secondly the command you are using is an Apple specific extension to the Handsfree service so while it will work on many newer devices it's not universally supported. Also you should not simply connect and send this command - there is an expected sequence of commands and responses when the handsfree device and audio gateway (your PC in this case) go through when first connected. This allows both sides to negotiate supported features. Then not only can you get indicator values but also receive notifications when these change. You'll have to refer to the HFP spec which has a flowchart describing this.

The IPHONEACCEV command is sent from the headset to de Android smartphone. Yes you are right that I need to send some AT commands before in order configure the communciation, but I least I expected to receive any command once I connect to the Headset, but as you can see in my code, the ReadAsync function which is called just after cli.Connect gets stuck with no data received from the headset.

I also tried to send an AT command ("\r\nAT + BRSF = 20\r\n") to the headset just after connecting and before the ReadAsync call, but the headset is still not sending any response.

@peterfoot
Copy link
Member

Check the IsConnected property of the BluetoothDeviceInfo returned from your picker. If Windows is connected to the Handsfree you won't also be able to connect to the service. There isn't a documented API to get the battery level in this case but clearly the value is there as Windows 10 surfaces it through the Settings > Devices page.

@soyxan
Copy link
Author

soyxan commented Jul 17, 2020

Yes, Connected property of the device from the picker is true.
Should I disconnect it before calling cli.Connect() ? How to do it from code?
Can I just disconnect the Handsfree service, keeping the other services connected and working (like Audio music, etc.)?
Has anything to do with: device.SetServiceState(BluetoothService.Handsfree, true/false)?

Thanks!

@soyxan
Copy link
Author

soyxan commented Jul 17, 2020

Ok, I have made some progress (thanks to you :) )

If I call device.SetServiceState(BluetoothService.Handsfree, false) before calling cli.Connect(device.DeviceAddress, BluetoothService.Handsfree) now I am able to connect to the headset and I get an AT command.

The only thing here is that when I call this SetServiceState my computer is not able to use the headset for calls. I would like to do both thing in parallel, is it possible? (my headset allow 2 connections in parallel, as it can be connected to my computer and my Android smartphone at the same time). Maybe implementing a BluetoothServer with your library just for this HFP AT commands?

@peterfoot
Copy link
Member

They sometimes support multiple devices but it's unlikely to support multiple connections from the same device. If you need to take a snapshot you could disable the service, connect and retrieve the battery and then disconnect and re-enable the windows service. I'm trying to find if there is another API which will expose the handsfree battery when it is connected to Windows...

@soyxan
Copy link
Author

soyxan commented Jul 17, 2020

So the process now is as follows:

BluetoothDevicePicker picker = new BluetoothDevicePicker();
BluetoothDeviceInfo device = await picker.PickSingleDeviceAsync();

device.SetServiceState(BluetoothService.Handsfree, false);

BluetoothClient cli = new BluetoothClient();
cli.Connect(device.DeviceAddress, BluetoothService.Handsfree);
if (cli.Connected) MessageBox.Show("YES");

NetworkStream stream;
stream = cli.GetStream();
// Stream read/write operations to get the battery level
stream.Close();
cli.Close();
device.SetServiceState(BluetoothService.Handsfree, true);

The only issue I am facing now is that when i reconnect the headset to windows with device.SetServiceState(BluetoothService.Handsfree, true) the sound quality is really bad. I checked in the audio properties and the Headset is now using 8000Hz for Handsfree. If I disconnect the headset from Windows and connect it again, it gets 16000hz back again. Is any chance to force by code to use 16000hz?

@sturla78
Copy link

Hello to everybody,
i have a similar problem, thanks to soyxan i have find a way to return a battery level of my headset using HFP. But i have a problem when the headset is in use (there is audio on my headset) and in the same moment execute the code. In this situation my headset stop audio, i can receive AT commands, but unfortunately i can't to reconnect to the headset (the SetServiceState at true not work or the channel remain busy in some way). To unlock this situation i must disable and reenable bluetooth device on windows 10 pc.

If the code is executed without audio (the headset is paired but in standby), then i can use correctly HFP on pc withpout problem.

At this point is possible according to your opinion know if:

  1. there is a alternative way less invasive that permit the restabliscing connetion during play audio

  2. if not possible the 1. point, an API that return the status of service HFP and if in use wait for a retry

Thanks

@SpartanX1
Copy link

SpartanX1 commented Oct 20, 2021

Hi Guys, I know I am late but I tried and it is indeed possible to get the battery level using only UWP and WinRT APIs in C#
Leaving it here in case someone wants to know.
https://github.com/SpartanX1/bluetooth_classic_battery_windows

Thanks guys, a lot of your comments helped me to understand how the communication happens.

@sturla78
Copy link

Thanks Spartan for the info, but a lot of time ago i have discovered a similar way. But the code that you have posted need a little bit modification in order to work with the JBL400BT headphone, is necessary responde to the AT commands setting new value in order to have as final sequential the IPHONEACCEV value.

@SpartanX1
Copy link

Hey @sturla78 you're right, method could be different for JBL, but I haven't tested it. In my case both Sony and Firebolt headphones worked with just sending an "OK" to whatever was incoming until I got the IPHONEACCEV

@Rast1234
Copy link

Rast1234 commented May 28, 2022

I am trying to get battery level from headset too, glad i've found this thread. I tried both 32feet api and approach based on @SpartanX1 code with winrt apis, both have requirement that device must be disconnected prior to establishing socket connection. Ok, but we send/read AT commands, at the same time in Windows Settings, headset becomes "connected" and battery level also gets displayed there! So windows surely does something else: our manual socket connection does not prevent it from reading battery level. Maybe it shares existing socket? I tried looking at BluetoothClient and StreamSocket and don't see any way to modify socket options (if there are any at all). Also there's a closed source app called "bluetoothgoodies" that claims to "just work"...

i am even thinking that any sniffer will do (driver level?) since reading battery state is a read-only operation. is there any simple way to tap into windows bluetooth stack? or maybe even usb...

@jzeb
Copy link

jzeb commented May 31, 2022

I also have a similar problem. Is there any official example code for your reference? It has been bothering me for several days, thank you

@steam3d
Copy link

steam3d commented Dec 5, 2022

@Rast1234 May be Windows uses cached data? I did not try to wait while battery information changed.

If you turn off all services:

  • Audio Sink
  • HandsFree Telephony
  • Remote Cortrol
  • Remotely Controllable Device

It does not matter to which service to connect. 0000111e-0000-1000-8000-00805f9b34fb or 00001108-0000-1000-8000-00805f9b34fb can provide battery. But other values are different.
image

image

UPD:
If you connect to any service first, after it Windows will be able to connect to this service without any trouble. So looks like Windows block socket for yourself and does not allow connect other application.

@steam3d
Copy link

steam3d commented Dec 6, 2022

App from bluetoothgoodies use custom driver to get battery. I do not know what it does, I checked all my headphones and does not see that this driver was used.

@Rast1234
Copy link

Rast1234 commented Dec 6, 2022

UPD: If you connect to any service first, after it Windows will be able to connect to this service without any trouble. So looks like Windows block socket for yourself and does not allow connect other application.

@steam3d that's exactly why i think it could be possible to work with the socket at a lower level, but no idea how. idk about caching, but windows UI sometimes shows out-of-date battery level after my device is disconnected so yeah, maybe it has an internal cache

@steam3d
Copy link

steam3d commented Dec 8, 2022

@Rast1234 Or Windows use HID device to collect battery information. Windows is mapped Bluetooth headphones also as a HID device that I suppose control the headphone buttons. HID device can provide battery information or at least event when battery is low.

https://www.bluetooth.com/specifications/specs/human-interface-device-profile-1-1-1/

4.3.1 Low Battery Notifications
Battery-powered Bluetooth HID devices may utilize the available HID controls in HID reports to inform the Bluetooth HID Host of a low battery condition. There are standard usages available for power status and battery reporting in the HID Specification; see USB HID Usage Tables [4], Generic Device Controls page.

We connect to RFcomm service, that lays on top of L2CAP.
image

https://www.bluetooth.com/specifications/specs/human-interface-device-profile-1-1-1/

The Bluetooth HID Protocol (HIDP) operates over the Bluetooth L2CAP layer and is an adaptation of the USB HID Protocol. The USB HID specification [5] defines Control and Interrupt channels which are mapped to USB Control and Interrupt pipes. This profile defines analogous logical channels which are mapped to L2CAP channels. See section
5.2 for details on how the L2CAP channels are opened and configured.

So Window can just operate of data on L2CAP layer and because of it does not have any trouble to get Battery information even if socket is busy.

Anyway I tried to find HID device to get more info but without success

@steam3d
Copy link

steam3d commented Jan 15, 2023

Well I made lot of test what I figure out:

  1. Windows caches battery value somewhere. When you connect headphones it will show battery from previous session and then update it to actual one. (I could not find where It can storage)
  2. When you connect to RFCOMM, Windows can't access to this service or stream. My headphones changed battery information and notify through RFCOMM, but Windows did not see this changes.

image

@konili
Copy link

konili commented Mar 27, 2023

Well I made lot of test what I figure out:

  1. Windows caches battery value somewhere. When you connect headphones it will show battery from previous session and then update it to actual one. (I could not find where It can storage)
  2. When you connect to RFCOMM, Windows can't access to this service or stream. My headphones changed battery information and notify through RFCOMM, but Windows did not see this changes.

image

Hi,
I always got a failure of "A socket operation failed because the destination host was down", which I selected RFcomm service of "0000111e-0000-1000-8000-00805f9b34fb", do you have any insight for this error?
OS Win10/Win11, BT device is a Jabra headset.
And I also tried on AirPod Pro, encountered another error
"Only one usage of each socket address (protocol/network address/port) is normally permitted."

@peterfoot
Copy link
Member

If Windows is connected to the Handsfree service then you won't be able to open a socket - and vice-versa - Windows won't get battery updates if you've disconnected and connected to the service yourself. If only the Windows cached value was accessible we'd be able to read this without interfering with the connection.

@steam3d
Copy link

steam3d commented Mar 27, 2023

@peterfoot Do you have any idea where Windows can storage cached value? I checked everything from devices properties to regedit.exe. Maybe I something miss

@m1adow
Copy link

m1adow commented Jul 3, 2023

Hi there. I have a problem with the subject of this thread. Guess someone could help me. I want to get the battery level of Bluetooth headphones via Handsfree service. I have two devices for tests: AirPods and Logitech G735. Windows can get battery level only for Airpods. And MacOS only for Logitech. This is strange to me but also we know that we can get battery levels for both devices. So I'm using this code but getting only one AT Command in response AT+BRSF=671 for AirPods and AT+BRSF=147 for Logitech. What am I doing wrong? I really appreciate any help you can provide.

private static async Task<byte> GetBatteryLevelAsync(Headphones headphones)
{
    return await Task.Run(() =>
    {
        using (var bluetoothClient = new BluetoothClient())
        {
            var device = bluetoothClient.PairedDevices.FirstOrDefault(d => d.DeviceName == headphones.Name);
            if (device != null)
            {
                device.SetServiceState(BluetoothService.Handsfree, false);
                bluetoothClient.Connect(device.DeviceAddress, BluetoothService.Handsfree);

                var stream = bluetoothClient.GetStream();
                if (stream != null && stream.CanRead && stream.CanWrite)
                {
                    while (true)
                    {
                        if (!stream.DataAvailable)
                        {
                            break;
                        }

                           byte[] responseBytes = new byte[1024];
                           int bytesRead = stream.Read(responseBytes, 0, responseBytes.Length);
                           string response = Encoding.ASCII.GetString(responseBytes, 0, bytesRead);

                           byte[] commandBytes = Encoding.ASCII.GetBytes(response);
                           stream.Write(commandBytes, 0, commandBytes.Length);
                       }
                    }

                   if (bluetoothClient.Connected)
                   {
                       bluetoothClient.Close();
                   }

                   device.SetServiceState(BluetoothService.Handsfree, true);
               }
            }

        return Task.FromResult((byte)1);
    });
}

@steam3d
Copy link

steam3d commented Jul 3, 2023

@m1adow What did you mean windows can get? Your code does nothing or what? What does the output console say?

@m1adow
Copy link

m1adow commented Jul 3, 2023

@m1adow What did you mean windows can get? Your code does nothing or what? What does the output console say?

I mean that in the Windows Settings app in the Bluetooth & Devices section, I can see headphones and their battery level.
image

My code is returning only this one line
image

@steam3d
Copy link

steam3d commented Jul 3, 2023

@m1adow I suppose mac can't get battery because it is Fake AirPods.
You code does nothing because headphone waiting for response. You need read about AT+ commands. You must response on each request from headphones.

@m1adow
Copy link

m1adow commented Jul 4, 2023

@steam3d Hello again! I successfully managed to get the battery from the code thanks to you. But for some reason between checking data availability from the stream, I need to add a small delay. If I run code without delay then in Output I will get "false". Is there a workaround to avoid this?

while (true)
{
    await Task.Delay(50); // how to avoid waiting for data?
    Debug.WriteLine(stream.DataAvailable);
    if (!stream.DataAvailable)
    {
        break;
    }

    byte[] buffer = new byte[1024];
    int bytesRead = await stream.ReadAsync(buffer);
    string receivedData = Encoding.ASCII.GetString(buffer, 0, bytesRead);
    recievedResponse.Append(receivedData);
    Debug.WriteLine(receivedData);

    await stream.WriteAsync(responseBytes);
}

@steam3d
Copy link

steam3d commented Jul 4, 2023

@m1adow Check Microsoft Rfcomm example on their GitHub. You do not need use while(true). https://github.com/microsoft/Windows-universal-samples/tree/main/Samples/BluetoothRfcommChat

You also need to know that enable and disable SetServiceState is incredible complicated task for windows. Windows will destroy audio endpoint and then create a new one (include a lot of GUID and other data), some app that use headset could stuck.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants