过程数据
通过 svc.process_data 访问过程映像:inputs_mapping / outputs_mapping 把整幅映像映射成结构体(0 拷贝),In[i] / Out[i] 与 read_bool("I0.0") 这类口单点读写。
过程数据就是控制器与设备之间每周期自动搬运的那段字节:写 Q(设备 → 控制器),读 I(控制器 → 设备)。最省事的用法是把这段字节一次映射成 ctypes 结构体 —— 之后读写字段就是直接读写过程映像本身,0 拷贝:不用拷贝、不用刷新,读到的永远是当前周期,写下的下个周期发出。
I 区只读:控制器每周期整段覆盖输入映像,就算你写进去也会在下一拍被冲掉,SDK 不拦也不报错。要写过程映像只有 Q 区一条路,改字节即改进程映像,没有任何提交调用。
映射之外还有三种单值入口,都是拷贝:按偏移取(pd.In[i] / pd.Out[i])、按地址取("I0.0" / "IW2" / "QD4")、整段取(svc.read())。好处是不用先建结构体,代价是每次取值都要把那一小段字节拷出来再按大端解释。
总线周期由内核完成,SDK 没有用户周期回调,调度(循环 / 定时器 / 线程)由你自己定。
GSDML 里那 20 字节的 Input(设备 → 控制器)就是 SDK 的 Q;GSDML 里那 32 字节的 Output(控制器 → 设备)就是 SDK 的 I。
设备是固定单模块(槽 1 / 子槽 1),本 SDK 的过程数据入口没有任何收 slot / subslot 参数的地方。
- 必须
_pack_ = 1:过程映像是线上字节流,不能有对齐填充。非 1 直接抛TypeError,不会静默错位。 - 窗口起点是映射那一次算出来的:I 侧净荷起点 = 先剥掉 2 个 IOPS 前缀,再看净荷开头连续有几个
0x80再剥(最多 4 个)。所以它取决于当次区内容,不是常量 —— 能用的窗口比该方向映像区长度少 2–6 字节;Q 侧是固定少 3 字节、不扫描。 - 判据只看 SDK 报的"可用"字节数(映射失败时
ValueError里会写"需要多少 / 可用多少"),别自己写死偏移。 - 多字节是网络大端:字段用
ctypes.BigEndianStructure。普通ctypes.Structure按本机小端解,值会反序,而且不报错。 - 生命周期是真的,地址稳定不是:映射在通道存续期有效;
stop()后映射还在(能读回旧值,但不再刷新);close()后那块共享内存已经解除映射,再用那个结构体等于踩失效指针。别把映射的起点当常量缓存,通道重来之后重新映射一次。 - Q 区映射是单 owner:读可并发,写请调用方自行串行化。
- I 侧的"只读"是约定、不是拦截:映射出来的结构体在 Python 层可写,写进去的字节下一拍被控制器冲掉。
In[i] / Out[i])是拷贝,不是视图pd.In[0].content / pd.Out[2].as_int16 每次取值都把那一小段字节拷出来再按大端解释,写入同理。[i] 定的只是偏移,值仍然是那一瞬间的快照 —— 它不是"下标定位拿到的活视图",也不复用缓存对象。所以连着取几个 In[i] 不是同一拍 —— 要保证同一拍,就一次取够(svc.read() / pd.inputs / read_area 一次拿一整段)。
真正的 0 拷贝只有一种形态:整幅映像的映射(inputs_mapping / outputs_mapping)。
使用案例
日常使用
① 映射到类(0 拷贝,持续用):start() 之后映射一次,拿到钉在过程映像上的结构体实例,之后当变量用 —— 赋值即写、取值即读,字段直落映像,不必再补 write()。
import ctypes
from darra_pns import PnSService
svc = PnSService() # 构造随时可调
svc.start() # 启动服务 + 建立过程数据通道
class FromPlc(ctypes.BigEndianStructure): # I:控制器 → 设备(只读)
_pack_ = 1
_fields_ = [("present", ctypes.c_uint8),
("recipe", ctypes.c_int16), # 大端
("part_no", ctypes.c_char * 16)] # 定长字节字段,读到第一个 NUL 为止
class ToPlc(ctypes.BigEndianStructure): # Q:设备 → 控制器(可写)
_pack_ = 1
_fields_ = [("ready", ctypes.c_uint8),
("busy", ctypes.c_uint8)]
inp = svc.process_data.inputs_mapping(FromPlc) # 映射一次
outp = svc.process_data.outputs_mapping(ToPlc)
outp.ready = 1 # 写:下个周期发出
present = inp.present # 读:永远是当前周期
part_no = inp.part_no.decode()
② 下标取元素(不建结构体):In[i] / Out[i] 按字节偏移取一个元素,元素上的每个成员就是一个定宽读写口。每次取值都是从当前映像重新取一份(拷贝)。
present = svc.process_data.In[0].content # I 偏移 0 的 1 字节
recipe = svc.process_data.In[1].as_int16 # I 偏移 1 起 2 字节,大端
flag = svc.process_data.In[4].get_bit(2) # I 偏移 4 那个字节的 bit2
svc.process_data.Out[0].content = 0x11 # Q 偏移 0 写 1 字节
svc.process_data.Out[2].as_int16 = 123 # Q 偏移 2 起 2 字节,大端
svc.process_data.Out[4].set_bit(0, True) # 只改该位,同字节其余位不动
③ 按地址取(照着工程软件里的地址写):区域字母 + 字节偏移 + 位,不用自己算字节。
done = svc.read_bool("I0.0") # 位
recipe = svc.read_int16("IW2") # 字(2 字节,大端)
counter = svc.read_int32("ID4") # 双字(4 字节,大端)
svc.write_bool("Q0.0", True)
svc.write_int16("QW2", 123)
svc.write_int32("QD4", 10000)
周期变化
映射取一次就够,同一份映射放进你自己的循环(或循环里的调度点)直接用:
import time
inp = svc.process_data.inputs_mapping(FromPlc)
outp = svc.process_data.outputs_mapping(ToPlc)
while running: # 你自己的节拍,例如 10 ms
outp.ready = 1 if inp.present else 0 # 读当前拍 → 算 → 写下拍发出
outp.busy = inp.recipe
time.sleep(0.01)
循环里读到的永远是当前周期、写下的下个周期发出,不需要任何额外调用。映射跨周期一直有效,不用每拍重新映射 —— 但不要把映射当成"偏移永远不变"的承诺:通道重建(重新 start() / close() 之后再开)之后要重新映射一次。
过程数据没有变更回调:SDK 不提供用户周期回调,svc.events 上能订的是运行态类事件(其中 IOPS 那个订了也不会触发,见 事件)。调度(循环 / 定时器 / 线程)由你自己定。
一律如实抛,不会静默降级。
结构体映射(零拷贝)
这条路只有两个入口,都长在 svc.process_data 上。它们把 ctypes 结构体钉在当前过程映像的净荷起点上(from_address,不是 from_buffer_copy),返回的结构体实例就是一块活视图。映射从这一次算出的净荷起点开始:要覆盖映像中段,就在结构体开头放一段占位数组(("_skip", ctypes.c_uint8 * k))顶开;只想按偏移取一段字节,直接用 svc.read_area()。字段地址 = 净荷起点 + 字段偏移,偏移按 _pack_ = 1 自己算(元素口那边有 item.offset 可以拿)。
inputs_mapping()
一句话:把 I 区(控制器 → 设备)净荷起点钉成一个 ctypes 结构体实例并返回 —— 读字段就是读当前周期的过程映像。0 拷贝。
def inputs_mapping(self, struct_type)
参数:
struct_type(ctypes.Structure子类)— 必须是类本身(传实例也抛TypeError);必须_pack_ = 1
返回值:
- 结构体实例 — 落在 live 输入映像上的引用,不是快照;带一个
_live_owner属性(钉住会话,避免只持有结构体时过程数据通道被摘掉)
说明:
-
在
start()之后调。下列情况一律报错,不返回半成品:情况 异常 消息要点 struct_type不是ctypes.Structure子类(含传实例)TypeError提示用 read_struct取快照_pack_不是 1TypeError结构体必须 _pack_ = 1 (无对齐填充)结构体大小为 0,或大于 65536 ValueError结构体大小非法: N结构体大于本次可用窗口 ValueError结构体大于输入过程映像: 需要 N, 可用 M净荷指针为 0 RuntimeError过程数据输入映像不可用: 净荷指针为空 (0x0)会话未建立(没 start())PnSServiceError码 NOT_INITIALIZED,本机服务会话未建立通道没就绪 / 该方向无映像 PnSServiceError码 NOT_AVAILABLE,过程数据未就绪, 无法映射 IO或过程数据输入映像不可用 -
起点每次映射重新算:先剥 2 个 IOPS 前缀,再看净荷开头连续几个
0x80再剥(最多 4 个)。所以可用窗口 = 该方向映像区长度 − 2…6 字节,而且跟着数据内容变。映射一次长期用是推荐用法,换来的是少一次解析;代价是偏移一旦漂移就是静默读错位 —— 不抛异常,尺寸门也拦不住(因为窗口只会变小)。要绝对对齐就每次接近时重新映射一次,或者干脆用整段svc.read()自己按偏移解。 -
结构体必须自净荷起点开始(与
pd.In[0]/svc.read()[0]同一起点)。要映射到映像中段,在结构体开头加一段占位:("_skip", ctypes.c_uint8 * k)。 -
多字节字段请用
ctypes.BigEndianStructure(PROFINET 线上大端)。ctypes.c_char * N读到第一个 NUL 就截断;写更短的串不会清掉字段尾部的旧字节,要清就得整段写成b"..."补 NUL。 -
I 侧只读是约定:结构体在 Python 层可写,写完不会报错,下一拍被控制器整段覆盖。
-
读可并发;
close()之后那块内存已经解除映射,就别再碰这个结构体了。
示例:
import ctypes
class FromPlc(ctypes.BigEndianStructure):
_pack_ = 1
_fields_ = [("present", ctypes.c_uint8),
("recipe", ctypes.c_int16),
("part_no", ctypes.c_char * 16)]
inp = svc.process_data.inputs_mapping(FromPlc) # 一次映射,长期用
print(inp.present, inp.recipe, inp.part_no.decode())
outputs_mapping()
一句话:把 Q 区(设备 → 控制器)净荷起点钉成一个 ctypes 结构体实例并返回 —— 给字段赋值就是改过程映像。0 拷贝。
def outputs_mapping(self, struct_type)
参数:
struct_type(ctypes.Structure子类)— 同inputs_mapping,必须是类且_pack_ = 1
返回值:
- 结构体实例 — 落在 live 输出映像上的引用,可读可写;同样带
_live_owner
说明:
- 失败形态与
inputs_mapping逐条对应,只有方向不同:消息里的字面量换成"输出"(结构体大于输出过程映像: 需要 N, 可用 M/过程数据输出映像不可用: 净荷指针为空 (0x…))。 - 窗口 = 该方向映像区长度 − 固定 3 字节(Q 侧不扫描
0x80)。 - 写字段直落:不需要再调
svc.write(),也不经过svc.output_committed的记账(那是整段写那条路的标记)。 - 服务侧(配置工具 / 服务自身的输出通道)自己产生新输出时会整窗下写,会覆盖你钉页直写的字节 —— 不要和服务侧写混用同一个 Q 窗口,只选一种写方式。
- 写是单 owner:读可并发,写请调用方自行串行化。
示例:
import ctypes
class ToPlc(ctypes.BigEndianStructure):
_pack_ = 1
_fields_ = [("ready", ctypes.c_uint8),
("busy", ctypes.c_uint8)]
outp = svc.process_data.outputs_mapping(ToPlc) # 一次映射,长期用
outp.ready = 1 # 改字段即改过程映像,下个周期发出
print(outp.busy) # 读回当前输出映像
直接访问(不映射也能用)
下面这一族与结构体映射彼此独立:不建映射可以直接用,建了映射也常并用(结构体放不下的字段用它解)。这一族全是拷贝。
svc.process_data.In / inn / Out
一句话:三个属性取到同一个门面 ProcessData 上的两个数组对象 —— In / inn 是只读的 I 数组,Out 是可写的 Q 数组。它本身不搬数据,只提供下标。
@property
def In(self) -> ProcessDataArray # I(控制器 → 设备):只读
@property
def inn(self) -> ProcessDataArray # In 的别名,同一个数组实例
@property
def Out(self) -> ProcessDataArray # Q(设备 → 控制器):可写
返回值:
ProcessDataArray— 过程数据数组对象,靠下标取元素(见下一条);In与inn是同一个实例(pd.inn is pd.In为真)
说明:
In是合法标识符,所以它当主名字;inn是给"全小写风格"用的别名,语义与In完全相同。- 数组上的
length会吞掉异常返回 0(见 字节数怎么拿),别拿 0 当"真的没有数据"的判据。 - I / Q 是两个独立对象,
In上写不了、Out上读的是本地最近写进去的影子值。
示例:
pd = svc.process_data
print(pd.In.length, pd.Out.length) # 两个方向的净荷字节数;取不到是 0
print(pd.inn is pd.In) # True:同一个数组
相关结构:
class ProcessDataArray: # svc.process_data.In / inn / Out
length # int,只读属性:本方向净荷字节数;取不到时静默返回 0
def __len__() # ≡ length
def __getitem__(offset) # → ProcessDataItem;offset 必须是 int 且不小于 0
def __setitem__(offset, value) # 仅 Out:等价于 Out[offset].content = value
class ProcessDataItem: # svc.process_data.In[i] / Out[i]
offset # int,只读属性:本项的字节偏移
content # int,读写属性:1 字节原件(0..255),写侧按 0xFF 截断
Content # content 的别名(C# 同名对照)
as_int8 / as_uint8 # int,读写属性:1 字节,有符号 / 无符号(uint8 与 content 同值)
as_int16 / as_uint16 # int,读写属性:2 字节,大端
as_int32 / as_uint32 # int,读写属性:4 字节,大端
as_int64 / as_uint64 # int,读写属性:8 字节,大端
as_float # float,读写属性:4 字节,大端 IEEE754
as_double # float,读写属性:8 字节,大端 IEEE754
as_bool # bool,读写属性:bit0 语义;写 True = 0x01、False = 0x00(整字节)
def get_bit(bit) # → bool:本项偏移处那一个字节的第 bit 位(0..7,0 = 最低位)
def set_bit(bit, value) # 仅 Out:只改目标位,同字节其余位不动
def __int__() / __index__() # int(item) ≡ item.content;hex(item) 也能直接用
def __eq__(other) # item 与整数比 content;item 与 True / False 一律不相等
pd.In[i] / pd.Out[i]
一句话:按字节偏移取一个元素视图 —— In[i] 读 I(相对 I 净荷起点),Out[i] 读写 Q。是拷贝,不是 0 拷贝。
def __getitem__(self, offset) -> ProcessDataItem
def __setitem__(self, offset, value) # 仅 Out:等价于 Out[offset].content = value
参数:
offset(int)— 字节偏移,相对本方向净荷起点;不要求 2 / 4 对齐,也不要求落在某个字段边界上value(int)— 仅Out[i] = v用,按 1 字节写(content口径,0xFF截断)
返回值:
ProcessDataItem— 元素视图;[i]本身只记偏移,不读也不写字节
说明:
- 每次
[i]都返回新对象,不缓存、不钉页;偏移可以是任意非负整数,越界不在取值时报,而在真正读写时按下面的口径报。 offset不是int→TypeError。offset小于 0 →ValueError(偏移不能为负: N)。In[i] = v(I 侧赋值)→RuntimeError(ProcessData.In 只读 I (控制器→设备); 写请用 ProcessData.Out[i].content)。Out[i] = v会把整字节写进 Q 映像(不是改位),通道没就绪时抛PnSServiceError(NOT_AVAILABLE)。- 偏移越界的异常类型按方向不同,这是刻意的:读 I 的单字节走服务索引器,越界是
PnSServiceError(INVALID_PARAM,地址越界: 偏移 N 超出过程映像区 M 字节);读 Q 的单字节和两个方向的多字节读越界都是ValueError(读过程数据越界: offset=N 超出 Q 区 M 字节/读过程数据失败 offset=N len=L 区长=M);写越界是PnSServiceError(INVALID_PARAM)。 - 多字节写是要么全写要么不写:先按净荷窗口核对"偏移 + 宽度",越界就在任何字节落地之前抛
PnSServiceError(INVALID_PARAM)—— 窗口尾部的字节一个都不会被改。多字节值骑在窗口末尾上时,结果是整段不落,不是"写半截"。
示例:
pd = svc.process_data
present = pd.In[0].content # I 偏移 0 的 1 字节
recipe = pd.In[1].as_int16 # I 偏移 1 起 2 字节(大端)
pd.Out[0].content = 0x11 # Q 偏移 0 写 1 字节
pd.Out[2] = 0x22 # 等价于 pd.Out[2].content = 0x22
for i in range(pd.Out.length): # 遍历整个输出映像
pd.Out[i].content = 0
item.offset
一句话:本元素视图记着的字节偏移。只读、纯记账。
@property
def offset(self) -> int
返回值:
int— 该元素相对本方向净荷起点的字节偏移
说明:
- 取值不碰过程映像,也不会失败 —— 就算偏移已经越界,
offset照样把数字还给你。 - 拿它做自己的地址算术时,别忘了过程映像是大端的。
示例:
item = svc.process_data.In[3]
print(item.offset) # 3
item.content / item.Content
一句话:1 字节原件(0..255)。In[i].content 读 I,Out[i].content 读写 Q —— 拷贝。
@property
def content(self) -> int
@content.setter
def content(self, value)
Content = content # 同名别名
参数:
value(int,仅写)— 只写低 8 位(按0xFF截断,超范围不抛)
返回值:
int— 那个字节的值(0..255)
说明:
- 写只对
Out:In[i].content = v抛RuntimeError(I 侧只读),异常在任何字节被写到之前就抛出来了。 - 写值不是
int→TypeError。 - 读越界:I 侧抛
PnSServiceError(INVALID_PARAM,地址越界: 偏移 N 超出过程映像区 M 字节);Q 侧抛ValueError(读过程数据越界: offset=N 超出 Q 区 M 字节)。 - 写越界 →
PnSServiceError(INVALID_PARAM,偏移越界: N)。 - 通道没就绪:读的异常类型同样按方向分 —— I 侧抛
PnSServiceError(会话没建立是NOT_INITIALIZED,会话在、通道没就绪是NOT_AVAILABLE);Q 侧读的是影子,会话没建立时影子为空,抛的是上一条那条ValueError(不是PnSServiceError)。写抛PnSServiceError(NOT_AVAILABLE,过程数据未就绪, 无法提交 IO)。 Content与content是同一个属性(对外是给对照语言用的同名别名)。- 一次取值 = 一份新的当前字节。同一元素上连着取两个成员,两次之间可能已经过了一个总线周期。
示例:
svc.process_data.Out[0].content = 0x11 # 写 Q
print(svc.process_data.Out[0].Content) # 别名,读回 0x11(Q 侧可读回本地影子)
print(svc.process_data.In[0].content) # 读 I:这是控制器发来的那一字节,不是刚写的值
item.as_int8 / as_uint8
一句话:把元素偏移处的 1 字节按有符号 / 无符号读或写。as_uint8 与 content 同值同字节。拷贝。
@property
def as_int8(self) -> int
@as_int8.setter
def as_int8(self, value: int)
@property
def as_uint8(self) -> int
@as_uint8.setter
def as_uint8(self, value: int)
参数:
value(int,仅写)— 按 1 字节截断(0xFF)
返回值:
int— 有符号时范围 −128..127;无符号时 0..255
说明:
- 有符号读:字节大于 127 就减 256(
0xFF→ −1)。 - 写侧两个口同一口径:先
& 0xFF再落字节。-1与255写下去是同一个字节。 - 失败形态与
content完全一致:I 侧写RuntimeError;越界读 I 是PnSServiceError、越界读 Q 是ValueError;写越界是PnSServiceError;值不是int是TypeError。 - 不要求对齐,也不与元素宽度绑定 —— 偏移是哪儿就取哪儿那一个字节。
示例:
svc.process_data.Out[3].as_int8 = -1 # 写 0xFF
print(svc.process_data.Out[3].as_uint8) # 255:同一字节,换无符号看
print(svc.process_data.Out[3].as_int8) # -1
item.as_int16 / as_uint16
一句话:2 字节,大端(PROFINET 线上序)。In[i] 读 I,Out[i] 读写 Q。拷贝。
@property
def as_int16(self) -> int
@as_int16.setter
def as_int16(self, value: int)
@property
def as_uint16(self) -> int
@as_uint16.setter
def as_uint16(self, value: int)
参数:
value(int,仅写)— 按 16 位截断后按无符号打包
返回值:
int— 有符号时范围 −32768..32767(0xFFFE→ −2);无符号时 0..65535
说明:
- 写侧不抛溢出:先按位宽取模(
& 0xFFFF)再落字节,超范围值按二补码取低位。as_int16 = -2与as_int16 = 0xFFFE落的是同一对字节。 - 写值不是
int→TypeError。 - 越界读(偏移 + 2 超过本方向净荷)→
ValueError(读过程数据失败 offset=N len=2 区长=M),I / Q 两个方向同一口径。 - I 侧写 →
RuntimeError;Q 侧写越界 →PnSServiceError(INVALID_PARAM);通道没就绪 →PnSServiceError(NOT_AVAILABLE)。 - 大端是硬口径:
as_int16不受本机字节序影响,也不接受在调用点换序。
示例:
svc.process_data.Out[2].as_int16 = 123
svc.process_data.Out[4].as_int16 = -2 # 落 FF FE
print(svc.process_data.Out[2].as_int16) # 123
print(svc.process_data.Out[4].as_uint16) # 65534
item.as_int32 / as_uint32
一句话:4 字节,大端。In[i] 读 I,Out[i] 读写 Q。拷贝。
@property
def as_int32(self) -> int
@as_int32.setter
def as_int32(self, value: int)
@property
def as_uint32(self) -> int
@as_uint32.setter
def as_uint32(self, value: int)
参数:
value(int,仅写)— 按 32 位截断(& 0xFFFFFFFF)
返回值:
int— 有符号时范围 −2147483648..2147483647;无符号时 0..4294967295
说明:
- 截断口径与
as_int16相同:超范围静默按二补码取低位,不抛。 - 越界读 →
ValueError(读过程数据失败 offset=N len=4 区长=M)。 - 其余失败形态同
as_int16(I 侧写RuntimeError、写越界PnSServiceError、非整数TypeError、通道没就绪PnSServiceError)。 - 不要求 4 字节对齐:
In[1].as_int32从偏移 1 取 4 字节,合法。
示例:
svc.process_data.Out[4].as_int32 = 10000
print(svc.process_data.In[1].as_int32) # 读 I:偏移 1 起 4 字节,合法、大端
print(svc.process_data.Out[4].as_int32) # 读 Q:刚写进去的 10000
item.as_int64 / as_uint64
一句话:8 字节,大端。In[i] 读 I,Out[i] 读写 Q。拷贝。
@property
def as_int64(self) -> int
@as_int64.setter
def as_int64(self, value: int)
@property
def as_uint64(self) -> int
@as_uint64.setter
def as_uint64(self, value: int)
参数:
value(int,仅写)— 按 64 位截断(& 0xFFFFFFFFFFFFFFFF)
返回值:
int— 有符号时是二补码解释;无符号时 0..18446744073709551615
说明:
- 64 位值在 Python 里就是普通
int,不会溢出成浮点。 - 越界读 →
ValueError(读过程数据失败 offset=N len=8 区长=M)。 - 其余失败形态同
as_int16。 - 8 字节的值要占 8 字节窗口:多数样例配置的 I 是 32 字节、Q 是 20 字节,落在两个方向都放得下,但别把它塞到偏移的尾巴上。
示例:
svc.process_data.Out[8].as_uint64 = 0x1122334455667788
print(hex(svc.process_data.Out[8].as_uint64)) # 0x1122334455667788
print(svc.process_data.In[8].as_uint64) # 读 I:控制器发来的那份,与上面无关
item.as_float / as_double
一句话:4 字节 / 8 字节,大端 IEEE754,位型原样进出,不做数值换算。拷贝。
@property
def as_float(self) -> float
@as_float.setter
def as_float(self, value)
@property
def as_double(self) -> float
@as_double.setter
def as_double(self, value)
参数:
value(int 或 float,仅写)— 收整数,会当成数值转换(1写进as_float就是1.0);不收str这类对象
返回值:
float— 大端 IEEE754 解出来的值
说明:
- 位型原样:写
NaN/inf不会被拦,读回来也是原样的位型 —— SDK 不做取值域检查、不做就近取偶。 - 越界读 →
ValueError(读过程数据失败 offset=N len=4 区长=M;as_double是len=8)。 - 其余失败形态同
as_int16。 as_float是 4 字节、as_double是 8 字节 —— 别按名字猜"double 一定是 8 字节所以 float 也是 8 字节"。
示例:
svc.process_data.Out[4].as_float = 1.5
print(svc.process_data.Out[4].as_float) # 1.5
svc.process_data.Out[8].as_double = -0.125
print(svc.process_data.Out[8].as_double) # -0.125
item.as_bool
一句话:整字节布尔,bit0 语义 —— 只有最低位是 1 才算真。In[i] 读 I,Out[i] 读写 Q。拷贝。
@property
def as_bool(self) -> bool
@as_bool.setter
def as_bool(self, value)
参数:
value(真值,仅写)— 真写0x01、假写0x00
返回值:
bool— 字节的最低位是否为 1
说明:
- 读:字节
0x02与0x80都判False,0x01判True。要按整字节 0 / 非 0 判真假,读content自己比较 —— 没有第二个"非 0 即真"的口子。 - 写是整字节写:写
True会把该字节置成0x01,同字节的 bit1–bit7 一并清掉;要按位读写请用get_bit()/set_bit()。 - 失败形态与
content一致(I 侧写RuntimeError;越界读 I 是PnSServiceError、越界读 Q 是ValueError;写越界PnSServiceError)。
示例:
svc.process_data.Out[1].as_bool = True
print(svc.process_data.Out[1].as_bool) # True:只看最低位
print(svc.process_data.Out[1].content) # 1:整字节被写成 0x01,bit1–bit7 一起清掉了
item.get_bit() / item.set_bit()
一句话:读 / 写该元素偏移处那一个字节的第 bit 位。get_bit 两个方向都能用,set_bit 只能对 Out。拷贝。
def get_bit(self, bit) -> bool
def set_bit(self, bit, value) -> None
参数:
bit(int)— 位号 0–7,0是最低位(LSB)value(真值,仅set_bit)— 目标位的新值
返回值:
bool— 该位当前是否为 1(get_bit)None—set_bit无返回值
说明:
- 位操作按字节寻址,与元素宽度无关:不管这个元素你打算按 1 / 2 / 4 / 8 字节哪种宽度解读,
bit始终指向元素偏移处的那一个字节,不会伸到相邻字节去。 - 位号不在 0–7 →
ValueError(位号必须在 0-7 之间: N)。 set_bit对 I 侧 →RuntimeError(I 侧只读)。get_bit越界读:I 侧PnSServiceError、Q 侧ValueError(与content同一口径);set_bit越界 →PnSServiceError。- 通道没就绪 →
PnSServiceError(NOT_AVAILABLE)。
示例:
svc.process_data.Out[0].set_bit(0, True) # 偏移 0 那个字节的 bit0
svc.process_data.Out[0].set_bit(7, False) # 同一个字节的 bit7
ready = svc.process_data.In[4].get_bit(2) # 偏移 4 那个字节的 bit2
int(item) / item 的比较
一句话:元素视图可以直接当整数用 —— int(item) 与 hex(item) 都取它那一字节。拷贝。
def __int__(self) -> int
def __index__(self) -> int
def __eq__(self, other) -> bool
返回值:
int— 与item.content同值
说明:
item == 5比的是content;item与True/False一律不相等(刻意不把布尔当整数比,免得item == True在 bit0 之外也悄悄成立)。- 两个元素视图互相比较,比的是"偏移 + 方向",不是那一格的字节值。
- 取值越界时的异常与
content相同。
示例:
item = svc.process_data.In[0]
print(int(item), hex(item), item == 0x12) # content / 十六进制 / 与整数比较
按地址读写(区域 + 字节 + 位)
不想自己算偏移、想照着工程软件里的地址写时用这一族:区域字母 + 字节偏移 + 位。
| 地址写法 | 是什么 | 本 SDK 的读写口 |
|---|---|---|
I0.0 / Q0.0 | 位地址:第 0 字节的第 0 位,位号只能 0–7 | read_bool / write_bool |
IB0 / QB0 | 字节(区域后可省类型字符:I0 与 IB0 同义) | 用 svc[offset] 读 I、svc[offset] = v 写 Q |
IW2 / QW2 | 字:2 字节,大端 | read_int16 / write_int16 |
ID4 / QD4 | 双字:4 字节,大端 | read_int32 / write_int32 |
- 不强制 2 / 4 对齐:
IW3/ID2都合法,偏移就是字节偏移。 - 不支持 M 区 / DB 区 →
PnSServiceError(INVALID_PARAM,不支持 M 地址: M0/不支持 DB 地址: DB1),不猜、不映射。 - 大小写不敏感(
i0.0/q5.7等价),首尾空白会被剥掉。 - Q 区读回的是本地影子值(你最近写进去的那份),I 区读的是控制器发来的当前值。
- 失败一律抛
PnSServiceError,具体文案见下面每个成员 —— 别只看错误码,INVALID_PARAM底下压着好几种不同的非法。
PnSService.parse_address()
一句话:只解析地址字符串,把 I0.0 / IB0 / IW2 / ID4 / Q… 拆成结构化的区域 + 偏移 + 位 + 宽度。不碰过程映像,不搬数据。
@staticmethod
def parse_address(address: str) -> PnSServiceAddress
参数:
address(str)— 地址文本,如"I0.0"/"IB0"/"IW2"/"QD4"
返回值:
PnSServiceAddress— 拆好的地址结构(字段见下方"相关结构");成功时slot/subslot恒为 1
说明:
-
想自己先校验地址、或要把地址换算成字节偏移时用它。解析成功不代表一定能读 —— 越界要等真去读写时才报。
-
解析失败一律
PnSServiceError(码INVALID_PARAM),触发面与文案:输入 文案 空串 / 全空白 地址不能为空M0/MB0不支持 M 地址: M0DB1/DBA不支持 DB 地址: DB1首字母不是 I / Q / M / D(如 X0)不支持的地址区域: X0位地址缺偏移或缺位号( I.0/I0.)地址格式非法 (位地址应为 I0.0 形式): I0.位号大于 7( I0.8)位号必须在 0-7 之间: I0.8缺偏移( I/IW)地址缺少偏移量: I偏移不是十进制无符号数( IA0/IB-1)字节偏移非法: IA0偏移大于 65535( IW65536)字节偏移超出范围: IW65536 -
length是宽度:位地址 = 1(跨 1 字节),B/ 无类型字符 = 1,W= 2,D= 4;bit为 −1 表示非位寻址。 -
address不是str→TypeError。
示例:
a = svc.parse_address("IW3") # 不强制 2 字节对齐
print(a.area, a.byte_offset, a.length, a.bit) # 1 3 2 -1
相关结构:
class PnSServiceArea: # 区域常量(普通类属性,不是枚举)
UNKNOWN = 0 # 未知
INPUT = 1 # I 区:只读(控制器 → 设备)
OUTPUT = 2 # Q 区:可写(设备 → 控制器)
MEMORY = 3 # M 区:不支持(解析失败)
DB = 4 # DB 区:不支持(解析失败)
class PnSServiceAddress: # parse_address() 的返回值(dataclass)
area # int:PnSServiceArea 的取值
slot # int:恒 1(I / Q 区固定;DB 编号属于 DB 区,本 SDK 不支持)
subslot # int:恒 1
byte_offset # int:字节偏移,相对本区域起点
bit # int:位号 0-7;-1 = 非位寻址
length # int:数据宽度(位 = 1 字节跨度,B / 无类型字符 = 1,W = 2,D = 4)
read_bool() / write_bool()
一句话:读写一个位 —— read_bool("I0.0") 读 I,write_bool("Q0.0", True) 写 Q。写是整字节读改写,只动目标位。拷贝(每次调用取一份当前字节)。
def read_bool(self, address: str) -> bool
def write_bool(self, address: str, value: bool) -> PnSErr
参数:
address(str)— 必须带位号,如"I0.0"/"Q5.7"value(真值,仅写)— 目标位的新值
返回值:
bool— 该位当前是否为 1(read_bool;Q 区读的是本地最近写进去的影子值)PnSErr— 成功码PnSErr.OK,且仅在那个字节已经落进过程映像时才返回 OK(write_bool)
说明:
- 地址不带位号(如
"IB0")→PnSServiceError(INVALID_PARAM,位地址必须带位号: IB0)。 - 地址本身非法 →
PnSServiceError(INVALID_PARAM,文案见parse_address()那张表)。 - 越界 →
PnSServiceError(INVALID_PARAM,地址越界: 偏移 N + 长度 M 超出过程映像区 K 字节)。 - 写 I 侧 →
PnSServiceError(NOT_AVAILABLE,I 区 (控制器 -> 设备) 只读, 不支持写),不静默丢。 - 会话没建立 →
PnSServiceError(NOT_INITIALIZED,本机服务会话未建立);通道没就绪 →PnSServiceError(NOT_AVAILABLE,过程数据未就绪, 无法读写 IO或过程数据未就绪, 无法提交 IO)。 - 写位是把同一个字节读回来、改位、再整字节写回去 —— 所以同字节的其它位不会被顺手清掉。
- 只支持位号 0–7;没有"位号 8 以上跨字节"的说法。
示例:
if svc.read_bool("I0.0"):
svc.write_bool("Q0.0", True)
else:
svc.write_bool("Q0.0", False)
read_int16() / write_int16()
一句话:读写一个字(2 字节,大端)。拷贝。
def read_int16(self, address: str) -> int
def write_int16(self, address: str, value: int) -> PnSErr
参数:
address(str)— 必须是W宽度且非位寻址,如"IW2"/"QW2"(IW3这类不对齐地址合法)value(int,仅写)— 按 16 位截断后按大端落字节(超范围静默取低位,不抛)
返回值:
int— 有符号值(最高位为 1 时按二补码解释:0xFFFE→ −2)。要无符号自己& 0xFFFFPnSErr— 成功码PnSErr.OK,且仅在那 2 字节已落进过程映像时才返回(write_int16)
说明:
- 地址宽度不对(
"IB0",或"I0.0"这种位地址)→PnSServiceError(INVALID_PARAM,字地址必须为 2 字节且非位寻址: IB0)。 - 越界 →
PnSServiceError(INVALID_PARAM,地址越界: 偏移 N + 长度 M 超出过程映像区 K 字节)。 - 写 I 侧 →
PnSServiceError(NOT_AVAILABLE,I 区 (控制器 -> 设备) 只读, 不支持写)。 - 其余失败(会话没建立
NOT_INITIALIZED、通道没就绪NOT_AVAILABLE)同read_bool()。 - 读回的是大端解出来的宿主整数,不需要你自己
struct.unpack。
示例:
recipe = svc.read_int16("IW2") # 有符号,大端
svc.write_int16("QW2", -2) # 落 FF FE
print(svc.read_int16("QW2")) # -2(读回本地影子)
read_int32() / write_int32()
一句话:读写一个双字(4 字节,大端)。拷贝。
def read_int32(self, address: str) -> int
def write_int32(self, address: str, value: int) -> PnSErr
参数:
address(str)— 必须是D宽度且非位寻址,如"ID4"/"QD4"(ID2这类不对齐地址合法)value(int,仅写)— 按 32 位截断后按大端落字节
返回值:
int— 有符号值(0xFFFFFFFE→ −2)。要无符号自己& 0xFFFFFFFFPnSErr— 成功码PnSErr.OK,且仅在 4 字节已落进过程映像时才返回(write_int32)
说明:
- 地址宽度不对 →
PnSServiceError(INVALID_PARAM,双字地址必须为 4 字节且非位寻址: IB0)。 - 越界 →
PnSServiceError(INVALID_PARAM,地址越界: 偏移 N + 长度 M 超出过程映像区 K 字节)。 - 写 I 侧 →
PnSServiceError(NOT_AVAILABLE,I 区 (控制器 -> 设备) 只读, 不支持写)。 - 其余失败同
read_int16()。 - 取不到无符号版:没有
read_uint32这类口,要无符号自己按位与。
示例:
counter = svc.read_int32("ID4")
svc.write_int32("QD4", 10000)
print(svc.read_int32("QD4")) # 10000(读回的是本地影子)
print(svc.read_int32("ID4") & 0xFFFFFFFF) # 无符号看法
整区入口(整段 / 短写 / 快照结构体)
这一节是"不映射"那条路:整段字节、按区域偏移的一段、快照结构体。全是拷贝。
svc.read()
一句话:读控制器 → 设备整段过程数据(I 区净荷),一次调用取一份。拷贝(每次新取,同一拍自洽)。
def read(self) -> bytes
返回值:
bytes— I 区净荷的当前内容;长度就是本方向净荷字节数
说明:
- 一次调用 = 一份快照:多个字段要一起用,就取这一次的字节,别分几次取。
- 通道没起来就如实抛。
- 会话没建立(没
start())→PnSServiceError(NOT_INITIALIZED,本机服务会话未建立)。 - 通道没就绪 →
PnSServiceError(NOT_AVAILABLE,过程数据未就绪, 无法读写 IO)。
示例:
frame = svc.read() # 一份 I 快照
present, recipe = frame[0], int.from_bytes(frame[1:3], "big")
print(len(frame), present, recipe)
pd.inputs / pd.Inputs
一句话:与 svc.read() 同源的 I 区快照,长在门面上。拷贝。
@property
def inputs(self) -> bytes
@property
def Inputs(self) -> bytes # 别名,同值
返回值:
bytes— I 区当前净荷;每次访问都新取一份
说明:
- 与
svc.read()逐字节一致,只是入口位置不同(svc.process_data.inputs)。 - 失败形态同
svc.read():NOT_INITIALIZED/NOT_AVAILABLE。 Inputs是对照语言的同名别名。注意方向:inputs在 Python 这里就是 I 区(控制器 → 设备),别被服务侧那两个反着的属性名带跑(见下文"名字反了")。
示例:
print(svc.process_data.inputs == svc.read()) # True
svc.write()
一句话:写设备 → 控制器整段过程数据(Q 区),允许短写。拷贝(把入参字节送进过程映像)。
def write(self, data: bytes) -> PnSErr
参数:
data(bytes)— 待写字节;从偏移 0 起覆盖,短写只覆盖前 N 字节,后面保持原样
返回值:
PnSErr— 成功码PnSErr.OK,且仅在这些字节已经落进过程映像时才返回 OK;调用方据此判别,不必靠读回值猜
说明:
- 超长如实抛,不静默截断(截掉的部分没送达就不算成功)。
- 空数据 →
PnSServiceError(INVALID_PARAM,写数据不能为空)。 - 越界 / 超长 →
PnSServiceError(INVALID_PARAM,地址越界: 偏移 N + 长度 M 超出过程映像区 K 字节)。 - 会话没建立 →
NOT_INITIALIZED;通道没就绪 →NOT_AVAILABLE(过程数据未就绪, 无法提交 IO,此时字节一个都没提交)。 - 写过之后
svc.output_committed为真(除非读的那一瞬间又有新的失败脏区)。 - 这里没有"提交调用"的概念 —— 每次
write()都是当场提交。
示例:
from darra_pns import PnSErr
rc = svc.write(b"\x11\x22\x33\x44") # 从偏移 0 起写 4 字节
if rc == PnSErr.OK:
print(svc.output_committed) # True:已落过程映像
pd.outputs / pd.Outputs
一句话:Q 区当前内容的影子读回;给属性赋值等于整段 / 短写。拷贝。
@property
def outputs(self) -> bytes
@outputs.setter
def outputs(self, value)
@property
def Outputs(self) -> bytes # 别名,同值;赋值同样转发
参数:
value(bytes / bytearray / memoryview,仅写)— 待写字节;短写只覆盖前 N 字节
返回值:
bytes— Q 区当前内容;取不到时是空字节串
说明:
- 读的是"你写出去那份"的回读(写什么读回什么)。没有会话时返回
b"",不抛。 - 写:赋值不是 bytes 家族 →
TypeError(outputs 必须为 bytes);空 →ValueError(写入不能为空);超过 65536 →ValueError(写入超过过程映像上限: N)。 - 往过程映像那一步失败时抛
PnSServiceError(NOT_AVAILABLE/NOT_INITIALIZED)。 - 要一次写多个不相邻的区间:自己合并成一段再一次写,或者每段各调一次 —— 每次调用都是一次独立提交。
示例:
svc.process_data.outputs = b"\x01\x00" # 短写:偏移 0 起覆盖前 2 字节
print(svc.process_data.outputs[:2]) # b'\x01\x00'
pd.copy_inputs_to() / pd.copy_to_outputs()
一句话:把 I 区快照拷进调用方缓冲 / 把调用方缓冲写进 Q 区,返回实际字节数。拷贝(两个方向都是)。
def copy_inputs_to(self, buf) -> int
def copy_to_outputs(self, src) -> int
参数:
buf(bytearray,仅copy_inputs_to)— 目标缓冲;按较小的一边拷src(bytes,仅copy_to_outputs)— 源字节;允许短写
返回值:
int— 实际拷贝 / 写入的字节数(copy_inputs_to是min(I 长度, len(buf));copy_to_outputs是len(src))
说明:
- 缓冲区太小不会报错,只拷得少 —— 返回值就是判据。
buf必须是bytearray(传memoryview/bytes都不收);src必须是bytes(传bytearray/memoryview都不收)。copy_to_outputs:超过 65536 →ValueError(写入超过过程映像上限: N);空 bytes 走到过程映像那一步会抛PnSServiceError(INVALID_PARAM,写数据不能为空)。- 底层提交失败(通道没就绪 / 会话没建立)→
PnSServiceError。 - 这两个口对应固定方向:
copy_inputs_to读 I、copy_to_outputs写 Q,没有反着来的版本。
示例:
buf = bytearray(4)
n = svc.process_data.copy_inputs_to(buf) # 读 I 到自己的缓冲
n = svc.process_data.copy_to_outputs(b"\x01\x02") # 写 Q,返回 2
svc.read_area() / svc.write_area()
一句话:按区域偏移读 / 写一段连续字节,一读一写各一次调用。拷贝。
def read_area(self, area, offset, length) -> bytes
def write_area(self, area, offset, data) -> PnSErr
参数:
area(int)— 区域:PnSServiceArea.INPUT(1)或.OUTPUT(2)offset(int)— 字节偏移,相对区域起点length(int)— 读多少字节(仅read_area)data(bytes)— 待写数据(非空,仅write_area)
返回值:
bytes— 按 offset / length 截出来的那一段(read_area)PnSErr— 成功码PnSErr.OK,且仅在这段字节已经落进过程映像时才返回(write_area)
说明:
read_area:区域不是 I / Q →PnSServiceError(INVALID_PARAM,区域非法: 3);offset/length为负 →INVALID_PARAM(offset / length 不能为负);越界 →INVALID_PARAM(区域越界: 偏移 N + 长度 M 超出过程映像区 K 字节);会话没建立 →NOT_INITIALIZED。write_area:写 I 区 →PnSServiceError(NOT_AVAILABLE,I 区 (控制器 -> 设备) 只读, 不支持写);区域非法 →INVALID_PARAM(区域非法: 3);空数据 →INVALID_PARAM(写数据不能为空);offset为负 →INVALID_PARAM(offset 不能为负);越界 →INVALID_PARAM(区域越界: 偏移 N + 长度 M 超出过程映像区 K 字节)。- 一次调用只写一段连续的字节;不相邻的多段就是多次调用(每次都是一次独立提交)。
data必须是bytes(传bytearray/memoryview都不收)。
示例:
from darra_pns import PnSServiceArea
part = svc.read_area(PnSServiceArea.INPUT, 4, 2) # 读 I 偏移 4 起 2 字节
rc = svc.write_area(PnSServiceArea.OUTPUT, 4, b"\x11") # 写 Q 偏移 4 起 1 字节,当场提交
svc.get_io()
一句话:一次调用同时取 Q 影子与 I 快照两个方向。拷贝(返回的是两个字节串)。
def get_io(self) -> Tuple[bytes, bytes]
返回值:
(Q 影子, I 快照)— 顺序是 Q 在前、I 在后(read_output()先、read_input()后)
说明:
- 想要"同一拍的两个方向"时用它,比分别调
svc.input/svc.output少一次判断。 - 会话没建立 →
PnSServiceError(NOT_INITIALIZED);通道没就绪 →PnSServiceError(NOT_AVAILABLE,过程数据未就绪, 无法读写 IO)。 - 日常读写请用
svc.read()/svc.write()/svc.process_data;这个方法更多是给"一次拿两方向"的调用点。
示例:
q_shadow, i_snapshot = svc.get_io() # 注意顺序:Q 在前
print(len(q_shadow), len(i_snapshot))
svc[offset] / svc[offset] = value
一句话:按字节偏移读 I 的一个字节、写 Q 的一个字节。拷贝。
def __getitem__(self, offset) -> int
def __setitem__(self, offset, value) -> None
参数:
offset(int)— 字节偏移,相对本方向净荷起点value(int,仅写)— 只写低 8 位
返回值:
int— 该字节(0..255,svc[offset])None— 赋值没有返回值,所以判别不了结果码;要判别返回值请用write_int16这类口
说明:
offset不是int→TypeError。offset为负 →PnSServiceError(INVALID_PARAM,偏移不能为负: N)。- 读越界 →
PnSServiceError(INVALID_PARAM,地址越界: 偏移 N 超出过程映像区 M 字节);写越界 →PnSServiceError(INVALID_PARAM,偏移越界: N)。 - 写值不是
int→PnSServiceError(INVALID_PARAM,字节值必须为 int)。 - 写之前会先查通道:没就绪 →
PnSServiceError(NOT_AVAILABLE,过程数据未就绪, 无法提交 IO);会话没建立 →NOT_INITIALIZED。 - 索引器是
In[i]/Out[i]底层走的那条路,但那里已经替你按方向分好了,日常直接用In[i]/Out[i]就好。
示例:
b = svc[0] # 读 I 偏移 0
svc[0] = 0x11 # 写 Q 偏移 0(当场提交)
read_struct() / write_struct()
一句话:不建映射、也不用 ctypes 结构体,用 struct 格式串一次读 / 写一整段 —— fmt 一律按大端解释。拷贝(快照)。
def read_struct(self, fmt=None) # 门面: svc.process_data.read_struct
def write_struct(self, fmt, *values) # 门面: svc.process_data.write_struct
# svc.read_struct(fmt) / svc.write_struct(fmt, *values) 是同一实现的转发
参数:
fmt— 三种形态:None/""(整段返回 bytes)、int(返回前 n 字节)、str(struct格式串,大端解包)values(仅写)— 按fmt打包的值;也可以只传一个bytes当整段载荷
返回值:
bytes或tuple—fmt为空给整段bytes;给str给解好的元组(read_struct)None—write_struct无返回值,成功即已提交
说明:
- 端序强制大端:格式串里带的
@/=/</>/!会被剥掉再换成>,所以"<Hi"与"Hi"结果一样。别指望写<能换成本机小端。 fmt为空串 → 返回整段bytes;传负数 →ValueError(读长度不能为负)。fmt不是合法格式串 →struct.error(例如bad char in struct format);参数个数不对 →struct.error(例如pack expected 2 items for packing (got 1))。- 值超出该格式的取值域 →
struct.error(例如'H' format requires 0 <= number <= 65535);这条路不按位宽截断,与Out[i].as_int16那族不同(那族超范围静默取低位)。 - 结构体大小非法(不大于 0,或超过 65536)→
ValueError(结构体大小非法: N)。 - I 区不足 →
ValueError(I 区不足: 需要 N 字节, 实际 M);写超过上限 →ValueError(结构体超过过程映像上限: N);写空 →ValueError(写入不能为空)。 - 会话没建立 →
PnSServiceError(NOT_INITIALIZED);通道没就绪 →PnSServiceError(NOT_AVAILABLE)。 - 这条路是快照:读一次解一次,不会跟着总线变;要 0 拷贝就用
inputs_mapping/outputs_mapping。
示例:
present, recipe = svc.read_struct("BH") # 大端解前 3 字节
head = svc.read_struct(8) # 前 8 字节 bytes
svc.write_struct("BH", 1, 2) # 大端打包写 Q(值必须在格式取值域内)
svc.write_struct(b"\x11\x22") # 整段载荷写 Q
svc.input / svc.output / output_committed
一句话:三个服务级属性 —— input 是 Q 影子的回读,output 是 I 区的快照,output_committed 是"本会话的 Q 是否已经落进过程映像"的判据。前两个是拷贝。
@property
def input(self) -> bytes # Q 影子回读(设备 → 控制器)
@property
def output(self) -> bytes # I 区快照(控制器 → 设备)
@property
def output_committed(self) -> bool
返回值:
bytes—input:你写出去那份 Q 的回读;output:控制器发来的 I 快照。没有会话时都返回b"",不抛bool—output_committed:本会话已成功把 Q 写进过程映像,且当前没有未提交的本地写
说明:
- 这三个是服务级属性,不是门面上的:读写 I 用
svc.read()/svc.process_data.inputs,读写 Q 用svc.write()/svc.process_data.outputs。 output_committed不是无条件为真:Q 从没写过、或提交失败留下脏区,都是False。"取到快照就算有效"的口径已经废除。stop()不复位这个标记(会话与映像内容都还在);close()复位。
示例:
svc.write(b"\x01\x02")
print(svc.output_committed) # True
print(svc.input[:2]) # b'\x01\x02':Q 影子回读
svc.input 是 Q,svc.output 是 I服务侧那两个属性的名字和 process_data 那套是反的 —— svc.input 读到的是你写出去的 Q(设备 → 控制器),svc.output 读到的是控制器发来的 I(控制器 → 设备)。
日常读写走 svc.read() / svc.write() / svc.process_data.In / Out 这套就够了;要用 svc.input / svc.output 之前先看一眼这里。同一个道理的还有 get_io():它按 (Q 影子, I 快照) 的顺序返回。
记录索引空间(非周期数据)
非周期数据走记录索引,索引号是分段的。本 SDK 只开放其中两段,两条路互不覆盖:
| 索引段 | 是什么 | 本 SDK 走哪 |
|---|---|---|
0x0000 – 0x7FFF | 用户 / 厂商区:控制器写进来、由设备侧保存的应用参数记录 | svc.get_records() / svc.records |
0x8000 起 | 标准索引区(期望实际标识、诊断、替代值等) | 本 SDK 不提供通用记录读口 |
0xAFF0 – 0xAFF4 | I&M 标识:I&M0(0xAFF0)与 I&M1–4(0xAFF1–0xAFF4) | svc.get_im_data() / svc.im_data |
svc.get_records() / svc.records
一句话:取用户区记录(索引 0x0000–0x7FFF)的快照。设备侧把控制器写来的记录保存下来,SDK 读出来给你看。只读观测。
def get_records(self) -> PnSServiceRecords
@property
def records(self) -> PnSServiceRecords # 同一实现
返回值:
PnSServiceRecords— dataclass:records(PnSServiceUserRecord列表,每条有slot/subslot/index/length/data_hex)、count、timestamp。没有记录就是空列表 +count = 0,不是失败
说明:
- 走服务控制面(不是过程数据通道),也不要求先
start()。 - 服务不可达 →
PnSServiceError(NO_DEVICE,原文无法连接服务 127.0.0.1:18840 (…))。 - 服务侧报错 / 响应不可解析 →
NOT_AVAILABLE(原文如服务返回 HTTP N: …、服务响应不是合法 JSON: …)。 - 字段缺失按 0 / 空处理,不虚构;
records不是列表时按空列表处理。 - I&M 不在这个口里:这里只收用户区那一段,
0xAFF0起的标识记录它永远看不到。 - 没有"从 SDK 写记录给控制器"的入口 —— 记录是控制器写、设备侧存、SDK 读。
- 写记录不会改变周期映像,读记录也不会 —— 与过程数据完全无关。
示例:
recs = svc.get_records()
print(recs.count, recs.timestamp)
for r in recs.records:
print(f"index=0x{r.index:04X} slot={r.slot} subslot={r.subslot} len={r.length} data={r.data_hex}")
svc.get_im_data() / svc.im_data
一句话:取设备标识 I&M 的观测快照 —— I&M 索引段(0xAFF0–0xAFF4)在本 SDK 里的唯一入口。
def get_im_data(self) -> PnSServiceImData
@property
def im_data(self) -> PnSServiceImData # 同一实现
返回值:
PnSServiceImData— dataclass:vendor_id/device_id、order_id/serial_number/hardware_revision/software_revision、station_name/product_name、im_supported/im14_supported、im_note、im1_tag_function/im1_tag_location/im2_date/im3_descriptor/im4_signature_hex、project_order_number/project_hardware_release/project_software_release
说明:
- 走服务控制面(不是过程数据通道),同样不要求先
start()。 - 失败形态与
get_records()相同:服务不可达NO_DEVICE、服务侧报错或响应不可解析NOT_AVAILABLE。 im14_supported为False表示 I&M1–4 这次没被带回来(字段为空串),不是"没有这个能力";订货号 / 序列号 / 软硬件版本等未接入原生栈的项,im_note里会如实写清楚。- 想按索引号自己读 I&M 记录 → 没有这种口;I&M 只从这个入口出,用户记录口永远看不到
0xAFF0段。 - 字段逐项含义见 诊断。
示例:
im = svc.im_data
print(f"vendor=0x{im.vendor_id:04X} device=0x{im.device_id:04X} station={im.station_name}")
print(im.im_supported, im.im14_supported)
print(im.im_note)
字节数怎么拿
长度由运行配置决定,不要写死 20 / 32(改过组态就变)。
| 想拿 | 走哪 |
|---|---|
| I 整段长度 | len(svc.read()) / len(svc.process_data.inputs) / svc.process_data.In.length |
| Q 整段长度 | len(svc.process_data.outputs) / svc.process_data.Out.length |
| 两个方向的配置长度 | svc.diag_snapshot.input_area_length / .output_area_length |
映射能用的窗口比上面那个配置长度还要小:I 侧少 2–6 字节(随内容),Q 侧固定少 3 字节。别自己减,映射失败时 ValueError 会把"需要 N / 可用 M"写给你。
In.length / Out.length 失败时静默返回 0没 start()、通道没起来的时候 length 返回 0,不抛异常。所以 0 既可能是"真的 0 字节",也可能是"读不到"—— 判断有没有起来请另看 svc.connected / svc.diag_snapshot.ok。