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:
1 parent
e36fc69edc
commit
83c646c597
7 files changed
+248
-21
No files matched your search
Executable
+53
@@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Erstellt eine Release-APK und benennt sie nach der Versionsnummer aus der
|
||||||
|
# pubspec.yaml, z. B. version: 353.0.0+353 -> a353.apk
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# In das Verzeichnis des Scripts wechseln (Projekt-Root)
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
PUBSPEC="pubspec.yaml"
|
||||||
|
if [[ ! -f "$PUBSPEC" ]]; then
|
||||||
|
echo "Fehler: $PUBSPEC nicht gefunden." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Version aus pubspec.yaml lesen (z. B. "353.0.0+353")
|
||||||
|
VERSION_LINE="$(grep -E '^version:' "$PUBSPEC" | head -n1)"
|
||||||
|
FULL_VERSION="$(echo "$VERSION_LINE" | sed -E 's/^version:[[:space:]]*//')"
|
||||||
|
|
||||||
|
# Major-Versionsnummer extrahieren (alles vor dem ersten Punkt)
|
||||||
|
MAJOR="${FULL_VERSION%%.*}"
|
||||||
|
|
||||||
|
if [[ -z "$MAJOR" ]]; then
|
||||||
|
echo "Fehler: Konnte Versionsnummer nicht aus '$VERSION_LINE' ermitteln." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
APK_NAME="a${MAJOR}.apk"
|
||||||
|
|
||||||
|
echo "Version aus pubspec.yaml: $FULL_VERSION"
|
||||||
|
echo "Ziel-APK-Name: $APK_NAME"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Release-APK bauen
|
||||||
|
flutter build apk --release
|
||||||
|
|
||||||
|
# Pfad zur erzeugten APK
|
||||||
|
BUILT_APK="build/app/outputs/flutter-apk/app-release.apk"
|
||||||
|
if [[ ! -f "$BUILT_APK" ]]; then
|
||||||
|
echo "Fehler: Erwartete APK nicht gefunden: $BUILT_APK" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ausgabeverzeichnis
|
||||||
|
OUTPUT_DIR="build/releases"
|
||||||
|
mkdir -p "$OUTPUT_DIR"
|
||||||
|
|
||||||
|
cp "$BUILT_APK" "$OUTPUT_DIR/$APK_NAME"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Fertig: $OUTPUT_DIR/$APK_NAME"
|
||||||
@@ -1,3 +1,8 @@
|
|||||||
|
// Direktimport nötig, damit build_runner die @Entity-Annotation auflösen kann,
|
||||||
|
// ohne auf die gerade erst generierte objectbox.g.dart angewiesen zu sein
|
||||||
|
// (zirkuläre Auflösung). Nicht entfernen, auch wenn der Lint ihn als unnötig meldet.
|
||||||
|
// ignore: unnecessary_import
|
||||||
|
import 'package:objectbox/objectbox.dart';
|
||||||
import 'package:stadtbote/main.dart';
|
import 'package:stadtbote/main.dart';
|
||||||
import 'package:stadtbote/objectbox.g.dart';
|
import 'package:stadtbote/objectbox.g.dart';
|
||||||
import 'package:stadtbote/tools/util.dart';
|
import 'package:stadtbote/tools/util.dart';
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ class TrackPosition {
|
|||||||
|
|
||||||
late int acc;
|
late int acc;
|
||||||
|
|
||||||
|
/// Bewegungszustand der Ortung (String-Label, z. B. "static", "moving_medium").
|
||||||
|
late String state;
|
||||||
|
|
||||||
|
/// Geschwindigkeit in m/s.
|
||||||
|
late double speed;
|
||||||
|
|
||||||
TrackPosition();
|
TrackPosition();
|
||||||
|
|
||||||
factory TrackPosition.fromJson(Map<String, dynamic> json) => _$TrackPositionFromJson(json);
|
factory TrackPosition.fromJson(Map<String, dynamic> json) => _$TrackPositionFromJson(json);
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ TrackPosition _$TrackPositionFromJson(Map<String, dynamic> json) =>
|
|||||||
..time = (json['time'] as num).toInt()
|
..time = (json['time'] as num).toInt()
|
||||||
..lat = (json['lat'] as num).toDouble()
|
..lat = (json['lat'] as num).toDouble()
|
||||||
..long = (json['long'] as num).toDouble()
|
..long = (json['long'] as num).toDouble()
|
||||||
..acc = (json['acc'] as num).toInt();
|
..acc = (json['acc'] as num).toInt()
|
||||||
|
..state = json['state'] as String
|
||||||
|
..speed = (json['speed'] as num).toDouble();
|
||||||
|
|
||||||
Map<String, dynamic> _$TrackPositionToJson(TrackPosition instance) =>
|
Map<String, dynamic> _$TrackPositionToJson(TrackPosition instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
@@ -19,4 +21,6 @@ Map<String, dynamic> _$TrackPositionToJson(TrackPosition instance) =>
|
|||||||
'lat': instance.lat,
|
'lat': instance.lat,
|
||||||
'long': instance.long,
|
'long': instance.long,
|
||||||
'acc': instance.acc,
|
'acc': instance.acc,
|
||||||
|
'state': instance.state,
|
||||||
|
'speed': instance.speed,
|
||||||
};
|
};
|
||||||
+120
-17
@@ -6,6 +6,7 @@ import 'dart:math';
|
|||||||
import 'package:archive/archive.dart';
|
import 'package:archive/archive.dart';
|
||||||
import 'package:dart_mq/dart_mq.dart';
|
import 'package:dart_mq/dart_mq.dart';
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
|
import 'package:geolocator/geolocator.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:mqtt5_client/mqtt5_client.dart';
|
import 'package:mqtt5_client/mqtt5_client.dart';
|
||||||
import 'package:mqtt5_client/mqtt5_server_client.dart';
|
import 'package:mqtt5_client/mqtt5_server_client.dart';
|
||||||
@@ -52,6 +53,7 @@ import '../main.dart';
|
|||||||
import '../model/session_status.dart';
|
import '../model/session_status.dart';
|
||||||
import 'constants.dart';
|
import 'constants.dart';
|
||||||
import 'messages/bwv_message.dart';
|
import 'messages/bwv_message.dart';
|
||||||
|
import 'track_state.dart';
|
||||||
import 'util.dart';
|
import 'util.dart';
|
||||||
|
|
||||||
enum ConnectionStateEnum { notConnected, connected }
|
enum ConnectionStateEnum { notConnected, connected }
|
||||||
@@ -77,6 +79,15 @@ class MqttCore with ProducerMixin, ConsumerMixin {
|
|||||||
|
|
||||||
Timer? _trackTimer;
|
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() {
|
MqttCore() {
|
||||||
instance = this;
|
instance = this;
|
||||||
|
|
||||||
@@ -627,25 +638,117 @@ class MqttCore with ProducerMixin, ConsumerMixin {
|
|||||||
void _startTrackTimer() {
|
void _startTrackTimer() {
|
||||||
_cancelTrackTimer();
|
_cancelTrackTimer();
|
||||||
|
|
||||||
_trackTimer ??= Timer.periodic(const Duration(seconds: 120), (Timer t) async {
|
// Zustand zurücksetzen, damit die erste Meldung als Zustandswechsel gilt.
|
||||||
try {
|
_lastTrackSentAt = null;
|
||||||
var position = await MyApp.getCurrentPosition();
|
_lastTrackState = null;
|
||||||
var trackMessage = TrackPosition();
|
_lastSampledPosition = null;
|
||||||
|
|
||||||
if (position != null) {
|
// Feste Sampling-Rate (= kleinste Intervallzeit). Bei jedem Sample wird
|
||||||
trackMessage.time = Util.toUnixSecondsUtc(position.timestamp);
|
// entschieden, ob eine Positionsmeldung fällig ist.
|
||||||
trackMessage.lat = position.latitude;
|
_trackTimer = Timer.periodic(TrackTuning.samplingInterval, (Timer t) async {
|
||||||
trackMessage.long = position.longitude;
|
await _sampleAndMaybeTrack();
|
||||||
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}");
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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() {
|
void _cancelTrackTimer() {
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/// Bewegungszustände für die adaptive Positionsmeldung (Tracking).
|
||||||
|
///
|
||||||
|
/// Je Zustand gibt es zwei Meldeintervalle: eines, wenn ein Auftrag aktiv ist
|
||||||
|
/// ("mit Auftrag"), und eines ohne aktiven Auftrag. Die kleinste vorkommende
|
||||||
|
/// Intervallzeit ([TrackTuning.samplingInterval]) wird als feste Sampling-Rate
|
||||||
|
/// verwendet; bei jedem Sample wird entschieden, ob eine Meldung fällig ist.
|
||||||
|
enum TrackState {
|
||||||
|
/// Keine Ortung verfügbar.
|
||||||
|
noFix('no_fix', 1800, 1800),
|
||||||
|
|
||||||
|
/// Statische Position (Positionsänderung < Ortungsradius / 2).
|
||||||
|
stationary('static', 600, 600),
|
||||||
|
|
||||||
|
/// In Bewegung, langsam (Geschwindigkeit < 1.5 m/s).
|
||||||
|
movingSlow('moving_slow', 60, 120),
|
||||||
|
|
||||||
|
/// In Bewegung, mittel (Geschwindigkeit < 17 m/s).
|
||||||
|
movingMedium('moving_medium', 15, 60),
|
||||||
|
|
||||||
|
/// In Bewegung, schnell (Geschwindigkeit >= 17 m/s).
|
||||||
|
movingFast('moving_fast', 30, 120);
|
||||||
|
|
||||||
|
const TrackState(
|
||||||
|
this.label,
|
||||||
|
this.intervalWithJobSeconds,
|
||||||
|
this.intervalWithoutJobSeconds,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Im JSON übertragenes String-Label des Zustands.
|
||||||
|
final String label;
|
||||||
|
|
||||||
|
/// Meldeintervall in Sekunden, wenn ein Auftrag aktiv ist.
|
||||||
|
final int intervalWithJobSeconds;
|
||||||
|
|
||||||
|
/// Meldeintervall in Sekunden ohne aktiven Auftrag.
|
||||||
|
final int intervalWithoutJobSeconds;
|
||||||
|
|
||||||
|
/// Liefert das passende Meldeintervall abhängig davon, ob ein Auftrag aktiv ist.
|
||||||
|
int intervalSeconds({required bool withJob}) =>
|
||||||
|
withJob ? intervalWithJobSeconds : intervalWithoutJobSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Feste Parameter für die Zustandsbestimmung des Trackings.
|
||||||
|
class TrackTuning {
|
||||||
|
TrackTuning._();
|
||||||
|
|
||||||
|
/// Feste Sampling-Rate = kleinste vorkommende Intervallzeit (15 s).
|
||||||
|
static const Duration samplingInterval = Duration(seconds: 15);
|
||||||
|
|
||||||
|
/// Obergrenze (exklusiv) für "langsam" in m/s.
|
||||||
|
static const double movingSlowMaxSpeed = 1.5;
|
||||||
|
|
||||||
|
/// Obergrenze (exklusiv) für "mittel" in m/s.
|
||||||
|
static const double movingMediumMaxSpeed = 17.0;
|
||||||
|
|
||||||
|
/// Vorlaufzeit, ab der ein akzeptierter Auftrag als aktiv gilt (15 min vor ordertime).
|
||||||
|
static const int activeJobLeadSeconds = 15 * 60;
|
||||||
|
}
|
||||||
+1
-3
@@ -16,13 +16,11 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 352.0.0+352
|
version: 354.0.0+354
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.9.0 <4.0.0'
|
sdk: '>=3.9.0 <4.0.0'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Dependencies specify other packages that your package needs in order to work.
|
# Dependencies specify other packages that your package needs in order to work.
|
||||||
# To automatically upgrade your package dependencies to the latest versions
|
# To automatically upgrade your package dependencies to the latest versions
|
||||||
# consider running `flutter pub upgrade --major-versions`. Alternatively,
|
# consider running `flutter pub upgrade --major-versions`. Alternatively,
|
||||||
|
|||||||
Reference in new issue
Block a user