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 or C# can include the option to update the device’s firmware using the class described below.
The full class can be found on Github.
Flutter/Dart Version:
// // 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); // // This code is provided AS IS by Upwind Tecnologias Lda. and can be freely // used for personal or commercial purposes // import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; import 'package:crypto/crypto.dart'; import 'dart:convert'; // 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; } }
C# Version:
// FirmwareUploader is a C# class that can be used to upload firmware from a .NET
// application to any device running Arduino OTA (Over The Air)
// Sample Usage:
string fileName = @"firmware.ino.bin";
string password = "[password]";
string IP = "[hostname]";
int Port = portnumber;
FileStream fs = new FileStream(fileName, FileMode.Open);
// create an Uploader object
Uploader uploader = new Uploader();
// update the device
uploader.FirmwareUpload(IP, Port, password, fs);
Console.Write(uploader.Log);
class Uploader
{
public string Log;
// Send/Receive timeout in seconds
public int Timeout = 5;
/// <summary>
/// Update a device running the Arduino OTA (Over The Air) module with new firmware
/// </summary>
/// <param name="deviceAddress">IP address of the device that we are updating</param>
/// <param name="devicePort">Port number on which ArduinoOTA is set to listen</param>
/// <param name="password">ArduinoOTA password if requested</param>
/// <param name="firmware">Stream containing the new firmware</param>
/// <returns>true if upload was successful</returns>
///
public bool FirmwareUpload(string deviceAddress, int devicePort, string password, Stream firmware)
{
//
// code adapted from Arduino's espota.py
//
bool success = false;
UdpClient udpClient = new UdpClient();
IPEndPoint RemoteIpEndPoint = new IPEndPoint(IPAddress.Any, 0);
Log = "";
try
{
string hash;
// get the MD5 hash of the file
using (var md5Hash = MD5.Create())
{
// Generate hash value(Byte Array) for input data
var hashBytes = md5Hash.ComputeHash(firmware);
// Convert hash byte array to string
hash = BitConverter.ToString(hashBytes).Replace("-", string.Empty).ToLower();
}
// We need the local port number to send it to the device in the first command
if (udpClient.Client.LocalEndPoint == null)
{
udpClient.Connect(deviceAddress, devicePort);
udpClient.Client.SendTimeout = Timeout * 1000;
udpClient.Client.ReceiveTimeout = Timeout * 1000;
}
int localPport = ((IPEndPoint)udpClient.Client.LocalEndPoint).Port;
// the first command is U_FLASH (value 0) followed by the local port, the file length and and the file hash
String command = String.Format("0 {0} {1} {2}\n", localPport, firmware.Length, hash);
Byte[] sendBytes = Encoding.ASCII.GetBytes(command);
udpClient.Send(sendBytes, sendBytes.Length);
// Blocks until a message is returned from the device
Byte[] receiveBytes = udpClient.Receive(ref RemoteIpEndPoint);
string returnData = Encoding.ASCII.GetString(receiveBytes);
if (returnData.StartsWith("AUTH"))
{
// device is requesting the password in a very specific format
string nonce = returnData.Split(' ')[1];
string cnonce = Hash(String.Format("{0}{1}{2}", firmware.Length, hash, deviceAddress)); // any random 32 character string will do here
string result = Hash(String.Format("{0}:{1}:{2}", Hash(password), nonce, cnonce));
string message = String.Format("200 {0} {1}\n", cnonce, result); // 200 = AUTH
// send the hashed password and wait for OK or ERR return from device
sendBytes = Encoding.ASCII.GetBytes(message);
udpClient.Send(sendBytes, sendBytes.Length);
receiveBytes = udpClient.Receive(ref RemoteIpEndPoint);
returnData = Encoding.ASCII.GetString(receiveBytes);
if (!returnData.StartsWith("OK"))
{
Exception e = new Exception("Authentication Failed");
}
}
// send firmware data to device in small blocks
const int block_size = 1460;
// start a TCP listener on the same port as the UDP listener and wait for device to connect back to us
TcpListener listener = new TcpListener(IPAddress.Any, localPport);
listener.Server.ReceiveTimeout = listener.Server.SendTimeout = Timeout * 1000;
listener.Start();
TcpClient tcpClient = listener.AcceptTcpClient();
tcpClient.SendBufferSize = block_size;
// rewind file to beginning, the call to ComputeHash moves the file pointer
firmware.Seek(0, SeekOrigin.Begin);
byte[] buf = new byte[block_size];
string ret = "";
while (true)
{
int bytes_read = firmware.Read(buf, 0, block_size);
if (bytes_read > 0)
{
tcpClient.GetStream().Write(buf, 0, bytes_read);
int len = tcpClient.GetStream().Read(buf, 0, block_size); // ignore any returned value
ret = Encoding.ASCII.GetString(buf, 0, len);
}
if (bytes_read < block_size) // end of file
break;
}
// keep reading until we receive ERR or OK
while (!ret.Contains("ERR") && !ret.Contains("OK"))
{
int len = tcpClient.GetStream().Read(buf, 0, block_size); // ignore any returned value
ret = Encoding.ASCII.GetString(buf, 0, len);
}
success = ret.Contains("OK");
Log += "========== FLASH ==========\nFrom " + RemoteIpEndPoint.Address.ToString() + ":" + RemoteIpEndPoint.Port.ToString() + "\n";
Log += ret;
// close all connections
tcpClient.Close();
listener.Stop();
udpClient.Close();
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
return success;
}
static string Hash(string value)
{
string hash = "";
using (var md5Hash = MD5.Create())
{
// Generate hash value(Byte Array) for input data
var hashBytes = md5Hash.ComputeHash(Encoding.ASCII.GetBytes(value));
// Convert hash byte array to string while removing the dashes
hash = BitConverter.ToString(hashBytes).Replace("-", string.Empty).ToLower();
}
return hash;
}
}