diff --git a/BrickController2/BrickController2.Android/PlatformServices/DI/PlatformServicesModule.cs b/BrickController2/BrickController2.Android/PlatformServices/DI/PlatformServicesModule.cs index ef38a5f3..ec254a14 100644 --- a/BrickController2/BrickController2.Android/PlatformServices/DI/PlatformServicesModule.cs +++ b/BrickController2/BrickController2.Android/PlatformServices/DI/PlatformServicesModule.cs @@ -2,10 +2,12 @@ using BrickController2.DeviceManagement.CaDA; using BrickController2.DeviceManagement.JieStar; using BrickController2.DeviceManagement.MouldKing; +using BrickController2.DeviceManagement.PowerBox; using BrickController2.Droid.PlatformServices.BluetoothLE; using BrickController2.Droid.PlatformServices.DeviceManagement.CaDA; using BrickController2.Droid.PlatformServices.DeviceManagement.JieStar; using BrickController2.Droid.PlatformServices.DeviceManagement.MouldKing; +using BrickController2.Droid.PlatformServices.DeviceManagement.PowerBox; using BrickController2.Droid.PlatformServices.GameController; using BrickController2.Droid.PlatformServices.Infrared; using BrickController2.Droid.PlatformServices.Localization; @@ -35,6 +37,7 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); } } } \ No newline at end of file diff --git a/BrickController2/BrickController2.Android/PlatformServices/DeviceManagement/PowerBox/PowerBoxPlatformService.cs b/BrickController2/BrickController2.Android/PlatformServices/DeviceManagement/PowerBox/PowerBoxPlatformService.cs new file mode 100644 index 00000000..34462c09 --- /dev/null +++ b/BrickController2/BrickController2.Android/PlatformServices/DeviceManagement/PowerBox/PowerBoxPlatformService.cs @@ -0,0 +1,24 @@ +using BrickController2.DeviceManagement.PowerBox; +using BrickController2.Protocols; + +namespace BrickController2.Droid.PlatformServices.DeviceManagement.PowerBox; + +public class PowerBoxPlatformService : IPowerBoxPlatformService +{ + private const int HeaderOffset = 15; + private const int PayloadLength = 18; + + public bool TryGetRfPayload(byte[] rawData, out byte[] rfPayload) + { + rfPayload = new byte[PayloadLength]; + int payloadLength = CryptTools.GetRfPayload(PowerBoxProtocol.SeedArray, PowerBoxProtocol.HeaderArray, rawData, HeaderOffset, PowerBoxProtocol.CTXValue1, PowerBoxProtocol.CTXValue2, rfPayload); + + // fill rest of array + for (int index = payloadLength; index < PayloadLength; index++) + { + rfPayload[index] = (byte)(index + 1); + } + + return true; + } +} diff --git a/BrickController2/BrickController2.Tests/DeviceManagement/DI/PowerBoxVendorTests.cs b/BrickController2/BrickController2.Tests/DeviceManagement/DI/PowerBoxVendorTests.cs new file mode 100644 index 00000000..a805dda3 --- /dev/null +++ b/BrickController2/BrickController2.Tests/DeviceManagement/DI/PowerBoxVendorTests.cs @@ -0,0 +1,67 @@ +using Autofac; +using BrickController2.DeviceManagement; +using BrickController2.DeviceManagement.PowerBox; +using BrickController2.PlatformServices.BluetoothLE; +using BrickController2.UI.Images; +using FluentAssertions; +using Moq; +using Xunit; +using PowerBoxVendor = BrickController2.DeviceManagement.PowerBox.PowerBox; + +namespace BrickController2.Tests.DeviceManagement.DI; + +public class PowerBoxVendorTests : VendorTestsBase +{ + private readonly DeviceFactory _deviceFactory; + + public PowerBoxVendorTests() + { + // Act + var container = InitializeContainer().Build(); + + _deviceFactory = container.Resolve(); + } + + protected override ContainerBuilder InitializeContainer() + { + var builder = base.InitializeContainer(); + + // Arrange + builder.RegisterInstance(Mock.Of()); + builder.RegisterInstance(Mock.Of()); + builder.RegisterInstance(Mock.Of()); // needed because RegisterDevice().WithImages("powerfunctions_image.png", "powerfunctions_image_small.png") + + // execute registration of vendor PowerBox + builder.RegisterModule(); + + return builder; + } + + [Theory] + [InlineData("Device")] + public void RegisterDevice_PowerBox_MBattery_ReturnedDevice(string address) + { + DeviceType deviceType = DeviceType.PowerBoxMBattery; + string name = "TestDevice"; + byte[] deviceData = [1, 2, 3]; + + var device = _deviceFactory(deviceType, name, address, deviceData, []); + + device.Should().NotBeNull(); + device.Should().BeOfType(); + } + + [Theory] + [InlineData("Device")] + public void RegisterDevice_PowerBox_ASeries_ReturnedDevice(string address) + { + DeviceType deviceType = DeviceType.PowerBoxASeries; + string name = "TestDevice"; + byte[] deviceData = [1, 2, 3]; + + var device = _deviceFactory(deviceType, name, address, deviceData, []); + + device.Should().NotBeNull(); + device.Should().BeOfType(); + } +} diff --git a/BrickController2/BrickController2.Tests/DeviceManagement/PowerBox/PowerBoxASeriesDatagramTests.cs b/BrickController2/BrickController2.Tests/DeviceManagement/PowerBox/PowerBoxASeriesDatagramTests.cs new file mode 100644 index 00000000..949cc483 --- /dev/null +++ b/BrickController2/BrickController2.Tests/DeviceManagement/PowerBox/PowerBoxASeriesDatagramTests.cs @@ -0,0 +1,113 @@ +using BrickController2.DeviceManagement.PowerBox; +using FluentAssertions; +using System; +using Xunit; + +namespace BrickController2.Tests.DeviceManagement.PowerBox; + +public sealed class PowerBoxAseriesDatagramTests : PowerBoxDatagramTestsBase +{ + private const byte PayloadIdentifierConnect1 = 0xa4; + private const byte PayloadIdentifierConnect2 = 0x5b; + private const byte PayloadIdentifierCommand1 = 0x40; + private const byte PayloadIdentifierCommand2 = 0xbf; + + /// + /// This test checks that the payload identifiers are correctly set in the connect datagram for each device address. + /// + [Fact] + public void TryGetTelegram_ConnectDatagram_PayloadIdentifier() + { + PowerBoxASeries device = new PowerBoxASeries("PowerBoxASeries", PowerBoxASeries.Device, [], _deviceRepository.Object, _bluetoothLEService.Object, _powerBoxPlatformService, _manager.Object); + + device.TryGetTelegram(true, out byte[] payload).Should().BeTrue(); + payload[0].Should().Be(PayloadIdentifierConnect1); + payload[7].Should().Be(PayloadIdentifierConnect2); + } + + /// + /// This test checks that the AppIdentifier is correctly included in the connect datagram payload for each device address. + /// + [Fact] + public void TryGetTelegram_ConnectDatagram_AppIdentifier() + { + PowerBoxASeries device = new PowerBoxASeries("PowerBoxASeries", PowerBoxASeries.Device, [], _deviceRepository.Object, _bluetoothLEService.Object, _powerBoxPlatformService, _manager.Object); + + device.TryGetTelegram(true, out byte[] payload).Should().BeTrue(); + payload[1].Should().Be(AppIdentifier1); + payload[2].Should().Be(AppIdentifier2); + } + + /// + /// This test checks that the payload identifiers are correctly set in the command datagram for each device address. + /// + [Fact] + public void TryGetTelegram_CommandDatagram_PayloadIdentifier() + { + PowerBoxASeries device = new PowerBoxASeries("PowerBoxASeries", PowerBoxASeries.Device, [], _deviceRepository.Object, _bluetoothLEService.Object, _powerBoxPlatformService, _manager.Object); + + device.TryGetTelegram(false, out byte[] payload).Should().BeTrue(); + payload[0].Should().Be(PayloadIdentifierCommand1); + payload[7].Should().Be(PayloadIdentifierCommand2); + } + + /// + /// This test checks that the AppIdentifier is correctly included in the command datagram payload for each device address. + /// + [Fact] + public void TryGetTelegram_CommandDatagram_AppIdentifier() + { + PowerBoxASeries device = new PowerBoxASeries("PowerBoxASeries", PowerBoxASeries.Device, [], _deviceRepository.Object, _bluetoothLEService.Object, _powerBoxPlatformService, _manager.Object); + + device.TryGetTelegram(false, out byte[] payload).Should().BeTrue(); + payload[1].Should().Be(AppIdentifier1); + payload[2].Should().Be(AppIdentifier2); + } + + /// + /// This test checks that the command datagram payload is correctly constructed based on the set output values for each device address. + /// + /// The set values for each channel. + /// The expected payload for the command datagram. + [Theory] + [InlineData(new float[] { 0.0f, 0.0f, 0.0f, 0.0f }, new byte[] { PayloadIdentifierCommand1, AppIdentifier1, AppIdentifier2, 0x00, 0x00, 0x00, 0x00, PayloadIdentifierCommand2 })] // all channels neutral + [InlineData(new float[] { 1.0f, 0.0f, 0.0f, 0.0f }, new byte[] { PayloadIdentifierCommand1, AppIdentifier1, AppIdentifier2, 0xf0, 0x00, 0x00, 0x00, PayloadIdentifierCommand2 })] // channel 1 maximum, others neutral + [InlineData(new float[] { 0.0f, 1.0f, 0.0f, 0.0f }, new byte[] { PayloadIdentifierCommand1, AppIdentifier1, AppIdentifier2, 0x0f, 0x00, 0x00, 0x00, PayloadIdentifierCommand2 })] // channel 2 maximum, others neutral + [InlineData(new float[] { 0.0f, 0.0f, 1.0f, 0.0f }, new byte[] { PayloadIdentifierCommand1, AppIdentifier1, AppIdentifier2, 0x00, 0xf0, 0x00, 0x00, PayloadIdentifierCommand2 })] // channel 3 maximum, others neutral + [InlineData(new float[] { 0.0f, 0.0f, 0.0f, 1.0f }, new byte[] { PayloadIdentifierCommand1, AppIdentifier1, AppIdentifier2, 0x00, 0x0f, 0x00, 0x00, PayloadIdentifierCommand2 })] // channel 4 maximum, others neutral + [InlineData(new float[] { -1.0f, 0.0f, 0.0f, 0.0f }, new byte[] { PayloadIdentifierCommand1, AppIdentifier1, AppIdentifier2, 0x70, 0x00, 0x00, 0x00, PayloadIdentifierCommand2 })] // channel 1 minimum, others neutral + [InlineData(new float[] { 0.0f, -1.0f, 0.0f, 0.0f }, new byte[] { PayloadIdentifierCommand1, AppIdentifier1, AppIdentifier2, 0x07, 0x00, 0x00, 0x00, PayloadIdentifierCommand2 })] // channel 2 minimum, others neutral + [InlineData(new float[] { 0.0f, 0.0f, -1.0f, 0.0f }, new byte[] { PayloadIdentifierCommand1, AppIdentifier1, AppIdentifier2, 0x00, 0x70, 0x00, 0x00, PayloadIdentifierCommand2 })] // channel 3 minimum, others neutral + [InlineData(new float[] { 0.0f, 0.0f, 0.0f, -1.0f }, new byte[] { PayloadIdentifierCommand1, AppIdentifier1, AppIdentifier2, 0x00, 0x07, 0x00, 0x00, PayloadIdentifierCommand2 })] // channel 4 minimum, others neutral + [InlineData(new float[] { 9.0f, 9.0f, 9.0f, 9.0f, }, new byte[] { PayloadIdentifierCommand1, AppIdentifier1, AppIdentifier2, 0xff, 0xff, 0x00, 0x00, PayloadIdentifierCommand2 })] // all channels above maximum, should be clamped to maximum + [InlineData(new float[] { -9.0f, -9.0f, -9.0f, -9.0f }, new byte[] { PayloadIdentifierCommand1, AppIdentifier1, AppIdentifier2, 0x77, 0x77, 0x00, 0x00, PayloadIdentifierCommand2 })] // all channels below minimum, should be clamped to minimum + public void TryGetTelegram_CommandDatagram_Payload(float[] setValues, byte[] expectedPayload) + { + PowerBoxASeries device = new PowerBoxASeries("PowerBoxASeries", PowerBoxASeries.Device, [], _deviceRepository.Object, _bluetoothLEService.Object, _powerBoxPlatformService, _manager.Object); + + // Set the output values for the device + for (int i = 0; i < setValues.Length; i++) + { + device.SetOutput(i, setValues[i]); + } + + // Get the command datagram payload + device.TryGetTelegram(false, out byte[] payload).Should().BeTrue(); + + // Check that the payload matches the expected values + payload.Should().Equal(expectedPayload); + } + + /// + /// Tests that setting an illegal channel index throws an ArgumentOutOfRangeException. + /// + [Fact] + public void TryGetTelegram_CommandDatagram_SetIllegalChannel() + { + PowerBoxASeries device = new PowerBoxASeries("PowerBoxASeries", PowerBoxASeries.Device, [], _deviceRepository.Object, _bluetoothLEService.Object, _powerBoxPlatformService, _manager.Object); + + Action action = () => device.SetOutput(device.NumberOfChannels, 0); + + action.Should().Throw(); + } +} diff --git a/BrickController2/BrickController2.Tests/DeviceManagement/PowerBox/PowerBoxDatagramTestsBase.cs b/BrickController2/BrickController2.Tests/DeviceManagement/PowerBox/PowerBoxDatagramTestsBase.cs new file mode 100644 index 00000000..89d2d6bb --- /dev/null +++ b/BrickController2/BrickController2.Tests/DeviceManagement/PowerBox/PowerBoxDatagramTestsBase.cs @@ -0,0 +1,37 @@ +using BrickController2.DeviceManagement; +using BrickController2.DeviceManagement.PowerBox; +using BrickController2.PlatformServices.BluetoothLE; +using Moq; + +namespace BrickController2.Tests.DeviceManagement.PowerBox; + +public abstract class PowerBoxDatagramTestsBase +{ + protected const byte AppIdentifier1 = 0x61; // This is the first byte of an randomly choosen AppIdentifier for UnitTesting + protected const byte AppIdentifier2 = 0x62; // This is the second byte of an randomly choosen AppIdentifier for UnitTesting + + protected static readonly byte[] AppIdentifier = [AppIdentifier1, AppIdentifier2]; + + /// + /// This class is a test implementation of the IPowerBoxPlatformService interface that simulates the behavior of the TryGetRfPayload method for testing purposes. + /// It always returns true and sets the rfPayload to the rawData provided. + /// + protected class TestPowerBoxPlatformService : IPowerBoxPlatformService + { + public bool TryGetRfPayload(byte[] rawData, out byte[] rfPayload) + { + rfPayload = rawData; + return true; // Simulate success + } + } + + protected readonly Mock _bluetoothLEService = new(MockBehavior.Strict); + protected readonly Mock _manager = new(MockBehavior.Strict); + protected readonly TestPowerBoxPlatformService _powerBoxPlatformService = new(); + internal readonly Mock _deviceRepository = new(MockBehavior.Strict); + + protected PowerBoxDatagramTestsBase() + { + _manager.Setup(x => x.GetAppId()).Returns(AppIdentifier); + } +} diff --git a/BrickController2/BrickController2.Tests/DeviceManagement/PowerBox/PowerBoxDeviceManagerTests.cs b/BrickController2/BrickController2.Tests/DeviceManagement/PowerBox/PowerBoxDeviceManagerTests.cs new file mode 100644 index 00000000..335fac92 --- /dev/null +++ b/BrickController2/BrickController2.Tests/DeviceManagement/PowerBox/PowerBoxDeviceManagerTests.cs @@ -0,0 +1,32 @@ +using BrickController2.DeviceManagement.PowerBox; +using BrickController2.UI.Services.AppIdentifier; +using BrickController2.UI.Services.Preferences; +using FluentAssertions; +using Moq; +using Xunit; + +namespace BrickController2.Tests.DeviceManagement.PowerBox; + +public class PowerBoxDeviceManagerTests +{ + private const byte AppIdentifier1 = 0x61; // 'a' = 0x61 + private const byte AppIdentifier2 = 0x62; // 'b' = 0x62 + + private readonly IPowerBoxDeviceManager _manager; + private readonly Mock _appIdentifierService = new(MockBehavior.Strict); + + public PowerBoxDeviceManagerTests() + { + _appIdentifierService.Setup(x => x.GetAppId(2)).Returns(new byte[] { AppIdentifier1, AppIdentifier2 }); + _manager = new PowerBoxDeviceManager(_appIdentifierService.Object); + } + + [Fact] + public void AppId_TwoBytesInPreferences_AllBytes() + { + var appId = _manager.GetAppId(); + appId.Length.Should().Be(2); + appId.Span[0].Should().Be(AppIdentifier1); + appId.Span[1].Should().Be(AppIdentifier2); + } +} diff --git a/BrickController2/BrickController2.Tests/DeviceManagement/PowerBox/PowerBoxMBatteryDatagramTests.cs b/BrickController2/BrickController2.Tests/DeviceManagement/PowerBox/PowerBoxMBatteryDatagramTests.cs new file mode 100644 index 00000000..b73484d2 --- /dev/null +++ b/BrickController2/BrickController2.Tests/DeviceManagement/PowerBox/PowerBoxMBatteryDatagramTests.cs @@ -0,0 +1,107 @@ +using BrickController2.DeviceManagement.PowerBox; +using FluentAssertions; +using System; +using Xunit; + +namespace BrickController2.Tests.DeviceManagement.PowerBox; + +public sealed class PowerBoxMBatteryDatagramTests : PowerBoxDatagramTestsBase +{ + private const byte PayloadIdentifierConnect1 = 0xa4; + private const byte PayloadIdentifierConnect2 = 0x5b; + private const byte PayloadIdentifierCommand1 = 0x40; + private const byte PayloadIdentifierCommand2 = 0xbf; + + /// + /// This test checks that the payload identifiers are correctly set in the connect datagram for each device address. + /// + [Fact] + public void TryGetTelegram_ConnectDatagram_PayloadIdentifier() + { + PowerBoxMBattery device = new PowerBoxMBattery("PowerBoxMBattery", PowerBoxMBattery.Device, [], _deviceRepository.Object, _bluetoothLEService.Object, _powerBoxPlatformService, _manager.Object); + + device.TryGetTelegram(true, out byte[] payload).Should().BeTrue(); + payload[0].Should().Be(PayloadIdentifierConnect1); + payload[7].Should().Be(PayloadIdentifierConnect2); + } + + /// + /// This test checks that the AppIdentifier is correctly included in the connect datagram payload for each device address. + /// + /// The address of the device to test. + [Fact] + public void TryGetTelegram_ConnectDatagram_AppIdentifier() + { + PowerBoxMBattery device = new PowerBoxMBattery("PowerBoxMBattery", PowerBoxMBattery.Device, [], _deviceRepository.Object, _bluetoothLEService.Object, _powerBoxPlatformService, _manager.Object); + + device.TryGetTelegram(true, out byte[] payload).Should().BeTrue(); + payload[1].Should().Be(AppIdentifier1); + payload[2].Should().Be(AppIdentifier2); + } + + /// + /// This test checks that the payload identifiers are correctly set in the command datagram for each device address. + /// + [Fact] + public void TryGetTelegram_CommandDatagram_PayloadIdentifier() + { + PowerBoxMBattery device = new PowerBoxMBattery("PowerBoxMBattery", PowerBoxMBattery.Device, [], _deviceRepository.Object, _bluetoothLEService.Object, _powerBoxPlatformService, _manager.Object); + + device.TryGetTelegram(false, out byte[] payload).Should().BeTrue(); + payload[0].Should().Be(PayloadIdentifierCommand1); + payload[7].Should().Be(PayloadIdentifierCommand2); + } + + /// + /// This test checks that the AppIdentifier is correctly included in the command datagram payload for each device address. + /// + [Fact] + public void TryGetTelegram_CommandDatagram_AppIdentifier() + { + PowerBoxMBattery device = new PowerBoxMBattery("PowerBoxMBattery", PowerBoxMBattery.Device, [], _deviceRepository.Object, _bluetoothLEService.Object, _powerBoxPlatformService, _manager.Object); + + device.TryGetTelegram(false, out byte[] payload).Should().BeTrue(); + payload[1].Should().Be(AppIdentifier1); + payload[2].Should().Be(AppIdentifier2); + } + + /// + /// This test checks that the command datagram payload is correctly constructed based on the set output values for each device address. + /// + /// The set values for each channel. + /// The expected payload for the command datagram. + [Theory] + [InlineData(new float[] { 0.0f }, new byte[] { PayloadIdentifierCommand1, AppIdentifier1, AppIdentifier2, 0x00, 0x00, 0x00, 0x00, PayloadIdentifierCommand2 })] // all channels neutral + [InlineData(new float[] { 1.0f }, new byte[] { PayloadIdentifierCommand1, AppIdentifier1, AppIdentifier2, 0xf7, 0xf7, 0x00, 0x00, PayloadIdentifierCommand2 })] // channel 1 maximum, others neutral + [InlineData(new float[] { -1.0f }, new byte[] { PayloadIdentifierCommand1, AppIdentifier1, AppIdentifier2, 0x7f, 0x7f, 0x00, 0x00, PayloadIdentifierCommand2 })] // channel 1 minimum, others neutral + [InlineData(new float[] { 9.0f }, new byte[] { PayloadIdentifierCommand1, AppIdentifier1, AppIdentifier2, 0xf7, 0xf7, 0x00, 0x00, PayloadIdentifierCommand2 })] // all channels above maximum, should be clamped to maximum + public void TryGetTelegram_CommandDatagram_Payload(float[] setValues, byte[] expectedPayload) + { + PowerBoxMBattery device = new PowerBoxMBattery("PowerBoxMBattery", PowerBoxMBattery.Device, [], _deviceRepository.Object, _bluetoothLEService.Object, _powerBoxPlatformService, _manager.Object); + + // Set the output values for the device + for (int i = 0; i < setValues.Length; i++) + { + device.SetOutput(i, setValues[i]); + } + + // Get the command datagram payload + device.TryGetTelegram(false, out byte[] payload).Should().BeTrue(); + + // Check that the payload matches the expected values + payload.Should().Equal(expectedPayload); + } + + /// + /// Tests that setting an illegal channel index throws an ArgumentOutOfRangeException. + /// + [Fact] + public void TryGetTelegram_CommandDatagram_SetIllegalChannel() + { + PowerBoxMBattery device = new PowerBoxMBattery("PowerBoxMBattery", PowerBoxMBattery.Device, [], _deviceRepository.Object, _bluetoothLEService.Object, _powerBoxPlatformService, _manager.Object); + + Action action = () => device.SetOutput(device.NumberOfChannels, 0); + + action.Should().Throw(); + } +} diff --git a/BrickController2/BrickController2.Tests/UI/Images/DeviceImagesResourceTests.cs b/BrickController2/BrickController2.Tests/UI/Images/DeviceImagesResourceTests.cs index 1d691a22..dabe7564 100644 --- a/BrickController2/BrickController2.Tests/UI/Images/DeviceImagesResourceTests.cs +++ b/BrickController2/BrickController2.Tests/UI/Images/DeviceImagesResourceTests.cs @@ -77,6 +77,7 @@ private static DeviceImageRegistry CreateRegistry() registry.Register(DeviceType.BuWizz2, "buwizz_image.png", "buwizz_image_small.png"); registry.Register(DeviceType.RemoteControl, "remotecontrol_image_small.png", "remotecontrol_image_small.png"); registry.Register(DeviceType.Infrared, "powerfunctions_image.png", "powerfunctions_image_small.png"); + registry.Register(DeviceType.PowerBoxMBattery, "powerboxmbattery_image.png", "powerboxmbattery_image.png"); return registry; } } diff --git a/BrickController2/BrickController2.WinUI/PlatformServices/DI/PlatformServicesModule.cs b/BrickController2/BrickController2.WinUI/PlatformServices/DI/PlatformServicesModule.cs index 7207d22e..d8b12b77 100644 --- a/BrickController2/BrickController2.WinUI/PlatformServices/DI/PlatformServicesModule.cs +++ b/BrickController2/BrickController2.WinUI/PlatformServices/DI/PlatformServicesModule.cs @@ -2,6 +2,7 @@ using BrickController2.DeviceManagement.CaDA; using BrickController2.DeviceManagement.JieStar; using BrickController2.DeviceManagement.MouldKing; +using BrickController2.DeviceManagement.PowerBox; using BrickController2.PlatformServices.BluetoothLE; using BrickController2.PlatformServices.InputDeviceService; using BrickController2.PlatformServices.Infrared; @@ -12,6 +13,7 @@ using BrickController2.Windows.PlatformServices.DeviceManagement.CaDA; using BrickController2.Windows.PlatformServices.DeviceManagement.JieStar; using BrickController2.Windows.PlatformServices.DeviceManagement.MouldKing; +using BrickController2.Windows.PlatformServices.DeviceManagement.PowerBox; using BrickController2.Windows.PlatformServices.GameController; using BrickController2.Windows.PlatformServices.Infrared; using BrickController2.Windows.PlatformServices.Localization; @@ -34,5 +36,6 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); } } \ No newline at end of file diff --git a/BrickController2/BrickController2.WinUI/PlatformServices/DeviceManagement/PowerBox/PowerBoxPlatformService.cs b/BrickController2/BrickController2.WinUI/PlatformServices/DeviceManagement/PowerBox/PowerBoxPlatformService.cs new file mode 100644 index 00000000..5e5ac7f2 --- /dev/null +++ b/BrickController2/BrickController2.WinUI/PlatformServices/DeviceManagement/PowerBox/PowerBoxPlatformService.cs @@ -0,0 +1,25 @@ +using BrickController2.DeviceManagement.PowerBox; +using BrickController2.Protocols; + +namespace BrickController2.Windows.PlatformServices.DeviceManagement.PowerBox; + +public class PowerBoxPlatformService : IPowerBoxPlatformService +{ + private const int HeaderOffset = 15; + private const int PayloadOffset = 3; + private const int PayloadLength = 23 + PayloadOffset; + + public bool TryGetRfPayload(byte[] rawData, out byte[] rfPayload) + { + rfPayload = new byte[PayloadLength]; + int payloadLength = CryptTools.GetRfPayload(PowerBoxProtocol.SeedArray, PowerBoxProtocol.HeaderArray, rawData, HeaderOffset, PowerBoxProtocol.CTXValue1, PowerBoxProtocol.CTXValue2, rfPayload, PayloadOffset); + + // fill rest of array + for (int index = payloadLength + PayloadOffset; index < PayloadLength; index++) + { + rfPayload[index] = (byte)(index + 1); + } + + return true; + } +} diff --git a/BrickController2/BrickController2.iOS/PlatformServices/DI/PlatformServicesModule.cs b/BrickController2/BrickController2.iOS/PlatformServices/DI/PlatformServicesModule.cs index 4d675a80..aadad457 100644 --- a/BrickController2/BrickController2.iOS/PlatformServices/DI/PlatformServicesModule.cs +++ b/BrickController2/BrickController2.iOS/PlatformServices/DI/PlatformServicesModule.cs @@ -2,10 +2,12 @@ using BrickController2.DeviceManagement.CaDA; using BrickController2.DeviceManagement.JieStar; using BrickController2.DeviceManagement.MouldKing; +using BrickController2.DeviceManagement.PowerBox; using BrickController2.iOS.PlatformServices.BluetoothLE; using BrickController2.iOS.PlatformServices.DeviceManagement.CaDA; using BrickController2.iOS.PlatformServices.DeviceManagement.JieStar; using BrickController2.iOS.PlatformServices.DeviceManagement.MouldKing; +using BrickController2.iOS.PlatformServices.DeviceManagement.PowerBox; using BrickController2.iOS.PlatformServices.GameController; using BrickController2.iOS.PlatformServices.Infrared; using BrickController2.iOS.PlatformServices.Localization; @@ -35,6 +37,7 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); } } } \ No newline at end of file diff --git a/BrickController2/BrickController2.iOS/PlatformServices/DeviceManagement/PowerBox/PowerBoxPlatformService.cs b/BrickController2/BrickController2.iOS/PlatformServices/DeviceManagement/PowerBox/PowerBoxPlatformService.cs new file mode 100644 index 00000000..e563d140 --- /dev/null +++ b/BrickController2/BrickController2.iOS/PlatformServices/DeviceManagement/PowerBox/PowerBoxPlatformService.cs @@ -0,0 +1,25 @@ +using BrickController2.DeviceManagement.PowerBox; +using BrickController2.Protocols; + +namespace BrickController2.iOS.PlatformServices.DeviceManagement.PowerBox; + +public class PowerBoxPlatformService : IPowerBoxPlatformService +{ + private const int HeaderOffset = 13; + private const int PayloadLength = 20; + + public bool TryGetRfPayload(byte[] rawData, out byte[] rfPayload) + { + rfPayload = new byte[PayloadLength]; + int payloadLength = CryptTools.GetRfPayload(PowerBoxProtocol.SeedArray, PowerBoxProtocol.HeaderArray, rawData, HeaderOffset, PowerBoxProtocol.CTXValue1, PowerBoxProtocol.CTXValue2, rfPayload); + + // fill rest of array + byte bVar = 0x12; // initial value + for (int index = payloadLength; index < PayloadLength; index++) + { + rfPayload[index] = bVar++; + } + + return true; + } +} diff --git a/BrickController2/BrickController2/DeviceManagement/DeviceType.cs b/BrickController2/BrickController2/DeviceManagement/DeviceType.cs index 13e7c1a8..f19e07dd 100644 --- a/BrickController2/BrickController2/DeviceManagement/DeviceType.cs +++ b/BrickController2/BrickController2/DeviceManagement/DeviceType.cs @@ -26,5 +26,7 @@ public enum DeviceType SBrickLight, JieStarSCM4, JieStarSCM8, + PowerBoxMBattery, + PowerBoxASeries } } diff --git a/BrickController2/BrickController2/DeviceManagement/PowerBox/IPowerBoxDeviceManager.cs b/BrickController2/BrickController2/DeviceManagement/PowerBox/IPowerBoxDeviceManager.cs new file mode 100644 index 00000000..6970f559 --- /dev/null +++ b/BrickController2/BrickController2/DeviceManagement/PowerBox/IPowerBoxDeviceManager.cs @@ -0,0 +1,11 @@ +using System; + +namespace BrickController2.DeviceManagement.PowerBox; + +/// +/// Interface for PowerBoxDeviceManager. +/// +public interface IPowerBoxDeviceManager +{ + ReadOnlyMemory GetAppId(); +} diff --git a/BrickController2/BrickController2/DeviceManagement/PowerBox/IPowerBoxPlatformService.cs b/BrickController2/BrickController2/DeviceManagement/PowerBox/IPowerBoxPlatformService.cs new file mode 100644 index 00000000..e73d1706 --- /dev/null +++ b/BrickController2/BrickController2/DeviceManagement/PowerBox/IPowerBoxPlatformService.cs @@ -0,0 +1,9 @@ +namespace BrickController2.DeviceManagement.PowerBox; + +/// +/// Interface definition for PowerBox specific PlatformService +/// +public interface IPowerBoxPlatformService +{ + bool TryGetRfPayload(byte[] rawData, out byte[] rfPayload); +} diff --git a/BrickController2/BrickController2/DeviceManagement/PowerBox/PowerBox.cs b/BrickController2/BrickController2/DeviceManagement/PowerBox/PowerBox.cs new file mode 100644 index 00000000..f97522d9 --- /dev/null +++ b/BrickController2/BrickController2/DeviceManagement/PowerBox/PowerBox.cs @@ -0,0 +1,29 @@ +using Autofac; +using BrickController2.DeviceManagement.DI; +using BrickController2.DeviceManagement.Vendors; + +namespace BrickController2.DeviceManagement.PowerBox; + +/// +/// Vendor: PowerBox and all its devices +/// +internal class PowerBox : Vendor +{ + public override string VendorName => "PowerBox"; + + protected override void Register(VendorBuilder builder) + { + // device manager + builder.ContainerBuilder.RegisterType() + .As() + .SingleInstance(); + + // manually added devices + builder.RegisterDevice() + .WithDeviceFactory(PowerBoxMBattery.Device, $"{PowerBoxMBattery.TypeName} Device") + .WithImage("powerboxmbattery_image.png"); + + builder.RegisterDevice() + .WithDeviceFactory(PowerBoxASeries.Device, $"{PowerBoxASeries.TypeName} Device"); + } +} diff --git a/BrickController2/BrickController2/DeviceManagement/PowerBox/PowerBoxASeries.cs b/BrickController2/BrickController2/DeviceManagement/PowerBox/PowerBoxASeries.cs new file mode 100644 index 00000000..aa65dcf0 --- /dev/null +++ b/BrickController2/BrickController2/DeviceManagement/PowerBox/PowerBoxASeries.cs @@ -0,0 +1,79 @@ +using BrickController2.PlatformServices.BluetoothLE; +using BrickController2.Protocols; +using System; + +namespace BrickController2.DeviceManagement.PowerBox; + +/// +/// PowerBox A Series 4 Channels +/// +internal class PowerBoxASeries : PowerBoxBaseNibble, IDeviceType +{ + public const string Device = "Device"; + + /// + /// Telegram to connect to the PowerBox devices + /// This telegram is sent on init and on reconnect conditions matching + /// + private static readonly byte[] Telegram_Connect_Device = [0xa4, 0xfe, 0x19, 0x80, 0x80, 0x80, 0x00, 0x5b]; + + /// + /// Base Telegram for PowerBox devices + /// + private static readonly byte[] Telegram_Base_Device = [0x40, 0xfe, 0x19, 0x00, 0x00, 0x00, 0x00, 0xbf]; + + /// + /// after this timespan and all channel's values equal to zero the connect telegram is sent + /// + private static readonly TimeSpan ReconnectTimeSpan = TimeSpan.FromSeconds(3); + + public PowerBoxASeries(string name, string address, byte[] deviceData, IDeviceRepository deviceRepository, IBluetoothLEService bleService, IPowerBoxPlatformService powerboxPlatformService, IPowerBoxDeviceManager powerboxDeviceManager) + : base(name, address, deviceData, deviceRepository, bleService, powerboxPlatformService, powerboxDeviceManager, Telegram_Connect_Device, Telegram_Base_Device) + { + } + + public static DeviceType Type => DeviceType.PowerBoxASeries; + + public static string TypeName => "PowerBox A Series"; + + public override DeviceType DeviceType => Type; + + /// + /// Gets the number of channels supported by the device. + /// + /// Channel 0..3: real existing channels + /// + /// + public override int NumberOfChannels => 4; + + /// + /// manufacturerId to advertise + /// + protected override ushort ManufacturerId => PowerBoxProtocol.ManufacturerID; + + /// + /// Get or create BluetoothAdvertisingDeviceHandler + /// + /// Instance of BluetoothAdvertisingDeviceHandler + protected override BluetoothAdvertisingDeviceHandler GetBluetoothAdvertisingDeviceHandler() + { + // PowerBoxASeries needs a BluetoothAdvertiser per module + return new BluetoothAdvertisingDeviceHandler(_bleService, ManufacturerId, TryGetTelegram, PowerBoxASeries.ReconnectTimeSpan); + } + + /// + /// Processes the value for the specified analog channel and returns the processed result. + /// + /// The channel number to process. Valid values are 0..3. + /// The input value to be processed for the specified channel. + /// A tuple containing the processed value and a flag indicating the success of the operation. + /// Thrown if is not a valid channel number. + protected override (byte value, bool flag) ProcessChannelValue(int channelNo, float value) => channelNo switch + { + 0 => SetOutput_AnalogChannel(value), + 1 => SetOutput_AnalogChannel(value), + 2 => SetOutput_AnalogChannel(value), + 3 => SetOutput_AnalogChannel(value), + _ => throw new ArgumentException($"Illegal Argument \"{channelNo}\"", nameof(channelNo)) + }; +} diff --git a/BrickController2/BrickController2/DeviceManagement/PowerBox/PowerBoxBaseByte.cs b/BrickController2/BrickController2/DeviceManagement/PowerBox/PowerBoxBaseByte.cs new file mode 100644 index 00000000..959e3f05 --- /dev/null +++ b/BrickController2/BrickController2/DeviceManagement/PowerBox/PowerBoxBaseByte.cs @@ -0,0 +1,203 @@ +using System; +using BrickController2.PlatformServices.BluetoothLE; + +namespace BrickController2.DeviceManagement.PowerBox; + +/// +/// PowerBox baseclass +/// +internal abstract class PowerBoxBaseByte : BluetoothAdvertisingDevice +{ + /// + /// offset to position of first channel in base telegram + /// + private const int CHANNEL_START_OFFSET = 3; + + /// + /// platform specific PowerBox stuff + /// + protected readonly IPowerBoxPlatformService _powerboxPlatformService; + + /// + /// Telegram to connect to the device + /// This telegram is sent on init and on reconnect conditions matching + /// + protected readonly byte[] _telegram_Connect; + + /// + /// base telegram + /// + protected readonly byte[] _telegram_Base; + + /// + /// array to hold the incoming output values for all channels. + /// + protected readonly float[] _storedValues; + + protected PowerBoxBaseByte(string name, string address, byte[] deviceData, IDeviceRepository deviceRepository, IBluetoothLEService bleService, IPowerBoxPlatformService powerboxPlatformService, IPowerBoxDeviceManager powerboxDeviceManager, byte[] telegram_Connect, byte[] telegram_Base) + : base(name, address, deviceData, deviceRepository, bleService) + { + _telegram_Connect = telegram_Connect; + _telegram_Base = telegram_Base; + _powerboxPlatformService = powerboxPlatformService; + _storedValues = new float[NumberOfChannels]; // initialize output values for all channels + + // bytes[1] and [2] of both telegrams can be set to a unique appId + ReadOnlySpan appId = powerboxDeviceManager.GetAppId().Span[..2]; + _telegram_Connect[1] = appId[0]; + _telegram_Connect[2] = appId[1]; + + _telegram_Base[1] = appId[0]; + _telegram_Base[2] = appId[1]; + + // initialize all channels in _telegram_Base and _storedValues to zero value + InitDevice(); + } + + /// + /// No voltage + /// + public override string BatteryVoltageSign => string.Empty; + + /// + /// Sets the output value for the specified channel. + /// + /// This method updates the output value for the specified channel and ensures the value is + /// within the valid range. If the value changes, the method triggers a notification to indicate that data has been + /// updated. + /// The channel number for which the output value is being set. Must be a valid channel index. + /// The output value to set. The value will be adjusted if it exceeds the allowable range. + public override void SetOutput(int channelNo, float value) + { + CheckChannel(channelNo); + value = CutOutputValue(value); + + // store the incoming value in the stored values array + _storedValues[channelNo] = value; + + lock (_outputLock) + { + // call the channel specific set function + bool valueChanged = SetChannelOutput(channelNo, value); + + // check for change + if (valueChanged) + { + _bluetoothAdvertisingDeviceHandler.NotifyDataChanged(); + } + } + } + + /// + /// Processes the specified channel value and returns a transformed result. + /// + /// The exact transformation logic and conditions for success are determined by the implementing + /// class. + /// The channel number to process. Must be a non-negative integer. + /// The input value associated with the channel to be processed. + /// A tuple containing the processed result: value: A byte + /// representing the transformed value for the specified channel. + /// flag: A boolean indicating whether the value is marked as zero () or + /// not (). + protected abstract (byte value, bool flag) ProcessChannelValue(int channelNo, float value); + + /// + /// Updates a specific byte in the telegram buffer and returns whether the value was changed. + /// + /// This method modifies the telegram buffer by updating the specified byte. The operation is + /// thread-safe and ensures exclusive access to the buffer during the + /// update. + /// The zero-based index of the byte in the telegram buffer to modify. + /// The value to set.param> + /// if the byte in the telegram buffer was modified; otherwise, if + /// the value remained unchanged. + protected virtual bool SetChannelValue(int byteOffset, byte setValue_byte) + { + lock (_outputLock) + { + byte originValue_byte = _telegram_Base[byteOffset]; + + _telegram_Base[byteOffset] = setValue_byte; + return _telegram_Base[byteOffset] != originValue_byte; + } + } + + /// + /// Sets the output value for the specified channel. + /// + /// This method handles both virtual and real channels. For virtual channels, the value is + /// processed and the modification status is returned. For real channels, the method calculates the appropriate + /// byte offset and channel-specific parameters, processes the value, and updates the channel state + /// accordingly. + /// The channel number for which the output value is to be set. Must be a valid channel identifier. + /// The output value to set for the specified channel. The value is processed before being applied. + /// if the channel's output value was modified; otherwise, . + protected bool SetChannelOutput(int channelNo, float value) + { + // real channel + int byteOffset = GetTargetPosition(channelNo); + (byte setValue_byte, bool zeroSet) = ProcessChannelValue(channelNo, value); + + _bluetoothAdvertisingDeviceHandler.SetChannelState(channelNo, zeroSet); // set global channel state + return SetChannelValue(byteOffset, setValue_byte); + } + + /// + /// This method sets the device to initial state before advertising starts + /// All channels are initialized with zeroValue. + /// + protected override void InitDevice() => ResetAllChannelsToZero(); + + /// + /// Disconnects the device and resets the output state of all channels to zero. + /// + /// This method ensures that all channels are set to a zero output state during the disconnection + /// process. It is intended to be called as part of the device's disconnection workflow. + protected override void DisconnectDevice() => ResetAllChannelsToZero(); + + /// + /// Attempts to retrieve the RF payload for the specified telegram type. + /// + /// This method delegates the retrieval of the RF payload to the underlying platform + /// service. + /// A boolean value indicating the type of telegram to retrieve. to retrieve the connect + /// telegram; to retrieve the base telegram. + /// When this method returns, contains the RF payload as a byte array if the operation succeeds; otherwise, . + /// if the RF payload was successfully retrieved; otherwise, . + protected internal bool TryGetTelegram(bool getConnectTelegram, out byte[] payload) + { + if (getConnectTelegram) + { + return _powerboxPlatformService.TryGetRfPayload(_telegram_Connect, out payload); + } + else + { + return _powerboxPlatformService.TryGetRfPayload(_telegram_Base, out payload); + } + } + + /// + /// Calculates the target position of a channel within the current instance. + /// + /// The channel number for which the position is calculated. Must be a non-negative integer. + /// The byte offset. + protected virtual int GetTargetPosition(int channelNo) + { + return CHANNEL_START_OFFSET + channelNo; + } + + /// + /// Resets the value and the output state of all channels to zero. + /// + private void ResetAllChannelsToZero() + { + const float zeroValue = 0.0f; + + for (int channelNo = 0; channelNo < NumberOfChannels; channelNo++) + { + _storedValues[channelNo] = zeroValue; + SetChannelOutput(channelNo, zeroValue); + } + } +} diff --git a/BrickController2/BrickController2/DeviceManagement/PowerBox/PowerBoxBaseNibble.cs b/BrickController2/BrickController2/DeviceManagement/PowerBox/PowerBoxBaseNibble.cs new file mode 100644 index 00000000..db4eb850 --- /dev/null +++ b/BrickController2/BrickController2/DeviceManagement/PowerBox/PowerBoxBaseNibble.cs @@ -0,0 +1,265 @@ +using System; +using BrickController2.PlatformServices.BluetoothLE; + +namespace BrickController2.DeviceManagement.PowerBox; + +/// +/// PowerBox baseclass +/// +internal abstract class PowerBoxBaseNibble : BluetoothAdvertisingDevice +{ + /// + /// offset to position of first channel in base telegram + /// + private const int CHANNEL_START_OFFSET = 3; + + /// + /// platform specific PowerBox stuff + /// + protected readonly IPowerBoxPlatformService _powerboxPlatformService; + + /// + /// Telegram to connect to the device + /// This telegram is sent on init and on reconnect conditions matching + /// + protected readonly byte[] _telegram_Connect; + + /// + /// base telegram + /// + protected readonly byte[] _telegram_Base; + + /// + /// array to hold the incoming output values for all channels. + /// + protected readonly float[] _storedValues; + + protected PowerBoxBaseNibble(string name, string address, byte[] deviceData, IDeviceRepository deviceRepository, IBluetoothLEService bleService, IPowerBoxPlatformService powerboxPlatformService, IPowerBoxDeviceManager powerboxDeviceManager, byte[] telegram_Connect, byte[] telegram_Base) + : base(name, address, deviceData, deviceRepository, bleService) + { + _telegram_Connect = telegram_Connect; + _telegram_Base = telegram_Base; + _powerboxPlatformService = powerboxPlatformService; + _storedValues = new float[NumberOfChannels]; // initialize output values for all channels + + // bytes[1] and [2] of both telegrams can be set to a unique appId + ReadOnlySpan appId = powerboxDeviceManager.GetAppId().Span[..2]; + _telegram_Connect[1] = appId[0]; + _telegram_Connect[2] = appId[1]; + + _telegram_Base[1] = appId[0]; + _telegram_Base[2] = appId[1]; + + // initialize all channels in _telegram_Base and _storedValues to zero value + InitDevice(); + } + + /// + /// No voltage + /// + public override string BatteryVoltageSign => string.Empty; + + /// + /// Sets the output value for the specified channel. + /// + /// This method updates the output value for the specified channel and ensures the value is + /// within the valid range. If the value changes, the method triggers a notification to indicate that data has been + /// updated. + /// The channel number for which the output value is being set. Must be a valid channel index. + /// The output value to set. The value will be adjusted if it exceeds the allowable range. + public override void SetOutput(int channelNo, float value) + { + CheckChannel(channelNo); + value = CutOutputValue(value); + + // store the incoming value in the stored values array + _storedValues[channelNo] = value; + + lock (_outputLock) + { + // call the channel specific set function + bool valueChanged = SetChannelOutput(channelNo, value); + + // check for change + if (valueChanged) + { + _bluetoothAdvertisingDeviceHandler.NotifyDataChanged(); + } + } + } + + /// + /// Processes the specified channel value and returns a transformed result. + /// + /// The exact transformation logic and conditions for success are determined by the implementing + /// class. + /// The channel number to process. Must be a non-negative integer. + /// The input value associated with the channel to be processed. + /// A tuple containing the processed result: value: A byte + /// representing the transformed value for the specified channel. + /// flag: A boolean indicating whether the value is marked as zero () or + /// not (). + protected abstract (byte value, bool flag) ProcessChannelValue(int channelNo, float value); + + /// + /// Updates a specific nibble of a byte in the telegram buffer and returns whether the value was changed. + /// + /// This method modifies the telegram buffer by updating either the lower or upper nibble of the + /// specified byte. The operation is thread-safe and ensures exclusive access to the buffer during the + /// update. + /// The zero-based index of the byte in the telegram buffer to modify. + /// A value indicating whether the lower nibble of the byte should be updated. to update the + /// lower nibble; to update the upper nibble. + /// The new nibble value to set, represented as a byte (0-15). + /// if the byte in the telegram buffer was modified; otherwise, if + /// the value remained unchanged. + protected bool SetChannelValue(int byteOffset, bool isLowerNibble, byte setValue_nibble) + { + lock (_outputLock) + { + byte originValue_byte = _telegram_Base[byteOffset]; + + byte setValue_byte; + if (isLowerNibble) + { + setValue_byte = (byte)((originValue_byte & 0xF0) + setValue_nibble); + } + else + { + setValue_byte = (byte)((originValue_byte & 0x0F) + (setValue_nibble << 4)); + } + _telegram_Base[byteOffset] = setValue_byte; + return _telegram_Base[byteOffset] != originValue_byte; + } + } + + /// + /// Converts a floating-point value into a nibble representation for an analog channel output. + /// + /// The method maps the input value to a nibble representation based on predefined ranges for + /// positive, negative, and zero values. The zero value is represented by a specific nibble constant. The caller can + /// use the returned boolean to determine if the nibble corresponds to the zero value. + /// The floating-point value to be converted. Negative values are mapped to the negative range, positive values are + /// mapped to the positive range, and zero is mapped to a predefined nibble. + /// A tuple containing the following: A representing + /// the nibble value for the analog channel output. A + /// indicating whether the nibble corresponds to the zero value. if the nibble represents + /// zero; otherwise, . + protected (byte setValue_Nibble, bool zeroSet) SetOutput_AnalogChannel(float value) + { + // MK4: ZEROVALUE_NIBBLE = 0x08, RANGE_POS_OFFSET = 0x08 + // value < 0: 7 6 5 4 3 2 1 RANGE_NEG: 0x07 + // value == 0: 0 8 + // value > 0: 9 A B C D E F RANGE_POS: 0x07 + + const byte RANGE_POS_OFFSET = 0x08; + const int RANGE_POS = 0x07; + const int RANGE_NEG = 0x07; + + const float MIN_NEG_RANGE_THRESHOLD = -1f / RANGE_NEG; // Minimum value for negative range + const float MIN_POS_RANGE_THRESHOLD = 1f / RANGE_POS; // Minimum value for positive range + + const byte ZEROVALUE_NIBBLE = 0x00; + + if (value <= MIN_NEG_RANGE_THRESHOLD) + { + float value_abs = Math.Min(0x07, -value * RANGE_NEG); + byte setValue_nibble = (byte)(0x0F & (byte)value_abs); + + return (setValue_nibble, false); + } + else if (value >= MIN_POS_RANGE_THRESHOLD) + { + float value_abs = Math.Min(0x0F, (value * RANGE_POS) + RANGE_POS_OFFSET); + byte setValue_nibble = (byte)(0x0F & (byte)(value_abs)); + + return (setValue_nibble, false); + } + else + { + return (ZEROVALUE_NIBBLE, true); + } + } + + /// + /// Sets the output value for the specified channel. + /// + /// This method handles both virtual and real channels. For virtual channels, the value is + /// processed and the modification status is returned. For real channels, the method calculates the appropriate + /// byte offset and channel-specific parameters, processes the value, and updates the channel state + /// accordingly. + /// The channel number for which the output value is to be set. Must be a valid channel identifier. + /// The output value to set for the specified channel. The value is processed before being applied. + /// if the channel's output value was modified; otherwise, . + protected bool SetChannelOutput(int channelNo, float value) + { + // real channel + (int byteOffset, bool isLowerNibble) = GetTargetPosition(channelNo); + (byte setValue_nibble, bool zeroSet) = ProcessChannelValue(channelNo, value); + + _bluetoothAdvertisingDeviceHandler.SetChannelState(channelNo, zeroSet); // set global channel state + return SetChannelValue(byteOffset, isLowerNibble, setValue_nibble); + } + + /// + /// This method sets the device to initial state before advertising starts + /// All channels are initialized with zeroValue. + /// + protected override void InitDevice() => ResetAllChannelsToZero(); + + /// + /// Disconnects the device and resets the output state of all channels to zero. + /// + /// This method ensures that all channels are set to a zero output state during the disconnection + /// process. It is intended to be called as part of the device's disconnection workflow. + protected override void DisconnectDevice() => ResetAllChannelsToZero(); + + /// + /// Attempts to retrieve the RF payload for the specified telegram type. + /// + /// This method delegates the retrieval of the RF payload to the underlying platform + /// service. + /// A boolean value indicating the type of telegram to retrieve. to retrieve the connect + /// telegram; to retrieve the base telegram. + /// When this method returns, contains the RF payload as a byte array if the operation succeeds; otherwise, . + /// if the RF payload was successfully retrieved; otherwise, . + protected internal bool TryGetTelegram(bool getConnectTelegram, out byte[] payload) + { + if (getConnectTelegram) + { + return _powerboxPlatformService.TryGetRfPayload(_telegram_Connect, out payload); + } + else + { + return _powerboxPlatformService.TryGetRfPayload(_telegram_Base, out payload); + } + } + + /// + /// Calculates the target position of a channel + /// + /// The channel number for which the position is calculated. Must be a non-negative integer. + /// A tuple containing the byte offset and a boolean indicating whether the target position is in the lower nibble. + protected virtual (int byteOffset, bool isLowerNibble) GetTargetPosition(int channelNo) + { + return ( + CHANNEL_START_OFFSET + (channelNo >> 1), // div 2 -> 2 channels per byte + (channelNo & 0x01) == 0x01 + ); + } + + /// + /// Resets the value and the output state of all channels to zero. + /// + private void ResetAllChannelsToZero() + { + const float zeroValue = 0.0f; + + for (int channelNo = 0; channelNo < NumberOfChannels; channelNo++) + { + _storedValues[channelNo] = zeroValue; + SetChannelOutput(channelNo, zeroValue); + } + } +} diff --git a/BrickController2/BrickController2/DeviceManagement/PowerBox/PowerBoxDeviceManager.cs b/BrickController2/BrickController2/DeviceManagement/PowerBox/PowerBoxDeviceManager.cs new file mode 100644 index 00000000..87d71d65 --- /dev/null +++ b/BrickController2/BrickController2/DeviceManagement/PowerBox/PowerBoxDeviceManager.cs @@ -0,0 +1,21 @@ +using System; +using BrickController2.UI.Services.AppIdentifier; + +namespace BrickController2.DeviceManagement.PowerBox; + +/// +/// Manager for PowerBox devices +/// +public class PowerBoxDeviceManager : IPowerBoxDeviceManager +{ + private const int AppIdentifierLength = 2; // PowerBox protocol defines 2 bytes for the app identifier + + private readonly ReadOnlyMemory _appIdentifier; + + public PowerBoxDeviceManager(IAppIdentifierService appIdentifierService) + { + _appIdentifier = appIdentifierService.GetAppId(AppIdentifierLength); + } + + public ReadOnlyMemory GetAppId() => _appIdentifier; +} \ No newline at end of file diff --git a/BrickController2/BrickController2/DeviceManagement/PowerBox/PowerBoxMBattery.cs b/BrickController2/BrickController2/DeviceManagement/PowerBox/PowerBoxMBattery.cs new file mode 100644 index 00000000..98ffec42 --- /dev/null +++ b/BrickController2/BrickController2/DeviceManagement/PowerBox/PowerBoxMBattery.cs @@ -0,0 +1,147 @@ +using BrickController2.PlatformServices.BluetoothLE; +using BrickController2.Protocols; +using System; + +namespace BrickController2.DeviceManagement.PowerBox; + +/// +/// PowerBox M Battery with 1 Channel +/// +internal class PowerBoxMBattery : PowerBoxBaseByte, IDeviceType +{ + public const string Device = "Device"; + + /// + /// Telegram to connect to the PowerBox devices + /// This telegram is sent on init and on reconnect conditions matching + /// + private static readonly byte[] Telegram_Connect_Device = [0xa4, 0xfe, 0x19, 0x80, 0x80, 0x80, 0x00, 0x5b]; + + /// + /// Base Telegram for PowerBox devices + /// + private static readonly byte[] Telegram_Base_Device = [0x40, 0xfe, 0x19, 0x00, 0x00, 0x00, 0x00, 0xbf]; + + /// + /// after this timespan and all channel's values equal to zero the connect telegram is sent + /// + private static readonly TimeSpan ReconnectTimeSpan = TimeSpan.FromSeconds(3); + + public PowerBoxMBattery(string name, string address, byte[] deviceData, IDeviceRepository deviceRepository, IBluetoothLEService bleService, IPowerBoxPlatformService powerboxPlatformService, IPowerBoxDeviceManager powerboxDeviceManager) + : base(name, address, deviceData, deviceRepository, bleService, powerboxPlatformService, powerboxDeviceManager, Telegram_Connect_Device, Telegram_Base_Device) + { + } + + public static DeviceType Type => DeviceType.PowerBoxMBattery; + + public static string TypeName => "PowerBox M Battery"; + + public override DeviceType DeviceType => Type; + + /// + /// Gets the number of channels supported by the device. + /// + /// Channel 0: real existing channel + /// + /// + public override int NumberOfChannels => 1; + + /// + /// manufacturerId to advertise + /// + protected override ushort ManufacturerId => PowerBoxProtocol.ManufacturerID; + + /// + /// Get or create BluetoothAdvertisingDeviceHandler + /// + /// Instance of BluetoothAdvertisingDeviceHandler + protected override BluetoothAdvertisingDeviceHandler GetBluetoothAdvertisingDeviceHandler() + { + // PowerBoxMBattery needs a BluetoothAdvertiser per module + return new BluetoothAdvertisingDeviceHandler(_bleService, ManufacturerId, TryGetTelegram, PowerBoxMBattery.ReconnectTimeSpan); + } + + /// + /// Updates a specific byte in the telegram buffer and returns whether the value was changed. + /// + /// This method modifies the telegram buffer by updating the specified byte. The operation is + /// thread-safe and ensures exclusive access to the buffer during the + /// update. + /// The zero-based index of the byte in the telegram buffer to modify. + /// The value to set. + /// if the byte in the telegram buffer was modified; otherwise, if + /// the value remained unchanged. + protected override bool SetChannelValue(int byteOffset, byte setValue_byte) + { + lock (_outputLock) + { + byte originValue1 = _telegram_Base[byteOffset]; + byte originValue2 = _telegram_Base[byteOffset + 1]; + + _telegram_Base[byteOffset] = setValue_byte; + _telegram_Base[byteOffset + 1] = setValue_byte; // very special: bytes are duplicated in the datagram + + return _telegram_Base[byteOffset] != originValue1 || + _telegram_Base[byteOffset + 1] != originValue2; + } + } + + /// + /// Converts a floating-point value into a byte representation for an analog channel output. + /// + /// The method maps the input value to a byte representation based on predefined ranges for + /// positive, negative, and zero values. The zero value is represented by a specific byte constant. The caller can + /// use the returned boolean to determine if the byte corresponds to the zero value. + /// The floating-point value to be converted. Negative values are mapped to the negative range, positive values are + /// mapped to the positive range, and zero is mapped to a predefined byte. + /// A tuple containing the following: A representing + /// the byte value for the analog channel output. A + /// indicating whether the byte corresponds to the zero value. if the byte represents + /// zero; otherwise, . + protected (byte setValue_Byte, bool zeroSet) SetOutput_AnalogChannel(float value) + { + // value < 0: 7f 6e 5d 4c 3b 2a 19 RANGE_NEG: 0x07 + // value == 0: 00 ZEROVALUE + // value > 0: 91 a2 b3 c4 d5 e6 f7 RANGE_POS: 0x07 + + const int RANGE_POS = 0x07; + const int RANGE_NEG = 0x07; + + const float MIN_NEG_RANGE_THRESHOLD = -1f / RANGE_NEG; // Minimum value for negative range + const float MIN_POS_RANGE_THRESHOLD = 1f / RANGE_POS; // Minimum value for positive range + + const byte ZEROVALUE = 0x00; + + if (value <= MIN_NEG_RANGE_THRESHOLD) + { + byte value_abs = (byte)Math.Min(0x07, -value * RANGE_NEG); + byte setValue_byte = (byte)((value_abs << 4) + value_abs + 8); + + return (setValue_byte, false); + } + else if (value >= MIN_POS_RANGE_THRESHOLD) + { + byte value_abs = (byte)Math.Min(0x07, value * RANGE_POS); + byte setValue_byte = (byte)(((value_abs + 8) << 4) + value_abs); + + return (setValue_byte, false); + } + else + { + return (ZEROVALUE, true); + } + } + + /// + /// Processes the value for the specified analog channel and returns the processed result. + /// + /// The channel number to process. Valid value is 0. + /// The input value to be processed for the specified channel. + /// A tuple containing the processed value and a flag indicating the success of the operation. + /// Thrown if is not the valid channel number 0. + protected override (byte value, bool flag) ProcessChannelValue(int channelNo, float value) => channelNo switch + { + 0 => SetOutput_AnalogChannel(value), + _ => throw new ArgumentException($"Illegal Argument \"{channelNo}\"", nameof(channelNo)) + }; +} diff --git a/BrickController2/BrickController2/Protocols/PowerBoxProtocol.cs b/BrickController2/BrickController2/Protocols/PowerBoxProtocol.cs new file mode 100644 index 00000000..cc9fb217 --- /dev/null +++ b/BrickController2/BrickController2/Protocols/PowerBoxProtocol.cs @@ -0,0 +1,44 @@ +namespace BrickController2.Protocols; + +/// +/// static class which implements the encryption algorithm for the advertising data +/// +public static class PowerBoxProtocol +{ + /// + /// ManufacturerID for PowerBox + /// + public const ushort ManufacturerID = 0xFF00; + + /// + /// CTXValue for Encryption + /// + public const byte CTXValue1 = 0x3f; + + /// + /// CTXValue for Encryption + /// + public const byte CTXValue2 = 0x25; + + /// + /// SeedArray + /// + public static readonly byte[] SeedArray = + { + 0xC1, + 0xC2, + 0xC3, + 0xC4, + 0xC5, + }; + + /// + /// HeaderArray + /// + public static readonly byte[] HeaderArray = + { + 0x71, // 0x71 (113) + 0x0f, // 0x0f (15) + 0x55, // 0x55 (85) + }; +} diff --git a/BrickController2/BrickController2/UI/Controls/DeviceChannelLabel.cs b/BrickController2/BrickController2/UI/Controls/DeviceChannelLabel.cs index 5d0a90d0..5d9420a8 100644 --- a/BrickController2/BrickController2/UI/Controls/DeviceChannelLabel.cs +++ b/BrickController2/BrickController2/UI/Controls/DeviceChannelLabel.cs @@ -17,6 +17,7 @@ public class DeviceChannelLabel : Label private readonly static string[] _mk6ChannelLetters = new[] { "A", "B", "C", "D", "E", "F" }; private readonly static string[] _sBrickLightChannelLetters = ["A", "B", "C", "D", "E", "F", "G", "H"]; private readonly static string[] _jieStarChannelLetters = ["A", "B", "C", "D", "E", "F", "G", "H"]; + private readonly static string[] _powerBoxChannelLetters = ["A", "B", "C", "D"]; 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); @@ -112,6 +113,10 @@ private void SetChannelText() case DeviceType.JieStarSCM8: SetChannelText(_jieStarChannelLetters); break; + case DeviceType.PowerBoxMBattery: + case DeviceType.PowerBoxASeries: + SetChannelText(_powerBoxChannelLetters); + break; default: Text = $"{Channel + 1}"; break; diff --git a/BrickController2/BrickController2/UI/Controls/DeviceChannelSelector.xaml b/BrickController2/BrickController2/UI/Controls/DeviceChannelSelector.xaml index dd974e99..2d6aaa4e 100644 --- a/BrickController2/BrickController2/UI/Controls/DeviceChannelSelector.xaml +++ b/BrickController2/BrickController2/UI/Controls/DeviceChannelSelector.xaml @@ -504,6 +504,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/BrickController2/BrickController2/UI/Controls/DeviceChannelSelector.xaml.cs b/BrickController2/BrickController2/UI/Controls/DeviceChannelSelector.xaml.cs index 5f1c68c2..33a5040d 100644 --- a/BrickController2/BrickController2/UI/Controls/DeviceChannelSelector.xaml.cs +++ b/BrickController2/BrickController2/UI/Controls/DeviceChannelSelector.xaml.cs @@ -121,6 +121,13 @@ public DeviceChannelSelector() SBrickLightSubchannel1.Command = new SafeCommand(() => UpdateSBrickSubchannel(SBrickProtocol.LIGHT_PORTS_COUNT * 1)); SBrickLightSubchannel2.Command = new SafeCommand(() => UpdateSBrickSubchannel(SBrickProtocol.LIGHT_PORTS_COUNT * 2)); SBrickLightSubchannel3.Command = new SafeCommand(() => UpdateSBrickSubchannel(SBrickProtocol.LIGHT_PORTS_COUNT * 3)); + // PowerBoxMBattery + PowerBoxMBatteryChannel0.Command = new SafeCommand(() => SelectedChannel = 0); + // PowerBoxASeries + PowerBoxASeriesChannel0.Command = new SafeCommand(() => SelectedChannel = 0); + PowerBoxASeriesChannel1.Command = new SafeCommand(() => SelectedChannel = 1); + PowerBoxASeriesChannel2.Command = new SafeCommand(() => SelectedChannel = 2); + PowerBoxASeriesChannel3.Command = new SafeCommand(() => SelectedChannel = 3); void UpdateSBrickPort(int channel) { @@ -195,6 +202,8 @@ private void OnDeviceChanged(Device device) CaDARaceCarSection.IsVisible = deviceType == DeviceType.CaDA_RaceCar; JieStarSCM4Section.IsVisible = deviceType == DeviceType.JieStarSCM4; JieStarSCM8Section.IsVisible = deviceType == DeviceType.JieStarSCM8; + PowerBoxASeriesSection.IsVisible = deviceType == DeviceType.PowerBoxASeries; + PowerBoxMBatterySection.IsVisible = deviceType == DeviceType.PowerBoxMBattery; } private static void OnSelectedChannelChanged(BindableObject bindable, object oldValue, object newValue) @@ -323,6 +332,13 @@ private void OnSelectedChannelChanged(int selectedChannel) SBrickLightSubchannel1.SelectedChannel = sBrickLightSubchannel; SBrickLightSubchannel2.SelectedChannel = sBrickLightSubchannel; SBrickLightSubchannel3.SelectedChannel = sBrickLightSubchannel; + // PowerBoxMBattery + PowerBoxMBatteryChannel0.SelectedChannel = selectedChannel; + // PowerBoxASeries + PowerBoxASeriesChannel0.SelectedChannel = selectedChannel; + PowerBoxASeriesChannel1.SelectedChannel = selectedChannel; + PowerBoxASeriesChannel2.SelectedChannel = selectedChannel; + PowerBoxASeriesChannel3.SelectedChannel = selectedChannel; } } } \ No newline at end of file diff --git a/BrickController2/BrickController2/UI/Images/powerboxaseries_image.png b/BrickController2/BrickController2/UI/Images/powerboxaseries_image.png new file mode 100644 index 00000000..80abd5c0 Binary files /dev/null and b/BrickController2/BrickController2/UI/Images/powerboxaseries_image.png differ diff --git a/BrickController2/BrickController2/UI/Images/powerboxaseries_image_small.png b/BrickController2/BrickController2/UI/Images/powerboxaseries_image_small.png new file mode 100644 index 00000000..f385b5d7 Binary files /dev/null and b/BrickController2/BrickController2/UI/Images/powerboxaseries_image_small.png differ diff --git a/BrickController2/BrickController2/UI/Images/powerboxmbattery_image.png b/BrickController2/BrickController2/UI/Images/powerboxmbattery_image.png new file mode 100644 index 00000000..023923df Binary files /dev/null and b/BrickController2/BrickController2/UI/Images/powerboxmbattery_image.png differ diff --git a/README.md b/README.md index 4554ccfc..4957abe3 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,8 @@ Cross platform mobile application for controlling your creations using a bluetoo - PFx Brick (lights & Power Functions ports only) - JieStar 4 Channel Smart Creative Module - JieStar 8 Channel Smart Creative Module +- PowerBox MBattery +- PowerBox A Series ## Supported controllers - Generic Bluetooth / USB gamepads