跳到主要内容

过程数据

过程数据就是每周期和控制器交换的那段 I/Q 字节。把 I/Q 映射成结构体之后,视图直接钉在过程映像上 —— 这就是本页要讲的 0 拷贝:映射本身不复制数据,也不为整帧另建缓冲区。

方向只有两个:写 Q(设备 → 控制器)、读 I(控制器 → 设备)。I 区由控制器每周期整段覆盖,所以它只读,写它无效。

总线周期由服务进程完成。SDK 不自建周期循环,也没有用户回调 —— 调度(循环 / 定时器)由你自己定。

过程数据只有共享区这一条通道:所有读写口落的都是共享区里的过程映像,通道进不去就如实抛 (过程数据未就绪 那几条)。

设备是固定的单模块:槽 1 / 子槽 1。本页所有入口都不收槽位参数,也不用收。

方向别绕晕

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

20 / 32 只是出厂组态的常见尺寸,别写死。长度以运行时为准,见 长度怎么拿

0 拷贝有两种形态

一是整幅映像的映射inputsMapping / outputsMapping(结构体,或 ByteBuffer)。 二是直接拿净荷指针tryGetLiveIoPayload 把 I / Q 净荷的 Pointer 与字节数交给你,指针就钉在共享区上。

按偏移取的元素(in().get(i) 这一族)是拷贝。每次取值都把那段字节从过程映像重新读出来再解。

使用案例

日常使用

① 映射到类(0 拷贝,持续用):结构体定义好,映射一次,之后当变量用。

import com.sun.jna.Structure;
import com.sun.jna.Structure.FieldOrder;

@FieldOrder({"present", "recipeHi", "recipeLo", "flag"})
public static class FromPlc extends Structure { // I:控制器 → 设备
public byte present; // 偏移 0
public byte recipeHi; // 偏移 1
public byte recipeLo; // 偏移 2
public byte flag; // 偏移 3
public FromPlc() { super(ALIGN_NONE); } // ALIGN_NONE = Pack 1,必须
}

@FieldOrder({"ready", "busy"})
public static class ToPlc extends Structure { // Q:设备 → 控制器
public byte ready;
public byte busy;
public ToPlc() { super(ALIGN_NONE); }
}
import com.darra.pns.service.PnSService;

try (PnSService device = new PnSService()) {
device.start();

FromPlc inp = device.processData().inputsMapping(FromPlc.class); // 映射一次
ToPlc outp = device.processData().outputsMapping(ToPlc.class);

inp.read(); // 过程映像 → 托管缓存:读到的是当前周期
byte present = inp.present;
outp.ready = (byte) (present + 1);
outp.write(); // 托管缓存 → 过程映像:净荷字节当场就在共享区
}

字段是托管缓存:映射时已自动 read() 一次,之后读最新要 read()、改完要 write()

② 单次读取(一次性取):不建映射,整段字节一次读出,或按偏移取一个值。

byte[] i = device.processData().inputs();                    // I 整段(一次读出,同一拍自洽)
short recipe = device.processData().in().get(2).asInt16(); // 偏移 2 的大端 i16

字符串字段在过程映像里是定长字节,从同一次读出的字节里解:

byte[] i = device.processData().inputs();
String partNo = new String(i, 4, 16, java.nio.charset.StandardCharsets.US_ASCII).trim(); // 偏移 4 起 16 字节

③ 按地址读写(区域 + 字节 + 位):不想自己数偏移,就照工程软件里的地址写。

boolean done  = device.readBool("I0.0");        // 位
short recipe = device.readInt16("IW2"); // 字
int counter = device.readInt32("ID4"); // 双字

device.writeBool("Q0.0", true);
device.writeInt16("QW2", (short) 123);
device.writeInt32("QD4", 10000);

周期变化

同一份映射放进你自己的循环直接用(映射只取一次,服务每周期搬运)。

FromPlc inp = device.processData().inputsMapping(FromPlc.class);
ToPlc outp = device.processData().outputsMapping(ToPlc.class);

while (running) { // 你的周期任务,节拍自定
inp.read(); // 当前周期 → 托管缓存
outp.ready = (byte) (inp.present + 1);
outp.write(); // 下个周期发出
Thread.sleep(10); // 抛 InterruptedException:所在方法要么声明 throws,要么自己 catch
}

循环里别再重新映射:每次映射都要重新解析一次过程映像起点(见 inputsMapping 里那段「起点会漂」的提醒)。省下这一次解析,代价是偏移一旦漂移就是静默读错位

事件流(events())报的是连接 / 状态 / 链路 / IO 质量这类运行态;净荷的变化按你自己的节拍去取 (read() / getIo() / 映射视图)。

结构体映射(零拷贝)

进 OP 后把 packed Structure overlay 到过程映像净荷上:视图钉在共享区上(0 拷贝,不是快照)。 close() / 重新映射后旧视图作废,要重取。

Structure 侧的两个硬性要求,不满足就映射不出来或读到脏数据:

@FieldOrder({"statusWord", "flag"})            // 必须显式列出全部字段,且顺序 = 过程映像里的字节顺序
public static class ServoIn extends Structure {
public short statusWord; // 多字节字段是**线上字节**,按宿主序读会错
public byte flag;
public ServoIn() { super(ALIGN_NONE); } // Pack 1,必须
}
  • 字段只放值类型,按声明顺序紧排(无填充)。位 / 布尔字段用 byte(JNA 把 boolean 字段当 4 字节 native int)。
  • ALIGN_NONE 关掉 JNA 的自动对齐填充。忘了它,字段位置就整体错开。
  • 结构体不合规(缺 @FieldOrder、字段与声明不符)由 JNA 自己抛,本 SDK 不额外校验。

inputsMapping(Class<T> type)

public <T extends Structure> T inputsMapping(Class<T> type)   // ProcessData

把 I 区净荷钉成 JNA Structure 视图并返回。0 拷贝,只读方向。

返回值:

  • T — 一个 Structure 子类实例,指针直接指向共享区 I 净荷;映射时已自动 read() 一次,字段里有初值

说明:

  • typenullNullPointerException,消息 type 不能为 null
  • 会话未建立(没 start() / connect())、共享区未映射、净荷指针为空、净荷长度 ≤ 0 → IllegalStateException,消息 过程数据输入映像不可用
  • 结构体 size() <= 0,或大于 I 净荷字节数 → IllegalStateException,消息 结构体大于输入过程映像
  • 映射的是线上字节(网络大端)。把 short / int 字段当宿主数值用会读到错值 —— 要宿主数值请走 readStruct,或改用元素口(它自带大端解码)。
  • 字段是托管缓存:读最新值要 inputs.read()。不 read() 就一直是你上次读到的旧值。
  • 视图指向的是 native 内存,I 区只读是协议约定,不是内存保护 —— 对输入视图调 write() 不会被拦,但会把字节写进共享区输入区,之后读回来的就不是控制器的值了。要写就写 Q。
  • 每次调用重新解析一次净荷起点与长度。close() / unmap 之后旧视图的指针失效,必须重取。
  • 映射只从净荷第 0 字节起,没有偏移参数。要映射中段,在 @FieldOrder 里最前面放一个占位字段把它顶开 (public byte[] _skip = new byte[4];,JNA 认这种定长数组声明);或者改用 readArea 按偏移取字节。

示例:

FromPlc inp = device.processData().inputsMapping(FromPlc.class);
inp.read();
byte present = inp.present;
映射视图的起点不是一个常量

I 侧净荷起点是映射那一刻按当前区内容算出来的:先跳过槽头的 2 个 IOPS 前缀,再看净荷开头连续有几个 0x80,最多再跳 4 个。也就是说起点在 2–6 字节之间随数据内容浮动,不是写死的偏移。

由此两条:

  • 能用的窗口比整个输入区小 2–6 字节。结构体尺寸只跟窗口比,所以尺寸门拦不住起点漂移。
  • 映射放在循环外取一次仍然是推荐用法,但换来的是少一次解析。代价是偏移一旦漂移就是静默读错位 —— 不抛异常,读出来的值是错位的。数据里可能连续出现 0x80 时,请每次重新映射,或改用 readStruct / readArea

Q 侧没有这个问题:Q 净荷起点固定,不扫描。

outputsMapping(Class<T> type)

public <T extends Structure> T outputsMapping(Class<T> type)   // ProcessData

把 Q 区净荷钉成 JNA Structure 视图并返回。0 拷贝,这是可写方向。

返回值:

  • T — 视图实例,指针直接指向共享区 Q 净荷;映射时已自动 read() 一次

说明:

  • typenullNullPointerException,消息 type 不能为 null
  • 会话未建立 / 共享区未映射 / 净荷指针为空 / 净荷长度 ≤ 0 → IllegalStateException,消息 过程数据输出映像不可用
  • 结构体 size() <= 0,或大于 Q 净荷字节数 → IllegalStateException,消息 结构体大于输出过程映像
  • 改完字段必须 outputs.write(),否则只在 JNA 的托管缓存里,过程映像不知道。
  • write() 写的是净荷字节本身,写完即落在共享区。它不走 SDK 那套提交协议(先数据、release fence、再置 OutputDirty)—— 那套在内置的整区 / 元素写口上由 SDK 完成。周期热路径要的就是这种"直接改净荷"。
  • 多字节字段同样是线上字节,理由见上面 @FieldOrder 那段。
  • 视图生命周期与 I 侧一致:close() 后可重取,不要留旧引用。
  • 映射同样只从净荷第 0 字节起,要中段见上面 inputsMapping 那条占位字段的写法。

示例:

ToPlc outp = device.processData().outputsMapping(ToPlc.class);
outp.ready = 1;
outp.busy = 0;
outp.write();

inputsMapping() / outputsMapping()

public ByteBuffer inputsMapping()    // ProcessData:I 净荷,只读
public ByteBuffer outputsMapping() // ProcessData:Q 净荷,可写

同一块过程映像的 ByteBuffer 形态,同样是 0 拷贝。不想写 Structure 子类、或者要自己按字节搬,用这两个。

返回值:

  • ByteBuffer — 直接包在共享区净荷上的 DirectByteBuffer(不是 allocateDirect),字节序已设为 BIG_ENDIAN

说明:

  • 会话不可用 → IllegalStateException,消息 过程数据输入映像不可用 / 过程数据输出映像不可用
  • I 侧返回的是 asReadOnlyBuffer() 的结果:往它写会抛 ReadOnlyBufferException
  • Q 侧可写。改缓冲里的字节就是改进程映像,不需要额外提交调用。
  • 字节序已经是网络大端。不要把 order() 改成 LITTLE_ENDIAN —— 过程数据在线上就是大端。
  • 缓冲区指向 native 内存,close() / unmap 后失效。
  • 位置 / 上限是你自己的事:SDK 每次返回一个新缓冲,position 从 0 起。

示例:

java.nio.ByteBuffer q = device.processData().outputsMapping();
q.putShort(2, (short) 123); // 偏移 2 写大端 i16
int first = device.processData().inputsMapping().get(0) & 0xFF;

tryGetLiveIoPayload(boolean output, Pointer[] pointer, int[] byteCount)

public boolean tryGetLiveIoPayload(boolean output, Pointer[] pointer, int[] byteCount)   // PnSService

把 I / Q 净荷的活指针交给你:pointer[0] 直接指向共享区里的净荷,同样是 0 拷贝,一个字节都不拷。 output = true 取 Q(设备 → 控制器),false 取 I(控制器 → 设备)。

返回值:

  • booleantruepointer[0] 是净荷首地址、byteCount[0] 是净荷字节数;falsepointer[0] = nullbyteCount[0] = 0

说明:

  • 不抛:两个数组为 null 或长度不足、会话未建立、共享区未映射、拿不到净荷(指针为空 / 净荷长度 ≤ 0) 一律返回 false,由调用方看返回值决定下一步。
  • 指针指向的是活的共享区内存,不是快照 —— 读到的是当前周期的字节。
  • 净荷窗口口径与映射完全一致:I 侧起点每次调用按数据内容扫描得出(2–6 字节浮动,见 inputsMapping 的提醒),Q 侧起点固定;byteCount[0] 就是这次算出来的净荷长度。
  • close() / unmap 之后指针失效,别再拿旧指针读写。
  • 自己按字节搬时记住:线上字节是大端,多字节字段按大端解(口径与取值族一致)。

示例:

com.sun.jna.Pointer[] p = new com.sun.jna.Pointer[1];
int[] n = new int[1];

if (device.tryGetLiveIoPayload(false, p, n)) { // I 净荷
byte first = p[0].getByte(0);
int len = n[0];
}

readStruct(byte[] dest) / readStruct(int size)

public int     readStruct(byte[] dest)   // ProcessData:I 前缀 → 调用方缓冲
public byte[] readStruct(int size) // ProcessData:I 前缀 → 新数组

快照,不是视图:拷出来的字节不会跟着总线再变。I 区只读方向。

返回值:

  • readStruct(byte[])int,实际拷贝字节数(= dest.length
  • readStruct(int)byte[],新分配的副本,长度 = size

说明:

  • destnullNullPointerException,消息 dest 不能为 null
  • size < 0IllegalArgumentException,消息 size 不能为负: N
  • dest.length == 0 / size == 0 → 返回 0 / 空数组,不读过程映像。
  • I 区字节数不足 → PnSServiceException,消息 I 区 21 字节不足结构体 24 字节(数字是实际值)。
  • 从 I 净荷第 0 字节起拷,没有偏移参数。要中段就用 readArea
  • 拷出来的是大端原样字节,不做字节序转换 —— 它只保证"这是一份同一拍的自洽切片"。

示例:

byte[] packed = device.processData().readStruct(4);       // I 前 4 字节
short recipe = (short) (((packed[1] & 0xFF) << 8) | (packed[2] & 0xFF));

readStruct(T dest)

public <T extends Packed> T readStruct(T dest)   // ProcessData

I 区前缀按大端解包进你的 Packed 结构体,返回 dest 便于链式。快照

返回值:

  • T — 传入的 dest 本身,字段已填好

说明:

  • destnullNullPointerException,消息 dest 不能为 null
  • dest.packedSize() < 0IllegalArgumentException,消息 packedSize 不能为负: N
  • I 区字节数不足 packedSize()PnSServiceException,消息同上一条 I 区 N 字节不足结构体 M 字节
  • 解包用的 ByteBuffer 已设成 BIG_ENDIANposition = 0、容量 = packedSize()。你在 unpack 里直接按大端读就行。
  • 得到的是快照:以后 I 区怎么变都跟它无关,要新值就再调一次。

示例:

PackedRecipe r = device.processData().readStruct(new PackedRecipe());
if (r.recipe > 1000) startWash();

writeStruct(byte[] packed) / writeStruct(Packed src)

public void writeStruct(byte[] packed)   // ProcessData:大端 packed 字节 → Q
public void writeStruct(Packed src) // ProcessData:结构体 → Q(大端打包后提交)

快照提交(拷贝路径),写 Q 方向。结构体走的是 SDK 的整区提交协议。

返回值:

  • 无(void)。正常返回 ⟺ 字节已进 Q 区。

说明:

  • packednullNullPointerException,消息 packed 不能为 null
  • srcnullNullPointerException,消息 src 不能为 null
  • src.packedSize() < 0IllegalArgumentException,消息 packedSize 不能为负: N
  • 空数组 / packedSize() == 0 → 不写,直接返回(不是错误)。
  • 数据长度 + 起始偏移 0 超出 Q 净荷 → PnSServiceException,消息 区域越界: 偏移 0 + 长度 M 超出过程映像区 K 字节
  • 只覆盖packedSize() 字节,其余字节保持原值。这是部分覆盖写,不是整帧替换。
  • 会话未建立 → PnSServiceException,消息 会话未建立 (请先 start() 或 WebAPI connect)
  • 从 I 区拷出来的字节喂给 writeStruct(byte[]) 是合法的 —— 两边都是大端原样字节。

示例:

byte[] frame = device.processData().readStruct(4);       // I 快照(大端字节)
device.processData().writeStruct(frame); // 原样搬去 Q

PackedRecipe r = new PackedRecipe();
r.recipe = 500;
device.processData().writeStruct(r); // 打包成大端后提交

Packed(结构体契约)

public interface Packed {
int packedSize(); // 打包后字节数(无对齐填充)
void pack(java.nio.ByteBuffer buf); // 结构体 → 大端缓冲(从 position 0 写 packedSize() 字节)
void unpack(java.nio.ByteBuffer buf); // 大端缓冲 → 结构体
}

readStruct / writeStruct 认的就是这个接口:不是 Structure 子类,就实现它。手工 pack / unpack,没有反射, 所以字节布局完全由你说了算 —— 大端也由你按契约写。

返回值:

  • packedSize()intpack / unpack 都读这么多字节
  • pack / unpack → 无

说明:

  • 传进来的 ByteBuffer 已是 BIG_ENDIANposition = 0、容量 = packedSize()
  • packedSize() 返回负数会被上层拒掉(packedSize 不能为负: N),别拿它当"未知"。
  • 自己按大端读写(buf.getShort() / buf.putInt() 配合已设好的字节序即可),不要在这里翻转字节序 —— 上层已经设过一次。
  • 本接口只管字节布局,不持有过程映像,也没有生命周期问题。

示例:

public static class PackedRecipe implements com.darra.pns.ProcessData.Packed {
public byte flag;
public short recipe;

@Override public int packedSize() { return 3; }
@Override public void pack(java.nio.ByteBuffer buf) { buf.put(flag).putShort(recipe); }
@Override public void unpack(java.nio.ByteBuffer buf) { flag = buf.get(); recipe = buf.getShort(); }
}

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

不建映射也能按偏移取用到的几个字段:pd.in().get(i) 读,pd.out().get(i) 写。这一族的好处是不必预先声明整段布局。

元素口是拷贝,不是视图

in().get(2).asInt16() 每次取值都是一份新的快照切片:先从共享区把整幅过程映像读回来,再切出那 2 字节解释成值。 写入同理。

所以它不是"下标定位拿到的活视图":get(i) 定的只是偏移,值仍然是那一瞬间的快照。 0 拷贝只有两条路 —— 整幅映像的映射(inputsMapping / outputsMapping),或直接拿净荷指针(tryGetLiveIoPayload)。

in() / out()

public DataArray in()    // ProcessData:I 数组,只读
public DataArray out() // ProcessData:Q 数组,可写

拿到两个方向的元素数组,之后按字节偏移取项。

返回值:

  • DataArray — 轻量包装(持有服务 + 方向),自身不持有数据,随便缓存

说明:

  • 不抛:两个方法只返回包装对象。真正的失败(会话未建立、过程映像不可用)在读 / 写那一刻才抛。
  • in() 只读,out() 可写。
  • device.processData() 每次调用返回新的 ProcessData(同一个会话),里面的 in() / out() 也是新的 包装。功能上等价,不必刻意缓存。

示例:

var pd = device.processData();
byte b = pd.in().get(0).getContent();

DataArray.length()

public int length()   // ProcessData.DataArray

本方向过程映像的净荷字节数。

返回值:

  • int — I 侧 = 输入净荷长度;Q 侧 = 输出净荷长度

说明:

  • 这个长度是每调用一次就去问一次运行态,不是缓存的常量。
  • 会话未建立 → PnSServiceException,消息 会话未建立 (请先 start() 或 WebAPI connect)
  • 共享区未映射 → PnSServiceException,消息 过程数据未就绪, 无法读写 IO
  • 它没法"安静地返回 0":拿不到过程映像时它抛,不是回 0。只有该方向净荷本身就是空的时候才是 0。
  • 长度只跟净荷算,不含槽头前缀。所以它比 diagSnapshot 里那个区域长度小几个字节,属正常。

示例:

int iLen = device.processData().in().length();
int qLen = device.processData().out().length();

DataArray.get(int offset)

public DataItem get(int offset)   // ProcessData.DataArray

按字节偏移取一个元素视图。

返回值:

  • DataItem — 只记住了服务、偏移和方向;不存数据字节,也不做任何过程映像访问

说明:

  • offset < 0IndexOutOfBoundsException,消息 偏移不能为负: N
  • 越界不在这里报get 只查负号。偏移 + 宽度是否超界,在真正读 / 写那一刻由服务侧报 区域越界: 偏移 N + 长度 M 超出过程映像区 K 字节PnSServiceException)。
  • 偏移是过程映像净荷的字节地址,不是槽头地址,也不强制 2 / 4 对齐 —— get(3).asInt32() 合法。
  • 每次调用返回新对象(没有按偏移缓存)。别拿对象身份当缓存键;稳态下每取一次会新建小对象。
  • 取到元素本身不读过程映像,所以这一步不抛服务侧异常。

示例:

var in = device.processData().in();
short recipe = in.get(2).asInt16();

DataArray.set(int offset, byte value)

public void set(int offset, byte value)   // ProcessData.DataArray

按字节偏移写一个字节。等价于 get(offset).setContent(value)。仅 Q 侧。

返回值:

  • 无(void)。正常返回 ⟺ 字节已提交。

说明:

  • offset < 0IndexOutOfBoundsException,消息 偏移不能为负: N
  • in() 返回的数组调用 → UnsupportedOperationException,消息 In 只读 (控制器→设备); 写请用 out().get(i).setContent
  • 偏移超界 → PnSServiceException,消息 区域越界: 偏移 N + 长度 1 超出过程映像区 K 字节
  • 会话未建立 → PnSServiceException,消息 会话未建立 (请先 start() 或 WebAPI connect)
  • 写的是整个字节,不做位操作。位改成 setBit

示例:

device.processData().out().set(0, (byte) 0x11);

DataItem.offset()

public int offset()   // ProcessData.DataItem

本元素当初 get(i) 时的字节偏移,原样返回。

返回值:

  • int — 非负偏移

说明:

  • 纯读字段,不抛,不访问过程映像。
  • 偏移是取元素那一刻记下的。过程映像长度变了,这个数不会跟着变。

示例:

var it = device.processData().out().get(6);
System.out.println("写到偏移 " + it.offset());

取值族(读)

In / Out 都能读:In 读控制器给的输入,Out 读 Q 影子(读回你刚写下的值,写后读一致)。 多字节一律大端(PROFINET 网络序)。

public byte    getContent()               // 1 字节原始值
public byte asInt8() // 有符号 8 位
public int asUInt8() // 无符号 8 位,返回 0..255
public short asInt16() // 大端 i16
public int asUInt16() // 大端 u16,返回 0..65535
public int asInt32() // 大端 i32
public long asUInt32() // 大端 u32,返回 0..2^32-1(用 long 装)
public long asInt64() // 大端 i64(8 字节)
public long asUInt64() // 大端 u64,同比特 long
public float asFloat() // 大端 IEEE754 float32(4 字节)
public double asDouble() // 大端 IEEE754 float64(8 字节)
public boolean asBool() // 整字节,bit0 语义
public boolean getBit(int bit) // 本字节第 bit 位,0 = LSB

相关结构:

// 读 = 从过程映像切一小段(拷贝)后按大端解码
// 偏移(字节) 方法 宽度 符号 / 返回类型 字节序
// +0 getContent 1 原始 byte ——
// +0 asInt8 1 有符号 → byte ——
// +0 asUInt8 1 无符号 → int (0..255) ——
// +0 asBool 1 整字节,bit0 为真 → boolean ——
// +0 getBit(n) 1 位 n(0..7,0 = LSB)→ boolean ——
// +0..+1 asInt16 2 有符号 → short 大端
// +0..+1 asUInt16 2 无符号 → int (0..65535) 大端
// +0..+3 asInt32 4 有符号 → int 大端
// +0..+3 asUInt32 4 无符号 → long 大端
// +0..+3 asFloat 4 IEEE754 → float 大端
// +0..+7 asInt64 8 有符号 → long 大端
// +0..+7 asUInt64 8 无符号同比特 → long 大端
// +0..+7 asDouble 8 IEEE754 → double 大端

说明:

  • 偏移 + 宽度超出本方向净荷 → PnSServiceException,消息 区域越界: 偏移 N + 长度 M 超出过程映像区 K 字节
  • 会话未建立 / 共享区未映射 → PnSServiceException,消息 会话未建立 (请先 start() 或 WebAPI connect) / 过程数据未就绪, 无法读写 IO
  • 每次取值都是一次独立的过程映像访问:同一表达式里的两次取值不保证同一拍。要同一拍就一次取够 (read() / inputs() / readArea / readStruct),两个方向一起要就 getIo()
  • 单次多字节取值内部不会撕裂:先切出一段字节,再整体解码。
  • asBool()bit0 语义:只有最低位为 1 才是 true —— 0x01true0x02false0x80false。不是"非 0 即真"。要按整字节 0 / 非 0 判真假,读 getContent() 自己比较。
  • 无符号值一律挑 asUInt*:线值 0xFFFF 读出来是 65535,不会变成负数。
  • 大端解码在 SDK 内部完成,不依赖 JVM 或平台的默认字节序。

示例:

var in = device.processData().in();
byte raw = in.get(0).getContent();
short sw = in.get(0).asInt16();
int pos = in.get(2).asInt32();
float vel = in.get(6).asFloat();
boolean rdy = in.get(0).getBit(2); // 偏移 0 的 bit2
boolean en = in.get(10).asBool(); // 偏移 10 的 bit0

赋值族(写)

只有 out()(Q 侧)可写。写下去的值在写的那一刻就落进共享区净荷。

public void setContent(byte value)               // 1 字节,整字节覆盖
public void asInt8(byte value)
public void asUInt8(int value) // 只取低 8 位
public void asInt16(short value)
public void asUInt16(int value) // 只取低 16 位
public void asInt32(int value)
public void asUInt32(long value) // 只取低 32 位
public void asInt64(long value)
public void asUInt64(long value)
public void asFloat(float value)
public void asDouble(double value)
public void asBool(boolean value) // 整字节:true → 0x01,false → 0x00
public void setBit(int bit, boolean value) // 只改该位,同字节其余位不动

相关结构:

// 写 = 按宽度大端编码(拷贝)后写回过程映像
// 偏移(字节) 方法 宽度 入参类型 取位宽方式 / 字节序
// +0 setContent 1 byte 整字节覆盖
// +0 asInt8 1 byte 整字节覆盖
// +0 asUInt8 1 int 只取低 8 位
// +0 asInt16 2 short 大端
// +0..+1 asUInt16 2 int 只取低 16 位,大端
// +0..+3 asInt32 4 int 大端
// +0..+3 asUInt32 4 long 只取低 32 位,大端
// +0..+3 asFloat 4 float 大端,位型原样(NaN 载荷不清洗)
// +0..+7 asInt64 8 long 大端
// +0..+7 asUInt64 8 long 大端
// +0..+7 asDouble 8 double 大端,位型原样
// +0 asBool 1 boolean true → 0x01 / false → 0x00(写整字节)
// +0 setBit(bit, v) 1 int, boolean 读改写:同字节其余位保持原值

说明:

  • in() 的元素写 → UnsupportedOperationException,消息 In 只读 (控制器→设备); 写请用 out().get(i).setContent。I 区由控制器每周期覆盖,写它没意义,所以类型上直接拒。
  • 偏移 + 宽度超界 → PnSServiceException,消息 区域越界: 偏移 N + 长度 M 超出过程映像区 K 字节
  • 会话未建立 / 共享区未映射 → PnSServiceException,消息 会话未建立 (请先 start() 或 WebAPI connect) / 过程数据未就绪, 无法读写 IO; 提交阶段拿不到共享区则报 过程数据未就绪, 无法提交 IO
  • 不抛范围错asUInt8(300)asUInt16(70000) 这类超宽值按位宽取模后写下去,不报错也不截断提示。 自己要范围校验就自己校验。
  • asBool(boolean) 写整个字节true0x01false0x00,非规范字节不保留。
  • setBit 是位域读改写:只改那一位,同字节其余位保持原值。
  • 写口本身没有"提交后再等一拍"的步骤 —— 落共享区就完了,什么时候发出去是总线周期的事。

示例:

var out = device.processData().out();
out.get(6).asFloat(12.5f); // 偏移 6..9 写大端 f32
out.get(0).asInt16((short) 0x000F); // 偏移 0..1 写大端 i16
out.get(4).setBit(3, true); // 只置偏移 4 的 bit3

getBit(int bit) / setBit(int bit, boolean value)

public boolean getBit(int bit)                      // ProcessData.DataItem,In / Out 都可读
public void setBit(int bit, boolean value) // ProcessData.DataItem,仅 Out

按位存取当前偏移这一个字节里的某一位。位号 0-70 = 最低位。

返回值:

  • getBitboolean,该位是否为 1

说明:

  • bit < 0 || bit > 7IllegalArgumentException,消息 位索引必须在 0-7 之间: N
  • 对 In 元素调 setBitUnsupportedOperationException(同一个"只读"消息)。 注意顺序:先检查方向、后检查位号 —— In 上写 setBit(9, true) 报的是只读,不是位号越界。
  • setBit 是读改写:先读该字节,改一位,再写回整字节。中间不会有别的字节被动过。
  • 位寻址的偏移仍是字节偏移,所以 I0.0 就是 get(0).getBit(0)

示例:

boolean ready = device.processData().in().get(0).getBit(0);
device.processData().out().get(0).setBit(0, true); // 只置 bit0

asValue(PnSDataType type) / writeValue(PnSDataType type, Object value)

public Object asValue(PnSDataType type)                          // ProcessData.DataItem 读
public void writeValue(PnSDataType type, Object value) // ProcessData.DataItem 写,仅 Out

按类型枚举取 / 赋装箱值,省掉自己挑方法名。等价于手写对应的 asInt16() / asFloat() 那一族。

返回值:

  • asValueObject:装箱值,具体类型随 type —— INTEGER8ByteINTEGER16ShortINTEGER32IntegerINTEGER64LongUNSIGNED8 / UNSIGNED16(含 BYTE / WORD)走 IntegerUNSIGNED32 / UNSIGNED64(含 DWORD / LWORD)走 Long;浮点走 Float / DoubleBOOLBoolean
  • 窄类型不会升位:asValue(INTEGER8 / INTEGER16) 拿到的是 Byte / Short, 写成 (Integer) v 会抛 ClassCastException;要 Integer 请按 UNSIGNED16 / INTEGER32

说明:

  • typenullNullPointerException,消息 type 不能为 null
  • writeValuevaluenullNullPointerException,消息 value 不能为 null
  • PnSDataType 的 15 个取值全部支持BOOL / INTEGER8·16·32·64 / UNSIGNED8·16·32·64 / REAL32 / REAL64 / BYTE·WORD·DWORD·LWORDBYTE / WORD / DWORD / LWORDUNSIGNED8 / 16 / 32 / 64 的别名(同宽度、同大端)。
  • writeValue(BOOL, ...) 接受 Boolean,也接受任何 Number(非 0 即真)。其它类型传非 Number 的值 → ClassCastExceptionBOOL 传既不是 Boolean 也不是 Number 的值 → IllegalArgumentException,消息 无法转为布尔: java.lang.String 这种形状。
  • 先转值、后判断方向:往 In 元素上 writeValue 且值本身非法时,抛的是转换异常,不是只读异常。 值合法才轮到 UnsupportedOperationException(同一个只读消息)。
  • 数值走 Java 的窄化转换:writeValue(INTEGER8, 300) 写下去的是 44,不报错。范围自己管。
  • 宽度 / 越界 / 会话类失败与赋值族完全一致(区域越界会话未建立过程数据未就绪 那几条)。

示例:

import com.darra.pns.enums.PnSDataType;

var in = device.processData().in();
var out = device.processData().out();

Object v = in.get(2).asValue(PnSDataType.UNSIGNED16); // 装箱的 Integer
out.get(2).writeValue(PnSDataType.WORD, 123); // WORD 就是 UNSIGNED16
out.get(4).writeValue(PnSDataType.REAL32, 12.5f);

元素口的代价与失败形态

读一次标量的真实开销是可以数的:两份整幅拷贝(I 净荷一份、Q 净荷一份)+ 你要的那几个字节。 写一次还要再加一份脏区拷贝和一次共享区写入。原因是元素口每次访问都会重新取一遍完整过程映像。

所以它适合单点或少量字段的改动,不适合放进 1 ms 周期热路径。整帧 / 批量 / 周期热路径请用 结构体映射read()

异常三类刻意分开报,方便定位是选错偏移、选错方向还是状态没就绪:

  • 偏移为负(get(i) / DataArray.set)→ IndexOutOfBoundsException
  • 偏移 + 宽度超出本方向净荷 → PnSServiceException(消息带 区域越界);
  • 写只读侧 → UnsupportedOperationException(元素口)/ PnSServiceExceptionsvc.in(offset) 那一族);
  • 会话未就绪 → PnSServiceException会话未建立 / 过程数据未就绪)。

线程安全:元素口本身不加锁,且写路径会改会话级的共享缓冲。同一个服务对象不要多线程并发读写过程数据, 要并发就自己加锁,或者用整幅映射 + 自己的调度。

服务侧的同名元素口

PnSService 上也有一套按偏移取元素的入口,与 processData().in()/out()同一份实现,只是异常口径不同:

public ProcessDataItem in(int offset)    // PnSService:只读 I
public ProcessDataItem out(int offset) // PnSService:写 Q

返回值:

  • ProcessDataItem — 元素视图,同样不持有数据字节

说明:

  • offset < 0PnSServiceException,消息 偏移不能为负: N(注意与 pd.in().get(i)IndexOutOfBoundsException 不同)。
  • 写只读侧 → PnSServiceException,消息 In 只读 I (控制器→设备); 写请用 out(offset)
  • 位号越界 → PnSServiceException,消息 位索引必须在 0-7 之间: N
  • 标量方法名与 DataItem 一致(getContent / setContent / asInt8asDouble / asBool / getBit / setBit),但没有 offset(),也没有 asValue / writeValue
  • 大端、宽度、越界口径与元素口完全相同。

示例:

short recipe = device.in(2).asInt16();
device.out(2).asInt16((short) 123);

整区与整帧(拷贝)

不建映射也能整段读写。下面这些都是拷贝,不是视图。

read() / inputs()

public byte[] read()      // PnSService:I 整段
public byte[] inputs() // ProcessData:I 整段(内部就是 read())

一次读出整段 I,同一拍自洽。这是"要一份不跟着变的快照"的标准做法。

返回值:

  • byte[] — I 净荷的完整副本,长度 = in().length();底层为空映像时返回 0 长度数组,不返回 null

说明:

  • 会话未建立 → PnSServiceException,消息 会话未建立 (请先 start() 或 WebAPI connect)
  • 共享区未映射 → PnSServiceException,消息 过程数据未就绪, 无法读写 IO
  • 拿到的是副本,之后过程映像怎么变都跟它无关。
  • 底层一次会同时取回 Q + I 两份,即使你只要 I(那次调用就是 getIo())。单次调用无所谓,别放进热路径反复调。
  • 要按偏移截取用 readArea, 要偏移 0 起的定长前缀用 readStruct

示例:

byte[] i = device.read();
System.out.println("I 区 " + i.length + " 字节");

getIo()

public byte[][] getIo()   // PnSService:一次取回 Q + I

一次调用同时取回两个方向的拷贝:[0] 是 Q 影子(设备 → 控制器),[1] 是 I(控制器 → 设备)。 想成对看 I 和 Q 时用它,不必为每个方向各调一次。

返回值:

  • byte[][][0] = Q 影子;[1] = I。底层为空映像时是 0 长度数组,不返回 null

说明:

  • 会话未建立 → PnSServiceException,消息 会话未建立 (请先 start() 或 WebAPI connect)
  • 共享区未映射 → PnSServiceException,消息 过程数据未就绪, 无法读写 IO
  • 两份都是拷贝:拿到之后过程映像怎么变都跟它们无关。要钉在共享区上的活视图,用 inputsMapping / outputsMapping
  • Q 那一份是影子:每次从共享区输出区读回,口径与 outputs() 一致。
  • read() 底下走的就是它 —— 只要 I 直接用 read() / inputs() 就行。

示例:

byte[][] io = device.getIo();
byte[] q = io[0]; // Q 影子(设备 → 控制器)
byte[] i = io[1]; // I(控制器 → 设备)

outputs()

public byte[] outputs()   // ProcessData:Q 影子

取一份 Q 区影子副本(设备 → 控制器)。写方向请用 copyToOutputs 或整区写口。

返回值:

  • byte[] — Q 净荷副本;底层为空时返回 0 长度数组

说明:

  • 会话未建立 / 共享区未映射 → PnSServiceException(消息同 read())。
  • 它是影子:每次调用都从共享区输出区读回一份拷贝(Q 的单一权威就是共享区),所以它不会领先于 共享区 —— 上次提交失败留下的本地脏区不会出现在它里面,读到的就是共享区当前值。 那种情况下"到底送达没有"看 ioStatus().outputValid() 是不是 false,不要靠读值去猜。
  • 改这个数组不会写回 Q 区,它只是一份拷贝。

示例:

byte[] q = device.processData().outputs();
System.out.println("Q 区 " + q.length + " 字节,第 0 字节 = " + (q[0] & 0xFF));

write(byte[] data)

public void write(byte[] data)   // PnSService:从偏移 0 写 Q

从 Q 区偏移 0 起部分覆盖写,允许短写。正常返回 ⟺ 字节已落共享区。

返回值:

  • 无(void

说明:

  • datanull 或长度为 0 → PnSServiceException,消息 写数据不能为空
  • 会话未建立 → PnSServiceException,消息 会话未建立 (请先 start() 或 WebAPI connect)
  • 数据长度超出 Q 净荷 → PnSServiceException,消息 区域越界: 偏移 0 + 长度 M 超出过程映像区 K 字节
  • 只覆盖前 data.length 字节,其余字节保持原值 —— 不是整帧替换。
  • 提交失败(通道未就绪)→ PnSServiceException,消息 过程数据未就绪, 无法提交 IO;此时脏区保留, 下次写会重试,ioStatus().outputValid()false(不冒充已送达)。
  • 写 Q 的落点就是共享区净荷:过程数据只有共享区这一条通道。

示例:

device.write(new byte[] { 0x01, 0x00 });     // 偏移 0 起 2 字节

readArea(PnSServiceArea area) / readArea(PnSServiceArea area, int offset, int length)

public byte[] readArea(PnSServiceArea area)                                  // PnSService
public byte[] readArea(PnSServiceArea area, int offset, int length) // PnSService

按区域读整段或读一段。I / Q 都能读。

返回值:

  • byte[] — 整区或截取的副本

说明:

  • area 不是 INPUT / OUTPUT(比如 UNKNOWN / MEMORY / DB)→ PnSServiceException, 消息 区域非法: MEMORY 这种形状。
  • offset < 0length < 0PnSServiceException,消息 offset / length 不能为负
  • offset + length 超出该区 → PnSServiceException,消息 区域越界: 偏移 N + 长度 M 超出过程映像区 K 字节
  • 会话未建立 / 共享区未映射 → PnSServiceException(消息同 read())。
  • 这是拷贝:不建映射也能用,且拿到的不跟着变。
  • 偏移不强制对齐,readArea(INPUT, 3, 2) 合法。

示例:

import com.darra.pns.service.PnSService.PnSServiceArea;

byte[] all = device.readArea(PnSServiceArea.INPUT); // I 整段
byte[] part = device.readArea(PnSServiceArea.INPUT, 4, 2); // 偏移 4 起 2 字节

writeArea(PnSServiceArea area, byte[] data) / writeArea(PnSServiceArea area, int offset, byte[] data)

public void writeArea(PnSServiceArea area, byte[] data)                        // PnSService
public void writeArea(PnSServiceArea area, int offset, byte[] data) // PnSService

按区域偏移部分覆盖写。当场提交,不延迟、不按节拍。

返回值:

  • 无(void)。正常返回 ⟺ 字节已落共享区。

说明:

  • INPUTPnSServiceException,消息 I 区 (控制器 -> 设备) 只读, 不支持写
  • area 不是 OUTPUTPnSServiceException,消息 区域非法: N
  • datanull 或空 → PnSServiceException,消息 写数据不能为空
  • offset < 0PnSServiceException,消息 offset 不能为负
  • 越界 → PnSServiceException,消息 区域越界: 偏移 N + 长度 M 超出过程映像区 K 字节
  • 提交失败 → PnSServiceException,消息 过程数据未就绪, 无法提交 IO;脏区保留供下次重试。
  • 一次只写一段连续字节。不相邻的改动分两次调,每次都是一次提交。

示例:

device.writeArea(PnSServiceArea.OUTPUT, 4, new byte[] { 0x11 });   // 偏移 4 写 1 字节
device.writeArea(PnSServiceArea.OUTPUT, new byte[] { 0x0F, 0x00 }); // 偏移 0 起 2 字节

copyInputsTo(byte[] destination) / copyToOutputs(byte[] source)

public int copyInputsTo(byte[] destination)   // ProcessData:I → 调用方缓冲
public int copyToOutputs(byte[] source) // ProcessData:调用方缓冲 → Q

成对的两个拷贝口:一个把 I 拷进你的缓冲,一个把你的缓冲写进 Q。两者都允许短拷。

返回值:

  • copyInputsToint,实际拷贝字节数 = min(I 长度, destination.length);底层为空时 0
  • copyToOutputsint,提交字节数 = source.length;空数组时 0

说明:

  • destination / sourcenullNullPointerException,消息 destination 不能为 null / source 不能为 null
  • 空数组不是错误copyInputsTo 返回 0,copyToOutputs 返回 0 且不写。
  • 数据超出对方长度:copyInputsTo 按短的截断(短拷,正常返回);copyToOutputs 走整区写, 超出 Q 净荷 → PnSServiceException,消息 区域越界: ...
  • 会话未建立 / 共享区未映射 → PnSServiceException(消息同 read() / write())。
  • 拿着固定缓冲反复调时记得看返回值:返回的字节数才是这次真正搬了多少。

示例:

byte[] buf = new byte[16];
int n = device.processData().copyInputsTo(buf); // I 前 n 字节
System.out.println("拷到 " + n + " 字节");

buf[0] = 0x0F;
int sent = device.processData().copyToOutputs(buf); // 从偏移 0 起写 Q

setByte(int offset, int value)

public void setByte(int offset, int value)   // PnSService:写 Q 区单字节

out(offset).setContent(v) 的便捷写口,同一落点。

返回值:

  • 无(void

说明:

  • 只有 value 的低 8 位有效,高 24 位直接丢掉(2560 效果一样)。
  • offset < 0PnSServiceException,消息 offset 不能为负
  • 越界 / 会话未建立 / 提交失败 → PnSServiceException,消息同 writeArea
  • 不能写 I 区:这里没有区域参数,它只写 Q。

示例:

device.setByte(0, 0x0F);

PnSServiceArea

public enum PnSServiceArea {
UNKNOWN, // 未知
INPUT, // I 区:只读(控制器 → 设备)
OUTPUT, // Q 区:可写(设备 → 控制器)
MEMORY, // M 区:不支持
DB // DB 区:不支持
}

整区读写口的区域参数。实际能用的只有 INPUTOUTPUTMEMORY / DB 只是为了让地址解析的 "不支持"有一个如实的落点,传进读写口一律 区域非法: MEMORY 这种形状。

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

不想自己算偏移、想照着工程软件里的地址写时用这一族:区域字母 + 字节偏移 + 位

parseAddress(String address)

public static Address parseAddress(String address)   // PnSService

把一个地址字符串解析成结构化地址。上面那六个读写口内部都先走它。

返回值:

  • Address — 含 area / slot / subslot / byteOffset / bit-1 = 非位寻址)/ length

相关结构:

public static final class Address {
public final PnSServiceArea area; // INPUT(I)/ OUTPUT(Q)
public final int slot; // 固定 1
public final int subslot; // 固定 1
public final int byteOffset; // 过程映像字节偏移(不是位偏置)
public final int bit; // 0-7 = 位地址;-1 = 非位寻址
public final int length; // 位地址 = 1;IB/QB = 1;IW/QW = 2;ID/QD = 4
}

说明:

  • 解析不查边界:它只看格式。偏移是否越界,在真正读写那一刻才比长度。
  • 位号只在 0–7;I0.8 直接是格式错,不是越界。
  • 不强制 2 / 4 对齐:IW3ID2 都合法,偏移就是字节偏移。
  • slot / subslot 恒为 1(固定单模块),没有参数能让它变成别的值。

示例:

var a = PnSService.parseAddress("IW2");
System.out.println(a.area + " 偏移 " + a.byteOffset + " 长度 " + a.length);

readBool(String address) / writeBool(String address, boolean value)

public boolean readBool(String address)                    // PnSService
public void writeBool(String address, boolean value) // PnSService

按位地址读写一个位。地址形如 I0.0 / Q0.0

返回值:

  • readBoolboolean,该位是否为 1

说明:

  • 地址字符串解析失败 → PnSServiceException,具体消息见地址表的错误串
  • 地址是字节 / 字 / 双字形式(没有位号)→ PnSServiceException,消息 位地址必须带位号: IB0
  • 地址越界 → PnSServiceException,消息 地址越界: 偏移 N + 长度 1 超出过程映像区 K 字节
  • writeBool读改写:先读回该字节,只改那一位,再整字节写回 —— 同字节其它位不会被带偏。
  • writeBool("I0.0", ...)PnSServiceException,消息 I 区 (控制器 -> 设备) 只读, 不支持写
  • readBool("Q0.0") 合法:Q 区可读(你写下的影子值)。

示例:

boolean done = device.readBool("I0.0");
device.writeBool("Q0.0", true);

readInt16(String address) / writeInt16(String address, short value)

public short readInt16(String address)                  // PnSService
public void writeInt16(String address, short value) // PnSService

按字地址读写 2 字节,大端

返回值:

  • readInt16short

说明:

  • 地址不是 2 字节形式(比如 IB2 / I0.0)→ PnSServiceException,消息 字地址必须为 2 字节且非位寻址: IB2
  • I 侧 → PnSServiceException,消息 I 区 (控制器 -> 设备) 只读, 不支持写
  • 越界 → PnSServiceException,消息 地址越界: 偏移 N + 长度 2 超出过程映像区 K 字节
  • 大端由 SDK 内部同一份编解码完成,跟元素口读到的是同一个值。
  • 无符号需求请用元素口 asUInt16()(本口返回 short0xFFFF 读出来是 -1)。

示例:

short recipe = device.readInt16("IW2");
device.writeInt16("QW2", (short) 123);

readInt32(String address) / writeInt32(String address, int value)

public int  readInt32(String address)                // PnSService
public void writeInt32(String address, int value) // PnSService

按双字地址读写 4 字节,大端

返回值:

  • readInt32int

说明:

  • 地址不是 4 字节形式 → PnSServiceException,消息 双字地址必须为 4 字节且非位寻址: IW2
  • I 侧 → PnSServiceException,消息 I 区 (控制器 -> 设备) 只读, 不支持写
  • 越界 → PnSServiceException,消息 地址越界: 偏移 N + 长度 4 超出过程映像区 K 字节
  • 无符号需求用元素口 asUInt32()(返回 long)。

示例:

int counter = device.readInt32("ID4");
device.writeInt32("QD4", 10000);

地址写法与错误串

地址写法是什么
I0.0 / Q0.0位地址:第 0 字节的第 0 位(位号只能 0–7)
IB0 / QB0字节(区域字母后写不写 B 都是字节,I0IB0 等价)
IW2 / QW2字(2 字节)
ID4 / QD4双字(4 字节)
  • 不强制 2 / 4 对齐IW3 / ID2 都合法,偏移就是字节偏移。
  • 不支持 M 区 / DB 区,如实抛。
  • 区域字母大小写不敏感:i0.0I0.0 一样。
  • 类型字母必须大写iw2 不是"字地址",它会被当成纯数字去解析,报 字节偏移非法: iw2
  • 偏移上限 0xFFFF,超过报 字节偏移超出范围

全部失败串(PnSServiceException,括号里是消息的 detail 段):

消息什么时候
地址不能为空传了 null / 空串 / 全空白
不支持 DB 地址: DB1.DBX0.0DB / db 开头
不支持的地址区域: X0.0首字母既不是 I / Q,也不是 M / DB
不支持 M 地址: M0.0首字母是 M
地址缺少偏移量: IW只剩区域字母 + 类型字母
地址格式非法 (位地址应为 I0.0 形式): I.0有点号,但点号贴在头或尾
位号必须在 0-7 之间: I0.8位号越界
字节偏移非法: IX0 / 位号非法: I0.x点号 / 类型字母后面不是纯数字
字节偏移超出范围: I70000偏移大于 0xFFFF
位地址必须带位号: IB0对非位地址调 readBool / writeBool
字地址必须为 2 字节且非位寻址: IB2对非字地址调 readInt16 / writeInt16
双字地址必须为 4 字节且非位寻址: IW2对非双字地址调 readInt32 / writeInt32
地址越界: 偏移 N + 长度 M 超出过程映像区 K 字节偏移 + 宽度超出该方向净荷
I 区 (控制器 -> 设备) 只读, 不支持写I / IB / IW / ID / I0.0
不支持 M/DB 地址区域落到 M / DB(地址口已在上游拦掉,属兜底)
PnSServiceException 的消息带前缀

PnSServiceExceptiongetMessage()未知错误 (<detail>) 这种形状:PnSErr.UNKNOWN 的中文名 + 括号里的细节。错误码可以用 code() 取(PnSErr),服务端原始码用 serverErrorCode() 取。 上面表里列的都是括号里那一段。

长度、下标与记录索引空间

长度怎么拿

长度由运行配置决定,不要写死 20 / 32(改过组态就变)。

想拿走哪
I 净荷长度device.read().length / processData().inputs().length / pd.in().length()
Q 净荷长度processData().outputs().length / pd.out().length()
两个方向的区域长度device.diagSnapshot().inputAreaLength() / .outputAreaLength()

区域长度和净荷长度不是一回事:区域长度含槽头那几字节前缀,净荷长度不含。所以区域长度会比净荷长度大 2–6 字节(I 侧前缀要按数据内容扫描)。要字节数给结构体用,请用净荷长度。

下标取元素:四个入口对照

同一件事(按偏移读写一个值)在 Java 里有四个入口,实现是同一个,差别只在异常类型和名字:

入口负偏移写只读侧位号越界asValue
pd.in().get(i) / pd.out().get(i)是(仅 out)IndexOutOfBoundsExceptionUnsupportedOperationExceptionIllegalArgumentException
svc.in(offset) / svc.out(offset)是(仅 out)PnSServiceExceptionPnSServiceExceptionPnSServiceException没有
pd.out().set(i, b)——是(仅 out)IndexOutOfBoundsExceptionUnsupportedOperationException————
svc.setByte(i, v)——PnSServiceException——————

选哪个都行,别混着用同一段偏移:两边算的是同一个净荷地址,混用不报错,只会自己绕晕。

两个记录索引空间

除了周期交换的 I/Q,还有一条非周期的记录通道。它有两个互不相通的索引空间,走两个不同的入口:

索引空间范围走哪个入口底层端点
用户区记录0x00000x7FFFdevice.getRecords()GET /api/records
I&M(标识与维护)0xAFF00xAFF4device.getImData()GET /api/im
两个空间别混用

getRecords() 只覆盖用户区,I&M 那几个索引不在它里面 —— 从记录 API 拿不到 I&M。 I&M 走 getImData()ImData:厂商标识 / 设备标识 / 订货号 / 序列号 / 硬件版本 / 软件版本 / 站名 / 产品名,以及 I&M1–4 的工程标识字段)。

反过来也一样:getImData() 只给 I&M,不给用户区记录。

public UserRecords getRecords()   // PnSService:用户区记录表
public ImData getImData() // PnSService:I&M 观测数据

返回值:

  • getRecords()UserRecordsrecords()UserRecord[],每条带 slot() / subslot() / index() / length() / dataHex())、count()timestamp()
  • getImData()ImData:见上

说明:

  • 会话未建立 → PnSServiceException,消息 会话未建立 (请先 start() 或 WebAPI connect)
  • 服务不可达 / 响应非法 → PnSServiceException,如实抛,不虚构"没有记录"。
  • 没有记录 != 失败:空表就是空表,count() 为 0。
  • 字段缺失按默认值返回(空串 / 0 / false),不编造。
  • 这两条是非周期通道,和过程数据不是一个东西,也不占用总线周期时间。

示例:

for (var r : device.getRecords().records()) {
System.out.printf("槽 %d 索引 0x%04X 长度 %d%n", r.slot(), r.index(), r.length());
}
String orderId = device.getImData().orderId();