Reputation: 2160
I have a serial communications protocol with several commands, all of which are specified in a text file. I have a library that provides a basic method of communicating, but I want to add parsing without having to hard-code all of the structures specifying the packet formats. From what I've found, this may be possible using XML or JSON, but I don't know where to start.
An example of a command format specified in my text file would be something like:
[255]
Name = Get Relay State
Param1 = Relay Number, uint8_t
Result1 = Relay State, uint8_t
So, the byte array a request of this command to get relay 1 would be FF 01
and its response would be something like FF 00
(indicating the relay is open).
This is a highly simplified example as there are hundreds of these commands, each with numerous inputs and outputs.
Is there some way I can extract the values, field names, and command name without explicitly declaring a struct
for every input and output in each command? I've done that with several so far but even with using code generation tools it is not flexible to changes in the protocol.
Upvotes: 0
Views: 859
Reputation: 3255
You can use the class I wrote to parse in a serial protocol that you define. I haven't tested this code, so it may require a few tweaks to get it running. This is based on the XML schema I have defined below:
<?xml version="1.0" encoding="utf-8" ?>
<protocol>
<!-- Get Relay State (255) -->
<command name="GetRelayState" value="255">
<parameters>
<parameter name="RelayNumber" type="Byte"/>
</parameters>
<results>
<result name="RelayState" type="Byte"/>
</results>
</command>
<!-- Get Voltage (254) -->
<command name="GetVoltage" value="254">
<parameters>
<parameter name="CircuitNumber" type="Short"/>
</parameters>
<results>
<result name="Voltage" type="Single"/>
<result name="MaxVoltage" type="Single"/>
<result name="MinVoltage" type="Single"/>
</results>
</command>
</protocol>
Here is the C# code
public class ProtocolManager
{
public event Action<Command> ResponseReceived;
// This will read the XML definition of your protocol into a Protocol object
public Protocol BuildProtocol()
{
var protocol = new Protocol
{
Commands = new List<Command>()
};
var xmlFile = new XmlDocument();
xmlFile.Load(
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Protocol.xml"));
var protocolElement = xmlFile["protocol"];
foreach (XmlNode command in protocolElement.ChildNodes)
{
// Load the command definition
var comm =
new Command
{
Name = command.Attributes["name"].Value,
Value = int.Parse(command.Attributes["value"].Value),
Parameters = new List<Parameter>(),
Results = new List<Result>()
};
// Load the list of parameters
foreach (XmlNode p in command.SelectSingleNode("parameters").ChildNodes)
{
comm.Parameters.Add(
new Parameter
{
Name = p.Attributes["name"].Value,
ParamType = Type.GetType(p.Attributes["type"].Value)
});
}
// Load the list of expected results
foreach (XmlNode p in command.SelectSingleNode("results").ChildNodes)
{
comm.Parameters.Add(
new Parameter
{
Name = p.Attributes["name"].Value,
ParamType = Type.GetType(p.Attributes["type"].Value)
});
}
protocol.Commands.Add(comm);
}
// You should now have a complete protocol which you can bind to UI controls
// such as dropdown lists, and be able to parse
return protocol;
}
// This will test the incoming stream for responses (packets)
public void TestProtocol(SerialPort port, Protocol p)
{
int bytesRead = 0;
var buffer = new byte[1024];
do
{
Thread.Sleep(50); // Give the port 50ms to receive a full response
bytesRead = port.Read(buffer, 0, buffer.Length);
// For the sake of simplicity assume we read the whole packet in a single read and we don't
// need to look for message encapsulation (STX, ETX).
var commandValue = buffer[0];
var command = p.Commands.SingleOrDefault(c => c.Value == commandValue);
if (command == null)
{
throw new NotSupportedException(
String.Format("Unknown command received {0}", commandValue));
}
// Read result parameters
int index = 1;
foreach (var r in command.Results)
{
// Here you need to implement every data type you are expecting to receive
// I have done 2
switch (r.ResultType.Name)
{
case "Byte":
r.Value = buffer[index];
index++; // Reading only a single byte
break;
case "Short":
r.Value = (float)(buffer[index] >> 8 | buffer[index]);
index += 2; // Reading a 16bit short is 2 bytes
break;
default:
throw new NotSupportedException(
String.Format("Unknown response type {0}", r.ResultType.Name));
}
}
// Notify listening client that we received a message response
// and it will contain the response parameters with values
var evt = ResponseReceived;
if (evt != null)
{
evt(command);
}
} while (bytesRead > 0);
// End of read / wait for next command to be sent
}
}
// Represents your serial protocol as a list of support commands
// with request/response parameters
public class Protocol
{
public List<Command> Commands { get; set; }
// Used to data-bind to comboboxes etc.
public List<string> GetCommandNames()
{
return Commands.Select(c => c.Name).OrderBy(c => c).ToList();
}
}
// A single command with a name, value and list of request/response parameters
public class Command
{
public string Name { get; set; }
public int Value { get; set; }
public List<Parameter> Parameters { get; set; }
public List<Result> Results { get; set; }
}
public class Parameter
{
public string Name { get; set; }
public Type ParamType { get; set; }
public object Value { get; set; }
}
public class Result
{
public string Name { get; set; }
public Type ResultType { get; set; }
public object Value { get; set; }
}
Upvotes: 1