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
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using System.Collections.ObjectModel;
using BrickController2.CreationManagement;
using BrickController2.DeviceManagement.Macros;
using FluentAssertions;
using Xunit;

namespace BrickController2.Tests.CreationManagement;

public class CreationMacroReferencesTests
{
[Fact]
public void GetMacroReferences_ReturnsEmpty_WhenNoMacroActions()
{
var creation = BuildCreation(new ControllerAction
{
DeviceId = "dev1",
ButtonType = ControllerButtonType.Sequence,
SequenceName = "seq"
});

creation.GetMacroReferences().Should().BeEmpty();
}

[Fact]
public void GetMacroReferences_ReturnsChannelScope_ForMacroActions()
{
var creation = BuildCreation(new ControllerAction
{
DeviceId = "dev1",
ButtonType = ControllerButtonType.Macro,
MacroId = "SetOutputLevel"
});

creation.GetMacroReferences().Should().ContainSingle()
.Which.Should().Be(("dev1", "SetOutputLevel", MacroScope.Channel));
}

[Fact]
public void GetMacroReferences_ReturnsDeviceScope_ForDeviceMacroActions()
{
var creation = BuildCreation(new ControllerAction
{
DeviceId = "dev1",
ButtonType = ControllerButtonType.DeviceMacro,
MacroId = "Reset"
});

creation.GetMacroReferences().Should().ContainSingle()
.Which.Should().Be(("dev1", "Reset", MacroScope.Device));
}

[Fact]
public void GetMacroReferences_DeduplicatesByDeviceMacroScope()
{
var creation = BuildCreation(
new ControllerAction { DeviceId = "dev1", ButtonType = ControllerButtonType.Macro, MacroId = "m" },
new ControllerAction { DeviceId = "dev1", ButtonType = ControllerButtonType.Macro, MacroId = "m" },
new ControllerAction { DeviceId = "dev1", ButtonType = ControllerButtonType.DeviceMacro, MacroId = "m" });

var references = creation.GetMacroReferences();

references.Should().HaveCount(2);
references.Should().Contain(("dev1", "m", MacroScope.Channel));
references.Should().Contain(("dev1", "m", MacroScope.Device));
}

[Fact]
public void GetMacroReferences_IgnoresMacroActions_WithEmptyMacroId()
{
var creation = BuildCreation(new ControllerAction
{
DeviceId = "dev1",
ButtonType = ControllerButtonType.Macro,
MacroId = string.Empty
});

creation.GetMacroReferences().Should().BeEmpty();
}

private static Creation BuildCreation(params ControllerAction[] actions)
{
var controllerEvent = new ControllerEvent
{
ControllerActions = new ObservableCollection<ControllerAction>(actions)
};
var profile = new ControllerProfile
{
ControllerEvents = new ObservableCollection<ControllerEvent> { controllerEvent }
};
return new Creation
{
ControllerProfiles = new ObservableCollection<ControllerProfile> { profile }
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
using BrickController2.DeviceManagement;
using BrickController2.DeviceManagement.BuWizz;
using BrickController2.DeviceManagement.Macros;
using BrickController2.PlatformServices.BluetoothLE;
using BrickController2.Settings;
using FluentAssertions;
using Moq;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Xunit;

namespace BrickController2.Tests.DeviceManagement.BuWizz;

public class BuWizzDeviceMacroTests
{
[Fact]
public void BuWizzDevice_AvailableMacros_ReturnsSetOutputLevelWithThreeChoices()
{
var device = new TestBuWizzDevice();

device.SupportsMacros.Should().BeTrue();
device.AvailableMacros.Should().ContainSingle();

var macro = device.AvailableMacros.Single();
macro.Id.Should().Be("SetOutputLevel");
macro.Scope.Should().Be(MacroScope.Device);
macro.Kind.Should().Be(MacroKind.OneShot);
macro.Choices.Select(c => c.Value).Should().Equal(
(int)BuWizzOutputLevels.Low,
(int)BuWizzOutputLevels.Normal,
(int)BuWizzOutputLevels.High);
}

[Fact]
public async Task BuWizzDevice_ExecuteMacroAsync_SetOutputLevelMacro_UsesSelectedChoiceValue()
{
var device = new TestBuWizzDevice();

await device.ExecuteMacroAsync(new MacroInvocation("SetOutputLevel", (int)BuWizzOutputLevels.High, null), CancellationToken.None);

device.LastSetOutputLevel.Should().Be((int)BuWizzOutputLevels.High);
}

[Fact]
public void BuWizz2Device_AvailableMacros_ReturnsSetOutputLevelWithFourChoices()
{
var device = new TestBuWizz2Device();

device.SupportsMacros.Should().BeTrue();
device.AvailableMacros.Should().ContainSingle();

var macro = device.AvailableMacros.Single();
macro.Id.Should().Be("SetOutputLevel");
macro.Scope.Should().Be(MacroScope.Device);
macro.Kind.Should().Be(MacroKind.OneShot);
macro.Choices.Select(c => c.Value).Should().Equal(
(int)BuWizz2OutputLevels.Low,
(int)BuWizz2OutputLevels.Normal,
(int)BuWizz2OutputLevels.High,
(int)BuWizz2OutputLevels.Ludicrous);
}

[Fact]
public async Task BuWizz2Device_ExecuteMacroAsync_SetOutputLevelMacro_UsesSelectedChoiceValue()
{
var device = new TestBuWizz2Device();

await device.ExecuteMacroAsync(new MacroInvocation("SetOutputLevel", (int)BuWizz2OutputLevels.Ludicrous, null), CancellationToken.None);

device.LastSetOutputLevel.Should().Be((int)BuWizz2OutputLevels.Ludicrous);
}

private sealed class TestBuWizzDevice : BuWizzDevice
{
public TestBuWizzDevice()
: base("test", "addr", new List<NamedSetting>(),
new Mock<IDeviceRepository>().Object,
new Mock<IBluetoothLEService>().Object)
{
}

public int? LastSetOutputLevel { get; private set; }

public override void SetOutputLevel(int value)
{
LastSetOutputLevel = value;
base.SetOutputLevel(value);
}
}

private sealed class TestBuWizz2Device : BuWizz2Device
{
public TestBuWizz2Device()
: base("test", "addr", [0x4e, 0x05, 0x42, 0x57, 0x00, 0x1b], new List<NamedSetting>(),
new Mock<IDeviceRepository>().Object,
new Mock<IBluetoothLEService>().Object)
{
}

public int? LastSetOutputLevel { get; private set; }

public override void SetOutputLevel(int value)
{
LastSetOutputLevel = value;
base.SetOutputLevel(value);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ public enum CreationValidationResult
Ok,
MissingControllerAction,
MissingDevice,
MissingSequence
MissingSequence,
MissingMacro,
}
}
69 changes: 67 additions & 2 deletions BrickController2/BrickController2/BusinessLogic/PlayLogic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Linq;
using BrickController2.CreationManagement;
using BrickController2.DeviceManagement;
using BrickController2.DeviceManagement.Macros;
using BrickController2.PlatformServices.InputDevice;

using static BrickController2.PlatformServices.InputDevice.InputDevices;
Expand Down Expand Up @@ -36,6 +37,7 @@ public CreationValidationResult ValidateCreation(Creation creation)
{
var deviceIds = creation.GetDeviceIds();
var sequenceNames = creation.GetSequenceNames();
var macroReferences = creation.GetMacroReferences();

if (deviceIds.Count == 0)
{
Expand All @@ -49,16 +51,42 @@ public CreationValidationResult ValidateCreation(Creation creation)
{
return CreationValidationResult.MissingSequence;
}
else if (macroReferences.Any(mr =>
{
var device = _deviceManager.GetDeviceById(mr.DeviceId);
return device == null || !device.AvailableMacros.Any(m => m.Id == mr.MacroId && m.Scope == mr.Scope);
}))
{
return CreationValidationResult.MissingMacro;
}

return CreationValidationResult.Ok;
}

public bool ValidateControllerAction(ControllerAction controllerAction)
{
var device = _deviceManager.GetDeviceById(controllerAction.DeviceId);
var sequence = _creationManager.Sequences.FirstOrDefault(s => s.Name == controllerAction.SequenceName);
if (device == null)
{
return false;
}

return device != null && (controllerAction.ButtonType != ControllerButtonType.Sequence || sequence != null);
if (controllerAction.ButtonType == ControllerButtonType.Sequence)
{
return _creationManager.Sequences.FirstOrDefault(s => s.Name == controllerAction.SequenceName) != null;
}

if (controllerAction.ButtonType == ControllerButtonType.Macro)
{
return device.AvailableMacros.Any(m => m.Id == controllerAction.MacroId && m.Scope == MacroScope.Channel);
}

if (controllerAction.ButtonType == ControllerButtonType.DeviceMacro)
{
return device.AvailableMacros.Any(m => m.Id == controllerAction.MacroId && m.Scope == MacroScope.Device);
}

return true;
}

public void StartPlay()
Expand Down Expand Up @@ -128,6 +156,29 @@ private static bool ShouldProcessButtonEvent(bool isPressed, ControllerAction co
return controllerAction.ButtonType == ControllerButtonType.Normal || isPressed;
}

private static void InvokeMacro(ControllerAction controllerAction, Device device, MacroScope scope)
{
var macro = device.AvailableMacros.FirstOrDefault(m => m.Id == controllerAction.MacroId && m.Scope == scope);
if (macro == null)
{
return;
}

int? channel = scope == MacroScope.Channel ? controllerAction.Channel : null;
var invocation = new MacroInvocation(macro.Id, controllerAction.MacroChoiceValue, channel);
_ = System.Threading.Tasks.Task.Run(async () =>
{
try
{
await device.ExecuteMacroAsync(invocation, System.Threading.CancellationToken.None);
}
catch
{
// fire-and-forget: swallow macro execution errors
}
});
}

private float ProcessButtonEvent(bool isPressed, ControllerAction controllerAction, Device device)
{
var previousOutputs = GetPreviousOutputs(controllerAction);
Expand Down Expand Up @@ -192,6 +243,20 @@ private float ProcessButtonEvent(bool isPressed, ControllerAction controllerActi
_sequencePlayer.ToggleSequence(controllerAction.DeviceId, controllerAction.Channel, controllerAction.IsInvert, sequence);
}
break;

case ControllerButtonType.Macro:
if (isPressed)
{
InvokeMacro(controllerAction, device, MacroScope.Channel);
}
break;

case ControllerButtonType.DeviceMacro:
if (isPressed)
{
InvokeMacro(controllerAction, device, MacroScope.Device);
}
break;
}

SetPreviousOutput(controllerAction, currentOutput);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ public class ControllerAction : NotifyPropertyChangedSource
private int _servoBaseAngle;
private int _stepperAngle;
private string _sequenceName = string.Empty;
private string _macroId = string.Empty;
private int? _macroChoiceValue;

[PrimaryKey, AutoIncrement]
[JsonIgnore]
Expand Down Expand Up @@ -118,6 +120,18 @@ public string SequenceName
set { _sequenceName = value; RaisePropertyChanged(); }
}

public string MacroId
{
get { return _macroId; }
set { _macroId = value; RaisePropertyChanged(); }
}

public int? MacroChoiceValue
{
get { return _macroChoiceValue; }
set { _macroChoiceValue = value; RaisePropertyChanged(); }
}

public override string ToString()
{
return $"{DeviceId} - {Channel}";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ public enum ControllerButtonType
PingPong,
Stop,
Accelerator,
Sequence
Sequence,
Macro,
DeviceMacro,
}
}
Loading