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
Expand Up @@ -3,6 +3,7 @@
using BrickController2.DeviceManagement;
using BrickController2.DeviceManagement.Lego;
using BrickController2.PlatformServices.BluetoothLE;
using BrickController2.PlatformServices.InputDevice;
using BrickController2.PlatformServices.InputDeviceService;

namespace BrickController2.Extensions;
Expand Down Expand Up @@ -34,13 +35,25 @@ internal static ContainerBuilder RegisterDevice<TDevice>(this ContainerBuilder b
/// Register input device service of <typeparamref name="TInputDeviceService"/> type.
/// </summary>
/// <returns>Registration instance to support fluent API</returns>
internal static ContainerBuilder RegisterInputDeviceService<TInputDeviceService>(this ContainerBuilder builder)
internal static IRegistrationBuilder<TInputDeviceService, ConcreteReflectionActivatorData, SingleRegistrationStyle> RegisterInputDeviceService<TInputDeviceService>(this ContainerBuilder builder)
where TInputDeviceService : IInputDeviceService, IStartable
{
builder.RegisterType<TInputDeviceService>()
return builder.RegisterType<TInputDeviceService>()
.As<IInputDeviceService>()
.As<IStartable>()
.SingleInstance(); // ensure it's started as soon as the container is built in Autofac
}

/// <summary>
/// Register input device service of <typeparamref name="TInputDeviceService"/> type.
/// </summary>
/// <returns>Registration instance to support fluent API</returns>
internal static ContainerBuilder RegisterInputDeviceService<TInputDeviceService, TInputDevice>(this ContainerBuilder builder)
where TInputDeviceService : IInputDeviceService<TInputDevice>, IStartable
where TInputDevice : class, IInputDevice
{
var registrationBuilder = RegisterInputDeviceService<TInputDeviceService>(builder)
.As<IInputDeviceService<TInputDevice>>();
return builder;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Autofac;
using BrickController2.Extensions;
using BrickController2.InputDeviceManagement.Sensors;
using BrickController2.PlatformServices.InputDeviceService;

namespace BrickController2.InputDeviceManagement.DI
Expand All @@ -8,6 +10,9 @@ public class InputDeviceManagementModule : Module
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<InputDeviceManagerService>().As<IInputDeviceManagerService>().As<IInputDeviceEventServiceInternal>().As<IInputDeviceEventService>().SingleInstance();

// generic sensor based input device
builder.RegisterInputDeviceService<InputSensorService, OrientationSensorController>();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using BrickController2.PlatformServices.InputDeviceService;
using BrickController2.UI.Services.Preferences;
using Microsoft.Extensions.Logging;
using Microsoft.Maui.Devices.Sensors;

namespace BrickController2.InputDeviceManagement.Sensors;

internal class InputSensorService : InputDeviceServiceBase<OrientationSensorController>, IInputDeviceService<OrientationSensorController>
{
internal const string OrientationSensorEnabledKey = "OrientationSensorEnabled";

private readonly IInputDeviceEventServiceInternal _deviceEventServiceInternal;
private readonly IPreferencesService _preferencesService;

public InputSensorService(IInputDeviceManagerService inputDeviceManagerService,
IInputDeviceEventServiceInternal deviceEventServiceInternal,
IPreferencesService preferencesService,
ILogger<InputSensorService> logger)
: base(inputDeviceManagerService, logger)
{
_deviceEventServiceInternal = deviceEventServiceInternal;
_preferencesService = preferencesService;
}

private static IOrientationSensor Sensor => OrientationSensor.Default;

public bool IsEnabled
{
get => _preferencesService.Get(OrientationSensorEnabledKey, false);
set => _preferencesService.Set(OrientationSensorEnabledKey, value);
}

public bool IsSupported => Sensor.IsSupported;

public override void Initialize()
{
if (!IsSupported || !IsEnabled)
{
return;
}

AddInputDevice(new OrientationSensorController(_deviceEventServiceInternal, Sensor));
}

public override void Stop()
{
while (TryRemoveInputDevice(out var controller))
{
_logger.LogDebug("Sensor controller has been removed InputDeviceId:{controllerId}", controller.InputDeviceId);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using BrickController2.PlatformServices.InputDevice;
using BrickController2.PlatformServices.InputDeviceService;
using Microsoft.Maui.Devices.Sensors;
using System;
using System.Linq;
using System.Numerics;

namespace BrickController2.InputDeviceManagement.Sensors;

public class OrientationSensorController : InputDeviceBase<IOrientationSensor>
{
private const double InversePI = 1.0 / Math.PI;
private const double InverseHalfPI = 1.0 / (Math.PI / 2.0);

public OrientationSensorController(IInputDeviceEventServiceInternal service,
IOrientationSensor sensor)
: base(service, sensor)
{
Name = "Orientation";
InputDeviceId = nameof(OrientationSensor);
}

public override void Start()
{
base.Start();
// Turn on orientation
InputDeviceDevice.ReadingChanged += Orientation_ReadingChanged;
InputDeviceDevice.Start(SensorSpeed.Game);
}

public override void Stop()
{
// Turn off orientation sensor
InputDeviceDevice.ReadingChanged -= Orientation_ReadingChanged;
InputDeviceDevice.Stop();
base.Stop();
}

private void Orientation_ReadingChanged(object? sender, OrientationSensorChangedEventArgs e)
{
var (Pitch, Roll, Yaw) = QuaternionToEulerAngles(e.Reading.Orientation);

RaiseEvents(
[
(nameof(Pitch), (float)Pitch),
(nameof(Roll), (float)Roll),
(nameof(Yaw), (float)Yaw)
]);
}

private void RaiseEvents((string eventName, float value)[] axisEvents)
{
var events = axisEvents
.Where(e => HasValueChanged(e.eventName, e.value))
.ToDictionary(e => (InputDeviceEventType.Axis, e.eventName), e => e.value);

RaiseEvent(events);
Comment on lines +51 to +57
}

private static (float Pitch, float Roll, float Yaw) QuaternionToEulerAngles(Quaternion q)
{
// X-Axis (Pitch): Tilting the top of the phone forward/backward
var sinPitch = 2.0 * (q.W * q.X + q.Y * q.Z);
var cosPitch = 1.0 - 2.0 * (q.X * q.X + q.Y * q.Y);
var pitch = Math.Atan2(sinPitch, cosPitch);

// Y-Axis (Roll): Tilting the phone left/right (banking)
var sinRoll = 2.0 * (q.W * q.Y - q.Z * q.X);
var roll = Math.Abs(sinRoll) >= 1
? Math.CopySign(Math.PI / 2, sinRoll) // Use 90 degrees if out of range
: Math.Asin(sinRoll);

// Z-Axis (Yaw/Azimuth): Rotating the phone like a compass on a table.
// normalize radians to percentage for gamepad axis compatibility
var sinYaw = 2.0 * (q.W * q.Z + q.X * q.Y);
var cosYaw = 1.0 - 2.0 * (q.Y * q.Y + q.Z * q.Z);
var yaw = Math.Atan2(sinYaw, cosYaw);

return (
(float)(pitch * InversePI), // <-180; 180> => <-1; 1>
(float)(roll * InverseHalfPI), // <-90; 90> => <-1; 1>
(float)(yaw * InversePI) // <-180; 180> => <-1; 1>
);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace BrickController2.PlatformServices.InputDeviceService;
using BrickController2.PlatformServices.InputDevice;

namespace BrickController2.PlatformServices.InputDeviceService;

public interface IInputDeviceService
{
Expand All @@ -13,3 +15,17 @@ public interface IInputDeviceService
/// </summary>
void Stop();
}

public interface IInputDeviceService<TInputDevice> : IInputDeviceService
where TInputDevice : class, IInputDevice
{
/// <summary>
/// Gets a value whether the input device is supported on this device.
/// </summary>
bool IsSupported { get; }

/// <summary>
/// Gets or sets a value whether the input device is enabled.
/// </summary>
bool IsEnabled { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ protected bool TryRemoveInputDevice(Predicate<TInputDevice> predicate, [MaybeNul
return _inputDeviceManagerService.TryRemoveInputDevice(predicate, out inputDevice);
}

/// <summary>
/// Try to remove the first found input device from the manager
/// </summary>
/// <param name="inputDevice">input device to be removed</param>
/// <returns>True on success</returns>
protected bool TryRemoveInputDevice([MaybeNullWhen(false)] out TInputDevice inputDevice)
=> _inputDeviceManagerService.TryRemoveInputDevice(x => true, out inputDevice);

/// <summary>
/// try to get inputdevice from the manager
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -762,4 +762,7 @@
<data name="SubchannelsPortMode" xml:space="preserve">
<value>Subchannels</value>
</data>
<data name="OrientationSensor" xml:space="preserve">
<value>Orientation Sensor</value>
</data>
</root>
10 changes: 9 additions & 1 deletion BrickController2/BrickController2/UI/Pages/SettingsPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>

<!-- Theme -->
Expand All @@ -44,7 +46,13 @@
<Label Grid.Column="0" Grid.Row="2" Text="{extensions:Translate Language}" FontSize="Large" VerticalOptions="Center"/>
<Button Grid.Column="1" Grid.Row="2" Text="{Binding CurrentLanguage}" Command="{Binding SelectLanguageCommand}" Style="{StaticResource PickerButtonStyle}" HorizontalOptions="FillAndExpand"/>

<BoxView Grid.ColumnSpan="2" Grid.Row="3" Style="{StaticResource DividerBoxViewStyle}" Margin="5,10,5,10"/>
<BoxView Grid.ColumnSpan="2" Grid.Row="3" Style="{StaticResource DividerBoxViewStyle}" Margin="5,10,5,10" IsVisible="{Binding IsOrientationSensorSupported}"/>

<!-- Orientation Sensor -->
<Label Grid.Column="0" Grid.Row="4" Text="{extensions:Translate OrientationSensor}" FontSize="Large" VerticalOptions="Center" IsVisible="{Binding IsOrientationSensorSupported}"/>
<Switch Grid.Column="1" Grid.Row="4" IsToggled="{Binding IsOrientationSensorEnabled}" HorizontalOptions="End" VerticalOptions="Center" IsVisible="{Binding IsOrientationSensorSupported}"/>

<BoxView Grid.ColumnSpan="2" Grid.Row="5" Style="{StaticResource DividerBoxViewStyle}" Margin="5,10,5,10"/>
</Grid>

</Grid>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
using BrickController2.UI.Commands;
using BrickController2.InputDeviceManagement.Sensors;
using BrickController2.PlatformServices.InputDeviceService;
using BrickController2.UI.Commands;
using BrickController2.UI.Services.Dialog;
using BrickController2.UI.Services.Localization;
using BrickController2.UI.Services.Navigation;
using BrickController2.UI.Services.Preferences;
using BrickController2.UI.Services.Theme;
using BrickController2.UI.Services.Translation;
using Microsoft.Maui.Controls;
Expand All @@ -17,6 +20,7 @@ public class SettingsPageViewModel : PageViewModelBase

private readonly IThemeService _themeService;
private readonly ILocalizationService _localizationService;
private readonly IInputDeviceService<OrientationSensorController> _orientationSensorService;
private readonly CreationListPageViewModel _parentViewModel;
private readonly IDialogService _dialogService;

Expand All @@ -26,12 +30,14 @@ public SettingsPageViewModel(
IDialogService dialogService,
IThemeService themeService,
ILocalizationService localizationService,
IInputDeviceService<OrientationSensorController> orientationSensorService,
NavigationParameters parameters) :
base(navigationService, translationService)
{
_themeService = themeService;
_dialogService = dialogService;
_localizationService = localizationService;
_orientationSensorService = orientationSensorService;
_parentViewModel = parameters.Get<CreationListPageViewModel>("parent");
SelectThemeCommand = new SafeCommand(SelectThemeAsync);
SelectLanguageCommand = new SafeCommand(SelectAppLanguageAsync);
Expand Down Expand Up @@ -66,6 +72,21 @@ public Language CurrentLanguage
public ICommand SelectThemeCommand { get; }
public ICommand SelectLanguageCommand { get; }

public bool IsOrientationSensorSupported => _orientationSensorService.IsSupported;

public bool IsOrientationSensorEnabled
{
get => _orientationSensorService.IsEnabled;
set
{
if (IsOrientationSensorEnabled != value)
{
_orientationSensorService.IsEnabled = value;
RaisePropertyChanged();
}
}
}

private async Task SelectThemeAsync()
{
var result = await _dialogService.ShowSelectionDialogAsync(
Expand Down
Loading