A PLC's description of one of its data types: a structure and its members, an array, an enumeration and its values, or another name for a type.
ADS.Client.data_type/3 asks for one by name (index group 0xF011), and
ADS.Client.data_types/2 uploads all of them (0xF00E, their size from 0xF00F). Each is an
entry of variable length, whose members are entries of their own inside it:
| Bytes | Field |
|---|---|
| 4 | the entry's length, padding included |
| 4 | version, 1 |
| 4, 4 | hash values |
| 4 | size in bytes |
| 4 | offset, of a member in its structure |
| 4 | ADS data type id (see ADS.Type.id_name/1) |
| 4 | flags |
| 2, 2, 2 | lengths of the name, the type and the comment |
| 2 | dimensions of an array |
| 2 | members of a structure |
| ... | the name, the type and the comment, each with a zero after it |
| 8 each | for each dimension, its first index and its number of elements |
| ... | each member, an entry of its own |
| 16 | the type's GUID, if flagged (0x0080) |
| size | a copy mask, if flagged (0x0200) |
| ... | methods, if flagged (0x0800): a count of 2 bytes, then entries of their own length |
| ... | attributes, if flagged (0x1000), as a symbol's (see ADS.Symbol) |
| ... | enumeration values, if flagged (0x2000): a count of 2 bytes, then each with a byte for the length of its name, the name and a zero, and the value in as many bytes as the type |
Beckhoff's manuals don't describe this layout either; it's written here from general knowledge
of the format, not from another implementation's code, and is yet to be checked against a PLC.
What follows the members is read as far as it makes sense, and left out after that. In a
structure, the offset and size of a BIT member (flagged 0x0020) count bits.
to_type/2 makes an ADS.Type of a description, for reading and writing its values.
Summary
Functions
The data type entry at the start of the bytes, as in the answer to a request by name; its
strings in UTF-8 if they are, as plain ASCII is, or else in the code page (see ADS.Codepage).
Every data type entry in the bytes, as uploaded; the strings as for decode/2.
The ADS.Type of a data type. resolve gives the type of a name the description refers to,
with the size it must have, as {:ok, type}, or anything else when it can't; ADS.Client asks
the PLC. What can't be made out, such as a member whose type is unknown, is {:raw, size}: its
bytes, so that the rest still decode.
Types
@type t() :: %ADS.DataType{ array: [{integer(), non_neg_integer()}], attributes: %{required(String.t()) => String.t()}, comment: String.t(), data_type: atom() | non_neg_integer(), enum: [{String.t(), integer()}], flags: non_neg_integer(), guid: String.t() | nil, hash: non_neg_integer(), members: [t()], name: String.t(), offset: non_neg_integer(), size: non_neg_integer(), type: String.t(), type_hash: non_neg_integer(), version: non_neg_integer() }
Functions
@spec decode(binary(), ADS.Codepage.t()) :: {:ok, t()} | :error
The data type entry at the start of the bytes, as in the answer to a request by name; its
strings in UTF-8 if they are, as plain ASCII is, or else in the code page (see ADS.Codepage).
@spec decode_all(binary(), ADS.Codepage.t()) :: {:ok, [t()]} | :error
Every data type entry in the bytes, as uploaded; the strings as for decode/2.
@spec to_type(t(), (String.t(), non_neg_integer() -> {:ok, ADS.Type.t()} | term())) :: ADS.Type.t()
The ADS.Type of a data type. resolve gives the type of a name the description refers to,
with the size it must have, as {:ok, type}, or anything else when it can't; ADS.Client asks
the PLC. What can't be made out, such as a member whose type is unknown, is {:raw, size}: its
bytes, so that the rest still decode.
iex> pump = %ADS.DataType{name: "ST_Pump", size: 16, data_type: :big_type, members: [
...> %ADS.DataType{name: "nSpeed", type: "INT", size: 2, offset: 0, data_type: :int16},
...> %ADS.DataType{name: "fTemp", type: "LREAL", size: 8, offset: 8, data_type: :real64}]}
iex> ADS.DataType.to_type(pump, fn _name, _size -> :error end)
{:struct, 16, [{"nSpeed", 0, :int}, {"fTemp", 8, :lreal}]}