过程数据
过程数据就是 I / Q 两段过程映像,服务每周期自动收发,不用你推。SDK 侧有两条路。
结构体映射把整幅映像按 #[repr(C, packed)] 结构体钉在共享区上。读写字段就是读写过程映像本身(0 拷贝,持续控制用)。
按偏移 / 按项的类型化读写每次调用取一小段解释给你(拷贝,零星读写用)。
写 Q(设备 → 控制器),读 I(控制器 → 设备)。I 区只读,控制器每周期整段覆盖它。 节拍由服务和内核完成,SDK 没有用户周期回调 —— 调度由你自己定。
GSDML 里那 20 字节的 Input(设备 → 控制器)就是 SDK 的 Q;GSDML 里那 32 字节的 Output(控制器 → 设备)就是 SDK 的 I。
两个名字是两套视角:GSDML 站在控制器那边命名,SDK 站在设备这边命名,指的是同一个方向。
设备是固定单模块:槽位 1、子槽位 1。SDK 没有任何入口收槽位 / 子槽位参数 —— 也没有第二个模块要选。
- 只有整幅映像的映射是 0 拷贝:
inputs_mapping::<T>()/outputs_mapping::<T>(),以及字节形态的inputs_slice()/outputs_slice()。 - 其余入口都不是 0 拷贝。
in_*/out_*每次调用拷一段;in_item()/out_item()拿到的句柄每次读写都是一次会话往返;read()/read_area()/read_struct()返回的是快照。 T必须#[repr(C, packed)](对齐为 1)。运行时先查align_of::<T>() == 1,不合格直接ServiceError::InvalidParam。- 多字节一律大端(PROFINET 网络序)。本 SDK 不做任何字节颠倒。
- bool 统一 bit0 语义:
0x01是true;0x02/0x80都是false。 - I 区只读。写 I 区是真的
Err,变体是ServiceError::OutputReadOnly—— 不是 panic,也不是静默丢弃。 - 映射和句柄都借住
PnSService。它们在的时候close()(要&mut self)借不到 —— 这是借用检查器给的保证,不是运行期检查。
使用案例
日常使用
① 结构体映射(0 拷贝,持续用):结构体定义好,映射一次,之后当变量用。unsafe 只出现在取映射那一次。
use darra_pns::{PnSService, ServiceResult};
#[repr(C, packed)]
struct FromPlc { present: u8, recipe: i16 } // I:控制器 → 设备
#[repr(C, packed)]
struct ToPlc { ready: u8, busy: u8 } // Q:设备 → 控制器(I / Q 分两套结构体)
fn main() -> ServiceResult<()> {
let device = PnSService::new(); // 构造不连总线,不占实时连接
device.start()?; // start 才建立过程数据通道
let pd = device.process_data();
let inp = unsafe { pd.inputs_mapping::<FromPlc>()? }; // &FromPlc:映射一次,只读
let outp = unsafe { pd.outputs_mapping::<ToPlc>()? }; // &mut ToPlc:可写
outp.ready = 1; // 写下的下个周期发出,不用提交调用
let present = inp.present; // 读到的永远是当前周期(packed 字段先拷到局部变量)
println!("{present}");
device.stop()
}
② 按偏移取一个值(一次性):不想定义结构体,或只要一段字节 / 一个值。
let pd = device.process_data();
let b: u8 = pd.in_u8(0)?; // I 区偏移 0 的 1 字节
let recipe = pd.in_i16(2)?; // I 区偏移 2 的大端 Int16
pd.out_u16(4, 123)?; // Q 区偏移 4 的大端 UInt16
③ 按项取元素(下标寻址,一次性):给一个下标,拿一个句柄,再选宽度解释它。
let pd = device.process_data();
let ready = pd.in_item(6).as_bool()?; // I 区第 6 字节,bit0 判真
let pos = pd.in_item(2).as_i32()?; // I 区第 2 字节起 4 字节,大端
pd.out_item(0).set_u16(0x000F)?; // Q 区第 0 字节起 2 字节
pd.out_item(0).set_bit(0, true)?; // 只置 bit0,同字节其余位不动
字符串字段在过程映像里是定长字节(尾部补 0)。一次读出该段字节再转 &str。
let raw = device.read()?; // I 区一份快照
let head = &raw[..raw.len().min(16)]; // 区短于 16 字节也不 panic
let part_no = std::str::from_utf8(head).unwrap_or("").trim_end_matches('\0');
周期变化
同一份映射放进你自己的循环直接用(映射只在循环外取一次;服务每周期搬运)。
while running { // 你自己的周期任务,节拍自定
let recipe = i16::from_be(inp.recipe); // 线上大端字节 → 宿主值,不转就是错位值
outp.ready = (recipe as u8) + 1; // 读当前拍 → 算 → 写下拍发出
std::thread::sleep(std::time::Duration::from_millis(10));
}
循环里不必重新映射,也不该重新映射(原因见 映射起点不是常量)。SDK 没有用户周期回调 —— 调度(循环 / 定时器 / 线程)由你自己定;events() 上的是运行态类事件(见 事件)。
结构体映射(零拷贝)
把整幅过程映像按 T 的布局直接解释,T 必须是 #[repr(C, packed)]。这是本 SDK 唯一的 0 拷贝形态(连同 *_slice 一起)。
映射窗口从净荷第 0 字节起(净荷起点每次现算,见 映射起点不是常量)。要映射映像中段的字段,在结构体开头放一段占位数组(_skip: [u8; k])顶开;或者改用 read_area / byte_at / in_item 按偏移取。
也可以自己包一层:ProcessData::new(&device),等价于 device.process_data(),不启动从站、不发请求。
inputs_mapping<T>()
把 I 区(控制器 → 设备)钉成 T,返回只读引用。映射一次,服务刷新后字段自己变 —— 不是快照。
pub unsafe fn inputs_mapping<T>(&self) -> ServiceResult<&'a T>
返回值:
ServiceResult<&'a T>— 钉在共享区净荷上的只读引用(0 拷贝)。'a是这个ProcessData借住PnSService的那一段生命周期,所以引用活不过服务对象
说明:
unsafe契约:T的字段顺序必须与线上字节逐字节一致。align_of::<T>() != 1(漏写packed)→ServiceError::InvalidParam("结构体对齐不是 1:请用 #[repr(C, packed)]")。这一条最先判,会话没建立、没start()也照样报这条。- 通道没建好 →
ServiceError::SharedMemory("过程数据输入映像不可用")。 size_of::<T>() == 0或大于净荷窗口 →ServiceError::InvalidParam("结构体大于输入过程映像")。- 净荷窗口长度是每次调用现算的(随区内容变化,见 映射起点不是常量),所以同一个
T「刚才还放得下、现在放不下」是可能的。 - 写它编译不过:签名是
&T,不是&mut T。I 区由控制器每周期覆盖,写进去也静默无效 —— 所以类型层面直接堵掉。 - packed 字段不能直接取引用:
println!("{}", inp.recipe)编译报错。先let v = inp.recipe;拷到局部变量再用。 - 结构体里只能放定宽字段(整数、浮点、
[u8; N])。String/Vec/ 引用会把指针叠在过程数据上,对齐门拦不住这种错。
示例:
#[repr(C, packed)]
struct FromPlc { present: u8, recipe: i16, counter: u32 }
let pd = device.process_data();
let inp: &FromPlc = unsafe { pd.inputs_mapping::<FromPlc>()? };
let recipe = inp.recipe; // 线上大端字节,当宿主 i16 用要自己转(见「大端打包 / 解包」)
let present = inp.present;
outputs_mapping<T>()
把 Q 区(设备 → 控制器)钉成 T,返回可写引用。改字段即改过程映像,下个周期发出。
pub unsafe fn outputs_mapping<T>(&self) -> ServiceResult<&'a mut T>
返回值:
ServiceResult<&'a mut T>— 可写引用,钉在净荷上(0 拷贝)
说明:
- 对齐门、尺寸门、
unsafe契约与inputs_mapping相同,只有报错文案换成输出侧:ServiceError::InvalidParam("结构体大于输出过程映像")、ServiceError::SharedMemory("过程数据输出映像不可用")。 - 别名
&mut是未定义行为:同一映像同一时刻只能有一个可写引用(或一个可写切片)。同一句柄取两次、两个句柄各取一个,都算别名。用作用域把它们串行化。 - 写进字段不需要任何「提交」调用 —— 字段就在过程映像里。
- 结构体放不下的字段可以按偏移补(
out_*/out_item),两条路改的是同一段映像。
示例:
#[repr(C, packed)]
struct ToPlc { ready: u8, busy: u8 }
let pd = device.process_data();
let outp: &mut ToPlc = unsafe { pd.outputs_mapping::<ToPlc>()? };
outp.ready = 1; // 下个周期发出
outp.busy = 0;
inputs_slice() / outputs_slice()
不经过结构体,直接取钉在净荷上的字节切片。
pub unsafe fn inputs_slice(&self) -> ServiceResult<&'a [u8]> // I 整段净荷,只读
pub unsafe fn outputs_slice(&self) -> ServiceResult<&'a mut [u8]> // Q 整段净荷,可写
返回值:
&'a [u8]/&'a mut [u8]— 钉在净荷上的切片,长度是净荷窗口长度(比区长度小,见 映射起点不是常量)
说明:
- 与
inputs_mapping/outputs_mapping是同一份 0 拷贝路径,只是视图形态是裸字节。 - 对齐门不适用(没有
T),但尺寸门换成:净荷窗口 ≤ 0 时 →ServiceError::SharedMemory("过程数据输入映像不可用")/("过程数据输出映像不可用")。 outputs_slice()与outputs_mapping()共享同一份独占契约:同一映像同时只能一个可写视图。unsafe是真的要小心:指针只在unmap/Drop之前有效,而且服务会并发改这些字节(I 侧尤其如此)。
示例:
let pd = device.process_data();
let frame: &[u8] = unsafe { pd.inputs_slice()? }; // 不拷贝
let x_words = unsafe { pd.outputs_slice()? }; // 改字节即写 Q
if frame.len() >= 2 {
x_words[0] = frame[0].wrapping_add(1);
}
live_io_payload(output)
裸指针出口:给一个 (*mut u8, usize),指针指向净荷起点,长度是净荷窗口字节数。
pub fn live_io_payload(&self, output: bool) -> ServiceResult<(*mut u8, usize)>
参数:
output(bool) —false= I 区(控制器 → 设备),true= Q 区(设备 → 控制器)
返回值:
(*mut u8, usize)— 净荷首指针 + 净荷窗口字节数
说明:
ProcessData::inputs_slice()/outputs_slice()就是拿这一对指针包出来的,所以它的存活期规则一样。- 不是
unsafe fn,但拿到的裸指针解引用仍是unsafe—— 而且它不查任何对齐(没有T可查)。 - 通道没建好 →
ServiceError::SharedMemory("过程数据输入映像不可用")/("过程数据输出映像不可用")。
示例:
let (ptr, len) = device.live_io_payload(true)?; // Q 区净荷
println!("Q 净荷 {len} 字节");
let q: &mut [u8] = unsafe { std::slice::from_raw_parts_mut(ptr, len) };
映射起点不是常量
「映射从净荷第 0 字节起」这句里的净荷起点,是每次取映射时按当前区内容现算的:
- I 侧:先剥 2 字节 IOPS 前缀,再看净荷开头连续有几个
0x80,最多再剥 4 个。 - Q 侧:固定剥 3 字节。
所以 I 侧能用的窗口比区长度小 2–6 字节,具体小多少随数据内容变化;Q 侧固定小 3 字节。
生命周期那一半是真的(借用结束 / unmap / 关通道之后失效)。地址稳定性不是 —— 不要写任何「地址恒定」的假设,也不要在跨周期的地方缓存指针。
「映射放循环外取一次」仍然是推荐用法,但它换来的是少一次解析,代价是:起点一旦漂移,你手上的引用就指在错位的地方 —— 这不会报 Err,尺寸门也拦不住(结构体小的时候照样「放得下」,只是读出来的值是错位的)。
直接访问(不映射也能用)
下面这组与结构体映射彼此独立:不映射可以单独用,映射了也常并用(结构体放不下的字段用它解)。
这些入口都不是 0 拷贝。in_* / out_* 每次调用拷一小段;按项句柄每次读写走一次会话往返。
in_item(offset) / out_item(offset)
取一个按项句柄:给一个字节下标,拿到句柄,之后用类型化方法读它、写它。
pub fn in_item(&self, offset: usize) -> ProcessDataItem<'a> // I 区:只读
pub fn out_item(&self, offset: usize) -> ProcessDataItem<'a> // Q 区:可读可写
参数:
offset(usize) — 相对本方向区起点的字节偏移
返回值:
ProcessDataItem<'a>— 轻量句柄(Clone + Copy)。它只持「服务 + 偏移 + 方向」三样,不存任何数据字节
说明:
- 不返回
Result:造句柄是纯本地操作,不读总线、不发请求、不需要start()。真正落会话的是句柄上的每次读写调用。 - 句柄不是快照:每次读 / 写都当场经服务读写落在过程映像上。同一个偏移的两次
content()之间,服务可能已经换过值。 - 句柄不是钉页:它不承担
*_slice那两条生命周期 / 指针有效期义务,也不是unsafe。要「源头变、字段自己变」请用结构体映射。 - 偏移越界不在造句柄时报,在读写时按
OutOfRange报(不同宽度的上限不一样,见下一条)。 - 句柄借住
PnSService:句柄在的时候close()借不到。
相关结构:
pub struct ProcessDataItem<'a> { /* 服务 + 偏移 + 方向;不存数据字节 */ }
impl<'a> ProcessDataItem<'a> {
pub fn offset(&self) -> usize; // 本项字节偏移(相对区起点)
pub fn is_writable(&self) -> bool; // true = Q 区项(可写);false = I 区项(只读)
}
impl<'a> ProcessDataItem<'a> {
// ── 读:方向由句柄决定(in_item 读 I 区,out_item 读 Q 区待发影子)──
pub fn content(&self) -> ServiceResult<u8>; // 1 字节原值,无符号
pub fn as_i8(&self) -> ServiceResult<i8>; // 1 字节,有符号
pub fn as_u8(&self) -> ServiceResult<u8>; // 1 字节,无符号(同 content)
pub fn as_i16(&self) -> ServiceResult<i16>; // 2 字节,有符号,大端
pub fn as_u16(&self) -> ServiceResult<u16>; // 2 字节,无符号,大端
pub fn as_i32(&self) -> ServiceResult<i32>; // 4 字节,有符号,大端
pub fn as_u32(&self) -> ServiceResult<u32>; // 4 字节,无符号,大端
pub fn as_i64(&self) -> ServiceResult<i64>; // 8 字节,有符号,大端
pub fn as_u64(&self) -> ServiceResult<u64>; // 8 字节,无符号,大端
pub fn as_f32(&self) -> ServiceResult<f32>; // 4 字节,IEEE754,大端
pub fn as_f64(&self) -> ServiceResult<f64>; // 8 字节,IEEE754,大端
pub fn as_bool(&self) -> ServiceResult<bool>; // 1 字节,bit0 语义
pub fn get_bit(&self, bit: u8) -> ServiceResult<bool>;// 本字节第 bit 位,0 = LSB
}
impl<'a> ProcessDataItem<'a> {
// ── 写:只有 out_item 的句柄能调;in_item 的句柄一律 OutputReadOnly ──
pub fn set_content(&self, value: u8) -> ServiceResult<()>;
pub fn set_i8(&self, value: i8) -> ServiceResult<()>; // 1 字节
pub fn set_u8(&self, value: u8) -> ServiceResult<()>; // 1 字节
pub fn set_i16(&self, value: i16) -> ServiceResult<()>; // 2 字节,大端
pub fn set_u16(&self, value: u16) -> ServiceResult<()>; // 2 字节,大端
pub fn set_i32(&self, value: i32) -> ServiceResult<()>; // 4 字节,大端
pub fn set_u32(&self, value: u32) -> ServiceResult<()>; // 4 字节,大端
pub fn set_i64(&self, value: i64) -> ServiceResult<()>; // 8 字节,大端
pub fn set_u64(&self, value: u64) -> ServiceResult<()>; // 8 字节,大端
pub fn set_f32(&self, value: f32) -> ServiceResult<()>; // 4 字节,IEEE754,大端
pub fn set_f64(&self, value: f64) -> ServiceResult<()>; // 8 字节,IEEE754,大端
pub fn set_bool(&self, value: bool) -> ServiceResult<()>;// 1 字节,true 写 1 / false 写 0
pub fn set_bit(&self, bit: u8, value: bool) -> ServiceResult<()>; // 读-改-写本字节第 bit 位
}
示例:
let pd = device.process_data();
let flag = pd.in_item(6).as_bool()?; // I 区第 6 字节,bit0
let raw = pd.in_item(6).content()?; // 同一个字节的原值
let item = pd.out_item(2);
item.set_i16(123)?; // Q 区偏移 2 起 2 字节
item.set_bit(0, true)?; // 再置 bit0
content() / as_() / set_()(按项类型化读写)
按句柄的偏移和方向,用指定宽度读写。读 I 区、或读 Q 区的待发影子;写只对 Q 区项成立。
pub fn content(&self) -> ServiceResult<u8>
pub fn as_i16(&self) -> ServiceResult<i16> // 其余 as_* 同形,见上方「相关结构」
pub fn set_i16(&self, value: i16) -> ServiceResult<()>
返回值:
ServiceResult<u8>/ServiceResult<i16>/ … — 该宽度的大端解释结果ServiceResult<()>—Ok⟺ 写入已落到共享区过程映像(当场提交,不按节拍延迟)
说明:
- 字节序:多字节一律大端(PROFINET 网络序)。
as_f32/as_f64是 IEEE754,同样大端。 - 宽度上限按方向不同:
- I 区项读多宽都行,只要
offset + 宽度 ≤ 输入区长度(不剥前缀)。 - Q 区项读也一样,写则按 Q 净荷窗口卡:
offset + 宽度 ≤ 输出区长度 − 3。超了 →ServiceError::OutOfRange(offset, 宽度, 上限)。
- I 区项读多宽都行,只要
- 只读项上调用任何
set_*→ServiceError::OutputReadOnly。这道门在会话访问之前:没start()也先报它。 - 未建立会话 →
ServiceError::NotConnected;会话在、没start()→ServiceError::NotStarted。 as_bool()只认 bit0:0x01是true,0x02/0x80都是false。本 SDK 没有「非 0 即真」的第二个入口 —— 要按整字节 0 / 非 0 判,读content()自己比。set_bool()写的是整字节0x01/0x00,不是位。- 每个
as_*/set_*都是一次独立的会话读写。两次调用之间服务可能已经换过值 —— 要同一拍,用一次read()/read_area()取一份快照。
示例:
let pd = device.process_data();
let ready = pd.in_item(0).as_bool()?; // 0x02 / 0x80 都读 false
let as_byte = pd.in_item(0).content()?; // 想按整字节判真假就读这个
pd.out_item(4).set_f32(1.5)?; // 大端 IEEE754
pd.out_item(0).set_bool(true)?; // 写整字节 1,不是位
get_bit(bit) / set_bit(bit, value)(按项位寻址)
按位读写句柄偏移处的那个字节。
pub fn get_bit(&self, bit: u8) -> ServiceResult<bool>
pub fn set_bit(&self, bit: u8, value: bool) -> ServiceResult<()>
参数:
bit(u8) — 位号0-7,0 = LSBvalue(bool) — 置位 / 清位
返回值:
ServiceResult<bool>— 该位当前值ServiceResult<()>— 置位成功(当场提交)
说明:
- 位访问器是按字节的,不是按宽度:
bit只寻址句柄偏移处的那一个字节,跟这个项你打算按什么宽度读无关。 bit > 7→ServiceError::InvalidParam("位号必须在 0-7 之间")。这道门先于任何会话访问,没start()也报它。set_bit()是位域读-改-写:只覆盖目标位,同字节其余位保持原值。set_bit()的两道门顺序是:先OutputReadOnly(只读项),再位号门。set_bit()内部要先读回同字节(比别的写口多一次会话读)。只读项那道门仍在最前:所以 I 区项上报的仍是OutputReadOnly,Q 区项在没start()时才报NotStarted。
示例:
let pd = device.process_data();
pd.out_item(0).set_bit(0, true)?; // 只置 bit0,同字节其余位不动
pd.out_item(0).set_bit(7, false)?; // 只清 bit7
let lsb = pd.in_item(3).get_bit(0)?; // 0 = LSB
in_i8() … in_f64() / in_bool() / in_bit()(按偏移读 I)
不想造句柄时,一步到位按偏移读一个值。读 I 区(控制器 → 设备),不剥前缀。
pub fn in_i8(&self, offset: usize) -> ServiceResult<i8>
pub fn in_u8(&self, offset: usize) -> ServiceResult<u8>
pub fn in_i16(&self, offset: usize) -> ServiceResult<i16>
pub fn in_u16(&self, offset: usize) -> ServiceResult<u16>
pub fn in_i32(&self, offset: usize) -> ServiceResult<i32>
pub fn in_u32(&self, offset: usize) -> ServiceResult<u32>
pub fn in_i64(&self, offset: usize) -> ServiceResult<i64>
pub fn in_u64(&self, offset: usize) -> ServiceResult<u64>
pub fn in_f32(&self, offset: usize) -> ServiceResult<f32>
pub fn in_f64(&self, offset: usize) -> ServiceResult<f64>
pub fn in_bool(&self, offset: usize) -> ServiceResult<bool>
pub fn in_bit(&self, offset: usize, bit: u8) -> ServiceResult<bool>
参数:
offset(usize) — I 区字节偏移。不强制 2 / 4 对齐,偏移就是字节偏移bit(u8) —in_bit的位号,0-7,0 = LSB
返回值:
- 对应宽度的值(
i8/u8/i16/u16/i32/u32/i64/u64/f32/f64/bool)
说明:
- 这些都是拷贝,不是视图。 每次调用经服务读一小段出来,再按大端解释。Rust 没有
In[i]这种活对象 —— 别把「有偏移参数」当成「拿到了活视图」。 - 多字节大端;
in_f32/in_f64是「按大端读 u32 / u64,再from_bits」。 - 宽度就是你要的字节数(
in_i16读 2 字节,in_f64读 8 字节),越界按上面的上限判。 - 失败形态:
- 未建立会话 →
ServiceError::NotConnected - 没
start()→ServiceError::NotStarted offset + 宽度超过输入区长度 →ServiceError::OutOfRange(offset, 宽度, 输入区长度),显示为地址越界: 偏移 N + 长度 M 超出过程映像区 K 字节- 通道不可用 →
ServiceError::SharedMemory("过程数据未就绪, 无法读 IO")
- 未建立会话 →
in_bit的位号门先跑:bit > 7→ServiceError::InvalidParam("位号必须在 0-7 之间")。in_bool与句柄上的as_bool()同一口径(bit0):0x02/0x80判false。要按整字节判真假,用in_u8自己比。
示例:
let pd = device.process_data();
let b = pd.in_u8(0)?; // I 区偏移 0 的 1 字节
let iw = pd.in_i16(3)?; // 偏移 3 的大端 Int16 —— 不强制 2 字节对齐
let f = pd.in_f32(8)?; // 偏移 8 的大端 float32
let bit0 = pd.in_bit(0, 0)?; // I 区 0 字节的第 0 位
out_i8() … out_f64() / out_bool() / out_bit()(按偏移写 Q)
写 Q 区(设备 → 控制器)。Ok ⟺ 字节已在共享区过程映像里,当场提交。
pub fn out_i8(&self, offset: usize, value: i8) -> ServiceResult<()>
pub fn out_u8(&self, offset: usize, value: u8) -> ServiceResult<()>
pub fn out_i16(&self, offset: usize, value: i16) -> ServiceResult<()>
pub fn out_u16(&self, offset: usize, value: u16) -> ServiceResult<()>
pub fn out_i32(&self, offset: usize, value: i32) -> ServiceResult<()>
pub fn out_u32(&self, offset: usize, value: u32) -> ServiceResult<()>
pub fn out_i64(&self, offset: usize, value: i64) -> ServiceResult<()>
pub fn out_u64(&self, offset: usize, value: u64) -> ServiceResult<()>
pub fn out_f32(&self, offset: usize, value: f32) -> ServiceResult<()>
pub fn out_f64(&self, offset: usize, value: f64) -> ServiceResult<()>
pub fn out_bool(&self, offset: usize, value: bool) -> ServiceResult<()>
pub fn out_bit(&self, offset: usize, bit: u8, value: bool) -> ServiceResult<()>
参数:
offset(usize) — Q 区字节偏移(相对 Q 净荷窗口起点),不强制 2 / 4 对齐value— 要写入的值bit(u8) —out_bit的位号,0-7
返回值:
ServiceResult<()>—Ok⟺ 写入已落到共享区(当场提交,不按节拍延迟,也不会「没提交却返回成功」)
说明:
- 多字节大端编码(
to_be_bytes),不是小端。 - 上限按 Q 净荷窗口卡:
offset + 宽度 ≤ 输出区长度 − 3。超了 →ServiceError::OutOfRange(offset, 宽度, 输出区长度 − 3)。 - 失败形态:
- 未建立会话 →
ServiceError::NotConnected - 没
start()→ServiceError::NotStarted - 越界 →
ServiceError::OutOfRange(…) - 通道不可用 →
ServiceError::SharedMemory("过程数据未就绪, 无法写 IO")
- 未建立会话 →
out_bit是同字节读-改-写:先读回该字节,改目标位,再整字节写回。所以它比别的写口多一次读,也没start()时先报NotStarted。out_bit的位号门先跑:bit > 7→ServiceError::InvalidParam("位号必须在 0-7 之间")。out_bool固定写规范字节:true写1,false写0。不是位,也不是「整字节非 0」。- 每次调用都是一次独立提交。多个不相邻的改动就是多次提交。
示例:
let pd = device.process_data();
pd.out_u8(0, 0x11)?; // Q 区偏移 0 的 1 字节
pd.out_i16(3, 123)?; // 偏移 3 的大端 Int16
pd.out_bit(0, 0, true)?; // 只置 Q 区 0 字节的 bit0
pd.out_bool(5, true)?; // 整字节写 1
read_struct<T>() / write_struct<T>(&T)
快照路径:I 区前几字节 → 结构体;结构体 → Q 区偏移 0。不是钉页,一次调用编解码一份。
PnSService 上有同名便捷方法,等价于 device.process_data().read_struct() / .write_struct()。快照路径不要求 #[repr(C, packed)] —— 它走自己声明的 pack / unpack,不做 transmute。
pub fn read_struct<T: ProcessDataPacked>(&self) -> ServiceResult<T>
pub fn write_struct<T: ProcessDataPacked>(&self, value: &T) -> ServiceResult<()>
返回值:
ServiceResult<T>— 解出来的结构体(快照,拷出来的值不跟着服务变)ServiceResult<()>—Ok⟺ 编码后的字节已写入 Q 区
说明:
T要实现ProcessDataPacked:pub trait ProcessDataPacked: Sized {
fn packed_size() -> usize;
fn unpack_from(buf: &[u8]) -> ServiceResult<Self>;
fn pack_into(&self, buf: &mut [u8]) -> ServiceResult<()>;
fn pack_to_vec(&self) -> ServiceResult<Vec<u8>>;
}- 你自己写
unpack_from/pack_into,字节序也由你定。建议与 SDK 口径一致走大端,直接用下节的unpack_i16_be/pack_i16_be一族,不要另写第二份字节序实现。 - 失败形态:
T::packed_size() == 0→ServiceError::InvalidParam("结构体紧排大小不能为 0")pack_into/unpack_from缓冲不足 →ServiceError::OutOfRange(0, 需要, 实得)read_struct:没start()→NotStarted;packed_size()大于输入区长度 →OutOfRangewrite_struct:没start()→NotStarted;编码后长度大于 Q 净荷窗口(输出区长度 − 3)→OutOfRange
read_struct从区偏移 0 读(不剥前缀);write_struct写到 Q 净荷窗口偏移 0(已剥 3)。两边起点口径不同,长度上限也不同。- 想「源头变、字段自己变」,用
inputs_mapping/outputs_mapping—— 快照路径不提供这个。
示例:
use darra_pns::{ServiceResult, ProcessDataPacked};
use darra_pns::service::process_data::{pack_i16_be, unpack_i16_be};
struct FromPlc { present: u8, recipe: i16 } // 不需要 packed、不需要 repr(C)
impl ProcessDataPacked for FromPlc {
fn packed_size() -> usize { 3 }
fn unpack_from(buf: &[u8]) -> ServiceResult<Self> {
Ok(Self { present: buf[0], recipe: unpack_i16_be(&buf[1..3])? })
}
fn pack_into(&self, buf: &mut [u8]) -> ServiceResult<()> {
buf[0] = self.present;
buf[1..3].copy_from_slice(&pack_i16_be(self.recipe));
Ok(())
}
}
let snap: FromPlc = device.read_struct()?; // 一份快照
device.write_struct(&snap)?; // 整段写 Q(从净荷偏移 0 起)
inputs() / write_outputs() / copy_inputs_to() / copy_to_outputs()
整段两方向的便利口,都在 ProcessData 上。与 device.read() / device.write() 是同一件事的两种叫法。
pub fn inputs(&self) -> ServiceResult<Vec<u8>> // = device.read()
pub fn write_outputs(&self, data: &[u8]) -> ServiceResult<()> // = device.write(data)
pub fn copy_inputs_to(&self, dest: &mut [u8]) -> ServiceResult<usize>
pub fn copy_to_outputs(&self, src: &[u8]) -> ServiceResult<usize>
参数:
dest(&mut [u8]) — 接收 I 区快照的调用方缓冲src(&[u8]) — 要写进 Q 区的数据data(&[u8]) — 同上
返回值:
Vec<u8>— I 区一份快照(不是 0 拷贝;同一拍一致,拷出来不跟着服务变)usize— 实际拷贝 / 写入的字节数ServiceResult<()>—Ok⟺ 字节已在共享区
说明:
copy_inputs_to取两边较短的长度拷,所以允许dest比 I 区短(返回实际拷了几字节),不会越界。copy_to_outputs按src.len()写,返回的也就是src.len();数据长过 Q 净荷窗口会报OutOfRange。write_outputs/copy_to_outputs都是从净荷偏移 0 起整段/短写,不做局部补丁。- 失败形态与
read()/write()相同(见下面两节)。
示例:
let pd = device.process_data();
let mut buf = [0u8; 8];
let n = pd.copy_inputs_to(&mut buf)?; // 只取前 8 字节,返回 8
println!("取了 {n} 字节");
pd.copy_to_outputs(&buf)?; // 短写 Q:从净荷偏移 0 起写这 8 字节
read() / write(data)
PnSService 上的整段两方向入口。也是 ProcessData::inputs() / write_outputs() 的落点。
pub fn read(&self) -> ServiceResult<Vec<u8>> // I 整段(控制器 → 设备)
pub fn write(&self, data: &[u8]) -> ServiceResult<()> // 写 Q(设备 → 控制器),从净荷偏移 0 短写
参数:
data(&[u8]) — 要写进 Q 区的字节,非空
返回值:
Vec<u8>— I 区一份快照。长度 = 输入区长度(按整区算),净荷从第 0 字节起、尾部补 0(前缀那 2–6 字节不出现在返回值里)ServiceResult<()>—Ok⟺ 字节已在共享区
说明:
read()没有「是否已start()」这道检查。stop()之后照样返回 I 快照(内容是停住那一刻的)。它只要求过程数据通道在。read()的失败形态只有一个:通道不可用 →ServiceError::SharedMemory("过程数据未就绪, 无法读 IO")。会话没建立、没start()、close()之后,报的都是它。write()走write_area(ServiceArea::Output, 0, data)那一套,失败形态见下节。- 一次
read()的返回值内部自洽:同一份快照上多个字段必然同一拍。这是多字段一致口径的取法。 - 跨多次调用不保证同一拍:每次
in_*/in_item都是一次新的会话读。
示例:
let raw: Vec<u8> = device.read()?; // 一份 I 快照
if raw.len() >= 2 {
let first_word = u16::from_be_bytes([raw[0], raw[1]]); // 大端
println!("{first_word}");
}
device.write(&[0x01, 0x00])?; // 从净荷偏移 0 起写 2 字节
read_area(area, offset, length) / write_area(area, offset, data)
按区域 + 偏移的批量读写,一次调用一段连续字节。
pub fn read_area(&self, area: ServiceArea, offset: usize, length: usize) -> ServiceResult<Vec<u8>>
pub fn write_area(&self, area: ServiceArea, offset: usize, data: &[u8]) -> ServiceResult<()>
参数:
area(ServiceArea) —ServiceArea::Input(I 区,控制器 → 设备)或ServiceArea::Output(Q 区,设备 → 控制器)。Unknown/Memory/Db一律被拒offset(usize) — 相对该区起点的字节偏移length(usize) — 读多少字节data(&[u8]) — 要写的字节,非空
返回值:
Vec<u8>— 该区间的一份快照ServiceResult<()>—Ok⟺ 字节已在共享区(当场提交,不按节拍延迟)
说明:
- 上限按方向不同:读 I 区卡输入区长度(不剥前缀);读 / 写 Q 区卡 Q 净荷窗口 = 输出区长度 − 3。
- 读没有短读:要么给够,要么
OutOfRange。 - 一次调用写一段连续区间。不相邻的改动自己合并成一次写;分多次调就是多次提交。
read_area的失败形态,按判定顺序:- 会话没建立 →
ServiceError::NotConnected - 没
start()→ServiceError::NotStarted area不是 Input / Output →ServiceError::InvalidAddress("区域非法: 区域名")- 通道不可用 →
ServiceError::SharedMemory("过程数据未就绪, 无法读 IO") offset + length溢出 →ServiceError::InvalidAddress("区域越界: offset + length 溢出")- 越界 →
ServiceError::OutOfRange(偏移, 长度, 区长度),显示为地址越界: 偏移 N + 长度 M 超出过程映像区 K 字节
- 会话没建立 →
write_area的失败形态,按判定顺序:- 会话没建立 →
ServiceError::NotConnected area == Input→ServiceError::OutputReadOnly(这道门先于NotStarted)area不是 Output →ServiceError::InvalidAddress("区域非法: 区域名")- 没
start()→ServiceError::NotStarted - 空数据 →
ServiceError::InvalidAddress("写数据不能为空") - 通道不可用 →
ServiceError::SharedMemory("过程数据未就绪, 无法写 IO") offset + data.len()溢出 →ServiceError::InvalidAddress("区域越界: offset + length 溢出")- 越界 →
ServiceError::OutOfRange(偏移, 长度, Q 净荷窗口长度)
- 会话没建立 →
OutOfRange里的偏移字段是u16,超过 65535 会截顶到 65535 再显示。
示例:
use darra_pns::ServiceArea;
let part = device.read_area(ServiceArea::Input, 4, 2)?; // I 区偏移 4 起 2 字节
device.write_area(ServiceArea::Output, 0, &[0x01, 0x00])?; // Q 区净荷偏移 0 起 2 字节
// 写 I 区是真的 Err,不是静默忽略
let err = device.write_area(ServiceArea::Input, 0, &[0x00]);
assert!(matches!(err, Err(darra_pns::ServiceError::OutputReadOnly)));
byte_at(offset) / set_byte_at(offset, value)
绝对偏移的单字节读写。
pub fn byte_at(&self, offset: usize) -> ServiceResult<u8> // 读 I 区 1 字节
pub fn set_byte_at(&self, offset: usize, value: u8) -> ServiceResult<()> // 写 Q 区 1 字节
参数:
offset(usize) — 相对该区起点的字节偏移value(u8) — 要写入的字节
返回值:
ServiceResult<u8>— 该字节的值ServiceResult<()>—Ok⟺ 已提交
说明:
- 就是
read_area(Input, offset, 1)与write_area(Output, offset, &[value])的一层包装,失败形态完全同上一节。 byte_at主要用来取read()拿到的那份快照里没有的东西:它按区偏移直读,和快照不是同一份。- 字符串 / 结构走不到的地方,用这一对按字节搬。
示例:
let b = device.byte_at(4)?; // I 区偏移 4
device.set_byte_at(4, b ^ 0x01)?; // Q 区偏移 4
get_io()
一次调用把两个方向的快照都取回来。
pub fn get_io(&self) -> ServiceResult<(Vec<u8>, Vec<u8>)>
返回值:
(Vec<u8>, Vec<u8>)—.0= Q 区(设备 → 控制器,待发影子),.1= I 区(控制器 → 设备)。顺序与(I, Q)相反,别记错
说明:
- 与
read()一样,没有「是否已start()」这道检查;会话没建立 →ServiceError::NotConnected。 - 通道不可用 →
ServiceError::SharedMemory("过程数据未就绪, 无法读 IO")。 - 两个元素是一次调用里取的,
.0里读回来的就是你之前写进 Q 的东西(Q 的单一权威是共享区)。
示例:
let (q_shadow, i_area) = device.get_io()?;
println!("Q 影子 {} 字节 / I {} 字节", q_shadow.len(), i_area.len());
按地址读写(区域 + 字节 + 位)
不想自己算偏移、想照着工程软件里的地址写时用这几个口:区域字母 + 字节偏移 + 位。地址对象是 ServiceAddress。
let done: bool = device.read_bool("I0.0")?; // 位
let recipe: i16 = device.read_int16("IW2")?; // 字
let counter: i32 = device.read_int32("ID4")?; // 双字
device.write_bool("Q0.0", true)?;
device.write_int16("QW2", 123)?;
device.write_int32("QD4", 10000)?;
| 地址写法 | 是什么 | 有对应入口吗 |
|---|---|---|
I0.0 / Q0.0 | 位地址:第 0 字节的第 0 位(位号只能 0–7) | 有:read_bool / write_bool |
IW2 / QW2 | 字(2 字节) | 有:read_int16 / write_int16 |
ID4 / QD4 | 双字(4 字节) | 有:read_int32 / write_int32 |
IB0 / QB0 | 字节(1 字节) | 能解析,但没有入口收它 |
- 不强制 2 / 4 对齐:
IW3/ID2都合法,偏移就是字节偏移。 - 不支持 M 区 / DB 区:
M…与DB…在解析阶段就被拒。 - 读 Q 地址(
QW2等)读的是 Q 区待发影子,写什么读回什么。 - 写 I 侧 →
ServiceError::OutputReadOnly,不静默。
parse_address(text)
把地址字符串解析成 ServiceAddress。所有地址入口内部都先走它,你也可以单独用它做校验。
pub fn parse_address(text: &str) -> ServiceResult<ServiceAddress> // 关联函数:PnSService::parse_address(…)
参数:
text(&str) — 地址串,首尾空白会被trim掉
返回值:
ServiceResult<ServiceAddress>— 解析结果
相关结构:
pub struct ServiceAddress {
pub area: ServiceArea, // Input(I 区)/ Output(Q 区)
pub slot: u16, // I / Q 区固定 1(SDK 没有槽位参数)
pub subslot: u16, // 固定 1
pub byte_offset: u16, // 字节偏移,最大 65535
pub bit: i32, // 位号 0-7;-1 = 非位寻址
pub length: usize, // 数据宽度:位 = 1,IB/QB = 1,IW/QW = 2,ID/QD = 4
}
说明:
- 失败形态(
ServiceError::InvalidAddress,消息里都带原地址串):- 空串 →
"地址不能为空" DB开头(不分大小写)→"不支持 DB 地址: {地址}"M开头 →"不支持 M 地址: {地址}"- 其它首字母 →
"不支持的地址区域: {地址}" - 有
.但点在最前 / 最后 →"地址格式非法 (位地址应为 I0.0 形式): {地址}" - 位号 > 7 →
"位号必须在 0-7 之间: {地址}" - 没有偏移数字 →
"地址缺少偏移量: {地址}" - 偏移 / 位号不是纯数字 →
"字节偏移非法: {地址}"/"位号非法: {地址}" - 数字 > 65535 →
"字节偏移超出范围: {地址}"/"位号超出范围: {地址}"
- 空串 →
- 区域字母不分大小写(
i0.0也行);类型字符B/W/D只认大写,iw2会被当成「无类型字符 + 偏移w2」而报字节偏移非法。 - 没有类型字符时按字节解析(
I0的length是 1)。 - 纯解析,不碰总线:会话没建立也能调。
示例:
use darra_pns::{PnSService, ServiceAddress};
let a: ServiceAddress = PnSService::parse_address("IW3")?; // 字地址,不强制 2 字节对齐
assert_eq!(a.byte_offset, 3);
assert_eq!(a.length, 2);
assert_eq!(a.bit, -1);
assert!(PnSService::parse_address("M0.0").is_err()); // 不支持 M 区
assert!(PnSService::parse_address("DB1.DBW0").is_err()); // 不支持 DB 区
read_bool(address)
读一个位地址(I0.0 / Q0.0)。
pub fn read_bool(&self, address: &str) -> ServiceResult<bool>
参数:
address(&str) — 必须带位号的地址,如I0.0
返回值:
ServiceResult<bool>— 该位当前值
说明:
- 地址必须带位号。
read_bool("IB0")→ServiceError::InvalidAddress("位地址必须带位号: IB0")。 - 读的是整字节,取
1 << bit那一位置:位号非法在解析阶段就被拒(位号必须在 0-7 之间: {地址})。 - 失败形态:
- 地址非法 →
ServiceError::InvalidAddress(…)(各种消息见parse_address) - 未建立会话 →
ServiceError::NotConnected - 没
start()→ServiceError::NotStarted - 越界 →
ServiceError::OutOfRange(字节偏移, 1, 区长度) - 通道不可用 →
ServiceError::SharedMemory("过程数据未就绪, 无法读 IO")
- 地址非法 →
Q地址读的是待发影子;I地址读的是输入区。
示例:
let run: bool = device.read_bool("I0.0")?;
let ready: bool = device.read_bool("Q0.0")?;
read_int16(address)
读一个字地址(IW2 / QW2),大端。
pub fn read_int16(&self, address: &str) -> ServiceResult<i16>
参数:
address(&str) — 字地址,必须解析出 2 字节且非位寻址
返回值:
ServiceResult<i16>— 大端 Int16
说明:
- 地址必须是 2 字节宽且不带位号。
read_int16("IB0")/read_int16("I0.0")→ServiceError::InvalidAddress("字地址必须为 2 字节且非位寻址: {地址}")。 - 偏移不强制 2 字节对齐:
IW3合法。 - 失败形态:
InvalidAddress(解析 / 宽度,见上)、NotConnected、NotStarted、OutOfRange(字节偏移, 2, 区长度)、SharedMemory("过程数据未就绪, 无法读 IO")。 - 大端解码,与
in_i16同一口径。
示例:
let recipe: i16 = device.read_int16("IW2")?;
let set_pt: i16 = device.read_int16("IW3")?; // 不强制对齐,合法
read_int32(address)
读一个双字地址(ID4 / QD4),大端。
pub fn read_int32(&self, address: &str) -> ServiceResult<i32>
参数:
address(&str) — 双字地址,必须解析出 4 字节且非位寻址
返回值:
ServiceResult<i32>— 大端 Int32
说明:
- 地址必须是 4 字节宽且不带位号。
read_int32("IW2")→ServiceError::InvalidAddress("双字地址必须为 4 字节且非位寻址: IW2")。 - 偏移不强制 4 字节对齐:
ID2合法。 - 失败形态同
read_int16,宽度换成 4。
示例:
let counter: i32 = device.read_int32("ID4")?;
write_bool(address, value)
写一个位地址(Q0.0)。
pub fn write_bool(&self, address: &str, value: bool) -> ServiceResult<()>
参数:
address(&str) — 必须带位号的地址value(bool) — 置位 / 清位
返回值:
ServiceResult<()>—Ok⟺ 已提交到共享区
说明:
- 位操作必须整字节读写,所以它是读-改-写:先读回该字节,改目标位,再整字节写回。同字节其它位不受影响。
- 正因为它要先读,未
start()时先报NotStarted,而不是「I 区只读」。 - 写
I地址(如I0.0)→ServiceError::OutputReadOnly。读那一步是合法的,卡在写那一步。 - 失败形态:
InvalidAddress("位地址必须带位号: {地址}")、NotConnected、NotStarted、OutputReadOnly、OutOfRange(字节偏移, 1, Q 净荷窗口)、SharedMemory("过程数据未就绪, 无法写 IO")。
示例:
device.write_bool("Q0.0", true)?;
let err = device.write_bool("I0.0", true); // 已 start():先读回合法,卡在写那一步 → I 区只读
assert!(matches!(err, Err(darra_pns::ServiceError::OutputReadOnly)));
write_int16(address, value)
写一个字地址(QW2),大端。
pub fn write_int16(&self, address: &str, value: i16) -> ServiceResult<()>
参数:
address(&str) — 字地址value(i16) — 要写入的值
返回值:
ServiceResult<()>—Ok⟺ 已提交
说明:
- 地址必须是 2 字节宽且不带位号,否则
ServiceError::InvalidAddress("字地址必须为 2 字节且非位寻址: {地址}")。 - 与
write_bool不同,它不需要先读:所以写IW2时直接就报OutputReadOnly,不看start()状态。 - 失败形态:
InvalidAddress、NotConnected、OutputReadOnly、NotStarted、OutOfRange(字节偏移, 2, Q 净荷窗口)、SharedMemory("过程数据未就绪, 无法写 IO")。 - 大端编码,与
out_i16同一口径。
示例:
device.write_int16("QW2", 123)?;
device.write_int16("QW3", 456)?; // 不强制对齐
write_int32(address, value)
写一个双字地址(QD4),大端。
pub fn write_int32(&self, address: &str, value: i32) -> ServiceResult<()>
参数:
address(&str) — 双字地址value(i32) — 要写入的值
返回值:
ServiceResult<()>—Ok⟺ 已提交
说明:
- 地址必须是 4 字节宽且不带位号,否则
ServiceError::InvalidAddress("双字地址必须为 4 字节且非位寻址: {地址}")。 - 不先读,失败形态同
write_int16,宽度换成 4。
示例:
device.write_int32("QD4", 10000)?;
大端打包 / 解包(快照解析用)
从一份快照(read() / read_area() / get_io() 的结果)里按偏移解析多个字段时,用这一组公共函数。它们是本 SDK 唯一的字节序实现 —— in_* / as_* / set_* 全都走它们。
路径:darra_pns::service::process_data(不在 crate 根)。
pub fn pack_u8(value: u8) -> [u8; 1]
pub fn pack_i16_be(value: i16) -> [u8; 2] // 大端
pub fn pack_u16_be(value: u16) -> [u8; 2] // 大端
pub fn pack_i32_be(value: i32) -> [u8; 4] // 大端
pub fn pack_u32_be(value: u32) -> [u8; 4] // 大端
pub fn pack_i64_be(value: i64) -> [u8; 8] // 大端
pub fn pack_u64_be(value: u64) -> [u8; 8] // 大端
pub fn pack_f32_be(value: f32) -> [u8; 4] // IEEE754,大端
pub fn pack_f64_be(value: f64) -> [u8; 8] // IEEE754,大端
pub fn unpack_u8(buf: &[u8]) -> ServiceResult<u8>
pub fn unpack_i8(buf: &[u8]) -> ServiceResult<i8>
pub fn unpack_i16_be(buf: &[u8]) -> ServiceResult<i16>
pub fn unpack_u16_be(buf: &[u8]) -> ServiceResult<u16>
pub fn unpack_i32_be(buf: &[u8]) -> ServiceResult<i32>
pub fn unpack_u32_be(buf: &[u8]) -> ServiceResult<u32>
pub fn unpack_i64_be(buf: &[u8]) -> ServiceResult<i64>
pub fn unpack_u64_be(buf: &[u8]) -> ServiceResult<u64>
pub fn unpack_f32_be(buf: &[u8]) -> ServiceResult<f32>
pub fn unpack_f64_be(buf: &[u8]) -> ServiceResult<f64>
pub fn bool_bit0(value: u8) -> bool // (value & 1) != 0
返回值:
[u8; N]— 编码后的字节ServiceResult<i16>/ … — 解出来的值bool— bit0 判定结果
说明:
- 解包从缓冲第 0 字节起,缓冲不足如实报错,不静默补 0:2 / 4 / 8 字节类型缓冲不够 →
ServiceError::OutOfRange(0, 需要, 实得);unpack_u8遇到空缓冲 →OutOfRange(0, 1, 0)。 - 不带
_be后缀的只有pack_u8/unpack_u8/unpack_i8—— 1 字节没有字节序。 bool_bit0(0x00)=false、bool_bit0(0x01)=true、bool_bit0(0x02)=false、bool_bit0(0x80)=false。本 SDK 不提供「整字节非 0 即真」的第二个实现。- 没有
pack_bool/unpack_bool;写 bool 用set_bool,或者自己写0 / 1。
示例:
use darra_pns::service::process_data::{bool_bit0, unpack_i16_be, unpack_i32_be};
let raw = device.read()?; // 一份快照(一次调用,同一拍)
let recipe = unpack_i16_be(&raw[2..4])?; // 偏移 2 的大端 Int16
let counter = unpack_i32_be(&raw[4..8])?; // 偏移 4 的大端 Int32
let ready = bool_bit0(raw[6]); // 0x02 / 0x80 都是 false
println!("{recipe} {counter} {ready}");
记录索引空间(下标寻址的另一套口径)
过程数据是周期的,记录(Record)是非周期的。两者都按「下标」定位,但是两个不同的索引空间,别混:
| 索引空间 | 范围 | 谁在读 | 走哪个入口 |
|---|---|---|---|
| 过程数据 I / Q | 字节偏移,0 … 区长度 | 你(本节上半部分全部入口) | process_data() 那一族 |
| 用户区记录 | 0x0000 – 0x7FFF | 控制器(IODRead / IODWrite) | device.records()(GET /api/records) |
| I&M 记录 | 0xAFF0 – 0xAFF4 | 控制器 | 不在记录入口里 —— 见 device.im_data()(GET /api/im) |
0xAFF0 – 0xAFF4 由协议栈自己处理,不进用户区记录通道,所以 records() 永远看不到它们。要读 I&M 用 im_data()。
records() / get_records()
读用户区记录表(Index 0x0000 – 0x7FFF)。
pub fn get_records(&self) -> ServiceResult<UserRecords>
pub fn records(&self) -> ServiceResult<UserRecords> // 属性口径别名,同一数据
返回值:
ServiceResult<UserRecords>— 记录表
相关结构:
pub struct UserRecords {
pub records: Vec<UserRecord>, // 缺字段为空表
pub count: i32, // 服务端 count;缺字段默认 0,不按数组长度回填
pub timestamp: String, // 快照时刻(ISO-8601 原文)
}
pub struct UserRecord {
pub slot: u16, // 槽位,缺字段默认 0
pub subslot: u16, // 子槽位,缺字段默认 0
pub index: u16, // 记录 Index(用户区 0x0000-0x7FFF)
pub length: u16, // 负载字节数,缺字段默认 0
pub data_hex: String, // 负载十六进制,缺字段为空
}
说明:
- 没有记录不是失败:
Ok+ 空records。 data_hex是十六进制文本,不是字节数组 —— 自己解。- 失败形态:
- 会话没建立 →
ServiceError::NotConnected - 服务不可达 →
ServiceError::ConnectFailed(host, port, 原因) - 服务返回非 200 →
ServiceError::HttpStatus(状态码, 原因) - 响应不是合法 JSON / 缺字段 →
ServiceError::BadResponse(原因)
- 会话没建立 →
- 没有写记录的入口:SDK 侧没有任何下发 IODWrite 的方法。用户区记录的写由控制器发起,SDK 只读这张表。
- 单条字段缺失如实返回默认值(0 / 空串),不虚构等价值。
示例:
for r in device.records()?.records {
println!(
"槽 {}/{} Index 0x{:04X} {} 字节 {}",
r.slot, r.subslot, r.index, r.length, r.data_hex
);
}
// I&M 不在这个索引空间里
let im = device.im_data()?;
println!("I&M 已启用: {} / 订货号 {}", im.im_supported, im.order_id);
映射与句柄的有效期
| 做了什么 | 已有的 &T / &mut T / slice / 句柄 | 重新取映射 |
|---|---|---|
stop() | 还在。只是读到的是停住不动的值 | 还能取新映射(stop 不卸通道) |
close() | 悬空(通道已 unmap) | 取映射报 SharedMemory;要重新 start() |
drop(device) | Drop 会放开通道,同上 | — |
- 标量与整区访问器是另一套口径:
stop()之后read_area/write_area/byte_at/in_*/out_*/in_item(…)上的每次读写都返回NotStarted。 - 但
read()没有这道检查:stop()之后照样返回 I 快照。 close(&mut self)要独占借用,而映射 / 句柄的'a一直借住PnSService—— 所以安全 Rust 里写不出「握着映射去 close」。这层保证来自借用检查器,不是运行期检查。let pd = device.process_data();
let inp = unsafe { pd.inputs_mapping::<FromPlc>()? };
device.close(); // 编译错误:device 已被不可变借用- 句柄(
ProcessDataItem)不是裸指针,所以它不承担指针有效期义务;但它借住服务,同样挡住close()。 - 映射的地址稳定性没有任何承诺 —— 见 映射起点不是常量。
字节数怎么拿
长度由运行配置决定,不要写死 20 / 32(改过组态就变)。
| 想拿 | 走哪 |
|---|---|
| I 区长度(整区,不剥前缀) | device.read()?.len() |
| 输入区配置长度 | device.diag_snapshot()?.input_area_length(u32) |
| 输出区配置长度 | device.diag_snapshot()?.output_area_length(u32) |
| Q 净荷窗口长度 | device.diag_snapshot()?.output_area_length.saturating_sub(3) as usize |
| Q 净荷窗口长度 | device.live_io_payload(true)?.1 |
| I 净荷窗口长度 | device.live_io_payload(false)?.1 |
read()?.len()是 I 整区长度(不剥前缀)。- 映射能用的 I 净荷窗口比它小 2–6 字节,具体小多少随数据内容变化。
- Q 侧一律按输出区长度减 3 算。
所以「映射放得下」不能照 read()?.len() 卡。好在尺寸门自己会拒:超了返回 ServiceError::InvalidParam("结构体大于输入过程映像") / ("结构体大于输出过程映像"),而且这条检查在通道检查之后 —— 没 start() 时先看到的是 SharedMemory。
Rust SDK 的过程数据通道只有 Windows 实现。非 Windows 目标上进程数据通道不可用(ServiceError::SharedMemory("PnS 过程数据通道仅支持 Windows")),实时连接相关入口报 ServiceError::RealtimeGate("PnS 实时连接仅支持 Windows")。