Files
MobilApp/lib/services/tilt_service.dart
T

230 lines
8.7 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = <double>[];
static const _windowLen = 25; // ~0,5 s 50 Hz-en
static const _movingStdThreshold = 0.35; // m/s²
DateTime _lastUiPush = DateTime.fromMillisecondsSinceEpoch(0);
@override
Future<void> 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<String, dynamic>;
_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<double>(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<void> 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<void> setPoleHeight(double meters) async {
if (meters <= 0) return;
poleHeight.value = meters;
final prefs = await SharedPreferences.getInstance();
await prefs.setDouble('tilt_pole_height', meters);
}
Future<void> clearCalibration() async {
isCalibrated.value = false;
final prefs = await SharedPreferences.getInstance();
await prefs.remove('tilt_reference');
}
}