├── .github └── FUNDING.yml ├── icon.png ├── .gitignore ├── src ├── WinBeacon │ ├── _Visibility.cs │ ├── BeaconEventArgs.cs │ ├── EddystoneEventArgs.cs │ ├── Extensions │ │ ├── ComparableExtensions.cs │ │ └── BeaconExtensions.cs │ ├── WinBeacon.csproj │ ├── Hub.cs │ ├── Beacon.cs │ └── Eddystone.cs ├── WinBeacon.Stack │ ├── _Visibility.cs │ ├── Hci │ │ ├── ICommandParameter.cs │ │ ├── Opcodes │ │ │ ├── InformationalParametersOpcode.cs │ │ │ ├── ControllerBasebandOpcode.cs │ │ │ └── LeControllerOpcode.cs │ │ ├── Commands │ │ │ ├── ResetCommand.cs │ │ │ ├── ReadLocalVersionCommand.cs │ │ │ ├── LeReadHostSupportedCommand.cs │ │ │ ├── LeSetEventMaskCommand.cs │ │ │ ├── SetEventMaskCommand.cs │ │ │ ├── LeSetAdvertisingEnableCommand.cs │ │ │ ├── LeSetScanEnableCommand.cs │ │ │ ├── LeWriteHostSupportedCommand.cs │ │ │ ├── LeSetAdvertisingDataCommand.cs │ │ │ ├── ReadBdAddrCommand.cs │ │ │ ├── LeSetScanParametersCommand.cs │ │ │ └── LeSetAdvertisingParametersCommand.cs │ │ ├── OpcodeGroup.cs │ │ ├── DataType.cs │ │ ├── Parameters │ │ │ ├── ByteCommandParameter.cs │ │ │ ├── ByteArrayCommandParameter.cs │ │ │ ├── BoolCommandParameter.cs │ │ │ └── UshortCommandParameter.cs │ │ ├── Events │ │ │ ├── LeAdvertisingEventType.cs │ │ │ ├── CommandCompleteEvent.cs │ │ │ ├── LeAdvertisingEvent.cs │ │ │ └── LeMetaEvent.cs │ │ ├── DataReceivedEventArgs.cs │ │ ├── Event.cs │ │ ├── Command.cs │ │ └── EventCode.cs │ ├── DeviceAddressReceivedEventArgs.cs │ ├── LeMetaEventReceivedEventArgs.cs │ ├── Transports │ │ ├── UsbHciEndpointType.cs │ │ ├── LibUsb │ │ │ ├── ILibUsbDevice.cs │ │ │ ├── LibUsbDevice.cs │ │ │ └── LibUsbTransport.cs │ │ └── UsbHciEndpointInfo.cs │ ├── Controllers │ │ ├── LibUsbController.cs │ │ └── LeController.cs │ ├── Extensions │ │ ├── UshortExtensions.cs │ │ ├── QueueExtensions.cs │ │ └── LibUsbDeviceExtensions.cs │ ├── WinBeaconException.cs │ ├── ITransport.cs │ ├── DeviceAddress.cs │ ├── WinBeacon.Stack.csproj │ └── ILeController.cs └── GlobalAssemblyInfo.cs ├── test ├── WinBeacon.Stack.Tests │ ├── _Visibility.cs │ ├── WinBeacon.Stack.Tests.csproj │ ├── Extensions │ │ ├── UshortExtensionsTests.cs │ │ └── QueueExtensionsTests.cs │ ├── Hci │ │ ├── ParameterTests.cs │ │ ├── EventTests.cs │ │ └── CommandTests.cs │ ├── Controllers │ │ ├── LibUsbLeControllerTests.cs │ │ └── LeControllerTests.cs │ └── Transports │ │ └── LibUsb │ │ └── LibUsbTransportTests.cs └── WinBeacon.Tests │ ├── WinBeacon.Tests.csproj │ ├── Extensions │ ├── BeaconExtensionsTests.cs │ └── ComparableExtensionsTests.cs │ ├── BeaconTests.cs │ ├── EddystoneUidTests.cs │ └── EddystoneUrlTests.cs ├── .nuget ├── NuGet.Config └── NuGet.targets ├── samples └── SampleConsole │ ├── App.config │ ├── SampleConsole.csproj │ └── Program.cs ├── LICENSE ├── ReadMe.md └── WinBeacon.sln /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: huysentruitw 2 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huysentruitw/win-beacon/HEAD/icon.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.suo 2 | *.vshost.* 3 | **/bin/* 4 | **/obj/* 5 | *.exe 6 | packages/ 7 | *.nupkg 8 | .vs/ 9 | -------------------------------------------------------------------------------- /src/WinBeacon/_Visibility.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly:InternalsVisibleTo("WinBeacon.Tests")] 4 | -------------------------------------------------------------------------------- /test/WinBeacon.Stack.Tests/_Visibility.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] 4 | -------------------------------------------------------------------------------- /.nuget/NuGet.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /samples/SampleConsole/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/_Visibility.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly:InternalsVisibleTo("WinBeacon.Tests")] 4 | [assembly:InternalsVisibleTo("WinBeacon.Stack.Tests")] 5 | [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] 6 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/Hci/ICommandParameter.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | namespace WinBeacon.Stack.Hci 8 | { 9 | internal interface ICommandParameter 10 | { 11 | byte[] ToByteArray(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/Hci/Opcodes/InformationalParametersOpcode.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | namespace WinBeacon.Stack.Hci.Opcodes 8 | { 9 | internal enum InformationalParametersOpcode : ushort 10 | { 11 | ReadLocalVersion = 0x0001, 12 | ReadBdAddr = 0x0009 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /samples/SampleConsole/SampleConsole.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.2 5 | 6 | Exe 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/Hci/Opcodes/ControllerBasebandOpcode.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | namespace WinBeacon.Stack.Hci.Opcodes 8 | { 9 | internal enum ControllerBasebandOpcode : ushort 10 | { 11 | SetEventMask = 0x0001, 12 | Reset = 0x0003, 13 | LeReadHostSupported = 0x006C, 14 | LeWriteHostSupported = 0x006D 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/Hci/Commands/ResetCommand.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using WinBeacon.Stack.Hci.Opcodes; 8 | 9 | namespace WinBeacon.Stack.Hci.Commands 10 | { 11 | internal class ResetCommand : Command 12 | { 13 | public ResetCommand() 14 | : base(OpcodeGroup.ControllerBaseband, (int)ControllerBasebandOpcode.Reset) 15 | { 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/Hci/OpcodeGroup.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | namespace WinBeacon.Stack.Hci 8 | { 9 | internal enum OpcodeGroup : byte 10 | { 11 | LinkControl = 0x01, 12 | LinkPolicy = 0x02, 13 | ControllerBaseband = 0x03, 14 | InformationalParameters = 0x04, 15 | StatusParameters = 0x05, 16 | LeController = 0x08, 17 | Vendor = 0x3F 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/Hci/Commands/ReadLocalVersionCommand.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using WinBeacon.Stack.Hci.Opcodes; 8 | 9 | namespace WinBeacon.Stack.Hci.Commands 10 | { 11 | internal class ReadLocalVersionCommand : Command 12 | { 13 | public ReadLocalVersionCommand() 14 | : base(OpcodeGroup.InformationalParameters, (int)InformationalParametersOpcode.ReadLocalVersion) 15 | { 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/Hci/Commands/LeReadHostSupportedCommand.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using WinBeacon.Stack.Hci.Opcodes; 8 | 9 | namespace WinBeacon.Stack.Hci.Commands 10 | { 11 | internal class LeReadHostSupportedCommand : Command 12 | { 13 | public LeReadHostSupportedCommand() 14 | : base(OpcodeGroup.ControllerBaseband, (int)ControllerBasebandOpcode.LeReadHostSupported) 15 | { 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/Hci/DataType.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | namespace WinBeacon.Stack.Hci 8 | { 9 | /// 10 | /// Transport data types. 11 | /// 12 | internal enum DataType 13 | { 14 | /// 15 | /// Asynchronous Connectionless data. 16 | /// 17 | Acl, 18 | /// 19 | /// Command data. 20 | /// 21 | Command 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/WinBeacon.Stack.Tests/WinBeacon.Stack.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net45 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/Hci/Parameters/ByteCommandParameter.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | namespace WinBeacon.Stack.Hci.Parameters 8 | { 9 | internal class ByteCommandParameter : ICommandParameter 10 | { 11 | public byte Value { get; private set; } 12 | 13 | public ByteCommandParameter(byte value) 14 | { 15 | Value = value; 16 | } 17 | 18 | public byte[] ToByteArray() 19 | { 20 | return new byte[] { Value }; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/Hci/Parameters/ByteArrayCommandParameter.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | namespace WinBeacon.Stack.Hci.Parameters 8 | { 9 | internal class ByteArrayCommandParameter : ICommandParameter 10 | { 11 | public byte[] Value { get; private set; } 12 | 13 | public ByteArrayCommandParameter(byte[] value) 14 | { 15 | Value = value; 16 | } 17 | 18 | public byte[] ToByteArray() 19 | { 20 | return Value; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/Hci/Parameters/BoolCommandParameter.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | namespace WinBeacon.Stack.Hci.Parameters 8 | { 9 | internal class BoolCommandParameter : ICommandParameter 10 | { 11 | public bool Value { get; private set; } 12 | 13 | public BoolCommandParameter(bool value) 14 | { 15 | Value = value; 16 | } 17 | 18 | public byte[] ToByteArray() 19 | { 20 | return new byte[] { (byte)(Value ? 1 : 0) }; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/Hci/Commands/LeSetEventMaskCommand.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using WinBeacon.Stack.Hci.Opcodes; 8 | using WinBeacon.Stack.Hci.Parameters; 9 | 10 | namespace WinBeacon.Stack.Hci.Commands 11 | { 12 | internal class LeSetEventMaskCommand : Command 13 | { 14 | public LeSetEventMaskCommand(byte[] mask) 15 | : base(OpcodeGroup.LeController, (int)LeControllerOpcode.SetEventMask) 16 | { 17 | Parameters.Add(new ByteArrayCommandParameter(mask)); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/Hci/Commands/SetEventMaskCommand.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using WinBeacon.Stack.Hci.Opcodes; 8 | using WinBeacon.Stack.Hci.Parameters; 9 | 10 | namespace WinBeacon.Stack.Hci.Commands 11 | { 12 | internal class SetEventMaskCommand : Command 13 | { 14 | public SetEventMaskCommand(byte[] mask) 15 | : base(OpcodeGroup.ControllerBaseband, (int)ControllerBasebandOpcode.SetEventMask) 16 | { 17 | Parameters.Add(new ByteArrayCommandParameter(mask)); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/Hci/Opcodes/LeControllerOpcode.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | namespace WinBeacon.Stack.Hci.Opcodes 8 | { 9 | internal enum LeControllerOpcode : ushort 10 | { 11 | SetEventMask = 0x0001, 12 | SetAdvertisingParameters = 0x0006, // http://stackoverflow.com/questions/21124993/is-there-a-way-to-increase-ble-advertisement-frequency-in-bluez 13 | SetAdvertisingData = 0x0008, 14 | SetAdvertisingEnable = 0x000A, 15 | SetScanParameters = 0x000B, 16 | SetScanEnable = 0x000C, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/WinBeacon.Tests/WinBeacon.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net45 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/Hci/Parameters/UshortCommandParameter.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | namespace WinBeacon.Stack.Hci.Parameters 8 | { 9 | internal class UshortCommandParameter : ICommandParameter 10 | { 11 | public ushort Value { get; private set; } 12 | 13 | public UshortCommandParameter(ushort value) 14 | { 15 | Value = value; 16 | } 17 | 18 | public byte[] ToByteArray() 19 | { 20 | return new byte[] { (byte)(Value & 0xFF), (byte)(Value >> 8) }; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/WinBeacon/BeaconEventArgs.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using System; 8 | 9 | namespace WinBeacon 10 | { 11 | /// 12 | /// Event arguments for Apple iBeacon events. 13 | /// 14 | public class BeaconEventArgs : EventArgs 15 | { 16 | /// 17 | /// Gets the detected beacon. 18 | /// 19 | public Beacon Beacon { get; private set; } 20 | 21 | internal BeaconEventArgs(Beacon beacon) 22 | { 23 | Beacon = beacon; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/Hci/Commands/LeSetAdvertisingEnableCommand.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using WinBeacon.Stack.Hci.Opcodes; 8 | using WinBeacon.Stack.Hci.Parameters; 9 | 10 | namespace WinBeacon.Stack.Hci.Commands 11 | { 12 | internal class LeSetAdvertisingEnableCommand : Command 13 | { 14 | public LeSetAdvertisingEnableCommand(bool enable) 15 | : base(OpcodeGroup.LeController, (int)LeControllerOpcode.SetAdvertisingEnable) 16 | { 17 | Parameters.Add(new BoolCommandParameter(enable)); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/WinBeacon/EddystoneEventArgs.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using System; 8 | 9 | namespace WinBeacon 10 | { 11 | /// 12 | /// Event arguments for Eddystone events. 13 | /// 14 | public class EddystoneEventArgs : EventArgs 15 | { 16 | /// 17 | /// Gets the detected beacon. 18 | /// 19 | public Eddystone Eddystone { get; private set; } 20 | 21 | internal EddystoneEventArgs(Eddystone eddystone) 22 | { 23 | Eddystone = eddystone; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/Hci/Commands/LeSetScanEnableCommand.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using WinBeacon.Stack.Hci.Opcodes; 8 | using WinBeacon.Stack.Hci.Parameters; 9 | 10 | namespace WinBeacon.Stack.Hci.Commands 11 | { 12 | internal class LeSetScanEnableCommand : Command 13 | { 14 | public LeSetScanEnableCommand(bool enable, bool filterDuplicates) 15 | : base(OpcodeGroup.LeController, (int)LeControllerOpcode.SetScanEnable) 16 | { 17 | Parameters.Add(new BoolCommandParameter(enable)); 18 | Parameters.Add(new BoolCommandParameter(filterDuplicates)); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/DeviceAddressReceivedEventArgs.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using System; 8 | 9 | namespace WinBeacon.Stack 10 | { 11 | /// 12 | /// Event arguments for the DeviceAddressReceived event. 13 | /// 14 | public class DeviceAddressReceivedEventArgs : EventArgs 15 | { 16 | /// 17 | /// Received device address. 18 | /// 19 | public DeviceAddress DeviceAddress { get; private set; } 20 | 21 | internal DeviceAddressReceivedEventArgs(DeviceAddress deviceAddress) 22 | { 23 | DeviceAddress = deviceAddress; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/LeMetaEventReceivedEventArgs.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using System; 8 | using WinBeacon.Stack.Hci.Events; 9 | 10 | namespace WinBeacon.Stack 11 | { 12 | /// 13 | /// Event arguments for the LeMetaEventReceived event. 14 | /// 15 | public class LeMetaEventReceivedEventArgs : EventArgs 16 | { 17 | /// 18 | /// Received LeMetaEvent. 19 | /// 20 | public LeMetaEvent LeMetaEvent { get; private set; } 21 | 22 | internal LeMetaEventReceivedEventArgs(LeMetaEvent leMetaEvent) 23 | { 24 | LeMetaEvent = leMetaEvent; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/Hci/Commands/LeWriteHostSupportedCommand.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using WinBeacon.Stack.Hci.Opcodes; 8 | using WinBeacon.Stack.Hci.Parameters; 9 | 10 | namespace WinBeacon.Stack.Hci.Commands 11 | { 12 | internal class LeWriteHostSupportedCommand : Command 13 | { 14 | public LeWriteHostSupportedCommand(bool supportedHost, bool simultaneousHost) 15 | : base(OpcodeGroup.ControllerBaseband, (int)ControllerBasebandOpcode.LeWriteHostSupported) 16 | { 17 | Parameters.Add(new BoolCommandParameter(supportedHost)); 18 | Parameters.Add(new BoolCommandParameter(simultaneousHost)); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/WinBeacon.Stack.Tests/Extensions/UshortExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using NUnit.Framework; 8 | 9 | namespace WinBeacon.Stack.Tests.Extensions 10 | { 11 | [TestFixture] 12 | public class UshortExtensionsTests 13 | { 14 | [Test] 15 | public void UshortExtensions_LoByte() 16 | { 17 | ushort input = 0x5386; 18 | byte output = input.LoByte(); 19 | Assert.AreEqual(0x86, output); 20 | } 21 | 22 | [Test] 23 | public void UshortExtensions_HiByte() 24 | { 25 | ushort input = 0x8429; 26 | byte output = input.HiByte(); 27 | Assert.AreEqual(0x84, output); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/Transports/UsbHciEndpointType.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | namespace WinBeacon.Stack.Transport 8 | { 9 | /// 10 | /// Bluetooth specific USB endpoint types. 11 | /// 12 | internal enum UsbBluetoothEndpointType 13 | { 14 | /// 15 | /// Command endpoint. 16 | /// 17 | Commands, 18 | /// 19 | /// Event endpoint. 20 | /// 21 | Events, 22 | /// 23 | /// ACL data in endpoint. 24 | /// 25 | AclDataIn, 26 | /// 27 | /// ACL data out endpoint. 28 | /// 29 | AclDataOut 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/Hci/Commands/LeSetAdvertisingDataCommand.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using System; 8 | using WinBeacon.Stack.Hci.Opcodes; 9 | using WinBeacon.Stack.Hci.Parameters; 10 | 11 | namespace WinBeacon.Stack.Hci.Commands 12 | { 13 | internal class LeSetAdvertisingDataCommand : Command 14 | { 15 | public LeSetAdvertisingDataCommand(byte[] data) 16 | : base(OpcodeGroup.LeController, (int)LeControllerOpcode.SetAdvertisingData) 17 | { 18 | if (data.Length > 31) 19 | throw new ArgumentOutOfRangeException(); 20 | Parameters.Add(new ByteCommandParameter((byte)data.Length)); 21 | Parameters.Add(new ByteArrayCommandParameter(data)); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/Hci/Commands/ReadBdAddrCommand.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using System.Linq; 8 | using WinBeacon.Stack.Hci.Opcodes; 9 | 10 | namespace WinBeacon.Stack.Hci.Commands 11 | { 12 | internal class ReadBdAddrCommand : Command 13 | { 14 | public ReadBdAddrCommand() 15 | : base(OpcodeGroup.InformationalParameters, (int)InformationalParametersOpcode.ReadBdAddr) 16 | { 17 | } 18 | 19 | internal override DeviceAddress ParseCommandResult(Events.CommandCompleteEvent e) 20 | { 21 | if (e.ResultData == null || e.ResultData.Length != 6) 22 | return DeviceAddress.Empty; 23 | return new DeviceAddress(e.ResultData.Reverse().ToArray()); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/Controllers/LibUsbController.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using WinBeacon.Stack.Transports.LibUsb; 8 | 9 | namespace WinBeacon.Stack.Controllers 10 | { 11 | /// 12 | /// LibUsb Bluetooth Low Energy controller. 13 | /// 14 | public class LibUsbLeController : LeController 15 | { 16 | /// 17 | /// Create a new LibUsbLeController instance. 18 | /// 19 | /// The Bluetooth dongle USB vendor identifier. 20 | /// The Bluetooth dongle USB product identifier. 21 | public LibUsbLeController(int vid, int pid) 22 | : base(new LibUsbTransport(new LibUsbDevice(vid, pid))) 23 | { 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/WinBeacon.Tests/Extensions/BeaconExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using NUnit.Framework; 8 | 9 | namespace WinBeacon.Tests.Extensions 10 | { 11 | [TestFixture] 12 | public class BeaconExtensionsTests 13 | { 14 | [Test] 15 | public void BeaconExtension_GetRange() 16 | { 17 | var beacon = new Beacon("", 0, 0, -39) { Rssi = -52 }; 18 | Assert.AreEqual(4.4668359215096309d, beacon.GetRange()); 19 | } 20 | 21 | [Test] 22 | public void BeaconExtension_GetApproximateIosRange() 23 | { 24 | var beacon = new Beacon("", 0, 0, -39) { Rssi = -52 }; 25 | Assert.AreEqual(8.3781601753285457d, beacon.GetApproximateIosRange()); 26 | beacon.Rssi = 0; 27 | Assert.AreEqual(-1d, beacon.GetApproximateIosRange()); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/WinBeacon.Stack/Transports/LibUsb/ILibUsbDevice.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using System.Collections.Generic; 8 | using LibUsbDotNet; 9 | using LibUsbDotNet.Info; 10 | using LibUsbDotNet.Main; 11 | 12 | namespace WinBeacon.Stack.Transports.LibUsb 13 | { 14 | /// 15 | /// Interface for wrapping LibUsbDotNet.UsbDevice. 16 | /// 17 | internal interface ILibUsbDevice 18 | { 19 | int Vid { get; } 20 | int Pid { get; } 21 | void Open(); 22 | void Close(); 23 | IEnumerable Configs { get; } 24 | UsbEndpointReader OpenEndpointReader(ReadEndpointID readEndpointID); 25 | UsbEndpointWriter OpenEndpointWriter(WriteEndpointID writeEndpointID); 26 | bool ControlTransfer(ref UsbSetupPacket setupPacket, object buffer, int bufferLength, out int lengthTransferred); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/Extensions/UshortExtensions.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | namespace WinBeacon.Stack 8 | { 9 | /// 10 | /// ushort extension methods. 11 | /// 12 | internal static class UshortExtensions 13 | { 14 | /// 15 | /// Get the least significant byte. 16 | /// 17 | /// The ushort. 18 | /// The least significant byte. 19 | public static byte LoByte(this ushort value) 20 | => (byte)(value & 0xFF); 21 | 22 | /// 23 | /// Get the most significant byte. 24 | /// 25 | /// The ushort. 26 | /// The most significant byte. 27 | public static byte HiByte(this ushort value) 28 | => (byte)(value >> 8); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/WinBeacon/Extensions/ComparableExtensions.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | namespace System 8 | { 9 | /// 10 | /// IComparable extension methods. 11 | /// 12 | public static class ComparableExtensions 13 | { 14 | /// 15 | /// Same as CompareTo but returns null instead of 0 if both items are equal. 16 | /// 17 | /// IComparable type. 18 | /// This instance. 19 | /// The other instance. 20 | /// Lexical relation between this and the other instance or null if both are equal. 21 | public static int? NullableCompareTo(this T @this, T other) where T : IComparable 22 | { 23 | var result = @this.CompareTo(other); 24 | return result != 0 ? result : (int?)null; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/Hci/Events/LeAdvertisingEventType.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | namespace WinBeacon.Stack.Hci.Events 8 | { 9 | /// 10 | /// Low Energy Advertising event types 11 | /// 12 | public enum LeAdvertisingEventType : byte 13 | { 14 | /// 15 | /// Connectable undirected advertising 16 | /// 17 | Ind = 0x00, 18 | /// 19 | /// Connectable directed advertising event. 20 | /// 21 | DirectInd = 0x01, 22 | /// 23 | /// Scannable undirected advertising event. 24 | /// 25 | ScanInd = 0x02, 26 | /// 27 | /// Non-connectable and non-scannable advertisement packets. 28 | /// 29 | NonConnInd = 0x03, 30 | /// 31 | /// Scan response. 32 | /// 33 | ScanRsp = 0x04 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015-2019 Wouter Huysentruit 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/WinBeacon.Stack/WinBeaconException.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using System; 8 | 9 | namespace WinBeacon.Stack 10 | { 11 | /// 12 | /// WinBeacon specific exception. 13 | /// 14 | public class WinBeaconException : Exception 15 | { 16 | /// 17 | /// Creates a new exception containing a formatted message. 18 | /// 19 | /// The message. 20 | public WinBeaconException(string message) 21 | : base(message) 22 | { 23 | } 24 | 25 | /// 26 | /// Creates a new exception containing a message and inner-exception. 27 | /// 28 | /// The message. 29 | /// The inner-exception. 30 | public WinBeaconException(string message, Exception innerException) 31 | : base(message, innerException) 32 | { 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/ITransport.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using System; 8 | using WinBeacon.Stack.Hci; 9 | 10 | namespace WinBeacon.Stack 11 | { 12 | /// 13 | /// Interface for classes that enable a transport link with the Bluetooth dongle. 14 | /// 15 | internal interface ITransport : IDisposable 16 | { 17 | /// 18 | /// Open the transport link. 19 | /// 20 | /// True on success, false on failure. 21 | void Open(); 22 | 23 | /// 24 | /// Close the transport link. 25 | /// 26 | void Close(); 27 | 28 | /// 29 | /// Send data to the dongle. 30 | /// 31 | void Send(byte[] data, DataType dataType); 32 | 33 | /// 34 | /// Event triggered on data reception from the dongle. 35 | /// 36 | event EventHandler DataReceived; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/WinBeacon.Tests/Extensions/ComparableExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using System; 8 | using NUnit.Framework; 9 | 10 | namespace WinBeacon.Tests.Extensions 11 | { 12 | [TestFixture] 13 | public class ComparableExtensionsTests 14 | { 15 | [Test] 16 | public void Comparable_Equal() 17 | { 18 | Assert.IsNull("abc".NullableCompareTo("abc"), "Should return null when both items are equal"); 19 | } 20 | 21 | [Test] 22 | public void Coparable_Bigger() 23 | { 24 | var result = (10).NullableCompareTo(2); 25 | Assert.NotNull(result); 26 | Assert.Greater(result.Value, 0, "Should return a value greater than zero"); 27 | } 28 | 29 | [Test] 30 | public void Coparable_Smaller() 31 | { 32 | var result = (55.9).NullableCompareTo(56.0); 33 | Assert.NotNull(result); 34 | Assert.Less(result.Value, 0, "Should return a value greater than zero"); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/Hci/DataReceivedEventArgs.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using System; 8 | 9 | namespace WinBeacon.Stack.Hci 10 | { 11 | /// 12 | /// Event arguments for the DataReceived event. 13 | /// 14 | internal class DataReceivedEventArgs : EventArgs 15 | { 16 | /// 17 | /// The received data. 18 | /// 19 | public byte[] Data { get; private set; } 20 | /// 21 | /// The type of the received data. 22 | /// 23 | public DataType DataType { get; private set; } 24 | 25 | /// 26 | /// Constructs a new DataReceivedEventArgs instance. 27 | /// 28 | /// The received data. 29 | /// The type of the received data. 30 | public DataReceivedEventArgs(byte[] data, DataType dataType) 31 | { 32 | Data = data; 33 | DataType = dataType; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/Transports/UsbHciEndpointInfo.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | namespace WinBeacon.Stack.Transport 8 | { 9 | /// 10 | /// USB endpoint info specific for Bluetooth usage. 11 | /// 12 | internal class UsbBluetoothEndpointInfo 13 | { 14 | /// 15 | /// The Bluetooth endpoint type. 16 | /// 17 | public UsbBluetoothEndpointType Type { get; private set; } 18 | 19 | /// 20 | /// The USB endpoint identifier. 21 | /// 22 | public byte Id { get; private set; } 23 | 24 | /// 25 | /// Creates a new UsbBluetoothEndpoint instance. 26 | /// 27 | /// The Bluetooth endpoint type. 28 | /// The USB endpoint identifier. 29 | public UsbBluetoothEndpointInfo(UsbBluetoothEndpointType type, byte id) 30 | { 31 | Type = type; 32 | Id = id; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/GlobalAssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | [assembly: AssemblyCompany("Huysentruit")] 6 | [assembly: AssemblyProduct("WinBeacon")] 7 | [assembly: AssemblyCopyright("Copyright © 2015 Huysentruit")] 8 | [assembly: AssemblyTrademark("")] 9 | [assembly: AssemblyCulture("")] 10 | 11 | // Setting ComVisible to false makes the types in this assembly not visible 12 | // to COM components. If you need to access a type in this assembly from 13 | // COM, set the ComVisible attribute to true on that type. 14 | [assembly: ComVisible(false)] 15 | 16 | // Version information for an assembly consists of the following four values: 17 | // 18 | // Major Version 19 | // Minor Version 20 | // Build Number 21 | // Revision 22 | // 23 | // You can specify all the values or you can default the Build and Revision Numbers 24 | // by using the '*' as shown below: 25 | // [assembly: AssemblyVersion("1.0.*")] 26 | [assembly: AssemblyVersion("1.0.0.0")] 27 | [assembly: AssemblyFileVersion("1.0.0.0")] 28 | 29 | [assembly: InternalsVisibleTo("WinBeacon.Tests")] 30 | [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // For Moq -------------------------------------------------------------------------------- /samples/SampleConsole/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using WinBeacon; 3 | 4 | namespace SampleConsole 5 | { 6 | class Program 7 | { 8 | static void Main(string[] args) 9 | { 10 | using (var hub = new Hub(0x0A5C, 0x21E8)) 11 | { 12 | hub.BeaconDetected += (sender, e) => Console.WriteLine(e.Beacon.ToString()); 13 | 14 | hub.EddystoneDetected += (sender, e) => 15 | { 16 | switch (e.Eddystone) 17 | { 18 | case EddystoneUid eddystoneUid: 19 | Console.WriteLine($"Eddystone UID: {eddystoneUid}"); 20 | break; 21 | case EddystoneUrl eddystoneUrl: 22 | Console.WriteLine($"Eddystone URL: {eddystoneUrl}"); 23 | break; 24 | } 25 | }; 26 | 27 | hub.EnableAdvertising(new Beacon("B9407F30-F5F8-466E-AFF9-25556B57FE6D", 1000, 2000, -52), TimeSpan.FromMilliseconds(200)); 28 | 29 | Console.ReadKey(); 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/DeviceAddress.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using System; 8 | 9 | namespace WinBeacon.Stack 10 | { 11 | /// 12 | /// Class that holds a 48-bit device address. 13 | /// 14 | public class DeviceAddress 15 | { 16 | /// 17 | /// The device address. 18 | /// 19 | public byte[] Address { get; private set; } 20 | 21 | /// 22 | /// Empty device address. 23 | /// 24 | public static DeviceAddress Empty { get; } = new DeviceAddress(new byte[6]); 25 | 26 | /// 27 | /// Creates a new instance. 28 | /// 29 | /// The 6 address bytes 30 | public DeviceAddress(byte[] address) 31 | { 32 | if (address == null) 33 | throw new ArgumentNullException(nameof(address)); 34 | if (address.Length != 6) 35 | throw new ArgumentOutOfRangeException("address", "Invalid length, should contain 6 bytes"); 36 | Address = address; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/WinBeacon/WinBeacon.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net45;net471;netstandard2.0 5 | true 6 | WinBeacon 7 | 1.0.0.0 8 | WinBeacon 9 | Wouter Huysentruit 10 | WinBeacon 11 | WinBeacon library for detecting and advertising of Bluetooth beacons 12 | https://github.com/huysentruitw/win-beacon 13 | false 14 | MIT 15 | https://github.com/huysentruitw/win-beacon 16 | 17 | 18 | 19 | true 20 | 21 | bin\WinBeacon.xml 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/WinBeacon.Stack.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net45;net471;netstandard2.0 5 | true 6 | WinBeacon.Stack 7 | 1.0.0.0 8 | WinBeacon.Stack 9 | Wouter Huysentruit 10 | WinBeacon 11 | WinBeacon library for detecting and advertising of Bluetooth beacons 12 | https://github.com/huysentruitw/win-beacon 13 | false 14 | MIT 15 | https://github.com/huysentruitw/win-beacon 16 | 17 | 18 | 19 | true 20 | 21 | bin\WinBeacon.Stack.xml 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/Hci/Events/CommandCompleteEvent.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using System; 8 | using System.Collections.Generic; 9 | 10 | namespace WinBeacon.Stack.Hci.Events 11 | { 12 | /// 13 | /// Event that occurs when a command has been executed. 14 | /// 15 | internal class CommandCompleteEvent : Event 16 | { 17 | public int NumberOfCommandsAllowedToSend { get; private set; } 18 | public ushort CommandOpcode { get; private set; } 19 | public byte CommandParameterDataLength { get; private set; } 20 | public byte[] ResultData { get; private set; } 21 | 22 | public static CommandCompleteEvent Parse(EventCode code, Queue data) 23 | { 24 | if (data.Count < 4) 25 | return null; 26 | return new CommandCompleteEvent 27 | { 28 | Code = code, 29 | NumberOfCommandsAllowedToSend = data.Dequeue(), 30 | CommandOpcode = (ushort)(data.Dequeue() + (data.Dequeue() << 8)), 31 | CommandParameterDataLength = data.Dequeue(), 32 | ResultData = data.DequeueAll() 33 | }; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/Hci/Commands/LeSetScanParametersCommand.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using System; 8 | using System.Diagnostics; 9 | using WinBeacon.Stack.Hci.Opcodes; 10 | using WinBeacon.Stack.Hci.Parameters; 11 | 12 | namespace WinBeacon.Stack.Hci.Commands 13 | { 14 | internal class LeSetScanParametersCommand : Command 15 | { 16 | public LeSetScanParametersCommand(bool activeScanning, ushort intervalInMs, ushort windowInMs, bool useRandomAddress, bool onlyAcceptWhitelistedAdvertisers) 17 | : base(OpcodeGroup.LeController, (int)LeControllerOpcode.SetScanParameters) 18 | { 19 | ushort intervalCode = (ushort)Math.Ceiling(intervalInMs / 0.625); 20 | ushort windowCode = (ushort)Math.Ceiling(windowInMs / 0.625); 21 | Debug.Assert(intervalCode >= 0x0004); 22 | Debug.Assert(intervalCode <= 0x4000); 23 | Debug.Assert(windowCode >= 0x0004); 24 | Debug.Assert(windowCode <= 0x4000); 25 | Parameters.Add(new BoolCommandParameter(activeScanning)); 26 | Parameters.Add(new UshortCommandParameter(intervalCode)); 27 | Parameters.Add(new UshortCommandParameter(windowCode)); 28 | Parameters.Add(new BoolCommandParameter(useRandomAddress)); 29 | Parameters.Add(new BoolCommandParameter(onlyAcceptWhitelistedAdvertisers)); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/WinBeacon.Stack.Tests/Hci/ParameterTests.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using NUnit.Framework; 8 | using WinBeacon.Stack.Hci.Parameters; 9 | 10 | namespace WinBeacon.Stack.Tests.Hci 11 | { 12 | [TestFixture] 13 | public class ParameterTests 14 | { 15 | [Test] 16 | public void Parameter_BoolCommandParameter() 17 | { 18 | var parameter = new BoolCommandParameter(false); 19 | Assert.AreEqual(new byte[] { 0x00 }, parameter.ToByteArray()); 20 | parameter = new BoolCommandParameter(true); 21 | Assert.AreEqual(new byte[] { 0x01 }, parameter.ToByteArray()); 22 | } 23 | 24 | [Test] 25 | public void Parameter_ByteArrayCommandParameter() 26 | { 27 | var data = new byte[] { 0x01, 0xAA, 0x55 }; 28 | var parameter = new ByteArrayCommandParameter(data); 29 | Assert.AreEqual(data, parameter.ToByteArray()); 30 | } 31 | 32 | [Test] 33 | public void Parameter_ByteCommandParameter() 34 | { 35 | var parameter = new ByteCommandParameter(0x57); 36 | Assert.AreEqual(new byte[] { 0x57 }, parameter.ToByteArray()); 37 | } 38 | 39 | [Test] 40 | public void Parameter_UshortCommandParameter() 41 | { 42 | var parameter = new UshortCommandParameter(0x5865); 43 | Assert.AreEqual(new byte[] { 0x65, 0x58 }, parameter.ToByteArray()); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/WinBeacon.Stack.Tests/Controllers/LibUsbLeControllerTests.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using System.Threading; 8 | using NUnit.Framework; 9 | using WinBeacon.Stack.Controllers; 10 | 11 | namespace WinBeacon.Stack.Tests.Controllers 12 | { 13 | [TestFixture] 14 | public class LibUsbLeControllerTests 15 | { 16 | private const int vid = 0x050D; 17 | private const int pid = 0x065A; 18 | 19 | /// 20 | /// This is a live test that needs some pre-requisites: 21 | /// * A BLE compatible dongle with WinUSB driver 22 | /// * The correct vid and pid combination in the consts above 23 | /// * A beacon that broadcasts at minimum 2Hz rate (iPad users can use the BLEBeacon app to advertise as a beacon) 24 | /// 25 | [Test] 26 | [Ignore("Can only be used with a real device")] 27 | public void LibUsbLeController_WaitForLeMetaEvent() 28 | { 29 | var leMetaEventReceived = false; 30 | using (var controller = new LibUsbLeController(vid, pid)) 31 | { 32 | controller.Open(); 33 | controller.EnableScanning(); 34 | controller.LeMetaEventReceived += (sender, e) => 35 | { 36 | if (e.LeMetaEvent.Code == Stack.Hci.EventCode.LeMeta) 37 | leMetaEventReceived = true; 38 | }; 39 | Thread.Sleep(1 * 1000); 40 | } 41 | Assert.IsTrue(leMetaEventReceived, "No meta event received"); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/Hci/Event.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using System.Collections.Generic; 8 | using WinBeacon.Stack.Hci.Events; 9 | 10 | namespace WinBeacon.Stack.Hci 11 | { 12 | /// 13 | /// Base class for Bluetooth events. 14 | /// 15 | public class Event 16 | { 17 | /// 18 | /// The event code. 19 | /// 20 | public EventCode Code { get; protected set; } 21 | 22 | /// 23 | /// Creates an event object. 24 | /// 25 | protected Event() 26 | { 27 | } 28 | 29 | /// 30 | /// Creates an event object with event code. 31 | /// 32 | protected Event(EventCode code) 33 | { 34 | Code = code; 35 | } 36 | 37 | internal static Event Parse(byte[] data) 38 | { 39 | return Parse(new Queue(data)); 40 | } 41 | 42 | internal static Event Parse(Queue data) 43 | { 44 | if (data.Count < 2) 45 | return null; 46 | EventCode code = (EventCode)data.Dequeue(); 47 | int payloadSize = data.Dequeue(); 48 | switch (code) 49 | { 50 | case EventCode.CommandComplete: 51 | return CommandCompleteEvent.Parse(code, data); 52 | case EventCode.LeMeta: 53 | return LeMetaEvent.Parse(code, data); 54 | default: 55 | return new Event(code); 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/ILeController.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using System; 8 | 9 | namespace WinBeacon.Stack 10 | { 11 | /// 12 | /// Interface for Bluetooth Low Energy controller classes. 13 | /// 14 | public interface ILeController : IDisposable 15 | { 16 | /// 17 | /// Open underlying transport connection. 18 | /// 19 | void Open(); 20 | 21 | /// 22 | /// Close underlying transport connection. 23 | /// 24 | void Close(); 25 | 26 | /// 27 | /// Enable Low Energy device scanning. 28 | /// 29 | void EnableScanning(); 30 | 31 | /// 32 | /// Disable Low Energy device scanning. 33 | /// 34 | void DisableScanning(); 35 | 36 | /// 37 | /// Enable Low Energy advertising. 38 | /// 39 | /// The advertisement data. 40 | /// Interval should be between 20 and 10240 ms. Defaults to 1280 ms. 41 | void EnableAdvertising(byte[] advertisementData, int advertisingIntervalInMs = 1280); 42 | 43 | /// 44 | /// Disable Low Energy advertising. 45 | /// 46 | void DisableAdvertising(); 47 | 48 | /// 49 | /// Fired for each received Low Energy meta event. 50 | /// 51 | event EventHandler LeMetaEventReceived; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test/WinBeacon.Stack.Tests/Extensions/QueueExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using System; 8 | using System.Collections.Generic; 9 | using NUnit.Framework; 10 | 11 | namespace WinBeacon.Stack.Tests.Extensions 12 | { 13 | [TestFixture] 14 | public class QueueExtensionsTests 15 | { 16 | [Test] 17 | public void QueueExtensions_DequeueAll() 18 | { 19 | var input = new byte[] { 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0 }; 20 | var queue = new Queue(input); 21 | byte[] output = queue.DequeueAll(); 22 | Assert.AreEqual(input, output); 23 | } 24 | 25 | [Test] 26 | public void QueueExtensions_Dequeue_Exact() 27 | { 28 | var input = new byte[] { 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0 }; 29 | var queue = new Queue(input); 30 | byte[] output = queue.Dequeue(3); 31 | Assert.AreEqual(new byte[] { 0x12, 0x34, 0x56 }, output); 32 | } 33 | 34 | [Test] 35 | public void QueueExtensions_Dequeue_TooMuch() 36 | { 37 | var input = new byte[] { 0x9A, 0xBC, 0xDE, 0xF0 }; 38 | var queue = new Queue(input); 39 | 40 | Assert.Throws(() => queue.Dequeue(6)); 41 | } 42 | 43 | [Test] 44 | public void QueueExtensions_Enqueue() 45 | { 46 | var input = new byte[] { 0x9A, 0xBC, 0xDE, 0xF0 }; 47 | var queue = new Queue(); 48 | queue.Enqueue(input); 49 | Assert.AreEqual(input.Length, queue.Count); 50 | Assert.AreEqual(input, queue.DequeueAll()); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/Extensions/QueueExtensions.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using System; 8 | using System.Collections.Generic; 9 | 10 | namespace WinBeacon.Stack 11 | { 12 | /// 13 | /// Queue extension methods. 14 | /// 15 | public static class QueueExtensions 16 | { 17 | /// 18 | /// Dequeue all items from the queue. 19 | /// 20 | /// Item type. 21 | /// The queue. 22 | /// Array of dequeued items. 23 | public static T[] DequeueAll(this Queue queue) 24 | { 25 | var result = new List(); 26 | while (queue.Count > 0) 27 | result.Add(queue.Dequeue()); 28 | return result.ToArray(); 29 | } 30 | 31 | /// 32 | /// Dequeue the specified number of items. 33 | /// 34 | /// Item type. 35 | /// The queue. 36 | /// Number of items to dequeue. 37 | /// Array of dequeued items. 38 | public static T[] Dequeue(this Queue queue, int count) 39 | { 40 | var result = new List(); 41 | for (int i = 0; i < count; i++) 42 | result.Add(queue.Dequeue()); 43 | return result.ToArray(); 44 | } 45 | 46 | /// 47 | /// Enqueue an array of items. 48 | /// 49 | /// Item type. 50 | /// The queue. 51 | /// The items. 52 | public static void Enqueue(this Queue queue, IEnumerable items) 53 | { 54 | foreach (T item in items) 55 | queue.Enqueue(item); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/Transports/LibUsb/LibUsbDevice.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using System.Collections.Generic; 8 | using LibUsbDotNet; 9 | using LibUsbDotNet.Info; 10 | using LibUsbDotNet.Main; 11 | 12 | namespace WinBeacon.Stack.Transports.LibUsb 13 | { 14 | /// 15 | /// LibUsbDotNet.UsbDevice wrapper that implements ILibUsbDevice. 16 | /// 17 | internal class LibUsbDevice : ILibUsbDevice 18 | { 19 | private UsbDevice _usbDevice; 20 | 21 | public int Vid { get; private set; } 22 | public int Pid { get; private set; } 23 | 24 | public LibUsbDevice(int vid, int pid) 25 | { 26 | Vid = vid; 27 | Pid = pid; 28 | } 29 | 30 | public void Open() 31 | { 32 | if (_usbDevice != null) 33 | return; 34 | _usbDevice = UsbDevice.OpenUsbDevice(new UsbDeviceFinder(Vid, Pid)) 35 | ?? throw new WinBeaconException("USB device not found, check VID & PID"); 36 | } 37 | 38 | public void Close() 39 | { 40 | if (_usbDevice == null) 41 | return; 42 | _usbDevice.Close(); 43 | _usbDevice = null; 44 | } 45 | 46 | public IEnumerable Configs => _usbDevice.Configs; 47 | 48 | public UsbEndpointReader OpenEndpointReader(ReadEndpointID readEndpointID) 49 | { 50 | return _usbDevice.OpenEndpointReader(readEndpointID); 51 | } 52 | 53 | public UsbEndpointWriter OpenEndpointWriter(WriteEndpointID writeEndpointID) 54 | { 55 | return _usbDevice.OpenEndpointWriter(writeEndpointID); 56 | } 57 | 58 | public bool ControlTransfer(ref UsbSetupPacket setupPacket, object buffer, int bufferLength, out int lengthTransferred) 59 | { 60 | return _usbDevice.ControlTransfer(ref setupPacket, buffer, bufferLength, out lengthTransferred); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/Hci/Command.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using System; 8 | using System.Collections.Generic; 9 | using WinBeacon.Stack.Hci.Events; 10 | 11 | namespace WinBeacon.Stack.Hci 12 | { 13 | internal class Command 14 | { 15 | public ushort Opcode { get; private set; } 16 | public List Parameters { get; private set; } 17 | 18 | internal Command(OpcodeGroup opcodeGroup, int opcodeCommand) 19 | { 20 | Opcode = (ushort)(opcodeCommand | ((byte)opcodeGroup << 10)); 21 | Parameters = new List(); 22 | } 23 | 24 | public byte[] ToByteArray() 25 | { 26 | var result = new List(); 27 | result.Add(Opcode.LoByte()); 28 | result.Add(Opcode.HiByte()); 29 | result.Add(0); // Length placeholder 30 | Parameters.ForEach(param => result.AddRange(param.ToByteArray())); 31 | result[2] = (byte)(result.Count - 3); 32 | return result.ToArray(); 33 | } 34 | 35 | internal Action CommandCompleteCallback = null; 36 | 37 | internal virtual void OnCommandComplete(CommandCompleteEvent e) 38 | { 39 | if (CommandCompleteCallback != null) 40 | CommandCompleteCallback(this, e); 41 | } 42 | } 43 | 44 | internal abstract class Command : Command 45 | { 46 | internal Command(OpcodeGroup opcodeGroup, int opcodeCommand) 47 | : base(opcodeGroup, opcodeCommand) 48 | { 49 | } 50 | 51 | internal abstract TResult ParseCommandResult(CommandCompleteEvent e); 52 | 53 | internal new Action CommandCompleteCallback = null; 54 | 55 | internal override void OnCommandComplete(CommandCompleteEvent e) 56 | { 57 | if (CommandCompleteCallback != null) 58 | CommandCompleteCallback(this, ParseCommandResult(e)); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test/WinBeacon.Stack.Tests/Hci/EventTests.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using NUnit.Framework; 8 | using WinBeacon.Stack.Hci; 9 | using WinBeacon.Stack.Hci.Events; 10 | 11 | namespace WinBeacon.Stack.Tests.Hci 12 | { 13 | [TestFixture] 14 | public class EventTests 15 | { 16 | [Test] 17 | public void Event_NullEvent() 18 | { 19 | var e = Event.Parse(new byte[] { 0x12 }); 20 | Assert.IsNull(e); 21 | } 22 | 23 | [Test] 24 | public void Event_SimpleEvent() 25 | { 26 | var e = Event.Parse(new byte[] { 0x12, 0x00 }); 27 | Assert.AreEqual(EventCode.RoleChanged, e.Code); 28 | } 29 | 30 | [Test] 31 | public void Event_CommandCompleteEventParse() 32 | { 33 | var e = (Event.Parse(new byte[] { 0x0E, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70 }) as CommandCompleteEvent); 34 | Assert.AreEqual(EventCode.CommandComplete, e.Code); 35 | Assert.AreEqual(0x4030, e.CommandOpcode); 36 | Assert.AreEqual(0x50, e.CommandParameterDataLength); 37 | Assert.AreEqual(0x20, e.NumberOfCommandsAllowedToSend); 38 | Assert.AreEqual(new byte[] { 0x60, 0x70 }, e.ResultData); 39 | } 40 | 41 | [Test] 42 | public void Event_LeAdvertisingEventParse() 43 | { 44 | var e = (Event.Parse(new byte[] { 0x3E, 0x10, 0x02, 0x01, 0x02, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x03, 0xBB, 0xCC, 0xDD, 0xEE }) as LeMetaEvent); 45 | Assert.AreEqual(EventCode.LeMeta, e.Code); 46 | Assert.AreEqual(LeMetaEvent.LeMetaSubEvent.AdvertisingReport, e.SubEvent); 47 | Assert.AreEqual(1, e.AdvertisingEvents.Length); 48 | var ae = e.AdvertisingEvents[0]; 49 | Assert.AreEqual(new byte[] { 0x99, 0x88, 0x77, 0x66, 0x55, 0x44 }, ae.Address); 50 | Assert.AreEqual(0x33, ae.AddressType); 51 | Assert.AreEqual(LeAdvertisingEventType.ScanInd, ae.EventType); 52 | Assert.AreEqual(new byte[] { 0xBB, 0xCC, 0xDD }, ae.Payload); 53 | Assert.AreEqual(-18, ae.Rssi); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/Hci/Events/LeAdvertisingEvent.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Text; 11 | 12 | namespace WinBeacon.Stack.Hci.Events 13 | { 14 | /// 15 | /// Low energy advertising event. 16 | /// 17 | public class LeAdvertisingEvent 18 | { 19 | /// 20 | /// The event type. 21 | /// 22 | public LeAdvertisingEventType EventType { get; private set; } 23 | /// 24 | /// The address type. 25 | /// 26 | public byte AddressType { get; private set; } 27 | /// 28 | /// The address. 29 | /// 30 | public byte[] Address { get; private set; } 31 | /// 32 | /// The payload. 33 | /// 34 | public byte[] Payload { get; private set; } 35 | /// 36 | /// The RSSI. 37 | /// 38 | public sbyte Rssi { get; private set; } 39 | 40 | /// 41 | /// Creates a string representation of this object. 42 | /// 43 | /// String representation. 44 | public override string ToString() 45 | { 46 | var sb = new StringBuilder(); 47 | sb.AppendFormat("EventType: {0}", EventType); 48 | sb.AppendFormat("AddressType: {0}", AddressType); 49 | sb.AppendFormat("Address: {0}", BitConverter.ToString(Address)); 50 | sb.AppendFormat("Payload = {0}", BitConverter.ToString(Payload).Replace("-", "")); 51 | sb.AppendFormat("RSSI = {0}", Rssi); 52 | return sb.ToString(); 53 | } 54 | 55 | internal static LeAdvertisingEvent Parse(Queue data) 56 | { 57 | return new LeAdvertisingEvent 58 | { 59 | EventType = (LeAdvertisingEventType)data.Dequeue(), 60 | AddressType = data.Dequeue(), 61 | Address = data.Dequeue(6).Reverse().ToArray(), 62 | Payload = data.Dequeue(data.Dequeue()), 63 | Rssi = (sbyte)data.Dequeue() 64 | }; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/WinBeacon/Extensions/BeaconExtensions.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Text.RegularExpressions; 11 | 12 | namespace WinBeacon 13 | { 14 | /// 15 | /// Beacon extension methods. 16 | /// 17 | public static class BeaconExtensions 18 | { 19 | /// 20 | /// Gets the calculated range in meters. 21 | /// 22 | /// The beacon instance. 23 | /// The calculated range in meters. 24 | public static double GetRange(this Beacon beacon) 25 | { 26 | double dbRatio = beacon.CalibratedTxPower - beacon.Rssi; 27 | double linearRatio = Math.Pow(10, dbRatio / 10); 28 | return Math.Sqrt(linearRatio); 29 | } 30 | 31 | /// 32 | /// Gets the calculated range in meters using a curve that approximates the iOS ranging. 33 | /// 34 | /// The beacon instance. 35 | /// The calculated range in meters. 36 | public static double GetApproximateIosRange(this Beacon beacon) 37 | { 38 | if (beacon.Rssi == 0) 39 | return -1; 40 | var ratio = (double)beacon.Rssi / (double)beacon.CalibratedTxPower; 41 | if (ratio < 1.0) 42 | return Math.Pow(ratio, 10); 43 | return 0.89979 * Math.Pow(ratio, 7.7095) + 0.111; 44 | } 45 | 46 | internal static string ToLittleEndianFormattedUuidString(this byte[] uuid) 47 | { 48 | var digits = new Queue(BitConverter.ToString(uuid).Replace("-", "").ToLower()); 49 | return new string((from c in "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" select c == '-' ? '-' : digits.Dequeue()).ToArray()); 50 | } 51 | 52 | internal static byte[] FromLittleEndianFormattedUuidString(this string uuid) 53 | { 54 | if (!Regex.IsMatch(uuid, @"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", RegexOptions.IgnoreCase)) 55 | throw new FormatException("Incorrect uuid format"); 56 | return (from Match m in Regex.Matches(uuid, @"[0-9a-f]{2}", RegexOptions.IgnoreCase) select Convert.ToByte(m.Value, 16)).ToArray(); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/Extensions/LibUsbDeviceExtensions.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Diagnostics; 10 | using System.Linq; 11 | using LibUsbDotNet; 12 | using LibUsbDotNet.Main; 13 | using WinBeacon.Stack.Transport; 14 | using WinBeacon.Stack.Transports.LibUsb; 15 | 16 | namespace WinBeacon.Stack 17 | { 18 | internal static class WinBeaconLibUsbExtensions 19 | { 20 | public static IEnumerable EnumerateBluetoothEndpointInfo(this ILibUsbDevice usbDevice) 21 | { 22 | var config0 = usbDevice.Configs.FirstOrDefault() 23 | ?? throw new WinBeaconException("USB device has no configurations"); 24 | 25 | var interface0Info = config0.InterfaceInfoList.FirstOrDefault() 26 | ?? throw new WinBeaconException("USB configuration does not contain an interface"); 27 | 28 | foreach (var endpointInfo in interface0Info.EndpointInfoList) 29 | { 30 | switch (endpointInfo.Descriptor.Attributes & 0x03) 31 | { 32 | case 0x02: 33 | yield return new UsbBluetoothEndpointInfo( 34 | type: (endpointInfo.Descriptor.EndpointID & 0x80) == 0x80 ? UsbBluetoothEndpointType.AclDataIn : UsbBluetoothEndpointType.AclDataOut, 35 | id: endpointInfo.Descriptor.EndpointID); 36 | break; 37 | case 0x03: 38 | yield return new UsbBluetoothEndpointInfo( 39 | type: UsbBluetoothEndpointType.Events, 40 | id: endpointInfo.Descriptor.EndpointID); 41 | break; 42 | } 43 | } 44 | } 45 | 46 | public static void SubscribeForDataReceived(this UsbEndpointBase endpoint, Action handler) 47 | { 48 | Debug.Assert(endpoint is UsbEndpointReader); 49 | (endpoint as UsbEndpointReader).SubscribeForDataReceived(handler); 50 | } 51 | 52 | public static void SubscribeForDataReceived(this UsbEndpointReader endpointReader, Action handler) 53 | { 54 | endpointReader.DataReceived += (sender, e) => 55 | { 56 | var data = new byte[e.Count]; 57 | if (e.Count > 0) 58 | Buffer.BlockCopy(e.Buffer, 0, data, 0, e.Count); 59 | handler(data); 60 | }; 61 | endpointReader.DataReceivedEnabled = true; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/Hci/Events/LeMetaEvent.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using System.Collections.Generic; 8 | using System.Text; 9 | 10 | namespace WinBeacon.Stack.Hci.Events 11 | { 12 | /// 13 | /// Low energy meta event. 14 | /// 15 | public class LeMetaEvent : Event 16 | { 17 | /// 18 | /// Low energy meta sub event. 19 | /// 20 | public enum LeMetaSubEvent : byte 21 | { 22 | /// 23 | /// Connection complete event. 24 | /// 25 | ConnectionComplete = 0x01, 26 | /// 27 | /// Advertising report event. 28 | /// 29 | AdvertisingReport = 0x02 30 | } 31 | 32 | /// 33 | /// The sub event of this meta event. 34 | /// 35 | public LeMetaSubEvent SubEvent { get; private set; } 36 | /// 37 | /// Advertising events included in this meta event. 38 | /// 39 | public LeAdvertisingEvent[] AdvertisingEvents { get; private set; } 40 | 41 | /// 42 | /// Creates a string representation of this object. 43 | /// 44 | /// String representation. 45 | public override string ToString() 46 | { 47 | var sb = new StringBuilder(); 48 | sb.AppendFormat("SubEvent = {0}", SubEvent); 49 | if (AdvertisingEvents != null) 50 | foreach (var e in AdvertisingEvents) 51 | sb.AppendFormat(", {0}", e); 52 | return sb.ToString(); 53 | } 54 | 55 | internal static LeMetaEvent Parse(EventCode code, Queue data) 56 | { 57 | var subEvent = (LeMetaSubEvent)data.Dequeue(); 58 | LeAdvertisingEvent[] events = null; 59 | switch (subEvent) 60 | { 61 | case LeMetaSubEvent.ConnectionComplete: 62 | // Not implemented 63 | break; 64 | case LeMetaSubEvent.AdvertisingReport: 65 | events = ParseAdvertisingReport(data); 66 | break; 67 | } 68 | return new LeMetaEvent 69 | { 70 | Code = code, 71 | SubEvent = subEvent, 72 | AdvertisingEvents = events 73 | }; 74 | } 75 | 76 | private static LeAdvertisingEvent[] ParseAdvertisingReport(Queue data) 77 | { 78 | int numberOfEvents = data.Dequeue(); 79 | var events = new LeAdvertisingEvent[numberOfEvents]; 80 | for (int i = 0; i < numberOfEvents; i++) 81 | events[i] = LeAdvertisingEvent.Parse(data); 82 | return events; 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /ReadMe.md: -------------------------------------------------------------------------------- 1 | # WinBeacon 2 | 3 | [![Build status](https://ci.appveyor.com/api/projects/status/qjr2y51nq77lihny/branch/master?svg=true)](https://ci.appveyor.com/project/huysentruitw/win-beacon/branch/master) 4 | 5 | ## Overview 6 | 7 | WinBeacon is a managed .NET (C#) library with a minimal Bluetooth LE Stack that is able to detect and act as iBeacon¹ and Eddystone. This stack doesn't support BLE devices, only the detection and transmission of BLE advertisement packets used by beacons. 8 | 9 | ## Supported operating systems 10 | 11 | * Windows XP (not tested, but it should work) 12 | * Windows 7 13 | * Windows 8 14 | * Windows 10* 15 | 16 | \* For Windows 10, you should be able to use [BluetoothLEAdvertisementWatcher](https://msdn.microsoft.com/en-us/library/windows.devices.bluetooth.advertisement.bluetoothleadvertisementwatcher.aspx) instead of this library. 17 | 18 | ## Supported BT4.0 LE dongles 19 | 20 | | Manufacturer | Product | Chipset | VID / PID | Compatible | 21 | | ------------ |:------- |:------- |:--------- |:---------- | 22 | | ASUS | USB-BT400 | BCM20702A0 | VID_0B05 PID_17CB | Yes | 23 | | Belkin | Mini Bluetooth 4.0 Adapter Class 2.10M | BCM20702A0 | VID_050D PID_065A | Yes | 24 | | Pluggable | USB Bluetooth 4.0 Low Energy Micro Adapter | BCM20702A0 | VID_0A5C PID_21E8 | Yes | 25 | | CSR | USB Bluetooth 4.0 | CSR8510 | VID_0A12 PID_0001 | Yes | 26 | 27 | If anyone can test with other BT4.0 dongle types, please let me know how it works out or send us a pull request. 28 | 29 | ## Installation 30 | 31 | This library needs raw USB access to a BT4.0 dongle. Therefore you should replace the original driver of the dongle with a WinUSB driver. 32 | This also means that the default Bluetooth stack is no longer used and Windows will no longer detect the dongle as a Bluetooth dongle until you re-install the original drivers. 33 | 34 | To replace or create a WinUSB driver for the BT4.0 dongle, we advise you to use the [Zadig tool](http://zadig.akeo.ie/). 35 | 36 | ## Get it on NuGet 37 | 38 | Install-Package WinBeacon 39 | 40 | ## Usage 41 | 42 | ### Detecting beacons 43 | 44 | ```C# 45 | using (var hub = new Hub(0x050D, 0x065A)) 46 | { 47 | hub.BeaconDetected += (sender, e) => 48 | { 49 | Console.WriteLine("Detected beacon: {0}", e.Beacon); 50 | }; 51 | Console.ReadKey(); 52 | } 53 | ``` 54 | 55 | ### Advertise as a beacon 56 | 57 | ```C# 58 | using (var hub = new Hub(0x050D, 0x065A)) 59 | { 60 | hub.EnableAdvertising(new Beacon("B9407F30-F5F8-466E-AFF9-25556B57FE6D", 1000, 2000, -52)); 61 | Console.ReadKey(); 62 | } 63 | ``` 64 | 65 | ### Detecting Eddystone 66 | 67 | Currently only the detection of Eddystone UID and URL is supported. 68 | 69 | ```C# 70 | using (var hub = new Hub(0x050D, 0x065A)) 71 | { 72 | hub.EddystoneDetected += (sender, e) => 73 | { 74 | switch (e.Eddystone) 75 | { 76 | case EddystoneUid eddystoneUid: 77 | Console.WriteLine($"Eddystone UID: {eddystoneUid}"); 78 | break; 79 | case EddystoneUrl eddystoneUrl: 80 | Console.WriteLine($"Eddystone URL: {eddystoneUrl}"); 81 | break; 82 | } 83 | }; 84 | Console.ReadKey(); 85 | } 86 | ``` 87 | 88 | ¹ iBeacon is a trademark of Apple inc. 89 | -------------------------------------------------------------------------------- /test/WinBeacon.Tests/BeaconTests.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using System.Collections.Generic; 8 | using NUnit.Framework; 9 | using WinBeacon.Stack.Hci.Events; 10 | 11 | namespace WinBeacon.Tests 12 | { 13 | [TestFixture] 14 | public class BeaconTests 15 | { 16 | private Beacon beacon; 17 | 18 | [SetUp] 19 | public void Setup() 20 | { 21 | beacon = new Beacon("4fe5d5f6-abce-ddfe-1587-123d1a4b567f", 1234, 5678, -48, 0xAABB) { Rssi = -52 }; 22 | } 23 | 24 | [Test] 25 | public void Beacon_Constructor() 26 | { 27 | Assert.AreEqual("4fe5d5f6-abce-ddfe-1587-123d1a4b567f", beacon.Uuid); 28 | Assert.AreEqual(1234, beacon.Major); 29 | Assert.AreEqual(5678, beacon.Minor); 30 | Assert.AreEqual(-48, beacon.CalibratedTxPower); 31 | Assert.AreEqual(0xAABB, beacon.CompanyId); 32 | } 33 | 34 | [Test] 35 | public void Beacon_ToAdvertisingData() 36 | { 37 | var data = new byte[] { 38 | 0x02, 0x01, 0x1A, 0x1A, 0xFF, 0xAA, 0xBB, 0x02, 0x15, 0x4F, 0xE5, 0xD5, 0xF6, 0xAB, 0xCE, 39 | 0xDD, 0xFE, 0x15, 0x87, 0x12, 0x3D, 0x1A, 0x4B, 0x56, 0x7F, 0x04, 0xD2, 0x16, 0x2E, 0xD0, 40 | 0x00 41 | }; 42 | Assert.AreEqual(data, beacon.ToAdvertisingData()); 43 | } 44 | 45 | [Test] 46 | public void Beacon_ParseDataWithFlags() 47 | { 48 | var data = new byte[] { 49 | 0x00, 0x00, 0xBC, 0x9A, 0x78, 0x56, 0x34, 0x12, 0x1E, 0x02, 0x01, 0x1A, 0x1A, 0xFF, 0xAA, 50 | 0xBB, 0x02, 0x15, 0x4F, 0xE5, 0xD5, 0xF6, 0xAB, 0xCE, 0xDD, 0xFE, 0x15, 0x87, 0x12, 0x3D, 51 | 0x1A, 0x4B, 0x56, 0x7F, 0x04, 0xD2, 0x16, 0x2E, 0xD0, 0xCC 52 | }; 53 | var queue = new Queue(data); 54 | var beacon = Beacon.Parse(LeAdvertisingEvent.Parse(queue)); 55 | Assert.AreEqual(new byte[] { 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC }, beacon.Address); 56 | Assert.AreEqual("4fe5d5f6-abce-ddfe-1587-123d1a4b567f", beacon.Uuid); 57 | Assert.AreEqual(1234, beacon.Major); 58 | Assert.AreEqual(5678, beacon.Minor); 59 | Assert.AreEqual(-48, beacon.CalibratedTxPower); 60 | Assert.AreEqual(0xAABB, beacon.CompanyId); 61 | Assert.AreEqual(-52, beacon.Rssi); 62 | Assert.IsFalse(beacon.IsAppleIBeacon); 63 | } 64 | 65 | [Test] 66 | public void Beacon_ParseDataWithoutFlags() 67 | { 68 | var data = new byte[] { 69 | 0x00, 0x00, 0x28, 0xEA, 0x6B, 0xB8, 0x5F, 0xD0, 0x1B, 0x1A, 0xFF, 0x4C, 0x00, 0x02, 0x15, 70 | 0xA4, 0x95, 0x00, 0x00, 0xC5, 0xB1, 0x4B, 0x44, 0xB5, 0x12, 0x13, 0x70, 0xF0, 0x2D, 0x74, 71 | 0xDE, 0x00, 0x02, 0x6D, 0x65, 0xC5, 0xBC 72 | }; 73 | var queue = new Queue(data); 74 | var beacon = Beacon.Parse(LeAdvertisingEvent.Parse(queue)); 75 | Assert.NotNull(beacon); 76 | Assert.AreEqual(0x4C00, beacon.CompanyId); 77 | Assert.AreEqual(-59, beacon.CalibratedTxPower); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /test/WinBeacon.Tests/EddystoneUidTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using NUnit.Framework; 3 | using WinBeacon.Stack.Hci.Events; 4 | 5 | namespace WinBeacon.Tests 6 | { 7 | [TestFixture] 8 | public class EddystoneUidTests 9 | { 10 | [Test] 11 | public void Eddystone_Parse_EddystoneUidAdvertisingEvent_ShouldNotBeNull() 12 | { 13 | // Act 14 | var eddystone = Eddystone.Parse(EddystoneUidAdvertisingEvent); 15 | 16 | // Assert 17 | Assert.That(eddystone, Is.Not.Null); 18 | } 19 | 20 | [Test] 21 | public void Eddystone_Parse_EddystoneUidAdvertisingEvent_ShouldNotBeOfTypeEddystoneUid() 22 | { 23 | // Act 24 | var eddystone = Eddystone.Parse(EddystoneUidAdvertisingEvent); 25 | 26 | // Assert 27 | Assert.That(eddystone, Is.InstanceOf()); 28 | } 29 | 30 | [Test] 31 | public void Eddystone_Parse_EddystoneUidAdvertisingEvent_ShouldReturnAddress() 32 | { 33 | // Act 34 | var eddystone = Eddystone.Parse(EddystoneUidAdvertisingEvent) as EddystoneUid; 35 | 36 | // Assert 37 | Assert.That(eddystone.Address, Is.EqualTo(new byte[] { 0xA6, 0xA5, 0xA4, 0xA3, 0xA2, 0xA1 })); 38 | } 39 | 40 | [Test] 41 | public void Eddystone_Parse_EddystoneUidAdvertisingEvent_ShouldReturnRssi() 42 | { 43 | // Act 44 | var eddystone = Eddystone.Parse(EddystoneUidAdvertisingEvent) as EddystoneUid; 45 | 46 | // Assert 47 | Assert.That(eddystone.Rssi, Is.EqualTo(-52)); 48 | } 49 | 50 | [Test] 51 | public void Eddystone_Parse_EddystoneUidAdvertisingEvent_ShouldReturnNamespace() 52 | { 53 | // Act 54 | var eddystone = Eddystone.Parse(EddystoneUidAdvertisingEvent) as EddystoneUid; 55 | 56 | // Assert 57 | Assert.That(eddystone.Namespace, Is.EqualTo(new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A })); 58 | } 59 | 60 | [Test] 61 | public void Eddystone_Parse_EddystoneUidAdvertisingEvent_ShouldReturnInstance() 62 | { 63 | // Act 64 | var eddystone = Eddystone.Parse(EddystoneUidAdvertisingEvent) as EddystoneUid; 65 | 66 | // Assert 67 | Assert.That(eddystone.Instance, Is.EqualTo(new byte[] { 0x11, 0x12, 0x13, 0x14, 0x15, 0x16 })); 68 | } 69 | 70 | [Test] 71 | public void Eddystone_Parse_EddystoneUidAdvertisingEvent_ShouldReturnCalibratedTxPower() 72 | { 73 | // Act 74 | var eddystone = Eddystone.Parse(EddystoneUidAdvertisingEvent) as EddystoneUid; 75 | 76 | // Assert 77 | Assert.That(eddystone.CalibratedTxPower, Is.EqualTo(-18)); 78 | } 79 | 80 | private static readonly LeAdvertisingEvent EddystoneUidAdvertisingEvent = LeAdvertisingEvent.Parse(new Queue(new byte[] 81 | { 82 | 0x02, // Low Energy Advertising event type: Scan IND 83 | 0x00, // Address type 84 | 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, // Bluetooth MAC address 85 | 0x1F, // Payload length 86 | 0x02, 0x01, 0x06, // Flags data 87 | 0x03, 0x03, 0xAA, 0xFE, // Complete list of 16-bit Service UUID (including 16-bit Eddystone UUID) 88 | 0x17, 89 | 0x16, // Service Data data type value 90 | 0xAA, 0xFE, // 16-bit Eddystone UUID 91 | 0x00, // UID frame type 92 | 0xEE, // Calibrated TX power 93 | 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, // Namespace 94 | 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, // Instance 95 | 0x00, 0x00, // Reserved for future use 96 | 0xCC, // RSSI 97 | })); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /test/WinBeacon.Stack.Tests/Transports/LibUsb/LibUsbTransportTests.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using LibUsbDotNet; 10 | using LibUsbDotNet.Info; 11 | using LibUsbDotNet.Main; 12 | using Moq; 13 | using NUnit.Framework; 14 | using WinBeacon.Stack.Hci; 15 | using WinBeacon.Stack.Transports.LibUsb; 16 | 17 | namespace WinBeacon.Stack.Tests.Transports.LibUsb 18 | { 19 | [TestFixture] 20 | public class LibUsbTransportTests 21 | { 22 | [Test] 23 | public void LibUsbTransport_Constructor() 24 | { 25 | using (var transport = new LibUsbTransport(new LibUsbDevice(0x1234, 0x5678))) 26 | { 27 | Assert.AreEqual(0x1234, transport.Vid); 28 | Assert.AreEqual(0x5678, transport.Pid); 29 | } 30 | } 31 | 32 | [Test] 33 | public void LibUsbTransport_OpenClose() 34 | { 35 | var usbDeviceMock = new Mock(); 36 | var usbTransportMock = new Mock(usbDeviceMock.Object); 37 | usbDeviceMock.Verify(x => x.Open(), Times.Never); 38 | usbTransportMock.Verify(x => x.OpenEndpoints(), Times.Never); 39 | 40 | usbTransportMock.Object.Open(); 41 | usbDeviceMock.Verify(x => x.Open(), Times.Once); 42 | usbTransportMock.Verify(x => x.OpenEndpoints(), Times.Once); 43 | usbDeviceMock.Verify(x => x.Close(), Times.Never); 44 | usbTransportMock.Verify(x => x.CloseEndpoints(), Times.Never); 45 | 46 | usbTransportMock.Object.Dispose(); 47 | usbDeviceMock.Verify(x => x.Close(), Times.Once); 48 | usbTransportMock.Verify(x => x.CloseEndpoints(), Times.Once); 49 | } 50 | 51 | [Test] 52 | public void LibUsbTransport_Send() 53 | { 54 | var aclData = new byte[] { 0x12, 0x34, 0x56 }; 55 | var commandData = new byte[] { 0x78, 0x9A, 0xBC }; 56 | var usbDeviceMock = new Mock(); 57 | var usbTransportMock = new Mock(usbDeviceMock.Object); 58 | 59 | usbTransportMock.Object.Send(aclData, DataType.Acl); 60 | usbTransportMock.Verify(x => x.SendCommand(It.IsAny()), Times.Never); 61 | usbTransportMock.Verify(x => x.SendAcl(It.Is(y => y.SequenceEqual(aclData))), Times.Once); 62 | 63 | usbTransportMock.Object.Send(commandData, DataType.Command); 64 | usbTransportMock.Verify(x => x.SendCommand(It.Is(y => y.SequenceEqual(commandData))), Times.Once); 65 | usbTransportMock.Verify(x => x.SendAcl(It.Is(y => y.SequenceEqual(aclData))), Times.Once); 66 | } 67 | 68 | [Test] 69 | public void LibUsbTransport_NoConfigurations() 70 | { 71 | var ex = Assert.Throws(() => 72 | { 73 | var usbDeviceMock = new Mock(); 74 | using (var transport = new LibUsbTransport(usbDeviceMock.Object)) 75 | transport.Open(); 76 | }); 77 | 78 | Assert.That(ex.Message, Is.EqualTo("USB device has no configurations")); 79 | } 80 | 81 | #region Helpers 82 | 83 | internal abstract class TestLibUsbDevice : ILibUsbDevice 84 | { 85 | public abstract int Vid { get; } 86 | public abstract int Pid { get; } 87 | public abstract void Open(); 88 | public abstract void Close(); 89 | 90 | public abstract UsbEndpointReader OpenEndpointReader(ReadEndpointID readEndpointID); 91 | public abstract UsbEndpointWriter OpenEndpointWriter(WriteEndpointID writeEndpointID); 92 | public abstract bool ControlTransfer(ref UsbSetupPacket setupPacket, object buffer, int bufferLength, out int lengthTransferred); 93 | 94 | public IEnumerable Configs 95 | { 96 | get { return Enumerable.Empty(); } 97 | } 98 | } 99 | 100 | #endregion 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /WinBeacon.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29001.49 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WinBeacon", "src\WinBeacon\WinBeacon.csproj", "{276A318E-8E7C-424B-BF6D-263E98BBADFD}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{F5C4ADE2-B6BF-4A85-877E-2EBCF8DFDA48}" 9 | ProjectSection(SolutionItems) = preProject 10 | .nuget\NuGet.Config = .nuget\NuGet.Config 11 | .nuget\NuGet.exe = .nuget\NuGet.exe 12 | .nuget\NuGet.targets = .nuget\NuGet.targets 13 | EndProjectSection 14 | EndProject 15 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{67875218-D5C5-4940-B7D2-A104093D4084}" 16 | EndProject 17 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{8D7F4EC7-F536-4CA1-A1A3-C9B46757F5E8}" 18 | EndProject 19 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E6F37106-FB24-4548-B783-DF926AB82E4C}" 20 | ProjectSection(SolutionItems) = preProject 21 | LICENSE = LICENSE 22 | ReadMe.md = ReadMe.md 23 | EndProjectSection 24 | EndProject 25 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WinBeacon.Tests", "test\WinBeacon.Tests\WinBeacon.Tests.csproj", "{5B839047-8FD5-4DF9-BFDD-FDF4912FF5BE}" 26 | EndProject 27 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WinBeacon.Stack", "src\WinBeacon.Stack\WinBeacon.Stack.csproj", "{40BE88D6-A9CF-4F91-9AC1-9C0ED796CF3A}" 28 | EndProject 29 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleConsole", "samples\SampleConsole\SampleConsole.csproj", "{8EDF3357-CD44-4DAA-B1BA-C22399CEA790}" 30 | EndProject 31 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{E8343CD3-E188-4C61-B9E4-CFEFA08AF50C}" 32 | EndProject 33 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WinBeacon.Stack.Tests", "test\WinBeacon.Stack.Tests\WinBeacon.Stack.Tests.csproj", "{F1326609-5E73-4D68-9D32-4CB9CFF1A4CA}" 34 | EndProject 35 | Global 36 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 37 | Debug|Any CPU = Debug|Any CPU 38 | Release|Any CPU = Release|Any CPU 39 | EndGlobalSection 40 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 41 | {276A318E-8E7C-424B-BF6D-263E98BBADFD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {276A318E-8E7C-424B-BF6D-263E98BBADFD}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {276A318E-8E7C-424B-BF6D-263E98BBADFD}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {276A318E-8E7C-424B-BF6D-263E98BBADFD}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {5B839047-8FD5-4DF9-BFDD-FDF4912FF5BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 46 | {5B839047-8FD5-4DF9-BFDD-FDF4912FF5BE}.Debug|Any CPU.Build.0 = Debug|Any CPU 47 | {5B839047-8FD5-4DF9-BFDD-FDF4912FF5BE}.Release|Any CPU.ActiveCfg = Release|Any CPU 48 | {5B839047-8FD5-4DF9-BFDD-FDF4912FF5BE}.Release|Any CPU.Build.0 = Release|Any CPU 49 | {40BE88D6-A9CF-4F91-9AC1-9C0ED796CF3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 50 | {40BE88D6-A9CF-4F91-9AC1-9C0ED796CF3A}.Debug|Any CPU.Build.0 = Debug|Any CPU 51 | {40BE88D6-A9CF-4F91-9AC1-9C0ED796CF3A}.Release|Any CPU.ActiveCfg = Release|Any CPU 52 | {40BE88D6-A9CF-4F91-9AC1-9C0ED796CF3A}.Release|Any CPU.Build.0 = Release|Any CPU 53 | {8EDF3357-CD44-4DAA-B1BA-C22399CEA790}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 54 | {8EDF3357-CD44-4DAA-B1BA-C22399CEA790}.Debug|Any CPU.Build.0 = Debug|Any CPU 55 | {8EDF3357-CD44-4DAA-B1BA-C22399CEA790}.Release|Any CPU.ActiveCfg = Release|Any CPU 56 | {8EDF3357-CD44-4DAA-B1BA-C22399CEA790}.Release|Any CPU.Build.0 = Release|Any CPU 57 | {F1326609-5E73-4D68-9D32-4CB9CFF1A4CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 58 | {F1326609-5E73-4D68-9D32-4CB9CFF1A4CA}.Debug|Any CPU.Build.0 = Debug|Any CPU 59 | {F1326609-5E73-4D68-9D32-4CB9CFF1A4CA}.Release|Any CPU.ActiveCfg = Release|Any CPU 60 | {F1326609-5E73-4D68-9D32-4CB9CFF1A4CA}.Release|Any CPU.Build.0 = Release|Any CPU 61 | EndGlobalSection 62 | GlobalSection(SolutionProperties) = preSolution 63 | HideSolutionNode = FALSE 64 | EndGlobalSection 65 | GlobalSection(NestedProjects) = preSolution 66 | {276A318E-8E7C-424B-BF6D-263E98BBADFD} = {67875218-D5C5-4940-B7D2-A104093D4084} 67 | {5B839047-8FD5-4DF9-BFDD-FDF4912FF5BE} = {8D7F4EC7-F536-4CA1-A1A3-C9B46757F5E8} 68 | {40BE88D6-A9CF-4F91-9AC1-9C0ED796CF3A} = {67875218-D5C5-4940-B7D2-A104093D4084} 69 | {8EDF3357-CD44-4DAA-B1BA-C22399CEA790} = {E8343CD3-E188-4C61-B9E4-CFEFA08AF50C} 70 | {F1326609-5E73-4D68-9D32-4CB9CFF1A4CA} = {8D7F4EC7-F536-4CA1-A1A3-C9B46757F5E8} 71 | EndGlobalSection 72 | GlobalSection(ExtensibilityGlobals) = postSolution 73 | SolutionGuid = {27350773-DADB-4EFA-A33E-5C2285875AE1} 74 | EndGlobalSection 75 | EndGlobal 76 | -------------------------------------------------------------------------------- /test/WinBeacon.Tests/EddystoneUrlTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using NUnit.Framework; 3 | using WinBeacon.Stack.Hci.Events; 4 | 5 | namespace WinBeacon.Tests 6 | { 7 | [TestFixture] 8 | public class EddystoneUrlTests 9 | { 10 | [Test] 11 | public void Eddystone_Parse_EddystoneUrlAdvertisingEvent_ShouldNotBeNull() 12 | { 13 | // Act 14 | var eddystone = Eddystone.Parse(EddystoneUrlAdvertisingEvent()); 15 | 16 | // Assert 17 | Assert.That(eddystone, Is.Not.Null); 18 | } 19 | 20 | [Test] 21 | public void Eddystone_Parse_EddystoneUrlAdvertisingEvent_ShouldNotBeOfTypeEddystoneUrl() 22 | { 23 | // Act 24 | var eddystone = Eddystone.Parse(EddystoneUrlAdvertisingEvent()); 25 | 26 | // Assert 27 | Assert.That(eddystone, Is.InstanceOf()); 28 | } 29 | 30 | [Test] 31 | public void Eddystone_Parse_EddystoneUrlAdvertisingEvent_ShouldReturnAddress() 32 | { 33 | // Act 34 | var eddystone = Eddystone.Parse(EddystoneUrlAdvertisingEvent()) as EddystoneUrl; 35 | 36 | // Assert 37 | Assert.That(eddystone.Address, Is.EqualTo(new byte[] { 0xA6, 0xA5, 0xA4, 0xA3, 0xA2, 0xA1 })); 38 | } 39 | 40 | [Test] 41 | public void Eddystone_Parse_EddystoneUrlAdvertisingEvent_ShouldReturnRssi() 42 | { 43 | // Act 44 | var eddystone = Eddystone.Parse(EddystoneUrlAdvertisingEvent()) as EddystoneUrl; 45 | 46 | // Assert 47 | Assert.That(eddystone.Rssi, Is.EqualTo(-52)); 48 | } 49 | 50 | [Test] 51 | public void Eddystone_Parse_EddystoneUrlAdvertisingEvent_ShouldReturnDecodedUrl() 52 | { 53 | var theories = new (byte SchemePrefix, byte ExpansionText, string ExpectedUrl)[] 54 | { 55 | (SchemePrefix: 0x00, ExpansionText: 0x00, ExpectedUrl: "http://www.google.com/"), 56 | (SchemePrefix: 0x01, ExpansionText: 0x00, ExpectedUrl: "https://www.google.com/"), 57 | (SchemePrefix: 0x02, ExpansionText: 0x00, ExpectedUrl: "http://google.com/"), 58 | (SchemePrefix: 0x03, ExpansionText: 0x00, ExpectedUrl: "https://google.com/"), 59 | 60 | (SchemePrefix: 0x00, ExpansionText: 0x01, ExpectedUrl: "http://www.google.org/"), 61 | (SchemePrefix: 0x00, ExpansionText: 0x02, ExpectedUrl: "http://www.google.edu/"), 62 | (SchemePrefix: 0x00, ExpansionText: 0x03, ExpectedUrl: "http://www.google.net/"), 63 | (SchemePrefix: 0x00, ExpansionText: 0x04, ExpectedUrl: "http://www.google.info/"), 64 | (SchemePrefix: 0x00, ExpansionText: 0x05, ExpectedUrl: "http://www.google.biz/"), 65 | (SchemePrefix: 0x00, ExpansionText: 0x06, ExpectedUrl: "http://www.google.gov/"), 66 | (SchemePrefix: 0x00, ExpansionText: 0x07, ExpectedUrl: "http://www.google.com"), 67 | (SchemePrefix: 0x00, ExpansionText: 0x08, ExpectedUrl: "http://www.google.org"), 68 | (SchemePrefix: 0x00, ExpansionText: 0x09, ExpectedUrl: "http://www.google.edu"), 69 | (SchemePrefix: 0x00, ExpansionText: 0x0A, ExpectedUrl: "http://www.google.net"), 70 | (SchemePrefix: 0x00, ExpansionText: 0x0B, ExpectedUrl: "http://www.google.info"), 71 | (SchemePrefix: 0x00, ExpansionText: 0x0C, ExpectedUrl: "http://www.google.biz"), 72 | (SchemePrefix: 0x00, ExpansionText: 0x0D, ExpectedUrl: "http://www.google.gov"), 73 | }; 74 | 75 | foreach (var theory in theories) 76 | { 77 | // Act 78 | var eddystone = Eddystone.Parse(EddystoneUrlAdvertisingEvent(theory.SchemePrefix, theory.ExpansionText)) as EddystoneUrl; 79 | 80 | // Assert 81 | Assert.That(eddystone.Url, Is.EqualTo(theory.ExpectedUrl)); 82 | } 83 | } 84 | 85 | [Test] 86 | public void Eddystone_Parse_EddystoneUrlAdvertisingEvent_ShouldReturnCalibratedTxPower() 87 | { 88 | // Act 89 | var eddystone = Eddystone.Parse(EddystoneUrlAdvertisingEvent()) as EddystoneUrl; 90 | 91 | // Assert 92 | Assert.That(eddystone.CalibratedTxPower, Is.EqualTo(-18)); 93 | } 94 | 95 | private static LeAdvertisingEvent EddystoneUrlAdvertisingEvent(byte schemePrefix = 0, byte expansionText = 0) 96 | => LeAdvertisingEvent.Parse(new Queue(new byte[] 97 | { 98 | 0x02, // Low Energy Advertising event type: Scan IND 99 | 0x00, // Address type 100 | 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, // Bluetooth MAC address 101 | 0x15, // Payload length 102 | 0x02, 0x01, 0x06, // Flags data 103 | 0x03, 0x03, 0xAA, 0xFE, // Complete list of 16-bit Service UUID (including 16-bit Eddystone UUID) 104 | 0x0D, 105 | 0x16, // Service Data data type value 106 | 0xAA, 0xFE, // 16-bit Eddystone UUID 107 | 0x10, // URL frame type 108 | 0xEE, // Calibrated TX power 109 | schemePrefix, 110 | 0x67, 0x6F, 0x6F, 0x67, 0x6C, 0x65, // google 111 | expansionText, 112 | 0xCC, // RSSI 113 | })); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/WinBeacon/Hub.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using System; 8 | using WinBeacon.Stack; 9 | using WinBeacon.Stack.Controllers; 10 | using WinBeacon.Stack.Hci.Events; 11 | 12 | namespace WinBeacon 13 | { 14 | /// 15 | /// Hub for detecting beacons and advertise as a beacon. 16 | /// 17 | public class Hub : IDisposable 18 | { 19 | private ILeController _controller; 20 | 21 | /// 22 | /// Creates a hub instance that uses LibUsb or WinUSB as transport. 23 | /// 24 | /// The VID of the BT4.0 dongle. 25 | /// The PID of the BT4.0 dongle. 26 | public Hub(int usbVid, int usbPid) 27 | { 28 | _controller = new LibUsbLeController(usbVid, usbPid); 29 | _controller.LeMetaEventReceived += controller_LeMetaEventReceived; 30 | _controller.Open(); 31 | _controller.EnableScanning(); 32 | } 33 | 34 | /// 35 | /// Destructor. 36 | /// 37 | ~Hub() 38 | { 39 | Dispose(); 40 | } 41 | 42 | /// 43 | /// Release used resources. 44 | /// 45 | public void Dispose() 46 | { 47 | if (_controller != null) 48 | { 49 | _controller.LeMetaEventReceived -= controller_LeMetaEventReceived; 50 | _controller.Dispose(); 51 | } 52 | 53 | _controller = null; 54 | } 55 | 56 | /// 57 | /// Enable advertising as an Apple iBeacon. 58 | /// 59 | /// The Apple iBeacon to emulate. 60 | /// The advertising interval. Interval should be between 20 ms and 10.24 seconds. Defaults to 1.28 seconds. 61 | public void EnableAdvertising(Beacon beacon, TimeSpan? advertisingInterval = null) 62 | { 63 | if (_controller == null) 64 | throw new ObjectDisposedException("controller"); 65 | if (beacon == null) 66 | throw new ArgumentNullException(nameof(beacon)); 67 | 68 | if (advertisingInterval.HasValue) 69 | { 70 | if (advertisingInterval.Value.TotalMilliseconds < 20 || advertisingInterval.Value.TotalMilliseconds > 10240) 71 | throw new ArgumentOutOfRangeException("advertisingInterval", "Interval should be between 20 ms and 10.24 seconds"); 72 | _controller.EnableAdvertising(beacon.ToAdvertisingData(), (ushort)advertisingInterval.Value.TotalMilliseconds); 73 | } 74 | else 75 | { 76 | _controller.EnableAdvertising(beacon.ToAdvertisingData()); 77 | } 78 | } 79 | 80 | /// 81 | /// Enable advertising as an Eddystone. 82 | /// 83 | /// The Eddystone to emulate. 84 | /// The advertising interval. Interval should be between 20 ms and 10.24 seconds. Defaults to 1.28 seconds. 85 | public void EnableAdvertising(Eddystone eddystone, TimeSpan? advertisingInterval = null) 86 | { 87 | throw new NotImplementedException(); 88 | } 89 | 90 | /// 91 | /// Disable advertising as a beacon. 92 | /// 93 | public void DisableAdvertising() 94 | { 95 | if (_controller == null) 96 | throw new ObjectDisposedException(nameof(_controller)); 97 | _controller.DisableAdvertising(); 98 | } 99 | 100 | /// 101 | /// Event fired when an Apple iBeacon is detected. This happens when the dongle receives an Apple iBeacon compatible advertising packet. 102 | /// 103 | public event EventHandler BeaconDetected; 104 | 105 | /// 106 | /// Event fired when an Eddystone is detected. This happens when the dongle receives an Eddystone compatible advertising packet. 107 | /// 108 | public event EventHandler EddystoneDetected; 109 | 110 | private void controller_LeMetaEventReceived(object sender, LeMetaEventReceivedEventArgs e) 111 | { 112 | if (e.LeMetaEvent == null) 113 | return; 114 | if (e.LeMetaEvent.SubEvent != LeMetaEvent.LeMetaSubEvent.AdvertisingReport) 115 | return; 116 | if (e.LeMetaEvent.AdvertisingEvents == null) 117 | return; 118 | 119 | foreach (var advertisingEvent in e.LeMetaEvent.AdvertisingEvents) 120 | { 121 | var beacon = Beacon.Parse(advertisingEvent); 122 | if (beacon != null) 123 | { 124 | BeaconDetected?.Invoke(this, new BeaconEventArgs(beacon)); 125 | continue; 126 | } 127 | 128 | var eddystone = Eddystone.Parse(advertisingEvent); 129 | if (eddystone != null) 130 | { 131 | EddystoneDetected?.Invoke(this, new EddystoneEventArgs(eddystone)); 132 | continue; 133 | } 134 | } 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /test/WinBeacon.Stack.Tests/Controllers/LeControllerTests.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Threading; 11 | using Moq; 12 | using NUnit.Framework; 13 | using WinBeacon.Stack.Controllers; 14 | using WinBeacon.Stack.Hci; 15 | using WinBeacon.Stack.Hci.Commands; 16 | 17 | namespace WinBeacon.Stack.Tests.Controllers 18 | { 19 | [TestFixture] 20 | public class LeControllerTests 21 | { 22 | [Test] 23 | public void LeController_OpenUnderlyingTransport() 24 | { 25 | var transportMock = new Mock(); 26 | var controller = new LeController(transportMock.Object); 27 | controller.Open(); 28 | transportMock.Verify(x => x.Open(), Times.Once()); 29 | transportMock.Verify(x => x.Close(), Times.Never()); 30 | } 31 | 32 | [Test] 33 | public void LeController_CloseUnderlyingTransport() 34 | { 35 | var transportMock = new Mock(); 36 | var controller = new LeController(transportMock.Object); 37 | controller.Open(); 38 | controller.Close(); 39 | transportMock.Verify(x => x.Open(), Times.Once()); 40 | transportMock.Verify(x => x.Close(), Times.Once()); 41 | } 42 | 43 | [Test] 44 | public void LeController_Dispose() 45 | { 46 | var transportMock = new Mock(); 47 | using (var controller = new LeController(transportMock.Object)) 48 | controller.Open(); 49 | transportMock.Verify(x => x.Open(), Times.Once()); 50 | transportMock.Verify(x => x.Close(), Times.Once()); 51 | } 52 | 53 | [Test] 54 | public void LeController_InitCommands() 55 | { 56 | var transportMock = new Mock { CallBase = true }; 57 | using (var controller = new LeController(transportMock.Object)) 58 | { 59 | controller.Open(); 60 | Thread.Sleep(10); 61 | } 62 | transportMock.Verify( 63 | transport => transport.Send( 64 | It.Is(x => x.SequenceEqual(new ResetCommand().ToByteArray())), 65 | It.Is(x => x == DataType.Command)), 66 | Times.Exactly(1)); 67 | transportMock.Verify( 68 | transport => transport.Send(It.IsAny(), It.Is(x => x == DataType.Command)), 69 | Times.Exactly(7)); 70 | } 71 | 72 | [Test] 73 | public void LeController_DeviceAddressReceived() 74 | { 75 | var transportMock = new Mock { CallBase = true }; 76 | transportMock 77 | .Setup(x => x.GetCommandCompletePayload(It.IsAny(), It.IsAny())) 78 | .Returns(new Func((data, dataType) => 79 | { 80 | var readBdAddrData = new ReadBdAddrCommand().ToByteArray(); 81 | if (!Enumerable.SequenceEqual(data, readBdAddrData)) 82 | return null; 83 | return new byte[] { 0x60, 0x50, 0x40, 0x30, 0x20, 0x10 }; 84 | })); 85 | 86 | using (var controller = new LeController(transportMock.Object)) 87 | { 88 | DeviceAddress deviceAddress = null; 89 | var done = new ManualResetEvent(false); 90 | controller.DeviceAddressReceived += (sender, e) => 91 | { 92 | deviceAddress = e.DeviceAddress; 93 | done.Set(); 94 | }; 95 | 96 | controller.Open(); 97 | Assert.IsTrue(done.WaitOne(1000, false)); 98 | Assert.AreEqual(new byte[] { 0x10, 0x20, 0x30, 0x40, 0x50, 0x60 }, deviceAddress.Address); 99 | } 100 | } 101 | 102 | #region Helpers 103 | 104 | internal class FakeCommandTransport : ITransport 105 | { 106 | public virtual void Send(byte[] data, DataType dataType) 107 | { 108 | var payload = GetCommandCompletePayload(data, dataType); 109 | ushort opcode = (ushort)(data[0] + (data[1] << 8)); 110 | var commandCompleteEvent = new List 111 | { 112 | (byte)EventCode.CommandComplete, 113 | (byte)(payload != null ? payload.Length : 0), 114 | 0x00, // NumberOfCommandsAllowedToSend 115 | data[0], // CommandOpcode lsb 116 | data[1], // CommandOpcode msb 117 | 0x00 // CommandParameterDataLength 118 | }; 119 | if (payload != null) 120 | commandCompleteEvent.AddRange(payload); 121 | if (DataReceived != null) 122 | DataReceived(this, new DataReceivedEventArgs(commandCompleteEvent.ToArray(), DataType.Command)); 123 | } 124 | public event EventHandler DataReceived; 125 | public virtual void Open() { } 126 | public virtual void Close() { } 127 | public virtual void Dispose() { } 128 | public virtual byte[] GetCommandCompletePayload(byte[] data, DataType dataType) { return null; } 129 | } 130 | 131 | #endregion 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/Hci/EventCode.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | namespace WinBeacon.Stack.Hci 8 | { 9 | /// 10 | /// Low Energy event codes 11 | /// 12 | public enum EventCode : byte 13 | { 14 | /// 15 | /// Inquiry complete. 16 | /// 17 | InquiryComplete = 0x01, 18 | /// 19 | /// Inquiry result. 20 | /// 21 | InquiryResult = 0x02, 22 | /// 23 | /// Connection complete. 24 | /// 25 | ConnectionComplete = 0x03, 26 | /// 27 | /// Connection request. 28 | /// 29 | ConnectionRequest = 0x04, 30 | /// 31 | /// Disconnection complete. 32 | /// 33 | DisconnectionComplete = 0x05, 34 | /// 35 | /// Authentication complete. 36 | /// 37 | AuthenticationComplete = 0x06, 38 | /// 39 | /// Remote name request complete. 40 | /// 41 | RemoteNameRequestComplete = 0x07, 42 | /// 43 | /// Encryption change. 44 | /// 45 | EncryptionChange = 0x08, 46 | /// 47 | /// Change connection link key complete. 48 | /// 49 | ChangeConnectionLinkKeyComplete = 0x09, 50 | /// 51 | /// Master link key complete. 52 | /// 53 | MasterLinkKeyComplete = 0x0A, 54 | /// 55 | /// Read remote supported features complete. 56 | /// 57 | ReadRemoteSupportedFeaturesComplete = 0x0B, 58 | /// 59 | /// Read remote version information complete. 60 | /// 61 | ReadRemoteVersionInformationComplete = 0x0C, 62 | /// 63 | /// Quality of service setup complete. 64 | /// 65 | QosSetupComplete = 0x0D, 66 | /// 67 | /// Command complete. 68 | /// 69 | CommandComplete = 0x0E, 70 | /// 71 | /// Command status. 72 | /// 73 | CommandStatus = 0x0F, 74 | /// 75 | /// Hardware error. 76 | /// 77 | HardwareError = 0x10, 78 | /// 79 | /// Flush occurred. 80 | /// 81 | FlushOccurred = 0x11, 82 | /// 83 | /// Role changed. 84 | /// 85 | RoleChanged = 0x12, 86 | /// 87 | /// Number of completed packets. 88 | /// 89 | NumberOfCompletedPackets = 0x13, 90 | /// 91 | /// Mode change. 92 | /// 93 | ModeChange = 0x14, 94 | /// 95 | /// Return link keys. 96 | /// 97 | ReturnLinkKeys = 0x15, 98 | /// 99 | /// Pin code request. 100 | /// 101 | PinCodeRequest = 0x16, 102 | /// 103 | /// Link key request. 104 | /// 105 | LinkKeyRequest = 0x17, 106 | /// 107 | /// Link key notification. 108 | /// 109 | LinkKeyNotification = 0x18, 110 | /// 111 | /// Loopback command. 112 | /// 113 | LoopbackCommand = 0x19, 114 | /// 115 | /// Data buffer overflow. 116 | /// 117 | DataBufferOverflow = 0x1A, 118 | /// 119 | /// Max slots change. 120 | /// 121 | MaxSlotsChange = 0x1B, 122 | /// 123 | /// Read clock offset complete. 124 | /// 125 | ReadClockOffsetComplete = 0x1C, 126 | /// 127 | /// Connection packet type changed. 128 | /// 129 | ConnectionPacketTypeChanged = 0x1D, 130 | /// 131 | /// Quality of service violation. 132 | /// 133 | QosViolation = 0x1E, 134 | /// 135 | /// Page scan mode change. 136 | /// 137 | PageScanModeChange = 0x1F, 138 | /// 139 | /// Page scan repetition mode change. 140 | /// 141 | PageScanRepetitionModeChange = 0x20, 142 | /// 143 | /// HCI flow specification complete. 144 | /// 145 | HciFlowSpecificationComplete = 0x21, 146 | /// 147 | /// Inquiry result with RSSI. 148 | /// 149 | InquiryResultWithRssi = 0x22, 150 | /// 151 | /// Read remote extended features complete. 152 | /// 153 | ReadRemoteExtendedFeaturesComplete = 0x23, 154 | /// 155 | /// Synchronous connection complete. 156 | /// 157 | SynchronousConnectionComplete = 0x2C, 158 | /// 159 | /// Synchronous connection changed. 160 | /// 161 | SynchronousConnectionChanged = 0x2D, 162 | /// 163 | /// Low energy meta event. 164 | /// 165 | LeMeta = 0x3E, 166 | /// 167 | /// Reserved for Bluetooth logo testing. 168 | /// 169 | ReservedBluetoothLogoTesting = 0xFE, 170 | /// 171 | /// Vendor specific events. 172 | /// 173 | VendorSpecificEvents = 0xFF 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/Transports/LibUsb/LibUsbTransport.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using LibUsbDotNet; 11 | using LibUsbDotNet.Main; 12 | using WinBeacon.Stack.Hci; 13 | using WinBeacon.Stack.Transport; 14 | 15 | namespace WinBeacon.Stack.Transports.LibUsb 16 | { 17 | internal class LibUsbTransport : ITransport 18 | { 19 | private static readonly TimeSpan writeTimeout = TimeSpan.FromSeconds(5); 20 | 21 | private readonly ILibUsbDevice _usbDevice; 22 | private readonly Dictionary _endpoints = new Dictionary(); 23 | 24 | internal LibUsbTransport(ILibUsbDevice usbDevice) 25 | { 26 | _usbDevice = usbDevice ?? throw new ArgumentNullException(nameof(usbDevice)); 27 | } 28 | 29 | ~LibUsbTransport() 30 | { 31 | Dispose(); 32 | } 33 | 34 | #region ITransport implementation 35 | 36 | public void Dispose() 37 | { 38 | Close(); 39 | } 40 | 41 | public void Open() 42 | { 43 | _usbDevice.Open(); 44 | OpenEndpoints(); 45 | } 46 | 47 | public void Close() 48 | { 49 | CloseEndpoints(); 50 | _usbDevice.Close(); 51 | } 52 | 53 | public void Send(byte[] data, DataType dataType) 54 | { 55 | if (_usbDevice == null) 56 | throw new WinBeaconException("Transport not open"); 57 | if (data == null) 58 | throw new ArgumentNullException("data"); 59 | switch (dataType) 60 | { 61 | case DataType.Command: 62 | SendCommand(data); 63 | break; 64 | case DataType.Acl: 65 | SendAcl(data); 66 | break; 67 | default: 68 | throw new ArgumentException("Invalid dataType"); 69 | } 70 | } 71 | 72 | public event EventHandler DataReceived; 73 | private void OnDataReceived(byte[] data, DataType dataType) 74 | { 75 | if (DataReceived != null) 76 | DataReceived(this, new DataReceivedEventArgs(data, dataType)); 77 | } 78 | 79 | #endregion 80 | 81 | public int Vid => _usbDevice.Vid; 82 | 83 | public int Pid => _usbDevice.Pid; 84 | 85 | private Dictionary GetBluetoothEndpointIds() 86 | { 87 | var endpointIds = new Dictionary 88 | { 89 | { UsbBluetoothEndpointType.Commands, 0x00 }, 90 | { UsbBluetoothEndpointType.Events, 0x81 }, 91 | { UsbBluetoothEndpointType.AclDataOut, 0x02 }, 92 | { UsbBluetoothEndpointType.AclDataIn, 0x82 } 93 | }; 94 | foreach (var info in _usbDevice.EnumerateBluetoothEndpointInfo()) 95 | endpointIds[info.Type] = info.Id; 96 | return endpointIds; 97 | } 98 | 99 | internal virtual void OpenEndpoints() 100 | { 101 | var endpointIds = GetBluetoothEndpointIds(); 102 | _endpoints[UsbBluetoothEndpointType.Commands] = _usbDevice.OpenEndpointWriter((WriteEndpointID)endpointIds[UsbBluetoothEndpointType.Commands]); 103 | _endpoints[UsbBluetoothEndpointType.Events] = _usbDevice.OpenEndpointReader((ReadEndpointID)endpointIds[UsbBluetoothEndpointType.Events]); 104 | _endpoints[UsbBluetoothEndpointType.AclDataOut] = _usbDevice.OpenEndpointWriter((WriteEndpointID)endpointIds[UsbBluetoothEndpointType.AclDataOut]); 105 | _endpoints[UsbBluetoothEndpointType.AclDataIn] = _usbDevice.OpenEndpointReader((ReadEndpointID)endpointIds[UsbBluetoothEndpointType.AclDataIn]); 106 | var failedEndpointTypes = from x in _endpoints where x.Value == null select x.Key; 107 | if (failedEndpointTypes.Count() > 0) 108 | throw new WinBeaconException($"Failed to open endpoint(s): {string.Join(" ", failedEndpointTypes)}"); 109 | _endpoints[UsbBluetoothEndpointType.Events].SubscribeForDataReceived(data => OnDataReceived(data, DataType.Command)); 110 | _endpoints[UsbBluetoothEndpointType.AclDataIn].SubscribeForDataReceived(data => OnDataReceived(data, DataType.Acl)); 111 | } 112 | 113 | internal virtual void CloseEndpoints() 114 | { 115 | var openEndpoints = from x in _endpoints where x.Value != null select x.Value; 116 | foreach (var endpoint in openEndpoints) 117 | { 118 | if (endpoint is UsbEndpointReader) 119 | (endpoint as UsbEndpointReader).DataReceivedEnabled = false; 120 | endpoint.Dispose(); 121 | } 122 | 123 | _endpoints.Clear(); 124 | } 125 | 126 | internal virtual void SendCommand(byte[] data) 127 | { 128 | var setupPacket = new UsbSetupPacket((byte)UsbRequestType.TypeClass | (byte)UsbRequestRecipient.RecipInterface, 0, 0, 0, (short)data.Length); 129 | if (!_usbDevice.ControlTransfer(ref setupPacket, data, data.Length, out int lengthTransferred)) 130 | throw new WinBeaconException("USB ControlTransfer failed"); 131 | if (lengthTransferred != data.Length) 132 | throw new WinBeaconException($"USB ControlTransfer didn't send all bytes. Sent {lengthTransferred} out of {data.Length} bytes"); 133 | } 134 | 135 | internal virtual void SendAcl(byte[] data) 136 | { 137 | var endpoint = _endpoints[UsbBluetoothEndpointType.AclDataOut] as UsbEndpointWriter; 138 | var errorCode = endpoint.Write(data, (int)writeTimeout.TotalMilliseconds, out int transferLength); 139 | if (errorCode != ErrorCode.Ok) 140 | throw new WinBeaconException($"USB write operation failed with error code: {errorCode}"); 141 | if (transferLength != data.Length) 142 | throw new WinBeaconException($"USB write operation didn't send all bytes. Sent {transferLength} out of {data.Length} bytes"); 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/WinBeacon/Beacon.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using System; 8 | using System.Collections.Generic; 9 | using WinBeacon.Stack; 10 | using WinBeacon.Stack.Hci.Events; 11 | 12 | namespace WinBeacon 13 | { 14 | /// 15 | /// Class that represents an Apple iBeacon. 16 | /// 17 | public class Beacon : IComparable 18 | { 19 | private const int AppleCompanyId = 0x4C00; 20 | 21 | /// 22 | /// UUID of the beacon. 23 | /// 24 | public string Uuid { get; private set; } 25 | 26 | /// 27 | /// Bluetooth MAC-address of the beacon. 28 | /// 29 | public byte[] Address { get; private set; } 30 | 31 | /// 32 | /// Major number of the beacon. 33 | /// 34 | public int Major { get; private set; } 35 | 36 | /// 37 | /// Minor number of the beacon. 38 | /// 39 | public int Minor { get; private set; } 40 | 41 | /// 42 | /// RSSI power of the beacon in dB. 43 | /// 44 | public int Rssi { get; internal set; } 45 | 46 | /// 47 | /// Calibrated TX power of the beacon in dB. 48 | /// 49 | public int CalibratedTxPower { get; private set; } 50 | 51 | /// 52 | /// CompanyId of the beacon (0x4C00 for Apple iBeacon). 53 | /// 54 | public int CompanyId { get; private set; } 55 | 56 | /// 57 | /// True if the beacon is an (emulation of) Apple iBeacon. 58 | /// 59 | public bool IsAppleIBeacon 60 | { 61 | get { return CompanyId == AppleCompanyId; } 62 | } 63 | 64 | private Beacon() 65 | { 66 | } 67 | 68 | /// 69 | /// Creates a beacon. 70 | /// 71 | /// UUID of the beacon. 72 | /// Major number of the beacon. 73 | /// Minor number of the beacon. 74 | /// CompanyId, defaults to comapny id of Apple (0x4C00). 75 | /// Calibrated TX power of the beacon in dB. 76 | public Beacon(string uuid, int major, int minor, int calibratedTxPower, int companyId = AppleCompanyId) 77 | { 78 | Uuid = uuid; 79 | Major = major; 80 | Minor = minor; 81 | CalibratedTxPower = calibratedTxPower; 82 | CompanyId = companyId; 83 | } 84 | 85 | /// 86 | /// Compare this beacon to an other instance. 87 | /// 88 | /// The other instance. 89 | /// A value that indicates the lexical relationship between the two comparands. 90 | public int CompareTo(object obj) 91 | { 92 | var other = obj as Beacon ?? throw new ArgumentException("Must be of type Beacon", nameof(obj)); 93 | return Uuid.NullableCompareTo(other.Uuid) 94 | ?? Major.NullableCompareTo(other.Major) 95 | ?? Minor.CompareTo(other.Minor); 96 | } 97 | 98 | /// 99 | /// Returns a string representation of the beacon data. 100 | /// 101 | /// String representation of the beacon data. 102 | public override string ToString() 103 | => $"UUID: {Uuid}, Address: {BitConverter.ToString(Address)}, Major: {Major}, Minor: {Minor}, RSSI: {Rssi}, TxPower: {CalibratedTxPower}dB, CompanyId: 0x{CompanyId:X}, Distance: {this.GetRange():0.00}m"; 104 | 105 | /// 106 | /// Parse low energy advertising event to a beacon instance. 107 | /// 108 | /// The event. 109 | /// The beacon or null in case of failure. 110 | internal static Beacon Parse(LeAdvertisingEvent e) 111 | { 112 | if (e.EventType == LeAdvertisingEventType.ScanRsp) 113 | return null; 114 | var payload = new Queue(e.Payload); 115 | while (payload.Count > 0) 116 | { 117 | var adLength = payload.Dequeue(); 118 | if (payload.Count < adLength) 119 | break; 120 | var adType = payload.Dequeue(); 121 | if (adType == 0xFF && adLength >= 26) 122 | { 123 | var companyId = (ushort)((payload.Dequeue() << 8) + payload.Dequeue()); 124 | var b0advInd = payload.Dequeue(); 125 | var b1advInd = payload.Dequeue(); 126 | var uuid = payload.Dequeue(16); 127 | var major = (ushort)((payload.Dequeue() << 8) + payload.Dequeue()); 128 | var minor = (ushort)((payload.Dequeue() << 8) + payload.Dequeue()); 129 | var txPower = (sbyte)payload.Dequeue(); 130 | return new Beacon 131 | { 132 | Uuid = uuid.ToLittleEndianFormattedUuidString(), 133 | Address = e.Address, 134 | Major = major, 135 | Minor = minor, 136 | Rssi = e.Rssi, 137 | CalibratedTxPower = txPower, 138 | CompanyId = companyId 139 | }; 140 | } 141 | else 142 | { 143 | if (adLength > 0) 144 | payload.Dequeue(adLength - 1); 145 | } 146 | } 147 | 148 | return null; 149 | } 150 | 151 | internal byte[] ToAdvertisingData() 152 | { 153 | var result = new List(); 154 | result.AddRange(new byte[] 155 | { 156 | 0x02, // ad1Length 157 | 0x01, // ad1Type 158 | 0x1A, // flags 159 | 0x1A, // ad2Length 160 | 0xFF, // ad2Type 161 | (byte)(CompanyId >> 8), 162 | (byte)(CompanyId & 0xFF), 163 | 0x02, // b0advInd 164 | 0x15 // b1advInd 165 | }); 166 | result.AddRange(Uuid.FromLittleEndianFormattedUuidString()); 167 | result.AddRange(new byte[] 168 | { 169 | (byte)(Major >> 8), 170 | (byte)(Major & 0xFF), 171 | (byte)(Minor >> 8), 172 | (byte)(Minor & 0xFF), 173 | (byte)CalibratedTxPower 174 | }); 175 | result.Add(0); 176 | return result.ToArray(); 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /.nuget/NuGet.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildProjectDirectory)\..\ 5 | 6 | 7 | false 8 | 9 | 10 | false 11 | 12 | 13 | true 14 | 15 | 16 | true 17 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | $([System.IO.Path]::Combine($(SolutionDir), ".nuget")) 31 | 32 | 33 | 34 | 35 | $(SolutionDir).nuget 36 | 37 | 38 | 39 | $(MSBuildProjectDirectory)\packages.$(MSBuildProjectName.Replace(' ', '_')).config 40 | $(MSBuildProjectDirectory)\packages.$(MSBuildProjectName).config 41 | 42 | 43 | 44 | $(MSBuildProjectDirectory)\packages.config 45 | $(PackagesProjectConfig) 46 | 47 | 48 | 49 | 50 | $(NuGetToolsPath)\NuGet.exe 51 | @(PackageSource) 52 | 53 | "$(NuGetExePath)" 54 | mono --runtime=v4.0.30319 "$(NuGetExePath)" 55 | 56 | $(TargetDir.Trim('\\')) 57 | 58 | -RequireConsent 59 | -NonInteractive 60 | 61 | "$(SolutionDir) " 62 | "$(SolutionDir)" 63 | 64 | 65 | $(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(NonInteractiveSwitch) $(RequireConsentSwitch) -solutionDir $(PaddedSolutionDir) 66 | $(NuGetCommand) pack "$(ProjectPath)" -Properties "Configuration=$(Configuration);Platform=$(Platform)" $(NonInteractiveSwitch) -OutputDirectory "$(PackageOutputDir)" -symbols 67 | 68 | 69 | 70 | RestorePackages; 71 | $(BuildDependsOn); 72 | 73 | 74 | 75 | 76 | $(BuildDependsOn); 77 | BuildPackage; 78 | 79 | 80 | 81 | 82 | 83 | 84 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 99 | 100 | 103 | 104 | 105 | 106 | 108 | 109 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 141 | 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/Controllers/LeController.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Threading; 10 | using WinBeacon.Stack.Hci; 11 | using WinBeacon.Stack.Hci.Commands; 12 | using WinBeacon.Stack.Hci.Events; 13 | 14 | namespace WinBeacon.Stack.Controllers 15 | { 16 | /// 17 | /// Base class for Bluetooth Low Energy controllers. 18 | /// 19 | public class LeController : ILeController 20 | { 21 | private readonly ITransport _transport; 22 | private readonly ManualResetEvent _cancelThread = new ManualResetEvent(false); 23 | private readonly Queue _commandQueue = new Queue(); 24 | private readonly AutoResetEvent _executeNextCommand = new AutoResetEvent(false); 25 | private Thread _commandExecutionThread; 26 | 27 | internal LeController(ITransport transport) 28 | { 29 | _transport = transport; 30 | } 31 | 32 | /// 33 | /// Destructor. 34 | /// 35 | ~LeController() 36 | { 37 | Dispose(); 38 | } 39 | 40 | /// 41 | /// Release all used resources. 42 | /// 43 | public void Dispose() 44 | { 45 | Close(); 46 | } 47 | 48 | /// 49 | /// Open underlying transport connection. 50 | /// 51 | public void Open() 52 | { 53 | if (_commandExecutionThread != null) 54 | return; 55 | _transport.Open(); 56 | _transport.DataReceived += transport_DataReceived; 57 | _cancelThread.Reset(); 58 | _commandExecutionThread = new Thread(CommandExecutionThreadProc); 59 | _commandExecutionThread.Start(); 60 | SendCommand(new ResetCommand()); 61 | SendCommand(new SetEventMaskCommand(new byte[] { 0xFF, 0xFF, 0xFB, 0xFF, 0x07, 0xF8, 0xBF, 0x3D })); 62 | SendCommand(new LeSetEventMaskCommand(new byte[] { 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 })); 63 | SendCommand(new LeWriteHostSupportedCommand(true, false)); 64 | SendCommand(new LeSetScanEnableCommand(false, false)); 65 | SendCommand(new LeSetScanParametersCommand(true, 10000, 10000, false, false)); 66 | SendCommand(new ReadBdAddrCommand() 67 | { 68 | CommandCompleteCallback = (cmd, deviceAddress) => 69 | { 70 | DeviceAddressReceived?.Invoke(this, new DeviceAddressReceivedEventArgs(deviceAddress)); 71 | } 72 | }); 73 | } 74 | 75 | /// 76 | /// Close underlying transport connection. 77 | /// 78 | public void Close() 79 | { 80 | if (_commandExecutionThread == null) 81 | return; 82 | _cancelThread.Set(); 83 | _commandExecutionThread.Join(); 84 | _commandExecutionThread = null; 85 | lock (_commandQueue) 86 | _commandQueue.Clear(); 87 | _transport.DataReceived -= transport_DataReceived; 88 | _transport.Close(); 89 | } 90 | 91 | /// 92 | /// Enable Low Energy device scanning. 93 | /// 94 | public void EnableScanning() 95 | { 96 | SendCommand(new LeSetScanEnableCommand(true, false)); 97 | } 98 | 99 | /// 100 | /// Disable Low Energy device scanning. 101 | /// 102 | public void DisableScanning() 103 | { 104 | SendCommand(new LeSetScanEnableCommand(false, false)); 105 | } 106 | 107 | /// 108 | /// Enable Low Energy advertising. 109 | /// 110 | /// The advertisement data. 111 | /// Interval should be between 20 and 10240 ms. Defaults to 1280 ms. 112 | public void EnableAdvertising(byte[] advertisementData, int advertisingIntervalInMs = 1280) 113 | { 114 | SendCommand(new LeSetAdvertisingDataCommand(advertisementData)); 115 | SendCommand(new LeSetAdvertisingParametersCommand( 116 | advertisingIntervalInMs, 117 | advertisingIntervalInMs, 118 | AdvertisingType.ConnectableUndirected, 119 | OwnAddressType.Public, 120 | PeerAddressType.Public, 121 | new byte[6], 122 | AdvertisingChannelMap.UseAllChannels, 123 | AdvertisingFilterPolicy.ConnectAllScanAll 124 | )); 125 | SendCommand(new LeSetAdvertisingEnableCommand(true)); 126 | } 127 | 128 | /// 129 | /// Disable Low Energy advertising. 130 | /// 131 | public void DisableAdvertising() 132 | { 133 | SendCommand(new LeSetAdvertisingEnableCommand(false)); 134 | } 135 | 136 | /// 137 | /// Fired for each received Low Energy meta event. 138 | /// 139 | public event EventHandler LeMetaEventReceived; 140 | 141 | /// 142 | /// Fired when the device address of this Bluetooth device was received. 143 | /// 144 | public event EventHandler DeviceAddressReceived; 145 | 146 | private void transport_DataReceived(object sender, Hci.DataReceivedEventArgs e) 147 | { 148 | if (e.DataType != DataType.Command) 149 | return; 150 | var evt = Event.Parse(e.Data); 151 | if (evt is CommandCompleteEvent) 152 | { 153 | var commandCompleteEvent = (evt as CommandCompleteEvent); 154 | Command command = null; 155 | lock (_commandQueue) 156 | { 157 | if (_commandQueue.Count > 0) 158 | { 159 | command = _commandQueue.Peek(); 160 | if (command.Opcode == commandCompleteEvent.CommandOpcode) 161 | _commandQueue.Dequeue(); 162 | else 163 | command = null; 164 | } 165 | } 166 | if (command != null) 167 | command.OnCommandComplete(commandCompleteEvent); 168 | _executeNextCommand.Set(); 169 | } 170 | else if (evt is LeMetaEvent) 171 | { 172 | LeMetaEventReceived?.Invoke(this, new LeMetaEventReceivedEventArgs(evt as LeMetaEvent)); 173 | } 174 | } 175 | 176 | private void CommandExecutionThreadProc() 177 | { 178 | var waitHandles = new WaitHandle[] { _cancelThread, _executeNextCommand }; 179 | while (WaitHandle.WaitAny(waitHandles) == 1) 180 | { 181 | Command command = null; 182 | lock (_commandQueue) 183 | if (_commandQueue.Count > 0) 184 | command = _commandQueue.Peek(); 185 | if (command != null) 186 | _transport.Send(command.ToByteArray(), DataType.Command); 187 | } 188 | } 189 | 190 | private void SendCommand(Command command) 191 | { 192 | lock (_commandQueue) 193 | { 194 | _commandQueue.Enqueue(command); 195 | if (_commandQueue.Count == 1) 196 | _executeNextCommand.Set(); 197 | } 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/WinBeacon/Eddystone.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Text; 11 | using WinBeacon.Stack; 12 | using WinBeacon.Stack.Hci.Events; 13 | 14 | namespace WinBeacon 15 | { 16 | /// 17 | /// Abstract base class that represents an Eddystone Beacon. 18 | /// 19 | public abstract class Eddystone 20 | { 21 | /// 22 | /// Bluetooth MAC-address of the beacon. 23 | /// 24 | public byte[] Address { get; private set; } 25 | 26 | /// 27 | /// RSSI power of the beacon in dB. 28 | /// 29 | public int Rssi { get; internal set; } 30 | 31 | /// 32 | /// Constructor. 33 | /// 34 | protected Eddystone() 35 | { 36 | } 37 | 38 | /// 39 | /// Parses an incoming for Eddystone frames. 40 | /// 41 | /// The incoming event frame. 42 | /// An instance or null in case the event could not be parsed as an Eddystone event. 43 | /// Implemented according to https://github.com/google/eddystone/blob/master/protocol-specification.md 44 | internal static Eddystone Parse(LeAdvertisingEvent e) 45 | { 46 | if (e.EventType == LeAdvertisingEventType.ScanRsp) 47 | return null; 48 | 49 | var payload = new Queue(e.Payload); 50 | 51 | // Skip flags 52 | var flagsLength = payload.Dequeue(); 53 | payload.Dequeue(flagsLength); 54 | 55 | // Extract and verify Eddystone UUID 56 | // The Complete List of 16-bit Service UUIDs as defined in The Bluetooth Core Specification Supplement (CSS) v5, Part A, § 1.1. 57 | // The Complete List of 16-bit Service UUIDs must contain the Eddystone Service UUID of 0xFEAA. This is included to allow background scanning on iOS devices. 58 | var eddystoneUuidLength = payload.Dequeue(); 59 | if (eddystoneUuidLength != 3) 60 | return null; 61 | payload.Dequeue(); 62 | var eddystoneUuid = (ushort)((payload.Dequeue() << 8) + payload.Dequeue()); 63 | if (eddystoneUuid != 0xAAFE) 64 | return null; 65 | 66 | // Extract service data 67 | // The Service Data data type, Ibid., § 1.11. 68 | // The Service Data - 16 bit UUID data type must be the Eddystone Service UUID of 0xFEAA 69 | var serviceDataLength = payload.Dequeue(); 70 | var serviceDataTypeValue = payload.Dequeue(); 71 | eddystoneUuid = (ushort)((payload.Dequeue() << 8) + payload.Dequeue()); 72 | if (eddystoneUuid != 0xAAFE) 73 | return null; 74 | var eddystoneFrameType = (EddystoneFrameType)payload.Dequeue(); 75 | Eddystone result = null; 76 | switch (eddystoneFrameType) 77 | { 78 | case EddystoneFrameType.Uid: 79 | result = EddystoneUid.ParseFrame(payload); 80 | break; 81 | case EddystoneFrameType.Url: 82 | result = EddystoneUrl.ParseFrame(payload); 83 | break; 84 | case EddystoneFrameType.Tlm: 85 | result = EddystoneTlm.ParseFrame(payload); 86 | break; 87 | case EddystoneFrameType.Eid: 88 | result = EddystoneEid.ParseFrame(payload); 89 | break; 90 | } 91 | 92 | if (result != null) 93 | { 94 | result.Address = e.Address; 95 | result.Rssi = e.Rssi; 96 | } 97 | 98 | return result; 99 | } 100 | } 101 | 102 | internal enum EddystoneFrameType : byte 103 | { 104 | Uid = 0x00, 105 | Url = 0x10, 106 | Tlm = 0x20, 107 | Eid = 0x30, 108 | } 109 | 110 | /// 111 | /// Class that represents an Eddystone Beacon sending Eddystone-UID frames. 112 | /// 113 | public sealed class EddystoneUid : Eddystone 114 | { 115 | /// 116 | /// Calibrated TX power of the beacon in dB at 0m. 117 | /// 118 | public int CalibratedTxPower { get; private set; } 119 | 120 | /// 121 | /// The 10-byte namespace. 122 | /// 123 | public byte[] Namespace { get; private set; } 124 | 125 | /// 126 | /// The 6-byte instance. 127 | /// 128 | public byte[] Instance { get; private set; } 129 | 130 | internal static EddystoneUid ParseFrame(Queue payload) 131 | { 132 | if (payload.Count != 19) 133 | return null; 134 | 135 | return new EddystoneUid 136 | { 137 | CalibratedTxPower = (sbyte)payload.Dequeue(), 138 | Namespace = payload.Dequeue(10), 139 | Instance = payload.Dequeue(6), 140 | }; 141 | } 142 | 143 | /// 144 | /// Returns a string representation of the Eddystone UID data. 145 | /// 146 | /// String representation of the Eddystone UID data. 147 | public override string ToString() 148 | => $"Namespace: {BitConverter.ToString(Namespace)}, Instance: {BitConverter.ToString(Instance)}, TxPower: {CalibratedTxPower}dB"; 149 | } 150 | 151 | /// 152 | /// Class that represents an Eddystone Beacon sending Eddystone-URL frames. 153 | /// 154 | public sealed class EddystoneUrl : Eddystone 155 | { 156 | /// 157 | /// Calibrated TX power of the beacon in dB at 0m. 158 | /// 159 | public int CalibratedTxPower { get; private set; } 160 | 161 | /// 162 | /// The URL broadcasted by the Eddystone Beacon. 163 | /// 164 | public string Url { get; private set; } 165 | 166 | /// 167 | /// Returns a string representation of the Eddystone UID data. 168 | /// 169 | /// String representation of the Eddystone UID data. 170 | public override string ToString() 171 | => $"URL: {Url}, TxPower: {CalibratedTxPower}dB"; 172 | 173 | internal static EddystoneUrl ParseFrame(Queue payload) 174 | { 175 | if (payload.Count < 3) 176 | return null; 177 | 178 | var calibratedTxPower = (sbyte)payload.Dequeue(); 179 | var schemePrefix = payload.Dequeue(); 180 | var encodedUrl = payload.DequeueAll(); 181 | 182 | return new EddystoneUrl 183 | { 184 | CalibratedTxPower = calibratedTxPower, 185 | Url = $"{GetSchemePrefix(schemePrefix)}{string.Join(string.Empty, encodedUrl.Select(DecodeEncodedChar))}", 186 | }; 187 | } 188 | 189 | private static string GetSchemePrefix(byte schemePrefix) 190 | { 191 | switch (schemePrefix) 192 | { 193 | case 0x00: return "http://www."; 194 | case 0x01: return "https://www."; 195 | case 0x02: return "http://"; 196 | case 0x03: return "https://"; 197 | default: return string.Empty; 198 | } 199 | } 200 | 201 | private static string DecodeEncodedChar(byte encodedChar) 202 | { 203 | switch (encodedChar) 204 | { 205 | case 0x00: return ".com/"; 206 | case 0x01: return ".org/"; 207 | case 0x02: return ".edu/"; 208 | case 0x03: return ".net/"; 209 | case 0x04: return ".info/"; 210 | case 0x05: return ".biz/"; 211 | case 0x06: return ".gov/"; 212 | case 0x07: return ".com"; 213 | case 0x08: return ".org"; 214 | case 0x09: return ".edu"; 215 | case 0x0A: return ".net"; 216 | case 0x0B: return ".info"; 217 | case 0x0C: return ".biz"; 218 | case 0x0D: return ".gov"; 219 | } 220 | 221 | if (encodedChar <= 0x20 || encodedChar >= 0x7F) 222 | return string.Empty; 223 | 224 | return new string((char)encodedChar, 1); 225 | } 226 | } 227 | 228 | /// 229 | /// Class that represents an Eddystone Beacon sending Eddystone-TLM frames. 230 | /// 231 | public sealed class EddystoneTlm : Eddystone 232 | { 233 | internal static EddystoneTlm ParseFrame(Queue payload) 234 | { 235 | return null; 236 | } 237 | } 238 | 239 | /// 240 | /// Class that represents an Eddystone Beacon sending Eddystone-EID frames. 241 | /// 242 | public sealed class EddystoneEid : Eddystone 243 | { 244 | internal static EddystoneEid ParseFrame(Queue payload) 245 | { 246 | return null; 247 | } 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /src/WinBeacon.Stack/Hci/Commands/LeSetAdvertisingParametersCommand.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using System; 8 | using WinBeacon.Stack.Hci.Opcodes; 9 | using WinBeacon.Stack.Hci.Parameters; 10 | 11 | namespace WinBeacon.Stack.Hci.Commands 12 | { 13 | internal class LeSetAdvertisingParametersCommand : Command 14 | { 15 | /// 16 | /// Set the advertising parameters 17 | /// The Advertising_Interval_Min and Advertising_Interval_Max shall not be set to less than 0x00A0 (100 ms) if the Advertising_Type is set to 0x02 (ADV_SCAN_IND) or 0x03 (ADV_NONCONN_IND). 18 | /// The Host shall not issue this command when advertising is enabled in the Controller; if it is the Command Disallowed error code shall be used. 19 | /// 20 | /// Minimum advertising interval for undirected and low duty cycle directed advertising. Ranges from 20 ms to 10.24 sec. Defaults to 1.28 sec. 21 | /// Maximum advertising interval for undirected and low duty cycle directed advertising. Ranges from 20 ms to 10.24 sec. Defaults to 1.28 sec. 22 | /// Used to determine the packet type that is used for advertising when advertising is enabled. 23 | /// This parameter indicates the type of address being used in the advertising packets. 24 | /// This parameter contains the Peer’s Identity Type. 25 | /// This parameter contains the peer’s Identity Address. 26 | /// This is a bit field that indicates the advertising channels that shall be used when transmitting advertising packets.At least one channel bit shall be set in the Advertising_Channel_Map parameter. 27 | /// This parameter shall be ignored when directed advertising is enabled. 28 | public LeSetAdvertisingParametersCommand( 29 | int minimumAdvertisingIntervalInMs, 30 | int maximumAdvertisingIntervalInMs, 31 | AdvertisingType advertisingType, 32 | OwnAddressType ownAdressType, 33 | PeerAddressType peerAdressType, 34 | byte[] peerAddress, 35 | AdvertisingChannelMap advertisingChannelMap, 36 | AdvertisingFilterPolicy advertisingFilterPolicy) 37 | : base(OpcodeGroup.LeController, (int)LeControllerOpcode.SetAdvertisingParameters) 38 | { 39 | if (peerAddress == null) throw new ArgumentNullException("peerAddress"); 40 | if (peerAddress.Length != 6) throw new ArgumentOutOfRangeException("peerAddress", "The peer address should consist of exactly 6 bytes."); 41 | 42 | ushort minimumAdvertisingIntervalCode = (ushort)Math.Ceiling(minimumAdvertisingIntervalInMs / 0.625); 43 | ushort maximumAdvertisingIntervalCode = (ushort)Math.Ceiling(maximumAdvertisingIntervalInMs / 0.625); 44 | if (advertisingType != AdvertisingType.ConnectableHighDutyCycleDirected) 45 | { 46 | if (minimumAdvertisingIntervalCode > 0x4000) throw new ArgumentOutOfRangeException("minimumAdvertisingIntervalCode", "Interval too long, should be less than 10240 ms"); 47 | if (maximumAdvertisingIntervalCode > 0x4000) throw new ArgumentOutOfRangeException("maximumAdvertisingIntervalCode", "Interval too long, should be less than 10240 ms"); 48 | if (advertisingType == AdvertisingType.ScannableUndirectedAdvertising || advertisingType == AdvertisingType.NonConnectableUndirected) 49 | { 50 | if (minimumAdvertisingIntervalCode < 0x00A0) throw new ArgumentOutOfRangeException("minimumAdvertisingIntervalCode", "Interval too short, should not be less than 100 ms"); 51 | if (maximumAdvertisingIntervalCode < 0x00A0) throw new ArgumentOutOfRangeException("maximumAdvertisingIntervalCode", "Interval too short, should not be less than 100 ms"); 52 | } 53 | else 54 | { 55 | if (minimumAdvertisingIntervalCode < 0x0020) throw new ArgumentOutOfRangeException("minimumAdvertisingIntervalCode", "Interval too short, should be more than 20 ms"); 56 | if (maximumAdvertisingIntervalCode < 0x0020) throw new ArgumentOutOfRangeException("maximumAdvertisingIntervalCode", "Interval too short, should be more than 20 ms"); 57 | } 58 | } 59 | 60 | Parameters.Add(new UshortCommandParameter(minimumAdvertisingIntervalCode)); 61 | Parameters.Add(new UshortCommandParameter(maximumAdvertisingIntervalCode)); 62 | Parameters.Add(new ByteCommandParameter((byte)advertisingType)); 63 | Parameters.Add(new ByteCommandParameter((byte)ownAdressType)); 64 | Parameters.Add(new ByteCommandParameter((byte)peerAdressType)); 65 | Parameters.Add(new ByteArrayCommandParameter(peerAddress)); 66 | Parameters.Add(new ByteCommandParameter((byte)advertisingChannelMap)); 67 | Parameters.Add(new ByteCommandParameter((byte)advertisingFilterPolicy)); 68 | } 69 | } 70 | 71 | #region Related enums 72 | 73 | /// 74 | /// The AdvertisingType is used to determine the packet type during advertising. 75 | /// 76 | internal enum AdvertisingType : byte 77 | { 78 | /// 79 | /// Connectable undirected advertising (ADV_IND) (default). 80 | /// 81 | ConnectableUndirected = 0x00, 82 | /// 83 | /// Connectable high duty cycle directed advertising (ADV_DIRECT_IND, high duty cycle). 84 | /// 85 | ConnectableHighDutyCycleDirected = 0x01, 86 | /// 87 | /// Scannable undirected advertising (ADV_SCAN_IND). 88 | /// 89 | ScannableUndirectedAdvertising = 0x02, 90 | /// 91 | /// Non connectable undirected advertising (ADV_NONCONN_IND). 92 | /// 93 | NonConnectableUndirected = 0x03, 94 | /// 95 | /// Connectable low duty cycle directed advertising (ADV_DIRECT_IND, low duty cycle). 96 | /// 97 | ConnectableLowDutyCycleDirected = 0x04 98 | } 99 | 100 | /// 101 | /// OwnAddressType parameter indicates the type of address being used in the advertising packets. 102 | /// 103 | internal enum OwnAddressType : byte 104 | { 105 | /// 106 | /// Public Device Address (default). 107 | /// 108 | Public = 0x00, 109 | /// 110 | /// Random Device Address. 111 | /// 112 | Random = 0x01, 113 | /// 114 | /// Controller generates Resolvable Private Address based on the local IRK from resolving list. If resolving list contains no matching entry, use public address. 115 | /// 116 | ResolveFromListOrPublic = 0x02, 117 | /// 118 | /// Controller generates Resolvable Private Address based on the local IRK from resolving list. If resolving list contains no matching entry, use random address from LE_Set_Random_Address. 119 | /// 120 | ResolveFromListOrRandom = 0x03 121 | } 122 | 123 | internal enum PeerAddressType : byte 124 | { 125 | /// 126 | /// Public Device Address (default) or Public Identity Address. 127 | /// 128 | Public = 0x00, 129 | /// 130 | /// Random Device Address or Random (static) Identity Address. 131 | /// 132 | Random = 0x01 133 | } 134 | 135 | /// 136 | /// The AdvertisingChannelMap is a flags field that indicates the advertising channels that shall be used when transmitting advertising packets. 137 | /// 138 | [Flags] 139 | internal enum AdvertisingChannelMap : byte 140 | { 141 | /// 142 | /// Channel 37 shall be used. 143 | /// 144 | UseChannel37 = 0x01, 145 | /// 146 | /// Channel 38 shall be used. 147 | /// 148 | UseChannel38 = 0x02, 149 | /// 150 | /// Channel 39 shall be used. 151 | /// 152 | UseChannel39 = 0x04, 153 | /// 154 | /// Default (all channels enabled). 155 | /// 156 | UseAllChannels = UseChannel37 | UseChannel38 | UseChannel39 157 | } 158 | 159 | internal enum AdvertisingFilterPolicy : byte 160 | { 161 | /// 162 | /// Process scan and connection requests from all devices (i.e., the White List is not in use) (default). 163 | /// 164 | ConnectAllScanAll = 0x00, 165 | /// 166 | /// Process connection requests from all devices and only scan requests from devices that are in the White List. 167 | /// 168 | ConnectAllScanWhiteList = 0x01, 169 | /// 170 | /// Process scan requests from all devices and only connection requests from devices that are in the White List.. 171 | /// 172 | ConnectWhiteListScanAll = 0x02, 173 | /// 174 | /// Process scan and connection requests only from devices in the White List. 175 | /// 176 | ConnectWhiteListScanWhiteList = 0x03 177 | } 178 | 179 | #endregion 180 | } 181 | -------------------------------------------------------------------------------- /test/WinBeacon.Stack.Tests/Hci/CommandTests.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Huysentruit Wouter 3 | * 4 | * See LICENSE file. 5 | */ 6 | 7 | using System; 8 | using NUnit.Framework; 9 | using WinBeacon.Stack.Hci; 10 | using WinBeacon.Stack.Hci.Commands; 11 | using WinBeacon.Stack.Hci.Events; 12 | 13 | namespace WinBeacon.Stack.Tests.Hci 14 | { 15 | [TestFixture] 16 | public class CommandTests 17 | { 18 | [Test] 19 | public void Command_SimpleToByteArray() 20 | { 21 | var command = new Command(OpcodeGroup.LeController, 0x1234); 22 | Assert.AreEqual(command.ToByteArray(), new byte[] { 0x34, 0x32, 0x00 }); 23 | } 24 | 25 | [Test] 26 | public void Command_WithParamToByteArray() 27 | { 28 | var parameter = new TestParameter { Data = new byte[] { 0x98, 0x76, 0x54, 0x32, 0x10 } }; 29 | var command = new Command(OpcodeGroup.InformationalParameters, 0x12345); 30 | command.Parameters.Add(parameter); 31 | Assert.AreEqual(command.ToByteArray(), new byte[] { 0x45, 0x33, 0x05, 0x98, 0x76, 0x54, 0x32, 0x10 }); 32 | } 33 | 34 | [Test] 35 | public void Command_OnCommandComplete() 36 | { 37 | Command callbackCommand = null; 38 | var command = new Command(OpcodeGroup.LeController, 0x1234); 39 | command.CommandCompleteCallback = (cmd, evt) => callbackCommand = cmd; 40 | Assert.IsNull(callbackCommand, "Callback called too early"); 41 | command.OnCommandComplete(new CommandCompleteEvent()); 42 | Assert.AreEqual(command, callbackCommand, "Command from callback differs"); 43 | } 44 | 45 | [Test] 46 | public void Command_GenericOnCommandComplete() 47 | { 48 | Command callbackCommand = null; 49 | var command = new GenericTestCommand(); 50 | command.CommandCompleteCallback = (cmd, result) => 51 | { 52 | callbackCommand = cmd; 53 | Assert.AreEqual(result, 0x4687, "Result incorrect"); 54 | }; 55 | command.OnCommandComplete(new CommandCompleteEvent()); 56 | Assert.AreEqual(command, callbackCommand, "Command from callback differs"); 57 | } 58 | 59 | [Test] 60 | public void Command_LeSetAdvertisingDataCommand() 61 | { 62 | var command = new LeSetAdvertisingDataCommand(new byte[] { 0x12, 0x57, 0x78, 0x32 }); 63 | Assert.AreEqual(new byte[] { 0x08, 0x20, 0x05, 0x04, 0x12, 0x57, 0x78, 0x32 }, command.ToByteArray()); 64 | } 65 | 66 | [Test] 67 | public void Command_LeSetAdvertisingEnableCommand() 68 | { 69 | var command = new LeSetAdvertisingEnableCommand(false); 70 | Assert.AreEqual(new byte[] { 0x0A, 0x20, 0x01, 0x00 }, command.ToByteArray()); 71 | command = new LeSetAdvertisingEnableCommand(true); 72 | Assert.AreEqual(new byte[] { 0x0A, 0x20, 0x01, 0x01 }, command.ToByteArray()); 73 | } 74 | 75 | [Test] 76 | public void Command_LeSetAdvertisingParametersCommand() 77 | { 78 | Assert.That(() => new LeSetAdvertisingParametersCommand(20, 20, AdvertisingType.ConnectableUndirected, OwnAddressType.Public, PeerAddressType.Public, new byte[6], AdvertisingChannelMap.UseAllChannels, AdvertisingFilterPolicy.ConnectAllScanAll), Throws.Nothing); 79 | Assert.That(() => new LeSetAdvertisingParametersCommand(19, 20, AdvertisingType.ConnectableUndirected, OwnAddressType.Public, PeerAddressType.Public, new byte[6], AdvertisingChannelMap.UseAllChannels, AdvertisingFilterPolicy.ConnectAllScanAll), Throws.InstanceOf()); 80 | Assert.That(() => new LeSetAdvertisingParametersCommand(20, 19, AdvertisingType.ConnectableUndirected, OwnAddressType.Public, PeerAddressType.Public, new byte[6], AdvertisingChannelMap.UseAllChannels, AdvertisingFilterPolicy.ConnectAllScanAll), Throws.InstanceOf()); 81 | Assert.That(() => new LeSetAdvertisingParametersCommand(10240, 10240, AdvertisingType.ConnectableUndirected, OwnAddressType.Public, PeerAddressType.Public, new byte[6], AdvertisingChannelMap.UseAllChannels, AdvertisingFilterPolicy.ConnectAllScanAll), Throws.Nothing); 82 | Assert.That(() => new LeSetAdvertisingParametersCommand(10241, 10240, AdvertisingType.ConnectableUndirected, OwnAddressType.Public, PeerAddressType.Public, new byte[6], AdvertisingChannelMap.UseAllChannels, AdvertisingFilterPolicy.ConnectAllScanAll), Throws.InstanceOf()); 83 | Assert.That(() => new LeSetAdvertisingParametersCommand(10240, 10241, AdvertisingType.ConnectableUndirected, OwnAddressType.Public, PeerAddressType.Public, new byte[6], AdvertisingChannelMap.UseAllChannels, AdvertisingFilterPolicy.ConnectAllScanAll), Throws.InstanceOf()); 84 | var command = new LeSetAdvertisingParametersCommand(200, 400, AdvertisingType.ConnectableUndirected, OwnAddressType.Public, PeerAddressType.Public, new byte[6], AdvertisingChannelMap.UseAllChannels, AdvertisingFilterPolicy.ConnectAllScanAll); 85 | Assert.That(command.ToByteArray(), Is.EqualTo(new byte[] { 0x06, 0x20, 0x0F, 0x40, 0x01, 0x80, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00 })); 86 | } 87 | 88 | [Test] 89 | public void Command_LeSetScanEnableCommand() 90 | { 91 | var command = new LeSetScanEnableCommand(false, false); 92 | Assert.AreEqual(new byte[] { 0x0C, 0x20, 0x02, 0x00, 0x00 }, command.ToByteArray()); 93 | command = new LeSetScanEnableCommand(false, true); 94 | Assert.AreEqual(new byte[] { 0x0C, 0x20, 0x02, 0x00, 0x01 }, command.ToByteArray()); 95 | command = new LeSetScanEnableCommand(true, false); 96 | Assert.AreEqual(new byte[] { 0x0C, 0x20, 0x02, 0x01, 0x00 }, command.ToByteArray()); 97 | command = new LeSetScanEnableCommand(true, true); 98 | Assert.AreEqual(new byte[] { 0x0C, 0x20, 0x02, 0x01, 0x01 }, command.ToByteArray()); 99 | } 100 | 101 | [Test] 102 | public void Command_LeSetScanParametersCommand() 103 | { 104 | var command = new LeSetScanParametersCommand(false, 0x1234, 0x2345, false, false); 105 | Assert.AreEqual(new byte[] { 0x0B, 0x20, 0x07, 0x00, 0x20, 0x1D, 0x6F, 0x38, 0x00, 0x00 }, command.ToByteArray()); 106 | command = new LeSetScanParametersCommand(false, 0x2345, 0x1234, false, true); 107 | Assert.AreEqual(new byte[] { 0x0B, 0x20, 0x07, 0x00, 0x6F, 0x38, 0x20, 0x1D, 0x00, 0x01 }, command.ToByteArray()); 108 | command = new LeSetScanParametersCommand(false, 0x1234, 0x2345, true, false); 109 | Assert.AreEqual(new byte[] { 0x0B, 0x20, 0x07, 0x00, 0x20, 0x1D, 0x6F, 0x38, 0x01, 0x00 }, command.ToByteArray()); 110 | command = new LeSetScanParametersCommand(false, 0x2345, 0x1234, true, true); 111 | Assert.AreEqual(new byte[] { 0x0B, 0x20, 0x07, 0x00, 0x6F, 0x38, 0x20, 0x1D, 0x01, 0x01 }, command.ToByteArray()); 112 | command = new LeSetScanParametersCommand(true, 0x1234, 0x2345, false, false); 113 | Assert.AreEqual(new byte[] { 0x0B, 0x20, 0x07, 0x01, 0x20, 0x1D, 0x6F, 0x38, 0x00, 0x00 }, command.ToByteArray()); 114 | command = new LeSetScanParametersCommand(true, 0x2345, 0x1234, false, true); 115 | Assert.AreEqual(new byte[] { 0x0B, 0x20, 0x07, 0x01, 0x6F, 0x38, 0x20, 0x1D, 0x00, 0x01 }, command.ToByteArray()); 116 | command = new LeSetScanParametersCommand(true, 0x1234, 0x2345, true, false); 117 | Assert.AreEqual(new byte[] { 0x0B, 0x20, 0x07, 0x01, 0x20, 0x1D, 0x6F, 0x38, 0x01, 0x00 }, command.ToByteArray()); 118 | command = new LeSetScanParametersCommand(true, 0x2345, 0x1234, true, true); 119 | Assert.AreEqual(new byte[] { 0x0B, 0x20, 0x07, 0x01, 0x6F, 0x38, 0x20, 0x1D, 0x01, 0x01 }, command.ToByteArray()); 120 | } 121 | 122 | [Test] 123 | public void Command_LeWriteHostSupportedCommand() 124 | { 125 | var command = new LeWriteHostSupportedCommand(false, false); 126 | Assert.AreEqual(new byte[] { 0x6D, 0x0C, 0x02, 0x00, 0x00 }, command.ToByteArray()); 127 | command = new LeWriteHostSupportedCommand(false, true); 128 | Assert.AreEqual(new byte[] { 0x6D, 0x0C, 0x02, 0x00, 0x01 }, command.ToByteArray()); 129 | command = new LeWriteHostSupportedCommand(true, false); 130 | Assert.AreEqual(new byte[] { 0x6D, 0x0C, 0x02, 0x01, 0x00 }, command.ToByteArray()); 131 | command = new LeWriteHostSupportedCommand(true, true); 132 | Assert.AreEqual(new byte[] { 0x6D, 0x0C, 0x02, 0x01, 0x01 }, command.ToByteArray()); 133 | } 134 | 135 | [Test] 136 | public void Command_ResetCommand() 137 | { 138 | var command = new ResetCommand(); 139 | Assert.AreEqual(new byte[] { 0x03, 0x0C, 0x00 }, command.ToByteArray()); 140 | } 141 | 142 | [Test] 143 | public void Command_ReadBdAddrCommand() 144 | { 145 | var command = new ReadBdAddrCommand(); 146 | Assert.AreEqual(new byte[] { 0x09, 0x10, 0x00 }, command.ToByteArray()); 147 | } 148 | 149 | [Test] 150 | public void Command_ReadLocalVersionCommand() 151 | { 152 | var command = new ReadLocalVersionCommand(); 153 | Assert.AreEqual(new byte[] { 0x01, 0x10, 0x00 }, command.ToByteArray()); 154 | } 155 | 156 | [Test] 157 | public void Command_LeReadHostSupportedCommand() 158 | { 159 | var command = new LeReadHostSupportedCommand(); 160 | Assert.AreEqual(new byte[] { 0x6C, 0x0C, 0x00 }, command.ToByteArray()); 161 | } 162 | 163 | [Test] 164 | public void Command_LeSetEventMaskCommand() 165 | { 166 | var command = new LeSetEventMaskCommand(new byte[] { 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88 }); 167 | Assert.AreEqual(new byte[] { 0x01, 0x20, 0x08, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88 }, command.ToByteArray()); 168 | } 169 | 170 | [Test] 171 | public void Command_SetEventMaskCommand() 172 | { 173 | var command = new SetEventMaskCommand(new byte[] { 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88 }); 174 | Assert.AreEqual(new byte[] { 0x01, 0x0C, 0x08, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88 }, command.ToByteArray()); 175 | } 176 | 177 | #region Helpers 178 | 179 | private class TestParameter : ICommandParameter 180 | { 181 | public byte[] Data { get; set; } 182 | public byte[] ToByteArray() { return Data; } 183 | } 184 | 185 | private class GenericTestCommand : Command 186 | { 187 | public GenericTestCommand() 188 | : base(OpcodeGroup.LinkControl, 0x7854) 189 | { 190 | } 191 | internal override ushort ParseCommandResult(CommandCompleteEvent e) 192 | { 193 | return 0x4687; 194 | } 195 | } 196 | 197 | #endregion 198 | } 199 | } 200 | --------------------------------------------------------------------------------