过程数据
过程数据就是每周期和控制器交换的那段 I/Q 字节。把 I/Q 映射成结构体之后,视图直接钉在过程映像上 —— 这就是本页要讲的 0 拷贝:映射本身不复制数据,也不为整帧另建缓冲区。
方向只有两个:写 Q(设备 → 控制器)、读 I(控制器 → 设备)。I 区由控制器每周期整段覆盖,所以它只读,写它无效。
总线周期由服务进程完成。SDK 不自建周期循环,也没有用户回调 —— 调度(循环 / 定时器)由你自己定。
过程数据只有共享区这一条通道:所有读写口落的都是共享区里的过程映像,通道进不去就如实抛
(过程数据未就绪 那几条)。
设备是固定的单模块:槽 1 / 子槽 1。本页所有入口都不收槽位参数,也不用收。
GSDML 里那 20 字节的 Input(设备 → 控制器)就是 SDK 的 Q;GSDML 里那 32 字节的 Output(控制器 → 设备) 就是 SDK 的 I。
20 / 32 只是出厂组态的常见尺寸,别写死。长度以运行时为准,见 长度怎么拿。
一是整幅映像的映射: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 字节 nativeint)。 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()一次,字段里有初值
说明:
type为null→NullPointerException,消息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()一次
说明:
type为null→NullPointerException,消息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(控制器 → 设备)。
返回值:
boolean—true:pointer[0]是净荷首地址、byteCount[0]是净荷字节数;false:pointer[0] = null、byteCount[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
说明:
dest为null→NullPointerException,消息dest 不能为 null。size < 0→IllegalArgumentException,消息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本身,字段已填好
说明:
dest为null→NullPointerException,消息dest 不能为 null。dest.packedSize() < 0→IllegalArgumentException,消息packedSize 不能为负: N。- I 区字节数不足
packedSize()→PnSServiceException,消息同上一条I 区 N 字节不足结构体 M 字节。 - 解包用的
ByteBuffer已设成BIG_ENDIAN、position = 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 区。
说明:
packed为null→NullPointerException,消息packed 不能为 null。src为null→NullPointerException,消息src 不能为 null。src.packedSize() < 0→IllegalArgumentException,消息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()→int,pack/unpack都读这么多字节pack/unpack→ 无
说明:
- 传进来的
ByteBuffer已是BIG_ENDIAN、position = 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 < 0→IndexOutOfBoundsException,消息偏移不能为负: 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 < 0→IndexOutOfBoundsException,消息偏移不能为负: 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——0x01→true,0x02→false,0x80→false。不是"非 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)写整个字节:true→0x01、false→0x00,非规范字节不保留。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-7,0 = 最低位。
返回值:
getBit→boolean,该位是否为 1
说明:
bit < 0 || bit > 7→IllegalArgumentException,消息位索引必须在 0-7 之间: N。- 对 In 元素调
setBit→UnsupportedOperationException(同一个"只读"消息)。 注意顺序:先检查方向、后检查位号 —— 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() 那一族。
返回值:
asValue→Object:装箱值,具体类型随type——INTEGER8走Byte、INTEGER16走Short、INTEGER32走Integer、INTEGER64走Long;UNSIGNED8/UNSIGNED16(含BYTE/WORD)走Integer,UNSIGNED32/UNSIGNED64(含DWORD/LWORD)走Long;浮点走Float/Double,BOOL走Boolean- 窄类型不会升位:
asValue(INTEGER8 / INTEGER16)拿到的是Byte/Short, 写成(Integer) v会抛ClassCastException;要Integer请按UNSIGNED16/INTEGER32取
说明:
type为null→NullPointerException,消息type 不能为 null。writeValue的value为null→NullPointerException,消息value 不能为 null。PnSDataType的 15 个取值全部支持:BOOL/INTEGER8·16·32·64/UNSIGNED8·16·32·64/REAL32/REAL64/BYTE·WORD·DWORD·LWORD。BYTE/WORD/DWORD/LWORD是UNSIGNED8/16/32/64的别名(同宽度、同大端)。writeValue(BOOL, ...)接受Boolean,也接受任何Number(非 0 即真)。其它类型传非Number的值 →ClassCastException;BOOL传既不是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(元素口)/PnSServiceException(svc.in(offset)那一族); - 会话未就绪 →
PnSServiceException(会话未建立/过程数据未就绪)。
线程安全:元素口本身不加锁,且写路径会改会话级的共享缓冲。同一个服务对象不要多线程并发读写过程数据, 要并发就自己加锁,或者用整幅映射 + 自己的调度。
服务侧的同名元素口
PnSService 上也有一套按偏移取元素的入口,与 processData().in()/out() 是同一份实现,只是异常口径不同:
public ProcessDataItem in(int offset) // PnSService:只读 I
public ProcessDataItem out(int offset) // PnSService:写 Q
返回值:
ProcessDataItem— 元素视图,同样不持有数据字节
说明:
offset < 0→PnSServiceException,消息偏移不能为负: N(注意与pd.in().get(i)的IndexOutOfBoundsException不同)。- 写只读侧 →
PnSServiceException,消息In 只读 I (控制器→设备); 写请用 out(offset)。 - 位号越界 →
PnSServiceException,消息位索引必须在 0-7 之间: N。 - 标量方法名与
DataItem一致(getContent/setContent/asInt8…asDouble/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)
说明:
data为null或长度为 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 < 0或length < 0→PnSServiceException,消息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)。正常返回 ⟺ 字节已落共享区。
说明:
- 写
INPUT→PnSServiceException,消息I 区 (控制器 -> 设备) 只读, 不支持写。 area不是OUTPUT→PnSServiceException,消息区域非法: N。data为null或空 →PnSServiceException,消息写数据不能为空。offset < 0→PnSServiceException,消息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。两者都允许短拷。
返回值:
copyInputsTo→int,实际拷贝字节数 =min(I 长度, destination.length);底层为空时 0copyToOutputs→int,提交字节数 =source.length;空数组时 0
说明:
destination/source为null→NullPointerException,消息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 位直接丢掉(256和0效果一样)。 offset < 0→PnSServiceException,消息offset 不能为负。- 越界 / 会话未建立 / 提交失败 →
PnSServiceException,消息同 writeArea。 - 不能写 I 区:这里没有区域参数,它只写 Q。
示例:
device.setByte(0, 0x0F);
PnSServiceArea
public enum PnSServiceArea {
UNKNOWN, // 未知
INPUT, // I 区:只读(控制器 → 设备)
OUTPUT, // Q 区:可写(设备 → 控制器)
MEMORY, // M 区:不支持
DB // DB 区:不支持
}
整区读写口的区域参数。实际能用的只有 INPUT 和 OUTPUT;MEMORY / 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 对齐:
IW3、ID2都合法,偏移就是字节偏移。 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。
返回值:
readBool→boolean,该位是否为 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 字节,大端。
返回值:
readInt16→short
说明:
- 地址不是 2 字节形式(比如
IB2/I0.0)→PnSServiceException,消息字地址必须为 2 字节且非位寻址: IB2。 - 写
I侧 →PnSServiceException,消息I 区 (控制器 -> 设备) 只读, 不支持写。 - 越界 →
PnSServiceException,消息地址越界: 偏移 N + 长度 2 超出过程映像区 K 字节。 - 大端由 SDK 内部同一份编解码完成,跟元素口读到的是同一个值。
- 无符号需求请用元素口
asUInt16()(本口返回short,0xFFFF读出来是-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 字节,大端。
返回值:
readInt32→int
说明:
- 地址不是 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 都是字节,I0 与 IB0 等价) |
IW2 / QW2 | 字(2 字节) |
ID4 / QD4 | 双字(4 字节) |
- 不强制 2 / 4 对齐:
IW3/ID2都合法,偏移就是字节偏移。 - 不支持 M 区 / DB 区,如实抛。
- 区域字母大小写不敏感:
i0.0和I0.0一样。 - 类型字母必须大写:
iw2不是"字地址",它会被当成纯数字去解析,报字节偏移非法: iw2。 - 偏移上限
0xFFFF,超过报字节偏移超出范围。
全部失败串(PnSServiceException,括号里是消息的 detail 段):
| 消息 | 什么时候 |
|---|---|
地址不能为空 | 传了 null / 空串 / 全空白 |
不支持 DB 地址: DB1.DBX0.0 | 以 DB / 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 的消息带前缀PnSServiceException 的 getMessage() 是 未知错误 (<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) | IndexOutOfBoundsException | UnsupportedOperationException | IllegalArgumentException | 有 |
svc.in(offset) / svc.out(offset) | 是 | 是(仅 out) | PnSServiceException | PnSServiceException | PnSServiceException | 没有 |
pd.out().set(i, b) | —— | 是(仅 out) | IndexOutOfBoundsException | UnsupportedOperationException | —— | —— |
svc.setByte(i, v) | —— | 是 | PnSServiceException | —— | —— | —— |
选哪个都行,别混着用同一段偏移:两边算的是同一个净荷地址,混用不报错,只会自己绕晕。
两个记录索引空间
除了周期交换的 I/Q,还有一条非周期的记录通道。它有两个互不相通的索引空间,走两个不同的入口:
| 索引空间 | 范围 | 走哪个入口 | 底层端点 |
|---|---|---|---|
| 用户区记录 | 0x0000–0x7FFF | device.getRecords() | GET /api/records |
| I&M(标识与维护) | 0xAFF0–0xAFF4 | device.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()→UserRecords:records()(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();