using HihaArvio.Models; using HihaArvio.Services.Interfaces; namespace HihaArvio.Services; /// /// Service for detecting shake gestures from accelerometer data. /// Implements shake detection algorithm per spec with 1.5g threshold and 4g max intensity. /// 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; } /// public ShakeData CurrentShakeData => _currentShakeData; /// public bool IsMonitoring => _isMonitoring; /// public event EventHandler? ShakeDataChanged; /// public void StartMonitoring() { if (_isMonitoring) { return; // Already monitoring } _isMonitoring = true; // Subscribe to accelerometer events _accelerometerService.ReadingChanged += OnAccelerometerReadingChanged; _accelerometerService.Start(); } /// 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); } /// public void Reset() { _currentShakeData = new ShakeData { IsShaking = false, Intensity = 0.0, Duration = TimeSpan.Zero }; _wasShakingLastUpdate = false; } /// 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); } } }