1

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?

Ron Inbar
  • 2,044
  • 1
  • 16
  • 26

2 Answers2

1

You would write it like you would write it with synchronous IO, and often that's much easier than the event based code that you have.

For example you could say:

await SendAsync("000E");
var received = await ReceiveAsync();
if (received != "000F") AbortConnection();

await makes it possible to use async IO with synchronous patterns.

usr
  • 168,620
  • 35
  • 240
  • 369
  • OK, but how do I implement the awaitable SendAsync and ReceiveAsync, given the event-based interface above? – Ron Inbar Jul 03 '17 at 14:14
  • There are some standard patterns for that. This is always possible. https://stackoverflow.com/questions/12858501/is-it-possible-to-await-an-event-instead-of-another-async-method You only need thin wrappers. All the logic goes into await based code calling the wrappers. To illustrate the point you could await a button click in a GUI. – usr Jul 03 '17 at 14:24
1

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(...);
Evk
  • 98,527
  • 8
  • 141
  • 191
  • Do you think it's still possible to use async/await when a single 000E request can get multiple 000F responses and each 000F has to be followed by its own DC23? – Ron Inbar Jul 04 '17 at 16:38
  • @RonInbar you can make multiple requests in a row if I understand correctly. First send 000E and expect 000F. Then send DC23 and expect 000F again as many times as necessary. – Evk Jul 04 '17 at 16:48
  • What happens is the 000E is sent to a *broadcast address* and each device on the bus responds with a 000F with its information. Then a DC23 request has to be sent out to each device, asking for more specific information from that device. Your code detaches the event handler when a response arrives, so it won't be able to deal with more than one response to a single request. What will happen if you don't detach the event handler? – Ron Inbar Jul 04 '17 at 17:09
  • 1
    That is very different situation. When response arrives you should not detach handler but instead call some callback function which will work with this client. And instead of returning success on first response - keep handler attached for some period (always). But do not modify this method - create separate one. If you will struggle with this - I can provide some example. – Evk Jul 04 '17 at 17:39
  • Do you think it would make sense to add an overload with the following signature `Task> RequestAsync(byte subnetID, byte deviceID, int opCode, int expectedResponseCode, int timeoutInMilliseconds)` that waits until the timeout expires and returns all the responses that it got so far? – Ron Inbar Jul 05 '17 at 13:01
  • Sure (but better name it different, like RequestMultipleAsync). Note however that in this case all your clients will have a delay up to `timeoutInMilliseconds` before you send them next chunk of data, because you will always wait the whole timeout interval. If that is acceptable - then it's a fine way to handle this. – Evk Jul 05 '17 at 13:05
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/148416/discussion-between-ron-inbar-and-evk). – Ron Inbar Jul 05 '17 at 13:13