Paul Ruiz
Published

Universal IoT Controller App

A universal app and IoT device platform that uses Bluetooth to configure custom controls on a smartphone. Practical ex: smart soda dispenser

IntermediateFull instructions provided1,361
Universal IoT Controller App

Things used in this project

Hardware components

ESP32
Espressif ESP32
×1

Software apps and online services

Flutter
Firebase
Google Firebase

Story

Read more

Code

Main.dart

Dart
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';

import './bluetooth.dart';
import './bluetoothconnect.dart';
import './settings.dart';

GetIt getIt = GetIt.instance;

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  final FirebaseApp app = await FirebaseApp.configure(
      name: 'db2',
      options:const FirebaseOptions(
        googleAppID: '',
        apiKey: '',
        databaseURL: '',
      ),
  );

  getIt.registerSingleton<ConfigNameController>(ConfigNameController("PROTO"));
  getIt.registerSingleton<SnackbarController>(SnackbarController());
  getIt.registerSingleton<BluetoothController>(BluetoothController());
  runApp(MainApp());
}

class MainApp extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Universal Controller',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: App(),
    );
  }
}

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).primaryColor,
        title: Text("Universal Controller"),
        actions: <Widget>[
          IconButton(
            icon: Icon(Icons.settings),
            onPressed: () {
              showDialog(
                context: context,
                builder: (BuildContext context) {
                  return AlertDialog(
                    content: SettingsButton(),
                  );
                },
              );
            },
          ),
          BluetoothConnect(
            deviceName: "PROTO",
          ),
        ],
      ),
      body: AppHomeScreen(),
    );
  }
}

class AppHomeScreen extends StatefulWidget {
  AppHomeScreen({this.app});
  final FirebaseApp app;

  @override
  createState() => _AppHomeScreenState();
}

class _AppHomeScreenState extends State<AppHomeScreen> {
  final bluetooth = getIt.get<BluetoothController>();

  @override
  Widget build(BuildContext context) {
    Color primary = Theme.of(context).primaryColor;
    TextStyle headline = DefaultTextStyle.of(context)
        .style
        .apply(fontSizeFactor: 2.0, color: primary);

    return Padding(
      padding: EdgeInsets.all(20),
      child: Scrollbar (
        child: Column(
          children: <Widget>[
            StreamBuilder<String>(
              initialData: "scanQRCode",
              stream: bluetooth.getStringStream(),
              builder: (context, snapshot) {
                print("data: " + snapshot.data);
                return getViewForItem(snapshot.data);
              },
            ),
          ],
        ),
      ),
    );
  }

  List<dynamic> view = null;

  Widget getViewForItem(String data) {
    if( view != null ) {
      //TODO: magic of casting to object types
      List<Widget> widgetList = List();
      for( final item in view ) {
        if( item != null ) {
          print("THIS IS A TEST: " + item.toString());
          switch(item["view_type"].toString() ) {
            case "text": {
              widgetList.add(Text(item["text"].toString() ));
              break;
            }
            case "button": {
              widgetList.add(RaisedButton(
                child: Text(item["text"].toString()),
                onPressed: () {
                  bluetooth.sendTextFieldValue(item["action"].toString());
                },
              ),);
            }
          }
        }
      }

      return Expanded(
        child: SizedBox(
            height: 200.0,
            child: ListView(children: widgetList) )
        );
    } else if( data == "scanQRCode" ) {
      return Text("QR code reader goes here");
  } else {
      FirebaseDatabase.instance.reference().child('request_id').child(data).once().then((DataSnapshot snapshot) {
        view = snapshot.value;
        (context as Element).reassemble();
      });
      return Text("Loading...");
    }
  }
}

Settings.dart

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

import 'package:rxdart/rxdart.dart';
import 'package:get_it/get_it.dart';

GetIt getIt = GetIt.instance;

class ConfigNameController {
  BehaviorSubject<String> _controller;

  ConfigNameController(String deviceName) {
    _controller = BehaviorSubject.seeded(deviceName);
  }

  Observable<String> get stream => _controller.stream;
  String get current => _controller.value;

  setDeviceName(String name) {
    _controller.add(name);
  }
}

class SettingsButton extends StatefulWidget {
  @override
  createState() => _SettingsButtonState();
}

class _SettingsButtonState extends State<SettingsButton> {
  final name = getIt.get<ConfigNameController>();

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: <Widget>[
        Row(
          children: <Widget>[
            Expanded(
              child: Text("Settings", style: Theme.of(context).textTheme.title),
            ),
            IconButton(
              padding: EdgeInsets.all(0),
              icon: Icon(Icons.close),
              color: Theme.of(context).primaryColor,
              onPressed: () => Navigator.pop(context),
            ),
          ],
        ),
        SizedBox(height: 10),
        TextFormField(
          decoration: InputDecoration(labelText: "Device Name"),
          initialValue: name.current,
          onFieldSubmitted: (String text) {
            name.setDeviceName(text);
          },
        ),
        SizedBox(height: 10),
      ],
    );
  }
}

bluetooth.dart

Dart
import 'dart:async';

import './bluetoothconnect.dart';

class RECIVE {
  static const SERVICE = '9c81420d-8a1e-49f8-a42f-d4679c7330be';
  static const STRING = '1394b0cc-e9b7-4cc1-bc50-d6c9a9505f21';
}

class SEND {
  static const SERVICE = 'f159cdfb-df60-4a4a-b1fa-004bcc379bb6';
  static const STRING = 'cd468881-1cda-47a1-9373-dc812d15d727';
}

class BluetoothController extends BluetoothConnector {

  var _textStream = StreamController<String>();

  @override
  void sendInit() {
    Future.delayed(Duration(seconds: 1)).then((value) => sendTextFieldValue("request_id"));

    subscribeServiceString(RECIVE.STRING, _textStream);
  }

  @override
  close() async {
//    _textStream.close();
//    _numberStream.close();
//    _boolStream.close();
  }

  BluetoothController() : super();


  sendTextFieldValue(String text) async {
    print('trying to send' + text);
    await writeServiceString(SEND.STRING, text, true);
  }

  Stream<String> getStringStream() {
    return _textStream.stream;
  }
}

bluetoothconnect.dart

Dart
import 'dart:math';

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

import 'package:rxdart/rxdart.dart';
import 'package:get_it/get_it.dart';

import 'dart:async';
import 'dart:collection';
import 'dart:convert';

import './settings.dart';
import './bluetooth.dart';

GetIt getIt = GetIt.instance;

class BluetoothConnect extends StatelessWidget {
  final bluetooth = getIt.get<BluetoothController>();
  final snackbar = getIt.get<SnackbarController>();

  BluetoothConnect({Key key, String deviceName}) : super(key: key) {
    bluetooth.setTargetDeviceName(deviceName);
  }

  @override
  Widget build(BuildContext context) {
    snackbar.stream.listen(
          (snack) {
        if (snack != null) {
          Scaffold.of(context).showSnackBar(snack);
        }
      },
    );

    return StreamBuilder<int>(
      stream: bluetooth.stream,
      initialData: 1,
      builder: (c, snapshot) {
        if (snapshot.data == 1) {
          return IconButton(
            icon: Icon(Icons.bluetooth),
            iconSize: 32,
            onPressed: () => bluetooth.startScan(),
          );
        } else if (snapshot.data == 2) {
          return IconButton(
            icon: Icon(Icons.bluetooth_searching),
            iconSize: 32,
            onPressed: () => bluetooth.stopScan(),
          );
        } else if (snapshot.data == 3) {
          return IconButton(
            icon: Icon(Icons.bluetooth_connected),
            iconSize: 32,
            onPressed: () => bluetooth.disconnect(),
          );
        } else {
          return IconButton(
            icon: Icon(Icons.bluetooth_disabled),
            iconSize: 32,
            onPressed: () => bluetooth.showSnackbar(
                Icons.bluetooth_disabled, 'Turn on Bluetooth!'),
          );
        }
      },
    );
  }
}

class SnackbarController {
  BehaviorSubject<Widget> _controller;
  String _lastMsg = "";

  SnackbarController() {
    _controller = BehaviorSubject.seeded(null);
  }

  Observable<Widget> get stream => _controller.stream;
  Widget get current => _controller.value;

  openSnackbar(Widget widget, String msg) {
    if (msg != _lastMsg) {
      _lastMsg = msg;
      _controller.add(widget);
    }
  }
}

class BluetoothConnector {
  final settings = getIt.get<ConfigNameController>();
  final snackbar = getIt.get<SnackbarController>();

  BehaviorSubject<int> _controller = BehaviorSubject.seeded(0);

  BluetoothDevice device;
  String targetDeviceName;

  bool isBtAvalible = false;
  bool isScanning = false;
  bool isConnected = false;
  bool isWriting = false;

  String _lastSnackbar = "";

  List<MapEntry<String, List<int>>> msgStack = List();
  HashMap<String, BluetoothCharacteristic> map;

  sendInit() async {}
  close() async {}

  BluetoothConnector() {
    settings.stream.listen((data) {
      targetDeviceName = data;
    });
    initDevice();
  }

  Observable<int> get stream => _controller.stream;
  int get current => _controller.value;

  startScan() async {
    if (!isScanning) {
      (await FlutterBlue.instance.connectedDevices).forEach((connected) {
        print('Connected: ${connected.name}');
        setDevice(connected);
      });
      FlutterBlue.instance.startScan(timeout: Duration(seconds: 16));
    }
  }

  stopScan() async {
    if (isScanning) {
      FlutterBlue.instance.stopScan();
    }
  }

  disconnect() async {
    if (device != null) {
      showSnackbar(Icons.bluetooth_disabled, 'Disonnected from ${device.name}');
      device.disconnect();
      close();

      map = null;
      device = null;
      isConnected = false;
      isScanning = false;

      calcState();
    }
  }

  setTargetDeviceName(String name) {
    targetDeviceName = name;
  }

  Future<void> initDevice() async {
    FlutterBlue.instance.state.listen((data) {
      bool isAvalible = (data == BluetoothState.on);
      isBtAvalible = isAvalible;
      if (isAvalible) {
        // showSnackbar(Icons.bluetooth, 'Bluetooth is online again!');
      }
      calcState();
    });

    FlutterBlue.instance.isScanning.listen((data) {
      isScanning = data;
      print(isScanning);
      calcState();
    });

    (await FlutterBlue.instance.connectedDevices).forEach((connected) {
      print('Connected: ${connected.name}');
      setDevice(connected);
    });
    FlutterBlue.instance.scanResults.listen((scans) {
      for (var scan in scans) {
        setScanResult(scan);
      }
    });
  }

  void calcState() {
    int stage = 1; // waiting for scanning
    stage = (isScanning) ? 2 : stage; // searching
    stage = (isConnected) ? 3 : stage; // connected
    stage = (!isBtAvalible) ? 0 : stage; // not avalible
    print(stage);
    _controller.add(stage);
  }

  Future<void> setScanResult(ScanResult scan) async {
    BluetoothDevice _device = scan.device;
    await setDevice(_device);
  }

  Future<void> setDevice(BluetoothDevice _device) async {
    if (_device.name == targetDeviceName && device == null) {
      print(_device.name);

      await _device.disconnect();
      await _device.connect();

      map = new HashMap<String, BluetoothCharacteristic>();

      showSnackbar(Icons.bluetooth, 'Connected to ${_device.name}');
      device = _device;
      isConnected = true;
      calcState();

      List<BluetoothService> services = await _device.discoverServices();

      for (var service in services) {
        for (var c in service.characteristics) {
          map[c.uuid.toString()] = c;
          bool read = c.properties.read;
          bool write = c.properties.write;
          bool notify = c.properties.notify;
          bool indicate = c.properties.indicate;
          String properties = "";
          properties += (read ? "R" : "") + (write ? "W" : "");
          properties += (notify ? "N" : "") + (indicate ? "I" : "");
          print('${c.serviceUuid} ${c.uuid} [$properties] found!');

          if (notify || indicate) {
            await c.setNotifyValue(true);
          }
        }
      }

      await sendInit();
    }
  }

  subscribeService<T>(String characteristicGuid, StreamController<T> controller,
      T Function(List<int>) parse) {
    if (map != null) {
      var characteristic = map[characteristicGuid];
      if (characteristic != null) {
        Stream<List<int>> listener = characteristic.value;

        listener.listen((onData) {
          if (!controller.isClosed) {
            T value = parse(onData);
            controller.sink.add(value);
          }
        });
      }
    }
  }

  subscribeServiceString(
      String guid, StreamController<String> controller) async {
    var parse = (List<int> data) => String.fromCharCodes(data);
    subscribeService<String>(guid, controller, parse);
  }

  void subscribeServiceInt(String guid, StreamController<int> controller) {
    var parse = (List<int> data) => bytesToInteger(data);
    subscribeService<int>(guid, controller, parse);
  }

  void subscribeServiceBool(String guid, StreamController<bool> controller) {
    var parse = (List<int> data) {
      if (data.isNotEmpty) {
        return data[0] != 0 ? true : false;
      }
      return false;
    };
    subscribeService<bool>(guid, controller, parse);
  }

  Future<List<int>> readService(String characteristicGuid) async {
    if (map != null) {
      BluetoothCharacteristic characteristic = map[characteristicGuid];
      print(map);
      if (characteristic != null) {
        return await characteristic.read();
      }
    }
    return List<int>();
  }

  Future<void> writeServiceInt(
      String characteristicGuid, int value, bool importand) async {
    int byte1 = value & 0xff;
    int byte2 = (value >> 8) & 0xff;
    await writeService(characteristicGuid, [byte1, byte2], importand);
  }

  Future<void> writeServiceString(
      String characteristicGuid, String msg, bool important) async {
    await writeService(characteristicGuid, utf8.encode(msg), important);
  }

  Future<void> writeServiceBool(
      String characteristicGuid, bool value, bool important) async {
    int byte = value ? 0x01 : 0x00;
    await writeService(characteristicGuid, [byte], important);
  }

  Future<void> writeService(
      String characteristicGuid, List<int> data, bool important) async {
    // print("$characteristicGuid $isWriting");
    if (!isWriting) {
      isWriting = true;
      await writeCharacteristics(characteristicGuid, data);

      if (msgStack.length > 0) {
        for (int i = 0; i < msgStack.length; i++) {
          await writeCharacteristics(msgStack[i].key, msgStack[i].value);
        }
        msgStack = List();
      }
      isWriting = false;
    } else if (important) {
      msgStack.add(MapEntry(characteristicGuid, data));
    }
  }

  Future<void> writeCharacteristics(
      String characteristicGuid, List<int> data) async {
    if (map != null) {
      var characteristic = map[characteristicGuid];
      if (characteristic != null) {
        await characteristic.write(data);
        return;
      }
    }
  }

  void showSnackbar(IconData icon, String msg) {
    if (msg != _lastSnackbar) {
      snackbar.openSnackbar(
        SnackBar(
          duration: Duration(seconds: 1),
          content: Row(
            children: <Widget>[
              Icon(icon),
              Text(msg),
            ],
          ),
        ),
        msg,
      );
    }
    _lastSnackbar = msg;
  }

  int bytesToInteger(List<int> bytes) {
    var value = 0;
    for (var i = 0, length = bytes.length; i < length; i++) {
      value += bytes[i] * pow(256, i);
    }
    return value;
  }
}

arduino code

Arduino
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>
#include <BLE2902.h>

#define DEVICENAME "PROTO"

#define SEND "9c81420d-8a1e-49f8-a42f-d4679c7330be"
#define SEND_STRING "1394b0cc-e9b7-4cc1-bc50-d6c9a9505f21"

#define RECIVE "f159cdfb-df60-4a4a-b1fa-004bcc379bb6"
#define RECIVE_STRING "cd468881-1cda-47a1-9373-dc812d15d727"
#define UI_ID = "dfa653";

bool deviceConnected = false;

BLECharacteristic *sSendString;

String strToString(std::string str) {
  return str.c_str();
}

int strToInt(std::string str) {
  const char* encoded = str.c_str();
  return 256 * int(encoded[1]) + int(encoded[0]);
}

double intToDouble(int value, double max) {
  return (1.0 * value) / max;
}

bool intToBool(int value) {
  if (value == 0) {
    return false;
  }
  return true;
}

class ConnectionServerCallbacks: public BLEServerCallbacks {
    void onConnect(BLEServer* pServer) {
      deviceConnected = true;
      Serial.println("Connected");
    };

    void onDisconnect(BLEServer* pServer) {
      deviceConnected = false;
      Serial.println("Disconnected");
    }
};

class WriteString: public BLECharacteristicCallbacks {
    void onWrite(BLECharacteristic *pCharacteristic) {
      String str = strToString(pCharacteristic->getValue());
      sSendString->setValue("default");
      Serial.print("Recived String:");
      Serial.println(str);
      if( str == "request_id") {
        Serial.println("sending response: request_id!");
        sSendString->setValue(ui_id); //randomly generated device UI identifier
        sSendString->notify();
      } else {
        //handle action! - this is where we'd interact with peripherals
        Serial.print("we got a new action: ");
        Serial.println(str);
      }

    }
};

void setup() {
  Serial.begin(115200);
  Serial.print("Device Name:");
  Serial.println(DEVICENAME);

  BLEDevice::init(DEVICENAME);
  BLEServer *btServer = BLEDevice::createServer();
  btServer->setCallbacks(new ConnectionServerCallbacks());

  BLEService *sRecive = btServer->createService(RECIVE);
  uint32_t cwrite = BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE;

  BLECharacteristic *sReciveString = sRecive->createCharacteristic(RECIVE_STRING, cwrite);
  sReciveString->setCallbacks(new WriteString());


  BLEService *sSend = btServer->createService(SEND);
  uint32_t cnotify = BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE  |
                     BLECharacteristic::PROPERTY_NOTIFY | BLECharacteristic::PROPERTY_INDICATE;

  sSendString = sSend->createCharacteristic(SEND_STRING, cnotify);
  sSendString->addDescriptor(new BLE2902());

  sRecive->start();
  sSend->start();
  
  BLEAdvertising *pAdvertising = btServer->getAdvertising();
  pAdvertising->start();
}

void loop() {}

Credits

Paul Ruiz
23 projects • 94 followers
Sr. Developer Relations Engineer @ Google DeepMind. IoT and robotics.
Contact

Comments

Please log in or sign up to comment.