mirror of
https://github.com/ivuorinen/hiha-arvio.git
synced 2026-02-11 12:48:50 +00:00
Implemented platform-agnostic accelerometer abstraction with iOS and desktop implementations: **IAccelerometerService Interface:** - Platform-agnostic abstraction for sensor input - SensorReading model (renamed from AccelerometerData to avoid MAUI conflict) - X, Y, Z acceleration values in g-force units - ReadingChanged event for continuous data stream - Start/Stop methods for sensor control - IsSupported property for platform capability detection **IosAccelerometerService (10 tests):** - Uses MAUI's Microsoft.Maui.Devices.Sensors.Accelerometer - Works on iOS devices and simulator (provides simulated data) - Conditional compilation (#if IOS || MACCATALYST) - Converts MAUI AccelerometerData to our SensorReading - Graceful failure when sensor unavailable - Pragma directives for conditional warnings **DesktopAccelerometerService (11 tests):** - Timer-based simulated sensor readings (~60Hz) - Generates realistic noise around 1g gravity baseline - Device-at-rest simulation: X≈0, Y≈0, Z≈1 - Includes SimulateShake() method for manual testing - Always reports IsSupported=true (simulation available) **ShakeDetectionService Integration:** - Updated constructor to require IAccelerometerService (breaking change) - StartMonitoring: subscribes to ReadingChanged, calls accelerometer.Start() - StopMonitoring: unsubscribes and calls accelerometer.Stop() - OnAccelerometerReadingChanged: pipes readings to ProcessAccelerometerReading - All existing shake detection logic unchanged **Platform-Specific DI:** - MauiProgram.cs uses conditional compilation - iOS/macOS Catalyst: IosAccelerometerService - Desktop/other platforms: DesktopAccelerometerService - All services registered as Singleton **Test Updates:** - ShakeDetectionServiceTests: now uses NSubstitute mock accelerometer - ServiceIntegrationTests: uses DesktopAccelerometerService - AccelerometerServiceContractTests: base class for implementation tests - IosAccelerometerServiceTests: 10 tests with platform-aware assertions - DesktopAccelerometerServiceTests: 11 tests with async timing **Technical Details:** - SensorReading record type with required init properties - Value equality for SensorReading (record type benefit) - Timer disposal in DesktopAccelerometerService - Event subscription safety (check for null, unsubscribe on stop) - 189 tests passing (165 previous + 24 accelerometer) - 0 warnings, 0 errors across all platforms Build verified: net8.0, iOS (net8.0-ios), macOS Catalyst (net8.0-maccatalyst) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
163 lines
4.6 KiB
C#
163 lines
4.6 KiB
C#
using HihaArvio.Models;
|
|
using HihaArvio.Services.Interfaces;
|
|
|
|
namespace HihaArvio.Services;
|
|
|
|
/// <summary>
|
|
/// Service for detecting shake gestures from accelerometer data.
|
|
/// Implements shake detection algorithm per spec with 1.5g threshold and 4g max intensity.
|
|
/// </summary>
|
|
public class ShakeDetectionService : IShakeDetectionService
|
|
{
|
|
// Per spec: 1.5g threshold for shake detection
|
|
private const double ShakeThresholdG = 1.5;
|
|
|
|
// Per spec: 4g is maximum expected shake intensity for normalization
|
|
private const double MaxShakeIntensityG = 4.0;
|
|
|
|
// Gravity constant (at rest, device experiences 1g)
|
|
private const double GravityG = 1.0;
|
|
|
|
private readonly IAccelerometerService _accelerometerService;
|
|
private ShakeData _currentShakeData;
|
|
private bool _isMonitoring;
|
|
private DateTimeOffset _shakeStartTime;
|
|
private bool _wasShakingLastUpdate;
|
|
|
|
public ShakeDetectionService(IAccelerometerService accelerometerService)
|
|
{
|
|
_accelerometerService = accelerometerService ?? throw new ArgumentNullException(nameof(accelerometerService));
|
|
|
|
_currentShakeData = new ShakeData
|
|
{
|
|
IsShaking = false,
|
|
Intensity = 0.0,
|
|
Duration = TimeSpan.Zero
|
|
};
|
|
_isMonitoring = false;
|
|
_wasShakingLastUpdate = false;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ShakeData CurrentShakeData => _currentShakeData;
|
|
|
|
/// <inheritdoc/>
|
|
public bool IsMonitoring => _isMonitoring;
|
|
|
|
/// <inheritdoc/>
|
|
public event EventHandler<ShakeData>? ShakeDataChanged;
|
|
|
|
/// <inheritdoc/>
|
|
public void StartMonitoring()
|
|
{
|
|
if (_isMonitoring)
|
|
{
|
|
return; // Already monitoring
|
|
}
|
|
|
|
_isMonitoring = true;
|
|
|
|
// Subscribe to accelerometer events
|
|
_accelerometerService.ReadingChanged += OnAccelerometerReadingChanged;
|
|
_accelerometerService.Start();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void StopMonitoring()
|
|
{
|
|
if (!_isMonitoring)
|
|
{
|
|
return; // Already stopped
|
|
}
|
|
|
|
_isMonitoring = false;
|
|
|
|
// Unsubscribe from accelerometer events
|
|
_accelerometerService.ReadingChanged -= OnAccelerometerReadingChanged;
|
|
_accelerometerService.Stop();
|
|
}
|
|
|
|
private void OnAccelerometerReadingChanged(object? sender, SensorReading reading)
|
|
{
|
|
ProcessAccelerometerReading(reading.X, reading.Y, reading.Z);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void Reset()
|
|
{
|
|
_currentShakeData = new ShakeData
|
|
{
|
|
IsShaking = false,
|
|
Intensity = 0.0,
|
|
Duration = TimeSpan.Zero
|
|
};
|
|
_wasShakingLastUpdate = false;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void ProcessAccelerometerReading(double x, double y, double z)
|
|
{
|
|
if (!_isMonitoring)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Calculate magnitude of acceleration vector
|
|
var magnitude = Math.Sqrt(x * x + y * y + z * z);
|
|
|
|
// Subtract gravity to get shake acceleration (device at rest = 1g)
|
|
var shakeAcceleration = Math.Max(0, magnitude - GravityG);
|
|
|
|
// Determine if shaking based on threshold
|
|
var isShaking = shakeAcceleration >= ShakeThresholdG;
|
|
|
|
// Normalize intensity to 0.0-1.0 range based on max expected shake
|
|
var normalizedIntensity = isShaking
|
|
? Math.Min(1.0, shakeAcceleration / MaxShakeIntensityG)
|
|
: 0.0;
|
|
|
|
// Track shake duration
|
|
TimeSpan duration;
|
|
if (isShaking)
|
|
{
|
|
if (!_wasShakingLastUpdate)
|
|
{
|
|
// Shake just started - reset start time
|
|
_shakeStartTime = DateTimeOffset.UtcNow;
|
|
duration = TimeSpan.Zero;
|
|
}
|
|
else
|
|
{
|
|
// Shake continuing - calculate duration
|
|
duration = DateTimeOffset.UtcNow - _shakeStartTime;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Not shaking - reset duration
|
|
duration = TimeSpan.Zero;
|
|
}
|
|
|
|
// Check if state changed
|
|
var stateChanged = isShaking != _wasShakingLastUpdate ||
|
|
Math.Abs(normalizedIntensity - _currentShakeData.Intensity) > 0.01 ||
|
|
duration != _currentShakeData.Duration;
|
|
|
|
// Update current state
|
|
_currentShakeData = new ShakeData
|
|
{
|
|
IsShaking = isShaking,
|
|
Intensity = normalizedIntensity,
|
|
Duration = duration
|
|
};
|
|
|
|
_wasShakingLastUpdate = isShaking;
|
|
|
|
// Fire event if state changed
|
|
if (stateChanged)
|
|
{
|
|
ShakeDataChanged?.Invoke(this, _currentShakeData);
|
|
}
|
|
}
|
|
}
|