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
Post a Comment