import 'dart:async'; import 'dart:convert'; import 'dart:math' as math; import 'package:get/get.dart'; import 'package:sensors_plus/sensors_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; /// "Telefon rúd-libella" — a kitűzőrúd függőlegességének jelzése a /// telefon gyorsulásmérőjéből. /// /// NEM tilt-kompenzáció (a pozíciót nem javítja), hanem tilt-JELZÉS: /// a fizikai szelencés libellát váltja ki a képernyőn. Előny az /// iránytűvel szemben: csak a gravitáció irányát méri, ezért a vevő és /// a fém rúd MÁGNESES zavara nem érinti. /// /// Kalibráció (mert a telefontartó sosem áll pontosan a rúd tengelyében): /// 1. lépés: rúd függőlegesen a szelencés libella szerint → 2 s átlagolt /// gravitáció-vektor = referencia. /// 2. lépés (opcionális, pontosabb): ugyanez a telefon rúd-tengely /// körüli 180°-os átforgatása után — a két vektor átlagában a tartó /// ferdesége kiesik, a valódi rúd-tengely marad. /// /// Használhatósági részletek: /// * MOZGÁS-detektálás: séta közben a gyorsulásmérő a lépések /// gyorsulását is méri, a "gravitáció" iránya hamis → a jelzés /// ilyenkor inaktív (szürke), csak megálláskor élesedik. Ez egybeesik /// a munkafolyamattal: a libella a tárolás előtti pillanatban számít. /// * SIMÍTÁS: exponenciális aluláteresztő szűrő, ~10 Hz-es kijelzési /// frissítés — enélkül a buborék idegesítően remegne. /// * A dőlésből számolt VÍZSZINTES HIBA a beszédes érték: /// hiba ≈ rúdmagasság × sin(dőlés) — 2 m-es rúdnál 0,5° már ~1,7 cm. class TiltService extends GetxService { static TiltService get to => Get.find(); // ── Reaktív állapot ────────────────────────────────────────────── final active = false.obs; final isCalibrated = false.obs; final isMoving = false.obs; final isCapturing = false.obs; // kalibrációs mintavétel fut /// Dőlés a kalibrált rúd-tengelyhez képest (fok). final tiltDeg = 0.0.obs; /// A buborék 2D iránya a telefon képernyő-síkjában (egység: a dőlés /// szinusza; a widget skálázza). x: jobbra +, y: felfelé +. final bubbleX = 0.0.obs; final bubbleY = 0.0.obs; /// Rúdmagasság (m) — a vízszintes hiba számításához; szerkeszthető. final poleHeight = 2.0.obs; /// Dőlésből számolt vízszintes eltolódás (cm). double get horizontalErrorCm => poleHeight.value * math.sin(tiltDeg.value * math.pi / 180) * 100; // ── Belső állapot ──────────────────────────────────────────────── StreamSubscription? _sub; // Referencia (rúd-tengely) egységvektor a telefon koordinátáiban. double _refX = 0, _refY = 0, _refZ = 1; // Szűrt gyorsulás-vektor. double _fx = 0, _fy = 0, _fz = 0; bool _filterSeeded = false; static const _alpha = 0.15; // aluláteresztő szűrő együttható // Mozgás-detektálás: |a| szórása csúszóablakban. final _magWindow = []; static const _windowLen = 25; // ~0,5 s 50 Hz-en static const _movingStdThreshold = 0.35; // m/s² DateTime _lastUiPush = DateTime.fromMillisecondsSinceEpoch(0); @override Future onInit() async { super.onInit(); final prefs = await SharedPreferences.getInstance(); poleHeight.value = prefs.getDouble('tilt_pole_height') ?? 2.0; final refJson = prefs.getString('tilt_reference'); if (refJson != null) { final r = jsonDecode(refJson) as Map; _refX = (r['x'] as num).toDouble(); _refY = (r['y'] as num).toDouble(); _refZ = (r['z'] as num).toDouble(); isCalibrated.value = true; } } @override void onClose() { _sub?.cancel(); super.onClose(); } /// A szenzor-stream csak aktív kitűzés/mérés módban fut (akku-kímélés). void setActive(bool value) { if (active.value == value) return; active.value = value; if (value) { _filterSeeded = false; _magWindow.clear(); _sub = accelerometerEventStream( samplingPeriod: const Duration(milliseconds: 20)) .listen(_onSample); } else { _sub?.cancel(); _sub = null; } } void _onSample(AccelerometerEvent e) { // Szűrés. if (!_filterSeeded) { _fx = e.x; _fy = e.y; _fz = e.z; _filterSeeded = true; } else { _fx += _alpha * (e.x - _fx); _fy += _alpha * (e.y - _fy); _fz += _alpha * (e.z - _fz); } // Mozgás-detektálás a NYERS magnitúdó szórásából. final mag = math.sqrt(e.x * e.x + e.y * e.y + e.z * e.z); _magWindow.add(mag); if (_magWindow.length > _windowLen) _magWindow.removeAt(0); if (_magWindow.length >= _windowLen ~/ 2) { final mean = _magWindow.reduce((a, b) => a + b) / _magWindow.length; final varSum = _magWindow.fold(0, (s, v) => s + (v - mean) * (v - mean)); final std = math.sqrt(varSum / _magWindow.length); isMoving.value = std > _movingStdThreshold; } // UI-frissítés max. ~12 Hz-en. final now = DateTime.now(); if (now.difference(_lastUiPush).inMilliseconds < 80) return; _lastUiPush = now; _updateTilt(); } void _updateTilt() { final fmag = math.sqrt(_fx * _fx + _fy * _fy + _fz * _fz); if (fmag < 1e-6) return; // Dőlés = a szűrt gravitáció és a referencia-tengely szöge. final dot = (_fx * _refX + _fy * _refY + _fz * _refZ) / fmag; tiltDeg.value = math.acos(dot.clamp(-1.0, 1.0)) * 180 / math.pi; // Buborék-irány: a gravitáció referencia-síkra vetített komponense, // a telefon képernyő-tengelyeiben. (x: jobbra, y: a képernyőn felfelé // — a szenzor y-tengelye a képernyőn felfelé mutat.) final gx = _fx / fmag, gy = _fy / fmag, gz = _fz / fmag; final proj = gx * _refX + gy * _refY + gz * _refZ; bubbleX.value = gx - proj * _refX; bubbleY.value = gy - proj * _refY; // A z-komponenst a 2D buborék nem jeleníti meg — függőleges tartónál // (telefon képernyője ~vízszintes normálissal) x/y hordozza az infót. } // ═════════════════════════════════════════════════════════════════ // Kalibráció // ═════════════════════════════════════════════════════════════════ /// [seconds] másodpercig átlagolja a NYERS gravitáció-vektort. /// Mozgás esetén a minta zajos lesz — a hívó UI figyelmeztet előtte. Future<({double x, double y, double z})> captureVector( {int seconds = 2}) async { isCapturing.value = true; double sx = 0, sy = 0, sz = 0; var n = 0; final sub = accelerometerEventStream( samplingPeriod: const Duration(milliseconds: 20)) .listen((e) { sx += e.x; sy += e.y; sz += e.z; n++; }); try { await Future.delayed(Duration(seconds: seconds)); } finally { await sub.cancel(); isCapturing.value = false; } if (n == 0) return (x: 0.0, y: 0.0, z: 1.0); final mag = math.sqrt(sx * sx + sy * sy + sz * sz); return (x: sx / mag, y: sy / mag, z: sz / mag); } /// Referencia mentése. Egy lépésnél [v2] = null; két lépésnél /// (180°-os átforgatás) a két egységvektor ÁTLAGA a rúd-tengely — /// a tartó ferdesége a két mérésben ellentétes előjelű, így kiesik. Future saveReference(({double x, double y, double z}) v1, [({double x, double y, double z})? v2]) async { double x = v1.x, y = v1.y, z = v1.z; if (v2 != null) { x = (v1.x + v2.x) / 2; y = (v1.y + v2.y) / 2; z = (v1.z + v2.z) / 2; } final mag = math.sqrt(x * x + y * y + z * z); if (mag < 1e-6) return; _refX = x / mag; _refY = y / mag; _refZ = z / mag; isCalibrated.value = true; final prefs = await SharedPreferences.getInstance(); await prefs.setString( 'tilt_reference', jsonEncode({'x': _refX, 'y': _refY, 'z': _refZ})); } Future setPoleHeight(double meters) async { if (meters <= 0) return; poleHeight.value = meters; final prefs = await SharedPreferences.getInstance(); await prefs.setDouble('tilt_pole_height', meters); } Future clearCalibration() async { isCalibrated.value = false; final prefs = await SharedPreferences.getInstance(); await prefs.remove('tilt_reference'); } }