.NET Class to update the firmware of a device running ArduinoOTA

Amyuni's Development Team would like to share with fellow developers general information about Windows and Web development. These are issues encountered by Amyuni's team and were either solved or pending solution.
Post Reply
Devteam
Posts: 121
Joined: Fri Oct 14 2005
Location: Montreal
Contact:

.NET Class to update the firmware of a device running ArduinoOTA

Post by Devteam »

Use Case
ArduinoOTA (Over The Air) is commonly used to update the firmware of IoT devices either during development or in production environments. During development, this is commonly done from the Arduino programming environment or Visual Studio, both of which run the espota.py Python script in the background. In production environments, we need to update the firmware of these devices over the wired or wireless network without having to install Python and running an external script. Example, we could have a mobile application developed with Xamarin or a desktop application developed in Visual Studio that can connect to a device and automatically update its firmware to the latest version.

Here is an example of how the code can be used:

Code: Select all

string fileName = @"C:\temp\firmware2.0.ino.bin";
string password = "MySecretPassword";
string IP = "192.168.1.154";
int Port = 8080;
	
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);
Source-code of the Uploader class:

Code: Select all

class Uploader
{
	/// <summary>
	/// Log contains any log messages generated by the FirmwareUpload method
	/// </summary>
	public string Log;

	/// <summary>
	/// Send/Receive timeout in seconds
	/// Default is 5 seconds
	/// </summary>
	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();
			}

			// ArduinoOTA will communicate back to us on 2 different ports
			// The first port is the local port number from which we are sending the data
			// The second port is any port we specify in the first command we send to the device
			// It sounds easier to use the same port for both, which is what we are doing here
			// 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));      // this hash is not actually used by ArduinoOTA so 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
			// ComputeHash returns upper case letters with each byte separated by a dash
			// we need to remove the dashes and convert to lower case for Arduino
			hash = BitConverter.ToString(hashBytes).Replace("-", string.Empty).ToLower();
		}

		return hash;
	}
}
Amyuni Development Team

Efficient and accurate conversion to PDF, XPS, PDF/A and more. Free trials at - https://www.amyuni.com
Post Reply