Arbeiten an der BT-Kommunikation, stand vor umbau
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 22s
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 22s
This commit is contained in:
@@ -2,7 +2,12 @@ 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 typeChar = "${base}1008";
|
||||
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";
|
||||
|
||||
// Gerätetypen aus deiner ble_mgmt.h
|
||||
static const int typeLeader = 0x01;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||
@@ -30,7 +31,9 @@ class DeviceProvider extends ChangeNotifier {
|
||||
_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];
|
||||
@@ -38,7 +41,9 @@ class DeviceProvider extends ChangeNotifier {
|
||||
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,
|
||||
@@ -67,14 +72,20 @@ class DeviceProvider extends ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
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<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) {
|
||||
@@ -84,7 +95,10 @@ class DeviceProvider extends ChangeNotifier {
|
||||
}
|
||||
|
||||
// Öffentlicher API-Entry: Namen auf Hardware schreiben
|
||||
Future<void> updateDeviceNameOnHardware(LasertagDevice ltDevice, String newName) async {
|
||||
Future<void> updateDeviceNameOnHardware(
|
||||
LasertagDevice ltDevice,
|
||||
String newName,
|
||||
) async {
|
||||
try {
|
||||
await ltDevice.btDevice.connect(
|
||||
license: License.free,
|
||||
@@ -94,9 +108,14 @@ class DeviceProvider extends ChangeNotifier {
|
||||
|
||||
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));
|
||||
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));
|
||||
|
||||
@@ -111,12 +130,17 @@ class DeviceProvider extends ChangeNotifier {
|
||||
|
||||
// Öffentlicher API-Entry: Lokale Liste aktualisieren
|
||||
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);
|
||||
@@ -129,24 +153,35 @@ class DeviceProvider extends ChangeNotifier {
|
||||
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 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 realName = utf8.decode(value);
|
||||
|
||||
updateDeviceName(device.id, realName, verified: true);
|
||||
|
||||
|
||||
await device.btDevice.disconnect();
|
||||
} catch (e) {
|
||||
debugPrint("Background Sync fehlgeschlagen für ${device.id}: $e");
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -158,6 +193,130 @@ 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);
|
||||
@@ -171,4 +330,4 @@ class DeviceProvider extends ChangeNotifier {
|
||||
_scanSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import '../../constants.dart';
|
||||
import '../../models/device_model.dart';
|
||||
import '../../providers/device_provider.dart';
|
||||
import '../widgets/rename_dialog.dart';
|
||||
import 'leader_config_screen.dart';
|
||||
|
||||
class DeviceSelectionScreen extends StatefulWidget {
|
||||
const DeviceSelectionScreen({super.key});
|
||||
@@ -90,16 +91,22 @@ class _DeviceSelectionScreenState extends State<DeviceSelectionScreen> {
|
||||
// : FontStyle.italic, // Kursiv, wenn nicht verifiziert
|
||||
color: device.isNameVerified
|
||||
? Theme.of(context).colorScheme.onSurface
|
||||
: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
subtitle: Text("BTLE-ID: ${device.id}"),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => RenameDialog(device: device),
|
||||
);
|
||||
if (device.isLeader) {
|
||||
_showLeaderOptions(context, device);
|
||||
} else {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => RenameDialog(device: device),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}, childCount: devices.length),
|
||||
@@ -120,4 +127,65 @@ class _DeviceSelectionScreenState extends State<DeviceSelectionScreen> {
|
||||
return Icons.device_unknown;
|
||||
}
|
||||
}
|
||||
|
||||
void _showLeaderOptions(BuildContext context, LasertagDevice device) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (context) {
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min, // Nur so groß wie der Inhalt
|
||||
children: [
|
||||
// Ein kleiner Griff oben für die Optik
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 12, bottom: 8),
|
||||
height: 4,
|
||||
width: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[300],
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text(
|
||||
device.name,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.edit),
|
||||
title: const Text('Anzeigename ändern'),
|
||||
onTap: () {
|
||||
Navigator.pop(context); // Bottom Sheet schließen
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => RenameDialog(device: device),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.hub),
|
||||
title: const Text('Als Game-Leader konfigurieren'),
|
||||
onTap: () {
|
||||
Navigator.pop(context); // Bottom Sheet zu
|
||||
Navigator.push(
|
||||
// Config Screen auf
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => LeaderConfigScreen(device: device),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16), // Abstand nach unten
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
207
software/app/lib/ui/screens/leader_config_screen.dart
Normal file
207
software/app/lib/ui/screens/leader_config_screen.dart
Normal file
@@ -0,0 +1,207 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'dart:math';
|
||||
import '../../models/device_model.dart';
|
||||
import '../../providers/device_provider.dart';
|
||||
|
||||
class LeaderConfigScreen extends StatefulWidget {
|
||||
final LasertagDevice device;
|
||||
const LeaderConfigScreen({super.key, required this.device});
|
||||
|
||||
@override
|
||||
State<LeaderConfigScreen> createState() => _LeaderConfigScreenState();
|
||||
}
|
||||
|
||||
class _LeaderConfigScreenState extends State<LeaderConfigScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
final TextEditingController _nameCtrl = TextEditingController();
|
||||
final TextEditingController _netNameCtrl = TextEditingController();
|
||||
final TextEditingController _panCtrl = TextEditingController();
|
||||
final TextEditingController _extPanCtrl = TextEditingController();
|
||||
final TextEditingController _netKeyCtrl = TextEditingController();
|
||||
|
||||
int _selectedChannel = 15;
|
||||
bool _isLoading = true;
|
||||
bool _isSaving = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadCurrentConfig();
|
||||
}
|
||||
|
||||
Future<void> _loadCurrentConfig() async {
|
||||
try {
|
||||
final config = await context.read<DeviceProvider>().readFullLeaderConfig(widget.device);
|
||||
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'];
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Fehler: $e")));
|
||||
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";
|
||||
|
||||
String cleanValue = with0x ? value.replaceFirst('0x', '') : value;
|
||||
final hexRegex = RegExp(r'^[0-9A-Fa-f]+$');
|
||||
|
||||
if (!hexRegex.hasMatch(cleanValue)) return "Nur Hex-Zeichen (0-9, A-F) erlaubt";
|
||||
if (cleanValue.length != requiredLength && requiredLength > 0) {
|
||||
return "Muss genau $requiredLength Zeichen lang sein";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("Leader-Konfiguration")),
|
||||
body: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Form(
|
||||
key: _formKey,
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
_buildSectionTitle("Allgemein"),
|
||||
TextFormField(
|
||||
controller: _nameCtrl,
|
||||
decoration: const InputDecoration(labelText: "Knoten-Name", helperText: "Max. 31 Zeichen"),
|
||||
maxLength: 31,
|
||||
validator: (v) => (v == null || v.isEmpty) ? "Name erforderlich" : null,
|
||||
),
|
||||
|
||||
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,
|
||||
),
|
||||
|
||||
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!),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
_buildSectionTitle("Thread Security"),
|
||||
|
||||
_buildValidatedHexField("PAN ID (16-bit)", _panCtrl, 4, true),
|
||||
_buildValidatedHexField("Extended PAN ID (64-bit)", _extPanCtrl, 16, false),
|
||||
_buildValidatedHexField("Network Key (128-bit)", _netKeyCtrl, 32, false),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
_buildActionButtons(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildValidatedHexField(String label, TextEditingController ctrl, int len, bool with0x) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: TextFormField(
|
||||
controller: ctrl,
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
suffixIcon: IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: () => setState(() => ctrl.text = (with0x ? "0x" : "") + _generateRandomHex(len ~/ 2)),
|
||||
),
|
||||
),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp(with0x ? r'[0-9A-Fa-fXx]' : r'[0-9A-Fa-f]')),
|
||||
],
|
||||
validator: (v) => _validateHex(v, len, label, with0x: with0x),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButtons() {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(child: OutlinedButton(onPressed: () => 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"),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
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(),
|
||||
};
|
||||
|
||||
await context.read<DeviceProvider>().provisionLeader(widget.device, config);
|
||||
if (mounted) Navigator.pop(context);
|
||||
} catch (e) {
|
||||
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Fehler: $e"), backgroundColor: Colors.red));
|
||||
} finally {
|
||||
if (mounted) setState(() => _isSaving = false);
|
||||
}
|
||||
}
|
||||
|
||||
String _generateRandomHex(int bytes) {
|
||||
final rand = Random();
|
||||
return List.generate(bytes, (_) => rand.nextInt(256).toRadixString(16).padLeft(2, '0')).join().toUpperCase();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameCtrl.dispose();
|
||||
_netNameCtrl.dispose();
|
||||
_panCtrl.dispose();
|
||||
_extPanCtrl.dispose();
|
||||
_netKeyCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -15,18 +15,17 @@ class RenameDialog extends StatefulWidget {
|
||||
class _RenameDialogState extends State<RenameDialog> {
|
||||
late TextEditingController _controller;
|
||||
bool _isSaving = false;
|
||||
bool _isReading = true; // Neu: Startet im Lade-Modus
|
||||
late bool _isReading; // Nicht mehr direkt auf true setzen
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TextEditingController(text: widget.device.name);
|
||||
if (!widget.device.isNameVerified) {
|
||||
// Wenn der Name bereits verifiziert ist, nicht von der Hardware lesen
|
||||
_isReading = false;
|
||||
} else {
|
||||
_fetchHardwareName(); // Namen beim Öffnen abfragen
|
||||
}
|
||||
|
||||
// Wenn der Name schon verifiziert ist, müssen wir nicht blockierend laden
|
||||
_isReading = !widget.device.isNameVerified;
|
||||
|
||||
_fetchHardwareName();
|
||||
}
|
||||
|
||||
Future<void> _fetchHardwareName() async {
|
||||
|
||||
Reference in New Issue
Block a user