diff --git a/BrickController2/BrickController2.Android/PlatformServices/DI/PlatformServicesModule.cs b/BrickController2/BrickController2.Android/PlatformServices/DI/PlatformServicesModule.cs index 78dd3e988..ef38a5f34 100644 --- a/BrickController2/BrickController2.Android/PlatformServices/DI/PlatformServicesModule.cs +++ b/BrickController2/BrickController2.Android/PlatformServices/DI/PlatformServicesModule.cs @@ -1,8 +1,10 @@ using Autofac; using BrickController2.DeviceManagement.CaDA; +using BrickController2.DeviceManagement.JieStar; using BrickController2.DeviceManagement.MouldKing; 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.GameController; using BrickController2.Droid.PlatformServices.Infrared; @@ -32,6 +34,7 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().As().InstancePerDependency(); 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/JieStar/JieStarPlatformService.cs b/BrickController2/BrickController2.Android/PlatformServices/DeviceManagement/JieStar/JieStarPlatformService.cs new file mode 100644 index 000000000..775016ed2 --- /dev/null +++ b/BrickController2/BrickController2.Android/PlatformServices/DeviceManagement/JieStar/JieStarPlatformService.cs @@ -0,0 +1,24 @@ +using BrickController2.DeviceManagement.JieStar; +using BrickController2.Protocols; + +namespace BrickController2.Droid.PlatformServices.DeviceManagement.JieStar; + +public class JieStarPlatformService : IJieStarPlatformService +{ + private const int HeaderOffset = 15; + private const int PayloadLength = 24; + + public bool TryGetRfPayload(byte ctxValue2, byte[] rawData, out byte[] rfPayload) + { + rfPayload = new byte[PayloadLength]; + int payloadLength = CryptTools.GetRfPayload(JieStarProtocol.SeedArray, JieStarProtocol.HeaderArray, rawData, HeaderOffset, JieStarProtocol.CTXValue1, 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.WinUI/PlatformServices/DI/PlatformServicesModule.cs b/BrickController2/BrickController2.WinUI/PlatformServices/DI/PlatformServicesModule.cs index d685be0ca..7207d22ef 100644 --- a/BrickController2/BrickController2.WinUI/PlatformServices/DI/PlatformServicesModule.cs +++ b/BrickController2/BrickController2.WinUI/PlatformServices/DI/PlatformServicesModule.cs @@ -1,5 +1,6 @@ using Autofac; using BrickController2.DeviceManagement.CaDA; +using BrickController2.DeviceManagement.JieStar; using BrickController2.DeviceManagement.MouldKing; using BrickController2.PlatformServices.BluetoothLE; using BrickController2.PlatformServices.InputDeviceService; @@ -9,6 +10,7 @@ using BrickController2.PlatformServices.SharedFileStorage; using BrickController2.Windows.PlatformServices.BluetoothLE; using BrickController2.Windows.PlatformServices.DeviceManagement.CaDA; +using BrickController2.Windows.PlatformServices.DeviceManagement.JieStar; using BrickController2.Windows.PlatformServices.DeviceManagement.MouldKing; using BrickController2.Windows.PlatformServices.GameController; using BrickController2.Windows.PlatformServices.Infrared; @@ -31,5 +33,6 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().As().InstancePerDependency(); 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/JieStar/JieStarPlatformService.cs b/BrickController2/BrickController2.WinUI/PlatformServices/DeviceManagement/JieStar/JieStarPlatformService.cs new file mode 100644 index 000000000..ee536bdd1 --- /dev/null +++ b/BrickController2/BrickController2.WinUI/PlatformServices/DeviceManagement/JieStar/JieStarPlatformService.cs @@ -0,0 +1,25 @@ +using BrickController2.DeviceManagement.JieStar; +using BrickController2.Protocols; + +namespace BrickController2.Windows.PlatformServices.DeviceManagement.JieStar; + +public class JieStarPlatformService : IJieStarPlatformService +{ + private const int HeaderOffset = 15; + private const int PayloadOffset = 3; + private const int PayloadLength = 24 + PayloadOffset; + + public bool TryGetRfPayload(byte ctxValue2, byte[] rawData, out byte[] rfPayload) + { + rfPayload = new byte[PayloadLength]; + int payloadLength = CryptTools.GetRfPayload(JieStarProtocol.SeedArray, JieStarProtocol.HeaderArray, rawData, HeaderOffset, JieStarProtocol.CTXValue1, 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.WinUI/PlatformServices/GameController/GameControllerService.cs b/BrickController2/BrickController2.WinUI/PlatformServices/GameController/GameControllerService.cs index 84c77338c..5256873fe 100644 --- a/BrickController2/BrickController2.WinUI/PlatformServices/GameController/GameControllerService.cs +++ b/BrickController2/BrickController2.WinUI/PlatformServices/GameController/GameControllerService.cs @@ -82,7 +82,7 @@ private void AddDevices(IEnumerable gamepads) int controllerNumber = GetFirstUnusedInputDeviceNumber(); var newController = new GamepadController(InputDeviceEventService, gamepad!, rawController, controllerNumber, dispatcher!.CreateTimer()); - // UniquePersistantDeviceId looks like "{wgi/nrid/]Xd\\h-M1mO]-il0l-4L\\-Gebf:^3->kBRhM-d4}\0" + // UniquePersistentDeviceId looks like "{wgi/nrid/]Xd\\h-M1mO]-il0l-4L\\-Gebf:^3->kBRhM-d4}\0" AddInputDevice(newController); } } diff --git a/BrickController2/BrickController2.iOS/PlatformServices/DI/PlatformServicesModule.cs b/BrickController2/BrickController2.iOS/PlatformServices/DI/PlatformServicesModule.cs index 74c35225c..4d675a809 100644 --- a/BrickController2/BrickController2.iOS/PlatformServices/DI/PlatformServicesModule.cs +++ b/BrickController2/BrickController2.iOS/PlatformServices/DI/PlatformServicesModule.cs @@ -1,9 +1,11 @@ using Autofac; using BrickController2.DeviceManagement.CaDA; +using BrickController2.DeviceManagement.JieStar; using BrickController2.DeviceManagement.MouldKing; using BrickController2.iOS.PlatformServices.BluetoothLE; -using BrickController2.iOS.PlatformServices.DeviceManagement.MouldKing; using BrickController2.iOS.PlatformServices.DeviceManagement.CaDA; +using BrickController2.iOS.PlatformServices.DeviceManagement.JieStar; +using BrickController2.iOS.PlatformServices.DeviceManagement.MouldKing; using BrickController2.iOS.PlatformServices.GameController; using BrickController2.iOS.PlatformServices.Infrared; using BrickController2.iOS.PlatformServices.Localization; @@ -32,6 +34,7 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().As().InstancePerDependency(); 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/JieStar/JieStarPlatformService.cs b/BrickController2/BrickController2.iOS/PlatformServices/DeviceManagement/JieStar/JieStarPlatformService.cs new file mode 100644 index 000000000..de0d786bc --- /dev/null +++ b/BrickController2/BrickController2.iOS/PlatformServices/DeviceManagement/JieStar/JieStarPlatformService.cs @@ -0,0 +1,25 @@ +using BrickController2.DeviceManagement.JieStar; +using BrickController2.Protocols; + +namespace BrickController2.iOS.PlatformServices.DeviceManagement.JieStar; + +public class JieStarPlatformService : IJieStarPlatformService +{ + private const int HeaderOffset = 13; + private const int PayloadLength = 26; + + public bool TryGetRfPayload(byte ctxValue2, byte[] rawData, out byte[] rfPayload) + { + rfPayload = new byte[PayloadLength]; + int payloadLength = CryptTools.GetRfPayload(JieStarProtocol.SeedArray, JieStarProtocol.HeaderArray, rawData, HeaderOffset, JieStarProtocol.CTXValue1, 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/CaDA/CaDADeviceManager.cs b/BrickController2/BrickController2/DeviceManagement/CaDA/CaDADeviceManager.cs index f5f4f99af..9e63f4864 100644 --- a/BrickController2/BrickController2/DeviceManagement/CaDA/CaDADeviceManager.cs +++ b/BrickController2/BrickController2/DeviceManagement/CaDA/CaDADeviceManager.cs @@ -126,14 +126,14 @@ private static bool IsCadaRaceCarRev2(ReadOnlySpan manufacturerData) => ma manufacturerData[7] == 0x85; /// - /// gets or creates an App-persistant AppIdentifier + /// Gets or creates an app-persistent AppIdentifier. /// - /// reference to preferencesService singleton + /// Reference to preferencesService singleton. /// byte array containing the AppIdentifier private static byte[] GetAppIdentifier(IPreferencesService preferencesService) { byte[] appIdChecksumMaskArray; - // gets or creates an App-persistant AppIdentifier + // gets or creates an app-persistent AppIdentifier try { if (preferencesService.ContainsKey(APPIDKEY, SECTION)) diff --git a/BrickController2/BrickController2/DeviceManagement/DeviceType.cs b/BrickController2/BrickController2/DeviceManagement/DeviceType.cs index ee535d3e9..13e7c1a8d 100644 --- a/BrickController2/BrickController2/DeviceManagement/DeviceType.cs +++ b/BrickController2/BrickController2/DeviceManagement/DeviceType.cs @@ -24,5 +24,7 @@ public enum DeviceType MK3_8, RemoteControl, SBrickLight, + JieStarSCM4, + JieStarSCM8, } } diff --git a/BrickController2/BrickController2/DeviceManagement/JieStar/IJieStarDeviceManager.cs b/BrickController2/BrickController2/DeviceManagement/JieStar/IJieStarDeviceManager.cs new file mode 100644 index 000000000..0654d5fb9 --- /dev/null +++ b/BrickController2/BrickController2/DeviceManagement/JieStar/IJieStarDeviceManager.cs @@ -0,0 +1,11 @@ +using System; + +namespace BrickController2.DeviceManagement.JieStar; + +/// +/// Manager for JIESTAR devices +/// +public interface IJieStarDeviceManager +{ + ReadOnlyMemory GetAppId(); +} diff --git a/BrickController2/BrickController2/DeviceManagement/JieStar/IJieStarPlatformService.cs b/BrickController2/BrickController2/DeviceManagement/JieStar/IJieStarPlatformService.cs new file mode 100644 index 000000000..e6d6ba6ac --- /dev/null +++ b/BrickController2/BrickController2/DeviceManagement/JieStar/IJieStarPlatformService.cs @@ -0,0 +1,9 @@ +namespace BrickController2.DeviceManagement.JieStar; + +/// +/// Interface definition for JieStar specific PlatformService +/// +public interface IJieStarPlatformService +{ + bool TryGetRfPayload(byte ctxValue2, byte[] rawData, out byte[] rfPayload); +} diff --git a/BrickController2/BrickController2/DeviceManagement/JieStar/JieStar.cs b/BrickController2/BrickController2/DeviceManagement/JieStar/JieStar.cs new file mode 100644 index 000000000..fc35b3713 --- /dev/null +++ b/BrickController2/BrickController2/DeviceManagement/JieStar/JieStar.cs @@ -0,0 +1,32 @@ +using Autofac; +using BrickController2.DeviceManagement.DI; +using BrickController2.DeviceManagement.Vendors; + +namespace BrickController2.DeviceManagement.JieStar; + +/// +/// Vendor: JIESTAR and all its devices +/// +internal class JieStar : Vendor +{ + public override string VendorName => "JIESTAR"; + + protected override void Register(VendorBuilder builder) + { + // device manager + builder.ContainerBuilder.RegisterType() + .As() + .SingleInstance(); + + // manually added devices + builder.RegisterDevice() + .WithDeviceFactory(JieStarSCM4.Device1, $"{JieStarSCM4.TypeName} Device 1") + .WithDeviceFactory(JieStarSCM4.Device2, $"{JieStarSCM4.TypeName} Device 2") + .WithDeviceFactory(JieStarSCM4.Device3, $"{JieStarSCM4.TypeName} Device 3"); + + builder.RegisterDevice() + .WithDeviceFactory(JieStarSCM8.Device1, $"{JieStarSCM8.TypeName} Device 1") + .WithDeviceFactory(JieStarSCM8.Device2, $"{JieStarSCM8.TypeName} Device 2") + .WithDeviceFactory(JieStarSCM8.Device3, $"{JieStarSCM8.TypeName} Device 3"); + } +} diff --git a/BrickController2/BrickController2/DeviceManagement/JieStar/JieStarBase.cs b/BrickController2/BrickController2/DeviceManagement/JieStar/JieStarBase.cs new file mode 100644 index 000000000..8431c5b62 --- /dev/null +++ b/BrickController2/BrickController2/DeviceManagement/JieStar/JieStarBase.cs @@ -0,0 +1,278 @@ +using System; +using BrickController2.PlatformServices.BluetoothLE; + +namespace BrickController2.DeviceManagement.JieStar; + +/// +/// JIESTAR baseclass for devices with a nibble per channel +/// +internal abstract class JieStarBase : BluetoothAdvertisingDevice +{ + /// + /// offset to position of first channel in base telegram + /// + private const int CHANNEL_START_OFFSET = 3; + + /// + /// platform specific JIESTAR stuff + /// + protected readonly IJieStarPlatformService _jieStarPlatformService; + + /// + /// 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; + + /// + /// The second context value for JieStar communication. + /// + private readonly byte _ctxValue2; + + /// + /// array to hold the incoming output values for all channels. + /// + protected readonly float[] _storedValues; + + protected JieStarBase(string name, string address, byte[] deviceData, IDeviceRepository deviceRepository, IBluetoothLEService bleService, IJieStarPlatformService jieStarPlatformService, IJieStarDeviceManager jieStarDeviceManager, byte[] telegram_Connect, byte[] telegram_Base, byte ctxValue2) + : base(name, address, deviceData, deviceRepository, bleService) + { + _telegram_Connect = telegram_Connect; + _telegram_Base = telegram_Base; + _ctxValue2 = ctxValue2; + _jieStarPlatformService = jieStarPlatformService; + _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 = jieStarDeviceManager.GetAppId().Span[..2]; + _telegram_Connect[1] = appId[0]; + _telegram_Connect[2] = appId[1]; + + _telegram_Base[1] = appId[0]; + _telegram_Base[2] = appId[1]; + } + + /// + /// 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() + { + const float zeroValue = 0.0f; + + for (int channelNo = 0; channelNo < NumberOfChannels; channelNo++) + { + _storedValues[channelNo] = zeroValue; // restore stored values to zero + SetChannelOutput(channelNo, zeroValue); // set all channels to zero using the channel specific function + } + } + + /// + /// 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() + { + const float zeroValue = 0.0f; + + for (int channelNo = 0; channelNo < NumberOfChannels; channelNo++) + { + // call _bluetoothAdvertisingDeviceHandler.SetChannelState() to set global channel state to zero + SetChannelOutput(channelNo, zeroValue); + } + } + + /// + /// 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 bool TryGetTelegram(bool getConnectTelegram, out byte[] payload) + { + if (getConnectTelegram) + { + return _jieStarPlatformService.TryGetRfPayload(_ctxValue2, _telegram_Connect, out payload); + } + else + { + return _jieStarPlatformService.TryGetRfPayload(_ctxValue2, _telegram_Base, out payload); + } + } + + /// + /// Calculates the target position of a channel within the current instance. + /// + /// The calculation takes into account the instance number and assumes that each instance + /// contains a fixed number of bytes for channels. Channels are packed two per byte, with the lower nibble + /// representing one channel and the upper nibble representing the other. + /// 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. + /// byteOffset: The byte offset within the data structure where the + /// channel is located. isLowerNibble: if the + /// channel is in the lower nibble of the byte; otherwise, . + protected virtual (int byteOffset, bool isLowerNibble) GetTargetPosition(int channelNo) + { + return ( + CHANNEL_START_OFFSET + (channelNo >> 1), // div 2 -> 2 channels per byte + (channelNo & 0x01) == 0x01 + ); + } +} diff --git a/BrickController2/BrickController2/DeviceManagement/JieStar/JieStarDeviceManager.cs b/BrickController2/BrickController2/DeviceManagement/JieStar/JieStarDeviceManager.cs new file mode 100644 index 000000000..105dd6f35 --- /dev/null +++ b/BrickController2/BrickController2/DeviceManagement/JieStar/JieStarDeviceManager.cs @@ -0,0 +1,70 @@ +using System; +using BrickController2.UI.Services.Preferences; + +namespace BrickController2.DeviceManagement.JieStar; + +/// +/// Manager for JIESTAR devices +/// +public class JieStarDeviceManager : IJieStarDeviceManager +{ + private const string SECTION = "JIESTAR"; + private const string APPIDKEY = "AppID"; + + + // this identifier is patched into the advertising data to identify the app + private readonly byte[] _appIdChecksumMaskArray; + + public JieStarDeviceManager(IPreferencesService preferencesService) + { + _appIdChecksumMaskArray = JieStarDeviceManager.GetAppIdentifier(preferencesService); + } + + public ReadOnlyMemory GetAppId() => _appIdChecksumMaskArray; + + /// + /// Gets or creates an app-persistent AppIdentifier. + /// + /// Reference to preferencesService singleton. + /// byte array containing the AppIdentifier + private static byte[] GetAppIdentifier(IPreferencesService preferencesService) + { + byte[] appIdChecksumMaskArray; + // gets or creates an app-persistent AppIdentifier + try + { + if (preferencesService.ContainsKey(APPIDKEY, SECTION)) + { + // throws exception if converting went wrong + appIdChecksumMaskArray = Convert.FromBase64String(preferencesService.Get(APPIDKEY, string.Empty, SECTION)); + + // check minimum length + if (appIdChecksumMaskArray?.Length >= 2) + { + return appIdChecksumMaskArray; // valid + } + } + } + catch // catch all exceptions + { + } + + // create new byte[] with random values + // * on first run + // * on exception + // * on length too short + appIdChecksumMaskArray = new byte[2]; + + Random.Shared.NextBytes(appIdChecksumMaskArray); + + try + { + preferencesService.Set(APPIDKEY, Convert.ToBase64String(appIdChecksumMaskArray), SECTION); + } + catch // catch all exceptions to keep app alive + { + } + + return appIdChecksumMaskArray; + } +} \ No newline at end of file diff --git a/BrickController2/BrickController2/DeviceManagement/JieStar/JieStarSCM4.cs b/BrickController2/BrickController2/DeviceManagement/JieStar/JieStarSCM4.cs new file mode 100644 index 000000000..149ee3ec5 --- /dev/null +++ b/BrickController2/BrickController2/DeviceManagement/JieStar/JieStarSCM4.cs @@ -0,0 +1,94 @@ +using BrickController2.PlatformServices.BluetoothLE; +using BrickController2.Protocols; +using System; + +namespace BrickController2.DeviceManagement.JieStar; + +/// +/// JIESTAR 4 Channel Smart Creative Module (SCM) +/// +internal class JieStarSCM4 : JieStarBase, IDeviceType +{ + public const string Device1 = "Device1"; + public const string Device2 = "Device2"; + public const string Device3 = "Device3"; + + /// + /// Telegram to connect to the SCM4 devices + /// This telegram is sent on init and on reconnect conditions matching + /// + private static readonly byte[] Telegram_Connect_Device = [0xa4, 0x1d, 0x74, 0x80, 0x80, 0x80, 0x80, 0x5b]; + + /// + /// Base Telegram for SCM4 devices + /// + private static readonly byte[] Telegram_Base_Device = [0x40, 0x1d, 0x74, 0x80, 0x80, 0x80, 0x80, 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 JieStarSCM4(string name, string address, byte[] deviceData, IDeviceRepository deviceRepository, IBluetoothLEService bleService, IJieStarPlatformService jieStarPlatformService, IJieStarDeviceManager jieStarDeviceManager) + : base(name, address, deviceData, deviceRepository, bleService, jieStarPlatformService, jieStarDeviceManager, Telegram_Connect_Device, Telegram_Base_Device, GetCTXValue2(address)) + { + } + + public static DeviceType Type => DeviceType.JieStarSCM4; + + public static string TypeName => "JIESTAR SCM 4"; + + public override DeviceType DeviceType => Type; + + /// + /// Gets the number of channels supported by the device. + /// + /// Channel 0..3: real existing channel + /// + /// + public override int NumberOfChannels => 4; + + /// + /// manufacturerId to advertise + /// + protected override ushort ManufacturerId => JieStarProtocol.ManufacturerID; + + /// + /// Get or create BluetoothAdvertisingDeviceHandler + /// + /// Instance of BluetoothAdvertisingDeviceHandler + protected override BluetoothAdvertisingDeviceHandler GetBluetoothAdvertisingDeviceHandler() + { + // JIESTAR SCM 4 needs a BluetoothAdvertiser per module + return new BluetoothAdvertisingDeviceHandler(_bleService, ManufacturerId, TryGetTelegram, JieStarSCM4.ReconnectTimeSpan); + } + + /// + /// Processes the value for the specified analog channel and returns the processed result. + /// + /// The channel number to process. Valid values are 0, 1, 2, 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 one of the valid channel numbers (0, 1, 2, 3). + protected override (byte value, bool flag) ProcessChannelValue(int channelNo, float value) => channelNo switch + { + >= 0 and <= 3 => SetOutput_AnalogChannel(value), + _ => throw new ArgumentException($"Illegal Argument \"{channelNo}\"", nameof(channelNo)) + }; + + /// + /// Get reference to Base-Telegram for the given address + /// + /// address + /// reference to Base-Telegram + private static byte GetCTXValue2(string address) + { + return address switch + { + JieStarSCM4.Device1 => JieStarProtocol.CTXValue2, + JieStarSCM4.Device2 => JieStarProtocol.CTXValue2 + 1, + JieStarSCM4.Device3 => JieStarProtocol.CTXValue2 + 2, + _ => throw new ArgumentException("Illegal Argument", nameof(address)) + }; + } +} diff --git a/BrickController2/BrickController2/DeviceManagement/JieStar/JieStarSCM8.cs b/BrickController2/BrickController2/DeviceManagement/JieStar/JieStarSCM8.cs new file mode 100644 index 000000000..8a71cbca6 --- /dev/null +++ b/BrickController2/BrickController2/DeviceManagement/JieStar/JieStarSCM8.cs @@ -0,0 +1,105 @@ +using BrickController2.PlatformServices.BluetoothLE; +using BrickController2.Protocols; +using System; + +namespace BrickController2.DeviceManagement.JieStar; + +/// +/// JIESTAR 8 Channel Smart Creative Module (SCM) +/// +internal class JieStarSCM8 : JieStarBase, IDeviceType +{ + public const string Device1 = "Device1"; + public const string Device2 = "Device2"; + public const string Device3 = "Device3"; + + /// + /// Telegram to connect to the SCM8 device(s) + /// This telegram is sent on init and on reconnect conditions matching + /// + private static readonly byte[] Telegram_Connect = [0xA4, 0x34, 0x17, 0x00, 0x00, 0x00, 0x00, 0x5B]; + + /// + /// Base Telegram for SCM8 device 1 + /// + private static readonly byte[] Telegram_Base_Device_1 = [0x41, 0x34, 0x17, 0x00, 0x00, 0x00, 0x00, 0xbf]; + + /// + /// Base Telegram for SCM8 device 2 + /// + private static readonly byte[] Telegram_Base_Device_2 = [0x42, 0x34, 0x17, 0x00, 0x00, 0x00, 0x00, 0xbe]; + + /// + /// Base Telegram for SCM8 device 3 + /// + private static readonly byte[] Telegram_Base_Device_3 = [0x43, 0x34, 0x17, 0x00, 0x00, 0x00, 0x00, 0xbd]; + + /// + /// 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 JieStarSCM8(string name, string address, byte[] deviceData, IDeviceRepository deviceRepository, IBluetoothLEService bleService, IJieStarPlatformService jieStarPlatformService, IJieStarDeviceManager jieStarDeviceManager) + : base(name, address, deviceData, deviceRepository, bleService, jieStarPlatformService, jieStarDeviceManager, JieStarSCM8.Telegram_Connect, GetTelegramBase(address), JieStarProtocol.CTXValue2) + { + } + + public static DeviceType Type => DeviceType.JieStarSCM8; + + public static string TypeName => "JIESTAR SCM 8"; + + public override DeviceType DeviceType => Type; + + /// + /// Gets the number of channels supported by the device. + /// + /// Channel 0..7: real existing channel + /// + /// + public override int NumberOfChannels => 8; + + /// + /// manufacturerId to advertise + /// + protected override ushort ManufacturerId => JieStarProtocol.ManufacturerID; + + /// + /// Get or create BluetoothAdvertisingDeviceHandler + /// + /// Instance of BluetoothAdvertisingDeviceHandler + protected override BluetoothAdvertisingDeviceHandler GetBluetoothAdvertisingDeviceHandler() + { + // JIESTAR SCM 8 needs a BluetoothAdvertiser per module + return new BluetoothAdvertisingDeviceHandler(_bleService, ManufacturerId, TryGetTelegram, JieStarSCM8.ReconnectTimeSpan); + } + + + /// + /// Processes the value for the specified analog channel and returns the processed result. + /// + /// The channel number to process. Valid values are 0, 1, 2, 3, 4, 5, 6, or 7. + /// 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 one of the valid channel numbers (0, 1, 2, 3, 4, 5, 6, or 7). + protected override (byte value, bool flag) ProcessChannelValue(int channelNo, float value) => channelNo switch + { + >= 0 and <= 7 => SetOutput_AnalogChannel(value), + _ => throw new ArgumentException($"Illegal Argument \"{channelNo}\"", nameof(channelNo)) + }; + + /// + /// Get reference to Base-Telegram for the given address + /// + /// address + /// reference to Base-Telegram + private static byte[] GetTelegramBase(string address) + { + return address switch + { + JieStarSCM8.Device1 => Telegram_Base_Device_1, + JieStarSCM8.Device2 => Telegram_Base_Device_2, + JieStarSCM8.Device3 => Telegram_Base_Device_3, + _ => throw new ArgumentException("Illegal Argument", nameof(address)) + }; + } +} diff --git a/BrickController2/BrickController2/DeviceManagement/MouldKing/MouldKing.cs b/BrickController2/BrickController2/DeviceManagement/MouldKing/MouldKing.cs index 0e377dcbf..a3219ab8c 100644 --- a/BrickController2/BrickController2/DeviceManagement/MouldKing/MouldKing.cs +++ b/BrickController2/BrickController2/DeviceManagement/MouldKing/MouldKing.cs @@ -5,7 +5,7 @@ namespace BrickController2.DeviceManagement.MouldKing; /// -/// Vendor: Mould King and all it's device and implementation of IBluetoothLEDeviceManager +/// Vendor: Mould King and all its devices and implementation of IBluetoothLEDeviceManager /// internal class MouldKing : Vendor { diff --git a/BrickController2/BrickController2/Protocols/JieStarProtocol.cs b/BrickController2/BrickController2/Protocols/JieStarProtocol.cs new file mode 100644 index 000000000..41c96011f --- /dev/null +++ b/BrickController2/BrickController2/Protocols/JieStarProtocol.cs @@ -0,0 +1,44 @@ +namespace BrickController2.Protocols; + +/// +/// static class which implements the encryption algorithm for the advertising data +/// +public static class JieStarProtocol +{ + /// + /// ManufacturerID for JIESTAR + /// + public const ushort ManufacturerID = 0xFFF0; + + /// + /// 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 815f171dd..5d0a90d05 100644 --- a/BrickController2/BrickController2/UI/Controls/DeviceChannelLabel.cs +++ b/BrickController2/BrickController2/UI/Controls/DeviceChannelLabel.cs @@ -16,6 +16,7 @@ public class DeviceChannelLabel : Label 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 string[] _sBrickLightChannelLetters = ["A", "B", "C", "D", "E", "F", "G", "H"]; + private readonly static string[] _jieStarChannelLetters = ["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); @@ -107,7 +108,10 @@ private void SetChannelText() Text = $"{_sBrickLightChannelLetters[Channel % 8]}.{Channel / 8}"; } break; - + case DeviceType.JieStarSCM4: + case DeviceType.JieStarSCM8: + SetChannelText(_jieStarChannelLetters); + break; default: Text = $"{Channel + 1}"; break; diff --git a/BrickController2/BrickController2/UI/Controls/DeviceChannelSelector.xaml b/BrickController2/BrickController2/UI/Controls/DeviceChannelSelector.xaml index e54ccfbc9..530242b2b 100644 --- a/BrickController2/BrickController2/UI/Controls/DeviceChannelSelector.xaml +++ b/BrickController2/BrickController2/UI/Controls/DeviceChannelSelector.xaml @@ -458,6 +458,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 929775b90..04a3fa1dd 100644 --- a/BrickController2/BrickController2/UI/Controls/DeviceChannelSelector.xaml.cs +++ b/BrickController2/BrickController2/UI/Controls/DeviceChannelSelector.xaml.cs @@ -67,29 +67,48 @@ public DeviceChannelSelector() PfxBrickChannel6.Command = new SafeCommand(() => SelectedChannel = 7); PfxBrickChannel7.Command = new SafeCommand(() => SelectedChannel = 8); PfxBrickChannel8.Command = new SafeCommand(() => SelectedChannel = 9); + // MK3_8 MK3_8Channel0.Command = new SafeCommand(() => SelectedChannel = 0); MK3_8Channel1.Command = new SafeCommand(() => SelectedChannel = 1); MK3_8Channel2.Command = new SafeCommand(() => SelectedChannel = 2); MK3_8Channel3.Command = new SafeCommand(() => SelectedChannel = 3); MK3_8Channel4.Command = new SafeCommand(() => SelectedChannel = 4); + // MK4 MK4Channel0.Command = new SafeCommand(() => SelectedChannel = 0); MK4Channel1.Command = new SafeCommand(() => SelectedChannel = 1); MK4Channel2.Command = new SafeCommand(() => SelectedChannel = 2); MK4Channel3.Command = new SafeCommand(() => SelectedChannel = 3); + // MK5 MK5Channel0.Command = new SafeCommand(() => SelectedChannel = 0); MK5Channel1.Command = new SafeCommand(() => SelectedChannel = 1); MK5Channel2.Command = new SafeCommand(() => SelectedChannel = 2); MK5Channel3.Command = new SafeCommand(() => SelectedChannel = 3); MK5Channel4.Command = new SafeCommand(() => SelectedChannel = 4); + // MK6 MK6Channel0.Command = new SafeCommand(() => SelectedChannel = 0); MK6Channel1.Command = new SafeCommand(() => SelectedChannel = 1); MK6Channel2.Command = new SafeCommand(() => SelectedChannel = 2); MK6Channel3.Command = new SafeCommand(() => SelectedChannel = 3); MK6Channel4.Command = new SafeCommand(() => SelectedChannel = 4); MK6Channel5.Command = new SafeCommand(() => SelectedChannel = 5); + // CaDARaceCar CaDARaceCarChannel0.Command = new SafeCommand(() => SelectedChannel = 0); CaDARaceCarChannel1.Command = new SafeCommand(() => SelectedChannel = 1); CaDARaceCarChannel2.Command = new SafeCommand(() => SelectedChannel = 2); + // JIESTAR SCM 4 + JieStarSCM4Channel0.Command = new SafeCommand(() => SelectedChannel = 0); + JieStarSCM4Channel1.Command = new SafeCommand(() => SelectedChannel = 1); + JieStarSCM4Channel2.Command = new SafeCommand(() => SelectedChannel = 2); + JieStarSCM4Channel3.Command = new SafeCommand(() => SelectedChannel = 3); + // JIESTAR SCM 8 + JieStarSCM8Channel0.Command = new SafeCommand(() => SelectedChannel = 0); + JieStarSCM8Channel1.Command = new SafeCommand(() => SelectedChannel = 1); + JieStarSCM8Channel2.Command = new SafeCommand(() => SelectedChannel = 2); + JieStarSCM8Channel3.Command = new SafeCommand(() => SelectedChannel = 3); + JieStarSCM8Channel4.Command = new SafeCommand(() => SelectedChannel = 4); + JieStarSCM8Channel5.Command = new SafeCommand(() => SelectedChannel = 5); + JieStarSCM8Channel6.Command = new SafeCommand(() => SelectedChannel = 6); + JieStarSCM8Channel7.Command = new SafeCommand(() => SelectedChannel = 7); // SBrick Light - special handling SBrickLightChannelA.Command = new SafeCommand(() => UpdateSBrickPort(0)); SBrickLightChannelB.Command = new SafeCommand(() => UpdateSBrickPort(1)); @@ -174,6 +193,8 @@ private void OnDeviceChanged(Device device) MK6Section.IsVisible = deviceType == DeviceType.MK6; MK_DIYSection.IsVisible = deviceType == DeviceType.MK_DIY; CaDARaceCarSection.IsVisible = deviceType == DeviceType.CaDA_RaceCar; + JieStarSCM4Section.IsVisible = deviceType == DeviceType.JieStarSCM4; + JieStarSCM8Section.IsVisible = deviceType == DeviceType.JieStarSCM8; } private static void OnSelectedChannelChanged(BindableObject bindable, object oldValue, object newValue) @@ -239,30 +260,50 @@ private void OnSelectedChannelChanged(int selectedChannel) PfxBrickChannel6.SelectedChannel = selectedChannel; PfxBrickChannel7.SelectedChannel = selectedChannel; PfxBrickChannel8.SelectedChannel = selectedChannel; + // MK3_8 MK3_8Channel0.SelectedChannel = selectedChannel; MK3_8Channel1.SelectedChannel = selectedChannel; MK3_8Channel2.SelectedChannel = selectedChannel; MK3_8Channel3.SelectedChannel = selectedChannel; MK3_8Channel4.SelectedChannel = selectedChannel; + // MK4 MK4Channel0.SelectedChannel = selectedChannel; MK4Channel1.SelectedChannel = selectedChannel; MK4Channel2.SelectedChannel = selectedChannel; MK4Channel3.SelectedChannel = selectedChannel; + // MK5 MK5Channel0.SelectedChannel = selectedChannel; MK5Channel1.SelectedChannel = selectedChannel; MK5Channel2.SelectedChannel = selectedChannel; MK5Channel3.SelectedChannel = selectedChannel; MK5Channel4.SelectedChannel = selectedChannel; + // MK6 MK6Channel0.SelectedChannel = selectedChannel; MK6Channel1.SelectedChannel = selectedChannel; MK6Channel2.SelectedChannel = selectedChannel; MK6Channel3.SelectedChannel = selectedChannel; MK6Channel4.SelectedChannel = selectedChannel; MK6Channel5.SelectedChannel = selectedChannel; + // MK_DIY MK_DIYChannel0.SelectedChannel = selectedChannel; MK_DIYChannel1.SelectedChannel = selectedChannel; MK_DIYChannel2.SelectedChannel = selectedChannel; MK_DIYChannel3.SelectedChannel = selectedChannel; + // JIESTAR SCM 4 + JieStarSCM4Channel0.SelectedChannel = selectedChannel; + JieStarSCM4Channel1.SelectedChannel = selectedChannel; + JieStarSCM4Channel2.SelectedChannel = selectedChannel; + JieStarSCM4Channel3.SelectedChannel = selectedChannel; + // JIESTAR SCM 8 + JieStarSCM8Channel0.SelectedChannel = selectedChannel; + JieStarSCM8Channel1.SelectedChannel = selectedChannel; + JieStarSCM8Channel2.SelectedChannel = selectedChannel; + JieStarSCM8Channel3.SelectedChannel = selectedChannel; + JieStarSCM8Channel4.SelectedChannel = selectedChannel; + JieStarSCM8Channel5.SelectedChannel = selectedChannel; + JieStarSCM8Channel6.SelectedChannel = selectedChannel; + JieStarSCM8Channel7.SelectedChannel = selectedChannel; + // CaDARaceCar CaDARaceCarChannel0.SelectedChannel = selectedChannel; CaDARaceCarChannel1.SelectedChannel = selectedChannel; CaDARaceCarChannel2.SelectedChannel = selectedChannel; diff --git a/BrickController2/BrickController2/UI/Images/jiestarscm4_image.png b/BrickController2/BrickController2/UI/Images/jiestarscm4_image.png new file mode 100644 index 000000000..e8209ee2c Binary files /dev/null and b/BrickController2/BrickController2/UI/Images/jiestarscm4_image.png differ diff --git a/BrickController2/BrickController2/UI/Images/jiestarscm4_image_small.png b/BrickController2/BrickController2/UI/Images/jiestarscm4_image_small.png new file mode 100644 index 000000000..4727268e0 Binary files /dev/null and b/BrickController2/BrickController2/UI/Images/jiestarscm4_image_small.png differ diff --git a/BrickController2/BrickController2/UI/Images/jiestarscm8_image.png b/BrickController2/BrickController2/UI/Images/jiestarscm8_image.png new file mode 100644 index 000000000..449f6c4d4 Binary files /dev/null and b/BrickController2/BrickController2/UI/Images/jiestarscm8_image.png differ diff --git a/BrickController2/BrickController2/UI/Images/jiestarscm8_image_small.png b/BrickController2/BrickController2/UI/Images/jiestarscm8_image_small.png new file mode 100644 index 000000000..d9add5f9e Binary files /dev/null and b/BrickController2/BrickController2/UI/Images/jiestarscm8_image_small.png differ diff --git a/README.md b/README.md index 13904be7f..4554ccfc6 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,8 @@ Cross platform mobile application for controlling your creations using a bluetoo - Mould King 6.0 Powered Module - CaDA Race Car - PFx Brick (lights & Power Functions ports only) +- JieStar 4 Channel Smart Creative Module +- JieStar 8 Channel Smart Creative Module ## Supported controllers - Generic Bluetooth / USB gamepads