From bc9a6a4682eae94e4bd7b1aa75612e18e80de9d6 Mon Sep 17 00:00:00 2001 From: Vit Nemecky Date: Wed, 31 Dec 2025 22:45:38 +0100 Subject: [PATCH 1/4] PoC Add SBrick Light - base 3*8 channels support --- .../{ => Vengit}/SBrickDeviceManagerTests.cs | 4 +- .../BrickController2/BrickController2.csproj | 2 + .../DI/DeviceManagementModule.cs | 3 +- .../DeviceManagement/DeviceType.cs | 1 + .../{ => Vengit}/SBrickDevice.cs | 2 +- .../{ => Vengit}/SBrickDeviceManager.cs | 2 +- .../Vengit/SBrickLightDevice.cs | 165 ++++++++++++++++++ .../DeviceManagement/Vengit/SBrickProtocol.cs | 36 ++++ .../DeviceManagement/Vengit/Vengit.cs | 24 +++ .../Protocols/GattProtocol.cs | 14 ++ .../UI/Controls/DeviceChannelLabel.cs | 5 + .../UI/Controls/DeviceChannelSelector.xaml | 50 ++++++ .../UI/Controls/DeviceChannelSelector.xaml.cs | 49 ++++++ .../Converters/DeviceTypeToImageConverter.cs | 3 + .../DeviceTypeToSmallImageConverter.cs | 3 + .../UI/Images/sbricklight_image.png | Bin 0 -> 55024 bytes .../UI/Images/sbricklight_image_small.png | Bin 0 -> 4956 bytes README.md | 1 + 18 files changed, 358 insertions(+), 6 deletions(-) rename BrickController2/BrickController2.Tests/DeviceManagement/{ => Vengit}/SBrickDeviceManagerTests.cs (91%) rename BrickController2/BrickController2/DeviceManagement/{ => Vengit}/SBrickDevice.cs (99%) rename BrickController2/BrickController2/DeviceManagement/{ => Vengit}/SBrickDeviceManager.cs (93%) create mode 100644 BrickController2/BrickController2/DeviceManagement/Vengit/SBrickLightDevice.cs create mode 100644 BrickController2/BrickController2/DeviceManagement/Vengit/SBrickProtocol.cs create mode 100644 BrickController2/BrickController2/DeviceManagement/Vengit/Vengit.cs create mode 100644 BrickController2/BrickController2/Protocols/GattProtocol.cs create mode 100644 BrickController2/BrickController2/UI/Images/sbricklight_image.png create mode 100644 BrickController2/BrickController2/UI/Images/sbricklight_image_small.png diff --git a/BrickController2/BrickController2.Tests/DeviceManagement/SBrickDeviceManagerTests.cs b/BrickController2/BrickController2.Tests/DeviceManagement/Vengit/SBrickDeviceManagerTests.cs similarity index 91% rename from BrickController2/BrickController2.Tests/DeviceManagement/SBrickDeviceManagerTests.cs rename to BrickController2/BrickController2.Tests/DeviceManagement/Vengit/SBrickDeviceManagerTests.cs index 111abb20..2497924f 100644 --- a/BrickController2/BrickController2.Tests/DeviceManagement/SBrickDeviceManagerTests.cs +++ b/BrickController2/BrickController2.Tests/DeviceManagement/Vengit/SBrickDeviceManagerTests.cs @@ -1,9 +1,9 @@ using BrickController2.DeviceManagement; -using BrickController2.DeviceManagement.Lego; +using BrickController2.DeviceManagement.Vengit; using FluentAssertions; using Xunit; -namespace BrickController2.Tests.DeviceManagement; +namespace BrickController2.Tests.DeviceManagement.Vengit; public class SBrickDeviceManagerTests : DeviceManagerTestBase { diff --git a/BrickController2/BrickController2/BrickController2.csproj b/BrickController2/BrickController2/BrickController2.csproj index 1c121d88..d8dcdfba 100644 --- a/BrickController2/BrickController2/BrickController2.csproj +++ b/BrickController2/BrickController2/BrickController2.csproj @@ -41,6 +41,8 @@ + + diff --git a/BrickController2/BrickController2/DeviceManagement/DI/DeviceManagementModule.cs b/BrickController2/BrickController2/DeviceManagement/DI/DeviceManagementModule.cs index f8fc1442..5147b95a 100644 --- a/BrickController2/BrickController2/DeviceManagement/DI/DeviceManagementModule.cs +++ b/BrickController2/BrickController2/DeviceManagement/DI/DeviceManagementModule.cs @@ -1,8 +1,8 @@ using Autofac; using BrickController2.DeviceManagement.BuWizz; using BrickController2.DeviceManagement.CaDA; -using BrickController2.DeviceManagement.Lego; using BrickController2.DeviceManagement.Vendors; +using BrickController2.DeviceManagement.Vengit; using BrickController2.Extensions; using BrickController2.PlatformServices.BluetoothLE; @@ -19,7 +19,6 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); - builder.RegisterType().Keyed(DeviceType.SBrick); builder.RegisterType().Keyed(DeviceType.BuWizz); builder.RegisterType().Keyed(DeviceType.BuWizz2); builder.RegisterType().Keyed(DeviceType.BuWizz3); diff --git a/BrickController2/BrickController2/DeviceManagement/DeviceType.cs b/BrickController2/BrickController2/DeviceManagement/DeviceType.cs index e7f2afd2..ee535d3e 100644 --- a/BrickController2/BrickController2/DeviceManagement/DeviceType.cs +++ b/BrickController2/BrickController2/DeviceManagement/DeviceType.cs @@ -23,5 +23,6 @@ public enum DeviceType MK5, MK3_8, RemoteControl, + SBrickLight, } } diff --git a/BrickController2/BrickController2/DeviceManagement/SBrickDevice.cs b/BrickController2/BrickController2/DeviceManagement/Vengit/SBrickDevice.cs similarity index 99% rename from BrickController2/BrickController2/DeviceManagement/SBrickDevice.cs rename to BrickController2/BrickController2/DeviceManagement/Vengit/SBrickDevice.cs index 99972cd0..87e23907 100644 --- a/BrickController2/BrickController2/DeviceManagement/SBrickDevice.cs +++ b/BrickController2/BrickController2/DeviceManagement/Vengit/SBrickDevice.cs @@ -6,7 +6,7 @@ using System.Threading; using System.Threading.Tasks; -namespace BrickController2.DeviceManagement +namespace BrickController2.DeviceManagement.Vengit { internal class SBrickDevice : BluetoothDevice { diff --git a/BrickController2/BrickController2/DeviceManagement/SBrickDeviceManager.cs b/BrickController2/BrickController2/DeviceManagement/Vengit/SBrickDeviceManager.cs similarity index 93% rename from BrickController2/BrickController2/DeviceManagement/SBrickDeviceManager.cs rename to BrickController2/BrickController2/DeviceManagement/Vengit/SBrickDeviceManager.cs index 4d44e6ee..d39b4ed5 100644 --- a/BrickController2/BrickController2/DeviceManagement/SBrickDeviceManager.cs +++ b/BrickController2/BrickController2/DeviceManagement/Vengit/SBrickDeviceManager.cs @@ -1,7 +1,7 @@ using System; using BrickController2.PlatformServices.BluetoothLE; -namespace BrickController2.DeviceManagement; +namespace BrickController2.DeviceManagement.Vengit; /// /// Manager for SBrick devices diff --git a/BrickController2/BrickController2/DeviceManagement/Vengit/SBrickLightDevice.cs b/BrickController2/BrickController2/DeviceManagement/Vengit/SBrickLightDevice.cs new file mode 100644 index 00000000..7c9ee750 --- /dev/null +++ b/BrickController2/BrickController2/DeviceManagement/Vengit/SBrickLightDevice.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using BrickController2.DeviceManagement.IO; +using BrickController2.Helpers; +using BrickController2.PlatformServices.BluetoothLE; + +using static BrickController2.DeviceManagement.Vengit.SBrickProtocol; + +namespace BrickController2.DeviceManagement.Vengit +{ + internal class SBrickLightDevice : BluetoothDevice + { + private const int BANK_0_CHANNELS = 16; + private const int BANK_1_CHANNELS = 8; + + private readonly OutputValuesGroup _bankOutputs0 = new(BANK_0_CHANNELS); + private readonly OutputValuesGroup _bankOutputs1 = new(BANK_1_CHANNELS); + + private IGattCharacteristic? _firmwareRevisionCharacteristic; + private IGattCharacteristic? _hardwareRevisionCharacteristic; + private IGattCharacteristic? _remoteControlCharacteristic; + + public SBrickLightDevice(string name, string address, byte[] deviceData, IDeviceRepository deviceRepository, IBluetoothLEService bleService) + : base(name, address, deviceRepository, bleService) + { + } + + public override DeviceType DeviceType => DeviceType.SBrickLight; + public override string BatteryVoltageSign => "V"; + public override int NumberOfChannels => BANK_0_CHANNELS + BANK_1_CHANNELS; + protected override bool AutoConnectOnFirstConnect => false; + + public override void SetOutput(int channel, float value) + { + CheckChannel(channel); + value = CutOutputValue(value); + + // for lights use 0-255 range + var rawValue = (byte)(Math.Abs(value) * 255); + + if (channel >= BANK_0_CHANNELS) + { + int lightChannel = channel - BANK_0_CHANNELS; + _bankOutputs1.SetOutput(lightChannel, rawValue); + } + else + { + _bankOutputs0.SetOutput(channel, rawValue); + } + } + + protected override Task ValidateServicesAsync(IEnumerable? services, CancellationToken token) + { + var deviceInformationService = services?.FirstOrDefault(s => s.Uuid == GattProtocol.DeviceInformationServiceUuid); + _firmwareRevisionCharacteristic = deviceInformationService?.Characteristics?.FirstOrDefault(c => c.Uuid == GattProtocol.FirmwareRevisionCharacteristicUuid); + _hardwareRevisionCharacteristic = deviceInformationService?.Characteristics?.FirstOrDefault(c => c.Uuid == GattProtocol.HardwareRevisionCharacteristicUuid); + + var remoteControlService = services?.FirstOrDefault(s => s.Uuid == SBrickProtocol.ServiceUuid); + _remoteControlCharacteristic = remoteControlService?.Characteristics?.FirstOrDefault(c => c.Uuid == RemoteControlCharacteristicUuid); + + return Task.FromResult( + _firmwareRevisionCharacteristic is not null && + _hardwareRevisionCharacteristic is not null && + _remoteControlCharacteristic is not null); + } + + protected override async Task AfterConnectSetupAsync(bool requestDeviceInformation, CancellationToken token) + { + try + { + if (requestDeviceInformation) + { + await ReadDeviceInfo(token).ConfigureAwait(false); + } + } + catch { } + + return true; + } + + protected override async Task ProcessOutputsAsync(CancellationToken token) + { + try + { + // reset outputs + _bankOutputs0.Initialize(); + _bankOutputs1.Initialize(); + + while (!token.IsCancellationRequested) + { + // process first bank 0 + bool changed = await TryProcessChanges(_bankOutputs0, LIGHTS_FLAGS_APPLY | LIGHTS_FLAGS_BANK_0, token); + + // process first bank 1 + if (await TryProcessChanges(_bankOutputs1, LIGHTS_FLAGS_APPLY | LIGHTS_FLAGS_BANK_1, token)) + { + changed = true; + } + + if (!changed) + { + await Task.Delay(10, token).ConfigureAwait(false); + } + } + } + catch + { + } + } + + private async Task TryProcessChanges(OutputValuesGroup valueBank, byte flags, CancellationToken token) + { + try + { + if (valueBank.TryGetValues(out var values)) + { + var command = BuildSetAllLights(flags, values); + var success = await _bleDevice!.WriteAsync(_remoteControlCharacteristic!, command, token).ConfigureAwait(false); + if (success) + { + // confirm successfull sending + valueBank.Commmit(); + await Task.Delay(5, token).ConfigureAwait(false); + return true; + } + } + return false; + } + catch + { + return false; + } + } + + private async Task ReadDeviceInfo(CancellationToken token) + { + var firmwareData = await _bleDevice!.ReadAsync(_firmwareRevisionCharacteristic!, token); + var firmwareVersion = firmwareData?.ToAsciiStringSafe(); + if (!string.IsNullOrEmpty(firmwareVersion)) + { + FirmwareVersion = firmwareVersion; + } + + var hardwareData = await _bleDevice.ReadAsync(_hardwareRevisionCharacteristic!, token); + var hardwareVersion = hardwareData?.ToAsciiStringSafe(); + if (!string.IsNullOrEmpty(hardwareVersion)) + { + HardwareVersion = hardwareVersion; + } + + // 0x0F Query ADC | voltage on 0x08 + await _bleDevice.WriteAsync(_remoteControlCharacteristic!, [0x0f, 0x08], token); + var voltageBuffer = await _bleDevice!.ReadAsync(_remoteControlCharacteristic!, token); + if (voltageBuffer is not null && voltageBuffer.Length >= 2) + { + var rawVoltage = voltageBuffer[0] + (voltageBuffer[1] << 8); + var voltage = (rawVoltage * 0.42567F) / 2047; + BatteryVoltage = voltage.ToString("F2"); + } + } + } +} diff --git a/BrickController2/BrickController2/DeviceManagement/Vengit/SBrickProtocol.cs b/BrickController2/BrickController2/DeviceManagement/Vengit/SBrickProtocol.cs new file mode 100644 index 00000000..d4120d7d --- /dev/null +++ b/BrickController2/BrickController2/DeviceManagement/Vengit/SBrickProtocol.cs @@ -0,0 +1,36 @@ +using System; + +namespace BrickController2.DeviceManagement.Vengit; + +/// +/// Contains implementation of SBrick protocol +/// +internal static class SBrickProtocol +{ + /// + /// SBrick - Remote control service UUID + /// + public static readonly Guid ServiceUuid = new("4dc591b0-857c-41de-b5f1-15abda665b0c"); + /// + /// Remote control service - Remote control commands characteristic UUID + /// + public static readonly Guid RemoteControlCharacteristicUuid = new("02b8cbcc-0e25-4bda-8790-a15f53e6010f"); + + // Light flags + public const byte LIGHTS_FLAGS_BANK_0 = 0x00; + public const byte LIGHTS_FLAGS_BANK_1 = 0x01; + public const byte LIGHTS_FLAGS_APPLY = 0x80; + + // message builders + public static byte[] BuildSetAllLights(byte flags, ReadOnlySpan values) + { + // 0x36 Set all lights + var buffer = new byte[2 + values.Length]; + + buffer[0] = 0x36; + buffer[1] = flags; + values.CopyTo(buffer.AsSpan(2)); + + return buffer; + } +} diff --git a/BrickController2/BrickController2/DeviceManagement/Vengit/Vengit.cs b/BrickController2/BrickController2/DeviceManagement/Vengit/Vengit.cs new file mode 100644 index 00000000..5ab66777 --- /dev/null +++ b/BrickController2/BrickController2/DeviceManagement/Vengit/Vengit.cs @@ -0,0 +1,24 @@ +using BrickController2.DeviceManagement.DI; +using BrickController2.DeviceManagement.Vendors; +using BrickController2.Extensions; + +namespace BrickController2.DeviceManagement.Vengit; + +/// +/// Vendor: Vengit and all its device types: SBrick, SBrick PLus, SBrick Light and implementation of IBluetoothLEDeviceManager +/// +internal class Vengit : Vendor +{ + public override string VendorName => "Vengit"; + + protected override void Register(VendorBuilder builder) + { + // classic devices + builder.ContainerBuilder + .RegisterDevice(DeviceType.SBrick) + .RegisterDevice(DeviceType.SBrickLight); + + // device manager + builder.RegisterDeviceManager(); + } +} diff --git a/BrickController2/BrickController2/Protocols/GattProtocol.cs b/BrickController2/BrickController2/Protocols/GattProtocol.cs new file mode 100644 index 00000000..259e0ed1 --- /dev/null +++ b/BrickController2/BrickController2/Protocols/GattProtocol.cs @@ -0,0 +1,14 @@ +using System; +using System.Buffers.Binary; + +namespace BrickController2.DeviceManagement.Vengit; + +/// +/// Generic GATT protocol +/// +internal static class GattProtocol +{ + public static readonly Guid DeviceInformationServiceUuid = new("0000180a-0000-1000-8000-00805f9b34fb"); + public static readonly Guid FirmwareRevisionCharacteristicUuid = new("00002a26-0000-1000-8000-00805f9b34fb"); + public static readonly Guid HardwareRevisionCharacteristicUuid = new("00002a27-0000-1000-8000-00805f9b34fb"); +} diff --git a/BrickController2/BrickController2/UI/Controls/DeviceChannelLabel.cs b/BrickController2/BrickController2/UI/Controls/DeviceChannelLabel.cs index ed52b8a7..a024be26 100644 --- a/BrickController2/BrickController2/UI/Controls/DeviceChannelLabel.cs +++ b/BrickController2/BrickController2/UI/Controls/DeviceChannelLabel.cs @@ -14,6 +14,7 @@ public class DeviceChannelLabel : Label private readonly static string[] _buwizz3ChannelLetters = new[] { "1", "2", "3", "4", "A", "B" }; private readonly static string[] _mk5ChannelLetters = ["AB", "T", "C", "AB+T", "TL"]; private readonly static string[] _mk6ChannelLetters = new[] { "A", "B", "C", "D", "E", "F" }; + private readonly static char[] _sBrickLightChannelLetters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']; public static readonly BindableProperty DeviceTypeProperty = BindableProperty.Create(nameof(DeviceType), typeof(DeviceType), typeof(DeviceChannelLabel), default(DeviceType), BindingMode.OneWay, null, OnDeviceChanged); public static readonly BindableProperty ChannelProperty = BindableProperty.Create(nameof(Channel), typeof(int), typeof(DeviceChannelLabel), 0, BindingMode.OneWay, null, OnChannelChanged); @@ -93,6 +94,10 @@ private void SetChannelText() SetChannelText(_mk5ChannelLetters); break; + case DeviceType.SBrickLight: // e.g. C.3 + Text = $"{_sBrickLightChannelLetters[Channel / 3]}.{1 + Channel % 3}"; + break; + default: Text = $"{Channel + 1}"; break; diff --git a/BrickController2/BrickController2/UI/Controls/DeviceChannelSelector.xaml b/BrickController2/BrickController2/UI/Controls/DeviceChannelSelector.xaml index e1c4bc47..f1333561 100644 --- a/BrickController2/BrickController2/UI/Controls/DeviceChannelSelector.xaml +++ b/BrickController2/BrickController2/UI/Controls/DeviceChannelSelector.xaml @@ -36,6 +36,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BrickController2/BrickController2/UI/Controls/DeviceChannelSelector.xaml.cs b/BrickController2/BrickController2/UI/Controls/DeviceChannelSelector.xaml.cs index 66e5f31a..5d3436fb 100644 --- a/BrickController2/BrickController2/UI/Controls/DeviceChannelSelector.xaml.cs +++ b/BrickController2/BrickController2/UI/Controls/DeviceChannelSelector.xaml.cs @@ -17,6 +17,30 @@ public DeviceChannelSelector() SBrickChannel1.Command = new SafeCommand(() => SelectedChannel = 1); SBrickChannel2.Command = new SafeCommand(() => SelectedChannel = 2); SBrickChannel3.Command = new SafeCommand(() => SelectedChannel = 3); + SBrickLightChannelA1.Command = new SafeCommand(() => SelectedChannel = 0); + SBrickLightChannelA2.Command = new SafeCommand(() => SelectedChannel = 1); + SBrickLightChannelA3.Command = new SafeCommand(() => SelectedChannel = 2); + SBrickLightChannelB1.Command = new SafeCommand(() => SelectedChannel = 3); + SBrickLightChannelB2.Command = new SafeCommand(() => SelectedChannel = 4); + SBrickLightChannelB3.Command = new SafeCommand(() => SelectedChannel = 5); + SBrickLightChannelC1.Command = new SafeCommand(() => SelectedChannel = 6); + SBrickLightChannelC2.Command = new SafeCommand(() => SelectedChannel = 7); + SBrickLightChannelC3.Command = new SafeCommand(() => SelectedChannel = 8); + SBrickLightChannelD1.Command = new SafeCommand(() => SelectedChannel = 9); + SBrickLightChannelD2.Command = new SafeCommand(() => SelectedChannel = 10); + SBrickLightChannelD3.Command = new SafeCommand(() => SelectedChannel = 11); + SBrickLightChannelE1.Command = new SafeCommand(() => SelectedChannel = 12); + SBrickLightChannelE2.Command = new SafeCommand(() => SelectedChannel = 13); + SBrickLightChannelE3.Command = new SafeCommand(() => SelectedChannel = 14); + SBrickLightChannelF1.Command = new SafeCommand(() => SelectedChannel = 15); + SBrickLightChannelF2.Command = new SafeCommand(() => SelectedChannel = 16); + SBrickLightChannelF3.Command = new SafeCommand(() => SelectedChannel = 17); + SBrickLightChannelG1.Command = new SafeCommand(() => SelectedChannel = 18); + SBrickLightChannelG2.Command = new SafeCommand(() => SelectedChannel = 19); + SBrickLightChannelG3.Command = new SafeCommand(() => SelectedChannel = 20); + SBrickLightChannelH1.Command = new SafeCommand(() => SelectedChannel = 21); + SBrickLightChannelH2.Command = new SafeCommand(() => SelectedChannel = 22); + SBrickLightChannelH3.Command = new SafeCommand(() => SelectedChannel = 23); BuWizzChannel0.Command = new SafeCommand(() => SelectedChannel = 0); BuWizzChannel1.Command = new SafeCommand(() => SelectedChannel = 1); BuWizzChannel2.Command = new SafeCommand(() => SelectedChannel = 2); @@ -111,6 +135,7 @@ private static void OnDeviceChanged(BindableObject bindable, object oldValue, ob { var deviceType = device.DeviceType; dcs.SbrickSection.IsVisible = deviceType == DeviceType.SBrick; + dcs.SbrickLightSection.IsVisible = deviceType == DeviceType.SBrickLight; dcs.BuWizzSection.IsVisible = deviceType == DeviceType.BuWizz || deviceType == DeviceType.BuWizz2; dcs.BuWizz3Section.IsVisible = deviceType == DeviceType.BuWizz3; dcs.InfraredSection.IsVisible = deviceType == DeviceType.Infrared; @@ -145,6 +170,30 @@ private static void OnSelectedChannelChanged(BindableObject bindable, object old dcs.SBrickChannel1.SelectedChannel = selectedChannel; dcs.SBrickChannel2.SelectedChannel = selectedChannel; dcs.SBrickChannel3.SelectedChannel = selectedChannel; + dcs.SBrickLightChannelA1.SelectedChannel = selectedChannel; + dcs.SBrickLightChannelA2.SelectedChannel = selectedChannel; + dcs.SBrickLightChannelA3.SelectedChannel = selectedChannel; + dcs.SBrickLightChannelB1.SelectedChannel = selectedChannel; + dcs.SBrickLightChannelB2.SelectedChannel = selectedChannel; + dcs.SBrickLightChannelB3.SelectedChannel = selectedChannel; + dcs.SBrickLightChannelC1.SelectedChannel = selectedChannel; + dcs.SBrickLightChannelC2.SelectedChannel = selectedChannel; + dcs.SBrickLightChannelC3.SelectedChannel = selectedChannel; + dcs.SBrickLightChannelD1.SelectedChannel = selectedChannel; + dcs.SBrickLightChannelD2.SelectedChannel = selectedChannel; + dcs.SBrickLightChannelD3.SelectedChannel = selectedChannel; + dcs.SBrickLightChannelE1.SelectedChannel = selectedChannel; + dcs.SBrickLightChannelE2.SelectedChannel = selectedChannel; + dcs.SBrickLightChannelE3.SelectedChannel = selectedChannel; + dcs.SBrickLightChannelF1.SelectedChannel = selectedChannel; + dcs.SBrickLightChannelF2.SelectedChannel = selectedChannel; + dcs.SBrickLightChannelF3.SelectedChannel = selectedChannel; + dcs.SBrickLightChannelG1.SelectedChannel = selectedChannel; + dcs.SBrickLightChannelG2.SelectedChannel = selectedChannel; + dcs.SBrickLightChannelG3.SelectedChannel = selectedChannel; + dcs.SBrickLightChannelH1.SelectedChannel = selectedChannel; + dcs.SBrickLightChannelH2.SelectedChannel = selectedChannel; + dcs.SBrickLightChannelH3.SelectedChannel = selectedChannel; dcs.BuWizzChannel0.SelectedChannel = selectedChannel; dcs.BuWizzChannel1.SelectedChannel = selectedChannel; dcs.BuWizzChannel2.SelectedChannel = selectedChannel; diff --git a/BrickController2/BrickController2/UI/Converters/DeviceTypeToImageConverter.cs b/BrickController2/BrickController2/UI/Converters/DeviceTypeToImageConverter.cs index ec45aa58..2d4ddf1a 100644 --- a/BrickController2/BrickController2/UI/Converters/DeviceTypeToImageConverter.cs +++ b/BrickController2/BrickController2/UI/Converters/DeviceTypeToImageConverter.cs @@ -28,6 +28,9 @@ public class DeviceTypeToImageConverter : IValueConverter case DeviceType.SBrick: return ResourceHelper.GetImageResource("sbrick_image.png"); + case DeviceType.SBrickLight: + return ResourceHelper.GetImageResource("sbricklight_image.png"); + case DeviceType.Infrared: return ResourceHelper.GetImageResource("infra_image.png"); diff --git a/BrickController2/BrickController2/UI/Converters/DeviceTypeToSmallImageConverter.cs b/BrickController2/BrickController2/UI/Converters/DeviceTypeToSmallImageConverter.cs index 532a6965..ebaa0d2e 100644 --- a/BrickController2/BrickController2/UI/Converters/DeviceTypeToSmallImageConverter.cs +++ b/BrickController2/BrickController2/UI/Converters/DeviceTypeToSmallImageConverter.cs @@ -23,6 +23,9 @@ public class DeviceTypeToSmallImageConverter : IValueConverter case DeviceType.SBrick: return ResourceHelper.GetImageResource("sbrick_image_small.png"); + case DeviceType.SBrickLight: + return ResourceHelper.GetImageResource("sbricklight_image_small.png"); + case DeviceType.Infrared: return ResourceHelper.GetImageResource("infra_image_small.png"); diff --git a/BrickController2/BrickController2/UI/Images/sbricklight_image.png b/BrickController2/BrickController2/UI/Images/sbricklight_image.png new file mode 100644 index 0000000000000000000000000000000000000000..7580bd9a8721bd0c427e2a55ac7b8628cdec3270 GIT binary patch literal 55024 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 z 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 aSN Date: Wed, 31 Dec 2025 23:34:19 +0100 Subject: [PATCH 2/4] Fix SBrick Light detection based on manufacturer data --- .../Vengit/SBrickDeviceManagerTests.cs | 51 ++++++++++++++++++- .../Vengit/SBrickDeviceManager.cs | 34 ++++++++++++- .../DeviceManagement/Vengit/SBrickProtocol.cs | 7 +++ 3 files changed, 88 insertions(+), 4 deletions(-) diff --git a/BrickController2/BrickController2.Tests/DeviceManagement/Vengit/SBrickDeviceManagerTests.cs b/BrickController2/BrickController2.Tests/DeviceManagement/Vengit/SBrickDeviceManagerTests.cs index 2497924f..d933acda 100644 --- a/BrickController2/BrickController2.Tests/DeviceManagement/Vengit/SBrickDeviceManagerTests.cs +++ b/BrickController2/BrickController2.Tests/DeviceManagement/Vengit/SBrickDeviceManagerTests.cs @@ -8,9 +8,10 @@ namespace BrickController2.Tests.DeviceManagement.Vengit; public class SBrickDeviceManagerTests : DeviceManagerTestBase { [Fact] - public void TryGetDevice_VengitManufacturerId_ReturnsSBrickDevice() + public void TryGetDevice_VengitManufacturerIdWithSBrickProductId_ReturnsSBrickDevice() { - byte[] manufacturerData = [0x98, 0x01]; + byte[] manufacturerData = [0x98, 0x01, + 0x02, 0x00, 0x00]; var scanResult = CreateScanResult(deviceName: default, manufacturerData: manufacturerData); var result = _manager.TryGetDevice(scanResult, out var device); @@ -25,6 +26,52 @@ public void TryGetDevice_VengitManufacturerId_ReturnsSBrickDevice() }); } + [Fact] + public void TryGetDevice_VengitManufacturerIdWithSBrickLighProductId_ReturnsSBrickLightDevice() + { + byte[] manufacturerData = [0x98, 0x01, + 0x02, 0x03, 0x00, + 0x06, 0x00, 0x01, 0x05, 0x00, 0x05, 0x19]; + var scanResult = CreateScanResult(deviceName: default, manufacturerData: manufacturerData); + + var result = _manager.TryGetDevice(scanResult, out var device); + + result.Should().BeTrue(); + device.Should().BeEquivalentTo(new FoundDevice() + { + DeviceAddress = scanResult.DeviceAddress, + DeviceName = scanResult.DeviceName, + DeviceType = DeviceType.SBrickLight, + ManufacturerData = manufacturerData + }); + } + + [Fact] + public void TryGetDevice_VengitManufacturerIdWithUnknownProductId_ReturnsSBrickDevice() + { + byte[] manufacturerData = [0x98, 0x01, + 0x06, 0x00, 0xAA, 0x00, 0x00, 0x00, 0x00]; + var scanResult = CreateScanResult(deviceName: default, manufacturerData: manufacturerData); + + var result = _manager.TryGetDevice(scanResult, out var device); + + result.Should().BeFalse(); + device.DeviceType.Should().Be(DeviceType.Unknown); + } + + [Fact] + public void TryGetDevice_VengitManufacturerIdWithMissingProductId_ReturnsSBrickDevice() + { + byte[] manufacturerData = [0x98, 0x01, + 0x02, 0x03, 0x00]; + var scanResult = CreateScanResult(deviceName: default, manufacturerData: manufacturerData); + + var result = _manager.TryGetDevice(scanResult, out var device); + + result.Should().BeFalse(); + device.DeviceType.Should().Be(DeviceType.Unknown); + } + [Fact] public void TryGetDevice_WrongManufacturerId_ReturnsFalse() { diff --git a/BrickController2/BrickController2/DeviceManagement/Vengit/SBrickDeviceManager.cs b/BrickController2/BrickController2/DeviceManagement/Vengit/SBrickDeviceManager.cs index d39b4ed5..a4dfdf22 100644 --- a/BrickController2/BrickController2/DeviceManagement/Vengit/SBrickDeviceManager.cs +++ b/BrickController2/BrickController2/DeviceManagement/Vengit/SBrickDeviceManager.cs @@ -1,6 +1,8 @@ using System; using BrickController2.PlatformServices.BluetoothLE; +using static BrickController2.DeviceManagement.Vengit.SBrickProtocol; + namespace BrickController2.DeviceManagement.Vengit; /// @@ -15,11 +17,39 @@ public bool TryGetDevice(ScanResult scanResult, out FoundDevice device) // check if there are any data and it matches Vengit prefix 0x0198 if (scanResult.TryGetManufacturerData(out var manufacturerData) && manufacturerData.StartsWith(ManufacturerId)) { - device = new FoundDevice(scanResult, DeviceType.SBrick, manufacturerData); - return true; + var productId = GetProductId(manufacturerData); + + device = productId switch + { + PRODUCT_ID_SBRICK => new FoundDevice(scanResult, DeviceType.SBrick, manufacturerData), + PRODUCT_ID_SBRICK_LIGHT => new FoundDevice(scanResult, DeviceType.SBrickLight, manufacturerData), + + _ => default + }; + return device.DeviceType != DeviceType.Unknown; } device = default; return false; } + + private static byte GetProductId(ReadOnlySpan manufacturerData) + { + // walk throuh SBrick Data Records to look for 0x00 Product type + int length = 2; + ReadOnlySpan dataRecord = manufacturerData; + + while (length < dataRecord.Length) + { + dataRecord = dataRecord[length..]; + length = 1 + dataRecord[0]; + + if (length > 2 && dataRecord[1] == DATA_RECORD_PRODUCT_TYPE) + { + return dataRecord[2]; + } + } + + return PRODUCT_ID_UNKNOWN; + } } diff --git a/BrickController2/BrickController2/DeviceManagement/Vengit/SBrickProtocol.cs b/BrickController2/BrickController2/DeviceManagement/Vengit/SBrickProtocol.cs index d4120d7d..05215624 100644 --- a/BrickController2/BrickController2/DeviceManagement/Vengit/SBrickProtocol.cs +++ b/BrickController2/BrickController2/DeviceManagement/Vengit/SBrickProtocol.cs @@ -21,6 +21,13 @@ internal static class SBrickProtocol public const byte LIGHTS_FLAGS_BANK_1 = 0x01; public const byte LIGHTS_FLAGS_APPLY = 0x80; + // data records + public const byte DATA_RECORD_PRODUCT_TYPE = 0x00; + + public const byte PRODUCT_ID_SBRICK = 0x00; + public const byte PRODUCT_ID_SBRICK_LIGHT = 0x01; + public const byte PRODUCT_ID_UNKNOWN = 0xFF; + // message builders public static byte[] BuildSetAllLights(byte flags, ReadOnlySpan values) { From 4ac8489b49293da48f198ef7fc3952e3c45c29ec Mon Sep 17 00:00:00 2001 From: Vit Nemecky Date: Thu, 1 Jan 2026 19:21:04 +0100 Subject: [PATCH 3/4] channel selector - version for phone and desktop --- .../ChannelSelectorRadioButton.xaml.cs | 8 +- .../UI/Controls/DeviceChannelLabel.cs | 4 + .../UI/Controls/DeviceChannelSelector.xaml | 83 ++++++++++++------- 3 files changed, 59 insertions(+), 36 deletions(-) diff --git a/BrickController2/BrickController2/UI/Controls/ChannelSelectorRadioButton.xaml.cs b/BrickController2/BrickController2/UI/Controls/ChannelSelectorRadioButton.xaml.cs index 054ed878..b737c10a 100644 --- a/BrickController2/BrickController2/UI/Controls/ChannelSelectorRadioButton.xaml.cs +++ b/BrickController2/BrickController2/UI/Controls/ChannelSelectorRadioButton.xaml.cs @@ -13,10 +13,10 @@ public ChannelSelectorRadioButton() InitializeComponent(); } - public static BindableProperty DeviceTypeProperty = BindableProperty.Create(nameof(DeviceType), typeof(DeviceType), typeof(ChannelSelectorRadioButton), default(DeviceType), BindingMode.OneWay, null, OnDeviceTypeChanged); - public static BindableProperty ChannelProperty = BindableProperty.Create(nameof(Channel), typeof(int), typeof(ChannelSelectorRadioButton), 0, BindingMode.OneWay, null, OnChannelChanged); - public static BindableProperty SelectedChannelProperty = BindableProperty.Create(nameof(SelectedChannel), typeof(int), typeof(ChannelSelectorRadioButton), 0, BindingMode.OneWay, null, OnSelectedChannelChanged); - public static BindableProperty CommandProperty = BindableProperty.Create(nameof(Command), typeof(ICommand), typeof(ChannelSelectorRadioButton), null, BindingMode.OneWay, null, OnCommandChanged); + public static readonly BindableProperty DeviceTypeProperty = BindableProperty.Create(nameof(DeviceType), typeof(DeviceType), typeof(ChannelSelectorRadioButton), default(DeviceType), BindingMode.OneWay, null, OnDeviceTypeChanged); + public static readonly BindableProperty ChannelProperty = BindableProperty.Create(nameof(Channel), typeof(int), typeof(ChannelSelectorRadioButton), 0, BindingMode.OneWay, null, OnChannelChanged); + public static readonly BindableProperty SelectedChannelProperty = BindableProperty.Create(nameof(SelectedChannel), typeof(int), typeof(ChannelSelectorRadioButton), 0, BindingMode.OneWay, null, OnSelectedChannelChanged); + public static readonly BindableProperty CommandProperty = BindableProperty.Create(nameof(Command), typeof(ICommand), typeof(ChannelSelectorRadioButton), null, BindingMode.OneWay, null, OnCommandChanged); public DeviceType DeviceType { diff --git a/BrickController2/BrickController2/UI/Controls/DeviceChannelLabel.cs b/BrickController2/BrickController2/UI/Controls/DeviceChannelLabel.cs index a024be26..919cbaf7 100644 --- a/BrickController2/BrickController2/UI/Controls/DeviceChannelLabel.cs +++ b/BrickController2/BrickController2/UI/Controls/DeviceChannelLabel.cs @@ -98,6 +98,10 @@ private void SetChannelText() Text = $"{_sBrickLightChannelLetters[Channel / 3]}.{1 + Channel % 3}"; break; + case DeviceType.Unknown: + Text = ""; + break; + default: Text = $"{Channel + 1}"; break; diff --git a/BrickController2/BrickController2/UI/Controls/DeviceChannelSelector.xaml b/BrickController2/BrickController2/UI/Controls/DeviceChannelSelector.xaml index f1333561..fe22d8a4 100644 --- a/BrickController2/BrickController2/UI/Controls/DeviceChannelSelector.xaml +++ b/BrickController2/BrickController2/UI/Controls/DeviceChannelSelector.xaml @@ -40,51 +40,70 @@ + + - - - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + From 1faa7c661a6a212b5e2a65fe0d8d0c05aee5637d Mon Sep 17 00:00:00 2001 From: Vit Nemecky Date: Thu, 1 Jan 2026 19:43:17 +0100 Subject: [PATCH 4/4] review comments + preserve git hitory --- .../{Vengit => }/SBrickDeviceManagerTests.cs | 9 +- .../{Vengit => }/SBrickDevice.cs | 2 +- .../{Vengit => }/SBrickDeviceManager.cs | 4 +- .../Vengit/SBrickLightDevice.cs | 229 +++++++++--------- .../DeviceManagement/Vengit/Vengit.cs | 2 +- .../UI/Controls/DeviceChannelSelector.xaml | 2 +- 6 files changed, 123 insertions(+), 125 deletions(-) rename BrickController2/BrickController2.Tests/DeviceManagement/{Vengit => }/SBrickDeviceManagerTests.cs (91%) rename BrickController2/BrickController2/DeviceManagement/{Vengit => }/SBrickDevice.cs (99%) rename BrickController2/BrickController2/DeviceManagement/{Vengit => }/SBrickDeviceManager.cs (92%) diff --git a/BrickController2/BrickController2.Tests/DeviceManagement/Vengit/SBrickDeviceManagerTests.cs b/BrickController2/BrickController2.Tests/DeviceManagement/SBrickDeviceManagerTests.cs similarity index 91% rename from BrickController2/BrickController2.Tests/DeviceManagement/Vengit/SBrickDeviceManagerTests.cs rename to BrickController2/BrickController2.Tests/DeviceManagement/SBrickDeviceManagerTests.cs index d933acda..2c72a1f2 100644 --- a/BrickController2/BrickController2.Tests/DeviceManagement/Vengit/SBrickDeviceManagerTests.cs +++ b/BrickController2/BrickController2.Tests/DeviceManagement/SBrickDeviceManagerTests.cs @@ -1,9 +1,8 @@ using BrickController2.DeviceManagement; -using BrickController2.DeviceManagement.Vengit; using FluentAssertions; using Xunit; -namespace BrickController2.Tests.DeviceManagement.Vengit; +namespace BrickController2.Tests.DeviceManagement; public class SBrickDeviceManagerTests : DeviceManagerTestBase { @@ -27,7 +26,7 @@ public void TryGetDevice_VengitManufacturerIdWithSBrickProductId_ReturnsSBrickDe } [Fact] - public void TryGetDevice_VengitManufacturerIdWithSBrickLighProductId_ReturnsSBrickLightDevice() + public void TryGetDevice_VengitManufacturerIdWithSBrickLightProductId_ReturnsSBrickLightDevice() { byte[] manufacturerData = [0x98, 0x01, 0x02, 0x03, 0x00, @@ -47,7 +46,7 @@ public void TryGetDevice_VengitManufacturerIdWithSBrickLighProductId_ReturnsSBri } [Fact] - public void TryGetDevice_VengitManufacturerIdWithUnknownProductId_ReturnsSBrickDevice() + public void TryGetDevice_VengitManufacturerIdWithUnknownProductId_ReturnsFalse() { byte[] manufacturerData = [0x98, 0x01, 0x06, 0x00, 0xAA, 0x00, 0x00, 0x00, 0x00]; @@ -60,7 +59,7 @@ public void TryGetDevice_VengitManufacturerIdWithUnknownProductId_ReturnsSBrickD } [Fact] - public void TryGetDevice_VengitManufacturerIdWithMissingProductId_ReturnsSBrickDevice() + public void TryGetDevice_VengitManufacturerIdWithMissingProductId_ReturnsFalse() { byte[] manufacturerData = [0x98, 0x01, 0x02, 0x03, 0x00]; diff --git a/BrickController2/BrickController2/DeviceManagement/Vengit/SBrickDevice.cs b/BrickController2/BrickController2/DeviceManagement/SBrickDevice.cs similarity index 99% rename from BrickController2/BrickController2/DeviceManagement/Vengit/SBrickDevice.cs rename to BrickController2/BrickController2/DeviceManagement/SBrickDevice.cs index 87e23907..99972cd0 100644 --- a/BrickController2/BrickController2/DeviceManagement/Vengit/SBrickDevice.cs +++ b/BrickController2/BrickController2/DeviceManagement/SBrickDevice.cs @@ -6,7 +6,7 @@ using System.Threading; using System.Threading.Tasks; -namespace BrickController2.DeviceManagement.Vengit +namespace BrickController2.DeviceManagement { internal class SBrickDevice : BluetoothDevice { diff --git a/BrickController2/BrickController2/DeviceManagement/Vengit/SBrickDeviceManager.cs b/BrickController2/BrickController2/DeviceManagement/SBrickDeviceManager.cs similarity index 92% rename from BrickController2/BrickController2/DeviceManagement/Vengit/SBrickDeviceManager.cs rename to BrickController2/BrickController2/DeviceManagement/SBrickDeviceManager.cs index a4dfdf22..5f99c6d4 100644 --- a/BrickController2/BrickController2/DeviceManagement/Vengit/SBrickDeviceManager.cs +++ b/BrickController2/BrickController2/DeviceManagement/SBrickDeviceManager.cs @@ -3,7 +3,7 @@ using static BrickController2.DeviceManagement.Vengit.SBrickProtocol; -namespace BrickController2.DeviceManagement.Vengit; +namespace BrickController2.DeviceManagement; /// /// Manager for SBrick devices @@ -35,7 +35,7 @@ public bool TryGetDevice(ScanResult scanResult, out FoundDevice device) private static byte GetProductId(ReadOnlySpan manufacturerData) { - // walk throuh SBrick Data Records to look for 0x00 Product type + // walk through SBrick Data Records to look for 0x00 Product type int length = 2; ReadOnlySpan dataRecord = manufacturerData; diff --git a/BrickController2/BrickController2/DeviceManagement/Vengit/SBrickLightDevice.cs b/BrickController2/BrickController2/DeviceManagement/Vengit/SBrickLightDevice.cs index 7c9ee750..ffef2a3f 100644 --- a/BrickController2/BrickController2/DeviceManagement/Vengit/SBrickLightDevice.cs +++ b/BrickController2/BrickController2/DeviceManagement/Vengit/SBrickLightDevice.cs @@ -9,157 +9,156 @@ using static BrickController2.DeviceManagement.Vengit.SBrickProtocol; -namespace BrickController2.DeviceManagement.Vengit +namespace BrickController2.DeviceManagement.Vengit; + +internal class SBrickLightDevice : BluetoothDevice { - internal class SBrickLightDevice : BluetoothDevice - { - private const int BANK_0_CHANNELS = 16; - private const int BANK_1_CHANNELS = 8; + private const int BANK_0_CHANNELS = 16; + private const int BANK_1_CHANNELS = 8; - private readonly OutputValuesGroup _bankOutputs0 = new(BANK_0_CHANNELS); - private readonly OutputValuesGroup _bankOutputs1 = new(BANK_1_CHANNELS); + private readonly OutputValuesGroup _bankOutputs0 = new(BANK_0_CHANNELS); + private readonly OutputValuesGroup _bankOutputs1 = new(BANK_1_CHANNELS); - private IGattCharacteristic? _firmwareRevisionCharacteristic; - private IGattCharacteristic? _hardwareRevisionCharacteristic; - private IGattCharacteristic? _remoteControlCharacteristic; + private IGattCharacteristic? _firmwareRevisionCharacteristic; + private IGattCharacteristic? _hardwareRevisionCharacteristic; + private IGattCharacteristic? _remoteControlCharacteristic; - public SBrickLightDevice(string name, string address, byte[] deviceData, IDeviceRepository deviceRepository, IBluetoothLEService bleService) - : base(name, address, deviceRepository, bleService) - { - } + public SBrickLightDevice(string name, string address, byte[] deviceData, IDeviceRepository deviceRepository, IBluetoothLEService bleService) + : base(name, address, deviceRepository, bleService) + { + } - public override DeviceType DeviceType => DeviceType.SBrickLight; - public override string BatteryVoltageSign => "V"; - public override int NumberOfChannels => BANK_0_CHANNELS + BANK_1_CHANNELS; - protected override bool AutoConnectOnFirstConnect => false; + public override DeviceType DeviceType => DeviceType.SBrickLight; + public override string BatteryVoltageSign => "V"; + public override int NumberOfChannels => BANK_0_CHANNELS + BANK_1_CHANNELS; + protected override bool AutoConnectOnFirstConnect => false; - public override void SetOutput(int channel, float value) - { - CheckChannel(channel); - value = CutOutputValue(value); + public override void SetOutput(int channel, float value) + { + CheckChannel(channel); + value = CutOutputValue(value); - // for lights use 0-255 range - var rawValue = (byte)(Math.Abs(value) * 255); + // for lights use 0-255 range + var rawValue = (byte)(Math.Abs(value) * 255); - if (channel >= BANK_0_CHANNELS) - { - int lightChannel = channel - BANK_0_CHANNELS; - _bankOutputs1.SetOutput(lightChannel, rawValue); - } - else - { - _bankOutputs0.SetOutput(channel, rawValue); - } + if (channel >= BANK_0_CHANNELS) + { + int lightChannel = channel - BANK_0_CHANNELS; + _bankOutputs1.SetOutput(lightChannel, rawValue); } - - protected override Task ValidateServicesAsync(IEnumerable? services, CancellationToken token) + else { - var deviceInformationService = services?.FirstOrDefault(s => s.Uuid == GattProtocol.DeviceInformationServiceUuid); - _firmwareRevisionCharacteristic = deviceInformationService?.Characteristics?.FirstOrDefault(c => c.Uuid == GattProtocol.FirmwareRevisionCharacteristicUuid); - _hardwareRevisionCharacteristic = deviceInformationService?.Characteristics?.FirstOrDefault(c => c.Uuid == GattProtocol.HardwareRevisionCharacteristicUuid); + _bankOutputs0.SetOutput(channel, rawValue); + } + } - var remoteControlService = services?.FirstOrDefault(s => s.Uuid == SBrickProtocol.ServiceUuid); - _remoteControlCharacteristic = remoteControlService?.Characteristics?.FirstOrDefault(c => c.Uuid == RemoteControlCharacteristicUuid); + protected override Task ValidateServicesAsync(IEnumerable? services, CancellationToken token) + { + var deviceInformationService = services?.FirstOrDefault(s => s.Uuid == GattProtocol.DeviceInformationServiceUuid); + _firmwareRevisionCharacteristic = deviceInformationService?.Characteristics?.FirstOrDefault(c => c.Uuid == GattProtocol.FirmwareRevisionCharacteristicUuid); + _hardwareRevisionCharacteristic = deviceInformationService?.Characteristics?.FirstOrDefault(c => c.Uuid == GattProtocol.HardwareRevisionCharacteristicUuid); - return Task.FromResult( - _firmwareRevisionCharacteristic is not null && - _hardwareRevisionCharacteristic is not null && - _remoteControlCharacteristic is not null); - } + var remoteControlService = services?.FirstOrDefault(s => s.Uuid == SBrickProtocol.ServiceUuid); + _remoteControlCharacteristic = remoteControlService?.Characteristics?.FirstOrDefault(c => c.Uuid == RemoteControlCharacteristicUuid); - protected override async Task AfterConnectSetupAsync(bool requestDeviceInformation, CancellationToken token) + return Task.FromResult( + _firmwareRevisionCharacteristic is not null && + _hardwareRevisionCharacteristic is not null && + _remoteControlCharacteristic is not null); + } + + protected override async Task AfterConnectSetupAsync(bool requestDeviceInformation, CancellationToken token) + { + try { - try + if (requestDeviceInformation) { - if (requestDeviceInformation) - { - await ReadDeviceInfo(token).ConfigureAwait(false); - } + await ReadDeviceInfo(token).ConfigureAwait(false); } - catch { } - - return true; } + catch { } - protected override async Task ProcessOutputsAsync(CancellationToken token) + return true; + } + + protected override async Task ProcessOutputsAsync(CancellationToken token) + { + try { - try + // reset outputs + _bankOutputs0.Initialize(); + _bankOutputs1.Initialize(); + + while (!token.IsCancellationRequested) { - // reset outputs - _bankOutputs0.Initialize(); - _bankOutputs1.Initialize(); + // process first bank 0 + bool changed = await TryProcessChanges(_bankOutputs0, LIGHTS_FLAGS_APPLY | LIGHTS_FLAGS_BANK_0, token); - while (!token.IsCancellationRequested) + // process additional bank 1 + if (await TryProcessChanges(_bankOutputs1, LIGHTS_FLAGS_APPLY | LIGHTS_FLAGS_BANK_1, token)) { - // process first bank 0 - bool changed = await TryProcessChanges(_bankOutputs0, LIGHTS_FLAGS_APPLY | LIGHTS_FLAGS_BANK_0, token); - - // process first bank 1 - if (await TryProcessChanges(_bankOutputs1, LIGHTS_FLAGS_APPLY | LIGHTS_FLAGS_BANK_1, token)) - { - changed = true; - } - - if (!changed) - { - await Task.Delay(10, token).ConfigureAwait(false); - } + changed = true; + } + + if (!changed) + { + await Task.Delay(10, token).ConfigureAwait(false); } - } - catch - { } } + catch + { + } + } - private async Task TryProcessChanges(OutputValuesGroup valueBank, byte flags, CancellationToken token) + private async Task TryProcessChanges(OutputValuesGroup valueBank, byte flags, CancellationToken token) + { + try { - try + if (valueBank.TryGetValues(out var values)) { - if (valueBank.TryGetValues(out var values)) + var command = BuildSetAllLights(flags, values); + var success = await _bleDevice!.WriteAsync(_remoteControlCharacteristic!, command, token).ConfigureAwait(false); + if (success) { - var command = BuildSetAllLights(flags, values); - var success = await _bleDevice!.WriteAsync(_remoteControlCharacteristic!, command, token).ConfigureAwait(false); - if (success) - { - // confirm successfull sending - valueBank.Commmit(); - await Task.Delay(5, token).ConfigureAwait(false); - return true; - } + // confirm successful sending + valueBank.Commmit(); + await Task.Delay(5, token).ConfigureAwait(false); + return true; } - return false; - } - catch - { - return false; } + return false; + } + catch + { + return false; } + } - private async Task ReadDeviceInfo(CancellationToken token) + private async Task ReadDeviceInfo(CancellationToken token) + { + var firmwareData = await _bleDevice!.ReadAsync(_firmwareRevisionCharacteristic!, token); + var firmwareVersion = firmwareData?.ToAsciiStringSafe(); + if (!string.IsNullOrEmpty(firmwareVersion)) { - var firmwareData = await _bleDevice!.ReadAsync(_firmwareRevisionCharacteristic!, token); - var firmwareVersion = firmwareData?.ToAsciiStringSafe(); - if (!string.IsNullOrEmpty(firmwareVersion)) - { - FirmwareVersion = firmwareVersion; - } + FirmwareVersion = firmwareVersion; + } - var hardwareData = await _bleDevice.ReadAsync(_hardwareRevisionCharacteristic!, token); - var hardwareVersion = hardwareData?.ToAsciiStringSafe(); - if (!string.IsNullOrEmpty(hardwareVersion)) - { - HardwareVersion = hardwareVersion; - } + var hardwareData = await _bleDevice.ReadAsync(_hardwareRevisionCharacteristic!, token); + var hardwareVersion = hardwareData?.ToAsciiStringSafe(); + if (!string.IsNullOrEmpty(hardwareVersion)) + { + HardwareVersion = hardwareVersion; + } - // 0x0F Query ADC | voltage on 0x08 - await _bleDevice.WriteAsync(_remoteControlCharacteristic!, [0x0f, 0x08], token); - var voltageBuffer = await _bleDevice!.ReadAsync(_remoteControlCharacteristic!, token); - if (voltageBuffer is not null && voltageBuffer.Length >= 2) - { - var rawVoltage = voltageBuffer[0] + (voltageBuffer[1] << 8); - var voltage = (rawVoltage * 0.42567F) / 2047; - BatteryVoltage = voltage.ToString("F2"); - } + // 0x0F Query ADC | voltage on 0x08 + await _bleDevice.WriteAsync(_remoteControlCharacteristic!, [0x0f, 0x08], token); + var voltageBuffer = await _bleDevice!.ReadAsync(_remoteControlCharacteristic!, token); + if (voltageBuffer is not null && voltageBuffer.Length >= 2) + { + var rawVoltage = voltageBuffer[0] + (voltageBuffer[1] << 8); + var voltage = (rawVoltage * 0.42567F) / 2047; + BatteryVoltage = voltage.ToString("F2"); } } } diff --git a/BrickController2/BrickController2/DeviceManagement/Vengit/Vengit.cs b/BrickController2/BrickController2/DeviceManagement/Vengit/Vengit.cs index 5ab66777..34505ee7 100644 --- a/BrickController2/BrickController2/DeviceManagement/Vengit/Vengit.cs +++ b/BrickController2/BrickController2/DeviceManagement/Vengit/Vengit.cs @@ -5,7 +5,7 @@ namespace BrickController2.DeviceManagement.Vengit; /// -/// Vendor: Vengit and all its device types: SBrick, SBrick PLus, SBrick Light and implementation of IBluetoothLEDeviceManager +/// Vendor: Vengit and all its device types: SBrick, SBrick Plus, SBrick Light and implementation of IBluetoothLEDeviceManager /// internal class Vengit : Vendor { diff --git a/BrickController2/BrickController2/UI/Controls/DeviceChannelSelector.xaml b/BrickController2/BrickController2/UI/Controls/DeviceChannelSelector.xaml index fe22d8a4..76ae73df 100644 --- a/BrickController2/BrickController2/UI/Controls/DeviceChannelSelector.xaml +++ b/BrickController2/BrickController2/UI/Controls/DeviceChannelSelector.xaml @@ -72,7 +72,7 @@