2
\$\begingroup\$

Summary: Parsing an incoming stream of events from a binary communication protocol, if we have some variations in devices to support and would not like to have one huge switch to include everything.

Previous binary protocol variability V2.0.

This version is way simpler and shorter. Also I removed all the interfaces and implemented just the IEnumerable by the main EventParser component:

    static void Main(string[] args)
    {
        var inputStream = FakeInputDataStream();
        var binaryReader = new BinaryReader(inputStream);
        var tokenizer = new TurboGadgetTokenizer(binaryReader, Log.Console);
        foreach (dynamic e in new EventParser(tokenizer))
            Handle(e);
    }

    static void Handle(SessionStart e) =>
        Console.WriteLine("Hardware Version = " + e.Hardware);

    static void Handle(TemperatureReport e) =>
        Console.WriteLine("Ambient Temperature = " + e.Ambient);

There is an assumption here that we work with TurboGadget device based on Freescale controller, so the following libraries are defined:

enter image description here

Demo.IO

Let’s define log:

public abstract class Log : IDisposable
{
    public static readonly Log Null = new TextLog(TextWriter.Null);
    public static readonly Log Console = new TextLog(System.Console.Out);

    public virtual void Dispose() { }
    public abstract void Write(string line);                
}

Where:

public class TextLog : Log
{
    public TextLog(TextWriter writer)
    {
        Writer = writer;
    }

    public override void Dispose() =>
        Writer.Dispose();

    public override void Write(string line) =>
        Writer.WriteLine(line);

    TextWriter Writer { get; }
}

This helps BinaryReader with Big Endian:

public static class BigEndian
{
    public static int ReadInt32BE(this BinaryReader reader) =>
        BitConverter.ToInt32(reader.ReadValue(4), 0);

    public static uint ReadUInt32BE(this BinaryReader reader) =>
        BitConverter.ToUInt32(reader.ReadValue(4), 0);

    public static short ReadInt16BE(this BinaryReader reader) =>
        BitConverter.ToInt16(reader.ReadValue(2), 0);

    public static ushort ReadUInt16BE(this BinaryReader reader) =>
        BitConverter.ToUInt16(reader.ReadValue(2), 0);

    public static long ReadInt64BE(this BinaryReader reader) =>
        BitConverter.ToInt64(reader.ReadValue(8), 0);

    public static float ReadSingleBE(this BinaryReader reader) =>
        BitConverter.ToSingle(reader.ReadValue(4), 0);

    public static double ReadDoubleBE(this BinaryReader reader) =>
        BitConverter.ToDouble(reader.ReadValue(8), 0);

    static byte[] ReadValue(this BinaryReader reader, int size)
    {
        var bytes = reader.ReadBytes(size);
        if (BitConverter.IsLittleEndian)
            Array.Reverse(bytes);

        return bytes;
    }
}

The Tokenizer class requires a little bit of explanation. Its responsibility to fetch and log primitives (tokens) the events consist of, for example, having an event SessionStart which includes two tokens of type Version for Firmware and Hardware. There is a high probability for the token to be reused across multiple event types. The class below is just an abstraction providing general infrastructure for logging, and a single Discriminator() token extraction (a token to determine an upcoming event type). One could say that it violates SRP, but further purification will make consumption scenarios problematic...

public abstract class Tokenizer : IDisposable
{
    protected Tokenizer(BinaryReader reader, Log log)
    {
        Reader = reader;
        Log = log;
    }

    public void Dispose()
    {
        Reader.Dispose();
        Log.Dispose();
    }

    public abstract Type Discriminator();

    protected T Token<T>(string token, T value)
    {
        Log.Write($"{token}={value}");
        return value;
    }

    protected T[] Token<T>(string token, IEnumerable<T> value)
    {
        var array = value.ToArray();
        Log.Write($"{token}=[{string.Join(",", array)}]");
        return array;
    }        

    protected abstract Protocol Protocol { get; }
    protected BinaryReader Reader { get; }
    Log Log { get; }
}

It depends on Protocol - a dictionary class to map a discriminator to the associated type.

public class Protocol
{
    public static readonly Protocol Undefined = 
        new Protocol(ImmutableDictionary<int, Type>.Empty);

    Protocol(IImmutableDictionary<int, Type> types)
    {
        Types = types;
    }

    public Protocol Support<TEvent>(int discriminator) =>
        new Protocol(Types.Add(discriminator, typeof(TEvent)));

    public Type this[int discriminator]
    {
        get
        {
            //@Heslacher - guard condition is two lines shorter :)
            if(!Types.ContainsKey(discriminator))
                throw new NotSupportedException();

            return Types[discriminator];
        }
    }

    IImmutableDictionary<int, Type> Types { get; }
}

EventParser is a façade class:

public class EventParser : IEnumerable
{
    public EventParser(Tokenizer tokenizer)
    {
        Tokenizer = tokenizer;
    }

    public IEnumerator GetEnumerator()
    {
        object e = null;
        using (Tokenizer)
            while (TryRead(out e))
                yield return e;
    }

    bool TryRead(out object e)
    {
        try
        {
            var type = Tokenizer.Discriminator();
            e = Activator.CreateInstance(type, Tokenizer);
            return true;
        }
        catch (ObjectDisposedException)
        {
            e = null;
            return false;
        }
    }

    Tokenizer Tokenizer { get; }
}

Now comes the library demonstration code. We will have couple of projects – one for the controller specific stuff, another one is for the concrete device.

Inheritance chain looks this way:

enter image description here

Demo.IO.Freescale - specific to controller, common firmware, etc.

// big-endian
public abstract class FreescaleTokenizer : Tokenizer
{
    protected FreescaleTokenizer(BinaryReader reader, Log log) 
        : base(reader, log)
    {
    }

    protected override Protocol Protocol => Protocol.Undefined
        .Support<SessionStart>(1);

    public override Type Discriminator() => 
        Token("discriminator", Protocol[Reader.ReadInt32BE()]);

    public virtual Version Version() =>
        Token("version", 
            new Version(Reader.ReadInt32BE(), Reader.ReadInt32BE()));

    public virtual DataChannel[] Channels() =>
        Token("channels", 
            Enumerable.Range(0, int.MaxValue)
                .Select(i => Reader.ReadByte())
                .TakeWhile(c => c != 0)
                .Select(c => (DataChannel)c));
}

Where SessionStart event is supposed to dehydrate itself:

public class SessionStart
{
    public SessionStart(FreescaleTokenizer tokenizer)
        : this(
              hardware: tokenizer.Version(),
              firmware: tokenizer.Version(),                  
              availableChannels: tokenizer.Channels())
    {
    }

    public SessionStart(
        Version hardware, 
        Version firmware, 
        params DataChannel[] availableChannels)
    {
        Hardware = hardware;
        Firmware = firmware;
        AvailableChannels = availableChannels;
    }

    public Version Hardware { get; }
    public Version Firmware { get; }
    public IEnumerable<DataChannel> AvailableChannels { get; }
}

Demo.IO.Freescale.TurboGadget - specific to concrete device

public class TurboGadgetTokenizer : FreescaleTokenizer
{
    public TurboGadgetTokenizer(BinaryReader reader, Log log) 
        : base(reader, log)
    {
    }

    protected override Protocol Protocol => base.Protocol
        .Support<TemperatureReport>(2);

    public virtual float Temperature() =>
        Token("temperature",
            Reader.ReadSingleBE());
}

Where our device specific event is:

public class TemperatureReport
{
    public TemperatureReport(TurboGadgetTokenizer tokenizer)
        :this(
             board: tokenizer.Temperature(),
             ambient: tokenizer.Temperature())
    {
    }

    public TemperatureReport(float board, float ambient)
    {
        Board = board;
        Ambient = ambient;
    }

    public float Board { get; } 
    public float Ambient { get; }
}
\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.