COMMS
Template library intended to help with implementation of communication protocols.
|
Fields are abstractions around value storage primitives and/or objects, such as integral values, floating point values, strings, arrays, etc.. Every field class is defined in comms::field namespace and exposes predefined interface in order to make template meta-programming as easy as possible. As an example let's take a look at comms::field::IntValue class which is used to define integral value field.
The main things to note are that every field definition class:
Also note that all the member function are NON-virtual, i.e. the field abstractions do not have polymorphic behaviour.
The available fields abstractions are:
Integral values are abstracted by comms::field::IntValue class, which receives at least two template parameters. The first one is a base class, from which the comms::field::IntValue will inherit. It must be a variant of comms::Field, with the option specifying endian used for data serialisation. The second template parameter is a basic integral type that is used to store the field's value.
For example:
The example above defines a field that uses std::int16_t type to store its value. The value can be accessed using value() member function:
When such field is serialised, 2 bytes (sizeof(std::int16_t)) are written to the output buffer, most significant first and less significant second (because MyFieldBase base class was defined using comms::option::def::BigEndian option).
Sometimes protocol specification tries to reduce amount of data transferred over I/O link. It may define serialisation length of the field that differs from standard length of basic integral types, such as std::int8_t, std::uint8_t, std::int16_t, std::uint16_t, std::int32_t, std::uint32_t, ... For example, some field may only have values between 0 and 10,0000,000, which may be encoded using only 3 bytes, and that's what the protocol specifies. The storage type for such value is going to be std::uint32_t, but there is a need to limit serialisation length for it. The COMMS library provides comms::option::def::FixedLength option, that can be used for this purpose.
There are protocols, that try to reduce amount of traffic over I/O link by using variable length when serialising numeric value. Usually it is Base-128 encoding, where the most significant bit in the byte indicates whether it is the last byte in the numeric encoding or the next one also needs to be taken into account. The COMMS library provides comms::option::def::VarLength option that can be used with comms::field::IntValue and modifies the behaviour of the latter to expose the required read()/write()/length() behaviour:
The field's base class (MyFieldBase) contains endian information which is used to determine which part of the value is serialised first.
There are cases when there is a need to add/subtract some predefined offset to/from the value of the field when serialisation takes place. Good example of such case would be serialising a "current year" value. Most protocols now specify it as an offset from year 2000 or later and serialised as a single byte, i.e. to specify year 2015 is to write value 15. However it may be inconvenient to manually adjust serialised/deserialised value by predefined offset 2000. To help with such case option comms::option::def::NumValueSerOffset can be used. For example:
Sometimes systems operate with floating point numbers. Let's say to handle the distance between two points on the map in meters. However, when communicating this information over the I/O link, the developers often scale the floating point value up in order to send such value as integer. For example, the distance is communicated in millimeters (when calculated and handled in meters). The definition of such field may look like:
The comms::option::def::ScalingRatio option allows scaling of serialised value (distance in mm) to handling value (distance in m) and vice versa:
The scaling may work in the opposite direction of increasing the number. For example, the field contains number of tens of millimeters between two points. It would be convenient to be able to convert it to proper millimeters number. As the result the field can be defined as:
The comms::option::def::ScalingRatio option allows scaling of serialised value (distance in tens of mm) to handling value (distance in mm) and vice verse:
Methods comms::field::IntValue::getScaled and comms::field::IntValue::setScaled take into account scaling ratio provided (with comms::option::def::ScalingRatio option) to the comms::field::IntValue field. If such option wasn't used
comms::option::def::ScalingRatio<1, 1> is assumed.
In addition to Scaling Value, the COMMS library provides an ability to specify field's value units and perform conversion between units of the same type. Let's get back to the same example of defining distance between two point, but instead of providing scaling ratio directly, the type of the units is specified.
The comms::option::def::UnitsMillimeters option specifies that field contains distance in millimeters, which allows COMMS library provide proper conversion to other distance units when necessary:
In the examples above the "units" specification may replace the "scaling" information. However, there are cases when it they may complement each other. For example, the field contains "latitude" information in degrees but multiplied by 10'000'000 to make integral value out of floating point.
The COMMS library uses the scaling ratio as well as units information to be able to convert the stored value between degrees and radians when needed.
The COMMS library provides mulitple options to specify the units of the field's value:
All the units conversion functions reside in comms::units namespace. NOTE, that conversion can be applied only between the units of the same type. The units compitability check is performed at compile time and the compilation will fail on attempt to set/get incompatible value, such as setting/getting "seconds" to/from the field specified as containing millimeters.
The whole units conversion functionality can be useful in a client code, that requires usage of particular unit types in its internal calculations. It can use conversion functions without any need to know scaling ratio and/or actual units of the field, the COMMS library will do all the necessary math calculation to provide the requested value.
There multiple common options that are applicable to all the fields, comms::field::IntValue included. Please refer to Common Options or Modifications for the Fields for more details.
Sometimes it is more convenient to operate with enum types instead of integral values. For example, the custom protocol message carries information of how to configure some external serial port, and one of the values is the baud rate. In order not to impose too much overhead on I/O link, the protocol developers decided to use single byte to indicate one standard baud rate:
Baud Rate | Serialisation Value |
---|---|
9600 | 0 |
14400 | 1 |
19200 | 2 |
28800 | 3 |
38400 | 4 |
57600 | 5 |
115200 | 6 |
It would be more convenient to define enum type to operate with, instead of using raw numbers.
comms::field::EnumValue is very similar to comms::field::IntValue. The main difference is using enum instead of integral type as a second template parameter. The default serialisation length is determined by the underlying type of the enum. That't why it is important to explicitly specify the underlying type of the enum when defining it, and not leave this to the compiler.
The comms::field::EnumValue field supports almost all the options that can be used with comms::field::IntValue: Modifying Serialisation Length, Variable Serialisation Length, Serialisation Offset, as well as Common Options or Modifications for the Fields.
Quite often messages in communication protocol use some kind of flags, where single bit has a independent meaning. It is more convenient to treat such flags as bitmasks rather than integral values. comms::field::BitmaskValue provides a convenient interface to handle such bitmasks.
By default the underlying storage type of the comms::field::BitmaskValue is unsigned, which makes the default serialisation length to be sizeof(unsigned). The modification of the underlying storage type as well as serialisation length can be done using comms::option::def::FixedLength option (see Modifying Serialisation Length). The underlying type will always be some unsigned integral type. If the serialisation length is specified to be 1 byte, the underlying storage type is std::uint8_t, if the serialisation length is 2 bytes, the underlying storage type is std::uint16_t, if the serialisation length is 3 or 4 bytes, the underlying storage type is std::uin32_t, etc...
All the Common Options or Modifications for the Fields can also be used with comms::field::BitmaskValue.
Quite often the bitmask fields contain reserved bits, which must preserve some values (usually 0). The comms::field::BitmaskValue fields support usage of comms::option::def::BitmaskReservedBits alias option. The template parameters of the option specify mask for reserved bits as well as their expected values. The check for the reserved bits values is performed inside comms::field::BitmaskValue::valid() member function.
Quite often there is a need to provide names for the bits in the comms::field::BitmaskValue field. It is possible to define it as external independent enum. However, it may be convenient to define it as internal type. It is possible to do by inheriting from appropriate comms::field::BitmaskValue type and use COMMS_BITMASK_BITS() macro to define names for bits. For example
is equivalent to defining:
NOTE, that provided names have found their way to BitIdx enum type, and got prefixed with BitIdx_. This indices may be used with comms::field::BitmaskValue::getBitValue() and comms::field::BitmaskValue::setBitValue() member functions.
Also note, that there is automatically generated BitIdx_numOfValues value to indicate end of the names list.
Due to the fact that the provided bit names may have =val suffixes, it excludes the ability to generate proper access functions for the named bits. However, the COMMS library also provides COMMS_BITMASK_BITS_ACCESS() macro, which can be used in addition to COMMS_BITMASK_BITS() one to generate the convenience functions. For example:
is equivalent to defining:
In case the bit names definition is sequential starting with index 0 and going up without and gaps, i.e. no =val suffixes are used, the usage of two separate COMMS_BITMASK_BITS() and COMMS_BITMASK_BITS_ACCESS() macros, can be unified into one COMMS_BITMASK_BITS_SEQ():
WARNING: Some compilers, such as clang or earlier versions of gcc (v4.9 and earlier) may have problems compiling the COMMS_BITMASK_BITS_ACCESS() and COMMS_BITMASK_BITS_SEQ() macros even though they contain valid C++11 code. If the compilation failure happens and the bitmask definition class is NOT a template one (like in the example above), then try to substitute the used macros with COMMS_BITMASK_BITS_ACCESS_NOTEMPLATE() and COMMS_BITMASK_BITS_SEQ_NOTEMPLATE() respectively. For example:
However, when the defined bitmask class is a template, then the inner definition of Base type, which specifies the exact type of the base class, is required. For example:
The same goes for COMMS_BITMASK_BITS_ACCESS() macro.
NOTE, that COMMS library also defines COMMS_MUST_DEFINE_BASE in case the base class definition is needed (going to be used). If the developed application is going to be multi-platform and compiled with various compilers (some of which may warn about unused private type) it is possible to use the defined symbol to add / remove the definition of the Base member type.
Many communication protocols try to pack multiple independent values into a one or several bytes to save traffic on I/O link. For example, to encode baud rate from example in Enum Value Fields section, only 3 bits are needed (values [0 - 6]). The serial port configuration may also require parity information, which may have only "None", "Even", and "Odd" values:
Parity | Serialisation Value |
---|---|
None | 0 |
Odd | 1 |
Even | 2 |
To encode parity value only 2 bits are needed. Together with the baud mentioned earlier, these two values will consume only 5 bits. Let's also use the remaining 3 bits to complete a single byte as some kind of flags.
Value | Number of bits |
---|---|
Baud | 3 |
Parity | 2 |
Flags | 3 |
These value must be accessed and treated as independent values. However, they must be bundled into a single byte when serialisation happens. The COMMS library provides comms::field::Bitfield field for this purpose.
Please pay attention to the following details:
Every member of the bitfield may use all the supported options. The comms::field::Bitfield itself may receive only options listed in its class description.
To get an access to the member fields use value() member function:
It would be convenient to access the member fields by name, rather than by index with std::get. It can be achieved by using COMMS_FIELD_MEMBERS_NAMES() macro inside field definition class.
It is equivalent to having the following enum, types and functions defined:
NOTE, that provided names baud, parity, and flags, have found their way to the following definitions:
As the result, the fields can be accessed using multiple ways: For example using FieldIdx enum
or using accessor functions:
SIDE NOTE: In addition to COMMS_FIELD_MEMBERS_NAMES() macro there is COMMS_FIELD_MEMBERS_ACCESS() one. It is very similar to COMMS_FIELD_MEMBERS_NAMES() but does NOT (re)define the inner Field_* types. It also does not require (except for clang) having base class to be (re)defined as inner Base type.
In fact COMMS_FIELD_MEMBERS_NAMES() is implemented as the wrapper around COMMS_FIELD_MEMBERS_ACCESS().
There are cases when multiple independent fields need to be bundled into a single field and expose the required interface of reading, writing, calculating length, checking field's contents validity, and bringing field's value into a consistent state. It may be required when a message contains sequence (see Array List Fields) of such bundles/structs. The COMMS library provides comms::field::Bundle field for this purpose. It is quite similar to comms::field::Bitfield described earlier. The difference is that every member field doesn't specify any length in bits, just bytes. For example:
The default behaviour of comms::field::Bundle may be extended with options. Please refer to the class documentation for the list of supported options.
Just like with the Bitfield Fields, the names to the member fields can be provided by using COMMS_FIELD_MEMBERS_NAMES() or COMMS_FIELD_MEMBERS_ACCESS() macro.
It will create similar enum and convenience access functions, just like described in previous Bitfield Fields section.
Some bundle fields in some protocols may contain a field, which holds a remaining serialization length of the following member fields in the bundle. The COMMS library has a built-in support for such cases, it requires usage of comms::option::def::RemLengthMemberField option to specify index of such field. For example:
Some communication protocols may define messages that transmit sequence of similar fields and/or raw data buffers. To make it easier to handle, the COMMS library provides comms::field::ArrayList field which provide a required interface to properly handle such sequences of data. It supports a sequence of raw bytes
as well as using sequence of any fields defined in comms::field namespace
By default the read operation on comms::field::ArrayList continues as long as there is data left in input buffer, and write operation serialises all the data stored in internal vector. These default behaviours can be changed using options described below.
Very often variable size sequences of raw bytes or other fields get prefixed with size information. The default behaviour of the comms::field::ArrayList is to read until the end of the buffer. Having sequence prefixed with number of elements to follow, allows earlier termination of the read operation, and allows having other independent fields to be appended after the sequence. The comms::field::ArrayList class supports comms::option::def::SequenceSizeFieldPrefix option that allows to specify type of the size field (usually a variant of comms::field::IntValue) to be serialised before the contents of comms::field::ArrayList being serialised. For example, the serialised raw bytes sequence is prefixed with 2 bytes of size information:
Some protocols prefix the sequence with serialisation length rather than number of elements to follow. In this case the comms::option::def::SequenceSerLengthFieldPrefix option needs to be used instead of comms::option::def::SequenceSizeFieldPrefix.
Also some protocols, for easier exchange of lists between nodes that use different versions of the same protocol, may require prefixing every element of the list with its serialisation length. In this case comms::option::def::SequenceElemSerLengthFieldPrefix option may be used. For example, the list of bundles prefixed with 2 bytes specifying number of elements to follow, and with every element prefixed with its serialisation length using variable length base-128 encoding may look like this:
When every element of the list is of fixed size, i.e. has the same serialisation length, it becomes redundant to prefix every element with its length. Instead, only first element can be prefixed with one, and all others may reuse the same information. To achieve such behaviour comms::option::def::SequenceElemFixedSerLengthFieldPrefix should be used instead. For example, the list of fixed length bundles prefixed with 1 byte specifying number of elements to follow, and with first element prefixed with 1 byte containing its serialisation length may look like this:
There may be cases when size information is detached from the sequence itself, i.e. there are other fields between the size field and the sequence itself. For example, the protocol specifies the following:
Byte Offset | Length | Description |
---|---|---|
0 | 1 | Number of elements in sequence |
1 | 1 | Some flags bitmask |
2 | 2 * N | Sequence of 2 byte integral values |
In this case the option comms::option::def::SequenceSizeFieldPrefix can NOT be used. In fact the size information is not a part of the sequence any more, it must be a separate independent field. When this field is successfully read, its value must be forced upon the sequence somehow before the read operation of the sequence takes place. To help with such forcing, comms::option::def::SequenceSizeForcingEnabled option was introduced. When this option used, the comms::field::ArrayList::forceReadElemCount member function of the field may be used to force number of elements that follow.
In addition to prefixing variable length lists with amount of elements to follow, some protocols may also prefix them with serialisation length of the single element. Such technique is usually used to maintain data exchange compatibility with earlier versions of the protocol, which may be used on the other side of the communication link. The COMMS library allows forcing the serialisation length of a single element when such information becomes available. It is similar to Detached Size Information. The option comms::option::def::SequenceElemLengthForcingEnabled needs to be used when defining the field type, and comms::field::ArrayList::forceReadElemLength() and comms::field::ArrayList::clearReadElemLengthForcing() functions to set/clear the forcing information.
Sometimes there is no information about size of the sequence up front. It may be terminating using some kind of special value. For example, the sequence of raw bytes is terminated by the value of 0. Such termination is achieved by using comms::option::def::SequenceTerminationFieldSuffix option.
In many cases the size of the sequence is defined in the protocol without any prefix or suffix to define the length of the sequence. To define such sequence comms::option::def::SequenceFixedSize option should be used. Below is example of how to define sequence of four unsigned 16 bit integer values.
NOTE, that comms::option::def::SequenceFixedSize option insures existence of the right number of elements "on the wire", but doesn't influence number of elements in the newly created list field:
Also nothing prevents from having too many values in the storage vector, but only specified number of the elements will be serialised:
By default, the internal data is stored using std::vector.
This behaviour can be modified using extra options such as comms::option::app::CustomStorageType, comms::option::app::FixedSizeStorage, comms::option::app::OrigDataView, or comms::option::app::SequenceFixedSizeUseFixedSizeStorage. HOWEVER, these options do not influence the way how list fields are being serialised, they influence the way how list value has been stored. As the result, they should NOT be used in protocol definition. Instead, provide a way to to the actual application to modify the default storage by passing extra options. For example:
All the Common Options or Modifications for the Fields are also applicable to comms::field::ArrayList field.
Many protocols have to transfer strings. They are defined using comms::field::String field.
It is very similar to comms::field::ArrayList it terms of value storage, read/write operations, and supported options. By default the value is stored as std::string.
Just like described in Value Storage section above the same options can be used to modify the storage type of the comms::field::String field, but should NOT be used in protocol definition, but instead there should be an ability to provide extra options.
Prefixing string with single byte of the size information will look like this:
See also Prefixing with Size Information.
Encoding of zero termination strings without size prefix can be defined like this:
See also Terminating Sequence with Suffix.
Another string example is to have zero terminated string, the serialisation of which occupies exactly 32 bytes, i.e. the string may have up to 31 non-zero characters. If string is too short, the serialisation data is padded by zeros until full length of 32 characters is produced.
NOTE, that the example above uses comms::option::def::SequenceTrailingFieldSuffix option, rather than comms::option::def::SequenceTerminationFieldSuffix. The options slightly differ. The "termination" one (comms::option::def::SequenceTerminationFieldSuffix) forces the field to stop reading when termination value is encountered, while "trailing" one (comms::option::def::SequenceTrailingFieldSuffix) doesn't check what it reads, the reading size must be limited by other means (comms::option::def::SequenceFixedSize in the example above). When the read is complete, it just consumes the termination character. Both options, however, force the termination character to be appended at the end during write operation.
Also note, that size limit is specified (using comms::option::def::SequenceFixedSize) to be 31. One more byte is added by the "trailing" suffix to complete to 32 bytes.
Just like with comms::field::ArrayList, it is possible to use static storage for fixed size strings:
or
HOWEVER, the app options should be used in protocol definition, only in application customization.
Floating point value fields (comms::field::FloatValue) are very similar to Integral Value Fields, but use float or double as its internal storage type. They abstract the IEEE 754 floating point values, which are serialised "as is" with either big or little endian encoding. The floating point value fields also support Value Units conversions.
Some protocols may define optional fields, which may exist or be missing based on information recorded in other fields. For example there is a "flags" bitmask field which specifies whether the following field exists or missing. The optional field may also be tentative, i.e. if there is enough data in the input buffer it exists, and missing otherwise. The COMMS library provides comms::field::Optional which is a mere wrapper around other fields and provides an ability to set the optional state of the field. Let's do the example of the int32 field existence based on bit 0 in processing bitmask:
Note, that default mode for the optional field is "tentative", which is updated after read operation:
It is easy to change the default mode of the comms::field::Optional field by providing comms::option::def::DefaultOptionalMode option with selected default mode or comms::option::def::MissingByDefault / comms::option::def::ExistsByDefault aliases.
Some protocols may include version information either in transport framing or in one of the messages. Such info may specify whether a specific field exists or not. Such fields need to be wrapped in comms::field::Optional field, which receives comms::option::def::ExistsBetweenVersions option to specify the numeric versions of the protocol between which the field exists.
If the field has been introduced in one of the version, but hasn't been removed yet, it is possible to use comms::option::def::ExistsSinceVersion alias to comms::option::def::ExistsBetweenVersions. Or the opposite, if the field has been introduced in the first version, but deprecated and removed in the later one, use comms::option::def::ExistsUntilVersion alias.
Usage of such version control option will automatically mark the optional field as existing or missing based on the provided version info in the setVersion() member function.
Some protocols may require usage of heterogeneous fields or lists of heterogeneous fields, i.e. the ones that can be of multiple types. Good example would be a list of properties, where every property is a key/value pair or a TLV (type/length/value) triplet. The key (or type) is usually a numeric ID of the property, while value can be any field of any length. As an example for the key/value pairs let's define three value types:
The COMMS library provides comms::field::Variant field to allow such heterogeneous fields. Let's implement the described example.
The common key type is easy to represent as enum.
And the relevant key fields as a variant of comms::field::EnumValue with only single acceptable value.
Please pay attention to usage of comms::option::def::FailOnInvalid option and having only single valid value in order to force failure of the read operation when the key doesn't match.
Then the KeyX and its corresponding ValueX need to be bundled together as a single PropertyX field.
Now we need a single field abstraction, which can be any of the specified above forms. The comms::field::Variant field class provides such an ability. As its second parameter it receives a tuple of supported types.
Similar to COMMS_FIELD_MEMBERS_NAMES() macro for Bundle Fields, the COMMS_VARIANT_MEMBERS_NAMES() macro generates the following convenience member enum and functions
NOTE, that the provided names have propagated into definition of FieldIdx enum, all initField_* and accessField_* functions, as well as inner Field_* types definitions.
Now it is easy to put such Variant field type into the list:
In this scenario, read operation on the list will invoke read operation of every MyVariant field, which in turn will try to perform read operation on Property1, Property2, and Property3 in the order of their definition inside the provided tuple. The read operation of the comms::field::Variant field type will stop when read operation of any of the contained types reports comms::ErrorStatus::Success as its status. That's why every key field needs to fail its read operation on invalid value.
The extension of the key/value pairs example above to TLV triplets is quite easy.
NOTE usage of comms::option::def::RemLengthMemberField option described earlier in Bundle Fields section. It informs the COMMS library about presence of the length value in the bundle, which will result in proper read length limitation for the value(s) to follow. Also in case the read of the following value(s) does not consume all the reported length, COMMS library will advance the read iterator to consume the remaining bytes resulting in read of the next field to be at correct location.
The length field in example above contains remaining length not including the field itself. Some protocols may include the length of the length field itself. If this is the case, just use comms::option::def::NumValueSerOffset option (described in Serialisation Offset) to add extra numeric offset to be added when the field's value is written.
RECOMMENDATION: In case of TLV property triplets it is recommended to create a dummy field with non-failing read to allow usage of unknown properties which can be introduced in later versions of the protocol.
And put such property at the end of the supported types tuple for the variant field definition.
The default constructed comms::field::Variant object from the examples above has an "invalid" state, i.e. hasn't been initialized and doesn't contain any valid field. It can be changed by providing comms::option::def::DefaultVariantIndex option.
When instantiating such MyVariant object, there is no need to perform initialization (construction) of the contained object.
SIDE NOTE: In addition to COMMS_VARIANT_MEMBERS_NAMES() macro there is COMMS_VARIANT_MEMBERS_ACCESS() one. It is very similar to COMMS_VARIANT_MEMBERS_NAMES() but does NOT (re)define the inner Field_* types. It also does not require (except for clang) having base class to be (re)defined as inner Base type.
In fact COMMS_VARIANT_MEMBERS_NAMES() is implemented as the wrapper around COMMS_VARIANT_MEMBERS_ACCESS().
There are options that suitable only to numeric fields, such as comms::field::IntValue, comms::field::EnumValue, comms::field::BitmaskValue.
There are options that suitable only for collection fields, such as comms::field::ArrayList, and comms::field::String.
There are also common options that can be used with all the fields that support options.
There may be a case when default construction of the field object should assign some custom value to the field, which differ to the usual defaults, such as assigning 0 to numeric fields or empty string to a string field.
One of the possible ways is to extend the defined field class and set the required value in the constructor.
Another way is to use comms::option::def::DefaultValueInitialiser option. It receives a template parameter, which has to be a type of initialisation class. It must provide operator() which is responsible to assign a custom value to the field. It is going to be invoked from the default constructor of the field.
For example:
NOTE that the used operator() specifies the field type as a template parameter. This is required because the passed reference is to one of the defined field's base classes, which is implementation dependent. Just use the provided value() member function to access the value.
The COMMS library also provides a simpler alias for comms::option::def::DefaultValueInitialiser to set default value for numeric fields. It is comms::option::def::DefaultNumValue.
Every field provides read() member function to perform read of the field's value. Sometimes the default "read" functionality may be incorrect or incomplete. For example, let's define a "bundle" field with two members. The first one is a "bitmask", while the second one is optional 2 byte "int" value. The second member exists only if least significant bit of the "bitmask" is not 0. In this case, the provided read() member function won't analyse the value of the read "bitmask" and won't modify "existing"/"missing" mode value of the optional field.
One way to implement custom read functionality is to extend the field definition and override the read() member function:
Please NOTE the following:
Every field provides valid() member function to validate the internal value. By default, every internal value of the field is considered to be valid, i.e. the valid() function will always return true.
One of the ways to provide custom validation logic is to extend the field definition and implement valid() member function:
Quite often the valid values of the numeric fields can be expressed in limited number of ranges: [minValid - maxValid]. The COMMS library provides comms::option::def::ValidNumValueRange option (and comms::option::def::ValidNumValue alias), which can be used multiple times. The field's value is considered to be valid if at least one of the provided ranges contains it. The range validation option can be used only with numeric value fields, such as comms::field::IntValue, or comms::field::EnumValue. For example:
Another example could be a single character numeric field with valid values range of [0x20 - 0x7e], as well as value 0. Such field can be defined as:
WARNING: Some older compilers (gcc-4.7) fail to compile valid C++11 code that allows usage of multiple comms::option::def::ValidNumValueRange options. If this is the case, please don't pass more than one comms::option::def::ValidNumValueRange option.
There is a also a convenience reserved bits checked option for use with bitmasks (comms::field::BitmaskValue). Many bitmask fields may have one or several reserved bits with predefined values they must contain. The option is comms::option::def::BitmaskReservedBits. It receives two template parameters: one for the mask indicating the reserved bits and another for the expected values of these bits.
For example, below is a definition of the 1 byte bitmask field that has two reserved bits, most and least significant. Both of them must be 0.
Every field provides refresh() member function used to bring the field's value into a consistent state. By default this function does nothing and returns false, meaning the field has NOT been updated. For complex fields, such as comms::field::Bitfield or comms::field::Bundle, the default behaviour is to invoke refresh() member function of each member field and return true if any of the calls returned true.
One way to change such default behaviour is to extend the field definition and implement refresh() member function. Let's take the same example as in Custom Read Functionality section. There is a "bundle" field with two members. The first one is a "bitmask", while the second one is optional 2 byte "int" value. The second member exists only if least significant bit of the "bitmask" is not 0. There is a chance of having inconsistent state when the least significant bit in the "bitmask" is set, but the optional "int" field is marked to be "missing". There is a need to provide the custom "refresh" logic that brings the field's contents into a consistent state.
NOTE the usage of comms::option::def::HasCustomRefresh option. It notifies other classes about existence of custom refresh functionality (instead of default one). Other classes may contain some inner logic to perform various optimisations if there is no custom refresh. Failure to specify this option may result in incorrect behaviour.
On some very rare occasions there may be a need to write custom write functionality. It can be achieved by writing custom write() member function
NOTE usage of comms::option::def::HasCustomWrite option. It is required to let prevent field holding comms::MessageBase class from attempting to optimize write operation due to possible incorrect behavior.
Some protocols may include version information either in the transport framing of every message or in one of the messages used to establish connection. Such info may influence whether some fields exist, as well as modify other aspects of the fields, such as validity ranges.
Every field provides setVersion() member function used to notify it about the version change and isVersionDependent() one to inquire at compile time whether the field contents may change after such notification. For most fields setVersion() function does nothing and returns false, meaning the field has NOT been updated (similar to Custom Refresh Functionality). For complex fields, such as comms::field::Bitfield or comms::field::Bundle, the default behaviour is to invoke setVersion() member function of each member field and return true if any of the calls returned true. For comms::field::ArrayList fields, the version information may be stored inside (only if the element's isVersionDependent() member function returns true) and used to notify every new field that is being read during read operation.
Usually, the default version handling provided by the COMMS library is good enough. However, there may be cases when custom operation needs to be performed during version update. For example, there is integral value field with valid values range [0, 5]. There is a need to report field as being invalid for any other numbers for all version up to 5. However, since version 6 the range is extended to [0, 10]. It can be defined as following:
NOTE, the usage of comms::option::def::HasCustomVersionUpdate option. It marks the defined field as "version dependent" and as the result its isVersionDependent() member function will return true.
Also NOTE, that by default the VersionType inner type is defined to be unsigned. If there is a need to change that, the comms::option::def::VersionType needs to be passed to the definition of the common base class of all the fields:
Sometimes the protocol specifications may impose a strict rules on disallowing invalid values, such as the message must be dropped when some field has an invalid value. It is easy to implement by forcing read() operation on such field to fail when reading an invalid value is recognised. The COMMS library provides comms::option::def::FailOnInvalid option to help with such task. For example:
The COMMS library also provides comms::option::def::IgnoreInvalid option. It DOESN'T report failure on read operation when the invalid value is discovered (like comms::option::def::FailOnInvalid does). Instead the field's internal value remains unchanged, although the read iterator is advanced as if the value is read. For example:
Some protocols may define some constants which are NOT being sent over I/O link. Sometimes it may be useful to still treat these values as message fields. Usage of comms::option::def::EmptySerialization (or comms::option::def::EmptySerialisation for those who prefer British spelling) can be used to achieve such effect.
As was mentioned earlier there are options that define how fields are (de)serialised. These options are expected to be used in protocol fields definition. However, there are options that are application specific. They may change the data structures being used for storage, or modify default value and/or valid values. In order to allow such application specific modifications, the defined fields should allow further extension, for example with extra variadic template arguments.
Please note, that current implementation gives preference to the options defined earlier. That's why TExtraOpts... should be listed before any other options. It will allow setting other default value needed by the application, and/or override the valid values ranges (using comms::option::def::ValidRangesClear). For example:
There may be a case when communication protocol demands implementation of some intricate field's logic that is not covered by the COMMS library. It is possible to provide custom implementation of the custom field and use it with other components provided by the library as long as it defines the following minimal interface:
The comms::Field class provides readData() and writeData() protected member functions that serialise data using endian provided as an option to the class. It makes sense to inherit from comms::Field with right option and reuse these functions inside:
Also to be consistent with the existing implementation of the fields in the COMMS library it is recommended to provide an accessor functions value() for internal data storage:
With time the COMMS library may grow by adding support for some other built-in fields as well as supporting extra options to the existing fields described in this tutorial. If such new field and/or option is not described in this tutorial, it should be easy enough for the developer to master. Please refer to the documentation of the field and/or option itself.