📦 地址自动组包(Packer)
Snet.Core.packer 为工业协议提供地址自动组包能力。读取大量分散地址时,它将地址分组、排序、合并为最少的批量读取结构,大幅减少与设备的往返次数。每个受支持的协议族都有专属的 Packer 实现,理解其地址格式与物理上限。
组包能力以两个层级开放:
| 层级 | API | 位置 |
|---|---|---|
| 直接使用(推荐) | IDaq.Packer / IDaq.UnPacker —— 高层 IPacker 接口 |
IPacker 接口 |
| 引擎(进阶) | PackerHandler + IPackerCore 组包器 |
本页 |
包: Snet.Core(自动包含)| 命名空间: Snet.Core.packer(入口:Snet.Core.handler)
▶️ 快速开始 — 直接通过 IDaq
每个数采驱动(IDaq)都继承 IPacker,组包与解包一步即可完成,无需手动装配处理器:
using Snet.Siemens;
using System.Collections.Concurrent;
using Snet.Model.data;
using Snet.Model.@enum;
using Snet.Model.@interface; // IPacker
await using var op = await SiemensOperate.InstanceAsync(new SiemensData.Basics
{
IpAddress = "192.168.1.100",
Port = 102
});
if (!(await op.OnAsync()).Status) return;
// 1) 组包:将分散地址合并为最少的批量读取
var address = new Address(new List<AddressDetails>
{
new AddressDetails("DB1.DBD0", DataType.Float),
new AddressDetails("DB1.DBD4", DataType.Float),
new AddressDetails("DB1.DBW8", DataType.Int),
new AddressDetails("DB1.DBX10.3", DataType.Bool)
});
var packed = await op.PackerAsync(address, "SiemensS7Net_S1200");
if (!packed.Status) return; // 组包失败 / 协议不支持
var packedAddr = packed.GetSource<Address>(); // ResultData = 优化后的 Address
if (packedAddr is null) return;
// 2) 一次往返读取组包后的批次
var result = await op.ReadAsync(packedAddr);
if (!result.Status) return; // 读取失败
var data = result.GetSource<ConcurrentDictionary<string, AddressValue>>();
if (data is null) return;
// 3) 解包:将每个批次的原始字节解析回各地址的值
var unpacked = await op.UnPackerAsync(data);
if (!unpacked.Status) Console.Error.WriteLine(unpacked.Message);
var values = unpacked.GetSource<List<ConcurrentDictionary<string, AddressValue>>>();
if (values is null) return;
foreach (var dict in values)
foreach (var v in dict)
Console.WriteLine($"{v.Key} = {v.Value.ResultValue}");
不支持组包的协议返回原地址;支持协议的 null/空点集可能得到 null。应检查 OperateResult 和 ResultData,不依赖“永不抛异常/永不为空”的承诺。完整 API 见 IPacker 接口。
▶️ 快速开始 — 处理器(引擎层)
using Snet.Core.handler; // PackerHandler 入口
using Snet.Core.packer; // ProtocolFamily, IPackerCore
// 创建处理器(单例池工厂;需传入名称,如 "myHandler")
var handler = await PackerHandler.InstanceAsync("myHandler");
// 为设备协议优化地址批次
// maxByteLength = 0 → 自动使用协议的保守安全上限
Address? optimized = handler.AddressAutoPackOrPassthrough(
address,
basics.ProtocolType.ToString(), // 如 "SiemensS7Net_S1200"、"ModbusTcpNet"
maxByteLength: 0,
format: Snet.Model.@enum.DataFormat.ABCD
);
AddressAutoPackOrPassthrough 是推荐的智能入口:
- 协议族支持组包 → 返回优化后的批量读取结构
- 协议族不支持组包 → 原样返回地址(透传)
- 输入为
null→ 返回null - 支持协议的空点集可返回 null;自定义组包器错误可从该低层处理器传播
⚙️ 安装
无需单独安装包——Snet.Core.packer 随 Snet.Core 发布,而 Snet.Core 被每个协议驱动自动引入。
🧠 核心概念
1. 组包管线
PackerBase.Pack() 对输入地址执行固定管线:
分离可组包 / 不可组包地址
→ GroupAndSort (按区域分组、去重、按 ByteIndex + BitIndex 排序)
→ SplitIntoBatches (按 maxByteLength 拆批次)
→ BuildResult (组包批次在 AddressDescribe 字段标记 "packer - {n}";透传项不带 "packer" 标记)
- 组包批次以
AddressDetails条目输出,DataType.ByteArray+ 总字节长 - 每个批次的
AddressExtendParam持有List<BytesModel>——批内各个打包地址及偏移 - 地址永不丢失——不可组包地址原样透传
读取后,每个组包批次必须用 BytesHandler.TransformAsync 拆包——返回的 AddressValue 携带 OriginalValue(批次原始字节)与 AddressExtendParam(各地址的 BytesModel 列表)。TransformAsync(AddressValue) 将批次原始字节解析回以地址名为键的 ConcurrentDictionary<string, AddressValue>:
using Snet.Core.handler;
using var bytesHandler = BytesHandler.Instance(Guid.NewGuid().ToString());
var unpacked = await bytesHandler.TransformAsync(kv.Value); // kv.Value = 一个组包批次
var values = unpacked.GetSource<ConcurrentDictionary<string, AddressValue>>();
if (values is null) return;
底层重载:TransformAsync(byte[] bytes, DateTime time, IEnumerable<BytesModel> transformParams) 按 BytesModel 映射转换任意字节。
仅 AddressDataType=ByteArray 且扩展参数可取得 List
字典重载可统一处理组包批与直接值,对各项 Status 做与运算,保留所有返回字典(包括失败/部分结果)。任一失败则整体 Status=false(部分点位解包失败);仍应读取 ResultData 并逐点检查 Quality,不要丢弃结果列表。空字典成功返回空列表,null 输入失败。同名多规格结果键可带 \u0001DataType/序号后缀,AddressValue.AddressName 保留原地址。
2. 内置注册协议族(24 个,23 个启用优化)
| 协议族 | 中文名 | Packer | MaxReadLength | PhysicalMaxBytes | 地址解析要点 |
|---|---|---|---|---|---|
Siemens |
西门子 S7 | SiemensPacker |
240 | 960 | AI/AQ 按字节;仅 T/C 编号×2;普通字节≤0x1FFFFF,T/C≤0xFFFFFF且禁点号;字区Byte/奇数文本透传 |
Modbus |
Modbus | ModbusPacker |
250 | 131070 | 零基地址 + 富修饰符(x=4;100、format=DCBA;100、100H、100.1、file=0;100);覆写 SplitIntoBatches |
Mitsubishi |
三菱 Melsec | MelsecPacker |
960 | 131070 | 仅支持驱动白名单中的 MC 设备代码;未知或格式错误的代码原样透传 |
Omron |
欧姆龙 FINS | FinsPacker |
1000 | 131070 | TIM/CNT 位访问;EM 库号十六进制 字地址≤65535 |
Fuji |
富士 SPH | FujiPacker |
210 | 210 | 固定 SPH:M1/M3/M10 及 I/Q;24 位字地址 |
Fuji_SPB |
富士 SPB | FujiPacker |
105 | 105 | 固定 SPB;Bool 无点号 X/Y/M/L/TC/CC 为绝对位号,批首转所在字 |
Fuji_CST |
富士 CST | FujiPacker |
210 | 210 | 固定 CommandSettingType;S 字节、BD/W9 双字;全部 Bool 透传 |
Keyence |
基恩士 KV | KeyencePacker |
960 | 131070 | 仅 MC Binary 语义(B/M/L/SM/X/Y/TS/TC/CS/CC 位区);X/Y/B 十六进制 |
Yokogawa |
横河 Link | YokogawaPacker |
500 | 131070 | X/Y/I/E/M/L 位区;T/C 当前值为字区,其 Bool 降级 位批另限128字节 |
Panasonic |
松下 Mewtocol | PanasonicPacker |
500 | 131070 | 位读仅 X/Y/R/L;复合十六进制位号(X1F → 31) |
Yaskawa |
安川 Memobus | YaskawaPacker |
250 | 250 | 无点号 UInt16 字地址及 SFC3/4/9/10;全部 Bool、数字点号降级 |
GE |
GE SRTP | GePacker |
128 | 128 | 地址1..65536(SRTP);S/SA/SB/SC/G 位区 |
Fatek |
永宏 Fatek | FatekPacker |
128 | 131070 | 位设备位流布局(46W 字读);字区 D/R/RT/RC;地址≤65535 |
Fanuc |
发那科 | FanucPacker |
128 | 128 | 字母前缀区 + 别名偏移(SDO/RDI/UI/SI...) |
LSis_Cnet |
LSis Cnet 串口 | LSisPacker |
500 | 131070 | 位宽 B/W/X/D/L 缩放;U/I/Q 复合区(U{a}.{b}.{c} / I{a}.{b}.{c}) |
LSis_Cpu |
LSis LSCpu(GLOFA 串口) | LSisPacker |
120 | 120 | 仅保留注册/诊断解析;SupportsAutoPack=false,原地址透传 |
LSis_FastEnet |
LSis FastEnet(XGB 以太网) | LSisPacker |
500 | 500 | 仅显式位宽 B/W/D/L Word 组包;X=位/无位宽/U/I/Q/点号降级 |
Cimon |
Cimon HMI | CimonPacker |
500 | 500 | cmd82 按字坐标×2,地址≤999999;全部 Bool 与 D 点号透传 |
XinJE |
信捷 | XinJEPacker |
240 | 131070 | X/Y 八进制编址 D8000 及 ID/QD 每 10000(至30000)分段,不跨转换边界 |
Vigor |
Vigor | VigorPacker |
128 | 131070 | D≥9000 → SD 区,M≥9000 → SM 区 |
Toyota |
丰田 ToyoPuc | ToyoPacker |
128 | 128 | prg=N; 前缀;末位十六进制字符为字内位(K1005 = K100 位 5);E/G 系列区域 |
AllenBradley |
罗克韦尔 SLC | AllenBradleyPacker |
255 | 255 | {类型}{文件号}:{元素号}(B3:0、N7:10);带 /位 寻址不可组包;文件号≤255、元素≤65535;ST 默认文件 1 |
MitsubishiFx |
三菱 FX 链接 | FxLinksPacker |
128 | 131070 | X/Y 八进制;s=N; 站号前缀;位流布局(WR 字读);位区 Word 降级 位批另限128字节 |
Beckhoff |
倍福 ADS | BeckhoffPacker |
1024 | 1024 | M/I/Q 内存寻址字节偏移(M100、M100.3);i= 句柄透传;ig= 仅 0x4020/0xF020/0xF030 优化;s= 符号地址降级(偏移不可预知) |
内置默认注册 24 个协议族,其中 23 个支持优化;LSis_Cpu 不优化。GetSupportAutoPackDeviceTypes() 与 GetFullAutoPackFamilies() 仅返回当前注册且启用优化的族,注册总数请用 PackerFactory.Count。用户可用 Register 覆盖默认注册。
所有 Packer 生成 |B / |W 区域键,同地址的 Bool 与 Word 读取互不冲突。区域无法承载的类型(如字区的 Bool)由 IsPackable 降级为原样保留,任何地址都不会丢失。
3. 不支持组包的协议
部分协议使用符号/标签寻址,无法安全组包——智能入口原样返回:
SiemensS7Plus(符号访问)MelsecCipNet(标签访问)OmronCipNet/OmronConnectedCipNet(标签访问)
VendorProtocolMap 有 59 条供应商名称映射,内置注册为 24 个族;两者不是同一计数。主要映射及边界:
- 不再按 Modbus 合批:Delta 五种、Inovance TCP/Serial/SerialOverTcp/EasyNet、MegMeet 三种协议整体透传;数字地址也不例外。
InovanceComputerLink例外,改映射MitsubishiFx(FX Links),s=是站号而非符号寻址。 - Toyo
ToyoPuc、AllenBradley(AllenBradleySLCNet、AllenBradleyDF1Serial、AllenBradleyPcccNet)、FX 链接(MelsecFxLinks、MelsecFxLinksOverTcp——原映射 Mitsubishi,现为独立协议族,X/Y 八进制编址)。 - 三菱 A4C
MelsecA4CNet/MelsecA4CNetOverTcp复用 A3C 兼容地址模型,映射到Mitsubishi协议族。 - 倍福 ADS
BeckhoffAdsNet/BeckhoffAdsNetWithAdsRouter→Beckhoff。M/I/Q 字节空间及点号 Bool 可优化;ig=仅原始字节内存组 16416/61472/61488 可优化。i=句柄、s=符号、0xF005/0xF080 等其他组、无点号 Bool 与点号 Word 全部透传。 - LSis:
LSCnet/LSCnetOverTcp→LSis_Cnet、LSFastEnet→LSis_FastEnet继续优化;LSCpu虽映射到LSis_Cpu,但关闭优化,因为该命令未编码可安全推进的起始地址。 - Fuji:
FujiSPHNet→Fuji,FujiSPB/FujiSPBOverTcp→Fuji_SPB,FujiCommandSettingType→Fuji_CST;不按地址文本猜测模式。 - Siemens PPI/MPI:
SiemensPPI、SiemensPPIOverTcp、SiemensMPI从映射移除,全部透传。
从映射中移除的协议(原样返回——地址模型与协议族 Packer 语义不兼容):KeyenceNanoSerial / KeyenceNanoSerialOverTcp / KeyenceKvOld(KV 老协议 ASCII / Nano 地址模型)、PanasonicMcNet(MC 语义与 Mewtocol 位号编排不同)、OmronHostLinkCMode / OmronHostLinkCModeOverTcp(2026-08 重构从映射移除——此前按 Omron 组包,现原样返回)。 当前还排除 SiemensFetchWriteNet、MelsecMcRNet、MelsecA1ENet、MelsecA1EAsciiNet、MelsecFxSerial、MelsecFxSerialOverTcp;其地址规则不兼容共享模型,保留原输入交由单点驱动处理。A3C/A4C 继续组包。
📚 API 参考
IPackerCore
Snet.Core.packer.IPackerCore 是底层引擎契约——不要与高层 Snet.Model.interface.IPacker(IDaq 成员接口,见 IPacker 接口)混淆。
| 成员 | 签名 | 描述 |
|---|---|---|
Family |
ProtocolFamily Family { get; } |
该 Packer 处理的协议族 |
Pack |
Address? Pack(Address address, int maxByteLength, DataFormat format) |
将地址集优化为批量读取结构;失败返回 null |
ParseAddress |
(int ByteIndex, int BitIndex) ParseAddress(string address) |
解析协议地址 →(字节偏移, 位索引);失败返回 (-1, 0) |
TryParseAddress |
bool TryParseAddress(string address, out int byteIndex, out int bitIndex) |
不抛异常的解析 |
GetRegionKey |
string GetRegionKey(string address) |
地址区域前缀(分组依据) |
MaxReadLength |
int MaxReadLength { get; } |
默认模型覆盖建议值(maxByteLength≤0);并非所有硬件/PDU 的保证 |
PhysicalMaxBytes |
int PhysicalMaxBytes { get; } |
当前模型允许上限(默认 = MaxReadLength;S7 为 960),不等同各 Driver 请求单位 |
SupportsAutoPack |
bool SupportsAutoPack { get; } |
是否支持自动组包优化 |
PackerBase
实现 IPackerCore 的公共管线抽象基类。子类只需实现 Family、ParseAddress、GetRegionKey、MaxReadLength。可覆写点:GroupAndSort、SplitIntoBatches、BuildResult、IsPackable;PhysicalMaxBytes 与 SupportsAutoPack(默认 true)。
受保护嵌套结构体:AddrInfo(FullName, RegionKey, ByteIndex, BitIndex, DataType, Length, EncodingType = EncodingType.UTF8, DataFormat = -1) 与 BatchItem(FullName, StartByte, Length, BoolIndex = 0, EncodingType = EncodingType.UTF8, DataFormat = -1, DataType = DataType.ByteArray) —— EncodingType 仅对 String/Char 项生效;DataFormat 为地址级格式覆盖(-1 = 使用全局格式);BatchItem.DataType 保留原始类型,供 BytesHandler 正确解包。
结果字段契约: 新建组包批的 Length 是模型覆盖字节数,扩展参数为 BytesModel 列表。所有未优化点(含普通真实点、禁用、虚拟、数组及超长点)保留原 AddressDetails 对象、Length 和全部扩展参数,不再附加单点解码模型。非法/溢出地址不会被修正为驱动合法地址。
IsPackable(string address, DataType dataType)(虚方法,默认 true)允许 Packer 将区域无法承载类型的地址降级(如 FxLinks 字区 D100 的 Bool)——原样保留而非组包。
位流批布局: ByteArray 批走驱动字读路径,rel = GetBitNumber(v) - headBit,拆批后重新锚定;默认 StartByte = rel / 8、BoolIndex = rel % 8。FINS 内存读命令为 01 01(位/字由区域码区分),字响应大端,偏移为 (rel / 8) ^ 1。启用族为 Melsec/Keyence/Fins/Fuji SPH或SPB/FxLinks/GE/Yokogawa/Fatek;LSis_Cpu 不优化,Yaskawa 不采用位流。FX Links 与 Yokogawa 的位批另限 128 字节,避免跨不连续分帧;不受更大的调用方上限放宽。
批首归一化(26.235.x 起): NormalizeBatchHead(string fullName)(虚方法,默认原样)重写批次首地址的 AddressName,使驱动地址解析器可消费——点号位地址(90.1)、format= 修饰符或驱动拒绝的区域形式按协议剥离/归一(Modbus:剥点 + 剥 format=,保留 s=/x=/w=/file=;Beckhoff/Cimon/Toyo:剥点;GE:归一到包含字节地址 M2→M1;LSis_Cnet:U/I/Q 点号地址不剥——驱动原生解析)。 新增 NormalizeBatchHead(string fullName, DataType dataType):默认转调旧单参数方法,Fuji SPB 按原类型把绝对位号转为字读批首。AllenBradley/Melsec/FINS/LSis/Fuji 的批首规范为大写,模型仍保留原名称;不是修复透传地址。 Beckhoff/Cimon/GE字区/Vigor也将批首转大写;解包模型仍保留原名称。
去重键为区域 + 地址名 + 数据类型 + Length + EncodingType + 地址级 DataFormat。仅完全相同的读取规格去重;同名多规格都保留。BytesHandler 结果键首次冲突添加 \u0001DataType,仍冲突时追加 \u0001 与递增序号;AddressValue.AddressName 始终保留原地址。 普通批偏移固定为当前地址字节坐标减批首坐标;重叠规格共享起点,长度取最大覆盖,不累加前一项宽度。
地址按完整数字、范围及缩放结果校验,不只识别前缀。非法输入原样保留,不回绕或截断:
| 协议族 | 当前源码边界 |
|---|---|
| Melsec / Keyence / Toyo | 先匹配最长合法设备前缀,再解析数值后缀;十六进制数值可含字母(WABC、XAF) |
| MitsubishiFx | 站号≤255、地址≤65535;拒绝非法修饰符 |
| LSis | Cpu 不优化;Cnet 复合段精确校验,FastEnet 仅显式区域/位宽形式 |
| Cimon | 校验十六进制位后缀、十进制数值及缩放溢出 |
| Fanuc / GE | 发出区域前校验别名偏移(含 Fanuc +7000)及字/一基地址缩放;Fanuc 别名转换后必须0..65535,GE R/AI/AQ Byte 透传 |
| Beckhoff / XinJE | 拒绝超范围索引组、非法后缀或缩放溢出,不允许整数回绕 |
| Panasonic / Vigor | Panasonic T/C 属于字区;Vigor C16/C32 按不同物理 DataCode(172/173)分区 |
PackerBase 新增受保护扩展点:
| 签名 | 作用 |
|---|---|
protected virtual int GetBitStreamByteOffset(int relativeBit) |
相对位号映射到真实响应字节,默认 / 8,FINS 覆写为再 ^ 1 |
protected virtual int GetRequiredReadLength(AddressDetails address) |
默认调用 GetResponseByteLength;寄存器 Bool 等协议覆写最小响应 |
protected virtual int GetResponseByteLength(AddressDetails address) |
文本按 Driver 读取单位换算;数值按固定宽度 |
protected virtual int MaxBitStreamBytes { get; } |
位流额外覆盖上限;默认 int.MaxValue,FX Links/Yokogawa 为128 |
protected virtual string NormalizeBatchHead(string fullName, DataType dataType) |
按原点类型生成驱动批首 |
protected static bool TryParseUnsigned(ReadOnlySpan<char> span, int radix, out int value) |
完整解析 8/10/16 进制非负 ASCII 整数,拒绝尾随文本及 Int32 溢出 |
文本 Length: CalcByteLength 对 String/Char 保留原 Length,EncodingType 只决定解码,不乘字符宽度。Siemens/Beckhoff/GE/Fuji_CST/LSis_Cnet/LSis_FastEnet 的 Length 按字节,其余族通常按字转成Length×2,Vigor C32覆写为Length×4。Length=0、AllenBradley 全部文本、GE 奇数 Length 文本均透传。模型覆盖字节不等于所有 Driver 请求单位;如 Toyo/Vigor 仍直传字数,可能多读,默认值不代表硬件验证。
FujiPacker() 默认为 SPH;新增 FujiPacker(ProtocolFamily family) 仅接受 Fuji/Fuji_SPB/Fuji_CST。LSisPacker(ProtocolFamily family) 仅接受三个 LSis 族,非法族均抛 ArgumentOutOfRangeException。
CanAutoPack 要求实际注册且 SupportsAutoPack = true,不能仅凭枚举可解析判定支持。当前源码行为可能晚于 NuGet 26.301.1。
当前工作树的额外限制: Modbus 点号只用于 Bool,显式 FC3/4 的 Bool 也须带点号;format=仅供32/64位数值,Bool/Byte/16位/文本降级。寄存器 Bool 在全局 ABCD/CDAB 下按低字节在后取位,BADC/DCBA 按原始响应取位;Packer 的 format 必须与实际 Modbus 连接一致。仅调整新生成批的模型,不改透传 ByteArray 的旧模型。
线缆字段边界: Siemens 普通字节号≤0x1FFFFF,T/C元素号≤0xFFFFFF且禁点号,T/C/AI/AQ 的Byte与奇数字节文本透传;MC十六进制或ZR地址≤0xFFFFFF,其余≤999999;Keyence MC按转换后的线缆值校验(EM输入≤899999,ZF按十六进制字段);Cimon≤999999;GE一基地址1..65536;Toyo转换后物理字号≤65535、非强制区prg≤255;Vigor转换后BCD地址≤999999(D先减9000,M≥9000映射0)。
Vigor C32: C200起每元素4字节,文本响应Length×4,其余字区Length×2。消费层仍直传元素数,普通区64元素/帧、C32为32元素/帧,可能分别多读2倍/4倍,不代表已消除多读或完成硬件验证。
PackerFactory(静态、线程安全注册表)
| 成员 | 签名 | 描述 |
|---|---|---|
Register |
void Register(IPackerCore packer) |
注册 Packer(ConcurrentDictionary<ProtocolFamily, IPackerCore>) |
GetPacker |
IPackerCore? GetPacker(ProtocolFamily family) |
按协议族查找 |
GetPacker |
IPackerCore? GetPacker(string deviceType) |
按设备类型查找(不区分大小写的枚举解析) |
GetRegisteredFamilies |
ProtocolFamily[] GetRegisteredFamilies() |
已注册协议族快照 |
Count |
int Count { get; } |
已注册 Packer 数量 |
ProtocolFamily
public enum ProtocolFamily
{
Siemens, Modbus, Mitsubishi, Omron, Fuji, Keyence, Yokogawa,
Panasonic, Yaskawa, GE, Fatek, Fanuc, LSis_Cnet, LSis_Cpu, LSis_FastEnet,
Cimon, XinJE, Vigor,
Toyota, AllenBradley, MitsubishiFx, Beckhoff, Fuji_SPB, Fuji_CST
}
每个值带 [Description] 中文名(如 西门子 S7、三菱 Melsec、欧姆龙 FINS)。
PackerHandler
入口类位于 Snet.Core.handler(文件位于 Snet.Core/handler/PackerHandler.cs),与 BytesHandler 同命名空间。它是 IDaq.Packer/IDaq.UnPacker 内部委托的单例池处理器。
编码说明(26.236.x 起): PackerHandler 静态构造函数注册 CodePagesEncodingProvider.Instance——类型首次使用即注册,GB2312/GBK(936)与 GB18030(54936)立即可解析,组包/解包 GB2312 编码字符串无需手动调用 Encoding.RegisterProvider。
public class PackerHandler : CoreUnify<PackerHandler, string>
{
public PackerHandler();
public PackerHandler(string basics);
public static string[] GetSupportAutoPackDeviceTypes(); // 已注册且启用优化的族名称
public static ProtocolFamily[] GetFullAutoPackFamilies(); // SupportsAutoPack == true 的协议族
public static bool CanAutoPack(string vendorProtocolTypeName);
public Address? AddressAutoPack(Address address, ProtocolFamily family, int maxByteLength = 0, DataFormat format = DataFormat.ABCD);
public Address? AddressAutoPack(Address address, string deviceType = "Siemens", int maxByteLength = 0, DataFormat format = DataFormat.ABCD);
public Task<Address?> AddressAutoPackAsync(Address address, ProtocolFamily family, int maxByteLength = 0, DataFormat format = DataFormat.ABCD, CancellationToken token = default);
public Task<Address?> AddressAutoPackAsync(Address address, string deviceType = "Siemens", int maxByteLength = 0, DataFormat format = DataFormat.ABCD, CancellationToken token = default);
public Address? AddressAutoPackOrPassthrough(Address? address, string vendorProtocolTypeName, int maxByteLength = 0, DataFormat format = DataFormat.ABCD);
public Task<Address?> AddressAutoPackOrPassthroughAsync(Address? address, string vendorProtocolTypeName, int maxByteLength = 0, DataFormat format = DataFormat.ABCD, CancellationToken token = default);
}
💻 代码示例
打包分散地址批次(西门子)
using Snet.Siemens;
using System.Collections.Concurrent;
using Snet.Core.handler; // PackerHandler, BytesHandler
using Snet.Model.data;
using Snet.Model.@enum;
await using var op = await SiemensOperate.InstanceAsync(new SiemensData.Basics
{
IpAddress = "192.168.1.100",
Port = 102
});
// 分散地址:DB1 中偏移不连续的字
var address = new Address(new List<AddressDetails>
{
new AddressDetails("DB1.DBD0", DataType.Float),
new AddressDetails("DB1.DBD4", DataType.Float),
new AddressDetails("DB1.DBW8", DataType.Int),
new AddressDetails("DB1.DBX10.3", DataType.Bool)
});
// 打包为最小批量读取(S7 上限:240 字节 / 960 物理)
var handler = await PackerHandler.InstanceAsync("myHandler");
var packed = handler.AddressAutoPackOrPassthrough(address, "SiemensS7Net_S1200", maxByteLength: 0);
if (!(await op.OnAsync()).Status) return;
// 一次往返读取打包后的批次
var result = await op.ReadAsync(packed!);
if (!result.Status) return;
var data = result.GetSource<ConcurrentDictionary<string, AddressValue>>();
if (data is null) return;
// 拆包:每个批次的原始字节必须经 TransformAsync 解析回各地址的值
using var bytesHandler = BytesHandler.Instance(Guid.NewGuid().ToString());
foreach (var kv in data)
{
// kv.Value 携带 OriginalValue(批次字节)+ AddressExtendParam(List<BytesModel>)
var unpacked = await bytesHandler.TransformAsync(kv.Value);
if (!unpacked.Status) Console.Error.WriteLine(unpacked.Message);
var values = unpacked.GetSource<ConcurrentDictionary<string, AddressValue>>();
if (values is null) continue;
foreach (var v in values)
Console.WriteLine($"{v.Key} = {v.Value.ResultValue}");
}
Modbus 富地址
ModbusPacker 接受零基地址 + 富修饰符(任意顺序;最后一个非修饰符段为地址本体):
using Snet.Modbus;
using Snet.Model.data;
using Snet.Model.@enum;
var op = await ModbusOperate.InstanceAsync(new ModbusData.Basics
{
IpAddress = "192.168.1.100",
Port = 502
});
var address = new Address(new List<AddressDetails>
{
new("R1", "100", DataType.Int), // 保持寄存器 100(FC3)
new("R2", "x=4;100", DataType.Int), // 输入寄存器 100(FC4)
new("C1", "100", DataType.Bool), // 线圈 100(FC1)
new("B1", "100.1", DataType.Bool), // 寄存器 100 的位 1(FC3)
new("R3", "100H", DataType.Int), // 十六进制地址 → 256
new("R4", "format=DCBA;200", DataType.Int), // 地址级格式覆盖
new("R5", "file=0;100", DataType.Int), // 文件寄存器(FC20/21)
new("R6", "s=2;100", DataType.Int) // 站号 2
});
var packed = await op.PackerAsync(address, "ModbusTcpNet");
40001–49999 范围的地址不再换算——地址零基原样传递(与 ReadAsync 一致)。Delta、MegMeet 及 Inovance 非 ComputerLink 协议当前整体透传;ComputerLink 使用 FX Links 模型。2026-08 重构起修饰符值严格校验(s=/x=/w= 必须可装入 byte、file= 装入 ushort、format= 必须是 ABCD/BADC/DCBA/CDAB 之一)——越界或未知的修饰符值使整个地址不可组包(原样保留,而非旧行为的静默跳过)。
不支持协议的透传
// 标签型协议原样返回输入
var unchanged = handler.AddressAutoPackOrPassthrough(address, "OmronCipNet");
// → 地址不变,不应用组包
查看已注册协议族
using Snet.Core.packer;
using Snet.Core.handler;
string[] types = PackerHandler.GetSupportAutoPackDeviceTypes();
foreach (var family in PackerFactory.GetRegisteredFamilies())
Console.WriteLine(family);
❓ 常见问题
1. 何时应使用自动组包?
当读取大量分散地址(分散的 DB 偏移、不连续寄存器)时,组包将地址合并为少量批量读取,往返次数从 N 降到约 1。对于密集、已连续的读取,组包无额外收益——直接使用 ReadAsync 即可。
2. maxByteLength 参数是什么意思?
0(推荐)使用保守的 MaxReadLength;正数受 PhysicalMaxBytes 和 **65535 字节(ushort.MaxValue)**约束。单点真实响应无法放入批次时降级,保留原 Length,不中止整次组包;全部透传点保留原对象及扩展参数。禁用/虚拟/非法/溢出点不被强制优化,组包不启用禁用点,也不修复非法地址。
3. 组包过程中地址会丢失吗?
不会。组包批次在 AddressDescribe 字段标记 packer - {n};不可组包地址不带该标记、原样透传。结果始终包含每个输入地址。
4. PackerHandler 位于哪个命名空间?
PackerHandler(及 BytesHandler)位于 Snet.Core.handler;组包引擎(PackerFactory 及各族 IPackerCore 实现)位于 Snet.Core.packer。两个命名空间都随 Snet.Core 包提供。
5. 读取组包批次后如何解析数据?
读取结果是原始批次字节,而非各地址的值。使用 BytesHandler.TransformAsync(AddressValue) 拆包:它从返回的 AddressValue 读取 OriginalValue(批次字节)与 AddressExtendParam(List<BytesModel>),返回以地址名为键的 ConcurrentDictionary<string, AddressValue>。参见上方快速开始示例。当同一地址名携带两种数据类型(如 Modbus 100 的 Bool + Int)时,两个值都保留——第二个键加 DataType 后缀。
🔗 相关资源
- IPacker 接口 — 高层
IDaq级组包 API - 地址系统
- DaqAbstract 基类
- CoreUnify 基类
