ADS.Type (yaads v0.1.0)

Copy Markdown View Source

The data types of a TwinCAT PLC, and their values as Elixir terms.

PLCTypeValue
BOOL:booltrue or false
BYTE, WORD, DWORD, LWORD:byte, :word, :dword, :lwordan integer
SINT, USINT, INT, UINT, DINT, UDINT, LINT, ULINT:sint, :usint, :int, ... :ulintan integer
REAL, LREAL:real, :lreala float, or :nan, :infinity, :neg_infinity
STRING(n){:string, n}, {:string, n, :utf8}, or {:string, n, codepage}a string
WSTRING(n){:wstring, n}a string
TIME, LTIME:time, :ltimean integer of ms, or of ns
TIME_OF_DAY, LTIME_OF_DAY:time_of_day, :ltime_of_daya Time
DATE, LDATE:date, :ldatea Date
DATE_AND_TIME, LDATE_AND_TIME:date_and_time, :ldate_and_timea NaiveDateTime
ARRAY [a..b, c..d] OF T{:array, t, [{a, b}, {c, d}]}a list, nested for each dimension
STRUCT, FUNCTION_BLOCK{:struct, size, [{name, offset, t}]}a map of the members' names to their values
UNION{:union, size, [{name, offset, t}]}a map of every member; one at a time when written
an enumeration{:enum, t, [{name, value}]}the name of the value, or its number if it has none
BIT, in a structure:bittrue or false
anything else{:raw, size}its bytes
iex> ADS.Type.encode(:int, -2)
<<254, 255>>
iex> ADS.Type.decode({:string, 10}, "Pump 1" <> <<0::40>>)
{:ok, "Pump 1"}
iex> ADS.Type.parse("ARRAY [1..3] OF LREAL")
{:ok, {:array, :lreal, [{1, 3}]}}

Numbers are little-endian, as everything in ADS. A STRING(n) takes n + 1 bytes, the last for the zero that ends it, and holds Windows-1252, as Beckhoff's own libraries read it, unless declared with the attribute 'TcEncoding' := 'UTF-8', or of another code page, {:string, n, codepage} (see ADS.Codepage and codepage/2); what comes out is UTF-8, always. A WSTRING(n) takes 2(n + 1), in UCS-2, which has no characters past U+FFFF. TIME counts ms and LTIME ns; TIME_OF_DAY ms since midnight and LTIME_OF_DAY ns; DATE and DATE_AND_TIME seconds since 1970, and LDATE and LDATE_AND_TIME ns, as signed 64-bit numbers. A PLC's dates and times have no time zone, so they are naive here; LDATE_AND_TIME and LTIME_OF_DAY keep microseconds, dropping the last three digits of their ns. A time of day past midnight, which no PLC makes but bytes can hold, is its number.

A structure's members, a union's and an array's elements are where the PLC puts them: given by the PLC's description of the type, or by layout/2, which follows TwinCAT 3's rules for alignment. In a structure, the offset of a :bit member counts bits, from the lowest bit of the structure's first byte.

encode/2 raises ArgumentError for a value that doesn't fit its type, such as 70000 as an INT or a struct with a member missing. decode/2 never raises for any bytes of the right size.

Summary

Types

A member of a structure: its name, offset in bytes (in bits for :bit), and type.

t()

Functions

The type itself, if it's one; raises ArgumentError if it isn't, as for a member that doesn't fit in its structure.

The type with its STRINGs in the code page, those that aren't of one already, nor UTF-8: as ADS.Client reads and writes the PLC's with the code page it has.

A value from its bytes, or :error if there aren't as many as the type takes.

A value as bytes; raises ArgumentError if it doesn't fit the type.

The type of a value of size bytes by its ADS data type id alone, or nil: for when a type's name says nothing parse/2 knows.

The name of an ADS data type id, as a symbol or a data type describes itself, or the id if it has none: :int16 for 2, :big_type for 65, the id of structures and arrays.

A structure of members, each {name, type}, placed by TwinCAT 3's rules for alignment: each member at an offset that's a multiple of the size of its type, or of :pack_mode if that's smaller; strings at any byte; consecutive BIT members together in bytes; and the structure as a whole a multiple of its most aligned member in size, so that an array of them stays aligned.

The type of a PLC type name, for the types TwinCAT has built in, strings, arrays of them, and subranges such as INT (0..100); :error for any other, such as a structure, whose description the PLC has. POINTER TO, REFERENCE TO, XINT and the like are as big as the target's addresses, which size, the size of a value, tells.

The size of a type's values in bytes.

Types

member()

@type member() :: {String.t(), non_neg_integer(), t()}

A member of a structure: its name, offset in bytes (in bits for :bit), and type.

t()

@type t() ::
  :bool
  | :bit
  | :byte
  | :word
  | :dword
  | :lword
  | :sint
  | :usint
  | :int
  | :uint
  | :dint
  | :udint
  | :lint
  | :ulint
  | :real
  | :lreal
  | :time
  | :ltime
  | :date
  | :date_and_time
  | :time_of_day
  | :ldate
  | :ldate_and_time
  | :ltime_of_day
  | {:string, non_neg_integer()}
  | {:string, non_neg_integer(), :utf8 | ADS.Codepage.t()}
  | {:wstring, non_neg_integer()}
  | {:array, t(), [{integer(), integer()}]}
  | {:struct, non_neg_integer(), [member()]}
  | {:union, non_neg_integer(), [member()]}
  | {:enum, t(), [{String.t(), integer()}]}
  | {:raw, non_neg_integer()}

Functions

check!(type)

@spec check!(term()) :: t()

The type itself, if it's one; raises ArgumentError if it isn't, as for a member that doesn't fit in its structure.

codepage(type, codepage)

@spec codepage(t(), ADS.Codepage.t()) :: t()

The type with its STRINGs in the code page, those that aren't of one already, nor UTF-8: as ADS.Client reads and writes the PLC's with the code page it has.

iex> ADS.Type.codepage({:array, {:string, 10}, [{1, 2}]}, :windows_1251)
{:array, {:string, 10, :windows_1251}, [{1, 2}]}

decode(type, bytes)

@spec decode(t(), binary()) :: {:ok, term()} | :error

A value from its bytes, or :error if there aren't as many as the type takes.

encode(type, value)

@spec encode(t(), term()) :: binary()

A value as bytes; raises ArgumentError if it doesn't fit the type.

from_id(id, size)

@spec from_id(atom() | non_neg_integer(), non_neg_integer()) :: t() | nil

The type of a value of size bytes by its ADS data type id alone, or nil: for when a type's name says nothing parse/2 knows.

iex> ADS.Type.from_id(:uint16, 2)
:uint
iex> ADS.Type.from_id(:string, 81)
{:string, 80}
iex> ADS.Type.from_id(:big_type, 12)
nil

id_name(id)

@spec id_name(non_neg_integer()) :: atom() | non_neg_integer()

The name of an ADS data type id, as a symbol or a data type describes itself, or the id if it has none: :int16 for 2, :big_type for 65, the id of structures and arrays.

layout(members, opts \\ [])

@spec layout([{String.t(), t()}], keyword()) :: t()

A structure of members, each {name, type}, placed by TwinCAT 3's rules for alignment: each member at an offset that's a multiple of the size of its type, or of :pack_mode if that's smaller; strings at any byte; consecutive BIT members together in bytes; and the structure as a whole a multiple of its most aligned member in size, so that an array of them stays aligned.

  • :pack_mode - 8, TwinCAT 3's default, or 4, 2, 1 or 0 (none), as with the attribute 'pack_mode' of the structure; TwinCAT 2 had 1 on x86

For a structure on the PLC, the description the PLC has of its type is surer.

iex> ADS.Type.layout([{"fVar", :lreal}, {"nVar1", :dint}, {"nVar2", :sint}])
{:struct, 16, [{"fVar", 0, :lreal}, {"nVar1", 8, :dint}, {"nVar2", 12, :sint}]}
iex> ADS.Type.layout([{"nVar2", :sint}, {"fVar", :lreal}], pack_mode: 1)
{:struct, 9, [{"nVar2", 0, :sint}, {"fVar", 1, :lreal}]}

parse(name, size \\ nil)

@spec parse(String.t(), non_neg_integer() | nil) :: {:ok, t()} | :error

The type of a PLC type name, for the types TwinCAT has built in, strings, arrays of them, and subranges such as INT (0..100); :error for any other, such as a structure, whose description the PLC has. POINTER TO, REFERENCE TO, XINT and the like are as big as the target's addresses, which size, the size of a value, tells.

iex> ADS.Type.parse("STRING(80)")
{:ok, {:string, 80}}
iex> ADS.Type.parse("ARRAY [0..1, -1..1] OF INT")
{:ok, {:array, :int, [{0, 1}, {-1, 1}]}}
iex> ADS.Type.parse("POINTER TO REAL", 8)
{:ok, :ulint}
iex> ADS.Type.parse("ST_Pump")
:error

size(type)

@spec size(t()) :: non_neg_integer()

The size of a type's values in bytes.