:Utility tables(class_id = 26)—— 把“任意表数据“封装成 COSEM 对象的通用集装箱)
DLMS/COSEM 蓝皮书解读二十三Utility tablesclass_id 26—— 把任意表数据封装成 COSEM 对象的通用集装箱系列说明本系列基于 DLMS UA《Blue Book蓝皮书第 16 版 · 第 2 部分》一个接口类一篇。第 21、22 篇都在讲通信端口怎么配载波双绞线、有线 M-Bus。本篇话题一转Utility tablesclass_id 26是一个与通信介质无关、纯数据载体的类——它把任意表table数据ANSI 标准表或厂商自定义表封装成一个 COSEM 对象用table_ID标识、用buffer存内容并支持按偏移/按索引的选择性访问。上篇回顾第 22 篇把 M-Bus 从站端口配好了设备能被主站找到。但很多时候主站想搬的不只是 DLMS 原生的属性数据还有别的体系定义的表比如 ANSI C12 的标准表、或表厂私有的配置表。这些结构 DLMS 并没有对应接口类。本篇Utility tables就是干这个的——一个通用集装箱让非 DLMS 原生的表数据也能在 COSEM 对象模型里被寻址、读取。0. 为什么需要这个类DLMS/COSEM 的强项是用接口类描述对象。但现实世界里表计里还躺着大量不是 DLMS 原生定义的数据结构ANSI 标准表ANSI C12 系列定义的那些表比如设备配置表、历史表厂商自定义表表厂私有的参数表、诊断表这些数据如果硬要映射成Data/Register之类的接口类要么丢结构、要么极繁琐。蓝皮书给的解法是——直接整张表塞进一个对象蓝皮书原文Utility tables, Overview“This IC allows encapsulating table data. Each ‘table’ is represented by an instance of this IC, identified by its logical name.”一句话一个表 一个Utility tables实例用logical_name寻址、table_ID标明是哪张表、buffer装原始字节。它就像一个万能集装箱把任何表结构原样搬进 COSEM 对象模型主站再按自己的理解去解析buffer里的字节。1. 类蓝图蓝皮书原文Utility tables“Utility tables 0…n class_id 26, version 0”Utility tables 0...n class_id 26, version 0属性静态/动态数据类型MinMaxDefShort namelogical_namestaticoctet-stringxtable_IDstaticlong-unsignedx 0x08lengthstaticdouble-long-unsignedx 0x10bufferstaticoctet-stringx 0x18没有方法Specific methods 一栏为空。全部static靠 GET/SET 访问。2. 属性逐条解读2.1 logical_name标识本Utility tables对象实例。6 字节 OBISstatic基址x。蓝皮书原文logical_name“Identifies the ‘Utility tables’ object instance.”2.2 table_ID表号Table number。蓝皮书原文table_ID“Table number. This table number is as specified in the ANSI standard and may be either a standard table or a manufacturer’s table.”staticlong-unsignedShort namex 0x08。两种来源ANSI 标准表standard table编号由 ANSI 标准规定厂商自定义表manufacturer’s table编号由厂商规定。背景说明非蓝皮书原文属 ANSI C12 领域常识ANSI C12.19 把表分成标准表如 0 号表是设备标识、1 号表是设备配置和厂商表通常编号高位段如 0x8000 起。table_ID具体取值含义取决于你的设备遵循哪份表定义蓝皮书只说按 ANSI 标准。2.3 lengthbuffer中的字节数。蓝皮书原文length“Number of octets in table buffer.”staticdouble-long-unsigned4 字节无符号最大约 4 GBShort namex 0x10。注意它是字节计数不是元素个数——buffer是裸 octet-string内部怎么分元素由表定义决定。2.4 buffer表的实际内容原始字节。蓝皮书原文buffer“Contents of the table.”staticoctet-stringShort namex 0x18。这就是那个集装箱本身——表里每一字节原样存放DLMS 不解释其内部结构。3. 没有方法靠选择性访问selective access取数本类没有 ACTION 方法。它真正的巧思在buffer 属性的选择性访问上——因为buffer可能很大、内部结构复杂主站往往只想取其中一段而不是整张表。蓝皮书原文Utility tables, Selective access“Selective access (see ) to the attribute buffer may be available (optional). The selective access parameters are defined in .”注意原文写的是may be available (optional)——也就是说选择性访问是可选的不是每个实现都支持。不支持时主站只能整表 GET。支持时提供两种 access selectorAccess selector参数含义1offset_access按偏移 字节数访问offset_selector2index_access按元素层级索引 元素数访问index_selector3.1 offset_selectorselector 1按字节偏移offset_selector :: structure { Offset: double-long-unsigned // 相对表起始的字节偏移 Count: long-unsigned // 请求/传输的字节数 }3.2 index_selectorselector 2按层级索引index_selector :: structure { Index: array long-unsigned // 标识表层级中元素的索引序列 Count: long-unsigned // 请求/传输的元素数 // 请求中 Count0 表示从选择点起整棵子树 }蓝皮书原文index_selector“Values of count greater than 1 return up to that many elements. A value of zero, when given in the context of a request, refers to the entire sub-tree of the hierarchy starting at the selection point.”关键语义index_access的Index是一个数组long-unsigned 序列表达的是层级路径——比如[1, 2]表示第 1 层的第 2 个子元素Count表示要取几个元素Count 0在请求里表示从选择点开始整棵子树全要。4. 【实战举例】示例 1一张厂商配置表的标准对象建模以下 OBIS / table_ID / 字节为示例非蓝皮书原文实际以设备对象列表为准属性取值说明logical_name0-0:65.16.1.255示例 OBIStable_ID0x0001厂商自定义表 0x0001示例编号length256buffer 共 256 字节buffer256 字节原始数据厂商表内部字节主站想读整张表就 GETbuffer短名x 0x18。但 256 字节一次拉可能过大于是用选择性访问。示例 2按偏移取前 16 字节selector 1offset_access请求 GETbuffer并带access_selector 1参数是offset_selector{ Offset 0, Count 16 }A-XDR 编码structure标签02Offset为double-long-unsigned标签06、4 字节 00 00 00 00Count为long-unsigned标签12、2 字节 00 10 1602 0A 06 04 00 00 00 00 12 02 00 10 示例selector1 参数offset0, count16含义从表头偏移 0 起取 16 字节。主站拿到这 16 字节后按厂商表定义解析。示例 3按层级索引取元素selector 2index_access请求 GETbuffer带access_selector 2参数index_selector{ Index [1, 2], Count 1 }A-XDR 编码structure标签02Index为array标签01、元素数 2、元素均为long-unsigned标签12值 100 01、值 200 02Count为long-unsigned00 0102 0E 01 02 12 02 00 01 12 02 00 02 12 02 00 01 示例selector2 参数Index[1,2], Count1含义取第 1 层第 2 个子元素那 1 个元素。Count 0时则取从该选择点起的整棵子树。示例 4SN 短名访问base name 为x由 logical_name 派生的 13 位短名属性Short nametable_IDx 0x08lengthx 0x10bufferx 0x18例如想快速确认缓冲区大小GET 短名x 0x10lengthdouble-long-unsigned即可不必拉整个buffer。示例 5与Profile genericclass_id 7的区别两者都装结构化数据但定位完全不同维度Utility tables (26)Profile generic (7)数据来源任意 ANSI/厂商表外部定义结构COSEM 捕获对象capture_objects 定义内部结构DLMS 不解释纯字节由 capture_objects 明确定义列选择性访问offset / index可选range_descriptor / entry_descriptor用途搬运非 DLMS 原生表计量档案/负荷曲线一句话Profile generic管DLMS 自己采的曲线Utility tables管别人定义的表。示例 6用 offset_access 读一张厂商表并逐字节解析假设table_ID 0x0001的厂商表前 16 字节布局以下布局为示例非蓝皮书原文字节偏移长度字段示例值0–12表版本uint160x 00 01→ 12–54累计脉冲数uint320x 00 0B EA 60→ 12345606–72状态字0x 00 008–158保留0x 00 …主站用 selector 1、offset_selector{ Offset0, Count16 }即示例 2 的 A-XDR02 0A 06 04 00 00 00 00 12 02 00 10GETbuffer拿回 16 字节00 01 00 0B EA 60 00 00 00 00 00 00 00 00 00 00按上表解析表版本1、累计脉冲数1234560、状态0。这就是Utility tables的搬运 主站自解析模式——DLMS 不解释内部解析规则由表定义ANSI/厂商给出。示例 7设备不支持选择性访问时的兜底若设备buffer的选择性访问未实现may be available (optional)主站只能 GET 整个buffer先 GETlength短名x 0x10确认字节数避免盲目拉大块若length在单帧范围内直接整表 GETbuffer短名x 0x18若过大依赖链路层分片/分段如 HDLC 的 segmented GET或退而求其次只取关键区段——但没有选择性访问就不能按 offset/index 精确定位只能主站侧自己切。示例 8用 index_access 逐层解析一张层级表假设table_ID 0x0001是一张ANSI 标准表内部是 2 层结构第 1 层是若干记录record第 2 层是记录内的字段field以下层级为示例非蓝皮书原文。取第 3 条记录index_selector{ Index [3], Count 1 }→ 返回第 3 条记录整体取第 3 条记录的第 2 个字段index_selector{ Index [3, 2], Count 1 }→ 返回该字段取第 3 条记录起的整棵子树index_selector{ Index [3], Count 0 }→Count 0在请求中表示从选择点起整棵子树。A-XDR 编码Index [3, 2], Count 1array 标签01、2 元素、各long-unsigned标签12Countlong-unsigned02 0E 01 02 12 02 00 03 12 02 00 02 12 02 00 01 示例selector2Index[3,2], Count1注意Index是数组即使只一层也要写成单元素数组[3]不是裸整数漏写数组包裹会让选择性访问参数结构不符、被设备拒绝。示例 9Utility tables 与 Data 类的取舍两者都装任意字节但定位不同以下对比为工程归纳非蓝皮书原文维度Utility tables (26)Data (1)标识table_ID表号 logical_name仅 logical_name内部结构不解释整张表字节单值或结构类型明确适用现成的 ANSI/厂商表自定义的单点/结构数据选择性访问offset / index可选无整属性 GET/SET经验法则数据已经是某份标准表/厂商表定义好的整体 → 用Utility tables数据是你自己在 DLMS 里新定义的点或结构 → 用Data。别把Data能装的下就不必用Utility tables当成铁律——当数据有明确表号语义、且要和 ANSI 体系互通时Utility tables才是正解。示例 10主站解析 Utility tables 的通用步骤以下流程为工程归纳非蓝皮书原文1. 枚举对象列表找到 class_id 26 的实例记下 logical_name 2. GET table_IDx0x08 → 确认这是哪份表标准表/厂商表 3. GET lengthx0x10 → 确认 buffer 字节数决定整取还是分段 4. 若设备支持选择性访问 4a. 按字节取 → selector1, offset_selector{Offset, Count} 4b. 按层级取 → selector2, index_selector{Index[], Count} 否则GET 整个 bufferx0x18 5. 按 table_ID 指向的表定义ANSI/厂商文档解析 buffer 字节 6. 解析规则不在 DLMS 内主站必须自带表字典。核心提醒Utility tables只负责搬运解析规则是主站侧的表字典。同一份buffer不知道table_ID对应的表定义就无从解析——所以第 2 步确认table_ID是整条链路的前提。5. 工程上容易踩的坑选择性访问是可选的原文明确写 “may be available (optional)”。别假设所有设备都支持 selector 1/2不支持时只能整表 GET要评估length会不会撑爆报文。length 是字节数不是元素数buffer是裸 octet-stringlength计的是字节。想把 buffer 当行/元素解析内部布局必须由表定义ANSI/厂商决定DLMS 不管。index_access 的 Index 是层级路径[1, 2]不是第 1 和第 2 个元素而是第 1 层里的第 2 个子节点。层级语义由表结构决定乱填会取到错误的子树。Count 0 在请求中的特殊含义selector 2 下Count 0表示整棵子树不是取 0 个。别当空请求用。table_ID 的编号空间要查清标准表按 ANSI、厂商表按厂商编号可能重叠在不同设备语义下。主站解析buffer前必须先确认table_ID指向哪份表定义否则字节错位。offset_access 越界Offset Count超过length时行为取决于实现可能截断或报错。取数前先 GETlength短名x 0x10确认边界。整表过大导致超时若设备不支持选择性访问且buffer很大整表 GET 可能触发长帧/分片或超时。优先用选择性访问分段拉取。offset_selector 的 Offset 是 double-long-unsigned4 字节别把Offset当成 2 字节的long-unsigned去编码。原文结构是Offset: double-long-unsigned示例 2 用的是 4 字节00 00 00 00若错编成 2 字节选择性访问参数会整体错位、被设备拒绝。6. 小结 下期预告本篇要点Utility tablesclass_id 26, version 0是与介质无关的数据集装箱把任意 ANSI/厂商表封装成 COSEM 对象四个属性table_ID表号long-unsigned、length字节数double-long-unsigned、buffer原始字节octet-string、logical_namebuffer 支持可选的选择性访问selector 1 offset_access偏移字节数selector 2 index_access层级索引元素数Count0 表整棵子树区别于Profile generic它管别人定义的表不管DLMS 自己采的数据。下一篇第 24 篇Modem configurationclass_id 27—— 调制解调器配置。本篇把非 DLMS 原生表数据装进了集装箱下一篇回到通信外设配置主线讲Modem调制解调器怎么在 COSEM 里建模——它同样是个让设备通过 PSTN/蜂窝等拨号网络接入主站的通信配置类和前面的端口类HDLC / 载波双绞线 / M-Bus形成不同通信外设的系列对照。参考资料DLMS UA《Blue Book Ed.16 Part 2 – COSEM interface classes》Utility tables(class_id 26, version 0) 章节。文中属性名、数据类型、Short name 偏移、offset_selector / index_selector 结构与引文均与原文一致示例中的 OBIS、table_ID、字节、A-XDR 编码为帮助理解而构造实际以设备对象列表为准。