Skip to Content

tag

Tag Archives: ADS

Obstacles when communicating with TwinCAT from F# (plus Workarounds)

I took a detour from main goal of project, which is type provider, in order to provide with lightweight fsharpish library for ADS communication. To my surprise a lot of drawbacks popped out, here you may find a descriptions of them, plus workarounds for some.

Marshalling structures with BOOL arrays.

On one hand, according to Infosys BOOL type has size of 1 byte (where 1 is true, 0 is false, while any other value is incorrect as it can’t be interpreted correctly), same 1 byte size for bool is in .NET. On the other hand, we have Win32 BOOL, which is 4 byte size (more detailed on SO).  Which is scenario we can easily handle in C#, but not in F#.

[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct BoolArrSample
{
  [MarshalAs(UnmanagedType.ByValArray, SizeConst = 10, ArraySubType = UnmanagedType.I1)]
  public readonly bool[] values;
}
.class public sequential ansi sealed beforefieldinit Structs.BoolArrSample
  extends [mscorlib]System.ValueType
{
  .pack 1
  .size 0

  .field public initonly marshal(fixed array[10] int8( bool[] values
}
[<Struct;StructLayout(LayoutKind.Sequential, Pack = 1)>]
type BoolArrSample =
  [<MarshalAs(UnmanagedType.ByValArray, SizeConst = 10, ArraySubType = UnmanagedType.I1)>]
  val  values: BOOL[]
.class public sequential ansi sealed serializable NuSoft.Ads.Experimental.Samples.BoolArrSample
  extends [mscorlib]System.ValueType
  implements class [mscorlib]System.IEquatable`1<valuetype NuSoft.Ads.Experimental.Samples.BoolArrSample>,
             [mscorlib]System.Collections.IStructuralEquatable,
             class [mscorlib]System.IComparable`1<valuetype NuSoft.Ads.Experimental.Samples.BoolArrSample>,
             [mscorlib]System.IComparable,
             [mscorlib]System.Collections.IStructuralComparable
{
  .custom instance void [FSharp.Core]Microsoft.FSharp.Core.StructAttribute::.ctor() = (
    01 00 00 00
  )
  .custom instance void [FSharp.Core]Microsoft.FSharp.Core.CompilationMappingAttribute::.ctor(valuetype [FSharp.Core]Microsoft.FSharp.Core.SourceConstructFlags) = (
    01 00 03 00 00 00 00 00
  )
  .pack 1
  .size 0

  .field assembly marshal(fixed array[10]( bool[] values@

  .property instance bool[] values()
  {
    .custom instance void [FSharp.Core]Microsoft.FSharp.Core.CompilationMappingAttribute::.ctor(valuetype [FSharp.Core]Microsoft.FSharp.Core.SourceConstructFlags,  int32) = (
      01 00 04 00 00 00 00 00 00 00 00 00
    )
    .get instance bool[] NuSoft.Ads.Experimental.Samples.BoolArrSample::get_values()
    {
      IL_0000: ldarg.0
      IL_0001: ldfld bool[] NuSoft.Ads.Experimental.Samples.BoolArrSample::values@
      IL_0006: ret
    }
  }
}

as you may notice, there’s no ArraySubType value in F# sample IL output. If you look at a visual fsharp sources you’ll notice that ArraySubType is disregarded. Click here for open issue.

Workaround for marhsalling byte arrays in  F#

Other than fixing the issue, one can use Struct array instead of  bool array, though it’s not much convenient.

[<AutoOpen>]
module Workarounds =
  open System.Runtime.InteropServices
  
  [<Struct;StructLayout(LayoutKind.Sequential, Pack=1)>]
  type BOOLSTRUCT = 
    val value: BOOL

    new(value) = {
      value=value
    }
    
  let inline BOOL (x: BOOL) = new BOOLSTRUCT(x)
  let inline BOOLSTRUCT (x: BOOLSTRUCT) = x.value

Marshalling TwinCAT TOD, DATETIME, TIME, DATE types

In TwinCAT time and date types are all 4 bytes, closest out-of-the-box equivalents in .Net world are DateTime for DATE_AND_TIME and DATE, and  TimeSpan for TIME_OF_DAY and TIME, problem is both of them are 8 bytes long.

Marshalling DateTime/TimeSpan values

If you read trough Infosys, you’ll find Reading and writing of TIME/DATE variables, it utilizes AdsStream and AdsBinaryReader/AdsBinaryWriter, but personally prefer to use other methods provided by TcAdsClient.dll.

[<AutoOpen>]
module Ext =
  type TcAdsClient with 
        member self.ReadDateTime(variableHandle) = 
          self.ReadAny(variableHandle, typedefof<uint32>) 
          :?> uint32
          |> int64 
          |> TwinCAT.Ads.Internal.PlcOpenDateConverterBase.ToDateTime

        member self.ReadTimeSpan(variableHandle) = 
          self.ReadAny(variableHandle, typedefof<uint32>) 
          :?> uint32
          |> int64 
          |> TwinCAT.Ads.Internal.PlcOpenTimeConverter.ToTimeSpan

        member self.WriteAny(variableHandle, value) = 
          let v = 
            value 
            |> TwinCAT.Ads.Internal.PlcOpenDateConverterBase.ToTicks
            |> uint32
          self.WriteAny(variableHandle, v) 
        
        member self.WriteAny(variableHandle, value) = 
          let v = 
            value 
            |> TwinCAT.Ads.Internal.PlcOpenTimeConverter.ToTicks
            |> uint32
          self.WriteAny(variableHandle, v) 

you can always use code from Infosys as an extension methods as well.

Marshalling DateTime/TimeSpan structure fields

When it comes to workaround for structure fields there is no workaround that I would like, at least not among those that I know to work. I had few approaches to solutions that do not work, and as I learned couldn’t even possible work. May say one learns by failing. Let’s start with one notable failure, as it also comes with F# missing functionality.

Custom marshallers

At first I thought that it would be possible to marshal one filed with usage of custom marshaller, well it doesn’t work that way.

I may be wrong at this, but Custom marshaller is not an option even if we invoked native method from tcadsdll.dll, as the signature

[DllImport("tcadsdll.dll", CharSet=CharSet.None, ExactSpelling=false)]
public static extern unsafe AdsErrorCode AdsSyncReadReq(byte* addr, uint indexGroup, uint indexOffset, int length, void* data);

expects void pointer not actual structure

Workaround

[<Struct;StructLayout(LayoutKind.Sequential, Pack = 1)>]
type DateTimeArraySample =
  [<MarshalAs(UnmanagedType.ByValArray, SizeConst = 10, ArraySubType = UnmanagedType.U4)>]
  val values: uint32[]

  member self.Values =
    self.values 
    |> Array.map (int64 >> TwinCAT.Ads.Internal.PlcOpenDateConverterBase.ToDateTime)

what I do not like about is obfuscating structure with additional properties, and even if we’d use extension properties/methods, it still involves duplicating every single DT/TOD/D/T fields.

Note on custom marshallers in F#

Putting it simply, they cannot be used ATM, SO question. Still not sure why, but actual code for read and write IL, is present in sources, yet when parsing MarshalAsAttribute exception is thrown explicitly, though it doesn’t seem that hard to fix it, I have it working on modified sources. If I only knew how to test it properly…

F# ADS Toolkit

Blog registered for “Daj się poznać” contest so I can start countdown until it begins as well as work on project.

To begin with let me explain what I’ll be working on and why I find it useful.

Background

I won’t go into detail what ADS (Automation Device System) is, you can read that at Infosys. What matters is, that it is a protocol that allows you to communicate with TwinCAT. Note that I’ll only focus on access via symbolic variable names.

If we were to read/write, let’s say uint16 variable, it would look like

use client = new TcAdsClient()

//connecto do ADS device
client.Connect(amsNetId,801)

//create variable handle
let myuintHandle = client.CreateVariableHandle ".myuint"

//read value and cast to short
let myuintValue = client.ReadAny(myuintHandle, Operators.typedefof<uint16>) :?> uint16

//write incremented value
client.WriteAny(myuintHandle,myuintValue + 1us)

(* some other logic *)

//remove handle
client.DeleteVariableHandle myuintHandle

//dispose client
client.Dispose()

If you look at highlighted lines, you’ll notice that this code is prone to errors. First of all we pass symbolic name as a string, if we make typo, in runtime we end up with “0x710 symbol not found” exception. Sure we can make it more convenient by wrapping it into generic read methods, but still we have to pay a lot of attention to naming and types. Yep, type names in TwinCAT follow IEC61131-3 specification . Our .NET uint16 is WORD/UINT in TwinCAT, for other types refer to Infosys.

Another tricky thing we can encounter are structures. Sure we can marshal whole structures, but as always, layout is important.

STRUCT
	lrealVal: LREAL := 1.23;
	dintVal1: DINT := 120000;
END_STRUCT
END_TYPE

TYPE TComplexStruct :
STRUCT
	intVal : INT:=1200;
	dintArr: ARRAY[0..3] OF DINT:= 1,2,3,4;
	boolVal: BOOL := FALSE;
	byteVal: BYTE:=10;
	stringVal : STRING(5) := 'hallo';
	simpleStruct1: TSimpleStruct;
END_STRUCT
END_TYPE
[<StructLayout(LayoutKind.Sequential, Pack=1)>]
type SimpleStruct =
  struct
    val lrealVal: double
    val dintVal1: int
  end

[<StructLayout(LayoutKind.Sequential, Pack=1)>]
type ComplexStruct =
  struct
    val intVal: int16
    [<MarshalAs(UnmanagedType.ByValArray, SizeConst=4)>]
    val dintArr: int array
    [<MarshalAs(UnmanagedType.I1)>]
    val boolVal: bool
    val byteVal: byte
    [<MarshalAs(UnmanagedType.ByValTStr, SizeConst=6)>]
    val stringVal: string
    val simpleStruct1: SimpleStruct
  end

Beside basically repeating same code on different sides, imagine situation where PLC developer slightly refactors his code and changes order of fields, no exceptions thrown but invalid values as a result.

Notice highlighted lines, when declaring .NET counterparts, you have to declare length 1 byte longer then TwinCAT due to string null terminator. That’s yet another thing you have to keep in mind when doing things manually.

I could go on with things we can mess up and end up debugging if we do things manually, but above are main problems I aim to address with this project.

First milestone – Convenient syntax and base classes

As a first milestone we should end up with set of base classes that we can extend manually to suit particular project we work on, and use it in convenient way

Second milestone – Type provider based on *.tpy file

Second milestone is to generate ADS client based on *.tpy file, it is generated when TwinCAT project is compiled, at least that’s default setting. It contains custom types definitions and variables. Format is XML based.