diff --git a/.gitignore b/.gitignore index 52aeb7a..96d3623 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ *.user *.userosscache *.sln.docstates +.idea/ # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs diff --git a/README.md b/README.md index 594a0a2..1c4277d 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # LifxNet -A .NET Standard 1.3 library for LIFX. -Supports .NET, UWP, Xamarin iOS, Xamarin Android, and any other .NET Platform that has implemented .NET Standard 1.3+. +A .NET Standard 2.0 library for LIFX. +Supports .NET, UWP, Xamarin iOS, Xamarin Android, and any other .NET Platform that has implemented .NET Standard 2.0+. For Cloud Protocol based implementation, check out [isaacrlevin's repo](https://github.com/isaacrlevin/LifxCloudClient) @@ -22,14 +22,14 @@ PM> Install-Package LifxNet Tested with LIFX 2.0 Firmware. -Based on the official [LIFX protocol docs](https://github.com/LIFX/lifx-protocol-docs) +Based on the official [LIFX protocol docs](https://lan.developer.lifx.com/docs) ####Usage ```csharp - client = await LifxNet.LifxClient.CreateAsync(); - client.DeviceDiscovered += Client_DeviceDiscovered; - client.DeviceLost += Client_DeviceLost; + client = new LifxClient(); + client.Discovered += Client_DeviceDiscovered; + client.Lost += Client_DeviceLost; client.StartDeviceDiscovery(); ... diff --git a/src/LifxNet.sln b/src/LifxNet.sln index 49eedb8..7a700a2 100644 --- a/src/LifxNet.sln +++ b/src/LifxNet.sln @@ -17,6 +17,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleApp.netcore", "Sample EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleApp.Universal", "SampleApps\SampleApp.Universal\SampleApp.Universal.csproj", "{9072BB5D-AF63-4E60-98A6-86AE8CF898B1}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LifxEmulator", "SampleApps\LifxEmulator\LifxEmulator.csproj", "{35655608-57DD-43AB-9D2E-81B329CC6473}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ColorSendTest", "SampleApps\ColorSendTest\ColorSendTest.csproj", "{EBB918D2-1614-43A8-B7F9-AD4F1285C9A7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -83,6 +87,38 @@ Global {9072BB5D-AF63-4E60-98A6-86AE8CF898B1}.Release|x86.ActiveCfg = Release|x86 {9072BB5D-AF63-4E60-98A6-86AE8CF898B1}.Release|x86.Build.0 = Release|x86 {9072BB5D-AF63-4E60-98A6-86AE8CF898B1}.Release|x86.Deploy.0 = Release|x86 + {35655608-57DD-43AB-9D2E-81B329CC6473}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {35655608-57DD-43AB-9D2E-81B329CC6473}.Debug|Any CPU.Build.0 = Debug|Any CPU + {35655608-57DD-43AB-9D2E-81B329CC6473}.Debug|ARM.ActiveCfg = Debug|Any CPU + {35655608-57DD-43AB-9D2E-81B329CC6473}.Debug|ARM.Build.0 = Debug|Any CPU + {35655608-57DD-43AB-9D2E-81B329CC6473}.Debug|x64.ActiveCfg = Debug|Any CPU + {35655608-57DD-43AB-9D2E-81B329CC6473}.Debug|x64.Build.0 = Debug|Any CPU + {35655608-57DD-43AB-9D2E-81B329CC6473}.Debug|x86.ActiveCfg = Debug|Any CPU + {35655608-57DD-43AB-9D2E-81B329CC6473}.Debug|x86.Build.0 = Debug|Any CPU + {35655608-57DD-43AB-9D2E-81B329CC6473}.Release|Any CPU.ActiveCfg = Release|Any CPU + {35655608-57DD-43AB-9D2E-81B329CC6473}.Release|Any CPU.Build.0 = Release|Any CPU + {35655608-57DD-43AB-9D2E-81B329CC6473}.Release|ARM.ActiveCfg = Release|Any CPU + {35655608-57DD-43AB-9D2E-81B329CC6473}.Release|ARM.Build.0 = Release|Any CPU + {35655608-57DD-43AB-9D2E-81B329CC6473}.Release|x64.ActiveCfg = Release|Any CPU + {35655608-57DD-43AB-9D2E-81B329CC6473}.Release|x64.Build.0 = Release|Any CPU + {35655608-57DD-43AB-9D2E-81B329CC6473}.Release|x86.ActiveCfg = Release|Any CPU + {35655608-57DD-43AB-9D2E-81B329CC6473}.Release|x86.Build.0 = Release|Any CPU + {EBB918D2-1614-43A8-B7F9-AD4F1285C9A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EBB918D2-1614-43A8-B7F9-AD4F1285C9A7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EBB918D2-1614-43A8-B7F9-AD4F1285C9A7}.Debug|ARM.ActiveCfg = Debug|Any CPU + {EBB918D2-1614-43A8-B7F9-AD4F1285C9A7}.Debug|ARM.Build.0 = Debug|Any CPU + {EBB918D2-1614-43A8-B7F9-AD4F1285C9A7}.Debug|x64.ActiveCfg = Debug|Any CPU + {EBB918D2-1614-43A8-B7F9-AD4F1285C9A7}.Debug|x64.Build.0 = Debug|Any CPU + {EBB918D2-1614-43A8-B7F9-AD4F1285C9A7}.Debug|x86.ActiveCfg = Debug|Any CPU + {EBB918D2-1614-43A8-B7F9-AD4F1285C9A7}.Debug|x86.Build.0 = Debug|Any CPU + {EBB918D2-1614-43A8-B7F9-AD4F1285C9A7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EBB918D2-1614-43A8-B7F9-AD4F1285C9A7}.Release|Any CPU.Build.0 = Release|Any CPU + {EBB918D2-1614-43A8-B7F9-AD4F1285C9A7}.Release|ARM.ActiveCfg = Release|Any CPU + {EBB918D2-1614-43A8-B7F9-AD4F1285C9A7}.Release|ARM.Build.0 = Release|Any CPU + {EBB918D2-1614-43A8-B7F9-AD4F1285C9A7}.Release|x64.ActiveCfg = Release|Any CPU + {EBB918D2-1614-43A8-B7F9-AD4F1285C9A7}.Release|x64.Build.0 = Release|Any CPU + {EBB918D2-1614-43A8-B7F9-AD4F1285C9A7}.Release|x86.ActiveCfg = Release|Any CPU + {EBB918D2-1614-43A8-B7F9-AD4F1285C9A7}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -90,5 +126,7 @@ Global GlobalSection(NestedProjects) = preSolution {9165A342-4844-4655-8D1E-D607CD3AA2BD} = {41199DCE-DF15-4BD2-B5A4-1836B3A5BF54} {9072BB5D-AF63-4E60-98A6-86AE8CF898B1} = {41199DCE-DF15-4BD2-B5A4-1836B3A5BF54} + {35655608-57DD-43AB-9D2E-81B329CC6473} = {41199DCE-DF15-4BD2-B5A4-1836B3A5BF54} + {EBB918D2-1614-43A8-B7F9-AD4F1285C9A7} = {41199DCE-DF15-4BD2-B5A4-1836B3A5BF54} EndGlobalSection EndGlobal diff --git a/src/LifxNet/Color.cs b/src/LifxNet/Color.cs deleted file mode 100644 index 569486b..0000000 --- a/src/LifxNet/Color.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace LifxNet -{ - /// - /// RGB Color structure - /// - public struct Color - { - /// - /// Red - /// - public byte R { get; set; } - - /// - /// Green - /// - public byte G { get; set; } - - /// - /// Blue - /// - public byte B { get; set; } - } -} \ No newline at end of file diff --git a/src/LifxNet/Device.cs b/src/LifxNet/Device.cs new file mode 100644 index 0000000..8f0fbf0 --- /dev/null +++ b/src/LifxNet/Device.cs @@ -0,0 +1,50 @@ +using System; +using System.Linq; + +namespace LifxNet { + /// + /// LIFX Generic Device + /// + public abstract class Device { + internal Device(string hostname, byte[] macAddress, byte service, uint port) { + if (hostname == null) + throw new ArgumentNullException(nameof(hostname)); + if (string.IsNullOrWhiteSpace(hostname)) + throw new ArgumentException(nameof(hostname)); + HostName = hostname; + MacAddress = macAddress; + Service = service; + Port = port; + LastSeen = DateTime.MinValue; + } + + /// + /// Hostname for the device + /// + public string HostName { get; internal set; } + + /// + /// Service ID + /// + public byte Service { get; } + + /// + /// Service port + /// + public uint Port { get; } + + internal DateTime LastSeen { get; set; } + + /// + /// Gets the MAC address + /// + public byte[] MacAddress { get; } + + /// + /// Gets the MAC address + /// + public string MacAddressName { + get { return string.Join(":", MacAddress.Take(6).Select(tb => tb.ToString("X2")).ToArray()); } + } + } +} \ No newline at end of file diff --git a/src/LifxNet/LifxClient.DeviceOperations.cs b/src/LifxNet/LifxClient.DeviceOperations.cs index 4fe5b0b..70a139b 100644 --- a/src/LifxNet/LifxClient.DeviceOperations.cs +++ b/src/LifxNet/LifxClient.DeviceOperations.cs @@ -1,14 +1,9 @@ using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using System.Threading; +using System.Diagnostics; using System.Threading.Tasks; -namespace LifxNet -{ - public partial class LifxClient : IDisposable - { +namespace LifxNet { + public partial class LifxClient { /// /// Turns the device on /// @@ -27,37 +22,28 @@ public partial class LifxClient : IDisposable /// /// /// - public async Task SetDevicePowerStateAsync(Device device, bool isOn) - { + public async Task SetDevicePowerStateAsync(Device device, bool isOn) { if (device == null) throw new ArgumentNullException(nameof(device)); - System.Diagnostics.Debug.WriteLine($"Sending DeviceSetPower({isOn}) to {device.HostName}"); - FrameHeader header = new FrameHeader() - { - Identifier = GetNextIdentifier(), - AcknowledgeRequired = true - }; + Debug.WriteLine($"Sending DeviceSetPower({isOn}) to {device.HostName}"); + FrameHeader header = new FrameHeader(GetNextIdentifier(), true); _ = await BroadcastMessageAsync(device.HostName, header, - MessageType.DeviceSetPower, (UInt16)(isOn ? 65535 : 0)).ConfigureAwait(false); + MessageType.DeviceSetPower, (ushort) (isOn ? 65535 : 0)).ConfigureAwait(false); } /// /// Gets the label for the device /// /// - /// - public async Task GetDeviceLabelAsync(Device device) - { + /// The device label + public async Task GetDeviceLabelAsync(Device device) { if (device == null) throw new ArgumentNullException(nameof(device)); - FrameHeader header = new FrameHeader() - { - Identifier = GetNextIdentifier(), - AcknowledgeRequired = false - }; - var resp = await BroadcastMessageAsync(device.HostName, header, MessageType.DeviceGetLabel).ConfigureAwait(false); + FrameHeader header = new FrameHeader(GetNextIdentifier()); + var resp = await BroadcastMessageAsync(device.HostName, header, + MessageType.DeviceGetLabel).ConfigureAwait(false); return resp.Label; } @@ -67,16 +53,11 @@ public async Task SetDevicePowerStateAsync(Device device, bool isOn) /// /// /// - public async Task SetDeviceLabelAsync(Device device, string label) - { + public async Task SetDeviceLabelAsync(Device device, string label) { if (device == null) throw new ArgumentNullException(nameof(device)); - FrameHeader header = new FrameHeader() - { - Identifier = GetNextIdentifier(), - AcknowledgeRequired = true - }; + FrameHeader header = new FrameHeader(GetNextIdentifier(), true); _ = await BroadcastMessageAsync( device.HostName, header, MessageType.DeviceSetLabel, label).ConfigureAwait(false); } @@ -84,34 +65,212 @@ public async Task SetDeviceLabelAsync(Device device, string label) /// /// Gets the device version /// - public Task GetDeviceVersionAsync(Device device) - { + public Task GetDeviceVersionAsync(Device device) { if (device == null) throw new ArgumentNullException(nameof(device)); - FrameHeader header = new FrameHeader() - { - Identifier = GetNextIdentifier(), - AcknowledgeRequired = false - }; + FrameHeader header = new FrameHeader(GetNextIdentifier()); return BroadcastMessageAsync(device.HostName, header, MessageType.DeviceGetVersion); } + + /// + /// Gets Host MCU firmware information. + /// + /// + /// + public Task GetDeviceHostFirmwareAsync(Device device) { + if (device == null) + throw new ArgumentNullException(nameof(device)); + + FrameHeader header = new FrameHeader(GetNextIdentifier()); + return BroadcastMessageAsync(device.HostName, header, + MessageType.DeviceGetHostFirmware); + } + + /// + /// Get Host MCU information. + /// + /// + /// + /// + public async Task GetHostInfoAsync(Device device) { + if (device == null) + throw new ArgumentNullException(nameof(device)); + + FrameHeader header = new FrameHeader(GetNextIdentifier()); + return await BroadcastMessageAsync(device.HostName, header, + MessageType.DeviceGetHostInfo); + } + + /// + /// Get Host Wifi information. + /// + /// + /// + /// + public async Task GetWifiInfoAsync(Device device) { + if (device == null) + throw new ArgumentNullException(nameof(device)); + + FrameHeader header = new FrameHeader(GetNextIdentifier()); + return await BroadcastMessageAsync(device.HostName, header, + MessageType.DeviceGetWifiInfo); + } + + /// + /// Get Host Wifi firmware information. + /// + /// + /// + /// + public async Task GetWifiFirmwareAsync(Device device) { + if (device == null) + throw new ArgumentNullException(nameof(device)); + + FrameHeader header = new FrameHeader(GetNextIdentifier()); + return await BroadcastMessageAsync(device.HostName, header, + MessageType.DeviceGetWifiFirmware); + } + + /// + /// Get device power level + /// Zero implies standby and non-zero sets a corresponding power draw level. Currently only 0 and 65535 are supported. + /// + /// + /// 0 for off, 1 for on + /// + public async Task GetPowerAsync(Device device) { + if (device == null) + throw new ArgumentNullException(nameof(device)); + + FrameHeader header = new FrameHeader(GetNextIdentifier()); + var level = await BroadcastMessageAsync(device.HostName, header, + MessageType.DeviceGetPower); + return level.Level == 0 ? 0 : 1; + } + + /// + /// Set Device power level. + /// Internally, Lifx offers a range from 0-65535, but actually only responds to 0 and 65535. + /// + /// + /// 0 for off, 1 for on + /// + /// + public async Task SetPowerAsync(Device device, int level) { + if (device == null) + throw new ArgumentNullException(nameof(device)); + + FrameHeader header = new FrameHeader(GetNextIdentifier(), true); + if (level != 0) level = 65535; + await BroadcastMessageAsync(device.HostName, header, + MessageType.DeviceSetPower, level); + } + + /// + /// Get run-time information. + /// + /// + /// + /// + public async Task GetInfoAsync(Device device) { + if (device == null) + throw new ArrayTypeMismatchException(nameof(device)); + FrameHeader header = new FrameHeader(GetNextIdentifier()); + return await BroadcastMessageAsync(device.HostName, header, + MessageType.DeviceGetInfo); + } + + /// + /// Set the device location label + /// + /// + /// + /// + /// + public async Task SetLocationAsync(Device device, string label) { + if (device == null) + throw new ArgumentNullException(nameof(device)); + + FrameHeader header = new FrameHeader(GetNextIdentifier(), true); + var rand = new Random(); + var location = new byte[16]; + rand.NextBytes(location); + var updated = DateTimeOffset.Now.ToUnixTimeSeconds(); + await BroadcastMessageAsync(device.HostName, header, + MessageType.DeviceSetLocation, location, label, updated); + } + + /// + /// Ask the device to return its location information. + /// + /// + /// + /// + public async Task GetLocationAsync(Device device) { + if (device == null) + throw new ArrayTypeMismatchException(nameof(device)); + FrameHeader header = new FrameHeader(GetNextIdentifier()); + return await BroadcastMessageAsync(device.HostName, header, + MessageType.DeviceGetLocation); + } + /// - /// Gets the device's host firmware + /// Set the device group. /// /// + /// The new group name /// - public Task GetDeviceHostFirmwareAsync(Device device) - { + /// + public async Task SetGroupAsync(Device device, string label) { if (device == null) throw new ArgumentNullException(nameof(device)); - FrameHeader header = new FrameHeader() - { - Identifier = GetNextIdentifier(), - AcknowledgeRequired = false - }; - return BroadcastMessageAsync(device.HostName, header, MessageType.DeviceGetHostFirmware); + FrameHeader header = new FrameHeader(GetNextIdentifier(), true); + var rand = new Random(); + var group = new byte[16]; + rand.NextBytes(group); + var updated = DateTimeOffset.Now.ToUnixTimeSeconds(); + await BroadcastMessageAsync(device.HostName, header, + MessageType.DeviceSetGroup, group, label, updated); + } + + /// + /// Get the device group. + /// + /// + /// + /// + public async Task GetGroupAsync(Device device) { + if (device == null) + throw new ArrayTypeMismatchException(nameof(device)); + FrameHeader header = new FrameHeader(GetNextIdentifier()); + return await BroadcastMessageAsync(device.HostName, header, + MessageType.DeviceGetGroup); + } + + /// + /// Request an arbitrary payload be echoed back. + /// + /// + /// + /// + /// + public async Task RequestEcho(Device device, byte[] payload) { + if (device == null) + throw new ArrayTypeMismatchException(nameof(device)); + FrameHeader header = new FrameHeader(GetNextIdentifier()); + // Truncate our input payload to be 64 bits exactly + var realPayload = new byte[64]; + for (var i = 0; i < realPayload.Length; i++) { + if (i < payload.Length) { + realPayload[i] = payload[i]; + } else { + realPayload[i] = 0; + } + } + return await BroadcastMessageAsync(device.HostName, header, + MessageType.DeviceEchoRequest, realPayload); } } -} +} \ No newline at end of file diff --git a/src/LifxNet/LifxClient.Discovery.cs b/src/LifxNet/LifxClient.Discovery.cs index 3f5b402..9576b1a 100644 --- a/src/LifxNet/LifxClient.Discovery.cs +++ b/src/LifxNet/LifxClient.Discovery.cs @@ -1,81 +1,88 @@ using System; using System.Collections.Generic; -using System.IO; +using System.Diagnostics; using System.Linq; -using System.Text; +using System.Net; using System.Threading; using System.Threading.Tasks; -namespace LifxNet -{ - public partial class LifxClient : IDisposable - { - private static uint identifier = 1; - private static object identifierLock = new object(); - private UInt32 discoverSourceID; - private CancellationTokenSource? _DiscoverCancellationSource; - private Dictionary DiscoveredBulbs = new Dictionary(); - - private static uint GetNextIdentifier() - { - lock (identifierLock) - return identifier++; +namespace LifxNet { + public partial class LifxClient { + private static uint _identifier = 1; + private static readonly object IdentifierLock = new object(); + private uint _discoverSourceId; + private CancellationTokenSource? _discoverCancellationSource; + private readonly Dictionary _discoveredBulbs = new Dictionary(); + private readonly int[] _stripIds = {31, 32, 38}; + private readonly int[] _tileIds = {55}; + private readonly int[] _switchIds = {70}; + + private static uint GetNextIdentifier() { + lock (IdentifierLock) { + _identifier++; + } + + return _identifier; } /// /// Event fired when a LIFX bulb is discovered on the network /// public event EventHandler? DeviceDiscovered; + /// /// Event fired when a LIFX bulb hasn't been seen on the network for a while (for more than 5 minutes) /// public event EventHandler? DeviceLost; private IList devices = new List(); - + /// /// Gets a list of currently known devices /// - public IEnumerable Devices { get { return devices; } } + public IEnumerable Devices => devices; /// - /// Event args for and events. + /// Event args for and events. /// - public sealed class DeviceDiscoveryEventArgs : EventArgs - { + public sealed class DeviceDiscoveryEventArgs : EventArgs { internal DeviceDiscoveryEventArgs(Device device) => Device = device; + /// /// The device the event relates to /// public Device Device { get; } } - private void ProcessDeviceDiscoveryMessage(System.Net.IPAddress remoteAddress, int remotePort, LifxResponse msg) - { - string id = msg.Header.TargetMacAddressName; //remoteAddress.ToString() - if (DiscoveredBulbs.ContainsKey(id)) //already discovered - { - DiscoveredBulbs[id].LastSeen = DateTime.UtcNow; //Update datestamp - DiscoveredBulbs[id].HostName = remoteAddress.ToString(); //Update hostname in case IP changed - - return; + private void ProcessDeviceDiscoveryMessage(IPAddress remoteAddress, LifxResponse msg) { + Debug.WriteLine("Processing device discovery message..."); + string id = msg.Header.TargetMacAddressName; //remoteAddress.ToString() + if (_discoveredBulbs.ContainsKey(id)) //already discovered + { + _discoveredBulbs[id].LastSeen = DateTime.UtcNow; //Update datestamp + _discoveredBulbs[id].HostName = remoteAddress.ToString(); //Update hostname in case IP changed + Debug.WriteLine("Device already discovered, skipping."); + return; } - if (msg.Source != discoverSourceID || //did we request the discovery? - _DiscoverCancellationSource == null || - _DiscoverCancellationSource.IsCancellationRequested) //did we cancel discovery? + + if (msg.Source != _discoverSourceId || //did we request the discovery? + _discoverCancellationSource == null || + _discoverCancellationSource.IsCancellationRequested) //did we cancel discovery? return; - var device = new LightBulb(remoteAddress.ToString(), msg.Header.TargetMacAddress, msg.Payload[0] - , BitConverter.ToUInt32(msg.Payload, 1)) - { - LastSeen = DateTime.UtcNow + var address = remoteAddress.ToString(); + var mac = msg.Header.TargetMacAddress; + var svc = msg.Payload.GetUint8(); + var port = msg.Payload.GetUInt32(); + var lastSeen = DateTime.UtcNow; + Debug.WriteLine("Creating generic device: " + address + " and " + port); + var device = new LightBulb(address, mac, svc, port) { + LastSeen = lastSeen }; - DiscoveredBulbs[id] = device; + + _discoveredBulbs[id] = device; devices.Add(device); - if (DeviceDiscovered != null) - { - DeviceDiscovered(this, new DeviceDiscoveryEventArgs(device)); - } + DeviceDiscovered?.Invoke(this, new DeviceDiscoveryEventArgs(device)); } /// @@ -84,123 +91,50 @@ private void ProcessDeviceDiscoveryMessage(System.Net.IPAddress remoteAddress, i /// /// /// - public void StartDeviceDiscovery() - { - if (_DiscoverCancellationSource != null && !_DiscoverCancellationSource.IsCancellationRequested) + public void StartDeviceDiscovery() { + // Reset our list of devices on discovery start + devices = new List(); + if (_discoverCancellationSource != null && !_discoverCancellationSource.IsCancellationRequested) return; - _DiscoverCancellationSource = new CancellationTokenSource(); - var token = _DiscoverCancellationSource.Token; - var source = discoverSourceID = GetNextIdentifier(); + _discoverCancellationSource = new CancellationTokenSource(); + var token = _discoverCancellationSource.Token; + _discoverSourceId = GetNextIdentifier(); //Start discovery thread - Task.Run(async () => - { - System.Diagnostics.Debug.WriteLine("Sending GetServices"); - FrameHeader header = new FrameHeader() - { - Identifier = source - }; - while (!token.IsCancellationRequested) - { - try - { - await BroadcastMessageAsync(null, header, MessageType.DeviceGetService); + Task.Run(async () => { + Debug.WriteLine("Sending GetServices..."); + FrameHeader header = new FrameHeader(_discoverSourceId); + while (!token.IsCancellationRequested) { + try { + await BroadcastMessageAsync("255.255.255.255", header, + MessageType.DeviceGetService); + } catch (Exception e) { + Debug.WriteLine("Broadcast exception: " + e.Message); } - catch { } - await Task.Delay(5000); + + await Task.Delay(10000, token); var lostDevices = devices.Where(d => (DateTime.UtcNow - d.LastSeen).TotalMinutes > 5).ToArray(); - if(lostDevices.Any()) - { - foreach(var device in lostDevices) - { - devices.Remove(device); - DiscoveredBulbs.Remove(device.MacAddressName); - if (DeviceLost != null) - DeviceLost(this, new DeviceDiscoveryEventArgs(device)); - } + if (!lostDevices.Any()) { + continue; + } + + foreach (var device in lostDevices) { + devices.Remove(device); + _discoveredBulbs.Remove(device.MacAddressName); + DeviceLost?.Invoke(this, new DeviceDiscoveryEventArgs(device)); } } - }); + }, token); } /// /// Stops device discovery /// /// - public void StopDeviceDiscovery() - { - if (_DiscoverCancellationSource == null || _DiscoverCancellationSource.IsCancellationRequested) + public void StopDeviceDiscovery() { + if (_discoverCancellationSource == null || _discoverCancellationSource.IsCancellationRequested) return; - _DiscoverCancellationSource.Cancel(); - _DiscoverCancellationSource = null; + _discoverCancellationSource.Cancel(); + _discoverCancellationSource = null; } } - - /// - /// LIFX Generic Device - /// - public abstract class Device - { - internal Device(string hostname, byte[] macAddress, byte service, UInt32 port) - { - if (hostname == null) - throw new ArgumentNullException(nameof(hostname)); - if(string.IsNullOrWhiteSpace(hostname)) - throw new ArgumentException(nameof(hostname)); - HostName = hostname; - MacAddress = macAddress; - Service = service; - Port = port; - LastSeen = DateTime.MinValue; - } - - /// - /// Hostname for the device - /// - public string HostName { get; internal set; } - - /// - /// Service ID - /// - public byte Service { get; } - - /// - /// Service port - /// - public UInt32 Port { get; } - - internal DateTime LastSeen { get; set; } - - /// - /// Gets the MAC address - /// - public byte[] MacAddress { get; } - - /// - /// Gets the MAC address - /// - public string MacAddressName - { - get - { - if (MacAddress == null) return string.Empty; - return string.Join(":", MacAddress.Take(6).Select(tb => tb.ToString("X2")).ToArray()); - } - } - } - /// - /// LIFX light bulb - /// - public sealed class LightBulb : Device - { - /// - /// Initializes a new instance of a bulb instead of relying on discovery. At least the host name must be provide for the device to be usable. - /// - /// Required - /// - /// - /// - public LightBulb(string hostname, byte[] macAddress, byte service = 0, UInt32 port = 0) : base(hostname, macAddress, service, port) - { - } - } -} +} \ No newline at end of file diff --git a/src/LifxNet/LifxClient.LightOperations.cs b/src/LifxNet/LifxClient.LightOperations.cs index 022895e..5639feb 100644 --- a/src/LifxNet/LifxClient.LightOperations.cs +++ b/src/LifxNet/LifxClient.LightOperations.cs @@ -1,15 +1,12 @@ using System; using System.Collections.Generic; -using System.IO; -using System.Text; -using System.Threading; +using System.Diagnostics; using System.Threading.Tasks; -namespace LifxNet -{ - public partial class LifxClient : IDisposable - { - private Dictionary> taskCompletions = new Dictionary>(); +namespace LifxNet { + public partial class LifxClient { + private readonly Dictionary> _taskCompletions = + new Dictionary>(); /// /// Turns a bulb on using the provided transition time @@ -23,7 +20,8 @@ public partial class LifxClient : IDisposable /// /// /// - public Task TurnBulbOnAsync(LightBulb bulb, TimeSpan transitionDuration) => SetLightPowerAsync(bulb, transitionDuration, true); + public Task TurnBulbOnAsync(LightBulb bulb, TimeSpan transitionDuration) => + SetLightPowerAsync(bulb, transitionDuration, true); /// /// Turns a bulb off using the provided transition time @@ -34,7 +32,8 @@ public partial class LifxClient : IDisposable /// /// /// - public Task TurnBulbOffAsync(LightBulb bulb, TimeSpan transitionDuration) => SetLightPowerAsync(bulb, transitionDuration, false); + public Task TurnBulbOffAsync(LightBulb bulb, TimeSpan transitionDuration) => + SetLightPowerAsync(bulb, transitionDuration, false); /// /// Turns a bulb on or off using the provided transition time @@ -49,26 +48,22 @@ public partial class LifxClient : IDisposable /// /// /// - public async Task SetLightPowerAsync(LightBulb bulb, TimeSpan transitionDuration, bool isOn) - { + public async Task SetLightPowerAsync(LightBulb bulb, TimeSpan transitionDuration, bool isOn) { if (bulb == null) - throw new ArgumentNullException("bulb"); - if (transitionDuration.TotalMilliseconds > UInt32.MaxValue || - transitionDuration.Ticks < 0) - throw new ArgumentOutOfRangeException("transitionDuration"); + throw new ArgumentNullException(nameof(bulb)); + if (transitionDuration.TotalMilliseconds > uint.MaxValue || + transitionDuration.Ticks < 0) + throw new ArgumentOutOfRangeException(nameof(transitionDuration)); - FrameHeader header = new FrameHeader() - { - Identifier = GetNextIdentifier(), - AcknowledgeRequired = true - }; + FrameHeader header = new FrameHeader(GetNextIdentifier(), true); - var b = BitConverter.GetBytes((UInt16)transitionDuration.TotalMilliseconds); + var b = BitConverter.GetBytes((ushort) transitionDuration.TotalMilliseconds); - System.Diagnostics.Debug.WriteLine($"Sending LightSetPower(on={isOn},duration={transitionDuration.TotalMilliseconds}ms) to {bulb.HostName}"); + Debug.WriteLine( + $"Sending LightSetPower(on={isOn},duration={transitionDuration.TotalMilliseconds}ms) to {bulb.HostName}"); await BroadcastMessageAsync(bulb.HostName, header, MessageType.LightSetPower, - (UInt16)(isOn ? 65535 : 0), b + (ushort) (isOn ? 65535 : 0), b ).ConfigureAwait(false); } @@ -77,42 +72,49 @@ await BroadcastMessageAsync(bulb.HostName, header, Mess /// /// /// - public async Task GetLightPowerAsync(LightBulb bulb) - { + public async Task GetLightPowerAsync(LightBulb bulb) { if (bulb == null) throw new ArgumentNullException(nameof(bulb)); - FrameHeader header = new FrameHeader() - { - Identifier = GetNextIdentifier(), - AcknowledgeRequired = true - }; + FrameHeader header = new FrameHeader(GetNextIdentifier(), true); return (await BroadcastMessageAsync( bulb.HostName, header, MessageType.LightGetPower).ConfigureAwait(false)).IsOn; } + /// + /// Sets color and temperature of bulb + /// + /// The bulb to set + /// The LifxColor to set the bulb to + /// An optional transition duration, in milliseconds. + /// + public Task SetColorAsync(LightBulb bulb, LifxColor color, int duration = 0) { + return SetColorAsync(bulb, (ushort)color.LifxHue, (ushort)color.LifxSaturation, (ushort)color.LifxBrightness, (ushort)color.K, + TimeSpan.FromMilliseconds(duration)); + } + /// /// Sets color and temperature for a bulb /// /// - /// + /// /// /// - public Task SetColorAsync(LightBulb bulb, Color color, UInt16 kelvin) => SetColorAsync(bulb, color, kelvin, TimeSpan.Zero); + public Task SetColorAsync(LightBulb bulb, LifxColor lifxColor, ushort kelvin) => + SetColorAsync(bulb, lifxColor, kelvin, TimeSpan.Zero); /// /// Sets color and temperature for a bulb and uses a transition time to the provided state /// /// - /// + /// /// /// /// - public Task SetColorAsync(LightBulb bulb, Color color, UInt16 kelvin, TimeSpan transitionDuration) - { + public Task SetColorAsync(LightBulb bulb, LifxColor lifxColor, ushort kelvin, TimeSpan transitionDuration) { if (bulb == null) throw new ArgumentNullException(nameof(bulb)); - var hsl = Utilities.RgbToHsl(color); + var hsl = Utilities.RgbToHsl(lifxColor); return SetColorAsync(bulb, hsl[0], hsl[1], hsl[2], kelvin, transitionDuration); } @@ -127,80 +129,57 @@ public Task SetColorAsync(LightBulb bulb, Color color, UInt16 kelvin, TimeSpan t /// /// public async Task SetColorAsync(LightBulb bulb, - UInt16 hue, - UInt16 saturation, - UInt16 brightness, - UInt16 kelvin, - TimeSpan transitionDuration) - { + ushort hue, + ushort saturation, + ushort brightness, + ushort kelvin, + TimeSpan transitionDuration) { if (bulb == null) throw new ArgumentNullException(nameof(bulb)); - if (transitionDuration.TotalMilliseconds > UInt32.MaxValue || - transitionDuration.Ticks < 0) + if (transitionDuration.TotalMilliseconds > uint.MaxValue || + transitionDuration.Ticks < 0) throw new ArgumentOutOfRangeException("transitionDuration"); - if (kelvin < 2500 || kelvin > 9000) - { + if (kelvin < 2500 || kelvin > 9000) { throw new ArgumentOutOfRangeException("kelvin", "Kelvin must be between 2500 and 9000"); } - System.Diagnostics.Debug.WriteLine("Setting color to {0}", bulb.HostName); - FrameHeader header = new FrameHeader() - { - Identifier = GetNextIdentifier(), - AcknowledgeRequired = true - }; - UInt32 duration = (UInt32)transitionDuration.TotalMilliseconds; - var durationBytes = BitConverter.GetBytes(duration); - var h = BitConverter.GetBytes(hue); - var s = BitConverter.GetBytes(saturation); - var b = BitConverter.GetBytes(brightness); - var k = BitConverter.GetBytes(kelvin); + Debug.WriteLine("Setting color to {0}", bulb.HostName); + FrameHeader header = new FrameHeader(GetNextIdentifier(), true); + var duration = (uint) transitionDuration.TotalMilliseconds; await BroadcastMessageAsync(bulb.HostName, header, - MessageType.LightSetColor, (byte)0x00, //reserved - hue, saturation, brightness, kelvin, //HSBK - duration + MessageType.LightSetColor, (byte) 0x00, //reserved + hue, saturation, brightness, kelvin, //HSBK + duration ); } - /* + public async Task SetBrightnessAsync(LightBulb bulb, - UInt16 brightness, - TimeSpan transitionDuration) - { + ushort brightness, + TimeSpan transitionDuration) { if (transitionDuration.TotalMilliseconds > UInt32.MaxValue || - transitionDuration.Ticks < 0) - throw new ArgumentOutOfRangeException("transitionDuration"); + transitionDuration.Ticks < 0) + throw new ArgumentOutOfRangeException(nameof(transitionDuration)); - FrameHeader header = new FrameHeader() - { - Identifier = (uint)randomizer.Next(), - AcknowledgeRequired = true - }; - UInt32 duration = (UInt32)transitionDuration.TotalMilliseconds; - var durationBytes = BitConverter.GetBytes(duration); - var b = BitConverter.GetBytes(brightness); + FrameHeader header = new FrameHeader(GetNextIdentifier(), true); + var duration = (uint) transitionDuration.TotalMilliseconds; await BroadcastMessageAsync(bulb.HostName, header, MessageType.SetLightBrightness, brightness, duration ); - }*/ - - /// - /// Gets the current state of the bulb - /// - /// - /// - public Task GetLightStateAsync(LightBulb bulb) - { + } + + /// + /// Gets the current state of the bulb + /// + /// + /// + public async Task GetLightStateAsync(LightBulb bulb) { if (bulb == null) throw new ArgumentNullException(nameof(bulb)); - FrameHeader header = new FrameHeader() - { - Identifier = GetNextIdentifier(), - AcknowledgeRequired = false - }; - return BroadcastMessageAsync( + FrameHeader header = new FrameHeader(GetNextIdentifier()); + return await BroadcastMessageAsync( bulb.HostName, header, MessageType.LightGet); } @@ -210,17 +189,12 @@ public Task GetLightStateAsync(LightBulb bulb) /// /// /// - public async Task GetInfraredAsync(LightBulb bulb) - { + public async Task GetInfraredAsync(LightBulb bulb) { if (bulb == null) throw new ArgumentNullException(nameof(bulb)); - FrameHeader header = new FrameHeader() - { - Identifier = GetNextIdentifier(), - AcknowledgeRequired = true - }; - return (await BroadcastMessageAsync( + FrameHeader header = new FrameHeader(GetNextIdentifier()); + return (await BroadcastMessageAsync( bulb.HostName, header, MessageType.InfraredGet).ConfigureAwait(false)).Brightness; } @@ -230,19 +204,14 @@ public async Task GetInfraredAsync(LightBulb bulb) /// /// /// - public async Task SetInfraredAsync(Device device, UInt16 brightness) - { + public async Task SetInfraredAsync(Device device, ushort brightness) { if (device == null) throw new ArgumentNullException(nameof(device)); - System.Diagnostics.Debug.WriteLine($"Sending SetInfrared({brightness}) to {device.HostName}"); - FrameHeader header = new FrameHeader() - { - Identifier = GetNextIdentifier(), - AcknowledgeRequired = true - }; - - _ = await BroadcastMessageAsync(device.HostName, header, - MessageType.InfraredSet, (UInt16)brightness).ConfigureAwait(false); + Debug.WriteLine($"Sending SetInfrared({brightness}) to {device.HostName}"); + FrameHeader header = new FrameHeader(GetNextIdentifier(), true); + + await BroadcastMessageAsync(device.HostName, header, + MessageType.InfraredSet, brightness).ConfigureAwait(false); } } -} +} \ No newline at end of file diff --git a/src/LifxNet/LifxClient.MultizoneOperations.cs b/src/LifxNet/LifxClient.MultizoneOperations.cs new file mode 100644 index 0000000..4e13cb1 --- /dev/null +++ b/src/LifxNet/LifxClient.MultizoneOperations.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace LifxNet { + public partial class LifxClient : IDisposable { + /// + /// This message is used for changing the color of either a single or multiple zones. + /// + /// Target device + /// Start index to target + /// End index to target + /// LifxColor to use + /// How long to fade + /// Whether the effect should be applied immediately or not. + /// + /// + /// + public async Task SetColorZonesAsync(Device device, int startIndex, int endIndex, LifxColor color, + TimeSpan transitionDuration, bool apply=false) { + if (device == null) + throw new ArgumentNullException(nameof(device)); + if (transitionDuration.TotalMilliseconds > uint.MaxValue || + transitionDuration.Ticks < 0) { + throw new ArgumentOutOfRangeException(nameof(transitionDuration)); + } + + if (startIndex > endIndex) throw new ArgumentOutOfRangeException(nameof(startIndex)); + var doApply = apply ? 0x01 : 0x00; + FrameHeader header = new FrameHeader(GetNextIdentifier(), true); + var duration = (uint) transitionDuration.TotalMilliseconds; + await BroadcastMessageAsync(device.HostName, header, + MessageType.SetColorZones, (byte) startIndex, (byte) endIndex, color, duration, doApply); + } + + /// + /// Set a zone of colors + /// + /// The device to set + /// Duration in ms + /// Start index of the zone. Should probably just be 0 for most cases. + /// An array of system.drawing.colors. For completeness, I should probably make an + /// overload for this that accepts HSB values, but that's kind of a pain. :P + /// Whether to apply the effect or immediately or not. defaults to false. + /// + /// Thrown if the device is null + /// Thrown if the duration is longer than the max + /// + public async Task SetExtendedColorZonesAsync(Device device, TimeSpan transitionDuration, uint index, + List colors, bool apply = false) { + if (device == null) + throw new ArgumentNullException(nameof(device)); + if (transitionDuration.TotalMilliseconds > uint.MaxValue || + transitionDuration.Ticks < 0) { + throw new ArgumentOutOfRangeException(nameof(transitionDuration)); + } + + FrameHeader header = new FrameHeader(GetNextIdentifier(), true); + var duration = (uint) transitionDuration.TotalMilliseconds; + var count = (byte) colors.Count; + var colorBytes = new List(); + foreach (var color in colors) { + colorBytes.AddRange(color.ToBytes()); + } + var doApply = apply ? 0x01 : 0x00; + + await BroadcastMessageAsync(device.HostName, header, + MessageType.SetExtendedColorZones, duration, doApply, index, count, colorBytes); + } + + /// + /// Try to get the color zones from our device. + /// + /// + /// + /// + public Task GetExtendedColorZonesAsync(Device device) { + if (device == null) + throw new ArgumentNullException(nameof(device)); + FrameHeader header = new FrameHeader(GetNextIdentifier()); + return BroadcastMessageAsync( + device.HostName, header, MessageType.GetExtendedColorZones); + } + + /// + /// Try to get the color zones from our device, non-extended. + /// + /// Target device + /// Start index of requested zones + /// End index of requested zones + /// Either a "StateZone" response for single-zone devices, or "StateMultiZone" response. + /// + public Task GetColorZonesAsync(Device device, int startIndex, int endIndex) { + if (device == null) + throw new ArgumentNullException(nameof(device)); + if (startIndex > endIndex) throw new ArgumentOutOfRangeException(nameof(startIndex)); + FrameHeader header = new FrameHeader(GetNextIdentifier()); + return BroadcastMessageAsync( + device.HostName, header, MessageType.GetColorZones, (byte) startIndex, (byte) endIndex); + } + + /// + /// Try to get the color zone from our device, non-extended. + /// + /// Target device + /// Selected index of the requested zone + /// Either a "StateZone" response for single-zone devices, or "StateMultiZone" response. + /// + public Task GetColorZoneAsync(Device device, int index) { + if (device == null) + throw new ArgumentNullException(nameof(device)); + FrameHeader header = new FrameHeader(GetNextIdentifier()); + return BroadcastMessageAsync( + device.HostName, header, MessageType.GetColorZones, (byte) index, (byte) index); + } + } +} \ No newline at end of file diff --git a/src/LifxNet/LifxClient.SwitchOperations.cs b/src/LifxNet/LifxClient.SwitchOperations.cs new file mode 100644 index 0000000..b8174e0 --- /dev/null +++ b/src/LifxNet/LifxClient.SwitchOperations.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading.Tasks; + +namespace LifxNet { + public partial class LifxClient { + /// + /// Get the power state of a relay on a switch device. + /// + /// + /// The relay on the switch starting from 0 + /// A StateRelayPower message. + /// + public async Task GetRelayPowerAsync(Device device, int relayIndex) { + if (device == null) + throw new ArgumentNullException(nameof(device)); + + FrameHeader header = new FrameHeader(GetNextIdentifier()); + return await BroadcastMessageAsync( + device.HostName, header, MessageType.GetRelayPower, (byte) relayIndex); + } + + /// + /// Set the power state of a relay on a switch device. + /// Current models of the LIFX switch do not have dimming capability, + /// so the two valid values are 0 for off and 65535 for on. + /// + /// + /// The relay on the switch starting from 0 + /// Whether to turn the device on or not. + /// A StateRelayPower message. + /// + public async Task SetRelayPowerAsync(Device device, int relayIndex, bool enable) { + if (device == null) + throw new ArgumentNullException(nameof(device)); + var level = enable ? 65535 : 0; + + FrameHeader header = new FrameHeader(GetNextIdentifier()); + return await BroadcastMessageAsync( + device.HostName, header, MessageType.SetRelayPower, (byte) relayIndex, (ushort) level); + } + } +} \ No newline at end of file diff --git a/src/LifxNet/LifxClient.TileOperations.cs b/src/LifxNet/LifxClient.TileOperations.cs new file mode 100644 index 0000000..e905b2f --- /dev/null +++ b/src/LifxNet/LifxClient.TileOperations.cs @@ -0,0 +1,91 @@ +using System; +using System.Threading.Tasks; + +namespace LifxNet { + public partial class LifxClient { + private const int Reserved = 0x00; + + /// + /// This message returns information about the tiles in the chain. + /// + /// + /// StateDeviceChainResponse + public async Task GetDeviceChainAsync(Device group) { + if (group == null) + throw new ArgumentNullException(nameof(group)); + + FrameHeader header = new FrameHeader(GetNextIdentifier()); + return await BroadcastMessageAsync( + group.HostName, header, MessageType.GetDeviceChain); + } + + /// + /// Used to tell each tile what their position is. + /// + /// + /// + /// + /// + /// + public async Task SetUserPositionAsync(Device group, int tileIndex, float userX, float userY) { + if (group == null) + throw new ArgumentNullException(nameof(group)); + + FrameHeader header = new FrameHeader(GetNextIdentifier()); + + await BroadcastMessageAsync(group.HostName, header, + MessageType.SetUserPosition, tileIndex, Reserved, userX, userY).ConfigureAwait(false); + } + + /// + /// Get the state of 64 pixels in the tile in a rectangle that has a starting point and width. + /// The tile_index is used to control the starting tile in the chain and length is used to get the state of + /// that many tiles beginning from the tile_index. This will result in a separate response from each tile. + /// + /// For the LIFX Tile it really only makes sense to set x and y to zero, and width to 8. + /// + /// + /// used to control the starting tile in the chain + /// used to get the state of that many tiles beginning from the tile_index. + /// Leave at 0 + /// Leave at 0 + /// Leave at 8 + /// StateTileState64Response + public async Task GetTileState64Async(Device device, int tileIndex, int length, + int x = 0, int y = 0, int width = 8) { + if (device == null) + throw new ArgumentNullException(nameof(device)); + + FrameHeader header = new FrameHeader(GetNextIdentifier()); + return await BroadcastMessageAsync( + device.HostName, header, MessageType.GetTileState64, tileIndex, length, Reserved, x, y, width); + } + + /// + /// Set the state of 64 pixels in the tile in a rectangle that has a starting point and width. + /// The tile_index is used to control the starting tile in the chain and length is used to get the state of + /// that many tiles beginning from the tile_index. This will result in a separate response from each tile. + /// + /// For the LIFX Tile it really only makes sense to set x and y to zero, and width to 8. + /// + /// + /// used to control the starting tile in the chain + /// used to get the state of that many tiles beginning from the tile_index. + /// + /// + /// Leave at 0 + /// Leave at 0 + /// Leave at 8 + /// StateTileState64Response + public async Task SetTileState64Async(Device device, int tileIndex, int length, + long duration, LifxColor[] colors, int x = 0, int y = 0, int width = 8) { + if (device == null) + throw new ArgumentNullException(nameof(device)); + + FrameHeader header = new FrameHeader(GetNextIdentifier()); + return await BroadcastMessageAsync( + device.HostName, header, MessageType.SetTileState64, tileIndex, length, Reserved, x, y, width, duration, + colors); + } + } +} \ No newline at end of file diff --git a/src/LifxNet/LifxClient.cs b/src/LifxNet/LifxClient.cs index 6ed088e..85963dc 100644 --- a/src/LifxNet/LifxClient.cs +++ b/src/LifxNet/LifxClient.cs @@ -1,300 +1,253 @@ using System; -using System.Collections.Generic; -using System.Linq; +using System.Diagnostics; using System.IO; -using System.Text; -using System.Threading.Tasks; -using System.Net.Sockets; +using System.Linq; using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; -namespace LifxNet -{ +namespace LifxNet { /// /// LIFX Client for communicating with bulbs /// - public partial class LifxClient : IDisposable - { - + public partial class LifxClient { private const int Port = 56700; - private UdpClient? _socket; - private bool _isRunning; + private readonly UdpClient _socket; + private bool _isRunning; - private LifxClient() - { + private LifxClient() { + IPEndPoint end = new IPEndPoint(IPAddress.Any, Port); + _socket = new UdpClient(end) {Client = {Blocking = false}, DontFragment = true}; + _socket.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); } /// /// Creates a new LIFX client. /// /// client - public static Task CreateAsync() - { + public static Task CreateAsync() { LifxClient client = new LifxClient(); client.Initialize(); - return Task.FromResult(client); + return Task.FromResult(client); } - private void Initialize() - { - IPEndPoint end = new IPEndPoint(IPAddress.Any, Port); - _socket = new UdpClient(end); - _socket.Client.Blocking = false; - _socket.DontFragment = true; - _socket.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); - _isRunning = true; - StartReceiveLoop(); + private void Initialize() { + _isRunning = true; + StartReceiveLoop(); } - private void StartReceiveLoop() - { - Task.Run(async () => - { - while (_isRunning && _socket != null) - try - { - var result = await _socket.ReceiveAsync(); - if (result.Buffer.Length > 0) - { - HandleIncomingMessages(result.Buffer, result.RemoteEndPoint); - } - } - catch { } - }); - } - - private void HandleIncomingMessages(byte[] data, System.Net.IPEndPoint endpoint) - { + + private void StartReceiveLoop() { + Task.Run(async () => { + while (_isRunning) + try { + var result = await _socket.ReceiveAsync(); + if (result.Buffer.Length > 0) { + HandleIncomingMessages(result.Buffer, result.RemoteEndPoint); + } + } catch { + // ignored + } + }); + } + + private void HandleIncomingMessages(byte[] data, IPEndPoint endpoint) { var remote = endpoint; var msg = ParseMessage(data); - if (msg.Type == MessageType.DeviceStateService) - { - ProcessDeviceDiscoveryMessage(remote.Address, remote.Port, msg); - } - else - { - if (taskCompletions.ContainsKey(msg.Source)) - { - var tcs = taskCompletions[msg.Source]; - tcs(msg); - } - else - { - //TODO - } + if (remote.Port == 56700) Debug.WriteLine("Incoming Message Type: " + msg.Type); + switch (msg.Type) { + case MessageType.DeviceStateService: + ProcessDeviceDiscoveryMessage(remote.Address, msg); + break; + default: + if (_taskCompletions.ContainsKey(msg.Source)) { + var tcs = _taskCompletions[msg.Source]; + tcs(msg); + } + + break; } - System.Diagnostics.Debug.WriteLine("Received from {0}:{1}", remote.ToString(), - string.Join(",", (from a in data select a.ToString("X2")).ToArray())); + if (remote.Port == 56700) + Debug.WriteLine("Received from {0}:{1}", remote, + string.Join(",", (from a in data select a.ToString("X2")).ToArray())); } /// /// Disposes the client /// - public void Dispose() - { - _isRunning = false; - _socket?.Dispose(); + public void Dispose() { + _isRunning = false; + _socket.Dispose(); } - private Task BroadcastMessageAsync(string? hostName, FrameHeader header, MessageType type, params object[] args) - where T : LifxResponse - - { - List payload = new List(); - if (args != null) - { - foreach (var arg in args) - { - if (arg is UInt16) - payload.AddRange(BitConverter.GetBytes((UInt16)arg)); - else if (arg is UInt32) - payload.AddRange(BitConverter.GetBytes((UInt32)arg)); - else if (arg is byte) - payload.Add((byte)arg); - else if (arg is byte[]) - payload.AddRange((byte[])arg); - else if (arg is string) - payload.AddRange(Encoding.UTF8.GetBytes(((string)arg).PadRight(32).Take(32).ToArray())); //All strings are 32 bytes - else - throw new NotSupportedException(args.GetType().FullName); - } - } - return BroadcastMessagePayloadAsync(hostName, header, type, payload.ToArray()); + private Task BroadcastMessageAsync(string hostName, FrameHeader header, MessageType type, + params object[] args) + where T : LifxResponse { + Debug.WriteLine("Broadcasting " + type + " to " + hostName); + var payload = new Payload(args); + + return BroadcastPayloadAsync(hostName, header, type, payload); } - private async Task BroadcastMessagePayloadAsync(string? hostName, FrameHeader header, MessageType type, byte[] payload) - where T : LifxResponse - { + + private async Task BroadcastPayloadAsync(string hostName, FrameHeader header, MessageType type, + Payload payload) + where T : LifxResponse { if (_socket == null) throw new InvalidOperationException("No valid socket"); -#if DEBUG - // MemoryStream ms = new MemoryStream(); - // await WritePacketToStreamAsync(ms.AsOutputStream(), header, (UInt16)type, payload).ConfigureAwait(false); - // var data = ms.ToArray(); - // System.Diagnostics.Debug.WriteLine( - // string.Join(",", (from a in data select a.ToString("X2")).ToArray())); -#endif - if (hostName == null) - { - hostName = "255.255.255.255"; - } + + MemoryStream ms = new MemoryStream(); + WritePacketToStream(ms, header, (UInt16) type, payload); + var data = ms.ToArray(); + Debug.WriteLine( + string.Join(",", (from a in data select a.ToString("X2")).ToArray())); + + TaskCompletionSource? tcs = null; - if (//header.AcknowledgeRequired && - header.Identifier > 0 && - typeof(T) != typeof(UnknownResponse)) - { + if ( //header.AcknowledgeRequired && + header.Identifier > 0 && + typeof(T) != typeof(UnknownResponse)) { tcs = new TaskCompletionSource(); - Action action = (r) => - { + Action action = (r) => { if (r.GetType() == typeof(T)) - tcs.TrySetResult((T)r); + tcs.TrySetResult((T) r); }; - taskCompletions[header.Identifier] = action; + _taskCompletions[header.Identifier] = action; + } + + using (MemoryStream stream = new MemoryStream()) { + WritePacketToStream(stream, header, (UInt16) type, payload); + var msg = stream.ToArray(); + await _socket.SendAsync(msg, msg.Length, hostName, Port); } - using (MemoryStream stream = new MemoryStream()) - { - WritePacketToStream(stream, header, (UInt16)type, payload); - var msg = stream.ToArray(); - await _socket.SendAsync(msg, msg.Length, hostName, Port); - } //{ // await WritePacketToStreamAsync(stream, header, (UInt16)type, payload).ConfigureAwait(false); //} T result = default(T); - if(tcs != null) - { - var _ = Task.Delay(1000).ContinueWith((t) => - { + if (tcs != null) { + var _ = Task.Delay(1000).ContinueWith((t) => { if (!t.IsCompleted) tcs.TrySetException(new TimeoutException()); }); try { result = await tcs.Task.ConfigureAwait(false); - } - finally - { - taskCompletions.Remove(header.Identifier); + } finally { + _taskCompletions.Remove(header.Identifier); } } + return result; } - private LifxResponse ParseMessage(byte[] packet) - { - using (MemoryStream ms = new MemoryStream(packet)) - { - var header = new FrameHeader(); - BinaryReader br = new BinaryReader(ms); - //frame - var size = br.ReadUInt16(); - if (packet.Length != size || size < 36) - throw new Exception("Invalid packet"); - var a = br.ReadUInt16(); //origin:2, reserved:1, addressable:1, protocol:12 - var source = br.ReadUInt32(); - //frame address - byte[] target = br.ReadBytes(8); - header.TargetMacAddress = target; - ms.Seek(6, SeekOrigin.Current); //skip reserved - var b = br.ReadByte(); //reserved:6, ack_required:1, res_required:1, - header.Sequence = br.ReadByte(); - //protocol header - var nanoseconds = br.ReadUInt64(); - header.AtTime = Utilities.Epoch.AddMilliseconds(nanoseconds * 0.000001); - var type = (MessageType)br.ReadUInt16(); - ms.Seek(2, SeekOrigin.Current); //skip reserved - return LifxResponse.Create(header, type, source, size > 36 ? br.ReadBytes(size - 36) : new byte[] { }); - } + private static LifxResponse ParseMessage(byte[] packet) { + using MemoryStream ms = new MemoryStream(packet); + var header = new FrameHeader(); + BinaryReader br = new BinaryReader(ms); + //frame + var size = br.ReadUInt16(); + if (packet.Length != size || size < 36) + throw new Exception("Invalid packet"); + br.ReadUInt16(); //origin:2, reserved:1, addressable:1, protocol:12 + var source = br.ReadUInt32(); + //frame address + byte[] target = br.ReadBytes(8); + header.TargetMacAddress = target; + ms.Seek(6, SeekOrigin.Current); //skip reserved + br.ReadByte(); //reserved:6, ack_required:1, res_required:1, + header.Sequence = br.ReadByte(); + //protocol header + var nanoseconds = br.ReadUInt64(); + header.AtTime = Utilities.Epoch.AddMilliseconds(nanoseconds * 0.000001); + var type = (MessageType) br.ReadUInt16(); + ms.Seek(2, SeekOrigin.Current); //skip reserved + return LifxResponse.Create(header, type, source, + new Payload(size > 36 ? br.ReadBytes(size - 36) : new byte[] { })); } - private void WritePacketToStream(Stream outStream, FrameHeader header, UInt16 type, byte[] payload) - { - using (var dw = new BinaryWriter(outStream) { /*ByteOrder = ByteOrder.LittleEndian*/ }) - { - //BinaryWriter bw = new BinaryWriter(ms); - #region Frame - //size uint16 - dw.Write((UInt16)((payload != null ? payload.Length : 0) + 36)); //length - // origin (2 bits, must be 0), reserved (1 bit, must be 0), addressable (1 bit, must be 1), protocol 12 bits must be 0x400) = 0x1400 - dw.Write((UInt16)0x3400); //protocol - dw.Write((UInt32)header.Identifier); //source identifier - unique value set by the client, used by responses. If 0, responses are broadcasted instead - #endregion Frame - - #region Frame address - //The target device address is 8 bytes long, when using the 6 byte MAC address then left - - //justify the value and zero-fill the last two bytes. A target device address of all zeroes effectively addresses all devices on the local network - dw.Write(header.TargetMacAddress); // target mac address - 0 means all devices - dw.Write(new byte[] { 0, 0, 0, 0, 0, 0 }); //reserved 1 - - //The client can use acknowledgements to determine that the LIFX device has received a message. - //However, when using acknowledgements to ensure reliability in an over-burdened lossy network ... - //causing additional network packets may make the problem worse. - //Client that don't need to track the updated state of a LIFX device can choose not to request a - //response, which will reduce the network burden and may provide some performance advantage. In - //some cases, a device may choose to send a state update response independent of whether res_required is set. - if (header.AcknowledgeRequired && header.ResponseRequired) - dw.Write((byte)0x03); - else if (header.AcknowledgeRequired) - dw.Write((byte)0x02); - else if (header.ResponseRequired) - dw.Write((byte)0x01); - else - dw.Write((byte)0x00); - //The sequence number allows the client to provide a unique value, which will be included by the LIFX - //device in any message that is sent in response to a message sent by the client. This allows the client - //to distinguish between different messages sent with the same source identifier in the Frame. See - //ack_required and res_required fields in the Frame Address. - dw.Write((byte)header.Sequence); - #endregion Frame address - - #region Protocol Header - //The at_time value should be zero for Set and Get messages sent by a client. - //For State messages sent by a device, the at_time will either be the device - //current time when the message was received or zero. StateColor is an example - //of a message that will return a non-zero at_time value - if (header.AtTime > DateTime.MinValue) - { - var time = header.AtTime.ToUniversalTime(); - dw.Write((UInt64)(time - new DateTime(1970, 01, 01)).TotalMilliseconds * 10); //timestamp - } - else - { - dw.Write((UInt64)0); - } - #endregion Protocol Header - dw.Write(type); //packet _type - dw.Write((UInt16)0); //reserved - if (payload != null) - dw.Write(payload); - dw.Flush(); + private static void WritePacketToStream(Stream outStream, FrameHeader header, ushort type, Payload payload) { + using var dw = new BinaryWriter(outStream); + + #region Frame + + //size uint16 + dw.Write((ushort) (payload.Length + 36)); //length + // origin (2 bits, must be 0), reserved (1 bit, must be 0), addressable (1 bit, must be 1), protocol 12 bits must be 0x400) = 0x1400 + dw.Write((ushort) 0x3400); //protocol + dw.Write(header + .Identifier); //source identifier - unique value set by the client, used by responses. If 0, responses are broadcast instead + + #endregion Frame + + #region Frame address + + //The target device address is 8 bytes long, when using the 6 byte MAC address then left - + //justify the value and zero-fill the last two bytes. A target device address of all zeroes effectively addresses all devices on the local network + dw.Write(header.TargetMacAddress); // target mac address - 0 means all devices + dw.Write(new byte[] {0, 0, 0, 0, 0, 0}); //reserved 1 + + //The client can use acknowledgements to determine that the LIFX device has received a message. + //However, when using acknowledgements to ensure reliability in an over-burdened lossy network ... + //causing additional network packets may make the problem worse. + //Client that don't need to track the updated state of a LIFX device can choose not to request a + //response, which will reduce the network burden and may provide some performance advantage. In + //some cases, a device may choose to send a state update response independent of whether res_required is set. + if (header.AcknowledgeRequired && header.ResponseRequired) + dw.Write((byte) 0x03); + else if (header.AcknowledgeRequired) + dw.Write((byte) 0x02); + else if (header.ResponseRequired) + dw.Write((byte) 0x01); + else + dw.Write((byte) 0x00); + //The sequence number allows the client to provide a unique value, which will be included by the LIFX + //device in any message that is sent in response to a message sent by the client. This allows the client + //to distinguish between different messages sent with the same source identifier in the Frame. See + //ack_required and res_required fields in the Frame Address. + dw.Write(header.Sequence); + + #endregion Frame address + + #region Protocol Header + + //The at_time value should be zero for Set and Get messages sent by a client. + //For State messages sent by a device, the at_time will either be the device + //current time when the message was received or zero. StateColor is an example + //of a message that will return a non-zero at_time value + if (header.AtTime > DateTime.MinValue) { + var time = header.AtTime.ToUniversalTime(); + dw.Write((ulong) (time - new DateTime(1970, 01, 01)).TotalMilliseconds * 10); //timestamp + } else { + dw.Write((ulong) 0); } + + #endregion Protocol Header + + dw.Write(type); //packet _type + dw.Write((ushort) 0); //reserved + dw.Write(payload.ToArray()); + dw.Flush(); } } - internal class FrameHeader - { - public UInt32 Identifier; + internal class FrameHeader { + public uint Identifier; public byte Sequence; public bool AcknowledgeRequired; public bool ResponseRequired; - public byte[] TargetMacAddress; - public DateTime AtTime; - public FrameHeader() - { - Identifier = 0; - Sequence = 0; - AcknowledgeRequired = false; - ResponseRequired = false; - TargetMacAddress = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 }; - AtTime = DateTime.MinValue; + public byte[] TargetMacAddress = {0, 0, 0, 0, 0, 0, 0, 0}; + public DateTime AtTime = DateTime.MinValue; + + public FrameHeader() { } - public string TargetMacAddressName - { - get - { - if (TargetMacAddress == null) return string.Empty; - return string.Join(":", TargetMacAddress.Take(6).Select(tb => tb.ToString("X2")).ToArray()); - } - } - } - -} + + public FrameHeader(uint id, bool acknowledgeRequired = false) { + Identifier = id; + AcknowledgeRequired = acknowledgeRequired; + } + + public string TargetMacAddressName { + get { return string.Join(":", TargetMacAddress.Take(6).Select(tb => tb.ToString("X2")).ToArray()); } + } + } +} \ No newline at end of file diff --git a/src/LifxNet/LifxColor.cs b/src/LifxNet/LifxColor.cs new file mode 100644 index 0000000..3783f3a --- /dev/null +++ b/src/LifxNet/LifxColor.cs @@ -0,0 +1,251 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Globalization; + +namespace LifxNet { + /// + /// Extend the normal System.Drawing.Color class and make it work with HSBK + /// + public class LifxColor { + private static double Tolerance + => 0.000000000000001; + + private Color _color; + + + /// + /// Red + /// + public byte R { + get => _color.R; + set => _color = Color.FromArgb(_color.A, value, _color.G, _color.B); + } + + /// + /// Green + /// + public byte G { + get => _color.G; + set => _color = Color.FromArgb(_color.A, _color.R, value, _color.B); + } + + /// + /// Blue + /// + public byte B { + get => _color.B; + set => _color = Color.FromArgb(_color.A, _color.R, _color.G, value); + } + + /// + /// The hue, in degrees, of this Color. The hue is measured in degrees, ranging from 0.0 through 360.0, in HSL color space. + /// + public float Hue { + get => _color.GetHue(); + set => _color = HsbToRgb(value, _color.GetSaturation(), _color.GetBrightness()); + } + + /// + /// The hue, in the standard Lifx format, of this Color. The lifx hue is measured from 0 - 65565. + /// + public int LifxHue => (int) _color.GetHue() / 360 * 65536; + + /// + /// The saturation of this Color. The saturation ranges from 0.0 through 1.0, where 0.0 is grayscale and 1.0 is the most saturated. + /// + public float Saturation { + get => _color.GetSaturation(); + set => _color = HsbToRgb(_color.GetHue(), value, _color.GetBrightness()); + } + + /// + /// The saturation of this color in Lifx format. Range is 0 - 65535; + /// + public int LifxSaturation => (int) _color.GetSaturation() * 65535; + + /// + /// The lightness of this Color. The lightness ranges from 0.0 through 1.0, where 0.0 represents black and 1.0 represents white. + /// + public float Brightness { + get => _color.GetBrightness(); + set => _color = HsbToRgb(_color.GetHue(), _color.GetSaturation(), value); + } + + /// + /// The brightness of this color. The brightness range is 0 - 65535 + /// + public int LifxBrightness => (int) _color.GetBrightness() * 65535; + + /// + /// The temperature of this Color. The temperature ranges from 2700-9000 + /// + public float K { + get; + set; + } + + /// + /// Retrieve the base System.Drawing.Color of this Color + /// + public Color Color => _color; + + /// + /// Create a new LifxColor + /// + public LifxColor() { + _color = Color.FromArgb(255, 0, 0, 0); + K = 2700; + } + + /// + /// Create a color from HSBK values + /// + /// Hue: range 0 to 65535. + /// Saturation: range 0 to 65535. + /// Brightness: range 0 to 65535. + /// Kelvin: range 2500° (warm) to 9000° (cool). Default is 2700. + public LifxColor(ushort h, ushort s, ushort b, ushort k = 2700) { + var hue = h / 65535 * 360; + var sat = s / 65535f; + var bri = b / 65535f; + _color = HsbToRgb(hue, sat, bri); + K = k; + } + + /// + /// Create a color from RGB Value, with default alpha of 255 + /// + /// Red: Range 0 to 255 + /// Green: Range 0 to 255 + /// Blue: Range 0 to 255 + public LifxColor(int r, int g, int b) { + _color = Color.FromArgb(255, r, g, b); + K = 2700; + } + + /// + /// Create a LifxColor from a System.Drawing.Color + /// + /// Base System.Drawing.Color + public LifxColor(Color color) { + _color = color; + K = 2700; + } + + /// + /// Serialize our color to a byte array + /// + /// HSBK formatted array of bytes. + public byte[] ToBytes() { + var output = new List(); + var hue = Hue / 360 * 65535; + var sat = Saturation * 65535; + var bri = Brightness * 65535; + foreach (var u in new[] {(ushort) hue, (ushort) sat, (ushort) bri, (ushort) K}) { + output.AddRange(BitConverter.GetBytes(u)); + } + + return output.ToArray(); + } + + + /// + /// Return System.Drawing.Color RGB string representation of the color + /// + /// + public string ToRgbString() { + return R + ", " + G + ", " + B; + } + + /// + /// Return Lifx HSBK string representation of the color + /// + /// + public string ToHsbkString() { + var hue = Hue / 360 * 65535; + var sat = 65535 * Saturation; + var bri = 65536 * Brightness; + return hue + ", " + sat + ", " + bri + ", " + K; + } + + + /// + /// Converts HSB to RGB, with a specified output Alpha. + /// Arguments are limited to the defined range: + /// does not raise exceptions. + /// + /// Hue, must be in [0, 360]. + /// Saturation, must be in [0, 1]. + /// Brightness, must be in [0, 1]. + /// Output Alpha, must be in [0, 255]. + private static Color HsbToRgb(double h, double s, double b, int a = 255) { + h = Math.Max(0D, Math.Min(360D, h)); + s = Math.Max(0D, Math.Min(1D, s)); + b = Math.Max(0D, Math.Min(1D, b)); + a = Math.Max(0, Math.Min(255, a)); + + var r = 0D; + var g = 0D; + var bl = 0D; + + if (Math.Abs(s) < 0.000000000000001) { + r = g = bl = b; + } else { + // the argb wheel consists of 6 sectors. Figure out which sector + // you're in. + var sectorPos = h / 60D; + var sectorNumber = (int)Math.Floor(sectorPos); + // get the fractional part of the sector + var fractionalSector = sectorPos - sectorNumber; + + // calculate values for the three axes of the argb. + var p = b * (1D - s); + var q = b * (1D - s * fractionalSector); + var t = b * (1D - s * (1D - fractionalSector)); + + // assign the fractional colors to r, g, and b based on the sector + // the angle is in. + switch (sectorNumber) { + case 0 : + r = b; + g = t; + bl = p; + break; + case 1 : + r = q; + g = b; + bl = p; + break; + case 2 : + r = p; + g = b; + bl = t; + break; + case 3 : + r = p; + g = q; + bl = b; + break; + case 4 : + r = t; + g = p; + bl = b; + break; + case 5 : + r = b; + g = p; + bl = q; + break; + } + } + + return Color.FromArgb( + a, + Math.Max(0, Math.Min(255, Convert.ToInt32(double.Parse($"{r * 255D:0.00}")))), + Math.Max(0, Math.Min(255, Convert.ToInt32(double.Parse($"{g * 255D:0.00}")))), + Math.Max(0, Math.Min(255, Convert.ToInt32(double.Parse($"{bl * 250D:0.00}"))))); + } + + } +} \ No newline at end of file diff --git a/src/LifxNet/LifxNet.csproj b/src/LifxNet/LifxNet.csproj index 1da0140..6745107 100644 --- a/src/LifxNet/LifxNet.csproj +++ b/src/LifxNet/LifxNet.csproj @@ -1,28 +1,31 @@  - netstandard1.3 + netstandard2.0 LifxNet LifxNet true Morten Nielsen 2.2.0 - Morten Nielsen + Digitalhigh, Morten Nielsen Library for controlling LIFX light bulbs - © Morten Nielsen 2015-2020 + © Morten Nielsen 2015-2021 iot, lifx home automation uwp winphone winrt MIT - https://github.com/dotMorten/LifxNet - https://github.com/dotMorten/LifxNet + https://github.com/d8ahazard/LifxNet + https://github.com/d8ahazard/LifxNet true $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb git 8.0 enable + 2.2.5 + Add initial support for Multizone + \ No newline at end of file diff --git a/src/LifxNet/LifxPacket.cs b/src/LifxNet/LifxPacket.cs index 176fe9d..1d5f302 100644 --- a/src/LifxNet/LifxPacket.cs +++ b/src/LifxNet/LifxPacket.cs @@ -1,53 +1,51 @@ using System; -using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -namespace LifxNet -{ - internal abstract class LifxPacket - { +namespace LifxNet { + internal abstract class LifxPacket { private byte[] _payload; private ushort _type; - protected LifxPacket(ushort type, byte[] payload) - { + + protected LifxPacket(ushort type, byte[] payload) { _type = type; _payload = payload; } - internal byte[] Payload { get { return _payload; } } - internal ushort Type { get { return _type; } } - protected LifxPacket(ushort type, object[] data) - { + internal byte[] Payload { + get { return _payload; } + } + + internal ushort Type { + get { return _type; } + } + + protected LifxPacket(ushort type, object[] data) { _type = type; - using (var ms = new MemoryStream()) - { - StreamWriter bw = new StreamWriter(ms); - foreach (var obj in data) - { - if (obj is byte) - { - bw.Write((byte)obj); - } - else if (obj is byte[]) - { - bw.Write((byte[])obj); - } - else if (obj is UInt16) - bw.Write((UInt16)obj); - else if (obj is UInt32) - bw.Write((UInt32)obj); - else + using var ms = new MemoryStream(); + StreamWriter bw = new StreamWriter(ms); + foreach (var obj in data) { + switch (obj) { + case byte b: + bw.Write(b); + break; + case byte[] bytes: + bw.Write(bytes); + break; + case ushort @ushort: + bw.Write(@ushort); + break; + case uint u: + bw.Write(u); + break; + default: throw new NotImplementedException(); } - _payload = ms.ToArray(); } + + _payload = ms.ToArray(); } - public static LifxPacket FromByteArray(byte[] data) - { + public static LifxPacket FromByteArray(byte[] data) { // preambleFields = [ // { name: "size" , type:type.uint16_le }, // { name: "protocol" , type:type.uint16_le }, @@ -73,26 +71,25 @@ public static LifxPacket FromByteArray(byte[] data) ulong timestamp = br.ReadUInt64(); ushort packetType = br.ReadUInt16(); // ReverseBytes(br.ReadUInt16()); byte[] reserved4 = br.ReadBytes(2); - byte[] payload = new byte[] { }; - if (len > 0) - { + byte[] payload = { }; + if (len > 0) { payload = br.ReadBytes(len); } - LifxPacket packet = new UnknownPacket(packetType, payload, bulbAddress, site) - { + + LifxPacket packet = new UnknownPacket(packetType, payload, bulbAddress, site) { TimeStamp = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(timestamp), }; //packet.Identifier = identifier; return packet; } - private class UnknownPacket : LifxPacket - { - public UnknownPacket(ushort packetType, byte[] payload, byte[] bulbAddress, byte[] site) : base(packetType, payload) - { + private class UnknownPacket : LifxPacket { + public UnknownPacket(ushort packetType, byte[] payload, byte[] bulbAddress, byte[] site) : base(packetType, + payload) { BulbAddress = bulbAddress; Site = site; } + public byte[] BulbAddress { get; } public DateTime TimeStamp { get; set; } public byte[] Site { get; set; } diff --git a/src/LifxNet/LifxResponses.cs b/src/LifxNet/LifxResponses.cs index 5357f78..0eb7258 100644 --- a/src/LifxNet/LifxResponses.cs +++ b/src/LifxNet/LifxResponses.cs @@ -1,43 +1,42 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Diagnostics; -namespace LifxNet -{ +namespace LifxNet { /// /// Base class for LIFX response types /// - public abstract class LifxResponse - { - internal static LifxResponse Create(FrameHeader header, MessageType type, UInt32 source, byte[] payload) - { - switch (type) - { - case MessageType.DeviceAcknowledgement: - return new AcknowledgementResponse(header, type, payload, source); - case MessageType.DeviceStateLabel: - return new StateLabelResponse(header, type, payload, source); - case MessageType.LightState: - return new LightStateResponse(header, type, payload, source); - case MessageType.LightStatePower: - return new LightPowerResponse(header, type, payload, source); - case MessageType.InfraredState: - return new InfraredStateRespone(header, type, payload, source); - case MessageType.DeviceStateVersion: - return new StateVersionResponse(header, type, payload, source); - case MessageType.DeviceStateHostFirmware: - return new StateHostFirmwareResponse(header, type, payload, source); - case MessageType.DeviceStateService: - return new StateServiceResponse(header, type, payload, source); - default: - return new UnknownResponse(header, type, payload, source); - } + public abstract class LifxResponse { + internal static LifxResponse Create(FrameHeader header, MessageType type, uint source, Payload payload) { + return type switch { + MessageType.DeviceAcknowledgement => new AcknowledgementResponse(header, type, payload, source), + MessageType.DeviceStateLabel => new StateLabelResponse(header, type, payload, source), + MessageType.LightState => new LightStateResponse(header, type, payload, source), + MessageType.LightStatePower => new LightPowerResponse(header, type, payload, source), + MessageType.InfraredState => new InfraredStateResponse(header, type, payload, source), + MessageType.DeviceStateVersion => new StateVersionResponse(header, type, payload, source), + MessageType.DeviceStateHostFirmware => new StateHostFirmwareResponse(header, type, payload, source), + MessageType.DeviceStateService => new StateServiceResponse(header, type, payload, source), + MessageType.StateExtendedColorZones => new StateExtendedColorZonesResponse(header, type, payload, + source), + MessageType.StateZone => new StateZoneResponse(header, type, payload, source), + MessageType.StateMultiZone => new StateMultiZoneResponse(header, type, payload, source), + MessageType.StateDeviceChain => new StateDeviceChainResponse(header, type, payload, source), + MessageType.StateTileState64 => new StateTileState64Response(header, type, payload, source), + MessageType.StateRelayPower => new StateRelayPowerResponse(header, type, payload, source), + MessageType.DeviceStateHostInfo => new StateHostInfoResponse(header, type, payload, source), + MessageType.DeviceStateWifiInfo => new StateWifiInfoResponse(header, type, payload, source), + MessageType.DeviceStateWifiFirmware => new StateWifiFirmwareResponse(header, type, payload, source), + MessageType.DeviceStatePower => new StatePowerResponse(header, type, payload, source), + MessageType.DeviceStateInfo => new StateInfoResponse(header, type, payload, source), + MessageType.DeviceStateLocation => new StateLocationResponse(header, type, payload, source), + MessageType.DeviceStateGroup => new StateGroupResponse(header, type, payload, source), + MessageType.DeviceEchoResponse => new EchoResponse(header, type, payload, source), + _ => new UnknownResponse(header, type, payload, source) + }; } - internal LifxResponse(FrameHeader header, MessageType type, byte[] payload, UInt32 source) - { + internal LifxResponse(FrameHeader header, MessageType type, Payload payload, uint source) { Header = header; Type = type; Payload = payload; @@ -45,149 +44,523 @@ internal LifxResponse(FrameHeader header, MessageType type, byte[] payload, UInt } internal FrameHeader Header { get; } - internal byte[] Payload { get; } + internal Payload Payload { get; } internal MessageType Type { get; } - internal UInt32 Source { get; } + internal uint Source { get; } } + /// /// Response to any message sent with ack_required set to 1. /// - internal class AcknowledgementResponse: LifxResponse - { - internal AcknowledgementResponse(FrameHeader header, MessageType type, byte[] payload, UInt32 source) : base(header, type, payload, source) { } + internal class AcknowledgementResponse : LifxResponse { + internal AcknowledgementResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base( + header, type, payload, source) { + } + } + + /// + /// The StateZone message represents the state of a single zone with the index field indicating which zone is represented. The count field contains the count of the total number of zones available on the device. + /// + public class StateZoneResponse : LifxResponse { + internal StateZoneResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(header, + type, payload, source) { + Count = payload.GetUInt16(); + Index = payload.GetUInt16(); + Color = new Payload().GetColor(); + } + + /// + /// Count - total number of zones on the device + /// + public ushort Count { get; } + + /// + /// Index - Zone the message starts from + /// + public ushort Index { get; } + + /// + /// The list of colors returned by the message + /// + public LifxColor Color { get; } + } + + + /// + /// Response to GetHostInfo message. + /// Provides host MCU information. + /// + public class StateHostInfoResponse : LifxResponse { + internal StateHostInfoResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(header, + type, payload, source) { + Signal = payload.GetFloat32(); + Tx = payload.GetUInt32(); + Rx = payload.GetUInt32(); + } + + /// + /// Bytes received since power on + /// + public uint Rx { get; set; } + + /// + /// Bytes transmitted since power on + /// + public uint Tx { get; set; } + + /// + /// Radio receive signal strength in milliWatts + /// + public float Signal { get; set; } + } + + + /// + /// Response to GetWifiInfo message. + /// Provides host Wifi information. + /// + public class StateWifiInfoResponse : LifxResponse { + internal StateWifiInfoResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(header, + type, payload, source) { + Signal = payload.GetFloat32(); + Tx = payload.GetUInt32(); + Rx = payload.GetUInt32(); + } + + /// + /// Bytes received since power on + /// + public uint Rx { get; set; } + + /// + /// Bytes transmitted since power on + /// + public uint Tx { get; set; } + + /// + /// Radio receive signal strength in milliWatts + /// + public float Signal { get; set; } + } + + /// + /// Response to GetWifiFirmware message. + /// Provides Wifi subsystem information. + /// + public class StateWifiFirmwareResponse : LifxResponse { + internal StateWifiFirmwareResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(header, + type, payload, source) { + Build = payload.GetUInt64(); + // Skip 64-bit reserved + payload.Advance(8); + VersionMinor = payload.GetUInt16(); + VersionMajor = payload.GetUInt16(); + } + + /// + /// Firmware build time (epoch time) + /// + public ulong Build { get; set; } + /// + /// Minor firmware version number + /// + public ushort VersionMinor { get; set; } + /// + /// Major firmware version number + /// + public ushort VersionMajor { get; set; } + + + } + + + /// + /// Provides device power level. + /// + public class StatePowerResponse : LifxResponse { + internal StatePowerResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(header, + type, payload, source) { + Level = payload.GetUInt16(); + } + + /// + /// Zero implies standby and non-zero sets a corresponding power draw level. Currently only 0 and 65535 are supported. + /// + public ulong Level { get; set; } + + } + + + /// + /// Provides run-time information of device. + /// + public class StateInfoResponse : LifxResponse { + internal StateInfoResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(header, + type, payload, source) { + Time = DateTimeOffset.FromUnixTimeSeconds(payload.GetInt64()).DateTime; + Uptime = payload.GetInt64(); + Downtime = payload.GetInt64(); + } + + /// + /// Current time + /// + public DateTime Time { get; set; } + + /// + /// Time since last power on (relative time in nanoseconds) + /// + public long Uptime { get; set; } + + /// + /// Last power off period, 5 second accuracy (in nanoseconds) + /// + public long Downtime { get; set; } + } + + + /// + /// Device location. + /// + public class StateLocationResponse : LifxResponse { + internal StateLocationResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(header, + type, payload, source) { + Location = payload.GetBytes(16); + Label = payload.GetString(32); + Updated = payload.GetUInt64(); + } + + public byte[] Location { get; set; } + + public string Label { get; set; } + + public ulong Updated { get; set; } + } + + /// + /// Device group. + /// + public class StateGroupResponse : LifxResponse { + internal StateGroupResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(header, + type, payload, source) { + Group = payload.GetBytes(16); + Label = payload.GetString(32); + Updated = payload.GetUInt64(); + } + + public byte[] Group { get; set; } + + public string Label { get; set; } + + public ulong Updated { get; set; } + } + + /// + /// Echo response with payload sent in the EchoRequest. + /// + public class EchoResponse : LifxResponse { + internal EchoResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(header, + type, payload, source) { + RequestPayload = payload.ToArray(); + } + + /// + /// Payload sent in the EchoRequest. + /// + public byte[] RequestPayload { get; set; } + } + + + /// + /// The StateZone message represents the state of a single zone with the index field indicating which zone is represented. The count field contains the count of the total number of zones available on the device. + /// + public class StateDeviceChainResponse : LifxResponse { + internal StateDeviceChainResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base( + header, + type, payload, source) { + Tiles = new List(); + StartIndex = payload.GetUint8(); + for (var i = 0; i < 16; i++) { + var tile = new Tile(); + tile.LoadBytes(payload); + Tiles.Add(tile); + } + TotalCount = payload.GetUint8(); + } + + /// + /// Count - total number of zones on the device + /// + public int TotalCount { get; } + + /// + /// Start Index - Zone the message starts from + /// + public byte StartIndex { get; } + + /// + /// The list of colors returned by the message + /// + public List Tiles { get; } + } + + /// + /// Get the list of colors currently being displayed by zones + /// + public class StateMultiZoneResponse : LifxResponse { + internal StateMultiZoneResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base( + header, type, payload, source) { + Colors = new LifxColor[8]; + Count = payload.GetUint8(); + Index = payload.GetUint8(); + for (var i = 0; i < 8; i++) { + Debug.WriteLine($"Reading color {i}."); + Colors[i] = payload.GetColor(); + } + Debug.WriteLine("Colors read."); + } + + /// + /// Count - total number of zones on the device + /// + public ushort Count { get; } + + /// + /// Index - Zone the message starts from + /// + public ushort Index { get; } + + /// + /// The list of colors returned by the message + /// + public LifxColor[] Colors { get; } + } + + + /// + /// Get the list of colors currently being displayed by zones + /// + public class StateExtendedColorZonesResponse : LifxResponse { + internal StateExtendedColorZonesResponse(FrameHeader header, MessageType type, Payload payload, uint source) : + base(header, type, payload, source) { + Colors = new List(); + Count = payload.GetUInt16(); + Index = payload.GetUInt16(); + while (payload.HasContent()) { + Colors.Add(payload.GetColor()); + } + } + + /// + /// Count - total number of zones on the device + /// + public ushort Count { get; private set; } + + /// + /// Index - Zone the message starts from + /// + public ushort Index { get; private set; } + + /// + /// The list of colors returned by the message + /// + public List Colors { get; private set; } } + /// /// Response to GetService message. /// Provides the device Service and port. /// If the Service is temporarily unavailable, then the port value will be 0. /// - internal class StateServiceResponse : LifxResponse - { - internal StateServiceResponse(FrameHeader header, MessageType type, byte[] payload, UInt32 source) : base(header, type, payload, source) - { - Service = payload[0]; - Port = BitConverter.ToUInt32(payload, 1); + internal class StateServiceResponse : LifxResponse { + internal StateServiceResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base( + header, type, payload, source) { + Service = payload.GetUint8(); + Port = payload.GetUInt32(); } - public Byte Service { get; } - public UInt32 Port { get; } + + private byte Service { get; } + private uint Port { get; } } + + /// + /// Response to any message sent with ack_required set to 1. + /// + public class StateTileState64Response : LifxResponse { + internal StateTileState64Response(FrameHeader header, MessageType type, Payload payload, uint source) : base( + header, type, payload, source) { + TileIndex = payload.GetUint8(); + // Skip one byte for reserved + payload.Advance(); + X = payload.GetUint8(); + Y = payload.GetUint8(); + Width = payload.GetUint8(); + Colors = new LifxColor[64]; + for (var i = 0; i < Colors.Length; i++) { + if (payload.HasContent()) { + Colors[i] = payload.GetColor(); + } else { + Debug.WriteLine($"Content size mismatch fetching colors: {i}/64: "); + } + } + } + + public uint TileIndex { get; } + public uint X { get; } + public uint Y { get; } + public uint Width { get; } + public LifxColor[] Colors { get; } + } + /// /// Response to GetLabel message. Provides device label. /// - internal class StateLabelResponse : LifxResponse - { - internal StateLabelResponse(FrameHeader header, MessageType type, byte[] payload, UInt32 source) : base(header, type, payload, source) - { - if (payload != null) - Label = Encoding.UTF8.GetString(payload, 0, payload.Length).Replace("\0", ""); + internal class StateLabelResponse : LifxResponse { + internal StateLabelResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(header, + type, payload, source) { + Label = payload.GetString().Replace("\0", ""); } - public string? Label { get; private set; } + + public string? Label { get; } } + /// /// Sent by a device to provide the current light state /// - public class LightStateResponse : LifxResponse - { - internal LightStateResponse(FrameHeader header, MessageType type, byte[] payload, UInt32 source) : base(header, type, payload, source) - { - Hue = BitConverter.ToUInt16(payload, 0); - Saturation = BitConverter.ToUInt16(payload, 2); - Brightness = BitConverter.ToUInt16(payload, 4); - Kelvin = BitConverter.ToUInt16(payload, 6); - IsOn = BitConverter.ToUInt16(payload, 10) > 0; - Label = Encoding.UTF8.GetString(payload, 12, 32).Replace("\0",""); + public class LightStateResponse : LifxResponse { + internal LightStateResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(header, + type, payload, source) { + Hue = payload.GetUInt16(); + Saturation = payload.GetUInt16(); + Brightness = payload.GetUInt16(); + Kelvin = payload.GetUInt16(); + IsOn = payload.GetUInt16() > 0; + Label = payload.GetString(32).Replace("\\0", ""); } + /// /// Hue /// - public UInt16 Hue { get; private set; } + public ushort Hue { get; } + /// /// Saturation (0=desaturated, 65535 = fully saturated) /// - public UInt16 Saturation { get; private set; } + public ushort Saturation { get; } + /// /// Brightness (0=off, 65535=full brightness) /// - public UInt16 Brightness { get; private set; } + public ushort Brightness { get; } + /// /// Bulb color temperature /// - public UInt16 Kelvin { get; private set; } + public ushort Kelvin { get; } + /// /// Power state /// - public bool IsOn { get; private set; } + public bool IsOn { get; } + /// /// Light label /// - public string Label { get; private set; } + public string Label { get; } } - internal class LightPowerResponse : LifxResponse - { - internal LightPowerResponse(FrameHeader header, MessageType type, byte[] payload, UInt32 source) : base(header, type, payload, source) - { - IsOn = BitConverter.ToUInt16(payload, 0) > 0; + + internal class LightPowerResponse : LifxResponse { + internal LightPowerResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(header, + type, payload, source) { + IsOn = payload.GetUInt16() > 0; } - public bool IsOn { get; private set; } + + public bool IsOn { get; } } - internal class InfraredStateRespone : LifxResponse - { - internal InfraredStateRespone(FrameHeader header, MessageType type, byte[] payload, UInt32 source) : base(header, type, payload, source) - { - Brightness = BitConverter.ToUInt16(payload, 0); + internal class InfraredStateResponse : LifxResponse { + internal InfraredStateResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base( + header, type, payload, source) { + Brightness = payload.GetUInt16(); } - public UInt16 Brightness { get; private set; } + + public ushort Brightness { get; } } /// /// Response to GetVersion message. Provides the hardware version of the device. /// - public class StateVersionResponse : LifxResponse - { - internal StateVersionResponse(FrameHeader header, MessageType type, byte[] payload, UInt32 source) : base(header, type, payload, source) - { - Vendor = BitConverter.ToUInt32(payload, 0); - Product = BitConverter.ToUInt32(payload, 4); - Version = BitConverter.ToUInt32(payload, 8); + public class StateVersionResponse : LifxResponse { + internal StateVersionResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base( + header, type, payload, source) { + Vendor = Payload.GetUInt32(); + Product = Payload.GetUInt32(); + Version = Payload.GetUInt32(); } + /// /// Vendor ID /// - public UInt32 Vendor { get; private set; } + public uint Vendor { get; } + /// /// Product ID /// - public UInt32 Product { get; private set; } + public uint Product { get; } + /// /// Hardware version /// - public UInt32 Version { get; private set; } + public uint Version { get; } } + /// /// Response to GetHostFirmware message. Provides host firmware information. /// - public class StateHostFirmwareResponse : LifxResponse - { - internal StateHostFirmwareResponse(FrameHeader header, MessageType type, byte[] payload, UInt32 source) : base(header, type, payload, source) - { - var nanoseconds = BitConverter.ToUInt64(payload, 0); + public class StateHostFirmwareResponse : LifxResponse { + internal StateHostFirmwareResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base( + header, type, payload, source) { + var nanoseconds = payload.GetUInt64(); Build = Utilities.Epoch.AddMilliseconds(nanoseconds * 0.000001); //8..15 UInt64 is reserved - Version = BitConverter.ToUInt32(payload, 16); + Version = payload.GetUInt32(); } + /// /// Firmware build time /// - public DateTime Build { get; private set; } + public DateTime Build { get; } + /// /// Firmware version /// - public UInt32 Version { get; private set; } + public uint Version { get; } } - internal class UnknownResponse : LifxResponse - { - internal UnknownResponse(FrameHeader header, MessageType type, byte[] payload, UInt32 source) : base(header, type, payload, source) { } + /// + /// Response to GetVersion message. Provides the hardware version of the device. + /// + public class StateRelayPowerResponse : LifxResponse { + internal StateRelayPowerResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base( + header, type, payload, source) { + RelayIndex = payload.GetUint8(); + Level = payload.GetUInt16(); + } + + /// + /// The relay on the switch starting from 0 + /// + public int RelayIndex { get; } + + /// + /// The value of the relay + /// + public int Level { get; } + } + + internal class UnknownResponse : LifxResponse { + internal UnknownResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(header, + type, payload, source) { + } } -} +} \ No newline at end of file diff --git a/src/LifxNet/LightBulb.cs b/src/LifxNet/LightBulb.cs new file mode 100644 index 0000000..a1e2e09 --- /dev/null +++ b/src/LifxNet/LightBulb.cs @@ -0,0 +1,19 @@ +namespace LifxNet { + /// + /// LIFX light bulb + /// + public sealed class LightBulb : Device { + /// + /// Initializes a new instance of a bulb instead of relying on discovery. At least the host name must be provide for the device to be usable. + /// + /// Required + /// + /// + /// + /// + public LightBulb(string hostname, byte[] macAddress, byte service = 0, uint port = 0, uint productId = 0) : + base(hostname, + macAddress, service, port) { + } + } +} \ No newline at end of file diff --git a/src/LifxNet/MessageType.cs b/src/LifxNet/MessageType.cs index fb34d9b..eab1a66 100644 --- a/src/LifxNet/MessageType.cs +++ b/src/LifxNet/MessageType.cs @@ -1,19 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace LifxNet -{ - internal enum MessageType : ushort - { +namespace LifxNet { + internal enum MessageType : ushort { //Device Messages DeviceGetService = 0x02, DeviceStateService = 0x03, + //Undocumented? DeviceGetTime = 0x04, DeviceSetTime = 0x05, DeviceStateTime = 0x06, + // Documented DeviceGetHostInfo = 12, DeviceStateHostInfo = 13, DeviceGetHostFirmware = 14, @@ -41,6 +35,7 @@ internal enum MessageType : ushort DeviceStateGroup = 53, DeviceEchoRequest = 58, DeviceEchoResponse = 59, + //Light messages LightGet = 101, LightSetColor = 102, @@ -50,14 +45,38 @@ internal enum MessageType : ushort LightSetPower = 117, LightStatePower = 118, LightSetWaveformOptional = 119, + //Infrared InfraredGet = 120, InfraredState = 121, InfraredSet = 122, + //Multi zone + SetColorZones = 501, + GetColorZones = 502, + StateZone = 503, + StateMultiZone = 506, + SetExtendedColorZones = 510, + GetExtendedColorZones = 511, + StateExtendedColorZones = 512, + + //Tile + GetDeviceChain = 701, + StateDeviceChain = 702, + SetUserPosition = 703, + GetTileState64 = 707, + StateTileState64 = 711, + SetTileState64 = 715, + + //Switch + GetRelayPower = 816, + SetRelayPower = 817, + StateRelayPower = 818, + //Unofficial LightGetTemperature = 0x6E, - //LightStateTemperature = 0x6f, + + //LightStateTemperature = 0x6f, SetLightBrightness = 0x68 } -} +} \ No newline at end of file diff --git a/src/LifxNet/Payload.cs b/src/LifxNet/Payload.cs new file mode 100644 index 0000000..face24c --- /dev/null +++ b/src/LifxNet/Payload.cs @@ -0,0 +1,422 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; + +namespace LifxNet { + /// + /// A wrapper class for a byte payload + /// Any time the payload is read from, our pointer increments + /// the proper number of bytes until the end is reached, + /// at which time a message will be logged. Should eventually throw an error or something... + /// + public class Payload { + private readonly List _data; + private readonly BinaryReader _br; + private readonly MemoryStream _ms; + private readonly long _len; + + /// + /// Get the length of the internal byte array + /// + public int Length => _data.Count; + + + /// + /// Initialize a new, empty payload where we can serialize outgoing data + /// + public Payload() { + _data = new List(); + _ms = new MemoryStream(_data.ToArray()); + _len = _ms.Length; + _br = new BinaryReader(_ms); + } + + /// + /// Create a payload from an array of objects + /// + /// + /// + public Payload(Object[] args) { + _data = new List(); + foreach (var arg in args) { + switch (arg) { + case byte b: + Add(b); + break; + case ushort @ushort: + Add((byte)@ushort); + break; + case uint u: + Add(u); + break; + case byte[] bytes: + Add(bytes); + break; + case string s: + Add(s.PadRight(32).Take(32).ToString()); + break; + case long l: + Add(l); + break; + case double d: + Add(d); + break; + case float f: + Add(f); + break; + case short s: + Add(s); + break; + case int i: + Add(i); + break; + case ulong u: + Add(u); + break; + case LifxColor c: + Add(c); + break; + case LifxColor[] colors: + Add(colors); + break; + case DateTime dt: + Add(dt); + break; + case Tile t: + Add(t); + break; + default: + Debug.WriteLine("Unsupported type!" + args.GetType().FullName); + throw new NotSupportedException(args.GetType().FullName); + } + } + + _ms = new MemoryStream(_data.ToArray()); + _len = _ms.Length; + _br = new BinaryReader(_ms); + } + + /// + /// Initialize with a byte array + /// + /// + public Payload(byte[] data) { + _data = data.ToList(); + _ms = new MemoryStream(data); + _len = _ms.Length; + _br = new BinaryReader(_ms); + } + + /// + /// Get the current position of the array + /// + public int Position => (int) _ms.Position; + + /// + /// Return our base byte list as an array + /// + /// + public byte[] ToArray() { + return _data.ToArray(); + } + + /// + /// Return our base byte list + /// + /// + public List ToList() { + return _data; + } + + /// + /// Serialize base byte list to a string + /// + /// + public override string ToString() { + return _data.ToString(); + } + + /// + /// Check to see if we still have data to read + /// + /// + public bool HasContent() { + return _ms.Position < _len; + } + + /// + /// Rewind our pointer N bytes + /// + /// How far to rewind. Default is 1. + public void Rewind(int len = 1) { + if (_ms.Position - len < 0) { + Reset(); + } else { + _ms.Seek(len * -1, SeekOrigin.Current); + } + } + + /// + /// Forward our pointer N bytes + /// + /// How far to advance. Default is 1. + public void Advance(int len = 1) { + if (_ms.Position + len < _len) { + _ms.Seek(len, SeekOrigin.Current); + } else { + FastForward(); + } + } + + /// + /// Forward the pointer to the end of the array + /// + public void FastForward() { + _ms.Seek(0, SeekOrigin.End); + } + + /// + /// Reset our pointer to 0 + /// + public void Reset() { + _ms.Seek(0, SeekOrigin.Begin); + } + + /// + /// Read LifxColor from array and increment pointer 8 bytes + /// + /// + public LifxColor GetColor() { + try { + var h = GetUInt16(); + var s = GetUInt16(); + var b = GetUInt16(); + var k = GetUInt16(); + return new LifxColor(h, s, b, k); + } catch (Exception e) { + Debug.WriteLine("Exception: " + e.Message); + } + + return new LifxColor(); + } + + /// + /// Get an array of bytes from the reader + /// + /// + /// + public byte[] GetBytes(int len) { + return _br.ReadBytes(len); + } + + /// + /// Read Uint8 from array and increment pointer 1 byte + /// + /// byte + public byte GetUint8() { + try { + return _br.ReadByte(); + } catch { + Debug.WriteLine("Error reading byte, pos is " + _ms.Position); + } + + return 0; + } + + /// + /// Read UInt16 from array and increment pointer 2 bytes + /// + /// ushort + public ushort GetUInt16() { + try { + return _br.ReadUInt16(); + } catch { + Debug.WriteLine($"Error getting Uint16 from payload, pointer {_ms.Position} of range: " + _len); + } + + return 0; + } + + /// + /// Read Int16 from array and increment pointer 2 bytes. + /// + /// short + public short GetInt16() { + try { + return _br.ReadInt16(); + } catch { + Debug.WriteLine($"Error getting int16 from payload, pointer {_ms.Position} of range: " + _len); + } + + return 0; + } + + /// + /// Read Int32 from array and increment pointer 4 bytes. + /// + /// int + public int GetInt32() { + try { + return _br.ReadInt32(); + } catch { + Debug.WriteLine($"Error getting Int32 from payload, pointer {_ms.Position} of range: " + _len); + } + + return 0; + } + + /// + /// Read a UInt32 from array and increment pointer 4 bytes. + /// + /// + public uint GetUInt32() { + try { + return _br.ReadUInt32(); + } catch { + Debug.WriteLine($"Error getting Uint32 from payload, pointer {_ms.Position} of range: " + _len); + } + + return 0; + } + + /// + /// Read an Int64 from array and increment pointer 8 bytes. + /// + /// long + public long GetInt64() { + try { + return _br.ReadInt64(); + } catch { + Debug.WriteLine($"Error getting Int64 from payload, pointer {_ms.Position} of range: " + _len); + } + + return 0; + } + + /// + /// Read a UInt64 from array and increment pointer 8 bytes. + /// + /// ulong + public ulong GetUInt64() { + try { + return _br.ReadUInt64(); + } catch { + Debug.WriteLine($"Error getting Uint64 from payload, pointer {_ms.Position} of range: " + _len); + } + + return 0; + } + + /// + /// Read a Float32 from array and increment pointer 4 bytes. + /// + /// float + public float GetFloat32() { + try { + return _br.ReadSingle(); + } catch { + Debug.WriteLine($"Error getting Float32 from payload, pointer {_ms.Position} of range: " + _len); + } + + return 0; + } + + /// + /// Read a string from our payload. + /// + /// The number of chars to read. If none specified, will read the entire payload + /// string + public string GetString(long length = -1) { + if (length == -1) length = _len - 1 - _ms.Position; + try { + return _br.ReadChars((int) length).ToString(); + } catch { + Debug.WriteLine($"Error getting string, pointer {_ms.Position} out of range: " + _len); + } + + return string.Empty; + } + + private void Add(string input) { + _data.AddRange(Encoding.ASCII.GetBytes(input)); + } + + private void Add(byte input) { + _data.Add(input); + } + + private void Add(byte[] input) { + _data.AddRange(input); + } + + private void Add(short input) { + _data.AddRange(BitConverter.GetBytes(input)); + } + + private void Add(int input) { + _data.AddRange(BitConverter.GetBytes(input)); + } + + private void Add(uint input) { + _data.AddRange(BitConverter.GetBytes(input)); + } + + private void Add(long input) { + _data.AddRange(BitConverter.GetBytes(input)); + } + + private void Add(float input) { + _data.AddRange(BitConverter.GetBytes(input)); + } + + private void Add(double input) { + _data.AddRange(BitConverter.GetBytes(input)); + } + + private void Add(ushort input) { + _data.AddRange(BitConverter.GetBytes(input)); + } + + private void Add(ulong input) { + _data.AddRange(BitConverter.GetBytes(input)); + } + + private void Add(LifxColor input) { + _data.AddRange(input.ToBytes()); + } + + private void Add(LifxColor[] input) { + foreach (var lc in input) { + Add(lc); + } + } + + private void Add(DateTime input) { + var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + Add(Convert.ToInt64((input - epoch).TotalSeconds)); + } + + private void Add(Tile input) { + Add(input.AccelMeasX); + Add(input.AccelMeasY); + Add(input.AccelMeasZ); + Add(0); + Add(input.UserX); + Add(input.UserY); + Add(input.Width); + Add(input.Height); + Add((byte) 0); + Add(input.DeviceVersionVendor); + Add(input.DeviceVersionProduct); + Add(input.DeviceVersionVersion); + Add(input.FirmwareBuild); + Add((ulong) 0); + Add(input.FirmwareVersionMinor); + Add(input.FirmwareVersionMajor); + Add((uint) 0); + } + } +} \ No newline at end of file diff --git a/src/LifxNet/TileGroup.cs b/src/LifxNet/TileGroup.cs new file mode 100644 index 0000000..955f6a2 --- /dev/null +++ b/src/LifxNet/TileGroup.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Diagnostics; + +namespace LifxNet { + [Serializable] + public class Tile { + public short AccelMeasX { get; set; } + public short AccelMeasY { get; set; } + public short AccelMeasZ { get; set; } + public float UserX { get; set; } + public float UserY { get; set; } + public byte Width { get; set; } + public byte Height { get; set; } + public uint DeviceVersionVendor { get; set; } + public uint DeviceVersionProduct { get; set; } + public uint DeviceVersionVersion { get; set; } + public ulong FirmwareBuild { get; set; } + public ushort FirmwareVersionMinor { get; set; } + public ushort FirmwareVersionMajor { get; set; } + + public Tile() { + } + + public void CreateDefault(int index) { + AccelMeasX = 0; + AccelMeasY = 0; + AccelMeasZ = 0; + UserX = index * .5f; + UserY = 8.06f; + Width = 8; + Height = 8; + DeviceVersionProduct = 55; + DeviceVersionVendor = 1; + DeviceVersionVersion = 10; + FirmwareBuild = 1532997580; + FirmwareVersionMajor = 50; + FirmwareVersionMinor = 3; + } + + /// + /// Read payload into tile + /// + /// + public void LoadBytes(Payload payload) { + + AccelMeasX = payload.GetInt16(); + AccelMeasY = payload.GetInt16(); + AccelMeasZ = payload.GetInt16(); + payload.Advance(2); + UserX = payload.GetFloat32(); + UserY = payload.GetFloat32(); + Width = payload.GetUint8(); + Height = payload.GetUint8(); + payload.Advance(); + DeviceVersionVendor = payload.GetUInt32(); + DeviceVersionProduct = payload.GetUInt32(); + DeviceVersionVersion = payload.GetUInt32(); + FirmwareBuild = payload.GetUInt64(); + payload.Advance(8); + FirmwareVersionMajor = payload.GetUInt16(); + FirmwareVersionMinor = payload.GetUInt16(); + payload.Advance(4); + } + + public byte[] ToBytes() { + var output = new List(); + output.AddRange(BitConverter.GetBytes(AccelMeasX)); + output.AddRange(BitConverter.GetBytes(AccelMeasY)); + output.AddRange(BitConverter.GetBytes(AccelMeasZ)); + output.AddRange(BitConverter.GetBytes((short) 0)); + output.AddRange(BitConverter.GetBytes(UserX)); + output.AddRange(BitConverter.GetBytes(UserY)); + output.Add(Width); + output.Add(Height); + output.Add(50); // Reserved + output.AddRange(BitConverter.GetBytes(DeviceVersionVendor)); + output.AddRange(BitConverter.GetBytes(DeviceVersionProduct)); + output.AddRange(BitConverter.GetBytes(DeviceVersionVersion)); + output.AddRange(BitConverter.GetBytes(FirmwareBuild)); + output.AddRange(BitConverter.GetBytes(FirmwareBuild)); + output.AddRange(BitConverter.GetBytes(FirmwareVersionMajor)); + output.AddRange(BitConverter.GetBytes(FirmwareVersionMinor)); + output.AddRange(BitConverter.GetBytes(uint.MinValue)); // Reserved + + return output.ToArray(); + } + + } +} \ No newline at end of file diff --git a/src/LifxNet/Utilities.cs b/src/LifxNet/Utilities.cs index bf280e6..6879a6b 100644 --- a/src/LifxNet/Utilities.cs +++ b/src/LifxNet/Utilities.cs @@ -1,17 +1,10 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -namespace LifxNet -{ - internal static class Utilities - { +namespace LifxNet { + internal static class Utilities { public static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); - - public static UInt16[] RgbToHsl(Color rgb) - { + + public static ushort[] RgbToHsl(LifxColor rgb) { // normalize red, green and blue values double r = (rgb.R / 255.0); double g = (rgb.G / 255.0); @@ -21,31 +14,22 @@ public static UInt16[] RgbToHsl(Color rgb) double min = Math.Min(r, Math.Min(g, b)); double h = 0.0; - if (max == r && g >= b) - { + if (max == r && g >= b) { h = 60 * (g - b) / (max - min); - } - else if (max == r && g < b) - { + } else if (max == r && g < b) { h = 60 * (g - b) / (max - min) + 360; - } - else if (max == g) - { + } else if (max == g) { h = 60 * (b - r) / (max - min) + 120; - } - else if (max == b) - { + } else if (max == b) { h = 60 * (r - g) / (max - min) + 240; } double s = (max == 0) ? 0.0 : (1.0 - (min / max)); - return new UInt16[] { - (UInt16)(h / 360 * 65535), - (UInt16)(s * 65535), - (UInt16)(max * 65535) + return new[] { + (ushort) (h / 360 * 65535), + (ushort) (s * 65535), + (ushort) (max * 65535) }; } - - } -} +} \ No newline at end of file diff --git a/src/LifxNet/json/products.json b/src/LifxNet/json/products.json new file mode 100644 index 0000000..14c6fa0 --- /dev/null +++ b/src/LifxNet/json/products.json @@ -0,0 +1,916 @@ +[ + { + "vid": 1, + "name": "LIFX", + "products": [ + { + "pid": 1, + "name": "LIFX Original 1000", + "features": { + "color": true, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2500, + 9000 + ] + } + }, + { + "pid": 3, + "name": "LIFX Color 650", + "features": { + "color": true, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2500, + 9000 + ] + } + }, + { + "pid": 10, + "name": "LIFX White 800 (Low Voltage)", + "features": { + "color": false, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2700, + 6500 + ] + } + }, + { + "pid": 11, + "name": "LIFX White 800 (High Voltage)", + "features": { + "color": false, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2700, + 6500 + ] + } + }, + { + "pid": 15, + "name": "LIFX Color 1000", + "features": { + "color": true, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2500, + 9000 + ] + } + }, + { + "pid": 18, + "name": "LIFX White 900 BR30 (Low Voltage)", + "features": { + "color": false, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2500, + 9000 + ] + } + }, + { + "pid": 19, + "name": "LIFX White 900 BR30 (High Voltage)", + "features": { + "color": false, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2500, + 9000 + ] + } + }, + { + "pid": 20, + "name": "LIFX Color 1000 BR30", + "features": { + "color": true, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2500, + 9000 + ] + } + }, + { + "pid": 22, + "name": "LIFX Color 1000", + "features": { + "color": true, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2500, + 9000 + ] + } + }, + { + "pid": 27, + "name": "LIFX A19", + "features": { + "color": true, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2500, + 9000 + ] + } + }, + { + "pid": 28, + "name": "LIFX BR30", + "features": { + "color": true, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2500, + 9000 + ] + } + }, + { + "pid": 29, + "name": "LIFX A19 Night Vision", + "features": { + "color": true, + "chain": false, + "matrix": false, + "infrared": true, + "multizone": false, + "temperature_range": [ + 2500, + 9000 + ] + } + }, + { + "pid": 30, + "name": "LIFX BR30 Night Vision", + "features": { + "color": true, + "chain": false, + "matrix": false, + "infrared": true, + "multizone": false, + "temperature_range": [ + 2500, + 9000 + ] + } + }, + { + "pid": 31, + "name": "LIFX Z", + "features": { + "color": true, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": true, + "temperature_range": [ + 2500, + 9000 + ] + } + }, + { + "pid": 32, + "name": "LIFX Z", + "features": { + "color": true, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": true, + "temperature_range": [ + 2500, + 9000 + ], + "min_ext_mz_firmware": 1532997580, + "min_ext_mz_firmware_components": [ + 2, + 77 + ] + } + }, + { + "pid": 36, + "name": "LIFX Downlight", + "features": { + "color": true, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2500, + 9000 + ] + } + }, + { + "pid": 37, + "name": "LIFX Downlight", + "features": { + "color": true, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2500, + 9000 + ] + } + }, + { + "pid": 38, + "name": "LIFX Beam", + "features": { + "color": true, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": true, + "temperature_range": [ + 2500, + 9000 + ], + "min_ext_mz_firmware": 1532997580, + "min_ext_mz_firmware_components": [ + 2, + 77 + ] + } + }, + { + "pid": 39, + "name": "LIFX Downlight White To Warm", + "features": { + "color": false, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 1500, + 9000 + ] + } + }, + { + "pid": 40, + "name": "LIFX Downlight", + "features": { + "color": true, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2500, + 9000 + ] + } + }, + { + "pid": 43, + "name": "LIFX A19", + "features": { + "color": true, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2500, + 9000 + ] + } + }, + { + "pid": 44, + "name": "LIFX BR30", + "features": { + "color": true, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2500, + 9000 + ] + } + }, + { + "pid": 45, + "name": "LIFX A19 Night Vision", + "features": { + "color": true, + "chain": false, + "matrix": false, + "infrared": true, + "multizone": false, + "temperature_range": [ + 2500, + 9000 + ] + } + }, + { + "pid": 46, + "name": "LIFX BR30 Night Vision", + "features": { + "color": true, + "chain": false, + "matrix": false, + "infrared": true, + "multizone": false, + "temperature_range": [ + 2500, + 9000 + ] + } + }, + { + "pid": 49, + "name": "LIFX Mini Color", + "features": { + "color": true, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2500, + 9000 + ] + } + }, + { + "pid": 50, + "name": "LIFX Mini White To Warm", + "features": { + "color": false, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 1500, + 4000 + ] + } + }, + { + "pid": 51, + "name": "LIFX Mini White", + "features": { + "color": false, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2700, + 2700 + ] + } + }, + { + "pid": 52, + "name": "LIFX GU10", + "features": { + "color": true, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2500, + 9000 + ] + } + }, + { + "pid": 53, + "name": "LIFX GU10", + "features": { + "color": true, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2500, + 9000 + ] + } + }, + { + "pid": 55, + "name": "LIFX Tile", + "features": { + "color": true, + "chain": true, + "matrix": true, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2500, + 9000 + ] + } + }, + { + "pid": 57, + "name": "LIFX Candle", + "features": { + "color": true, + "chain": false, + "matrix": true, + "infrared": false, + "multizone": false, + "temperature_range": [ + 1500, + 9000 + ] + } + }, + { + "pid": 59, + "name": "LIFX Mini Color", + "features": { + "color": true, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2500, + 9000 + ] + } + }, + { + "pid": 60, + "name": "LIFX Mini White To Warm", + "features": { + "color": false, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 1500, + 4000 + ] + } + }, + { + "pid": 61, + "name": "LIFX Mini White", + "features": { + "color": false, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2700, + 2700 + ] + } + }, + { + "pid": 62, + "name": "LIFX A19", + "features": { + "color": true, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2500, + 9000 + ] + } + }, + { + "pid": 63, + "name": "LIFX BR30", + "features": { + "color": true, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2500, + 9000 + ] + } + }, + { + "pid": 64, + "name": "LIFX A19 Night Vision", + "features": { + "color": true, + "chain": false, + "matrix": false, + "infrared": true, + "multizone": false, + "temperature_range": [ + 2500, + 9000 + ] + } + }, + { + "pid": 65, + "name": "LIFX BR30 Night Vision", + "features": { + "color": true, + "chain": false, + "matrix": false, + "infrared": true, + "multizone": false, + "temperature_range": [ + 2500, + 9000 + ] + } + }, + { + "pid": 66, + "name": "LIFX Mini White", + "features": { + "color": false, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2700, + 2700 + ] + } + }, + { + "pid": 68, + "name": "LIFX Candle", + "features": { + "color": false, + "chain": false, + "matrix": true, + "infrared": false, + "multizone": false, + "temperature_range": [ + 1500, + 9000 + ] + } + }, + { + "pid": 70, + "name": "LIFX Switch", + "features": { + "color": false, + "relays": true, + "chain": false, + "matrix": false, + "buttons": true, + "infrared": false, + "multizone": false + } + }, + { + "pid": 81, + "name": "LIFX Candle White To Warm", + "features": { + "color": false, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2200, + 6500 + ] + } + }, + { + "pid": 82, + "name": "LIFX Filament Clear", + "features": { + "color": false, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2100, + 2100 + ] + } + }, + { + "pid": 85, + "name": "LIFX Filament Amber", + "features": { + "color": false, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2000, + 2000 + ] + } + }, + { + "pid": 87, + "name": "LIFX Mini White", + "features": { + "color": false, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2700, + 2700 + ] + } + }, + { + "pid": 88, + "name": "LIFX Mini White", + "features": { + "color": false, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2700, + 2700 + ] + } + }, + { + "pid": 89, + "name": "LIFX Switch", + "features": { + "color": false, + "relays": true, + "chain": false, + "matrix": false, + "buttons": true, + "infrared": false, + "multizone": false + } + }, + { + "pid": 90, + "name": "LIFX Clean", + "features": { + "hev": true, + "color": true, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2500, + 9000 + ] + } + }, + { + "pid": 91, + "name": "LIFX Color", + "features": { + "color": true, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2500, + 9000 + ] + } + }, + { + "pid": 92, + "name": "LIFX Color", + "features": { + "color": true, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2500, + 9000 + ] + } + }, + { + "pid": 94, + "name": "LIFX BR30", + "features": { + "color": true, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2500, + 9000 + ] + } + }, + { + "pid": 96, + "name": "LIFX Candle White To Warm", + "features": { + "color": false, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2200, + 6500 + ] + } + }, + { + "pid": 97, + "name": "LIFX A19", + "features": { + "color": true, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2500, + 9000 + ] + } + }, + { + "pid": 98, + "name": "LIFX BR30", + "features": { + "color": true, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2500, + 9000 + ] + } + }, + { + "pid": 99, + "name": "LIFX Clean", + "features": { + "hev": true, + "color": true, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2500, + 9000 + ] + } + }, + { + "pid": 100, + "name": "LIFX Filament Clear", + "features": { + "color": false, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2100, + 2100 + ] + } + }, + { + "pid": 101, + "name": "LIFX Filament Amber", + "features": { + "color": false, + "chain": false, + "matrix": false, + "infrared": false, + "multizone": false, + "temperature_range": [ + 2000, + 2000 + ] + } + }, + { + "pid": 109, + "name": "LIFX A19 Night Vision", + "features": { + "color": true, + "chain": false, + "matrix": false, + "infrared": true, + "multizone": false, + "temperature_range": [ + 2500, + 9000 + ] + } + }, + { + "pid": 110, + "name": "LIFX BR30 Night Vision", + "features": { + "color": true, + "chain": false, + "matrix": false, + "infrared": true, + "multizone": false, + "temperature_range": [ + 2500, + 9000 + ] + } + }, + { + "pid": 111, + "name": "LIFX A19 Night Vision", + "features": { + "color": true, + "chain": false, + "matrix": false, + "infrared": true, + "multizone": false, + "temperature_range": [ + 2500, + 9000 + ] + } + } + ] + } +] \ No newline at end of file diff --git a/src/SampleApps/ColorSendTest/ColorSendTest.csproj b/src/SampleApps/ColorSendTest/ColorSendTest.csproj new file mode 100644 index 0000000..ae654fa --- /dev/null +++ b/src/SampleApps/ColorSendTest/ColorSendTest.csproj @@ -0,0 +1,16 @@ + + + + Exe + net5.0 + + + + + + + + + + + diff --git a/src/SampleApps/ColorSendTest/Program.cs b/src/SampleApps/ColorSendTest/Program.cs new file mode 100644 index 0000000..1acf481 --- /dev/null +++ b/src/SampleApps/ColorSendTest/Program.cs @@ -0,0 +1,353 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.Linq; +using System.Threading.Tasks; +using LifxNet; +using Newtonsoft.Json; + +namespace ColorSendTest { + class Program { + private static LifxClient _client; + private static List _devicesBulb; + private static List _devicesMulti; + private static List _devicesMultiV2; + private static List _devicesTile; + private static List _devicesSwitch; + + static async Task Main(string[] args) { + var tr1 = new TextWriterTraceListener(Console.Out); + _devicesBulb = new List(); + _devicesMulti = new List(); + _devicesMultiV2 = new List(); + _devicesTile = new List(); + _devicesSwitch = new List(); + _client = LifxClient.CreateAsync().Result; + _client.DeviceDiscovered += ClientDeviceDiscovered; + _client.DeviceLost += ClientDeviceLost; + Console.WriteLine("Enumerating devices, please wait 15 seconds..."); + _client.StartDeviceDiscovery(); + await Task.Delay(5000); + _client.StopDeviceDiscovery(); + Trace.Listeners.Add(tr1); + Console.WriteLine("Please select a device type to test (Enter a number):"); + if (_devicesBulb.Count > 0) { + Console.WriteLine("1: Bulbs"); + } + + if (_devicesMulti.Count > 0) { + Console.WriteLine("2: Multi Zone V1"); + } + + if (_devicesMultiV2.Count > 0) { + Console.WriteLine("3: Multi Zone V2"); + } + + if (_devicesTile.Count > 0) { + Console.WriteLine("4: Tiles"); + } + + if (_devicesSwitch.Count > 0) { + Console.WriteLine("5: Switch"); + } + + var selection = int.Parse(Console.ReadLine() ?? "0"); + switch (selection) { + case 1: + Console.WriteLine("Flashing bulbs on and off."); + await FlashBulbs(); + break; + case 2: + Console.WriteLine("Flashing multizone v1 devices on and off."); + await FlashMultizone(); + break; + case 3: + Console.WriteLine("Flashing multizone v2 devices on and off."); + await FlashMultizoneV2(); + break; + case 4: + Console.WriteLine("Flashing tile devices on and off."); + await FlashTiles(); + break; + case 5: + Console.WriteLine("Toggling switches is not enabled yet."); + FlashSwitches(); + break; + } + + Console.WriteLine("All done!"); + Console.ReadKey(); + } + + private static async Task FlashBulbs() { + // Save our existing states + var stateList = new List(); + var red = new LifxColor(255, 0, 0); + var black = new LifxColor(0, 0, 0); + foreach (var b in _devicesBulb) { + var bulb = (LightBulb) b; + var state = await _client.GetLightStateAsync(bulb); + stateList.Add(state); + await _client.SetPowerAsync(b, 1); + await _client.SetBrightnessAsync(bulb, 255, TimeSpan.Zero); + } + + Console.WriteLine($"Flashing {_devicesBulb.Count} bulbs."); + foreach (var bulb in _devicesBulb.Cast()) { + _client.SetColorAsync(bulb, red, 2700).ConfigureAwait(false); + } + + await Task.Delay(1000); + foreach (var bulb in _devicesBulb.Cast()) { + _client.SetColorAsync(bulb, black, 2700).ConfigureAwait(false); + } + + await Task.Delay(500); + + foreach (var bulb in _devicesBulb.Cast()) { + _client.SetColorAsync(bulb, red, 2700).ConfigureAwait(false); + } + + await Task.Delay(1000); + foreach (var bulb in _devicesBulb.Cast()) { + _client.SetColorAsync(bulb, black, 2700).ConfigureAwait(false); + } + + await Task.Delay(500); + // Set them to red + var idx = 0; + Console.WriteLine("Restoring bulb states."); + foreach (var b in _devicesBulb) { + var bulb = (LightBulb) b; + var state = stateList[idx]; + await _client.SetBrightnessAsync(bulb, state.Brightness, TimeSpan.Zero); + await _client.SetPowerAsync(bulb, state.IsOn ? 1 : 0); + idx++; + } + } + + private static async Task FlashMultizone() { + var stateList = new List(); + var responses = new List(); + foreach (var m in _devicesMulti) { + var state = await _client.GetPowerAsync(m); + stateList.Add(state); + var zoneState = await _client.GetColorZonesAsync(m,0,8); + responses.Add(zoneState); + _client.SetPowerAsync(m, 1).ConfigureAwait(false); + } + + var idx = 0; + foreach (var m in _devicesMulti) { + var state = responses[idx]; + var count = state.Count; + var start = state.Index; + var total = start - count; + for (var i = start; i < count; i++) { + var pi = i * 1.0f; + var progress = (start - pi) / total; + var apply = i == count - 1; + _client.SetColorZonesAsync(m, i, i, Rainbow(progress), TimeSpan.Zero, apply); + } + idx++; + } + + await Task.Delay(2000); + + idx = 0; + Debug.WriteLine("Setting v1 multi to rainbow!"); + var black = new LifxColor(0, 0, 0); + foreach (var m in _devicesMulti) { + var state = responses[idx]; + var count = state.Count; + var start = state.Index; + var total = start - count; + for (var i = start; i < count; i++) { + _client.SetColorZonesAsync(m, i, i, black, TimeSpan.Zero, true); + } + idx++; + } + + idx = 0; + Debug.WriteLine("Setting v1 multi to black/disabling."); + foreach (var m in _devicesMulti) { + var power = stateList[idx]; + if (power == 0) { + _client.SetPowerAsync(m, power); + } + } + } + + private static async Task FlashMultizoneV2() { + var stateList = new List(); + var responses = new List(); + foreach (var m in _devicesMulti) { + var state = await _client.GetPowerAsync(m); + stateList.Add(state); + var zoneState = await _client.GetExtendedColorZonesAsync(m); + responses.Add(zoneState); + _client.SetPowerAsync(m, 1); + } + Debug.WriteLine("Setting devices to rainbow!"); + var idx = 0; + foreach (var m in _devicesMulti) { + var state = responses[idx]; + var count = state.Count; + var start = state.Index; + var total = start - count; + var colors = new List(); + + for (var i = start; i < count; i++) { + var pi = i * 1.0f; + var progress = (start - pi) / total; + colors.Add(Rainbow(progress)); + } + _client.SetExtendedColorZonesAsync(m, TimeSpan.Zero, start, colors, true); + idx++; + } + + await Task.Delay(2000); + Debug.WriteLine("Setting v2 to black."); + + idx = 0; + var black = new LifxColor(0, 0, 0); + foreach (var m in _devicesMulti) { + var state = responses[idx]; + var count = state.Count; + var start = state.Index; + var colors = new List(); + for (var i = start; i < count; i++) { + colors.Add(black); + } + _client.SetExtendedColorZonesAsync(m, TimeSpan.Zero, start, colors,true); + idx++; + } + + idx = 0; + Debug.WriteLine("Resetting v2 multizone."); + + foreach (var m in _devicesMulti) { + var power = stateList[idx]; + if (power == 0) { + _client.SetPowerAsync(m, power); + } + } + } + + private static async Task FlashTiles() { + var chains = new List(); + foreach (var t in _devicesTile) { + var state = _client.GetDeviceChainAsync(t).Result; + chains.Add(state); + _client.SetPowerAsync(t, 1); + } + + var idx = 0; + Debug.WriteLine("Rainbowing tiles!"); + + foreach (var t in _devicesTile) { + var state = chains[idx]; + var tidx = 0; + var colors = new List(); + for (var c = 0; c < 64; c++) { + var pi = c * 1.0f; + var progress = pi / 64; + colors.Add(Rainbow(progress)); + } + for (var i = state.StartIndex; i < state.TotalCount; i++) { + _client.SetTileState64Async(t, i, 64, 1000, colors.ToArray()); + } + idx++; + } + + await Task.Delay(2000); + + idx = 0; + Debug.WriteLine("Turning off tiles."); + foreach (var t in _devicesTile) { + var state = chains[idx]; + var colors = new List(); + for (var c = 0; c < 64; c++) { + colors.Add(new LifxColor(0,0,0)); + } + for (var i = state.StartIndex; i < state.TotalCount; i++) { + _client.SetTileState64Async(t, i, 64, 1000, colors.ToArray()); + } + + _client.SetPowerAsync(t, 0); + idx++; + } + } + + private static void FlashSwitches() { + + } + + private static LifxColor Rainbow(float progress) { + Console.WriteLine("Progress is " + progress); + var div = Math.Abs(progress % 1) * 6; + var ascending = (int) (div % 1 * 255); + var descending = 255 - ascending; + var output = (int) div switch { + 0 => Color.FromArgb(255, ascending, 0), + 1 => Color.FromArgb(descending, 255, 0), + 2 => Color.FromArgb(0, 255, ascending), + 3 => Color.FromArgb(0, descending, 255), + 4 => Color.FromArgb(ascending, 0, 255), + _ => Color.FromArgb(255, 0, descending) + }; + return new LifxColor(output); + } + + private static void ClientDeviceLost(object sender, LifxClient.DeviceDiscoveryEventArgs e) + { + Console.WriteLine("Device lost"); + } + + private static async void ClientDeviceDiscovered(object sender, LifxClient.DeviceDiscoveryEventArgs e) + { + Console.WriteLine($"Device {e.Device.MacAddressName} found @ {e.Device.HostName}"); + var version = await _client.GetDeviceVersionAsync(e.Device); + var added = false; + // Multi-zone devices + if (version.Product == 31 || version.Product == 32 || version.Product == 38) { + var extended = false; + // If new Z-LED or Beam, check if FW supports "extended" commands. + if (version.Product == 32 || version.Product == 38) { + if (version.Version >= 1532997580) { + extended = true; + } + } + + if (extended) { + added = true; + Console.WriteLine("Adding V2 Multi zone Device."); + _devicesMultiV2.Add(e.Device); + } else { + added = true; + Console.WriteLine("Adding V1 Multi zone Device."); + _devicesMulti.Add(e.Device); + } + } + + // Tile + if (version.Product == 55) { + added = true; + Console.WriteLine("Adding Tile Device"); + _devicesTile.Add(e.Device); + } + // Switch + if (version.Product == 70) { + added = true; + Console.WriteLine("Adding Switch Device."); + _devicesSwitch.Add(e.Device); + } + + if (!added) { + Console.WriteLine("Adding Bulb."); + _devicesBulb.Add(e.Device); + } + } + } +} \ No newline at end of file diff --git a/src/SampleApps/LifxEmulator/LifxEmulator.csproj b/src/SampleApps/LifxEmulator/LifxEmulator.csproj new file mode 100644 index 0000000..d7e88b2 --- /dev/null +++ b/src/SampleApps/LifxEmulator/LifxEmulator.csproj @@ -0,0 +1,76 @@ + + + + + Debug + AnyCPU + {35655608-57DD-43AB-9D2E-81B329CC6473} + Exe + Properties + LifxEmulator + LifxEmulator + v4.7.2 + 512 + 8 + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\..\packages\Colorful.Console.1.2.15\lib\net461\Colorful.Console.dll + True + + + + ..\..\packages\Newtonsoft.Json.13.0.1-beta1\lib\net45\Newtonsoft.Json.dll + True + + + + + + + + + + + + + + + + + {9ecd8c00-1c8c-4abb-b327-525d9e719c20} + LifxNet + + + + + + + + + diff --git a/src/SampleApps/LifxEmulator/LifxPacket.cs b/src/SampleApps/LifxEmulator/LifxPacket.cs new file mode 100644 index 0000000..fff3fbb --- /dev/null +++ b/src/SampleApps/LifxEmulator/LifxPacket.cs @@ -0,0 +1,98 @@ +using System; +using System.IO; + +namespace LifxEmulator { + internal abstract class LifxPacket { + private byte[] _payload; + private ushort _type; + + protected LifxPacket(ushort type, byte[] payload) { + _type = type; + _payload = payload; + } + + internal byte[] Payload { + get { return _payload; } + } + + internal ushort Type { + get { return _type; } + } + + protected LifxPacket(ushort type, object[] data) { + _type = type; + using var ms = new MemoryStream(); + StreamWriter bw = new StreamWriter(ms); + foreach (var obj in data) { + switch (obj) { + case byte b: + bw.Write(b); + break; + case byte[] bytes: + bw.Write(bytes); + break; + case ushort @ushort: + bw.Write(@ushort); + break; + case uint u: + bw.Write(u); + break; + default: + throw new NotImplementedException(); + } + } + + _payload = ms.ToArray(); + } + + public static LifxPacket FromByteArray(byte[] data) { + // preambleFields = [ + // { name: "size" , type:type.uint16_le }, + // { name: "protocol" , type:type.uint16_le }, + // { name: "reserved1" , type:type.byte4 } , + // { name: "bulbAddress", type:type.byte6 } , + // { name: "reserved2" , type:type.byte2 } , + // { name: "site" , type:type.byte6 } , + // { name: "reserved3" , type:type.byte2 } , + // { name: "timestamp" , type:type.uint64 } , + // { name: "packetType" , type:type.uint16_le }, + // { name: "reserved4" , type:type.byte2 } , + // ]; + MemoryStream ms = new MemoryStream(data); + var br = new BinaryReader(ms); + //Header + ushort len = br.ReadUInt16(); //ReverseBytes(br.ReadUInt16()); //size uint16 + ushort protocol = br.ReadUInt16(); // ReverseBytes(br.ReadUInt16()); //origin = 0 + var identifier = br.ReadUInt32(); + byte[] bulbAddress = br.ReadBytes(6); + byte[] reserved2 = br.ReadBytes(2); + byte[] site = br.ReadBytes(6); + byte[] reserved3 = br.ReadBytes(2); + ulong timestamp = br.ReadUInt64(); + ushort packetType = br.ReadUInt16(); // ReverseBytes(br.ReadUInt16()); + byte[] reserved4 = br.ReadBytes(2); + byte[] payload = { }; + if (len > 0) { + payload = br.ReadBytes(len); + } + + LifxPacket packet = new UnknownPacket(packetType, payload, bulbAddress, site) { + TimeStamp = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(timestamp), + }; + //packet.Identifier = identifier; + return packet; + } + + private class UnknownPacket : LifxPacket { + public UnknownPacket(ushort packetType, byte[] payload, byte[] bulbAddress, byte[] site) : base(packetType, + payload) { + BulbAddress = bulbAddress; + Site = site; + } + + public byte[] BulbAddress { get; } + public DateTime TimeStamp { get; set; } + public byte[] Site { get; set; } + } + } +} \ No newline at end of file diff --git a/src/SampleApps/LifxEmulator/LifxResponses.cs b/src/SampleApps/LifxEmulator/LifxResponses.cs new file mode 100644 index 0000000..7d6f4c8 --- /dev/null +++ b/src/SampleApps/LifxEmulator/LifxResponses.cs @@ -0,0 +1,414 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.Text; +using LifxNet; +using Newtonsoft.Json; + +namespace LifxEmulator { + /// + /// Base class for LIFX response types + /// + public abstract class LifxResponse { + internal static LifxResponse Create(FrameHeader header, MessageType type, uint source, Payload payload, int deviceVersion) { + payload.Reset(); + switch (type) { + case MessageType.DeviceGetService: + type = MessageType.DeviceStateService; + return new StateServiceResponse(header, type, source); + case MessageType.DeviceEchoRequest: + type = MessageType.DeviceEchoResponse; + return new EchoResponse(header, type, payload, source); + case MessageType.DeviceGetInfo: + type = MessageType.DeviceStateInfo; + return new StateInfoResponse(header, type, source); + case MessageType.LightGet: + type = MessageType.LightState; + return new LightStateResponse(header, type, source); + case MessageType.DeviceGetVersion: + type = MessageType.DeviceStateVersion; + return new StateVersionResponse(header, type, source, deviceVersion); + case MessageType.DeviceGetHostFirmware: + type = MessageType.DeviceStateHostFirmware; + return new StateHostFirmwareResponse(header, type, source, deviceVersion); + case MessageType.GetExtendedColorZones: + type = MessageType.StateExtendedColorZones; + return new StateExtendedColorZonesResponse(header, type, source); + case MessageType.GetColorZones: + type = MessageType.StateMultiZone; + return new StateMultiZoneResponse(header, type, source); + case MessageType.GetDeviceChain: + type = MessageType.StateDeviceChain; + return new StateDeviceChainResponse(header, type, source); + case MessageType.GetRelayPower: + type = MessageType.StateRelayPower; + return new StateRelayPowerResponse(header, type, source); + case MessageType.DeviceGetPower: + type = MessageType.DeviceStatePower; + return new StatePowerResponse(header, type, source); + case MessageType.DeviceSetPower: + type = MessageType.DeviceAcknowledgement; + return new AcknowledgementResponse(header, type, source); + default: + type = MessageType.DeviceAcknowledgement; + return new AcknowledgementResponse(header, type, source); + } + } + + internal LifxResponse(FrameHeader header, MessageType type, uint source) { + Header = header; + Type = type; + Source = source; + } + + internal FrameHeader Header { get; } + internal Payload Payload { get; set; } + internal MessageType Type { get; } + internal uint Source { get; } + } + + /// + /// Response to GetService message. + /// Provides the device Service and port. + /// If the Service is temporarily unavailable, then the port value will be 0. + /// + internal class StateServiceResponse : LifxResponse { + internal StateServiceResponse(FrameHeader header, MessageType type, uint source) : base( + header, type, source) { + Service = 1; + Port = 56700; + Payload = new Payload(new object[]{Service, Port}); + } + + private byte Service { get; } + private ulong Port { get; } + } + + /// + /// Response to any message sent with ack_required set to 1. + /// + internal class AcknowledgementResponse : LifxResponse { + internal AcknowledgementResponse(FrameHeader header, MessageType type, uint source) : base( + header, type, source) { + } + } + + /// + /// Get the list of colors currently being displayed by zones + /// + public class StateMultiZoneResponse : LifxResponse { + internal StateMultiZoneResponse(FrameHeader header, MessageType type, uint source) : base( + header, type, source) { + Count = 8; + Index = 0; + Colors = new LifxColor[Count]; + for (var i = Index; i < Count; i++) { + Colors[i] = new LifxColor(255,0,0); + } + + var args = new List {(byte)Count, (byte)Index}; + args.AddRange(Colors); + Payload = new Payload(args.ToArray()); + } + + /// + /// Count - total number of zones on the device + /// + public ushort Count { get; } + + /// + /// Index - Zone the message starts from + /// + public ushort Index { get; } + + /// + /// The list of colors returned by the message + /// + public LifxColor[] Colors { get; } + } + + + /// + /// Provides run-time information of device. + /// + public class StateInfoResponse : LifxResponse { + internal StateInfoResponse(FrameHeader header, MessageType type, uint source) : base(header, + type, source) { + Time = DateTime.Now; + Uptime = 5000; + Downtime = 100000; + var args = new List {Time, Uptime, Downtime}; + Payload = new Payload(args.ToArray() ); + } + + /// + /// Current time + /// + public DateTime Time { get; set; } + + /// + /// Time since last power on (relative time in nanoseconds) + /// + public long Uptime { get; set; } + + /// + /// Last power off period, 5 second accuracy (in nanoseconds) + /// + public long Downtime { get; set; } + } + + + /// + /// Echo response with payload sent in the EchoRequest. + /// + public class EchoResponse : LifxResponse { + internal EchoResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(header, + type, source) { + RequestPayload = payload.ToArray(); + Payload = payload; + } + + /// + /// Payload sent in the EchoRequest. + /// + public byte[] RequestPayload { get; set; } + } + + + /// + /// The StateZone message represents the state of a single zone with the index field indicating which zone is represented. The count field contains the count of the total number of zones available on the device. + /// + public class StateDeviceChainResponse : LifxResponse { + internal StateDeviceChainResponse(FrameHeader header, MessageType type, uint source) : base( + header, + type, source) { + Tiles = new List(); + TotalCount = 16; + StartIndex = 0; + var args = new List(); + args.Add(StartIndex); + for (var i = StartIndex; i < TotalCount; i++) { + var tile = new Tile(); + tile.CreateDefault(i); + Tiles.Add(tile); + args.Add(tile.ToBytes()); + } + args.Add(TotalCount); + Payload = new Payload(args.ToArray()); + Payload.Rewind(); + } + + /// + /// Count - total number of zones on the device + /// + public byte TotalCount { get; } + + /// + /// Start Index - Zone the message starts from + /// + public byte StartIndex { get; } + + /// + /// The list of colors returned by the message + /// + public List Tiles { get; } + } + + public class StatePowerResponse : LifxResponse { + internal StatePowerResponse(FrameHeader header, MessageType type, uint source) : base(header, + type, source) { + Level = 65535; + var args = new List {Level}; + Payload = new Payload(args.ToArray()); + } + + /// + /// Zero implies standby and non-zero sets a corresponding power draw level. Currently only 0 and 65535 are supported. + /// + public ulong Level { get; set; } + + } + + /// + /// Get the list of colors currently being displayed by zones + /// + public class StateExtendedColorZonesResponse : LifxResponse { + internal StateExtendedColorZonesResponse(FrameHeader header, MessageType type, uint source) : + base(header, type, source) { + Colors = new List(); + Count = 8; + Index = 0; + for (var i = Index; i < Count; i++) { + Colors.Add(new LifxColor(255,0,0)); + } + + var args = new List {Count, Index, (byte) Colors.Count}; + args.AddRange(Colors); + Payload = new Payload(args.ToArray()); + } + + /// + /// Count - total number of zones on the device + /// + public ushort Count { get; private set; } + + /// + /// Index - Zone the message starts from + /// + public ushort Index { get; private set; } + + /// + /// The list of colors returned by the message + /// + public List Colors { get; private set; } + } + + /// + /// Sent by a device to provide the current light state + /// + public class LightStateResponse : LifxResponse { + internal LightStateResponse(FrameHeader header, MessageType type, uint source) : base(header, + type, source) { + + var args = new List { + new LifxColor(255, 0, 0), + 0, + (uint) 65535, + "Test Light", + (ulong) 0 + }; + Payload = new Payload(args.ToArray()); + } + + /// + /// Hue + /// + public ushort Hue { get; } + + /// + /// Saturation (0=desaturated, 65535 = fully saturated) + /// + public ushort Saturation { get; } + + /// + /// Brightness (0=off, 65535=full brightness) + /// + public ushort Brightness { get; } + + /// + /// Bulb color temperature + /// + public ushort Kelvin { get; } + + /// + /// Power state + /// + public bool IsOn { get; } + + /// + /// Light label + /// + public string Label { get; } + } + + /// + /// Response to GetVersion message. Provides the hardware version of the device. + /// + public class StateVersionResponse : LifxResponse { + internal StateVersionResponse(FrameHeader header, MessageType type, uint source, int deviceVersion) : base( + header, type, source) { + Product = 32; + + switch (deviceVersion) { + case 0: + Product = 1; + break; + case 1: + Product = 31; + break; + case 2: + Product = 32; + break; + case 3: + Product = 38; + break; + case 4: + Product = 55; + break; + case 5: + Product = 70; + break; + } + Vendor = 1; + Version = 1; + var args = new List {Vendor, Product, Version}; + Payload = new Payload(args.ToArray()); + } + + /// + /// Vendor ID + /// + public uint Vendor { get; } + + /// + /// Product ID + /// + public uint Product { get; } + + /// + /// Hardware version + /// + public uint Version { get; } + } + + /// + /// Response to GetHostFirmware message. Provides host firmware information. + /// + public class StateHostFirmwareResponse : LifxResponse { + internal StateHostFirmwareResponse(FrameHeader header, MessageType type, uint source, int deviceVersion) : base( + header, type, source) { + Build = DateTime.Now; + ulong reserved = 0; + ulong version = 1532997580; + var args = new List {Build, reserved, version}; + Payload = new Payload(args.ToArray()); + } + + /// + /// Firmware build time + /// + public DateTime Build { get; } + + /// + /// Firmware version + /// + public uint VersionMinor { get; } + public uint VersionMajor { get; } + + } + + /// + /// Response to GetVersion message. Provides the hardware version of the device. + /// + public class StateRelayPowerResponse : LifxResponse { + internal StateRelayPowerResponse(FrameHeader header, MessageType type, uint source) : base( + header, type, source) { + RelayIndex = 0; + Level = 65536; + Payload = new Payload(new object[]{RelayIndex, Level}); + } + + /// + /// The relay on the switch starting from 0 + /// + public int RelayIndex { get; } + + /// + /// The value of the relay + /// + public int Level { get; } + } + + +} \ No newline at end of file diff --git a/src/SampleApps/LifxEmulator/Payload.cs b/src/SampleApps/LifxEmulator/Payload.cs new file mode 100644 index 0000000..30b5727 --- /dev/null +++ b/src/SampleApps/LifxEmulator/Payload.cs @@ -0,0 +1,413 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using LifxNet; + +namespace LifxEmulator { + /// + /// A wrapper class for a byte payload + /// Any time the payload is read from, our pointer increments + /// the proper number of bits until the end is reached, + /// at which time a message will be logged. Should eventually throw an error or something... + /// + public class Payload { + private List _data; + private readonly BinaryReader _br; + private readonly MemoryStream _ms; + private readonly long _len; + + /// + /// Get the length of the internal byte array + /// + public int Length => _data.Count; + + + /// + /// Initialize a new, empty payload where we can serialize outgoing data + /// + public Payload() { + _data = new List(); + _ms = new MemoryStream(_data.ToArray()); + _len = _ms.Length; + _br = new BinaryReader(_ms); + } + + /// + /// Create a payload from an array of objects + /// + /// + /// + public Payload(Object[] args) { + _data = new List(); + foreach (var arg in args) { + switch (arg) { + case byte b: + Add(b); + break; + case ushort @ushort: + Add((byte)@ushort); + break; + case uint u: + Add(u); + break; + case byte[] bytes: + Add(bytes); + break; + case string s: + Add(s.PadRight(32).Take(32).ToString()); + break; + case long l: + Add(l); + break; + case double d: + Add(d); + break; + case float f: + Add(f); + break; + case short s: + Add(s); + break; + case int i: + Add(i); + break; + case ulong u: + Add(u); + break; + case LifxColor c: + Add(c); + break; + case LifxColor[] colors: + Add(colors); + break; + case DateTime dt: + Add(dt); + break; + case Tile t: + Add(t); + break; + default: + Debug.WriteLine("Unsupported type!" + args.GetType().FullName); + throw new NotSupportedException(args.GetType().FullName); + } + } + + _ms = new MemoryStream(_data.ToArray()); + _len = _ms.Length; + _br = new BinaryReader(_ms); + } + + /// + /// Initialize with a byte array + /// + /// + public Payload(byte[] data) { + _data = data.ToList(); + _ms = new MemoryStream(data); + _len = _ms.Length; + _br = new BinaryReader(_ms); + } + + /// + /// Return our base byte list as an array + /// + /// + public byte[] ToArray() { + return _data.ToArray(); + } + + /// + /// Return our base byte list + /// + /// + public List ToList() { + return _data; + } + + /// + /// Serialize base byte list to a string + /// + /// + public override string ToString() { + return _data.ToString(); + } + + /// + /// Check to see if we still have data to read + /// + /// + public bool HasContent() { + return _ms.Position < _len; + } + + /// + /// Rewind our pointer N bits + /// + /// How far to rewind. Default is 1. + public void Rewind(int len = 1) { + if (_ms.Position - len < 0) { + Reset(); + } else { + _ms.Seek(len * -1, SeekOrigin.Current); + } + } + + /// + /// Forward our pointer N bits + /// + /// How far to advance. Default is 1. + public void Advance(int len = 1) { + if (_ms.Position + len < _len) { + _ms.Seek(len, SeekOrigin.Current); + } else { + FastForward(); + } + } + + /// + /// Forward the pointer to the end of the array + /// + public void FastForward() { + _ms.Seek(0, SeekOrigin.End); + } + + /// + /// Reset our pointer to 0 + /// + public void Reset() { + _ms.Seek(0, SeekOrigin.Begin); + } + + /// + /// Read LifxColor from array and increment pointer 8 bytes + /// + /// + public LifxColor GetColor() { + try { + var h = GetUInt16(); + var s = GetUInt16(); + var b = GetUInt16(); + var k = GetUInt16(); + return new LifxColor(h, s, b, k); + } catch (Exception e) { + Debug.WriteLine("Exception: " + e.Message); + } + + return new LifxColor(); + } + + public byte[] GetBytes(int len) { + return _br.ReadBytes(len); + } + + /// + /// Read Uint8 from array and increment pointer 1 byte + /// + /// byte + public byte GetUint8() { + try { + return _br.ReadByte(); + } catch { + Debug.WriteLine("Error reading byte, pos is " + _ms.Position); + } + + return 0; + } + + /// + /// Read UInt16 from array and increment pointer 2 bits + /// + /// ushort + public ushort GetUInt16() { + try { + return _br.ReadUInt16(); + } catch { + Debug.WriteLine($"Error getting Uint16 from payload, pointer {_ms.Position} of range: " + _len); + } + + return 0; + } + + /// + /// Read Int16 from array and increment pointer 2 bits. + /// + /// short + public short GetInt16() { + try { + return _br.ReadInt16(); + } catch { + Debug.WriteLine($"Error getting int16 from payload, pointer {_ms.Position} of range: " + _len); + } + + return 0; + } + + /// + /// Read Int32 from array and increment pointer 4 bits. + /// + /// int + public int GetInt32() { + try { + return _br.ReadInt32(); + } catch { + Debug.WriteLine($"Error getting Int32 from payload, pointer {_ms.Position} of range: " + _len); + } + + return 0; + } + + /// + /// Read a UInt32 from array and increment pointer 4 bits. + /// + /// + public uint GetUInt32() { + try { + return _br.ReadUInt32(); + } catch { + Debug.WriteLine($"Error getting Uint32 from payload, pointer {_ms.Position} of range: " + _len); + } + + return 0; + } + + /// + /// Read an Int64 from array and increment pointer 8 bits. + /// + /// long + public long GetInt64() { + try { + return _br.ReadInt64(); + } catch { + Debug.WriteLine($"Error getting Int64 from payload, pointer {_ms.Position} of range: " + _len); + } + + return 0; + } + + /// + /// Read a UInt64 from array and increment pointer 8 bits. + /// + /// ulong + public ulong GetUInt64() { + try { + return _br.ReadUInt64(); + } catch { + Debug.WriteLine($"Error getting Uint64 from payload, pointer {_ms.Position} of range: " + _len); + } + + return 0; + } + + /// + /// Read a Float32 from array and increment pointer 4 bits. + /// + /// float + public float GetFloat32() { + try { + return _br.ReadSingle(); + } catch { + Debug.WriteLine($"Error getting Float32 from payload, pointer {_ms.Position} of range: " + _len); + } + + return 0; + } + + /// + /// Read a string from our payload. + /// + /// The number of chars to read. If none specified, will read the entire payload + /// string + public string GetString(long length = -1) { + if (length == -1) length = _len - 1 - _ms.Position; + try { + return _br.ReadChars((int) length).ToString(); + } catch { + Debug.WriteLine($"Error getting string, pointer {_ms.Position} out of range: " + _len); + } + + return string.Empty; + } + + private void Add(string input) { + _data.AddRange(Encoding.ASCII.GetBytes(input)); + } + + private void Add(byte input) { + _data.Add(input); + } + + private void Add(byte[] input) { + _data.AddRange(input); + } + + private void Add(short input) { + _data.AddRange(BitConverter.GetBytes(input)); + } + + private void Add(int input) { + _data.AddRange(BitConverter.GetBytes(input)); + } + + private void Add(uint input) { + _data.AddRange(BitConverter.GetBytes(input)); + } + + private void Add(long input) { + _data.AddRange(BitConverter.GetBytes(input)); + } + + private void Add(float input) { + _data.AddRange(BitConverter.GetBytes(input)); + } + + private void Add(double input) { + _data.AddRange(BitConverter.GetBytes(input)); + } + + private void Add(ushort input) { + _data.AddRange(BitConverter.GetBytes(input)); + } + + private void Add(ulong input) { + _data.AddRange(BitConverter.GetBytes(input)); + } + + private void Add(LifxColor input) { + _data.AddRange(input.ToBytes()); + } + + private void Add(LifxColor[] input) { + foreach (var lc in input) { + Add(lc); + } + } + + private void Add(DateTime input) { + var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + Add(Convert.ToInt64((input - epoch).TotalSeconds)); + } + + private void Add(Tile input) { + Add(input.AccelMeasX); + Add(input.AccelMeasY); + Add(input.AccelMeasZ); + Add(0); + Add(input.UserX); + Add(input.UserY); + Add(input.Width); + Add(input.Height); + Add((byte) 0); + Add(input.DeviceVersionVendor); + Add(input.DeviceVersionProduct); + Add(input.DeviceVersionVersion); + Add(input.FirmwareBuild); + Add((ulong) 0); + Add(input.FirmwareVersionMinor); + Add(input.FirmwareVersionMajor); + Add((uint) 0); + } + } +} \ No newline at end of file diff --git a/src/SampleApps/LifxEmulator/Program.cs b/src/SampleApps/LifxEmulator/Program.cs new file mode 100644 index 0000000..d1f6c6e --- /dev/null +++ b/src/SampleApps/LifxEmulator/Program.cs @@ -0,0 +1,303 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; +using LifxNet; +using Console = Colorful.Console; + +namespace LifxEmulator { + internal static class Program { + private static bool _quitFlag; + private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + private static int _deviceVersion; + + public static void Main(string[] args) { + var tr1 = new TextWriterTraceListener(Console.Out); + Trace.Listeners.Add(tr1); + Console.CancelKeyPress += HandleClose; + Console.WriteLine("What device would you like to emulate? 1-4"); + Console.WriteLine("(0) - Bulb"); + Console.WriteLine("(1) - Z-LED Gen 1"); + Console.WriteLine("(2) - Z-LED Gen 2"); + Console.WriteLine("(3) - Beam"); + Console.WriteLine("(4) - Tile"); + Console.WriteLine("(5) - Switch"); + + _deviceVersion = int.Parse(Console.ReadLine() ?? "0"); + Console.WriteLine("Emulation mode: " + _deviceVersion); + StartListener().Wait(); + } + + private static void HandleClose(object sender, ConsoleCancelEventArgs args) { + _quitFlag = true; + } + + private static async Task StartListener() { + + var end = new IPEndPoint(IPAddress.Any, 56700); + var client = new UdpClient(end) {Client = {Blocking = false}}; + client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + client.Client.SendBufferSize = 4096; + client.Client.ReceiveBufferSize = 4096; + Console.WriteLine("Starting listener..."); + while (_quitFlag == false) { + Console.WriteLine("Loop."); + try { + var result = await client.ReceiveAsync(); + Console.WriteLine($"Message received from {result.RemoteEndPoint.Address}."); + if (result.Buffer.Length <= 0) { + continue; + } + + Console.WriteLine("Got something..."); + await HandleIncomingMessages(result.Buffer, result.RemoteEndPoint, client); + } catch (Exception e) { + Console.WriteLine(e.ToString()); + } + } + Console.WriteLine("Canceled."); + } + + + private static async Task HandleIncomingMessages(byte[] data, IPEndPoint endpoint, UdpClient client) { + var remote = endpoint; + var msg = await ParseMessage(data); + + if (msg.GetType() != typeof(AcknowledgementResponse) || msg.Header.AcknowledgeRequired) { + Debug.WriteLine("Responding to " + remote.Address); + await BroadcastMessageAsync(remote, msg, client); + } + } + + private static async Task BroadcastMessageAsync(IPEndPoint target, LifxResponse message, UdpClient client) { + + using var stream = new MemoryStream(); + WritePacketToStream(stream, message.Header, (ushort) message.Type, message.Payload); + var msg = stream.ToArray(); + var text = string.Join(",", (from a in msg select a.ToString("X2")).ToArray()); + Debug.WriteLine($"Sending message to {target.Address}: " + text); + + await client.SendAsync(msg, msg.Length, target); + } + + private static void WritePacketToStream(Stream outStream, FrameHeader header, ushort type, Payload payload) { + if (payload == null) { + Console.WriteLine("No payload, creating new..."); + payload = new Payload(); + } + using var dw = new BinaryWriter(outStream); + + #region Frame + + Console.WriteLine($"Sending {payload.Length + 36} length message..."); + //size uint16 + dw.Write((ushort) (payload.ToArray().Length + 36)); //length + // origin (2 bits, must be 0), reserved (1 bit, must be 0), addressable (1 bit, must be 1), protocol 12 bits must be 0x400) = 0x1400 + dw.Write((ushort) 0x3400); //protocol + dw.Write(header.Identifier); //source identifier - unique value set by the client, used by responses. If 0, responses are broadcast instead + + #endregion Frame + + #region Frame address + + //The target device address is 8 bytes long, when using the 6 byte MAC address then left - + //justify the value and zero-fill the last two bytes. A target device address of all zeroes effectively addresses all devices on the local network + dw.Write(header.TargetMacAddress); // target mac address - 0 means all devices + dw.Write(new byte[] {0, 0, 0, 0, 0, 0}); //reserved 1 + + //The client can use acknowledgements to determine that the LIFX device has received a message. + //However, when using acknowledgements to ensure reliability in an over-burdened lossy network ... + //causing additional network packets may make the problem worse. + //Client that don't need to track the updated state of a LIFX device can choose not to request a + //response, which will reduce the network burden and may provide some performance advantage. In + //some cases, a device may choose to send a state update response independent of whether res_required is set. + if (header.AcknowledgeRequired && header.ResponseRequired) + dw.Write((byte) 0x03); + else if (header.AcknowledgeRequired) + dw.Write((byte) 0x02); + else if (header.ResponseRequired) + dw.Write((byte) 0x01); + else + dw.Write((byte) 0x00); + //The sequence number allows the client to provide a unique value, which will be included by the LIFX + //device in any message that is sent in response to a message sent by the client. This allows the client + //to distinguish between different messages sent with the same source identifier in the Frame. See + //ack_required and res_required fields in the Frame Address. + dw.Write(header.Sequence); + + #endregion Frame address + + #region Protocol Header + + //The at_time value should be zero for Set and Get messages sent by a client. + //For State messages sent by a device, the at_time will either be the device + //current time when the message was received or zero. StateColor is an example + //of a message that will return a non-zero at_time value + if (header.AtTime > DateTime.MinValue) { + var time = header.AtTime.ToUniversalTime(); + dw.Write((ulong) (time - new DateTime(1970, 01, 01)).TotalMilliseconds * 10); //timestamp + } else { + dw.Write((ulong) 0); + } + + #endregion Protocol Header + + dw.Write(type); //packet _type + dw.Write((ushort) 0); //reserved + dw.Write(payload.ToArray()); + dw.Flush(); + } + + private static async Task ParseMessage(byte[] packet) { + using MemoryStream ms = new MemoryStream(packet); + BinaryReader br = new BinaryReader(ms); + //frame + var size = br.ReadUInt16(); + if (packet.Length != size || size < 36) + throw new Exception("Invalid packet"); + br.ReadUInt16(); //origin:2, reserved:1, addressable:1, protocol:12 + var source = br.ReadUInt32(); + var header = new FrameHeader(source); + //frame address + byte[] target = br.ReadBytes(8); + + ms.Seek(6, SeekOrigin.Current); //skip reserved + br.ReadByte(); //reserved:6, ack_required:1, res_required:1, + header.Sequence = br.ReadByte(); + //protocol header + var nanoseconds = br.ReadUInt64(); + header.AtTime = Epoch.AddMilliseconds(nanoseconds * 0.000001); + var type = (MessageType) br.ReadUInt16(); + Console.WriteLine($"Incoming type is {type}"); + ms.Seek(2, SeekOrigin.Current); //skip reserved + var payload = new Payload(size > 36 ? br.ReadBytes(size - 36) : new byte[] { }); + if (type == MessageType.SetColorZones) { + var start = payload.GetUint8(); + var end = payload.GetUint8(); + var color = payload.GetColor(); + Debug.WriteLine($"Setting zones {start} - {end} to {color.ToHsbkString()}", color.Color); + } + + if (type == MessageType.SetTileState64) { + payload.Advance(9); + Console.WriteLine("Colors: "); + for (var i = 0; i < 64; i++) { + var color = payload.GetColor(); + Console.Write(i.ToString(), color.Color); + } + Console.WriteLine(""); + } + var res = LifxResponse.Create(header, type, source, + payload,_deviceVersion); + await Task.FromResult(true); + return res; + } + } + + internal class FrameHeader { + public uint Identifier; + public byte Sequence; + public bool AcknowledgeRequired; + public bool ResponseRequired; + public byte[] TargetMacAddress = {0, 0, 0, 0, 0, 0, 0, 0}; + public DateTime AtTime = DateTime.MinValue; + + public FrameHeader() { + } + + public FrameHeader(uint id, bool acknowledgeRequired = false) { + Identifier = id; + AcknowledgeRequired = acknowledgeRequired; + } + + public string TargetMacAddressName { + get { return string.Join(":", TargetMacAddress.Take(6).Select(tb => tb.ToString("X2")).ToArray()); } + } + } + + internal enum MessageType : ushort { + //Device Messages + DeviceGetService = 0x02, + DeviceStateService = 0x03, + //Undocumented? + DeviceGetTime = 0x04, + DeviceSetTime = 0x05, + DeviceStateTime = 0x06, + // Documented + DeviceGetHostInfo = 12, + DeviceStateHostInfo = 13, + DeviceGetHostFirmware = 14, + DeviceStateHostFirmware = 15, + DeviceGetWifiInfo = 16, + DeviceStateWifiInfo = 17, + DeviceGetWifiFirmware = 18, + DeviceStateWifiFirmware = 19, + DeviceGetPower = 20, + DeviceSetPower = 21, + DeviceStatePower = 22, + DeviceGetLabel = 23, + DeviceSetLabel = 24, + DeviceStateLabel = 25, + DeviceGetVersion = 32, + DeviceStateVersion = 33, + DeviceGetInfo = 34, + DeviceStateInfo = 35, + DeviceAcknowledgement = 45, + DeviceGetLocation = 48, + DeviceSetLocation = 49, + DeviceStateLocation = 50, + DeviceGetGroup = 51, + DeviceSetGroup = 52, + DeviceStateGroup = 53, + DeviceEchoRequest = 58, + DeviceEchoResponse = 59, + + //Light messages + LightGet = 101, + LightSetColor = 102, + LightSetWaveform = 103, + LightState = 107, + LightGetPower = 116, + LightSetPower = 117, + LightStatePower = 118, + LightSetWaveformOptional = 119, + + //Infrared + InfraredGet = 120, + InfraredState = 121, + InfraredSet = 122, + + //Multi zone + SetColorZones = 501, + GetColorZones = 502, + StateZone = 503, + StateMultiZone = 506, + SetExtendedColorZones = 510, + GetExtendedColorZones = 511, + StateExtendedColorZones = 512, + + //Tile + GetDeviceChain = 701, + StateDeviceChain = 702, + SetUserPosition = 703, + GetTileState64 = 707, + StateTileState64 = 711, + SetTileState64 = 715, + + //Switch + GetRelayPower = 816, + SetRelayPower = 817, + StateRelayPower = 818, + + //Unofficial + LightGetTemperature = 0x6E, + + //LightStateTemperature = 0x6f, + SetLightBrightness = 0x68 + } +} \ No newline at end of file diff --git a/src/SampleApps/LifxEmulator/Properties/AssemblyInfo.cs b/src/SampleApps/LifxEmulator/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..ff8ea90 --- /dev/null +++ b/src/SampleApps/LifxEmulator/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("LifxEmulator")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("LifxEmulator")] +[assembly: AssemblyCopyright("Copyright © 2021")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("35655608-57DD-43AB-9D2E-81B329CC6473")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/src/SampleApps/LifxEmulator/packages.config b/src/SampleApps/LifxEmulator/packages.config new file mode 100644 index 0000000..9a394a4 --- /dev/null +++ b/src/SampleApps/LifxEmulator/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/SampleApps/SampleApp.Universal/App.xaml.cs b/src/SampleApps/SampleApp.Universal/App.xaml.cs index cf63249..503bf7c 100644 --- a/src/SampleApps/SampleApp.Universal/App.xaml.cs +++ b/src/SampleApps/SampleApp.Universal/App.xaml.cs @@ -1,18 +1,9 @@ using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices.WindowsRuntime; +using System.Diagnostics; using Windows.ApplicationModel; using Windows.ApplicationModel.Activation; -using Windows.Foundation; -using Windows.Foundation.Collections; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; -using Windows.UI.Xaml.Controls.Primitives; -using Windows.UI.Xaml.Data; -using Windows.UI.Xaml.Input; -using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Navigation; namespace SampleApp.Universal @@ -20,16 +11,15 @@ namespace SampleApp.Universal /// /// Provides application-specific behavior to supplement the default Application class. /// - sealed partial class App : Application - { + sealed partial class App { /// /// Initializes the singleton application object. This is the first line of authored code /// executed, and as such is the logical equivalent of main() or WinMain(). /// public App() { - this.InitializeComponent(); - this.Suspending += OnSuspending; + InitializeComponent(); + Suspending += OnSuspending; } /// @@ -40,9 +30,9 @@ public App() protected override void OnLaunched(LaunchActivatedEventArgs e) { #if DEBUG - if (System.Diagnostics.Debugger.IsAttached) + if (Debugger.IsAttached) { - this.DebugSettings.EnableFrameRateCounter = true; + DebugSettings.EnableFrameRateCounter = true; } #endif Frame rootFrame = Window.Current.Content as Frame; diff --git a/src/SampleApps/SampleApp.Universal/MainPage.xaml.cs b/src/SampleApps/SampleApp.Universal/MainPage.xaml.cs index 461db84..97b44c2 100644 --- a/src/SampleApps/SampleApp.Universal/MainPage.xaml.cs +++ b/src/SampleApps/SampleApp.Universal/MainPage.xaml.cs @@ -1,19 +1,13 @@ using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices.WindowsRuntime; using System.Threading.Tasks; -using Windows.Foundation; -using Windows.Foundation.Collections; +using Windows.UI.Core; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Controls.Primitives; -using Windows.UI.Xaml.Data; using Windows.UI.Xaml.Input; -using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Navigation; +using LifxNet; // The Blank Page item template is documented at http://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x409 @@ -25,55 +19,56 @@ namespace SampleApp.Universal public sealed partial class MainPage : Page { - ObservableCollection bulbs = new ObservableCollection(); - LifxNet.LifxClient client = null; + ObservableCollection bulbs = new ObservableCollection(); + LifxClient _client; public MainPage() { - this.InitializeComponent(); + InitializeComponent(); bulbList.ItemsSource = bulbs; } protected async override void OnNavigatedTo(NavigationEventArgs e) { base.OnNavigatedTo(e); - client = await LifxNet.LifxClient.CreateAsync(); - client.DeviceDiscovered += Client_DeviceDiscovered; - client.DeviceLost += Client_DeviceLost; - client.StartDeviceDiscovery(); + _client = await LifxClient.CreateAsync(); + _client.DeviceDiscovered += ClientDeviceDeviceDiscovered; + _client.DeviceLost += ClientDeviceDeviceLost; + _client.StartDeviceDiscovery(); + await Task.FromResult(true); } protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) { - client.DeviceDiscovered -= Client_DeviceDiscovered; - client.DeviceLost -= Client_DeviceLost; - client.StopDeviceDiscovery(); - client = null; + _client.DeviceDiscovered -= ClientDeviceDeviceDiscovered; + _client.DeviceLost -= ClientDeviceDeviceLost; + _client.StopDeviceDiscovery(); + _client = null; base.OnNavigatingFrom(e); } - private void Client_DeviceLost(object sender, LifxNet.LifxClient.DeviceDiscoveryEventArgs e) + private void ClientDeviceDeviceLost(object sender, LifxClient.DeviceDiscoveryEventArgs e) { - var _ = Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () => + var _ = Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { - var bulb = e.Device as LifxNet.LightBulb; + var bulb = e.Device as LightBulb; if (bulbs.Contains(bulb)) bulbs.Remove(bulb); }); } - private void Client_DeviceDiscovered(object sender, LifxNet.LifxClient.DeviceDiscoveryEventArgs e) + private void ClientDeviceDeviceDiscovered(object sender, LifxClient.DeviceDiscoveryEventArgs e) { - var _ = Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () => + var _ = Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { - var bulb = e.Device as LifxNet.LightBulb; + var bulb = e.Device as LightBulb; if (!bulbs.Contains(bulb)) bulbs.Add(bulb); }); } - private async void bulbList_SelectionChanged(object sender, Windows.UI.Xaml.Controls.SelectionChangedEventArgs e) + private async void bulbList_SelectionChanged(object sender, SelectionChangedEventArgs e) { - var bulb = bulbList.SelectedItem as LifxNet.LightBulb; + var bulb = bulbList.SelectedItem as LightBulb; if (bulb != null) { - var state = await client.GetLightStateAsync(bulb); + var state = await _client.GetLightStateAsync(bulb); Name.Text = state.Label; PowerState.IsOn = state.IsOn; hue = state.Hue; @@ -91,16 +86,16 @@ private async void bulbList_SelectionChanged(object sender, Windows.UI.Xaml.Cont private async void PowerState_Toggled(object sender, RoutedEventArgs e) { - var bulb = bulbList.SelectedItem as LifxNet.LightBulb; + var bulb = bulbList.SelectedItem as LightBulb; if (bulb != null) { - await client.SetDevicePowerStateAsync(bulb, PowerState.IsOn); + await _client.SetDevicePowerStateAsync(bulb, PowerState.IsOn); } } private void brightnessSlider_ValueChanged(object sender, RangeBaseValueChangedEventArgs e) { - var bulb = bulbList.SelectedItem as LifxNet.LightBulb; + var bulb = bulbList.SelectedItem as LightBulb; if (bulb != null) SetColor(bulb, null, null, (UInt16)e.NewValue); } @@ -108,9 +103,9 @@ private void brightnessSlider_ValueChanged(object sender, RangeBaseValueChangedE private Action pendingUpdateColorAction; private Task pendingUpdateColor; - private async void SetColor(LifxNet.LightBulb bulb, ushort? hue, ushort? saturation, ushort? brightness) + private async void SetColor(LightBulb bulb, ushort? hue, ushort? saturation, ushort? brightness) { - if (client == null || bulb == null) return; + if (_client == null || bulb == null) return; //Is a task already running? This avoids updating too often. //Come back and execute last call when currently running operation is complete if (pendingUpdateColor != null) @@ -122,13 +117,13 @@ private async void SetColor(LifxNet.LightBulb bulb, ushort? hue, ushort? saturat this.hue = hue.HasValue ? hue.Value : this.hue; this.saturation = saturation.HasValue ? saturation.Value : this.saturation; var b = brightness.HasValue ? brightness.Value : (UInt16)brightnessSlider.Value; - var setColorTask = client.SetColorAsync(bulb, this.hue, this.saturation, b, 2700, TimeSpan.Zero); + var setColorTask = _client.SetColorAsync(bulb, this.hue, this.saturation, b, 2700, TimeSpan.Zero); var throttleTask = Task.Delay(50); //Ensure task takes minimum 50 ms (no more than 20 messages per second) - pendingUpdateColor = Task.WhenAll(new Task[] { setColorTask, throttleTask }); + pendingUpdateColor = Task.WhenAll(setColorTask, throttleTask); try { Task timeoutTask = Task.Delay(2000); - await Task.WhenAny(new Task[] { timeoutTask, pendingUpdateColor }); + await Task.WhenAny(timeoutTask, pendingUpdateColor); if (!pendingUpdateColor.IsCompleted) { //timeout @@ -150,7 +145,7 @@ private void ColorGrid_Tapped(object sender, TappedRoutedEventArgs e) var p = e.GetPosition(elm); var Hue = p.X / elm.ActualWidth * 65535; var Sat = p.Y / elm.ActualHeight * 65535; - var bulb = bulbList.SelectedItem as LifxNet.LightBulb; + var bulb = bulbList.SelectedItem as LightBulb; if (bulb != null) { SetColor(bulb, (ushort)Hue, (ushort)Sat, null); diff --git a/src/SampleApps/SampleApp.Universal/Properties/AssemblyInfo.cs b/src/SampleApps/SampleApp.Universal/Properties/AssemblyInfo.cs index 90e7f60..f077197 100644 --- a/src/SampleApps/SampleApp.Universal/Properties/AssemblyInfo.cs +++ b/src/SampleApps/SampleApp.Universal/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following diff --git a/src/SampleApps/SampleApp.Universal/SampleApp.Universal.csproj b/src/SampleApps/SampleApp.Universal/SampleApp.Universal.csproj index 7d23535..c55fe69 100644 --- a/src/SampleApps/SampleApp.Universal/SampleApp.Universal.csproj +++ b/src/SampleApps/SampleApp.Universal/SampleApp.Universal.csproj @@ -131,7 +131,7 @@ - 5.1.0 + 6.2.12 diff --git a/src/SampleApps/SampleApp.netcore/Program.cs b/src/SampleApps/SampleApp.netcore/Program.cs index 45e4b0a..4aaea08 100644 --- a/src/SampleApps/SampleApp.netcore/Program.cs +++ b/src/SampleApps/SampleApp.netcore/Program.cs @@ -1,38 +1,67 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using LifxNet; +using Newtonsoft.Json; -namespace SampleApp.NET462 +namespace SampleApp.netcore { class Program { - static LifxNet.LifxClient client; - static void Main(string[] args) - { - var task = LifxNet.LifxClient.CreateAsync(); - task.Wait(); - client = task.Result; - client.DeviceDiscovered += Client_DeviceDiscovered; - client.DeviceLost += Client_DeviceLost; - client.StartDeviceDiscovery(); + static LifxClient _client; + static void Main(string[] args) { + _client = LifxClient.CreateAsync().Result; + _client.DeviceDiscovered += ClientDeviceDiscovered; + _client.DeviceLost += ClientDeviceLost; + _client.StartDeviceDiscovery(); Console.ReadKey(); } - private static void Client_DeviceLost(object sender, LifxClient.DeviceDiscoveryEventArgs e) + private static void ClientDeviceLost(object sender, LifxClient.DeviceDiscoveryEventArgs e) { Console.WriteLine("Device lost"); } - private static async void Client_DeviceDiscovered(object sender, LifxClient.DeviceDiscoveryEventArgs e) + private static async void ClientDeviceDiscovered(object sender, LifxClient.DeviceDiscoveryEventArgs e) { Console.WriteLine($"Device {e.Device.MacAddressName} found @ {e.Device.HostName}"); - var version = await client.GetDeviceVersionAsync(e.Device); - //var label = await client.GetDeviceLabelAsync(e.Device); - var state = await client.GetLightStateAsync(e.Device as LightBulb); - Console.WriteLine($"{state.Label}\n\tIs on: {state.IsOn}\n\tHue: {state.Hue}\n\tSaturation: {state.Saturation}\n\tBrightness: {state.Brightness}\n\tTemperature: {state.Kelvin}"); + var version = await _client.GetDeviceVersionAsync(e.Device); + var state = await _client.GetLightStateAsync((e.Device as LightBulb)!); + Console.WriteLine("Version info: " + JsonConvert.SerializeObject(version)); + Console.WriteLine("State info: " + JsonConvert.SerializeObject(state)); + + // Multi-zone devices + if (version.Product == 31 || version.Product == 32 || version.Product == 38) { + Console.WriteLine("Device is multi-zone, enumerating data."); + var extended = false; + // If new Z-LED or Beam, check if FW supports "extended" commands. + if (version.Product == 32 || version.Product == 38) { + if (version.Version >= 1532997580) { + extended = true; + Console.WriteLine("Enabling extended firmware features."); + } + } + + if (extended) { + var zones = await _client.GetExtendedColorZonesAsync(e.Device); + Console.WriteLine("Zones: " + JsonConvert.SerializeObject(zones)); + } else { + // Original device only supports eight zones? + var zones = await _client.GetColorZonesAsync(e.Device, 0, 8); + Console.WriteLine("Zones: " + JsonConvert.SerializeObject(zones)); + } + } + + // Tile + if (version.Product == 55) { + Console.WriteLine("Device is a tile group, enumerating data."); + var chain = await _client.GetDeviceChainAsync(e.Device); + Console.WriteLine("Tile chain: " + JsonConvert.SerializeObject(chain)); + } + // Switch + if (version.Product == 70) { + Console.WriteLine("Device is a switch, enumerating data."); + var switchState = await _client.GetRelayPowerAsync(e.Device, 0); + Console.WriteLine($"Switch State: {switchState.Level}"); + } } } } diff --git a/src/SampleApps/SampleApp.netcore/SampleApp.netcore.csproj b/src/SampleApps/SampleApp.netcore/SampleApp.netcore.csproj index f779fba..15d7581 100644 --- a/src/SampleApps/SampleApp.netcore/SampleApp.netcore.csproj +++ b/src/SampleApps/SampleApp.netcore/SampleApp.netcore.csproj @@ -7,5 +7,9 @@ + + + + \ No newline at end of file