Skip to main content

Arduino OTA Firmware Update Using Flutter/Dart

 Arduino OTA (Over The Air) is commonly used to update the firmware of Arduino based home automation or IoT devices. Arduino provides a Python script in the file espota.py to upload firmware from the command line. Multi-platform applications that are built using Flutter can include the option to update the device's firmware using the class described below.

The full class can be downloaded from the following link.

////////////////////////////////////////////////////////////////////////////////
//                                                                            //
// Upwind Water Management Systems                                            //
//                                                                            //
// This code is provided AS IS by Upwind Tecnologias Lda. and can be freely   //
// used for personal or commercial purposes                                   //
//                                                                            //
// For more information, you can reach us at info@upwindtec.pt                //
// https://www.upwindtec.pt                                                   //
//                                                                            //
////////////////////////////////////////////////////////////////////////////////
//
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
import 'dart:convert';
//
// FirmwareUploader is a Flutter/Dart class that can be used to upload firmware from a mobile
// application to any device running Arduino OTA (Over The Air)
// Sample Usage:
// ByteData data = await DefaultAssetBundle.of(context).load('assets/firmware.bin');
// (firmware.bin should be added to the assets section of pubspec.yaml)
// var uploader = FirmwareUploader();
// await uploader.upload(OTAAddress, OTAPort, OTAPassword, data);
//

// internal enum to track upload _status
enum FirmwareUploaderStatus {
  connecting,
  authorizing,
  updating,
  waitingForResponse,
  finished,
}

class FirmwareUploader {
  String log = "";
  FirmwareUploaderStatus _status = FirmwareUploaderStatus.connecting;

  // Send/Receive timeout in seconds
  int timeout = 5;

//
// Upload new firmware to device, returns true is successful
// In case of failure, check the log member variable for errors
//
  Future<bool> upload(String deviceAddress, int devicePort, String password,
      ByteData firmware_) async {
    bool success = false;
    _status = FirmwareUploaderStatus.connecting;

    final Uint8List firmware = firmware_.buffer.asUint8List();

    // start a UDP socket to initiate communication with the device
    RawDatagramSocket.bind(InternetAddress.anyIPv4, 0)
        .then((RawDatagramSocket udpSocket) async {
      // UDP messages received from device are captured in the listen method
      udpSocket.listen((e) async {
        try {
          Datagram? datagram = udpSocket.receive();
          if (datagram != null) {
            String returnData = String.fromCharCodes(datagram.data);
            switch (_status) {
              case FirmwareUploaderStatus.connecting:
                if (returnData.startsWith("AUTH")) {
                  // Firware update is protected with a password

                  // ArduinoOTA requests the password in a very specific format
                  String nonce = returnData.split(' ')[1];
                  String passwordHash = _hashString(password);
                  String cnonce = _hashString(
                      "${firmware.length} $passwordHash $deviceAddress"); // this hash is not used so any random 32 character string will do
                  String result = _hashString("$passwordHash:$nonce:$cnonce");
                  String message = "200 $cnonce $result\n"; // 200 = AUTH

                  // send the hashed password and wait for OK or ERR return from device
                  _status = FirmwareUploaderStatus.authorizing;
                  udpSocket.send(message.codeUnits,
                      InternetAddress(deviceAddress), devicePort);
                } else {
                  // no authentication required, upload firmware
                  _status = FirmwareUploaderStatus.updating;
                  success = await _uploadData(udpSocket.port, firmware);
                  udpSocket.close();
                }
                break;

              case FirmwareUploaderStatus.authorizing:
                if (!returnData.startsWith("OK")) {
                  udpSocket.close();
                  throw (Exception("Authentication Failed"));
                } else {
                  // Autentication succeeded, start the upload
                  _status = FirmwareUploaderStatus.updating;
                  success = await _uploadData(udpSocket.port, firmware);
                  udpSocket.close();
                }
                break;

              default:
                break;
            }
          }
        } on Exception catch (e) {
          log += e.toString();
          success = false;
          _status = FirmwareUploaderStatus.finished;
        }
      });

      try {
        // the first command is U_FLASH (value 0) followed by the local port, the file length and and the file hash
        String hash;

        // get the MD5 hash of the stream
        var hashBytes = md5.convert(firmware).toString();

        hash = hashBytes.toLowerCase();

        String command = "0 ${udpSocket.port} ${firmware.length} $hash\n";
        udpSocket.send(
            command.codeUnits, InternetAddress(deviceAddress), devicePort);
      } on Exception catch (e) {
        log += e.toString();
        _status = FirmwareUploaderStatus.finished;
        udpSocket.close();
      }
    });

    // wait for the upload to finish before returning
    while (_status != FirmwareUploaderStatus.finished) {
      await Future.delayed(const Duration(seconds: 1));
    }

    return success;
  }

//
// Once the device has accepted the connection, we can start actually uploading the firmware
// Note that this method should be called within 1 second of the end of the upload method (i.e. no breakpoints in between)
// This is because ArduinoOTA will try to connect back to us within 1 sec
// The actual uploading of the data uses TCP on the same port as the UDP used to start the communication
//
  Future<bool> _uploadData(int localPort, Uint8List firmware) async {
    bool success = false;

    // start a TCP listener on the same port as the UDP listener and wait for device to connect back to us
    ServerSocket.bind(InternetAddress.anyIPv4, localPort)
        .then((ServerSocket listener) {
      listener.timeout(Duration(seconds: timeout));
      listener.listen((Socket sender) async {
        // the listen method will capture any messages sent back from the device
        try {
          sender.listen(
            (Uint8List data) {
              String response = String.fromCharCodes(data);
              switch (_status) {
                case FirmwareUploaderStatus.updating:
                  // ignore any returned data while updating
                  break;
                case FirmwareUploaderStatus.waitingForResponse:
                  // keep reading until we receive ERR or OK
                  if (!response.contains("ERR") && !response.contains("OK")) {
                    break;
                  }

                  success = response.contains("OK");
                  listener.close();
                  _status = FirmwareUploaderStatus.finished;
                  if (!success) {
                    log += "Firmware update failed with error: $response\n";
                  }
                  break;
                default:
                  break;
              }
            },
            onError: (error) {
              listener.close();
            },
            onDone: () {
              listener.close();
            },
          );

          // send firmware data to device in small blocks
          int blockSize = 1460;
          int length = firmware.length;
          int pos = 0;
          while (length > 0) {
            sender.add(firmware
                .getRange(pos, pos + blockSize)
                .toList(growable: false));
            await sender.flush();
            pos += blockSize;
            length -= blockSize;
            blockSize = min(blockSize, length);
          }
          _status = FirmwareUploaderStatus.waitingForResponse;
        } on Exception catch (e) {
          success = false;
          _status = FirmwareUploaderStatus.finished;
          listener.close();
          log += e.toString();
        }
      });
    });

    // wait for the upload to finish
    while (_status != FirmwareUploaderStatus.finished) {
      await Future.delayed(const Duration(seconds: 1));
    }
    return success;
  }

  String _hashString(String value) {
    String hash = "";
    // Generate hash value for input string
    var hashBytes = md5.convert(utf8.encode(value)).toString();

    // Arduino requires lower case with no separators
    hash = hashBytes.toLowerCase();
    return hash;
  }
}

Comments

Popular posts from this blog

Remote Pool Cover Opener - Open Source

This is the first of a series of devices which we will be developing in order to help home owners take control of their homes in innovative ways. While developing these devices, our main concerns are: Privacy:  Most devices that we find on the market require that you create an account on different websites and share a number of private data with these websites. The devices we will develop work within your private network and can be controlled with your cell phones without the need for external accounts or authorising any third party to access your private network. Flexibility:  We will show you how to adapt the devices to fit your own needs or re-purpose them for a completely different task. The Pool Cover Opener that we describe in this article can be used as a remote control to open your pool cover or can be reused to open your garage door or the front gate in the same way. Simplicity : For hobbyists who like to tinker with electronics and/or programming, we offer them the d...