Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UniversalBle.onValueChanged returns duplicate data on Android #15

Open
HosamHasanRamadan opened this issue Feb 29, 2024 · 9 comments
Open

Comments

@HosamHasanRamadan
Copy link

HosamHasanRamadan commented Feb 29, 2024

First of all I want to thank you for your amazing effort, The plugin API is simple and straight forward

Issue:
I have BLE device with two Characteristic (write: writeWithoutResponse) and (read: notify).
If I connect to my BLE device and disconnect and reconnect again , the readings from the devices will be duplicated.
The duplication is equal to the number of reconnection , like if you reconnect 3 times the data will be duplicated 3 times.

This issue happens only on Android

Steps to reproduce the issue:
1- Scan
2- Stop scan
3- Select BLE device and connect
4- Discover services
5- Set notifying to read characteristic
6- Write command
7- onValueChanged called and get the result
8- Disconnect
9- Reconnect with same steps
10- onValueChanged called and the result is repeated twice

Flutter doctor

[✓] Flutter (Channel stable, 3.16.9, on macOS 14.3.1 23D60 darwin-arm64, locale en-EG)
• Flutter version 3.16.9 on channel stable at /Users/hosamhasan/fvm/versions/3.16.9
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision 41456452f2 (5 weeks ago), 2024-01-25 10:06:23 -0800
• Engine revision f40e976bed
• Dart version 3.2.6
• DevTools version 2.28.5

[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
• Android SDK at /Users/hosamhasan/Library/Android/sdk
• Platform android-34, build-tools 34.0.0
• ANDROID_HOME = /Users/hosamhasan/Library/Android/sdk
• Java binary at: /Users/hosamhasan/Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
• Java version OpenJDK Runtime Environment (build 17.0.7+0-17.0.7b1000.6-10550314)
• All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 15.2)
• Xcode at /Applications/Xcode-15.2.0.app/Contents/Developer
• Build 15C500b
• CocoaPods version 1.15.2

[✓] Chrome - develop for the web
• Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2023.1)
• Android Studio at /Users/hosamhasan/Applications/Android Studio.app/Contents
• Flutter plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/9212-flutter
• Dart plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/6351-dart
• Java version OpenJDK Runtime Environment (build 17.0.7+0-17.0.7b1000.6-10550314)

[✓] IntelliJ IDEA Community Edition (version 2023.3.2)
• IntelliJ at /Users/hosamhasan/Applications/IntelliJ IDEA Community Edition.app
• Flutter plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/9212-flutter
• Dart plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/6351-dart

[✓] VS Code (version 1.86.2)
• VS Code at /Applications/Visual Studio Code.app/Contents
• Flutter extension version 3.83.20240201

[✓] Connected device (4 available)
• Lenovo TB 8505FS (mobile) • HA16YBY9 • android-arm64 • Android 10 (API 29)
• Hosam’s iPhone (mobile) • 00008101-000A6D303AF8001E • ios • iOS 17.3.1 21D61
• macOS (desktop) • macos • darwin-arm64 • macOS 14.3.1 23D60 darwin-arm64
• Chrome (web) • chrome • web-javascript • Google Chrome 122.0.6261.94

[✓] Network resources
• All expected network resources are available.

• No issues found!

Code
import 'dart:async';
import 'dart:developer';
import 'dart:typed_data';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

import 'package:universal_ble/universal_ble.dart';


class Example extends StatefulWidget {
  const Example({super.key});

  @override
  State<Example> createState() => _ExampleState();
}

class _ExampleState extends State<Example> {
  final List<BleScanResult> foundDevices = [];

  BleScanResult? connectedDevice;
  final result = <Uint8List>[];
  String textResult = '';
  @override
  void initState() {
    super.initState();

    UniversalBle.onScanResult = (scanResult) {
      log(scanResult.name ?? scanResult.deviceId);

      if (foundDevices.map((e) => e.deviceId).contains(scanResult.deviceId)) {
        return;
      }
      if (scanResult.name == null) return;
      foundDevices.add(scanResult);
      setState(() {});
    };
    UniversalBle.onAvailabilityChange = (state) {
      print(state.name);
    };
    UniversalBle.onValueChanged = (
      String deviceId,
      String characteristicId,
      Uint8List value,
    ) {
      result.add(value);
    };

    UniversalBle.onConnectionChanged =
        (String deviceId, BleConnectionState state) {
      print('$deviceId -- $state');
    };
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: SafeArea(
        child: Scaffold(
          body: Column(
            children: [
              Wrap(
                children: [
                  TextButton(
                    onPressed: () async {
                      UniversalBle.startScan();
                    },
                    child: Text('Scan'),
                  ),
                  TextButton(
                    onPressed: () async {
                      UniversalBle.stopScan();
                    },
                    child: Text('Stop'),
                  ),
                  TextButton(
                    onPressed: () async {
                      if (connectedDevice != null) {
                        UniversalBle.disconnect(connectedDevice!.deviceId)
                            .then((value) {
                          connectedDevice = null;
                          foundDevices.clear();
                          textResult = '';
                          setState(() {});
                        });
                      }
                    },
                    child: Text('Disconnect'),
                  ),
                  TextButton(
                    onPressed: setNotifying,
                    child: const Text('Notifying'),
                  ),
                  TextButton(
                    onPressed: () async {
                      if (connectedDevice != null) {
                        await UniversalBle.discoverServices(
                          connectedDevice!.deviceId,
                        );
                      }
                    },
                    child: Text('Discover Services'),
                  ),
                  TextButton(
                    onPressed: write,
                    child: Text('Write'),
                  ),
                ],
              ),
              Text(
                'Result:\n$textResult',
                textAlign: TextAlign.center,
              ),
              Text('Scan Result'),
              Expanded(
                child: ListView(
                  children: [
                    ...foundDevices.map(
                          (value) {
                            return ListTile(
                              onTap: () {
                                UniversalBle.connect(value.deviceId);

                                connectedDevice = value;
                              },
                              title: Text('${value.name}'),
                            );
                          },
                        ) ??
                        [],
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  void setNotifying() {
    final readCharacteristicUuid = '';
    final serviceUuid = '';

    if (connectedDevice != null) {
      UniversalBle.setNotifiable(
        connectedDevice!.deviceId,
        serviceUuid,
        readCharacteristicUuid,
        BleInputProperty.notification,
      );
    }
  }

  Future<void> write() async {
    final writeCharacteristicUuid = '';
    final serviceUuid = '';
    final command = Uint8List.fromList([]);

    if (connectedDevice != null) {
      UniversalBle.writeValue(
        connectedDevice!.deviceId,
        serviceUuid,
        writeCharacteristicUuid,
        command,
        BleOutputProperty.withoutResponse,
      );
    }
  }
}
@fotiDim
Copy link
Contributor

fotiDim commented Feb 29, 2024

Kudos for the very detailed issue report! We will have a look and report back soon.

@rohitsangwan01
Copy link
Contributor

@HosamHasanRamadan i tried replicating this, seems to be working fine for me, can you cross check with NrfConnect app, sometimes issue can be with peripheral as well, if peripheral is not clearing characteristic handlers on disconnecting

@chipweinberger
Copy link

You are probably forgetting to cancel the original onValueChanged.listen resulting in multiple listens.

@rohitsangwan01
Copy link
Contributor

@chipweinberger onValueChanged is a callback method, so we don't really have to call listen or anything
we just have to set a method which can handle value changes

UniversalBle.onValueChanged = (String deviceId, String characteristicId, Uint8List value) {
  print('onValueChanged $deviceId, $characteristicId, ${hex.encode(value)}');
}

hence even if we set it multiple times, only latest one will get the updates

@iBog
Copy link

iBog commented Jul 16, 2024

Same problem, reproduced on old gen devices only (example: Galaxy S10 on Android 7, Pixel 5 on Android 14),
latest 22-24 year devices works fine.
My situation:
BLE Heart Rate device, which activated automatically by contacts touch event,
App detects device in Search by name and service and automatically connect to Characteristic update via setNotifiable().
After disconnect by timeout (BLE contacts was untouched), App reconnected to device again, and received double, triple, etc
onValueChanged callback.

@Sravdar
Copy link

Sravdar commented Nov 6, 2024

I am having the same problem. If i unsubscribe before disconnecting it works fine but in case of premature disconnection (loss of signal, server reset etc.) there is no way to unsubscribe from the characteristic.

Was anyone able to find workaround for this problem?

@rohitsangwan01
Copy link
Contributor

@Sravdar if device disconnect, you don't have to unsubscribe from chars, Android should clean up connection

@Sravdar
Copy link

Sravdar commented Nov 7, 2024

@rohitsangwan01 Sadly it does not.

I have created example code to demonstrate the issue. In this code device connects to a ESP32 device which sends back the value it recieved as a notification.
https://gist.github.com/Sravdar/2b37e9db4b20f16687bbc701baf62b20

Disconnect Carefully buton closes notification before disconnecting which doesn't cause any issue. Otherwise for each disconnection you get duplicated value change callback. Closing bluetooth at anytime and reopening it fixes duplications.

This has been tested on "Redmi Note 8" runing android "11 RKQ.201004.002".
The problem doesn't occur on windows machine.

@rohitsangwan01
Copy link
Contributor

@Sravdar Thank you for the detailed information. We’ll try to replicating the issue and implementing a fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants