mirror of
https://github.com/scratchfoundation/scratch-link.git
synced 2024-11-14 19:05:03 -05:00
Mac BT: implement 'connect', remove 'ouiPrefix'
This commit is contained in:
parent
8fa4207a2b
commit
5312199e6b
10 changed files with 453 additions and 27 deletions
|
@ -61,11 +61,8 @@ internal abstract class BTSession<TDevice, TDeviceId> : Session
|
|||
throw new JsonRpc2Exception(JsonRpc2Error.InvalidParams("majorDeviceClass and minorDeviceClass required"));
|
||||
}
|
||||
|
||||
// TODO: parse ouiPrefixString to bytes
|
||||
var ouiPrefixString = args?.TryGetProperty("ouiPrefix")?.GetString();
|
||||
|
||||
this.availableDevices.Clear();
|
||||
return this.DoDiscover((byte)majorDeviceClass, (byte)minorDeviceClass, null);
|
||||
return this.DoDiscover((byte)majorDeviceClass, (byte)minorDeviceClass);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -73,9 +70,8 @@ internal abstract class BTSession<TDevice, TDeviceId> : Session
|
|||
/// </summary>
|
||||
/// <param name="majorDeviceClass">Discover peripherals with this major device class.</param>
|
||||
/// <param name="minorDeviceClass">Discover peripherals with this minor device class.</param>
|
||||
/// <param name="ouiPrefix">If set, discover peripherals matching this 3-byte OUI prefix.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
protected abstract Task<object> DoDiscover(byte majorDeviceClass, byte minorDeviceClass, byte[] ouiPrefix);
|
||||
protected abstract Task<object> DoDiscover(byte majorDeviceClass, byte minorDeviceClass);
|
||||
|
||||
/// <summary>
|
||||
/// Implement the JSON-RPC "connect" request to connect to a particular peripheral.
|
||||
|
@ -88,7 +84,19 @@ internal abstract class BTSession<TDevice, TDeviceId> : Session
|
|||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
protected Task<object> HandleConnect(string methodName, JsonElement? args)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
var peripheralId = args?.TryGetProperty("peripheralId")?.GetString();
|
||||
|
||||
if (peripheralId == null)
|
||||
{
|
||||
throw new JsonRpc2Exception(JsonRpc2Error.InvalidParams("peripheralId required"));
|
||||
}
|
||||
|
||||
if (!this.availableDevices.TryGetValue(peripheralId, out var device))
|
||||
{
|
||||
throw new JsonRpc2Exception(JsonRpc2Error.InvalidRequest(string.Format("Device {0} not available for connection", peripheralId)));
|
||||
}
|
||||
|
||||
return this.DoConnect(device);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -12,7 +12,7 @@ using System.Threading.Tasks;
|
|||
|
||||
/// <summary>
|
||||
/// Adaptor to use "await" with events.
|
||||
/// Events which fire after construction but before calling <see cref="MakeTask"/> will be queued and can be retrieved later.
|
||||
/// Events which fire after construction but before calling <c>MakeTask</c> will be queued and can be retrieved later.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of EventArgs returned by this event.</typeparam>
|
||||
public class EventAwaiter<T> : IDisposable
|
||||
|
@ -44,6 +44,92 @@ public class EventAwaiter<T> : IDisposable
|
|||
addHandler(this.EventHandler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convenience method to create an instance of the <see cref="EventAwaiter{T}"/> class, await its task, dispose of the instance, and return the event task's results.
|
||||
/// </summary>
|
||||
/// <param name="targetEvent">The event to hook.</param>
|
||||
/// <param name="timeout">How long to wait for the event. If the timeout expires the Task will throw a <see cref="TimeoutException"/>.</param>
|
||||
/// <param name="cancellationToken">The cancellation token to use to cancel the operation.</param>
|
||||
/// <param name="action">Optional action to perform after hooking the event but before awaiting the event task. Usually this is code which will trigger the event.</param>
|
||||
/// <returns>A Task which will return the args passed to the event when it triggers.</returns>
|
||||
public static async ValueTask<T> MakeTask(EventHandler<T> targetEvent, TimeSpan timeout, CancellationToken cancellationToken, Action action = null)
|
||||
{
|
||||
using (var awaiter = new EventAwaiter<T>(targetEvent))
|
||||
{
|
||||
if (action != null)
|
||||
{
|
||||
action();
|
||||
}
|
||||
|
||||
return await awaiter.MakeTask(timeout, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convenience method to create an instance of the <see cref="EventAwaiter{T}"/> class, await its task, dispose of the instance, and return the event task's results.
|
||||
/// </summary>
|
||||
/// <param name="targetEvent">The event to hook.</param>
|
||||
/// <param name="timeout">How long to wait for the event. If the timeout expires the Task will throw a <see cref="TimeoutException"/>.</param>
|
||||
/// <param name="cancellationToken">The cancellation token to use to cancel the operation.</param>
|
||||
/// <param name="asyncAction">Optional async action to await after hooking the event but before awaiting the event task. Usually this is code which will trigger the event.</param>
|
||||
/// <returns>A Task which will return the args passed to the event when it triggers.</returns>
|
||||
public static async ValueTask<T> MakeTask(EventHandler<T> targetEvent, TimeSpan timeout, CancellationToken cancellationToken, Func<Task> asyncAction = null)
|
||||
{
|
||||
using (var awaiter = new EventAwaiter<T>(targetEvent))
|
||||
{
|
||||
if (asyncAction != null)
|
||||
{
|
||||
await asyncAction();
|
||||
}
|
||||
|
||||
return await awaiter.MakeTask(timeout, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convenience method to create an instance of the <see cref="EventAwaiter{T}"/> class, await its task, dispose of the instance, and return the event task's results.
|
||||
/// </summary>
|
||||
/// <param name="addHandler">An action to add a handler to the event, like <c>handler => foo.MyEvent += handler</c>.</param>
|
||||
/// <param name="removeHandler">An action to remove a handler from the event, like <c>handler => foo.MyEvent += handler</c>.</param>
|
||||
/// <param name="timeout">How long to wait for the event. If the timeout expires the Task will throw a <see cref="TimeoutException"/>.</param>
|
||||
/// <param name="cancellationToken">The cancellation token to use to cancel the operation.</param>
|
||||
/// <param name="action">Optional action to perform after hooking the event but before awaiting the event task. Usually this is code which will trigger the event.</param>
|
||||
/// <returns>A Task which will return the args passed to the event when it triggers.</returns>
|
||||
public static async ValueTask<T> MakeTask(Action<EventHandler<T>> addHandler, Action<EventHandler<T>> removeHandler, TimeSpan timeout, CancellationToken cancellationToken, Action action = null)
|
||||
{
|
||||
using (var awaiter = new EventAwaiter<T>(addHandler, removeHandler))
|
||||
{
|
||||
if (action != null)
|
||||
{
|
||||
action();
|
||||
}
|
||||
|
||||
return await awaiter.MakeTask(timeout, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convenience method to create an instance of the <see cref="EventAwaiter{T}"/> class, await its task, dispose of the instance, and return the event task's results.
|
||||
/// </summary>
|
||||
/// <param name="addHandler">An action to add a handler to the event, like <c>handler => foo.MyEvent += handler</c>.</param>
|
||||
/// <param name="removeHandler">An action to remove a handler from the event, like <c>handler => foo.MyEvent += handler</c>.</param>
|
||||
/// <param name="timeout">How long to wait for the event. If the timeout expires the Task will throw a <see cref="TimeoutException"/>.</param>
|
||||
/// <param name="cancellationToken">The cancellation token to use to cancel the operation.</param>
|
||||
/// <param name="asyncAction">Optional async action to await after hooking the event but before awaiting the event task. Usually this is code which will trigger the event.</param>
|
||||
/// <returns>A Task which will return the args passed to the event when it triggers.</returns>
|
||||
public static async ValueTask<T> MakeTask(Action<EventHandler<T>> addHandler, Action<EventHandler<T>> removeHandler, TimeSpan timeout, CancellationToken cancellationToken, Func<Task> asyncAction = null)
|
||||
{
|
||||
using (var awaiter = new EventAwaiter<T>(addHandler, removeHandler))
|
||||
{
|
||||
if (asyncAction != null)
|
||||
{
|
||||
await asyncAction();
|
||||
}
|
||||
|
||||
return await awaiter.MakeTask(timeout, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose of this <see cref="EventAwaiter{T}"/> instance, including ensuring that the event is no longer hooked.
|
||||
/// </summary>
|
||||
|
|
|
@ -6,25 +6,26 @@ namespace ScratchLink.Mac.BT;
|
|||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using CoreBluetooth;
|
||||
using Fleck;
|
||||
using Foundation;
|
||||
using IOBluetooth;
|
||||
using ScratchLink.BT;
|
||||
using ScratchLink.JsonRpc;
|
||||
using ScratchLink.Mac.BT.Rfcomm;
|
||||
|
||||
/// <summary>
|
||||
/// Implements a BT session on MacOS.
|
||||
/// </summary>
|
||||
internal class MacBTSession : BTSession<BluetoothDevice, BluetoothDeviceAddress>
|
||||
{
|
||||
private const int KIOReturnSuccess = 0;
|
||||
|
||||
private DeviceInquiry inquiry;
|
||||
private readonly DeviceInquiry inquiry;
|
||||
|
||||
private DeviceClassMajor searchClassMajor;
|
||||
private DeviceClassMinor searchClassMinor;
|
||||
private byte[] ouiPrefix;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MacBTSession"/> class.
|
||||
|
@ -49,7 +50,7 @@ internal class MacBTSession : BTSession<BluetoothDevice, BluetoothDeviceAddress>
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Task<object> DoDiscover(byte majorDeviceClass, byte minorDeviceClass, byte[] ouiPrefix)
|
||||
protected override Task<object> DoDiscover(byte majorDeviceClass, byte minorDeviceClass)
|
||||
{
|
||||
this.inquiry.Stop();
|
||||
this.inquiry.ClearFoundDevices();
|
||||
|
@ -59,9 +60,10 @@ internal class MacBTSession : BTSession<BluetoothDevice, BluetoothDeviceAddress>
|
|||
this.inquiry.SearchType = DeviceSearchType.Classic;
|
||||
this.inquiry.InquiryLength = 20;
|
||||
this.inquiry.UpdateNewDeviceNames = true;
|
||||
var inquiryStatus = this.inquiry.Start();
|
||||
if (inquiryStatus != KIOReturnSuccess)
|
||||
var inquiryStatus = (IOReturn)this.inquiry.Start();
|
||||
if (inquiryStatus != IOReturn.Success)
|
||||
{
|
||||
Debug.Print("Failed to start inquiry: {0}", inquiryStatus.ToDebugString());
|
||||
throw new JsonRpc2Exception(JsonRpc2Error.ServerError(-32500, "Device inquiry failed to start"));
|
||||
}
|
||||
|
||||
|
@ -69,9 +71,54 @@ internal class MacBTSession : BTSession<BluetoothDevice, BluetoothDeviceAddress>
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Task<object> DoConnect(BluetoothDevice device)
|
||||
protected override async Task<object> DoConnect(BluetoothDevice device)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
this.inquiry.Stop();
|
||||
|
||||
var rfcommDelegate = new RfcommChannelEventDelegate();
|
||||
rfcommDelegate.RfcommChannelDataEvent += this.RfcommDelegate_RfcommChannelData;
|
||||
|
||||
var openChannelResult = await EventAwaiter<RfcommChannelOpenCompleteEventArgs>.MakeTask(
|
||||
h => rfcommDelegate.RfcommChannelOpenCompleteEvent += h,
|
||||
h => rfcommDelegate.RfcommChannelOpenCompleteEvent -= h,
|
||||
TimeSpan.FromSeconds(15),
|
||||
this.CancellationToken,
|
||||
() =>
|
||||
{
|
||||
// OpenRfcommChannelSync sometimes returns "general error" even when the connection will succeed later.
|
||||
// Ignore its return value and check for error status on the RfcommChannelOpenComplete event instead.
|
||||
device.OpenRfcommChannelSync(out var connectedChannel, 1, rfcommDelegate);
|
||||
});
|
||||
|
||||
if (openChannelResult.Error != IOReturn.Success)
|
||||
{
|
||||
Debug.Print("Opening RFCOMM channel failed: {0}", openChannelResult.Error.ToDebugString());
|
||||
throw new JsonRpc2Exception(JsonRpc2Error.ServerError(-32500, "Could not connect to RFCOMM channel."));
|
||||
}
|
||||
|
||||
var connectedChannel = openChannelResult.Channel;
|
||||
|
||||
// Connect is done already; don't wait for this run loop / session to complete.
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
// run loop specifically for this device: necessary to get delegate callbacks
|
||||
// TODO: is it actually necessary with this new implementation?
|
||||
while (connectedChannel?.IsOpen ?? false)
|
||||
{
|
||||
await Task.Delay(500);
|
||||
}
|
||||
|
||||
await this.SendErrorNotification(JsonRpc2Error.ApplicationError("RFCOMM run loop exited"), this.CancellationToken);
|
||||
|
||||
this.EndSession();
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void RfcommDelegate_RfcommChannelData(object sender, RfcommChannelDataEventArgs e)
|
||||
{
|
||||
Debug.Print("Received {0} bytes", e.Data.Length);
|
||||
}
|
||||
|
||||
private async void Inquiry_DeviceFoundAsync(object sender, DeviceFoundEventArgs e)
|
||||
|
@ -90,16 +137,6 @@ internal class MacBTSession : BTSession<BluetoothDevice, BluetoothDeviceAddress>
|
|||
return;
|
||||
}
|
||||
|
||||
if (this.ouiPrefix != null)
|
||||
{
|
||||
if ((this.ouiPrefix[0] != e.Device.Address.Data[0]) ||
|
||||
(this.ouiPrefix[1] != e.Device.Address.Data[1]) ||
|
||||
(this.ouiPrefix[2] != e.Device.Address.Data[2]))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await this.OnDeviceFound(e.Device, e.Device.Address, e.Device.NameOrAddress, e.Device.Rssi);
|
||||
}
|
||||
}
|
||||
|
|
18
scratch-link-mac/BT/Rfcomm/RfcommChannelDataEventArgs.cs
Normal file
18
scratch-link-mac/BT/Rfcomm/RfcommChannelDataEventArgs.cs
Normal file
|
@ -0,0 +1,18 @@
|
|||
// <copyright file="RfcommChannelDataEventArgs.cs" company="Scratch Foundation">
|
||||
// Copyright (c) Scratch Foundation. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace ScratchLink.Mac.BT.Rfcomm;
|
||||
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// Event args for receiving data through an RFCOMM channel.
|
||||
/// </summary>
|
||||
public class RfcommChannelDataEventArgs : RfcommChannelEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the data received through the RFCOMM channel.
|
||||
/// </summary>
|
||||
public byte[] Data { get; set; }
|
||||
}
|
19
scratch-link-mac/BT/Rfcomm/RfcommChannelEventArgs.cs
Normal file
19
scratch-link-mac/BT/Rfcomm/RfcommChannelEventArgs.cs
Normal file
|
@ -0,0 +1,19 @@
|
|||
// <copyright file="RfcommChannelEventArgs.cs" company="Scratch Foundation">
|
||||
// Copyright (c) Scratch Foundation. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace ScratchLink.Mac.BT.Rfcomm;
|
||||
|
||||
using System;
|
||||
using IOBluetooth;
|
||||
|
||||
/// <summary>
|
||||
/// Generic event args for an RFCOMM channel event.
|
||||
/// </summary>
|
||||
public class RfcommChannelEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the channel associated with the event.
|
||||
/// </summary>
|
||||
public RfcommChannel Channel { get; set; }
|
||||
}
|
169
scratch-link-mac/BT/Rfcomm/RfcommChannelEventDelegate.cs
Normal file
169
scratch-link-mac/BT/Rfcomm/RfcommChannelEventDelegate.cs
Normal file
|
@ -0,0 +1,169 @@
|
|||
// <copyright file="RfcommChannelEventDelegate.cs" company="Scratch Foundation">
|
||||
// Copyright (c) Scratch Foundation. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace ScratchLink.Mac.BT.Rfcomm;
|
||||
|
||||
using System;
|
||||
using IOBluetooth;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
/// <summary>
|
||||
/// Converts <see cref="RfcommChannelDelegate"/> callbacks into events.
|
||||
/// </summary>
|
||||
public class RfcommChannelEventDelegate : RfcommChannelDelegate
|
||||
{
|
||||
/// <summary>
|
||||
/// Event triggered when the RFCOMM channel has closed.
|
||||
/// </summary>
|
||||
public event EventHandler<RfcommChannelEventArgs> RfcommChannelClosedEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Event triggered when the RFCOMM channel's control signals have changed.
|
||||
/// </summary>
|
||||
public event EventHandler<RfcommChannelEventArgs> RfcommChannelControlSignalsChangedEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Event triggered when the RFCOMM channel has received data.
|
||||
/// </summary>
|
||||
public event EventHandler<RfcommChannelDataEventArgs> RfcommChannelDataEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Event triggered when the RFCOMM channel's flow control has changed.
|
||||
/// </summary>
|
||||
public event EventHandler<RfcommChannelEventArgs> RfcommChannelFlowControlChangedEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Event triggered when opening the RFCOMM has completed (succeeded or failed).
|
||||
/// </summary>
|
||||
public event EventHandler<RfcommChannelOpenCompleteEventArgs> RfcommChannelOpenCompleteEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Event triggered when there is space in the RFCOMM channel's queue.
|
||||
/// </summary>
|
||||
public event EventHandler<RfcommChannelEventArgs> RfcommChannelQueueSpaceAvailableEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Event triggered when a write to the RFCOMM channel has completed.
|
||||
/// </summary>
|
||||
public event EventHandler<RfcommChannelWriteCompleteEventArgs> RfcommChannelWriteCompleteEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Callback triggered when the RFCOMM channel has closed.
|
||||
/// </summary>
|
||||
/// <param name="rfcommChannel">The RFCOMM channel for which this callback is being called.</param>
|
||||
public override void RfcommChannelClosed(RfcommChannel rfcommChannel)
|
||||
{
|
||||
if (this.RfcommChannelClosedEvent == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.RfcommChannelClosedEvent.Invoke(rfcommChannel, new RfcommChannelEventArgs { Channel = rfcommChannel });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Callback triggered when the RFCOMM channel's control signals have changed.
|
||||
/// </summary>
|
||||
/// <param name="rfcommChannel">The RFCOMM channel for which this callback is being called.</param>
|
||||
public override void RfcommChannelControlSignalsChanged(RfcommChannel rfcommChannel)
|
||||
{
|
||||
if (this.RfcommChannelControlSignalsChangedEvent == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.RfcommChannelControlSignalsChangedEvent.Invoke(rfcommChannel, new RfcommChannelEventArgs { Channel = rfcommChannel });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Callback triggered when the RFCOMM channel has received data.
|
||||
/// </summary>
|
||||
/// <param name="rfcommChannel">The RFCOMM channel for which this callback is being called.</param>
|
||||
/// <param name="dataPointer">A pointer to the data received.</param>
|
||||
/// <param name="dataLength">The number of bytes of data received.</param>
|
||||
public override void RfcommChannelData(RfcommChannel rfcommChannel, IntPtr dataPointer, nuint dataLength)
|
||||
{
|
||||
if (this.RfcommChannelDataEvent == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var output = new byte[dataLength];
|
||||
Marshal.Copy(dataPointer, output, 0, (int)dataLength);
|
||||
this.RfcommChannelDataEvent.Invoke(rfcommChannel, new RfcommChannelDataEventArgs
|
||||
{
|
||||
Channel = rfcommChannel,
|
||||
Data = output,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Callback triggered when the RFCOMM channel's flow control has changed.
|
||||
/// </summary>
|
||||
/// <param name="rfcommChannel">The RFCOMM channel for which this callback is being called.</param>
|
||||
public override void RfcommChannelFlowControlChanged(RfcommChannel rfcommChannel)
|
||||
{
|
||||
if (this.RfcommChannelFlowControlChangedEvent == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.RfcommChannelFlowControlChangedEvent.Invoke(rfcommChannel, new RfcommChannelEventArgs { Channel = rfcommChannel });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Callback triggered when opening the RFCOMM has completed (succeeded or failed).
|
||||
/// </summary>
|
||||
/// <param name="rfcommChannel">The RFCOMM channel for which this callback is being called.</param>
|
||||
/// <param name="error">An <c>IOReturn</c> whether opening the channel succeeded or failed.</param>
|
||||
public override void RfcommChannelOpenComplete(RfcommChannel rfcommChannel, int error)
|
||||
{
|
||||
if (this.RfcommChannelOpenCompleteEvent == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.RfcommChannelOpenCompleteEvent.Invoke(rfcommChannel, new RfcommChannelOpenCompleteEventArgs
|
||||
{
|
||||
Channel = rfcommChannel,
|
||||
Error = (IOReturn)error,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Callback triggered when there is space in the RFCOMM channel's queue.
|
||||
/// </summary>
|
||||
/// <param name="rfcommChannel">The RFCOMM channel for which this callback is being called.</param>
|
||||
public override void RfcommChannelQueueSpaceAvailable(RfcommChannel rfcommChannel)
|
||||
{
|
||||
if (this.RfcommChannelQueueSpaceAvailableEvent == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.RfcommChannelQueueSpaceAvailableEvent.Invoke(rfcommChannel, new RfcommChannelEventArgs { Channel = rfcommChannel });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Callback triggered when a write to the RFCOMM channel has completed.
|
||||
/// </summary>
|
||||
/// <param name="rfcommChannel">The RFCOMM channel for which this callback is being called.</param>
|
||||
/// <param name="refcon">The "reference constant" passed when initiating this write.</param>
|
||||
/// <param name="error">The error encountered during this write, if any.</param>
|
||||
public override void RfcommChannelWriteComplete(RfcommChannel rfcommChannel, IntPtr refcon, int error)
|
||||
{
|
||||
if (this.RfcommChannelWriteCompleteEvent == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.RfcommChannelWriteCompleteEvent.Invoke(rfcommChannel, new RfcommChannelWriteCompleteEventArgs
|
||||
{
|
||||
Channel = rfcommChannel,
|
||||
RefCon = refcon,
|
||||
Error = (IOReturn)error,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
// <copyright file="RfcommChannelOpenCompleteEventArgs.cs" company="Scratch Foundation">
|
||||
// Copyright (c) Scratch Foundation. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace ScratchLink.Mac.BT.Rfcomm;
|
||||
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// Event args for when opening an RFCOMM channel has completed (succeeded or failed).
|
||||
/// </summary>
|
||||
public class RfcommChannelOpenCompleteEventArgs : RfcommChannelEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the error encountered while attempting to open the RFCOMM channel, if any.
|
||||
/// </summary>
|
||||
public IOReturn Error { get; set; }
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
// <copyright file="RfcommChannelWriteCompleteEventArgs.cs" company="Scratch Foundation">
|
||||
// Copyright (c) Scratch Foundation. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace ScratchLink.Mac.BT.Rfcomm;
|
||||
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// Event args for the completion of an RFCOMM write.
|
||||
/// </summary>
|
||||
public class RfcommChannelWriteCompleteEventArgs : RfcommChannelEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the reference constant.
|
||||
/// </summary>
|
||||
public IntPtr RefCon { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an <c>IOReturn</c> value indicating whether an error occurred during the write.
|
||||
/// </summary>
|
||||
public IOReturn Error { get; set; }
|
||||
}
|
42
scratch-link-mac/IOReturn.cs
Normal file
42
scratch-link-mac/IOReturn.cs
Normal file
|
@ -0,0 +1,42 @@
|
|||
// <copyright file="IOReturn.cs" company="Scratch Foundation">
|
||||
// Copyright (c) Scratch Foundation. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace ScratchLink.Mac;
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
/// <summary>
|
||||
/// Corresponds to <c>IOReturn</c> in <c>IOKit</c>, also known as <c>kern_return_t</c>.
|
||||
/// </summary>
|
||||
public enum IOReturn : int
|
||||
{
|
||||
/// <summary>
|
||||
/// Success.
|
||||
/// </summary>
|
||||
Success = 0,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for <see cref="IOReturn"/>.
|
||||
/// </summary>
|
||||
public static class IOReturnExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts an <see cref="IOReturn"/> value to a developer-friendly string.
|
||||
/// This uses <c>mach_error_string</c> from the macOS system.
|
||||
/// The resulting string might help a developer but they're not usually friendly to lay-people.
|
||||
/// </summary>
|
||||
/// <param name="ioReturn">The value to convert.</param>
|
||||
/// <returns>A developer-readable string corresponding to the error code.</returns>
|
||||
public static string ToDebugString(this IOReturn ioReturn)
|
||||
{
|
||||
var ptr = Mach_error_string(ioReturn);
|
||||
var str = Marshal.PtrToStringAuto(ptr);
|
||||
return str;
|
||||
}
|
||||
|
||||
[DllImport("__Internal", EntryPoint = "mach_error_string")]
|
||||
private static extern IntPtr Mach_error_string([MarshalAs(UnmanagedType.I4)] IOReturn ioReturn);
|
||||
}
|
|
@ -88,7 +88,13 @@
|
|||
<Compile Include="BLE\MacBLESession.cs" />
|
||||
<Compile Include="BLE\MacBLEEndpoint.cs" />
|
||||
<Compile Include="BT\MacBTSession.cs" />
|
||||
<Compile Include="BT\Rfcomm\RfcommChannelDataEventArgs.cs" />
|
||||
<Compile Include="BT\Rfcomm\RfcommChannelEventDelegate.cs" />
|
||||
<Compile Include="BT\Rfcomm\RfcommChannelEventArgs.cs" />
|
||||
<Compile Include="BT\Rfcomm\RfcommChannelOpenCompleteEventArgs.cs" />
|
||||
<Compile Include="BT\Rfcomm\RfcommChannelWriteCompleteEventArgs.cs" />
|
||||
<Compile Include="Extensions\NSExtensions.cs" />
|
||||
<Compile Include="IOReturn.cs" />
|
||||
<Compile Include="MacSessionManager.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
|
Loading…
Reference in a new issue