跳到主要内容

过程数据

通过 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 没有用户周期回调,调度(循环 / 定时器 / 线程)由你自己定。

Input / Output 与 I / Q 对着看

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

设备是固定单模块(槽 1 / 子槽 1),本 SDK 的过程数据入口没有任何收 slot / subslot 参数的地方。

结构体映射契约(0 拷贝那条路)
  • 必须 _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_typectypes.Structure 子类)— 必须是本身(传实例也抛 TypeError);必须 _pack_ = 1

返回值:

  • 结构体实例 — 落在 live 输入映像上的引用,不是快照;带一个 _live_owner 属性(钉住会话,避免只持有结构体时过程数据通道被摘掉)

说明:

  • start() 之后调。下列情况一律报错,不返回半成品:

    情况异常消息要点
    struct_type 不是 ctypes.Structure 子类(含传实例)TypeError提示用 read_struct 取快照
    _pack_ 不是 1TypeError结构体必须 _pack_ = 1 (无对齐填充)
    结构体大小为 0,或大于 65536ValueError结构体大小非法: N
    结构体大于本次可用窗口ValueError结构体大于输入过程映像: 需要 N, 可用 M
    净荷指针为 0RuntimeError过程数据输入映像不可用: 净荷指针为空 (0x0)
    会话未建立(没 start()PnSServiceErrorNOT_INITIALIZED本机服务会话未建立
    通道没就绪 / 该方向无映像PnSServiceErrorNOT_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_typectypes.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 — 过程数据数组对象,靠下标取元素(见下一条);Ininn 是同一个实例(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 不是 intTypeError
  • offset 小于 0 → ValueError偏移不能为负: N)。
  • In[i] = v(I 侧赋值)→ RuntimeErrorProcessData.In 只读 I (控制器→设备); 写请用 ProcessData.Out[i].content)。
  • Out[i] = v 会把整字节写进 Q 映像(不是改位),通道没就绪时抛 PnSServiceErrorNOT_AVAILABLE)。
  • 偏移越界的异常类型按方向不同,这是刻意的:读 I 的单字节走服务索引器,越界是 PnSServiceErrorINVALID_PARAM地址越界: 偏移 N 超出过程映像区 M 字节);读 Q 的单字节和两个方向的多字节读越界都是 ValueError读过程数据越界: offset=N 超出 Q 区 M 字节 / 读过程数据失败 offset=N len=L 区长=M);写越界是 PnSServiceErrorINVALID_PARAM)。
  • 多字节写是要么全写要么不写:先按净荷窗口核对"偏移 + 宽度",越界就在任何字节落地之前抛 PnSServiceErrorINVALID_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)

说明:

  • 写只对 OutIn[i].content = vRuntimeError(I 侧只读),异常在任何字节被写到之前就抛出来了。
  • 写值不是 intTypeError
  • 读越界:I 侧抛 PnSServiceErrorINVALID_PARAM地址越界: 偏移 N 超出过程映像区 M 字节);Q 侧抛 ValueError读过程数据越界: offset=N 超出 Q 区 M 字节)。
  • 写越界 → PnSServiceErrorINVALID_PARAM偏移越界: N)。
  • 通道没就绪:读的异常类型同样按方向分 —— I 侧抛 PnSServiceError(会话没建立是 NOT_INITIALIZED,会话在、通道没就绪是 NOT_AVAILABLE);Q 侧读的是影子,会话没建立时影子为空,抛的是上一条那条 ValueError(不是 PnSServiceError)。写抛 PnSServiceErrorNOT_AVAILABLE过程数据未就绪, 无法提交 IO)。
  • Contentcontent 是同一个属性(对外是给对照语言用的同名别名)。
  • 一次取值 = 一份新的当前字节。同一元素上连着取两个成员,两次之间可能已经过了一个总线周期。

示例:

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_uint8content 同值同字节。拷贝

@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 再落字节。-1255 写下去是同一个字节。
  • 失败形态与 content 完全一致:I 侧写 RuntimeError;越界读 I 是 PnSServiceError、越界读 Q 是 ValueError;写越界是 PnSServiceError;值不是 intTypeError
  • 不要求对齐,也不与元素宽度绑定 —— 偏移是哪儿就取哪儿那一个字节。

示例:

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 = -2as_int16 = 0xFFFE 落的是同一对字节。
  • 写值不是 intTypeError
  • 越界读(偏移 + 2 超过本方向净荷)→ ValueError读过程数据失败 offset=N len=2 区长=M),I / Q 两个方向同一口径。
  • I 侧写 → RuntimeError;Q 侧写越界 → PnSServiceErrorINVALID_PARAM);通道没就绪 → PnSServiceErrorNOT_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 区长=Mas_doublelen=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

说明:

  • 读:字节 0x020x80 都判 False0x01True。要按整字节 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
  • Noneset_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
  • 通道没就绪 → PnSServiceErrorNOT_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 比的是 contentitemTrue / 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–7read_bool / write_bool
IB0 / QB0字节(区域后可省类型字符:I0IB0 同义)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 区PnSServiceErrorINVALID_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 地址: M0
    DB1 / 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 不是 strTypeError

示例:

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且仅在那个字节已经落进过程映像时才返回 OKwrite_bool

说明:

  • 地址不带位号(如 "IB0")→ PnSServiceErrorINVALID_PARAM位地址必须带位号: IB0)。
  • 地址本身非法 → PnSServiceErrorINVALID_PARAM,文案见 parse_address() 那张表)。
  • 越界 → PnSServiceErrorINVALID_PARAM地址越界: 偏移 N + 长度 M 超出过程映像区 K 字节)。
  • 写 I 侧 → PnSServiceErrorNOT_AVAILABLEI 区 (控制器 -> 设备) 只读, 不支持写),不静默丢。
  • 会话没建立 → PnSServiceErrorNOT_INITIALIZED本机服务会话未建立);通道没就绪 → PnSServiceErrorNOT_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)。要无符号自己 & 0xFFFF
  • PnSErr — 成功码 PnSErr.OK,且仅在那 2 字节已落进过程映像时才返回(write_int16

说明:

  • 地址宽度不对("IB0",或 "I0.0" 这种位地址)→ PnSServiceErrorINVALID_PARAM字地址必须为 2 字节且非位寻址: IB0)。
  • 越界 → PnSServiceErrorINVALID_PARAM地址越界: 偏移 N + 长度 M 超出过程映像区 K 字节)。
  • 写 I 侧 → PnSServiceErrorNOT_AVAILABLEI 区 (控制器 -> 设备) 只读, 不支持写)。
  • 其余失败(会话没建立 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)。要无符号自己 & 0xFFFFFFFF
  • PnSErr — 成功码 PnSErr.OK,且仅在 4 字节已落进过程映像时才返回(write_int32

说明:

  • 地址宽度不对 → PnSServiceErrorINVALID_PARAM双字地址必须为 4 字节且非位寻址: IB0)。
  • 越界 → PnSServiceErrorINVALID_PARAM地址越界: 偏移 N + 长度 M 超出过程映像区 K 字节)。
  • 写 I 侧 → PnSServiceErrorNOT_AVAILABLEI 区 (控制器 -> 设备) 只读, 不支持写)。
  • 其余失败同 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())→ PnSServiceErrorNOT_INITIALIZED本机服务会话未建立)。
  • 通道没就绪 → PnSServiceErrorNOT_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;调用方据此判别,不必靠读回值猜

说明:

  • 超长如实抛,不静默截断(截掉的部分没送达就不算成功)。
  • 空数据 → PnSServiceErrorINVALID_PARAM写数据不能为空)。
  • 越界 / 超长 → PnSServiceErrorINVALID_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 家族 → TypeErroroutputs 必须为 bytes);空 → ValueError写入不能为空);超过 65536 → ValueError写入超过过程映像上限: N)。
  • 往过程映像那一步失败时抛 PnSServiceErrorNOT_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_tomin(I 长度, len(buf))copy_to_outputslen(src)

说明:

  • 缓冲区太小不会报错,只拷得少 —— 返回值就是判据。
  • buf 必须是 bytearray(传 memoryview / bytes 都不收);src 必须是 bytes(传 bytearray / memoryview 都不收)。
  • copy_to_outputs:超过 65536 → ValueError写入超过过程映像上限: N);空 bytes 走到过程映像那一步会抛 PnSServiceErrorINVALID_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 → PnSServiceErrorINVALID_PARAM区域非法: 3);offset / length 为负 → INVALID_PARAMoffset / length 不能为负);越界 → INVALID_PARAM区域越界: 偏移 N + 长度 M 超出过程映像区 K 字节);会话没建立 → NOT_INITIALIZED
  • write_area:写 I 区 → PnSServiceErrorNOT_AVAILABLEI 区 (控制器 -> 设备) 只读, 不支持写);区域非法 → INVALID_PARAM区域非法: 3);空数据 → INVALID_PARAM写数据不能为空);offset 为负 → INVALID_PARAMoffset 不能为负);越界 → 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 少一次判断。
  • 会话没建立 → PnSServiceErrorNOT_INITIALIZED);通道没就绪 → PnSServiceErrorNOT_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 不是 intTypeError
  • offset 为负 → PnSServiceErrorINVALID_PARAM偏移不能为负: N)。
  • 读越界 → PnSServiceErrorINVALID_PARAM地址越界: 偏移 N 超出过程映像区 M 字节);写越界 → PnSServiceErrorINVALID_PARAM偏移越界: N)。
  • 写值不是 intPnSServiceErrorINVALID_PARAM字节值必须为 int)。
  • 写之前会先查通道:没就绪 → PnSServiceErrorNOT_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 字节)、strstruct 格式串,大端解包)
  • values(仅写)— 按 fmt 打包的值;也可以只传一个 bytes 当整段载荷

返回值:

  • bytestuplefmt 为空给整段 bytes;给 str 给解好的元组(read_struct
  • Nonewrite_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 区不足 → ValueErrorI 区不足: 需要 N 字节, 实际 M);写超过上限 → ValueError结构体超过过程映像上限: N);写空 → ValueError写入不能为空)。
  • 会话没建立 → PnSServiceErrorNOT_INITIALIZED);通道没就绪 → PnSServiceErrorNOT_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

一句话:三个服务级属性 —— inputQ 影子的回读,outputI 区的快照,output_committed 是"本会话的 Q 是否已经落进过程映像"的判据。前两个是拷贝

@property
def input(self) -> bytes # Q 影子回读(设备 → 控制器)
@property
def output(self) -> bytes # I 区快照(控制器 → 设备)
@property
def output_committed(self) -> bool

返回值:

  • bytesinput:你写出去那份 Q 的回读;output:控制器发来的 I 快照。没有会话时都返回 b""不抛
  • booloutput_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 走哪
0x00000x7FFF用户 / 厂商区:控制器写进来、由设备侧保存的应用参数记录svc.get_records() / svc.records
0x8000标准索引区(期望实际标识、诊断、替代值等)本 SDK 不提供通用记录读口
0xAFF00xAFF4I&M 标识:I&M0(0xAFF0)与 I&M1–4(0xAFF10xAFF4svc.get_im_data() / svc.im_data

svc.get_records() / svc.records

一句话:取用户区记录(索引 0x00000x7FFF)的快照。设备侧把控制器写来的记录保存下来,SDK 读出来给你看。只读观测

def get_records(self) -> PnSServiceRecords
@property
def records(self) -> PnSServiceRecords # 同一实现

返回值:

  • PnSServiceRecords — dataclass:recordsPnSServiceUserRecord 列表,每条有 slot / subslot / index / length / data_hex)、counttimestamp。没有记录就是空列表 + count = 0不是失败

说明:

  • 走服务控制面(不是过程数据通道),也不要求先 start()
  • 服务不可达 → PnSServiceErrorNO_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 索引段(0xAFF00xAFF4)在本 SDK 里的唯一入口

def get_im_data(self) -> PnSServiceImData
@property
def im_data(self) -> PnSServiceImData # 同一实现

返回值:

  • PnSServiceImData — dataclass:vendor_id / device_idorder_id / serial_number / hardware_revision / software_revisionstation_name / product_nameim_supported / im14_supportedim_noteim1_tag_function / im1_tag_location / im2_date / im3_descriptor / im4_signature_hexproject_order_number / project_hardware_release / project_software_release

说明:

  • 走服务控制面(不是过程数据通道),同样不要求先 start()
  • 失败形态与 get_records() 相同:服务不可达 NO_DEVICE、服务侧报错或响应不可解析 NOT_AVAILABLE
  • im14_supportedFalse 表示 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