Please note: An updated solution is here
Let’s imagine that we designed more or less universal firmware for a microcontroller unit to be used across multiple types of devices. So we have something common in the binary data exchange protocol, and something different.
The most common way to define incoming command parsing is usually about switching
over one huge enum
with all the command discriminators (command selection codes), so system knows what command to read. The problem is enum
+ switch
are difficult to maintain and almost impossible to distribute code between helper library, shared controller, and specific device related projects.
Proposed solution has been divided into three libraries where we can redefine protocol specifics.
namespace Company.Hardware
First of all, classes to map commands to discriminators:
public class Commands
{
public static readonly Commands None = new Commands();
protected Commands()
{
}
public virtual Type this[int discriminator]
{
get { throw new NotSupportedException(); }
}
public Commands Include<TCommand>(int discriminator) =>
new Commands<TCommand>(discriminator, this);
}
class Commands<TCommand> : Commands
{
public Commands(int discriminator, Commands next)
{
Discriminator = discriminator;
Next = next;
}
public override Type this[int discriminator] =>
discriminator == Discriminator ?
typeof(TCommand) :
Next[discriminator];
int Discriminator { get; }
Commands Next { get; }
}
Mapping schema might be defined as:
Commands Commands = Commands.None
.Include<VersionCommand>(10),
.Include<ReadyCommand>(20);
We can get command Type
back:
Type type = Commands[20];
Next class allows to read and log low level primitives of the protocol; command components like discriminator itself. We will inherit it later to define more specific versions of the protocol. It is basically a tokenizer.
public abstract class ProtocolReader : IDisposable
{
protected ProtocolReader(BinaryReader reader, TextWriter log)
{
Reader = reader;
Log = log ?? TextWriter.Null;
}
public void Dispose()
{
Reader.Dispose();
Log.Dispose();
}
public Type ReadDiscriminator() =>
Commands[
Write("discriminator={0}",
Reader.ReadInt32())];
protected T Write<T>(string format, T value)
{
Log.WriteLine(format, value);
return value;
}
protected virtual Commands Commands => Commands.None;
protected BinaryReader Reader { get; }
protected TextWriter Log { get; }
}
Now a façade to read commands:
public class CommandReader : IDisposable
{
public CommandReader(ProtocolReader reader)
{
Reader = reader;
}
public void Dispose() => Reader.Dispose();
public dynamic Read()
{
try
{
var type = Reader.ReadDiscriminator();
return Activator.CreateInstance(type, Reader);
}
catch (ObjectDisposedException)
{
return null;
}
catch(Exception)
{
throw;
}
}
ProtocolReader Reader { get; }
}
namespace Company.Hardware.Controller
Let’s define more types of primitives and a command to read:
public abstract class ControllerReader : ProtocolReader
{
public ControllerReader(BinaryReader reader, TextWriter log)
: base(reader, log)
{
}
protected override Commands Commands => base.Commands
.Include<VersionCommand>(1);
public Version ReadVersion() =>
Write("version={0}",
new Version(
Reader.ReadInt32(),
Reader.ReadInt32()));
}
The only responsibility of the command is to rehydrate itself properly:
public class VersionCommand
{
public VersionCommand(ControllerReader reader)
{
Hardware = reader.ReadVersion();
Firmware = reader.ReadVersion();
}
public Version Hardware { get; }
public Version Firmware { get; }
}
namespace Company.Hardware.Controller.Device
Here the specifics come. Let’s define one more command.
public class ReadyCommand
{
public ReadyCommand(DeviceReader reader)
{
Status = reader.ReadStatus();
}
public string Status { get; }
}
And a final version of protocol reader:
public class DeviceReader : ControllerReader
{
public DeviceReader(BinaryReader reader, TextWriter log)
: base(reader, log)
{
}
protected override Commands Commands => base.Commands
.Include<ReadyCommand>(2);
public string ReadStatus() =>
Write("status={0}",
Reader.ReadString());
}
Demo
static void Main(string[] args)
{
using (var inputStream = FakeInputDataStream())
using (var binaryReader = new BinaryReader(inputStream))
using (var protocolReader = new DeviceReader(binaryReader, Console.Out))
using (var reader = new CommandReader(protocolReader))
while(true)
{
var command = reader.Read();
if (command != null)
Handle(command);
else
break;
}
}
static void Handle(VersionCommand command) =>
Console.WriteLine("VERSION " + command.Hardware);
static void Handle(ReadyCommand command) =>
Console.WriteLine("READY " + command.Status);