WIFI connection support added
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
/// Verbindungsart, wie sie der Nutzer in den Einstellungen waehlt (Doku
|
||||
/// Kommunikationsschicht Abschnitt 7: "Art: WLAN * USB (spaeter)").
|
||||
/// Protokollneutraler Enum-Typ analog zu [LinkState] - deshalb bewusst auch
|
||||
/// hier in transport/ statt in der UI, aber ohne jede Netzwerk-/Plattform-
|
||||
/// Abhaengigkeit, damit er gefahrlos von database/ und ui/ importiert werden
|
||||
/// kann.
|
||||
enum ConnectionType {
|
||||
wifi,
|
||||
cellular5g,
|
||||
bluetooth,
|
||||
usb,
|
||||
}
|
||||
|
||||
/// Vorgabe-Port der mLRS-WLAN-Bruecke (Doku Abschnitt 3: "Port 14550").
|
||||
const int kDefaultWifiPort = 14550;
|
||||
@@ -0,0 +1,168 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import '../link_transport.dart';
|
||||
|
||||
/// UDP-Transport zur mLRS-WLAN-Bruecke (Doku Kommunikationsschicht Abschnitt
|
||||
/// 3). Erste, einfachste Ausbaustufe: lokal auf [_getPort] binden und auf
|
||||
/// eingehende Pakete hoeren (Stufe 1 - ohne Netzwerk-Bindung, siehe Doku
|
||||
/// Abschnitt 4; Stufe 2/3 sind bewusst noch nicht umgesetzt).
|
||||
///
|
||||
/// Setzt das in Abschnitt 3 beschriebene Muster um: lokal auf dem Zielport
|
||||
/// binden statt auf einen fluechtigen Port (sonst kommt kein Byte an), und
|
||||
/// die Gegenstelle ausschliesslich aus dem Verkehr lernen statt sie fest
|
||||
/// einzucodieren. Eine vom Nutzer eingetragene Host-Vorgabe fuellt nur das
|
||||
/// allererste ausgehende Paket vor.
|
||||
class UdpTransport implements LinkTransport {
|
||||
UdpTransport({
|
||||
required Future<String?> Function() getHost,
|
||||
required Future<int> Function() getPort,
|
||||
}) : _getHost = getHost,
|
||||
_getPort = getPort;
|
||||
|
||||
final Future<String?> Function() _getHost;
|
||||
final Future<int> Function() _getPort;
|
||||
|
||||
final _incomingController = StreamController<Uint8List>.broadcast();
|
||||
final _stateController = StreamController<LinkState>.broadcast();
|
||||
|
||||
RawDatagramSocket? _socket;
|
||||
StreamSubscription<RawSocketEvent>? _socketSub;
|
||||
InternetAddress? _peerAddress;
|
||||
int? _peerPort;
|
||||
int? _boundPort;
|
||||
|
||||
LinkState _state = LinkState.disconnected;
|
||||
LinkErrorReason? _lastErrorReason;
|
||||
String? _lastErrorMessage;
|
||||
|
||||
@override
|
||||
Stream<Uint8List> get incoming => _incomingController.stream;
|
||||
|
||||
@override
|
||||
Stream<LinkState> get state => _stateController.stream;
|
||||
|
||||
@override
|
||||
LinkErrorReason? get lastErrorReason => _lastErrorReason;
|
||||
|
||||
@override
|
||||
String? get lastErrorMessage => _lastErrorMessage;
|
||||
|
||||
/// Tatsaechlich gebundener lokaler Port, erst nach [connect] bekannt -
|
||||
/// weicht von der Vorgabe ab, wenn diese belegt war (Doku 3/8).
|
||||
int? get boundPort => _boundPort;
|
||||
|
||||
/// Gegenstelle, sobald aus dem ersten eingehenden Paket gelernt bzw. aus
|
||||
/// der Host-Vorgabe vorbelegt (Doku 3).
|
||||
String? get peerDescription =>
|
||||
_peerAddress == null ? null : '${_peerAddress!.address}:$_peerPort';
|
||||
|
||||
@override
|
||||
Future<void> connect() async {
|
||||
_lastErrorReason = null;
|
||||
_lastErrorMessage = null;
|
||||
_setState(LinkState.connecting);
|
||||
|
||||
final configuredPort = await _getPort();
|
||||
final host = await _getHost();
|
||||
if (host != null && host.isNotEmpty) {
|
||||
final parsed = InternetAddress.tryParse(host);
|
||||
if (parsed != null) {
|
||||
_peerAddress = parsed;
|
||||
_peerPort = configuredPort;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
RawDatagramSocket socket;
|
||||
try {
|
||||
// reuseAddress: false, damit ein tatsaechlich belegter Port hier
|
||||
// erkennbar mit SocketException scheitert - Darts Standard
|
||||
// (reuseAddress: true) wuerde den Konflikt sonst still zulassen.
|
||||
socket = await RawDatagramSocket.bind(
|
||||
InternetAddress.anyIPv4,
|
||||
configuredPort,
|
||||
reuseAddress: false,
|
||||
);
|
||||
} on SocketException {
|
||||
// Port belegt (Doku 3/8: "ist 14550 belegt, auf einen fluechtigen
|
||||
// ausweichen, aber das protokollieren") - Ausweichen ist kein Grund,
|
||||
// die Verbindung abzubrechen.
|
||||
socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0);
|
||||
_lastErrorMessage =
|
||||
'Port $configuredPort belegt, ausgewichen auf ${socket.port}.';
|
||||
}
|
||||
_socket = socket;
|
||||
_boundPort = socket.port;
|
||||
_socketSub = socket.listen(
|
||||
_onSocketEvent,
|
||||
onError: (Object e) =>
|
||||
_setError(LinkErrorReason.connectionFailed, e.toString()),
|
||||
);
|
||||
_setState(LinkState.connected);
|
||||
} on SocketException catch (e) {
|
||||
_fail(LinkErrorReason.connectionFailed, e.message);
|
||||
}
|
||||
}
|
||||
|
||||
void _onSocketEvent(RawSocketEvent event) {
|
||||
if (event != RawSocketEvent.read) return;
|
||||
final datagram = _socket?.receive();
|
||||
if (datagram == null) return;
|
||||
_peerAddress = datagram.address;
|
||||
_peerPort = datagram.port;
|
||||
_incomingController.add(datagram.data);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> disconnect() async {
|
||||
await _socketSub?.cancel();
|
||||
_socketSub = null;
|
||||
_socket?.close();
|
||||
_socket = null;
|
||||
_boundPort = null;
|
||||
_setState(LinkState.disconnected);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> send(Uint8List data) async {
|
||||
final socket = _socket;
|
||||
final peer = _peerAddress;
|
||||
final port = _peerPort;
|
||||
if (socket == null || peer == null || port == null) {
|
||||
throw const LinkConnectException(
|
||||
LinkErrorReason.connectionFailed,
|
||||
'send() ohne aktive Verbindung oder bekannte Gegenstelle aufgerufen.',
|
||||
);
|
||||
}
|
||||
socket.send(data, peer, port);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_socketSub?.cancel();
|
||||
_socket?.close();
|
||||
_socket = null;
|
||||
_incomingController.close();
|
||||
_stateController.close();
|
||||
}
|
||||
|
||||
void _setError(LinkErrorReason reason, String message) {
|
||||
_lastErrorReason = reason;
|
||||
_lastErrorMessage = message;
|
||||
_setState(LinkState.error);
|
||||
}
|
||||
|
||||
Never _fail(LinkErrorReason reason, String message) {
|
||||
_setError(reason, message);
|
||||
throw LinkConnectException(reason, message);
|
||||
}
|
||||
|
||||
void _setState(LinkState next) {
|
||||
_state = next;
|
||||
if (!_stateController.isClosed) _stateController.add(next);
|
||||
}
|
||||
|
||||
LinkState get currentState => _state;
|
||||
}
|
||||
Reference in New Issue
Block a user