engineering

December 12, 2019   |   15min read

Bluetooth Low Energy Simulator—A New Hope in IoT Development

We live in the XXI century, where animals might still not talk to us, but things are definitely starting to. We’re walking around with our mission control centers (aka smartphones) that can handle our money, our communication, our news, our smart homes and every other smart solution that surrounds us, in short—our lives.

Training for a race? You can do so with a smart muscle-oxygen monitor Humon. Want to improve your sleep quality? Get a smartband, smartwatch, or a sleep-tracker (all of which use Bluetooth Low Energy and communicate with your phone). Trying to get somewhere on a bicycle? Use BLE-enabled navigation to minimize distractions like notifications but still know how to get where you want. Perhaps you’d like to know when the office coffee is ready? We got you covered, man!

But with the advent of the Internet of Things and the increasing interconnectedness of the world came new challenges for us, developers.

Developing a BLE-enabled app

If you ever worked on a mobile app that connects to a Bluetooth Low Energy device, you know it’s not the easiest task. There are many screens and internal states in the app that are connected to the peripheral and its behavior. The app is often being created alongside the firmware (sometimes even hardware (!!!)) of the device, the connection can be fickle, the external interface unstable, internal logic buggy. As a result, the team is wasting a lot of time on Known Issues™ when testing their features.

In general, there are two ways to approach this:

  1. First, there’s an easy, time-consuming way: use a physical smartphone and BLE device, go through all the hassle to connect and set up the device to the state we need it to be in and recreate the testing conditions.
  2. Then there’s the hard way: abstract the device, create a simple mock-up that can hide away the actual BLE handling. This will let you work on an Android emulator/iOS simulator, saving you some time later and let you run automated tests on your CIs. At the same time, it increases the maintenance cost and introduces a new risk by not testing the actual communication every time you run your code. After all, that Bluetooth peripheral is probably at the heart of our application and it mustn’t disconnect unexpectedly or behave strangely.

Our friends at Frontside—Austin-based frontend software engineering and architecture consultancy—have recognized the need for a more effective solution. They asked us to develop an open source solution for a reliable BLE-enabled app development that everyone could benefit from.

And so in comes…

BLEmulator /pronun.: bleh-mulator/, a Bluetooth Low Energy simulator.

The future is here

BLEmulator is here to make your life easier! It handles all your production BLE-related code and simulates the behavior of a real peripheral and system’s Bluetooth stack. It is simple and flexible, letting you create both the basic mock and a full simulation of a BLE-enabled device. And the best thing—it’s open sourced!

It lets you test the concept behind your device without the cost of the hardware prototyping. It lets your mobile team move forward without waiting for firmware or prototypes, just with a specification. It lets you work using only Android emulator or iOS simulator, thus allowing for greater mobility, easier remote work and avoiding the limited availability of physical smartphones. It lets you test your apps in automated tests run by your CI.

Currently BLEmulator is available for Flutter and works only with our FlutterBleLib.

How it works

Flutter, as a multiplatform framework, needs native dependencies for both platforms. Normally, those would be a part of the library itself, but we used a different approach here. Polidea has a React Native BLE library, called react-native-ble-plx, an awesome work of our colleagues. We’ve decided to extract all the native logic from it to a separate library, known as Multiplatform BLE Adapter. This way we have created a common core that is used by both react-native-ble-plx and our Flutter plugin, FlutterBleLib. As a side effect we created a common abstraction used in the native bridge, the BleAdapter, which is a perfect entry point for simulation!

This is what FlutterBleLib’s data flow looks like:

  1. Call a method on one of the FlutterBleLib objects (BleManager, Peripheral, Service, Characteristic)
  2. Dart code sends a method name and its parameters over to a native bridge
  3. Native bridge receives the data and deserializes it if need be
  4. Native bridge calls an appropriate method on the BleAdapter instance from the Multiplatform BLE Adapter
  5. BleAdapter calls the method on either RxAndroidBle or RxBluetoothKit, depending on the platform
  6. RxAndroidBle/RxBluetoothKit calls a system method
  7. System returns a response to the intermediary
  8. Intermediary returns the response to the BleAdapter
  9. BleAdapter responds to the native bridge
  10. Native bridge maps the response and sends it over to Dart
  11. Dart parses the response and returns it to the original caller

text

We had our work laid out for us—points 4 to 9 are the ideal entry points with a set external contract. By injecting a different implementation of the BleAdapter you can switch the simulation on anytime we want.

Next, we had to decide where the simulation should take place. We opted for keeping as much of the simulation as possible in Dart, for two main reasons:

  1. One definition for both platforms

It is important to minimize both the number of places where it’s possible to make a mistake and the amount of work needed to create a simulated peripheral and maintain it.

  1. Language native to the platform

This has a few advantages. First, developers will work more efficiently with a known tool, so we should avoid introducing additional languages. Secondly, we didn’t want to limit things that are possible to do on the simulated peripheral. If you’d like to request an answer from some HTTP servers (perhaps with a more advanced simulation, running the firmware itself?), you can do so without any problems with the same code you’d write for any other HTTP communication in your app.

The simulated call-route looks like this:

  1. Call a method on one of the FlutterBleLib objects (BleManager, Peripheral, Service, Characteristic)
  2. Dart code sends the method’s name and its parameters over to the native bridge
  3. Native bridge receives the data and deserializes it if need be
  4. Native bridge calls the appropriate method on the SimulatedAdapter

    Changed portion starts now

  5. BleAdapter—in this case the one from the simulator—forwards the call to the BLEmulator’s native bridge
  6. BLEmulator either does the logic itself (if it doesn’t involve the peripheral) or calls the appropriate method on the peripheral supplied by the user
  7. Response is passed to the BLEmulator’s native bridge
  8. BLEmulator’s native bridge passes the response to the SimulatedAdapter
  9. SimulatedAdapter responds to the native bridge

    Back to original flow

  10. Native bridge maps the response and sends it over to Dart
  11. Dart parses the response and returns it to the original caller

text

This way you use all of your BLE handling code and work on types provided by FlutterBleLib no matter which backend you are using, be that the real system BT stack or the simulation. This also means you can test the interaction with a peripheral in automated tests on your CI!

How to use it

We’ve covered how it works and what possibilities it provides, so let’s now jump into how to use it.

  1. Add dependency to blemulator in your pubspec.yml
  2. Create your own simulated peripheral using plugin-provided classes SimulatedPeripheral, SimulatedService and SimulatedCharacteristic (I’ll cover it in detail in the next section)
  3. Add the peripheral to the BLEmulator using Blemulator.addPeripheral(SimulatedPeripheral)
  4. Call Blemulator.simulate() before calling BleManager.createClient() from FlutterBleLib

That’s it, only four steps and you’re up and running! Well, I admit that the most complex step has kind of been skipped, so let’s talk about the second point—defining the peripheral.

Peripheral contract

I will base the following examples on CC2541 SensorTag by Texas Instruments, focusing on IR temperature sensor.

We need to know how the UUIDs of the service and its characteristics look like. We’re interested in two places in the documentation.

text

text

UUIDs we’re interested in:

  • IR temperature service: F000AA00-0451-4000-B000-000000000000 This service contains all of the characteristics pertaining to the temperature sensor.
  • IR temperature data: F000AA01-0451-4000-B000-000000000000

    The temperature data that can be read or monitored. The format of the data is ObjectLSB:ObjectMSB:AmbientLSB:AmbientMSB. It will emit a notification every configurable period while monitored.

  • IR temperature config: F000AA02-0451-4000-B000-000000000000

    On/off switch for the sensor. There are two valid values for this characteristic::

    • 00—sensor put to sleep (IR temperature data will be four bytes of zeros)
    • 01—sensor enabled (IR temperature data will emit correct readings)
  • IR temperature period: F000AA03-0451-4000-B000-000000000000

    The interval between notifications.The lower limit is 300 ms, the upper limit is 1000 ms. The value of the characteristic is multiplied by 10, so the supported values are between 30 and 100.

That’s all we were looking for, so let’s go to the implementation!

Simplest peripheral

The simplest simulation will accept any value and succeed in all operations.

In Dart it looks like this:

class SensorTag extends SimulatedPeripheral {
  SensorTag(String id)
      : super(
            name: "SensorTag",
            id: id,
            advertisementInterval: Duration(milliseconds: 800),
            services: [
              SimulatedService(
                  uuid: "F000AA00-0451-4000-B000-000000000000",
                  isAdvertised: true,
                  characteristics: [
                    SimulatedCharacteristic(
                        uuid: "F000AA01-0451-4000-B000-000000000000",
                        value: Uint8List.fromList([101, 12, 64, 12]),
                        isNotifiable: true,
                        isWritableWithResponse: false,
                        isWritableWithoutResponse: false,
                        convenienceName: "IR Temperature Data"),
                    SimulatedCharacteristic(
                        uuid: "F000AA02-0451-4000-B000-000000000000",
                        value: Uint8List.fromList([0]),
                        convenienceName: "IR Temperature Config"),
                    SimulatedCharacteristic(
                        uuid: "F000AA03-0451-4000-B000-000000000000",
                        value: Uint8List.fromList([50]),
                        convenienceName: "IR Temperature Period"),
                  ],
                  convenienceName: "Temperature service")
            ]);
}

Short and concise, looks almost like a JSON.

Here’s what happens: we created a peripheral named SensorTag which has a runtime specified ID (any string is fine, but it must be unique among peripherals known to BLEmulator). While peripheral scan is turned on it will advertise using default scan information every 800 milliseconds. It contains one service, whose UUID is contained in the advertisement data. The service contains 3 characteristics, just like on a real device, and all of them are readable. First of the characteristics cannot be written to, but supports notifications; the other two cannot be monitored but can be written to. There are no invalid values for the characteristics. The argument convenienceName is not used in any way by the BLEmulator, but makes the definition easier to read.

IR Temperature Config and IR Temperature Period accept and set any value passed to it. IR Temperature Data characteristic supports notifications, but never actually sends any, since we have not defined them in any way.

BLEmulator provides the basic behavior out of the box, taking care of the happy path for all your constructs, minimizing the amount of work needed. While it may suffice for some tests and basic specification adherence checks, it doesn’t try to behave like a real device.

We need to implement a custom behavior!

We kept both simplicity and flexibility when creating BLEmulator. We wanted to give developers the necessary control over every aspect of the created peripheral, while requiring as little work from them as possible to create a working definition. To achieve this goal, we’ve decided to create the default implementation of all the methods that might be your entry point for custom behavior and let you decide what to override.

Now, let’s add some logic.

Make connection take time

A real peripheral will probably take some time before getting connected. To achieve this, you only need to override one method:

  
  Future<bool> onConnectRequest() async {
    await Future.delayed(Duration(milliseconds: 200));
    return super.onConnectRequest();
  }

That’s it. Now the connection takes 200 milliseconds!

Denying connection

A similar case. We want to keep the delay, but return an error that connection could not be established. You could override the method and throw a SimulatedBleError yourself, but you can also do:

  
  Future<bool> onConnectRequest() async {
    await Future.delayed(Duration(milliseconds: 200));
    return false;
  }

Disconnection initialized by the peripheral

Let’s say you want to check the reconnection process or simulate getting out of range. You can ask a colleague to run to the other side of the office with a real peripheral, or add some debug button and in its onPress do:

yourPeripheralInstance.onDisconnect();

(Although the first option seems to be more satisfying.)

Modifying RSSI in the scan information

Alright, let’s say we want to sort peripherals by their perceived signal strength and we need to test it. We create a few simulated peripherals, leave one with static RSSI and then in the other one do:


ScanResult scanResult() {
    scanInfo.rssi = -20 - Random.nextInt(50);
    return super.scanResult();
}

This way, you can have a couple of devices with variable RSSI and test the feature.

Negotiating MTU

BLEmulator does most of the logic by itself, thus limiting the MTU to supported range of 23 to 512, but if you need to limit it further, you should override the requestMtu() method:

const my_max_mtu = 256;


Future<int> requestMtu(int requestedMtu) async {
  return super.requestMtu(min(requestedMtu, my_max_mtu));
}

BLEmulator will automatically negotiate the highest supported MTU on iOS.

Forcing values to supported range

To limit values accepted by a characteristic, you have to create a new class extending SimulatedCharacteristic.

class ExampleCharacteristic extends SimulatedCharacteristic {
  ExampleCharacteristic({ String uuid, String convenienceName})
      : super(
            uuid: uuid,
            value: Uint8List.fromList([0]),
            writableWithResponse: true,
            writableWithoutResponse: false,
            convenienceName: convenienceName,
        );

  
  Future<void> write(Uint8List value) {
    int valueAsInt = value[0];
    if (valueAsInt != 0 && valueAsInt != 1) {
      return Future.error(SimulatedBleError(
          BleErrorCode.CharacteristicWriteFailed, "Unsupported value"));
    } else {
      return super.write(value); //this propagates value through the blemulator,
      // allowing you to react to changes done to this characteristic
    }
  }
}

The characteristic now limits the input to either 0 or 1 on the first byte, ignores any additional bytes, and returns an error if the value exceeds the supported range. To support returning an error, a characteristic has to return a response to writing operation, hence setting writableWithoutResponse to false.

Turning the sensor on

We’d like to know when the temperature sensor is turned on or off.

To achieve this, we’ll create a new service with hardcoded UUIDs:

class TemperatureService extends SimulatedService {
  static const String _temperatureConfigUuid =
      "F000AA02-0451-4000-B000-000000000000";

  bool _readingTemperature = false;

  TemperatureService(
      { String uuid,
       bool isAdvertised,
      String convenienceName})
      : super(
            uuid: uuid,
            isAdvertised: isAdvertised,
            characteristics: [
              //skipping unimportant
              BooleanCharacteristic(
                uuid: _temperatureConfigUuid,
                initialValue: false,
                convenienceName: "IR Temperature Config",
              ),
              //skipping unimportant
            ],
            convenienceName: convenienceName) {
    characteristicByUuid(_temperatureConfigUuid).monitor().listen((value) {
      _readingTemperature = value[0] == 1;
    });
  }
}

The characteristic we’ve defined cannot be monitored through FlutterBleLib, since it’s missing isNotifiable: true, but it can be, for your convenience, monitored at the BLEmulator level. This makes it easier to control the overall flow we’re simulating, simplifies the structure and lets us avoid unnecessary extensions of the base classes.

Emitting notifications

We’re still missing emitting notifications from the IR Temperature Data characteristic. Let’s take care of it.

class TemperatureService extends SimulatedService {
  static const String _temperatureDataUuid =
      "F000AA01-0451-4000-B000-000000000000";
  static const String _temperatureConfigUuid =
      "F000AA02-0451-4000-B000-000000000000";
  static const String _temperaturePeriodUuid =
      "F000AA03-0451-4000-B000-000000000000";

  bool _readingTemperature = false;

  TemperatureService(
      { String uuid,
       bool isAdvertised,
      String convenienceName})
      : super(
            //skipping unimportant
            );

    _emitTemperature();
  }

  void _emitTemperature() async {
    while (true) {
      Uint8List delayBytes =
          await characteristicByUuid(_temperaturePeriodUuid).read();
      int delay = delayBytes[0] * 10;
      await Future.delayed(Duration(milliseconds: delay));

      SimulatedCharacteristic temperatureDataCharacteristic =
          characteristicByUuid(_temperatureDataUuid);

      if (temperatureDataCharacteristic.isNotifying) {
        if (_readingTemperature) {
          temperatureDataCharacteristic
              .write(Uint8List.fromList([0, 0, 200, Random().nextInt(255)]));
        } else {
          temperatureDataCharacteristic.write(Uint8List.fromList([0, 0, 0, 0]));
        }
      }
    }
  }
}

_emitTemperature() is called in the constructor of the Temperature Service and runs in an infinite loop. Each interval, specified by the value of IR Temperature Period characteristic, checks if there’s a listener (isNotifying). If there is one, then it writes data (zeroes or a random value, depending on whether the sensor is on or off) to the IR Temperature Data characteristic. SimulatedCharacteristic.write() notifies any active listeners of the new value.

Advanced peripheral in action

You can find a complete example of a more advanced peripheral on the BLEmulator’s repository. If you’d like to give it a go, just clone the repository and run its example.

Use in automated testing

A big thank you, here, to my fellow Polidean Paweł Byszewski for his research of BLEmulator’s use in automated tests.

Flutter has a different execution context for the tested app and the test itself, meaning you cannot just share a simulated peripheral between the two and modify the behavior from the test. What you can do is add a data handler to the test driver by using enableFlutterDriverExtension(handler: DataHandler), pass the simulated peripherals to the main() of your app and pass string messages to the handler inside the app’s execution context.

It boils down to: Wrapper for app

class Command {
    Command.fromJson();

    String toJson();
}

List<SimulatedPeripheral> peripherals;

void handler(String message) {
    Command command = Command.fromJson(message);
    //find peripheral, pass the command to it
}

void main() {
  enableFlutterDriverExtension(handler: handler);

  app.main(peripherals: peripherals);
}

Your peripheral

class YourPeripheral extends SimulatedPeripheral {
    // skipping unimportant

  
  void handleDeviceCommand(DeviceCommand deviceCommand) {
    switch (deviceCommand.commandType) {
      case CommandType.DISCONNECT:
        onDisconnect();
        break;
      default:
        break;
    }
  }
}

Your test

//inside specific test function
var command = DeviceCommand(CommandType.DISCONNECT, deviceId);
driver.requestData(jsonEncode(command));

Thanks to this mechanism, you could initialize the peripheral anyway you wanted and call any of the behaviors you have predefined inside your simulated device.

See for yourself

The best thing about all this is that you don’t have to believe me! Check it out yourself on GitHub. Use it, have fun with it, try to break it, and let us know all about it!

Would you like to see it on other platforms as well? Reach out to us and we’ll make it happen! Be sure to check out our other libraries, and don’t hesitate to contact us if you’d like us to work on your project.

Mikołaj Kojdecki

Software Engineer

Did you enjoy the read?

If you have any questions, don’t hesitate to ask!