Libella megjelnítése mérőrúdhoz, transzverzális rajzolása az aktuális kitűzendő ponthoz

This commit is contained in:
2026-07-07 02:48:37 +02:00
parent ecb4892bc8
commit d3857f7fde
7 changed files with 629 additions and 0 deletions
+8
View File
@@ -9,6 +9,7 @@ import 'package:terepi_seged/services/app_database.dart';
import 'package:terepi_seged/services/coord_converter_service.dart';
import 'package:terepi_seged/services/gnss/gnss_service.dart';
import 'package:terepi_seged/services/project_service.dart';
import 'package:terepi_seged/services/tilt_service.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import '../../models/stakeout_point.dart';
@@ -53,12 +54,14 @@ class StakeoutService extends GetxService {
active.value = value;
if (value) {
WakelockPlus.enable();
if (Get.isRegistered<TiltService>()) TiltService.to.setActive(true);
await load();
_applyPhase();
_scheduleHaptic();
} else {
_hapticTimer?.cancel();
WakelockPlus.disable();
if (Get.isRegistered<TiltService>()) TiltService.to.setActive(false);
}
}
@@ -467,6 +470,11 @@ class StakeoutService extends GetxService {
devInline: inline,
devCrossline: crossline,
devDz: t.planEovZ != null ? t.planEovZ! - useAlt : null,
tiltDeg: Get.isRegistered<TiltService>() &&
TiltService.to.isCalibrated.value &&
!TiltService.to.isMoving.value
? TiltService.to.tiltDeg.value
: null,
fixQuality: gnss.gpsQuality.value,
accuracy: gnss.horizontalAccuracy,
stakedAt: DateTime.now(),
+229
View File
@@ -0,0 +1,229 @@
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');
}
}