跳到主要内容

过程数据

通过 device.ProcessData 读写过程数据。​

过程数据就是设备与控制器每周期互换的那一段字节。映射成结构体后,读写字段就是直接读写过程映像本身 —— 0 拷贝:不用拷贝、不用刷新,读到的永远是当前周期,写下的下个周期发出。

方向:写 Q(设备 → 控制器),读 I(控制器 → 设备)。I 区只读 —— 控制器每周期整段覆盖,写它静默无效。

总线周期由内核完成,不必自写周期循环;任意线程可同时用。

先把 I / Q 两个词对齐

GSDML 里那 20 字节的 Input(设备 → 控制器)就是 SDK 的 Q;GSDML 里那 32 字节的 Output(控制器 → 设备)就是 SDK 的 I。

两个词在配置文件和 SDK 里是反的,别按字面理解。SDK 一律按数据流的方向说:写 Q、读 I。

结构体契约
  • 必须 [StructLayout(LayoutKind.Sequential, Pack = 1)]。偏移按声明顺序紧排自算,不是 Marshal 的布局(Marshal 里 bool 是 4 字节 BOOL,映射按 1 字节算)。
    • 一个属性都不标:抛 ArgumentException,消息里说"必须标注 [StructLayout(LayoutKind.Sequential, Pack=1)]"。
    • 只写 Sequential 不写 Pack:Pack 为 0,抛 ArgumentException,消息里说"必须 Pack=1(无对齐填充)"。
  • 可用字段:数值(sbyte / byte / short / ushort / int / uint / long / ulong / float / double)、bool(1 字节)、枚举(按底层类型宽度)、定长 byte[](必须标 [MarshalAs(UnmanagedType.ByValArray, SizeConst = N)])、嵌套结构体(递归,同样 Sequential Pack = 1)。
  • string 字段抛 NotSupportedException(改用定长 byte[]);char 字段抛 NotSupportedException(改用 ushort)。结构体含引用字段(string / 类 / 数组 / 委托 / 接口)抛 InvalidOperationException("结构体含引用字段,不能映射到过程映像")。
  • 结构体大小必须 1 - 65536 字节,否则抛 ArgumentException,消息形如"结构体大小非法 (须在 1 - 65536 字节): 类型全名"。
  • 多字节字段是网络大端(PROFINET 网络序)。直接当宿主 short / int 读会字节颠倒,要宿主数值走 Read<T> / Write<T>(大端编解码)。
  • I / Q 分两套结构体,不要混在一个里。
0 拷贝只有一种形态

真正的 0 拷贝只有整幅映像的结构体映射 —— ProcessData.InputsMapping<T>() / ProcessData.OutputsMapping<T>()。

In[i] / Out[i] 那一族、Inputs / Outputs、ReadArea / WriteArea、Read<T> / Write<T> 全都是拷贝:每次取值都拷一小段字节再解释。

别把「按偏移取到的元素」当成活视图 —— [i] 定的只是偏移,值仍然是那一瞬间的快照。

通道未就绪时如实报错

读写如实报错,不会静默降级。

使用案例​

日常使用​

① 映射到结构体(0 拷贝,持续用):Start 之后映射一次,拿到钉在过程映像上的 ref,之后当变量读写。

using System.Runtime.InteropServices;
using Darra.PnS;

[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct FromPlc // I:控制器 → 设备
{
public byte Present; // 偏移 0
public short Recipe; // 偏移 1:多字节是网络大端
}

[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct ToPlc // Q:设备 → 控制器
{
public byte Ready; // 偏移 0
public byte Busy; // 偏移 1
}

using (var device = new DarraPnS())
{
PnSErrorCode rc = device.Start();
if (rc != PnSErrorCode.Success)
return;

ref readonly FromPlc inp = ref device.ProcessData.InputsMapping<FromPlc>(); // 映射一次
ref ToPlc outp = ref device.ProcessData.OutputsMapping<ToPlc>();

byte present = inp.Present; // 读:当前周期
outp.Ready = 1; // 写:下个周期发出,没有提交调用
device.Stop();
}

② 单次读取(一次性取):不要结构体,只取一份当前值。

byte[] i = device.ProcessData.Inputs;               // I 整段:一次读出,同一拍自洽
short recipe = device.ProcessData.In[2].AsInt16; // 按字节偏移类型化读一个值
int counter = device.ProcessData.In[4].AsInt32;

字符串字段在过程映像里就是一段定长字节:映射不收引用字段,用快照一次读出。

[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct FromPlcText
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
public byte[] PartNo;
}

var text = new FromPlcText();
PnSErrorCode rc = device.ProcessData.Read(ref text); // I 区快照读出(大端解码)
string partNo = System.Text.Encoding.ASCII.GetString(text.PartNo).TrimEnd('\0');

③ 按 TIA 地址读一个值:地址写法和 PLC 里一样,不用自己数字节偏移。

device.ReadBool("I0.0", out bool done);      // 位
device.ReadInt16("IW2", out short recipe); // 字
device.ReadInt32("ID4", out int counter); // 双字

device.WriteBool("Q0.0", true); // 写 Q
device.WriteInt16("QW2", 123);

周期变化​

映射取一次就够,同一份映射放进你自己的周期循环(或循环里的调度点)直接用:

ref readonly FromPlc inp = ref device.ProcessData.InputsMapping<FromPlc>();
ref ToPlc outp = ref device.ProcessData.OutputsMapping<ToPlc>();

while (running) // 你自己的周期任务,节拍自定
{
outp.Ready = (byte)(inp.Present + 1); // 读当前拍 → 算 → 写下拍发出
Thread.Sleep(10);
}

循环里读到的永远是当前周期、写下的下个周期发出,不需要任何额外调用。SDK 没有用户周期回调,调度(循环 / 定时器 / 事件)由你自己定。

映射放在循环外取一次仍然是推荐用法,但要知道它换来的是什么:省掉每次解析,代价是偏移一旦漂移就是静默读错位(见下面 InputsMapping<T>() 的说明)。

结构体映射(零拷贝)​

映射没有偏移参数:起点永远是净荷第 0 字节。要映射中段,在结构体开头放一段占位 byte[] 顶开;或改用 In[i] / ReadArea 按偏移取。

InputsMapping<T>()​

把 I 区(控制器 → 设备)的净荷钉成只读结构体,0 拷贝。映射一次,之后读字段就是读当前周期。

public unsafe ref readonly T InputsMapping<T>() where T : struct

返回值:

  • ref readonly T — 钉在过程映像净页上的只读引用,任意线程可读

说明:

  • 只在 Start() 之后可用。 服务没 Start 或已 Stop,抛 InvalidOperationException,消息是「过程数据输入映像不可用: 原因」。原因取 LastServiceError,例如「服务未 Start 或已 Stop: 过程映像不可用, 请先 Start()」;没有原因时只留基础文案,不带尾部冒号。
  • 通道没映射:LastServiceError 是「过程数据通道未映射, 过程映像不可用 (请先 Start() 建立过程数据通道)」,抛出的异常同上。
  • 结构体含引用字段:InvalidOperationException("结构体含引用字段,不能映射到过程映像")。
  • 结构体大于净荷窗口:InvalidOperationException("结构体大于输入过程映像")。
  • 结构体布局非法:ArgumentException(未标 Sequential / Pack != 1 / 大小越界 / 字段类型不支持,见上面的结构体契约)。
  • I 区只读:控制器每周期整段覆盖,写了下一拍就被冲掉,所以返回的是 ref readonly T,类型上直接禁写。要写过程数据用 OutputsMapping<T>()。
  • 起点不是常量。 净荷起点是每次映射时按当前区内容扫出来的:先剥 2 个 IOPS 前缀,再看净荷开头连续有几个 0x80,最多再剥 4 个。所以同一个结构体在不同时刻可能钉在差几个字节的位置上,能用的窗口比 ProcessData.Inputs.Length 小 2-6 字节。
  • 映射放循环外取一次是推荐用法:省掉每次解析。代价是上面那条 —— 偏移一旦漂移就是静默读错位,不抛异常,尺寸门也拦不住(尺寸门只保证不越出净荷窗口、不戳出映射页,不校验主站侧 GSDML 配的长度;结构体大于主站实际下发的 I 长度时,尾部字段读到的是没刷新的字节,SDK 如实返回、不伪造)。
  • 生命周期:ref 的有效期 = 映射存续期。Stop() 只停刷新、不卸映射(读回旧值);Dispose() 卸映射后 ref 立即失效,再读是未定义内存,不会抛异常提醒。
  • 任意线程可读。

示例:

ref readonly FromPlc inp = ref device.ProcessData.InputsMapping<FromPlc>();
byte present = inp.Present; // 当前周期值

InputsMapping<T>(onChanged)​

映射成结构体实例并挂数据变化回调。不是 0 拷贝 —— 它是 1 ms 轮询快照;金路径还是上面那个无参重载。

public InputProcessDataInstance<T> InputsMapping<T>(Action onChanged) where T : struct
public InputProcessDataInstance<T> InputsMapping<T>(Action<ProcessDataStructChangedEventArgs<T>> onChanged) where T : struct

返回值:

  • InputProcessDataInstance<T> — 轮询封装;.ValueSnapshot 是最新一份快照(不会自己跟着总线变),.PollingError 是后台失败原因

回调参数:

public sealed class ProcessDataStructChangedEventArgs<T> : EventArgs where T : struct
{
public T Previous { get; } // 变化前的快照
public T Current { get; } // 变化后的快照
}

带参数的重载给 Previous / Current;不带参数的重载只通知"变了"。两个重载其它行为一样。

说明:

  • 构造时就先读一次快照。 没 Start 就构造,抛 InvalidOperationException("读取输入过程数据初始快照失败: rc=InvalidHandle")。所以回调不可能在 Start 之前响。
  • 后台线程名叫 Darra.PnS.InputProcessData(IsBackground = true),每 1 ms 读一次快照,只在内容变了才回调(逐字段深度比较,不是按字节比)。
  • 回调跑在那个后台线程上,且在锁外调用 —— 回调里读 ValueSnapshot 不会自锁。
  • 回调里抛异常 = 轮询永久停掉。 异常被吞进 PollingError,只写一条 Trace.TraceError,之后再也不回调;再读 ValueSnapshot 才把原因抛出来:InvalidOperationException("输入过程数据后台轮询已停止", 内层异常)。
  • 快照读失败(例如中途 Stop 了)同样把 InvalidOperationException("读取输入过程数据快照失败: rc=...") 记进 PollingError 并停轮询。
  • PollingError 没失败时为 null。
  • 想停就 Dispose():幂等;最多等 1000 ms 收线程,超时把 TimeoutException 记进 PollingError。
  • Q 侧没有对应的回调重载。
  • 这不是总线周期通知,也不能当 0 拷贝用。

示例:

InputProcessDataInstance<FromPlc> watch = device.ProcessData.InputsMapping<FromPlc>(
(ProcessDataStructChangedEventArgs<FromPlc> e) =>
Console.WriteLine($"{e.Previous.Recipe} -> {e.Current.Recipe}"));

FromPlc now = watch.ValueSnapshot; // 最新一份快照
watch.Dispose(); // 停轮询

OutputsMapping<T>()​

把 Q 区(设备 → 控制器)的净荷钉成可写结构体,0 拷贝。映射一次,改字段即改过程映像。

public unsafe ref T OutputsMapping<T>() where T : struct

返回值:

  • ref T — 可写引用,钉在输出净荷上;改完字段下个周期发出

说明:

  • 异常与 InputsMapping<T>() 一一对应,只把"输入"换成"输出":InvalidOperationException 的消息分别是「过程数据输出映像不可用: 原因」和「结构体大于输出过程映像」;含引用字段、布局非法同样抛。
  • 没有任何提交调用。 内核周期 DPC 每周期原样取走输出区组帧,不按脏标记门控 —— 改字段就够了。
  • 起点同样是扫出来的;Q 侧固定剥 3 字节前缀,所以能用的窗口正好等于 ProcessData.Outputs.Length。
  • 钉页是单 owner:同一映像同一时刻只允许一个可写映射。读可并发,写请调用方自行串行化。
  • Dispose() 卸映射后 ref 失效;Stop() 只停刷新、不卸映射。
  • 旧名 OutputsMapping<T>(ref T) 已删除。它是快照提交,和这个钉页同名不同义,现在叫 SubmitStruct<T>(ref T),没留 [Obsolete] 转发。新代码别写旧名。

示例:

ref ToPlc outp = ref device.ProcessData.OutputsMapping<ToPlc>();
outp.Ready = 1; // 下个周期发出
outp.Busy = 0;

Read<T>(ref T)​

【快照】I 区 → 结构体,大端解码。要 0 拷贝请用 InputsMapping<T>()。

public PnSErrorCode Read<T>(ref T data) where T : struct

返回值:

  • PnSErrorCode — 成功是 Success;失败见下

说明:

  • 未 Start → InvalidHandle。
  • 结构体大于 I 区 → LengthMismatch。这里的边界是整段 ProcessData.Inputs.Length,比映射窗口宽松。
  • 通道未就绪(映射掉了 / 读异常)→ NativeError,原因写在 LastServiceError。
  • 结构体布局非法 → ArgumentException(快照和映射走同一套布局校验)。
  • 含引用字段/string / char → NotSupportedException 或 ArgumentException,与映射同一套判据。
  • bool 字段按 bit0 编解码:解码时只有最低位算真(0x02 / 0x80 解出 false),编码时只写出 1 / 0。跟元素口的 AsBool 同一口径。
  • 不是 0 拷贝:每次调用拷一份当前快照再解,拿到的实例不会跟着总线变。
  • device.ReadStruct<T>(ref x) 与它同义。

示例:

FromPlc rx = new FromPlc();
PnSErrorCode rc = device.ProcessData.Read(ref rx);

Write<T>(ref T)​

【快照】结构体 → Q 区,大端编码。要 0 拷贝请用 OutputsMapping<T>()。

public PnSErrorCode Write<T>(ref T data) where T : struct

返回值:

  • PnSErrorCode — Success 表示整段已当场提交到共享区

说明:

  • 未 Start → InvalidHandle。
  • 结构体大于 Q 区 → LengthMismatch。边界就是 ProcessData.Outputs.Length。
  • 通道未就绪/提交失败 → NativeError,原因写在 LastServiceError。提交失败时那片脏字节留在本地影子不丢,下一次写会带上,同时 IoStatus.OutputValid 如实为 false(不冒充已送达)。
  • 结构体布局非法 → ArgumentException;定长 byte[] 字段的实际长度和 SizeConst 不符 → ArgumentException,消息形如「byte[] 字段 X 长度必须为 N 字节」。
  • I 区没有对应的入口(写 I 静默无效,所以不给)。
  • device.WriteStruct<T>(ref x) 与它同义。

示例:

ToPlc tx = new ToPlc { Ready = 1, Busy = 0 };
PnSErrorCode rc = device.ProcessData.Write(ref tx);

SubmitStruct<T>(ref T)​

Write<T> 的别名,一字不差转发到 Write。

public PnSErrorCode SubmitStruct<T>(ref T data) where T : struct

返回值:

  • PnSErrorCode — 同 Write<T>

说明:

  • 失败形态和 Write<T> 完全一样(未启动 InvalidHandle / 越界 LengthMismatch / 提交失败 NativeError / 布局非法 ArgumentException)。
  • 它是旧名 OutputsMapping<T>(ref T) 的取代者。新代码写 SubmitStruct<T> 或 Write<T> 都行,两者同义。

示例:

device.ProcessData.SubmitStruct(ref tx);

GetFieldAddress<T>(fieldName)​

把结构体字段名翻成 PLC 风格地址,省得自己数字节。

public string GetFieldAddress<T>(string fieldName) where T : struct
public string GetFieldAddress<T>(string fieldName, PnSAddressArea area) where T : struct

返回值:

  • string — 地址字符串;不带 area 时默认按 I 区(与 Read 同方向)

格式按字段类型定:

  • bool → I<偏移>.0(该字节的第 0 位,和 ReadBool("I0.0") 读的是同一个字节位)
  • 2 字节(short / ushort / 2 字节枚举)→ IW<偏移>
  • 4 字节(int / uint / float / 4 字节枚举)→ ID<偏移>
  • 1 字节 / 8 字节 / 定长数组 / 嵌套结构体 → I<偏移>(起始字节地址;地址化 API 没有 8 字节直读)
  • 方向由 area 定:PnSAddressArea.Input 出 I,PnSAddressArea.Output 出 Q

说明:

  • 字段名区分大小写(按 Ordinal 比)。找不到抛 ArgumentException("结构体 类型名 中不存在字段: 名字")。
  • 字段名空 / 全空白:ArgumentException("字段名不能为空")。
  • area 不是 Input / Output:ArgumentException("仅支持 Input (I 区) / Output (Q 区) 地址")。
  • 结构体布局非法:ArgumentException(判据同映射)。
  • 生成出来的地址可能未对齐(偏移就是声明偏移),因为地址化读写不强制对齐,两者自洽。

示例:

string addr = device.ProcessData.GetFieldAddress<FromPlc>("Recipe");              // "IW1"
string qAddr = device.ProcessData.GetFieldAddress<ToPlc>("Ready", PnSAddressArea.Output); // "Q0"(Ready 是 1 字节,不带 .位号)
short recipe;
device.ReadInt16(addr, out recipe);

直接访问(不映射也能用)​

下面这组调用与结构体映射彼此独立:不映射可以直接用,映射了也常并用。

In / Out​

public ProcessDataArrayInstance In  { get; }   // I 区(控制器 → 设备):只读
public ProcessDataArrayInstance Out { get; } // Q 区(设备 → 控制器):可读可写

返回值:

  • ProcessDataArrayInstance — 字节偏移访问器;每次访问属性都新建一个,且是"还没选偏移"的状态

说明:

  • In 是 I 区,控制器每周期覆盖 → 只读。对 In 赋值或写它的元素成员一律抛 InvalidOperationException。
  • Out 是 Q 区,每次成员赋值都当场提交一次,成功的写下个周期发出。
  • 刚拿到手、还没写 [i] 就用它的成员:InvalidOperationException("请先通过 ProcessData.In[index] 或 ProcessData.Out[index] 选择字节偏移")。

示例:

byte first = device.ProcessData.In[0].Content;

In[i] / Out[i]​

public ProcessDataArrayInstance this[int index] { get; }

选择过程映像里的绝对字节偏移。i 是字节地址,不是元素序号 —— 这一点和别的数组不一样。

参数:

  • index (int) — 本方向的字节偏移,相对本方向过程映像起点

返回值:

  • ProcessDataArrayInstance — 带偏移的访问器;本身不搬数据,取值时才读

说明:

  • index 小于 0 或大于 65535 抛 ArgumentOutOfRangeException。
  • 索引合法但那一拍读不出来(没 Start / 偏移超出当前区长度 / 通道没起来):取值时抛 InvalidOperationException,消息里带偏移和错误码。
  • 元素口没有 Length 属性。长度从 ProcessData.Inputs.Length / ProcessData.Outputs.Length 拿。
  • 8 字节字段用 AsInt64 / AsUInt64 按偏移取;或直接走结构体映射。
  • Out[i] 逐字段写是每次一次提交;要一次改多个字段,用 OutputsMapping<T>() 钉页 —— 改字段不需要提交调用。

相关结构:

public sealed class ProcessDataArrayInstance      // ProcessData.In[i] / Out[i] 的类型
{
// 下标与原始字节
public ProcessDataArrayInstance this[int index] { get; } // 绝对字节偏移;< 0 或 > 65535 抛 ArgumentOutOfRangeException
public byte Content { get; set; } // 原始字节;In 只读,Out 可写

// 定宽标量(多字节一律网络大端,PROFINET 网络序)
public sbyte AsInt8 { get; set; } // 有符号 8 位,1 字节
public byte AsUInt8 { get; set; } // 无符号 8 位,1 字节;与 Content 同一个字节
public short AsInt16 { get; set; } // 有符号 16 位,2 字节
public ushort AsUInt16 { get; set; } // 无符号 16 位,2 字节
public int AsInt32 { get; set; } // 有符号 32 位,4 字节
public uint AsUInt32 { get; set; } // 无符号 32 位,4 字节
public long AsInt64 { get; set; } // 有符号 64 位,8 字节
public ulong AsUInt64 { get; set; } // 无符号 64 位,8 字节
public float AsFloat { get; set; } // IEEE754 单精度,4 字节
public double AsDouble { get; set; } // IEEE754 双精度,8 字节
public bool AsBool { get; set; } // bit0 语义;写整字节 1 / 0,不是只改 bit0

// 位
public bool GetBit(int bitIndex); // 0-7,0 = 最低位;In / Out 都可读
public void SetBit(int bitIndex, bool value); // 只改这一位,同字节其余位不动;仅 Out

// 按数据类型进出
public object AsValue(PnSDataType type); // 按 PnSDataType 读 boxed 值
public void WriteValue(PnSDataType type, object value); // 按 PnSDataType 写;仅 Out
}

每个口的失败形态:

  • 读失败统一抛 InvalidOperationException。单字节口(Content / AsInt8 / AsUInt8 / AsBool)的消息形如「读取 I 区过程数据字节失败: index=0, rc=LengthMismatch」;多字节口(AsInt16 及以上)形如「读取 I 区过程数据失败: index=2, length=2, rc=LengthMismatch」。方向是 Q 时消息里的字母就是 Q。
  • 写失败(Q 侧):单字节口「写入 Q 区过程数据字节失败: index=0, rc=...」,多字节口「写入 Q 区过程数据失败: index=2, length=2, rc=...」。
  • 写 I 侧:In[i] 上的任何写口(Content / AsXxx / SetBit)一律抛 InvalidOperationException("ProcessData.In 对应 I 区 (控制器→设备),只允许读取"),一个都不落。
  • 每次取值都是一份新拷贝:读出来的 byte / short / ... 是那一瞬间的值,不会跟着总线变。

AsBool 是 bit0 语义:字节 0x02 / 0x80 这类非规范值读 false,0x00 / 0x01 才是 false / true。要按整字节 0 / 非 0 判真假,读 Content 自己比。

AsBool 的 setter 写的是整个字节

device.ProcessData.Out[i].AsBool = value 把该字节整体写成 1 或 0,不是只改 bit0 —— device.ProcessData.Out[0].AsBool = true 会把同一字节的 bit1-7 一起清掉。

同一字节里既有布尔位又有其它标志位时,用 SetBit(i, value):它只覆盖目标位,其余位保持不变。

示例:

byte  b  = device.ProcessData.In[0].Content;      // 读:当前拍
short iw = device.ProcessData.In[2].AsInt16; // 大端解码
int id = device.ProcessData.In[4].AsInt32;
bool f = device.ProcessData.In[0].AsBool; // bit0 语义

device.ProcessData.Out[0].Content = 0x11; // 写:下个周期发出
device.ProcessData.Out[2].AsInt16 = 123;

GetBit(bitIndex) / SetBit(bitIndex, value)​

单独用同一个字节里的某一位。

public bool GetBit(int bitIndex);                 // In / Out 都可读
public void SetBit(int bitIndex, bool value); // 仅 Out

参数:

  • bitIndex (int) — 位号 0-7,0 = 最低位(LSB)
  • value (bool) — true 置 1,false 清 0

返回值:

  • bool — 该位是否为 1

说明:

  • bitIndex 不在 0-7:抛 ArgumentOutOfRangeException,消息是「位下标必须在 0-7」。这条检查在最前面,对 In 调用也一样先报位号错。
  • 位号合法、但方向是 In:SetBit 走到写入时抛 InvalidOperationException("ProcessData.In 对应 I 区 (控制器→设备),只允许读取")。
  • SetBit 是读-改-写同一字节:先读当前字节,改掉目标位,再整字节写回,同字节其余位不动。
  • 读不到字节时抛的是 Content 那两条读失败异常。

示例:

bool run = device.ProcessData.In[3].GetBit(0);      // 读 I 的第 3 字节 bit0
device.ProcessData.Out[0].SetBit(0, true); // 只置 Q 的第 0 字节 bit0

AsValue(type) / WriteValue(type, value)​

按数据类型枚举进出,适合类型在运行期才定的场合(表格驱动、变量联想表)。

public object AsValue(PnSDataType type);                    // In 读 I 区;Out 读 Q 影子
public void WriteValue(PnSDataType type, object value); // 仅 Out

参数:

  • type (PnSDataType) — 过程数据类型
  • value (object) — 要写入的值,须能转成对应的 CLR 类型

返回值:

  • object — 装箱值:bool / sbyte / short / int / long / byte / ushort / uint / ulong / float / double

支持的取值(宽度与字节序同上面的定宽口):

  • Bool → bool(bit0 语义)
  • Integer8 / Integer16 / Integer32 / Integer64 → sbyte / short / int / long
  • Unsigned8 / Unsigned16 / Unsigned32 / Unsigned64 → byte / ushort / uint / ulong
  • Real32 / Real64 → float / double
  • Byte / Word / DWord / LWord 分别是 Unsigned8 / 16 / 32 / 64 的别名,同一个宽度

说明:

  • type 不受支持:ArgumentOutOfRangeException,消息形如「不受支持的过程数据类型: xxx」。
  • WriteValue 的 value 为 null:ArgumentNullException。
  • value 转不过去:Convert 自己抛(InvalidCastException / FormatException / OverflowException)。
  • 对 In 调 WriteValue:落到写口时抛 InvalidOperationException("ProcessData.In 对应 I 区 (控制器→设备),只允许读取")。
  • 读不到字节时抛的是 Content 那两条读失败异常。

示例:

object v = device.ProcessData.In[4].AsValue(PnSDataType.Integer32);
device.ProcessData.Out[4].WriteValue(PnSDataType.DWord, 10000);

整帧与整区​

映射放不下的字段(字符串、变长数据、整批快照)走这几个口。全是拷贝 —— 每次调用取到的都是那一刻的快照:单次调用返回的这份内部自洽,跨多次调用不保证同一拍。要同一拍就一次读够。

Inputs / Outputs​

public byte[] Inputs  { get; }        // I 整段:每次访问都给一份新的快照
public byte[] Outputs { get; set; } // Q 整段:get 给快照;set 提交整段

返回值:

  • byte[] — 那一刻的快照,不是 0 拷贝;拷出来不会跟着总线变

说明:

  • 这两个属性声明在 device.ProcessData 上,不在 device 上。
  • Inputs 每次访问都新分配一个 byte[]。读不出来(没 Start / 通道没起来)返回空数组,不抛 —— 判断"读到没有"要看 Length,不要用异常。
  • Outputs 的 get 同理,读不出来返回空数组。
  • Outputs 的 set 是短写:只覆盖前 N 字节,不要求长度等于整区。赋 null 抛 ArgumentNullException;提交失败抛 InvalidOperationException("写输出过程数据失败 rc=" + 错误码)。
  • Inputs 已经剥掉开头的 IOPS 前缀,净荷从下标 0 起,别自己再加偏移;Inputs.Length 按 I 区整段长度算(前缀字节仍计入长度、尾部补 0),所以有效净荷只有前 Length - 前缀 字节。Outputs.Length 是输出区长度减 3(净荷窗)。见下面「字节数怎么拿」。
  • 高频循环里别用这两个属性:每次访问都新分配一个大数组。要么用上面的映射,要么改用 In[i] / Out[i] 按需取。

示例:

byte[] frame = device.ProcessData.Inputs;
string name = System.Text.Encoding.ASCII.GetString(frame, 0, 16).TrimEnd('\0');

ReadArea(area, out data) / WriteArea(area, data)​

按区域一次性读 / 写整段,替代逐地址读写的来回。

public PnSErrorCode ReadArea(PnSAddressArea area, out byte[] data);
public PnSErrorCode WriteArea(PnSAddressArea area, byte[] data);

参数:

  • area (PnSAddressArea) — Input = I 区(只读)/ Output = Q 区(只写)。Memory / Db / Unknown 一律 InvalidArgument:不支持 M 区、不支持 DB。

返回值:

  • PnSErrorCode — Success 才是成功;data 是那一刻的区域快照

ReadArea 的失败形态:

  • 区域不是 Input / Output → InvalidArgument。
  • 未 Start → InvalidHandle。
  • 通道未就绪、或读共享区抛异常 → NativeError,原因写进 LastServiceError。
  • 返回的 data 长度就是 ProcessData.Inputs.Length(I 区)/ ProcessData.Outputs.Length(Q 区),整区全量,没有"越界"这一说。

WriteArea 的失败形态:

  • 写 I 区 → NotSupported(不抛)。这一条在未启动检查之前,没 Start 也一样是 NotSupported。
  • 区域不是 Output(例如 Memory / Db)→ InvalidArgument。
  • 未 Start → InvalidHandle。
  • data 为 null 或空数组 → InvalidArgument。
  • data.Length 和 ProcessData.Outputs.Length 不一致 → LengthMismatch。长度必须完全相等,不允许多也不允许少。
  • 提交失败 → NativeError。

说明:

  • ReadArea 走共享区现值,不阻塞。
  • WriteArea 写入 Q 影子后当场单次提交:Success 就是整区已经落在共享区。
  • 一次调用读写整区,比逐个 Read* / Write* 省往返。

示例:

device.ReadArea(PnSAddressArea.Input, out byte[] i);      // I 整段
byte[] q = new byte[device.ProcessData.Outputs.Length];
Array.Copy(i, q, q.Length);
device.WriteArea(PnSAddressArea.Output, q); // Q 整段,长度必须相等

Read(out fromPlc) / Write(toPlc)​

区域整段的便捷包装,语义同上。

public PnSErrorCode Read(out byte[] fromPlc);    // = ReadArea(Input), 读 I 区
public PnSErrorCode Write(byte[] toPlc); // 写 Q 区, 允许短写

参数:

  • toPlc (byte[]) — 要发出去的 Q 区字节,允许短写(只覆盖前 N 字节)

返回值:

  • PnSErrorCode — 失败码同 ReadArea / WriteArea

说明:

  • Read 就是 ReadArea(PnSAddressArea.Input, out fromPlc),失败形态完全一样。
  • Write 与 WriteArea(Output, ...) 的差别只有一个:Write 允许长度小于整区(短写),WriteArea 要求长度完全相等。
  • toPlc 为 null 或空数组 → InvalidArgument。
  • 长度超出当前 Q 区 → LengthMismatch。
  • 未 Start → InvalidHandle;提交失败 → NativeError。

示例:

device.Write(new byte[] { 0x01, 0x00 });    // 短写 Q:只覆盖前 2 字节

device[offset](indexer)​

按绝对字节偏移读写一个字节的语法糖。

public byte this[int offset] { get; set; }

参数:

  • offset (int) — 绝对字节偏移

返回值:

  • byte — 该偏移的字节

说明:

  • 不是视图。 get 是「读整段 I 区 → 取第 offset 个字节」,每次取值都整区拷一份。要高频用别走这里。
  • get:offset 小于 0 抛 ArgumentOutOfRangeException;读失败或 offset 超出当前区长度抛 InvalidOperationException,消息形如「读过程数据失败 offset=N rc=错误码 服务端原因」。
  • set:offset 小于 0 或大于 65535 抛 ArgumentOutOfRangeException;写失败抛 InvalidOperationException,消息形如「写过程数据失败 offset=N rc=错误码 服务端原因」。
  • get 读 I 区、set 写 Q 区,一个属性两个方向,偏移语义各自独立(不是同一个区的读回)。

示例:

byte b = device[0];    // 读 I 区偏移 0
device[0] = 0x11; // 写 Q 区偏移 0

按地址读写(区域 + 字节 + 位)​

不想自己算偏移、想照着 TIA 里的地址写时用这几个口:区域字母 + 字节偏移 + 位。

device.ReadBool("I0.0", out bool done);       // 位
device.ReadInt16("IW2", out short recipe); // 字
device.ReadInt32("ID4", out int counter); // 双字

device.WriteBool("Q0.0", true);
device.WriteInt16("QW2", 123);
device.WriteInt32("QD4", 10000);
地址写法是什么
I0.0 / Q0.0位地址:第 0 字节的第 0 位(位号只能 0-7)
IB0 / QB0字节(1 字节)
IW2 / QW2字(2 字节)
ID4 / QD4双字(4 字节)

共用约定:

  • 不强制 2 / 4 对齐。 IW3 / ID2 都合法,偏移就是字节偏移 —— 和 GetFieldAddress 生成的未对齐地址自洽。
  • 不支持 M 区 / DB 区。
  • 读口是 PnSErrorCode + out 取值;写口返回 PnSErrorCode,不抛。
  • 地址格式错不抛异常,一律返回 InvalidArgument(内部解析器自己抛的 ArgumentException 被吃掉,换成错误码)。
  • 写口都是读-改-写:先读同地址的当前值,改掉目标字节 / 位,再整段写回。读不回来就直接返回那次的失败码,不会写一半。
  • ReadBool("Q0.0") 读的是 Q 区本地影子(最近写进去的值),不是线上回读。
写 I 侧:返回值 vs 抛异常,两套口径

整区 WriteArea 与地址化写(WriteBool / WriteInt16 / WriteInt32)遇到 I 侧不抛,返回 PnSErrorCode.NotSupported —— 记得判返回值,别以为没异常就写成功了。

只有元素口(In[i].Content = ... 以及 In[i] 上的各类型化赋值)才抛 InvalidOperationException。

ReadBool(address, out value)​

读一个位。

public PnSErrorCode ReadBool(string address, out bool value)

参数:

  • address (string) — 位地址,如 I0.0 / Q0.0;位号 0-7,0 = 最低位
  • value (out bool) — 读到的位值;失败时为 false

返回值:

  • PnSErrorCode — 成功是 Success

说明:

  • 地址解析不了(格式错 / M 区 / DB 区)→ InvalidArgument。
  • 地址是字节 / 字 / 双字(没有 .位号)→ InvalidArgument。
  • 未 Start → InvalidHandle;偏移超出当前区 → LengthMismatch;通道未就绪 → NativeError。
  • 位号超出 0-7 的地址(如 I0.8)在解析阶段就被拒 → InvalidArgument。
  • I 读的是 I 区 CPM;Q 读的是 Q 区本地影子(最近写入值)。
  • 不抛异常。要看原因读 LastServiceError。

示例:

if (device.ReadBool("I0.0", out bool done) == PnSErrorCode.Success && done)
device.WriteBool("Q0.0", true);

ReadInt16(address, out value) / ReadInt32(address, out value)​

读一个字 / 双字,大端解码。

public PnSErrorCode ReadInt16(string address, out short value)
public PnSErrorCode ReadInt32(string address, out int value)

参数:

  • address (string) — IW2 / QW2(16 位)、ID4 / QD4(32 位)
  • value (out short / out int) — 读到的值;失败时为 0

返回值:

  • PnSErrorCode — 成功是 Success

说明:

  • 宽度必须对得上:ReadInt16 要 2 字节地址,ReadInt32 要 4 字节地址。
    • 给了位地址(I0.0)→ InvalidArgument。
    • ReadInt16("IB0")(1 字节)、ReadInt16("ID0")(4 字节)→ InvalidArgument。
  • 地址解析不了(格式错 / M 区 / DB 区)→ InvalidArgument。
  • 未 Start → InvalidHandle;偏移加宽度超出当前区 → LengthMismatch;通道未就绪 → NativeError。
  • 偏移不要求对齐:IW3 读的是第 3、4 两个字节,大端拼成一个 short。
  • ReadInt32 返回 int(有符号),无符号场景自己按位解释。

示例:

device.ReadInt16("IW3", out short recipe);    // 未对齐也合法
device.ReadInt32("ID4", out int counter);

WriteBool(address, value) / WriteInt16(address, value) / WriteInt32(address, value)​

写一个位 / 字 / 双字,大端编码。

public PnSErrorCode WriteBool(string address, bool value)
public PnSErrorCode WriteInt16(string address, short value)
public PnSErrorCode WriteInt32(string address, int value)

参数:

  • address (string) — 位地址 Q0.0、字地址 QW2、双字地址 QD4
  • value — 要写入的值

返回值:

  • PnSErrorCode — Success 表示已经落共享区

说明:

  • 写 I 区不抛,返回 NotSupported(I 只读)。注意顺序:这三个口是读-改-写,未 Start 时前一步"读同地址现值"先失败,返回的是 InvalidHandle;NotSupported 只在已 Start 之后出现。
  • 写 Q 区都是读-改-写:先读同地址现值,只改这一段,再整段写回。读不回来就返回那次的码,不会写一半。
  • WriteBool 只动目标位,同字节其余位保持不变(不走 AsBool 那条"整字节 1 / 0"的路)。
  • 地址解析不了(格式错 / M 区 / DB 区)→ InvalidArgument;宽度对不上 → InvalidArgument;未 Start → InvalidHandle;偏移加宽度超出 Q 区 → LengthMismatch;提交失败 → NativeError。

示例:

device.WriteBool("Q0.0", true);       // 只改 Q 区第 0 字节的 bit0
device.WriteInt16("QW2", 123);
device.WriteInt32("QD4", 10000);

PnSAddress.Parse(address)​

上面那些口用的地址解析器,也可以自己拿去先解析、再校验。

public static PnSAddress Parse(string address)

返回值:

  • PnSAddress — 解析结果;成员有 Area / ByteOffset / BitNumber / DataType / DataLengthBytes / SpanBytes / IsBit / Original / DbNumber
public sealed class PnSAddress
{
public PnSAddressArea Area { get; } // Input(I 区)/ Output(Q 区)
public int ByteOffset { get; } // 字节偏移
public int BitNumber { get; } // 位号 0-7;仅 DataType 为 Bit 时有效
public PnSAddressDataType DataType { get; } // Bit / Byte / Word / DWord
public int DataLengthBytes { get; } // 位 = 0,字节 = 1,字 = 2,双字 = 4
public int SpanBytes { get; } // 访问跨度:位算 1 字节,其余同 DataLengthBytes
public bool IsBit { get; }
public int DbNumber { get; } // 保留字段;不支持 DB,恒为 0
public string Original { get; } // 原始地址字符串
public void ValidateRange(int imageLength); // 越界检查
}

说明: 这个口会抛异常(和 Read* / Write* 返回错误码的口径不同)。消息都是源码原文:

  • 地址为空 / 全空白:ArgumentException("地址不能为空")
  • DB 开头:ArgumentException("不支持 DB")
  • M 开头:ArgumentException("不支持 M 区")
  • 区域字母不是 I / Q / M:ArgumentException("不支持的地址区域: 原地址")
  • 位地址不成形(如 I0. / I.a):ArgumentException("地址格式非法 (位地址应为 I0.0 形式): 原地址")
  • 区域后面没有数字:ArgumentException("地址缺少偏移量: 原地址")
  • 偏移或位号不是纯数字 / 为负:ArgumentException("字节偏移非法: 原地址") 或 ArgumentException("位号非法: 原地址")
  • 位号超出 0-7(如 I0.8):ArgumentException("位号必须在 0-7 之间: 原地址")
  • 不带类型字符的地址(I0 / Q7)按字节处理,和 HSL 惯例一致;位地址必须写 .位号。

ValidateRange(imageLength) 另外抛 ArgumentOutOfRangeException,消息形如「地址 IW7 需要 2 字节, 超过过程映像长度 4」。

示例:

PnSAddress a = PnSAddress.Parse("IW3");
int off = a.ByteOffset; // 3,未对齐也照样解析
a.ValidateRange(device.ProcessData.Inputs.Length);

下标与索引空间​

字节下标​

过程映像用字节偏移当下标:ProcessData.In[i] / Out[i],i 就是字节地址,不是元素序号。偏移不强制 2 / 4 对齐,In[3] 和 IW3 指的是同一个位置。

映射的结构体从净页第 0 字节起排,所以「按偏移取元素」和「结构体字段」是两把尺子:结构体布局对上了,两者的偏移就一致(这也是 GetFieldAddress 能生成地址的原因)。

记录索引空间​

诊断侧的记录是另一个地址轴,和过程映像的字节偏移没关系。它分两段,走两个不同的入口:

  • 用户区 0x0000-0x7FFF —— device.Records(GET /api/records)。控制器写下来的用户记录都在这一段里。字段与用法见诊断。
  • I&M 0xAFF0-0xAFF4 —— device.ImData(GET /api/im)。设备标识记录,由协议栈自己管理,不进用户区记录表。

所以在 Records 里翻 0xAFF0 是找不到的:用户区记录表只覆盖 0x0000-0x7FFF,I&M 走 ImData 这条路。两个端点别当成一张表。

字节数怎么拿​

长度由运行配置决定,不要写死 20 / 32(改过组态就变)。没有 InputsByteCount / OutputsByteCount 这类口。

想拿走哪
I 侧整段长度device.ProcessData.Inputs.Length
Q 侧整段长度device.ProcessData.Outputs.Length
两个方向的配置长度device.DiagSnapshot.InputAreaLength / OutputAreaLength(uint)
三个长度不是同一个数
  • Inputs 的快照已剥掉开头的 IOPS 前缀(净荷从下标 0 起);Inputs.Length 仍按整区长度算,前缀字节计入长度、尾部补 0。
  • Outputs.Length = 输出区长度 减 3(净荷窗)。
  • 结构体映射能用的窗口又比 Inputs.Length 小 2-6 字节:起点是每次映射时扫出来的(先剥 2 个 IOPS 前缀,再看净页开头连续有几个 0x80,最多再剥 4 个)。
  • ReadArea(Input) 返回的长度等于 Inputs.Length;WriteArea(Output, data) 要求的长度等于 Outputs.Length。

所以「映射放得下」和「Inputs.Length 够大」不是一回事 —— 结构体大小要按映射窗口算,别照着 Inputs.Length 卡。拿不准就让映射自己报错:超了会抛 InvalidOperationException("结构体大于输入过程映像")。