Compare commits

...

2 Commits

Author SHA1 Message Date
65688b7b99 Fixed BLE Scan activation after Disconect
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 23s
2026-01-12 16:18:42 +01:00
395d577b78 BLE Handling 2026-01-12 15:10:54 +01:00
10 changed files with 196 additions and 177 deletions

View File

@@ -46,6 +46,7 @@ LOG_MODULE_REGISTER(ble_mgmt, CONFIG_BLE_MGMT_LOG_LEVEL);
/* --- Global Variables --- */
static uint8_t device_role = 0; // Store device type for provisioning
static uint8_t adv_enabled = 0; // Track advertising state
static struct k_work_delayable adv_restart_work;
/* --- GATT Callbacks --- */
@@ -105,11 +106,8 @@ static ssize_t write_lasertag_val(struct bt_conn *conn, const struct bt_gatt_att
if (bt_uuid_cmp(attr->uuid, BT_UUID_LT_PROV_NAME_CHAR) == 0)
{
rc = lasertag_set_device_name(buf, len);
if (rc == 0 && adv_enabled) {
LOG_INF("Stopping advertising to update device name");
ble_mgmt_adv_stop();
LOG_INF("Restarting advertising with new device name");
ble_mgmt_adv_start();
if (rc == 0 ) {
bt_set_name(lasertag_get_device_name());
}
}
else if (bt_uuid_cmp(attr->uuid, BT_UUID_LT_PROV_PANID_CHAR) == 0)
@@ -221,10 +219,24 @@ static const struct bt_data ad[] = {
BT_DATA(BT_DATA_MANUFACTURER_DATA, mfg_data, sizeof(mfg_data)),
};
static void adv_restart_work_handler(struct k_work *work)
{
if (adv_enabled == 0)
{
int err = ble_mgmt_adv_start();
if (err) {
LOG_ERR("Fehler beim verzögerten Neustarten des Advertisings (err %d)", err);
} else {
LOG_INF("Advertising nach Verzögerung erfolgreich neu gestartet");
}
}
}
int ble_mgmt_init(uint8_t device_type)
{
device_role = device_type;
k_work_init_delayable(&adv_restart_work, adv_restart_work_handler);
int err = bt_enable(NULL);
if (err)
return err;
@@ -269,4 +281,25 @@ int ble_mgmt_adv_stop(void)
adv_enabled = 0;
}
return err;
}
}
static void connected(struct bt_conn *conn, uint8_t err)
{
if (err) {
LOG_ERR("Verbindung fehlgeschlagen (err %u)", err);
} else {
LOG_INF("Host verbunden");
adv_enabled = 0;
}
}
static void disconnected(struct bt_conn *conn, uint8_t reason)
{
LOG_INF("Verbindung getrennt (Grund %u)", reason);
k_work_reschedule(&adv_restart_work, K_MSEC(100));
}
BT_CONN_CB_DEFINE(conn_callbacks) = {
.connected = connected,
.disconnected = disconnected,
};

View File

@@ -3,9 +3,9 @@
"appTitle": "Lasertag Mission Control",
"selectLeader": "Leader auswählen",
"searching": "Suche nach Knoten...",
"listTypeLeader": "Gameleadergeräte",
"listTypeLeader": "Game LEader",
"typeWeapon": "Waffe",
"typeVest": "Weste",
"typeBeacon": "Beacon",
"listTypeEquipment": "Ausrüstungsgeräte"
"listTypeEquipment": "Sonstiges"
}

View File

@@ -115,7 +115,7 @@ abstract class AppLocalizations {
/// No description provided for @listTypeLeader.
///
/// In de, this message translates to:
/// **'Gameleadergeräte'**
/// **'Game LEader'**
String get listTypeLeader;
/// No description provided for @typeWeapon.
@@ -139,7 +139,7 @@ abstract class AppLocalizations {
/// No description provided for @listTypeEquipment.
///
/// In de, this message translates to:
/// **'Ausrüstungsgeräte'**
/// **'Sonstiges'**
String get listTypeEquipment;
}

View File

@@ -18,7 +18,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get searching => 'Suche nach Knoten...';
@override
String get listTypeLeader => 'Gameleadergeräte';
String get listTypeLeader => 'Game LEader';
@override
String get typeWeapon => 'Waffe';
@@ -30,5 +30,5 @@ class AppLocalizationsDe extends AppLocalizations {
String get typeBeacon => 'Beacon';
@override
String get listTypeEquipment => 'Ausrüstungsgeräte';
String get listTypeEquipment => 'Sonstiges';
}

View File

@@ -1,14 +1,13 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; // Neu
import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter/material.dart';
import 'package:lasertag_app/l10n/app_localizations.dart';
import 'package:provider/provider.dart';
import 'providers/device_provider.dart';
import 'ui/screens/device_selection_screen.dart';
import 'providers/device_provider.dart'; // Neu
void main() {
runApp(
// Den Provider hier für die ganze App bereitstellen
ChangeNotifierProvider(
create: (context) => DeviceProvider(),
child: const LasertagApp(),

View File

@@ -1,4 +1,5 @@
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import '../../constants.dart';
class LasertagDevice {
@@ -7,6 +8,7 @@ class LasertagDevice {
final int type;
final bool isConnected;
final BluetoothDevice btDevice;
final bool isNameVerified;
LasertagDevice({
required this.id,
@@ -14,8 +16,19 @@ class LasertagDevice {
required this.type,
required this.btDevice,
this.isConnected = false,
this.isNameVerified = false,
});
// Hilfsmethode: Ist es ein Leader?
bool get isLeader => type == LasertagUUIDs.typeLeader;
LasertagDevice copyWith({String? name, bool? isNameVerified}) {
return LasertagDevice(
id: id,
type: type,
btDevice: btDevice,
name: name ?? this.name,
isNameVerified: isNameVerified ?? this.isNameVerified,
isConnected: isConnected,
);
}
}

View File

@@ -1,9 +1,11 @@
import 'dart:async';
import 'dart:convert';
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';
import '../models/device_model.dart';
class DeviceProvider extends ChangeNotifier {
final List<LasertagDevice> _discoveredLeaders = [];
@@ -13,6 +15,49 @@ class DeviceProvider extends ChangeNotifier {
List<LasertagDevice> get leaders => _discoveredLeaders;
List<LasertagDevice> get peripherals => _discoveredPeripherals;
// Öffentlicher API-Entry: Scan starten
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;
_scanSubscription?.cancel();
_scanSubscription = FlutterBluePlus.scanResults.listen((results) {
for (ScanResult r in results) {
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,
type: mfgData[0],
btDevice: r.device,
isConnected: false,
isNameVerified: false, // Initial immer unversichert (Kursiv)
);
_addDeviceToLists(device);
}
}
});
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) {
@@ -22,21 +67,14 @@ 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));
// Namen von der Hardware lesen
List<int> value = await characteristic.read();
String hardwareName = utf8.decode(value);
// Lokalen Cache in der App ebenfalls aktualisieren, um Caching zu umgehen
updateDeviceName(ltDevice.id, hardwareName);
updateDeviceName(ltDevice.id, hardwareName, verified: true);
return hardwareName;
} catch (e) {
@@ -45,10 +83,8 @@ class DeviceProvider extends ChangeNotifier {
}
}
Future<void> updateDeviceNameOnHardware(
LasertagDevice ltDevice,
String newName,
) async {
// Öffentlicher API-Entry: Namen auf Hardware schreiben
Future<void> updateDeviceNameOnHardware(LasertagDevice ltDevice, String newName) async {
try {
await ltDevice.btDevice.connect(
license: License.free,
@@ -58,21 +94,13 @@ class DeviceProvider extends ChangeNotifier {
await Future.delayed(const Duration(milliseconds: 500));
List<BluetoothService> services = await ltDevice.btDevice
.discoverServices();
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));
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);
updateDeviceName(ltDevice.id, newName, verified: true);
await ltDevice.btDevice.disconnect();
} catch (e) {
@@ -81,98 +109,51 @@ class DeviceProvider extends ChangeNotifier {
}
}
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();
_discoveredPeripherals.clear();
notifyListeners();
// 1. Bluetooth-Status prüfen
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) {
bool hasService = r.advertisementData.serviceUuids.contains(
Guid(LasertagUUIDs.provService),
);
if (!hasService) continue;
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,
type: type,
btDevice: r.device,
isConnected: false,
);
_addDeviceToLists(device);
}
}
});
// 3. Bluetooth-Scan starten
try {
await FlutterBluePlus.startScan(timeout: const Duration(seconds: 15));
} catch (e) {
debugPrint("Scan-Fehler: $e");
}
// Öffentlicher API-Entry: Lokale Liste aktualisieren
void updateDeviceName(String id, String newName, {bool verified = false}) {
_updateDeviceInLists(id, (old) => old.copyWith(name: newName, isNameVerified: verified));
}
void _addDeviceToLists(LasertagDevice device) {
if (device.isLeader) {
if (!_discoveredLeaders.any((d) => d.id == device.id)) {
_discoveredLeaders.add(device);
_sortList(_discoveredLeaders);
notifyListeners();
}
} else {
if (!_discoveredPeripherals.any((d) => d.id == device.id)) {
_discoveredPeripherals.add(device);
_sortList(_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
}
}
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));
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) {
for (var list in [_discoveredLeaders, _discoveredPeripherals]) {
int index = list.indexWhere((d) => d.id == id);
if (index != -1) {
list[index] = updateFn(list[index]);
_sortList(list);
notifyListeners();
return;
}
}
}
@@ -190,4 +171,4 @@ class DeviceProvider extends ChangeNotifier {
_scanSubscription?.cancel();
super.dispose();
}
}
}

View File

@@ -1,10 +1,11 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:lasertag_app/l10n/app_localizations.dart';
import '../../constants.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 {
const DeviceSelectionScreen({super.key});
@@ -29,7 +30,15 @@ class _DeviceSelectionScreenState extends State<DeviceSelectionScreen> {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(title: Text(l10n.appTitle)),
appBar: AppBar(
title: Text(l10n.appTitle),
actions: [
IconButton(
onPressed: () => context.read<DeviceProvider>().startScan(),
icon: Icon(Icons.refresh),
),
],
),
body: CustomScrollView(
slivers: [
_buildHeader(context, l10n.listTypeLeader),
@@ -74,16 +83,24 @@ class _DeviceSelectionScreenState extends State<DeviceSelectionScreen> {
),
title: Text(
device.name,
style: const TextStyle(fontWeight: FontWeight.bold),
style: TextStyle(
fontWeight: FontWeight.bold,
// fontStyle: device.isNameVerified
// ? FontStyle.normal
// : FontStyle.italic, // Kursiv, wenn nicht verifiziert
color: device.isNameVerified
? Theme.of(context).colorScheme.onSurface
: 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),
);
},
showDialog(
context: context,
builder: (context) => RenameDialog(device: device),
);
},
);
}, childCount: devices.length),
);

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../models/device_model.dart';
import '../../providers/device_provider.dart';
@@ -20,7 +21,12 @@ class _RenameDialogState extends State<RenameDialog> {
void initState() {
super.initState();
_controller = TextEditingController(text: widget.device.name);
_fetchHardwareName(); // Namen beim Öffnen abfragen
if (!widget.device.isNameVerified) {
// Wenn der Name bereits verifiziert ist, nicht von der Hardware lesen
_isReading = false;
} else {
_fetchHardwareName(); // Namen beim Öffnen abfragen
}
}
Future<void> _fetchHardwareName() async {

View File

@@ -1,30 +0,0 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:lasertag_app/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}