esptouch_flutter 0.2.2

  • Readme
  • Changelog
  • Example
  • Installing
  • 85

esptouch_flutter #

Flutter plugin package which contains an API for ESP-Touch written in Dart combined with platform-specific implementation for Android using Java and iOS using Objective-C.

This package provides a high customizability to the ESP Touch tasks and an idiomatic Dart interface for launching tasks.

Custom task parameters lets the users of this plugin change how long the task runs, you could set it to hours, if this is what your workflow requires.

If you enjoy using this, I'd really appreciate a star on GitHub!

Usage #

Example app #

For a complete example app, see the example folder in the repository.

The example app lets you configure WiFi SSID, BSSID, password, the duration of the task, expected task count and many more.

For a simplest possible app, see the smaho-engineering/esptouch_flutter_kotlin_example repository.

Code snippets #

import 'package:esptouch_flutter/esptouch_flutter.dart';

// Somewhere in your widget...
final ESPTouchTask task = ESPTouchTask(
  ssid: 'My WiFi network',
  bssid: 'ab:cd:ef:12:23:34',
  password: 'I love SMAHO',
);
final Stream<ESPTouchResult> stream = task.execute();
final printResult = (ESPTouchResult result) {
 print('IP: ${result.ip} MAC: ${result.bssid}');
};
StreamSubscription<ESPTouchResult> streamSubscription = stream.listen(printResult);
// Don't forget to cancel your stream subscription:
streamSubscription.cancel();

If you would like to customize the task, provide ESPTouchTaskParameter instance as taskParameter to ESPTouchTask.

final ESPTouchTask task = ESPTouchTask(
  ssid: 'My WiFi network',
  bssid: 'ab:cd:ef:12:23:34',
  password: 'I love Flutter and ESP-Touch, too',
  // Tweak the task using task parameters
  taskParameter: ESPTouchTaskParameter(waitUdpReceiving: Duration(hour: 12)),
);
// You can still stop the task at any point by calling .cancel on the stream subscription:
streamSubscription.cancel();

In the code example, I specify the types for clarity. You can omit them as Dart can infer them.

In a real world example, you'd get the WiFi credentials from a TextEditingControlller (e.g. _passwordController.text) and you could display the configured devices to the user, save it locally in SQLite or send it to your backend.

API reference #

The Full API reference is available on pub.dev.

We put effort into the docs, so if something is still not clear, please open an issue. We will try top help you out and update the docs.

Fundamentals #

ESP-Touch #

Using ESP-Touch, you can configure network for ESP8266 and ESP32 devices.

Espressif’s ESP-Touch protocol implements the Smart Config technology to help users connect ESP8266EX- and ESP32-embedded devices to a Wi-Fi network through simple configuration on a smartphone.

Resources

You can read more about ESP-Touch here:

The original iOS and Android apps had to be heavily customized and tweaked in order to support custom task parameters.

Known issues #

  • We needed full customizability for the touch task parameters, these made changing a significant chunk of the Android and iOS platform side code necessary. We added builders for the task parameters as this looked like a solution that required the least changes to the platform code.
  • The only way I could implement this plugin is by pasting the platform-specific code into this project. One reason for this was that it wasn't published anywhere as easily installable packages, and the second reason is that I had to tweak the platform code to support configuration of the ESPTouchTaskParameters. This means that new changes to the original git repositories do not automatically show up in this project, so we need to manually update the Android and iOS code.
  • The underlying Android and iOS apps support different task parameters. There were multiple changes especially around IPv4 vs IPv6 handling. The plugin does not handle all of these differences.
  • Keeping track of finished tasks is necessary on Flutter's side.
  • AES support (last I checked the support differred on Android and iOS, so I haven't added them)

Contribute #

This is an open-source project built by the SMAHO Engineering team from Munich to wrap Espressif's ESP-Touch mobile app kits.

If you discover issues or know how to improve the project, please contribute: open a pull request or issue.

Flutter #

If you are coming from IoT background, you might not know what Flutter is.

Flutter is Google's UI toolkit for creating beautiful, native experiences for iOS and Android from a single codebase. For help getting started with Flutter, view the online documentation.

Flutter plugin packages

This repository contains a Flutter plugin package for ESP-Touch. A plugin package is a specialized package that includes platform-specific implementation code for Android and iOS.

Platform channels

The Flutter portion of the app sends messages to its host, the iOS or Android portion of the app, over a platform channel. This plugin relies on platform channels (event channels) heavily.

Get started #

If you'd like to contribute, the best way to get started is by running the example app.

  1. Install Flutter.

  2. Configure your IDE.

    studio .
    
    • To work with Android-specific code (.gradle, .java, ...), use Android Studio.
    studio android
    studio example/android
    
    • To work with iOS-specific code (.m, .h, ...), use Xcode and/or AppCode.
    open example/ios/Runner.xcworkspace
    appcode example/ios/Runner.xcworkspace
    

    Make sure you don't see any red squiggly lines, a properly configured IDE will help a lot during development.

    This is my recommended setup, but other IDEs should also work, e.g Visual Studio Code, IntelliJ, or Vim.

  3. Use a real phone for development. The plugin will not work in emulators and simulators, so you need real phones for development. Run flutter devices to verify.

  4. Run the example app

    1. cd example
    2. Install packages flutter packages get
    3. Run the app flutter run. After some time (~1 minute), you should see the app opening on your phone.
  5. Prepare embedded devices. To verify that the ESP-Touch app works, you need some hardware with ESP8266 and ESP32 to connect to your WiFi network.

Keep in mind that hot-reload and hot-restart will not reload platform code, so if you change java or obj-c files, you'll need to restart and recompile your app.

Style guides #

As we are writing Dart, Java and Objective-C code, we need familiarize ourselves with each language's style guide. Whenever possible, prefer automated solutions or IDE configs that help other developers write consistent code "automatically".

When this is not available, fallback to these style guides:

0.2.2 #

Remove EspTouchDemo-Info.plist

No idea why it was there but it breaks my build 🤬

0.2.1 #

  • Add simple example

0.2.0 #

  • Migrated to AndroidX
  • Change compileSdkVersion to 28
    • Fix #3: flutter build apk should now work again

0.1.5 #

  • Fix podspecs for iOS

0.1.4 #

  • Request run-time permissions for location on Android M+ to make BSSID/SSID information work
  • Execute event sink methods on the main thread

0.1.3 #

Add example app screenshots

0.1.2 #

More docs improvements. Add missing license.

0.1.1 #

Improve documentation.

0.1.0 #

Initial release. Support ESP-Touch on iOS and Android. Fully configurable from your Dart code.

example/lib/main.dart

import 'dart:async';

import 'package:esptouch_example/task_parameter_details.dart';
import 'package:esptouch_example/wifi_info.dart';
import 'package:esptouch_flutter/esptouch_flutter.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() => runApp(MyApp());

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

const helperSSID =
    "SSID is the technical term for a network name. When you set up a wireless home network, you give it a name to distinguish it from other networks in your neighbourhood.";
const helperBSSID =
    "BSSID is the MAC address of the wireless access point (router).";
const helperPassword = "The password of the Wi-Fi network";

class _MyAppState extends State<MyApp> {
  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
  final TextEditingController _ssid = TextEditingController();
  final TextEditingController _bssid = TextEditingController();
  final TextEditingController _password = TextEditingController();
  final TextEditingController _expectedTaskResults =
      TextEditingController(); // TODO; is it the same as threshold?
  final TextEditingController _intervalGuideCode = TextEditingController();
  final TextEditingController _intervalDataCode = TextEditingController();
  final TextEditingController _timeoutGuideCode = TextEditingController();
  final TextEditingController _timeoutDataCode = TextEditingController();
  final TextEditingController _repeat = TextEditingController();

  // final TextEditingController _oneLength = TextEditingController();
  // final TextEditingController _macLength = TextEditingController();
  // final TextEditingController _ipLength = TextEditingController();
  final TextEditingController _portListening = TextEditingController();
  final TextEditingController _portTarget = TextEditingController();
  final TextEditingController _waitUdpReceiving = TextEditingController();
  final TextEditingController _waitUdpSending = TextEditingController();
  final TextEditingController _thresholdSucBroadcastCount =
      TextEditingController();
  ESPTouchPacket _packet = ESPTouchPacket.broadcast;

  @override
  void dispose() {
    _ssid.dispose();
    _bssid.dispose();
    _password.dispose();
    _expectedTaskResults.dispose();
    _intervalGuideCode.dispose();
    _intervalDataCode.dispose();
    _timeoutGuideCode.dispose();
    _timeoutDataCode.dispose();
    _repeat.dispose();
    _portListening.dispose();
    _portTarget.dispose();
    _waitUdpReceiving.dispose();
    _waitUdpSending.dispose();
    _thresholdSucBroadcastCount.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primaryColor: Colors.deepOrange,
        buttonTheme: ButtonThemeData(
          buttonColor: Colors.deepOrange,
          textTheme: ButtonTextTheme.primary,
        ),
      ),
      home: Scaffold(
        appBar: AppBar(
          title: Text(
            'ESP-TOUCH',
            style: TextStyle(
              fontFamily: 'serif-monospace',
              fontWeight: FontWeight.w800,
            ),
          ),
        ),
        // Using builder to get context without creating new widgets
        //  https://docs.flutter.io/flutter/material/Scaffold/of.html
        body: Builder(builder: (BuildContext context) {
          return Center(
            child: form(context),
          );
        }),
      ),
    );
  }

  bool fetchingWifiInfo = false;

  void fetchWifiInfo() async {
    setState(() {
      fetchingWifiInfo = true;
    });
    try {
      _ssid.text = await ssid;
      _bssid.text = await bssid;
    } finally {
      setState(() {
        fetchingWifiInfo = false;
      });
    }
  }

  createTask() {
    final taskParameter = ESPTouchTaskParameter();
    if (_intervalGuideCode.text.isNotEmpty) {
      taskParameter.intervalGuideCode =
          Duration(milliseconds: int.parse(_intervalGuideCode.text));
    }
    if (_intervalDataCode.text.isNotEmpty) {
      taskParameter.intervalDataCode =
          Duration(milliseconds: int.parse(_intervalDataCode.text));
    }
    if (_timeoutGuideCode.text.isNotEmpty) {
      taskParameter.timeoutGuideCode =
          Duration(milliseconds: int.parse(_timeoutGuideCode.text));
    }
    if (_timeoutDataCode.text.isNotEmpty) {
      taskParameter.timeoutDataCode =
          Duration(milliseconds: int.parse(_timeoutDataCode.text));
    }
    if (_repeat.text.isNotEmpty) {
      taskParameter.repeat = int.parse(_repeat.text);
    }
    if (_portListening.text.isNotEmpty) {
      taskParameter.portListening = int.parse(_portListening.text);
    }
    if (_portTarget.text.isNotEmpty) {
      taskParameter.portTarget = int.parse(_portTarget.text);
    }
    if (_waitUdpSending.text.isNotEmpty) {
      taskParameter.waitUdpSending =
          Duration(milliseconds: int.parse(_waitUdpSending.text));
    }
    if (_waitUdpReceiving.text.isNotEmpty) {
      taskParameter.waitUdpReceiving =
          Duration(milliseconds: int.parse(_waitUdpReceiving.text));
    }
    if (_thresholdSucBroadcastCount.text.isNotEmpty) {
      taskParameter.thresholdSucBroadcastCount =
          int.parse(_thresholdSucBroadcastCount.text);
    }
    if (_expectedTaskResults.text.isNotEmpty) {
      taskParameter.expectedTaskResults = int.parse(_expectedTaskResults.text);
    }

    return ESPTouchTask(
      ssid: _ssid.text,
      bssid: _bssid.text,
      password: _password.text,
      packet: _packet,
      taskParameter: taskParameter,
    );
  }

  Widget form(BuildContext context) {
    Color color = Theme.of(context).primaryColor;
    return Form(
      key: _formKey,
      child: ListView(
        padding: const EdgeInsets.symmetric(horizontal: 16),
        children: <Widget>[
          Center(
            child: OutlineButton(
              highlightColor: Colors.transparent,
              highlightedBorderColor: color,
              onPressed: fetchingWifiInfo ? null : fetchWifiInfo,
              child: fetchingWifiInfo
                  ? Text(
                      'Fetching WiFi info',
                      style: TextStyle(color: Colors.grey),
                    )
                  : Text(
                      'Use current Wi-Fi',
                      style: TextStyle(color: color),
                    ),
            ),
          ),
          TextFormField(
            controller: _ssid,
            decoration: const InputDecoration(
              labelText: 'SSID',
              hintText: 'Tony\'s iPhone',
              helperText: helperSSID,
            ),
          ),
          TextFormField(
            controller: _bssid,
            decoration: const InputDecoration(
              labelText: 'BSSID',
              hintText: '00:a0:c9:14:c8:29',
              helperText: helperBSSID,
            ),
          ),
          TextFormField(
            controller: _password,
            decoration: const InputDecoration(
              labelText: 'Password',
              hintText: r'V3Ry.S4F3-P@$$w0rD',
              helperText: helperPassword,
            ),
          ),
          RadioListTile(
            title: Text('Broadcast'),
            value: ESPTouchPacket.broadcast,
            groupValue: _packet,
            onChanged: setPacket,
            activeColor: color,
          ),
          RadioListTile(
            title: Text('Multicast'),
            value: ESPTouchPacket.multicast,
            groupValue: _packet,
            onChanged: setPacket,
            activeColor: color,
          ),
          TaskParameterDetails(
            color: color,
            expectedTaskResults: _expectedTaskResults,
            intervalGuideCode: _intervalGuideCode,
            intervalDataCode: _intervalDataCode,
            timeoutGuideCode: _timeoutGuideCode,
            timeoutDataCode: _timeoutDataCode,
            repeat: _repeat,
            portListening: _portListening,
            portTarget: _portTarget,
            waitUdpReceiving: _waitUdpReceiving,
            waitUdpSending: _waitUdpSending,
            thresholdSucBroadcastCount: _thresholdSucBroadcastCount,
          ),
          Center(
            child: RaisedButton(
              onPressed: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => TaskRoute(task: createTask()),
                  ),
                );
              },
              child: const Text('Execute'),
            ),
          ),
        ],
      ),
    );
  }

  void setPacket(ESPTouchPacket packet) {
    setState(() {
      _packet = packet;
    });
  }
}

class TaskRoute extends StatefulWidget {
  final ESPTouchTask task;

  TaskRoute({this.task});

  @override
  State<StatefulWidget> createState() {
    return TaskRouteState();
  }
}

class TaskRouteState extends State<TaskRoute> {
  Stream<ESPTouchResult> _stream;
  StreamSubscription<ESPTouchResult> _streamSubscription;

  @override
  void initState() {
    _stream = widget.task.execute();
    _streamSubscription = _stream.listen((value) {
      // TODO(smaho): Don't use StreamBuilder and listen in the same example
      print('Received value in TaskRouteState $value');
    });
    super.initState();
  }

  @override
  dispose() {
    _streamSubscription?.cancel();
    super.dispose();
  }

  Widget waitingState(BuildContext context) {
    return Center(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.center,
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          CircularProgressIndicator(
            valueColor: AlwaysStoppedAnimation(Theme.of(context).primaryColor),
          ),
          SizedBox(height: 16),
          Text('Waiting for results'),
        ],
      ),
    );
  }

  Widget error(BuildContext context, String s) {
    return Center(
      child: Text(
        s,
        style: TextStyle(color: Colors.red),
      ),
    );
  }

  copyValue(BuildContext context, String label, String v) {
    return () {
      Clipboard.setData(ClipboardData(text: v));
      Scaffold.of(context).showSnackBar(
          SnackBar(content: Text('Copied $label to clipboard: $v')));
    };
  }

  Widget noneState(BuildContext context) {
    return Text('None');
  }

  Widget resultList(BuildContext context) {
    return ListView.builder(
      itemCount: _results.length,
      itemBuilder: (_, index) {
        final result = _results.toList(growable: false)[index];
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: <Widget>[
              GestureDetector(
                onLongPress: copyValue(context, 'BSSID', result.bssid),
                child: Row(
                  children: <Widget>[
                    Text('BSSID: ', style: Theme.of(context).textTheme.body2),
                    Text(result.bssid,
                        style: TextStyle(fontFamily: 'monospace')),
                  ],
                ),
              ),
              GestureDetector(
                onLongPress: copyValue(context, 'IP', result.ip),
                child: Row(
                  children: <Widget>[
                    Text('IP: ', style: Theme.of(context).textTheme.body2),
                    Text(result.ip, style: TextStyle(fontFamily: 'monospace')),
                  ],
                ),
              )
            ],
          ),
        );
      },
    );
  }

  final Set<ESPTouchResult> _results = Set();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Task'),
      ),
      body: Container(
        child: StreamBuilder<ESPTouchResult>(
          builder: (context, AsyncSnapshot<ESPTouchResult> snapshot) {
            if (snapshot.hasError) {
              return error(context, 'Error in StreamBuilder');
            }
            if (!snapshot.hasData) {
              return Center(
                child: CircularProgressIndicator(
                  valueColor:
                      AlwaysStoppedAnimation(Theme.of(context).primaryColor),
                ),
              );
            }
            switch (snapshot.connectionState) {
              case ConnectionState.active:
                _results.add(snapshot.data);
                return resultList(context);
              case ConnectionState.none:
                return noneState(context);
              case ConnectionState.done:
                return resultList(context);
              case ConnectionState.waiting:
                return waitingState(context);
            }
          },
          stream: _stream,
        ),
      ),
    );
  }
}

Use this package as a library

1. Depend on it

Add this to your package's pubspec.yaml file:


dependencies:
  esptouch_flutter: ^0.2.2

2. Install it

You can install packages from the command line:

with Flutter:


$ flutter pub get

Alternatively, your editor might support flutter pub get. Check the docs for your editor to learn more.

3. Import it

Now in your Dart code, you can use:


import 'package:esptouch_flutter/esptouch_flutter.dart';
  
Popularity:
Describes how popular the package is relative to other packages. [more]
71
Health:
Code health derived from static analysis. [more]
100
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
100
Overall:
Weighted score of the above. [more]
85
Learn more about scoring.

We analyzed this package on Mar 27, 2020, and provided a score, details, and suggestions below. Analysis was completed with status completed using:

  • Dart: 2.7.1
  • pana: 0.13.6
  • Flutter: 1.12.13+hotfix.8

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=2.1.0 <3.0.0
flutter 0.0.0
Transitive dependencies
collection 1.14.11 1.14.12
meta 1.1.8
sky_engine 0.0.99
typed_data 1.1.6
vector_math 2.0.8