wifi connection established by app, process socket binding, primary os connection untouched
This commit is contained in:
@@ -78,6 +78,12 @@ class AppSettingsTable extends Table {
|
||||
IntColumn get connectionWifiPort =>
|
||||
integer().withDefault(const Constant(kDefaultWifiPort))();
|
||||
|
||||
/// SSID-Praefix fuer den app-initiierten WLAN-Verbindungsaufbau per
|
||||
/// `WifiNetworkSpecifier` (Doku Kommunikationsschicht v2 Abschnitt 4/8),
|
||||
/// Standard "mLRS-", ueberschreibbar.
|
||||
TextColumn get connectionWifiSsidPrefix =>
|
||||
text().withDefault(const Constant(kDefaultWifiSsidPrefix))();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
@@ -89,7 +95,7 @@ class AppDatabase extends _$AppDatabase {
|
||||
AppDatabase.forTesting(super.executor);
|
||||
|
||||
@override
|
||||
int get schemaVersion => 4;
|
||||
int get schemaVersion => 5;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
@@ -121,6 +127,12 @@ class AppDatabase extends _$AppDatabase {
|
||||
appSettingsTable.connectionWifiPort,
|
||||
);
|
||||
}
|
||||
if (from < 5) {
|
||||
await m.addColumn(
|
||||
appSettingsTable,
|
||||
appSettingsTable.connectionWifiSsidPrefix,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -1358,7 +1358,7 @@ class $AppSettingsTableTable extends AppSettingsTable
|
||||
false,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: false,
|
||||
defaultValue: const Constant('bluetooth'),
|
||||
defaultValue: const Constant('wifi'),
|
||||
);
|
||||
static const VerificationMeta _connectionWifiHostMeta =
|
||||
const VerificationMeta('connectionWifiHost');
|
||||
@@ -1382,6 +1382,18 @@ class $AppSettingsTableTable extends AppSettingsTable
|
||||
requiredDuringInsert: false,
|
||||
defaultValue: const Constant(kDefaultWifiPort),
|
||||
);
|
||||
static const VerificationMeta _connectionWifiSsidPrefixMeta =
|
||||
const VerificationMeta('connectionWifiSsidPrefix');
|
||||
@override
|
||||
late final GeneratedColumn<String> connectionWifiSsidPrefix =
|
||||
GeneratedColumn<String>(
|
||||
'connection_wifi_ssid_prefix',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: false,
|
||||
defaultValue: const Constant(kDefaultWifiSsidPrefix),
|
||||
);
|
||||
@override
|
||||
List<GeneratedColumn> get $columns => [
|
||||
id,
|
||||
@@ -1392,6 +1404,7 @@ class $AppSettingsTableTable extends AppSettingsTable
|
||||
connectionType,
|
||||
connectionWifiHost,
|
||||
connectionWifiPort,
|
||||
connectionWifiSsidPrefix,
|
||||
];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@@ -1471,6 +1484,15 @@ class $AppSettingsTableTable extends AppSettingsTable
|
||||
),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('connection_wifi_ssid_prefix')) {
|
||||
context.handle(
|
||||
_connectionWifiSsidPrefixMeta,
|
||||
connectionWifiSsidPrefix.isAcceptableOrUnknown(
|
||||
data['connection_wifi_ssid_prefix']!,
|
||||
_connectionWifiSsidPrefixMeta,
|
||||
),
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@@ -1512,6 +1534,10 @@ class $AppSettingsTableTable extends AppSettingsTable
|
||||
DriftSqlType.int,
|
||||
data['${effectivePrefix}connection_wifi_port'],
|
||||
)!,
|
||||
connectionWifiSsidPrefix: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}connection_wifi_ssid_prefix'],
|
||||
)!,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1536,8 +1562,8 @@ class AppSettingsTableData extends DataClass
|
||||
final bool connectionAutoConnect;
|
||||
|
||||
/// Gewaehlte Verbindungsart (Doku 7: "Art: WLAN * USB (spaeter)"),
|
||||
/// gespeichert als [ConnectionType.name]. Standard bleibt Bluetooth, damit
|
||||
/// bestehende Installationen ihr bisheriges Verhalten behalten.
|
||||
/// gespeichert als [ConnectionType.name]. App-Default ist WLAN; die
|
||||
/// Nutzerauswahl wird danach dauerhaft gemerkt.
|
||||
final String connectionType;
|
||||
|
||||
/// Vom Nutzer eingetragene Ziel-IP der mLRS-WLAN-Bruecke (Doku 3:
|
||||
@@ -1547,6 +1573,11 @@ class AppSettingsTableData extends DataClass
|
||||
|
||||
/// Lokaler UDP-Port fuer die mLRS-WLAN-Bruecke (Doku 3), Standard 14550.
|
||||
final int connectionWifiPort;
|
||||
|
||||
/// SSID-Praefix fuer den app-initiierten WLAN-Verbindungsaufbau per
|
||||
/// `WifiNetworkSpecifier` (Doku Kommunikationsschicht v2 Abschnitt 4/8),
|
||||
/// Standard "mLRS-", ueberschreibbar.
|
||||
final String connectionWifiSsidPrefix;
|
||||
const AppSettingsTableData({
|
||||
required this.id,
|
||||
this.activeDroneProfileId,
|
||||
@@ -1556,6 +1587,7 @@ class AppSettingsTableData extends DataClass
|
||||
required this.connectionType,
|
||||
this.connectionWifiHost,
|
||||
required this.connectionWifiPort,
|
||||
required this.connectionWifiSsidPrefix,
|
||||
});
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
@@ -1578,6 +1610,9 @@ class AppSettingsTableData extends DataClass
|
||||
map['connection_wifi_host'] = Variable<String>(connectionWifiHost);
|
||||
}
|
||||
map['connection_wifi_port'] = Variable<int>(connectionWifiPort);
|
||||
map['connection_wifi_ssid_prefix'] = Variable<String>(
|
||||
connectionWifiSsidPrefix,
|
||||
);
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -1599,6 +1634,7 @@ class AppSettingsTableData extends DataClass
|
||||
? const Value.absent()
|
||||
: Value(connectionWifiHost),
|
||||
connectionWifiPort: Value(connectionWifiPort),
|
||||
connectionWifiSsidPrefix: Value(connectionWifiSsidPrefix),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1624,6 +1660,9 @@ class AppSettingsTableData extends DataClass
|
||||
json['connectionWifiHost'],
|
||||
),
|
||||
connectionWifiPort: serializer.fromJson<int>(json['connectionWifiPort']),
|
||||
connectionWifiSsidPrefix: serializer.fromJson<String>(
|
||||
json['connectionWifiSsidPrefix'],
|
||||
),
|
||||
);
|
||||
}
|
||||
@override
|
||||
@@ -1640,6 +1679,9 @@ class AppSettingsTableData extends DataClass
|
||||
'connectionType': serializer.toJson<String>(connectionType),
|
||||
'connectionWifiHost': serializer.toJson<String?>(connectionWifiHost),
|
||||
'connectionWifiPort': serializer.toJson<int>(connectionWifiPort),
|
||||
'connectionWifiSsidPrefix': serializer.toJson<String>(
|
||||
connectionWifiSsidPrefix,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1652,6 +1694,7 @@ class AppSettingsTableData extends DataClass
|
||||
String? connectionType,
|
||||
Value<String?> connectionWifiHost = const Value.absent(),
|
||||
int? connectionWifiPort,
|
||||
String? connectionWifiSsidPrefix,
|
||||
}) => AppSettingsTableData(
|
||||
id: id ?? this.id,
|
||||
activeDroneProfileId: activeDroneProfileId.present
|
||||
@@ -1669,6 +1712,8 @@ class AppSettingsTableData extends DataClass
|
||||
? connectionWifiHost.value
|
||||
: this.connectionWifiHost,
|
||||
connectionWifiPort: connectionWifiPort ?? this.connectionWifiPort,
|
||||
connectionWifiSsidPrefix:
|
||||
connectionWifiSsidPrefix ?? this.connectionWifiSsidPrefix,
|
||||
);
|
||||
AppSettingsTableData copyWithCompanion(AppSettingsTableCompanion data) {
|
||||
return AppSettingsTableData(
|
||||
@@ -1694,6 +1739,9 @@ class AppSettingsTableData extends DataClass
|
||||
connectionWifiPort: data.connectionWifiPort.present
|
||||
? data.connectionWifiPort.value
|
||||
: this.connectionWifiPort,
|
||||
connectionWifiSsidPrefix: data.connectionWifiSsidPrefix.present
|
||||
? data.connectionWifiSsidPrefix.value
|
||||
: this.connectionWifiSsidPrefix,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1707,7 +1755,8 @@ class AppSettingsTableData extends DataClass
|
||||
..write('connectionAutoConnect: $connectionAutoConnect, ')
|
||||
..write('connectionType: $connectionType, ')
|
||||
..write('connectionWifiHost: $connectionWifiHost, ')
|
||||
..write('connectionWifiPort: $connectionWifiPort')
|
||||
..write('connectionWifiPort: $connectionWifiPort, ')
|
||||
..write('connectionWifiSsidPrefix: $connectionWifiSsidPrefix')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
@@ -1722,6 +1771,7 @@ class AppSettingsTableData extends DataClass
|
||||
connectionType,
|
||||
connectionWifiHost,
|
||||
connectionWifiPort,
|
||||
connectionWifiSsidPrefix,
|
||||
);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
@@ -1734,7 +1784,8 @@ class AppSettingsTableData extends DataClass
|
||||
other.connectionAutoConnect == this.connectionAutoConnect &&
|
||||
other.connectionType == this.connectionType &&
|
||||
other.connectionWifiHost == this.connectionWifiHost &&
|
||||
other.connectionWifiPort == this.connectionWifiPort);
|
||||
other.connectionWifiPort == this.connectionWifiPort &&
|
||||
other.connectionWifiSsidPrefix == this.connectionWifiSsidPrefix);
|
||||
}
|
||||
|
||||
class AppSettingsTableCompanion extends UpdateCompanion<AppSettingsTableData> {
|
||||
@@ -1746,6 +1797,7 @@ class AppSettingsTableCompanion extends UpdateCompanion<AppSettingsTableData> {
|
||||
final Value<String> connectionType;
|
||||
final Value<String?> connectionWifiHost;
|
||||
final Value<int> connectionWifiPort;
|
||||
final Value<String> connectionWifiSsidPrefix;
|
||||
const AppSettingsTableCompanion({
|
||||
this.id = const Value.absent(),
|
||||
this.activeDroneProfileId = const Value.absent(),
|
||||
@@ -1755,6 +1807,7 @@ class AppSettingsTableCompanion extends UpdateCompanion<AppSettingsTableData> {
|
||||
this.connectionType = const Value.absent(),
|
||||
this.connectionWifiHost = const Value.absent(),
|
||||
this.connectionWifiPort = const Value.absent(),
|
||||
this.connectionWifiSsidPrefix = const Value.absent(),
|
||||
});
|
||||
AppSettingsTableCompanion.insert({
|
||||
this.id = const Value.absent(),
|
||||
@@ -1765,6 +1818,7 @@ class AppSettingsTableCompanion extends UpdateCompanion<AppSettingsTableData> {
|
||||
this.connectionType = const Value.absent(),
|
||||
this.connectionWifiHost = const Value.absent(),
|
||||
this.connectionWifiPort = const Value.absent(),
|
||||
this.connectionWifiSsidPrefix = const Value.absent(),
|
||||
});
|
||||
static Insertable<AppSettingsTableData> custom({
|
||||
Expression<int>? id,
|
||||
@@ -1775,6 +1829,7 @@ class AppSettingsTableCompanion extends UpdateCompanion<AppSettingsTableData> {
|
||||
Expression<String>? connectionType,
|
||||
Expression<String>? connectionWifiHost,
|
||||
Expression<int>? connectionWifiPort,
|
||||
Expression<String>? connectionWifiSsidPrefix,
|
||||
}) {
|
||||
return RawValuesInsertable({
|
||||
if (id != null) 'id': id,
|
||||
@@ -1790,6 +1845,8 @@ class AppSettingsTableCompanion extends UpdateCompanion<AppSettingsTableData> {
|
||||
'connection_wifi_host': connectionWifiHost,
|
||||
if (connectionWifiPort != null)
|
||||
'connection_wifi_port': connectionWifiPort,
|
||||
if (connectionWifiSsidPrefix != null)
|
||||
'connection_wifi_ssid_prefix': connectionWifiSsidPrefix,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1802,6 +1859,7 @@ class AppSettingsTableCompanion extends UpdateCompanion<AppSettingsTableData> {
|
||||
Value<String>? connectionType,
|
||||
Value<String?>? connectionWifiHost,
|
||||
Value<int>? connectionWifiPort,
|
||||
Value<String>? connectionWifiSsidPrefix,
|
||||
}) {
|
||||
return AppSettingsTableCompanion(
|
||||
id: id ?? this.id,
|
||||
@@ -1814,6 +1872,8 @@ class AppSettingsTableCompanion extends UpdateCompanion<AppSettingsTableData> {
|
||||
connectionType: connectionType ?? this.connectionType,
|
||||
connectionWifiHost: connectionWifiHost ?? this.connectionWifiHost,
|
||||
connectionWifiPort: connectionWifiPort ?? this.connectionWifiPort,
|
||||
connectionWifiSsidPrefix:
|
||||
connectionWifiSsidPrefix ?? this.connectionWifiSsidPrefix,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1850,6 +1910,11 @@ class AppSettingsTableCompanion extends UpdateCompanion<AppSettingsTableData> {
|
||||
if (connectionWifiPort.present) {
|
||||
map['connection_wifi_port'] = Variable<int>(connectionWifiPort.value);
|
||||
}
|
||||
if (connectionWifiSsidPrefix.present) {
|
||||
map['connection_wifi_ssid_prefix'] = Variable<String>(
|
||||
connectionWifiSsidPrefix.value,
|
||||
);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -1863,7 +1928,8 @@ class AppSettingsTableCompanion extends UpdateCompanion<AppSettingsTableData> {
|
||||
..write('connectionAutoConnect: $connectionAutoConnect, ')
|
||||
..write('connectionType: $connectionType, ')
|
||||
..write('connectionWifiHost: $connectionWifiHost, ')
|
||||
..write('connectionWifiPort: $connectionWifiPort')
|
||||
..write('connectionWifiPort: $connectionWifiPort, ')
|
||||
..write('connectionWifiSsidPrefix: $connectionWifiSsidPrefix')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
@@ -2507,6 +2573,7 @@ typedef $$AppSettingsTableTableCreateCompanionBuilder =
|
||||
Value<String> connectionType,
|
||||
Value<String?> connectionWifiHost,
|
||||
Value<int> connectionWifiPort,
|
||||
Value<String> connectionWifiSsidPrefix,
|
||||
});
|
||||
typedef $$AppSettingsTableTableUpdateCompanionBuilder =
|
||||
AppSettingsTableCompanion Function({
|
||||
@@ -2518,6 +2585,7 @@ typedef $$AppSettingsTableTableUpdateCompanionBuilder =
|
||||
Value<String> connectionType,
|
||||
Value<String?> connectionWifiHost,
|
||||
Value<int> connectionWifiPort,
|
||||
Value<String> connectionWifiSsidPrefix,
|
||||
});
|
||||
|
||||
class $$AppSettingsTableTableFilterComposer
|
||||
@@ -2568,6 +2636,11 @@ class $$AppSettingsTableTableFilterComposer
|
||||
column: $table.connectionWifiPort,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<String> get connectionWifiSsidPrefix => $composableBuilder(
|
||||
column: $table.connectionWifiSsidPrefix,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
}
|
||||
|
||||
class $$AppSettingsTableTableOrderingComposer
|
||||
@@ -2618,6 +2691,11 @@ class $$AppSettingsTableTableOrderingComposer
|
||||
column: $table.connectionWifiPort,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<String> get connectionWifiSsidPrefix => $composableBuilder(
|
||||
column: $table.connectionWifiSsidPrefix,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
}
|
||||
|
||||
class $$AppSettingsTableTableAnnotationComposer
|
||||
@@ -2666,6 +2744,11 @@ class $$AppSettingsTableTableAnnotationComposer
|
||||
column: $table.connectionWifiPort,
|
||||
builder: (column) => column,
|
||||
);
|
||||
|
||||
GeneratedColumn<String> get connectionWifiSsidPrefix => $composableBuilder(
|
||||
column: $table.connectionWifiSsidPrefix,
|
||||
builder: (column) => column,
|
||||
);
|
||||
}
|
||||
|
||||
class $$AppSettingsTableTableTableManager
|
||||
@@ -2713,6 +2796,7 @@ class $$AppSettingsTableTableTableManager
|
||||
Value<String> connectionType = const Value.absent(),
|
||||
Value<String?> connectionWifiHost = const Value.absent(),
|
||||
Value<int> connectionWifiPort = const Value.absent(),
|
||||
Value<String> connectionWifiSsidPrefix = const Value.absent(),
|
||||
}) => AppSettingsTableCompanion(
|
||||
id: id,
|
||||
activeDroneProfileId: activeDroneProfileId,
|
||||
@@ -2722,6 +2806,7 @@ class $$AppSettingsTableTableTableManager
|
||||
connectionType: connectionType,
|
||||
connectionWifiHost: connectionWifiHost,
|
||||
connectionWifiPort: connectionWifiPort,
|
||||
connectionWifiSsidPrefix: connectionWifiSsidPrefix,
|
||||
),
|
||||
createCompanionCallback:
|
||||
({
|
||||
@@ -2733,6 +2818,7 @@ class $$AppSettingsTableTableTableManager
|
||||
Value<String> connectionType = const Value.absent(),
|
||||
Value<String?> connectionWifiHost = const Value.absent(),
|
||||
Value<int> connectionWifiPort = const Value.absent(),
|
||||
Value<String> connectionWifiSsidPrefix = const Value.absent(),
|
||||
}) => AppSettingsTableCompanion.insert(
|
||||
id: id,
|
||||
activeDroneProfileId: activeDroneProfileId,
|
||||
@@ -2742,6 +2828,7 @@ class $$AppSettingsTableTableTableManager
|
||||
connectionType: connectionType,
|
||||
connectionWifiHost: connectionWifiHost,
|
||||
connectionWifiPort: connectionWifiPort,
|
||||
connectionWifiSsidPrefix: connectionWifiSsidPrefix,
|
||||
),
|
||||
withReferenceMapper: (p0) => p0
|
||||
.map((e) => (e.readTable(table), BaseReferences(db, table, e)))
|
||||
|
||||
@@ -22,6 +22,7 @@ class AppSettingsRepository {
|
||||
ConnectionType connectionType,
|
||||
String? connectionWifiHost,
|
||||
int connectionWifiPort,
|
||||
String connectionWifiSsidPrefix,
|
||||
})> load() async {
|
||||
final row = await (_db.select(_db.appSettingsTable)
|
||||
..where((s) => s.id.equals(_rowId)))
|
||||
@@ -37,6 +38,8 @@ class AppSettingsRepository {
|
||||
),
|
||||
connectionWifiHost: row?.connectionWifiHost,
|
||||
connectionWifiPort: row?.connectionWifiPort ?? kDefaultWifiPort,
|
||||
connectionWifiSsidPrefix:
|
||||
row?.connectionWifiSsidPrefix ?? kDefaultWifiSsidPrefix,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -104,6 +107,18 @@ class AppSettingsRepository {
|
||||
));
|
||||
}
|
||||
|
||||
/// SSID-Praefix fuer den app-initiierten WLAN-Verbindungsaufbau (Doku
|
||||
/// Kommunikationsschicht v2 Abschnitt 4/8), Standard "mLRS-".
|
||||
Future<String> loadConnectionWifiSsidPrefix() async =>
|
||||
(await load()).connectionWifiSsidPrefix;
|
||||
|
||||
Future<void> setConnectionWifiSsidPrefix(String prefix) async {
|
||||
await _upsert(AppSettingsTableCompanion(
|
||||
id: const Value(_rowId),
|
||||
connectionWifiSsidPrefix: Value(prefix),
|
||||
));
|
||||
}
|
||||
|
||||
Future<void> _upsert(AppSettingsTableCompanion companion) async {
|
||||
await _db.into(_db.appSettingsTable).insertOnConflictUpdate(companion);
|
||||
}
|
||||
|
||||
@@ -13,3 +13,8 @@ enum ConnectionType {
|
||||
|
||||
/// Vorgabe-Port der mLRS-WLAN-Bruecke (Doku Abschnitt 3: "Port 14550").
|
||||
const int kDefaultWifiPort = 14550;
|
||||
|
||||
/// Vorgabe-SSID-Praefix der mLRS-WLAN-Bruecke (Doku Kommunikationsschicht v2
|
||||
/// Abschnitt 3/8: "mLRS-xxxx AP UDP" - die SSID enthaelt eine Zufallszahl,
|
||||
/// daher Praefixvergleich statt exakter SSID).
|
||||
const String kDefaultWifiSsidPrefix = 'mLRS-';
|
||||
|
||||
@@ -30,6 +30,18 @@ enum LinkErrorReason {
|
||||
/// getrennt.
|
||||
connectionFailed,
|
||||
|
||||
/// Die per WifiNetworkSpecifier angeforderte mLRS-Netz konnte nicht
|
||||
/// zustande kommen (Doku Kommunikationsschicht v2, Abschnitt 4) - z.B.
|
||||
/// Nutzer hat den Systemdialog abgelehnt/keine passende SSID in Reichweite.
|
||||
/// Android meldet das ueber `onUnavailable()` statt einer Exception.
|
||||
networkUnavailable,
|
||||
|
||||
/// Das zuvor verfuegbare mLRS-Netz wurde waehrend einer bestehenden
|
||||
/// Verbindung getrennt (`onLost()`) - z.B. ausser Reichweite oder Bridge
|
||||
/// ausgeschaltet. Der Socket ist damit ungueltig und muss samt
|
||||
/// Bindungsfenster neu erzeugt werden (Doku Abschnitt 8).
|
||||
networkLost,
|
||||
|
||||
/// Sonstiger, nicht naeher klassifizierter Fehler.
|
||||
unknown,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// Ereignis der per [MlrsNetworkController.requestNetwork] angeforderten
|
||||
/// mLRS-WLAN-Verbindung (Doku Kommunikationsschicht v2, Abschnitt 4).
|
||||
sealed class MlrsNetworkEvent {
|
||||
const MlrsNetworkEvent();
|
||||
}
|
||||
|
||||
/// Das Netz ist verfuegbar (Nutzer hat den Systemdialog bestaetigt).
|
||||
/// [gatewayAddress] ist die Gateway-Adresse des Netzes, falls ermittelbar -
|
||||
/// dient als Vorbelegung der Zieladresse, bevor eine Gegenstelle aus dem
|
||||
/// Verkehr gelernt wurde (Doku Abschnitt 3).
|
||||
class MlrsNetworkAvailable extends MlrsNetworkEvent {
|
||||
const MlrsNetworkAvailable({this.gatewayAddress});
|
||||
|
||||
final String? gatewayAddress;
|
||||
}
|
||||
|
||||
/// Die Anfrage konnte nicht erfuellt werden (Nutzer hat den Systemdialog
|
||||
/// abgelehnt, keine passende SSID in Reichweite, oder Systemtimeout).
|
||||
class MlrsNetworkUnavailable extends MlrsNetworkEvent {
|
||||
const MlrsNetworkUnavailable();
|
||||
}
|
||||
|
||||
/// Ein zuvor verfuegbares Netz wurde waehrend einer bestehenden Verbindung
|
||||
/// getrennt (Doku Abschnitt 8: "Bindung haftet am Socket ... hoeren bewusst
|
||||
/// auf zu funktionieren").
|
||||
class MlrsNetworkLost extends MlrsNetworkEvent {
|
||||
const MlrsNetworkLost();
|
||||
}
|
||||
|
||||
/// Plattform-Bruecke zum app-initiierten WLAN-Verbindungsaufbau
|
||||
/// (`WifiNetworkSpecifier`/`ConnectivityManager`, Doku Kommunikationsschicht
|
||||
/// v2 Abschnitt 4) - kapselt den einzigen Platform-Channel-Zugriff dieses
|
||||
/// Features. [UdpTransport] programmiert ausschliesslich gegen dieses
|
||||
/// Interface, damit es in Tests durch eine Fake-Implementierung ersetzt
|
||||
/// werden kann, ohne einen echten Android-Kanal zu brauchen.
|
||||
abstract class MlrsNetworkController {
|
||||
/// Ergebnisse zur zuletzt per [requestNetwork] gestellten Anfrage -
|
||||
/// broadcast, damit sowohl [UdpTransport.connect] (wartet auf das erste
|
||||
/// Ereignis) als auch ein laufender Verbindungsueberwacher (reagiert auf
|
||||
/// ein spaeteres [MlrsNetworkLost]) gleichzeitig zuhoeren koennen.
|
||||
Stream<MlrsNetworkEvent> get events;
|
||||
|
||||
/// Loest `ConnectivityManager.requestNetwork()` mit einem
|
||||
/// `WifiNetworkSpecifier` aus, dessen SSID-Muster auf [ssidPrefix] passt.
|
||||
/// Das Ergebnis kommt asynchron ueber [events] (available/unavailable).
|
||||
Future<void> requestNetwork(String ssidPrefix);
|
||||
|
||||
/// Meldet den Callback ab und geht damit von der aktuellen Anfrage
|
||||
/// zurueck - keine weiteren [events] danach, bis erneut angefragt wird.
|
||||
Future<void> releaseNetwork();
|
||||
|
||||
/// Bindet den App-Prozess kurzzeitig an das zuletzt verfuegbare Netz
|
||||
/// (Doku Abschnitt 4: "Bindungsfenster"). Liefert false, wenn aktuell kein
|
||||
/// Netz verfuegbar ist.
|
||||
Future<bool> bindProcessToNetwork();
|
||||
|
||||
/// Loest die Prozessbindung wieder - muss unmittelbar nach dem Erzeugen
|
||||
/// des UDP-Sockets aufgerufen werden, sonst laufen auch Karten/Wetter
|
||||
/// ueber das mLRS-Netz statt der Standardroute.
|
||||
Future<void> unbindProcessFromNetwork();
|
||||
|
||||
/// Ergebnis von
|
||||
/// `WifiManager.isStaConcurrencyForLocalOnlyConnectionsSupported()` (Doku
|
||||
/// Abschnitt 4/8) - null falls nicht ermittelbar (API < 30). Nur zur
|
||||
/// Anzeige/zum Verbindungsprotokoll, nicht handlungsrelevant.
|
||||
Future<bool?> isStaConcurrencySupported();
|
||||
|
||||
void dispose();
|
||||
}
|
||||
|
||||
/// Echte Implementierung ueber MethodChannel/EventChannel gegen
|
||||
/// `MlrsNetworkPlugin.kt` (android/app/.../MlrsNetworkPlugin.kt).
|
||||
class MethodChannelMlrsNetworkController implements MlrsNetworkController {
|
||||
MethodChannelMlrsNetworkController()
|
||||
: _methodChannel = const MethodChannel('com.dmc.dmc_app/mlrs_network'),
|
||||
_eventChannel =
|
||||
const EventChannel('com.dmc.dmc_app/mlrs_network_events');
|
||||
|
||||
final MethodChannel _methodChannel;
|
||||
final EventChannel _eventChannel;
|
||||
|
||||
@override
|
||||
late final Stream<MlrsNetworkEvent> events =
|
||||
_eventChannel.receiveBroadcastStream().map(_toEvent);
|
||||
|
||||
MlrsNetworkEvent _toEvent(dynamic raw) {
|
||||
final map = Map<Object?, Object?>.from(raw as Map);
|
||||
return switch (map['type']) {
|
||||
'available' => MlrsNetworkAvailable(
|
||||
gatewayAddress: map['gatewayAddress'] as String?,
|
||||
),
|
||||
'lost' => const MlrsNetworkLost(),
|
||||
_ => const MlrsNetworkUnavailable(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> requestNetwork(String ssidPrefix) => _methodChannel
|
||||
.invokeMethod('requestNetwork', {'ssidPrefix': ssidPrefix});
|
||||
|
||||
@override
|
||||
Future<void> releaseNetwork() => _methodChannel.invokeMethod('releaseNetwork');
|
||||
|
||||
@override
|
||||
Future<bool> bindProcessToNetwork() async =>
|
||||
(await _methodChannel.invokeMethod<bool>('bindProcessToNetwork')) ??
|
||||
false;
|
||||
|
||||
@override
|
||||
Future<void> unbindProcessFromNetwork() =>
|
||||
_methodChannel.invokeMethod('unbindProcessFromNetwork');
|
||||
|
||||
@override
|
||||
Future<bool?> isStaConcurrencySupported() =>
|
||||
_methodChannel.invokeMethod<bool>('isStaConcurrencySupported');
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Keine eigenen Ressourcen - Abonnenten von [events] verwalten ihre
|
||||
// StreamSubscription selbst (siehe UdpTransport.dispose).
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,57 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../link_transport.dart';
|
||||
import 'mlrs_network_controller.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).
|
||||
/// UDP-Transport zur mLRS-WLAN-Bruecke (Doku Kommunikationsschicht v2,
|
||||
/// Abschnitt 3/4). Die Verbindung zum WLAN wird ausschliesslich von der App
|
||||
/// selbst aufgebaut (`WifiNetworkSpecifier` ueber [MlrsNetworkController]),
|
||||
/// nicht ueber die Systemeinstellungen - sonst wuerde das mLRS-Netz zur
|
||||
/// Standardroute des Geraets und Kartenkacheln/Wetter fielen aus.
|
||||
///
|
||||
/// 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.
|
||||
/// Ablauf von [connect]:
|
||||
/// 1. Netz mit passendem SSID-Praefix anfordern, auf `available`/
|
||||
/// `unavailable` warten (Doku Abschnitt 4).
|
||||
/// 2. Zieladresse vorbelegen: konfigurierter Host, sonst die vom Netz
|
||||
/// gemeldete Gateway-Adresse, sonst ungelernt (Doku Abschnitt 3).
|
||||
/// 3. Kurzes Bindungsfenster: Prozess an das Netz binden, UDP-Socket
|
||||
/// erzeugen, Prozessbindung sofort wieder loesen (Doku Abschnitt 4) - der
|
||||
/// Socket selbst bleibt danach am WLAN gebunden.
|
||||
/// 4. Weiterhin auf das Netz hoeren: geht es verloren (`lost`), ist der
|
||||
/// Socket ungueltig (Doku Abschnitt 8) und die Verbindung wird mit
|
||||
/// [LinkErrorReason.networkLost] beendet. Automatisches Wiederverbinden
|
||||
/// mit ansteigender Wartezeit (Doku Abschnitt 8) ist noch nicht
|
||||
/// umgesetzt - der Nutzer stoesst es aktuell ueber "Connect" erneut an.
|
||||
class UdpTransport implements LinkTransport {
|
||||
UdpTransport({
|
||||
required Future<String?> Function() getHost,
|
||||
required Future<int> Function() getPort,
|
||||
required Future<String> Function() getSsidPrefix,
|
||||
MlrsNetworkController? networkController,
|
||||
}) : _getHost = getHost,
|
||||
_getPort = getPort;
|
||||
_getPort = getPort,
|
||||
_getSsidPrefix = getSsidPrefix,
|
||||
_networkController =
|
||||
networkController ?? MethodChannelMlrsNetworkController();
|
||||
|
||||
final Future<String?> Function() _getHost;
|
||||
final Future<int> Function() _getPort;
|
||||
final Future<String> Function() _getSsidPrefix;
|
||||
final MlrsNetworkController _networkController;
|
||||
|
||||
final _incomingController = StreamController<Uint8List>.broadcast();
|
||||
final _stateController = StreamController<LinkState>.broadcast();
|
||||
|
||||
RawDatagramSocket? _socket;
|
||||
StreamSubscription<RawSocketEvent>? _socketSub;
|
||||
StreamSubscription<MlrsNetworkEvent>? _networkLossSub;
|
||||
InternetAddress? _peerAddress;
|
||||
int? _peerPort;
|
||||
int? _boundPort;
|
||||
bool? _staConcurrencySupported;
|
||||
|
||||
LinkState _state = LinkState.disconnected;
|
||||
LinkErrorReason? _lastErrorReason;
|
||||
@@ -50,31 +70,63 @@ class UdpTransport implements LinkTransport {
|
||||
String? get lastErrorMessage => _lastErrorMessage;
|
||||
|
||||
/// Tatsaechlich gebundener lokaler Port, erst nach [connect] bekannt -
|
||||
/// weicht von der Vorgabe ab, wenn diese belegt war (Doku 3/8).
|
||||
/// weicht von der Vorgabe ab, wenn diese belegt war (Doku 3/9).
|
||||
int? get boundPort => _boundPort;
|
||||
|
||||
/// Gegenstelle, sobald aus dem ersten eingehenden Paket gelernt bzw. aus
|
||||
/// der Host-Vorgabe vorbelegt (Doku 3).
|
||||
/// Host-Vorgabe/Gateway-Adresse vorbelegt (Doku Abschnitt 3).
|
||||
String? get peerDescription =>
|
||||
_peerAddress == null ? null : '${_peerAddress!.address}:$_peerPort';
|
||||
|
||||
/// Ergebnis von `isStaConcurrencyForLocalOnlyConnectionsSupported()`, ab
|
||||
/// dem ersten erfolgreichen [connect] verfuegbar - null vorher oder falls
|
||||
/// nicht ermittelbar (API < 30, Doku Abschnitt 4/8).
|
||||
bool? get staConcurrencySupported => _staConcurrencySupported;
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
final ssidPrefix = await _getSsidPrefix();
|
||||
final firstEvent = Completer<MlrsNetworkEvent>();
|
||||
final requestSub = _networkController.events.listen((event) {
|
||||
if (!firstEvent.isCompleted) firstEvent.complete(event);
|
||||
});
|
||||
|
||||
try {
|
||||
await _networkController.requestNetwork(ssidPrefix);
|
||||
final event = await firstEvent.future;
|
||||
await requestSub.cancel();
|
||||
|
||||
if (event is! MlrsNetworkAvailable) {
|
||||
_fail(
|
||||
LinkErrorReason.networkUnavailable,
|
||||
'No WiFi network matching SSID prefix "$ssidPrefix" found, or the '
|
||||
'system connection dialog was dismissed.',
|
||||
);
|
||||
}
|
||||
final available = event;
|
||||
|
||||
// Ab hier auf Netzverlust reagieren - auch waehrend des kurzen
|
||||
// Bindungsfensters unten, nicht erst danach (Doku Abschnitt 8).
|
||||
_networkLossSub = _networkController.events.listen(_onNetworkEvent);
|
||||
|
||||
final configuredHost = await _getHost();
|
||||
final configuredPort = await _getPort();
|
||||
final prefillHost = (configuredHost != null && configuredHost.isNotEmpty)
|
||||
? configuredHost
|
||||
: available.gatewayAddress;
|
||||
if (prefillHost != null) {
|
||||
final parsed = InternetAddress.tryParse(prefillHost);
|
||||
if (parsed != null) {
|
||||
_peerAddress = parsed;
|
||||
_peerPort = configuredPort;
|
||||
}
|
||||
}
|
||||
|
||||
await _networkController.bindProcessToNetwork();
|
||||
RawDatagramSocket socket;
|
||||
try {
|
||||
// reuseAddress: false, damit ein tatsaechlich belegter Port hier
|
||||
@@ -86,13 +138,15 @@ class UdpTransport implements LinkTransport {
|
||||
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.
|
||||
// Port belegt (Doku 3/9: "ist 14550 belegt, ausweichen, aber
|
||||
// protokollieren") - Ausweichen ist kein Grund, abzubrechen.
|
||||
socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0);
|
||||
_lastErrorMessage =
|
||||
'Port $configuredPort in use, fell back to ${socket.port}.';
|
||||
} finally {
|
||||
await _networkController.unbindProcessFromNetwork();
|
||||
}
|
||||
|
||||
_socket = socket;
|
||||
_boundPort = socket.port;
|
||||
_socketSub = socket.listen(
|
||||
@@ -100,12 +154,47 @@ class UdpTransport implements LinkTransport {
|
||||
onError: (Object e) =>
|
||||
_setError(LinkErrorReason.connectionFailed, e.toString()),
|
||||
);
|
||||
|
||||
_staConcurrencySupported =
|
||||
await _networkController.isStaConcurrencySupported();
|
||||
|
||||
_setState(LinkState.connected);
|
||||
} on SocketException catch (e) {
|
||||
await requestSub.cancel();
|
||||
await _networkLossSub?.cancel();
|
||||
_networkLossSub = null;
|
||||
await _networkController.releaseNetwork();
|
||||
_fail(LinkErrorReason.connectionFailed, e.message);
|
||||
} on MissingPluginException {
|
||||
await requestSub.cancel();
|
||||
_fail(
|
||||
LinkErrorReason.unknown,
|
||||
'mLRS network channel unavailable on this platform.',
|
||||
);
|
||||
} on PlatformException catch (e) {
|
||||
await requestSub.cancel();
|
||||
_fail(
|
||||
LinkErrorReason.unknown,
|
||||
e.message ?? 'Platform error requesting the mLRS network.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onNetworkEvent(MlrsNetworkEvent event) {
|
||||
if (event is! MlrsNetworkLost) return;
|
||||
_networkLossSub?.cancel();
|
||||
_networkLossSub = null;
|
||||
_socketSub?.cancel();
|
||||
_socketSub = null;
|
||||
_socket?.close();
|
||||
_socket = null;
|
||||
_boundPort = null;
|
||||
_setError(
|
||||
LinkErrorReason.networkLost,
|
||||
'The mLRS WiFi network was lost.',
|
||||
);
|
||||
}
|
||||
|
||||
void _onSocketEvent(RawSocketEvent event) {
|
||||
if (event != RawSocketEvent.read) return;
|
||||
final datagram = _socket?.receive();
|
||||
@@ -117,11 +206,14 @@ class UdpTransport implements LinkTransport {
|
||||
|
||||
@override
|
||||
Future<void> disconnect() async {
|
||||
await _networkLossSub?.cancel();
|
||||
_networkLossSub = null;
|
||||
await _socketSub?.cancel();
|
||||
_socketSub = null;
|
||||
_socket?.close();
|
||||
_socket = null;
|
||||
_boundPort = null;
|
||||
await _networkController.releaseNetwork();
|
||||
_setState(LinkState.disconnected);
|
||||
}
|
||||
|
||||
@@ -141,9 +233,12 @@ class UdpTransport implements LinkTransport {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_networkLossSub?.cancel();
|
||||
_socketSub?.cancel();
|
||||
_socket?.close();
|
||||
_socket = null;
|
||||
unawaited(_networkController.releaseNetwork());
|
||||
_networkController.dispose();
|
||||
_incomingController.close();
|
||||
_stateController.close();
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ final wifiTransportProvider = Provider<UdpTransport>((ref) {
|
||||
final transport = UdpTransport(
|
||||
getHost: settingsRepo.loadConnectionWifiHost,
|
||||
getPort: settingsRepo.loadConnectionWifiPort,
|
||||
getSsidPrefix: settingsRepo.loadConnectionWifiSsidPrefix,
|
||||
);
|
||||
ref.onDispose(transport.dispose);
|
||||
return transport;
|
||||
@@ -96,6 +97,7 @@ class WifiConnectionLogNotifier
|
||||
StreamSubscription<LinkState>? _stateSub;
|
||||
StreamSubscription<void>? _dataSub;
|
||||
int _packetCount = 0;
|
||||
String? _lastLoggedPeer;
|
||||
|
||||
int get packetCount => _packetCount;
|
||||
|
||||
@@ -105,8 +107,9 @@ class WifiConnectionLogNotifier
|
||||
_stateSub?.cancel();
|
||||
_dataSub?.cancel();
|
||||
_packetCount = 0;
|
||||
_lastLoggedPeer = null;
|
||||
_stateSub = transport.state.listen(_onStateChanged);
|
||||
_dataSub = transport.incoming.listen((_) => _packetCount++);
|
||||
_dataSub = transport.incoming.listen((_) => _onPacket(transport));
|
||||
ref.onDispose(() {
|
||||
_stateSub?.cancel();
|
||||
_dataSub?.cancel();
|
||||
@@ -114,18 +117,38 @@ class WifiConnectionLogNotifier
|
||||
return const [];
|
||||
}
|
||||
|
||||
void _onPacket(UdpTransport transport) {
|
||||
_packetCount++;
|
||||
// Gegenstelle wird aus dem ersten eingehenden Paket gelernt (Doku
|
||||
// Kommunikationsschicht v2, Abschnitt 3/9) - nur beim ersten Mal bzw.
|
||||
// bei Aenderung protokollieren, nicht pro Paket.
|
||||
final peer = transport.peerDescription;
|
||||
if (peer != null && peer != _lastLoggedPeer) {
|
||||
_lastLoggedPeer = peer;
|
||||
_append('Peer learned: $peer');
|
||||
}
|
||||
}
|
||||
|
||||
void _onStateChanged(LinkState linkState) {
|
||||
final transport = ref.read(wifiTransportProvider);
|
||||
final message = switch (linkState) {
|
||||
LinkState.disconnected => 'Disconnected',
|
||||
LinkState.connecting => 'Connecting...',
|
||||
LinkState.connected => 'Listening on port ${transport.boundPort}',
|
||||
LinkState.connecting => 'Requesting mLRS network...',
|
||||
LinkState.connected =>
|
||||
'Network available, listening on port ${transport.boundPort}'
|
||||
' (STA concurrency: ${_formatStaConcurrency(transport.staConcurrencySupported)})',
|
||||
LinkState.error => 'Error: '
|
||||
'${transport.lastErrorMessage ?? transport.lastErrorReason?.name ?? 'unknown'}',
|
||||
};
|
||||
_append(message);
|
||||
}
|
||||
|
||||
String _formatStaConcurrency(bool? supported) => switch (supported) {
|
||||
true => 'supported',
|
||||
false => 'not supported',
|
||||
null => 'unknown',
|
||||
};
|
||||
|
||||
void _append(String message) {
|
||||
final entry = WifiConnectionLogEntry(DateTime.now(), message);
|
||||
final next = [...state, entry];
|
||||
|
||||
@@ -121,6 +121,7 @@ class _WifiPanel extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
class _WifiPanelState extends ConsumerState<_WifiPanel> {
|
||||
late final TextEditingController _ssidPrefixController;
|
||||
late final TextEditingController _hostController;
|
||||
late final TextEditingController _portController;
|
||||
bool _controllersSeeded = false;
|
||||
@@ -128,12 +129,14 @@ class _WifiPanelState extends ConsumerState<_WifiPanel> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_ssidPrefixController = TextEditingController();
|
||||
_hostController = TextEditingController();
|
||||
_portController = TextEditingController();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ssidPrefixController.dispose();
|
||||
_hostController.dispose();
|
||||
_portController.dispose();
|
||||
super.dispose();
|
||||
@@ -153,6 +156,7 @@ class _WifiPanelState extends ConsumerState<_WifiPanel> {
|
||||
// ueberschrieben.
|
||||
if (!_controllersSeeded && connectionSettings != null) {
|
||||
_controllersSeeded = true;
|
||||
_ssidPrefixController.text = connectionSettings.connectionWifiSsidPrefix;
|
||||
_hostController.text = connectionSettings.connectionWifiHost ?? '';
|
||||
_portController.text = connectionSettings.connectionWifiPort.toString();
|
||||
}
|
||||
@@ -171,9 +175,17 @@ class _WifiPanelState extends ConsumerState<_WifiPanel> {
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
'Opens a local UDP socket and listens for the mLRS bridge '
|
||||
'(port 14550 by default). The peer address is learned from the '
|
||||
'first incoming packet.',
|
||||
'The app connects to the mLRS bridge WiFi itself (a system dialog '
|
||||
'will ask you to confirm) so your normal mobile data connection '
|
||||
'keeps working for maps and weather. The peer address is learned '
|
||||
'from the first incoming packet.',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
'Do not manually connect to or save the mLRS network in your '
|
||||
'system WiFi settings - that would make it the default route and '
|
||||
'break maps/weather.',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
if (errorReason != null) ...[
|
||||
@@ -187,15 +199,15 @@ class _WifiPanelState extends ConsumerState<_WifiPanel> {
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: TextField(
|
||||
controller: _hostController,
|
||||
controller: _ssidPrefixController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Host (optional)',
|
||||
hintText: 'auto (learned from traffic)',
|
||||
labelText: 'SSID prefix',
|
||||
isDense: true,
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onSubmitted: (value) => _saveHost(value),
|
||||
onEditingComplete: () => _saveHost(_hostController.text),
|
||||
onSubmitted: (value) => _saveSsidPrefix(value),
|
||||
onEditingComplete: () =>
|
||||
_saveSsidPrefix(_ssidPrefixController.text),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
@@ -215,6 +227,18 @@ class _WifiPanelState extends ConsumerState<_WifiPanel> {
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: _hostController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Host (optional)',
|
||||
hintText: 'auto (network gateway, then learned from traffic)',
|
||||
isDense: true,
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onSubmitted: (value) => _saveHost(value),
|
||||
onEditingComplete: () => _saveHost(_hostController.text),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
@@ -258,6 +282,15 @@ class _WifiPanelState extends ConsumerState<_WifiPanel> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _saveSsidPrefix(String value) async {
|
||||
final trimmed = value.trim();
|
||||
if (trimmed.isEmpty) return;
|
||||
await ref
|
||||
.read(appSettingsRepositoryProvider)
|
||||
.setConnectionWifiSsidPrefix(trimmed);
|
||||
ref.invalidate(connectionSettingsProvider);
|
||||
}
|
||||
|
||||
Future<void> _saveHost(String value) async {
|
||||
final trimmed = value.trim();
|
||||
await ref
|
||||
@@ -293,16 +326,25 @@ class _WifiErrorHint extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final text = switch (reason) {
|
||||
LinkErrorReason.networkUnavailable =>
|
||||
'No matching WiFi network found, or the system connection dialog '
|
||||
'was dismissed. Make sure the mLRS bridge is powered on and not '
|
||||
'saved/connected in system WiFi settings, then tap "Connect" '
|
||||
'again.',
|
||||
LinkErrorReason.networkLost =>
|
||||
'The mLRS WiFi network was lost (out of range or bridge powered '
|
||||
'off). Tap "Connect" to reconnect.',
|
||||
LinkErrorReason.connectionFailed => 'Connection to the bridge failed.',
|
||||
_ => 'Connection error: ${reason.name}.',
|
||||
};
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: DmcColors.warnRed.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'Connection error: ${reason.name}.',
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
child: Text(text, style: const TextStyle(fontSize: 12)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -543,7 +585,13 @@ class _ErrorHint extends StatelessWidget {
|
||||
'Connection to the device failed.',
|
||||
null,
|
||||
),
|
||||
LinkErrorReason.unknown => ('Unknown connection error.', null),
|
||||
// networkUnavailable/networkLost betreffen nur den WLAN-Transport
|
||||
// (siehe _WifiErrorHint) - hier nur der Vollstaendigkeit halber, damit
|
||||
// der gemeinsame LinkErrorReason-Enum exhaustiv bleibt.
|
||||
LinkErrorReason.networkUnavailable ||
|
||||
LinkErrorReason.networkLost ||
|
||||
LinkErrorReason.unknown =>
|
||||
('Unknown connection error.', null),
|
||||
};
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
|
||||
Reference in New Issue
Block a user