2

Here's the problem I'm trying to solve...

I'm writing a program that controls a piece of test equipment. The program polls the instrument via serial interface every second, and gets a string of test data in return.

Connection speed is 9600 baud, and the instrument only sends about 100 characters at a time, so performance is not an issue.

One of the requirements is a "cycle" mode, which is should work as follows:

  1. The user sets up three timer intervals - Pre-Test time, Test time, and Inter-test time - and clicks a Start button on the UI.

  2. The program commands the instrument to start a test cycle, and displays data received by the instrument until the Pre-Test time has expired.

  3. The program then displays AND records the data until the Test time has expired.

  4. The program then displays data until the Inter-test time has expired.

  5. Steps 2 - 4 are repeated until the user clicks a Stop button on the UI.

In the past, I've written simple state machines for this kind of problem. But for this project, (and with the help of SO Rx experts), I've been able to use Rx to parse the serial data, and am very happy with the resulting code.

My intuition tells me that this problem might also be nicely handled by Rx, but it's not at all clear how to proceed.

So, is my intuition correct, or would you suggest a different approach?

If Rx is a good fit, could someone provide some quick and dirty sample code that demonstrates a solution?

Edit...as per Enigmativity:

Here's the code that reads the serial data. In a nutshell, it converts a stream of serial data into individual packet strings e.g. "A0", "C00004", "H0501100", etc. These are passed to the ProcessPacket method, which interprets the packets as instrument status, pressures, etc.

    /// <summary>
    /// Create a Reactive Extensions subscription to the selected serial port
    /// </summary>
    private IDisposable SetupSerialDataSubscription()
    {
        // Create Observable stream of chars from serial port's DataRecieved event
        var serialData = Observable
                .FromEventPattern<SerialDataReceivedEventArgs>(_serialPort, "DataReceived")

                // Collapse groups of chars into stream of individual chars
                .SelectMany(_ =>
                {
                    int bytesToRead = _serialPort.BytesToRead;
                    byte[] bytes = new byte[bytesToRead];
                    int nbrBytesRead = _serialPort.Read(bytes, 0, bytesToRead);

                    // Empty array if no bytes were actually read
                    if (nbrBytesRead == 0)
                        return new char[0];

                    var chars = Encoding.ASCII.GetChars(bytes);

                    return chars;
                })

                // Strip out null chars, which can cause trouble elsewhere (e.g. Console.WriteLine)
               .SkipWhile(c => c == '\0');

        // Emits packets, one string per packet, e.g. "A0", "C00004", "H0501100", etc
        var packets = serialData
            .Scan(string.Empty, (prev, cur) => char.IsLetter(cur) ? cur.ToString() : prev + cur)
            .Where(IsCompletePacket);

        // Process each packet, on the UI thread
        var packetsSubscription = packets
            .ObserveOn(this)
            .Subscribe(packet => ProcessPacket(packet) );

        return packetsSubscription;
    }
Tom Bushell
  • 5,865
  • 4
  • 45
  • 60
  • I think you need to provide at least some code - at the very least the observable's and their signatures - for us to provide a nice concise answer. – Enigmativity Nov 11 '14 at 01:40
  • 1
    @Enigmativity I think it's too harsh to close this for no-code, when the OP says "it's not at all clear how to proceed" - not at all uncommon with Rx. I think OP has demonstrated effort and set a clear question which has value, and I've shown a reasonable concise answer is possible. If there was non-working code that hadn't been posted, I'd say that was a fair criticism, but that doesn't apply here. I can't see what code could be reasonably provided that would add value. What did you have in mind? I suppose OP can add stubs for the data, but does it make much difference? – James World Nov 11 '14 at 15:44
  • @JamesWorld - I appreciate your comment. The thinking that I went thru was basically along the lines that I needed to decipher what the input streams would be, what type they would have, etc, before I could try to code a solution. The chances of me getting the inputs right would be fairly low so then the solution could easily be wrong. It's a lot of thinking with the potential of a poor result. Had the OP fleshed out the question with a little more detail that he should know then I think it would be a much more useful question. Bottom-line, my brain hurt too much when deciphering the question. – Enigmativity Nov 11 '14 at 22:54
  • @Enigmativity It's a close call for sure... let's see if I get any love from OP! :) – James World Nov 12 '14 at 00:13
  • @Enigmativity - I have added some code to the question – Tom Bushell Nov 13 '14 at 18:44

1 Answers1

2

You could do this. Use a helper method to emit an infinite IEnumerable which delivers a recording signal (true/false) and a timespan:

public IEnumerable<Tuple<TimeSpan, bool>> GetTestPeriods()
{
    TimeSpan preTestTime = TimeSpan.FromSeconds(5);
    TimeSpan testTime = TimeSpan.FromSeconds(3);
    TimeSpan interTestTime = TimeSpan.FromSeconds(2);

    yield return Tuple.Create(preTestTime, false);
    while(true)
    {
        yield return Tuple.Create(testTime, true);
        yield return Tuple.Create(interTestTime, false);
    }
}

This can be used to create a cycle stream that outputs a recording signal at the appropriate times:

// cycle outputs true at start of recording period
// false at start of a display period
var cycle = GetTestPeriods()
    .Select(s => Observable.Return(s.Item2)
        .Concat(Observable.Empty<bool>().Delay(s.Item1)))
    .Concat();

Now we can make some random data:

// make some random data
var random = new Random();
var dummyData = Observable.Interval(TimeSpan.FromSeconds(1))
                          .Select(_ => random.Next(10));

Set up a subject to signal stopping (you could use FromEventPattern to hook a button click here instead):

var stop = new Subject<Unit>();

And then combine the data with the record signal until stop is signaled. This pairs each data point with a boolean indicating if it is to be displayed or recorded:

var output = cycle.CombineLatest(dummyData, (c,d) => Tuple.Create(c,d))
    .TakeUntil(stop);

This stream you could publish and subscribe a couple of streams using Where filters to get the data to record separately from the data to display. Here's a test that timestamps the stream and dumps the data out:

// timestamp data and "record" or "display" it.
output.Timestamp()
    .Subscribe(x => {
        if(x.Value.Item1)
            Console.WriteLine(x.Timestamp + ": Recording " + x.Value.Item2);
        else
            Console.WriteLine(x.Timestamp + ": Displaying " + x.Value.Item2);
    },
    () => Console.WriteLine("Stopped")); 

Finally some code that emits the stop signal:

Console.ReadLine();
stop.OnNext(Unit.Default);
James World
  • 29,019
  • 9
  • 86
  • 120
  • You have correctly interpreted my requirements, and I've successfully tested your solution in LINQPad. It works, except for one quirk - every time is one second too long e.g. if test time is set to 3 seconds, actual time will be 4 seconds. Probably an off by one error somewhere - I think I can fix it if I go this route. – Tom Bushell Nov 13 '14 at 18:22