230 lines
8.7 KiB
Dart
230 lines
8.7 KiB
Dart
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');
|
||
}
|
||
}
|