PhilipsHue / flutter_reactive_ble

Flutter library that handles BLE operations for multiple devices.
https://developers.meethue.com/
Other
665 stars 331 forks source link

Incomplete response when readCharacteristic on iOS #668

Closed v11 closed 1 year ago

v11 commented 1 year ago

First of all, thank you very much for this library.

Describe the bug I have an app where i read a characteristic from a BLE device (ESP32). The BLE device is scanning wi-fi networks and provides this data over the characteristic. Via my app i read this characteristic.

Everything is working fine. But yesterday I have experienced that sometimes (in a case where a lot of networks are disovered), not the complete data gets transmitted to my app. In the data, then some closing brackets are missinng and the decoding fails.

I have debugged this issue with the BT inspector app and there the full data is available and can be read.

In the flutter_reactive_ble package it is stated the follwing:

/// Be aware that a read request could be satisfied by a notification delivered
/// for the same characteristic via [characteristicValueStream] before the actual
/// read response arrives (due to the design of iOS BLE API).

I am wondering if this is the issue. But i have no idea how to workaround.

Data i recieve:

[
{"macaddress":"24:6F:28:D1:5D:DC"},
{"ssid":"FotokiteOfficeGuest"},
{"ssid":"FotokiteOffice"},
{"ssid":"FotokiteFactory"},
{"ssid":"FotokiteOffice"},
{"ssid":"FotokiteOfficeGuest"},
{"ssid":"FotokiteFactory"},
{"ssid":"fkG072B_2"},
{"ssid":"fkG092B_2"},
{"ssid":"DIRECT-B4-HP M479fdw Color LJ"},
{"ssid":"FotokiteOfficeGuest"},
{"ssid":"FotokiteFactory"},
{"ssid":"fkG132B_2"},
{"ssid":"FotokiteOffice"},
{"ssid":"Street-Files Agency 2.4 GHz"},
{"ssid":"Salt_2GHz_736A13"},
{"ssid":"DIRECT-4C-HP ENVY 5540 series"},

Data i expect to recieve:

[
{"macaddress":"24:6F:28:D1:5D:DC"},
{"ssid":"fkG072B_2"},
{"ssid":"FotokiteOffice"},
{"ssid":"FotokiteFactory"},
{"ssid":"FotokiteOfficeGuest"},
{"ssid":"fkG132B_2"},
{"ssid":"FotokiteFactory"},
{"ssid":"FotokiteOfficeGuest"},
{"ssid":"fkG092B_2"},
{"ssid":"FotokiteOffice"},
{"ssid":"DIRECT-B4-HP M479fdw Color LJ"},
{"ssid":"FotokiteOffice"},
{"ssid":"FotokiteFactory"},
{"ssid":"FotokiteOfficeGuest"},
{"ssid":"UPCD5B6DA7"},
{"ssid":"UPC Wi-Free"},
{"ssid":"qwp-69288"},
{"ssid":"hrc-23816"}
]

This is how i read the data:

Future<void> readFromBluetoothDevice(DiscoveredDevice device) async {

    final characteristic = QualifiedCharacteristic(serviceId: serviceUuid, characteristicId: characteristicUuid, deviceId: device.id);

    final response = await flutterReactiveBle.readCharacteristic(characteristic);

    ...

Smartphone / tablet

Peripheral device

Additional context

Liberations commented 1 year ago

ios传入的UUID格式是两位 9CF1 android的是00009cf1-0000-1000-8000-00805f9b34fb 我这边特别处理下貌似可以了

v11 commented 1 year ago

Not sure what you mean. Can i just switch to this shorter ID?

Liberations commented 1 year ago

Uuid.parse("9CF1")

v11 commented 1 year ago

Now i get the following exception: _Exception (Exception: GenericFailure<CharacteristicValueUpdateError>(code: CharacteristicValueUpdateError.unknown, message: "A service C201 is not found in the peripheral E8CB3A98-9F28-F36A-2DA5-BD8A951130E6 (make sure it has been discovered)"))

Could you (or someone else) be a bit more specific? I have read that in IOS you need to have this 16bit UUID. But is it for the service or for the characteristic or both?

Here is my full code:

// Summary
// This view is using a bluetooth (ble) connection to get network names (ssid) from the device.

// Check status with checkBluetoothStatus(). Status needs to be ready.
// Start scanning for bluetooth devices with startBluetoothScan()
// Connect to the device with connectBluetoothDevice()
// Multiple bluetooth errors will be handled:
// - No bluetooth
// - No location services (Android only)
// - bluetooth not authorized

// Read from ble device with readFromBluetoothDevice()
// Decode data and save ssid into a list ssidList
// Show list of networks in the view

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
import 'package:mynotes/components/spacer.dart';
import 'package:mynotes/views/setup/views/setup_03_wifi_password_view.dart';
import 'package:mynotes/views/setup/services/setup_constants.dart';
import 'package:mynotes/views/setup/services/ssid_name.dart';
import 'package:mynotes/views/setup/services/setup_services.dart';
import 'package:app_settings/app_settings.dart';
import 'package:page_route_transition/page_route_transition.dart';

class SetupWifiView extends StatefulWidget {
  final SetupType setupType;

  const SetupWifiView({
    super.key,
    required this.setupType,
  });

  @override
  State<SetupWifiView> createState() => _SetupWifiViewState();
}

class _SetupWifiViewState extends State<SetupWifiView> with WidgetsBindingObserver {
  // BLE: A Universally Unique Identifier (UUID) is a globally unique 128-bit (16-byte) number that is used to identify profiles, services, and data types in a Generic Attribute (GATT) profile.
  final Uuid serviceUuid = Uuid.parse("4fafc201-1fb5-459e-8fcc-c5c9c331914b");
  final Uuid characteristicUuid = Uuid.parse("beb5483e-36e1-4688-b7f5-ea07361b26a8");

  // BLE: GUI state management: Location Services
  bool isBleStatusUnauthorized = false;

  // BLE: GUI state management: Location Services
  bool isLocationServicesDisabled = false;

  // BLE: GUI state management: Not used yet
  bool isBluetoothPoweredOff = false;

  // BLE: GUI state management: BLE device is connected
  bool isBluetoothConnected = false;

  // BLE: GUI state management: BLE device has sent Data
  bool hasBluetoothData = false;

  // BLE: GUI state management: BLE device has sent Data
  bool isBottomSheet = false;

  // BLE: Bluetooth related variables
  final flutterReactiveBle = FlutterReactiveBle();
  late DiscoveredDevice bleDevice;
  StreamSubscription<BleStatus>? statusStream;
  StreamSubscription<DiscoveredDevice>? scanStream;
  StreamSubscription<ConnectionStateUpdate>? connectedStream;

  // BLE: List of wifi SSIDname instances
  List<SSIDname>? ssidList;

  // Device ID (mac address)
  late String macaddress;

  @override
  void initState() {
    super.initState();

    print('02 wifi view in ${widget.setupType}');

    // Get notified when App is paused/resume
    WidgetsBinding.instance.addObserver(this);

    // Check BLE Status
    checkBluetoothStatus();
  }

  @override
  void dispose() {
    super.dispose();

    // Remove binding
    WidgetsBinding.instance.removeObserver(this);

    // Disconnect the ble device
    disconnectDevice();
  }

  @override
  Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
    // This functions gets called when the app is paused/resume
    super.didChangeAppLifecycleState(state);

    if (state == AppLifecycleState.inactive) {
      print('app observer: app inactive');

      // Disconnect the ble device
      disconnectDevice();
    } else if (state == AppLifecycleState.resumed) {
      print('app observer: app resumed');

      // Disconnect the ble device
      checkBluetoothStatus();
    }
  }

  Future<void> checkBluetoothStatus() async {
    // Reset all state
    setState(() {
      isBleStatusUnauthorized = false;
      isLocationServicesDisabled = false;
      isBluetoothPoweredOff = false;
      isBluetoothConnected = false;
      hasBluetoothData = false;
    });

    // It is important to initialize bluetooth completetly
    // On iOS in release mode, this will lead to errors if this is not done.
    statusStream = flutterReactiveBle.statusStream.listen((event) {
      switch (event) {
        case BleStatus.unauthorized:
          {
            // Location services are Android only
            print('bluetooth not authorized');

            setState(() {
              isBleStatusUnauthorized = true;
              isLocationServicesDisabled = false;
              isBluetoothPoweredOff = false;
              isBluetoothConnected = false;
              hasBluetoothData = false;
            });

            break;
          }
        case BleStatus.locationServicesDisabled:
          {
            // Location services are Android only
            print('location services off');

            setState(() {
              isBleStatusUnauthorized = false;
              isLocationServicesDisabled = true;
              isBluetoothPoweredOff = false;
              isBluetoothConnected = false;
              hasBluetoothData = false;
            });

            break;
          }
        case BleStatus.poweredOff:
          {
            print('bluetooth is off');

            setState(() {
              isBleStatusUnauthorized = false;
              isBluetoothPoweredOff = true;
              isLocationServicesDisabled = false;
              isBluetoothConnected = false;
              hasBluetoothData = false;
            });

            break;
          }
        case BleStatus.ready:
          {
            print('bluetooth is ready');

            setState(() {
              isBleStatusUnauthorized = false;
              isBluetoothPoweredOff = false;
              isLocationServicesDisabled = false;
              isBluetoothConnected = false;
              hasBluetoothData = false;
            });

            // Now start scanning
            startBluetoothScanning();
            break;
          }
        default:
      }
    });
  }

  Future<void> startBluetoothScanning() async {
    scanStream = flutterReactiveBle.scanForDevices(withServices: []).listen((device) {
      print('scanning for ble devices ...');
      // Search for ble device name
      if (device.name == 'Inklay') {
        print('inklay found');

        // Assign device
        bleDevice = device;

        // Connect to device
        connectBluetoothDevice(device);
      }
    });
  }

  Future<void> connectBluetoothDevice(DiscoveredDevice device) async {
    // Stop scanning
    print('scanning stopped');
    await scanStream!.cancel();

    // Connect to device and listen to state change
    Stream<ConnectionStateUpdate> currentConnectionStream = flutterReactiveBle.connectToAdvertisingDevice(
      id: device.id,
      prescanDuration: const Duration(seconds: 2),
      withServices: [],
    );

    print('connect to inklay ...');

    // Connect to device and listen to state change
    connectedStream = currentConnectionStream.listen((event) {
      switch (event.connectionState) {
        // Connected
        case DeviceConnectionState.connected:
          {
            print('connected to inklay');

            setState(() {
              isBluetoothConnected = true;
            });

            // Read bluetooth data
            readFromBluetoothDevice(device);

            break;
          }
        // Disconnect
        case DeviceConnectionState.disconnected:
          {
            print('device disconnected');

            // Navigate back to Bluetooth Welcome Screen
            // Navigator.pop(context);

            break;
          }
        default:
      }
    });
  }

  disconnectDevice() {
    // This function disconnects the app from the device and cancel all the subscribed streams

    print('disconnect device');

    // Stop checking for status updates
    if (statusStream != null) {
      print('- bluetooth status stream stopped');
      statusStream!.cancel();
      statusStream == null;
    }

    // Stop scanning
    if (scanStream != null) {
      print('- scanning stopped');
      scanStream!.cancel();
      scanStream == null;
    }

    // Cancel stream to disconnect a ble device
    if (connectedStream != null) {
      print('- device disconnected');
      connectedStream!.cancel();
      connectedStream == null;
    }
  }

  Future<void> readFromBluetoothDevice(DiscoveredDevice device) async {
    // This function reads ssid network names from the device

    print('read from bluetooth device ...');

    // Characteristic
    final characteristic = QualifiedCharacteristic(serviceId: serviceUuid, characteristicId: characteristicUuid, deviceId: device.id);

    // Read characteristic
    // Needs to be decoded: [91, 123, 34, 115, 115, 105, 100, 34, 58, 34, 76, 117, 107, 97, 115 ... ]
    final response = await flutterReactiveBle.readCharacteristic(characteristic);

    if (response.isNotEmpty) {
      // try {
      if (!response.toString().contains("[]")) {
        // Decoding
        print("response : " + response.toString());

        // [{macaddress: 24:6F:28:D1:5D:DC}, {"ssid":"Lukas-Wi-Fi"},{"ssid":"mkt-45633"},{"ssid":"MatchX_MX190x_RHYM"},{"ssid":"UPC9541478_2GEXT"}]
        var readval = DecimalToText(response.toString(), "0x${serviceUuid.toString().toUpperCase().substring(4, 8)}");
        print("readval : " + readval.toString());

        print("hello");

        // {"tags":  [{macaddress: 24:6F:28:D1:5D:DC}, {"ssid":"Lukas-Wi-Fi"},{"ssid":"mkt-45633"},{"ssid":"MatchX_MX190x_RHYM"},{"ssid":"UPC9541478_2GEXT"}]}
        String jstring = '{"tags":  ' + readval.toString() + '}';
        print("jstring : " + jstring.toString());

        print("hello");

        // [{macaddress: 24:6F:28:D1:5D:DC}, {ssid: Lukas-Wi-Fi}, {ssid: mkt-45633}, {ssid: MatchX_MX190x_RHYM}, {ssid: UPC9541478_2GEXT}]
        // List: Cecodes strings to JSON objects.
        var tagObjsJson = jsonDecode(jstring)['tags'] as List;
        print("tagObjsJson : " + tagObjsJson.toString());

        // Get Macadress and remove from List
        // print("Macaddress: " + tagObjsJson[0]['macaddress'].toString());
        macaddress = tagObjsJson[0]['macaddress'].toString();
        tagObjsJson.removeAt(0);

        // Create List with ssid instance
        List<SSIDname> tagObjs = tagObjsJson.map((tagJson) => SSIDname.fromJson(tagJson)).toList();

        ssidList = tagObjs;

        print('read successful');
        // Assign list
        setState(() {
          hasBluetoothData = true;
        });
      }
      // } catch (e) {
      //   print('error decoding');
      // }
    }
  }

  Widget widgetWifiNetwork(SSIDname? htResultNetwork) {
    return Container(
      padding: const EdgeInsets.all(15),
      margin: const EdgeInsets.only(bottom: 2),
      decoration: const BoxDecoration(
          border: Border(
        bottom: BorderSide(
          color: Color.fromARGB(255, 185, 185, 185),
          width: 1.0,
        ),
      )),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text('${htResultNetwork?.ssid}'),
          ElevatedButton(
            onPressed: () {
              // Disconnect
              disconnectDevice();

              Navigator.push(
                context,
                PageRouteTransitionBuilder(
                    page: SetupWifiPasswordView(
                      ssid: htResultNetwork!.ssid,
                      macaddress: macaddress,
                      setupType: widget.setupType,
                    ),
                    effect: TransitionEffect.none),
              );
            },
            child: const Text('Connect'),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: SetupType.wifi == widget.setupType ? const Text('Change Wi-Fi') : const Text('Inklay Setup'),
      ),
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          //
          // State 0
          // How to enable this state:
          // Android: Remove location permission from app info
          // IOS: Remove bluetooth access from app settings
          Visibility(
            visible: isBleStatusUnauthorized,
            child: Container(
              padding: const EdgeInsets.all(15),
              // child: const Text('Please check your Bluetooth permissions in settings.'),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Platform.isAndroid
                      ? const Text('Please enable Location permission in App Info.')
                      : const Text('Please allow Bluetooth access in App Settings.'),
                  const ComponentsSpacer.small(),
                  ElevatedButton(
                    onPressed: (() {
                      AppSettings.openAppSettings();
                    }),
                    child: const Text('Open App Settings'),
                  ),
                ],
              ),
            ),
          ),
          //
          // State 0
          Visibility(
            visible: !isBleStatusUnauthorized,
            child: Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  //
                  // 1
                  // How to enable this state:
                  // Android: Turn off settings > location > use location
                  // IOS: Not available
                  Visibility(
                    visible: isLocationServicesDisabled,
                    child: Container(
                      padding: const EdgeInsets.all(15),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          const Text('Please turn on Location.'),
                          const ComponentsSpacer.small(),
                          // Works IOS
                          ElevatedButton(
                            onPressed: (() {
                              AppSettings.openLocationSettings();
                            }),
                            child: const Text('Open Location Settings'),
                          ),
                        ],
                      ),
                    ),
                  ),
                  //
                  // 1
                  Visibility(
                    visible: !isLocationServicesDisabled,
                    child: Expanded(
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          //
                          // 2
                          // Bluetooth OFF
                          Visibility(
                            visible: isBluetoothPoweredOff,
                            child: Container(
                              padding: const EdgeInsets.all(15.0),
                              child: const Text('Please turn on Bluetooth'),
                            ),
                          ),
                          //
                          // 2
                          // Bluetooth ON
                          Visibility(
                            visible: !isBluetoothPoweredOff,
                            child: Container(
                              padding: const EdgeInsets.all(15.0),
                              child: Column(
                                crossAxisAlignment: CrossAxisAlignment.start,
                                children: [
                                  //
                                  // 3
                                  Visibility(
                                    visible: !isBluetoothConnected,
                                    child: Row(
                                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                                      children: const [
                                        Text('BLE: Connecting ...'),
                                        SizedBox(
                                          height: 15.0,
                                          width: 15.0,
                                          child: CircularProgressIndicator(
                                            strokeWidth: 2,
                                          ),
                                        ),
                                      ],
                                    ),
                                  ),
                                  //
                                  // 3
                                  Visibility(
                                    visible: isBluetoothConnected,
                                    child: const Text('BLE: Connected to Inklay'),
                                  ),
                                ],
                              ),
                            ),
                          ),

                          // Instructions: Only show when data is not available
                          Visibility(
                            visible: !hasBluetoothData && !isBleStatusUnauthorized && !isLocationServicesDisabled && !isBluetoothPoweredOff,
                            child: Expanded(
                              child: Container(
                                // height: double.infinity,
                                decoration: const BoxDecoration(
                                  // color: Colors.red,
                                  border: Border(
                                    top: BorderSide(
                                      color: Color.fromARGB(255, 185, 185, 185),
                                      width: 1.0,
                                    ),
                                  ),
                                ),
                                padding: const EdgeInsets.all(15),
                                child: Column(
                                  // shrinkWrap: true,
                                  mainAxisAlignment: MainAxisAlignment.center,
                                  crossAxisAlignment: CrossAxisAlignment.center,
                                  children: const [
                                    Text(
                                      'Enable Bluetooth paring',
                                      style: TextStyle(fontWeight: FontWeight.bold),
                                    ),
                                    ComponentsSpacer.small(),
                                    Text('1. Remove the magnetic front cover.'),
                                    Text('2. Press and hold the power button for 2 seconds.'),
                                    Text('3. The LED will turn blue.'),
                                  ],
                                ),
                              ),
                            ),
                          ),

                          // Wifi networks: Only show when data is available
                          Visibility(
                            visible: hasBluetoothData,
                            child: Expanded(
                              child: Container(
                                decoration: const BoxDecoration(
                                  border: Border(
                                    top: BorderSide(
                                      color: Color.fromARGB(255, 185, 185, 185),
                                      width: 1.0,
                                    ),
                                  ),
                                ),
                                child: ListView.builder(
                                  padding: const EdgeInsets.all(0.0),
                                  itemCount: ssidList?.length,
                                  itemBuilder: (BuildContext context, int index) {
                                    return widgetWifiNetwork(ssidList?[index]);
                                  },
                                ),
                              ),
                            ),
                          ),
                        ],
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}
Liberations commented 1 year ago

after case DeviceConnectionState.connected: try to discoverServices,like below

var resUUID = await flutterReactiveBle.discoverServices(id);
 resUUID.forEach((element) {
            debugPrint("resUUID" + element.serviceId.toString());
   });
v11 commented 1 year ago

This are the services:

I/flutter (11430): resUUID 00001801-0000-1000-8000-00805f9b34fb
I/flutter (11430): resUUID 00001800-0000-1000-8000-00805f9b34fb
I/flutter (11430): resUUID 4fafc201-1fb5-459e-8fcc-c5c9c331914b

And what should i do with this services?

v11 commented 1 year ago

I think, the problem was related to a buffer which was too small. The JSON response did not have a complete body including closing brackets, which then failed on decoding.