跳到主要内容

过程数据

过程数据就是 I / Q 两段过程映像,服务每周期自动收发,不用你推。SDK 侧有两条路。

结构体映射把整幅映像按 #[repr(C, packed)] 结构体钉在共享区上。读写字段就是读写过程映像本身(0 拷贝,持续控制用)。 按偏移 / 按项的类型化读写每次调用取一小段解释给你(拷贝,零星读写用)。

写 Q(设备 → 控制器),读 I(控制器 → 设备)。I 区只读,控制器每周期整段覆盖它。 节拍由服务和内核完成,SDK 没有用户周期回调 —— 调度由你自己定。

方向:GSDML 里的 Input / Output 就是 SDK 的 Q / I

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 语义:0x01true0x02 / 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, 宽度, 上限)
  • 只读项上调用任何 set_*ServiceError::OutputReadOnly这道门在会话访问之前:没 start() 也先报它。
  • 未建立会话 → ServiceError::NotConnected;会话在、没 start()ServiceError::NotStarted
  • as_bool() 只认 bit00x01true0x02 / 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-70 = LSB
  • value (bool) — 置位 / 清位

返回值:

  • ServiceResult<bool> — 该位当前值
  • ServiceResult<()> — 置位成功(当场提交)

说明:

  • 位访问器是按字节的,不是按宽度:bit 只寻址句柄偏移处的那一个字节,跟这个项你打算按什么宽度读无关。
  • bit > 7ServiceError::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 > 7ServiceError::InvalidParam("位号必须在 0-7 之间")
  • in_bool 与句柄上的 as_bool() 同一口径(bit0):0x02 / 0x80false。要按整字节判真假,用 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 > 7ServiceError::InvalidParam("位号必须在 0-7 之间")
  • out_bool 固定写规范字节:true1false0。不是位,也不是「整字节非 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() == 0ServiceError::InvalidParam("结构体紧排大小不能为 0")
    • pack_into / unpack_from 缓冲不足 → ServiceError::OutOfRange(0, 需要, 实得)
    • read_struct:没 start()NotStartedpacked_size() 大于输入区长度 → OutOfRange
    • write_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_outputssrc.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 的失败形态,按判定顺序:
    1. 会话没建立 → ServiceError::NotConnected
    2. start()ServiceError::NotStarted
    3. area 不是 Input / Output → ServiceError::InvalidAddress("区域非法: 区域名")
    4. 通道不可用 → ServiceError::SharedMemory("过程数据未就绪, 无法读 IO")
    5. offset + length 溢出 → ServiceError::InvalidAddress("区域越界: offset + length 溢出")
    6. 越界 → ServiceError::OutOfRange(偏移, 长度, 区长度),显示为 地址越界: 偏移 N + 长度 M 超出过程映像区 K 字节
  • write_area 的失败形态,按判定顺序:
    1. 会话没建立 → ServiceError::NotConnected
    2. area == InputServiceError::OutputReadOnly这道门先于 NotStarted
    3. area 不是 Output → ServiceError::InvalidAddress("区域非法: 区域名")
    4. start()ServiceError::NotStarted
    5. 空数据 → ServiceError::InvalidAddress("写数据不能为空")
    6. 通道不可用 → ServiceError::SharedMemory("过程数据未就绪, 无法写 IO")
    7. offset + data.len() 溢出 → ServiceError::InvalidAddress("区域越界: offset + length 溢出")
    8. 越界 → 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」而报 字节偏移非法
  • 没有类型字符时按字节解析(I0length 是 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(解析 / 宽度,见上)、NotConnectedNotStartedOutOfRange(字节偏移, 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("位地址必须带位号: {地址}")NotConnectedNotStartedOutputReadOnlyOutOfRange(字节偏移, 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() 状态。
  • 失败形态:InvalidAddressNotConnectedOutputReadOnlyNotStartedOutOfRange(字节偏移, 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) = falsebool_bit0(0x01) = truebool_bit0(0x02) = falsebool_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() 那一族
用户区记录0x00000x7FFF控制器(IODRead / IODWrite)device.records()(GET /api/records
I&M 记录0xAFF00xAFF4控制器不在记录入口里 —— 见 device.im_data()(GET /api/im

0xAFF00xAFF4 由协议栈自己处理,不进用户区记录通道,所以 records() 永远看不到它们。要读 I&M 用 im_data()

records() / get_records()

读用户区记录表(Index 0x00000x7FFF)。

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_lengthu32
输出区配置长度device.diag_snapshot()?.output_area_lengthu32
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

只支持 Windows

Rust SDK 的过程数据通道只有 Windows 实现。非 Windows 目标上进程数据通道不可用(ServiceError::SharedMemory("PnS 过程数据通道仅支持 Windows")),实时连接相关入口报 ServiceError::RealtimeGate("PnS 实时连接仅支持 Windows")