msp over wifi implemented
This commit is contained in:
@@ -23,6 +23,7 @@ class TelemetryFrame {
|
||||
required this.lon,
|
||||
required this.altitudeM,
|
||||
required this.speedMs,
|
||||
required this.headingDeg,
|
||||
required this.armed,
|
||||
this.activeWaypointIndex,
|
||||
});
|
||||
@@ -31,6 +32,10 @@ class TelemetryFrame {
|
||||
final double lon;
|
||||
final double altitudeM;
|
||||
final double speedMs;
|
||||
|
||||
/// Kurs ueber Grund in Grad (0 = Norden, im Uhrzeigersinn) - dreht den
|
||||
/// Drohnen-Marker (Doku 3.10, [DroneMarkerIcon]).
|
||||
final double headingDeg;
|
||||
final bool armed;
|
||||
final int? activeWaypointIndex;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ class MockFlightControllerLink implements FlightControllerLink {
|
||||
int? _activeWaypointIndex;
|
||||
StreamController<TelemetryFrame>? _telemetryController;
|
||||
Timer? _ticker;
|
||||
double _heading = 0;
|
||||
|
||||
@override
|
||||
FcCapabilities get capabilities => const FcCapabilities(
|
||||
@@ -22,11 +23,17 @@ class MockFlightControllerLink implements FlightControllerLink {
|
||||
Future<void> connect() async {
|
||||
_telemetryController = StreamController<TelemetryFrame>.broadcast();
|
||||
_ticker = Timer.periodic(const Duration(seconds: 1), (_) {
|
||||
// Kurs dreht sich langsam weiter statt fest zu stehen, damit die
|
||||
// Rotation des Drohnen-Markers (DroneMarkerIcon) auch ohne Hardware
|
||||
// sichtbar ueberprueft werden kann (Doku 9: UI-Entwicklung ohne
|
||||
// Hardware).
|
||||
_heading = (_heading + 5) % 360;
|
||||
_telemetryController?.add(TelemetryFrame(
|
||||
lat: 52.5,
|
||||
lon: 13.4,
|
||||
altitudeM: 120,
|
||||
speedMs: 18,
|
||||
headingDeg: _heading,
|
||||
armed: _armed,
|
||||
activeWaypointIndex: _activeWaypointIndex,
|
||||
));
|
||||
|
||||
@@ -10,6 +10,7 @@ class MspGpsReading {
|
||||
required this.lat,
|
||||
required this.lon,
|
||||
required this.speedMs,
|
||||
required this.headingDeg,
|
||||
});
|
||||
|
||||
/// 0 = kein Fix (`fc_msp.c`: `gpsSol.fixType`).
|
||||
@@ -19,6 +20,10 @@ class MspGpsReading {
|
||||
final double lon;
|
||||
final double speedMs;
|
||||
|
||||
/// Kurs ueber Grund in Grad (0 = Norden, im Uhrzeigersinn), aus
|
||||
/// `groundCourse` - nur bei [hasFix] aussagekraeftig.
|
||||
final double headingDeg;
|
||||
|
||||
bool get hasFix => fixType != 0;
|
||||
}
|
||||
|
||||
@@ -31,6 +36,7 @@ MspGpsReading parseMspRawGps(Uint8List payload) {
|
||||
lat: data.getInt32(2, Endian.little) / 1e7,
|
||||
lon: data.getInt32(6, Endian.little) / 1e7,
|
||||
speedMs: data.getUint16(12, Endian.little) / 100.0,
|
||||
headingDeg: data.getUint16(14, Endian.little) / 10.0,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ class MspTelemetryPoller {
|
||||
lon: gps.lon,
|
||||
altitudeM: altitudeM,
|
||||
speedMs: gps.speedMs,
|
||||
headingDeg: gps.headingDeg,
|
||||
armed: _armed,
|
||||
activeWaypointIndex: _activeWaypointIndex,
|
||||
));
|
||||
|
||||
@@ -101,6 +101,15 @@ class UdpTransport implements LinkTransport {
|
||||
|
||||
@override
|
||||
Future<void> connect() async {
|
||||
// Idempotent: mehrere Aufrufer koennen unabhaengig voneinander "sorg
|
||||
// dafuer, dass wir verbunden sind" anfordern (z.B. der Auto-Connect-
|
||||
// Trigger beim Wechsel in den Fly-Modus UND der MSP-Telemetrie-Consumer,
|
||||
// der denselben Transport braucht) - ohne diese Sperre wuerde der
|
||||
// zweite Aufruf eine weitere WifiNetworkSpecifier-Anfrage samt
|
||||
// Systemdialog auslösen, waehrend die erste noch laeuft.
|
||||
if (_state == LinkState.connecting || _state == LinkState.connected) {
|
||||
return;
|
||||
}
|
||||
_lastErrorReason = null;
|
||||
_lastErrorMessage = null;
|
||||
_setState(LinkState.connecting);
|
||||
|
||||
@@ -1,16 +1,37 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../transport/connection_type.dart';
|
||||
import '../../transport/flight_controller_link.dart';
|
||||
import '../../transport/mock/mock_flight_controller_link.dart';
|
||||
import '../../transport/msp/msp_flight_controller_link.dart';
|
||||
import 'connection_settings_provider.dart';
|
||||
import 'wifi_connection_provider.dart';
|
||||
|
||||
/// FlightControllerLink fuer den Fly-Modus (Doku 3.1) - aktuell fest auf den
|
||||
/// Mock verdrahtet, bis ein echter Transport (MSP, Doku 4.19) existiert.
|
||||
/// FlightControllerLink fuer den Fly-Modus (Doku 3.1): bei WLAN als
|
||||
/// gewaehlter Verbindungsart ein echter MSP-Link ueber den UDP-Transport,
|
||||
/// sonst (Bluetooth/5G/USB, oder solange die Einstellungen noch nicht
|
||||
/// geladen sind) weiterhin der Mock fuer UI-Entwicklung ohne Hardware -
|
||||
/// Bluetooth ist auf der eingesetzten mLRS-Bruecke ohnehin nicht moeglich
|
||||
/// (Doku Kommunikationsschicht v2, Abschnitt 1), 5G/USB sind noch nicht
|
||||
/// umgesetzt.
|
||||
///
|
||||
/// connectionType wird bewusst per ref.read() einmalig beim Eintritt in den
|
||||
/// Fly-Modus gelesen, nicht reaktiv beobachtet - sonst wuerde jede
|
||||
/// Einstellungsaenderung waehrend des Flugs (z.B. Tippen im
|
||||
/// SSID-Praefix-Feld, das ueberall im Settings-Screen ein
|
||||
/// `ref.invalidate(connectionSettingsProvider)` ausloest) einen Neuaufbau
|
||||
/// dieses Providers samt Trennen/Neuverbinden des Transports ausloesen.
|
||||
///
|
||||
/// autoDispose: verbindet erst, wenn der Fly-Screen tatsaechlich beobachtet
|
||||
/// wird, und trennt/stoppt den Mock-Ticker wieder, sobald niemand mehr
|
||||
/// zuhoert (z.B. Rueckkehr in den Plan-Modus) statt endlos im Hintergrund
|
||||
/// wird, und trennt sich wieder (Doku Abschnitt 8: "Verlassen des
|
||||
/// Fly-Modus gibt den Netz-Request frei"), sobald niemand mehr zuhoert
|
||||
/// (z.B. Rueckkehr in den Plan-Modus) statt endlos im Hintergrund
|
||||
/// weiterzulaufen.
|
||||
final flightControllerLinkProvider = Provider.autoDispose<FlightControllerLink>((ref) {
|
||||
final link = MockFlightControllerLink();
|
||||
final connectionType = ref.read(connectionSettingsProvider).value?.connectionType;
|
||||
final FlightControllerLink link = connectionType == ConnectionType.wifi
|
||||
? MspFlightControllerLink(transport: ref.watch(wifiTransportProvider))
|
||||
: MockFlightControllerLink();
|
||||
ref.onDispose(link.disconnect);
|
||||
return link;
|
||||
});
|
||||
|
||||
@@ -149,7 +149,7 @@ class FlyScreen extends ConsumerWidget {
|
||||
width: 28,
|
||||
height: 28,
|
||||
alignment: Alignment.center,
|
||||
child: const DroneMarkerIcon(),
|
||||
child: DroneMarkerIcon(headingDeg: telemetry.headingDeg),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
|
||||
@@ -1,15 +1,84 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Live-Positionsmarker der Drohne im Fly-Modus (Doku 3.10) - noch ohne
|
||||
/// Peilungsanzeige, da TelemetryFrame aktuell keinen Kurs (heading)
|
||||
/// mitliefert (nur MockFlightControllerLink, siehe Doku 4.19).
|
||||
/// Live-Positionsmarker der Drohne im Fly-Modus (Doku 3.10), Papierflieger-
|
||||
/// Form aus design/B_paperplane.svg - nativ als [CustomPainter] statt als
|
||||
/// SVG-Asset nachgebaut (Format bewusst geaendert: die drei Pfade sind
|
||||
/// einfache Dreiecke, eine zusaetzliche SVG-Renderer-Abhaengigkeit lohnt sich
|
||||
/// dafuer nicht, siehe auch mountain_peak_icon.dart/wind_arrow_icon.dart fuer
|
||||
/// dasselbe Muster). Zeigt unrotiert (Spitze nach oben) bei fehlendem
|
||||
/// [headingDeg], sonst im Uhrzeigersinn um den Kurs ueber Grund gedreht.
|
||||
class DroneMarkerIcon extends StatelessWidget {
|
||||
const DroneMarkerIcon({super.key, this.size = 22});
|
||||
const DroneMarkerIcon({super.key, this.size = 28, this.headingDeg});
|
||||
|
||||
final double size;
|
||||
|
||||
/// Kurs ueber Grund in Grad (0 = Norden, im Uhrzeigersinn) aus
|
||||
/// TelemetryFrame.headingDeg.
|
||||
final double? headingDeg;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Icon(Icons.airplanemode_active, color: const Color(0xFF4D9DFF), size: size);
|
||||
return Transform.rotate(
|
||||
angle: (headingDeg ?? 0) * math.pi / 180,
|
||||
child: CustomPaint(
|
||||
size: Size.square(size),
|
||||
painter: const _PaperPlanePainter(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PaperPlanePainter extends CustomPainter {
|
||||
const _PaperPlanePainter();
|
||||
|
||||
// 1:1 aus design/B_paperplane.svg uebernommene Koordinaten (viewBox
|
||||
// 0 0 200 200): Spitze oben, zwei Fluegel unten, Heckkerbe dazwischen.
|
||||
static const _viewBoxSize = 200.0;
|
||||
static const _nose = Offset(100, 22);
|
||||
static const _rightWing = Offset(164, 172);
|
||||
static const _tailNotch = Offset(100, 138);
|
||||
static const _leftWing = Offset(36, 172);
|
||||
static const _rightFill = Color(0xFFFF2E93);
|
||||
static const _leftFill = Color(0xFFB01464);
|
||||
static const _strokeWidth = 7.0;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final scale = size.width / _viewBoxSize;
|
||||
Offset s(Offset p) => p * scale;
|
||||
|
||||
final rightHalf = Path()
|
||||
..moveTo(s(_nose).dx, s(_nose).dy)
|
||||
..lineTo(s(_rightWing).dx, s(_rightWing).dy)
|
||||
..lineTo(s(_tailNotch).dx, s(_tailNotch).dy)
|
||||
..close();
|
||||
final leftHalf = Path()
|
||||
..moveTo(s(_nose).dx, s(_nose).dy)
|
||||
..lineTo(s(_leftWing).dx, s(_leftWing).dy)
|
||||
..lineTo(s(_tailNotch).dx, s(_tailNotch).dy)
|
||||
..close();
|
||||
|
||||
canvas.drawPath(rightHalf, Paint()..color = _rightFill);
|
||||
canvas.drawPath(leftHalf, Paint()..color = _leftFill);
|
||||
|
||||
final outline = Path()
|
||||
..moveTo(s(_nose).dx, s(_nose).dy)
|
||||
..lineTo(s(_rightWing).dx, s(_rightWing).dy)
|
||||
..lineTo(s(_tailNotch).dx, s(_tailNotch).dy)
|
||||
..lineTo(s(_leftWing).dx, s(_leftWing).dy)
|
||||
..close();
|
||||
canvas.drawPath(
|
||||
outline,
|
||||
Paint()
|
||||
..color = Colors.white
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = _strokeWidth * scale
|
||||
..strokeJoin = StrokeJoin.round,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _PaperPlanePainter oldDelegate) => false;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ Timer _startFakeFlightController(LoopbackTransport transport) {
|
||||
d.setInt32(2, 525200000, Endian.little); // lat 52.52
|
||||
d.setInt32(6, 134050000, Endian.little); // lon 13.405
|
||||
d.setUint16(12, 500, Endian.little); // 5.00 m/s
|
||||
d.setUint16(14, 2700, Endian.little); // 270.0 deg
|
||||
payload = d.buffer.asUint8List();
|
||||
case MspCommands.altitude:
|
||||
final d = ByteData(10);
|
||||
@@ -67,6 +68,7 @@ void main() {
|
||||
expect(frame.lon, closeTo(13.405, 1e-6));
|
||||
expect(frame.altitudeM, closeTo(50.0, 1e-6));
|
||||
expect(frame.speedMs, closeTo(5.0, 1e-6));
|
||||
expect(frame.headingDeg, closeTo(270.0, 1e-6));
|
||||
|
||||
await link.disconnect();
|
||||
responder.cancel();
|
||||
|
||||
@@ -28,6 +28,7 @@ void main() {
|
||||
expect(reading.lat, closeTo(52.52, 1e-9));
|
||||
expect(reading.lon, closeTo(13.405, 1e-9));
|
||||
expect(reading.speedMs, closeTo(12.34, 1e-9));
|
||||
expect(reading.headingDeg, closeTo(90.0, 1e-9));
|
||||
});
|
||||
|
||||
test('fixType 0 bedeutet kein Fix', () {
|
||||
|
||||
@@ -25,6 +25,7 @@ import 'package:dmc_app/ui/providers/geocoding_provider.dart';
|
||||
import 'package:dmc_app/ui/providers/mission_meta_provider.dart';
|
||||
import 'package:dmc_app/ui/providers/sharing_provider.dart';
|
||||
import 'package:dmc_app/ui/providers/telemetry_provider.dart';
|
||||
import 'package:dmc_app/transport/mock/mock_flight_controller_link.dart';
|
||||
import 'package:dmc_app/ui/widgets/full_value_chart.dart';
|
||||
import 'package:dmc_app/ui/widgets/halo_menu.dart';
|
||||
import 'package:dmc_app/ui/widgets/header_wind_pill.dart';
|
||||
@@ -68,8 +69,21 @@ class _FakeSharingService extends SharingService {
|
||||
// path_provider-Plugin (echtes Dateisystem) benoetigt wird.
|
||||
AppDatabase _testDatabase() => AppDatabase.forTesting(NativeDatabase.memory());
|
||||
|
||||
// flightControllerLinkProvider waehlt inzwischen je nach Verbindungsart
|
||||
// (Settings-Default: WLAN) zwischen echtem MSP-ueber-UDP-Link und dem Mock -
|
||||
// in Tests immer explizit den Mock erzwingen, sonst wuerde der Fly-Modus
|
||||
// versuchen, einen echten Platform-Channel (WifiNetworkSpecifier) zu
|
||||
// verwenden, der in der Testumgebung nie eine Antwort liefert und den Test
|
||||
// haengen liesse.
|
||||
ProviderContainer _testContainer() => ProviderContainer(
|
||||
overrides: [appDatabaseProvider.overrideWithValue(_testDatabase())],
|
||||
overrides: [
|
||||
appDatabaseProvider.overrideWithValue(_testDatabase()),
|
||||
flightControllerLinkProvider.overrideWith((ref) {
|
||||
final link = MockFlightControllerLink();
|
||||
ref.onDispose(link.disconnect);
|
||||
return link;
|
||||
}),
|
||||
],
|
||||
);
|
||||
|
||||
/// Der Fly-Modus startet ueber telemetryProvider einen Mock-Telemetrie-
|
||||
@@ -81,7 +95,14 @@ Future<void> _stopTelemetry(ProviderContainer container) =>
|
||||
container.read(flightControllerLinkProvider).disconnect();
|
||||
|
||||
Widget _wrap(Widget child) => ProviderScope(
|
||||
overrides: [appDatabaseProvider.overrideWithValue(_testDatabase())],
|
||||
overrides: [
|
||||
appDatabaseProvider.overrideWithValue(_testDatabase()),
|
||||
flightControllerLinkProvider.overrideWith((ref) {
|
||||
final link = MockFlightControllerLink();
|
||||
ref.onDispose(link.disconnect);
|
||||
return link;
|
||||
}),
|
||||
],
|
||||
child: BlocProvider(
|
||||
create: (_) => AppModeCubit(),
|
||||
child: MaterialApp(home: child),
|
||||
|
||||
Reference in New Issue
Block a user