diff --git a/software/app/lib/constants.dart b/software/app/lib/constants.dart index dc1b339..4709d7d 100644 --- a/software/app/lib/constants.dart +++ b/software/app/lib/constants.dart @@ -1,6 +1,7 @@ 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"; // Gerätetypen aus deiner ble_mgmt.h diff --git a/software/app/lib/models/device_model.dart b/software/app/lib/models/device_model.dart index f6b5f3c..297c7be 100644 --- a/software/app/lib/models/device_model.dart +++ b/software/app/lib/models/device_model.dart @@ -1,3 +1,4 @@ +import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import '../../constants.dart'; class LasertagDevice { @@ -5,11 +6,13 @@ class LasertagDevice { final String name; final int type; final bool isConnected; + final BluetoothDevice btDevice; LasertagDevice({ required this.id, required this.name, required this.type, + required this.btDevice, this.isConnected = false, }); diff --git a/software/app/lib/providers/device_provider.dart b/software/app/lib/providers/device_provider.dart index 360faea..bb69cc8 100644 --- a/software/app/lib/providers/device_provider.dart +++ b/software/app/lib/providers/device_provider.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; +import 'dart:convert'; import '../models/device_model.dart'; import '../constants.dart'; @@ -12,6 +13,66 @@ class DeviceProvider extends ChangeNotifier { List get leaders => _discoveredLeaders; List get peripherals => _discoveredPeripherals; + Future updateDeviceNameOnHardware( + LasertagDevice ltDevice, + String newName, + ) async { + try { + await ltDevice.btDevice.connect(); + List 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), + ); + + // 1. Auf Hardware schreiben (UTF-8) + await characteristic.write(utf8.encode(newName)); + + // 2. Lokal in der App-Liste aktualisieren + updateDeviceName(ltDevice.id, newName); + + await ltDevice.btDevice.disconnect(); + } catch (e) { + debugPrint("Hardware-Fehler: $e"); + rethrow; + } + } + + void updateDeviceName(String id, String newName) { + // In den Leadern suchen + int leaderIndex = _discoveredLeaders.indexWhere((d) => d.id == id); + if (leaderIndex != -1) { + final old = _discoveredLeaders[leaderIndex]; + _discoveredLeaders[leaderIndex] = LasertagDevice( + id: old.id, + name: newName, + type: old.type, + btDevice: old.btDevice, + isConnected: old.isConnected, + ); + notifyListeners(); + return; + } + + // In der Ausrüstung suchen + int peripheralIndex = _discoveredPeripherals.indexWhere((d) => d.id == id); + if (peripheralIndex != -1) { + final old = _discoveredPeripherals[peripheralIndex]; + _discoveredPeripherals[peripheralIndex] = LasertagDevice( + id: old.id, + name: newName, + type: old.type, + btDevice: old.btDevice, + isConnected: old.isConnected, + ); + notifyListeners(); + } + } + void startScan() async { // Listen leeren für neuen Scan _discoveredLeaders.clear(); @@ -19,21 +80,27 @@ class DeviceProvider extends ChangeNotifier { notifyListeners(); // 1. Bluetooth-Status prüfen - await FlutterBluePlus.adapterState.where((s) => s == BluetoothAdapterState.on).first; + await FlutterBluePlus.adapterState + .where((s) => s == BluetoothAdapterState.on) + .first; // 2. Scan-Ergebnisse verarbeiten _scanSubscription?.cancel(); _scanSubscription = FlutterBluePlus.scanResults.listen((results) { for (ScanResult r in results) { - final mfgData = r.advertisementData.manufacturerData[65535]; // Unsere ID 0xFFFF - + final mfgData = + r.advertisementData.manufacturerData[65535]; // Unsere ID 0xFFFF + if (mfgData != null && mfgData.isNotEmpty) { int type = mfgData[0]; // Typ-Byte vom nRF52 - + 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: type, + btDevice: r.device, isConnected: false, ); @@ -82,4 +149,4 @@ class DeviceProvider extends ChangeNotifier { _scanSubscription?.cancel(); super.dispose(); } -} \ No newline at end of file +} diff --git a/software/app/lib/ui/screens/device_selection_screen.dart b/software/app/lib/ui/screens/device_selection_screen.dart index f57a221..49424c2 100644 --- a/software/app/lib/ui/screens/device_selection_screen.dart +++ b/software/app/lib/ui/screens/device_selection_screen.dart @@ -3,6 +3,7 @@ import 'package:provider/provider.dart'; import '../../models/device_model.dart'; import '../../providers/device_provider.dart'; import '../../constants.dart'; +import '../widgets/rename_dialog.dart'; import 'package:lasertag_app/l10n/app_localizations.dart'; class DeviceSelectionScreen extends StatefulWidget { @@ -24,7 +25,7 @@ class _DeviceSelectionScreenState extends State { @override Widget build(BuildContext context) { // Hier "lauscht" der Screen jetzt auf Änderungen im Provider - final provider = context.watch(); + final provider = context.watch(); final l10n = AppLocalizations.of(context)!; return Scaffold( @@ -78,8 +79,11 @@ class _DeviceSelectionScreenState extends State { subtitle: Text("BTLE-ID: ${device.id}"), trailing: const Icon(Icons.chevron_right), onTap: () { - debugPrint("${device.name} ausgewählt für Konfiguration"); - }, + showDialog( + context: context, + builder: (context) => RenameDialog(device: device), + ); +}, ); }, childCount: devices.length), ); @@ -99,4 +103,55 @@ class _DeviceSelectionScreenState extends State { return Icons.device_unknown; } } + + void _showNameEditDialog(BuildContext context, LasertagDevice device) { + final TextEditingController nameController = TextEditingController( + text: device.name, + ); + + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Row( + children: [ + Icon( + _getIconForType(device.type), + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(width: 10), + const Text("Gerät umbenennen"), + ], + ), + content: TextField( + controller: nameController, + decoration: const InputDecoration( + labelText: "Anzeigename", + border: OutlineInputBorder(), + ), + autofocus: true, + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text("Abbrechen"), + ), + ElevatedButton( + onPressed: () { + if (nameController.text.trim().isNotEmpty) { + // Provider aktualisieren + context.read().updateDeviceName( + device.id, + nameController.text.trim(), + ); + Navigator.pop(context); + } + }, + child: const Text("Speichern"), + ), + ], + ); + }, + ); + } } diff --git a/software/app/lib/ui/widgets/rename_dialog.dart b/software/app/lib/ui/widgets/rename_dialog.dart new file mode 100644 index 0000000..82f48e4 --- /dev/null +++ b/software/app/lib/ui/widgets/rename_dialog.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../../models/device_model.dart'; +import '../../providers/device_provider.dart'; + +class RenameDialog extends StatefulWidget { + final LasertagDevice device; + const RenameDialog({super.key, required this.device}); + + @override + State createState() => _RenameDialogState(); +} + +class _RenameDialogState extends State { + late TextEditingController _controller; + bool _isSaving = false; + + @override + void initState() { + super.initState(); + _controller = TextEditingController(text: widget.device.name); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text("${widget.device.name} umbenennen"), + content: TextField( + controller: _controller, + decoration: const InputDecoration(labelText: "Neuer Name"), + enabled: !_isSaving, + ), + actions: [ + TextButton( + onPressed: _isSaving ? null : () => Navigator.pop(context), + child: const Text("Abbrechen"), + ), + ElevatedButton( + onPressed: _isSaving ? null : _save, + child: _isSaving + ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)) + : const Text("Speichern"), + ), + ], + ); + } + + Future _save() async { + setState(() => _isSaving = true); + try { + await context.read().updateDeviceNameOnHardware( + widget.device, + _controller.text.trim() + ); + 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); + } + } +} \ No newline at end of file