From 50f43e1feda3f9615f7f2771ff33f65f901d7b32 Mon Sep 17 00:00:00 2001 From: d8ahazard Date: Sat, 13 Feb 2021 12:23:35 -0600 Subject: [PATCH 01/37] Update gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) 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 From a7c0ae88a6dfd66a197a0294777293727eba5051 Mon Sep 17 00:00:00 2001 From: d8ahazard Date: Sat, 13 Feb 2021 12:28:02 -0600 Subject: [PATCH 02/37] Refactor "Color" to "LifxColor" to avoid constant confusion with system.drawing.color, add new tricks This bugs me. Also, make LifxColor a class that can handle RGB and HSB values seamlessly, versus having to do weird stuff to convert between. --- src/LifxNet/Color.cs | 23 ---- src/LifxNet/LifxClient.LightOperations.cs | 10 +- src/LifxNet/LifxColor.cs | 138 ++++++++++++++++++++++ src/LifxNet/Utilities.cs | 2 +- 4 files changed, 144 insertions(+), 29 deletions(-) delete mode 100644 src/LifxNet/Color.cs create mode 100644 src/LifxNet/LifxColor.cs 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/LifxClient.LightOperations.cs b/src/LifxNet/LifxClient.LightOperations.cs index 022895e..f2e059a 100644 --- a/src/LifxNet/LifxClient.LightOperations.cs +++ b/src/LifxNet/LifxClient.LightOperations.cs @@ -95,24 +95,24 @@ public async Task GetLightPowerAsync(LightBulb bulb) /// 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, UInt16 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, UInt16 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); } diff --git a/src/LifxNet/LifxColor.cs b/src/LifxNet/LifxColor.cs new file mode 100644 index 0000000..f77a06c --- /dev/null +++ b/src/LifxNet/LifxColor.cs @@ -0,0 +1,138 @@ +using System; +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); + } + + public float Hue { + get => _color.GetHue(); + set => _color = HsbToColor(value, _color.GetSaturation(), _color.GetBrightness()); + } + + public float Saturation { + get => _color.GetSaturation(); + set => _color = HsbToColor(_color.GetHue(), value, _color.GetBrightness()); + } + + public float Brightness { + get => _color.GetBrightness(); + set => _color = HsbToColor(_color.GetHue(), _color.GetSaturation(), value); + } + + public LifxColor(short h, short s, short b, short k) { + _color = HsbToColor(h, s, b); + } + + public LifxColor(int a, int r, int g, int b) { + _color = Color.FromArgb(a, r, g, b); + } + + public LifxColor(int r, int g, int b) { + _color = Color.FromArgb(255, r, g, b); + } + + public LifxColor(Color color) { + _color = color; + } + + + private static Color HsbToColor(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)); + + double r = 0D; + double g = 0D; + double bl = 0D; + + if (Math.Abs(s) < tolerance) + r = g = bl = b; + else { + // the argb wheel consists of 6 sectors. Figure out which sector + // you're in. + double sectorPos = h / 60D; + int sectorNumber = (int) Math.Floor(sectorPos); + // get the fractional part of the sector + double fractionalSector = sectorPos - sectorNumber; + + // calculate values for the three axes of the argb. + double p = b * (1D - s); + double q = b * (1D - s * fractionalSector); + double 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}",CultureInfo.InvariantCulture)))), + Math.Max(0, Math.Min(255, Convert.ToInt32(double.Parse($"{g * 255D:0.00}",CultureInfo.InvariantCulture)))), + Math.Max(0, Math.Min(255, Convert.ToInt32(double.Parse($"{bl * 250D:0.00}", CultureInfo.InvariantCulture))))); + } + } +} \ No newline at end of file diff --git a/src/LifxNet/Utilities.cs b/src/LifxNet/Utilities.cs index bf280e6..5fabac2 100644 --- a/src/LifxNet/Utilities.cs +++ b/src/LifxNet/Utilities.cs @@ -10,7 +10,7 @@ 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 UInt16[] RgbToHsl(LifxColor rgb) { // normalize red, green and blue values double r = (rgb.R / 255.0); From 6295e0838d5d5bbf885f1dbb120db6405f7486e9 Mon Sep 17 00:00:00 2001 From: d8ahazard Date: Sat, 13 Feb 2021 12:29:09 -0600 Subject: [PATCH 03/37] Add state extended color zones message response... First of several new additions - this is for zoned devices. --- src/LifxNet/LifxResponses.cs | 36 ++++++++++++++++++++++++++++++++++++ src/LifxNet/MessageType.cs | 17 +++++++++-------- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/src/LifxNet/LifxResponses.cs b/src/LifxNet/LifxResponses.cs index 5357f78..8c3eeee 100644 --- a/src/LifxNet/LifxResponses.cs +++ b/src/LifxNet/LifxResponses.cs @@ -31,6 +31,8 @@ internal static LifxResponse Create(FrameHeader header, MessageType type, UInt32 return new StateHostFirmwareResponse(header, type, payload, source); case MessageType.DeviceStateService: return new StateServiceResponse(header, type, payload, source); + case MessageType.StateExtendedColorZones: + return new StateExtendedColorZonesResponse(header, type, payload, source); default: return new UnknownResponse(header, type, payload, source); } @@ -56,6 +58,40 @@ internal class AcknowledgementResponse: LifxResponse { internal AcknowledgementResponse(FrameHeader header, MessageType type, byte[] payload, UInt32 source) : base(header, type, payload, source) { } } + + /// + /// Get the list of colors currently being displayed by zones + /// + public class StateExtendedColorZonesResponse : LifxResponse + { + internal StateExtendedColorZonesResponse(FrameHeader header, MessageType type, byte[] payload, UInt32 source) : base(header, type, payload, source) + { + Colors = new List(); + Count = BitConverter.ToUInt16(payload, 0); + Index = BitConverter.ToUInt16(payload, 2); + for (var i = 4; i < payload.Length; i += 4) { + if (i + 3 < payload.Length) continue; + var h = BitConverter.ToInt16(payload, i); + var s = BitConverter.ToInt16(payload, i + 1); + var b = BitConverter.ToInt16(payload, i + 2); + var k = BitConverter.ToInt16(payload, i + 3); + Colors.Add(new LifxColor(h,s,b,k)); + } + } + /// + /// Count - total number of zones on the device + /// + public UInt16 Count { get; private set; } + /// + /// Index - Zone the message starts from + /// + public UInt16 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. diff --git a/src/LifxNet/MessageType.cs b/src/LifxNet/MessageType.cs index fb34d9b..5700869 100644 --- a/src/LifxNet/MessageType.cs +++ b/src/LifxNet/MessageType.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace LifxNet +namespace LifxNet { internal enum MessageType : ushort { @@ -54,7 +48,14 @@ internal enum MessageType : ushort InfraredGet = 120, InfraredState = 121, InfraredSet = 122, - + SetColorZones = 501, + GetColorZones = 502, + StateZone = 503, + StateMultiZone = 506, + SetExtendedColorZones = 510, + GetExtendedColorZones = 511, + StateExtendedColorZones = 512, + //Unofficial LightGetTemperature = 0x6E, //LightStateTemperature = 0x6f, From 820be00afd8cd6d867894246296e58c3108ea525 Mon Sep 17 00:00:00 2001 From: d8ahazard Date: Sat, 13 Feb 2021 12:30:43 -0600 Subject: [PATCH 04/37] Add multizone partial class Start adding necessary methods to control multi-zone devices. --- src/LifxNet/LifxClient.MultizoneOperations.cs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/LifxNet/LifxClient.MultizoneOperations.cs diff --git a/src/LifxNet/LifxClient.MultizoneOperations.cs b/src/LifxNet/LifxClient.MultizoneOperations.cs new file mode 100644 index 0000000..cf4f567 --- /dev/null +++ b/src/LifxNet/LifxClient.MultizoneOperations.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; + +namespace LifxNet { + public partial class LifxClient : IDisposable { + + /// + /// 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 + /// + /// Thrown if the bulb is null + /// Thrown if the duration is longer than the max + /// + public async Task SetExtendedColorZoneAsync(LightBulb bulb, TimeSpan transitionDuration, int index, List colors) { + if (bulb == null) + throw new ArgumentNullException(nameof(bulb)); + if (transitionDuration.TotalMilliseconds > UInt32.MaxValue || + transitionDuration.Ticks < 0) {throw new ArgumentOutOfRangeException(nameof(transitionDuration));} + + FrameHeader header = new FrameHeader { + Identifier = GetNextIdentifier(), + AcknowledgeRequired = true + }; + UInt32 duration = (UInt32)transitionDuration.TotalMilliseconds; + var cArgs = new List(); + foreach (var color in colors) { + var hsl = Utilities.RgbToHsl(color); + cArgs.Add(hsl[0]); + cArgs.Add(hsl[1]); + cArgs.Add(hsl[2]); + cArgs.Add(2700); + } + + await BroadcastMessageAsync(bulb.HostName, header, + MessageType.SetExtendedColorZones, duration, (byte) 0x01, index, colors.Count, cArgs); + } + + + + } +} \ No newline at end of file From 279c3ff101522fab0b66280ff180c94ade255a9a Mon Sep 17 00:00:00 2001 From: d8ahazard Date: Sat, 13 Feb 2021 12:31:36 -0600 Subject: [PATCH 05/37] Code cleanup! Generic linting. Fix names, privacy declarations, other misc stuff. --- src/LifxNet/LifxClient.DeviceOperations.cs | 25 +-- src/LifxNet/LifxClient.Discovery.cs | 152 +++++++++--------- src/LifxNet/LifxClient.LightOperations.cs | 50 +++--- src/LifxNet/LifxClient.cs | 53 +++--- src/LifxNet/LifxNet.csproj | 6 + src/LifxNet/LifxPacket.cs | 43 +++-- src/LifxNet/LifxResponses.cs | 19 +-- src/LifxNet/Utilities.cs | 6 +- .../SampleApp.Universal/App.xaml.cs | 19 +-- .../SampleApp.Universal/MainPage.xaml.cs | 54 +++---- .../Properties/AssemblyInfo.cs | 1 - src/SampleApps/SampleApp.netcore/Program.cs | 16 +- 12 files changed, 194 insertions(+), 250 deletions(-) diff --git a/src/LifxNet/LifxClient.DeviceOperations.cs b/src/LifxNet/LifxClient.DeviceOperations.cs index 4fe5b0b..ca1b5da 100644 --- a/src/LifxNet/LifxClient.DeviceOperations.cs +++ b/src/LifxNet/LifxClient.DeviceOperations.cs @@ -1,14 +1,10 @@ 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 - { + public partial class LifxClient { /// /// Turns the device on /// @@ -31,9 +27,8 @@ 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() - { + Debug.WriteLine($"Sending DeviceSetPower({isOn}) to {device.HostName}"); + FrameHeader header = new FrameHeader { Identifier = GetNextIdentifier(), AcknowledgeRequired = true }; @@ -52,8 +47,7 @@ public async Task SetDevicePowerStateAsync(Device device, bool isOn) if (device == null) throw new ArgumentNullException(nameof(device)); - FrameHeader header = new FrameHeader() - { + FrameHeader header = new FrameHeader { Identifier = GetNextIdentifier(), AcknowledgeRequired = false }; @@ -72,8 +66,7 @@ public async Task SetDeviceLabelAsync(Device device, string label) if (device == null) throw new ArgumentNullException(nameof(device)); - FrameHeader header = new FrameHeader() - { + FrameHeader header = new FrameHeader { Identifier = GetNextIdentifier(), AcknowledgeRequired = true }; @@ -89,8 +82,7 @@ public Task GetDeviceVersionAsync(Device device) if (device == null) throw new ArgumentNullException(nameof(device)); - FrameHeader header = new FrameHeader() - { + FrameHeader header = new FrameHeader { Identifier = GetNextIdentifier(), AcknowledgeRequired = false }; @@ -106,8 +98,7 @@ public Task GetDeviceHostFirmwareAsync(Device device) if (device == null) throw new ArgumentNullException(nameof(device)); - FrameHeader header = new FrameHeader() - { + FrameHeader header = new FrameHeader { Identifier = GetNextIdentifier(), AcknowledgeRequired = false }; diff --git a/src/LifxNet/LifxClient.Discovery.cs b/src/LifxNet/LifxClient.Discovery.cs index 3f5b402..a067b73 100644 --- a/src/LifxNet/LifxClient.Discovery.cs +++ b/src/LifxNet/LifxClient.Discovery.cs @@ -1,35 +1,34 @@ 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(); + 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 static uint GetNextIdentifier() { - lock (identifierLock) - return identifier++; + lock (IdentifierLock) + return _identifier++; } /// /// Event fired when a LIFX bulb is discovered on the network /// - public event EventHandler? DeviceDiscovered; + public event EventHandler? Discovered; /// /// 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; + public event EventHandler? Lost; private IList devices = new List(); @@ -39,30 +38,30 @@ private static uint GetNextIdentifier() public IEnumerable Devices { get { return devices; } } /// - /// Event args for and events. + /// Event args for and events. /// - public sealed class DeviceDiscoveryEventArgs : EventArgs + public sealed class DiscoveryEventArgs : EventArgs { - internal DeviceDiscoveryEventArgs(Device device) => Device = device; + internal DiscoveryEventArgs(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) + private void ProcessDeviceDiscoveryMessage(IPAddress remoteAddress, 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 + 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; + 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] @@ -70,33 +69,29 @@ private void ProcessDeviceDiscoveryMessage(System.Net.IPAddress remoteAddress, i { LastSeen = DateTime.UtcNow }; - DiscoveredBulbs[id] = device; + _discoveredBulbs[id] = device; devices.Add(device); - if (DeviceDiscovered != null) - { - DeviceDiscovered(this, new DeviceDiscoveryEventArgs(device)); - } + Discovered?.Invoke(this, new DiscoveryEventArgs(device)); } /// /// Begins searching for bulbs. /// - /// - /// + /// + /// /// public void StartDeviceDiscovery() { - if (_DiscoverCancellationSource != null && !_DiscoverCancellationSource.IsCancellationRequested) + if (_discoverCancellationSource != null && !_discoverCancellationSource.IsCancellationRequested) return; - _DiscoverCancellationSource = new CancellationTokenSource(); - var token = _DiscoverCancellationSource.Token; - var source = discoverSourceID = GetNextIdentifier(); + _discoverCancellationSource = new CancellationTokenSource(); + var token = _discoverCancellationSource.Token; + var source = _discoverSourceId = GetNextIdentifier(); //Start discovery thread - Task.Run(async () => + Task.Run(async () => { - System.Diagnostics.Debug.WriteLine("Sending GetServices"); - FrameHeader header = new FrameHeader() - { + Debug.WriteLine("Sending GetServices"); + FrameHeader header = new FrameHeader { Identifier = source }; while (!token.IsCancellationRequested) @@ -104,22 +99,24 @@ public void StartDeviceDiscovery() try { await BroadcastMessageAsync(null, header, MessageType.DeviceGetService); + } catch { + // ignored } - catch { } - await Task.Delay(5000); + + await Task.Delay(5000, token); var lostDevices = devices.Where(d => (DateTime.UtcNow - d.LastSeen).TotalMinutes > 5).ToArray(); - if(lostDevices.Any()) + if (!lostDevices.Any()) { + continue; + } + + foreach(var device in lostDevices) { - foreach(var device in lostDevices) - { - devices.Remove(device); - DiscoveredBulbs.Remove(device.MacAddressName); - if (DeviceLost != null) - DeviceLost(this, new DeviceDiscoveryEventArgs(device)); - } + devices.Remove(device); + _discoveredBulbs.Remove(device.MacAddressName); + Lost?.Invoke(this, new DiscoveryEventArgs(device)); } } - }); + }, token); } /// @@ -128,10 +125,10 @@ public void StartDeviceDiscovery() /// public void StopDeviceDiscovery() { - if (_DiscoverCancellationSource == null || _DiscoverCancellationSource.IsCancellationRequested) + if (_discoverCancellationSource == null || _discoverCancellationSource.IsCancellationRequested) return; - _DiscoverCancellationSource.Cancel(); - _DiscoverCancellationSource = null; + _discoverCancellationSource.Cancel(); + _discoverCancellationSource = null; } } @@ -170,27 +167,26 @@ internal Device(string hostname, byte[] macAddress, byte service, UInt32 port) 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 + /// + /// 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()); + } + } + } + /// + /// 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. @@ -202,5 +198,5 @@ public sealed class LightBulb : Device 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 f2e059a..1629a69 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>(); + public partial class LifxClient { + private readonly Dictionary> _taskCompletions = new Dictionary>(); /// /// Turns a bulb on using the provided transition time @@ -52,20 +49,19 @@ public partial class LifxClient : IDisposable public async Task SetLightPowerAsync(LightBulb bulb, TimeSpan transitionDuration, bool isOn) { if (bulb == null) - throw new ArgumentNullException("bulb"); + throw new ArgumentNullException(nameof(bulb)); if (transitionDuration.TotalMilliseconds > UInt32.MaxValue || transitionDuration.Ticks < 0) - throw new ArgumentOutOfRangeException("transitionDuration"); + throw new ArgumentOutOfRangeException(nameof(transitionDuration)); - FrameHeader header = new FrameHeader() - { + FrameHeader header = new FrameHeader { Identifier = GetNextIdentifier(), AcknowledgeRequired = true }; var b = BitConverter.GetBytes((UInt16)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 @@ -82,8 +78,7 @@ public async Task GetLightPowerAsync(LightBulb bulb) if (bulb == null) throw new ArgumentNullException(nameof(bulb)); - FrameHeader header = new FrameHeader() - { + FrameHeader header = new FrameHeader { Identifier = GetNextIdentifier(), AcknowledgeRequired = true }; @@ -143,19 +138,13 @@ public async Task SetColorAsync(LightBulb bulb, 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() - { + 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); - + var duration = (UInt32)transitionDuration.TotalMilliseconds; + await BroadcastMessageAsync(bulb.HostName, header, MessageType.LightSetColor, (byte)0x00, //reserved hue, saturation, brightness, kelvin, //HSBK @@ -195,8 +184,7 @@ public Task GetLightStateAsync(LightBulb bulb) { if (bulb == null) throw new ArgumentNullException(nameof(bulb)); - FrameHeader header = new FrameHeader() - { + FrameHeader header = new FrameHeader { Identifier = GetNextIdentifier(), AcknowledgeRequired = false }; @@ -215,12 +203,11 @@ public async Task GetInfraredAsync(LightBulb bulb) if (bulb == null) throw new ArgumentNullException(nameof(bulb)); - FrameHeader header = new FrameHeader() - { + FrameHeader header = new FrameHeader { Identifier = GetNextIdentifier(), AcknowledgeRequired = true }; - return (await BroadcastMessageAsync( + return (await BroadcastMessageAsync( bulb.HostName, header, MessageType.InfraredGet).ConfigureAwait(false)).Brightness; } @@ -234,15 +221,14 @@ public async Task SetInfraredAsync(Device device, UInt16 brightness) { if (device == null) throw new ArgumentNullException(nameof(device)); - System.Diagnostics.Debug.WriteLine($"Sending SetInfrared({brightness}) to {device.HostName}"); - FrameHeader header = new FrameHeader() - { + 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); + MessageType.InfraredSet, brightness).ConfigureAwait(false); } } } diff --git a/src/LifxNet/LifxClient.cs b/src/LifxNet/LifxClient.cs index 6ed088e..62669e2 100644 --- a/src/LifxNet/LifxClient.cs +++ b/src/LifxNet/LifxClient.cs @@ -1,11 +1,12 @@ using System; using System.Collections.Generic; -using System.Linq; +using System.Diagnostics; using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; using System.Text; using System.Threading.Tasks; -using System.Net.Sockets; -using System.Net; namespace LifxNet { @@ -62,27 +63,23 @@ private void StartReceiveLoop() }); } - private void HandleIncomingMessages(byte[] data, System.Net.IPEndPoint endpoint) + 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 - } + 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(), + Debug.WriteLine("Received from {0}:{1}", remote, string.Join(",", (from a in data select a.ToString("X2")).ToArray())); } @@ -143,12 +140,12 @@ private async Task BroadcastMessagePayloadAsync(string? hostName, FrameHea typeof(T) != typeof(UnknownResponse)) { tcs = new TaskCompletionSource(); - Action action = (r) => + Action action = r => { if (r.GetType() == typeof(T)) tcs.TrySetResult((T)r); }; - taskCompletions[header.Identifier] = action; + _taskCompletions[header.Identifier] = action; } using (MemoryStream stream = new MemoryStream()) @@ -160,10 +157,10 @@ private async Task BroadcastMessagePayloadAsync(string? hostName, FrameHea //{ // await WritePacketToStreamAsync(stream, header, (UInt16)type, payload).ConfigureAwait(false); //} - T result = default(T); + T result = default; if(tcs != null) { - var _ = Task.Delay(1000).ContinueWith((t) => + var _ = Task.Delay(1000).ContinueWith(t => { if (!t.IsCompleted) tcs.TrySetException(new TimeoutException()); @@ -173,7 +170,7 @@ private async Task BroadcastMessagePayloadAsync(string? hostName, FrameHea } finally { - taskCompletions.Remove(header.Identifier); + _taskCompletions.Remove(header.Identifier); } } return result; @@ -208,7 +205,7 @@ private LifxResponse ParseMessage(byte[] packet) private void WritePacketToStream(Stream outStream, FrameHeader header, UInt16 type, byte[] payload) { - using (var dw = new BinaryWriter(outStream) { /*ByteOrder = ByteOrder.LittleEndian*/ }) + using (var dw = new BinaryWriter(outStream)) { //BinaryWriter bw = new BinaryWriter(ms); #region Frame @@ -216,7 +213,7 @@ private void WritePacketToStream(Stream outStream, FrameHeader header, UInt16 ty 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 + dw.Write(header.Identifier); //source identifier - unique value set by the client, used by responses. If 0, responses are broadcasted instead #endregion Frame #region Frame address @@ -243,7 +240,7 @@ private void WritePacketToStream(Stream outStream, FrameHeader header, UInt16 ty //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); + dw.Write(header.Sequence); #endregion Frame address #region Protocol Header diff --git a/src/LifxNet/LifxNet.csproj b/src/LifxNet/LifxNet.csproj index 1da0140..8cd79cd 100644 --- a/src/LifxNet/LifxNet.csproj +++ b/src/LifxNet/LifxNet.csproj @@ -25,4 +25,10 @@ + + + C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\3.1.0\ref\netcoreapp3.1\System.Drawing.Primitives.dll + + + \ No newline at end of file diff --git a/src/LifxNet/LifxPacket.cs b/src/LifxNet/LifxPacket.cs index 176fe9d..76b1619 100644 --- a/src/LifxNet/LifxPacket.cs +++ b/src/LifxNet/LifxPacket.cs @@ -1,9 +1,5 @@ using System; -using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace LifxNet { @@ -22,28 +18,27 @@ protected LifxPacket(ushort type, byte[] payload) 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) @@ -73,7 +68,7 @@ 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[] { }; + byte[] payload = { }; if (len > 0) { payload = br.ReadBytes(len); diff --git a/src/LifxNet/LifxResponses.cs b/src/LifxNet/LifxResponses.cs index 8c3eeee..2d7b7f5 100644 --- a/src/LifxNet/LifxResponses.cs +++ b/src/LifxNet/LifxResponses.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Text; -using System.Threading.Tasks; namespace LifxNet { @@ -24,7 +22,7 @@ internal static LifxResponse Create(FrameHeader header, MessageType type, UInt32 case MessageType.LightStatePower: return new LightPowerResponse(header, type, payload, source); case MessageType.InfraredState: - return new InfraredStateRespone(header, type, payload, source); + return new InfraredStateResponse(header, type, payload, source); case MessageType.DeviceStateVersion: return new StateVersionResponse(header, type, payload, source); case MessageType.DeviceStateHostFirmware: @@ -104,18 +102,17 @@ internal StateServiceResponse(FrameHeader header, MessageType type, byte[] paylo Service = payload[0]; Port = BitConverter.ToUInt32(payload, 1); } - public Byte Service { get; } - public UInt32 Port { get; } + + private Byte Service { get; } + private UInt32 Port { 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 StateLabelResponse(FrameHeader header, MessageType type, byte[] payload, UInt32 source) : base(header, type, payload, source) { + Label = Encoding.UTF8.GetString(payload, 0, payload.Length).Replace("\0", ""); } public string? Label { get; private set; } } @@ -167,9 +164,9 @@ internal LightPowerResponse(FrameHeader header, MessageType type, byte[] payload public bool IsOn { get; private set; } } - internal class InfraredStateRespone : LifxResponse + internal class InfraredStateResponse : LifxResponse { - internal InfraredStateRespone(FrameHeader header, MessageType type, byte[] payload, UInt32 source) : base(header, type, payload, source) + internal InfraredStateResponse(FrameHeader header, MessageType type, byte[] payload, UInt32 source) : base(header, type, payload, source) { Brightness = BitConverter.ToUInt16(payload, 0); } diff --git a/src/LifxNet/Utilities.cs b/src/LifxNet/Utilities.cs index 5fabac2..aa35394 100644 --- a/src/LifxNet/Utilities.cs +++ b/src/LifxNet/Utilities.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace LifxNet { @@ -39,7 +35,7 @@ public static UInt16[] RgbToHsl(LifxColor rgb) } double s = (max == 0) ? 0.0 : (1.0 - (min / max)); - return new UInt16[] { + return new[] { (UInt16)(h / 360 * 65535), (UInt16)(s * 65535), (UInt16)(max * 65535) diff --git a/src/SampleApps/SampleApp.Universal/App.xaml.cs b/src/SampleApps/SampleApp.Universal/App.xaml.cs index cf63249..c46e5fc 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 @@ -28,8 +19,8 @@ sealed partial class App : Application /// public App() { - this.InitializeComponent(); - this.Suspending += OnSuspending; + InitializeComponent(); + Suspending += OnSuspending; } /// @@ -40,9 +31,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..6638749 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,52 +19,52 @@ 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 = await LifxClient.CreateAsync(); + client.Discovered += Client_DeviceDiscovered; + client.Lost += Client_DeviceLost; client.StartDeviceDiscovery(); } protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) { - client.DeviceDiscovered -= Client_DeviceDiscovered; - client.DeviceLost -= Client_DeviceLost; + client.Discovered -= Client_DeviceDiscovered; + client.Lost -= Client_DeviceLost; client.StopDeviceDiscovery(); client = null; base.OnNavigatingFrom(e); } - private void Client_DeviceLost(object sender, LifxNet.LifxClient.DeviceDiscoveryEventArgs e) + private void Client_DeviceLost(object sender, LifxClient.DiscoveryEventArgs 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 Client_DeviceDiscovered(object sender, LifxClient.DiscoveryEventArgs 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); @@ -91,7 +85,7 @@ 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); @@ -100,7 +94,7 @@ private async void PowerState_Toggled(object sender, RoutedEventArgs e) 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,7 +102,7 @@ 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; //Is a task already running? This avoids updating too often. @@ -124,11 +118,11 @@ private async void SetColor(LifxNet.LightBulb bulb, ushort? hue, ushort? saturat var b = brightness.HasValue ? brightness.Value : (UInt16)brightnessSlider.Value; 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 +144,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.netcore/Program.cs b/src/SampleApps/SampleApp.netcore/Program.cs index 45e4b0a..9bf5161 100644 --- a/src/SampleApps/SampleApp.netcore/Program.cs +++ b/src/SampleApps/SampleApp.netcore/Program.cs @@ -1,32 +1,28 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using LifxNet; namespace SampleApp.NET462 { class Program { - static LifxNet.LifxClient client; + static LifxClient client; static void Main(string[] args) { - var task = LifxNet.LifxClient.CreateAsync(); + var task = LifxClient.CreateAsync(); task.Wait(); client = task.Result; - client.DeviceDiscovered += Client_DeviceDiscovered; - client.DeviceLost += Client_DeviceLost; + client.Discovered += Client_DeviceDiscovered; + client.Lost += Client_DeviceLost; client.StartDeviceDiscovery(); Console.ReadKey(); } - private static void Client_DeviceLost(object sender, LifxClient.DeviceDiscoveryEventArgs e) + private static void Client_DeviceLost(object sender, LifxClient.DiscoveryEventArgs e) { Console.WriteLine("Device lost"); } - private static async void Client_DeviceDiscovered(object sender, LifxClient.DeviceDiscoveryEventArgs e) + private static async void Client_DeviceDiscovered(object sender, LifxClient.DiscoveryEventArgs e) { Console.WriteLine($"Device {e.Device.MacAddressName} found @ {e.Device.HostName}"); var version = await client.GetDeviceVersionAsync(e.Device); From dafc945c5620529d2ee783e8686efc4ccf1669eb Mon Sep 17 00:00:00 2001 From: d8ahazard Date: Sun, 14 Feb 2021 10:06:22 -0600 Subject: [PATCH 06/37] Add remainder of multi-zone messages Add legacy zone setters/getters, response messages. --- src/LifxNet/LifxClient.MultizoneOperations.cs | 68 +++++++++++++++++- src/LifxNet/LifxResponses.cs | 71 +++++++++++++++++++ 2 files changed, 136 insertions(+), 3 deletions(-) diff --git a/src/LifxNet/LifxClient.MultizoneOperations.cs b/src/LifxNet/LifxClient.MultizoneOperations.cs index cf4f567..656d503 100644 --- a/src/LifxNet/LifxClient.MultizoneOperations.cs +++ b/src/LifxNet/LifxClient.MultizoneOperations.cs @@ -5,6 +5,34 @@ namespace LifxNet { public partial class LifxClient : IDisposable { + + /// + /// This message is used for changing the color of either a single or multiple zones. + /// + /// Target bulb + /// Start index to target + /// End index to target + /// LifxColor to use + /// How long to fade + /// + /// + /// + public async Task SetColorZones(LightBulb bulb, int startIndex, int endIndex, LifxColor Color, + TimeSpan transitionDuration) { + if (bulb == null) + throw new ArgumentNullException(nameof(bulb)); + if (transitionDuration.TotalMilliseconds > UInt32.MaxValue || + transitionDuration.Ticks < 0) {throw new ArgumentOutOfRangeException(nameof(transitionDuration));} + if (startIndex > endIndex) throw new ArgumentOutOfRangeException(nameof(startIndex)); + + FrameHeader header = new FrameHeader { + Identifier = GetNextIdentifier(), + AcknowledgeRequired = true + }; + UInt32 duration = (UInt32)transitionDuration.TotalMilliseconds; + await BroadcastMessageAsync(bulb.HostName, header, + MessageType.SetColorZones, startIndex, endIndex, Color, duration, 0x01); + } /// /// Set a zone of colors @@ -18,7 +46,7 @@ public partial class LifxClient : IDisposable { /// Thrown if the bulb is null /// Thrown if the duration is longer than the max /// - public async Task SetExtendedColorZoneAsync(LightBulb bulb, TimeSpan transitionDuration, int index, List colors) { + public async Task SetExtendedColorZonesAsync(LightBulb bulb, TimeSpan transitionDuration, int index, List colors) { if (bulb == null) throw new ArgumentNullException(nameof(bulb)); if (transitionDuration.TotalMilliseconds > UInt32.MaxValue || @@ -28,8 +56,8 @@ public async Task SetExtendedColorZoneAsync(LightBulb bulb, TimeSpan transitionD Identifier = GetNextIdentifier(), AcknowledgeRequired = true }; - UInt32 duration = (UInt32)transitionDuration.TotalMilliseconds; var cArgs = new List(); + uint duration = (uint)transitionDuration.TotalMilliseconds; foreach (var color in colors) { var hsl = Utilities.RgbToHsl(color); cArgs.Add(hsl[0]); @@ -41,8 +69,42 @@ public async Task SetExtendedColorZoneAsync(LightBulb bulb, TimeSpan transitionD await BroadcastMessageAsync(bulb.HostName, header, MessageType.SetExtendedColorZones, duration, (byte) 0x01, index, colors.Count, cArgs); } - + /// + /// Try to get the color zones from our device. + /// + /// + /// + /// + public Task GetExtendedColorZonesAsync(LightBulb bulb) + { + if (bulb == null) + throw new ArgumentNullException(nameof(bulb)); + FrameHeader header = new FrameHeader { + Identifier = GetNextIdentifier(), + AcknowledgeRequired = false + }; + return BroadcastMessageAsync( + bulb.HostName, header, MessageType.GetExtendedColorZones); + } + + /// + /// Try to get the color zones from our device, non-extended. + /// + /// Target bulb + /// Either a "StateZone" response for single-zone devices, or "StateMultiZone" response. + /// + public Task GetColorZonesAsync(LightBulb bulb) + { + if (bulb == null) + throw new ArgumentNullException(nameof(bulb)); + FrameHeader header = new FrameHeader { + Identifier = GetNextIdentifier(), + AcknowledgeRequired = false + }; + return BroadcastMessageAsync( + bulb.HostName, header, MessageType.GetColorZones); + } } } \ No newline at end of file diff --git a/src/LifxNet/LifxResponses.cs b/src/LifxNet/LifxResponses.cs index 2d7b7f5..ffaf539 100644 --- a/src/LifxNet/LifxResponses.cs +++ b/src/LifxNet/LifxResponses.cs @@ -31,6 +31,10 @@ internal static LifxResponse Create(FrameHeader header, MessageType type, UInt32 return new StateServiceResponse(header, type, payload, source); case MessageType.StateExtendedColorZones: return new StateExtendedColorZonesResponse(header, type, payload, source); + case MessageType.StateZone: + return new StateZoneResponse(header, type, payload, source); + case MessageType.StateMultiZone: + return new StateMultiZoneResponse(header, type, payload, source); default: return new UnknownResponse(header, type, payload, source); } @@ -57,6 +61,73 @@ internal class AcknowledgementResponse: LifxResponse internal AcknowledgementResponse(FrameHeader header, MessageType type, byte[] payload, UInt32 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, byte[] payload, UInt32 source) : base(header, type, payload, source) + { + Count = BitConverter.ToUInt16(payload, 0); + Index = BitConverter.ToUInt16(payload, 2); + var h = BitConverter.ToInt16(payload, 4); + var s = BitConverter.ToInt16(payload, 6); + var b = BitConverter.ToInt16(payload, 8); + var k = BitConverter.ToInt16(payload, 10); + Color = new LifxColor(h, s, b, k); + + } + /// + /// Count - total number of zones on the device + /// + public UInt16 Count { get; private set; } + /// + /// Index - Zone the message starts from + /// + public UInt16 Index { get; private set; } + /// + /// The list of colors returned by the message + /// + public LifxColor Color { get; private set; } + + } + + + /// + /// Get the list of colors currently being displayed by zones + /// + public class StateMultiZoneResponse : LifxResponse + { + internal StateMultiZoneResponse(FrameHeader header, MessageType type, byte[] payload, UInt32 source) : base(header, type, payload, source) + { + Colors = new List(); + Count = BitConverter.ToUInt16(payload, 0); + Index = BitConverter.ToUInt16(payload, 2); + for (var i = 4; i < payload.Length; i += 4) { + if (i + 3 < payload.Length) continue; + var h = BitConverter.ToInt16(payload, i); + var s = BitConverter.ToInt16(payload, i + 1); + var b = BitConverter.ToInt16(payload, i + 2); + var k = BitConverter.ToInt16(payload, i + 3); + Colors.Add(new LifxColor(h,s,b,k)); + } + } + /// + /// Count - total number of zones on the device + /// + public UInt16 Count { get; private set; } + /// + /// Index - Zone the message starts from + /// + public UInt16 Index { get; private set; } + /// + /// The list of colors returned by the message + /// + public List Colors { get; private set; } + + } + + /// /// Get the list of colors currently being displayed by zones /// From 1e46c28c4ddb6bc5db6e344484f70574298e9e5c Mon Sep 17 00:00:00 2001 From: d8ahazard Date: Sun, 14 Feb 2021 10:07:02 -0600 Subject: [PATCH 07/37] Add "ToBytes()" for LifxColor Now we can just serialize any color directly to a bytewise HSBK value. --- src/LifxNet/LifxClient.MultizoneOperations.cs | 8 ++------ src/LifxNet/LifxColor.cs | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/LifxNet/LifxClient.MultizoneOperations.cs b/src/LifxNet/LifxClient.MultizoneOperations.cs index 656d503..c3018bd 100644 --- a/src/LifxNet/LifxClient.MultizoneOperations.cs +++ b/src/LifxNet/LifxClient.MultizoneOperations.cs @@ -56,14 +56,10 @@ public async Task SetExtendedColorZonesAsync(LightBulb bulb, TimeSpan transition Identifier = GetNextIdentifier(), AcknowledgeRequired = true }; - var cArgs = new List(); uint duration = (uint)transitionDuration.TotalMilliseconds; + var cArgs = new List(); foreach (var color in colors) { - var hsl = Utilities.RgbToHsl(color); - cArgs.Add(hsl[0]); - cArgs.Add(hsl[1]); - cArgs.Add(hsl[2]); - cArgs.Add(2700); + cArgs.AddRange(color.ToBytes()); } await BroadcastMessageAsync(bulb.HostName, header, diff --git a/src/LifxNet/LifxColor.cs b/src/LifxNet/LifxColor.cs index f77a06c..543dd60 100644 --- a/src/LifxNet/LifxColor.cs +++ b/src/LifxNet/LifxColor.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Drawing; using System.Globalization; @@ -66,6 +67,19 @@ public LifxColor(Color color) { _color = color; } + /// + /// Serialize our color to a byte array + /// + /// HSBK formatted array of bytes. I think + public byte[] ToBytes() { + var output = new List(); + output.AddRange(BitConverter.GetBytes(Hue)); + output.AddRange(BitConverter.GetBytes(Saturation)); + output.AddRange(BitConverter.GetBytes(Brightness)); + output.AddRange(BitConverter.GetBytes(2700)); + return output.ToArray(); + } + private static Color HsbToColor(double h, double s, double b, int a = 255) { h = Math.Max(0D, Math.Min(360D, h)); From 01ded4036ea5795914a5f5b96067d7c3d10188bb Mon Sep 17 00:00:00 2001 From: d8ahazard Date: Sun, 14 Feb 2021 10:09:02 -0600 Subject: [PATCH 08/37] Allow direct serialization of Colors to payload using LifxColor.ToBytes() Convert else/if to switch, add option to serialize LifxColor directly, instead of having to do it before calling our broadcast method. --- src/LifxNet/LifxClient.cs | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/src/LifxNet/LifxClient.cs b/src/LifxNet/LifxClient.cs index 62669e2..cdad855 100644 --- a/src/LifxNet/LifxClient.cs +++ b/src/LifxNet/LifxClient.cs @@ -98,21 +98,27 @@ private Task BroadcastMessageAsync(string? hostName, FrameHeader header, M { 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 + foreach (var arg in args) { + switch (arg) { + case ushort @ushort: + payload.AddRange(BitConverter.GetBytes(@ushort)); + break; + case uint u: + payload.AddRange(BitConverter.GetBytes(u)); + break; + case byte b: + payload.Add(b); + break; + case byte[] bytes: + payload.AddRange(bytes); + break; + case string s: + payload.AddRange(Encoding.UTF8.GetBytes(s.PadRight(32).Take(32).ToArray())); //All strings are 32 bytes + break; + case LifxColor c: + payload.AddRange(c.ToBytes()); + break; + default: throw new NotSupportedException(args.GetType().FullName); } } From 2d26ba5090774ccb4ed63577ebd5d1cea6127ec7 Mon Sep 17 00:00:00 2001 From: d8ahazard Date: Sun, 14 Feb 2021 12:29:20 -0600 Subject: [PATCH 09/37] Type adjustments, code cleanup Use proper variable types everywhere as specified by Lifx docs. Code cleanup --- src/LifxNet/LifxClient.DeviceOperations.cs | 29 +- src/LifxNet/LifxClient.Discovery.cs | 68 ++-- src/LifxNet/LifxClient.LightOperations.cs | 74 ++-- src/LifxNet/LifxClient.MultizoneOperations.cs | 53 +-- src/LifxNet/LifxClient.cs | 355 +++++++++--------- src/LifxNet/LifxColor.cs | 335 ++++++++++------- src/LifxNet/LifxPacket.cs | 42 ++- src/LifxNet/LifxResponses.cs | 149 ++++---- src/LifxNet/MessageType.cs | 15 +- src/LifxNet/Utilities.cs | 36 +- 10 files changed, 588 insertions(+), 568 deletions(-) diff --git a/src/LifxNet/LifxClient.DeviceOperations.cs b/src/LifxNet/LifxClient.DeviceOperations.cs index ca1b5da..4014694 100644 --- a/src/LifxNet/LifxClient.DeviceOperations.cs +++ b/src/LifxNet/LifxClient.DeviceOperations.cs @@ -2,8 +2,7 @@ using System.Diagnostics; using System.Threading.Tasks; -namespace LifxNet -{ +namespace LifxNet { public partial class LifxClient { /// /// Turns the device on @@ -23,8 +22,7 @@ public partial class LifxClient { /// /// /// - public async Task SetDevicePowerStateAsync(Device device, bool isOn) - { + public async Task SetDevicePowerStateAsync(Device device, bool isOn) { if (device == null) throw new ArgumentNullException(nameof(device)); Debug.WriteLine($"Sending DeviceSetPower({isOn}) to {device.HostName}"); @@ -34,7 +32,7 @@ public async Task SetDevicePowerStateAsync(Device device, bool isOn) }; _ = await BroadcastMessageAsync(device.HostName, header, - MessageType.DeviceSetPower, (UInt16)(isOn ? 65535 : 0)).ConfigureAwait(false); + MessageType.DeviceSetPower, (UInt16) (isOn ? 65535 : 0)).ConfigureAwait(false); } /// @@ -42,8 +40,7 @@ public async Task SetDevicePowerStateAsync(Device device, bool isOn) /// /// /// - public async Task GetDeviceLabelAsync(Device device) - { + public async Task GetDeviceLabelAsync(Device device) { if (device == null) throw new ArgumentNullException(nameof(device)); @@ -51,7 +48,8 @@ public async Task SetDevicePowerStateAsync(Device device, bool isOn) Identifier = GetNextIdentifier(), AcknowledgeRequired = false }; - var resp = await BroadcastMessageAsync(device.HostName, header, MessageType.DeviceGetLabel).ConfigureAwait(false); + var resp = await BroadcastMessageAsync(device.HostName, header, + MessageType.DeviceGetLabel).ConfigureAwait(false); return resp.Label; } @@ -61,8 +59,7 @@ 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)); @@ -77,8 +74,7 @@ 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)); @@ -88,13 +84,13 @@ public Task GetDeviceVersionAsync(Device device) }; return BroadcastMessageAsync(device.HostName, header, MessageType.DeviceGetVersion); } + /// /// Gets the device's host firmware /// /// /// - public Task GetDeviceHostFirmwareAsync(Device device) - { + public Task GetDeviceHostFirmwareAsync(Device device) { if (device == null) throw new ArgumentNullException(nameof(device)); @@ -102,7 +98,8 @@ public Task GetDeviceHostFirmwareAsync(Device device) Identifier = GetNextIdentifier(), AcknowledgeRequired = false }; - return BroadcastMessageAsync(device.HostName, header, MessageType.DeviceGetHostFirmware); + return BroadcastMessageAsync(device.HostName, header, + MessageType.DeviceGetHostFirmware); } } -} +} \ No newline at end of file diff --git a/src/LifxNet/LifxClient.Discovery.cs b/src/LifxNet/LifxClient.Discovery.cs index a067b73..003ee05 100644 --- a/src/LifxNet/LifxClient.Discovery.cs +++ b/src/LifxNet/LifxClient.Discovery.cs @@ -6,8 +6,7 @@ using System.Threading; using System.Threading.Tasks; -namespace LifxNet -{ +namespace LifxNet { public partial class LifxClient { private static uint _identifier = 1; private static readonly object IdentifierLock = new object(); @@ -15,8 +14,7 @@ public partial class LifxClient { private CancellationTokenSource? _discoverCancellationSource; private readonly Dictionary _discoveredBulbs = new Dictionary(); - private static uint GetNextIdentifier() - { + private static uint GetNextIdentifier() { lock (IdentifierLock) return _identifier++; } @@ -25,48 +23,50 @@ private static uint GetNextIdentifier() /// Event fired when a LIFX bulb is discovered on the network /// public event EventHandler? Discovered; + /// /// Event fired when a LIFX bulb hasn't been seen on the network for a while (for more than 5 minutes) /// public event EventHandler? Lost; private IList devices = new List(); - + /// /// Gets a list of currently known devices /// - public IEnumerable Devices { get { return devices; } } + public IEnumerable Devices { + get { return devices; } + } /// /// Event args for and events. /// - public sealed class DiscoveryEventArgs : EventArgs - { + public sealed class DiscoveryEventArgs : EventArgs { internal DiscoveryEventArgs(Device device) => Device = device; + /// /// The device the event relates to /// public Device Device { get; } } - private void ProcessDeviceDiscoveryMessage(IPAddress remoteAddress, LifxResponse msg) - { + private void ProcessDeviceDiscoveryMessage(IPAddress remoteAddress, LifxResponse msg) { string id = msg.Header.TargetMacAddressName; //remoteAddress.ToString() - if (_discoveredBulbs.ContainsKey(id)) //already discovered + 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; } + 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)) - { + , BitConverter.ToUInt32(msg.Payload, 1)) { LastSeen = DateTime.UtcNow }; _discoveredBulbs[id] = device; @@ -80,24 +80,20 @@ private void ProcessDeviceDiscoveryMessage(IPAddress remoteAddress, LifxResponse /// /// /// - public void StartDeviceDiscovery() - { + public void StartDeviceDiscovery() { if (_discoverCancellationSource != null && !_discoverCancellationSource.IsCancellationRequested) return; _discoverCancellationSource = new CancellationTokenSource(); var token = _discoverCancellationSource.Token; var source = _discoverSourceId = GetNextIdentifier(); //Start discovery thread - Task.Run(async () => - { + Task.Run(async () => { Debug.WriteLine("Sending GetServices"); FrameHeader header = new FrameHeader { Identifier = source }; - while (!token.IsCancellationRequested) - { - try - { + while (!token.IsCancellationRequested) { + try { await BroadcastMessageAsync(null, header, MessageType.DeviceGetService); } catch { // ignored @@ -109,8 +105,7 @@ public void StartDeviceDiscovery() continue; } - foreach(var device in lostDevices) - { + foreach (var device in lostDevices) { devices.Remove(device); _discoveredBulbs.Remove(device.MacAddressName); Lost?.Invoke(this, new DiscoveryEventArgs(device)); @@ -123,8 +118,7 @@ public void StartDeviceDiscovery() /// Stops device discovery /// /// - public void StopDeviceDiscovery() - { + public void StopDeviceDiscovery() { if (_discoverCancellationSource == null || _discoverCancellationSource.IsCancellationRequested) return; _discoverCancellationSource.Cancel(); @@ -135,13 +129,11 @@ public void StopDeviceDiscovery() /// /// LIFX Generic Device /// - public abstract class Device - { - internal Device(string hostname, byte[] macAddress, byte service, UInt32 port) - { + 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)) + if (string.IsNullOrWhiteSpace(hostname)) throw new ArgumentException(nameof(hostname)); HostName = hostname; MacAddress = macAddress; @@ -175,19 +167,15 @@ internal Device(string hostname, byte[] macAddress, byte service, UInt32 port) /// /// Gets the MAC address /// - public string MacAddressName - { - get - { - return string.Join(":", MacAddress.Take(6).Select(tb => tb.ToString("X2")).ToArray()); - } + public string MacAddressName { + get { return string.Join(":", MacAddress.Take(6).Select(tb => tb.ToString("X2")).ToArray()); } } } + /// /// LIFX light bulb /// - public sealed class LightBulb : Device - { + 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. /// @@ -195,8 +183,8 @@ public sealed class LightBulb : Device /// /// /// - public LightBulb(string hostname, byte[] macAddress, byte service = 0, UInt32 port = 0) : base(hostname, macAddress, service, port) - { + 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 1629a69..bbb3a45 100644 --- a/src/LifxNet/LifxClient.LightOperations.cs +++ b/src/LifxNet/LifxClient.LightOperations.cs @@ -3,10 +3,10 @@ using System.Diagnostics; using System.Threading.Tasks; -namespace LifxNet -{ +namespace LifxNet { public partial class LifxClient { - private readonly Dictionary> _taskCompletions = new Dictionary>(); + private readonly Dictionary> _taskCompletions = + new Dictionary>(); /// /// Turns a bulb on using the provided transition time @@ -20,7 +20,8 @@ public partial class LifxClient { /// /// /// - 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 @@ -31,7 +32,8 @@ public partial class LifxClient { /// /// /// - 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 @@ -46,12 +48,11 @@ public partial class LifxClient { /// /// /// - 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(nameof(bulb)); if (transitionDuration.TotalMilliseconds > UInt32.MaxValue || - transitionDuration.Ticks < 0) + transitionDuration.Ticks < 0) throw new ArgumentOutOfRangeException(nameof(transitionDuration)); FrameHeader header = new FrameHeader { @@ -59,12 +60,13 @@ public async Task SetLightPowerAsync(LightBulb bulb, TimeSpan transitionDuration AcknowledgeRequired = true }; - var b = BitConverter.GetBytes((UInt16)transitionDuration.TotalMilliseconds); + var b = BitConverter.GetBytes((UInt16) transitionDuration.TotalMilliseconds); - 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 + (UInt16) (isOn ? 65535 : 0), b ).ConfigureAwait(false); } @@ -73,8 +75,7 @@ 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)); @@ -93,7 +94,8 @@ public async Task GetLightPowerAsync(LightBulb bulb) /// /// /// - public Task SetColorAsync(LightBulb bulb, LifxColor lifxColor, UInt16 kelvin) => SetColorAsync(bulb, lifxColor, kelvin, TimeSpan.Zero); + public Task SetColorAsync(LightBulb bulb, LifxColor lifxColor, UInt16 kelvin) => + SetColorAsync(bulb, lifxColor, kelvin, TimeSpan.Zero); /// /// Sets color and temperature for a bulb and uses a transition time to the provided state @@ -103,8 +105,7 @@ public async Task GetLightPowerAsync(LightBulb bulb) /// /// /// - public Task SetColorAsync(LightBulb bulb, LifxColor lifxColor, UInt16 kelvin, TimeSpan transitionDuration) - { + public Task SetColorAsync(LightBulb bulb, LifxColor lifxColor, UInt16 kelvin, TimeSpan transitionDuration) { if (bulb == null) throw new ArgumentNullException(nameof(bulb)); var hsl = Utilities.RgbToHsl(lifxColor); @@ -126,15 +127,13 @@ public async Task SetColorAsync(LightBulb bulb, UInt16 saturation, UInt16 brightness, UInt16 kelvin, - TimeSpan transitionDuration) - { + TimeSpan transitionDuration) { if (bulb == null) throw new ArgumentNullException(nameof(bulb)); if (transitionDuration.TotalMilliseconds > UInt32.MaxValue || - transitionDuration.Ticks < 0) + 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"); } @@ -143,12 +142,12 @@ public async Task SetColorAsync(LightBulb bulb, Identifier = GetNextIdentifier(), AcknowledgeRequired = true }; - var duration = (UInt32)transitionDuration.TotalMilliseconds; - + var duration = (UInt32) 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 ); } @@ -175,13 +174,12 @@ await BroadcastMessageAsync(bulb.HostName, header, ); }*/ - /// - /// Gets the current state of the bulb - /// - /// - /// - public Task GetLightStateAsync(LightBulb bulb) - { + /// + /// Gets the current state of the bulb + /// + /// + /// + public Task GetLightStateAsync(LightBulb bulb) { if (bulb == null) throw new ArgumentNullException(nameof(bulb)); FrameHeader header = new FrameHeader { @@ -198,15 +196,14 @@ 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( bulb.HostName, header, MessageType.InfraredGet).ConfigureAwait(false)).Brightness; } @@ -217,8 +214,7 @@ public async Task GetInfraredAsync(LightBulb bulb) /// /// /// - public async Task SetInfraredAsync(Device device, UInt16 brightness) - { + public async Task SetInfraredAsync(Device device, UInt16 brightness) { if (device == null) throw new ArgumentNullException(nameof(device)); Debug.WriteLine($"Sending SetInfrared({brightness}) to {device.HostName}"); @@ -231,4 +227,4 @@ public async Task SetInfraredAsync(Device device, UInt16 brightness) 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 index c3018bd..1da8cd1 100644 --- a/src/LifxNet/LifxClient.MultizoneOperations.cs +++ b/src/LifxNet/LifxClient.MultizoneOperations.cs @@ -5,6 +5,8 @@ namespace LifxNet { public partial class LifxClient : IDisposable { + private const byte Apply = 0x01; + /// /// This message is used for changing the color of either a single or multiple zones. @@ -12,28 +14,31 @@ public partial class LifxClient : IDisposable { /// Target bulb /// Start index to target /// End index to target - /// LifxColor to use + /// LifxColor to use /// How long to fade /// /// /// - public async Task SetColorZones(LightBulb bulb, int startIndex, int endIndex, LifxColor Color, + public async Task SetColorZones(LightBulb bulb, int startIndex, int endIndex, LifxColor color, TimeSpan transitionDuration) { if (bulb == null) throw new ArgumentNullException(nameof(bulb)); if (transitionDuration.TotalMilliseconds > UInt32.MaxValue || - transitionDuration.Ticks < 0) {throw new ArgumentOutOfRangeException(nameof(transitionDuration));} + transitionDuration.Ticks < 0) { + throw new ArgumentOutOfRangeException(nameof(transitionDuration)); + } + if (startIndex > endIndex) throw new ArgumentOutOfRangeException(nameof(startIndex)); - + FrameHeader header = new FrameHeader { Identifier = GetNextIdentifier(), AcknowledgeRequired = true }; - UInt32 duration = (UInt32)transitionDuration.TotalMilliseconds; + var duration = (uint) transitionDuration.TotalMilliseconds; await BroadcastMessageAsync(bulb.HostName, header, - MessageType.SetColorZones, startIndex, endIndex, Color, duration, 0x01); + MessageType.SetColorZones, (byte) startIndex, (byte) endIndex, color, duration, Apply); } - + /// /// Set a zone of colors /// @@ -46,34 +51,37 @@ await BroadcastMessageAsync(bulb.HostName, header, /// Thrown if the bulb is null /// Thrown if the duration is longer than the max /// - public async Task SetExtendedColorZonesAsync(LightBulb bulb, TimeSpan transitionDuration, int index, List colors) { + public async Task SetExtendedColorZonesAsync(LightBulb bulb, TimeSpan transitionDuration, uint index, + List colors) { if (bulb == null) throw new ArgumentNullException(nameof(bulb)); if (transitionDuration.TotalMilliseconds > UInt32.MaxValue || - transitionDuration.Ticks < 0) {throw new ArgumentOutOfRangeException(nameof(transitionDuration));} - + transitionDuration.Ticks < 0) { + throw new ArgumentOutOfRangeException(nameof(transitionDuration)); + } + FrameHeader header = new FrameHeader { Identifier = GetNextIdentifier(), AcknowledgeRequired = true }; - uint duration = (uint)transitionDuration.TotalMilliseconds; - var cArgs = new List(); + var duration = (uint) transitionDuration.TotalMilliseconds; + var count = (byte) colors.Count; + var colorBytes = new List(); foreach (var color in colors) { - cArgs.AddRange(color.ToBytes()); + colorBytes.AddRange(color.ToBytes()); } - + await BroadcastMessageAsync(bulb.HostName, header, - MessageType.SetExtendedColorZones, duration, (byte) 0x01, index, colors.Count, cArgs); + MessageType.SetExtendedColorZones, duration, Apply, index, count, colorBytes); } - + /// /// Try to get the color zones from our device. /// /// /// /// - public Task GetExtendedColorZonesAsync(LightBulb bulb) - { + public Task GetExtendedColorZonesAsync(LightBulb bulb) { if (bulb == null) throw new ArgumentNullException(nameof(bulb)); FrameHeader header = new FrameHeader { @@ -88,19 +96,20 @@ public Task GetExtendedColorZonesAsync(LightBul /// Try to get the color zones from our device, non-extended. /// /// Target bulb + /// Start index of requested zones + /// End index of requested zones /// Either a "StateZone" response for single-zone devices, or "StateMultiZone" response. /// - public Task GetColorZonesAsync(LightBulb bulb) - { + public Task GetColorZonesAsync(LightBulb bulb, int startIndex, int endIndex) { if (bulb == null) throw new ArgumentNullException(nameof(bulb)); + if (startIndex > endIndex) throw new ArgumentOutOfRangeException(nameof(startIndex)); FrameHeader header = new FrameHeader { Identifier = GetNextIdentifier(), AcknowledgeRequired = false }; return BroadcastMessageAsync( - bulb.HostName, header, MessageType.GetColorZones); + bulb.HostName, header, MessageType.GetColorZones, (byte) startIndex, (byte) endIndex); } - } } \ No newline at end of file diff --git a/src/LifxNet/LifxClient.cs b/src/LifxNet/LifxClient.cs index cdad855..51b220e 100644 --- a/src/LifxNet/LifxClient.cs +++ b/src/LifxNet/LifxClient.cs @@ -8,63 +8,53 @@ using System.Text; 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 bool _isRunning; - private LifxClient() - { + private LifxClient() { } /// /// 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); + private void Initialize() { + IPEndPoint end = new IPEndPoint(IPAddress.Any, Port); _socket = new UdpClient(end); - _socket.Client.Blocking = false; + _socket.Client.Blocking = false; _socket.DontFragment = true; - _socket.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); - _isRunning = true; - StartReceiveLoop(); + _socket.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + _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, IPEndPoint endpoint) - { + 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 { + // ignored + } + }); + } + + private void HandleIncomingMessages(byte[] data, IPEndPoint endpoint) { var remote = endpoint; var msg = ParseMessage(data); switch (msg.Type) { @@ -72,31 +62,29 @@ private void HandleIncomingMessages(byte[] data, IPEndPoint endpoint) ProcessDeviceDiscoveryMessage(remote.Address, msg); break; default: - if (_taskCompletions.ContainsKey(msg.Source)) - { + if (_taskCompletions.ContainsKey(msg.Source)) { var tcs = _taskCompletions[msg.Source]; tcs(msg); } + break; } + 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; + public void Dispose() { + _isRunning = false; _socket?.Dispose(); } - private Task BroadcastMessageAsync(string? hostName, FrameHeader header, MessageType type, params object[] args) - where T : LifxResponse - - { + private Task BroadcastMessageAsync(string? hostName, FrameHeader header, MessageType type, + params object[] args) + where T : LifxResponse { List payload = new List(); foreach (var arg in args) { switch (arg) { @@ -113,7 +101,8 @@ private Task BroadcastMessageAsync(string? hostName, FrameHeader header, M payload.AddRange(bytes); break; case string s: - payload.AddRange(Encoding.UTF8.GetBytes(s.PadRight(32).Take(32).ToArray())); //All strings are 32 bytes + payload.AddRange( + Encoding.UTF8.GetBytes(s.PadRight(32).Take(32).ToArray())); //All strings are 32 bytes break; case LifxColor c: payload.AddRange(c.ToBytes()); @@ -122,11 +111,13 @@ private Task BroadcastMessageAsync(string? hostName, FrameHeader header, M throw new NotSupportedException(args.GetType().FullName); } } + return BroadcastMessagePayloadAsync(hostName, header, type, payload.ToArray()); } - private async Task BroadcastMessagePayloadAsync(string? hostName, FrameHeader header, MessageType type, byte[] payload) - where T : LifxResponse - { + + private async Task BroadcastMessagePayloadAsync(string? hostName, FrameHeader header, MessageType type, + byte[] payload) + where T : LifxResponse { if (_socket == null) throw new InvalidOperationException("No valid socket"); #if DEBUG @@ -136,168 +127,160 @@ private async Task BroadcastMessagePayloadAsync(string? hostName, FrameHea // System.Diagnostics.Debug.WriteLine( // string.Join(",", (from a in data select a.ToString("X2")).ToArray())); #endif - if (hostName == null) - { - hostName = "255.255.255.255"; - } + hostName ??= "255.255.255.255"; 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 => - { - if (r.GetType() == typeof(T)) - tcs.TrySetResult((T)r); - }; - _taskCompletions[header.Identifier] = action; + + void Action(LifxResponse r) { + if (r.GetType() == typeof(T)) tcs.TrySetResult((T) r); + } + + _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; - 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); - } + if (tcs == null) { + return result; } + + 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); + } + 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, 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)) - { - //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(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(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, UInt16 type, byte[] payload) { + using var dw = new BinaryWriter(outStream); + //BinaryWriter bw = new BinaryWriter(ms); + + #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 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(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(); } } - internal class FrameHeader - { + internal class FrameHeader { public UInt32 Identifier; public byte Sequence; public bool AcknowledgeRequired; public bool ResponseRequired; public byte[] TargetMacAddress; public DateTime AtTime; - public FrameHeader() - { + + public FrameHeader() { Identifier = 0; Sequence = 0; AcknowledgeRequired = false; ResponseRequired = false; - TargetMacAddress = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 }; + TargetMacAddress = new byte[] {0, 0, 0, 0, 0, 0, 0, 0}; AtTime = DateTime.MinValue; } - public string TargetMacAddressName - { - get - { - if (TargetMacAddress == null) return string.Empty; - return string.Join(":", TargetMacAddress.Take(6).Select(tb => tb.ToString("X2")).ToArray()); - } - } - } - -} + + public string TargetMacAddressName { + get { + if (TargetMacAddress == null) return string.Empty; + 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 index 543dd60..588412d 100644 --- a/src/LifxNet/LifxColor.cs +++ b/src/LifxNet/LifxColor.cs @@ -3,150 +3,193 @@ 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); - } - - public float Hue { - get => _color.GetHue(); - set => _color = HsbToColor(value, _color.GetSaturation(), _color.GetBrightness()); - } - - public float Saturation { - get => _color.GetSaturation(); - set => _color = HsbToColor(_color.GetHue(), value, _color.GetBrightness()); - } - - public float Brightness { - get => _color.GetBrightness(); - set => _color = HsbToColor(_color.GetHue(), _color.GetSaturation(), value); - } - - public LifxColor(short h, short s, short b, short k) { - _color = HsbToColor(h, s, b); - } - - public LifxColor(int a, int r, int g, int b) { - _color = Color.FromArgb(a, r, g, b); - } - - public LifxColor(int r, int g, int b) { - _color = Color.FromArgb(255, r, g, b); - } - - public LifxColor(Color color) { - _color = color; - } - - /// - /// Serialize our color to a byte array - /// - /// HSBK formatted array of bytes. I think - public byte[] ToBytes() { - var output = new List(); - output.AddRange(BitConverter.GetBytes(Hue)); - output.AddRange(BitConverter.GetBytes(Saturation)); - output.AddRange(BitConverter.GetBytes(Brightness)); - output.AddRange(BitConverter.GetBytes(2700)); - return output.ToArray(); - } - - - private static Color HsbToColor(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)); - - double r = 0D; - double g = 0D; - double bl = 0D; - - if (Math.Abs(s) < tolerance) - r = g = bl = b; - else { - // the argb wheel consists of 6 sectors. Figure out which sector - // you're in. - double sectorPos = h / 60D; - int sectorNumber = (int) Math.Floor(sectorPos); - // get the fractional part of the sector - double fractionalSector = sectorPos - sectorNumber; - - // calculate values for the three axes of the argb. - double p = b * (1D - s); - double q = b * (1D - s * fractionalSector); - double 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}",CultureInfo.InvariantCulture)))), - Math.Max(0, Math.Min(255, Convert.ToInt32(double.Parse($"{g * 255D:0.00}",CultureInfo.InvariantCulture)))), - Math.Max(0, Math.Min(255, Convert.ToInt32(double.Parse($"{bl * 250D:0.00}", CultureInfo.InvariantCulture))))); - } - } +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); + } + + /// + /// Hue + /// + public float Hue { + get => _color.GetHue(); + set => _color = HsbToColor(value, _color.GetSaturation(), _color.GetBrightness()); + } + + /// + /// Saturation + /// + public float Saturation { + get => _color.GetSaturation(); + set => _color = HsbToColor(_color.GetHue(), value, _color.GetBrightness()); + } + + /// + /// Brightness + /// + public float Brightness { + get => _color.GetBrightness(); + set => _color = HsbToColor(_color.GetHue(), _color.GetSaturation(), value); + } + + /// + /// Create a color from HSBK values + /// + /// Hue + /// Saturation + /// Brightness + /// Temp, currently ignored + public LifxColor(short h, short s, short b, short k = 2700) { + _color = HsbToColor(h, s, b); + } + + /// + /// Create a color from ARGB values + /// + /// Alpha + /// Red + /// Green + /// Blue + public LifxColor(int a, int r, int g, int b) { + _color = Color.FromArgb(a, r, g, b); + } + + /// + /// Create a color from RGB Value, with default alpha of 255 + /// + /// Red + /// Green + /// Blue + public LifxColor(int r, int g, int b) { + _color = Color.FromArgb(255, r, g, b); + } + + /// + /// Create a LifxColor from a System.Drawing.Color + /// + /// Input Color + public LifxColor(Color color) { + _color = color; + } + + /// + /// Serialize our color to a byte array + /// + /// HSBK formatted array of bytes. + public byte[] ToBytes() { + var output = new List(); + foreach (var u in new ushort[] {(ushort) Hue, (ushort) Saturation, (ushort) Brightness, 2700}) + output.AddRange(BitConverter.GetBytes(u)); + return output.ToArray(); + } + + + /// + /// Convert HSB Values to a standard Color object + /// + /// Hue + /// Saturation + /// Brightness + /// Alpha, default is 255 + /// + private static Color HsbToColor(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)); + + double r = 0D; + double g = 0D; + double bl = 0D; + + if (Math.Abs(s) < Tolerance) + r = g = bl = b; + else { + // the argb wheel consists of 6 sectors. Figure out which sector + // you're in. + double sectorPos = h / 60D; + int sectorNumber = (int) Math.Floor(sectorPos); + // get the fractional part of the sector + double fractionalSector = sectorPos - sectorNumber; + + // calculate values for the three axes of the argb. + double p = b * (1D - s); + double q = b * (1D - s * fractionalSector); + double 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}", CultureInfo.InvariantCulture)))), + Math.Max(0, + Math.Min(255, Convert.ToInt32(double.Parse($"{g * 255D:0.00}", CultureInfo.InvariantCulture)))), + Math.Max(0, + Math.Min(255, Convert.ToInt32(double.Parse($"{bl * 250D:0.00}", CultureInfo.InvariantCulture))))); + } + } } \ No newline at end of file diff --git a/src/LifxNet/LifxPacket.cs b/src/LifxNet/LifxPacket.cs index 76b1619..1d5f302 100644 --- a/src/LifxNet/LifxPacket.cs +++ b/src/LifxNet/LifxPacket.cs @@ -1,22 +1,25 @@ using System; using System.IO; -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); @@ -38,11 +41,11 @@ protected LifxPacket(ushort type, object[] data) throw new NotImplementedException(); } } + _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 }, @@ -69,25 +72,24 @@ public static LifxPacket FromByteArray(byte[] data) ushort packetType = br.ReadUInt16(); // ReverseBytes(br.ReadUInt16()); byte[] reserved4 = br.ReadBytes(2); byte[] payload = { }; - if (len > 0) - { + 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 ffaf539..db2a0d4 100644 --- a/src/LifxNet/LifxResponses.cs +++ b/src/LifxNet/LifxResponses.cs @@ -2,17 +2,13 @@ using System.Collections.Generic; using System.Text; -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) - { + 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: @@ -40,8 +36,7 @@ internal static LifxResponse Create(FrameHeader header, MessageType type, UInt32 } } - internal LifxResponse(FrameHeader header, MessageType type, byte[] payload, UInt32 source) - { + internal LifxResponse(FrameHeader header, MessageType type, byte[] payload, UInt32 source) { Header = header; Type = type; Payload = payload; @@ -53,21 +48,22 @@ internal LifxResponse(FrameHeader header, MessageType type, byte[] payload, UInt internal MessageType Type { get; } internal UInt32 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, byte[] payload, UInt32 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, byte[] payload, UInt32 source) : base(header, type, payload, source) - { + public class StateZoneResponse : LifxResponse { + internal StateZoneResponse(FrameHeader header, MessageType type, byte[] payload, UInt32 source) : base(header, + type, payload, source) { Count = BitConverter.ToUInt16(payload, 0); Index = BitConverter.ToUInt16(payload, 2); var h = BitConverter.ToInt16(payload, 4); @@ -75,31 +71,31 @@ internal StateZoneResponse(FrameHeader header, MessageType type, byte[] payload, var b = BitConverter.ToInt16(payload, 8); var k = BitConverter.ToInt16(payload, 10); Color = new LifxColor(h, s, b, k); - } + /// /// Count - total number of zones on the device /// public UInt16 Count { get; private set; } + /// /// Index - Zone the message starts from /// public UInt16 Index { get; private set; } + /// /// The list of colors returned by the message /// public LifxColor Color { get; private set; } - } - - + + /// /// Get the list of colors currently being displayed by zones /// - public class StateMultiZoneResponse : LifxResponse - { - internal StateMultiZoneResponse(FrameHeader header, MessageType type, byte[] payload, UInt32 source) : base(header, type, payload, source) - { + public class StateMultiZoneResponse : LifxResponse { + internal StateMultiZoneResponse(FrameHeader header, MessageType type, byte[] payload, UInt32 source) : base( + header, type, payload, source) { Colors = new List(); Count = BitConverter.ToUInt16(payload, 0); Index = BitConverter.ToUInt16(payload, 2); @@ -109,32 +105,33 @@ internal StateMultiZoneResponse(FrameHeader header, MessageType type, byte[] pay var s = BitConverter.ToInt16(payload, i + 1); var b = BitConverter.ToInt16(payload, i + 2); var k = BitConverter.ToInt16(payload, i + 3); - Colors.Add(new LifxColor(h,s,b,k)); + Colors.Add(new LifxColor(h, s, b, k)); } } + /// /// Count - total number of zones on the device /// public UInt16 Count { get; private set; } + /// /// Index - Zone the message starts from /// public UInt16 Index { get; private set; } + /// /// The list of colors returned by the message /// public List Colors { get; private set; } - } - - + + /// /// Get the list of colors currently being displayed by zones /// - public class StateExtendedColorZonesResponse : LifxResponse - { - internal StateExtendedColorZonesResponse(FrameHeader header, MessageType type, byte[] payload, UInt32 source) : base(header, type, payload, source) - { + public class StateExtendedColorZonesResponse : LifxResponse { + internal StateExtendedColorZonesResponse(FrameHeader header, MessageType type, byte[] payload, UInt32 source) : + base(header, type, payload, source) { Colors = new List(); Count = BitConverter.ToUInt16(payload, 0); Index = BitConverter.ToUInt16(payload, 2); @@ -144,32 +141,34 @@ internal StateExtendedColorZonesResponse(FrameHeader header, MessageType type, b var s = BitConverter.ToInt16(payload, i + 1); var b = BitConverter.ToInt16(payload, i + 2); var k = BitConverter.ToInt16(payload, i + 3); - Colors.Add(new LifxColor(h,s,b,k)); + Colors.Add(new LifxColor(h, s, b, k)); } } + /// /// Count - total number of zones on the device /// public UInt16 Count { get; private set; } + /// /// Index - Zone the message starts from /// public UInt16 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) - { + 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); } @@ -177,121 +176,135 @@ internal StateServiceResponse(FrameHeader header, MessageType type, byte[] paylo private Byte Service { get; } private UInt32 Port { 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) { + internal class StateLabelResponse : LifxResponse { + internal StateLabelResponse(FrameHeader header, MessageType type, byte[] payload, UInt32 source) : base(header, + type, payload, source) { Label = Encoding.UTF8.GetString(payload, 0, payload.Length).Replace("\0", ""); } + public string? Label { get; private set; } } + /// /// 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) - { + 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",""); + Label = Encoding.UTF8.GetString(payload, 12, 32).Replace("\0", ""); } + /// /// Hue /// public UInt16 Hue { get; private set; } + /// /// Saturation (0=desaturated, 65535 = fully saturated) /// public UInt16 Saturation { get; private set; } + /// /// Brightness (0=off, 65535=full brightness) /// public UInt16 Brightness { get; private set; } + /// /// Bulb color temperature /// public UInt16 Kelvin { get; private set; } + /// /// Power state /// public bool IsOn { get; private set; } + /// /// Light label /// public string Label { get; private set; } } - internal class LightPowerResponse : LifxResponse - { - internal LightPowerResponse(FrameHeader header, MessageType type, byte[] payload, UInt32 source) : base(header, type, payload, source) - { + + 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; } + public bool IsOn { get; private set; } } - internal class InfraredStateResponse : LifxResponse - { - internal InfraredStateResponse(FrameHeader header, MessageType type, byte[] payload, UInt32 source) : base(header, type, payload, source) - { + internal class InfraredStateResponse : LifxResponse { + internal InfraredStateResponse(FrameHeader header, MessageType type, byte[] payload, UInt32 source) : base( + header, type, payload, source) { Brightness = BitConverter.ToUInt16(payload, 0); } + public UInt16 Brightness { get; private set; } } /// /// 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) - { + 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); } + /// /// Vendor ID /// public UInt32 Vendor { get; private set; } + /// /// Product ID /// public UInt32 Product { get; private set; } + /// /// Hardware version /// public UInt32 Version { get; private set; } } + /// /// 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) - { + 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); Build = Utilities.Epoch.AddMilliseconds(nanoseconds * 0.000001); //8..15 UInt64 is reserved Version = BitConverter.ToUInt32(payload, 16); } + /// /// Firmware build time /// public DateTime Build { get; private set; } + /// /// Firmware version /// public UInt32 Version { get; private set; } } - internal class UnknownResponse : LifxResponse - { - internal UnknownResponse(FrameHeader header, MessageType type, byte[] payload, UInt32 source) : base(header, type, payload, source) { } + internal class UnknownResponse : LifxResponse { + internal UnknownResponse(FrameHeader header, MessageType type, byte[] payload, UInt32 source) : base(header, + type, payload, source) { + } } -} +} \ No newline at end of file diff --git a/src/LifxNet/MessageType.cs b/src/LifxNet/MessageType.cs index 5700869..d6e6c85 100644 --- a/src/LifxNet/MessageType.cs +++ b/src/LifxNet/MessageType.cs @@ -1,7 +1,5 @@ -namespace LifxNet -{ - internal enum MessageType : ushort - { +namespace LifxNet { + internal enum MessageType : ushort { //Device Messages DeviceGetService = 0x02, DeviceStateService = 0x03, @@ -35,6 +33,7 @@ internal enum MessageType : ushort DeviceStateGroup = 53, DeviceEchoRequest = 58, DeviceEchoResponse = 59, + //Light messages LightGet = 101, LightSetColor = 102, @@ -44,6 +43,7 @@ internal enum MessageType : ushort LightSetPower = 117, LightStatePower = 118, LightSetWaveformOptional = 119, + //Infrared InfraredGet = 120, InfraredState = 121, @@ -55,10 +55,11 @@ internal enum MessageType : ushort SetExtendedColorZones = 510, GetExtendedColorZones = 511, StateExtendedColorZones = 512, - + //Unofficial LightGetTemperature = 0x6E, - //LightStateTemperature = 0x6f, + + //LightStateTemperature = 0x6f, SetLightBrightness = 0x68 } -} +} \ No newline at end of file diff --git a/src/LifxNet/Utilities.cs b/src/LifxNet/Utilities.cs index aa35394..889e0a5 100644 --- a/src/LifxNet/Utilities.cs +++ b/src/LifxNet/Utilities.cs @@ -1,13 +1,10 @@ using System; -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(LifxColor rgb) - { + + public static UInt16[] RgbToHsl(LifxColor rgb) { // normalize red, green and blue values double r = (rgb.R / 255.0); double g = (rgb.G / 255.0); @@ -17,31 +14,22 @@ public static UInt16[] RgbToHsl(LifxColor 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)(h / 360 * 65535), - (UInt16)(s * 65535), - (UInt16)(max * 65535) + (UInt16) (h / 360 * 65535), + (UInt16) (s * 65535), + (UInt16) (max * 65535) }; } - - } -} +} \ No newline at end of file From 5c18611adf96828b128f7ed7d2bcc4338d0df80e Mon Sep 17 00:00:00 2001 From: d8ahazard Date: Mon, 15 Feb 2021 10:04:30 -0600 Subject: [PATCH 10/37] Massive Updates Bump .net standard version to 2.0, update project details. Add "Payload" class to handle smarter/cleaner parsing of responses from Lifx with auto error-handling and logging. Add tile message types. Add tile response types. Split devices into their own classes for easier management. Create TileGroup, Switch, and Strip device types, instead of calling everything "bulb". Add device product check on discovery to determine proper device type to return. Update readme Add default initializer for LifxColor. --- README.md | 12 +- src/LifxNet/Device.cs | 56 ++++ src/LifxNet/LifxClient.DeviceOperations.cs | 2 +- src/LifxNet/LifxClient.Discovery.cs | 96 ++----- src/LifxNet/LifxClient.LightOperations.cs | 28 +- src/LifxNet/LifxClient.MultizoneOperations.cs | 4 +- src/LifxNet/LifxClient.TileOperations.cs | 7 + src/LifxNet/LifxClient.cs | 54 ++-- src/LifxNet/LifxColor.cs | 7 + src/LifxNet/LifxNet.csproj | 17 +- src/LifxNet/LifxResponses.cs | 228 ++++++++++------ src/LifxNet/LightBulb.cs | 17 ++ src/LifxNet/MessageType.cs | 9 +- src/LifxNet/Payload.cs | 251 ++++++++++++++++++ src/LifxNet/Strip.cs | 18 ++ src/LifxNet/Switch.cs | 18 ++ src/LifxNet/TileGroup.cs | 66 +++++ src/LifxNet/Utilities.cs | 8 +- .../SampleApp.Universal/App.xaml.cs | 3 +- .../SampleApp.Universal/MainPage.xaml.cs | 3 +- .../SampleApp.Universal.csproj | 2 +- src/SampleApps/SampleApp.netcore/Program.cs | 16 +- 22 files changed, 696 insertions(+), 226 deletions(-) create mode 100644 src/LifxNet/Device.cs create mode 100644 src/LifxNet/LifxClient.TileOperations.cs create mode 100644 src/LifxNet/LightBulb.cs create mode 100644 src/LifxNet/Payload.cs create mode 100644 src/LifxNet/Strip.cs create mode 100644 src/LifxNet/Switch.cs create mode 100644 src/LifxNet/TileGroup.cs 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/Device.cs b/src/LifxNet/Device.cs new file mode 100644 index 0000000..f494066 --- /dev/null +++ b/src/LifxNet/Device.cs @@ -0,0 +1,56 @@ +using System; +using System.Linq; + +namespace LifxNet { + /// + /// LIFX Generic Device + /// + public abstract class Device { + internal Device(string hostname, byte[] macAddress, byte service, uint port, uint productId) { + 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; + ProductId = productId; + } + + /// + /// Hostname for the device + /// + public string HostName { get; internal set; } + + /// + /// Service ID + /// + public byte Service { get; } + + /// + /// Service port + /// + public uint Port { get; } + + /// + /// Product ID. Not a part of the "real" message, but handy to have regardless + /// + public uint ProductId { 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 4014694..8d497de 100644 --- a/src/LifxNet/LifxClient.DeviceOperations.cs +++ b/src/LifxNet/LifxClient.DeviceOperations.cs @@ -32,7 +32,7 @@ public async Task SetDevicePowerStateAsync(Device device, bool isOn) { }; _ = await BroadcastMessageAsync(device.HostName, header, - MessageType.DeviceSetPower, (UInt16) (isOn ? 65535 : 0)).ConfigureAwait(false); + MessageType.DeviceSetPower, (ushort) (isOn ? 65535 : 0)).ConfigureAwait(false); } /// diff --git a/src/LifxNet/LifxClient.Discovery.cs b/src/LifxNet/LifxClient.Discovery.cs index 003ee05..b313a11 100644 --- a/src/LifxNet/LifxClient.Discovery.cs +++ b/src/LifxNet/LifxClient.Discovery.cs @@ -13,6 +13,9 @@ public partial class LifxClient { 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) @@ -65,12 +68,33 @@ private void ProcessDeviceDiscoveryMessage(IPAddress remoteAddress, LifxResponse _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; + var device = new LightBulb(address, mac, svc, port) { + LastSeen = lastSeen }; - _discoveredBulbs[id] = device; - devices.Add(device); + var ver = GetDeviceVersionAsync(device).Result; + + if (_stripIds.Contains((int) ver.Product)) { + var dev = new Strip(address, mac, svc, port, ver.Product){LastSeen = lastSeen}; + _discoveredBulbs[id] = dev; + devices.Add(dev); + } else if (_switchIds.Contains((int) ver.Product)) { + var dev = new Switch(address, mac, svc, port, ver.Product){LastSeen = lastSeen}; + _discoveredBulbs[id] = dev; + devices.Add(dev); + } else if (_tileIds.Contains((int) ver.Product)) { + var dev = new TileGroup(address, mac, svc, port, ver.Product){LastSeen = lastSeen}; + _discoveredBulbs[id] = dev; + devices.Add(dev); + } else { + _discoveredBulbs[id] = device; + devices.Add(device); + } + Discovered?.Invoke(this, new DiscoveryEventArgs(device)); } @@ -125,66 +149,4 @@ public void StopDeviceDiscovery() { _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 { 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 bbb3a45..8956059 100644 --- a/src/LifxNet/LifxClient.LightOperations.cs +++ b/src/LifxNet/LifxClient.LightOperations.cs @@ -5,7 +5,7 @@ namespace LifxNet { public partial class LifxClient { - private readonly Dictionary> _taskCompletions = + private readonly Dictionary> _taskCompletions = new Dictionary>(); /// @@ -51,7 +51,7 @@ public Task TurnBulbOffAsync(LightBulb bulb, TimeSpan transitionDuration) => public async Task SetLightPowerAsync(LightBulb bulb, TimeSpan transitionDuration, bool isOn) { if (bulb == null) throw new ArgumentNullException(nameof(bulb)); - if (transitionDuration.TotalMilliseconds > UInt32.MaxValue || + if (transitionDuration.TotalMilliseconds > uint.MaxValue || transitionDuration.Ticks < 0) throw new ArgumentOutOfRangeException(nameof(transitionDuration)); @@ -60,13 +60,13 @@ public async Task SetLightPowerAsync(LightBulb bulb, TimeSpan transitionDuration AcknowledgeRequired = true }; - var b = BitConverter.GetBytes((UInt16) transitionDuration.TotalMilliseconds); + var b = BitConverter.GetBytes((ushort) transitionDuration.TotalMilliseconds); 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); } @@ -94,7 +94,7 @@ public async Task GetLightPowerAsync(LightBulb bulb) { /// /// /// - public Task SetColorAsync(LightBulb bulb, LifxColor lifxColor, UInt16 kelvin) => + public Task SetColorAsync(LightBulb bulb, LifxColor lifxColor, ushort kelvin) => SetColorAsync(bulb, lifxColor, kelvin, TimeSpan.Zero); /// @@ -105,7 +105,7 @@ public Task SetColorAsync(LightBulb bulb, LifxColor lifxColor, UInt16 kelvin) => /// /// /// - public Task SetColorAsync(LightBulb bulb, LifxColor lifxColor, 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(lifxColor); @@ -123,14 +123,14 @@ public Task SetColorAsync(LightBulb bulb, LifxColor lifxColor, UInt16 kelvin, Ti /// /// public async Task SetColorAsync(LightBulb bulb, - UInt16 hue, - UInt16 saturation, - UInt16 brightness, - UInt16 kelvin, + ushort hue, + ushort saturation, + ushort brightness, + ushort kelvin, TimeSpan transitionDuration) { if (bulb == null) throw new ArgumentNullException(nameof(bulb)); - if (transitionDuration.TotalMilliseconds > UInt32.MaxValue || + if (transitionDuration.TotalMilliseconds > uint.MaxValue || transitionDuration.Ticks < 0) throw new ArgumentOutOfRangeException("transitionDuration"); if (kelvin < 2500 || kelvin > 9000) { @@ -142,7 +142,7 @@ public async Task SetColorAsync(LightBulb bulb, Identifier = GetNextIdentifier(), AcknowledgeRequired = true }; - var duration = (UInt32) transitionDuration.TotalMilliseconds; + var duration = (uint) transitionDuration.TotalMilliseconds; await BroadcastMessageAsync(bulb.HostName, header, MessageType.LightSetColor, (byte) 0x00, //reserved @@ -196,7 +196,7 @@ 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)); @@ -214,7 +214,7 @@ 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)); Debug.WriteLine($"Sending SetInfrared({brightness}) to {device.HostName}"); diff --git a/src/LifxNet/LifxClient.MultizoneOperations.cs b/src/LifxNet/LifxClient.MultizoneOperations.cs index 1da8cd1..f7309cc 100644 --- a/src/LifxNet/LifxClient.MultizoneOperations.cs +++ b/src/LifxNet/LifxClient.MultizoneOperations.cs @@ -23,7 +23,7 @@ public async Task SetColorZones(LightBulb bulb, int startIndex, int endIndex, Li TimeSpan transitionDuration) { if (bulb == null) throw new ArgumentNullException(nameof(bulb)); - if (transitionDuration.TotalMilliseconds > UInt32.MaxValue || + if (transitionDuration.TotalMilliseconds > uint.MaxValue || transitionDuration.Ticks < 0) { throw new ArgumentOutOfRangeException(nameof(transitionDuration)); } @@ -55,7 +55,7 @@ public async Task SetExtendedColorZonesAsync(LightBulb bulb, TimeSpan transition List colors) { if (bulb == null) throw new ArgumentNullException(nameof(bulb)); - if (transitionDuration.TotalMilliseconds > UInt32.MaxValue || + if (transitionDuration.TotalMilliseconds > uint.MaxValue || transitionDuration.Ticks < 0) { throw new ArgumentOutOfRangeException(nameof(transitionDuration)); } diff --git a/src/LifxNet/LifxClient.TileOperations.cs b/src/LifxNet/LifxClient.TileOperations.cs new file mode 100644 index 0000000..fc7bc9a --- /dev/null +++ b/src/LifxNet/LifxClient.TileOperations.cs @@ -0,0 +1,7 @@ +using System; + +namespace LifxNet { + public partial class LifxClient { + + } +} \ No newline at end of file diff --git a/src/LifxNet/LifxClient.cs b/src/LifxNet/LifxClient.cs index 51b220e..0d40d2d 100644 --- a/src/LifxNet/LifxClient.cs +++ b/src/LifxNet/LifxClient.cs @@ -14,35 +14,35 @@ namespace LifxNet { /// public partial class LifxClient { private const int Port = 56700; - private UdpClient? _socket; + private readonly UdpClient _socket; private bool _isRunning; - private LifxClient() { - } /// - /// Creates a new LIFX client. + /// Create our client directly, and instantiate a new UDP client /// - /// client - public static Task CreateAsync() { - LifxClient client = new LifxClient(); - client.Initialize(); - return Task.FromResult(client); - } - - private void Initialize() { + public LifxClient() { IPEndPoint end = new IPEndPoint(IPAddress.Any, Port); - _socket = new UdpClient(end); - _socket.Client.Blocking = false; - _socket.DontFragment = true; + _socket = new UdpClient(end) {Client = {Blocking = false}, DontFragment = true}; _socket.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); _isRunning = true; StartReceiveLoop(); } + /// + /// Create our client directly with a re-usable UDP client. + /// + /// Optional UDPClient. + public LifxClient(UdpClient client) { + _socket = client; + _isRunning = true; + StartReceiveLoop(); + } + + private void StartReceiveLoop() { Task.Run(async () => { - while (_isRunning && _socket != null) + while (_isRunning) try { var result = await _socket.ReceiveAsync(); if (result.Buffer.Length > 0) { @@ -142,7 +142,7 @@ void Action(LifxResponse r) { } using (MemoryStream stream = new MemoryStream()) { - WritePacketToStream(stream, header, (UInt16) type, payload); + WritePacketToStream(stream, header, (ushort) type, payload); var msg = stream.ToArray(); await _socket.SendAsync(msg, msg.Length, hostName, Port); } @@ -189,10 +189,10 @@ private static LifxResponse ParseMessage(byte[] packet) { 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[] { }); + return LifxResponse.Create(header, type, source, new Payload(size > 36 ? br.ReadBytes(size - 36) : new byte[] { })); } - private static void WritePacketToStream(Stream outStream, FrameHeader header, UInt16 type, byte[] payload) { + private static void WritePacketToStream(Stream outStream, FrameHeader header, ushort type, byte[] payload) { using var dw = new BinaryWriter(outStream); //BinaryWriter bw = new BinaryWriter(ms); @@ -203,7 +203,7 @@ private static void WritePacketToStream(Stream outStream, FrameHeader header, UI // 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 broadcasted instead + .Identifier); //source identifier - unique value set by the client, used by responses. If 0, responses are broadcast instead #endregion Frame @@ -244,26 +244,25 @@ private static void WritePacketToStream(Stream outStream, FrameHeader header, UI //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 + dw.Write((ulong) (time - new DateTime(1970, 01, 01)).TotalMilliseconds * 10); //timestamp } else { - dw.Write((UInt64) 0); + dw.Write((ulong) 0); } #endregion Protocol Header dw.Write(type); //packet _type - dw.Write((UInt16) 0); //reserved - if (payload != null) - dw.Write(payload); + dw.Write((ushort) 0); //reserved + dw.Write(payload); dw.Flush(); } } internal class FrameHeader { - public UInt32 Identifier; + public uint Identifier; public byte Sequence; public bool AcknowledgeRequired; - public bool ResponseRequired; + public readonly bool ResponseRequired; public byte[] TargetMacAddress; public DateTime AtTime; @@ -278,7 +277,6 @@ public FrameHeader() { public string TargetMacAddressName { get { - if (TargetMacAddress == null) return string.Empty; return string.Join(":", TargetMacAddress.Take(6).Select(tb => tb.ToString("X2")).ToArray()); } } diff --git a/src/LifxNet/LifxColor.cs b/src/LifxNet/LifxColor.cs index 588412d..38c78d5 100644 --- a/src/LifxNet/LifxColor.cs +++ b/src/LifxNet/LifxColor.cs @@ -13,6 +13,13 @@ private static double Tolerance private Color _color; + /// + /// Create a new black LifxColor + /// + public LifxColor() { + _color = Color.FromArgb(255, 0, 0, 0); + } + /// /// Red /// diff --git a/src/LifxNet/LifxNet.csproj b/src/LifxNet/LifxNet.csproj index 8cd79cd..eb2cbc2 100644 --- a/src/LifxNet/LifxNet.csproj +++ b/src/LifxNet/LifxNet.csproj @@ -1,34 +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.4 + Add initial support for Multizone - - - C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\3.1.0\ref\netcoreapp3.1\System.Drawing.Primitives.dll - - \ No newline at end of file diff --git a/src/LifxNet/LifxResponses.cs b/src/LifxNet/LifxResponses.cs index db2a0d4..bfef335 100644 --- a/src/LifxNet/LifxResponses.cs +++ b/src/LifxNet/LifxResponses.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; using System.Text; namespace LifxNet { @@ -7,7 +9,7 @@ namespace LifxNet { /// Base class for LIFX response types /// public abstract class LifxResponse { - internal static LifxResponse Create(FrameHeader header, MessageType type, UInt32 source, byte[] payload) { + internal static LifxResponse Create(FrameHeader header, MessageType type, uint source, Payload payload) { switch (type) { case MessageType.DeviceAcknowledgement: return new AcknowledgementResponse(header, type, payload, source); @@ -31,12 +33,16 @@ internal static LifxResponse Create(FrameHeader header, MessageType type, UInt32 return new StateZoneResponse(header, type, payload, source); case MessageType.StateMultiZone: return new StateMultiZoneResponse(header, type, payload, source); + case MessageType.StateDeviceChain: + return new StateDeviceChainResponse(header, type, payload, source); + case MessageType.StateTileState64: + return new StateTileState64Response(header, type, payload, source); default: return 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; @@ -44,16 +50,16 @@ 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( + internal AcknowledgementResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base( header, type, payload, source) { } } @@ -62,67 +68,98 @@ internal AcknowledgementResponse(FrameHeader header, MessageType type, byte[] pa /// 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, byte[] payload, UInt32 source) : base(header, + internal StateZoneResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(header, type, payload, source) { - Count = BitConverter.ToUInt16(payload, 0); - Index = BitConverter.ToUInt16(payload, 2); - var h = BitConverter.ToInt16(payload, 4); - var s = BitConverter.ToInt16(payload, 6); - var b = BitConverter.ToInt16(payload, 8); - var k = BitConverter.ToInt16(payload, 10); + Count = payload.GetUInt16(); + Index = payload.GetUInt16(); + var h = payload.GetInt16(); + var s = payload.GetInt16(); + var b = payload.GetInt16(); + var k = payload.GetInt16(); Color = new LifxColor(h, s, b, k); + payload.Reset(); } /// /// Count - total number of zones on the device /// - public UInt16 Count { get; private set; } + public ushort Count { get; } /// /// Index - Zone the message starts from /// - public UInt16 Index { get; private set; } + public ushort Index { get; } /// /// The list of colors returned by the message /// - public LifxColor Color { get; private set; } + public LifxColor Color { get; } } + + /// + /// 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(); + while (payload.HasContent()) { + var tile = new Tile(); + tile.LoadPayload(payload); + Tiles.Add(tile); + } + TotalCount = payload.GetUint8(); + if (TotalCount != Tiles.Count) Debug.WriteLine($"Warning, tile count doesn't match: {TotalCount} : {Tiles.Count}"); + payload.Reset(); + } + + /// + /// 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; } + } /// /// Get the list of colors currently being displayed by zones /// public class StateMultiZoneResponse : LifxResponse { - internal StateMultiZoneResponse(FrameHeader header, MessageType type, byte[] payload, UInt32 source) : base( + internal StateMultiZoneResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base( header, type, payload, source) { Colors = new List(); - Count = BitConverter.ToUInt16(payload, 0); - Index = BitConverter.ToUInt16(payload, 2); - for (var i = 4; i < payload.Length; i += 4) { - if (i + 3 < payload.Length) continue; - var h = BitConverter.ToInt16(payload, i); - var s = BitConverter.ToInt16(payload, i + 1); - var b = BitConverter.ToInt16(payload, i + 2); - var k = BitConverter.ToInt16(payload, i + 3); - Colors.Add(new LifxColor(h, s, b, k)); + Count = payload.GetUInt16(); + Index = payload.GetUInt16(); + while (payload.HasContent()) { + Colors.Add(payload.GetColor()); } + payload.Reset(); } /// /// Count - total number of zones on the device /// - public UInt16 Count { get; private set; } + public ushort Count { get; } /// /// Index - Zone the message starts from /// - public UInt16 Index { get; private set; } + public ushort Index { get; } /// /// The list of colors returned by the message /// - public List Colors { get; private set; } + public List Colors { get; } } @@ -130,30 +167,26 @@ internal StateMultiZoneResponse(FrameHeader header, MessageType type, byte[] pay /// Get the list of colors currently being displayed by zones /// public class StateExtendedColorZonesResponse : LifxResponse { - internal StateExtendedColorZonesResponse(FrameHeader header, MessageType type, byte[] payload, UInt32 source) : + internal StateExtendedColorZonesResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(header, type, payload, source) { Colors = new List(); - Count = BitConverter.ToUInt16(payload, 0); - Index = BitConverter.ToUInt16(payload, 2); - for (var i = 4; i < payload.Length; i += 4) { - if (i + 3 < payload.Length) continue; - var h = BitConverter.ToInt16(payload, i); - var s = BitConverter.ToInt16(payload, i + 1); - var b = BitConverter.ToInt16(payload, i + 2); - var k = BitConverter.ToInt16(payload, i + 3); - Colors.Add(new LifxColor(h, s, b, k)); + Count = payload.GetUInt16(); + Index = payload.GetUInt16(); + while (payload.HasContent()) { + Colors.Add(payload.GetColor()); } + payload.Reset(); } /// /// Count - total number of zones on the device /// - public UInt16 Count { get; private set; } + public ushort Count { get; private set; } /// /// Index - Zone the message starts from /// - public UInt16 Index { get; private set; } + public ushort Index { get; private set; } /// /// The list of colors returned by the message @@ -167,143 +200,178 @@ internal StateExtendedColorZonesResponse(FrameHeader header, MessageType type, b /// 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( + internal StateServiceResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base( header, type, payload, source) { - Service = payload[0]; - Port = BitConverter.ToUInt32(payload, 1); + Service = payload.GetUint8(); + Port = payload.GetUInt32(); + payload.Reset(); } - private Byte Service { get; } - private UInt32 Port { get; } + private byte Service { get; } + private uint Port { get; } + } + + /// + /// Response to any message sent with ack_required set to 1. + /// + internal 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, + internal StateLabelResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(header, type, payload, source) { - Label = Encoding.UTF8.GetString(payload, 0, payload.Length).Replace("\0", ""); + Label = payload.GetString().Replace("\0", ""); + payload.Reset(); } - 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, + internal LightStateResponse(FrameHeader header, MessageType type, Payload payload, uint 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", ""); + Hue = payload.GetUInt16(); + Saturation = payload.GetUInt16(); + Brightness = payload.GetUInt16(); + Kelvin = payload.GetUInt16(); + IsOn = payload.GetUInt16() > 0; + Label = payload.GetString(32).Replace("\\0", ""); + payload.Reset(); } /// /// 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, + internal LightPowerResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(header, type, payload, source) { - IsOn = BitConverter.ToUInt16(payload, 0) > 0; + IsOn = payload.GetUInt16() > 0; + payload.Reset(); } - public bool IsOn { get; private set; } + public bool IsOn { get; } } internal class InfraredStateResponse : LifxResponse { - internal InfraredStateResponse(FrameHeader header, MessageType type, byte[] payload, UInt32 source) : base( + internal InfraredStateResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base( header, type, payload, source) { - Brightness = BitConverter.ToUInt16(payload, 0); + Brightness = payload.GetUInt16(); + payload.Reset(); } - 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( + internal StateVersionResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base( header, type, payload, source) { - Vendor = BitConverter.ToUInt32(payload, 0); - Product = BitConverter.ToUInt32(payload, 4); - Version = BitConverter.ToUInt32(payload, 8); + Vendor = Payload.GetUInt32(); + Product = Payload.GetUInt32(); + Version = Payload.GetUInt32(); + payload.Reset(); } /// /// 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( + internal StateHostFirmwareResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base( header, type, payload, source) { - var nanoseconds = BitConverter.ToUInt64(payload, 0); + var nanoseconds = payload.GetUInt64(); Build = Utilities.Epoch.AddMilliseconds(nanoseconds * 0.000001); //8..15 UInt64 is reserved - Version = BitConverter.ToUInt32(payload, 16); + Version = payload.GetUInt32(); + payload.Reset(); } /// /// 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, + internal UnknownResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(header, type, payload, source) { } } diff --git a/src/LifxNet/LightBulb.cs b/src/LifxNet/LightBulb.cs new file mode 100644 index 0000000..6832df6 --- /dev/null +++ b/src/LifxNet/LightBulb.cs @@ -0,0 +1,17 @@ +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, int productId = 0) : base(hostname, + macAddress, service, port, productId) { + } + } +} \ No newline at end of file diff --git a/src/LifxNet/MessageType.cs b/src/LifxNet/MessageType.cs index d6e6c85..442dd43 100644 --- a/src/LifxNet/MessageType.cs +++ b/src/LifxNet/MessageType.cs @@ -48,6 +48,7 @@ internal enum MessageType : ushort { InfraredGet = 120, InfraredState = 121, InfraredSet = 122, + //Multi zone SetColorZones = 501, GetColorZones = 502, StateZone = 503, @@ -55,7 +56,13 @@ internal enum MessageType : ushort { SetExtendedColorZones = 510, GetExtendedColorZones = 511, StateExtendedColorZones = 512, - + //Tile + GetDeviceChain = 701, + StateDeviceChain = 702, + SetUserPosition = 703, + GetTileState64 = 707, + StateTileState64 = 711, + SetTileState64 = 715, //Unofficial LightGetTemperature = 0x6E, diff --git a/src/LifxNet/Payload.cs b/src/LifxNet/Payload.cs new file mode 100644 index 0000000..b215e73 --- /dev/null +++ b/src/LifxNet/Payload.cs @@ -0,0 +1,251 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Diagnostics; +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 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 byte[] Data { get; set; } + private int Pointer { get; set; } + + /// + /// Get the length of the internal byte array + /// + public int Length => Data.Length; + + /// + /// Initialize with a byte array + /// + /// + public Payload(byte[] data) { + Data = data; + } + + /// + /// Return our base byte array + /// + /// + public byte[] ToArray() { + return Data; + } + + /// + /// Convert base byte array to a list + /// + /// + public List ToList() { + return Data.ToList(); + } + + /// + /// Serialize base byte array to a string + /// + /// + public override string ToString() { + return Data.ToString(); + } + + /// + /// Check to see if we still have data to read + /// + /// + public bool HasContent() { + return Pointer < Data.Length; + } + + /// + /// Rewind our pointer N bits + /// + /// How far to rewind. Default is 1. + public void Rewind(int len = 1) { + Pointer -= len; + if (Pointer < 0) Pointer = 0; + } + + /// + /// Forward our pointer N bits + /// + /// How far to advance. Default is 1. + public void Advance(int len = 1) { + Pointer += len; + if (Pointer >= Data.Length) Pointer = Data.Length - 1; + } + + /// + /// Forward the pointer to the end of the array + /// + public void FastForward() { + Pointer = Data.Length - 1; + } + + /// + /// Reset our pointer to 0 + /// + public void Reset() { + Pointer = 0; + } + + /// + /// Read LifxColor from array and increment pointer 8 bytes + /// + /// + public LifxColor GetColor() { + if (Pointer + 16 < Data.Length) { + var h = GetUInt16(); + var s = GetUInt16(); + var b = GetUInt16(); + var k = GetUInt16(); + return new LifxColor(h, s, b, k); + } + FastForward(); + Debug.WriteLine($"Error getting color, pointer {Pointer} is out of range: " + Data.Length); + return new LifxColor(); + } + + /// + /// Read Uint8 from array and increment pointer 1 byte + /// + /// byte + public byte GetUint8() { + if (Pointer + 1 < Data.Length) { + var output = Data[Pointer]; + Pointer++; + return output; + } + FastForward(); + Debug.WriteLine($"Error getting Uint8 from payload, pointer {Pointer} out of range: " + Data.Length); + return 0; + } + + /// + /// Read UInt16 from array and increment pointer 2 bits + /// + /// ushort + public ushort GetUInt16() { + if (Pointer + 2 < Data.Length) { + var output = BitConverter.ToUInt16(Data.ToArray(), Pointer); + Pointer += 2; + return output; + } + FastForward(); + Debug.WriteLine($"Error getting Uint16 from payload, pointer {Pointer} of range: " + Data.Length); + return 0; + } + + /// + /// Read Int16 from array and increment pointer 2 bits. + /// + /// short + public short GetInt16() { + if (Pointer + 2 < Data.Length) { + var output = BitConverter.ToInt16(Data.ToArray(), Pointer); + Pointer += 2; + return output; + } + FastForward(); + Debug.WriteLine($"Error getting int16 from payload, pointer {Pointer} of range: " + Data.Length); + return 0; + } + + /// + /// Read Int32 from array and increment pointer 4 bits. + /// + /// int + public int GetInt32() { + if (Pointer + 4 < Data.Length) { + var output = BitConverter.ToInt32(Data.ToArray(), Pointer); + Pointer += 4; + return output; + } + FastForward(); + Debug.WriteLine($"Error getting Int32 from payload, pointer {Pointer} of range: " + Data.Length); + return 0; + } + + /// + /// Read a UInt32 from array and increment pointer 4 bits. + /// + /// + public uint GetUInt32() { + if (Pointer + 4 < Data.Length) { + var output = BitConverter.ToUInt32(Data.ToArray(), Pointer); + Pointer += 4; + return output; + } + FastForward(); + Debug.WriteLine($"Error getting Uint32 from payload, pointer {Pointer} of range: " + Data.Length); + return 0; + } + + /// + /// Read an Int64 from array and increment pointer 8 bits. + /// + /// long + public long GetInt64() { + if (Pointer + 8 < Data.Length) { + var output = BitConverter.ToInt64(Data.ToArray(), Pointer); + Pointer += 8; + return output; + } + FastForward(); + Debug.WriteLine($"Error getting Int64 from payload, pointer {Pointer} of range: " + Data.Length); + return 0; + } + + /// + /// Read a UInt64 from array and increment pointer 8 bits. + /// + /// ulong + public ulong GetUInt64() { + if (Pointer + 8 < Data.Length) { + var output = BitConverter.ToUInt64(Data.ToArray(), Pointer); + Pointer += 8; + return output; + } + FastForward(); + Debug.WriteLine($"Error getting Uint64 from payload, pointer {Pointer} of range: " + Data.Length); + return 0; + } + + /// + /// Read a Float32 from array and increment pointer 4 bits. + /// + /// float + public float GetFloat32() { + if (Pointer + 4 < Data.Length) { + var output = BitConverter.ToSingle(Data.ToArray(), Pointer); + Pointer += 4; + return output; + } + FastForward(); + Debug.WriteLine($"Error getting Float32 from payload, pointer {Pointer} of range: " + Data.Length); + 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(int length = -1) { + if (length == -1) length = Data.Length - 1 - Pointer; + if (Pointer + length < Data.Length) { + var output = Encoding.UTF8.GetString(Data, Pointer, length); + Pointer += length; + return output; + } + var str = Encoding.UTF8.GetString(Data, Pointer, Data.Length - 1 - Pointer); + Debug.WriteLine($"Error getting string, pointer {Pointer} out of range: " + Data.Length); + FastForward(); + return str; + } + } +} \ No newline at end of file diff --git a/src/LifxNet/Strip.cs b/src/LifxNet/Strip.cs new file mode 100644 index 0000000..719caa5 --- /dev/null +++ b/src/LifxNet/Strip.cs @@ -0,0 +1,18 @@ +namespace LifxNet { + /// + /// LIFX multizone + /// + public sealed class Strip : Device { + /// + /// Initializes a new instance of a strip instead of relying on discovery. At least the host name must be provide for the device to be usable. + /// + /// Required + /// + /// + /// + /// + public Strip(string hostname, byte[] macAddress, byte service = 0, uint port = 0, uint productId = 0) : base(hostname, + macAddress, service, port, productId) { + } + } +} \ No newline at end of file diff --git a/src/LifxNet/Switch.cs b/src/LifxNet/Switch.cs new file mode 100644 index 0000000..2353541 --- /dev/null +++ b/src/LifxNet/Switch.cs @@ -0,0 +1,18 @@ +namespace LifxNet { + /// + /// LIFX switch + /// + public sealed class Switch : 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 Switch(string hostname, byte[] macAddress, byte service = 0, uint port = 0, uint productId = 0) : base(hostname, + macAddress, service, port, productId) { + } + } +} \ No newline at end of file diff --git a/src/LifxNet/TileGroup.cs b/src/LifxNet/TileGroup.cs new file mode 100644 index 0000000..8f3c3d4 --- /dev/null +++ b/src/LifxNet/TileGroup.cs @@ -0,0 +1,66 @@ +using System; + +namespace LifxNet { + /// + /// LIFX tile + /// + public sealed class TileGroup : 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 TileGroup(string hostname, byte[] macAddress, byte service = 0, uint port = 0, uint productId = 0) : base(hostname, + macAddress, service, port, productId) { + } + + public void LoadPayload() { + + } + } + + public class Tile { + public int AccelMeasX { get; set; } + public int AccelMeasY { get; set; } + public int AccelMeasZ { get; set; } + public float UserX { get; set; } + public float UserY { get; set; } + public int Width { get; set; } + public int Height { get; set; } + public uint DeviceVersionVendor { get; set; } + public uint DeviceVersionProduct { get; set; } + public uint DeviceVersionVersion { get; set; } + public long FirmwareBuild { get; set; } + public short FirmwareVersionMinor { get; set; } + public short FirmwareVersionMajor { get; set; } + + public Tile() { + + } + + public void LoadPayload(Payload payload) { + AccelMeasX = payload.GetInt16(); + AccelMeasY = payload.GetInt16(); + AccelMeasZ = payload.GetInt16(); + // Skip 2 bytes for reserved + payload.Advance(2); + UserX = payload.GetFloat32(); + UserY = payload.GetFloat32(); + Width = payload.GetUint8(); + Height = payload.GetUint8(); + // Skip 2 bytes for reserved + payload.Advance(2); + DeviceVersionVendor = payload.GetUInt32(); + DeviceVersionProduct = payload.GetUInt32(); + DeviceVersionVendor = payload.GetUInt32(); + FirmwareBuild = payload.GetInt64(); + // Skip 8 bytes for reserved + payload.Advance(8); + FirmwareVersionMinor = payload.GetInt16(); + FirmwareVersionMajor = payload.GetInt16(); + } + } +} \ No newline at end of file diff --git a/src/LifxNet/Utilities.cs b/src/LifxNet/Utilities.cs index 889e0a5..6879a6b 100644 --- a/src/LifxNet/Utilities.cs +++ b/src/LifxNet/Utilities.cs @@ -4,7 +4,7 @@ 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(LifxColor 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); @@ -26,9 +26,9 @@ public static UInt16[] RgbToHsl(LifxColor rgb) { double s = (max == 0) ? 0.0 : (1.0 - (min / max)); return new[] { - (UInt16) (h / 360 * 65535), - (UInt16) (s * 65535), - (UInt16) (max * 65535) + (ushort) (h / 360 * 65535), + (ushort) (s * 65535), + (ushort) (max * 65535) }; } } diff --git a/src/SampleApps/SampleApp.Universal/App.xaml.cs b/src/SampleApps/SampleApp.Universal/App.xaml.cs index c46e5fc..503bf7c 100644 --- a/src/SampleApps/SampleApp.Universal/App.xaml.cs +++ b/src/SampleApps/SampleApp.Universal/App.xaml.cs @@ -11,8 +11,7 @@ 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(). diff --git a/src/SampleApps/SampleApp.Universal/MainPage.xaml.cs b/src/SampleApps/SampleApp.Universal/MainPage.xaml.cs index 6638749..c0f1574 100644 --- a/src/SampleApps/SampleApp.Universal/MainPage.xaml.cs +++ b/src/SampleApps/SampleApp.Universal/MainPage.xaml.cs @@ -29,10 +29,11 @@ public MainPage() protected async override void OnNavigatedTo(NavigationEventArgs e) { base.OnNavigatedTo(e); - client = await LifxClient.CreateAsync(); + client = new LifxClient(); client.Discovered += Client_DeviceDiscovered; client.Lost += Client_DeviceLost; client.StartDeviceDiscovery(); + await Task.FromResult(true); } protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) { 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 9bf5161..78301bd 100644 --- a/src/SampleApps/SampleApp.netcore/Program.cs +++ b/src/SampleApps/SampleApp.netcore/Program.cs @@ -5,15 +5,13 @@ namespace SampleApp.NET462 { class Program { - static LifxClient client; + static LifxClient _client; static void Main(string[] args) { - var task = LifxClient.CreateAsync(); - task.Wait(); - client = task.Result; - client.Discovered += Client_DeviceDiscovered; - client.Lost += Client_DeviceLost; - client.StartDeviceDiscovery(); + _client = new LifxClient(); + _client.Discovered += Client_DeviceDiscovered; + _client.Lost += Client_DeviceLost; + _client.StartDeviceDiscovery(); Console.ReadKey(); } @@ -25,9 +23,9 @@ private static void Client_DeviceLost(object sender, LifxClient.DiscoveryEventAr private static async void Client_DeviceDiscovered(object sender, LifxClient.DiscoveryEventArgs e) { Console.WriteLine($"Device {e.Device.MacAddressName} found @ {e.Device.HostName}"); - var version = await client.GetDeviceVersionAsync(e.Device); + var version = await _client.GetDeviceVersionAsync(e.Device); //var label = await client.GetDeviceLabelAsync(e.Device); - var state = await client.GetLightStateAsync(e.Device as LightBulb); + 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}"); } } From ebd98ca41ebb01a3d39bc9c34634b51cc29e3814 Mon Sep 17 00:00:00 2001 From: d8ahazard Date: Tue, 16 Feb 2021 13:51:15 -0600 Subject: [PATCH 11/37] Cleanup, fixes, add remaining device "stuff". --- src/LifxNet/Device.cs | 10 +- src/LifxNet/LifxClient.DeviceOperations.cs | 25 +- src/LifxNet/LifxClient.Discovery.cs | 74 ++--- src/LifxNet/LifxClient.LightOperations.cs | 59 ++-- src/LifxNet/LifxClient.MultizoneOperations.cs | 23 +- src/LifxNet/LifxClient.SwitchOperations.cs | 42 +++ src/LifxNet/LifxClient.TileOperations.cs | 86 +++++- src/LifxNet/LifxClient.cs | 153 ++++------ src/LifxNet/LifxNet.csproj | 2 +- src/LifxNet/LifxResponses.cs | 41 ++- src/LifxNet/LightBulb.cs | 6 +- src/LifxNet/MessageType.cs | 8 + src/LifxNet/Payload.cs | 289 +++++++++++++----- src/LifxNet/Strip.cs | 18 -- src/LifxNet/Switch.cs | 18 -- src/LifxNet/TileGroup.cs | 22 -- 16 files changed, 502 insertions(+), 374 deletions(-) create mode 100644 src/LifxNet/LifxClient.SwitchOperations.cs delete mode 100644 src/LifxNet/Strip.cs delete mode 100644 src/LifxNet/Switch.cs diff --git a/src/LifxNet/Device.cs b/src/LifxNet/Device.cs index f494066..8f0fbf0 100644 --- a/src/LifxNet/Device.cs +++ b/src/LifxNet/Device.cs @@ -6,7 +6,7 @@ namespace LifxNet { /// LIFX Generic Device /// public abstract class Device { - internal Device(string hostname, byte[] macAddress, byte service, uint port, uint productId) { + internal Device(string hostname, byte[] macAddress, byte service, uint port) { if (hostname == null) throw new ArgumentNullException(nameof(hostname)); if (string.IsNullOrWhiteSpace(hostname)) @@ -16,7 +16,6 @@ internal Device(string hostname, byte[] macAddress, byte service, uint port, uin Service = service; Port = port; LastSeen = DateTime.MinValue; - ProductId = productId; } /// @@ -33,12 +32,7 @@ internal Device(string hostname, byte[] macAddress, byte service, uint port, uin /// Service port /// public uint Port { get; } - - /// - /// Product ID. Not a part of the "real" message, but handy to have regardless - /// - public uint ProductId { get; } - + internal DateTime LastSeen { get; set; } /// diff --git a/src/LifxNet/LifxClient.DeviceOperations.cs b/src/LifxNet/LifxClient.DeviceOperations.cs index 8d497de..e470340 100644 --- a/src/LifxNet/LifxClient.DeviceOperations.cs +++ b/src/LifxNet/LifxClient.DeviceOperations.cs @@ -26,10 +26,7 @@ public async Task SetDevicePowerStateAsync(Device device, bool isOn) { if (device == null) throw new ArgumentNullException(nameof(device)); Debug.WriteLine($"Sending DeviceSetPower({isOn}) to {device.HostName}"); - FrameHeader header = new FrameHeader { - Identifier = GetNextIdentifier(), - AcknowledgeRequired = true - }; + FrameHeader header = new FrameHeader(GetNextIdentifier(), true); _ = await BroadcastMessageAsync(device.HostName, header, MessageType.DeviceSetPower, (ushort) (isOn ? 65535 : 0)).ConfigureAwait(false); @@ -44,10 +41,7 @@ public async Task SetDevicePowerStateAsync(Device device, bool isOn) { if (device == null) throw new ArgumentNullException(nameof(device)); - FrameHeader header = new FrameHeader { - Identifier = GetNextIdentifier(), - AcknowledgeRequired = false - }; + FrameHeader header = new FrameHeader(GetNextIdentifier()); var resp = await BroadcastMessageAsync(device.HostName, header, MessageType.DeviceGetLabel).ConfigureAwait(false); return resp.Label; @@ -63,10 +57,7 @@ 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); } @@ -78,10 +69,7 @@ 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(), true); return BroadcastMessageAsync(device.HostName, header, MessageType.DeviceGetVersion); } @@ -94,10 +82,7 @@ public Task GetDeviceHostFirmwareAsync(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.DeviceGetHostFirmware); } diff --git a/src/LifxNet/LifxClient.Discovery.cs b/src/LifxNet/LifxClient.Discovery.cs index b313a11..9576b1a 100644 --- a/src/LifxNet/LifxClient.Discovery.cs +++ b/src/LifxNet/LifxClient.Discovery.cs @@ -13,39 +13,40 @@ public partial class LifxClient { private uint _discoverSourceId; private CancellationTokenSource? _discoverCancellationSource; private readonly Dictionary _discoveredBulbs = new Dictionary(); - private readonly int[] _stripIds = {31,32,38}; + private readonly int[] _stripIds = {31, 32, 38}; private readonly int[] _tileIds = {55}; private readonly int[] _switchIds = {70}; private static uint GetNextIdentifier() { - lock (IdentifierLock) - return _identifier++; + lock (IdentifierLock) { + _identifier++; + } + + return _identifier; } /// /// Event fired when a LIFX bulb is discovered on the network /// - public event EventHandler? Discovered; + 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? Lost; + 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 DiscoveryEventArgs : EventArgs { - internal DiscoveryEventArgs(Device device) => Device = device; + public sealed class DeviceDiscoveryEventArgs : EventArgs { + internal DeviceDiscoveryEventArgs(Device device) => Device = device; /// /// The device the event relates to @@ -54,12 +55,13 @@ public sealed class DiscoveryEventArgs : EventArgs { } 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; } @@ -73,57 +75,43 @@ private void ProcessDeviceDiscoveryMessage(IPAddress remoteAddress, LifxResponse 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 }; - var ver = GetDeviceVersionAsync(device).Result; - - if (_stripIds.Contains((int) ver.Product)) { - var dev = new Strip(address, mac, svc, port, ver.Product){LastSeen = lastSeen}; - _discoveredBulbs[id] = dev; - devices.Add(dev); - } else if (_switchIds.Contains((int) ver.Product)) { - var dev = new Switch(address, mac, svc, port, ver.Product){LastSeen = lastSeen}; - _discoveredBulbs[id] = dev; - devices.Add(dev); - } else if (_tileIds.Contains((int) ver.Product)) { - var dev = new TileGroup(address, mac, svc, port, ver.Product){LastSeen = lastSeen}; - _discoveredBulbs[id] = dev; - devices.Add(dev); - } else { - _discoveredBulbs[id] = device; - devices.Add(device); - } - Discovered?.Invoke(this, new DiscoveryEventArgs(device)); + _discoveredBulbs[id] = device; + devices.Add(device); + DeviceDiscovered?.Invoke(this, new DeviceDiscoveryEventArgs(device)); } /// /// Begins searching for bulbs. /// - /// - /// + /// + /// /// 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(); + _discoverSourceId = GetNextIdentifier(); //Start discovery thread Task.Run(async () => { - Debug.WriteLine("Sending GetServices"); - FrameHeader header = new FrameHeader { - Identifier = source - }; + Debug.WriteLine("Sending GetServices..."); + FrameHeader header = new FrameHeader(_discoverSourceId); while (!token.IsCancellationRequested) { try { - await BroadcastMessageAsync(null, header, MessageType.DeviceGetService); - } catch { - // ignored + await BroadcastMessageAsync("255.255.255.255", header, + MessageType.DeviceGetService); + } catch (Exception e) { + Debug.WriteLine("Broadcast exception: " + e.Message); } - await Task.Delay(5000, token); + await Task.Delay(10000, token); var lostDevices = devices.Where(d => (DateTime.UtcNow - d.LastSeen).TotalMinutes > 5).ToArray(); if (!lostDevices.Any()) { continue; @@ -132,7 +120,7 @@ public void StartDeviceDiscovery() { foreach (var device in lostDevices) { devices.Remove(device); _discoveredBulbs.Remove(device.MacAddressName); - Lost?.Invoke(this, new DiscoveryEventArgs(device)); + DeviceLost?.Invoke(this, new DeviceDiscoveryEventArgs(device)); } } }, token); diff --git a/src/LifxNet/LifxClient.LightOperations.cs b/src/LifxNet/LifxClient.LightOperations.cs index 8956059..3ae19a4 100644 --- a/src/LifxNet/LifxClient.LightOperations.cs +++ b/src/LifxNet/LifxClient.LightOperations.cs @@ -55,10 +55,7 @@ public async Task SetLightPowerAsync(LightBulb bulb, TimeSpan transitionDuration 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((ushort) transitionDuration.TotalMilliseconds); @@ -79,10 +76,7 @@ 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; } @@ -138,10 +132,7 @@ public async Task SetColorAsync(LightBulb bulb, } Debug.WriteLine("Setting color to {0}", bulb.HostName); - FrameHeader header = new FrameHeader { - Identifier = GetNextIdentifier(), - AcknowledgeRequired = true - }; + FrameHeader header = new FrameHeader(GetNextIdentifier(), true); var duration = (uint) transitionDuration.TotalMilliseconds; await BroadcastMessageAsync(bulb.HostName, header, @@ -151,42 +142,32 @@ await BroadcastMessageAsync(bulb.HostName, header, ); } - /* + 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) { + 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); } @@ -200,10 +181,7 @@ public async Task GetInfraredAsync(LightBulb bulb) { if (bulb == null) throw new ArgumentNullException(nameof(bulb)); - FrameHeader header = new FrameHeader { - Identifier = GetNextIdentifier(), - AcknowledgeRequired = true - }; + FrameHeader header = new FrameHeader(GetNextIdentifier()); return (await BroadcastMessageAsync( bulb.HostName, header, MessageType.InfraredGet).ConfigureAwait(false)).Brightness; } @@ -218,12 +196,9 @@ public async Task SetInfraredAsync(Device device, ushort brightness) { if (device == null) throw new ArgumentNullException(nameof(device)); Debug.WriteLine($"Sending SetInfrared({brightness}) to {device.HostName}"); - FrameHeader header = new FrameHeader { - Identifier = GetNextIdentifier(), - AcknowledgeRequired = true - }; + FrameHeader header = new FrameHeader(GetNextIdentifier(), true); - _ = await BroadcastMessageAsync(device.HostName, header, + await BroadcastMessageAsync(device.HostName, header, MessageType.InfraredSet, brightness).ConfigureAwait(false); } } diff --git a/src/LifxNet/LifxClient.MultizoneOperations.cs b/src/LifxNet/LifxClient.MultizoneOperations.cs index f7309cc..543bcef 100644 --- a/src/LifxNet/LifxClient.MultizoneOperations.cs +++ b/src/LifxNet/LifxClient.MultizoneOperations.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Net; using System.Threading.Tasks; namespace LifxNet { @@ -19,7 +18,7 @@ public partial class LifxClient : IDisposable { /// /// /// - public async Task SetColorZones(LightBulb bulb, int startIndex, int endIndex, LifxColor color, + public async Task SetColorZonesAsync(LightBulb bulb, int startIndex, int endIndex, LifxColor color, TimeSpan transitionDuration) { if (bulb == null) throw new ArgumentNullException(nameof(bulb)); @@ -30,10 +29,7 @@ public async Task SetColorZones(LightBulb bulb, int startIndex, int endIndex, Li if (startIndex > endIndex) throw new ArgumentOutOfRangeException(nameof(startIndex)); - FrameHeader header = new FrameHeader { - Identifier = GetNextIdentifier(), - AcknowledgeRequired = true - }; + FrameHeader header = new FrameHeader(GetNextIdentifier(), true); var duration = (uint) transitionDuration.TotalMilliseconds; await BroadcastMessageAsync(bulb.HostName, header, MessageType.SetColorZones, (byte) startIndex, (byte) endIndex, color, duration, Apply); @@ -60,10 +56,7 @@ public async Task SetExtendedColorZonesAsync(LightBulb bulb, TimeSpan transition throw new ArgumentOutOfRangeException(nameof(transitionDuration)); } - FrameHeader header = new FrameHeader { - Identifier = GetNextIdentifier(), - AcknowledgeRequired = true - }; + FrameHeader header = new FrameHeader(GetNextIdentifier(), true); var duration = (uint) transitionDuration.TotalMilliseconds; var count = (byte) colors.Count; var colorBytes = new List(); @@ -84,10 +77,7 @@ await BroadcastMessageAsync(bulb.HostName, header, public Task GetExtendedColorZonesAsync(LightBulb bulb) { if (bulb == null) throw new ArgumentNullException(nameof(bulb)); - FrameHeader header = new FrameHeader { - Identifier = GetNextIdentifier(), - AcknowledgeRequired = false - }; + FrameHeader header = new FrameHeader(GetNextIdentifier()); return BroadcastMessageAsync( bulb.HostName, header, MessageType.GetExtendedColorZones); } @@ -104,10 +94,7 @@ public Task GetColorZonesAsync(LightBulb bulb, int startIndex, int if (bulb == null) throw new ArgumentNullException(nameof(bulb)); if (startIndex > endIndex) throw new ArgumentOutOfRangeException(nameof(startIndex)); - FrameHeader header = new FrameHeader { - Identifier = GetNextIdentifier(), - AcknowledgeRequired = false - }; + FrameHeader header = new FrameHeader(GetNextIdentifier()); return BroadcastMessageAsync( bulb.HostName, header, MessageType.GetColorZones, (byte) startIndex, (byte) endIndex); } diff --git a/src/LifxNet/LifxClient.SwitchOperations.cs b/src/LifxNet/LifxClient.SwitchOperations.cs new file mode 100644 index 0000000..226fe22 --- /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(LightBulb bulb, int relayIndex) { + if (bulb == null) + throw new ArgumentNullException(nameof(bulb)); + + FrameHeader header = new FrameHeader(GetNextIdentifier()); + return await BroadcastMessageAsync( + bulb.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(LightBulb bulb, int relayIndex, bool enable) { + if (bulb == null) + throw new ArgumentNullException(nameof(bulb)); + var level = enable ? 65535 : 0; + + FrameHeader header = new FrameHeader(GetNextIdentifier()); + return await BroadcastMessageAsync( + bulb.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 index fc7bc9a..7d3b3a0 100644 --- a/src/LifxNet/LifxClient.TileOperations.cs +++ b/src/LifxNet/LifxClient.TileOperations.cs @@ -1,7 +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(LightBulb 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(LightBulb 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(LightBulb group, int tileIndex, int length, + int x = 0, int y = 0, int width = 8) { + if (group == null) + throw new ArgumentNullException(nameof(group)); + + FrameHeader header = new FrameHeader(GetNextIdentifier()); + return await BroadcastMessageAsync( + group.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(LightBulb bulb, int tileIndex, int length, + long duration, LifxColor[] colors, int x = 0, int y = 0, int width = 8) { + if (bulb == null) + throw new ArgumentNullException(nameof(bulb)); + + FrameHeader header = new FrameHeader(GetNextIdentifier()); + return await BroadcastMessageAsync( + bulb.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 0d40d2d..e8f1ed3 100644 --- a/src/LifxNet/LifxClient.cs +++ b/src/LifxNet/LifxClient.cs @@ -1,11 +1,9 @@ using System; -using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Net; using System.Net.Sockets; -using System.Text; using System.Threading.Tasks; namespace LifxNet { @@ -17,28 +15,27 @@ public partial class LifxClient { private readonly UdpClient _socket; private bool _isRunning; - - /// - /// Create our client directly, and instantiate a new UDP client - /// - public 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); - _isRunning = true; - StartReceiveLoop(); } + /// - /// Create our client directly with a re-usable UDP client. + /// Creates a new LIFX client. /// - /// Optional UDPClient. - public LifxClient(UdpClient client) { - _socket = client; + /// client + public static Task CreateAsync() { + LifxClient client = new LifxClient(); + client.Initialize(); + return Task.FromResult(client); + } + + private void Initialize() { _isRunning = true; - StartReceiveLoop(); + StartReceiveLoop(); } - private void StartReceiveLoop() { Task.Run(async () => { @@ -57,6 +54,7 @@ private void StartReceiveLoop() { private void HandleIncomingMessages(byte[] data, IPEndPoint endpoint) { var remote = endpoint; var msg = ParseMessage(data); + if (remote.Port == 56700) Debug.WriteLine("Message Type: " + msg.Type); switch (msg.Type) { case MessageType.DeviceStateService: ProcessDeviceDiscoveryMessage(remote.Address, msg); @@ -70,8 +68,9 @@ private void HandleIncomingMessages(byte[] data, IPEndPoint endpoint) { break; } - Debug.WriteLine("Received from {0}:{1}", remote, - 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())); } /// @@ -79,70 +78,45 @@ private void HandleIncomingMessages(byte[] data, IPEndPoint endpoint) { /// public void Dispose() { _isRunning = false; - _socket?.Dispose(); + _socket.Dispose(); } - private Task BroadcastMessageAsync(string? hostName, FrameHeader header, MessageType type, + private Task BroadcastMessageAsync(string hostName, FrameHeader header, MessageType type, params object[] args) where T : LifxResponse { - List payload = new List(); - foreach (var arg in args) { - switch (arg) { - case ushort @ushort: - payload.AddRange(BitConverter.GetBytes(@ushort)); - break; - case uint u: - payload.AddRange(BitConverter.GetBytes(u)); - break; - case byte b: - payload.Add(b); - break; - case byte[] bytes: - payload.AddRange(bytes); - break; - case string s: - payload.AddRange( - Encoding.UTF8.GetBytes(s.PadRight(32).Take(32).ToArray())); //All strings are 32 bytes - break; - case LifxColor c: - payload.AddRange(c.ToBytes()); - break; - default: - throw new NotSupportedException(args.GetType().FullName); - } - } + Debug.WriteLine("Broadcasting " + type + " to " + hostName); + var payload = new Payload(args); - return BroadcastMessagePayloadAsync(hostName, header, type, payload.ToArray()); + return BroadcastPayloadAsync(hostName, header, type, payload); } - private async Task BroadcastMessagePayloadAsync(string? hostName, FrameHeader header, MessageType type, - byte[] payload) + 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 - 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)) { tcs = new TaskCompletionSource(); - - void Action(LifxResponse r) { - if (r.GetType() == typeof(T)) tcs.TrySetResult((T) r); - } - - _taskCompletions[header.Identifier] = Action; + Action action = (r) => { + if (r.GetType() == typeof(T)) + tcs.TrySetResult((T) r); + }; + _taskCompletions[header.Identifier] = action; } using (MemoryStream stream = new MemoryStream()) { - WritePacketToStream(stream, header, (ushort) type, payload); + WritePacketToStream(stream, header, (UInt16) type, payload); var msg = stream.ToArray(); await _socket.SendAsync(msg, msg.Length, hostName, Port); } @@ -150,19 +124,17 @@ void Action(LifxResponse r) { //{ // await WritePacketToStreamAsync(stream, header, (UInt16)type, payload).ConfigureAwait(false); //} - T result = default; - if (tcs == null) { - return result; - } - - 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); + T result = default(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); + } } return result; @@ -189,12 +161,12 @@ private static LifxResponse ParseMessage(byte[] packet) { 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[] { })); + return LifxResponse.Create(header, type, source, + new Payload(size > 36 ? br.ReadBytes(size - 36) : new byte[] { })); } - private static void WritePacketToStream(Stream outStream, FrameHeader header, ushort type, byte[] payload) { + private static void WritePacketToStream(Stream outStream, FrameHeader header, ushort type, Payload payload) { using var dw = new BinaryWriter(outStream); - //BinaryWriter bw = new BinaryWriter(ms); #region Frame @@ -253,7 +225,7 @@ private static void WritePacketToStream(Stream outStream, FrameHeader header, us dw.Write(type); //packet _type dw.Write((ushort) 0); //reserved - dw.Write(payload); + dw.Write(payload.ToArray()); dw.Flush(); } } @@ -262,23 +234,20 @@ internal class FrameHeader { public uint Identifier; public byte Sequence; public bool AcknowledgeRequired; - public readonly bool ResponseRequired; - public byte[] TargetMacAddress; - public DateTime AtTime; + public bool ResponseRequired; + public byte[] TargetMacAddress = {0, 0, 0, 0, 0, 0, 0, 0}; + public DateTime AtTime = DateTime.MinValue; 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 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()); - } + get { return string.Join(":", TargetMacAddress.Take(6).Select(tb => tb.ToString("X2")).ToArray()); } } } } \ No newline at end of file diff --git a/src/LifxNet/LifxNet.csproj b/src/LifxNet/LifxNet.csproj index eb2cbc2..6745107 100644 --- a/src/LifxNet/LifxNet.csproj +++ b/src/LifxNet/LifxNet.csproj @@ -19,7 +19,7 @@ git 8.0 enable - 2.2.4 + 2.2.5 Add initial support for Multizone diff --git a/src/LifxNet/LifxResponses.cs b/src/LifxNet/LifxResponses.cs index bfef335..2c2f326 100644 --- a/src/LifxNet/LifxResponses.cs +++ b/src/LifxNet/LifxResponses.cs @@ -10,6 +10,7 @@ namespace LifxNet { /// public abstract class LifxResponse { internal static LifxResponse Create(FrameHeader header, MessageType type, uint source, Payload payload) { + payload.Reset(); switch (type) { case MessageType.DeviceAcknowledgement: return new AcknowledgementResponse(header, type, payload, source); @@ -37,6 +38,8 @@ internal static LifxResponse Create(FrameHeader header, MessageType type, uint s return new StateDeviceChainResponse(header, type, payload, source); case MessageType.StateTileState64: return new StateTileState64Response(header, type, payload, source); + case MessageType.StateRelayPower: + return new StateRelayPowerResponse(header, type, payload, source); default: return new UnknownResponse(header, type, payload, source); } @@ -96,12 +99,13 @@ internal StateZoneResponse(FrameHeader header, MessageType type, Payload payload public LifxColor Color { get; } } - + /// /// 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, + internal StateDeviceChainResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base( + header, type, payload, source) { Tiles = new List(); StartIndex = payload.GetUint8(); @@ -110,8 +114,10 @@ internal StateDeviceChainResponse(FrameHeader header, MessageType type, Payload tile.LoadPayload(payload); Tiles.Add(tile); } + TotalCount = payload.GetUint8(); - if (TotalCount != Tiles.Count) Debug.WriteLine($"Warning, tile count doesn't match: {TotalCount} : {Tiles.Count}"); + if (TotalCount != Tiles.Count) + Debug.WriteLine($"Warning, tile count doesn't match: {TotalCount} : {Tiles.Count}"); payload.Reset(); } @@ -143,6 +149,7 @@ internal StateMultiZoneResponse(FrameHeader header, MessageType type, Payload pa while (payload.HasContent()) { Colors.Add(payload.GetColor()); } + payload.Reset(); } @@ -175,6 +182,7 @@ internal StateExtendedColorZonesResponse(FrameHeader header, MessageType type, P while (payload.HasContent()) { Colors.Add(payload.GetColor()); } + payload.Reset(); } @@ -210,11 +218,11 @@ internal StateServiceResponse(FrameHeader header, MessageType type, Payload payl private byte Service { get; } private uint Port { get; } } - + /// /// Response to any message sent with ack_required set to 1. /// - internal class StateTileState64Response : LifxResponse { + public class StateTileState64Response : LifxResponse { internal StateTileState64Response(FrameHeader header, MessageType type, Payload payload, uint source) : base( header, type, payload, source) { TileIndex = payload.GetUint8(); @@ -232,6 +240,7 @@ internal StateTileState64Response(FrameHeader header, MessageType type, Payload } } } + public uint TileIndex { get; } public uint X { get; } public uint Y { get; } @@ -370,6 +379,28 @@ internal StateHostFirmwareResponse(FrameHeader header, MessageType type, Payload public uint Version { get; } } + /// + /// 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(); + payload.Reset(); + } + + /// + /// 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) { diff --git a/src/LifxNet/LightBulb.cs b/src/LifxNet/LightBulb.cs index 6832df6..a1e2e09 100644 --- a/src/LifxNet/LightBulb.cs +++ b/src/LifxNet/LightBulb.cs @@ -10,8 +10,10 @@ public sealed class LightBulb : Device { /// /// /// - public LightBulb(string hostname, byte[] macAddress, byte service = 0, uint port = 0, int productId = 0) : base(hostname, - macAddress, service, port, productId) { + /// + 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 442dd43..0f0aafb 100644 --- a/src/LifxNet/MessageType.cs +++ b/src/LifxNet/MessageType.cs @@ -48,6 +48,7 @@ internal enum MessageType : ushort { InfraredGet = 120, InfraredState = 121, InfraredSet = 122, + //Multi zone SetColorZones = 501, GetColorZones = 502, @@ -56,6 +57,7 @@ internal enum MessageType : ushort { SetExtendedColorZones = 510, GetExtendedColorZones = 511, StateExtendedColorZones = 512, + //Tile GetDeviceChain = 701, StateDeviceChain = 702, @@ -63,6 +65,12 @@ internal enum MessageType : ushort { GetTileState64 = 707, StateTileState64 = 711, SetTileState64 = 715, + + //Switch + GetRelayPower = 816, + SetRelayPower = 817, + StateRelayPower = 818, + //Unofficial LightGetTemperature = 0x6E, diff --git a/src/LifxNet/Payload.cs b/src/LifxNet/Payload.cs index b215e73..fe42073 100644 --- a/src/LifxNet/Payload.cs +++ b/src/LifxNet/Payload.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -using System.Data.Common; using System.Diagnostics; +using System.IO; using System.Linq; using System.Text; @@ -13,44 +13,114 @@ namespace LifxNet { /// at which time a message will be logged. Should eventually throw an error or something... /// public class Payload { - private byte[] Data { get; set; } - private int Pointer { get; set; } - + 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.Length; + 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); + } + + public Payload(Object[] args) { + _data = new List(); + foreach (var arg in args) { + switch (arg) { + case ushort @ushort: + Add(@ushort); + break; + case uint u: + Add(u); + break; + case byte b: + Add(b); + break; + case byte[] bytes: + Add(bytes); + break; + case string s: + Add(s.PadRight(32).Take(32).ToString()); + break; + case LifxColor c: + Add(c); + break; + case LifxColor[] colors: + Add(colors); + 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; + 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; + _data = data.ToList(); + _ms = new MemoryStream(data); + _len = _ms.Length; + _br = new BinaryReader(_ms); } /// - /// Return our base byte array + /// Return our base byte list as an array /// /// public byte[] ToArray() { - return Data; + return _data.ToArray(); } /// - /// Convert base byte array to a list + /// Return our base byte list /// /// public List ToList() { - return Data.ToList(); + return _data; } /// - /// Serialize base byte array to a string + /// Serialize base byte list to a string /// /// public override string ToString() { - return Data.ToString(); + return _data.ToString(); } /// @@ -58,7 +128,7 @@ public override string ToString() { /// /// public bool HasContent() { - return Pointer < Data.Length; + return _ms.Position < _len; } /// @@ -66,8 +136,11 @@ public bool HasContent() { /// /// How far to rewind. Default is 1. public void Rewind(int len = 1) { - Pointer -= len; - if (Pointer < 0) Pointer = 0; + if (_ms.Position - len < 0) { + Reset(); + } else { + _ms.Seek(len * -1, SeekOrigin.Current); + } } /// @@ -75,22 +148,25 @@ public void Rewind(int len = 1) { /// /// How far to advance. Default is 1. public void Advance(int len = 1) { - Pointer += len; - if (Pointer >= Data.Length) Pointer = Data.Length - 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() { - Pointer = Data.Length - 1; + _ms.Seek(0, SeekOrigin.End); } /// /// Reset our pointer to 0 /// public void Reset() { - Pointer = 0; + _ms.Seek(0, SeekOrigin.Begin); } /// @@ -98,30 +174,30 @@ public void Reset() { /// /// public LifxColor GetColor() { - if (Pointer + 16 < Data.Length) { + 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); } - FastForward(); - Debug.WriteLine($"Error getting color, pointer {Pointer} is out of range: " + Data.Length); + return new LifxColor(); } - + /// /// Read Uint8 from array and increment pointer 1 byte /// /// byte public byte GetUint8() { - if (Pointer + 1 < Data.Length) { - var output = Data[Pointer]; - Pointer++; - return output; + try { + return _br.ReadByte(); + } catch { + Debug.WriteLine("Error reading byte, pos is " + _ms.Position); } - FastForward(); - Debug.WriteLine($"Error getting Uint8 from payload, pointer {Pointer} out of range: " + Data.Length); + return 0; } @@ -130,13 +206,12 @@ public byte GetUint8() { /// /// ushort public ushort GetUInt16() { - if (Pointer + 2 < Data.Length) { - var output = BitConverter.ToUInt16(Data.ToArray(), Pointer); - Pointer += 2; - return output; + try { + return _br.ReadUInt16(); + } catch { + Debug.WriteLine($"Error getting Uint16 from payload, pointer {_ms.Position} of range: " + _len); } - FastForward(); - Debug.WriteLine($"Error getting Uint16 from payload, pointer {Pointer} of range: " + Data.Length); + return 0; } @@ -145,13 +220,12 @@ public ushort GetUInt16() { /// /// short public short GetInt16() { - if (Pointer + 2 < Data.Length) { - var output = BitConverter.ToInt16(Data.ToArray(), Pointer); - Pointer += 2; - return output; + try { + return _br.ReadInt16(); + } catch { + Debug.WriteLine($"Error getting int16 from payload, pointer {_ms.Position} of range: " + _len); } - FastForward(); - Debug.WriteLine($"Error getting int16 from payload, pointer {Pointer} of range: " + Data.Length); + return 0; } @@ -160,58 +234,54 @@ public short GetInt16() { /// /// int public int GetInt32() { - if (Pointer + 4 < Data.Length) { - var output = BitConverter.ToInt32(Data.ToArray(), Pointer); - Pointer += 4; - return output; + try { + return _br.ReadInt32(); + } catch { + Debug.WriteLine($"Error getting Int32 from payload, pointer {_ms.Position} of range: " + _len); } - FastForward(); - Debug.WriteLine($"Error getting Int32 from payload, pointer {Pointer} of range: " + Data.Length); + return 0; } - + /// /// Read a UInt32 from array and increment pointer 4 bits. /// /// public uint GetUInt32() { - if (Pointer + 4 < Data.Length) { - var output = BitConverter.ToUInt32(Data.ToArray(), Pointer); - Pointer += 4; - return output; + try { + return _br.ReadUInt32(); + } catch { + Debug.WriteLine($"Error getting Uint32 from payload, pointer {_ms.Position} of range: " + _len); } - FastForward(); - Debug.WriteLine($"Error getting Uint32 from payload, pointer {Pointer} of range: " + Data.Length); + return 0; } - + /// /// Read an Int64 from array and increment pointer 8 bits. /// /// long public long GetInt64() { - if (Pointer + 8 < Data.Length) { - var output = BitConverter.ToInt64(Data.ToArray(), Pointer); - Pointer += 8; - return output; + try { + return _br.ReadInt64(); + } catch { + Debug.WriteLine($"Error getting Int64 from payload, pointer {_ms.Position} of range: " + _len); } - FastForward(); - Debug.WriteLine($"Error getting Int64 from payload, pointer {Pointer} of range: " + Data.Length); + return 0; } - + /// /// Read a UInt64 from array and increment pointer 8 bits. /// /// ulong public ulong GetUInt64() { - if (Pointer + 8 < Data.Length) { - var output = BitConverter.ToUInt64(Data.ToArray(), Pointer); - Pointer += 8; - return output; + try { + return _br.ReadUInt64(); + } catch { + Debug.WriteLine($"Error getting Uint64 from payload, pointer {_ms.Position} of range: " + _len); } - FastForward(); - Debug.WriteLine($"Error getting Uint64 from payload, pointer {Pointer} of range: " + Data.Length); + return 0; } @@ -220,13 +290,12 @@ public ulong GetUInt64() { /// /// float public float GetFloat32() { - if (Pointer + 4 < Data.Length) { - var output = BitConverter.ToSingle(Data.ToArray(), Pointer); - Pointer += 4; - return output; + try { + return _br.ReadSingle(); + } catch { + Debug.WriteLine($"Error getting Float32 from payload, pointer {_ms.Position} of range: " + _len); } - FastForward(); - Debug.WriteLine($"Error getting Float32 from payload, pointer {Pointer} of range: " + Data.Length); + return 0; } @@ -235,17 +304,69 @@ public float GetFloat32() { /// /// The number of chars to read. If none specified, will read the entire payload /// string - public string GetString(int length = -1) { - if (length == -1) length = Data.Length - 1 - Pointer; - if (Pointer + length < Data.Length) { - var output = Encoding.UTF8.GetString(Data, Pointer, length); - Pointer += length; - return output; + 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); } - var str = Encoding.UTF8.GetString(Data, Pointer, Data.Length - 1 - Pointer); - Debug.WriteLine($"Error getting string, pointer {Pointer} out of range: " + Data.Length); - FastForward(); - return str; } } } \ No newline at end of file diff --git a/src/LifxNet/Strip.cs b/src/LifxNet/Strip.cs deleted file mode 100644 index 719caa5..0000000 --- a/src/LifxNet/Strip.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace LifxNet { - /// - /// LIFX multizone - /// - public sealed class Strip : Device { - /// - /// Initializes a new instance of a strip instead of relying on discovery. At least the host name must be provide for the device to be usable. - /// - /// Required - /// - /// - /// - /// - public Strip(string hostname, byte[] macAddress, byte service = 0, uint port = 0, uint productId = 0) : base(hostname, - macAddress, service, port, productId) { - } - } -} \ No newline at end of file diff --git a/src/LifxNet/Switch.cs b/src/LifxNet/Switch.cs deleted file mode 100644 index 2353541..0000000 --- a/src/LifxNet/Switch.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace LifxNet { - /// - /// LIFX switch - /// - public sealed class Switch : 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 Switch(string hostname, byte[] macAddress, byte service = 0, uint port = 0, uint productId = 0) : base(hostname, - macAddress, service, port, productId) { - } - } -} \ No newline at end of file diff --git a/src/LifxNet/TileGroup.cs b/src/LifxNet/TileGroup.cs index 8f3c3d4..11325e3 100644 --- a/src/LifxNet/TileGroup.cs +++ b/src/LifxNet/TileGroup.cs @@ -1,27 +1,6 @@ using System; namespace LifxNet { - /// - /// LIFX tile - /// - public sealed class TileGroup : 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 TileGroup(string hostname, byte[] macAddress, byte service = 0, uint port = 0, uint productId = 0) : base(hostname, - macAddress, service, port, productId) { - } - - public void LoadPayload() { - - } - } - public class Tile { public int AccelMeasX { get; set; } public int AccelMeasY { get; set; } @@ -38,7 +17,6 @@ public class Tile { public short FirmwareVersionMajor { get; set; } public Tile() { - } public void LoadPayload(Payload payload) { From a95448c8a55487233662de8a75bcf2050aedb704 Mon Sep 17 00:00:00 2001 From: d8ahazard Date: Tue, 16 Feb 2021 13:51:30 -0600 Subject: [PATCH 12/37] Re-rename methods... --- .../SampleApp.Universal/MainPage.xaml.cs | 14 +++++++------- src/SampleApps/SampleApp.netcore/Program.cs | 17 ++++++++--------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/SampleApps/SampleApp.Universal/MainPage.xaml.cs b/src/SampleApps/SampleApp.Universal/MainPage.xaml.cs index c0f1574..1751a0f 100644 --- a/src/SampleApps/SampleApp.Universal/MainPage.xaml.cs +++ b/src/SampleApps/SampleApp.Universal/MainPage.xaml.cs @@ -29,21 +29,21 @@ public MainPage() protected async override void OnNavigatedTo(NavigationEventArgs e) { base.OnNavigatedTo(e); - client = new LifxClient(); - client.Discovered += Client_DeviceDiscovered; - client.Lost += Client_DeviceLost; + client = await LifxClient.CreateAsync(); + client.DeviceDiscovered += ClientDeviceDeviceDiscovered; + client.DeviceLost += ClientDeviceDeviceLost; client.StartDeviceDiscovery(); await Task.FromResult(true); } protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) { - client.Discovered -= Client_DeviceDiscovered; - client.Lost -= Client_DeviceLost; + client.DeviceDiscovered -= ClientDeviceDeviceDiscovered; + client.DeviceLost -= ClientDeviceDeviceLost; client.StopDeviceDiscovery(); client = null; base.OnNavigatingFrom(e); } - private void Client_DeviceLost(object sender, LifxClient.DiscoveryEventArgs e) + private void ClientDeviceDeviceLost(object sender, LifxClient.DeviceDiscoveryEventArgs e) { var _ = Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { @@ -53,7 +53,7 @@ private void Client_DeviceLost(object sender, LifxClient.DiscoveryEventArgs e) }); } - private void Client_DeviceDiscovered(object sender, LifxClient.DiscoveryEventArgs e) + private void ClientDeviceDeviceDiscovered(object sender, LifxClient.DeviceDiscoveryEventArgs e) { var _ = Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { diff --git a/src/SampleApps/SampleApp.netcore/Program.cs b/src/SampleApps/SampleApp.netcore/Program.cs index 78301bd..fa6a735 100644 --- a/src/SampleApps/SampleApp.netcore/Program.cs +++ b/src/SampleApps/SampleApp.netcore/Program.cs @@ -6,27 +6,26 @@ namespace SampleApp.NET462 class Program { static LifxClient _client; - static void Main(string[] args) - { - _client = new LifxClient(); - _client.Discovered += Client_DeviceDiscovered; - _client.Lost += Client_DeviceLost; + 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.DiscoveryEventArgs e) + private static void ClientDeviceLost(object sender, LifxClient.DeviceDiscoveryEventArgs e) { Console.WriteLine("Device lost"); } - private static async void Client_DeviceDiscovered(object sender, LifxClient.DiscoveryEventArgs 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); + 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}"); + Console.WriteLine($"Product: {version.Product}\n\tVendor: {version.Vendor}\n\tVersion: {version.Version} "); } } } From cc3c82c491e846ad8e7ee624582af008c9f4621d Mon Sep 17 00:00:00 2001 From: d8ahazard Date: Tue, 16 Feb 2021 15:17:26 -0600 Subject: [PATCH 13/37] Add remaining missing messages This should be a near-100% complete set of features, as per the official documentation --- src/LifxNet/LifxClient.DeviceOperations.cs | 194 ++++++++++++++++++- src/LifxNet/LifxResponses.cs | 209 +++++++++++++++++++++ src/LifxNet/MessageType.cs | 2 + src/LifxNet/Payload.cs | 4 + 4 files changed, 405 insertions(+), 4 deletions(-) diff --git a/src/LifxNet/LifxClient.DeviceOperations.cs b/src/LifxNet/LifxClient.DeviceOperations.cs index e470340..30702f2 100644 --- a/src/LifxNet/LifxClient.DeviceOperations.cs +++ b/src/LifxNet/LifxClient.DeviceOperations.cs @@ -36,7 +36,7 @@ public async Task SetDevicePowerStateAsync(Device device, bool isOn) { /// Gets the label for the device /// /// - /// + /// The device label public async Task GetDeviceLabelAsync(Device device) { if (device == null) throw new ArgumentNullException(nameof(device)); @@ -69,15 +69,15 @@ public Task GetDeviceVersionAsync(Device device) { if (device == null) throw new ArgumentNullException(nameof(device)); - FrameHeader header = new FrameHeader(GetNextIdentifier(), true); + FrameHeader header = new FrameHeader(GetNextIdentifier()); return BroadcastMessageAsync(device.HostName, header, MessageType.DeviceGetVersion); } /// - /// Gets the device's host firmware + /// Gets Host MCU firmware information. /// /// - /// + /// public Task GetDeviceHostFirmwareAsync(Device device) { if (device == null) throw new ArgumentNullException(nameof(device)); @@ -86,5 +86,191 @@ public Task GetDeviceHostFirmwareAsync(Device device) 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 bulb 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); + } + + /// + /// Set the device group. + /// + /// + /// The new group name + /// + /// + public async Task SetGroupAsync(Device device, string label) { + if (device == null) + throw new ArgumentNullException(nameof(device)); + + 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/LifxResponses.cs b/src/LifxNet/LifxResponses.cs index 2c2f326..55fe659 100644 --- a/src/LifxNet/LifxResponses.cs +++ b/src/LifxNet/LifxResponses.cs @@ -40,6 +40,22 @@ internal static LifxResponse Create(FrameHeader header, MessageType type, uint s return new StateTileState64Response(header, type, payload, source); case MessageType.StateRelayPower: return new StateRelayPowerResponse(header, type, payload, source); + case MessageType.DeviceStateHostInfo: + return new StateHostInfoResponse(header, type, payload, source); + case MessageType.DeviceStateWifiInfo: + return new StateWifiInfoResponse(header, type, payload, source); + case MessageType.DeviceStateWifiFirmware: + return new StateWifiFirmwareResponse(header, type, payload, source); + case MessageType.DeviceStatePower: + return new StatePowerResponse(header, type, payload, source); + case MessageType.DeviceStateInfo: + return new StateInfoResponse(header, type, payload, source); + case MessageType.DeviceStateLocation: + return new StateLocationResponse(header, type, payload, source); + case MessageType.DeviceStateGroup: + return new StateGroupResponse(header, type, payload, source); + case MessageType.DeviceEchoResponse: + return new EchoResponse(header, type, payload, source); default: return new UnknownResponse(header, type, payload, source); } @@ -98,7 +114,200 @@ internal StateZoneResponse(FrameHeader header, MessageType type, Payload payload /// 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(); + payload.Reset(); + } + + /// + /// 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(); + payload.Reset(); + } + + /// + /// 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(); + payload.Reset(); + } + + /// + /// 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(); + payload.Reset(); + } + + /// + /// 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(); + payload.Reset(); + } + + /// + /// 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(); + payload.Reset(); + } + + 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(); + payload.Reset(); + } + 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.Reset(); + } + + /// + /// 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. diff --git a/src/LifxNet/MessageType.cs b/src/LifxNet/MessageType.cs index 0f0aafb..eab1a66 100644 --- a/src/LifxNet/MessageType.cs +++ b/src/LifxNet/MessageType.cs @@ -3,9 +3,11 @@ internal enum MessageType : ushort { //Device Messages DeviceGetService = 0x02, DeviceStateService = 0x03, + //Undocumented? DeviceGetTime = 0x04, DeviceSetTime = 0x05, DeviceStateTime = 0x06, + // Documented DeviceGetHostInfo = 12, DeviceStateHostInfo = 13, DeviceGetHostFirmware = 14, diff --git a/src/LifxNet/Payload.cs b/src/LifxNet/Payload.cs index fe42073..1bf297d 100644 --- a/src/LifxNet/Payload.cs +++ b/src/LifxNet/Payload.cs @@ -187,6 +187,10 @@ public LifxColor GetColor() { return new LifxColor(); } + public byte[] GetBytes(int len) { + return _br.ReadBytes(len); + } + /// /// Read Uint8 from array and increment pointer 1 byte /// From 694664823eafdd03312ae801b36a5e796704bb3f Mon Sep 17 00:00:00 2001 From: d8ahazard Date: Tue, 16 Feb 2021 17:07:13 -0600 Subject: [PATCH 14/37] Fix return type for GetColorZonesAsync Function should return a StateColorZones, not LifxResponse. --- src/LifxNet/LifxClient.MultizoneOperations.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/LifxNet/LifxClient.MultizoneOperations.cs b/src/LifxNet/LifxClient.MultizoneOperations.cs index 543bcef..32c3d80 100644 --- a/src/LifxNet/LifxClient.MultizoneOperations.cs +++ b/src/LifxNet/LifxClient.MultizoneOperations.cs @@ -90,12 +90,12 @@ public Task GetExtendedColorZonesAsync(LightBul /// End index of requested zones /// Either a "StateZone" response for single-zone devices, or "StateMultiZone" response. /// - public Task GetColorZonesAsync(LightBulb bulb, int startIndex, int endIndex) { + public Task GetColorZonesAsync(LightBulb bulb, int startIndex, int endIndex) { if (bulb == null) throw new ArgumentNullException(nameof(bulb)); if (startIndex > endIndex) throw new ArgumentOutOfRangeException(nameof(startIndex)); FrameHeader header = new FrameHeader(GetNextIdentifier()); - return BroadcastMessageAsync( + return BroadcastMessageAsync( bulb.HostName, header, MessageType.GetColorZones, (byte) startIndex, (byte) endIndex); } } From 7c791c493ec08f117ac672e96d309c7c248a532b Mon Sep 17 00:00:00 2001 From: d8ahazard Date: Tue, 16 Feb 2021 17:15:56 -0600 Subject: [PATCH 15/37] Update net core test app to enumerate device-specific info --- src/SampleApps/SampleApp.netcore/Program.cs | 34 ++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/SampleApps/SampleApp.netcore/Program.cs b/src/SampleApps/SampleApp.netcore/Program.cs index fa6a735..3860022 100644 --- a/src/SampleApps/SampleApp.netcore/Program.cs +++ b/src/SampleApps/SampleApp.netcore/Program.cs @@ -1,7 +1,7 @@ using System; using LifxNet; -namespace SampleApp.NET462 +namespace SampleApp.netcore { class Program { @@ -26,6 +26,38 @@ private static async void ClientDeviceDiscovered(object sender, LifxClient.Devic 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}"); Console.WriteLine($"Product: {version.Product}\n\tVendor: {version.Vendor}\n\tVersion: {version.Version} "); + + // 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) { + var fwVersion = await _client.GetDeviceHostFirmwareAsync(e.Device); + if (fwVersion.Version >= 1532997580) extended = true; + } + + int zoneCount; + if (extended) { + var zones = await _client.GetExtendedColorZonesAsync((e.Device as LightBulb)!); + zoneCount = zones.Count; + } else { + // Original device only supports eight zones? + var zones = await _client.GetColorZonesAsync((e.Device as LightBulb)!, 0, 8); + zoneCount = zones.Count; + } + Console.WriteLine($"Device is multi-zone.\r\nExtended Support: {extended}\r\nZone Count: {zoneCount}"); + } + + // Tile + if (version.Product == 55) { + var chain = await _client.GetDeviceChainAsync((e.Device as LightBulb)!); + Console.WriteLine($"Device is a tile group.\r\nTile count: {chain.TotalCount}"); + } + // Switch + if (version.Product == 70) { + var switchState = await _client.GetRelayPowerAsync((e.Device as LightBulb)!, 0); + Console.WriteLine($"Device is a switch. \r\nSwitch State: {switchState.Level}"); + } } } } From 7cd6aba7a151f17aad326f5c93bfcaaac6141407 Mon Sep 17 00:00:00 2001 From: d8ahazard Date: Mon, 22 Feb 2021 08:43:33 -0600 Subject: [PATCH 16/37] Use generic device for tile/switch/multizone ops, not "bulb". It would probably be "better" to create an individual device class for each one, but that requires an additional call to the device to figure out what it is, which seems to be a bit out of scope for the library currently. So, instead, we'll just use the generic "device". --- src/LifxNet/LifxClient.MultizoneOperations.cs | 42 +++++++++---------- src/LifxNet/LifxClient.SwitchOperations.cs | 20 ++++----- src/LifxNet/LifxClient.TileOperations.cs | 24 +++++------ 3 files changed, 43 insertions(+), 43 deletions(-) diff --git a/src/LifxNet/LifxClient.MultizoneOperations.cs b/src/LifxNet/LifxClient.MultizoneOperations.cs index 32c3d80..0946622 100644 --- a/src/LifxNet/LifxClient.MultizoneOperations.cs +++ b/src/LifxNet/LifxClient.MultizoneOperations.cs @@ -10,7 +10,7 @@ public partial class LifxClient : IDisposable { /// /// This message is used for changing the color of either a single or multiple zones. /// - /// Target bulb + /// Target device /// Start index to target /// End index to target /// LifxColor to use @@ -18,10 +18,10 @@ public partial class LifxClient : IDisposable { /// /// /// - public async Task SetColorZonesAsync(LightBulb bulb, int startIndex, int endIndex, LifxColor color, + public async Task SetColorZonesAsync(Device device, int startIndex, int endIndex, LifxColor color, TimeSpan transitionDuration) { - if (bulb == null) - throw new ArgumentNullException(nameof(bulb)); + if (device == null) + throw new ArgumentNullException(nameof(device)); if (transitionDuration.TotalMilliseconds > uint.MaxValue || transitionDuration.Ticks < 0) { throw new ArgumentOutOfRangeException(nameof(transitionDuration)); @@ -31,26 +31,26 @@ public async Task SetColorZonesAsync(LightBulb bulb, int startIndex, int endInde FrameHeader header = new FrameHeader(GetNextIdentifier(), true); var duration = (uint) transitionDuration.TotalMilliseconds; - await BroadcastMessageAsync(bulb.HostName, header, + await BroadcastMessageAsync(device.HostName, header, MessageType.SetColorZones, (byte) startIndex, (byte) endIndex, color, duration, Apply); } /// /// Set a zone of colors /// - /// The device to set + /// 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 /// - /// Thrown if the bulb is null + /// Thrown if the device is null /// Thrown if the duration is longer than the max /// - public async Task SetExtendedColorZonesAsync(LightBulb bulb, TimeSpan transitionDuration, uint index, + public async Task SetExtendedColorZonesAsync(Device device, TimeSpan transitionDuration, uint index, List colors) { - if (bulb == null) - throw new ArgumentNullException(nameof(bulb)); + if (device == null) + throw new ArgumentNullException(nameof(device)); if (transitionDuration.TotalMilliseconds > uint.MaxValue || transitionDuration.Ticks < 0) { throw new ArgumentOutOfRangeException(nameof(transitionDuration)); @@ -64,39 +64,39 @@ public async Task SetExtendedColorZonesAsync(LightBulb bulb, TimeSpan transition colorBytes.AddRange(color.ToBytes()); } - await BroadcastMessageAsync(bulb.HostName, header, + await BroadcastMessageAsync(device.HostName, header, MessageType.SetExtendedColorZones, duration, Apply, index, count, colorBytes); } /// /// Try to get the color zones from our device. /// - /// + /// /// /// - public Task GetExtendedColorZonesAsync(LightBulb bulb) { - if (bulb == null) - throw new ArgumentNullException(nameof(bulb)); + public Task GetExtendedColorZonesAsync(Device device) { + if (device == null) + throw new ArgumentNullException(nameof(device)); FrameHeader header = new FrameHeader(GetNextIdentifier()); return BroadcastMessageAsync( - bulb.HostName, header, MessageType.GetExtendedColorZones); + device.HostName, header, MessageType.GetExtendedColorZones); } /// /// Try to get the color zones from our device, non-extended. /// - /// Target bulb + /// 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(LightBulb bulb, int startIndex, int endIndex) { - if (bulb == null) - throw new ArgumentNullException(nameof(bulb)); + 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( - bulb.HostName, header, MessageType.GetColorZones, (byte) startIndex, (byte) endIndex); + device.HostName, header, MessageType.GetColorZones, (byte) startIndex, (byte) endIndex); } } } \ No newline at end of file diff --git a/src/LifxNet/LifxClient.SwitchOperations.cs b/src/LifxNet/LifxClient.SwitchOperations.cs index 226fe22..b8174e0 100644 --- a/src/LifxNet/LifxClient.SwitchOperations.cs +++ b/src/LifxNet/LifxClient.SwitchOperations.cs @@ -6,17 +6,17 @@ 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(LightBulb bulb, int relayIndex) { - if (bulb == null) - throw new ArgumentNullException(nameof(bulb)); + public async Task GetRelayPowerAsync(Device device, int relayIndex) { + if (device == null) + throw new ArgumentNullException(nameof(device)); FrameHeader header = new FrameHeader(GetNextIdentifier()); return await BroadcastMessageAsync( - bulb.HostName, header, MessageType.GetRelayPower, (byte) relayIndex); + device.HostName, header, MessageType.GetRelayPower, (byte) relayIndex); } /// @@ -24,19 +24,19 @@ public async Task GetRelayPowerAsync(LightBulb bulb, in /// 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(LightBulb bulb, int relayIndex, bool enable) { - if (bulb == null) - throw new ArgumentNullException(nameof(bulb)); + 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( - bulb.HostName, header, MessageType.SetRelayPower, (byte) relayIndex, (ushort) level); + 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 index 7d3b3a0..e905b2f 100644 --- a/src/LifxNet/LifxClient.TileOperations.cs +++ b/src/LifxNet/LifxClient.TileOperations.cs @@ -10,7 +10,7 @@ public partial class LifxClient { /// /// /// StateDeviceChainResponse - public async Task GetDeviceChainAsync(LightBulb group) { + public async Task GetDeviceChainAsync(Device group) { if (group == null) throw new ArgumentNullException(nameof(group)); @@ -27,7 +27,7 @@ public async Task GetDeviceChainAsync(LightBulb group) /// /// /// - public async Task SetUserPositionAsync(LightBulb group, int tileIndex, float userX, float userY) { + public async Task SetUserPositionAsync(Device group, int tileIndex, float userX, float userY) { if (group == null) throw new ArgumentNullException(nameof(group)); @@ -44,21 +44,21 @@ await BroadcastMessageAsync(group.HostName, header, /// /// 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(LightBulb group, int tileIndex, int length, + public async Task GetTileState64Async(Device device, int tileIndex, int length, int x = 0, int y = 0, int width = 8) { - if (group == null) - throw new ArgumentNullException(nameof(group)); + if (device == null) + throw new ArgumentNullException(nameof(device)); FrameHeader header = new FrameHeader(GetNextIdentifier()); return await BroadcastMessageAsync( - group.HostName, header, MessageType.GetTileState64, tileIndex, length, Reserved, x, y, width); + device.HostName, header, MessageType.GetTileState64, tileIndex, length, Reserved, x, y, width); } /// @@ -68,7 +68,7 @@ public async Task GetTileState64Async(LightBulb group, /// /// 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. /// @@ -77,14 +77,14 @@ public async Task GetTileState64Async(LightBulb group, /// Leave at 0 /// Leave at 8 /// StateTileState64Response - public async Task SetTileState64Async(LightBulb bulb, int tileIndex, int length, + public async Task SetTileState64Async(Device device, int tileIndex, int length, long duration, LifxColor[] colors, int x = 0, int y = 0, int width = 8) { - if (bulb == null) - throw new ArgumentNullException(nameof(bulb)); + if (device == null) + throw new ArgumentNullException(nameof(device)); FrameHeader header = new FrameHeader(GetNextIdentifier()); return await BroadcastMessageAsync( - bulb.HostName, header, MessageType.SetTileState64, tileIndex, length, Reserved, x, y, width, duration, + device.HostName, header, MessageType.SetTileState64, tileIndex, length, Reserved, x, y, width, duration, colors); } } From 0578c00fd2c3de9123d67e8dac4eb9647a112ddb Mon Sep 17 00:00:00 2001 From: d8ahazard Date: Mon, 22 Feb 2021 08:48:44 -0600 Subject: [PATCH 17/37] Update documentation --- src/LifxNet/LifxClient.DeviceOperations.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LifxNet/LifxClient.DeviceOperations.cs b/src/LifxNet/LifxClient.DeviceOperations.cs index 30702f2..70a139b 100644 --- a/src/LifxNet/LifxClient.DeviceOperations.cs +++ b/src/LifxNet/LifxClient.DeviceOperations.cs @@ -202,7 +202,7 @@ await BroadcastMessageAsync(device.HostName, header, } /// - /// Ask the bulb to return its location information. + /// Ask the device to return its location information. /// /// /// From e169159a502640c7de869c689426c652e7721d1b Mon Sep 17 00:00:00 2001 From: d8ahazard Date: Mon, 22 Feb 2021 08:50:00 -0600 Subject: [PATCH 18/37] Update logging in sampleapp, fix device calls Update logs in netcore sample app to provide more info so we can remotely debug issues. Update object passed to device calls (device vs. bulb) --- src/SampleApps/SampleApp.netcore/Program.cs | 22 ++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/SampleApps/SampleApp.netcore/Program.cs b/src/SampleApps/SampleApp.netcore/Program.cs index 3860022..fc9db92 100644 --- a/src/SampleApps/SampleApp.netcore/Program.cs +++ b/src/SampleApps/SampleApp.netcore/Program.cs @@ -29,34 +29,42 @@ private static async void ClientDeviceDiscovered(object sender, LifxClient.Devic // 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) { + Console.WriteLine("Device is v2 z-led or beam, checking fw."); var fwVersion = await _client.GetDeviceHostFirmwareAsync(e.Device); - if (fwVersion.Version >= 1532997580) extended = true; + Console.WriteLine($"Firmware version is {fwVersion}."); + if (fwVersion.Version >= 1532997580) { + extended = true; + Console.WriteLine("Enabling extended firmware features."); + } } int zoneCount; if (extended) { - var zones = await _client.GetExtendedColorZonesAsync((e.Device as LightBulb)!); + var zones = await _client.GetExtendedColorZonesAsync(e.Device); zoneCount = zones.Count; } else { // Original device only supports eight zones? var zones = await _client.GetColorZonesAsync((e.Device as LightBulb)!, 0, 8); zoneCount = zones.Count; } - Console.WriteLine($"Device is multi-zone.\r\nExtended Support: {extended}\r\nZone Count: {zoneCount}"); + Console.WriteLine($"Extended Support: {extended}\r\nZone Count: {zoneCount}"); } // Tile if (version.Product == 55) { - var chain = await _client.GetDeviceChainAsync((e.Device as LightBulb)!); - Console.WriteLine($"Device is a tile group.\r\nTile count: {chain.TotalCount}"); + Console.WriteLine("Device is a tile group, enumerating data."); + var chain = await _client.GetDeviceChainAsync(e.Device); + Console.WriteLine($"Tile count: {chain.TotalCount}"); } // Switch if (version.Product == 70) { - var switchState = await _client.GetRelayPowerAsync((e.Device as LightBulb)!, 0); - Console.WriteLine($"Device is a switch. \r\nSwitch State: {switchState.Level}"); + Console.WriteLine("Device is a switch, enumerating data."); + var switchState = await _client.GetRelayPowerAsync(e.Device, 0); + Console.WriteLine($"Switch State: {switchState.Level}"); } } } From aeea25f6c5d8fc32fcdf2a06f5dc06ff5112d775 Mon Sep 17 00:00:00 2001 From: d8ahazard Date: Tue, 9 Mar 2021 11:46:38 -0600 Subject: [PATCH 19/37] Fix Multizone request Make StateMultizoneRequest return StateMultiZone response, versus StateZone response. --- src/LifxNet/LifxClient.MultizoneOperations.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/LifxNet/LifxClient.MultizoneOperations.cs b/src/LifxNet/LifxClient.MultizoneOperations.cs index 0946622..ef6b622 100644 --- a/src/LifxNet/LifxClient.MultizoneOperations.cs +++ b/src/LifxNet/LifxClient.MultizoneOperations.cs @@ -90,13 +90,28 @@ public Task GetExtendedColorZonesAsync(Device d /// 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) { + 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( + 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 From 7c71e0391b72308d457b8e62e492d75486c6e2d1 Mon Sep 17 00:00:00 2001 From: d8ahazard Date: Tue, 9 Mar 2021 11:47:50 -0600 Subject: [PATCH 20/37] Use ushort for LifxColor constructor --- src/LifxNet/LifxColor.cs | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/LifxNet/LifxColor.cs b/src/LifxNet/LifxColor.cs index 38c78d5..8b5a4a5 100644 --- a/src/LifxNet/LifxColor.cs +++ b/src/LifxNet/LifxColor.cs @@ -75,21 +75,10 @@ public float Brightness { /// Saturation /// Brightness /// Temp, currently ignored - public LifxColor(short h, short s, short b, short k = 2700) { + public LifxColor(ushort h, ushort s, ushort b, ushort k = 2700) { _color = HsbToColor(h, s, b); } - - /// - /// Create a color from ARGB values - /// - /// Alpha - /// Red - /// Green - /// Blue - public LifxColor(int a, int r, int g, int b) { - _color = Color.FromArgb(a, r, g, b); - } - + /// /// Create a color from RGB Value, with default alpha of 255 /// @@ -114,7 +103,7 @@ public LifxColor(Color color) { /// HSBK formatted array of bytes. public byte[] ToBytes() { var output = new List(); - foreach (var u in new ushort[] {(ushort) Hue, (ushort) Saturation, (ushort) Brightness, 2700}) + foreach (var u in new ushort[] {(ushort) Hue, (ushort) Saturation, (ushort) Brightness,(ushort) 2700}) output.AddRange(BitConverter.GetBytes(u)); return output.ToArray(); } From 9d65e8ccdf717f3d83ca9ca43ba988ccadac7690 Mon Sep 17 00:00:00 2001 From: d8ahazard Date: Tue, 9 Mar 2021 11:48:21 -0600 Subject: [PATCH 21/37] Various LifxResponse fixes --- src/LifxNet/LifxResponses.cs | 143 ++++++++++++++--------------------- 1 file changed, 57 insertions(+), 86 deletions(-) diff --git a/src/LifxNet/LifxResponses.cs b/src/LifxNet/LifxResponses.cs index 55fe659..24fc362 100644 --- a/src/LifxNet/LifxResponses.cs +++ b/src/LifxNet/LifxResponses.cs @@ -10,55 +10,32 @@ namespace LifxNet { /// public abstract class LifxResponse { internal static LifxResponse Create(FrameHeader header, MessageType type, uint source, Payload payload) { - payload.Reset(); - 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 InfraredStateResponse(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); - case MessageType.StateExtendedColorZones: - return new StateExtendedColorZonesResponse(header, type, payload, source); - case MessageType.StateZone: - return new StateZoneResponse(header, type, payload, source); - case MessageType.StateMultiZone: - return new StateMultiZoneResponse(header, type, payload, source); - case MessageType.StateDeviceChain: - return new StateDeviceChainResponse(header, type, payload, source); - case MessageType.StateTileState64: - return new StateTileState64Response(header, type, payload, source); - case MessageType.StateRelayPower: - return new StateRelayPowerResponse(header, type, payload, source); - case MessageType.DeviceStateHostInfo: - return new StateHostInfoResponse(header, type, payload, source); - case MessageType.DeviceStateWifiInfo: - return new StateWifiInfoResponse(header, type, payload, source); - case MessageType.DeviceStateWifiFirmware: - return new StateWifiFirmwareResponse(header, type, payload, source); - case MessageType.DeviceStatePower: - return new StatePowerResponse(header, type, payload, source); - case MessageType.DeviceStateInfo: - return new StateInfoResponse(header, type, payload, source); - case MessageType.DeviceStateLocation: - return new StateLocationResponse(header, type, payload, source); - case MessageType.DeviceStateGroup: - return new StateGroupResponse(header, type, payload, source); - case MessageType.DeviceEchoResponse: - return new EchoResponse(header, type, payload, source); - default: - return new UnknownResponse(header, type, payload, source); - } + 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, Payload payload, uint source) { @@ -91,12 +68,7 @@ internal StateZoneResponse(FrameHeader header, MessageType type, Payload payload type, payload, source) { Count = payload.GetUInt16(); Index = payload.GetUInt16(); - var h = payload.GetInt16(); - var s = payload.GetInt16(); - var b = payload.GetInt16(); - var k = payload.GetInt16(); - Color = new LifxColor(h, s, b, k); - payload.Reset(); + Color = new Payload().GetColor(); } /// @@ -126,7 +98,6 @@ internal StateHostInfoResponse(FrameHeader header, MessageType type, Payload pay Signal = payload.GetFloat32(); Tx = payload.GetUInt32(); Rx = payload.GetUInt32(); - payload.Reset(); } /// @@ -156,7 +127,6 @@ internal StateWifiInfoResponse(FrameHeader header, MessageType type, Payload pay Signal = payload.GetFloat32(); Tx = payload.GetUInt32(); Rx = payload.GetUInt32(); - payload.Reset(); } /// @@ -187,7 +157,6 @@ internal StateWifiFirmwareResponse(FrameHeader header, MessageType type, Payload payload.Advance(8); VersionMinor = payload.GetUInt16(); VersionMajor = payload.GetUInt16(); - payload.Reset(); } /// @@ -214,7 +183,6 @@ public class StatePowerResponse : LifxResponse { internal StatePowerResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(header, type, payload, source) { Level = payload.GetUInt16(); - payload.Reset(); } /// @@ -234,7 +202,6 @@ internal StateInfoResponse(FrameHeader header, MessageType type, Payload payload Time = DateTimeOffset.FromUnixTimeSeconds(payload.GetInt64()).DateTime; Uptime = payload.GetInt64(); Downtime = payload.GetInt64(); - payload.Reset(); } /// @@ -263,7 +230,6 @@ internal StateLocationResponse(FrameHeader header, MessageType type, Payload pay Location = payload.GetBytes(16); Label = payload.GetString(32); Updated = payload.GetUInt64(); - payload.Reset(); } public byte[] Location { get; set; } @@ -282,7 +248,6 @@ internal StateGroupResponse(FrameHeader header, MessageType type, Payload payloa Group = payload.GetBytes(16); Label = payload.GetString(32); Updated = payload.GetUInt64(); - payload.Reset(); } public byte[] Group { get; set; } @@ -299,7 +264,6 @@ public class EchoResponse : LifxResponse { internal EchoResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(header, type, payload, source) { RequestPayload = payload.ToArray(); - payload.Reset(); } /// @@ -318,22 +282,39 @@ internal StateDeviceChainResponse(FrameHeader header, MessageType type, Payload type, payload, source) { Tiles = new List(); StartIndex = payload.GetUint8(); - while (payload.HasContent()) { + for (var i = 0; i < 16; i++) { var tile = new Tile(); - tile.LoadPayload(payload); + tile.AccelMeasX = payload.GetInt16(); + tile.AccelMeasY = payload.GetInt16(); + tile.AccelMeasZ = payload.GetInt16(); + // Skip 2 bytes for reserved + payload.Advance(2); + tile.UserX = payload.GetFloat32(); + tile.UserY = payload.GetFloat32(); + tile.Width = payload.GetUint8(); + tile.Height = payload.GetUint8(); + // Skip 2 bytes for reserved + payload.Advance(); + tile.DeviceVersionVendor = payload.GetUInt32(); + tile.DeviceVersionProduct = payload.GetUInt32(); + tile.DeviceVersionVersion = payload.GetUInt32(); + tile.FirmwareBuild = payload.GetInt64(); + // Skip 8 bytes for reserved + payload.Advance(8); + tile.FirmwareVersionMinor = payload.GetInt16(); + tile.FirmwareVersionMajor = payload.GetInt16(); Tiles.Add(tile); } - + Console.WriteLine("current payload idx is " + payload.Position); TotalCount = payload.GetUint8(); if (TotalCount != Tiles.Count) Debug.WriteLine($"Warning, tile count doesn't match: {TotalCount} : {Tiles.Count}"); - payload.Reset(); } /// /// Count - total number of zones on the device /// - public byte TotalCount { get; } + public int TotalCount { get; } /// /// Start Index - Zone the message starts from @@ -352,14 +333,14 @@ internal StateDeviceChainResponse(FrameHeader header, MessageType type, Payload public class StateMultiZoneResponse : LifxResponse { internal StateMultiZoneResponse(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()); + 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(); } - - payload.Reset(); + Debug.WriteLine("Colors read."); } /// @@ -375,7 +356,7 @@ internal StateMultiZoneResponse(FrameHeader header, MessageType type, Payload pa /// /// The list of colors returned by the message /// - public List Colors { get; } + public LifxColor[] Colors { get; } } @@ -391,8 +372,6 @@ internal StateExtendedColorZonesResponse(FrameHeader header, MessageType type, P while (payload.HasContent()) { Colors.Add(payload.GetColor()); } - - payload.Reset(); } /// @@ -421,7 +400,6 @@ internal StateServiceResponse(FrameHeader header, MessageType type, Payload payl header, type, payload, source) { Service = payload.GetUint8(); Port = payload.GetUInt32(); - payload.Reset(); } private byte Service { get; } @@ -464,7 +442,6 @@ internal class StateLabelResponse : LifxResponse { internal StateLabelResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(header, type, payload, source) { Label = payload.GetString().Replace("\0", ""); - payload.Reset(); } public string? Label { get; } @@ -482,7 +459,6 @@ internal LightStateResponse(FrameHeader header, MessageType type, Payload payloa Kelvin = payload.GetUInt16(); IsOn = payload.GetUInt16() > 0; Label = payload.GetString(32).Replace("\\0", ""); - payload.Reset(); } /// @@ -520,7 +496,6 @@ internal class LightPowerResponse : LifxResponse { internal LightPowerResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base(header, type, payload, source) { IsOn = payload.GetUInt16() > 0; - payload.Reset(); } public bool IsOn { get; } @@ -530,7 +505,6 @@ internal class InfraredStateResponse : LifxResponse { internal InfraredStateResponse(FrameHeader header, MessageType type, Payload payload, uint source) : base( header, type, payload, source) { Brightness = payload.GetUInt16(); - payload.Reset(); } public ushort Brightness { get; } @@ -545,7 +519,6 @@ internal StateVersionResponse(FrameHeader header, MessageType type, Payload payl Vendor = Payload.GetUInt32(); Product = Payload.GetUInt32(); Version = Payload.GetUInt32(); - payload.Reset(); } /// @@ -574,7 +547,6 @@ internal StateHostFirmwareResponse(FrameHeader header, MessageType type, Payload Build = Utilities.Epoch.AddMilliseconds(nanoseconds * 0.000001); //8..15 UInt64 is reserved Version = payload.GetUInt32(); - payload.Reset(); } /// @@ -596,7 +568,6 @@ internal StateRelayPowerResponse(FrameHeader header, MessageType type, Payload p header, type, payload, source) { RelayIndex = payload.GetUint8(); Level = payload.GetUInt16(); - payload.Reset(); } /// From 209a32fc61f333037008850b76b45d632ffe835c Mon Sep 17 00:00:00 2001 From: d8ahazard Date: Tue, 9 Mar 2021 11:48:49 -0600 Subject: [PATCH 22/37] Add datetime, tile support to payload Ensure payload can properly serialize a tile and datetime. --- src/LifxNet/Payload.cs | 61 +++++++++++++++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 10 deletions(-) diff --git a/src/LifxNet/Payload.cs b/src/LifxNet/Payload.cs index 1bf297d..c5459b2 100644 --- a/src/LifxNet/Payload.cs +++ b/src/LifxNet/Payload.cs @@ -34,31 +34,30 @@ public Payload() { _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(@ushort); + Add((byte)@ushort); break; case uint u: Add(u); break; - case byte b: - Add(b); - break; case byte[] bytes: Add(bytes); break; case string s: Add(s.PadRight(32).Take(32).ToString()); break; - case LifxColor c: - Add(c); - break; - case LifxColor[] colors: - Add(colors); - break; case long l: Add(l); break; @@ -77,6 +76,18 @@ public Payload(Object[] args) { 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); @@ -99,6 +110,11 @@ public Payload(byte[] data) { _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 /// @@ -372,5 +388,30 @@ private void Add(LifxColor[] 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 From 9f0feea22b71aaf9de0abc397d23f290e5c9ed0a Mon Sep 17 00:00:00 2001 From: d8ahazard Date: Tue, 9 Mar 2021 11:50:50 -0600 Subject: [PATCH 23/37] Update TileGroup --- src/LifxNet/TileGroup.cs | 35 +++++++++++++++-------------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/src/LifxNet/TileGroup.cs b/src/LifxNet/TileGroup.cs index 11325e3..a77d893 100644 --- a/src/LifxNet/TileGroup.cs +++ b/src/LifxNet/TileGroup.cs @@ -1,6 +1,7 @@ using System; namespace LifxNet { + [Serializable] public class Tile { public int AccelMeasX { get; set; } public int AccelMeasY { get; set; } @@ -19,26 +20,20 @@ public class Tile { public Tile() { } - public void LoadPayload(Payload payload) { - AccelMeasX = payload.GetInt16(); - AccelMeasY = payload.GetInt16(); - AccelMeasZ = payload.GetInt16(); - // Skip 2 bytes for reserved - payload.Advance(2); - UserX = payload.GetFloat32(); - UserY = payload.GetFloat32(); - Width = payload.GetUint8(); - Height = payload.GetUint8(); - // Skip 2 bytes for reserved - payload.Advance(2); - DeviceVersionVendor = payload.GetUInt32(); - DeviceVersionProduct = payload.GetUInt32(); - DeviceVersionVendor = payload.GetUInt32(); - FirmwareBuild = payload.GetInt64(); - // Skip 8 bytes for reserved - payload.Advance(8); - FirmwareVersionMinor = payload.GetInt16(); - FirmwareVersionMajor = payload.GetInt16(); + public void CreateDefault(int index) { + AccelMeasX = 0; + AccelMeasY = 0; + AccelMeasZ = 0; + UserX = index * .5f; + UserY = index * 1; + Width = 8; + Height = 8; + DeviceVersionProduct = 55; + DeviceVersionVendor = 1; + FirmwareBuild = 1532997580; + FirmwareVersionMajor = 1; + FirmwareVersionMajor = 1; } + } } \ No newline at end of file From 7540ae0bc451c38cade21746d85f511bc95d73a4 Mon Sep 17 00:00:00 2001 From: d8ahazard Date: Tue, 9 Mar 2021 11:53:40 -0600 Subject: [PATCH 24/37] Add LifxEmulator Test App Testing encoding/decoding of responses when you don't actually own any Lifx Devices is pretty hard, so I created a simple "LifxEmulator" app that can sit within a LAN and respond to messages from LifxNet like a real device. Some emulation features are still WIP, but the majority of "the things" work. --- src/LifxNet.sln | 19 + .../LifxEmulator/LifxEmulator.csproj | 70 +++ src/SampleApps/LifxEmulator/LifxPacket.cs | 98 +++++ src/SampleApps/LifxEmulator/LifxResponses.cs | 393 +++++++++++++++++ src/SampleApps/LifxEmulator/Payload.cs | 413 ++++++++++++++++++ src/SampleApps/LifxEmulator/Program.cs | 283 ++++++++++++ .../LifxEmulator/Properties/AssemblyInfo.cs | 35 ++ src/SampleApps/LifxEmulator/packages.config | 4 + 8 files changed, 1315 insertions(+) create mode 100644 src/SampleApps/LifxEmulator/LifxEmulator.csproj create mode 100644 src/SampleApps/LifxEmulator/LifxPacket.cs create mode 100644 src/SampleApps/LifxEmulator/LifxResponses.cs create mode 100644 src/SampleApps/LifxEmulator/Payload.cs create mode 100644 src/SampleApps/LifxEmulator/Program.cs create mode 100644 src/SampleApps/LifxEmulator/Properties/AssemblyInfo.cs create mode 100644 src/SampleApps/LifxEmulator/packages.config diff --git a/src/LifxNet.sln b/src/LifxNet.sln index 49eedb8..21bb671 100644 --- a/src/LifxNet.sln +++ b/src/LifxNet.sln @@ -17,6 +17,8 @@ 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 Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -83,6 +85,22 @@ 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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -90,5 +108,6 @@ 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} EndGlobalSection EndGlobal diff --git a/src/SampleApps/LifxEmulator/LifxEmulator.csproj b/src/SampleApps/LifxEmulator/LifxEmulator.csproj new file mode 100644 index 0000000..7865db3 --- /dev/null +++ b/src/SampleApps/LifxEmulator/LifxEmulator.csproj @@ -0,0 +1,70 @@ + + + + + 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\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..bf9ca55 --- /dev/null +++ b/src/SampleApps/LifxEmulator/LifxResponses.cs @@ -0,0 +1,393 @@ +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); + 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.AddRange(Tiles); + 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; } + } + + /// + /// 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..4112023 --- /dev/null +++ b/src/SampleApps/LifxEmulator/Program.cs @@ -0,0 +1,283 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; +using Newtonsoft.Json; + +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 res = LifxResponse.Create(header, type, source, + new Payload(size > 36 ? br.ReadBytes(size - 36) : new byte[] { }),_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..87a2bd7 --- /dev/null +++ b/src/SampleApps/LifxEmulator/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file From d14436a291b649e20fed46b80914ca250b69fab1 Mon Sep 17 00:00:00 2001 From: d8ahazard Date: Tue, 9 Mar 2021 11:54:46 -0600 Subject: [PATCH 25/37] Update sample apps Make the .netcore app return basic info about switch/tile/multizone devices if discovered. --- src/LifxNet/json/products.json | 916 ++++++++++++++++++ .../SampleApp.Universal/MainPage.xaml.cs | 26 +- src/SampleApps/SampleApp.netcore/Program.cs | 17 +- .../SampleApp.netcore.csproj | 4 + 4 files changed, 942 insertions(+), 21 deletions(-) create mode 100644 src/LifxNet/json/products.json 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/SampleApp.Universal/MainPage.xaml.cs b/src/SampleApps/SampleApp.Universal/MainPage.xaml.cs index 1751a0f..97b44c2 100644 --- a/src/SampleApps/SampleApp.Universal/MainPage.xaml.cs +++ b/src/SampleApps/SampleApp.Universal/MainPage.xaml.cs @@ -20,7 +20,7 @@ public sealed partial class MainPage : Page { ObservableCollection bulbs = new ObservableCollection(); - LifxClient client; + LifxClient _client; public MainPage() { InitializeComponent(); @@ -29,18 +29,18 @@ public MainPage() protected async override void OnNavigatedTo(NavigationEventArgs e) { base.OnNavigatedTo(e); - client = await LifxClient.CreateAsync(); - client.DeviceDiscovered += ClientDeviceDeviceDiscovered; - client.DeviceLost += ClientDeviceDeviceLost; - 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 -= ClientDeviceDeviceDiscovered; - client.DeviceLost -= ClientDeviceDeviceLost; - client.StopDeviceDiscovery(); - client = null; + _client.DeviceDiscovered -= ClientDeviceDeviceDiscovered; + _client.DeviceLost -= ClientDeviceDeviceLost; + _client.StopDeviceDiscovery(); + _client = null; base.OnNavigatingFrom(e); } private void ClientDeviceDeviceLost(object sender, LifxClient.DeviceDiscoveryEventArgs e) @@ -68,7 +68,7 @@ private async void bulbList_SelectionChanged(object sender, SelectionChangedEven 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; @@ -89,7 +89,7 @@ private async void PowerState_Toggled(object sender, RoutedEventArgs e) var bulb = bulbList.SelectedItem as LightBulb; if (bulb != null) { - await client.SetDevicePowerStateAsync(bulb, PowerState.IsOn); + await _client.SetDevicePowerStateAsync(bulb, PowerState.IsOn); } } @@ -105,7 +105,7 @@ private void brightnessSlider_ValueChanged(object sender, RangeBaseValueChangedE 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) @@ -117,7 +117,7 @@ private async void SetColor(LightBulb bulb, ushort? hue, ushort? saturation, ush 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(setColorTask, throttleTask); try diff --git a/src/SampleApps/SampleApp.netcore/Program.cs b/src/SampleApps/SampleApp.netcore/Program.cs index fc9db92..6bbc82d 100644 --- a/src/SampleApps/SampleApp.netcore/Program.cs +++ b/src/SampleApps/SampleApp.netcore/Program.cs @@ -1,5 +1,8 @@ using System; +using System.Diagnostics; +using System.Text.Json.Serialization; using LifxNet; +using Newtonsoft.Json; namespace SampleApp.netcore { @@ -24,8 +27,8 @@ private static async void ClientDeviceDiscovered(object sender, LifxClient.Devic Console.WriteLine($"Device {e.Device.MacAddressName} found @ {e.Device.HostName}"); var version = await _client.GetDeviceVersionAsync(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}"); - Console.WriteLine($"Product: {version.Product}\n\tVendor: {version.Vendor}\n\tVersion: {version.Version} "); + 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) { @@ -35,23 +38,21 @@ private static async void ClientDeviceDiscovered(object sender, LifxClient.Devic if (version.Product == 32 || version.Product == 38) { Console.WriteLine("Device is v2 z-led or beam, checking fw."); var fwVersion = await _client.GetDeviceHostFirmwareAsync(e.Device); - Console.WriteLine($"Firmware version is {fwVersion}."); + Console.WriteLine("Firmware:" + JsonConvert.SerializeObject(fwVersion)); if (fwVersion.Version >= 1532997580) { extended = true; Console.WriteLine("Enabling extended firmware features."); } } - int zoneCount; if (extended) { var zones = await _client.GetExtendedColorZonesAsync(e.Device); - zoneCount = zones.Count; + Console.WriteLine("Zones: " + JsonConvert.SerializeObject(zones)); } else { // Original device only supports eight zones? - var zones = await _client.GetColorZonesAsync((e.Device as LightBulb)!, 0, 8); - zoneCount = zones.Count; + var zones = await _client.GetColorZonesAsync(e.Device, 0, 8); + Console.WriteLine("Zones: " + JsonConvert.SerializeObject(zones)); } - Console.WriteLine($"Extended Support: {extended}\r\nZone Count: {zoneCount}"); } // Tile 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 From 0813a3bb828b98e944ddb475cd00854aac76c0c8 Mon Sep 17 00:00:00 2001 From: d8ahazard Date: Wed, 10 Mar 2021 11:17:17 -0600 Subject: [PATCH 26/37] Fix Tile Chain Response Parsing Thanks to a few intrepid users who collected me "proper" packet responses from Lifx Tile devices, we now have Chain responses working properly. --- src/LifxNet/LifxResponses.cs | 24 +------ src/LifxNet/TileGroup.cs | 74 +++++++++++++++++--- src/SampleApps/LifxEmulator/LifxResponses.cs | 2 +- src/SampleApps/SampleApp.netcore/Program.cs | 9 +-- 4 files changed, 68 insertions(+), 41 deletions(-) diff --git a/src/LifxNet/LifxResponses.cs b/src/LifxNet/LifxResponses.cs index 24fc362..406e5bf 100644 --- a/src/LifxNet/LifxResponses.cs +++ b/src/LifxNet/LifxResponses.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Drawing; using System.Text; +using Newtonsoft.Json; namespace LifxNet { /// @@ -284,31 +285,10 @@ internal StateDeviceChainResponse(FrameHeader header, MessageType type, Payload StartIndex = payload.GetUint8(); for (var i = 0; i < 16; i++) { var tile = new Tile(); - tile.AccelMeasX = payload.GetInt16(); - tile.AccelMeasY = payload.GetInt16(); - tile.AccelMeasZ = payload.GetInt16(); - // Skip 2 bytes for reserved - payload.Advance(2); - tile.UserX = payload.GetFloat32(); - tile.UserY = payload.GetFloat32(); - tile.Width = payload.GetUint8(); - tile.Height = payload.GetUint8(); - // Skip 2 bytes for reserved - payload.Advance(); - tile.DeviceVersionVendor = payload.GetUInt32(); - tile.DeviceVersionProduct = payload.GetUInt32(); - tile.DeviceVersionVersion = payload.GetUInt32(); - tile.FirmwareBuild = payload.GetInt64(); - // Skip 8 bytes for reserved - payload.Advance(8); - tile.FirmwareVersionMinor = payload.GetInt16(); - tile.FirmwareVersionMajor = payload.GetInt16(); + tile.LoadBytes(payload); Tiles.Add(tile); } - Console.WriteLine("current payload idx is " + payload.Position); TotalCount = payload.GetUint8(); - if (TotalCount != Tiles.Count) - Debug.WriteLine($"Warning, tile count doesn't match: {TotalCount} : {Tiles.Count}"); } /// diff --git a/src/LifxNet/TileGroup.cs b/src/LifxNet/TileGroup.cs index a77d893..955f6a2 100644 --- a/src/LifxNet/TileGroup.cs +++ b/src/LifxNet/TileGroup.cs @@ -1,21 +1,24 @@ using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Diagnostics; namespace LifxNet { [Serializable] public class Tile { - public int AccelMeasX { get; set; } - public int AccelMeasY { get; set; } - public int AccelMeasZ { get; set; } + 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 int Width { get; set; } - public int Height { 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 long FirmwareBuild { get; set; } - public short FirmwareVersionMinor { get; set; } - public short FirmwareVersionMajor { get; set; } + public ulong FirmwareBuild { get; set; } + public ushort FirmwareVersionMinor { get; set; } + public ushort FirmwareVersionMajor { get; set; } public Tile() { } @@ -25,15 +28,64 @@ public void CreateDefault(int index) { AccelMeasY = 0; AccelMeasZ = 0; UserX = index * .5f; - UserY = index * 1; + UserY = 8.06f; Width = 8; Height = 8; DeviceVersionProduct = 55; DeviceVersionVendor = 1; + DeviceVersionVersion = 10; FirmwareBuild = 1532997580; - FirmwareVersionMajor = 1; - FirmwareVersionMajor = 1; + 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/SampleApps/LifxEmulator/LifxResponses.cs b/src/SampleApps/LifxEmulator/LifxResponses.cs index bf9ca55..eadb180 100644 --- a/src/SampleApps/LifxEmulator/LifxResponses.cs +++ b/src/SampleApps/LifxEmulator/LifxResponses.cs @@ -186,8 +186,8 @@ internal StateDeviceChainResponse(FrameHeader header, MessageType type, uint sou var tile = new Tile(); tile.CreateDefault(i); Tiles.Add(tile); + args.Add(tile.ToBytes()); } - args.AddRange(Tiles); args.Add(TotalCount); Payload = new Payload(args.ToArray()); Payload.Rewind(); diff --git a/src/SampleApps/SampleApp.netcore/Program.cs b/src/SampleApps/SampleApp.netcore/Program.cs index 6bbc82d..4aaea08 100644 --- a/src/SampleApps/SampleApp.netcore/Program.cs +++ b/src/SampleApps/SampleApp.netcore/Program.cs @@ -1,6 +1,4 @@ using System; -using System.Diagnostics; -using System.Text.Json.Serialization; using LifxNet; using Newtonsoft.Json; @@ -36,10 +34,7 @@ private static async void ClientDeviceDiscovered(object sender, LifxClient.Devic var extended = false; // If new Z-LED or Beam, check if FW supports "extended" commands. if (version.Product == 32 || version.Product == 38) { - Console.WriteLine("Device is v2 z-led or beam, checking fw."); - var fwVersion = await _client.GetDeviceHostFirmwareAsync(e.Device); - Console.WriteLine("Firmware:" + JsonConvert.SerializeObject(fwVersion)); - if (fwVersion.Version >= 1532997580) { + if (version.Version >= 1532997580) { extended = true; Console.WriteLine("Enabling extended firmware features."); } @@ -59,7 +54,7 @@ private static async void ClientDeviceDiscovered(object sender, LifxClient.Devic if (version.Product == 55) { Console.WriteLine("Device is a tile group, enumerating data."); var chain = await _client.GetDeviceChainAsync(e.Device); - Console.WriteLine($"Tile count: {chain.TotalCount}"); + Console.WriteLine("Tile chain: " + JsonConvert.SerializeObject(chain)); } // Switch if (version.Product == 70) { From 622aec64d22fd107c4c08436175cae0d1c1aab68 Mon Sep 17 00:00:00 2001 From: d8ahazard Date: Wed, 10 Mar 2021 11:17:53 -0600 Subject: [PATCH 27/37] Update payload Use proper terminology when describing payload methods. Make _data readonly. --- src/LifxNet/Payload.cs | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/LifxNet/Payload.cs b/src/LifxNet/Payload.cs index c5459b2..face24c 100644 --- a/src/LifxNet/Payload.cs +++ b/src/LifxNet/Payload.cs @@ -9,11 +9,11 @@ namespace LifxNet { /// /// 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, + /// 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 List _data; + private readonly List _data; private readonly BinaryReader _br; private readonly MemoryStream _ms; private readonly long _len; @@ -148,7 +148,7 @@ public bool HasContent() { } /// - /// Rewind our pointer N bits + /// Rewind our pointer N bytes /// /// How far to rewind. Default is 1. public void Rewind(int len = 1) { @@ -160,7 +160,7 @@ public void Rewind(int len = 1) { } /// - /// Forward our pointer N bits + /// Forward our pointer N bytes /// /// How far to advance. Default is 1. public void Advance(int len = 1) { @@ -203,6 +203,11 @@ public LifxColor GetColor() { return new LifxColor(); } + /// + /// Get an array of bytes from the reader + /// + /// + /// public byte[] GetBytes(int len) { return _br.ReadBytes(len); } @@ -222,7 +227,7 @@ public byte GetUint8() { } /// - /// Read UInt16 from array and increment pointer 2 bits + /// Read UInt16 from array and increment pointer 2 bytes /// /// ushort public ushort GetUInt16() { @@ -236,7 +241,7 @@ public ushort GetUInt16() { } /// - /// Read Int16 from array and increment pointer 2 bits. + /// Read Int16 from array and increment pointer 2 bytes. /// /// short public short GetInt16() { @@ -250,7 +255,7 @@ public short GetInt16() { } /// - /// Read Int32 from array and increment pointer 4 bits. + /// Read Int32 from array and increment pointer 4 bytes. /// /// int public int GetInt32() { @@ -264,7 +269,7 @@ public int GetInt32() { } /// - /// Read a UInt32 from array and increment pointer 4 bits. + /// Read a UInt32 from array and increment pointer 4 bytes. /// /// public uint GetUInt32() { @@ -278,7 +283,7 @@ public uint GetUInt32() { } /// - /// Read an Int64 from array and increment pointer 8 bits. + /// Read an Int64 from array and increment pointer 8 bytes. /// /// long public long GetInt64() { @@ -292,7 +297,7 @@ public long GetInt64() { } /// - /// Read a UInt64 from array and increment pointer 8 bits. + /// Read a UInt64 from array and increment pointer 8 bytes. /// /// ulong public ulong GetUInt64() { @@ -306,7 +311,7 @@ public ulong GetUInt64() { } /// - /// Read a Float32 from array and increment pointer 4 bits. + /// Read a Float32 from array and increment pointer 4 bytes. /// /// float public float GetFloat32() { From 6d21da21bcdb112bbece36f5888adfc1418bcb50 Mon Sep 17 00:00:00 2001 From: d8ahazard Date: Wed, 10 Mar 2021 13:12:37 -0600 Subject: [PATCH 28/37] Add apply option to multizone setters Because the command is buffered, we can choose whether to apply it immediately or not. --- src/LifxNet/LifxClient.MultizoneOperations.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/LifxNet/LifxClient.MultizoneOperations.cs b/src/LifxNet/LifxClient.MultizoneOperations.cs index ef6b622..4e13cb1 100644 --- a/src/LifxNet/LifxClient.MultizoneOperations.cs +++ b/src/LifxNet/LifxClient.MultizoneOperations.cs @@ -4,9 +4,6 @@ namespace LifxNet { public partial class LifxClient : IDisposable { - private const byte Apply = 0x01; - - /// /// This message is used for changing the color of either a single or multiple zones. /// @@ -15,11 +12,12 @@ public partial class LifxClient : IDisposable { /// 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) { + TimeSpan transitionDuration, bool apply=false) { if (device == null) throw new ArgumentNullException(nameof(device)); if (transitionDuration.TotalMilliseconds > uint.MaxValue || @@ -28,11 +26,11 @@ public async Task SetColorZonesAsync(Device device, int startIndex, int endIndex } 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, Apply); + MessageType.SetColorZones, (byte) startIndex, (byte) endIndex, color, duration, doApply); } /// @@ -43,12 +41,13 @@ await BroadcastMessageAsync(device.HostName, header, /// 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) { + List colors, bool apply = false) { if (device == null) throw new ArgumentNullException(nameof(device)); if (transitionDuration.TotalMilliseconds > uint.MaxValue || @@ -63,9 +62,10 @@ public async Task SetExtendedColorZonesAsync(Device device, TimeSpan transitionD foreach (var color in colors) { colorBytes.AddRange(color.ToBytes()); } + var doApply = apply ? 0x01 : 0x00; await BroadcastMessageAsync(device.HostName, header, - MessageType.SetExtendedColorZones, duration, Apply, index, count, colorBytes); + MessageType.SetExtendedColorZones, duration, doApply, index, count, colorBytes); } /// From ddd6b42b9621aab37a31e81cfcba536d0393b311 Mon Sep 17 00:00:00 2001 From: d8ahazard Date: Wed, 10 Mar 2021 13:12:56 -0600 Subject: [PATCH 29/37] Cleanup --- src/LifxNet/LifxResponses.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/LifxNet/LifxResponses.cs b/src/LifxNet/LifxResponses.cs index 406e5bf..0eb7258 100644 --- a/src/LifxNet/LifxResponses.cs +++ b/src/LifxNet/LifxResponses.cs @@ -1,9 +1,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Drawing; -using System.Text; -using Newtonsoft.Json; namespace LifxNet { /// From 630ee74bb00dc8ec2b17588fbe0b93231d471122 Mon Sep 17 00:00:00 2001 From: d8ahazard Date: Wed, 10 Mar 2021 13:13:16 -0600 Subject: [PATCH 30/37] Add app to test color sending --- src/LifxNet.sln | 19 + .../ColorSendTest/ColorSendTest.csproj | 16 + src/SampleApps/ColorSendTest/Program.cs | 349 ++++++++++++++++++ 3 files changed, 384 insertions(+) create mode 100644 src/SampleApps/ColorSendTest/ColorSendTest.csproj create mode 100644 src/SampleApps/ColorSendTest/Program.cs diff --git a/src/LifxNet.sln b/src/LifxNet.sln index 21bb671..7a700a2 100644 --- a/src/LifxNet.sln +++ b/src/LifxNet.sln @@ -19,6 +19,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleApp.Universal", "Samp 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 @@ -101,6 +103,22 @@ Global {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 @@ -109,5 +127,6 @@ Global {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/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..22c875c --- /dev/null +++ b/src/SampleApps/ColorSendTest/Program.cs @@ -0,0 +1,349 @@ +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 void Main(string[] args) { + var tr1 = new TextWriterTraceListener(Console.Out); + Trace.Listeners.Add(tr1); + _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; + _client.StartDeviceDiscovery(); + Task.Delay(15000); + _client.StopDeviceDiscovery(); + Console.WriteLine("Please select a device type to test (Enter a number):"); + if (_devicesBulb.Count > 0) { + Console.WriteLine("Bulbs: 1"); + } + + if (_devicesMulti.Count > 0) { + Console.WriteLine("Multi Zone V1: 2"); + } + + if (_devicesMultiV2.Count > 0) { + Console.WriteLine("Multi Zone V2: 3"); + } + + if (_devicesTile.Count > 0) { + Console.WriteLine("Tiles: 4"); + } + + if (_devicesSwitch.Count > 0) { + Console.WriteLine("Switch: 5"); + } + + var selection = int.Parse(Console.ReadLine() ?? "0"); + switch (selection) { + case 1: + Console.WriteLine("Flashing bulbs on and off."); + FlashBulbs().ConfigureAwait(true); + break; + case 2: + Console.WriteLine("Flashing multizone v1 devices on and off."); + FlashMultizone().ConfigureAwait(true); + break; + case 3: + Console.WriteLine("Flashing multizone v2 devices on and off."); + FlashMultizoneV2().ConfigureAwait(true); + break; + case 4: + Console.WriteLine("Flashing tile devices on and off."); + FlashTiles().ConfigureAwait(true); + 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); + await _client.SetPowerAsync(m, 1); + } + + 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 progress = (start - i) / 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++) { + await _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) { + await _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); + await _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 progress = (start - i) / 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) { + await _client.SetPowerAsync(m, power); + } + } + } + + private static async Task FlashTiles() { + var chains = new List(); + foreach (var t in _devicesTile) { + var state = await _client.GetDeviceChainAsync(t); + chains.Add(state); + await _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 progress = c / 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() { + + } + + public static LifxColor Rainbow(float progress) { + var div = Math.Abs(progress % 1) * 6; + var ascending = (int) (div % 1 * 255); + var descending = 255 - ascending; + var alpha = 0; + var output = (int) div switch { + 0 => Color.FromArgb(alpha, 255, ascending, 0), + 1 => Color.FromArgb(alpha, descending, 255, 0), + 2 => Color.FromArgb(alpha, 0, 255, ascending), + 3 => Color.FromArgb(alpha, 0, descending, 255), + 4 => Color.FromArgb(alpha, ascending, 0, 255), + _ => Color.FromArgb(alpha, 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 state = await _client.GetLightStateAsync((e.Device as LightBulb)!); + Console.WriteLine("Version info: " + JsonConvert.SerializeObject(version)); + Console.WriteLine("State info: " + JsonConvert.SerializeObject(state)); + var added = false; + // 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) { + added = true; + _devicesMultiV2.Add(e.Device); + } else { + added = true; + _devicesMulti.Add(e.Device); + } + } + + // Tile + if (version.Product == 55) { + added = true; + _devicesTile.Add(e.Device); + } + // Switch + if (version.Product == 70) { + added = true; + _devicesSwitch.Add(e.Device); + } + + if (!added) { + _devicesBulb.Add(e.Device); + } + } + } +} \ No newline at end of file From 47f195270f1f17a43e343335937d8b9e9362b3c5 Mon Sep 17 00:00:00 2001 From: d8ahazard Date: Wed, 10 Mar 2021 14:14:28 -0600 Subject: [PATCH 31/37] Update logging, add proper ToString() method for LifxColor --- src/LifxNet/LifxClient.cs | 2 +- src/LifxNet/LifxColor.cs | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/LifxNet/LifxClient.cs b/src/LifxNet/LifxClient.cs index e8f1ed3..85963dc 100644 --- a/src/LifxNet/LifxClient.cs +++ b/src/LifxNet/LifxClient.cs @@ -54,7 +54,7 @@ private void StartReceiveLoop() { private void HandleIncomingMessages(byte[] data, IPEndPoint endpoint) { var remote = endpoint; var msg = ParseMessage(data); - if (remote.Port == 56700) Debug.WriteLine("Message Type: " + msg.Type); + if (remote.Port == 56700) Debug.WriteLine("Incoming Message Type: " + msg.Type); switch (msg.Type) { case MessageType.DeviceStateService: ProcessDeviceDiscoveryMessage(remote.Address, msg); diff --git a/src/LifxNet/LifxColor.cs b/src/LifxNet/LifxColor.cs index 8b5a4a5..5a8f81f 100644 --- a/src/LifxNet/LifxColor.cs +++ b/src/LifxNet/LifxColor.cs @@ -109,6 +109,15 @@ public byte[] ToBytes() { } + /// + /// Return rgb string representation of the color + /// + /// + public override string ToString() { + return R + ", " + G + ", " + B; + } + + /// /// Convert HSB Values to a standard Color object /// From 21cfa42fc7804f271dc673788e1cfc39908a06f9 Mon Sep 17 00:00:00 2001 From: d8ahazard Date: Wed, 10 Mar 2021 14:14:43 -0600 Subject: [PATCH 32/37] Fix color send test app --- src/SampleApps/ColorSendTest/Program.cs | 35 ++++++++++++++----------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/SampleApps/ColorSendTest/Program.cs b/src/SampleApps/ColorSendTest/Program.cs index 22c875c..884d21a 100644 --- a/src/SampleApps/ColorSendTest/Program.cs +++ b/src/SampleApps/ColorSendTest/Program.cs @@ -16,7 +16,7 @@ class Program { private static List _devicesTile; private static List _devicesSwitch; - static void Main(string[] args) { + static async Task Main(string[] args) { var tr1 = new TextWriterTraceListener(Console.Out); Trace.Listeners.Add(tr1); _devicesBulb = new List(); @@ -28,7 +28,7 @@ static void Main(string[] args) { _client.DeviceDiscovered += ClientDeviceDiscovered; _client.DeviceLost += ClientDeviceLost; _client.StartDeviceDiscovery(); - Task.Delay(15000); + await Task.Delay(15000); _client.StopDeviceDiscovery(); Console.WriteLine("Please select a device type to test (Enter a number):"); if (_devicesBulb.Count > 0) { @@ -55,19 +55,19 @@ static void Main(string[] args) { switch (selection) { case 1: Console.WriteLine("Flashing bulbs on and off."); - FlashBulbs().ConfigureAwait(true); + await FlashBulbs(); break; case 2: Console.WriteLine("Flashing multizone v1 devices on and off."); - FlashMultizone().ConfigureAwait(true); + await FlashMultizone(); break; case 3: Console.WriteLine("Flashing multizone v2 devices on and off."); - FlashMultizoneV2().ConfigureAwait(true); + await FlashMultizoneV2(); break; case 4: Console.WriteLine("Flashing tile devices on and off."); - FlashTiles().ConfigureAwait(true); + await FlashTiles(); break; case 5: Console.WriteLine("Toggling switches is not enabled yet."); @@ -134,7 +134,7 @@ private static async Task FlashMultizone() { stateList.Add(state); var zoneState = await _client.GetColorZonesAsync(m,0,8); responses.Add(zoneState); - await _client.SetPowerAsync(m, 1); + _client.SetPowerAsync(m, 1).ConfigureAwait(false); } var idx = 0; @@ -144,7 +144,8 @@ private static async Task FlashMultizone() { var start = state.Index; var total = start - count; for (var i = start; i < count; i++) { - var progress = (start - i) / total; + 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); } @@ -162,7 +163,7 @@ private static async Task FlashMultizone() { var start = state.Index; var total = start - count; for (var i = start; i < count; i++) { - await _client.SetColorZonesAsync(m, i, i, black, TimeSpan.Zero, true); + _client.SetColorZonesAsync(m, i, i, black, TimeSpan.Zero, true); } idx++; } @@ -172,7 +173,7 @@ private static async Task FlashMultizone() { foreach (var m in _devicesMulti) { var power = stateList[idx]; if (power == 0) { - await _client.SetPowerAsync(m, power); + _client.SetPowerAsync(m, power); } } } @@ -185,7 +186,7 @@ private static async Task FlashMultizoneV2() { stateList.Add(state); var zoneState = await _client.GetExtendedColorZonesAsync(m); responses.Add(zoneState); - await _client.SetPowerAsync(m, 1); + _client.SetPowerAsync(m, 1); } Debug.WriteLine("Setting devices to rainbow!"); var idx = 0; @@ -197,7 +198,8 @@ private static async Task FlashMultizoneV2() { var colors = new List(); for (var i = start; i < count; i++) { - var progress = (start - i) / total; + var pi = i * 1.0f; + var progress = (start - pi) / total; colors.Add(Rainbow(progress)); } _client.SetExtendedColorZonesAsync(m, TimeSpan.Zero, start, colors, true); @@ -227,7 +229,7 @@ private static async Task FlashMultizoneV2() { foreach (var m in _devicesMulti) { var power = stateList[idx]; if (power == 0) { - await _client.SetPowerAsync(m, power); + _client.SetPowerAsync(m, power); } } } @@ -235,9 +237,9 @@ private static async Task FlashMultizoneV2() { private static async Task FlashTiles() { var chains = new List(); foreach (var t in _devicesTile) { - var state = await _client.GetDeviceChainAsync(t); + var state = _client.GetDeviceChainAsync(t).Result; chains.Add(state); - await _client.SetPowerAsync(t, 1); + _client.SetPowerAsync(t, 1); } var idx = 0; @@ -248,7 +250,8 @@ private static async Task FlashTiles() { var tidx = 0; var colors = new List(); for (var c = 0; c < 64; c++) { - var progress = c / 64; + var pi = c * 1.0f; + var progress = pi / 64; colors.Add(Rainbow(progress)); } for (var i = state.StartIndex; i < state.TotalCount; i++) { From ecf9c2e6632fab4416cddbaa4fb0944e5d0ffa64 Mon Sep 17 00:00:00 2001 From: d8ahazard Date: Wed, 10 Mar 2021 14:15:52 -0600 Subject: [PATCH 33/37] Update Lifx Emulator Add Statepower response for testing purposes Add more logging, colored console --- .../LifxEmulator/LifxEmulator.csproj | 6 ++++ src/SampleApps/LifxEmulator/LifxResponses.cs | 21 +++++++++++++ src/SampleApps/LifxEmulator/Program.cs | 31 +++++++++++++++++-- src/SampleApps/LifxEmulator/packages.config | 1 + 4 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/SampleApps/LifxEmulator/LifxEmulator.csproj b/src/SampleApps/LifxEmulator/LifxEmulator.csproj index 7865db3..d7e88b2 100644 --- a/src/SampleApps/LifxEmulator/LifxEmulator.csproj +++ b/src/SampleApps/LifxEmulator/LifxEmulator.csproj @@ -33,6 +33,11 @@ 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 @@ -40,6 +45,7 @@ + diff --git a/src/SampleApps/LifxEmulator/LifxResponses.cs b/src/SampleApps/LifxEmulator/LifxResponses.cs index eadb180..7d6f4c8 100644 --- a/src/SampleApps/LifxEmulator/LifxResponses.cs +++ b/src/SampleApps/LifxEmulator/LifxResponses.cs @@ -44,6 +44,12 @@ internal static LifxResponse Create(FrameHeader header, MessageType type, uint s 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); @@ -208,6 +214,21 @@ internal StateDeviceChainResponse(FrameHeader header, MessageType type, uint sou /// 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 diff --git a/src/SampleApps/LifxEmulator/Program.cs b/src/SampleApps/LifxEmulator/Program.cs index 4112023..f3b665f 100644 --- a/src/SampleApps/LifxEmulator/Program.cs +++ b/src/SampleApps/LifxEmulator/Program.cs @@ -1,11 +1,14 @@ 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 Newtonsoft.Json; +using LifxNet; +using Console = Colorful.Console; namespace LifxEmulator { internal static class Program { @@ -172,8 +175,32 @@ private static async Task ParseMessage(byte[] packet) { 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}"); + } + + if (type == MessageType.SetTileState64) { + var idx = payload.GetUint8(); + var len = payload.GetUint8(); + payload.Advance(); // reserved + var x = payload.GetUint8(); + var y = payload.GetUint8(); + var width = payload.GetUint8(); + var duration = payload.GetUInt32(); + var colors = new List(); + Console.WriteLine("Colors: "); + for (var i = 0; i < 64; i++) { + var color = payload.GetColor(); + Console.Write(i.ToString(),Color.FromArgb(color.R, color.G, color.B)); + } + Console.WriteLine(""); + } var res = LifxResponse.Create(header, type, source, - new Payload(size > 36 ? br.ReadBytes(size - 36) : new byte[] { }),_deviceVersion); + payload,_deviceVersion); await Task.FromResult(true); return res; } diff --git a/src/SampleApps/LifxEmulator/packages.config b/src/SampleApps/LifxEmulator/packages.config index 87a2bd7..9a394a4 100644 --- a/src/SampleApps/LifxEmulator/packages.config +++ b/src/SampleApps/LifxEmulator/packages.config @@ -1,4 +1,5 @@  + \ No newline at end of file From 92622861742d4fbed3c923eca612f6aef93b4016 Mon Sep 17 00:00:00 2001 From: d8ahazard Date: Thu, 11 Mar 2021 10:04:09 -0600 Subject: [PATCH 34/37] LifxColor Improvements Add methods to retrieve Lifx Color formatted values directly. Make documentation more useful Replace HSB conversion method. --- src/LifxNet/LifxColor.cs | 253 +++++++++++++++++++++++---------------- 1 file changed, 152 insertions(+), 101 deletions(-) diff --git a/src/LifxNet/LifxColor.cs b/src/LifxNet/LifxColor.cs index 5a8f81f..3783f3a 100644 --- a/src/LifxNet/LifxColor.cs +++ b/src/LifxNet/LifxColor.cs @@ -13,13 +13,7 @@ private static double Tolerance private Color _color; - /// - /// Create a new black LifxColor - /// - public LifxColor() { - _color = Color.FromArgb(255, 0, 0, 0); - } - + /// /// Red /// @@ -45,56 +39,98 @@ public byte B { } /// - /// Hue + /// 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 = HsbToColor(value, _color.GetSaturation(), _color.GetBrightness()); + set => _color = HsbToRgb(value, _color.GetSaturation(), _color.GetBrightness()); } /// - /// Saturation + /// 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 = HsbToColor(_color.GetHue(), value, _color.GetBrightness()); + set => _color = HsbToRgb(_color.GetHue(), value, _color.GetBrightness()); } /// - /// Brightness + /// 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 = HsbToColor(_color.GetHue(), _color.GetSaturation(), value); + 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 - /// Saturation - /// Brightness - /// Temp, currently ignored + /// 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) { - _color = HsbToColor(h, s, b); + 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 - /// Green - /// Blue + /// 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 /// - /// Input Color + /// Base System.Drawing.Color public LifxColor(Color color) { _color = color; + K = 2700; } /// @@ -103,98 +139,113 @@ public LifxColor(Color color) { /// HSBK formatted array of bytes. public byte[] ToBytes() { var output = new List(); - foreach (var u in new ushort[] {(ushort) Hue, (ushort) Saturation, (ushort) Brightness,(ushort) 2700}) + 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 rgb string representation of the color + /// Return System.Drawing.Color RGB string representation of the color /// /// - public override string ToString() { + public string ToRgbString() { return R + ", " + G + ", " + B; } - - + /// - /// Convert HSB Values to a standard Color object + /// Return Lifx HSBK string representation of the color /// - /// Hue - /// Saturation - /// Brightness - /// Alpha, default is 255 /// - private static Color HsbToColor(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)); - - double r = 0D; - double g = 0D; - double bl = 0D; - - if (Math.Abs(s) < Tolerance) - r = g = bl = b; - else { - // the argb wheel consists of 6 sectors. Figure out which sector - // you're in. - double sectorPos = h / 60D; - int sectorNumber = (int) Math.Floor(sectorPos); - // get the fractional part of the sector - double fractionalSector = sectorPos - sectorNumber; - - // calculate values for the three axes of the argb. - double p = b * (1D - s); - double q = b * (1D - s * fractionalSector); - double 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}", CultureInfo.InvariantCulture)))), - Math.Max(0, - Math.Min(255, Convert.ToInt32(double.Parse($"{g * 255D:0.00}", CultureInfo.InvariantCulture)))), - Math.Max(0, - Math.Min(255, Convert.ToInt32(double.Parse($"{bl * 250D:0.00}", CultureInfo.InvariantCulture))))); + 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 From 0d9abed26dde8d002a111b180ef270fb0cb1d9e2 Mon Sep 17 00:00:00 2001 From: d8ahazard Date: Thu, 11 Mar 2021 10:04:38 -0600 Subject: [PATCH 35/37] Add shorter SetColorAsync method This one only needs the device and color, and an optional transition time in MS... --- src/LifxNet/LifxClient.LightOperations.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/LifxNet/LifxClient.LightOperations.cs b/src/LifxNet/LifxClient.LightOperations.cs index 3ae19a4..5639feb 100644 --- a/src/LifxNet/LifxClient.LightOperations.cs +++ b/src/LifxNet/LifxClient.LightOperations.cs @@ -81,6 +81,18 @@ public async Task GetLightPowerAsync(LightBulb bulb) { 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 /// From 8d923dc950715def008dc6ad3b35b52af7ed4035 Mon Sep 17 00:00:00 2001 From: d8ahazard Date: Thu, 11 Mar 2021 10:05:04 -0600 Subject: [PATCH 36/37] Improve Color Send Test logging. --- src/SampleApps/ColorSendTest/Program.cs | 43 +++++++++++++------------ 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/src/SampleApps/ColorSendTest/Program.cs b/src/SampleApps/ColorSendTest/Program.cs index 884d21a..1acf481 100644 --- a/src/SampleApps/ColorSendTest/Program.cs +++ b/src/SampleApps/ColorSendTest/Program.cs @@ -18,7 +18,6 @@ class Program { static async Task Main(string[] args) { var tr1 = new TextWriterTraceListener(Console.Out); - Trace.Listeners.Add(tr1); _devicesBulb = new List(); _devicesMulti = new List(); _devicesMultiV2 = new List(); @@ -27,28 +26,30 @@ static async Task Main(string[] args) { _client = LifxClient.CreateAsync().Result; _client.DeviceDiscovered += ClientDeviceDiscovered; _client.DeviceLost += ClientDeviceLost; + Console.WriteLine("Enumerating devices, please wait 15 seconds..."); _client.StartDeviceDiscovery(); - await Task.Delay(15000); + 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("Bulbs: 1"); + Console.WriteLine("1: Bulbs"); } if (_devicesMulti.Count > 0) { - Console.WriteLine("Multi Zone V1: 2"); + Console.WriteLine("2: Multi Zone V1"); } if (_devicesMultiV2.Count > 0) { - Console.WriteLine("Multi Zone V2: 3"); + Console.WriteLine("3: Multi Zone V2"); } if (_devicesTile.Count > 0) { - Console.WriteLine("Tiles: 4"); + Console.WriteLine("4: Tiles"); } if (_devicesSwitch.Count > 0) { - Console.WriteLine("Switch: 5"); + Console.WriteLine("5: Switch"); } var selection = int.Parse(Console.ReadLine() ?? "0"); @@ -282,19 +283,19 @@ private static async Task FlashTiles() { private static void FlashSwitches() { } - - public static LifxColor Rainbow(float progress) { + + 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 alpha = 0; var output = (int) div switch { - 0 => Color.FromArgb(alpha, 255, ascending, 0), - 1 => Color.FromArgb(alpha, descending, 255, 0), - 2 => Color.FromArgb(alpha, 0, 255, ascending), - 3 => Color.FromArgb(alpha, 0, descending, 255), - 4 => Color.FromArgb(alpha, ascending, 0, 255), - _ => Color.FromArgb(alpha, 255, 0, descending) + 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); } @@ -308,27 +309,24 @@ private static async void ClientDeviceDiscovered(object sender, LifxClient.Devic { Console.WriteLine($"Device {e.Device.MacAddressName} found @ {e.Device.HostName}"); 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)); var added = false; // 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) { 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); } } @@ -336,15 +334,18 @@ private static async void ClientDeviceDiscovered(object sender, LifxClient.Devic // 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); } } From 7a28c52ebf752a690a5d18ff3f5a6801b92b244c Mon Sep 17 00:00:00 2001 From: d8ahazard Date: Thu, 11 Mar 2021 10:05:36 -0600 Subject: [PATCH 37/37] Improve logging --- src/SampleApps/LifxEmulator/Program.cs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/SampleApps/LifxEmulator/Program.cs b/src/SampleApps/LifxEmulator/Program.cs index f3b665f..d1f6c6e 100644 --- a/src/SampleApps/LifxEmulator/Program.cs +++ b/src/SampleApps/LifxEmulator/Program.cs @@ -180,22 +180,15 @@ private static async Task ParseMessage(byte[] packet) { var start = payload.GetUint8(); var end = payload.GetUint8(); var color = payload.GetColor(); - Debug.WriteLine($"Setting zones {start} - {end} to {color}"); + Debug.WriteLine($"Setting zones {start} - {end} to {color.ToHsbkString()}", color.Color); } if (type == MessageType.SetTileState64) { - var idx = payload.GetUint8(); - var len = payload.GetUint8(); - payload.Advance(); // reserved - var x = payload.GetUint8(); - var y = payload.GetUint8(); - var width = payload.GetUint8(); - var duration = payload.GetUInt32(); - var colors = new List(); + payload.Advance(9); Console.WriteLine("Colors: "); for (var i = 0; i < 64; i++) { var color = payload.GetColor(); - Console.Write(i.ToString(),Color.FromArgb(color.R, color.G, color.B)); + Console.Write(i.ToString(), color.Color); } Console.WriteLine(""); }