Adaptive Positionsmeldung nach Bewegungszustand

- TrackState-Zustände (no_fix/static/moving_slow/moving_medium/moving_fast)
  mit je eigenem Meldeintervall mit/ohne Auftrag (lib/tools/track_state.dart)
- Festes Sampling (15s) statt fixem 120s-Timer; Sofortmeldung bei
  Zustandswechsel, sonst nach Zustandsintervall (mqtt_core.dart)
- track-JSON um state (String) und speed (m/s) erweitert
- Statisch-Erkennung über GPS-Genauigkeit, aktiver Auftrag = akzeptiert
  und <=15min vor ordertime
- chat_message.dart: Direktimport von objectbox.dart ergaenzt, behebt
  build_runner-Aufloesungsfehler (ChatMessage-Entity)
- Release-Build-Script (build_release_apk.sh)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
SvenandClaude Opus 4.8 committed 2026-06-24 11:48:31 +02:00
1 parent e36fc69edc
commit 83c646c597
7 files changed
+248 -21

No files matched your search

+120 -17
View File
@@ -6,6 +6,7 @@ import 'dart:math';
import 'package:archive/archive.dart';
import 'package:dart_mq/dart_mq.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:geolocator/geolocator.dart';
import 'package:http/http.dart' as http;
import 'package:mqtt5_client/mqtt5_client.dart';
import 'package:mqtt5_client/mqtt5_server_client.dart';
@@ -52,6 +53,7 @@ import '../main.dart';
import '../model/session_status.dart';
import 'constants.dart';
import 'messages/bwv_message.dart';
import 'track_state.dart';
import 'util.dart';
enum ConnectionStateEnum { notConnected, connected }
@@ -77,6 +79,15 @@ class MqttCore with ProducerMixin, ConsumerMixin {
Timer? _trackTimer;
/// Zeitpunkt der letzten gesendeten Positionsmeldung.
DateTime? _lastTrackSentAt;
/// Zuletzt gemeldeter Bewegungszustand (für Erkennung von Zustandswechseln).
TrackState? _lastTrackState;
/// Zuletzt gesampelte Position (Referenz für die Statisch-Erkennung).
Position? _lastSampledPosition;
MqttCore() {
instance = this;
@@ -627,25 +638,117 @@ class MqttCore with ProducerMixin, ConsumerMixin {
void _startTrackTimer() {
_cancelTrackTimer();
_trackTimer ??= Timer.periodic(const Duration(seconds: 120), (Timer t) async {
try {
var position = await MyApp.getCurrentPosition();
var trackMessage = TrackPosition();
// Zustand zurücksetzen, damit die erste Meldung als Zustandswechsel gilt.
_lastTrackSentAt = null;
_lastTrackState = null;
_lastSampledPosition = null;
if (position != null) {
trackMessage.time = Util.toUnixSecondsUtc(position.timestamp);
trackMessage.lat = position.latitude;
trackMessage.long = position.longitude;
trackMessage.acc = position.accuracy.toInt();
_publishMessage("track", trackMessage.toJson(), false);
} else {
_publishMessage("track", <String, dynamic>{}, false);
}
} catch (ex) {
Util.logException("Error in periodic track timer: {$ex}");
}
// Feste Sampling-Rate (= kleinste Intervallzeit). Bei jedem Sample wird
// entschieden, ob eine Positionsmeldung fällig ist.
_trackTimer = Timer.periodic(TrackTuning.samplingInterval, (Timer t) async {
await _sampleAndMaybeTrack();
});
// Direkt nach Session-Start eine erste Meldung absetzen.
_sampleAndMaybeTrack();
}
/// Ermittelt den aktuellen Zustand und sendet eine Positionsmeldung, falls
/// ein Zustandswechsel vorliegt oder das Meldeintervall abgelaufen ist.
Future<void> _sampleAndMaybeTrack() async {
try {
var position = await MyApp.getCurrentPosition();
var state = _determineTrackState(position);
var withJob = _hasActiveJob();
var now = DateTime.now();
var isTransition = state != _lastTrackState;
var intervalSeconds = state.intervalSeconds(withJob: withJob);
var elapsedEnough = _lastTrackSentAt == null ||
now.difference(_lastTrackSentAt!).inSeconds >= intervalSeconds;
if (isTransition || elapsedEnough) {
_sendTrack(position, state);
_lastTrackSentAt = now;
_lastTrackState = state;
}
// Referenzposition für die Statisch-Erkennung immer aktualisieren.
_lastSampledPosition = position;
} catch (ex) {
Util.logException("Error in periodic track timer: {$ex}");
}
}
/// Bestimmt den Bewegungszustand aus der aktuellen Position.
TrackState _determineTrackState(Position? position) {
if (position == null) {
return TrackState.noFix;
}
// Statisch: Positionsänderung seit letztem Sample < Ortungsradius / 2,
// wobei der Ortungsradius die GPS-Genauigkeit (accuracy) ist.
var last = _lastSampledPosition;
if (last != null) {
var distance = Geolocator.distanceBetween(
last.latitude,
last.longitude,
position.latitude,
position.longitude,
);
if (distance < position.accuracy / 2) {
return TrackState.stationary;
}
}
var speed = position.speed; // m/s
if (speed < TrackTuning.movingSlowMaxSpeed) {
return TrackState.movingSlow;
} else if (speed < TrackTuning.movingMediumMaxSpeed) {
return TrackState.movingMedium;
} else {
return TrackState.movingFast;
}
}
/// Prüft, ob ein Auftrag aktiv ist: akzeptierter Job, dessen Auftragszeit
/// höchstens [TrackTuning.activeJobLeadSeconds] in der Zukunft liegt.
bool _hasActiveJob() {
final box = MyApp.objectBoxStore.box<Job>();
final query = box.query(Job_.state.equals(Constants.stateAccepted)).build();
var jobs = query.find();
query.close();
var nowSeconds = Util.nowUnixSecondsUtc();
for (var job in jobs) {
if (nowSeconds >= job.ordertime - TrackTuning.activeJobLeadSeconds) {
return true;
}
}
return false;
}
/// Baut die Positionsmeldung und veröffentlicht sie auf dem Topic "track".
void _sendTrack(Position? position, TrackState state) {
var trackMessage = TrackPosition();
trackMessage.state = state.label;
if (position != null) {
trackMessage.time = Util.toUnixSecondsUtc(position.timestamp);
trackMessage.lat = position.latitude;
trackMessage.long = position.longitude;
trackMessage.acc = position.accuracy.toInt();
trackMessage.speed = position.speed;
} else {
// Keine Ortung verfügbar: nur Zustand + Zeitstempel melden.
trackMessage.time = Util.nowUnixSecondsUtc();
trackMessage.lat = 0.0;
trackMessage.long = 0.0;
trackMessage.acc = 0;
trackMessage.speed = 0.0;
}
_publishMessage("track", trackMessage.toJson(), false);
}
void _cancelTrackTimer() {