地址自动组包 - Snet Docs

📦 地址自动组包(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 时按字节批解码;其他值以单点字典返回原 AddressValue 同对象,Status 由 Quality==Normal 决定。已识别字节批须质量正常且 OriginalValue 为 byte[],否则按每个模型返回 Exception 质量点并置 Status=false。偏移、长度或 BoolIndex 越界保留失败项,成功项仍可读取。空字节对非空模型逐项校验边界,空模型可成功返回空字典。异步取消可能传播异常,不承诺取消仅返回失败结果。

字典重载可统一处理组包批与直接值,对各项 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 覆盖默认注册。

PhysicalMaxBytes = 131070 是当前模型可接受的上限,实际 Driver 可能分帧、使用不同请求单位;并非所有硬件读长保证。单个优化批上限仍为65535字节。全部数组原对象透传,文本按 Driver 响应单位及有效上限决定是否优化;最小真实响应也计入限制。 位/字分区

所有 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 后缀。


🔗 相关资源