Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
527 changes: 53 additions & 474 deletions BrickController2/BrickController2/DeviceManagement/ControlPlusDevice.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using BrickController2.CreationManagement;

namespace BrickController2.DeviceManagement.IO;

internal readonly record struct ChannelConfig
{
public ChannelOutputType OutputType { get; init; }
public int MaxServoAngle { get; init; }
public int ServoBaseAngle { get; init; }
public int StepperAngle { get; init; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;

namespace BrickController2.DeviceManagement.IO;

/// <summary>
/// Thread-safe, key-indexed store for per-key state structs.
/// Supports atomic read, write, and functional update.
/// </summary>
internal sealed class ChannelStateStore<TKey, TValue>
where TKey : notnull
where TValue : struct
{
private readonly ConcurrentDictionary<TKey, TValue> _states = new();
private readonly TValue _default;

public ChannelStateStore(TValue initialState = default)
{
_states = new();
_default = initialState;
}

public int Count => _states.Count;

/// <summary>Returns the current state for the given key or the default state if the key does not exist.</summary>
public TValue Get(TKey key) => _states.TryGetValue(key, out var value) ? value : _default;

/// <summary>Removes the current state for the given key.</summary>
public bool Remove(TKey key) => _states.TryRemove(key, out var _);

/// <summary>Upsert the state for the given key.</summary>
public void Set(TKey key, TValue state = default)
{
_states[key] = state;
}

/// <summary>
/// Atomically updates the state for the given key using the provided updater function.
/// Returns the new state.
/// </summary>
public TValue Update(TKey key, Func<TValue, TValue> updater) => _states.AddOrUpdate(key, (k) => updater(_default), (k, o) => updater(o));

/// <summary>Clears all persisted states.</summary>
public void Clear() => _states.Clear();

/// <summary>Returns the maximum value of a projection over all stored states, or default if empty.</summary>
public TResult? Max<TResult>(Func<TValue, TResult> selector)
{
TResult? max = default;
bool first = true;
foreach (var kvp in _states)
{
var val = selector(kvp.Value);
if (first || Comparer<TResult>.Default.Compare(val, max) > 0)
{
max = val;
first = false;
}
}
return max;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System;

namespace BrickController2.DeviceManagement.Lego;

/// <summary>
/// Immutable snapshot of a motor channel's position feedback.
/// </summary>
internal record struct ChannelAttachmentInfo(
ushort DeviceId,
bool IsUpdated,
DateTime UpdateTime)
{
public static readonly ChannelAttachmentInfo Initial = new(
DeviceId: 0,
IsUpdated: false,
UpdateTime: DateTime.MinValue);

/// <summary>Returns a new state with an updated device ID and timestamp.</summary>
public ChannelAttachmentInfo WithDevice(ushort deviceId) =>
this with { DeviceId = deviceId, IsUpdated = true, UpdateTime = DateTime.Now };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;

namespace BrickController2.DeviceManagement.Lego;

/// <summary>
/// Immutable snapshot of a motor channel's position feedback.
/// </summary>
internal record struct ChannelPositionState(
int Current,
bool IsUpdated,
DateTime UpdateTime)
{
public static readonly ChannelPositionState Initial = new(
Current: 0,
IsUpdated: false,
UpdateTime: DateTime.MinValue);

/// <summary>Returns a new state with an updated position and timestamp.</summary>
public ChannelPositionState WithPosition(int position) =>
this with { Current = position, IsUpdated = true, UpdateTime = DateTime.Now };

/// <summary>Clears the IsUpdated flag after the update has been consumed.</summary>
public ChannelPositionState ConsumeUpdate() =>
this with { IsUpdated = false };
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,11 @@ namespace BrickController2.DeviceManagement.Lego;
/// <summary>
/// Represents a LEGO® Powered Up 88010 Remote Control
/// </summary>
internal class RemoteControl : BluetoothDevice
internal class RemoteControl : WirelessProtocolBasedDevice
{
private const string ENABLED_SETTING_NAME = "RemoteControlEnabled";
private const bool DEFAULT_ENABLED = false;

private IGattCharacteristic? _characteristic;
private InputDeviceBase<RemoteControl>? _inputController;

public RemoteControl(string name, string address, IEnumerable<NamedSetting> settings, IDeviceRepository deviceRepository, IBluetoothLEService bleService)
Expand All @@ -33,12 +32,8 @@ public RemoteControl(string name, string address, IEnumerable<NamedSetting> sett

public override int NumberOfChannels => 0;

public override string BatteryVoltageSign => "%";

public bool IsEnabled => GetSettingValue(ENABLED_SETTING_NAME, DEFAULT_ENABLED);

protected override bool AutoConnectOnFirstConnect => false;

public override void SetOutput(int channel, float value) => throw new InvalidOperationException();

internal void ConnectInputController<TController>(TController inputController) where TController : InputDeviceBase<RemoteControl>
Expand All @@ -64,70 +59,34 @@ internal void ResetEvents() => RaiseButtonEvents(

protected override Task ProcessOutputsAsync(CancellationToken token) => Task.CompletedTask;

protected override async Task<bool> ValidateServicesAsync(IEnumerable<IGattService>? services, CancellationToken token)
{
var service = services?.FirstOrDefault(s => s.Uuid == ServiceUuid);
_characteristic = service?.Characteristics?.FirstOrDefault(c => c.Uuid == CharacteristicUuid);

if (_characteristic is not null)
{
return await _bleDevice!.EnableNotificationAsync(_characteristic, token);
}

return false;
}

protected override async Task<bool> AfterConnectSetupAsync(bool requestDeviceInformation, CancellationToken token)
{
// wait until ports finish communicating with the hub
await Task.Delay(250, token);
await AwaitForHubConnectedAsync(TimeSpan.FromMilliseconds(250), token);

if (requestDeviceInformation)
{
// Request battery voltage
await _bleDevice!.WriteAsync(_characteristic!, [0x05, 0x00, 0x01, 0x06, 0x05], token);
await Task.Delay(TimeSpan.FromMilliseconds(50), token);
await RequestHubPropertiesAsync(token);
}

// setup ports - 0x04 - REMOTE_MODE_KEYS
var remoteButtonA = BuildPortInputFormatSetup(REMOTE_BUTTONS_LEFT, REMOTE_MODE_KEYS, interval: 1);
await _bleDevice!.WriteAsync(_characteristic!, remoteButtonA, token);
await WriteAsync(remoteButtonA, token);

var remoteButtonB = BuildPortInputFormatSetup(REMOTE_BUTTONS_RIGHT, REMOTE_MODE_KEYS, interval: 1);
return await _bleDevice!.WriteAsync(_characteristic!, remoteButtonB, token);
}

protected override void BeforeDisconnectCleanup()
{
_characteristic = null;
return await WriteAsync(remoteButtonB, token);
}

protected override void OnCharacteristicChanged(Guid characteristicGuid, byte[] data)
protected override bool TryProcessMessageData(byte messageType, ReadOnlySpan<byte> data)
{
if (data.Length < 4)
switch (messageType)
{
return;
}

var messageCode = data[2];

switch (messageCode)
{
case MESSAGE_TYPE_HUB_PROPERTIES: // Hub properties
if (data.Length >= 6 &&
data[3] == HUB_PROPERTY_VOLTAGE &&
data[4] == HUB_PROPERTY_OPERATION_UPDATE)
{
BatteryVoltage = data[5].ToString("F0");
}
break;

case MESSAGE_TYPE_HW_NETWORK_COMMANDS: // HW network commands
if (data.Length == 5 && data[3] == 0x02)
{
// HW button state
RaiseButtonEvents([("Home", GetButtonValue(data[4]))]);
break;
return true;
}
break;
case MESSAGE_TYPE_PORT_VALUE: // 0x45 Port Value / RemoteButton
Expand All @@ -136,19 +95,17 @@ protected override void OnCharacteristicChanged(Guid characteristicGuid, byte[]
switch (data[3])
{
case REMOTE_BUTTONS_LEFT:
OnButtonEvents("A.Plus", "A", "A.Minus", data.AsSpan(4));
OnButtonEvents("A.Plus", "A", "A.Minus", data.Slice(4));
break;
case REMOTE_BUTTONS_RIGHT:
OnButtonEvents("B.Plus", "B", "B.Minus", data.AsSpan(4));
break;
default:
OnButtonEvents("B.Plus", "B", "B.Minus", data.Slice(4));
break;
}
return true;
}
break;
default:
break;
}
return base.TryProcessMessageData(messageType, data);
}

private void OnButtonEvents(string plus, string stop, string minus, ReadOnlySpan<byte> flags)
Expand Down
Loading
Loading