S7NetPlus / s7netplus

S7.NET+ -- A .NET library to connect to Siemens Step7 devices
MIT License
1.35k stars 595 forks source link

Can't write or read Strings within nested classes #455

Closed JaxMcCleary closed 1 year ago

JaxMcCleary commented 2 years ago

Hello,

I am pretty new to this sps subject. I used your library to read a database that contains 1 lifebit on the top level as well as 8 nested structures (4 for reading and 4 for writing). (Partial image of the database definition from the sps developer) The idea was to create a class with nested classes so i could use ReadClassAsync and WriteClassAsync. I am testing against snap7 right now and everything works except strings in the class.

This are the nested classes:

    [Serializable]
    public class FillingStationInputData
    {
          public byte HANDSHAKE { get; set; }
          public UInt32 PART_NO { get; set; }
          [S7String(S7StringType.S7String, 20)]
          public string BATCH_NO = string.Empty;
          [S7String(S7StringType.S7String, 3)]
          public string PART_TYPE = string.Empty;

          public byte BARREL_NO { get; set; }

          public bool ENABLE_FILLING { get; set; }

          public FillingStationInputData()
          {

          }

    }
    [Serializable]
    public class FillingStationOutputData
    {
        public byte HANDSHAKE { get; set; }
        public System.DateTime DATE { get; set; }
        public UInt32 PART_NO { get; set; }
        [S7String(S7StringType.S7String, 3)]
        public string BATCH_NO = string.Empty;

        public ushort BARREL_NO { get; set; }

        public float FILLWEIGHT { get; set; }
        public ushort BARREL_POS { get; set; }
        public ushort BARREL_TYPE { get; set; }

        public FillingStationOutputData()
        {

        }

    }
    [Serializable]
    public class SpsDatabase 
    {
        public bool LIFE_BIT { get; set; }
        public FillingStationInputData MES_SPS_ABF1 { get; set; }
        public FillingStationInputData MES_SPS_ABF2 { get; set; }
        public FillingStationInputData MES_SPS_ABF3 { get; set; }
        public FillingStationInputData MES_SPS_ABF4 { get; set; }

        public FillingStationOutputData SPS_MES_ABF1 { get; set; }
        public FillingStationOutputData SPS_MES_ABF2 { get; set; }
        public FillingStationOutputData SPS_MES_ABF3 { get; set; }
        public FillingStationOutputData SPS_MES_ABF4 { get; set; }

        public SpsDatabase() 
        {

        }

    }

I wanted to use properties only but the attribute S7String is only valid for Fields so I had to change from property to fields for strings. The test was fairly easy:

`        public async Task TestWrite() 
        {
            PlcConnection.Open();
            try 
            {
                Database = new SpsDatabase()
                {
                    LIFE_BIT = true,
                    MES_SPS_ABF1 = new FillingStationInputData() { BARREL_NO = 1, BATCH_NO = "ABF1", ENABLE_FILLING = false, HANDSHAKE = 1, PART_NO = 2342, PART_TYPE = "L" },
                    MES_SPS_ABF2 = new FillingStationInputData() { BARREL_NO = 2, BATCH_NO = "ABF2", ENABLE_FILLING = false, HANDSHAKE = 1, PART_NO = 2343, PART_TYPE = "L" },
                    MES_SPS_ABF3 = new FillingStationInputData() { BARREL_NO = 3, BATCH_NO = "ABF3", ENABLE_FILLING = false, HANDSHAKE = 1, PART_NO = 2344, PART_TYPE = "L" },
                    MES_SPS_ABF4 = new FillingStationInputData() { BARREL_NO = 4, BATCH_NO = "ABF4", ENABLE_FILLING = false, HANDSHAKE = 1, PART_NO = 2345, PART_TYPE = "L" },
                    SPS_MES_ABF1 = new FillingStationOutputData() { DATE = DateTime.Now, BARREL_NO = 1, BARREL_POS = 2, BARREL_TYPE = 2, BATCH_NO = "ABF1", FILLWEIGHT = 200, HANDSHAKE = 1, PART_NO = 2342 },
                    SPS_MES_ABF2 = new FillingStationOutputData() { DATE = DateTime.Now, BARREL_NO = 2, BARREL_POS = 2, BARREL_TYPE = 2, BATCH_NO = "ABF2", FILLWEIGHT = 200, HANDSHAKE = 1, PART_NO = 2343 },
                    SPS_MES_ABF3 = new FillingStationOutputData() { DATE = DateTime.Now, BARREL_NO = 3, BARREL_POS = 2, BARREL_TYPE = 2, BATCH_NO = "ABF3", FILLWEIGHT = 200, HANDSHAKE = 1, PART_NO = 2344 },
                    SPS_MES_ABF4 = new FillingStationOutputData() { DATE = DateTime.Now, BARREL_NO = 4, BARREL_POS = 2, BARREL_TYPE = 2, BATCH_NO = "ABF4", FILLWEIGHT = 200, HANDSHAKE = 1, PART_NO = 2345 }
                };
                await PlcConnection.WriteClassAsync(Database, 0);
            }
            catch(Exception ex) 
            {

            }

            PlcConnection.Close();
        }
        public async Task TestRead() 
        {
            PlcConnection.Open();
            SpsDatabase db = new SpsDatabase();
            try 
            {
                await PlcConnection.ReadClassAsync(db, 0);
            }
            catch(Exception ex) 
            {

            }
            PlcConnection.Close();
        }
`

When i read the written test data everything is correct except the strings. They are empty. I don't get any runtime exception while writing the database.

I am currently not sure what causes this issue.

scamille commented 2 years ago

Can you check what happens if you read the DB until the first part in a non-nested way?

bool
byte
dword
string[20]

Does the string contain anything useful?

If that still leads to no useful string, as a next debug step, you could define the following class:

bool
byte
dword
byte[22]

And check what is in the first 2 bytes of the supposed string. My knowledge of S7 types is a bit rusty, but I think the first byte is supposed to be the capacity (should be 20), and the second byte is the runtime size of the string content (0 to 20).

What often happens is that PLC developers just initialize strings with something that just memsets everything to 0, creating a invalid string where the capacity does not match what it is supposed to be. I am not sure what S7Plus exactly does in that case, but it might just give you an empty string.

JaxMcCleary commented 1 year ago

Sorry for the delayed answer and thank you for your input. Apparently there are some issues with the fact that the database is marked as "S7_Optimized_Access" flase. In the past days I've learned that if it is not optimized you can not read the variables externally by name. You need to read the variables with the offset and length. So I assume that's why I have some trouble with the function ReadClass and WriteClass. I assume the function takes the properties names and try to access the s7 variables with a combination of class name and property name.

scamille commented 1 year ago

The documentation for requiring unoptimized access is quite a bit hidden at https://github.com/S7NetPlus/s7netplus/wiki/S7-1200-1500-Notes#s7-12001500-notes

S7NetPlus can only work with unoptimized data blocks, as far as I know.

mycroes commented 1 year ago

Yes, only non-optimized blocks can be used. When using ReadClass or WriteClass the fields or properties are read or written in order of declaration. There's no magic happening with member names, just concatenation of all the members to one byte array.

I'm in the midst of releasing 0.16.0, that will also support the string attribute on classes.