convertersystems / opc-ua-client

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

Struct is read as ByteString rather than Encodable #253

Closed rmnbs closed 1 year ago

rmnbs commented 1 year ago

Hi,

I'm trying to create an MVVM application for a beckhoff automation project.

I'm trying to read my Status structure.

[DataTypeId("ns=4;s=<StructuredDataType>:Status")]
[BinaryEncodingId("ns=4;s=<StructuredDataType>:Status__DefaultBinary")]
public class Status : Structure
{
/*
<opc:StructuredType Name="Status" BaseType="ua:ExtensionObject" xmlns:opc="http://opcfoundation.org/BinarySchema/">
  <opc:Field Name="Init" TypeName="opc:Boolean" />
  <opc:Field Name="Ready" TypeName="opc:Boolean" />
  <opc:Field Name="Busy" TypeName="opc:Boolean" />
  <opc:Field Name="EMR_Safe" TypeName="opc:Boolean" />
  <opc:Field Name="SPACER_Safe" TypeName="opc:Boolean" />
  <opc:Field Name="Error" TypeName="opc:Boolean" />
  <opc:Field Name="ErrorCode" TypeName="opc:UInt32" />
  <opc:Field Name="AtStation" TypeName="tns:Stations" />
  <opc:Field Name="ProcessState" TypeName="tns:EMRStates" />
  <opc:Field Name="GUIControl" TypeName="opc:Boolean" />
  <opc:Field Name="AllLocked" TypeName="opc:Boolean" />
</opc:StructuredType>
*/

    public bool Init { get; set; } = false;
    public bool Ready{ get; set; } = false;
    public bool Busy { get; set; } = false;
    public bool EMR_Safe { get; set; } = false;
    public Boolean SPACER_Safe { get; set; } = false;
    public Boolean Error { get; set; } = false;
    public UInt32 ErrorCode { get; set; } = 0;
    public Stations AtStation { get; set; } = Stations.NoStation;
    public EMRStates ProcessState { get; set; } = EMRStates.NotReady;
    public Boolean GUIControl { get; set; } = false;
    public Boolean AllLocked { get; set; } = false;

    public override void Encode(IEncoder encoder)
    {
        encoder.PushNamespace("urn:BeckhoffAutomation:Ua:PLC1");
        encoder.WriteBoolean("Init", Init);
        encoder.WriteBoolean("Ready", Ready);
        encoder.WriteBoolean("Busy", Busy);
        encoder.WriteBoolean("EMR_Safe", EMR_Safe);
        encoder.WriteBoolean("SPACER_Safe", SPACER_Safe);
        encoder.WriteBoolean("Error", Error);
        encoder.WriteUInt32("ErrorCode", ErrorCode);
        encoder.WriteEnumeration("AtStation", AtStation);
        encoder.WriteEnumeration("ProcessState", ProcessState);
        encoder.WriteBoolean("GUIControl", GUIControl);
        encoder.WriteBoolean("AllLocked", AllLocked);
        encoder.PopNamespace();
    }

    public override void Decode(IDecoder decoder)
    {
        decoder.PushNamespace("urn:BeckhoffAutomation:Ua:PLC1");
        Init = decoder.ReadBoolean("Init");
        Ready = decoder.ReadBoolean("Ready");
        Busy = decoder.ReadBoolean("Busy");
        EMR_Safe = decoder.ReadBoolean("EMR_Safe");
        SPACER_Safe = decoder.ReadBoolean("SPACER_Safe");
        Error = decoder.ReadBoolean("Error");
        ErrorCode = decoder.ReadUInt32("ErrorCode");
        AtStation = decoder.ReadEnumeration<Stations>("AtStation");
        ProcessState = decoder.ReadEnumeration<EMRStates>("ProcessState");
        GUIControl = decoder.ReadBoolean("GUIControl");
        AllLocked = decoder.ReadBoolean("AllLocked");
        decoder.PopNamespace();
    }
}

I am unable read my struct with the MonitoredItem / Structure pattern.

To find out why I created a small Terminal app trying to read just that structure.

var clientDescription = new ApplicationDescription
{
    ApplicationName = "Workstation.UaClient.FeatureTests",
    ApplicationUri = $"urn:{System.Net.Dns.GetHostName()}:Workstation.UaClient.FeatureTests",
    ApplicationType = ApplicationType.Client
};

// create a 'ClientSessionChannel', a client-side channel that opens a 'session' with the server.
var channel = new ClientSessionChannel(
    clientDescription,
    null, // no x509 certificates
    new UserNameIdentity("", ""), // not the actual user identity
    "opc.tcp://localhost:4840", // not the actual endpoint.
    SecurityPolicyUris.None); // no encryption

await channel.OpenAsync();

// build a ReadRequest. See 'OPC UA Spec Part 4' paragraph 5.10.2
var readRequest = new ReadRequest {
    // set the NodesToRead to an array of ReadValueIds.
    NodesToRead = new[] {
        // construct a ReadValueId from a NodeId and AttributeId.
        new ReadValueId {
            // you can parse the nodeId from a string.
            // e.g. NodeId.Parse("ns=2;s=Demo.Static.Scalar.Double")
            NodeId = NodeId.Parse("ns=4;s=GVL.Status"),
            // variable class nodes have a Value attribute.
            AttributeId = AttributeIds.Value
        }
    }
};

var readResult = await channel.ReadAsync(readRequest);
var a = readResult.Results[0].Variant.Value as ExtensionObject; 
Console.WriteLine(a.BodyType);
var serverStatus = readResult.Results[0].GetValueOrDefault<CustomTypeLibrary.Status>();

What I get is that BodyType is of type ByteString. for GetValueOrDefault to function properly I would need an Encodable.

I don't understand why i'm reading the data as a ByteString. For context I have tried multiple ways to configure the structure. directly on the structure itself

{attribute 'OPC.UA.DA.StructuredType' := '1'}
{attribute 'OPC.UA.DA' := '2'}
TYPE Status :
STRUCT
    Init : BOOL;
    Ready : BOOL;
    Busy : BOOL;
    EMR_Safe : BOOL; (* Safe if EMR is not in RedZone *)
    SPACER_Safe : BOOL := FALSE; (* False on default so EMR only sets it to true when Spacer sends Event/Spacer_safe *)
    Error : BOOL;
    ErrorCode : UDINT; 
    AtStation : Stations;
    ProcessState : EMRStates;
    GUIControl : BOOL; // sets to True when GUI and control is assumed by GUI on this mode, TCPip commands are ignored basically teach mode
    AllLocked : BOOL;
END_STRUCT
END_TYPE

or on the variable

    {attribute 'OPC.UA.DA.StructuredType' := '1'}
    {attribute 'OPC.UA.DA' := '2'}
    Status : Status;

without success. I feel like it's a configuration issue somewhere on the client or server but I can't find it. If you have any idea about what is it I am doing wrong I'll gladly accept it. Thanks.

rmnbs commented 1 year ago

Ok, so I found out why it didn't work. So i'm letting this here for people with the same problem,

do not do [BinaryEncodingId("ns=4;s=<StructuredDataType>:Status__DefaultBinary")] do [BinaryEncodingId("nsu=urn:BeckhoffAutomation:Ua:PLC1;s=<StructuredDataType>:Status__DefaultBinary")]

ismdiego commented 1 year ago

Thank you for your error solving.

Just one question, why did you put "nsu=urn:BeckhoffAutomation:Ua:PLC1" in the NodeId?

Is it because you defined the namespace this way?

encoder.PushNamespace("urn:BeckhoffAutomation:Ua:PLC1");
//...
decoder.PushNamespace("urn:BeckhoffAutomation:Ua:PLC1");

So I ask myself... if one uses this in the Encode (and similar in Decode):

encoder.PushNamespace("my:namespace");

then it should be (?): nsu=my:namespace

Is this OK or are there some rules to follow when defining namespaces? Maybe the PLC vendor has some other rules? (in this case, how did you found that full namespace of your structure?)

Thank you very much

rmnbs commented 1 year ago

I can try to answer this question but do not give my words too much credit, i'm fairly new to opcUA and still discovering its particularities.

So, the only way I was able to make it work with Beckhoff's own opc UA server and my associated Twincat Project was to put nsu=urn:BeckhoffAutomation:Ua:PLC1 in the [BinaryEncodingId] declaration and use

encoder.PushNamespace("urn:BeckhoffAutomation:Ua:PLC1");
//...
decoder.PushNamespace("urn:BeckhoffAutomation:Ua:PLC1");

in the Encode/Decode

I guess if your BinaryEncodingIds are in "my:namespace" then that's what you should use in your code because when I explore the server BeckhoffAutomation:Ua:PLC1 is "where" this data is declared.

I am afraid this may be dependent of your opc ua server provider, for example last time I checked B&R's opc ua server did not support custom structure types.

I also found this repo https://github.com/RoddenLab/HMI-Demo in the list of project dependent of opc-ua-client. you'll find a duplicate of this issue in his.

I originally used the OPC ua browser VS extension provided by the creator of this library. With it you can auto generate MonitoringItem and DataTypes by dragging them from the browser to the code file.

the generated code for my DataType was

[DataTypeId("ns=4;s=<StructuredDataType>:Status")]
[BinaryEncodingId("ns=4;s=<StructuredDataType>:Status__DefaultBinary")]
public class Status : Structure
{
    // Properties Omitted for brevity
    public override void Encode(IEncoder encoder)
    {
        base.Encode(encoder);
        encoder.PushNamespace("urn:BeckhoffAutomation:Ua:PLC1");
        encoder.PopNamespace();
    }

    public override void Decode(IDecoder decoder)
    {
        base.Decode(decoder);
        decoder.PushNamespace("urn:BeckhoffAutomation:Ua:PLC1");
        decoder.PopNamespace();
    }
}

which is incorrect in my case (It may be a Beckhoff quirk).

Once changed to :

[DataTypeId("ns=4;s=<StructuredDataType>:Status")]
[BinaryEncodingId("nsu=urn:BeckhoffAutomation:Ua:PLC1;s=<StructuredDataType>:Status__DefaultBinary")]
public class Status : Structure
{
    // Properties Omitted for brevity
    public override void Encode(IEncoder encoder)
    {
        base.Encode(encoder);
        encoder.PushNamespace("urn:BeckhoffAutomation:Ua:PLC1");
        encoder.PopNamespace();
    }

    public override void Decode(IDecoder decoder)
    {
        base.Decode(decoder);
        decoder.PushNamespace("urn:BeckhoffAutomation:Ua:PLC1");
        decoder.PopNamespace();
    }
}

Then once completed with the Structure's members and filled out Decode/Encode I can read/write my data.

ismdiego commented 1 year ago

Thank you very much for your explanation. It helps me a lot.