jefffhaynes / BinarySerializer

A declarative serialization framework for controlling formatting of data at the byte and bit level using field bindings, converters, and code.
MIT License
292 stars 62 forks source link

Advice on approach #76

Closed smremde closed 6 years ago

smremde commented 6 years ago

Hi,

I'm trying to use this package to interface with Sick Lidars. The protocol is a weird mashup of the an ASCII and binary protocol and wanted some advice.

The protocol specification is here:

https://www.sick.com/media/docs/7/27/927/Technical_information_Telegram_Listing_Ranging_sensors_LMS1xx_LMS5xx_TiM5xx_NAV310_LD_OEM15xx_LD_LRS36xx_en_IM0045927.PDF

The issue is that the packet type is a two strings (0x20 terminated!) one fixed length and one variable length

image

I initially started with

public class Packet
{
    // header 02020202 ignored for sycing
    [FieldOrder(1)]
    public uint Length { get; set; }

    [FieldOrder(2)]
    [FieldLength("Length")]
    [FieldChecksum("Checksum", Mode = ChecksumMode.Xor)]
    public PacketContent Content { get; set; }

    [FieldOrder(3)]
    public byte Checksum { get; set; }
}

public class PacketContent
{
    [FieldOrder(0)]
    [FieldLength(4)]
    public MessageType MessageType { get; set; }

    [FieldOrder(1)]
    [SerializeUntil((byte)20)]
    public string MessageSubType { get; set; }

    [FieldOrder(2)]
    [Subtype("MessageType", "SetAccessMode", typeof(sMN_SetAccessMode))]
    public PacketPayload Payload { get; set; }
}

public enum MessageType
{
    [SerializeAsEnum("sRN ")]
    sRN,
    [SerializeAsEnum("sWN ")]
    sWN,
    [SerializeAsEnum("sMN ")]
    sMN,
    [SerializeAsEnum("sEN ")]
    sEN,
    [SerializeAsEnum("sRA ")]
    sRA,
    [SerializeAsEnum("sWA ")]
    sWA,
    [SerializeAsEnum("sEA ")]
    sEA,
    [SerializeAsEnum("sSN ")]
    sSN,
}

,

But actually, the subtype depends on both fields here.

Also, using when serialising an field with the attribute [SerializeUntil((byte)20)] the string is null terminated.

Any advice?

jefffhaynes commented 6 years ago

Yikes. I have considered adding something-other-than-null terminated string support. Maybe that would be appropriate. But let me play around with it. These mixed ascii/bin protocols are deadly and sometimes there’s just no good way to cleanly support them.

— Planning for failure is even dumber than regular planning


From: Stephen Remde notifications@github.com Sent: Thursday, November 2, 2017 7:00:59 AM To: jefffhaynes/BinarySerializer Cc: Subscribed Subject: [jefffhaynes/BinarySerializer] Advice on approach (#76)

Hi,

I'm trying to use this package to interface with Sick Lidars. The protocol is a weird mashup of the an ASCII and binary protocol and wanted some advice.

The protocol specification is here:

https://www.sick.com/media/docs/7/27/927/Technical_information_Telegram_Listing_Ranging_sensors_LMS1xx_LMS5xx_TiM5xx_NAV310_LD_OEM15xx_LD_LRS36xx_en_IM0045927.PDF

The issue is that the packet type is a two strings (0x20 terminated!) one fixed length and one variable length

[image]https://user-images.githubusercontent.com/986075/32322218-a88f2322-bfbb-11e7-8cba-c395c55cf6f8.png

I initially started with

public class Packet { // header 02020202 ignored for sycing [FieldOrder(1)] public uint Length { get; set; }

[FieldOrder(2)]
[FieldLength("Length")]
[FieldChecksum("Checksum", Mode = ChecksumMode.Xor)]
public PacketContent Content { get; set; }

[FieldOrder(3)]
public byte Checksum { get; set; }

}

public class PacketContent { [FieldOrder(0)] [FieldLength(4)] public MessageType MessageType { get; set; }

[FieldOrder(1)]
[SerializeUntil((byte)20)]
public string MessageSubType { get; set; }

[FieldOrder(2)]
[Subtype("MessageType", "SetAccessMode", typeof(sMN_SetAccessMode))]
public PacketPayload Payload { get; set; }

}

public enum MessageType { [SerializeAsEnum("sRN ")] sRN, [SerializeAsEnum("sWN ")] sWN, [SerializeAsEnum("sMN ")] sMN, [SerializeAsEnum("sEN ")] sEN, [SerializeAsEnum("sRA ")] sRA, [SerializeAsEnum("sWA ")] sWA, [SerializeAsEnum("sEA ")] sEA, [SerializeAsEnum("sSN ")] sSN, }

,

But actually, the subtype depends on both fields here.

Also, using when serialising an field with the attribute [SerializeUntil((byte)20)] the string is null terminated.

Any advice?

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHubhttps://github.com/jefffhaynes/BinarySerializer/issues/76, or mute the threadhttps://github.com/notifications/unsubscribe-auth/AJSKR_DzAYFxVMUJ7JXGOxSSNtct1kDuks5syaDqgaJpZM4QPgbD.

smremde commented 6 years ago

My approach has been to Implement an IBinarySerializable

    public class PacketType : IBinarySerializable
    {
        [Ignore]
        public string Type { get; set; }

        public PacketType()
        {
        }

        public PacketType(string type)
        {
            Type = type;
        }

        public void Deserialize(Stream stream, Endianness endianness, BinarySerializationContext serializationContext)
        {
            StringBuilder sb = new StringBuilder();

            var spacecount = 0;
            while (spacecount <2)
            {
                var b = stream.ReadByte();
                if (b == -1)
                    throw new InvalidOperationException("Reached end of stream before end of PacketType.");
                if (b == 0x20)
                    spacecount++;
                sb.Append((char)b);
            }

            Type = sb.ToString();
        }

        public void Serialize(Stream stream, Endianness endianness, BinarySerializationContext serializationContext)
        {
            var bytes = new ASCIIEncoding().GetBytes(Type);
            stream.Write(bytes, 0, bytes.Length);
        }

        public override bool Equals(object obj)
        {
            var other = obj as PacketType;
            return (other != null && other.Type == Type) || obj.ToString() == Type;
        }

        public override int GetHashCode()
        {
            return Type.GetHashCode();
        }
    }

I have overridden the equals to implement string comparison so the subtype will work on the parent class:

    public class PacketContent
    {
        [FieldOrder(0)]
        public PacketType  PacketType { get; set; }

        [FieldOrder(1)]
        [Subtype("PacketType", "sMN SetAccessMode ", typeof(sMN_SetAccessMode))]
        [Subtype("PacketType", "sAN SetAccessMode ", typeof(sAN_SetAccessMode))]
        [Subtype("PacketType", "sMN mLMPsetscancfg ", typeof(sMN_mLMPsetscancfg))]
        public PacketPayload Payload { get; set; }
    }

This unfortunately means I need to populate PacketType manually

            ser.Serialize(ms, new Packet
            {
                Content = new PacketContent
                {
                    PacketType = new PacketType("sMN SetAccessMode "),
                    Payload = new sMN_SetAccessMode
                    {
                        UserLevel =  UserLevel.AuthorizedClient,
                        Password= 0xF4724744
                    }
                }
            });

Do you see a work around for this?

jefffhaynes commented 6 years ago

I'm going to add support for non-null string terminators and see where that gets us

jefffhaynes commented 6 years ago

Requires 7.6:


public enum CommandClass
{
    [SerializeAsEnum("sRN")]
    Srn,

    [SerializeAsEnum("sWN")]
    Swn,

    [SerializeAsEnum("sMN")]
    Smn,

    [SerializeAsEnum("sEN")]
    Sen,

    [SerializeAsEnum("sRA")]
    Sra,

    [SerializeAsEnum("sWA")]
    Swa,

    [SerializeAsEnum("sEA")]
    Sea,

    [SerializeAsEnum("sSN")]
    Ssn
}

public enum CommandType
{
    SetAccessMode,

    [SerializeAsEnum("mLMPsetscancfg")]
    SetScanConfig,

    [SerializeAsEnum("LMDscandatacfg")]
    ScanDataConfig

    // etc
}

public class Packet
{
    [FieldOrder(0)]
    public uint Length { get; set; }

    [FieldOrder(1)]
    [FieldLength("Length")]
    [FieldChecksum("Checksum", Mode = ChecksumMode.Xor)]
    public PacketContent Content { get; set; }

    [FieldOrder(2)]
    public byte Checksum { get; set; }
}

public class PacketContent
{
    [FieldOrder(0)]
    [SerializeAs(SerializedType.TerminatedString, StringTerminator = 0x20)]
    public CommandClass CommandClass { get; set; }

    [FieldOrder(1)]
    [Subtype("CommandClass", CommandClass.Smn, typeof(SmnCommandContainer))]
    public CommandContainer Payload { get; set; }
}

public abstract class CommandContainer
{
    [SerializeAs(SerializedType.TerminatedString, StringTerminator = 0x20)]
    public CommandType CommandType { get; set; }
}

public class SmnCommandContainer : CommandContainer
{
    [Subtype("CommandType", CommandType.SetAccessMode, typeof(SetAccessModeCommand))]
    public Command Command { get; set; }
}

public abstract class Command
{
}

public enum UserLevel : byte
{
    AuthorizedClient = 0x3
}

public class SetAccessModeCommand : Command
{
    [FieldOrder(0)]
    public UserLevel UserLevel { get; set; } = UserLevel.AuthorizedClient;

    [FieldOrder(1)]
    [FieldLength(4)]
    public byte[] Password { get; set; } = { 0xF4, 0x72, 0x47, 0x44 };
 }

The key here is using a different class for the command "class" vs the specific command. That plus the new terminator thing makes it pretty straightforward.

Added this as a test.