Umbau Thread-Config an/vom leader in einem paket
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 12s

This commit is contained in:
2026-01-13 10:17:53 +01:00
parent 38396738a6
commit a041d5a49c
10 changed files with 559 additions and 334 deletions

View File

@@ -2,16 +2,17 @@ class LasertagUUIDs {
static const String base = "03afe2cf-6c64-4a22-9289-c3ae820c";
static const String provService = "${base}1000";
static const String provNameChar = "${base}1001";
static const String provPanIdChar = "${base}1002";
static const String provChanChar = "${base}1003";
static const String provExtPanIdChar = "${base}1004";
static const String provNetKeyChar = "${base}1005";
static const String provNetNameChar = "${base}1006";
// static const String provPanIdChar = "${base}1002";
// static const String provChanChar = "${base}1003";
// static const String provExtPanIdChar = "${base}1004";
// static const String provNetKeyChar = "${base}1005";
// static const String provNetNameChar = "${base}1006";
static const String provTypeChar = "${base}1008";
static const String provConfigChar = "${base}100c";
// Gerätetypen aus deiner ble_mgmt.h
static const int typeLeader = 0x01;
static const int typeWeapon = 0x02;
static const int typeVest = 0x03;
static const int typeBeacon = 0x04;
}
}

View File

@@ -0,0 +1,87 @@
import 'dart:convert';
import 'dart:typed_data';
class DeviceConfig {
final int systemState;
final BigInt gameId;
final int panId;
final int channel;
final String extPanId;
final String networkKey;
final String networkName;
final String nodeName;
DeviceConfig({
required this.systemState,
required this.gameId,
required this.panId,
required this.channel,
required this.extPanId,
required this.networkKey,
required this.networkName,
required this.nodeName,
});
factory DeviceConfig.fromBytes(List<int> bytes) {
final data = ByteData.sublistView(Uint8List.fromList(bytes));
// Manuelles Decoding von 64-Bit (2x 32-Bit), da getUint64 nicht Standard ist
final low = data.getUint32(1, Endian.little);
final high = data.getUint32(5, Endian.little);
final fullGameId = (BigInt.from(high) << 32) | BigInt.from(low);
return DeviceConfig(
systemState: data.getUint8(0),
gameId: fullGameId,
panId: data.getUint16(9, Endian.little),
channel: data.getUint8(11),
extPanId: _bytesToHex(bytes.sublist(12, 20)),
networkKey: _bytesToHex(bytes.sublist(20, 36)),
networkName: _decodeString(bytes.sublist(36, 53)),
nodeName: _decodeString(bytes.sublist(53, 86)),
);
}
Uint8List toBytes() {
final bytes = Uint8List(86);
final data = ByteData.view(bytes.buffer);
data.setUint8(0, systemState);
// Encoding von 64-Bit BigInt in zwei 32-Bit Segmente
data.setUint32(1, (gameId & BigInt.from(0xFFFFFFFF)).toInt(), Endian.little);
data.setUint32(5, (gameId >> 32).toInt(), Endian.little);
data.setUint16(9, panId, Endian.little);
data.setUint8(11, channel);
_writeHexToBuffer(bytes, 12, extPanId);
_writeHexToBuffer(bytes, 20, networkKey);
_writeStringToBuffer(bytes, 36, 17, networkName);
_writeStringToBuffer(bytes, 53, 33, nodeName);
return bytes;
}
static String _decodeString(List<int> bytes) {
int nullIdx = bytes.indexOf(0);
return utf8.decode(nullIdx == -1 ? bytes : bytes.sublist(0, nullIdx)).trim();
}
static void _writeStringToBuffer(Uint8List buffer, int offset, int maxLen, String val) {
final encoded = utf8.encode(val);
for (int i = 0; i < maxLen - 1 && i < encoded.length; i++) {
buffer[offset + i] = encoded[i];
}
// Buffer ist bereits mit 0 initialisiert, Terminator steht also am Ende
}
static String _bytesToHex(List<int> bytes) =>
bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join().toUpperCase();
static void _writeHexToBuffer(Uint8List buffer, int offset, String hex) {
for (int i = 0; i < hex.length; i += 2) {
buffer[offset + (i ~/ 2)] = int.parse(hex.substring(i, i + 2), radix: 16);
}
}
}

View File

@@ -1,5 +1,6 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
@@ -7,6 +8,7 @@ import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import '../constants.dart';
import '../models/device_model.dart';
import '../models/device_config_model.dart'; // WICHTIG: Korrekter Import
class DeviceProvider extends ChangeNotifier {
final List<LasertagDevice> _discoveredLeaders = [];
@@ -16,172 +18,129 @@ class DeviceProvider extends ChangeNotifier {
List<LasertagDevice> get leaders => _discoveredLeaders;
List<LasertagDevice> get peripherals => _discoveredPeripherals;
// Öffentlicher API-Entry: Scan starten
// ... startScan bleibt gleich ...
void startScan() async {
_discoveredLeaders.clear();
_discoveredPeripherals.clear();
notifyListeners();
await FlutterBluePlus.stopScan(); // Scan sauber stoppen vor Neustart
await FlutterBluePlus.adapterState
.where((s) => s == BluetoothAdapterState.on)
.first;
await FlutterBluePlus.stopScan();
await FlutterBluePlus.adapterState.where((s) => s == BluetoothAdapterState.on).first;
_scanSubscription?.cancel();
_scanSubscription = FlutterBluePlus.scanResults.listen((results) {
for (ScanResult r in results) {
bool hasService = r.advertisementData.serviceUuids.contains(
Guid(LasertagUUIDs.provService),
);
bool hasService = r.advertisementData.serviceUuids.contains(Guid(LasertagUUIDs.provService));
if (!hasService) continue;
final mfgData = r.advertisementData.manufacturerData[65535];
if (mfgData != null && mfgData.isNotEmpty) {
final device = LasertagDevice(
id: r.device.remoteId.toString(),
name: r.device.platformName.isEmpty
? "Unbekannt"
: r.device.platformName,
name: r.device.platformName.isEmpty ? "Unbekannt" : r.device.platformName,
type: mfgData[0],
btDevice: r.device,
isConnected: false,
isNameVerified: false, // Initial immer unversichert (Kursiv)
isNameVerified: false,
);
_addDeviceToLists(device);
}
}
});
try {
await FlutterBluePlus.startScan(timeout: const Duration(seconds: 15));
} catch (e) {
debugPrint("Scan-Fehler: $e");
}
try { await FlutterBluePlus.startScan(timeout: const Duration(seconds: 15)); } catch (e) { debugPrint("Scan-Fehler: $e"); }
}
// Öffentlicher API-Entry: Namen von der Hardware lesen
Future<String> readDeviceNameFromHardware(LasertagDevice ltDevice) async {
try {
if (!ltDevice.btDevice.isConnected) {
await ltDevice.btDevice.connect(
license: License.free,
timeout: const Duration(seconds: 5),
);
await ltDevice.btDevice.connect(license: License.free, timeout: const Duration(seconds: 5));
}
List<BluetoothService> srv = await ltDevice.btDevice.discoverServices();
var c = srv.firstWhere((s) => s.uuid == Guid(LasertagUUIDs.provService))
.characteristics.firstWhere((c) => c.uuid == Guid(LasertagUUIDs.provNameChar));
List<int> value = await c.read();
String hardwareName = utf8.decode(value);
updateDeviceName(ltDevice.id, hardwareName, verified: true);
return hardwareName;
} finally { await ltDevice.btDevice.disconnect(); }
}
Future<void> updateDeviceNameOnHardware(LasertagDevice ltDevice, String newName) async {
try {
await ltDevice.btDevice.connect(license: License.free, timeout: const Duration(seconds: 10));
List<BluetoothService> srv = await ltDevice.btDevice.discoverServices();
var c = srv.firstWhere((s) => s.uuid == Guid(LasertagUUIDs.provService))
.characteristics.firstWhere((c) => c.uuid == Guid(LasertagUUIDs.provNameChar));
await c.write(utf8.encode(newName));
updateDeviceName(ltDevice.id, newName, verified: true);
} finally { await ltDevice.btDevice.disconnect(); }
}
// Die neuen optimierten Methoden
Future<DeviceConfig> readDeviceConfig(LasertagDevice device) async {
try {
if (!device.btDevice.isConnected) {
await device.btDevice.connect(license: License.free, timeout: const Duration(seconds: 10));
}
List<BluetoothService> srv = await device.btDevice.discoverServices();
var c = srv.firstWhere((s) => s.uuid == Guid(LasertagUUIDs.provService))
.characteristics.firstWhere((c) => c.uuid == Guid(LasertagUUIDs.provConfigChar));
final bytes = await c.read();
return DeviceConfig.fromBytes(bytes);
} finally { await device.btDevice.disconnect(); }
}
Future<void> provisionDevice(LasertagDevice device, DeviceConfig config) async {
try {
if (!device.btDevice.isConnected) {
await device.btDevice.connect(license: License.free, timeout: const Duration(seconds: 10));
}
List<BluetoothService> services = await ltDevice.btDevice
.discoverServices();
var service = services.firstWhere(
(s) => s.uuid == Guid(LasertagUUIDs.provService),
);
var characteristic = service.characteristics.firstWhere(
(c) => c.uuid == Guid(LasertagUUIDs.provNameChar),
);
List<int> value = await characteristic.read();
String hardwareName = utf8.decode(value);
updateDeviceName(ltDevice.id, hardwareName, verified: true);
await ltDevice.btDevice.disconnect();
return hardwareName;
} catch (e) {
debugPrint("Fehler beim Lesen der Hardware: $e");
rethrow;
}
// MTU nur auf Android anfordern, auf macOS/iOS macht das das OS automatisch
if (Platform.isAndroid) {
try {
await device.btDevice.requestMtu(250);
// await Future.delayed(const Duration(milliseconds: 200));
} catch (e) {
debugPrint("MTU Request failed (Android only): $e");
}
}
List<BluetoothService> srv = await device.btDevice.discoverServices();
var c = srv.firstWhere((s) => s.uuid == Guid(LasertagUUIDs.provService))
.characteristics.firstWhere((c) => c.uuid == Guid(LasertagUUIDs.provConfigChar));
await c.write(config.toBytes(), allowLongWrite: true);
updateDeviceName(device.id, config.nodeName, verified: true);
} finally { await device.btDevice.disconnect(); }
}
// Öffentlicher API-Entry: Namen auf Hardware schreiben
Future<void> updateDeviceNameOnHardware(
LasertagDevice ltDevice,
String newName,
) async {
try {
await ltDevice.btDevice.connect(
license: License.free,
autoConnect: false,
timeout: const Duration(seconds: 10),
);
await Future.delayed(const Duration(milliseconds: 500));
List<BluetoothService> services = await ltDevice.btDevice
.discoverServices();
var service = services.firstWhere(
(s) => s.uuid == Guid(LasertagUUIDs.provService),
);
var characteristic = service.characteristics.firstWhere(
(c) => c.uuid == Guid(LasertagUUIDs.provNameChar),
);
await characteristic.write(utf8.encode(newName));
updateDeviceName(ltDevice.id, newName, verified: true);
await ltDevice.btDevice.disconnect();
} catch (e) {
debugPrint("Hardware-Fehler: $e");
rethrow;
}
}
// Öffentlicher API-Entry: Lokale Liste aktualisieren
// ... Hilfsmethoden updateDeviceName, _addDeviceToLists, _verifyNameInBackground (mit license!), _sortList etc. bleiben ...
void updateDeviceName(String id, String newName, {bool verified = false}) {
_updateDeviceInLists(
id,
(old) => old.copyWith(name: newName, isNameVerified: verified),
);
_updateDeviceInLists(id, (old) => old.copyWith(name: newName, isNameVerified: verified));
}
void _addDeviceToLists(LasertagDevice device) {
List<LasertagDevice> list = device.isLeader
? _discoveredLeaders
: _discoveredPeripherals;
List<LasertagDevice> list = device.isLeader ? _discoveredLeaders : _discoveredPeripherals;
if (!list.any((d) => d.id == device.id)) {
list.add(device);
_sortList(list);
notifyListeners();
_verifyNameInBackground(device); // Hintergrund-Check anstoßen
_verifyNameInBackground(device);
}
}
Future<void> _verifyNameInBackground(LasertagDevice device) async {
if (device.isNameVerified) return;
try {
await device.btDevice.connect(
license: License.free,
timeout: const Duration(seconds: 5),
);
List<BluetoothService> services = await device.btDevice
.discoverServices();
var service = services.firstWhere(
(s) => s.uuid == Guid(LasertagUUIDs.provService),
);
var characteristic = service.characteristics.firstWhere(
(c) => c.uuid == Guid(LasertagUUIDs.provNameChar),
);
await device.btDevice.connect(license: License.free, timeout: const Duration(seconds: 5));
List<BluetoothService> services = await device.btDevice.discoverServices();
var characteristic = services.firstWhere((s) => s.uuid == Guid(LasertagUUIDs.provService))
.characteristics.firstWhere((c) => c.uuid == Guid(LasertagUUIDs.provNameChar));
List<int> value = await characteristic.read();
String realName = utf8.decode(value);
updateDeviceName(device.id, realName, verified: true);
await device.btDevice.disconnect();
updateDeviceName(device.id, utf8.decode(value), verified: true);
} catch (e) {
debugPrint("Background Sync fehlgeschlagen für ${device.id}: $e");
}
debugPrint("Background Sync fehlgeschlagen für ${device.id}");
} finally { await device.btDevice.disconnect(); }
}
void _updateDeviceInLists(
String id,
LasertagDevice Function(LasertagDevice) updateFn,
) {
void _updateDeviceInLists(String id, LasertagDevice Function(LasertagDevice) updateFn) {
for (var list in [_discoveredLeaders, _discoveredPeripherals]) {
int index = list.indexWhere((d) => d.id == id);
if (index != -1) {
@@ -193,130 +152,6 @@ class DeviceProvider extends ChangeNotifier {
}
}
Future<void> provisionLeader(
LasertagDevice device,
Map<String, dynamic> config,
) async {
try {
if (!device.btDevice.isConnected) {
await device.btDevice.connect(
license: License.free,
timeout: const Duration(seconds: 10),
);
}
List<BluetoothService> services = await device.btDevice
.discoverServices();
var s = services.firstWhere(
(s) => s.uuid == Guid(LasertagUUIDs.provService),
);
// Hilfsfunktion zum Finden einer Charakteristik
BluetoothCharacteristic findChar(String uuid) =>
s.characteristics.firstWhere((c) => c.uuid == Guid(uuid));
// 1. Namen schreiben
await findChar(
LasertagUUIDs.provNameChar,
).write(utf8.encode(config['name']));
await findChar(
LasertagUUIDs.provNetNameChar,
).write(utf8.encode(config['netName']));
// 2. Kanal (8-bit)
await findChar(LasertagUUIDs.provChanChar).write([config['chan']]);
// 3. PAN ID (16-bit little endian für Zephyr)
final panData = ByteData(2)..setUint16(0, config['panId'], Endian.little);
await findChar(
LasertagUUIDs.provPanIdChar,
).write(panData.buffer.asUint8List());
// 4. Hex-Werte (ExtPAN 8 Bytes, NetKey 16 Bytes)
await findChar(
LasertagUUIDs.provExtPanIdChar,
).write(_hexToBytes(config['extPan']));
await findChar(
LasertagUUIDs.provNetKeyChar,
).write(_hexToBytes(config['netKey']));
// Lokalen Namen in der App-Liste aktualisieren
updateDeviceName(device.id, config['name'], verified: true);
await device.btDevice.disconnect();
} catch (e) {
debugPrint("Provisionierungs-Fehler: $e");
rethrow;
}
}
// Hilfsmethode: Wandelt "DEADBEEF" in [0xDE, 0xAD, 0xBE, 0xEF] um
List<int> _hexToBytes(String hex) {
List<int> bytes = [];
for (int i = 0; i < hex.length; i += 2) {
bytes.add(int.parse(hex.substring(i, i + 2), radix: 16));
}
return bytes;
}
Future<Map<String, dynamic>> readFullLeaderConfig(
LasertagDevice ltDevice,
) async {
try {
if (!ltDevice.btDevice.isConnected) {
await ltDevice.btDevice.connect(
license: License.free,
timeout: const Duration(seconds: 10),
);
}
List<BluetoothService> services = await ltDevice.btDevice
.discoverServices();
var s = services.firstWhere(
(s) => s.uuid == Guid(LasertagUUIDs.provService),
);
BluetoothCharacteristic findChar(String uuid) =>
s.characteristics.firstWhere((c) => c.uuid == Guid(uuid));
// Alle Daten von der Hardware abfragen
final nameBytes = await findChar(LasertagUUIDs.provNameChar).read();
final netNameBytes = await findChar(LasertagUUIDs.provNetNameChar).read();
final chanBytes = await findChar(LasertagUUIDs.provChanChar).read();
final panBytes = await findChar(LasertagUUIDs.provPanIdChar).read();
final extPanBytes = await findChar(LasertagUUIDs.provExtPanIdChar).read();
final netKeyBytes = await findChar(LasertagUUIDs.provNetKeyChar).read();
// Bytes in passende Dart-Typen umwandeln
return {
'name': utf8.decode(nameBytes).trim(),
'netName': utf8.decode(netNameBytes).trim(),
'chan': chanBytes.isNotEmpty ? chanBytes[0] : 15,
'panId': panBytes.length >= 2
? ByteData.sublistView(
Uint8List.fromList(panBytes),
).getUint16(0, Endian.little)
: 0xABCD,
'extPan': _bytesToHex(extPanBytes),
'netKey': _bytesToHex(netKeyBytes),
};
} catch (e) {
debugPrint("Fehler beim Laden der Leader-Config: $e");
rethrow;
} finally {
// Verbindung nach dem Einlesen wieder trennen
await ltDevice.btDevice.disconnect();
}
}
// Hilfsmethode: Bytes in Hex-String umwandeln
String _bytesToHex(List<int> bytes) {
return bytes
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join()
.toUpperCase();
}
void _sortList(List<LasertagDevice> list) {
list.sort((a, b) {
int typeComp = a.type.compareTo(b.type);
@@ -330,4 +165,4 @@ class DeviceProvider extends ChangeNotifier {
_scanSubscription?.cancel();
super.dispose();
}
}
}

View File

@@ -86,9 +86,9 @@ class _DeviceSelectionScreenState extends State<DeviceSelectionScreen> {
device.name,
style: TextStyle(
fontWeight: FontWeight.bold,
// fontStyle: device.isNameVerified
// ? FontStyle.normal
// : FontStyle.italic, // Kursiv, wenn nicht verifiziert
fontStyle: device.isNameVerified
? FontStyle.normal
: FontStyle.italic, // Kursiv, wenn nicht verifiziert
color: device.isNameVerified
? Theme.of(context).colorScheme.onSurface
: Theme.of(

View File

@@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'dart:math';
import '../../models/device_model.dart';
import '../../models/device_config_model.dart';
import '../../providers/device_provider.dart';
class LeaderConfigScreen extends StatefulWidget {
@@ -26,48 +27,42 @@ class _LeaderConfigScreenState extends State<LeaderConfigScreen> {
bool _isLoading = true;
bool _isSaving = false;
// Speichert die aktuelle Konfiguration der Hardware (für RAM-Werte wie Game-ID)
late DeviceConfig _currentConfig;
@override
void initState() {
super.initState();
_loadCurrentConfig();
}
/// Lädt die kompakte Konfiguration über die neue 0x100C Charakteristik
Future<void> _loadCurrentConfig() async {
try {
final config = await context.read<DeviceProvider>().readFullLeaderConfig(widget.device);
final config = await context.read<DeviceProvider>().readDeviceConfig(widget.device);
_currentConfig = config;
if (mounted) {
setState(() {
_nameCtrl.text = config['name'];
_netNameCtrl.text = config['netName'];
_selectedChannel = config['chan'];
_panCtrl.text = "0x${config['panId'].toRadixString(16).toUpperCase()}";
_extPanCtrl.text = config['extPan'];
_netKeyCtrl.text = config['netKey'];
_nameCtrl.text = config.nodeName;
_netNameCtrl.text = config.networkName;
_selectedChannel = config.channel;
_panCtrl.text = "0x${config.panId.toRadixString(16).toUpperCase()}";
_extPanCtrl.text = config.extPanId;
_netKeyCtrl.text = config.networkKey;
_isLoading = false;
});
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Fehler: $e")));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Fehler beim Laden: $e"), backgroundColor: Colors.red),
);
Navigator.pop(context);
}
}
}
// Korrektur 1: Die fehlende Hilfsmethode hinzufügen
Widget _buildSectionTitle(String title) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Text(
title,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold
)
),
);
}
String? _validateHex(String? value, int requiredLength, String fieldName, {bool with0x = false}) {
if (value == null || value.isEmpty) return "$fieldName darf nicht leer sein";
@@ -86,7 +81,16 @@ class _LeaderConfigScreenState extends State<LeaderConfigScreen> {
return Scaffold(
appBar: AppBar(title: const Text("Leader-Konfiguration")),
body: _isLoading
? const Center(child: CircularProgressIndicator())
? const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text("Lade Konfiguration via BLE..."),
],
),
)
: Form(
key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
@@ -99,22 +103,24 @@ class _LeaderConfigScreenState extends State<LeaderConfigScreen> {
decoration: const InputDecoration(labelText: "Knoten-Name", helperText: "Max. 31 Zeichen"),
maxLength: 31,
validator: (v) => (v == null || v.isEmpty) ? "Name erforderlich" : null,
enabled: !_isSaving,
),
TextFormField(
controller: _netNameCtrl,
decoration: const InputDecoration(labelText: "Thread Netzwerk-Name", helperText: "1-16 Zeichen"),
maxLength: 16,
// Korrektur 2: .isEmpty statt .length < 1 nutzen
validator: (v) => (v == null || v.isEmpty || v.length > 16) ? "1-16 Zeichen erlaubt" : null,
enabled: !_isSaving,
),
DropdownButtonFormField<int>(
// Korrektur 3: initialValue statt value nutzen (neue Flutter Regel)
initialValue: _selectedChannel,
decoration: const InputDecoration(labelText: "Thread-Kanal (11-26)"),
items: List.generate(16, (i) => 11 + i).map((ch) => DropdownMenuItem(value: ch, child: Text("Kanal $ch"))).toList(),
onChanged: (v) => setState(() => _selectedChannel = v!),
items: List.generate(16, (i) => 11 + i)
.map((ch) => DropdownMenuItem(value: ch, child: Text("Kanal $ch")))
.toList(),
onChanged: _isSaving ? null : (v) => setState(() => _selectedChannel = v!),
),
const SizedBox(height: 24),
@@ -132,16 +138,32 @@ class _LeaderConfigScreenState extends State<LeaderConfigScreen> {
);
}
Widget _buildSectionTitle(String title) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Text(
title,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold
)
),
);
}
Widget _buildValidatedHexField(String label, TextEditingController ctrl, int len, bool with0x) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: TextFormField(
controller: ctrl,
enabled: !_isSaving,
decoration: InputDecoration(
labelText: label,
suffixIcon: IconButton(
icon: const Icon(Icons.refresh),
onPressed: () => setState(() => ctrl.text = (with0x ? "0x" : "") + _generateRandomHex(len ~/ 2)),
onPressed: _isSaving ? null : () => setState(() {
ctrl.text = (with0x ? "0x" : "") + _generateRandomHex(len ~/ 2);
}),
),
),
inputFormatters: [
@@ -155,36 +177,58 @@ class _LeaderConfigScreenState extends State<LeaderConfigScreen> {
Widget _buildActionButtons() {
return Row(
children: [
Expanded(child: OutlinedButton(onPressed: () => Navigator.pop(context), child: const Text("Abbrechen"))),
Expanded(
child: OutlinedButton(
onPressed: _isSaving ? null : () => Navigator.pop(context),
child: const Text("Abbrechen"),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: _isSaving ? null : _startProvisioning,
child: _isSaving ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)) : const Text("Lobby"),
child: _isSaving
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))
: const Text("Lobby"),
),
),
],
);
}
/// Erstellt das neue DeviceConfig Objekt und sendet es als Block an die Hardware
Future<void> _startProvisioning() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _isSaving = true);
try {
final config = {
'name': _nameCtrl.text.trim(),
'netName': _netNameCtrl.text.trim(),
'chan': _selectedChannel,
'panId': int.parse(_panCtrl.text.replaceFirst('0x', ''), radix: 16),
'extPan': _extPanCtrl.text.trim(),
'netKey': _netKeyCtrl.text.trim(),
};
// Neues Model-Objekt erstellen
final newConfig = DeviceConfig(
systemState: _currentConfig.systemState, // Unverändert übernehmen
gameId: _currentConfig.gameId, // Unverändert übernehmen
nodeName: _nameCtrl.text.trim(),
networkName: _netNameCtrl.text.trim(),
channel: _selectedChannel,
panId: int.parse(_panCtrl.text.replaceFirst('0x', ''), radix: 16),
extPanId: _extPanCtrl.text.trim(),
networkKey: _netKeyCtrl.text.trim(),
);
await context.read<DeviceProvider>().provisionLeader(widget.device, config);
if (mounted) Navigator.pop(context);
// Optimierte Methode im Provider aufrufen
await context.read<DeviceProvider>().provisionDevice(widget.device, newConfig);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Konfiguration erfolgreich übertragen!")),
);
Navigator.pop(context);
}
} catch (e) {
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Fehler: $e"), backgroundColor: Colors.red));
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Fehler beim Speichern: $e"), backgroundColor: Colors.red),
);
}
} finally {
if (mounted) setState(() => _isSaving = false);
}
@@ -192,7 +236,9 @@ class _LeaderConfigScreenState extends State<LeaderConfigScreen> {
String _generateRandomHex(int bytes) {
final rand = Random();
return List.generate(bytes, (_) => rand.nextInt(256).toRadixString(16).padLeft(2, '0')).join().toUpperCase();
return List.generate(bytes, (_) => rand.nextInt(256).toRadixString(16).padLeft(2, '0'))
.join()
.toUpperCase();
}
@override