convertersystems / opc-ua-client

Visualize and control your enterprise using OPC Unified Architecture (OPC UA) and Visual Studio.
MIT License
395 stars 112 forks source link

decode sub-structure failes while complete structure can get decoded #174

Closed djonasdev closed 3 years ago

djonasdev commented 3 years ago

I have a S7-1500 plc as an opc server with the following declaration of the Structure:

Image Pasted at 2020-11-16 10-09

In my code I use the following Structure to successfully encode the complete RecipeInfo Node:

[BinaryEncodingId("nsu=http://www.siemens.com/simatic-s7-opcua;s=TE_\"HTOPC_RecipeInfo\"")]
internal class UdtRecipeInfoStructure : UdtStructure
{
    public UdtRecipeInfoGeneralStructure General { get; set; }
    public UdtRecipeInfoTrayStructure Tray { get; set; }
    public UdtRecipeInfoCarrierStructure Carrier { get; set; }
    public UdtRecipeInfoTesterStructure Tester { get; set; }
    public UdtRecipeInfoDispenserStructure Dispenser { get; set; }

    protected override void DecodeSafely(IDecoder decoder)
    {
        General = decoder.ReadEncodable<UdtRecipeInfoGeneralStructure>(nameof(General));
        Tray = decoder.ReadEncodable<UdtRecipeInfoTrayStructure>(nameof(Tray));
        Carrier = decoder.ReadEncodable<UdtRecipeInfoCarrierStructure>(nameof(Carrier));
        Tester = decoder.ReadEncodable<UdtRecipeInfoTesterStructure>(nameof(Tester));
        Dispenser = decoder.ReadEncodable<UdtRecipeInfoDispenserStructure>(nameof(Dispenser));
    }

    protected override void EncodeSafely(IEncoder encoder)
    {
        encoder.WriteEncodable(nameof(General), General);
        encoder.WriteEncodable(nameof(Tray), Tray);
        encoder.WriteEncodable(nameof(Carrier), Carrier);
        encoder.WriteEncodable(nameof(Tester), Tester);
        encoder.WriteEncodable(nameof(Dispenser), Dispenser);
    }

    [BinaryEncodingId("nsu=http://www.siemens.com/simatic-s7-opcua;s=TE_\"\"HTOPC_RecipeInfo\".\"General\"\"")]
    internal class UdtRecipeInfoGeneralStructure : UdtStructure
    {
        public byte RobTypeNumber { get; set; }
        public ushort RobPcbWeightBefore { get; set; }
        public ushort RobPcbWeigthAfter { get; set; }
        public bool RobFlipPcb { get; set; }

        protected override void DecodeSafely(IDecoder decoder)
        {
            RobTypeNumber = decoder.ReadByte(nameof(RobTypeNumber));
            RobPcbWeightBefore = decoder.ReadUInt16(nameof(RobPcbWeightBefore));
            RobPcbWeigthAfter = decoder.ReadUInt16(nameof(RobPcbWeigthAfter));
            RobFlipPcb = decoder.ReadBoolean(nameof(RobFlipPcb));
        }

        protected override void EncodeSafely(IEncoder encoder)
        {
            encoder.WriteByte(nameof(RobTypeNumber), RobTypeNumber);
            encoder.WriteUInt16(nameof(RobPcbWeightBefore), RobPcbWeightBefore);
            encoder.WriteUInt16(nameof(RobPcbWeigthAfter), RobPcbWeigthAfter);
            encoder.WriteBoolean(nameof(RobFlipPcb), RobFlipPcb);
        }
    }

    [BinaryEncodingId("nsu=http://www.siemens.com/simatic-s7-opcua;s=TE_\"\"HTOPC_RecipeInfo\".\"Tray\"\"")]
    internal class UdtRecipeInfoTrayStructure : UdtStructure
    {
        public string SapNumber { get; set; }
        public byte Format { get; set; }
        public ushort EmptyWeight { get; set; }
        public float StackHeight { get; set; }
        public byte MaxStackCount { get; set; }
        public byte CavityCount { get; set; }
        public UdtRecipeInfoTrayPosStructure[] Pos { get; set; }

        protected override void DecodeSafely(IDecoder decoder)
        {
            SapNumber = decoder.ReadString(nameof(SapNumber));
            Format = decoder.ReadByte(nameof(Format));
            EmptyWeight = decoder.ReadUInt16(nameof(EmptyWeight));
            StackHeight = decoder.ReadFloat(nameof(StackHeight));
            MaxStackCount = decoder.ReadByte(nameof(MaxStackCount));
            CavityCount = decoder.ReadByte(nameof(CavityCount));
            Pos = decoder.ReadEncodableArray<UdtRecipeInfoTrayPosStructure>(nameof(Pos));
        }

        protected override void EncodeSafely(IEncoder encoder)
        {
            encoder.WriteString(nameof(SapNumber), SapNumber);
            encoder.WriteByte(nameof(Format), Format);
            encoder.WriteUInt16(nameof(EmptyWeight), EmptyWeight);
            encoder.WriteFloat(nameof(StackHeight), StackHeight);
            encoder.WriteByte(nameof(MaxStackCount), MaxStackCount);
            encoder.WriteByte(nameof(CavityCount), CavityCount);
            encoder.WriteEncodableArray(nameof(Pos), Pos);
        }

        [BinaryEncodingId("nsu=http://www.siemens.com/simatic-s7-opcua;s=TE_\"\"HTOPC_RecipeInfo\".\"Tray\".\"Pos\"\"")]
        internal class UdtRecipeInfoTrayPosStructure : UdtStructure
        {
            public float X { get; set; }
            public float Y { get; set; }
            public float Z { get; set; }
            public float Rot { get; set; }

            protected override void DecodeSafely(IDecoder decoder)
            {
                X = decoder.ReadFloat(nameof(X));
                Y = decoder.ReadFloat(nameof(Y));
                Z = decoder.ReadFloat(nameof(Z));
                Rot = decoder.ReadFloat(nameof(Rot));
            }

            protected override void EncodeSafely(IEncoder encoder)
            {
                encoder.WriteFloat(nameof(X), X);
                encoder.WriteFloat(nameof(Y), Y);
                encoder.WriteFloat(nameof(Z), Z);
                encoder.WriteFloat(nameof(Rot), Rot);
            }
        }
    }

    [BinaryEncodingId("nsu=http://www.siemens.com/simatic-s7-opcua;s=TE_\"\"HTOPC_RecipeInfo\".\"Carrier\"\"")]
    internal class UdtRecipeInfoCarrierStructure : UdtStructure
    {
        public byte CavityCount { get; set; }
        public bool DummysNeeded { get; set; }
        public UdtRecipeInfoTrayStructure.UdtRecipeInfoTrayPosStructure[] Pos { get; set; }

        protected override void DecodeSafely(IDecoder decoder)
        {
            CavityCount = decoder.ReadByte(nameof(CavityCount));
            DummysNeeded = decoder.ReadBoolean(nameof(DummysNeeded));
            Pos = decoder.ReadEncodableArray<UdtRecipeInfoTrayStructure.UdtRecipeInfoTrayPosStructure>(nameof(Pos));
        }

        protected override void EncodeSafely(IEncoder encoder)
        {
            encoder.WriteByte(nameof(CavityCount), CavityCount);
            encoder.WriteBoolean(nameof(DummysNeeded), DummysNeeded);
            encoder.WriteEncodableArray(nameof(Pos), Pos);
        }
    }

    [BinaryEncodingId("nsu=http://www.siemens.com/simatic-s7-opcua;s=TE_\"\"HTOPC_RecipeInfo\".\"Tester\"\"")]
    internal class UdtRecipeInfoTesterStructure : UdtStructure
    {
        public byte Target { get; set; }
        public bool Flip { get; set; }

        protected override void DecodeSafely(IDecoder decoder)
        {
            Target = decoder.ReadByte(nameof(Target));
            Flip = decoder.ReadBoolean(nameof(Flip));
        }

        protected override void EncodeSafely(IEncoder encoder)
        {
            encoder.WriteByte(nameof(Target), Target);
            encoder.WriteBoolean(nameof(Flip), Flip);
        }
    }

    [BinaryEncodingId("nsu=http://www.siemens.com/simatic-s7-opcua;s=TE_\"\"HTOPC_RecipeInfo\".\"Dispenser\"\"")]
    internal class UdtRecipeInfoDispenserStructure : UdtStructure
    {
        public bool Process { get; set; }

        protected override void DecodeSafely(IDecoder decoder)
        {
            Process = decoder.ReadBoolean(nameof(Process));
        }

        protected override void EncodeSafely(IEncoder encoder)
        {
            encoder.WriteBoolean(nameof(Process), Process);
        }
    }
}

public abstract class UdtStructure : Structure
{
    protected readonly Logger Logger;

    public UdtStructure()
    {
        Logger = LogManager.GetLogger(GetType().FullName);
    }

    public override void Decode(IDecoder decoder)
    {
        try
        {
            DecodeSafely(decoder);
        }
        catch (Exception e)
        {
            Logger.Error(e, "error when decoding bytestream");
        }
    }

    public override void Encode(IEncoder encoder)
    {
        try
        {
            EncodeSafely(encoder);
        }
        catch (Exception e)
        {
            Logger.Error(e, "error when encoding bytestream");
        }
    }

    protected abstract void DecodeSafely(IDecoder decoder);
    protected abstract void EncodeSafely(IEncoder encoder);
}
// successfully read
var rCarrierInfo = await ChildNodes[RECIPE_INFO_NODE].ReadValue();
// successfully decode
var carrierInfo = rCarrierInfo.Result.GetValueOrDefault<UdtRecipeInfoStructure>();

// change some values
carrierInfo.General.RobFlipPcb = true;
carrierInfo.General.RobPcbWeightBefore = 91;
carrierInfo.General.RobPcbWeigthAfter = 92;
carrierInfo.General.RobTypeNumber = 93;

// successfully able to write back the changes to the node
var rWriteCarrierInfo = await ChildNodes[RECIPE_INFO_NODE].WriteValue(new DataValue(carrierInfo));

// successfully read
var rGeneral = await ChildNodes[RECIPE_INFO_NODE].ChildNodes[GENERAL_NODE].ReadValue();

// decode is not called and result is null
var general = rGeneral.Result.GetValueOrDefault<UdtRecipeInfoStructure.UdtRecipeInfoGeneralStructure>();

grafik

grafik

Have I missed something why I cannot convert the General node with GetValueOrDefault<>? On the other hand, I can easily convert the entire RecipeInfo Node and write it back.

awcullen commented 3 years ago

When you see that the Result value has a BodyType of ByteString, then the decoder did not find the BinaryEncodingId in it's registry.

Looking at the attribute for the type, I think I see some extra quotes:

[BinaryEncodingId("nsu=http://www.siemens.com/simatic-s7-opcua;s=TE_\"\"HTOPC_RecipeInfo\".\"General\"\"")]

Try:

[BinaryEncodingId("nsu=http://www.siemens.com/simatic-s7-opcua;s=TE_\"HTOPC_RecipeInfo\".\"General\"")]

(Sorry for the delay, I'm starting a new job :) )

awcullen commented 3 years ago

I'd like to see if we could auto-generate your custom data types using @quinmars project https://github.com/quinmars/UaTypeGenerator

You could share with us the data types of your Siemens S7 PLC program by clicking "Export OPC UA XML file.." image

quinmars commented 3 years ago

Oh, interesting, I fear the UaTypeGenerator does not support quotes atm. I have to check that.

quinmars commented 3 years ago

I have extend UaTypeGenerator to support some of the Siemens oddities, like quotes and nested types. I haven't tested yet if the type library of UaClient will actually find nested types like:

    public static class LL1_HMI_Btn
    {
        /// <summary>
        /// Class for the LL1_HMI_Btn._Gate data type.
        /// </summary>
        [Workstation.ServiceModel.Ua.DataTypeId("nsu=http://www.siemens.com/simatic-s7-opcua;s=DT_\"LL1_HMI_Btn\".\"Gate\"")]
        public class _Gate : SimaticStructures
        {
            /// <summary>
            /// Class for the LL1_HMI_Btn._Gate._Q6_2 data type.
            /// </summary>
            [Workstation.ServiceModel.Ua.DataTypeId("nsu=http://www.siemens.com/simatic-s7-opcua;s=DT_\"LL1_HMI_Btn\".\"Gate\".\"Q6_2\"")]
            public class _Q6_2 : SimaticStructures
            {
                /// <summary>
                /// The Open property.
                /// </summary>
                public DataTyp_HMI_Button Open { get; set; }
quinmars commented 3 years ago

I just realized that in my example the BinaryEncodingId is missing, but is also missing in the TIA export file.

awcullen commented 3 years ago

Darn.. We're very close. I'll see how to report a issue with Siemens.

awcullen commented 3 years ago

Should I consider having the UA Client look up the DefaultEncodingId of each data type in the TypeLibrary upon connecting?

quinmars commented 3 years ago

I'd prefer to have the encoding IDs at generation time. I imagine that the binary encoding ID could be easily guessed from the data type ID. Looking at the encoding ID's of @dojo90, it seems I just have to replace the DT_ by a TE_ to get the binary encoding ids. @awcullen can you verify my assumption? I do not have access to a Siemens PLC at the moment. I would than add a switch to UaTypeGenerator that generates the binary encoding ids for Siemens PLCs.

awcullen commented 3 years ago

Yes, Siemens replaced the DT by a TE to get the binary encoding ids.

quinmars commented 3 years ago

Thanks! Than I will added that to the type generator.

quinmars commented 3 years ago

Done.

    public static class LL1_HMI_Btn
    {
        /// <summary>
        /// Class for the LL1_HMI_Btn._Gate data type.
        /// </summary>
        [Workstation.ServiceModel.Ua.BinaryEncodingId("nsu=http://www.siemens.com/simatic-s7-opcua;s=TE_\"LL1_HMI_Btn\".\"Gate\"")]
        [Workstation.ServiceModel.Ua.DataTypeId("nsu=http://www.siemens.com/simatic-s7-opcua;s=DT_\"LL1_HMI_Btn\".\"Gate\"")]
        public class _Gate : SimaticStructures
        {
            /// <summary>
            /// Class for the LL1_HMI_Btn._Gate._Q6_2 data type.
            /// </summary>
            [Workstation.ServiceModel.Ua.BinaryEncodingId("nsu=http://www.siemens.com/simatic-s7-opcua;s=TE_\"LL1_HMI_Btn\".\"Gate\".\"Q6_2\"")]
            [Workstation.ServiceModel.Ua.DataTypeId("nsu=http://www.siemens.com/simatic-s7-opcua;s=DT_\"LL1_HMI_Btn\".\"Gate\".\"Q6_2\"")]
            public class _Q6_2 : SimaticStructures
            {
                /// <summary>
                /// The Open property.
                /// </summary>
                public DataTyp_HMI_Button Open { get; set; }

@dojo90 you might give it now a try.

djonasdev commented 3 years ago

Thanks for your efforts šŸ‘

When you see that the Result value has a BodyType of ByteString, then the decoder did not find the BinaryEncodingId in it's registry.

Looking at the attribute for the type, I think I see some extra quotes:

[BinaryEncodingId("nsu=http://www.siemens.com/simatic-s7-opcua;s=TE_\"\"HTOPC_RecipeInfo\".\"General\"\"")]

Try:

[BinaryEncodingId("nsu=http://www.siemens.com/simatic-s7-opcua;s=TE_\"HTOPC_RecipeInfo\".\"General\"")]

(Sorry for the delay, I'm starting a new job :) )

If I declared the UDTs incorrectly, why can I deserialize the entire object while I can't get the child object deserialized?

I'd like to see if we could auto-generate your custom data types using @quinmars project https://github.com/quinmars/UaTypeGenerator

You could share with us the data types of your Siemens S7 PLC program by clicking "Export OPC UA XML file.." image

Unfortunately I could not read the XML. See https://github.com/quinmars/UaTypeGenerator/issues/1

Yes, Siemens replaced the DT by a TE to get the binary encoding ids.

I'd prefer to have the encoding IDs at generation time. I imagine that the binary encoding ID could be easily guessed from the data type ID. Looking at the encoding ID's of @dojo90, it seems I just have to replace the DT_ by a TE_ to get the binary encoding ids. @awcullen can you verify my assumption? I do not have access to a Siemens PLC at the moment. I would than add a switch to UaTypeGenerator that generates the binary encoding ids for Siemens PLCs.

As you can see from my example, I have already set a "TE" as prefix everywhere. As I said, I can deserialize the whole object, only a part of it does not work.


Is it now necessary to add both attributes (BinaryEncodingId, DataTypeId), as exampled here https://github.com/convertersystems/opc-ua-client/issues/174#issuecomment-739572829 ?

djonasdev commented 3 years ago

When you see that the Result value has a BodyType of ByteString, then the decoder did not find the BinaryEncodingId in it's registry.

Looking at the attribute for the type, I think I see some extra quotes:

[BinaryEncodingId("nsu=http://www.siemens.com/simatic-s7-opcua;s=TE_\"\"HTOPC_RecipeInfo\".\"General\"\"")]

Try:

[BinaryEncodingId("nsu=http://www.siemens.com/simatic-s7-opcua;s=TE_\"HTOPC_RecipeInfo\".\"General\"")]

(Sorry for the delay, I'm starting a new job :) )

I have now tested it again and removed the double quotes. Now it works as expected! Both as a complete object and as a child object. šŸ‘šŸ‘

But why it worked with the apparently "incorrect" declaration, you have to find out.