mirror of
https://github.com/dotMorten/NmeaParser.git
synced 2026-01-20 15:40:16 +01:00
Add bluetooth support to desktop app
This commit is contained in:
parent
abf660df7e
commit
ab91ba3c09
152
src/SampleApp.WinDesktop/BluetoothDevice.cs
Normal file
152
src/SampleApp.WinDesktop/BluetoothDevice.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue