title: Control ESP32 via Flutter BLE
body: <p>I am using this library (<a href="https://pub.dev/packages/flutter_blue/example" rel="nofollow noreferrer">https://pub.dev/packages/flutter_blue/example</a>) to control my ESP32 (Arduino). I have two buttons that work perfectly, one to turn off the led and one to turn on the led. My problem is that I want to receive data that the arduino sends (notify). I don’t know how to do it.</p>
  <h2>main.dart</h2>
  <pre><code>// Copyright 2017, Paul DeMarco.
  // All rights reserved. Use of this source code is governed by a
  // BSD-style license that can be found in the LICENSE file.
  
  import 'dart:async';
  import 'dart:convert' show utf8;
  
  import 'package:flutter/material.dart';
  import 'package:flutter_blue/flutter_blue.dart';
  import 'package:minertti/widgets.dart';
  
  void main() {
    runApp(FlutterBlueApp());
  }
  
  class FlutterBlueApp extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
      return MaterialApp(
        color: Colors.lightBlue,
        home: StreamBuilder&lt;BluetoothState&gt;(
            stream: FlutterBlue.instance.state,
            initialData: BluetoothState.unknown,
            builder: (c, snapshot) {
              final state = snapshot.data;
              if (state == BluetoothState.on) {
                return FindDevicesScreen();
              }
              return BluetoothOffScreen(state: state);
            }),
      );
    }
  }
  
  class BluetoothOffScreen extends StatelessWidget {
    const BluetoothOffScreen({Key? key, this.state}) : super(key: key);
  
    final BluetoothState? state;
  
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        backgroundColor: Colors.lightBlue,
        body: Center(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: &lt;Widget&gt;[
              Icon(
                Icons.bluetooth_disabled,
                size: 200.0,
                color: Colors.white54,
              ),
              Text(
                'Bluetooth Adapter is ${state != null ? state.toString().substring(15) : 'not available'}.',
                style: Theme.of(context)
                    .primaryTextTheme
                    .subtitle1
                    ?.copyWith(color: Colors.white),
              ),
            ],
          ),
        ),
      );
    }
  }
  
  class FindDevicesScreen extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          title: Text('Find Devices'),
        ),
        body: RefreshIndicator(
          onRefresh: () =&gt;
              FlutterBlue.instance.startScan(timeout: Duration(seconds: 4)),
          child: SingleChildScrollView(
            child: Column(
              children: &lt;Widget&gt;[
                StreamBuilder&lt;List&lt;BluetoothDevice&gt;&gt;(
                  stream: Stream.periodic(Duration(seconds: 2))
                      .asyncMap((_) =&gt; FlutterBlue.instance.connectedDevices),
                  initialData: [],
                  builder: (c, snapshot) =&gt; Column(
                    children: snapshot.data!
                        .map((d) =&gt; ListTile(
                              title: Text(d.name),
                              subtitle: Text(d.id.toString()),
                              trailing: StreamBuilder&lt;BluetoothDeviceState&gt;(
                                stream: d.state,
                                initialData: BluetoothDeviceState.disconnected,
                                builder: (c, snapshot) {
                                  if (snapshot.data ==
                                      BluetoothDeviceState.connected) {
                                    return RaisedButton(
                                      child: Text('OPEN'),
                                      onPressed: () =&gt; Navigator.of(context).push(
                                          MaterialPageRoute(
                                              builder: (context) =&gt;
                                                  DeviceScreen(device: d))),
                                    );
                                  }
                                  return Text(snapshot.data.toString());
                                },
                              ),
                            ))
                        .toList(),
                  ),
                ),
                StreamBuilder&lt;List&lt;ScanResult&gt;&gt;(
                  stream: FlutterBlue.instance.scanResults,
                  initialData: [],
                  builder: (c, snapshot) =&gt; Column(
                    children: snapshot.data!
                        .map(
                          (r) =&gt; ScanResultTile(
                            result: r,
                            onTap: () =&gt; Navigator.of(context)
                                .push(MaterialPageRoute(builder: (context) {
                              r.device.connect();
                              return DeviceScreen(device: r.device);
                            })),
                          ),
                        )
                        .toList(),
                  ),
                ),
              ],
            ),
          ),
        ),
        floatingActionButton: StreamBuilder&lt;bool&gt;(
          stream: FlutterBlue.instance.isScanning,
          initialData: false,
          builder: (c, snapshot) {
            if (snapshot.data!) {
              return FloatingActionButton(
                child: Icon(Icons.stop),
                onPressed: () =&gt; FlutterBlue.instance.stopScan(),
                backgroundColor: Colors.red,
              );
            } else {
              return FloatingActionButton(
                  child: Icon(Icons.search),
                  onPressed: () =&gt; FlutterBlue.instance
                      .startScan(timeout: Duration(seconds: 4)));
            }
          },
        ),
      );
    }
  }
  
  class DeviceScreen extends StatelessWidget {
    const DeviceScreen({Key? key, required this.device}) : super(key: key);
  
    final BluetoothDevice device;
  
    List&lt;Widget&gt; _buildServiceTiles(List&lt;BluetoothService&gt; services) {
      return services
          //just show the last service
          .sublist(services.length - 1)
          .map(
            (s) =&gt; ServiceTile(
              service: s,
              characteristicTiles: s.characteristics
                  .map(
                    (c) =&gt; CharacteristicTile(
                      characteristic: c,
                      //turnOff: () =&gt; c.read(),
                      turnOff: () =&gt; c.write(utf8.encode(&quot;0&quot;)),
                      turnOn: () =&gt; c.write(utf8.encode(&quot;1&quot;)),
                      extraButton: () =&gt; c.write(utf8.encode(&quot;2&quot;)),
  
                      onNotificationPressed: () async {
                        await c.setNotifyValue(!c.isNotifying);
                        await c.read();
                      },
                      descriptorTiles: c.descriptors
                          .map(
                            (d) =&gt; DescriptorTile(
                              descriptor: d,
                              //turnOff: () =&gt; d.read(),
                              //turnOn: () =&gt; d.read(),
                            ),
                          )
                          .toList(),
                    ),
                  )
                  .toList(),
            ),
          )
          .toList();
    }
  
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          title: Text(device.name),
          actions: &lt;Widget&gt;[
            StreamBuilder&lt;BluetoothDeviceState&gt;(
              stream: device.state,
              initialData: BluetoothDeviceState.connecting,
              builder: (c, snapshot) {
                VoidCallback? onPressed;
                String text;
                switch (snapshot.data) {
                  case BluetoothDeviceState.connected:
                    onPressed = () =&gt; device.disconnect();
                    text = 'DISCONNECT';
                    break;
                  case BluetoothDeviceState.disconnected:
                    onPressed = () =&gt; device.connect();
                    text = 'CONNECT';
                    break;
                  default:
                    onPressed = null;
                    text = snapshot.data.toString().substring(21).toUpperCase();
                    break;
                }
                return FlatButton(
                    onPressed: onPressed,
                    child: Text(
                      text,
                      style: Theme.of(context)
                          .primaryTextTheme
                          .button
                          ?.copyWith(color: Colors.white),
                    ));
              },
            )
          ],
        ),
        body: SingleChildScrollView(
          child: Column(
            children: &lt;Widget&gt;[
              StreamBuilder&lt;BluetoothDeviceState&gt;(
                stream: device.state,
                initialData: BluetoothDeviceState.connecting,
                builder: (c, snapshot) =&gt; ListTile(
                  leading: (snapshot.data == BluetoothDeviceState.connected)
                      ? Icon(Icons.bluetooth_connected)
                      : Icon(Icons.bluetooth_disabled),
                  title: Text(
                      'Device is ${snapshot.data.toString().split('.')[1]}.'),
                  subtitle: Text('${device.id}'),
                  trailing: StreamBuilder&lt;bool&gt;(
                    stream: device.isDiscoveringServices,
                    initialData: false,
                    builder: (c, snapshot) =&gt; IndexedStack(
                      index: snapshot.data! ? 1 : 0,
                      children: &lt;Widget&gt;[
                        IconButton(
                          icon: Icon(Icons.refresh),
                          onPressed: () =&gt; device.discoverServices(),
                        ),
                        IconButton(
                          icon: SizedBox(
                            child: CircularProgressIndicator(
                              valueColor: AlwaysStoppedAnimation(Colors.grey),
                            ),
                            width: 18.0,
                            height: 18.0,
                          ),
                          onPressed: null,
                        )
                      ],
                    ),
                  ),
                ),
              ),
              StreamBuilder&lt;int&gt;(
                stream: device.mtu,
                initialData: 0,
                builder: (c, snapshot) =&gt; ListTile(
                  title: Text('MTU Size'),
                  subtitle: Text('${snapshot.data} bytes'),
                  trailing: IconButton(
                    icon: Icon(Icons.edit),
                    onPressed: () =&gt; device.requestMtu(223),
                  ),
                ),
              ),
              StreamBuilder&lt;List&lt;BluetoothService&gt;&gt;(
                stream: device.services,
                initialData: [],
                builder: (c, snapshot) {
                  return Column(
                    children: _buildServiceTiles(snapshot.data!),
                  );
                },
              ),
            ],
          ),
        ),
      );
    }
  }
  </code></pre>
  <h2>device.dart</h2>
  <pre><code>// Copyright 2017, Paul DeMarco.
  // All rights reserved. Use of this source code is governed by a
  // BSD-style license that can be found in the LICENSE file.
  import 'dart:convert' show utf8;
  import 'package:flutter/material.dart';
  import 'package:flutter_blue/flutter_blue.dart';
  
  class ScanResultTile extends StatelessWidget {
    const ScanResultTile({Key? key, required this.result, this.onTap})
        : super(key: key);
  
    final ScanResult result;
    final VoidCallback? onTap;
  
    Widget _buildTitle(BuildContext context) {
      if (result.device.name.length &gt; 0) {
        return Column(
          mainAxisAlignment: MainAxisAlignment.start,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: &lt;Widget&gt;[
            Text(
              result.device.name,
              overflow: TextOverflow.ellipsis,
            ),
            Text(
              result.device.id.toString(),
              style: Theme.of(context).textTheme.caption,
            )
          ],
        );
      } else {
        return Text(result.device.id.toString());
      }
    }
  
    Widget _buildAdvRow(BuildContext context, String title, String value) {
      return Padding(
        padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0),
        child: Row(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: &lt;Widget&gt;[
            Text(title, style: Theme.of(context).textTheme.caption),
            SizedBox(
              width: 12.0,
            ),
            Expanded(
              child: Text(
                value,
                style: Theme.of(context)
                    .textTheme
                    .caption
                    ?.apply(color: Colors.black),
                softWrap: true,
              ),
            ),
          ],
        ),
      );
    }
  
    String getNiceHexArray(List&lt;int&gt; bytes) {
      return '[${bytes.map((i) =&gt; i.toRadixString(16).padLeft(2, '0')).join(', ')}]'
          .toUpperCase();
    }
  
    String getNiceManufacturerData(Map&lt;int, List&lt;int&gt;&gt; data) {
      if (data.isEmpty) {
        return 'N/A';
      }
      List&lt;String&gt; res = [];
      data.forEach((id, bytes) {
        res.add(
            '${id.toRadixString(16).toUpperCase()}: ${getNiceHexArray(bytes)}');
      });
      return res.join(', ');
    }
  
    String getNiceServiceData(Map&lt;String, List&lt;int&gt;&gt; data) {
      if (data.isEmpty) {
        return 'N/A';
      }
      List&lt;String&gt; res = [];
      data.forEach((id, bytes) {
        res.add('${id.toUpperCase()}: ${getNiceHexArray(bytes)}');
      });
      return res.join(', ');
    }
  
    @override
    Widget build(BuildContext context) {
      return ExpansionTile(
        title: _buildTitle(context),
        leading: Text(result.rssi.toString()),
        trailing: RaisedButton(
          child: Text('CONNECT'),
          color: Colors.black,
          textColor: Colors.white,
          onPressed: (result.advertisementData.connectable) ? onTap : null,
        ),
        children: &lt;Widget&gt;[
          _buildAdvRow(
              context, 'Complete Local Name', result.advertisementData.localName),
          _buildAdvRow(context, 'Tx Power Level',
              '${result.advertisementData.txPowerLevel ?? 'N/A'}'),
          _buildAdvRow(context, 'Manufacturer Data',
              getNiceManufacturerData(result.advertisementData.manufacturerData)),
          _buildAdvRow(
              context,
              'Service UUIDs',
              (result.advertisementData.serviceUuids.isNotEmpty)
                  ? result.advertisementData.serviceUuids.join(', ').toUpperCase()
                  : 'N/A'),
          _buildAdvRow(context, 'Service Data',
              getNiceServiceData(result.advertisementData.serviceData)),
        ],
      );
    }
  }
  
  class ServiceTile extends StatelessWidget {
    final BluetoothService service;
    final List&lt;CharacteristicTile&gt; characteristicTiles;
  
    const ServiceTile(
        {Key? key, required this.service, required this.characteristicTiles})
        : super(key: key);
  
    @override
    Widget build(BuildContext context) {
      if (characteristicTiles.length &gt; 0) {
        return ExpansionTile(
          title: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: &lt;Widget&gt;[
              Text('Service'),
              Text('0x${service.uuid.toString().toUpperCase().substring(4, 8)}',
                  style: Theme.of(context).textTheme.bodyText1?.copyWith(
                      color: Theme.of(context).textTheme.caption?.color))
            ],
          ),
          children: characteristicTiles,
        );
      } else {
        return ListTile(
          title: Text('Service'),
          subtitle:
              Text('0x${service.uuid.toString().toUpperCase().substring(4, 8)}'),
        );
      }
    }
  }
  
  class CharacteristicTile extends StatelessWidget {
    final BluetoothCharacteristic characteristic;
    final List&lt;DescriptorTile&gt; descriptorTiles;
    final VoidCallback? turnOff;
    final VoidCallback? extraButton;
    final VoidCallback? turnOn;
    final VoidCallback? onNotificationPressed;
  
    const CharacteristicTile(
        {Key? key,
        required this.characteristic,
        required this.descriptorTiles,
        this.turnOff,
        this.extraButton,
        this.turnOn,
        this.onNotificationPressed})
        : super(key: key);
  
    @override
    Widget build(BuildContext context) {
      return StreamBuilder&lt;List&lt;int&gt;&gt;(
        stream: characteristic.value,
        initialData: characteristic.lastValue,
        builder: (c, snapshot) {
          final value = snapshot.data;
          return ExpansionTile(
            title: ListTile(
              title: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                crossAxisAlignment: CrossAxisAlignment.start,
                children: &lt;Widget&gt;[
                  Text('Characteristic'),
                  Text(
                      '0x${characteristic.uuid.toString().toUpperCase().substring(4, 8)}',
                      style: Theme.of(context).textTheme.bodyText1?.copyWith(
                          color: Theme.of(context).textTheme.caption?.color))
                ],
              ),
              subtitle: Text(value.toString()),
              contentPadding: EdgeInsets.all(0.0),
            ),
            trailing: Row(
              mainAxisSize: MainAxisSize.min,
              children: &lt;Widget&gt;[
                IconButton(
                  icon: Icon(
                    Icons.lightbulb_outline,
                    color: Theme.of(context).iconTheme.color?.withOpacity(0.5),
                  ),
                  onPressed: turnOff,
                ),
                IconButton(
                  icon: Icon(Icons.lightbulb,
                      color: Theme.of(context).iconTheme.color?.withOpacity(0.5)),
                  onPressed: turnOn,
                ),
                //iconButton for add
                IconButton(
                  icon: Icon(
                    Icons.add_circle,
                    color: Theme.of(context).iconTheme.color?.withOpacity(0.5),
                  ),
                  onPressed: extraButton,
                ),
                IconButton(
                  icon: Icon(
                      characteristic.isNotifying
                          ? Icons.sync_disabled
                          : Icons.sync,
                      color: Theme.of(context).iconTheme.color?.withOpacity(0.5)),
                  onPressed: onNotificationPressed,
                )
              ],
            ),
            children: descriptorTiles,
          );
        },
      );
    }
  }
  
  class DescriptorTile extends StatelessWidget {
    final BluetoothDescriptor descriptor;
    final VoidCallback? turnOff;
    final VoidCallback? turnOn;
    final VoidCallback? extraButton;
  
    const DescriptorTile(
        {Key? key,
        required this.descriptor,
        this.turnOff,
        this.extraButton,
        this.turnOn})
        : super(key: key);
  
    @override
    Widget build(BuildContext context) {
      return ListTile(
        title: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: &lt;Widget&gt;[
            Text('Descriptor'),
            Text('0x${descriptor.uuid.toString().toUpperCase().substring(4, 8)}',
                style: Theme.of(context)
                    .textTheme
                    .bodyText1
                    ?.copyWith(color: Theme.of(context).textTheme.caption?.color))
          ],
        ),
        subtitle: StreamBuilder&lt;List&lt;int&gt;&gt;(
          stream: descriptor.value,
          initialData: descriptor.lastValue,
          builder: (c, snapshot) =&gt; Text(snapshot.data.toString()),
        ),
        trailing: Row(
          mainAxisSize: MainAxisSize.min,
          children: &lt;Widget&gt;[
            IconButton(
              icon: Icon(
                Icons.lightbulb_outline,
                color: Theme.of(context).iconTheme.color?.withOpacity(0.5),
              ),
              onPressed: turnOff,
            ),
            IconButton(
              icon: Icon(
                Icons.lightbulb,
                color: Theme.of(context).iconTheme.color?.withOpacity(0.5),
              ),
              onPressed: turnOn,
            ),
            IconButton(
              icon: Icon(
                Icons.add_circle,
                color: Theme.of(context).iconTheme.color?.withOpacity(0.5),
              ),
              onPressed: extraButton,
            )
          ],
        ),
      );
    }
  }
  
  class AdapterStateTile extends StatelessWidget {
    const AdapterStateTile({Key? key, required this.state}) : super(key: key);
  
    final BluetoothState state;
  
    @override
    Widget build(BuildContext context) {
      return Container(
        color: Colors.redAccent,
        child: ListTile(
          title: Text(
            'Bluetooth adapter is ${state.toString().substring(15)}',
            style: Theme.of(context).primaryTextTheme.subtitle1,
          ),
          trailing: Icon(
            Icons.error,
            color: Theme.of(context).primaryTextTheme.subtitle1?.color,
          ),
        ),
      );
    }
  }
  </code></pre>
  <h2>ESP32 Code:</h2>
  <pre><code>#include &lt;BLEDevice.h&gt;
  #include &lt;BLEServer.h&gt;
  #include &lt;BLEUtils.h&gt;
  #include &lt;BLE2902.h&gt;
  
  BLECharacteristic *pCharacteristic;
  bool deviceConnected = false;
  float txValue = 0;
  const int readPin = 32; // Use GPIO number. See ESP32 board pinouts
  const int LED = 2; // Could be different depending on the dev board. I used the DOIT ESP32 dev board.
  
  //std::string rxValue; // Could also make this a global var to access it in loop()
  
  // See the following for generating UUIDs:
  // https://www.uuidgenerator.net/
  
  #define SERVICE_UUID           &quot;6E400001-B5A3-F393-E0A9-E50E24DCCA9E&quot; // UART service UUID
  #define CHARACTERISTIC_UUID_RX &quot;6E400002-B5A3-F393-E0A9-E50E24DCCA9E&quot;
  #define CHARACTERISTIC_UUID_TX &quot;6E400003-B5A3-F393-E0A9-E50E24DCCA9E&quot;
  
  class MyServerCallbacks: public BLEServerCallbacks {
      void onConnect(BLEServer* pServer) {
        deviceConnected = true;
      };
  
      void onDisconnect(BLEServer* pServer) {
        deviceConnected = false;
      }
  };
  
  class MyCallbacks: public BLECharacteristicCallbacks {
      void onWrite(BLECharacteristic *pCharacteristic) {
        std::string rxValue = pCharacteristic-&gt;getValue();
  
        if (rxValue.length() &gt; 0) {
          Serial.println(&quot;*********&quot;);
          Serial.print(&quot;Received Value: &quot;);
  
          for (int i = 0; i &lt; rxValue.length(); i++) {
            Serial.print(rxValue[i]);
          }
  
          Serial.println();
  
          // Do stuff based on the command received from the app
          if (rxValue.find(&quot;1&quot;) != -1) { 
            Serial.println(&quot;Turning ON!&quot;);
            digitalWrite(LED, HIGH);
          }
          else if (rxValue.find(&quot;0&quot;) != -1) {
            Serial.println(&quot;Turning OFF!&quot;);
            digitalWrite(LED, LOW);
          }
  
          Serial.println();
          Serial.println(&quot;*********&quot;);
        }
      }
  };
  
  void setup() {
    Serial.begin(115200);
  
    pinMode(LED, OUTPUT);
  
    // Create the BLE Device
    BLEDevice::init(&quot;ESP32&quot;); // Give it a name
  
    // Create the BLE Server
    BLEServer *pServer = BLEDevice::createServer();
    pServer-&gt;setCallbacks(new MyServerCallbacks());
  
    // Create the BLE Service
    BLEService *pService = pServer-&gt;createService(SERVICE_UUID);
  
    // Create a BLE Characteristic
    pCharacteristic = pService-&gt;createCharacteristic(
                        CHARACTERISTIC_UUID_TX,
                        BLECharacteristic::PROPERTY_NOTIFY
                      );
                        
    pCharacteristic-&gt;addDescriptor(new BLE2902());
  
    BLECharacteristic *pCharacteristic = pService-&gt;createCharacteristic(
                                           CHARACTERISTIC_UUID_RX,
                                           BLECharacteristic::PROPERTY_WRITE
                                         );
  
    pCharacteristic-&gt;setCallbacks(new MyCallbacks());
  
    // Start the service
    pService-&gt;start();
  
    // Start advertising
    pServer-&gt;getAdvertising()-&gt;start();
    Serial.println(&quot;Waiting a client connection to notify...&quot;);
  }
  
  void loop() {
    if (deviceConnected) {
      // Fabricate some arbitrary junk for now...
      txValue = analogRead(readPin) / 3.456; // This could be an actual sensor reading!
  
      // Let's convert the value to a char array:
      char txString[8]; // make sure this is big enuffz
      dtostrf(txValue, 1, 2, txString); // float_val, min_width, digits_after_decimal, char_buffer
      
  //    pCharacteristic-&gt;setValue(&amp;txValue, 1); // To send the integer value
  //    pCharacteristic-&gt;setValue(&quot;Hello!&quot;); // Sending a test message
      pCharacteristic-&gt;setValue(txString);
      
      pCharacteristic-&gt;notify(); // Send the value to the app!
      Serial.print(&quot;*** Sent Value: &quot;);
      Serial.print(txString);
      Serial.println(&quot; ***&quot;);
  
      // You can add the rxValue checks down here instead
      // if you set &quot;rxValue&quot; as a global var at the top!
      // Note you will have to delete &quot;std::string&quot; declaration
      // of &quot;rxValue&quot; in the callback function.
  //    if (rxValue.find(&quot;A&quot;) != -1) { 
  //      Serial.println(&quot;Turning ON!&quot;);
  //      digitalWrite(LED, HIGH);
  //    }
  //    else if (rxValue.find(&quot;B&quot;) != -1) {
  //      Serial.println(&quot;Turning OFF!&quot;);
  //      digitalWrite(LED, LOW);
  //    }
    }
    delay(1000);
  }
  </code></pre>
  <p>The app correctly receives the information, but in a strange code that I don't understand. How do I convert it to letters and numbers?</p>
  <p>In device.dart you can see that there is a part that says: &quot;<strong>subtitle: Text(value.toString())</strong>&quot;</p>
  <p>That is the one that sends the data, but I don't understand it.</p>
  <pre><code>...
  builder: (c, snapshot) {
          final value = snapshot.data;
          return ExpansionTile(
            title: ListTile(
              title: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                crossAxisAlignment: CrossAxisAlignment.start,
                children: &lt;Widget&gt;[
                  Text('Characteristic'),
                  Text(
                      '0x${characteristic.uuid.toString().toUpperCase().substring(4, 8)}',
                      style: Theme.of(context).textTheme.bodyText1?.copyWith(
                          color: Theme.of(context).textTheme.caption?.color))
                ],
              ),
              subtitle: Text(value.toString()),
  
  ...
  </code></pre>
  <p>Arduino sends: 98.96 and I receive this data from <strong>value.toString()</strong>: [57, 56, 46, 57, 54]</p>