Search code examples
c#asynchronoustaskcompletionsource

UDP protocol with TaskCompletionSource and async/await


I have a library that talks to a hardware device using UDP. The conversation goes something like this:

|------------000E------------>|
|                             |
|<-----------000F-------------|
|                             |
|------------DC23------------>|
|                             |
|<-----------DC24-------------|

First I send out opcode 000E and expect to get a 000F in response. Once I get the 000F, I send out a DC23 and expect a DC24 in response. (There is additional information included in the response along with the opcode.) In the future, more steps may need to be added to this conversation.

The object in charge of communicating with the device has the following interface:

public class Communication : ICommunication
{
    public Communication();
    public bool Send_LAN(byte subnetID, byte deviceID, int operateCode, ref byte[] addtional);
    public event DataArrivalHandler DataArrival;
    public delegate void DataArrivalHandler(byte subnetID, byte deviceID, int deviceType, int operateCode, int lengthOfAddtional, ref byte[] addtional);
}

When I try to write this code naively I end up with a switch statement in the DataArrival event handler that does different things according to the response code, like so:

    private void _com_DataArrival(byte subnetID, byte deviceID, int deviceTypeCode, int operateCode, int lengthOfAddtional, ref byte[] addtional)
    {
        Debug.WriteLine($"OpCode: 0x{operateCode:X4}");
        switch (operateCode)
        {
        case 0x000F: // Response to scan
             // Process the response...
             _com.Send_LAN(subnet, device, 0xDC23, ...);
             break;
        case 0xDC24:
             // Continue processing...
             break;
        }
    }

It's beginning to look like it's going to turn into a state machine. I think there has to be a better way to do it using TaskCompletionSource and async/await.

How do I go about doing this?


Solution

  • If you just want to know how to use TaskCompletionSource here - you can do it for example like this:

    public Task<Response> RequestAsync(byte subnetID, byte deviceID, int deviceType, int operateCode, ref byte[] addtional, int expectedResponseCode, CancellationToken ct = default(CancellationToken)) {
        var tcs = new TaskCompletionSource<Response>();           
        DataArrivalHandler handler = null;
        handler = (byte sub, byte device, int type, int opCode, int length, ref byte[] additional) => {
            // got something, check if that is what we are waiting for
            if (opCode == expectedResponseCode) {
                DataArrival -= handler;
                // construct response here
                Response res = null; // = new Response(subnetID, deviceID, etc)
                tcs.TrySetResult(res);
            }
        };
        DataArrival += handler;
        // you can use cancellation for timeouts also
        ct.Register(() =>
        {
            DataArrival -= handler;
            tcs.TrySetCanceled(ct);
        });
        if (!Send_LAN(subnetID, deviceID, operateCode, ref addtional)) {
            DataArrival -= handler;                
            // throw here, or set exception on task completion source, or set result to null
            tcs.TrySetException(new Exception("Send_LAN returned false"));
        }
        return tcs.Task;
    }
    
    public class Response {
        public byte SubnetID { get; set; }
        // etc
    }
    

    Then you can use it in request-response manner:

    var response = await communication.RequestAsync(...);