Add bluetooth support to desktop app

This commit is contained in:
Morten Nielsen 2020-09-23 20:45:59 -07:00
parent abf660df7e
commit ab91ba3c09
4 changed files with 258 additions and 38 deletions

View file

@ -0,0 +1,152 @@
// Bluetooth device using the Win10 contracts
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Threading.Tasks;
using Windows.Networking.Sockets;
using Windows.Devices.Bluetooth.Rfcomm;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading;
using Windows.Devices.Enumeration;
using Windows.Networking.Proximity;
using NmeaParser;
namespace SampleApp.WinDesktop
{
public class BluetoothDevice : NmeaDevice
{
private Windows.Devices.Bluetooth.Rfcomm.RfcommDeviceService? m_deviceService;
private Windows.Networking.Proximity.PeerInformation? m_devicePeer;
private StreamSocket? m_socket;
private bool m_disposeService;
private SemaphoreSlim m_semaphoreSlim = new SemaphoreSlim(1, 1);
/// <summary>
/// Gets a list of bluetooth devices that supports serial communication
/// </summary>
/// <returns>A set of bluetooth devices available that supports serial connections</returns>
public static async Task<IEnumerable<RfcommDeviceService>> GetBluetoothSerialDevicesAsync()
{
List<RfcommDeviceService> services = new List<RfcommDeviceService>();
if (Windows.Foundation.Metadata.ApiInformation.IsTypePresent("Windows.Devices.Bluetooth.Rfcomm.RfcommDeviceService"))
{
string serialDeviceType = RfcommDeviceService.GetDeviceSelector(RfcommServiceId.SerialPort);
var devices = await DeviceInformation.FindAllAsync(serialDeviceType);
foreach (var d in devices)
services.Add(await RfcommDeviceService.FromIdAsync(d.Id));
}
return services;
}
/// <summary>
/// Initializes a new instance of the <see cref="BluetoothDevice"/> class.
/// </summary>
/// <param name="service">The RF Comm Device service.</param>
/// <param name="disposeService">Whether this devicee should also dispose the RfcommDeviceService provided when this device disposes.</param>
public BluetoothDevice(RfcommDeviceService service, bool disposeService = false)
{
m_deviceService = service ?? throw new ArgumentNullException(nameof(service));
m_disposeService = disposeService;
}
public RfcommDeviceService Service => m_deviceService;
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
if (m_disposeService && m_deviceService != null)
m_deviceService.Dispose();
m_deviceService = null;
m_devicePeer = null;
base.Dispose(disposing);
}
/// <inheritdoc />
protected override async Task<System.IO.Stream> OpenStreamAsync()
{
var socket = new Windows.Networking.Sockets.StreamSocket();
socket.Control.KeepAlive = true;
if (m_devicePeer != null)
{
await socket.ConnectAsync(m_devicePeer.HostName, "1");
}
else if (m_deviceService != null)
{
await socket.ConnectAsync(m_deviceService.ConnectionHostName, m_deviceService.ConnectionServiceName);
}
else
throw new InvalidOperationException();
m_socket = socket;
return new DummyStream(); //We're going to use WinRT buffers instead and will handle read/write, so no reason to return a real stream. This is mainly done to avoid locking issues reading and writing at the same time
}
private class DummyStream : Stream
{
public override bool CanRead => false;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => throw new NotSupportedException();
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
public override void Flush() => throw new NotSupportedException();
public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException();
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
}
/// <inheritdoc />
protected override Task CloseStreamAsync(System.IO.Stream stream)
{
if (m_socket == null)
throw new InvalidOperationException("No connection to close");
m_socket.Dispose();
m_socket = null;
return Task.FromResult(true);
}
/// <inheritdoc />
protected override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
// Reading and writing to the Bluetooth serial connection at the same time seems very unstable in UWP,
// so we use a semaphore to ensure we don't read and write at the same time
await m_semaphoreSlim.WaitAsync().ConfigureAwait(false);
if (m_socket == null)
throw new InvalidOperationException("Socket not initialized");
try
{
var r = await m_socket.InputStream.ReadAsync(buffer.AsBuffer(), (uint)count, Windows.Storage.Streams.InputStreamOptions.None);
return (int)r.Length;
}
finally
{
m_semaphoreSlim.Release();
}
}
/// <inheritdoc />
public override bool CanWrite => true;
/// <inheritdoc />
public override async Task WriteAsync(byte[] buffer, int offset, int length)
{
if (m_socket == null)
throw new InvalidOperationException("Device not open");
// Reading and writing to the Bluetooth serial connection at the same time seems very unstable in UWP,
// so we use a semaphore to ensure we don't read and write at the same time
await m_semaphoreSlim.WaitAsync().ConfigureAwait(false);
try
{
await m_socket.OutputStream.WriteAsync(buffer.AsBuffer(offset, length)).AsTask().ConfigureAwait(false);
}
finally
{
m_semaphoreSlim.Release();
}
}
}
}

View file

@ -88,6 +88,12 @@
<Button Content="Connect" HorizontalAlignment="Left" Padding="20,5" Margin="0,5" Click="ConnectToSerialButton_Click" />
</StackPanel>
</GroupBox>
<GroupBox Header="Open Bluetooth device" Width="200" HorizontalAlignment="Left">
<StackPanel>
<ComboBox x:Name="bluetoothDevices" />
<Button Content="Connect" HorizontalAlignment="Left" Padding="20,5" Margin="0,5" Click="ConnectToBluetoothButton_Click" />
</StackPanel>
</GroupBox>
</StackPanel>
</Grid>
</TabItem>

View file

@ -30,7 +30,7 @@ namespace SampleApp.WinDesktop
InitializeComponent();
//Get list of serial ports for device tab
var availableSerialPorts = System.IO.Ports.SerialPort.GetPortNames().OrderBy(s=>s);
var availableSerialPorts = System.IO.Ports.SerialPort.GetPortNames().OrderBy(s => s);
serialPorts.ItemsSource = availableSerialPorts;
serialPorts.SelectedIndex = 0;
// Use serial portName:
@ -41,6 +41,37 @@ namespace SampleApp.WinDesktop
//Use a log file for playing back logged data
var device = new NmeaParser.NmeaFileDevice("NmeaSampleData.txt") { EmulatedBaudRate = 9600, BurstRate = TimeSpan.FromSeconds(1d) };
_ = StartDevice(device);
LoadBluetoothDevices();
}
public class DeviceInfo
{
public Func<Task<NmeaParser.NmeaDevice>> CreateMethod { get; set; }
public string DisplayName { get; set; }
public override string ToString() => DisplayName;
}
private async void LoadBluetoothDevices()
{
var deviceList = new List<DeviceInfo>();
if (Windows.Foundation.Metadata.ApiInformation.IsTypePresent("Windows.Devices.Bluetooth.Rfcomm.RfcommDeviceService"))
{
var btdevices = await BluetoothDevice.GetBluetoothSerialDevicesAsync();
foreach (var item in btdevices)
{
deviceList.Add(new DeviceInfo()
{
DisplayName = $"{item.Device.Name} (Bluetooth)",
CreateMethod = () =>
{
return Task.FromResult<NmeaParser.NmeaDevice>(new BluetoothDevice(item));
}
});
}
}
bluetoothDevices.ItemsSource = deviceList;
if (deviceList.Count > 0)
bluetoothDevices.SelectedIndex = 0;
}
/// <summary>
@ -74,7 +105,7 @@ namespace SampleApp.WinDesktop
satSnr.ClearGsv();
//Start new device
currentDevice = device;
foreach(var child in MessagePanel.Children.OfType<UnknownMessageControl>().ToArray())
foreach (var child in MessagePanel.Children.OfType<UnknownMessageControl>().ToArray())
{
MessagePanel.Children.Remove(child);
}
@ -88,6 +119,10 @@ namespace SampleApp.WinDesktop
((NmeaParser.SerialPortDevice)device).Port.PortName,
((NmeaParser.SerialPortDevice)device).Port.BaudRate);
}
else if (device is BluetoothDevice bd)
{
currentDeviceInfo.Text = $"Bluetooth {bd.Service.Device.Name}";
}
await device.OpenAsync();
gnssMonitorView.Monitor = monitor = new GnssMonitor(device);
gnssMonitorView.Monitor.LocationChanged += Monitor_LocationChanged;
@ -103,42 +138,42 @@ namespace SampleApp.WinDesktop
private void device_MessageReceived(object sender, NmeaParser.NmeaMessageReceivedEventArgs args)
{
Dispatcher.BeginInvoke((Action) delegate()
{
messages.Enqueue(args.Message.ToString());
if (messages.Count > 100) messages.Dequeue(); //Keep message queue at 100
Dispatcher.BeginInvoke((Action)delegate ()
{
messages.Enqueue(args.Message.ToString());
if (messages.Count > 100) messages.Dequeue(); //Keep message queue at 100
output.Text = string.Join("\n", messages.ToArray());
output.Select(output.Text.Length - 1, 0); //scroll to bottom
output.Select(output.Text.Length - 1, 0); //scroll to bottom
if (args.Message is NmeaParser.Messages.Gsv gpgsv)
{
satView.SetGsv(gpgsv);
satSnr.SetGsv(gpgsv);
}
else if (args.Message is NmeaParser.Messages.Rmc)
gprmcView.Message = args.Message as NmeaParser.Messages.Rmc;
else if (args.Message is NmeaParser.Messages.Gga)
gpggaView.Message = args.Message as NmeaParser.Messages.Gga;
else if (args.Message is NmeaParser.Messages.Gsa)
gpgsaView.Message = args.Message as NmeaParser.Messages.Gsa;
else if (args.Message is NmeaParser.Messages.Gll)
gpgllView.Message = args.Message as NmeaParser.Messages.Gll;
else if (args.Message is NmeaParser.Messages.Garmin.Pgrme)
pgrmeView.Message = args.Message as NmeaParser.Messages.Garmin.Pgrme;
else
{
var ctrl = MessagePanel.Children.OfType<UnknownMessageControl>().Where(c => c.Message.MessageType == args.Message.MessageType).FirstOrDefault();
if (ctrl == null)
{
ctrl = new UnknownMessageControl()
{
Style = this.Resources["card"] as Style
};
MessagePanel.Children.Add(ctrl);
}
ctrl.Message = args.Message;
}
});
{
satView.SetGsv(gpgsv);
satSnr.SetGsv(gpgsv);
}
else if (args.Message is NmeaParser.Messages.Rmc)
gprmcView.Message = args.Message as NmeaParser.Messages.Rmc;
else if (args.Message is NmeaParser.Messages.Gga)
gpggaView.Message = args.Message as NmeaParser.Messages.Gga;
else if (args.Message is NmeaParser.Messages.Gsa)
gpgsaView.Message = args.Message as NmeaParser.Messages.Gsa;
else if (args.Message is NmeaParser.Messages.Gll)
gpgllView.Message = args.Message as NmeaParser.Messages.Gll;
else if (args.Message is NmeaParser.Messages.Garmin.Pgrme)
pgrmeView.Message = args.Message as NmeaParser.Messages.Garmin.Pgrme;
else
{
var ctrl = MessagePanel.Children.OfType<UnknownMessageControl>().Where(c => c.Message.MessageType == args.Message.MessageType).FirstOrDefault();
if (ctrl == null)
{
ctrl = new UnknownMessageControl()
{
Style = this.Resources["card"] as Style
};
MessagePanel.Children.Add(ctrl);
}
ctrl.Message = args.Message;
}
});
}
//Browse to nmea file and create device from selected file
@ -153,7 +188,7 @@ namespace SampleApp.WinDesktop
{
await StartDevice(device);
}
catch(System.Exception ex)
catch (System.Exception ex)
{
MessageBox.Show("Failed to start device: " + ex.Message);
}
@ -177,13 +212,39 @@ namespace SampleApp.WinDesktop
MessageBox.Show("Failed to start device: " + ex.Message);
}
}
catch(System.Exception ex)
catch (System.Exception ex)
{
MessageBox.Show("Error connecting: " + ex.Message);
}
}
private async void ConnectToBluetoothButton_Click(object sender, RoutedEventArgs e)
{
try
{
var info = bluetoothDevices.SelectedItem as DeviceInfo;
if (info != null)
{
var device = await info.CreateMethod();
try
{
await StartDevice(device);
}
catch (System.Exception ex)
{
MessageBox.Show("Failed to start device: " + ex.Message);
}
}
}
catch (System.Exception ex)
{
MessageBox.Show("Error connecting: " + ex.Message);
}
}
}
public class ReverseConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)

View file

@ -20,6 +20,7 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<PackageReference Include="Esri.ArcGISRuntime.WPF" Version="100.9.0" />
<PackageReference Include="Microsoft.Windows.SDK.Contracts" Version="10.0.17134.1000" />
<Content Include="car.glb">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>