Python DLMS

该库是基于开源的C-DLMS库进行开发的,并且支持 Client / Server 模式。

支持 DLMS 功能的模组型号如下:

系列 型号
EC200U EC200UCN_AA_DLMS

💡 Tips

  • 本库支持Client/Server模式,适用于智能电表等物联网设备。
  • 商业应用请走商务流程。

DLMS COSEM Objects

COSEM Objects 共有属性和方法

Event hooks

dlms 模块中的每个对象都是某个子类的实例,为每个对象添加了以下功能:
六个属性(CosemObject,基类)
on_before_read、on_after_read、on_before_write、on_after_write、on_before_action、on_after_action ,每个对象都可以设置为一个回调函数。

import dlms
# 使用clock对象+on_before_read举例子
#on_after_read、on_before_write、on_after_write、on_before_action、on_after_action也类似
clock = dlms.Clock("0.0.1.0.0.255")

def on_before_read_clock(self, event):
    if event.index == 2:  # attr 2 = time
        print("[DLMS] Client is reading clock.time")
        # 返回 True 或 None → 允许读取(C 层已从 RTC 刷新了时间)
        return True

    elif event.index == 3:  # attr 3 = time_zone
        print("[DLMS] Client is reading clock.time_zone")
        return True

    return True  # 默认允许

# 绑定 handler 在client/server通信中才能触发
clock.on_before_read = on_before_read_clock

Per-instance access control

access_dict 属性接受一个与 access 构造器参数格式相同的访问字典,并会覆盖该特定对象实例的类级别默认值。

每个对象都有一个默认的访问字典,该字典由 set_default_access 函数设置。

import dlms
# 使用Clock对象举例子,因为每个对象都可以设置access_dict属性,所以可以针对每个对象设置不同的访问字典
# 类级别:所有 Clock 默认客户端只读
dlms.set_default_access(dlms.Clock, {
    2: (dlms.AccessMode.READ, dlms.Authentication.NONE),   # time 只读
    3: (dlms.AccessMode.READ, dlms.Authentication.NONE),   # time_zone 只读
})

# 两个 Clock 对象
clk_a = dlms.Clock("0.0.1.0.0.255")  # 允许客户端只读
clk_b = dlms.Clock("0.0.2.0.0.255")  # 允许客户端只读

# 但 clk_b 需要允许 HLS 客户端同步时间 → 实例级别覆盖
clk_b.access_dict = {
    2: (dlms.AccessMode.READ_WRITE, dlms.Authentication.HIGH),  # time 可写
}

obj.idx(name)+obj.attr_name(index)

每个对象都可以使用这两种方法

import dlms
#Clock对象使用obj.idx和obj.attr_name举例子,每个对象都可以这样参考clock的例子来写
# 1. 创建 Clock 对象
clock = dlms.Clock("0.0.1.0.0.255", time_zone=8, deviation=0)
print("=== COSEM Objects ===")
print(clock)
print()
# 2. idx(name) — Python 名 → DLMS 编号
print("clock.idx('time')        = {}".format(clock.idx('time')))         # 2
print("clock.idx('time_zone')   = {}".format(clock.idx('time_zone')))    # 3
print("clock.idx('status')      = {}".format(clock.idx('status')))       # 4
print("clock.idx('begin')       = {}".format(clock.idx('begin')))        # 5
print("clock.idx('end')         = {}".format(clock.idx('end')))          # 6
print("clock.idx('deviation')   = {}".format(clock.idx('deviation')))    # 7
print("clock.idx('enabled')     = {}".format(clock.idx('enabled')))      # 8
print("clock.idx('base')        = {}".format(clock.idx('base')))         # 9
# 验证:不存在的名字会抛异常
print()
try:
    clock.idx('foobar')
except Exception as e:
    print("clock.idx('foobar') throw exception: {}".format(type(e).__name__, e))

# 3. attr_name(index) — DLMS 编号 → Python 名
print()
print("=== clock.attr_name() — DLMS Number → Python attr ===")
for i in range(2, 10):
    print("clock.attr_name({}) = '{}'".format(i, clock.attr_name(i)))

# 4. attrs() — 可持久化属性列表(跳过 VOLATILE)
print()
print("=== clock.attrs() — Persistent attribute list ===")
attrs_list = clock.attrs()
print("backup {} attributes:".format(len(attrs_list)))
for index, name in attrs_list:
    print("clock.attr_name({}) = '{}'".format(index, name))

print()
print("note: (2, 'time') had been skipped, because it's DLMS_ATTR_VOLATILE")

# 5. 实际使用:用 idx() 配合 client.read()
print()
print("=== real situation ===")
print("example client.read(clock, 2):")
print("  client.read(clock, clock.idx('time'))")
print("  client.read(clock, clock.idx('time_zone'))")
print("This way, the code becomes more readable and there is no need to remember the Blue Book numbers.")

obj.idx(name)+obj.attr_name(index) 实际应用

在事件处理程序和客户端代码中使用这些方法,可以避免硬编码魔术数字,从而无需查阅蓝皮书进行解码。例如,所以clock.idx('time') 返回 client.read(clock, clock.idx('time')) 等价于 client.read(clock, 2),但具有自文档化特性。 obj.attrs() 返回一个包含所有复杂属性(如 (attr_index, attr_name) 元组)的列表,这些属性将被 BinarySerializer 保存。不包括瞬态属性(如 Clock.time 和 ProfileGeneric.buffer)。该列表在构建自定义序列化器时尤为有用。

import dlms #该例子不完整,仅供说明参考
# × 没有 idx():必须记住每个属性的 Blue Book 编号,
client.read(clock, 2)       # 2 是什么?time?time_zone?
client.write(clock, 7, 60)  # 7 是什么?deviation?enabled?

# √ 有 idx():用 Python 属性名,自文档化
client.read(clock, clock.idx('time'))
client.write(clock, clock.idx('deviation'), 60)

Data

DLMS 中最通用的数据容器。可存储 任意类型 (整数、字符串、字节、布尔值、空值,甚至引用其他对象的属性),其 COSEM ID=1

Blue Book 属性

编号 名称 类型 说明
1 logical_name octet-string(6) OBIS 代码,只读
2 value CHOICE(任意类型) 存储的实际数据

构造函数

Data(logical_name: str, nocopy: bool = False, access: dict = None)
参数 类型 默认值 说明
logical_name str 必传 OBIS 代码字符串,如 "1.0.1.8.0.255"
nocopy bool False 若为 True value 可设为 (obj, attr_index) 元组引用其他属性
access dict None 实例级权限,格式 {attr_index: (AccessMode, Authentication)}

支持的 value 类型

import dlms
#初始化Data对象
data = dlms.Data("1.0.1.8.0.255",nocopy=False,access={2: (dlms.AccessMode.READ_WRITE, dlms.Authentication.HIGH)})
#Data对象有logical_name和value属性
print(data.logical_name)  #输出“1.0.1.8.0.255”
print(data.value)         #输出None

#另一种输出Data对象的属性的方式
print(data)  # <Data ln='1.0.1.8.0.255', value=None>

#设置Data对象的value属性
data.value = 42           # 整数
print(data.value)         # 输出42

data.value = b"\x01\x02"  # 字节串
print(data.value)         # 输出DLMS协议的b'\x01\x02' 

data.value = "hello"      # 字符串
print(data.value)         # 输出DLMS协议的"hello"

data.value = True         # 布尔值
print(data.value)         # 输出True

data.value = None         # 设置为None
print(data.value)         # 输出None

实时引用(BYREF)模式( nocopy=True

nocopy=True 构造 Data 对象,并将 (target_object, attribute_index) 元组赋值给 value ,即可启用实时引用模式。此后当客户端读取该 Data 对象时,服务器会在读取的那一刻透明地从目标对象取回被引用属性的 当前值 ,而不做任何复制。该模式最常见的用途是暴露位于 SecuritySetup 对象内部的调用计数器(invocation counter)。

import dlms
data = dlms.Data("1.0.1.8.0.255",nocopy=True,access={2: (dlms.AccessMode.READ_WRITE, dlms.Authentication.HIGH)})
sec = dlms.SecuritySetup("0.0.43.0.2.255")
data.value = (sec, 6)       # 获取SecuritySetup对象的in_invocation_counter访问权
print(data.value)           # 输出sec.min_invocation_counter的值,开始未赋值,输出0

sec.min_invocation_counter = 100
print(data.value)           # 输出sec.min_invocation_counter的值,现在为100

on_before_write — 写前验证

Python 属性一览

属性 类型 可写 说明
logical_name bytes × OBIS 代码(6 字节)
value int | bytes | str | bool | None | tuple 数据值,支持引用模式
on_before_read Callable 读前钩子(继承自 CosemObject)
on_after_read Callable 读后钩子(继承自 CosemObject)
on_before_write Callable 写前钩子(继承自 CosemObject)
on_after_write Callable 写后钩子(继承自 CosemObject)
on_before_action Callable 动作前钩子(继承自 CosemObject)
on_after_action Callable 动作后钩子(继承自 CosemObject)
access_dict dict 实例级访问控制

访问控制

import dlms

# 构造函数中指定
data = dlms.Data("1.0.1.8.0.255",access={2: (dlms.AccessMode.READ_WRITE, dlms.Authentication.HIGH)})
# 输出{2: (AccessMode.READ_WRITE, Authentication.HIGH)} 安全模式的返回值可看安全模型(Security)章节
print(data.access_dict)
# 或事后修改
data.access_dict = {2: (dlms.AccessMode.READ, dlms.Authentication.HIGH)}
 # 输出{2: (AccessMode.READ_WRITE, Authentication.HIGH)}  安全模式的返回值可看安全模型(Security)章节
print(data.access_dict) 

典型用例

场景 示例
存储电表序列号 data.value = 12345678
存储固件版本字符串 data.value = "v1.2.3"
存储自定义状态标志 data.value = True
事件通知的消息载体 data.value = "Tamper detected"
BYREF 联动读写 data.value = (other_obj, 2)

Register

DLMS 中表示 单一物理测量量 的标准对象。用于电能读数、瞬时功率、电压、电流及类似测量。内置 scaler (缩放因子)实现原始整数值到物理量的转换, unit 由 OBIS 代码自动派生,其 COSEM ID=3

Blue Book 属性

Python 属性 类型 DLMS 属性编号 说明
logical_name str 1 OBIS 代码,只读
value int 2 原始整数值,尚未应用 scaler
scaler int 3(部分) 10 的幂次指数。
unit int(只读) 3(部分) 物理单位, Unit 枚举值。由 OBIS 代码在构造时自动派生,不可修改
import dlms
reg = dlms.Register(
    "1.0.1.8.0.255",
    0, 
    scaler=-3,
    access={2: (dlms.AccessMode.READ_WRITE, dlms.Authentication.HIGH)})
print(reg)               #输出<Register ln='1.0.1.8.0.255', value=0, scaler=-3, unit=Unit.ACTIVE_ENERGY>
#属性修改或者不可修改
print(reg.logical_name)  #输出“1.0.1.8.0.255” reg.logical_name初始化后不可修改
reg.value = 12345678
print(reg.value)         # 输出12345678
reg.scaler = -5
print(reg.scaler)        # 输出-5
print(reg.unit)          # 输出Unit.ACTIVE_ENERGY ,OBIS 代码在构造时自动派生,不可修改

构造函数

Register(logical_name: str, default_value: int, scaler: int = 1, access: dict = None)
参数 类型 默认值 说明
logical_name str 必传 OBIS 代码字符串,如 "1.0.1.8.0.255"
default_value int 必传 初始值,也是 reset() 恢复的目标值
scaler int 1 10 的幂指数。 -3 表示原始值 ÷ 1000
access dict None 实例级权限,格式 {attr_index: (AccessMode, Authentication)}

物理量转换公式

physical_value = raw_value * (10 ** scaler)
scaler 含义 示例
0 原始值即物理值 value=100 100 kWh
-3 原始值 ÷ 1000 value=12345678 12345.678 kWh
3 原始值 × 1000 value=12 12000 kWh

unit (只读)由 OBIS 代码自动派生——例如 1.0.1.8.0.255 自动映射到 Unit.ACTIVE_ENERGY ,无需手动设置。

import dlms
reg = dlms.Register(
    "1.0.1.8.0.255", 
    0, 
    scaler=-3,
    access={2: (dlms.AccessMode.READ_WRITE, dlms.Authentication.HIGH)})
reg.value = 1234789
raw_value = reg.value * (10 ** reg.scaler) # 1234.789
方法 COSEM 方法 说明
reset() 1 value 重置为构造时的 default_value

如需 reset() 时同步清除硬件状态,使用 on_before_action 拦截:

# 默认行为:C 层直接恢复 value → 0
# reg.reset() 该方法并注册开到python库中,而是在通信时,触发before_reset回调
# 成功return ture则调用C层的reset方法,否则不调用C层的reset方法
import dlms
def before_reset(self, event):
    if event.index == 1:  # method 1 = reset
        # 此处可以调用硬件 API 清零外部计数器
        pass
    return True  # 允许执行

reg.on_before_action = before_reset

Python 属性一览

属性 类型 可写 说明
logical_name bytes × OBIS 代码(6 字节)
value int 当前测量值(原始整数,未应用 scaler)
scaler int 10 的幂指数,用于计算物理量
unit int × 物理单位( Unit 枚举),由 OBIS 代码自动派生,不可修改
on_before_read Callable 读前钩子(继承自 CosemObject)
on_after_read Callable 读后钩子(继承自 CosemObject)
on_before_write Callable 写前钩子(继承自 CosemObject)
on_after_write Callable 写后钩子(继承自 CosemObject)
on_before_action Callable 动作前钩子(继承自 CosemObject)
on_after_action Callable 动作后钩子(继承自 CosemObject)
access_dict dict 实例级访问控制

访问控制

import dlms
# 构造函数中指定
reg = dlms.Register("1.0.1.8.0.255", 0, scaler=-3,
    access={2: (dlms.AccessMode.READ_WRITE, dlms.Authentication.HIGH)})

# 或事后修改
reg.access_dict = {2: (dlms.AccessMode.READ_WRITE, dlms.Authentication.HIGH)}

典型用例

场景 示例
总正向有功电能,scaler=-3 换算 kWh Register("1.0.1.8.0.255", 0, scaler=-3)
单相瞬时电压,scaler=-1 换算 V Register("1.0.32.7.0.255", 0, scaler=-1)
单相瞬时电流,scaler=-2 换算 A Register("1.0.31.7.0.255", 0, scaler=-2)
单相有功功率 Register("1.0.21.7.0.255", 0, scaler=0)

ExtendedRegister

Register 的扩展版本。在 value scaler unit 之外增加了 status (状态码)和 capture_time (捕获时间戳)两个属性,用于记录测量值产生时的上下文信息,其 COSEM ID=4

Blue Book 属性

编号 名称 类型 说明
1 logical_name octet-string(6) OBIS 代码,只读
2 value CHOICE 当前测量值(支持多种数值类型)
3 scaler int 10 的幂次指数。
4 unit int 物理单位, Unit 枚举值。由 OBIS 代码在构造时自动派生,不可修改
5 status int 应用定义的状态码, 0 =OK, None =未记录
6 capture_time octet-string 值的捕获时间戳

unit (只读)由 OBIS 代码自动派生,逻辑与 Register 完全一致。

import dlms
ext_reg = dlms.ExtendedRegister(
        "1.0.2.8.0.255",
        value=0,
        scaler=-3,
        status=0,
        capture_time=(2025, 1, 1, 0, 0, 0),
        access={2: (dlms.AccessMode.READ_WRITE, dlms.Authentication.HIGH)}
)
print(ext_reg) 
# 输出<ExtendedRegister ln='1.0.2.8.0.255', value=0, scaler=-3, unit=Unit.ACTIVE_ENERGY, status=0, capture_time=(2025, 1, 1, 0, 0, 0)>
#同时也可以单个打印:ext_reg.logical_name...
#部分属性可进行修改,例:
print(ext_reg.capture_time) #输出(2025, 1, 1, 0, 0, 0)
ext_reg.capture_time = (2026, 1, 1, 0, 0, 0) #变更为 2026, 1, 1, 0, 0, 0)
#部分属性不可修改,例:
print(ext_reg.logical_name) #输出“1.0.2.8.0.255” ext_reg.logical_name初始化后不可修改


构造函数

ExtendedRegister(logical_name: str, value: int | float | str = 0, scaler: int = 0,
                 status: int | None = None, capture_time: tuple | None = None,
                 access: dict = None)
参数 类型 默认值 说明
logical_name str 必传 OBIS 代码字符串,如 "1.0.2.8.0.255"
value int | float | str 0 初始测量值
scaler int 0 10 的幂指数。 -3 表示原始值 ÷ 1000
status int | None None 状态码, 0 =OK, None =未记录状态
capture_time tuple | None None 捕获时间戳 (year, month, day, hour, min, sec)
access dict None 实例级权限,格式 {attr_index: (AccessMode, Authentication)}

物理量转换公式(同 Register)

physical_value = raw_value * (10 ** scaler)
import dlms
ext_reg = dlms.ExtendedRegister(
        "1.0.2.8.0.255",
        value=0,
        scaler=-3,
        status=0,
        capture_time=(2025, 1, 1, 0, 0, 0),
        access={2: (dlms.AccessMode.READ_WRITE, dlms.Authentication.HIGH)}
)
ext_reg.value = 1234789
raw_value = ext_reg.value * (10 ** ext_reg.scaler) # 1234.789
方法 COSEM 方法 说明
reset() 1 value 重置为构造时的默认值;行为与 Register 一致
# 默认行为:C 层直接恢复 value → 0
# reext_regg.reset() 该方法并注册开到python库中,而是在通信时,触发before_reset回调
# 成功return ture则调用C层的reset方法,否则不调用C层的reset方法
import dlms
def before_reset(self, event):
    if event.index == 1:  # method 1 = reset
        # 此处可以调用硬件 API 清零外部计数器
        pass
    return True  # 允许执行

ext_reg.on_before_action = before_reset

Python 属性一览

属性 类型 可写 说明
logical_name bytes × OBIS 代码(6 字节)
value int | float | str 当前测量值(原始值,未应用 scaler)
scaler int 10 的幂指数,用于计算物理量
unit int × 物理单位( Unit 枚举),由 OBIS 代码自动派生
status int | None 状态码, 0 =OK, None =未记录
capture_time tuple | None 捕获时间戳 (year, month, day, hour, min, sec)
on_before_read Callable 读前钩子(继承自 CosemObject)
on_after_read Callable 读后钩子(继承自 CosemObject)
on_before_write Callable 写前钩子(继承自 CosemObject)
on_after_write Callable 写后钩子(继承自 CosemObject)
on_before_action Callable 动作前钩子(继承自 CosemObject)
on_after_action Callable 动作后钩子(继承自 CosemObject)
access_dict dict 实例级访问控制

对比 Register

特性 Register ExtendedRegister
value 类型 int int | float | str
status
capture_time
reset()

访问控制

import dlms
# 构造函数中指定
ext_reg = dlms.ExtendedRegister("1.0.2.8.0.255", value=0, scaler=-3,
    access={2: (dlms.AccessMode.READ_WRITE, dlms.Authentication.HIGH)})

# 或事后修改
ext_reg.access_dict = {2: (dlms.AccessMode.READ_WRITE, dlms.Authentication.HIGH)}

典型用例

场景 示例
总反向有功电能,带状态和捕获时间 ExtendedRegister("1.0.2.8.0.255", value=0, scaler=-3, status=0, capture_time=(2025, 1, 15, 10, 30, 0))
瞬时功率,浮动状态码 ExtendedRegister("1.0.21.7.0.255", value=0, scaler=0, status=None)

ProfileGeneric

DLMS 标准对象,用于 历史数据存储 (负荷曲线、事件日志、账单记录)。内部维护一个有序行缓冲区,每行是若干捕获对象在某一时刻的快照,其 COSEM ID=7

RAM Buffer (C 侧管理 外部存储 (Python 管理)
数据存放 gxProfileGeneric.buffer (堆内存) flash / JSONL / EEPROM / 用户自定
自动 capture cosem_invoke → 写入 RAM buffer on_before_action → 你的 flash 写入函数
客户端 read cosem_getValue → 从 RAM buffer 序列化 on_before_read → 你返回 list[list]
排序(FIFO 等) C 侧自动淘汰旧行 仅协议上报; 实际排序由 Python 实现
持久化 掉电丢失 自己控制
BinarySerializer N/A 自动跳过(buffer 标记为 COMPLEX)
适用场景 开发调试、小容量测试 生产环境 ── 必须使用

Blue Book 属性

编号 名称 DLMS 类型 Python 接口 说明
1 logical_name octet-string(6) logical_name OBIS 代码,只读
2 buffer ARRAY of STRUCTURE on_before_read 提供 历史数据行;Python 侧不能直接读写 通过 buffer_size 查看行数)
3 capture_objects ARRAY of STRUCTURE capture_objects 定义每行捕获的对象、属性和数据索引
4 capture_period uint32 capture_period 自动捕获周期(秒), 0 表示禁用
5 sort_method enum sort_method FIFO、LIFO、LARGEST 或 SMALLEST
6 sort_object STRUCTURE 见下方三个 Python 属性 LARGEST/SMALLEST 的排序参照
7 entries_in_use uint32 entries_in_use 当前历史记录数
8 profile_entries uint32 profile_entries 最大历史记录数

标准属性只有 1 到 8。 sort_object_attribute_index sort_object_data_index 不是属性 7、8,而是标准属性 6 sort_object 的组成部分:

Python 属性 对应 C 字段 默认值
sort_object sortObject None
sort_object_attribute_index sortObjectAttributeIndex 2
sort_object_data_index sortObjectDataIndex 0

排序方式常量

常量 说明
ProfileGeneric.FIFO 1 先进先出(最常用)
ProfileGeneric.LIFO 2 后进先出
ProfileGeneric.LARGEST 3 sort_object 排序,取最大值
ProfileGeneric.SMALLEST 4 sort_object 排序,取最小值
#使用DLMS RAM Buffer 模式
import dlms
clock = dlms.Clock("0.0.1.0.0.255", time_zone=480)
reg = dlms.Register("1.0.1.8.0.255", 0, scaler=-3)

# 先创建空壳
profile = dlms.ProfileGeneric("1.0.99.1.0.255")
print(profile.logical_name)                 # 输出 "1.0.99.1.0.255"
print(profile.buffer_size)                  #输出 0 当输出等于profile_entries时,表示buffer已满
# DLMS 属性 7,当前行数。每次 capture 自动 +1,上限 = profile_entries。
# 正常只读,不要手动改。
print(profile.entries_in_use)   

# 再逐步填充属性
profile.capture_objects = [clock, reg]       #  裸对象写法
profile.capture_period = 900                 # 15 分钟 更新 capture_objects对象的值
profile.sort_method = dlms.ProfileGeneric.FIFO
profile.profile_entries = 5                  # 5 行

# 需要capture_period设置为LARGEST和SMALLEST才生效,否则不生效
profile.sort_object = reg                    
profile.sort_object_attribute_index = 2      # 默认就是 2,可改
profile.sort_object_data_index = 0           # 默认就是 0,可改
# buffer 变化(FIFO, profile_entries=5):
# T0 (12:00):  [(2025,8,2,12, 0,0),  100]   ← 第 1 行
# T1 (12:15):  [(2025,8,2,12,15,0),  110]   ← 第 2 行
# T2 (12:30):  [(2025,8,2,12,30,0),  105]   ← 第 3 行
# T3 (12:45):  [(2025,8,2,12,45,0),   98]   ← 第 4 行
# T4 (13:00):  [(2025,8,2,13, 0,0),  130]   ← 第 5 行,buffer 满了
#──────────────────────────────────────────
# T5 (13:15):  [(2025,8,2,13,15,0),  125]   ← 踢掉 T0,T5 加末尾
# T6 (13:30):  [(2025,8,2,13,30,0),  115]   ← 踢掉 T1,T6 加末尾

#使用DLMS RAM Buffer 模式
import dlms
clock = dlms.Clock("0.0.1.0.0.255", time_zone=480)
reg = dlms.Register("1.0.1.8.0.255", 0, scaler=-3)

# 先创建空壳
profile = dlms.ProfileGeneric("1.0.99.1.0.255")
# 再逐步填充属性
profile.capture_objects = [clock, reg]       # 裸对象写法
profile.capture_period = 900                 # 15 分钟 更新 capture_objects所有对象的值
profile.sort_method = dlms.ProfileGeneric.LARGEST
profile.profile_entries = 5                  # 3 行

# 需要capture_period设置为LARGEST和SMALLEST才生效,否则不生效
profile.sort_object = reg                    # 按 reg 的值比大小
profile.sort_object_attribute_index = 2      # reg 的属性 2 = value(默认)
profile.sort_object_data_index = 0           # 取整个值(默认,reg 是整数不需要子索引)


# sort_object_attribute_index和sort_object_data_index的用法
# 按 Clock 的时间比大小,只比"小时"字段
# profile.sort_object = clock
# profile.sort_object_attribute_index = 2      # Clock 的属性 2 = datetime 值
# profile.sort_object_data_index = 3           # datetime=(年,月,日,时,分,秒),索引 3 = 小时
buffer 演变(LARGEST, profile_entries=3):
T0 (12:00):  [(12, 0,0),  100]                                   reg 值: 100
T1 (12:15):  [(12,15,0),  110]                                   reg 值: 100, 110
T2 (12:30):  [(12,30,0),  105]                                   reg 值: 100, 110, 105 → 满了!
─────────────────────────────────────────────────────────────────
T3 (12:45):  [(12,45,0),   98]  ← reg=110 最大 → 踢掉 T1 那行
             结果:[(12,0,0),100]  [(12,30,0),105]  [(12,45,0),98]


#sort_object_attribute_index和sort_object_data_index的用法
#sort_method = LARGEST,sort_object = clock,data_index = 3(按"小时"比较)
#T0 (12:00):  [(12, 0,0),  100]   时=12
#T1 (14:00):  [(14, 0,0),  105]   时=14
#T2 (12:30):  [(12,30,0),  110]   时=12  → 满了!
#─────────────────────────────────────
#T3 (16:00):  [(16, 0,0),   98]   ← 时=14 最大 → 踢掉 T1
#            结果:[(12,0,0),100]  [(12,30,0),110]  [(16,0,0),98]

构造函数

ProfileGeneric(logical_name: str, capture_objects: list = None,
               capture_period: int = None, sort_method: int = None,
               sort_object: object = None, profile_entries: int = None,
               entries_in_use: int = None, access: dict = None)
参数 类型 默认值 说明
logical_name str 必传 OBIS 代码字符串,如 "1.0.99.1.0.255"
capture_objects list None 每行捕获的内容,支持三种格式(见下方说明)
capture_period int 0 自动捕获间隔(秒), 0 禁用,常见值 900 (15 分钟)
sort_method int 0 (无效) 排序方式(见下方常量表)。 默认 0 不是有效值 ,请始终显式传入常量如 ProfileGeneric.FIFO
sort_object object None 排序参照对象,仅 LARGEST / SMALLEST 时需要
profile_entries int 0 缓冲区最大容量,如 8928 (93 天 × 15 分钟)
entries_in_use int 0 当前已有行数
access dict None 实例级权限( 仅限关键字参数 ),格式 {attr_index: (AccessMode, Authentication)}

重要说明

  • buffer (属性 2)带有 AttributeFlag.COMPLEX 标志, BinarySerializer 不会自动持久化 。缓冲区需单独序列化。
  • capture_objects sort_object 也标记为 COMPLEX,序列化器同样跳过。
  • 必须 设置 on_before_read handler 来提供历史数据,否则客户端读到空缓冲区:
#使用DLMS 自定义存储 模式
import dlms

clock = dlms.Clock("0.0.1.0.0.255")
reg   = dlms.Register("1.0.1.8.0.255", 0, scaler=-3)

profile = dlms.ProfileGeneric(
    "1.0.99.1.0.255",
    capture_objects=[clock, reg],             # 裸对象 → 默认 attr=2, data=0
    capture_period=900,                       # 15 分钟自动捕获
    sort_method=dlms.ProfileGeneric.FIFO,
    profile_entries=8928,                     # 93 天 × 96 行/天
    access={
        2: (dlms.AccessMode.READ, dlms.Authentication.NONE)
    }
)

# ===== 写:自动 or 手动触发 → 存到外部 =====
def on_capture(self, event):
    if event.action != 1:                    # action=1 = capture
        return True
    row = [clock.time, reg.value]            # 拍快照
    append_row_to_flash(row)                 # 实现append_row_to_flash 写入操作
    return True

# ===== 读:客户端请求 buffer → 从外部取出 =====
def on_read_buffer(self, event):
    if event.index != self.idx('buffer'):    # 非 buffer 属性走默认
        return True
    return load_rows_from_flash()            # 实现append_row_to_flash 读取操作

profile.on_before_action = on_capture
profile.on_before_read   = on_read_buffer


Python 属性一览

属性 类型 可写 说明
logical_name bytes × OBIS 代码(6 字节)
capture_objects list 每行捕获 (obj, attr_index, data_index) 三元组列表
capture_period int 自动捕获间隔(秒)
sort_method int 排序方式
sort_object object 排序参照对象
sort_object_attribute_index int 排序参照属性索引
sort_object_data_index int 排序参照数据索引
profile_entries int 缓冲区最大容量
entries_in_use int 当前行数
buffer_size int × 缓冲区数据字节数(只读)
on_before_read Callable 读前钩子(继承自 CosemObject)
on_after_read Callable 读后钩子(继承自 CosemObject)
on_before_write Callable 写前钩子(继承自 CosemObject)
on_after_write Callable 写后钩子(继承自 CosemObject)
on_before_action Callable 动作前钩子(继承自 CosemObject)
on_after_action Callable 动作后钩子(继承自 CosemObject)
access_dict dict 实例级访问控制

方法

方法 说明
deinit() 释放 C 侧资源,移除对象前调用

访问控制

import dlms
# 构造函数中指定
profile = dlms.ProfileGeneric("1.0.99.1.0.255",
    access={
        2: (dlms.AccessMode.READ, dlms.Authentication.LOW),
        1: (dlms.AccessMode.AUTHENTICATED_WRITE, dlms.Authentication.HIGH),
    })

# 或事后修改
profile.access_dict = {2: (dlms.AccessMode.READ, dlms.Authentication.LOW)}

Clock

设备的实时时钟对象,典型 OBIS 代码 0.0.1.0.0.255 。包含时区偏移和夏令时(DST)配置。C 层在每次读写 time 属性时自动与硬件 RTC 同步,无需额外 handler 即可实现基本时间管理,其 COSEM ID=8

Blue Book 属性

编号 名称 类型 说明
1 logical_name octet-string(6) OBIS 代码,只读
2 time octet-string 当前日期和时间(⏩ VOLATILE,每读自动从 RTC 刷新)
3 time_zone int16 UTC 偏移量(分钟)
4 status int8 时钟状态标志 (不建议手动更改)
5 begin octet-string DST 开始时间
6 end octet-string DST 结束时间
7 deviation int8 DST 生效时的偏移量(分钟)
8 enabled bool DST 调整是否启用
9 base enum 时钟源

默认 DST 配置:

参数 默认值 含义
begin (-1, 3, -1, 2, 0, 0, 0, 0) 3 月最后一个周日 2:00
end (-1, 10, -1, 3, 0, 0, 0, 0) 10 月最后一个周日 3:00

时间 tuple 中的 -1 表示"不指定"(wildcard),如 (-1, -1, -1, 8, 0, 0) 表示"每天 8:00"。

时钟源常量( base 参数)

常量 说明
Clock.BASE_NONE 0 未指定
Clock.BASE_CRYSTAL 1 晶体振荡器
Clock.BASE_FREQUENCY_50 2 50 Hz
Clock.BASE_FREQUENCY_60 3 60 Hz
Clock.BASE_GPS 4 GPS 授时
Clock.BASE_RADIO 5 无线电授时
import dlms
# 手动设置(写完自动同步到硬件RTC)
clock.time = (2025, 8, 3, 14, 30, 0)     # 年,月,日,时,分,秒

# 客户端 GET → 自动从硬件RTC刷新后回复
# 客户端每次读到的都是准确的实时值
# Python 直接读 → 读的是C struct缓存,可能是旧值
print(clock.time)   # 如果半小时前设的,还显示半小时前

# 这个值不会自动变,硬件RTC不报存时区,需要手动填写
clock.time_zone = 480    # UTC+8(东八区),单位:分钟
clock.time_zone = -300   # UTC-5(美国东部)

# bit5 由 DST 判断逻辑自动设置/清除,不建议手动改这个位
clock.status = 0         # Clock OK
clock.status = 4         # doubtful_value = 时间不可信
clock.status = 1         # clock_ok(0和1都表示OK)

# 元组: (年, 月, 日, 时, 分, 秒, 毫秒, 星期), -1表示通配
# 第9个元素是 extra_info 标志位(可选)
# 默认值:begin=2月最后一天2:00, end=10月最后一天3:00
clock.begin = (-1, 3, -1, 2, 0, 0, 0, 0)    # DST开始 3月最后一个周日 2:00
clock.end   = (-1, 10, -1, 3, 0, 0, 0, 0)   # DST结束 10月最后一个周日 3:00

clock.deviation = 60       # DST 生效时偏移60分钟(默认)
clock.deviation = 30       # 有些地区只偏移30分钟

clock.enabled = True       # 启用夏时令自动调整
clock.enabled = False      # 禁用

#仅告诉客户端时钟源,不负责同步
clock.base = dlms.Clock.BASE_FREQUENCY_50  # 时钟源:50Hz电网频率
clock.base = dlms.Clock.BASE_FREQUENCY_60  # 时钟源:60Hz电网频率
clock.base = dlms.Clock.BASE_GPS           # 时钟源:GPS授时
clock.base = dlms.Clock.BASE_RADIO         # 时钟源:无线电授时

构造函数

Clock(logical_name: str, begin: tuple = ..., end: tuple = ..., time_zone: int = 0,
      deviation: int = 60, base: int = BASE_FREQUENCY_50, access: dict = None)
参数 类型 默认值 说明
logical_name str 必传 OBIS 代码字符串,通常为 "0.0.1.0.0.255"
begin tuple 见下方 DST 开始时间,默认 3 月最后一个周日 2:00
end tuple 见下方 DST 结束时间,默认 10 月最后一个周日 3:00
time_zone int 0 UTC 偏移量(分钟), -480 =UTC−8(PST), 480 =UTC+8
deviation int 60 DST 偏移量(分钟),通常为 60
base int BASE_FREQUENCY_50 时钟源,可选 BASE_CRYSTAL / BASE_FREQUENCY_50 / BASE_FREQUENCY_60 / BASE_GPS / BASE_RADIO
access dict None 实例级权限,格式 {attr_index: (AccessMode, Authentication)}

方法(COSEM 动作)

方法 COSEM 方法 说明
adjust_to_quarter() 1 四舍五入到最近 15 分钟
adjust_to_minute() 3 四舍五入到整分钟
adjust_to_preset_time() 4 应用预存的目标时间
preset_adjusting_time(tuple) 5 存储目标时间供 adjust_to_preset_time() 使用
shift_time(seconds) 6 按秒数偏移(正数前进,负数后退)

C 层在每次读写 time 属性时自动与硬件 RTC 双向同步,无需 handler。事件钩子可用于审计日志等额外用途。

import dlms
# ============================================================
# 1. 初始化(含 DST 配置)
# ============================================================
clock = dlms.Clock(
    "0.0.1.0.0.255",
    time_zone=480,                       # UTC+8(东八区),单位:分钟
    deviation=60,                        # DST 生效时偏移 60 分钟
    begin=(-1, 3, -1, 2, 0, 0, 0, 0),   # DST start: 每年3月最后一个周日 2:00
    end=(-1, 10, -1, 3, 0, 0, 0, 0),    # DST end:   每年10月最后一个周日 3:00
    base=dlms.Clock.BASE_CRYSTAL,        # 告知客户端:内部晶振
    access={
        2: (dlms.AccessMode.READ, dlms.Authentication.LOW),
        1: (dlms.AccessMode.AUTHENTICATED_WRITE, dlms.Authentication.HIGH),
    }
)
# —— 中国无 DST,但保留代码作为参考 ——
clock.enabled = False

# 同步一次正确时间到硬件 RTC
import utime
rtc = utime.localtime()
clock.time = (rtc[0], rtc[1], rtc[2], rtc[3], rtc[4], rtc[5])
# 另一台设备作为 client,远程操作这台表,该例子仅供理解
client = dlms.Client(client_address=0x10, server_address=0x01, ...)
client.connect(...)
objs = client.get_objects()
clock = [o for o in objs if type(o).__name__ == "Clock"][0]

# 标准校时两步走:
client.method(clock, 5, (2026, 8, 3, 14, 30, 0))  # 先预设时间
client.method(clock, 4, None)                      # 再激活

# 或直接对齐刻钟/分钟/偏移:
client.method(clock, 1, None)   # adjust_to_quarter
client.method(clock, 3, None)   # adjust_to_minute
client.method(clock, 6, 3600)   # shift_time 快进 1 小时

Python 属性一览

属性 类型 可写 说明
logical_name bytes × OBIS 代码(6 字节)
time tuple 当前日期时间 (year, month, day, hour, minute, sec) ,每读自动从 RTC 刷新
time_zone int UTC 偏移量(分钟)
status int 时钟状态标志
begin tuple DST 开始时间
end tuple DST 结束时间
deviation int DST 偏移量(分钟)
enabled bool DST 是否启用
base int 时钟源(见上方常量表)
on_before_read Callable 读前钩子(继承自 CosemObject)
on_after_read Callable 读后钩子(继承自 CosemObject)
on_before_write Callable 写前钩子(继承自 CosemObject)
on_after_write Callable 写后钩子(继承自 CosemObject)
on_before_action Callable 动作前钩子(继承自 CosemObject)
on_after_action Callable 动作后钩子(继承自 CosemObject)
access_dict dict 实例级访问控制

访问控制

import dlms
# 构造函数中指定
clock = dlms.Clock("0.0.1.0.0.255",
    access={2: (dlms.AccessMode.READ_WRITE, dlms.Authentication.HIGH)})

# 或事后修改
clock.access_dict = {2: (dlms.AccessMode.READ_WRITE, dlms.Authentication.HIGH)}

典型用例

场景 示例
UTC+8 时区,无 DST Clock("0.0.1.0.0.255", time_zone=480, deviation=0)
UTC−8 (PST),美国 DST Clock("0.0.1.0.0.255", time_zone=-480, deviation=60)
GPS 授时 Clock("0.0.1.0.0.255", base=Clock.BASE_GPS)

CompactData

模板驱动的紧凑编码对象。将多个捕获对象的属性值编码为一个紧凑二进制缓冲区, 省去每个值的类型标签 ,显著减少窄带链路(如 PLC、SMS)上的传输字节数,其 COSEM ID=62

template_description (属性 5)由 Gurux 自动维护,描述了类型模板,客户端据此解码缓冲区。 capture_objects 使用与 ProfileGeneric 相同的三元组格式。

Blue Book 属性

编号 名称 类型 说明
1 logical_name octet-string(6) OBIS 代码,只读
2 buffer octet-string 紧凑编码后的原始字节(可手动写入预编码 payload)
3 capture_objects ARRAY of STRUCT 定义捕获哪些对象的哪些属性,格式同 ProfileGeneric
4 template_id uint8 模板标识符(0–255)
5 template_description octet-string 类型模板描述(只读,自动维护)
6 capture_method enum 捕获模式

捕获模式( capture_method

常量 说明
CaptureMethod.IMPLICIT 0 每次读属性 2(buffer)时自动刷新编码
CaptureMethod.INVOKE 1 客户端调用方法 2 时才刷新编码
#例子仅用clock和register对象做演示
import dlms
clock = dlms.Clock("0.0.1.0.0.255", time_zone=480)
reg = dlms.Register("1.0.1.8.0.255", 0, scaler=-3)
cd = dlms.CompactData("0.0.96.60.0.255")

print(cd)                    #<CompactData ln='0.0.96.60.0.255', objs=2, buf=4, method=IMPLICIT>
cd.buffer = b'123456'
print(cd.buffer)             #b'123456'
cd.capture_objects = [(clock, 2, 0), (reg, 2, 0)]
print(cd.capture_objects)    #[(None, 2, 0), (None, 2, 0)]
cd.template_id=1
print(cd.template_id)        #1
cd.capture_method=dlms.CaptureMethod.IMPLICIT
print(cd.capture_method)     #IMPLICIT

构造函数

CompactData(logical_name: str, capture_objects: list = None, template_id: int = 0,
            capture_method: int = CaptureMethod.IMPLICIT, access: dict = None)
参数 类型 默认值 说明
logical_name str 必传 OBIS 代码字符串,如 "0.0.96.60.0.255"
capture_objects list None 捕获列表,每元素为 (obj, attr_idx, data_idx) 三元组
template_id int 0 模板标识符(0–255)
capture_method int CaptureMethod.IMPLICIT IMPLICIT =读时自动编码, INVOKE =客户端方法触发
access dict None 实例级权限,格式 {attr_index: (AccessMode, Authentication)}
import dlms
cd = dlms.CompactData(
    "0.0.96.60.0.255",
    capture_objects=[(clock, 2, 0), (reg, 2, 0)],
    template_id=1,
    capture_method=dlms.CaptureMethod.IMPLICIT,
    access={2: (dlms.AccessMode.READ_WRITE, dlms.Authentication.HIGH)}
)

Python 属性一览

属性 类型 可写 说明
logical_name bytes × OBIS 代码(6 字节)
buffer bytes 紧凑编码后的原始字节(也可手动写入预编码 payload)
capture_objects list (obj, attr_idx, data_idx) 三元组列表
template_id int 模板标识符(0–255)
template_description bytes × 类型模板描述(自动维护)
capture_method int 捕获模式( IMPLICIT / INVOKE
on_before_read Callable 读前钩子(继承自 CosemObject)
on_after_read Callable 读后钩子(继承自 CosemObject)
on_before_write Callable 写前钩子(继承自 CosemObject)
on_after_write Callable 写后钩子(继承自 CosemObject)
on_before_action Callable 动作前钩子(继承自 CosemObject)
on_after_action Callable 动作后钩子(继承自 CosemObject)
access_dict dict 实例级访问控制

方法

方法 说明
capture(server) 手动触发捕获 (非请求上下文中)。将当前捕获对象的值编码到 buffer,并刷新 template_description 。传入一个运行中的 Server 实例。
idx(name) 返回 Python 属性名对应的 Blue Book 属性编号(继承自 CosemObject)

⚠️ 重要: on_before_read / on_before_action handler 禁止设置 event.handled = True 。handler 返回后 Gurux 必须继续执行紧凑编码。

IMPLICIT 模式 + on_before_read (推荐)

import dlms

cd = dlms.CompactData(
    "0.0.96.60.0.255",
    capture_objects=[(clock, 2, 0), (reg, 2, 0)],
    template_id=1,
    capture_method=dlms.CaptureMethod.IMPLICIT,
)

# 在编码前更新捕获对象的值(如从传感器读取)
def refresh(self, event):
    if event.index == self.idx('buffer'):
        reg.value = read_sensor()   # 在编码前刷新硬件读数
        # 不要 return False — 编码必须继续执行

cd.on_before_read = refresh

手动捕获:启动时预填 buffer

# 在 server.run() 之前手动触发一次捕获
reg.value = 12345678
cd.capture(server)  # 将当前值编码进 buffer 并更新 template_description

访问控制

# 构造函数中指定
cd = dlms.CompactData("0.0.96.60.0.255",
    access={2: (dlms.AccessMode.READ, dlms.Authentication.NONE)})

# 或事后修改
cd.access_dict = {2: (dlms.AccessMode.READ, dlms.Authentication.NONE)}

典型用例

场景 示例
窄带 PLC 上报电能和时间 CompactData("0.0.96.60.0.255", capture_objects=[(clock, 2, 0), (reg, 2, 0)], template_id=1)
SMS 推送紧凑编码 CompactData("0.0.96.60.0.255", capture_objects=[(reg_a, 2, 0), (reg_b, 2, 0)], capture_method=CaptureMethod.INVOKE)

DisconnectControl

可控供电开关继电器。用于远程断开/合上负载(如欠费断电、远程资产管理)。C 层默认行为只更新内存中的 output_state control_state ;如需驱动真实硬件继电器,用 on_before_action 拦截,其 COSEM ID=70

Blue Book 属性

编号 名称 类型 说明
1 logical_name octet-string(6) OBIS 代码,只读
2 output_state bool True =负载接通, False =负载断开
3 control_state enum 0 =断开, 1 =接通, 2 =等待重连
4 control_mode uint8 操作模式(0–6,见 Blue Book)
import dlms
disconnect_ctl = dlms.DisconnectControl("0.0.96.3.10.255")
print(disconnect_ctl.logical_name)     #输出0.0.96.3.10.255
disconnect_ctl.output_state = True     
print(disconnect_ctl.output_state)     #输出 True
disconnect_ctl.control_mode = 1 
print(disconnect_ctl.control_mode)     #输出 1

构造函数

DisconnectControl(logical_name: str, output_state: bool = False,
                  control_state: int = 0, control_mode: int = 0,
                  access: dict = None)
参数 类型 默认值 说明
logical_name str 必传 OBIS 代码字符串,如 "0.0.96.3.10.255"
output_state bool False 初始继电器状态, True =接通, False =断开
control_state int 0 控制状态码, 0 =断开, 1 =接通, 2 =等待重连
control_mode int 0 操作模式(0–6),决定允许哪些手动/远程操作
access dict None 实例级权限,格式 {attr_index: (AccessMode, Authentication)}

控制状态码

名称 说明
0 DISCONNECTED 已断开
1 CONNECTED 已接通
2 READY_FOR_RECONNECTION 等待重连
import dlms
disconnect_ctl = dlms.DisconnectControl(
    "0.0.96.3.10.255",
    output_state=True,
    control_state=1,
    control_mode=1,
    access={
        2: (dlms.AccessMode.READ, dlms.Authentication.NONE),           
        4: (dlms.AccessMode.AUTHENTICATED_WRITE, dlms.Authentication.HIGH),
    }
    )

方法(COSEM 动作)

方法 COSEM 方法 说明
remote_disconnect() 1 远程断开负载,默认行为更新 output_state=False control_state=0
remote_reconnect() 2 远程合上负载,默认行为更新 output_state=True control_state=1

默认行为只更新内存中的 C 结构体。如需驱动真实硬件 GPIO,使用 on_before_action 拦截。

硬件联动: on_before_action

import dlms
from machine import Pin
# 假设 GPIO 引脚用于控制继电器
gpio_relay = Pin(Pin.GPIO1, Pin.OUT, Pin.PULL_DISABLE, 1)

def relay_handler(self, event):
    if event.index == 1:
        gpio_relay.write(0)   # remote_disconnect → 断开硬件
    elif event.index == 2:
        gpio_relay.write(1)   # remote_reconnect → 合上硬件
    return True               # 允许 C 层继续更新内存状态

disconnect_ctl = dlms.DisconnectControl(
    "0.0.96.3.10.255",
    output_state=True,
    control_state=1,
    control_mode=1,
    )
disconnect_ctl.on_before_action = relay_handler

Python 属性一览

属性 类型 可写 说明
logical_name bytes × OBIS 代码(6 字节)
output_state bool 继电器状态, True =接通, False =断开
control_state int 控制状态, 0 =断开, 1 =接通, 2 =等待重连
control_mode int 操作模式(0–6)
on_before_read Callable 读前钩子(继承自 CosemObject)
on_after_read Callable 读后钩子(继承自 CosemObject)
on_before_write Callable 写前钩子(继承自 CosemObject)
on_after_write Callable 写后钩子(继承自 CosemObject)
on_before_action Callable 动作前钩子(继承自 CosemObject)
on_after_action Callable 动作后钩子(继承自 CosemObject)
access_dict dict 实例级访问控制

访问控制

import dlms
# 构造函数中指定:正常遥测可读,远程通断需要 HIGH 认证
disconnect_ctl = dlms.DisconnectControl("0.0.96.3.10.255",
    output_state=True,
    control_state=1,
    control_mode=1,
    access={
        2: (dlms.AccessMode.READ, dlms.Authentication.NONE),           # output_state 可读
        4: (dlms.AccessMode.AUTHENTICATED_WRITE, dlms.Authentication.HIGH),  # control_mode 需 HIGH 认证
    })

# 或事后修改
disconnect_ctl.access_dict = {4: (dlms.AccessMode.AUTHENTICATED_WRITE, dlms.Authentication.HIGH)}

典型用例

场景 示例
欠费断电,默认接通 DisconnectControl("0.0.96.3.10.255", output_state=True, control_state=1, control_mode=1)
初始断开,等待远程合闸 DisconnectControl("0.0.96.3.10.255", output_state=False, control_state=0, control_mode=1)

ScriptTable

COSEM Class ID = 9,用于将"一组对 DLMS 对象的操作"打包成 脚本(Script)
供远程客户端通过 Execute 方法一键触发,实现断连/复电、电价切换、限载等自动化场景。


ScriptTable
├── Script #1 (id=1)          ← 由 add_script() 添加
│   ├── ScriptAction (Write)   → 写目标对象属性
│   └── ScriptAction (Execute) → 调用目标对象方法
├── Script #2 (id=2)
└── ...

构造函数

ScriptTable:

ScriptTable(logical_name: str, access: dict = None)
参数 类型 默认值 说明
logical_name str 必传 OBIS 代码字符串,如 "0.0.10.0.106.255"
access dict None 实例级权限,格式 {attr_index: (AccessMode, Authentication)}
import dlms
script_table = dlms.ScriptTable(
    "0.0.10.0.106.255",
    access={1:(dlms.AccessMode.AUTHENTICATED_WRITE,dlms.Authentication.HIGH),
    })

ScriptAction:

ScriptAction(type: int, target: object, method: int = None,
             attribute: int = None, parameter: int | bool = None)
参数 类型 默认值 说明
type int 必传 动作类型,决定这条动作"干什么": ScriptAction.Write (1) = 写目标对象的 属性 ScriptAction.Execute (2) = 调用目标对象的 方法 。见下方"两种动作类型"
target object 必传 动作作用于的 目标 DLMS 对象实例 Register DisconnectControl Clock 等)。必须是已创建好的对象,C 层执行时会向其发 SET (Write) 或 ACTION (Execute) 请求。传非 DLMS 对象会报 TypeError
method int None type=Execute 时使用 (必填,不能为 0)。目标对象 方法的编号 ,如 DisconnectControl 中 1 =remote_disconnect(断开)、 2 =remote_reconnect(恢复)。具体编号含义取决于目标对象类
attribute int None type=Write 时使用 (必填,不能为 0)。目标对象 属性的编号 ,如 Register 中 2 =value(当前值)。执行时把 parameter 的值写入该属性
parameter int | bool None 动作携带的数据: Write 时 = 要写入属性的新值; Execute 时 = 传给方法调用的参数。可选,不传则动作不带数据。仅支持 int (按大小自动转为 int8/int16/int32)和 bool ,其它类型报 TypeError

ScriptAction 类型

类型 说明
ScriptAction.Write 1 把目标对象的指定属性设为给定值
ScriptAction.Execute 2 调用目标对象的指定方法
import dlms

tariff_reg  = dlms.Register("1.0.1.8.0.255", default_value=0)
tariff_reg1  = dlms.Register("1.0.1.8.0.255", default_value=0)
disconnect  = dlms.DisconnectControl("0.0.2.0.0.255")
disconnect1  = dlms.DisconnectControl("0.0.2.0.0.255")

script_table = dlms.ScriptTable("0.0.10.0.106.255")

action_a = dlms.ScriptAction(
    type=dlms.ScriptAction.Write,
    target=tariff_reg,
    attribute=2,
    parameter=2,          
)
action_b = dlms.ScriptAction(
    type=dlms.ScriptAction.Write,
    target=tariff_reg1,
    attribute=2,
    parameter=2,          
)

action_c = dlms.ScriptAction(
    type=dlms.ScriptAction.Execute,
    target=disconnect,
    method=2,
    parameter=0,
)

action_d = dlms.ScriptAction(
    type=dlms.ScriptAction.Execute,
    target=disconnect1,
    method=2,
    parameter=0,
)

script_table.add_script(id=3, actions=[action_a,action_c]) #关联组: ID:3对应action_a, action_c
script_table.add_script(id=2, actions=action_b)          #关联组: ID2对应action_b
script_table.add_script(id=4, actions=action_d)          #关联组: ID2对应action_b
print(script_table.get_scripts())                        # [3,2,4]
script_table.remove_script(3)                            #删除关联组:3
print(script_table.get_scripts())                        # [2,4]

#填写id为2是失败的,因为2不是设置为Execute,而4是成功的
script_table.execute_script(4)                           #True

Python 属性一览

属性 类型 可写 说明
logical_name bytes × OBIS 代码(6 字节)
on_before_read Callable 读前钩子(继承自 CosemObject)
on_after_read Callable 读后钩子(继承自 CosemObject)
on_before_write Callable 写前钩子(继承自 CosemObject)
on_after_write Callable 写后钩子(继承自 CosemObject)
on_before_action Callable 动作前钩子(继承自 CosemObject)
on_after_action Callable 动作后钩子(继承自 CosemObject)
access_dict dict 实例级访问控制
方法 说明
add_script(id, actions) 添加脚本。 actions 可以是单个 ScriptAction ScriptAction 列表
get_scripts() 返回配置的脚本 ID 列表
remove_script(id) 按 ID 删除脚本,成功返回 True
execute_script(id) 该接口需要在回调中进行执行,本地调用仅测试时使用
deinit() 释放 C 侧资源

on_before_action 在脚本执行前触发。返回 False 可阻止整个脚本执行;返回 True 后 C 层按序迭代所有动作。

import dlms
#该服务器例子仅供参考,服务器和客户端通信时,如何触发execute,不能正常运行
script_table = dlms.ScriptTable("0.0.10.0.106.255")
# ... 添加脚本 1、2、99 ...

def handle_script_execute(self, event):
    script_id = event.parameters
    print("[ScriptTable] Python handler: Execute script ID {}".format(script_id))

    # 场景 1:脚本 1 —— 做日志/校验后交给 C 执行
    if script_id == 1:
        print("[ScriptTable] Preparing to close disconnect control...")
        return True          # 放行,C 层按序执行动作

    # 场景 2:脚本 99 —— 纯 Python 实现,不走 C
    elif script_id == 99:
        print("[ScriptTable] Custom Python-only script")
        return False         # 阻止 C 执行

    # 场景 3:其它脚本 —— 默认放行
    return True

script_table.on_before_action = handle_script_execute

#客户端例子
import dlms
client = dlms.Client(
    client_address=0x41,
    server_address=1,
    authentication=dlms.Authentication.HIGH,
    password=b"your_password",
)
client.connect(serial)

# 获取 ScriptTable 对象
objs = client.get_objects()
st = next(o for o in objs if type(o).__name__ == "ScriptTable")

# 触发 script_id=1:发送 Action-Request,方法号=1,参数=script_id
result = client.method(st, 1, 1)   # (对象, 方法号=1固定, script_id)


ActivityCalendar

分层费率计划表。以"季节 → 周 → 日"的三层结构组织定时脚本执行(其 COSEM ID=20 ):

季节 (Season)
 └── 周模板 (Week Profile)
      └── 日模板 (Day Profile)
           └── 定时动作列表: ((hour, min, sec), script_table, script_id)

日历采用 主备双版本 设计: active (活跃,对客户端可见)和 passive (待生效,在后台预先构建)。调用 activate_passive_calendar() 将 passive 版本提升为 active。

Blue Book 属性

编号 名称 类型 说明
1 logical_name octet-string(6) OBIS 代码,只读
2 calendar_name_active visible-string 活跃日历名称
3–5 (season / week / day 模板,active) complex COSEM 复杂结构,Python 方法操作
6 calendar_name_passive visible-string 待生效日历名称
7–9 (season / week / day 模板,passive) complex COSEM 复杂结构,Python 方法操作
10 activate_passive_calendar_time octet-string passive → active 的触发时间
方法 说明
add_season_profile(season_name, start_time, week_name, passive=False) 添加季节。 start_time (month, day, hour) 三元组
add_week_profile(name, monday, tuesday, ..., sunday, passive=False) 添加周模板,每天映射到一个 day_id
add_day_profile(day_id, actions, passive=False) 添加日模板。 actions ((hour, min, sec), script_table, script_id) 元组列表
get_season_profiles(passive=False) 获取季节列表
get_week_profiles(passive=False) 获取周模板列表
get_day_profiles(passive=False) 获取日模板列表
clear_profiles(passive=False) 清空所有模板
copy_active_to_passive() 将 active 日历复制到 passive
activate_passive_calendar() 动作 1 :将 passive 提升为 active
deinit() 释放 C 侧资源

所有构建方法默认修改 passive 日历( passive=True )。构建完成后调用 activate_passive_calendar() 使之生效。

import dlms
from dlms import AccessMode, Authentication
st = dlms.ScriptTable("0.0.10.0.0.255")
st = dlms.ScriptTable("0.0.10.0.0.255")   # 准备 ScriptTable 供定时动作引用
r = dlms.Register("1.1.1.8.0.255",default_value=0)        # 要写入的目标寄存器

st.add_script(id=1, actions=[dlms.ScriptAction(
    type=dlms.ScriptAction.Write, target=r, attribute=2, parameter=150
)])
st.add_script(id=2, actions=[dlms.ScriptAction(
    type=dlms.ScriptAction.Write, target=r, attribute=2, parameter=80
)])
cal = dlms.ActivityCalendar("0.0.13.0.0.255",
    access={2: (AccessMode.READ_WRITE, Authentication.LOW)})

# ── 属性 1: logical_name ──
print(cal.logical_name)  # b'\x00\x00\r\x00\x00\xff'
# ── 属性 2: calendar_name_active ──
cal.calendar_name_active = "Summer2026"
print(cal.calendar_name_active)         # "Summer2024"
# ── 属性 3–5: season / week / day (active 侧)──
# 通过 add_* 方法 + passive=False 直接操作 active
cal.add_day_profile(1, [((8, 0, 0), st, 1)], passive=False)
cal.add_week_profile("WorkWeek", monday=1, tuesday=1, wednesday=1,
                     thursday=1, friday=1, saturday=1, sunday=1,
                     passive=False)
cal.add_season_profile("Summer", (6, 1, 0), "WorkWeek", passive=False)

# 读取 active 侧的完整的数据
for s in cal.get_season_profiles(passive=False):
    print(s["name"], s["start_time"])   # Summer, (6, 1, ...)
for w in cal.get_week_profiles(passive=False):
    print(w["name"], w["monday"])       # WorkWeek, 1

# ── 属性 6: calendar_name_passive ──
cal.calendar_name_passive = "Winter2026"
print(cal.calendar_name_passive)  # "Winter2024"

# ── 属性 7–9: season / week / day (passive 侧)──
# passive=True 是默认值,可省略
cal.add_day_profile(1, [((6, 0, 0), st, 1), ((22, 0, 0), st, 2)])
cal.add_week_profile("AllWeek", monday=1, tuesday=1, wednesday=1,
                     thursday=1, friday=1, saturday=1, sunday=1)
cal.add_season_profile("Winter", (1, 1, 0), "AllWeek")
# 遍历 passive 模板(默认 passive=True)
for d in cal.get_day_profiles():
    for a in d["actions"]:
        print(a["time"], a["selector"])  # (6,0,0) 1  /  (22,0,0) 2

# ── 属性 10: activate_passive_calendar ─
cal.activate_passive_calendar()       # passive → active

构造函数

ActivityCalendar(logical_name: str, calendar_name_active: str = "",
                 calendar_name_passive: str = "", access: dict = None)
参数 类型 默认值 说明
logical_name str 必传 OBIS 代码字符串,如 "0.0.13.0.0.255"
calendar_name_active str "" 活跃日历名称
calendar_name_passive str "" 待生效日历名称
access dict None 实例级权限,格式 {attr_index: (AccessMode, Authentication)}

通配符

常量 说明
dlms.ANY -1 在时间字段中表示"任意",如 (ANY, ANY, ANY, 8, 0, 0) 表示"每天 8:00"
import dlms
from dlms import AccessMode, Authentication

cal = dlms.ActivityCalendar("0.0.13.0.0.255",
    calendar_name_active="SummerRates",
    calendar_name_passive="WinterRates",
    access={2: (AccessMode.READ, Authentication.NONE)})

Python 属性一览

属性 类型 可写 说明
logical_name bytes × OBIS 代码(6 字节)
calendar_name_active str 活跃日历名称
calendar_name_passive str 待生效日历名称
on_before_read Callable 读前钩子(继承自 CosemObject)
on_after_read Callable 读后钩子(继承自 CosemObject)
on_before_write Callable 写前钩子(继承自 CosemObject)
on_after_write Callable 写后钩子(继承自 CosemObject)
on_before_action Callable 动作前钩子(继承自 CosemObject)
on_after_action Callable 动作后钩子(继承自 CosemObject)
access_dict dict 实例级访问控制

访问控制

import dlms
cal = dlms.ActivityCalendar("0.0.13.0.0.255",
    calendar_name_active="SummerRates",
    calendar_name_passive="WinterRates",
    access={2: (dlms.AccessMode.READ, dlms.Authentication.NONE)})

# 或事后修改
cal.access_dict = {2: (dlms.AccessMode.READ_WRITE, dlms.Authentication.HIGH)}

典型用例

场景 示例
分层费率 峰谷分时、季节切换

SingleActionSchedule

单次定时脚本调度器(其 COSEM ID=22 )。在指定的日期/时间组合触发 ScriptTable 中某个脚本的执行。支持通配符 dlms.ANY ,C 层自动将当前时间与 execution_times 列表匹配,命中时调用对应的 (ScriptTable, selector)

Blue Book 属性

编号 名称 类型 说明
1 logical_name octet-string(6) OBIS 代码,只读
2 executed_script STRUCT 要执行的 (ScriptTable, selector) 元组; 读取 ("A.B.C.D.E.F", int) 元组
3 execution_type enum 执行类型,当前固定为 1 (Type 1:按日期/时间表达式匹配)
4 execution_times ARRAY of STRUCT 可读写,时间元组列表(6 或 7 元组;读取始终返回 7 元组)

构造函数

SingleActionSchedule(logical_name: str, executed_script: tuple = None,
                     execution_type: int = 1, execution_times: list = None,
                     access: dict = None)
参数 类型 默认值 说明
logical_name str 必传 OBIS 代码字符串,如 "0.0.15.0.1.255"
executed_script tuple None (script_table_instance, script_id) 二元组
execution_type int 1 执行类型,固定为 1 (Type 1:日期/时间表达式)
execution_times list None 时间表达式列表,每项 6 或 7 元组; -1 表示通配
access dict None 实例级权限,格式 {attr_index: (AccessMode, Authentication)}

执行时间表达式

(year, month, day, hour, minute, second[, day_of_week])
位置 字段 范围 说明
0 year 任意整数 / ANY 年份
1 month 1–12 / ANY 月份
2 day 1–31 / ANY
3 hour 0–23 / ANY 小时
4 minute 0–59 / ANY 分钟
5 second 0–59 / ANY
6 day_of_week 0–7 / ANY 0=全周, 1=周一, ..., 7=周日

任意位置使用 dlms.ANY (值为 -1 )表示通配,如 (ANY, ANY, ANY, 2, 0, 0, ANY) = 每天 02:00:00。6 元组(省略 day_of_week )等价于 7 元组末尾补 -1 dlms.ANY = -1

import dlms
from dlms import AccessMode, Authentication

A = dlms.ANY

# 准备脚本
st = dlms.ScriptTable("0.0.10.0.0.255")
r1 = dlms.Register("1.1.1.8.0.255",0)
r2 = dlms.Register("1.1.2.8.0.255",0)
st.add_script(id=1, actions=[dlms.ScriptAction(
    type=dlms.ScriptAction.Write, target=r1, attribute=2, parameter=150
)])
st.add_script(id=2, actions=[dlms.ScriptAction(
    type=dlms.ScriptAction.Write, target=r2, attribute=2, parameter=80
)])

#写法1:
# sas = dlms.SingleActionSchedule("0.0.15.0.1.255",
#     executed_script=(st, 1),
#     execution_times=[(dlms.ANY, dlms.ANY, dlms.ANY, 2, 0, 0, dlms.ANY)],
#     access={2: (AccessMode.READ_WRITE, Authentication.HIGH)})

#写法2(主要介绍属性):
# ── 属性 1: logical_name ──
sas = dlms.SingleActionSchedule("0.0.15.0.0.255")
# 只读,返回 6 字节 OBIS 代码
print(sas.logical_name)  # b'\x00\x00\x0f\x00\x00\xff'

# ── 属性 2: executed_script ──
# 写入: (ScriptTable, selector=script_id)
sas.executed_script = (st, 1)
# 读取: 返回 (字符串, int) — 注意不是 (对象, int)!
print(sas.executed_script)  # ("0.0.10.0.0.255", 1)

# ── 属性 3: execution_type ──
print(sas.execution_type)  # 1(TYPE1)
sas.execution_type = 1     # 可写

# ── 属性 4: execution_times ──
# 写入:支持 多项 6 元组(省略 dow,自动补 -1)或 7 元组
sas.execution_times = [
    (A, A, A, 2, 0, 0),           # 6 元组: 每天 02:00:00
    (A, A, A, 12, 0, 0, -1),      # 7 元组: 每天 12:00:00,忽略星期
    (2026, 1, 1, 0, 0, 0, -1),    # 一次性: 2026-01-01 00:00:00
    (A, A, A, 8, 0, 0, 1),        # 每周一 08:00:00
]

# 读取:始终返回 7 元组
for t in sas.execution_times:
    print(t)  # (2020, 1, 1, 2, 0, 0, -1), ...

# 追加时间(读取 → 修改列表 → 写回)
times = sas.execution_times
times.append((A, A, A, 18, 0, 0))  # 追加每天 18:00
sas.execution_times = times

# ── 钩子(继承自 CosemObject)──
def on_before_write(sender, attr_index, value, context):
    print(f"Write to attr {attr_index}: {value}")
    return dlms.DLMSEvent.ALLOW

sas.on_before_write = on_before_write

Python 属性一览

属性 类型 可写 说明
logical_name bytes × OBIS 代码(6 字节)
executed_script tuple (script_table, script_id) 要执行的脚本
execution_type int 执行类型,固定为 1
execution_times list 执行时间表达式列表
on_before_read Callable 读前钩子(继承自 CosemObject)
on_after_read Callable 读后钩子(继承自 CosemObject)
on_before_write Callable 写前钩子(继承自 CosemObject)
on_after_write Callable 写后钩子(继承自 CosemObject)
on_before_action Callable 动作前钩子(继承自 CosemObject)
on_after_action Callable 动作后钩子(继承自 CosemObject)
access_dict dict 实例级访问控制

方法

方法 说明
deinit() 释放 C 侧资源

访问控制

import dlms
schedule = dlms.SingleActionSchedule("0.0.15.0.1.255",
    executed_script=(script_table, 1),
    execution_times=[(dlms.ANY, dlms.ANY, dlms.ANY, 2, 0, 0, dlms.ANY)],
    access={2: (dlms.AccessMode.READ_WRITE, dlms.Authentication.HIGH)})

# 或事后修改
schedule.access_dict = {2: (dlms.AccessMode.READ_WRITE, dlms.Authentication.HIGH)}

典型用例

场景 示例
每天凌晨 2:00 执行抄表脚本 execution_times=[(ANY, ANY, ANY, 2, 0, 0, ANY)]
每月 1 号 0:00 执行结算脚本 execution_times=[(ANY, 1, 1, 0, 0, 0, ANY)]
每周一 8:00 上报周报 execution_times=[(ANY, ANY, ANY, 8, 0, 0, 1)]

RegisterMonitor

阈值监控器(其 COSEM ID=21 )。监控一个 DLMS 对象的某个属性值,当值跨越预设阈值时自动触发 ScriptTable 脚本。C 层后台线程每秒轮询一次;调用 server.monitor() 可立即手动检查。

Blue Book 属性

编号 名称 类型 说明
1 logical_name octet-string(6) OBIS 代码,只读
2 thresholds ARRAY of int 阈值列表,从低到高排序
3 monitored_value STRUCT 被监视的 (dlms_object, attribute_index)
4 actions ARRAY of STRUCT 每个阈值对应一对 "up"/"down" 动作脚本

RegisterMonitor没有 COSEM 动作方法 ,不会触发 on_before_action / on_after_action

构造函数

RegisterMonitor(logical_name: str, access: dict = None)
参数 类型 默认值 说明
logical_name str 必传 OBIS 代码字符串,如 "0.0.16.1.0.255"
access dict None 实例级权限,格式 {attr_index: (AccessMode, Authentication)}
import dlms
from dlms import AccessMode, Authentication

reg = dlms.Register("1.0.1.8.0.255",0)
st = dlms.ScriptTable("0.0.10.0.0.255")
r_relay = dlms.Register("1.1.1.8.0.255",0)

st.add_script(id=1, actions=[dlms.ScriptAction(
    type=dlms.ScriptAction.Write, target=r_relay, attribute=2, parameter=1
)])
st.add_script(id=2, actions=[dlms.ScriptAction(
    type=dlms.ScriptAction.Write, target=r_relay, attribute=2, parameter=0
)])

rm = dlms.RegisterMonitor(
    "0.0.16.1.0.255",
    access={2: (AccessMode.READ_WRITE, Authentication.HIGH)})

# ── 属性 2: thresholds ──
# 写入时自动同步 lastValues(内部数组),支持 int 和 float
rm.thresholds = [1000, 5000, 10000]
print(rm.thresholds)  # [1000, 5000, 10000]
# ── 属性 3: monitored_value ──
rm.monitored_value = (reg, reg.idx('value')) 
print(rm.monitored_value)      # (<Register ...>, 2)

# ── 属性 4: actions ──
# 长度必须与 thresholds 一致,down 可以设为 None
rm.thresholds = [1000, 5000]
rm.actions = [
    {"up": (st, 1), "down": (st, 2)},   # 阈值 1000:双向动作
    {"up": (st, 1), "down": None},      # 阈值 5000:仅 up 动作
]
# 关键步骤:注册到 server 后,find_python_object_by_c_ptr 才能工作
# 不注册server 返回值是(None, 1) (None, 2) (None, 1) (None, 0)
for a in rm.actions:
    print(a["up"], a["down"])  # (<ScriptTable...>, 1) , (<ScriptTable...>, 2)

# ── 钩子 ──
def on_before_read(sender, attr_index, context):
    if attr_index == 3:
        print("About to read monitored_value")
    return dlms.DLMSEvent.ALLOW

rm.on_before_read = on_before_read

Python 属性一览

属性 类型 可写 说明
logical_name bytes × OBIS 代码(6 字节)
thresholds list[int | float] 阈值列表,从低到高排序
monitored_value tuple | None (dlms_object, attribute_index) 被监视的目标
actions list[dict] 每个阈值一条, "up" / "down" 键对应 (ScriptTable, selector) None
on_before_read Callable 读前钩子(继承自 CosemObject)
on_after_read Callable 读后钩子(继承自 CosemObject)
on_before_write Callable 写前钩子(继承自 CosemObject)
on_after_write Callable 写后钩子(继承自 CosemObject)
access_dict dict 实例级访问控制

方法

方法 说明
deinit() 释放 C 侧资源

工作方式

server.run() 启动后,后台线程每秒轮询一次:
  monitored_value → 当前值 = reg.value
  比较 thresholds 列表 → 找到所处的区间
  如果跨越了阈值 → 触发对应的 "up" 或 "down" 脚本

server.monitor()  → 立即手动触发一次检查(无需等待下一秒)

actions 格式

#仅解释参考,程序不完整
rm.actions = [
    {"up": (script_table, 1), "down": (script_table, 2)},   # 阈值 1 的动作
    {"up": (script_table, 3), "down": None},                 # 阈值 2 只有 up 动作
]
# "up"   → 值从下方跨越此阈值时触发
# "down" → 值从上方跨越此阈值时触发
# None   → 此方向不触发任何动作

访问控制

import dlms
rm = dlms.RegisterMonitor("0.0.16.1.0.255",
    access={2: (dlms.AccessMode.READ_WRITE, dlms.Authentication.HIGH)})

# 或事后修改
rm.access_dict = {2: (dlms.AccessMode.READ_WRITE, dlms.Authentication.HIGH)}

典型用例

场景 示例
低电量告警 + 过载断电 thresholds = [1000, 50000] , 低于 1000 告警 / 高于 50000 断电
欠费跳闸(单阈值) thresholds = [0] , 余额向下跨 0 时断开
多级电价切换 thresholds = [1000, 5000, 10000] , 各阈值触发不同费率脚本

PushSetup

数据主动推送控制器(其 COSEM ID=40 )。定义"推送什么数据、推到哪个地址、在什么时间窗口内发送"。C 层负责将 objectList 中所有对象的当前值序列化为 DLMS DATA-NOTIFICATION PDU,Python 侧通过 generate_pdu() 获取字节后自行选择传输方式发送。

Blue Book 属性

编号 名称 类型 说明
1 logical_name octet-string(6) OBIS 代码,只读
2 objectList ARRAY of STRUCT 推送的数据项列表
3 destination visible-string 目标地址, "ip:port" 格式
4 communicationWindow ARRAY of STRUCT 推送时间窗口列表
5 randomisationStartInterval uint16 随机延迟区间(秒),防止多设备同时推送
6 retries uint8 重试次数
7 retryDelay uint32 重试间隔(秒)
import dlms
from dlms import AccessMode, Authentication
A = dlms.ANY

# ── 准备对象 ──
reg = dlms.Register("1.0.1.8.0.255", 12345)
clock = dlms.Clock("0.0.1.0.0.255")

ps = dlms.PushSetup("0.0.25.9.0.255")

# ── 属性 1: logical_name ──
print(ps.logical_name)  # b'\x00\x00\x19\t\x00\xff'

# ── 属性 2: objectList ──
ps.objectList = [reg, (clock, 2, 0)]  # 混合格式
# 需要先注册到 server 后才能输出 (<Register...>, 2, 0) , (<Clock...>, 2, 0)
# 否则输出的是(None, 2, 0) , (None, 2, 0)
# 没有 server 时 py_obj 为 None(C→Python 反向查找需要注册表),
# 但 attr_idx/data_idx 始终正确,不影响 C 层数据 仅例子只展示属性如何使用
for item in ps.objectList:
    print(item) 

# ── 属性 3: destination ──
ps.destination = "10.0.0.1:4059"
print(ps.destination)  # "10.0.0.1:4059"

# ── 属性 4: communicationWindow ──
ps.communicationWindow = [
    [(-1, -1, -1,  8, 0, 0), (-1, -1, -1, 20, 0, 0)],
]

# ── 属性 5/6/7 ──
# 场景 1: 宽松(信号差,允许长时间重试)
ps.randomisationStartInterval = 10   # 最多随机延迟 10s
ps.retries = 5                        # 最多重试 5 次
ps.retryDelay = 120                   # 每次间隔 2 分钟
# 总耗时上限 ≈ 10 + 5×(120+send_time) ≈ 10 分钟

# 场景 2: 紧凑(信号好,失败快速放弃)
ps.randomisationStartInterval = 1    # 最多延迟 1s
ps.retries = 1                        # 只重试 1 次
ps.retryDelay = 10                    # 10s 后重试
# 总耗时上限 ≈ 1 + 1×(10+send_time) ≈ 11 秒

# 场景 3: 不重试(数据不重要,丢了就丢了)
ps.retries = 0                        # 不重试
# randomisationStartInterval 仍然生效

# ── generate_pdu() ──
# 触发 on_before_read 钩子后序列化所有对象值
def on_before_read(sender, attr_index, context):
    print("PushSetup reading attr {}".format(attr_index))
    return dlms.DLMSEvent.ALLOW

ps.on_before_read = on_before_read
# pdu = ps.generate_pdu()  # 返回 bytes,可用 socket.send(pdu)

# ── Push action 钩子 ──
def on_push_action(sender, event):
    if event.index == 1:
        pdu = sender.generate_pdu()
        # mobile_conn.send(pdu)  ← Python 侧传输
        print("Pushed {} bytes".format(len(pdu)))
        return True

ps.on_before_action = on_push_action

# ── 清理 ──
ps.deinit(ps)   # dest[1] bug, 需手动传 self
reg.deinit(reg)
clock.deinit(clock)

构造函数

PushSetup(logical_name: str, objectList: list = None, destination: str = None,
          retries: int = 3, retryDelay: int = 60,
          randomisationStartInterval: int = 0,
          communicationWindow: list = None)
import dlms
A = dlms.ANY

# 准备被推送的对象
reg = dlms.Register("1.0.1.8.0.255", 0)
clock = dlms.Clock("0.0.1.0.0.255")

# 创建 PushSetup
ps = dlms.PushSetup(
    "0.0.25.9.0.255",
    objectList=[(reg, 2, 0), (clock, 2, 0)],
    destination="192.168.1.100:4059",
    retries=3,
    retryDelay=60,
    randomisationStartInterval=5,
)
# 设置通信窗口
ps.communicationWindow = [
    [(-1, -1, -1,  9, 0, 0), (-1, -1, -1, 12, 0, 0)],
    [(-1, -1, -1, 14, 0, 0), (-1, -1, -1, 17, 0, 0)],
]
参数 类型 默认值 说明
logical_name str 必传 OBIS 代码字符串,如 "0.0.25.9.0.255"
objectList list None 推送的数据项,每项为 (obj, attr_idx, data_idx) 三元组
destination str None 目标地址, "ip:port" 格式
retries int 3 推送失败重试次数
retryDelay int 60 重试间隔(秒)
randomisationStartInterval int 0 随机延迟区间(秒),避免多设备集中推送
communicationWindow list None 推送允许时间窗口, [[start_tuple, end_tuple], ...]

Python 属性一览

属性 类型 可写 说明
logical_name bytes × OBIS 代码(6 字节)
objectList list (obj, attr_idx, data_idx) 三元组列表
destination str 目标 "ip:port" 地址
retries int 重试次数
retryDelay int 重试间隔(秒)
randomisationStartInterval int 随机延迟区间(秒)
communicationWindow list 推送时间窗口 [[start, end], ...]
on_before_read Callable 读前钩子(继承自 CosemObject)
on_after_read Callable 读后钩子(继承自 CosemObject)
on_before_write Callable 写前钩子(继承自 CosemObject)
on_after_write Callable 写后钩子(继承自 CosemObject)
on_before_action Callable 动作前钩子(继承自 CosemObject)
on_after_action Callable 动作后钩子(继承自 CosemObject)
access_dict dict 实例级访问控制

方法

方法 说明
generate_pdu() 编码当前属性值为 DLMS DATA-NOTIFICATION PDU,返回 bytes
deinit() 释放 C 侧资源

生成推送流程

客户端调用 COSEM 方法 1(push)
        │
        ▼
on_before_action(self, event)  ← 你的 handler
        │
        ├── self.generate_pdu() →  bytes(DATA-NOTIFICATION PDU)
        │
        └── mobile_conn.send(pdu) → 通过 relay UDP 发出

generate_pdu() 做了 3 件事:

  1. 读取 objectList 中所有对象的当前属性值
  2. 按 DLMS 结构格式序列化
  3. 包装为 DATA-NOTIFICATION 命令,返回 bytes

你的 handler 只负责传输: 调用 generate_pdu() 拿数据 → mobile_conn.send(pdu) 发出。
访问控制

# ⚠️ 构造时不支持 access 参数
ps = dlms.PushSetup("0.0.25.9.0.255",
    objectList=[(reg, 2, 0)],
    destination="192.168.1.100:4059")

# 全局默认访问控制(推荐方式)
dlms.set_default_access(dlms.PushSetup, {
    dlms.Authentication.NONE: {
        2: dlms.AccessMode.READ,
        3: dlms.AccessMode.READ,
        4: dlms.AccessMode.READ,
    },
})

典型用例

场景 示例
定期上报电能和时间 objectList=[(reg, 2, 0), (clock, 2, 0)] , destination="203.0.113.1:4060"
仅在白天推送 communicationWindow=[[(-1,-1,-1,6,0,0), (-1,-1,-1,22,0,0)]]
告警主动推送 objectList=[(event_code, 2, 0)] , retries=5

GsmDiagnostic

蜂窝网络实时诊断对象。DLMS 客户端可远程读取设备连接状态。调用 update() 从 QuecPython net 模块刷新所有字段;配合 on_before_read 可在每次客户端读取前自动刷新,其 COSEM ID=47,OBIS 0.0.25.6.0.255

Blue Book 属性

编号 名称 类型 说明
1 logical_name octet-string(6) OBIS 代码,只读
2 operator_name visible-string 运营商名称,如 "A1 Srbija" ,不可用时为 None
3 status enum 网络注册状态(⏩ VOLATILE)
4 circuit_switch_status enum 电路交换连接状态(⏩ VOLATILE)
5 packet_switch_status enum 数据技术类型(⏩ VOLATILE)
6 cell_info STRUCT 服务小区详细信息(⏩ COMPLEX + VOLATILE)
7 adjacentCells array 只读, adjacent_cells 返回 AdjacentCell 列表副本
8 captureTime octet-string C 端存储但 Python 未暴露 getter

构造函数只有 logical_name 一个参数,无 access 参数。

构造函数

GsmDiagnostic(logical_name: str)
参数 类型 默认值 说明
logical_name str 必传 OBIS 代码字符串,如 "0.0.25.6.0.255"
import dlms

gsm = dlms.GsmDiagnostic("0.0.25.6.0.255")

# ── 属性 1: logical_name ──
print(gsm.logical_name)  # b'\x00\x00\x19\x06\x00\xff'

# ── 属性 2: operator_name ──
gsm.operator_name = "China Mobile"
print(gsm.operator_name)  # "China Mobile"

# ── 属性 3: status ──
print(gsm.status)  # 4 (初始默认 UNKNOWN)
gsm.status = 1      # 手动设为 HOME

# ── 属性 4/5: CS/PS 状态 ──
print(gsm.circuit_switch_status, gsm.packet_switch_status)  # 0, 0

# ── update(需 modem 在线)──
gsm.update()
print(gsm.operator_name)         # 真实运营商
print(gsm.status)                # 真实注册状态
print(gsm.packet_switch_status)  # 1=GPRS / 5=LTE

# ── 属性 6: cell_info(只读,每次新建副本)──
ci = gsm.cell_info
print(ci.cell_id, ci.location_id, ci.signal_quality)
print(ci.mobile_country_code, ci.mobile_network_code)

# ── 属性 7: adjacent_cells(只读)──
print(gsm.adjacent_cells_count)
for adj in gsm.adjacent_cells:
    print("  cell=%d, signal=%d" % (adj.cell_id, adj.signal_quality))

# ── Legacy 快捷属性 ──
print(gsm.cell_id, gsm.signal_quality)  # 同 ci.cell_id, ci.signal_quality
gsm.signal_quality = -75                # 可直接写 embedded 结构体

# ── 钩子:读前自动刷新 ──
def on_before_read(sender, attr_index, context):
    if attr_index == 3:  # status 被读取前
        sender.update()
    return dlms.DLMSEvent.ALLOW

gsm.on_before_read = on_before_read

Python 属性一览

属性 类型 可写 说明
logical_name bytes × OBIS 代码(6 字节)
operator_name str|None 运营商名称, update() 自动填充
status int 注册状态:0=未注册, 1=归属, 2=搜索中, 3=被拒, 4=未知, 5=漫游
circuit_switch_status int CS 域状态:0=非活跃, 1=活跃, 2=未知
packet_switch_status int PS 域制式:0=非活跃, 1=GPRS, 5=LTE…( update() 根据服务小区自动判定)
cell_info GsmCellInfo × 只读 ,每次读取 m_new_obj + memcpy 新建副本
adjacent_cells list[AdjacentCell] × 只读 ,每次读取新建副本列表
adjacent_cells_count int × 只读 ,邻区总数
cell_id int Legacy :等价 cell_info.cell_id ,直接读写嵌入的 cellInfo.cellId
location_id int Legacy :等价 cell_info.location_id
signal_quality int Legacy :等价 cell_info.signal_quality
ber int Legacy :等价 cell_info.ber
mobile_country_code int Legacy :等价 cell_info.mobile_country_code
mobile_network_code int Legacy :等价 cell_info.mobile_network_code
channel_number int Legacy :等价 cell_info.channel_number
on_before_read Callable 读前钩子(继承自 CosemObject)
on_after_read Callable 读后钩子
on_before_write Callable 写前钩子
on_after_write Callable 写后钩子
access_dict dict 继承自 CosemObject,构造时不支持传入

Legacy 属性 cell_id location_id 等 7 个字段直接映射到嵌入的 gxGSMCellInfo 结构体,等价于 cell_info.xxx 的快捷方式。保留是为了向后兼容。

注册状态常量( status

常量 说明
0 NOT_REGISTERED 未注册到任何网络
1 HOME_NETWORK 已注册到归属网络
2 SEARCHING 正在搜索网络
3 DENIED 注册被拒绝
4 UNKNOWN 状态未知
5 ROAMING 漫游中

数据技术常量( packet_switch_status

说明
0 INACTIVE
1 GPRS
2 EGPRS
3 UMTS
4 HSDPA
5 LTE
方法 说明
update() 调用 QuecPython net 模块( net.getCellInfo() / net.getSignal() / net.operatorName() / net.getState() )刷新所有属性。网络不可用时仅打印警告,不抛异常

GsmCellInfo — 服务小区详情

属性 类型 说明
cell_id int 小区标识(CID)
location_id int 位置区码(LAC)或跟踪区码(TAC for LTE)
signal_quality int 接收信号强度(dBm,通常为负值)
ber int 误码率等级(0–7,GSM 05.08)
mobile_country_code int 移动国家码(MCC,如 220 表示塞尔维亚)
mobile_network_code int 移动网络码(MNC)
channel_number int ARFCN / UARFCN / EARFCN

AdjacentCell — 邻小区

属性 类型 说明
cell_id int 邻小区标识
signal_quality int 接收信号强度(dBm)

GsmDiagnostic 上同时暴露旧式快捷访问属性( cell_id location_id signal_quality ber mobile_country_code mobile_network_code channel_number ),它们直接映射到 cell_info 的同名字段。

访问控制

import dlms
from dlms import AccessMode, Authentication

gsm = dlms.GsmDiagnostic("0.0.25.6.0.255")

dlms.set_default_access(dlms.GsmDiagnostic, {
    dlms.Authentication.NONE: {
        2: dlms.AccessMode.READ,  # operator_name
        3: dlms.AccessMode.READ,  # status
        4: dlms.AccessMode.READ,  # circuit_switch_status
        5: dlms.AccessMode.READ,  # packet_switch_status
        6: dlms.AccessMode.READ,  # cell_info
    },
})

典型用例

场景 示例
远程诊断信号强度 gsm.update(); print(gsm.cell_info.signal_quality)
判断是否在归属网络 if gsm.status == 1: print("Home")
扫描邻小区数量 print(gsm.adjacent_cells_count)

SecuritySetup

DLMS 安全配置对象(其 COSEM ID=64 )。定义服务器端的安全策略、加密套件、系统标题、密钥和证书。每个 AssociationLogicalName / AssociationShortName 可通过 security_setup 属性引用一个 SecuritySetup 实例,从而为不同逻辑设备绑定独立的安全配置。

初始化副作用 :构造函数在 bb_init 后检查全局 SERVER_SYSTEM_TITLE[8] 数组,若非空则自动设置 serverSystemTitle 。写入 server_system_title 时还会同步更新 serverSettings.base.cipher.systemTitle (C 层加密引擎实际使用的值)。

Blue Book 属性

编号 名称 类型 说明
1 logical_name octet-string(6) OBIS 代码,只读
2 server_system_title octet-string(8) 服务端系统标题(8 字节)
3 client_system_title octet-string(8) 客户端系统标题(8 字节)
4 security_policy enum 安全策略(见 SecurityPolicy 表)
5 security_suite enum 安全套件版本(0/1/2)
6 certificates ARRAY of STRUCT 证书列表(⏩ COMPLEX)
7 min_invocation_counter uint32 最低调用计数器
8 gak octet-string 全局认证密钥(16 或 32 字节)
9 guek octet-string 全局单播加密密钥(Block Cipher Key,16 或 32 字节)
10 gbek octet-string 全局广播加密密钥

构造函数只有 logical_name 一个参数,无 access 参数。

构造函数

SecuritySetup(logical_name: str)
参数 类型 说明
logical_name str OBIS 代码字符串,如 "0.0.43.0.1.255"

关联的枚举类型

SecurityPolicy — 定义消息级安全保护策略( dlms.SecurityPolicy.XXX ):

常量 说明
SecurityPolicy.NOTHING 0 无保护
SecurityPolicy.AUTHENTICATED 1 仅认证(Suite V0)
SecurityPolicy.ENCRYPTED 2 仅加密(Suite V0)
SecurityPolicy.AUTHENTICATED_ENCRYPTED 3 认证+加密(Suite V0;HighGMac 预建立关联用此值)
SecurityPolicy.AUTHENTICATED_REQUEST 0x4 请求认证(Suite V1)
SecurityPolicy.ENCRYPTED_REQUEST 0x8 请求加密(Suite V1)
SecurityPolicy.DIGITALLY_SIGNED_REQUEST 0x10 请求签名(Suite V1)
SecurityPolicy.AUTHENTICATED_RESPONSE 0x20 响应认证(Suite V1)
SecurityPolicy.ENCRYPTED_RESPONSE 0x40 响应加密(Suite V1)
SecurityPolicy.DIGITALLY_SIGNED_RESPONSE 0x80 响应签名(Suite V1)

V0 是互斥单选 (0–3), V1 是位掩码组合 (0x4–0x80 可 \| 组合)。 SecurityPolicy 枚举本身不区分版本,具体语义由 security_suite 决定。

import dlms
from dlms import SecurityPolicy, CertificateEntity, CertificateType

sec = dlms.SecuritySetup("0.0.43.0.1.255")

# ── 属性 1: logical_name ──
print(sec.logical_name)  # b'\x00\x00+\x00\x01\xff'

# ── 属性 2: server_system_title ──
# 写入:同时更新 serverSettings.base.cipher.systemTitle(C 层加密引擎)
sec.server_system_title = b'GRX12345'
print(sec.server_system_title)  # b'GRX12345'

# ── 属性 3: client_system_title ──
sec.client_system_title = b'CLI12345'
print(sec.client_system_title)  # b'CLI12345'

# ── 属性 4: security_policy ──
sec.security_policy = SecurityPolicy.AUTHENTICATED_ENCRYPTED  # Suite V0: 认证+加密
# Suite V1 位掩码组合:
# sec.security_policy = SecurityPolicy.AUTHENTICATED_REQUEST | SecurityPolicy.ENCRYPTED_REQUEST

# ── 属性 5: security_suite ──
sec.security_suite = 1  # V1: AES-GCM-128 + ECDSA P-256

# ── 属性 6: certificates ──
sec.certificates = [{
    "entity":         CertificateEntity.SERVER,
    "type":           CertificateType.DIGITAL_SIGNATURE,
    "serial_number":  "123456",
    "issuer":         "CN=Test CA",
    "subject":        "CN=Test Server",
    "subject_alt_name": "",
}]
for cert in sec.certificates:
    print(cert["serial_number"])  # "123456"

# ── 属性 7: min_invocation_counter ──
sec.min_invocation_counter = 0
print(sec.min_invocation_counter)  # 0

# ── 属性 8/9/10: 密钥 ──
sec.gak  = b'\x00' * 16   # 认证密钥,16 或 32 字节
sec.guek = b'\x01' * 16   # 单播加密密钥
sec.gbek = b'\x02' * 16   # 广播加密密钥

# ── 绑定到 Association ──
assoc = dlms.AssociationLogicalName("0.0.40.0.1.255")
assoc.auth_mechanism = "HighGMac"
assoc.clientSAP = 0x10
assoc.security_setup = sec  # 一个 SecuritySetup 可被多个 Association 共享

# ── 调用计数器零拷贝暴露 ──
# 用 nocopy Data 对象直接引用 SecuritySetup 的 min_invocation_counter(属性 7→6)
inv = dlms.Data("0.0.43.1.0.255", nocopy=True,
    access={dlms.Authentication.NONE: {2: dlms.AccessMode.READ}})
inv.value = (sec, 6)  # nocopy 引用 SecuritySetup 的 min_invocation_counter

Python 属性一览

属性 类型 可写 说明
logical_name bytes × OBIS 代码(6 字节)
security_policy int 安全策略(SecurityPolicy 枚举)
security_suite int 安全套件版本(0/1/2)
min_invocation_counter int 最低调用计数器
server_system_title bytes 服务端系统标题(8 字节)
client_system_title bytes 客户端系统标题(8 字节)
guek bytes 全局单播加密密钥
gak bytes 全局认证密钥
gbek bytes 全局广播加密密钥
certificates list 证书列表,每项为 dict
on_before_read Callable 读前钩子(继承自 CosemObject)
on_after_read Callable 读后钩子(继承自 CosemObject)
on_before_write Callable 写前钩子(继承自 CosemObject)
on_after_write Callable 写后钩子(继承自 CosemObject)
access_dict dict 实例级访问控制

方法

方法 说明
deinit() 释放 C 侧资源

访问控制

import dlms
from dlms import AccessMode, Authentication

sec = dlms.SecuritySetup("0.0.43.0.1.255")
sec.security_policy = dlms.SecurityPolicy.AUTHENTICATED_ENCRYPTED

# 全局默认访问控制(推荐方式)
dlms.set_default_access(dlms.SecuritySetup, {
    dlms.Authentication.HIGH: {
        2: dlms.AccessMode.READ,     # server_system_title
        3: dlms.AccessMode.READ,     # client_system_title
        4: dlms.AccessMode.READ,     # security_policy
        5: dlms.AccessMode.READ,     # security_suite
        6: dlms.AccessMode.READ,     # certificates
        7: dlms.AccessMode.READ,     # min_invocation_counter
    },
})

网络传输对象

以下对象配置 DLMS 通信栈: IecHdlcSetup (HDLC 帧参数)、 LocalPortSetup (光口 Mode E 协商)、 GprsSetup (蜂窝 APN)、 IPv4Setup (IP 地址)、 TcpUdpSetup (TCP/UDP 端口)、 MacAddressSetup (MAC 地址)。


IecHdlcSetup

IEC 62056-46 HDLC 链路层参数配置对象(其 COSEM ID=23 )。定义 HDLC 通信的物理/链路层参数:波特率、窗口大小、帧长度、超时和设备地址。该实例通过 dlms.set_hdlc(hdlc) 注册为全局 HDLC 配置(驱动 SerialConnection 等连接类型)。

波特率内部转换 commSpeed 在 C 层以 DLMS_BAUD_RATE 枚举(1 字节)存储(编译器 -fshort-enums ),Python 读写时自动通过 baud_int_to_enum / baud_enum_to_int 在整数和枚举索引间转换。支持的速率:300, 600, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200。

Blue Book 属性

编号 名称 类型 说明
1 logical_name octet-string(6) OBIS 代码 只读, bytes
2 comm_speed enum 通信速率(baud)
3 window_size_rx uint8 接收窗口大小
4 window_size_tx uint8 发送窗口大小
5 max_info_len_tx uint16 发送最大信息长度
6 max_info_len_rx uint16 接收最大信息长度
7 inactivity_timeout uint16 非活动超时(秒)
8 device_address uint16 设备地址(默认 0x10
interCharachterTimeout uint16
import dlms

hdlc = dlms.IecHdlcSetup("0.0.22.0.0.255")

# ── 属性 1: logical_name ──
print(hdlc.logical_name)  # b'\x00\x00\x16\x00\x00\xff'

# ── 属性 2: commSpeed ──
print(hdlc.commSpeed)  # 9600(默认)
hdlc.commSpeed = 19200
print(hdlc.commSpeed)  # 19200

# ── 属性 3/4: 窗口大小 ──
print(hdlc.windowSizeRx, hdlc.windowSizeTx)  # 1, 1
hdlc.windowSizeRx = 2

# ── 属性 5/6: 最大帧长度 ──
print(hdlc.maxInfoLenTx, hdlc.maxInfoLenRx)  # 128, 128
hdlc.maxInfoLenTx = 256

# ── 属性 7: timeout ──
print(hdlc.timeout)  # 120
hdlc.timeout = 60

# ── 属性 8: deviceAddr ──
print("0x%02x" % hdlc.deviceAddr)  # 0x10
hdlc.deviceAddr = 0x20

# ── 注册为全局配置 ──
dlms.set_hdlc(hdlc)

# ── 钩子 ──
def on_before_write(sender, attr_index, value, context):
    if attr_index == 2:  # commSpeed
        print("Changing baud rate to {}".format(value))
    return dlms.DLMSEvent.ALLOW

hdlc.on_before_write = on_before_write

构造函数

IecHdlcSetup(logical_name: str, commSpeed: int = 9600,
             windowSizeRx: int = 1, windowSizeTx: int = 1,
             maxInfoLenTx: int = 128, maxInfoLenRx: int = 128,
             timeout: int = 120, deviceAddr: int = 0x10)

构造函数只有 logical_name 必传,其余参数都带默认值。无 access 参数。

参数 类型 默认值 说明
logical_name str 必传 OBIS 代码,如 "0.0.22.0.0.255"
commSpeed int 9600 波特率;支持 300/600/1200/2400/4800/9600/19200/38400/57600/115200
windowSizeRx int 1 接收窗口大小(HDLC 滑动窗口)
windowSizeTx int 1 发送窗口大小
maxInfoLenTx int 128 最大发送帧信息字段长度(字节)
maxInfoLenRx int 128 最大接收帧信息字段长度
timeout int 120 无通信超时(秒)
deviceAddr int 0x10 HDLC 设备地址(1 字节,0x00–0xFF)
import dlms

hdlc = dlms.IecHdlcSetup(
    "0.0.22.0.0.255",
    commSpeed=9600,
    windowSizeRx=1,
    windowSizeTx=1,
    maxInfoLenTx=128,
    maxInfoLenRx=128,
    timeout=120,
    deviceAddr=0x10,
)

# 注册为全局 HDLC 配置(SerialConnection 等依赖此配置)
dlms.set_hdlc(hdlc)

Python 属性一览

属性 类型 可写 说明
logical_name bytes × OBIS 代码(6 字节)
commSpeed int 波特率整数,读写自动转换枚举(未知值回退到 9600)
windowSizeRx int 接收窗口大小
windowSizeTx int 发送窗口大小
maxInfoLenTx int 最大发送帧长度
maxInfoLenRx int 最大接收帧长度
timeout int 无通信超时(秒)
deviceAddr int HDLC 设备地址(1 字节)
on_before_read Callable 读前钩子
on_after_read Callable 读后钩子
on_before_write Callable 写前钩子
on_after_write Callable 写后钩子
on_before_action Callable 动作前钩子
on_after_action Callable 动作后钩子
access_dict dict 继承自 CosemObject

deinit :IecHdlcSetup 内部只有嵌入的基元字段和枚举,无动态分配的堆内存,不需要显式释放。

波特率对照表

commSpeed 枚举索引 说明
300 0
600 1
1200 2
2400 3
4800 4
9600 5 默认值
19200 6
38400 7
57600 8
115200 9

传入不在上表的波特率值 → 自动回退到 9600。

访问控制

import dlms
from dlms import AccessMode, Authentication

hdlc = dlms.IecHdlcSetup("0.0.22.0.0.255")

dlms.set_default_access(dlms.IecHdlcSetup, {
    dlms.Authentication.NONE: {
        2: dlms.AccessMode.READ,     # commSpeed: 公开读
        3: dlms.AccessMode.READ,     # windowSizeRx
        4: dlms.AccessMode.READ,     # windowSizeTx
        5: dlms.AccessMode.READ,     # maxInfoLenTx
        7: dlms.AccessMode.READ,     # timeout
    },
    dlms.Authentication.HIGH: {
        2: dlms.AccessMode.READ_WRITE,  # commSpeed: HIGH 可写
        8: dlms.AccessMode.READ,        # deviceAddr
    },
})

LocalPortSetup

IEC 62056-21 光学端口配置对象(其 COSEM ID=19 )。定义本地光学端口(红外/串口)的 Mode E 协议协商参数:默认模式、波特率切换、响应时间、设备地址和三级密码保护。

与 IecHdlcSetup 的区别 LocalPortSetup 配置的是 光学端口协议层 (Mode E 协商 + 波特率切换 + 密码), IecHdlcSetup 配置的是 HDLC 链路层 (帧格式、窗口大小、超时)。实际通信栈中两者协同:光学端口先用 Mode E 握手切换到目的波特率,然后在上层跑 HDLC。

Blue Book 属性

编号 名称 说明
1 logical_name OBIS 代码
2 default_mode 默认光口协议模式(DLMS_OPTICAL_PROTOCOL_MODE)
3 default_baud 默认波特率(典型 300 bps)
4 proposed_baud 协商后切换的目标波特率(9600/19200)
5 response_time 响应超时(ms)
6 device_address 设备地址
7 password_1 P1 密码(最低安全级)
8 password_2 P2 密码(中等安全级)
9 password_5 P5 密码(最高安全级)
import dlms

# ── 创建光学端口配置 ──
port = dlms.LocalPortSetup(
    "0.0.128.0.0.255",
    default_mode=0,        # Mode E
    default_baud=0,        # 300 bps(枚举索引 0)
    proposed_baud=5,       # 9600 bps(枚举索引 5)
    response_time=0,       # 20ms
    device_address=b"MTR001",
    password_1=b"00000000",
    password_2=b"12345678",
    password_5=b"87654321",
)

# ── 属性 1: logical_name ──
print(port.logical_name)  # b'\x00\x00\x80\x00\x00\xff'

# ── 属性 2: default_mode ──
print(port.default_mode)  # 0(DEFAULT / Mode E)
port.default_mode = 1     # 切换为 NET (HDLC) 模式

# ── 属性 3/4: 波特率 ──
print(port.default_baud, port.proposed_baud)  # 0, 5
# ⚠️ 传枚举索引,不要传整数!
port.proposed_baud = 6   # 19200 bps

# ── 属性 5: response_time ──
print(port.response_time)  # 0(20ms)
port.response_time = 1     # 200ms

# ── 属性 6: device_address ──
print(port.device_address)  # b'MTR001'
port.device_address = b"ABC"  # 最大 6 字节

# ── 属性 7/8/9: 三级密码 ──
print(port.password_1)  # b'00000000'(未设置返回 None)
port.password_2 = b"newpass123"

# ── 钩子 ──
def on_before_read(sender, attr_index, context):
    # 密码属性被读取前可以拦截(安全考虑)
    if attr_index in (7, 8, 9):
        return dlms.DLMSEvent.DENY  # 禁止从 DLMS 读取密码
    return dlms.DLMSEvent.ALLOW

port.on_before_read = on_before_read

构造函数

LocalPortSetup(logical_name: str, default_mode: int = 0,
               default_baud: int = 300, proposed_baud: int = 9600,
               response_time: int = 1000, device_address: bytes = None,
               password_1: bytes = None, password_2: bytes = None,
               password_5: bytes = None)

构造函数 9 个参数,仅 logical_name 必传。无 access 参数。无 deinit (全为 gxByteBuffer 嵌入字段,无 gxmalloc 堆分配)。

import dlms
local_port = dlms.LocalPortSetup(
    "0.0.19.0.0.255",
    default_baud=300,
    proposed_baud=9600,
    device_address=b"MTR001",
    password_1=b"00000000",
)
hdlc = dlms.IecHdlcSetup("0.0.22.0.0.255", commSpeed=9600, deviceAddr=0x10)
conn = dlms.OpticalConnection(uart_port=1, local_port_setup=local_port, hdlc_setup=hdlc)

访问控制

import dlms
from dlms import AccessMode, Authentication

port = dlms.LocalPortSetup("0.0.128.0.0.255")

port.access_dict = {
    2: (AccessMode.READ,        Authentication.NONE),   # default_mode
    3: (AccessMode.READ,        Authentication.NONE),   # default_baud
    4: (AccessMode.READ,        Authentication.NONE),   # proposed_baud
    5: (AccessMode.READ,        Authentication.NONE),   # response_time
    6: (AccessMode.READ,        Authentication.NONE),   # device_address
    7: (AccessMode.READ_WRITE,  Authentication.HIGH),   # password_1
    8: (AccessMode.READ_WRITE,  Authentication.HIGH),   # password_2
    9: (AccessMode.READ_WRITE,  Authentication.HIGH),   # password_5
}

GprsSetup

GPRS/蜂窝网络接入点配置对象(其 COSEM ID=45 )。定义 APN(接入点名称)和 SIM PIN 码。通常与 IPv4Setup 配合使用: IPv4Setup.datalink_reference = gprs 将 IP 层绑定到 GPRS 数据链路层。

Blue Book 属性

编号 名称 类型 说明
1 logical_name octet-string(6) 只读, bytes
2 apn visible-string 可读写,接入点名称
3 pinCode uint16 可读写, pin_code
4 defaultQualityOfService structure 内部属性变量
5 requestedQualityOfService structure 内部属性变量
import dlms

gprs = dlms.GprsSetup("0.1.25.0.0.255")

# ── 属性 1: logical_name ──
print(gprs.logical_name)  # b'\x00\x01\x19\x00\x00\xff'

# ── 属性 2: apn ──
print(gprs.apn)         # None(默认未设置)
gprs.apn = "internet"
print(gprs.apn)         # "internet"
gprs.apn = "m2m.carrier.net"

# ── 属性 3: pin_code ──
print(gprs.pin_code)    # 0(默认无 PIN)
gprs.pin_code = 1234

# ── 配合 IPv4Setup 使用 ──
ipv4 = dlms.IPv4Setup(
    "0.0.25.1.0.255",
    datalink_reference=gprs,   # IP 层绑定到 GPRS
    ip_address="0.0.0.0",      # DHCP
    use_dhcp=True,
    primary_dns_address="8.8.8.8",
    secondary_dns_address="8.8.4.4",
)

# ── 钩子 ──
def on_before_write(sender, attr_index, value, context):
    if attr_index == 2:  # apn
        print("Changing APN to {}".format(value))
    return dlms.DLMSEvent.ALLOW

gprs.on_before_write = on_before_write

构造函数

GprsSetup(logical_name: str, apn: str = None, pin_code: int = 0)
参数 类型 默认值 说明
logical_name str 必传 OBIS 代码,如 "0.1.25.0.0.255"
apn str None 接入点名称(如 "internet" "m2m.carrier.net" )。 注意:默认 None ""
pin_code int 0 SIM 卡 PIN 码(0 = 无 PIN)
import dlms
gprs = dlms.GprsSetup("0.0.25.0.0.255", apn="internet", pin_code=0)

Python 属性一览

属性 类型 可写 说明
logical_name bytes × OBIS 代码(6 字节)
apn str|None 读取未设置时返回 None (不是 ""
pin_code int SIM PIN 码
on_before_read Callable 读前钩子
on_after_read Callable 读后钩子
on_before_write Callable 写前钩子
on_after_write Callable 写后钩子
on_before_action Callable 动作前钩子
on_after_action Callable 动作后钩子
access_dict dict 继承自 CosemObject

访问控制

import dlms
from dlms import AccessMode, Authentication

gprs = dlms.GprsSetup("0.1.25.0.0.255")

dlms.set_default_access(dlms.GprsSetup, {
    dlms.Authentication.NONE: {
        2: dlms.AccessMode.READ,      # apn: 公开读
    },
    dlms.Authentication.HIGH: {
        2: dlms.AccessMode.READ_WRITE,  # apn: HIGH 认证可写
        3: dlms.AccessMode.READ_WRITE,  # pin_code
    },
})

IPv4Setup

IPv4 网络层配置对象(其 COSEM ID=42 )。定义 IP 地址、子网掩码、网关、DNS 和 DHCP 开关。通过 datalink_reference 绑定下层数据链路对象( GprsSetup MacAddressSetup ),与 TcpUdpSetup 配合构成完整的 DLMS 网络通信栈。

内部转换 :IP 地址在 C 层以 uint32_t (网络字节序)存储,Python 读写时自动通过 ip_str_to_uint32 / uint32_to_ip_str 做字符串↔整数的双向转换。传入非法 IP → 返回 0.0.0.0

┌─────────────────────────────────────┐
│  TcpUdpSetup (COSEM 41)             │  ← 传输层
│  port=4059, ip_reference=ipv4       │
├─────────────────────────────────────┤
│  IPv4Setup (COSEM 42)               │  ← 网络层
│  datalink_reference=gprs|mac        │
├─────────────────────────────────────┤
│  GprsSetup (45) / MacAddressSetup   │  ← 数据链路层
│  apn="internet" / mac="AA:BB:..."   │
└─────────────────────────────────────┘

Blue Book 属性

编号 名称 类型 说明
1 logical_name octet-string(6) 只读, bytes
2 dataLinkLayer object-ref 可读写, datalink_reference
3 ipAddress uint32 可读写, ip_address (字符串↔uint32 自动转换)
4 multicastIPAddress array multicast_ip_address 仅存 Python 引用,未解析到 C 数组
5 ipOptions array 内部属性
6 subnetMask uint32 可读写, subnet_mask
7 gatewayIPAddress uint32 可读写, gateway_ip_address
8 useDHCP bool 可读写, use_dhcp
9 primaryDNSAddress uint32 可读写, primary_dns_address
10 secondaryDNSAddress uint32 可读写, secondary_dns_address
import dlms

# ── 绑定数据链路层 ──
gprs = dlms.GprsSetup("0.1.25.0.0.255", apn="internet")

ipv4 = dlms.IPv4Setup(
    "0.0.25.1.0.255",
    datalink_reference=gprs,
    use_dhcp=True,
)

# ── 属性 1: logical_name ──
print(ipv4.logical_name)  # b'\x00\x00\x19\x01\x00\xff'

# ── 属性 2: datalink_reference ──
print(ipv4.datalink_reference)  # <GprsSetup ...>
# 可事后修改
mac = dlms.MacAddressSetup("0.0.25.4.0.255")
ipv4.datalink_reference = mac    # 切换到以太网

# ── 属性 3: ip_address ──
print(ipv4.ip_address)  # "0.0.0.0"(DHCP 模式,未分配)
ipv4.ip_address = "192.168.1.100"   # 静态 IP
ipv4.use_dhcp = False               # 关闭 DHCP 才用静态 IP

# ── 属性 6/7: 子网掩码 & 网关 ──
ipv4.subnet_mask = "255.255.255.0"
ipv4.gateway_ip_address = "192.168.1.1"
print(ipv4.subnet_mask, ipv4.gateway_ip_address)

# ── 属性 8: use_dhcp ──
print(ipv4.use_dhcp)   # False
ipv4.use_dhcp = False   # 切换为静态 IP

# ── 属性 9/10: DNS ──
ipv4.primary_dns_address = "8.8.8.8"
ipv4.secondary_dns_address = "8.8.4.4"
print(ipv4.primary_dns_address, ipv4.secondary_dns_address)

# ── 属性 4: multicast_ip_address(仅 Python 侧存储)──
ipv4.multicast_ip_address = ["224.0.0.1", "224.0.0.251"]
print(ipv4.multicast_ip_address)  # ['224.0.0.1', '224.0.0.251']

# ── 钩子 ──
def on_before_write(sender, attr_index, value, context):
    if attr_index == 3:  # ip_address
        print("Changing IP to {}".format(value))
    return dlms.DLMSEvent.ALLOW

ipv4.on_before_write = on_before_write

构造函数

IPv4Setup(logical_name: str, datalink_reference: object = None,
          ip_address: str = "0.0.0.0", subnet_mask: str = "255.255.255.0",
          gateway_ip_address: str = "0.0.0.0", use_dhcp: bool = True,
          primary_dns_address: str = "0.0.0.0",
          secondary_dns_address: str = "0.0.0.0")
参数 类型 编号 默认值 说明
logical_name str 1 必传 OBIS 代码,如 "0.0.25.1.0.255"
datalink_reference DLMS 对象 2 None 数据链路层对象( GprsSetup MacAddressSetup
ip_address str 3 None IPv4 地址字符串(如 "192.168.1.10"
multicast_ip_address list 4 None 组播地址列表(仅存 Python 引用,不转换到 C)
subnet_mask str 6 None 子网掩码(如 "255.255.255.0"
gateway_ip_address str 7 None 网关地址
use_dhcp bool 8 True 是否启用 DHCP
primary_dns_address str 9 None 首选 DNS 服务器
secondary_dns_address str 10 None 备用 DNS 服务器
import dlms

gprs = dlms.GprsSetup("0.1.25.0.0.255", apn="internet")

ipv4 = dlms.IPv4Setup(
    "0.0.25.1.0.255",
    datalink_reference=gprs,
    ip_address="0.0.0.0",
    subnet_mask="255.255.255.0",
    gateway_ip_address="192.168.1.1",
    use_dhcp=True,
    primary_dns_address="8.8.8.8",
    secondary_dns_address="8.8.4.4",
)

Python 属性一览

属性 类型 可写 说明
logical_name bytes × OBIS 代码(6 字节)
datalink_reference DLMS对象| None 指向 GprsSetup / MacAddressSetup 。写入时自动 gx_from_mp 取 C 指针
ip_address str 字符串 IP,读写自动 uint32 string 转换
multicast_ip_address list 仅存 Python 引用, 不解析到 C 语言 multicastIPAddress 数组
subnet_mask str 子网掩码字符串
gateway_ip_address str 网关地址字符串
use_dhcp bool DHCP 开关( True / False
primary_dns_address str 首选 DNS 地址
secondary_dns_address str 备用 DNS 地址
on_before_read Callable 读前钩子
on_after_read Callable 读后钩子
on_before_write Callable 写前钩子
on_after_write Callable 写后钩子
on_before_action Callable 动作前钩子
on_after_action Callable 动作后钩子
access_dict dict 继承自 CosemObject

multicast_ip_address 的特殊处理 :构造函数和多线程 setter 都只把值存入 self->multicast_ip_list (Python 引用), 不解析为 C 语言 gxArray multicastIPAddress variantArray multicastIPAddress 。C 结构体中的该字段始终为空( arr_init / va_init 后的初始状态)。


访问控制

import dlms
from dlms import AccessMode, Authentication

ipv4 = dlms.IPv4Setup("0.0.25.1.0.255")

dlms.set_default_access(dlms.IPv4Setup, {
    dlms.Authentication.NONE: {
        2: dlms.AccessMode.READ,     # datalink_reference
        3: dlms.AccessMode.READ,     # ip_address
        8: dlms.AccessMode.READ,     # use_dhcp
    },
    dlms.Authentication.HIGH: {
        3: dlms.AccessMode.READ_WRITE,   # ip_address: HIGH 认证可写
        7: dlms.AccessMode.READ_WRITE,   # gateway_ip_address
        9: dlms.AccessMode.READ_WRITE,   # primary_dns_address
    },
})

TcpUdpSetup

TCP/UDP 传输层配置对象(其 COSEM ID=41 )。定义 DLMS 通信的端口号、IP 层引用、最大分段大小(MSS/MTU)、最大并发连接数和无通信超时。在网络栈中位于最顶层(传输层),通过 ip_reference 引用下层的 IPv4Setup

网络栈三层结构 GprsSetup/MacAddressSetup (数据链路)→ IPv4Setup (网络)→ TcpUdpSetup (传输),通过各自的对象引用串联成完整通信路径。

Blue Book 属性

编号 名称 类型 说明
1 logical_name octet-string(6) 只读, bytes
2 port uint16 可读写,默认 4059
3 ipReference object-ref 可读写, ip_reference (指向 IPv4Setup
4 maximumSegmentSize uint16 可读写, max_segment_size ,默认 1460
5 maximumSimultaneousConnections uint8 可读写, max_simultaneous_connections ,默认 1
6 inactivityTimeout uint16 ✅读写, inactivity_timeout (秒),默认 120
import dlms

# ── 构建完整网络栈 ──
gprs = dlms.GprsSetup("0.1.25.0.0.255", apn="internet")
ipv4 = dlms.IPv4Setup("0.0.25.1.0.255", datalink_reference=gprs, use_dhcp=True)
tcp  = dlms.TcpUdpSetup("0.0.25.2.0.255", ip_reference=ipv4)

# ── 属性 1: logical_name ──
print(tcp.logical_name)  # b'\x00\x00\x19\x02\x00\xff'

# ── 属性 2: port ──
print(tcp.port)    # 4059(默认 DLMS 端口)
tcp.port = 80    # 可改为其他端口

# ── 属性 3: ip_reference ──
print(tcp.ip_reference)  # <IPv4Setup ...>
tcp.ip_reference = None  # 可清除引用

# ── 属性 4: max_segment_size ──
print(tcp.max_segment_size)  # 1460(默认)
tcp.max_segment_size = 536   # 最小 MTU(IPv4 要求)

# ── 属性 5: max_simultaneous_connections ──
print(tcp.max_simultaneous_connections)  # 1
tcp.max_simultaneous_connections = 3     # 最多 3 个并发客户端

# ── 属性 6: inactivity_timeout ──
print(tcp.inactivity_timeout)  # 120 秒
tcp.inactivity_timeout = 0     # 禁用超时

# ── 钩子 ──
def on_before_write(sender, attr_index, value, context):
    if attr_index == 2:  # port
        print("Changing port to {}".format(value))
    return dlms.DLMSEvent.ALLOW

tcp.on_before_write = on_before_write

构造函数

TcpUdpSetup(logical_name: str, port: int = 4059,
            ip_reference: object = None, max_simultaneous_connections: int = 1,
            inactivity_timeout: int = 0)

构造函数 6 个参数,仅 logical_name 必传。无 access 参数(与 .pyi 声明不同)。无 deinit

参数 类型 编号 默认值 说明
logical_name str 1 必传 OBIS 代码,如 "0.0.25.2.0.255"
port int 2 4059 TCP/UDP 端口号(DLMS 标准端口)
ip_reference DLMS 对象 3 None 下层 IPv4Setup 对象引用。写入时自动 gx_from_mp 取 C 指针
max_segment_size int 4 1460 最大分段大小 / MTU(字节)
max_simultaneous_connections int 5 1 最大并发连接数
inactivity_timeout int 6 120 无通信超时(秒,0 = 不超时)
import dlms

gprs = dlms.GprsSetup("0.1.25.0.0.255", apn="internet")
ipv4 = dlms.IPv4Setup("0.0.25.1.0.255", datalink_reference=gprs, use_dhcp=True)

tcp = dlms.TcpUdpSetup(
    "0.0.25.2.0.255",
    port=4059,
    ip_reference=ipv4,
    max_segment_size=1460,
    max_simultaneous_connections=1,
    inactivity_timeout=120,
)

Python 属性一览

属性 类型 可写 说明
logical_name bytes × OBIS 代码(6 字节)
port int 端口号,默认 4059(DLMS 标准端口)
ip_reference IPv4Setup|None 下层 IP 配置引用,同时存 Python 引用(防 GC)和 C 指针 c_obj.ipSetup
max_segment_size int MSS/MTU 大小(C 层 uint16_t
max_simultaneous_connections int 最大并发连接(C 层 unsigned char ,超出 255 截断)
inactivity_timeout int 无通信超时秒数(C 层 uint16_t
on_before_read Callable 读前钩子
on_after_read Callable 读后钩子
on_before_write Callable 写前钩子
on_after_write Callable 写后钩子
on_before_action Callable 动作前钩子
on_after_action Callable 动作后钩子
access_dict dict 继承自 CosemObject

访问控制

import dlms
from dlms import AccessMode, Authentication

tcp = dlms.TcpUdpSetup("0.0.25.2.0.255")

dlms.set_default_access(dlms.TcpUdpSetup, {
    dlms.Authentication.NONE: {
        2: dlms.AccessMode.READ,     # port: 公开读
        4: dlms.AccessMode.READ,     # max_segment_size
        5: dlms.AccessMode.READ,     # max_simultaneous_connections
        6: dlms.AccessMode.READ,     # inactivity_timeout
    },
    dlms.Authentication.HIGH: {
        2: dlms.AccessMode.READ_WRITE,   # port: HIGH 认证可写
        4: dlms.AccessMode.READ_WRITE,   # max_segment_size
        6: dlms.AccessMode.READ_WRITE,   # inactivity_timeout
    },
})

MacAddressSetup

以太网/蜂窝 MAC 地址配置对象(其 COSEM ID=43 )。定义数据链路层的 MAC 地址(6 字节)。与 GprsSetup 同级属于数据链路层,可通过 IPv4Setup.datalink_reference 引用,构成以太网通信路径。

Blue Book 属性

编号 名称 类型 说明
1 logical_name str OBIS 代码,如 "0.0.25.4.0.255"
2 mac_address bytes None MAC 地址,必须恰好 6 字节(如 b"\x00\x11\x22\x33\x44\x55"

构造函数

MacAddressSetup(logical_name: str, mac_address: bytes = None)
import dlms

mac = dlms.MacAddressSetup("0.0.25.4.0.255")

# ── 属性 1: logical_name ──
print(mac.logical_name)  # b'\x00\x00\x19\x04\x00\xff'

# ── 属性 2: mac_address ──
print(mac.mac_address)   # None(默认未设置)
mac.mac_address = b"\x00\x11\x22\x33\x44\x55"
print(mac.mac_address)   # b'\x00\x11"3DU'

# 十六进制格式化显示
print(":".join(["%02X" % b for b in mac.mac_address]))  # "00:11:22:33:44:55"

# 打印对象(自动显示 MAC)
print(mac)  # <MacAddressSetup ln='0.0.25.4.0.255', mac_address=00:11:22:33:44:55>

# ── 长度校验 ──
# mac.mac_address = b"\x00\x11"          # ValueError: must be 6 bytes
# mac.mac_address = b"\x00" * 7          # ValueError: must be 6 bytes
# mac.mac_address = "00:11:22:33:44:55"  # TypeError: must be bytes

# ── 配合 IPv4Setup 使用(以太网通信路径)──
ipv4 = dlms.IPv4Setup(
    "0.0.25.1.0.255",
    datalink_reference=mac,      # 绑定以太网 MAC
    ip_address="192.168.1.100",
    subnet_mask="255.255.255.0",
    use_dhcp=False,
)

# ── 钩子 ──
def on_before_write(sender, attr_index, value, context):
    if attr_index == 2:  # mac_address
        print(":".join(["{:02X}".format(b) for b in mac.mac_address]))  # "00:11:22:33:44:55"
    return dlms.DLMSEvent.ALLOW

mac.on_before_write = on_before_write

访问控制

import dlms
from dlms import AccessMode, Authentication

mac = dlms.MacAddressSetup("0.0.25.4.0.255")

dlms.set_default_access(dlms.MacAddressSetup, {
    dlms.Authentication.NONE: {
        2: dlms.AccessMode.READ,     # mac_address: 公开读
    },
    dlms.Authentication.HIGH: {
        2: dlms.AccessMode.READ_WRITE,   # mac_address: HIGH 认证可写
    },
})

典型用例

mac = dlms.MacAddressSetup("0.0.25.0.0.255", mac_address=b'\x00\x11\x22\x33\x44\x55')

M-Bus 对象

M-Bus 从机端口配置对象(其 COSEM ID=25 )。当 QuecPython 设备作为 M-Bus 从机/表计 时,向 DLMS 客户端(主站/集中器)暴露自身 M-Bus 端口的物理参数:默认波特率、当前可用波特率、地址分配状态和总线地址。


MbusSlavePortSetup

当 QuecPython 设备 作为 M-Bus 从站/仪表 时使用。暴露设备的 M-Bus 端口参数给 DLMS 客户端(主站/集中器)读取,其 COSEM ID = 25

Blue Book 属性

编号 名称 类型 说明
1 logical_name octet-string(6) OBIS 代码
2 default_baud enum 默认 M-Bus 波特率
3 available_baud enum 当前可用波特率
4 address_state enum 地址分配状态
5 bus_address uint8 M-Bus 主站地址(0–255,有效从站范围 1–250)

构造函数

MbusSlavePortSetup(logical_name: str, access: dict = None)

AddressState 枚举

常量 说明
AddressState.NONE 0 自上次上电后未分配地址
AddressState.ASSIGNED 1 地址已分配(手动或自动)

类名全小写用 dlms.AddressState.NONE ,也可直接用整数 0 / 1

波特率校验

MbusSlavePortSetup 使用 baud_int_to_enum + 往返校验(与 IecHdlcSetup 相同):

import dlms
from dlms import AddressState, AccessMode, Authentication

slave = dlms.MbusSlavePortSetup("0.0.24.9.0.255",
    access={
        5: (AccessMode.READ_WRITE, Authentication.HIGH),  # bus_address 需 HIGH
    })

# ── 属性 1: logical_name ──
print(slave.logical_name)  # b'\x00\x00\x18\t\x00\xff'

# ── 属性 2: default_baud ──
print(slave.default_baud)   # 9600(构造默认)
slave.default_baud = 2400
print(slave.default_baud)   # 2400

# ── 属性 3: available_baud ──
print(slave.available_baud) # 9600
slave.available_baud = 2400
print(slave.available_baud) # 2400

# ── 属性 4: address_state ──
print(slave.address_state)  # 0(NONE,默认)
slave.address_state = AddressState.ASSIGNED  # 或直接用 1

# ── 属性 5: bus_address ──
print(slave.bus_address)    # 0(默认未设置)
slave.bus_address = 1       # 有效从机地址 1–250

# ── 完整典型配置 ──
slave.default_baud   = 9600
slave.available_baud = 9600
slave.address_state  = AddressState.ASSIGNED
slave.bus_address    = 1

# ── 钩子:客户端读前刷新状态 ──
def on_before_read(sender, attr_index, context):
    if attr_index == 4:  # address_state
        sender.address_state = AddressState.ASSIGNED
    return dlms.DLMSEvent.ALLOW

slave.on_before_read = on_before_read

Python 属性一览

属性 类型 可写 说明
logical_name bytes × OBIS 代码(6 字节)
default_baud int 默认波特率(默认 9600)。传入非法值抛 ValueError
available_baud int 当前可用波特率(默认 9600)
address_state int AddressState.NONE (0) / ASSIGNED (1)
bus_address int M-Bus 从机地址(0–255,1–250 为有效从机地址,0/251–255 保留)
on_before_read Callable 读前钩子(继承自 CosemObject)
on_after_read Callable 读后钩子
on_before_write Callable 写前钩子
on_after_write Callable 写后钩子
access_dict dict 实例级访问控制(构造时支持传入)

M-Bus class 25 无 COSEM 动作方法 on_before_action / on_after_action 永远不会被触发。

访问控制

import dlms
from dlms import AccessMode, Authentication

slave = dlms.MbusSlavePortSetup("0.0.24.9.0.255")

dlms.set_default_access(dlms.MbusSlavePortSetup, {
    dlms.Authentication.NONE: {
        2: dlms.AccessMode.READ,     # default_baud: 公开读
        3: dlms.AccessMode.READ,     # available_baud
        4: dlms.AccessMode.READ,     # address_state
        5: dlms.AccessMode.READ,     # bus_address
    },
    dlms.Authentication.HIGH: {
        2: dlms.AccessMode.READ_WRITE,   # default_baud: HIGH 可写
        4: dlms.AccessMode.READ_WRITE,   # address_state
        5: dlms.AccessMode.READ_WRITE,   # bus_address
    },
})

MbusMasterPortSetup

M-Bus 主站端口配置对象(其 COSEM ID=74 )。当 QuecPython 设备作为 M-Bus 主站/集中器 时,向 DLMS 客户端暴露主站端口的通信速率(波特率)。

Blue Book 属性(仅一个)

属性 类型 说明
logical_name octet-string(6) OBIS 代码
comm_speed int 通信速率(300/600/…/115200)

波特率校验

使用 baud_int_to_enum + 往返校验(与 MbusSlavePortSetup 相同):

comm_speed 枚举索引
300 0
600 1
1200 2
2400 3
4800 4
9600 5
19200 6
38400 7
57600 8
115200 9

构造函数

MbusMasterPortSetup(logical_name: str, access: dict = None)
import dlms
from dlms import AccessMode, Authentication

master = dlms.MbusMasterPortSetup("0.0.24.3.0.255")

# ── 属性 1: logical_name ──
print(master.logical_name)  # b'\x00\x00\x18\x03\x00\xff'

# ── 属性 2: comm_speed ──
print(master.comm_speed)    # 9600(默认)
master.comm_speed = 2400
print(master.comm_speed)    # 2400

# 非法值会被拒绝
# master.comm_speed = 12345  # ValueError

# ── 实例级访问控制 ──
master = dlms.MbusMasterPortSetup("0.0.24.3.0.255",
    access={2: (AccessMode.READ_WRITE, Authentication.HIGH)})

# ── 钩子 ──
def on_before_write(sender, attr_index, value, context):
    if attr_index == 2:
        print("Changing M-Bus master baud rate to {}".format(value))
    return dlms.DLMSEvent.ALLOW

master.on_before_write = on_before_write

Python 属性一览

属性 类型 可写 说明
logical_name bytes × OBIS 代码(6 字节)
comm_speed int 波特率(默认 9600),传入非法值抛 ValueError
on_before_read Callable 读前钩子(继承自 CosemObject)
on_after_read Callable 读后钩子
on_before_write Callable 写前钩子
on_after_write Callable 写后钩子
access_dict dict 实例级访问控制(构造时支持传入)

访问控制

import dlms
from dlms import AccessMode, Authentication

master = dlms.MbusMasterPortSetup("0.0.24.3.0.255")

dlms.set_default_access(dlms.MbusMasterPortSetup, {
    dlms.Authentication.NONE: {
        2: dlms.AccessMode.READ,      # comm_speed: 公开读
    },
    dlms.Authentication.HIGH: {
        2: dlms.AccessMode.READ_WRITE,  # comm_speed: HIGH 可写
    },
})

MbusPortSetup

M-Bus 端口配置对象(其 COSEM ID=76 )。描述 M-Bus 主站所连接的 单个从机端口 的完整通信参数:从机标识(地址/ID/厂商/版本/设备类型)、通信配置(数据头类型/PDU 大小)和监听窗口。DLMS 客户端通过它了解 M-Bus 从机的物理能力和当前状态。

MbusSlavePortSetup / MbusMasterPortSetup 的区别

  • MbusSlavePortSetup (25): 从机自己 暴露自身端口
  • MbusMasterPortSetup (74): 主站 暴露自己的通信速率
  • MbusPortSetup (76): 主站视角 描述某个从机端口的完整档案(相当于主站为每个从机建一份配置记录),通常配合 MbusClient (72) 使用

Blue Book 属性

编号 名称 说明
1 logical_name OBIS 代码
2 profile_selection 关联的 ProfileGeneric OBIS(⏩ COMPLEX)
3 port_communication_status 端口状态
4 data_header_type 数据头类型
5 primary_address 主站地址(0–255)
6 identification_number 标识号(只读)
7 manufacturer_id 厂家 ID(2 字节,只读)
8 mbus_version 协议版本(只读)
9 device_type 设备类型(只读)
10 max_pdu_size 最大 PDU 大小
11 listening_window 监听窗口列表 [[start_tuple, end_tuple], ...] (⏩ COMPLEX)

构造函数

MbusPortSetup(logical_name: str, access: dict = None)

枚举常量

port_communication_status DLMS_MBUS_PORT_COMMUNICATION_STATE ):

说明
0 NO_ACCESS — 无访问权限
1 TEMPORARY_NO_ACCESS — 临时无访问
2 LIMITED_ACCESS — 受限访问
3 UNLIMITED_ACCESS — 完全访问
4 WMBUS — 无线 M-Bus

data_header_type DLMS_MBUS_DATA_HEADER_TYPE ):

说明
0 NONE — 不使用数据头
1 SHORT — 短数据头
2 LONG — 长数据头

device_type DLMS_MBUS_METER_TYPE ):

表计类型 表计类型
0 OTHER 8 HEAT_COST_ALLOCATOR
1 OIL 10 GAS_MODE2
2 ENERGY 11 HEAT_MODE2
3 GAS 12 HOT_WATER_MODE2
4 HEAT 13 WATER_MODE2
5 STEAM 14 HEAT_COST_ALLOCATOR_MODE2
6 HOT_WATER 0x0F UNKNOWN
7 WATER
import dlms
A = dlms.ANY

port = dlms.MbusPortSetup("0.0.24.7.0.255")

# ── 属性 1: logical_name ──
print(port.logical_name)  # b'\x00\x00\x18\x07\x00\xff'

# ── 属性 2: profile_selection ──
port.profile_selection = "0.0.24.1.0.255"   # 关联的 profile OBIS
print(port.profile_selection)               # "0.0.24.1.0.255"

# ── 属性 3/4: 状态与头类型 ──
print(port.port_communication_status)  # 0 (NO_ACCESS)
port.port_communication_status = 3     # UNLIMITED_ACCESS
port.data_header_type = 2              # LONG 数据头

# ── 属性 5–9: 从机标识 ──
port.primary_address = 1
port.identification_number = 12345678
port.manufacturer_id = 0x1234
port.mbus_version = 1
port.device_type = 3                   # GAS 气表

# ── 属性 10: max_pdu_size ──
port.max_pdu_size = 240

# ── 属性 11: listening_window ──
port.listening_window = [
    [(A, A, A,  8, 0, 0), (A, A, A, 18, 0, 0)],   # 每天 8:00–18:00
    [(A, A, A, 20, 0, 0), (A, A, A, 22, 0, 0)],   # 每天 20:00–22:00
]
for start, end in port.listening_window:
    print(start, end)

# ── 配合 MbusClient 使用 ──
client = dlms.MbusClient("0.0.24.1.0.255", mbus_port=port)
client.capture_period = 900   # 15 分钟采集一次

Python 属性一览

属性 类型 可写 说明
logical_name bytes × OBIS 代码(6 字节)
profile_selection str 关联 profile 的 OBIS 代码(6 字节),读写用 "A.B.C.D.E.F" 字符串格式
port_communication_status int 端口通信状态(见下方枚举)
data_header_type int 数据头类型:0=None, 1=Short, 2=Long
primary_address int 从机主地址(0–255)
identification_number int 从机标识号(uint32)
manufacturer_id int 2 字节厂商码(uint16)
mbus_version int M-Bus 协议版本
device_type int 表计类型(见下方枚举)
max_pdu_size int 最大 PDU 大小(字节)
listening_window list[list] [[start_6tuple, end_6tuple], ...] 监听窗口列表
on_before_read Callable 读前钩子
on_after_read Callable 读后钩子
on_before_write Callable 写前钩子
on_after_write Callable 写后钩子
access_dict dict 实例级访问控制

访问控制

import dlms
from dlms import AccessMode, Authentication

port = dlms.MbusPortSetup("0.0.24.7.0.255")

dlms.set_default_access(dlms.MbusPortSetup, {
    dlms.Authentication.NONE: {
        5: dlms.AccessMode.READ,      # primary_address: 公开读
        6: dlms.AccessMode.READ,      # identification_number
        9: dlms.AccessMode.READ,      # device_type
    },
    dlms.Authentication.HIGH: {
        2: dlms.AccessMode.READ_WRITE,   # profile_selection
        5: dlms.AccessMode.READ_WRITE,   # primary_address: HIGH 可写
        11: dlms.AccessMode.READ_WRITE,  # listening_window
    },
})

MbusClient

M-Bus 客户端对象(其 COSEM ID=72 )。描述 M-Bus 主站所连接的单个从机表计 ——定义从机的标识(地址/ID/厂商/设备类型)、采集配置(周期 + 采集记录定义)和当前状态(访问号/状态/告警/配置字/密钥状态)。每个从机表对应一个 MbusClient 实例。

MbusPortSetup (76)              MbusDiagnostic (77)
    ▲  mbus_port                    ▲ 每通道一个
    │  ┌─────────────────────────────────────────┐
    ├──┤  MbusClient (72) — 每个从机表一个        │
    │  │  capture_definition / capture_period    │
    │  │  identification / status / alarm        │
    │  └─────────────────────────────────────────┘
    │        8 个方法 ← on_before_action 钩子实现
    ▼
MbusMasterPortSetup (74) — 主站自身波特率

⚠️ 与 dlms.Client 完全是两回事 dlms.MbusClient 数据模型对象 (CosemObject 子类,放进服务器对象模型供远程读取); dlms.Client 通信引擎 (主动连远端 DLMS 设备执行 read/write)。

Blue Book 属性

编号 名称 类型 说明
1 logical_name octet-string(6) 只读, bytes
2 mBusPort object-ref 可读写, mbus_port (指向 MbusPortSetup
3 captureDefinition array 可读写, capture_definition (bytes, bytes) 键值对列表)
4 capturePeriod uint32 可读写, capture_period (秒)
5 primaryAddress uint8 可读写, primary_address
6 identificationNumber uint32 读写(attr 标记只读,但 Python setter 存在), identification_number
7 manufacturerID uint16 读写, manufacturer_id
8 dataHeaderVersion uint8 读写, version
9 deviceType uint8 读写, device_type
10 accessNumber uint8 读写, access_number (VOLATILE)
11 status uint8 读写, status (VOLATILE)
12 alarm uint8 读写, alarm (VOLATILE)
13 configuration uint16 可读写, configuration
14 encryptionKeyStatus enum 可读写, encryption_key_status

属性 6–12、14 在 attr_table 标记 READONLY / VOLATILE (供 DLMS 客户端语义使用),但 Python setter 全部存在 ——因为数据是 Python 钩子从 M-Bus 硬件采回来后写入的。

import dlms
from dlms import AccessMode, Authentication

# ── 关联端口 ──
port = dlms.MbusPortSetup("0.0.24.7.0.255")
port.primary_address = 1

client = dlms.MbusClient("0.0.24.1.0.255",
    mbus_port=port,
    access={
        dlms.Authentication.NONE: {2: dlms.AccessMode.READ_WRITE,
                                    3: dlms.AccessMode.READ_WRITE,
                                    4: dlms.AccessMode.READ_WRITE},
    })

# ── 属性 2: mbus_port ──
print(client.mbus_port)         # <MbusPortSetup ...>
client.mbus_port = None         # 可解除
client.mbus_port = port         # 可恢复

# ── 属性 3: capture_definition ──
client.capture_definition = [
    (b'\x02\x04', b'\x09\x06'),   # (数据记录key, 值key)
    (b'\x01\x02\x03', b'\x01'),
]
for key, val in client.capture_definition:
    print(key, val)   # b'\x02\x04' b'\x09\x06' ...

# ── 属性 4: capture_period ──
client.capture_period = 900   # 15 分钟

# ── 属性 5: primary_address ──
client.primary_address = 1

# ── 属性 6–9: 从机标识 ──
client.identification_number = 12345678
client.manufacturer_id = 0x4D41   # 'MA'
client.version = 1
client.device_type = 3            # GAS

# ── 属性 10–12: 运行状态 ──
client.access_number = 5          # 采集后递增
client.status = 0x05
client.alarm = 0x01

# ── 属性 13/14 ──
client.configuration = 0x0100
client.encryption_key_status = 2  # KEY_INUSE

# ── 采集动作(核心)──
def on_capture(self, event):
    if event.index == 3:  # capture 方法
        # 真实 M-Bus 硬件读取从这里开始
        # data = mbus_read(client.primary_address, client.capture_definition)
        # client.status = ...
        # client.access_number += 1
        return True
    return True

client.on_before_action = on_capture

构造函数

MbusClient(logical_name: str, mbus_port: object = None, access: dict = None)
import dlms
from dlms import AccessMode, Authentication

# ── 先创建关联端口 ──
port = dlms.MbusPortSetup("0.0.24.7.0.255")
port.primary_address = 1

# ── 实例化 MbusClient ──
client = dlms.MbusClient(
    "0.0.24.1.0.255",        # logical_name: OBIS 代码(必传)
    mbus_port=port,          # 关联 MbusPortSetup(可省略,之后用 client.mbus_port = port 补)
    access={                 # 实例级权限(可省略)
        2: (AccessMode.READ,        Authentication.NONE),   # mbus_port
        3: (AccessMode.READ_WRITE,  Authentication.HIGH),   # capture_definition
        4: (AccessMode.READ_WRITE,  Authentication.HIGH),   # capture_period
    },
)
参数 类型 编号 默认值 说明
logical_name str 1 必传 OBIS 代码,如 "0.0.24.1.0.255"
mbus_port DLMS 对象 2 None 关联的 MbusPortSetup (或任意 DLMS 对象),写入时自动 gx_from_mp 取 C 指针
access dict None 实例级权限(无校验)

Python 属性一览

属性 类型 可写 说明
logical_name bytes × OBIS 代码(6 字节)
mbus_port MbusPortSetup|None 端口引用,双存(Python 引用 + C 指针 c_obj.mBusPort )。读回时优先 Python 引用,否则 find_python_object_by_c_ptr 反向查找
capture_definition list[(bytes,bytes)] 采集记录定义: (data_key_bytes, value_key_bytes) 对列表
capture_period int 采集周期(秒)
primary_address int 从机主地址
identification_number int 从机标识号
manufacturer_id int 2 字节厂商码
version int 数据头版本
device_type int 表计类型
access_number int 访问号计数器(每次通信递增)
status int 从机状态字节
alarm int 从机告警字节
configuration int 2 字节配置字
encryption_key_status int 加密密钥状态( DLMS_MBUS_ENCRYPTION_KEY_STATUS
on_before_read Callable 读前钩子
on_after_read Callable 读后钩子
on_before_action Callable 动作前钩子——8 个方法的实现入口
on_after_action Callable 动作后钩子
access_dict dict 实例级访问控制

方法(全部通过 on_before_action 派发)

序号 方法名 说明
1 slave_install 安装从站
2 slave_deinstall 卸载从站
3 capture 捕获数据(触发测量值更新)
4 reset_alarm 清零告警
5 synchronize_clock 同步时钟
6 data_send 发送数据
7 set_encryption_key 设置加密密钥
8 transfer_key 传输密钥

所有方法需要 on_before_action handler 实现实际的 M-Bus 物理通信。

def on_action(self, event):
    method_names = {1: "slave_install", 2: "slave_deinstall", 3: "capture",
                    4: "reset_alarm", 5: "synchronize_clock", 6: "data_send",
                    7: "set_encryption_key", 8: "transfer_key"}
    if event.index == 3:  # capture
        # TODO: 调用 M-Bus 硬件 API 读取从机数据,写入本对象属性
        return True   # 成功
    return False  # 其他方法未实现 → 拒绝

client.on_before_action = on_action

访问控制

import dlms
from dlms import AccessMode, Authentication

client = dlms.MbusClient("0.0.24.1.0.255")

dlms.set_default_access(dlms.MbusClient, {
    dlms.Authentication.NONE: {
        2: dlms.AccessMode.READ,      # mbus_port
        4: dlms.AccessMode.READ,      # capture_period
    },
    dlms.Authentication.HIGH: {
        3: dlms.AccessMode.READ_WRITE,   # capture_definition
        4: dlms.AccessMode.READ_WRITE,   # capture_period
        5: dlms.AccessMode.READ_WRITE,   # primary_address
    },
    # 方法(1–8)在方法索引下配置:
    # dlms.Authentication.HIGH: {1: dlms.AccessMode.AUTHENTICATED_WRITE, ...}
})

MbusDiagnostic

M-Bus 通道诊断对象(其 COSEM ID=77 )。监控 单个 M-Bus 通信通道 的链路质量:信号强度、通道 ID、链路状态、广播帧计数、收发帧统计和最后采集时间。通常与 MbusClient 搭配——每个通道一个 MbusDiagnostic

类比 GsmDiagnostic GsmDiagnostic 是蜂窝网络的"信号仪表盘", MbusDiagnostic 是 M-Bus 总线/无线通道的"信号仪表盘"——都是被动数据载体,数值靠钩子从硬件刷新。

Blue Book 属性

编号 名称 类型 说明
1 logical_name octet-string(6) 只读, bytes
2 receivedSignalStrength uint8 可读写, received_signal_strength (dBm/dBμV)
3 channelId uint8 可读写, channel_id
4 linkStatus enum 可读写, link_status DLMS_MBUS_LINK_STATUS
5 broadcastFrames array 可读写, broadcast_frames (dict 列表)
6 transmissions uint32 可读写, transmissions
7 receivedFrames uint32 可读写, received_frames (校验正确)
8 failedReceivedFrames uint32 可读写, failed_received_frames (校验错误)
9 captureTime structure 可读写, capture_time (dict)

构造函数有 access 参数(无校验),CosemObject 子类,无 deinit broadcastFrames arr_clear 管理, captureTime 嵌入式)。

import dlms
from dlms import AccessMode, Authentication

diag = dlms.MbusDiagnostic("0.0.24.8.0.255")

# ── 属性 1: logical_name ──
print(diag.logical_name)  # b'\x00\x00\x18\x08\x00\xff'

# ── 属性 2: received_signal_strength ──
print(diag.received_signal_strength)  # 0(默认)
diag.received_signal_strength = 100   # dBμV

# ── 属性 3: channel_id ──
diag.channel_id = 1

# ── 属性 4: link_status ──
print(diag.link_status)   # 0 (NONE)
diag.link_status = 1      # NORMAL

# ── 属性 5: broadcast_frames ──
diag.broadcast_frames = [
    {"client_id": 3, "counter": 7, "timestamp": (-1, -1, -1, 8, 0, 0)},
    {"client_id": 5, "counter": 20, "timestamp": None},
]
for f in diag.broadcast_frames:
    print(f["client_id"], f["counter"], f["timestamp"])

# ── 属性 6–8: 收发帧统计 ──
diag.transmissions = 0
diag.received_frames = 0
diag.failed_received_frames = 0

# ── 属性 9: capture_time ──
diag.capture_time = {"attribute_id": 2, "timestamp": (2026, 3, 6, 12, 0, 0)}
print(diag.capture_time["attribute_id"])  # 2
print(diag.capture_time["timestamp"])     # (2026, 3, 6, 12, 0, 0)

# ── 钩子:读前刷新信号强度(从硬件)──
def on_before_read(sender, attr_index, context):
    if attr_index == 2:  # received_signal_strength
        # sender.received_signal_strength = mbus_get_rssi()
        pass
    return dlms.DLMSEvent.ALLOW

diag.on_before_read = on_before_read

# ── 钩子:reset 动作 ──
def on_reset(sender, event):
    if event.index == 1:
        print("Reset counters requested")
        event.handled = 0   # 让 Gurux 清 C 侧计数器
        return True
    return True

diag.on_before_action = on_reset

构造函数

MbusDiagnostic(logical_name: str, access: dict = None)
参数 类型 编号 默认值 说明
logical_name str 1 必传 OBIS 代码,如 "0.0.24.8.0.255"
access dict None 实例级权限, {attr_index: (AccessMode, Authentication)} (无校验)
import dlms
from dlms import AccessMode, Authentication

diag = dlms.MbusDiagnostic(
    "0.0.24.8.0.255",                       # logical_name(必传)
    access={                                 # 实例级权限(可选)
        2: (AccessMode.READ,       Authentication.NONE),   # received_signal_strength
        3: (AccessMode.READ,       Authentication.NONE),   # channel_id
        4: (AccessMode.READ,       Authentication.NONE),   # link_status
        5: (AccessMode.READ_WRITE, Authentication.HIGH),   # broadcast_frames
        6: (AccessMode.READ,       Authentication.NONE),   # transmissions
        7: (AccessMode.READ,       Authentication.NONE),   # received_frames
        8: (AccessMode.READ,       Authentication.NONE),   # failed_received_frames
        9: (AccessMode.READ,       Authentication.NONE),   # capture_time
        1: (AccessMode.AUTHENTICATED_WRITE, Authentication.HIGH),  # reset 方法
    },
)

Python 属性一览

属性 类型 可写 说明
logical_name bytes × OBIS 代码(6 字节)
received_signal_strength int 接收信号强度(dBm / dBμV)
channel_id int 当前使用的通道 ID
link_status int 链路状态(见枚举表)
broadcast_frames list[dict] [{"client_id": int, "counter": int, "timestamp": 6tuple}, ...]
transmissions int 已发送帧数(uint32)
received_frames int 校验正确的接收帧数(uint32)
failed_received_frames int 校验错误的接收帧数(uint32)
capture_time dict {"attribute_id": int, "timestamp": 6tuple}
on_before_read Callable 读前钩子(刷新信号强度等)
on_after_read Callable 读后钩子
on_before_action Callable 动作前钩子——method 1 reset 实现入口
on_after_action Callable 动作后钩子
access_dict dict 实例级访问控制

link_status 枚举(DLMS_MBUS_LINK_STATUS)

说明
0 NONE — 从未收到数据
1 NORMAL — 正常
2 TEMPORARILY_INTERRUPTED — 临时中断
3 PERMANENTLY_INTERRUPTED — 永久中断

broadcast_frames 格式

# 每项为 dict,timestamp 是 6 元组(-1 表示通配)
diag.broadcast_frames = [
    {"client_id": 3, "counter": 7, "timestamp": (-1, -1, -1, 8, 0, 0)},
    {"client_id": 5, "counter": 20, "timestamp": None},   # timestamp 可省略(None)
]

capture_time 格式

# 表示"最后一次状态变化的时间"
diag.capture_time = {"attribute_id": 2, "timestamp": (2026, 3, 6, 12, 0, 0)}
方法号 名称 说明
1 reset 复位计数器。 默认行为是 no-op ,实际清零逻辑写在 on_before_action 钩子中;除非你 event.handled = 1 阻止,否则 Gurux 也会清 C 侧计数器
def on_reset(self, event):
    if event.index == 1:  # reset
        # 这里复位硬件计数器
        event.handled = 0   # 0=让 Gurux 也清 C 侧计数器(默认);1=阻止默认
        return True
diag.on_before_action = on_reset

访问控制

import dlms
from dlms import AccessMode, Authentication

diag = dlms.MbusDiagnostic("0.0.24.8.0.255")

dlms.set_default_access(dlms.MbusDiagnostic, {
    dlms.Authentication.NONE: {
        2: dlms.AccessMode.READ,      # received_signal_strength
        3: dlms.AccessMode.READ,      # channel_id
        4: dlms.AccessMode.READ,      # link_status
        6: dlms.AccessMode.READ,      # transmissions
        7: dlms.AccessMode.READ,      # received_frames
        8: dlms.AccessMode.READ,      # failed_received_frames
    },
    dlms.Authentication.HIGH: {
        1: dlms.AccessMode.AUTHENTICATED_WRITE,  # reset() 方法需 HIGH 认证
        5: dlms.AccessMode.READ_WRITE,           # broadcast_frames
    },
})

G3-PLC 对象(ITU-T G.9903)

以下三个对象提供 G3-PLC 窄带电力线通信的 COSEM 属性结构。 重点: 与 M-Bus 对象一样,这些目前为纯数据持有者—— dlms 模块暴露 COSEM 属性结构供 DLMS 客户端读取配置和统计数据, 不实现 G3-PLC 传输本身 。应用层负责通过 GenericConnection 挂接 G3-PLC 调制解调器进行物理层通信,并将从传输层获取的数据填充到这些 COSEM 对象中。

注意: 当前版本中 G3-PLC 动作方法没有 C 层派发,所有动作必须通过 on_before_action handler 实现。


G3PlcMacCounters

G3-PLC MAC 层计数器对象(其 COSEM ID=90 )。记录设备在 G3-PLC(ITU-T G.9903)电力线通信网络中的 MAC 层数据包统计:收发数据包/命令包数量、CSMA 冲突失败、无 ACK、坏 CRC、广播收发计数。全部为 uint32_t 无符号计数器。

Blue Book 属性

编号 名称 类型 说明
1 logical_name octet-string(6) OBIS 代码
2 tx_data_packet_count uint32 发送数据包总数
3 rx_data_packet_count uint32 接收数据包总数
4 tx_cmd_packet_count uint32 发送命令包总数
5 rx_cmd_packet_count uint32 接收命令包总数
6 csma_fail_count uint32 CSMA 失败次数
7 csma_no_ack_count uint32 CSMA 未收到 ACK 次数
8 bad_crc_count uint32 坏 CRC 帧数
9 tx_data_broadcast_count uint32 发送广播数据包数
10 rx_data_broadcast_count uint32 接收广播数据包数

构造函数

G3PlcMacCounters(logical_name: str, access: dict = None)
import dlms
from dlms import AccessMode, Authentication

counters = dlms.G3PlcMacCounters("0.0.29.1.0.255")

# ── 属性 1: logical_name ──
print(counters.logical_name)  # b'\x00\x00\x1d\x01\x00\xff'

# ── 属性 2–5: 数据/命令包统计 ──
counters.tx_data_packet_count = 100
counters.rx_data_packet_count = 98
counters.tx_cmd_packet_count = 20
counters.rx_cmd_packet_count = 19

# ── 属性 6–8: 通信质量指标 ──
counters.csma_fail_count = 3      # 信道冲突
counters.csma_no_ack_count = 1    # 无 ACK
counters.bad_crc_count = 0        # 坏帧

# ── 属性 9/10: 广播统计 ──
counters.tx_data_broadcast_count = 5
counters.rx_data_broadcast_count = 4

# ── 读前钩子:从 PHY 同步计数 ──
def on_before_read(sender, attr_index, context):
    # 从 G3-PLC 调制解调器寄存器读取并更新
    # sender.tx_data_packet_count = plc_get_counter(0x01)
    pass
    return dlms.DLMSEvent.ALLOW
counters.on_before_read = on_before_read

# ── reset 动作钩子(唯一实现)──
def do_reset(sender, event):
    if event.index == 1:
        sender.tx_data_packet_count = 0
        sender.rx_data_packet_count = 0
        sender.tx_cmd_packet_count = 0
        sender.rx_cmd_packet_count = 0
        sender.csma_fail_count = 0
        sender.csma_no_ack_count = 0
        sender.bad_crc_count = 0
        sender.tx_data_broadcast_count = 0
        sender.rx_data_broadcast_count = 0
        # plc_reset_counters()  ← 硬件清零
        return True
    return True
counters.on_before_action = do_reset

Python 属性一览

属性 类型 可写 说明
logical_name bytes × OBIS 代码(6 字节)
tx_data_packet_count int 成功发送的数据包数
rx_data_packet_count int 成功接收的数据包数
tx_cmd_packet_count int 成功发送的命令包数
rx_cmd_packet_count int 成功接收的命令包数
csma_fail_count int CSMA 退避达到 macMaxCSMABackoffs 的次数(信道冲突失败)
csma_no_ack_count int 发送单播数据帧未收到 ACK 的次数
bad_crc_count int 接收到的坏 CRC 帧数
tx_data_broadcast_count int 发送的广播帧数
rx_data_broadcast_count int 成功接收的广播帧数
on_before_read Callable 读前钩子(从 PHY 同步计数)
on_after_read Callable 读后钩子
on_before_action Callable 动作前钩子——method 1 reset 的唯一实现
on_after_action Callable 动作后钩子
access_dict dict 实例级访问控制

访问控制

import dlms
from dlms import AccessMode, Authentication

counters = dlms.G3PlcMacCounters("0.0.29.1.0.255")

dlms.set_default_access(dlms.G3PlcMacCounters, {
    dlms.Authentication.NONE: {
        2: dlms.AccessMode.READ,   # tx_data_packet_count
        3: dlms.AccessMode.READ,   # rx_data_packet_count
        4: dlms.AccessMode.READ,   # tx_cmd_packet_count
        5: dlms.AccessMode.READ,   # rx_cmd_packet_count
        6: dlms.AccessMode.READ,   # csma_fail_count
        7: dlms.AccessMode.READ,   # csma_no_ack_count
        8: dlms.AccessMode.READ,   # bad_crc_count
        9: dlms.AccessMode.READ,   # tx_data_broadcast_count
        10: dlms.AccessMode.READ,  # rx_data_broadcast_count
    },
    dlms.Authentication.HIGH: {
        1: dlms.AccessMode.AUTHENTICATED_WRITE,  # reset() 方法需 HIGH 认证
    },
})

G3PlcMacSetup

G3-PLC MAC 层配置对象(其 COSEM ID=91 )。配置 G3-PLC(ITU-T G.9903)电力线通信设备的 MAC 层参数:网络身份(短地址/PAN ID/协调器)、加密密钥表、CSMA 退避参数、邻居表、MAC 位置表和帧重试等。 这是 G3-PLC 家族中属性最多的对象 (25 个属性)。

数据流 :G3-PLC MAC 层由调制解调器硬件实现,这些参数需要下发到 PHY/MAC 固件生效。 Gurux 只存储不执行 ——真实配置生效逻辑(写寄存器)需在钩子或应用层完成。

Blue Book 核心属性

编号 名称 类型 说明
1 logical_name octet-string(6) OBIS 代码
2 short_address uint16 本节点 MAC 短地址
3 rc_coord uint8 到协调器的路由成本
4 pan_id uint16 PAN 标识符
5 key_table ARRAY 密钥表 [{"id": int, "key": bytes(16)}, ...] (⏩ COMPLEX)
6 frame_counter uint32 帧计数器
7 tone_mask bytes 活跃子载波 packed bit-array
8 tmr_ttl uint8 TMR 生命周期
9 max_frame_retries uint8 最大帧重传次数
10 neighbour_table_entry_ttl uint8 邻区条目生命周期(秒)
11 neighbour_table ARRAY 邻区表 [{short_address, lqi, valid_time, ...}, ...] (⏩ COMPLEX)
12 high_priority_window_size int 高优先级窗口大小
13 cscm_fairness_limit int CSCM 公平性限制
14 beacon_randomization_window_length int 信标随机化窗口长度
15/16 mac_a / mac_k int MAC 层 A/K 参数
17 min_cw_attempts int 最小竞争窗口尝试次数
18 cenelec_legacy_mode int CENELEC 兼容模式
19 fcc_legacy_mode int FCC 兼容模式
20 max_be int 最大退避指数
21 max_csma_backoffs int 最大 CSMA 退避次数
22 min_be int 最小退避指数
23 mac_broadcast_max_cw_enabled int 广播最大竞争窗口开关(bool)
24 mac_transmit_atten int 输出衰减(dB)
25 mac_pos_table list[dict] [{"short_address", "lqi", "valid_time"}]
26 mac_duplicate_detection_ttl int 重复帧检测时间(秒)

构造函数

G3PlcMacSetup(logical_name: str, access: dict = None)
import dlms
from dlms import AccessMode, Authentication

mac = dlms.G3PlcMacSetup("0.0.29.0.0.255")

# ── 属性 2–4: 网络身份 ──
mac.short_address = 0x1001
mac.rc_coord = 0x0000
mac.pan_id = 0x1234

# ── 属性 5: key_table ──
mac.key_table = [
    {"id": 1, "key": b"\x01\x02\x03\x04\x05\x06\x07\x08"},
    {"id": 2, "key": b"\x11\x12\x13\x14\x15\x16\x17\x18"},
]
for entry in mac.key_table:
    print(entry["id"], entry["key"])

# ── 属性 6: frame_counter ──
mac.frame_counter = 0

# ── 属性 7: tone_mask(bytes)──
mac.tone_mask = b"\xff\xff"   # 全频段启用

# ── 属性 8–14: 时序参数 ──
mac.tmr_ttl = 60
mac.max_frame_retries = 3
mac.neighbour_table_entry_ttl = 600
mac.high_priority_window_size = 2
mac.cscm_fairness_limit = 1
mac.beacon_randomization_window_length = 2

# ── 属性 15–24: CSMA 与模式 ──
mac.mac_a = 1
mac.mac_k = 8
mac.min_cw_attempts = 4
mac.cenelec_legacy_mode = 0
mac.fcc_legacy_mode = 0
mac.max_be = 8
mac.max_csma_backoffs = 4
mac.min_be = 3
mac.mac_broadcast_max_cw_enabled = 0
mac.mac_transmit_atten = 0

# ── 属性 25: mac_pos_table ──
mac.mac_pos_table = [
    {"short_address": 0x1002, "lqi": 200, "valid_time": 300},
]

# ── 属性 26: mac_duplicate_detection_ttl ──
mac.mac_duplicate_detection_ttl = 30

# ── 属性 11: neighbour_table ──
mac.neighbour_table = [{
    "short_address": 0x1002,
    "payload_modulation_scheme": 1,
    "tone_map": b"\xff",
    "modulation": 1,
    "tx_gain": 0,
    "tx_res": 0,
    "tx_coeff": b"\x01\x02",
    "lqi": 200,
    "phase_differential": 0,
    "tmr_valid_time": 50,
    "no_data": 0,
}]
for n in mac.neighbour_table:
    print(n["short_address"], n["lqi"])

# ── get_neighbour_table 动作钩子(唯一实现)──
def on_get_neighbour(self, event):
    if event.index == 1:
        # 从 G3-PLC 硬件读取真实邻居表,写入 mac.neighbour_table
        # self.neighbour_table = plc_get_neighbours()
        return True
    return True
mac.on_before_action = on_get_neighbour

Python 属性一览(钩子回调)

属性 类型 可写 说明
on_before_read Callable 读前钩子
on_after_read Callable 读后钩子
on_before_action Callable 动作前钩子——method 1 get_neighbour_table 的唯一实现
on_after_action Callable 动作后钩子
access_dict dict 实例级访问控制

邻区表应在客户端读取前从 G3-PLC 调制解调器刷新。

访问控制

import dlms
from dlms import AccessMode, Authentication

mac = dlms.G3PlcMacSetup("0.0.29.0.0.255")

dlms.set_default_access(dlms.G3PlcMacSetup, {
    dlms.Authentication.NONE: {
        2: dlms.AccessMode.READ,   # short_address
        4: dlms.AccessMode.READ,   # pan_id
        6: dlms.AccessMode.READ,   # frame_counter
    },
    dlms.Authentication.HIGH: {
        2: dlms.AccessMode.READ_WRITE,   # short_address: HIGH 可写
        5: dlms.AccessMode.READ_WRITE,   # key_table: HIGH 可写(密钥安全)
        7: dlms.AccessMode.READ_WRITE,   # tone_mask
        1: dlms.AccessMode.AUTHENTICATED_WRITE,  # get_neighbour_table 方法
    },
})

G3Plc6LoWPAN

G3-PLC 6LoWPAN 适配层配置对象(其 COSEM ID=92 )。管理 G3-PLC(ITU-T G.9903)网络中节点的 6LoWPAN/LOADng 路由配置 :最大跳数、弱链路阈值、安全级别、前缀表、路由配置/路由表、上下文信息表、黑名单表、广播日志表、组表、目的地址表和 LQI 阈值。

协议栈位置 G3PlcMacSetup (91) 管 MAC 层(CSMA/帧重试), G3Plc6LoWPAN (92) 管其上的 6LoWPAN 适配层 (IPv6 压缩 + LOADng 路由)。每个字段都有对应的 PIB 属性(0x02–0xF0),最终写入 G3-PLC 调制解调器寄存器生效。

Blue Book 核心属性

编号 名称 类型 说明
1 logical_name octet-string(6) OBIS 代码
2 max_hops uint8 最大 LOADng 路由跳数(PIB 0x02)
3 weak_lqi_value uint8 "弱链路"的 LQI 阈值(PIB 0x04 低阈值)
4 security_level uint8 适配帧最低安全等级
5 prefix_table bytes PAN 前缀列表(⏩ COMPLEX)
6 routing_configuration ARRAY LOADng 路由参数,14 字段 dict(⏩ COMPLEX)
7 broadcast_log_table_entry_ttl uint16 广播日志 TTL(分钟)
8 routing_table ARRAY LOADng 路由表 [{destination, next_hop, cost, hop_count, weak_link_count, valid_time}] (⏩ COMPLEX)
9 context_information_table ARRAY 6LoWPAN 上下文信息 [{cid, context_length, context, compression, valid_lifetime}] (⏩ COMPLEX)
10 blacklist_table ARRAY 黑名单邻区 [{neighbour_address, valid_time}] (⏩ COMPLEX)
11 broadcast_log_table ARRAY 广播日志 [{source_address, sequence_number, valid_time}] (⏩ COMPLEX)
12 group_table ARRAY 本设备注册的组地址(uint16 列表)(⏩ COMPLEX)
13 max_join_wait_time uint16 网络加入超时(秒,LBD,PIB 0x20)
14 path_discovery_time uint8 路径发现超时(秒,PIB 0x21)
15 active_key_index uint8 活跃 GMK 密钥索引(PIB 0x22)
16 metric_type uint8 LOADng 路由度量类型(PIB 0x03)
17 coord_short_address uint16 协调器短地址(PIB 0x08)
18 disable_default_routing uint8 1=禁用 LOADng 默认路由(PIB 0xF0)
19 device_type uint8 设备类型( DLMS_PAN_DEVICE_TYPE ,PIB 0x10)
20 default_coord_route_enabled uint8 1=创建到协调器的默认路由(PIB 0x24)
21 destination_address ARRAY 本路由器提供连通性的地址列表(uint16)(⏩ COMPLEX,PIB 0x23)
22 low_lqi uint8 低 LQI 阈值(PIB 0x04)
23 high_lqi uint8 高 LQI 阈值(PIB 0x04)

22 个属性全暴露 (编号 2–23)。构造函数有 access 参数(无校验),CosemObject 子类。 无 COSEM 动作方法 (class 92 未定义任何 method, on_before_action / on_after_action 不触发)。

构造函数

G3Plc6LoWPAN(logical_name: str, access: dict = None)
import dlms
from dlms import AccessMode, Authentication

pan = dlms.G3Plc6LoWPAN("0.0.29.2.0.255")

# ── 属性 1: logical_name ──
print(pan.logical_name)  # b'\x00\x00\x1d\x02\x00\xff'

# ── 属性 2–4: 基本参数 ──
pan.max_hops = 8
pan.weak_lqi_value = 150
pan.security_level = 5

# ── 属性 5: prefix_table ──
pan.prefix_table = b"\x20\x01\x0d\xb8"  # 前缀字节

# ── 属性 6: routing_configuration(14 字段)──
pan.routing_configuration = [{
    "net_traversal_time": 30,
    "routing_table_entry_ttl": 300,
    "kr": 3, "km": 2, "kc": 3, "kq": 2, "kh": 3, "krt": 2,
    "rreq_retries": 3,
    "rreq_req_wait": 5,
    "blacklist_table_entry_ttl": 600,
    "unicast_rreq_gen_enable": 1,
    "rlc_enabled": 1,
    "add_rev_link_cost": 1,
}]

# ── 属性 7/13–18: 时序与路由 ──
pan.broadcast_log_table_entry_ttl = 30
pan.max_join_wait_time = 60
pan.path_discovery_time = 5
pan.active_key_index = 1
pan.metric_type = 0
pan.coord_short_address = 0x0000
pan.disable_default_routing = 0
pan.device_type = 2
pan.default_coord_route_enabled = 1

# ── 属性 8: routing_table(6 字段)──
pan.routing_table = [{
    "destination_address": 0x1002,
    "next_hop_address": 0x1003,
    "route_cost": 10,
    "hop_count": 2,
    "weak_link_count": 0,
    "valid_time": 300,
}]

# ── 属性 9: context_information_table(5 字段)──
pan.context_information_table = [{
    "cid": 1,
    "context_length": 8,
    "context": b"\x20\x01\x0d\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
    "compression": 1,
    "valid_lifetime": 600,
}]

# ── 属性 10/11: 黑名单 & 广播日志 ──
pan.blacklist_table = [{"neighbour_address": 0x2001, "valid_time": 600}]
pan.broadcast_log_table = [{"source_address": 0x1001, "sequence_number": 5, "valid_time": 30}]

# ── 属性 12/21: 组表 & 目的地址(uint16 列表)──
pan.group_table = [0x0001, 0x0002]
pan.destination_address = [0x1001, 0x1002]

# ── 属性 22/23: LQI 阈值 ──
pan.low_lqi = 100
pan.high_lqi = 200

Python 属性一览

属性 类型 可写 说明
on_before_read Callable 读前钩子
on_after_read Callable 读后钩子
access_dict dict 实例级访问控制

子结构 dict 格式

字段数
routing_configuration 14 net_traversal_time , routing_table_entry_ttl , kr , km , kc , kq , kh , krt , rreq_retries , rreq_req_wait , blacklist_table_entry_ttl , unicast_rreq_gen_enable , rlc_enabled , add_rev_link_cost
routing_table 6 destination_address , next_hop_address , route_cost , hop_count , weak_link_count , valid_time
context_information_table 5 cid , context_length , context (bytes,16), compression , valid_lifetime
blacklist_table 2 neighbour_address , valid_time
broadcast_log_table 3 source_address , sequence_number , valid_time

访问控制

import dlms
from dlms import AccessMode, Authentication

pan = dlms.G3Plc6LoWPAN("0.0.29.2.0.255")

dlms.set_default_access(dlms.G3Plc6LoWPAN, {
    dlms.Authentication.NONE: {
        2: dlms.AccessMode.READ,   # max_hops
        8: dlms.AccessMode.READ,   # routing_table
        19: dlms.AccessMode.READ,  # device_type
    },
    dlms.Authentication.HIGH: {
        2: dlms.AccessMode.READ_WRITE,   # max_hops: HIGH 可写
        6: dlms.AccessMode.READ_WRITE,   # routing_configuration
        8: dlms.AccessMode.READ_WRITE,   # routing_table
        9: dlms.AccessMode.READ_WRITE,   # context_information_table
    },
})

预付费子系统(Prepayment)

预付费子系统由四个协作类组成:

类 ID 类名 OBIS 角色
111 Account 0.0.19.0.0.255 顶层控制器,串联 Credit / Charge / TokenGateway
112 Credit 0.0.19.10.0.255 信用额度(余额)管理
113 Charge 0.0.19.20.0.255 基于消费量的费用计算
115 TokenGateway 0.0.19.40.0.255 Token 入口、验证与执行

重要 :这四个类 均无 C 端动作调度 。所有方法(method)处理必须通过 on_before_action 回调在 Python 侧实现。


数据流概述

客户端/键盘 → TokenGateway.enter(token)
                         ↓
              on_before_action 验证 token
                         ↓
              TokenGateway.token / time / status 更新
                         ↓
              Account 根据 token_gateway_configurations
              按比例分配额度到对应 Credit
                         ↓
              Credit.current_credit_amount 增加
              Credit.status 更新
                         ↓
              Account 重算 available_credit /
              current_credit_status
                         ↓
              Charge.collect() → 根据 tariff 扣除 Credit 余额

TokenGateway

预付费(prepayment)计量中的 Token 网关 对象,典型 OBIS 代码 0.0.19.40.0.255 。负责充值 Token 的 录入(enter)→ 验证(verify)→ 执行(execute) 全流程管理:接收远端/本地/手动投递的充值 Token,记录最后一次处理的 Token 及其元数据,并将结果信用暴露给 Credit / Account 等对象使用。

Blue Book 属性

编号 Python 名称 类型 说明
1 logical_name octet-string(6) OBIS 代码,只读
2 token bytes 最后一次接受的 token 原始数据
3 time tuple(6) token 处理时间 (年,月,日,时,分,秒)
4 descriptions list[str] token 描述列表(每条描述是一个字符串)
5 delivery_method TokenDelivery Token 投递方式(REMOTE / LOCAL / MANUAL)
6 status TokenStatusCode Token 处理状态码
7 data_value bytes Bit 数组形式的附加数据

方法(均需通过 on_before_action 实现)

编号 名称 说明
1 enter 录入 token
2 verify 验证 token
3 execute 执行 token

构造函数

TokenGateway(logical_name: str, access: dict = None)
import dlms
from dlms import AccessMode, Authentication

gw = dlms.TokenGateway("0.0.19.40.0.255",access={2: (AccessMode.READ_WRITE, Authentication.HIGH)})

# ── 属性 1: logical_name ──
print(gw.logical_name)  # "0.0.19.40.0.255"

# ── 属性 2: token(最后一次处理的 Token 原始数据)──
gw.token = b"\x12\x34\x56\x78"

# ── 属性 3: time(处理时间戳)──
gw.time = (2024, 1, 15, 10, 30, 0)

# ── 属性 4: descriptions(信用类型描述列表)──
gw.descriptions = ["Gas meter token", "Electric token"]

# ── 属性 5: delivery_method(投递方式)──
gw.delivery_method = dlms.TokenDelivery.REMOTE

# ── 属性 6: status(处理状态)──
gw.status = dlms.TokenStatusCode.FORMAT_OK

# ── 属性 7: data_value(处理后的位数据)──
gw.data_value = b"\x01\x02"

# ── 方法处理:enter(1) / verify(2) / execute(3) 统一入口 ──
def on_token_action(self, event):
    if event.index == 1:   # enter
        raw = self.token
        print("[TokenGateway] enter token={!r}".format(raw))
        if raw and raw[0] != 0x00:
            self.status = dlms.TokenStatusCode.VALIDATION_OK
            self.time = (2025, 6, 15, 10, 30, 0)
        else:
            self.status = dlms.TokenStatusCode.TOKEN_FORMAT_FAILURE
    elif event.index == 2: # verify
        # ...解密 / STS 校验逻辑...
        self.status = dlms.TokenStatusCode.AUTHENTICATION_OK
    elif event.index == 3: # execute
        # ...解析 Token 金额并更新对应 Credit...
        self.status = dlms.TokenStatusCode.TOKEN_EXECUTION_OK
    return True

gw.on_before_action = on_token_action
方法 COSEM 方法 说明
enter 1 录入一个充值 Token(标准入口)
verify 2 验证 Token 的有效性
execute 3 执行 Token,将信用更新到 Credit 对象

重要 :三个方法 全部 经由 on_before_action 派发,需自行实现处理逻辑。Gurux 原生实现仅处理方法 1( enter ),对 2/3 会报错;本移植将其统一标记为已处理以保持一致行为。

枚举: TokenStatusCode

Token 处理结果码(对应 DLMS_TOKEN_STATUS_CODE_* ),通过 dlms.TokenStatusCode 访问:

常量 说明
TokenStatusCode.FORMAT_OK 0 格式正确
TokenStatusCode.AUTHENTICATION_OK 1 认证通过
TokenStatusCode.VALIDATION_OK 2 验证通过
TokenStatusCode.TOKEN_EXECUTION_OK 3 执行成功
TokenStatusCode.TOKEN_FORMAT_FAILURE 4 格式错误
TokenStatusCode.AUTHENTICATION_FAILURE 5 认证失败
TokenStatusCode.VALIDATION_RESULT_FAILURE 6 验证结果失败
TokenStatusCode.TOKEN_EXECUTION_RESULT_FAILURE 7 执行结果失败
TokenStatusCode.TOKEN_RECEIVED 8 Token 已接收(待处理)

枚举: TokenDelivery

Token 投递通道(对应 DLMS_TOKEN_DELIVERY_* ),通过 dlms.TokenDelivery 访问:

常量 说明
TokenDelivery.REMOTE 0 远程投递(如通过通信网络下发)
TokenDelivery.LOCAL 1 本地投递(如按键/本地接口录入)
TokenDelivery.MANUAL 2 手动投递(如人工抄表写入)
参数 类型 默认值 说明
logical_name str 必传 OBIS 代码字符串,如 "0.0.19.40.0.255"
access dict None 实例级权限,格式 {auth: {attr_index: AccessMode}}

Python 属性一览

属性 类型 可写 说明
on_before_read Callable 读前钩子(继承自 CosemObject)
on_after_read Callable 读后钩子(继承自 CosemObject)
on_before_write Callable 写前钩子(继承自 CosemObject)
on_after_write Callable 写后钩子(继承自 CosemObject)
on_before_action Callable 动作前钩子( 所有方法的实现入口
on_after_action Callable 动作后钩子(继承自 CosemObject)
access_dict dict 实例级访问控制

访问控制

import dlms
# 构造函数中指定
gateway = dlms.TokenGateway("0.0.19.40.0.255",
    access={2: (AccessMode.READ_WRITE, Authentication.HIGH)})

# 或事后修改
gateway.access_dict = {2: (AccessMode.READ_WRITE, Authentication.HIGH)}

典型用例

场景 示例
远程下发充值 delivery_method = TokenDelivery.REMOTE
本地键盘录入 delivery_method = TokenDelivery.LOCAL
校验失败上报 status = TokenStatusCode.AUTHENTICATION_FAILURE

Credit

预付费(prepayment)计量中的 信用额度 对象,典型 OBIS 代码 0.0.19.10.0.255 。管理单个充值信用的余额与配置:跟踪当前余额( current_credit_amount ),定义信用类型、优先级、告警阈值、欠费上限等参数。一个 Account 下可同时存在多个 Credit 对象,配合 TokenGateway / Charge 完成充值 → 扣费全流程。

Blue Book 属性

编号 名称 类型 说明
1 logical_name octet-string(6) OBIS 代码,只读
2 current_credit_amount int32 当前信用余额(有符号)
3 type enum 信用类型( CreditType
4 priority uint8 优先级(值越小越优先消耗)
5 warning_threshold int32 低余额告警阈值
6 limit int32 余额下限(可为负数,表示允许的债务额度)
7 credit_configuration bitmask 信用配置位掩码( CreditConfiguration
8 status uint8 信用生命周期状态( CreditStatus ,⏩ VOLATILE)
9 preset_credit_amount int32 预设信用额度(下次充值时载入)
10 credit_available_threshold int32 信用可用阈值(低于此值阻断)
11 period octet-string 周期时间 (year, month, day, hour, min, sec)

构造函数

Credit(logical_name: str, access: dict = None)
import dlms
from dlms import AccessMode, Authentication

credit = dlms.Credit("0.0.19.10.0.255", access={2: (AccessMode.READ_WRITE, Authentication.HIGH)})

# ── 属性 1: logical_name ──
print(credit.logical_name)  # "0.0.19.10.0.255"

# ── 属性 2: current_credit_amount(当前余额)──
credit.current_credit_amount = 5000

# ── 属性 3: type(信用类型)──
credit.type = dlms.CreditType.TOKEN

# ── 属性 4: priority(优先级,越小越优先消耗)──
credit.priority = 1

# ── 属性 5: warning_threshold(告警阈值)──
credit.warning_threshold = 500

# ── 属性 6: limit(下限,允许 200 的债务)──
credit.limit = -200

# ── 属性 7: credit_configuration(配置位掩码,可组合)──
credit.credit_configuration = (dlms.CreditConfiguration.VISUAL |
                               dlms.CreditConfiguration.TOKENS)

# ── 属性 8: status(生命周期状态)──
credit.status = dlms.CreditStatus.ENABLED

# ── 属性 9: preset_credit_amount(预设充值额度)──
credit.preset_credit_amount = 10000

# ── 属性 10: credit_available_threshold(可用阈值)──
credit.credit_available_threshold = 100

# ── 属性 11: period(周期时间)──
credit.period = (2024, 1, 1, 0, 0, 0)

# ── 方法处理:update_amount(1) / set_amount_to_value(2) / invoke_credit(3) ──
def on_credit_action(self, event):
    if event.index == 1:   # update_amount:按增量调整余额
        delta = event.parameters if isinstance(event.parameters, int) else 0
        self.current_credit_amount += delta
        print("[Credit] update_amount delta={} balance={}".format(
            delta, self.current_credit_amount))
    elif event.index == 2: # set_amount_to_value:设为绝对值
        self.current_credit_amount = event.parameters
        print("[Credit] set_amount_to_value balance={}".format(
            self.current_credit_amount))
    elif event.index == 3: # invoke_credit:从预设额度充值
        self.current_credit_amount += self.preset_credit_amount
        self.status = dlms.CreditStatus.IN_USE
        print("[Credit] invoke_credit balance={}".format(
            self.current_credit_amount))
    return True

credit.on_before_action = on_credit_action

方法(COSEM 动作)

方法 COSEM 方法 说明
update_amount 1 按增量调整当前信用余额(delta 可为负)
set_amount_to_value 2 将余额设为指定绝对值
invoke_credit 3 激活信用:从 preset_credit_amount 载入充值额度

重要 :三个方法 全部 经由 on_before_action 派发,需自行实现处理逻辑(C 层在 events.c 中统一置 e->handled=1 )。

枚举: CreditType

信用类型(对应 DLMS_CREDIT_TYPE_* ),通过 dlms.CreditType 访问:

常量 说明
CreditType.TOKEN 0 Token 信用(充值 Token 带来的信用)
CreditType.RESERVED 1 预留信用
CreditType.EMERGENCY 2 紧急信用(欠费断电后临时供电)
CreditType.TIME_BASED 3 按时间计费的信用
CreditType.CONSUMPTION_BASED 4 按用量计费的信用

枚举: CreditStatus

信用生命周期状态(对应 DLMS_CREDIT_STATUS_* ),通过 dlms.CreditStatus 访问:

常量 说明
CreditStatus.ENABLED 0 已启用
CreditStatus.SELECTABLE 1 可被选择/激活
CreditStatus.INVOKED 2 已被调用/激活
CreditStatus.IN_USE 3 使用中
CreditStatus.CONSUMED 4 已消耗

枚举: CreditConfiguration

信用配置位掩码(对应 DLMS_CREDIT_CONFIGURATION_* ),通过 dlms.CreditConfiguration 访问,可多值按位或:

常量 说明
CreditConfiguration.NONE 0x00 无配置
CreditConfiguration.VISUAL 0x01 需要视觉指示
CreditConfiguration.CONFIRMATION 0x02 激活前需要确认
CreditConfiguration.PAID_BACK 0x04 信用金额需要偿还
CreditConfiguration.RESETTABLE 0x08 可重置
CreditConfiguration.TOKENS 0x10 可接收 Token 充值
credit.credit_configuration = (dlms.CreditConfiguration.VISUAL |
                               dlms.CreditConfiguration.TOKENS)

枚举: CreditCollectionConfiguration

信用回收条件位掩码(对应 DLMS_CREDIT_COLLECTION_CONFIGURATION_* ),通过 dlms.CreditCollectionConfiguration 访问,可多值按位或:

常量 说明
CreditCollectionConfiguration.NONE 0x00
CreditCollectionConfiguration.DISCONNECTED 0x01 断电状态下回收
CreditCollectionConfiguration.LOAD_LIMITING 0x02 限载状态下回收
CreditCollectionConfiguration.FRIENDLY_CREDIT 0x04 友好信用(暂缓断电)

Python 属性一览

属性 类型 可写 说明
on_before_read Callable 读前钩子(继承自 CosemObject)
on_after_read Callable 读后钩子(继承自 CosemObject)
on_before_write Callable 写前钩子(继承自 CosemObject)
on_after_write Callable 写后钩子(继承自 CosemObject)
on_before_action Callable 动作前钩子( 所有方法的实现入口
on_after_action Callable 动作后钩子(继承自 CosemObject)
access_dict dict 实例级访问控制

访问控制

import dlms
from dlms import AccessMode, Authentication

# 构造函数中指定
credit = dlms.Credit("0.0.19.10.0.255",
    access={2: (AccessMode.READ_WRITE, Authentication.HIGH)})

# 或事后修改
credit.access_dict = {2: (AccessMode.READ_WRITE, Authentication.HIGH)}

典型用例

场景 示例
Token 充值信用 type = CreditType.TOKEN credit_configuration |= TOKENS
紧急供电 type = CreditType.EMERGENCY
允许欠费 limit = -200
充值后激活 invoke_credit (方法 3,从 preset_credit_amount 载入)

Charge

预付费(prepayment)计量中的 计费 对象,典型 OBIS 代码 0.0.19.20.0.255 。根据能量/用量消耗,通过费率表( unit_charge_active / unit_charge_passive )计算费用,并将结果累计到 Account 的聚合债务( total_amount_remaining )中。一个 Account 下可同时存在多个 Charge 对象(不同计费项),配合 Credit / TokenGateway 完成充值 → 扣费全流程。

Blue Book 属性

编号 名称 类型 说明
1 logical_name octet-string(6) OBIS 代码,只读
2 total_amount_paid int32 累计已付金额(有符号)
3 charge_type enum 计费类型( ChargeType
4 priority uint8 优先级(决定计费项应用顺序)
5 unit_charge_active structure 当前生效的费率表(⏩ COMPLEX)
6 unit_charge_passive structure 待生效(下一周期)费率表(⏩ COMPLEX)
7 unit_charge_activation_time octet-string passive 变为 active 的时间 (year, month, day, hour, min, sec)
8 period uint32 计费周期(秒)
9 charge_configuration bitmask 计费配置位掩码( ChargeConfiguration
10 last_collection_time octet-string 上次计费回收时间 (year, month, day, hour, min, sec)
11 last_collection_amount int32 上次计费回收金额
12 total_amount_remaining int32 尚欠余额(有符号)
13 proportion uint16 比例因子(0~65535)

构造函数

Charge(logical_name: str, access: dict = None)
import dlms
from dlms import AccessMode, Authentication
energy_register = dlms.ExtendedRegister("1.1.1.8.0.255")
charge = dlms.Charge("0.0.19.20.0.255", access={2: (AccessMode.READ_WRITE, Authentication.HIGH)})

# ── 属性 1: logical_name ──
print(charge.logical_name)  # "0.0.19.20.0.255"

# ── 属性 2: total_amount_paid(累计已付)──
charge.total_amount_paid = 5000

# ── 属性 3: charge_type(计费类型)──
charge.charge_type = dlms.ChargeType.CONSUMPTION_BASED_COLLECTION

# ── 属性 4: priority(优先级,越小越优先应用)──
charge.priority = 1

# ── 属性 5: unit_charge_active(当前生效费率表)──
charge.unit_charge_active = {
    "charge_per_unit_scaling": {"commodity_scale": 0, "price_scale": 2},
    "commodity": {"target": energy_register, "attribute_index": 2},
    "charge_tables": [
        {"index": b"\x01", "charge_per_unit": 100},
        {"index": b"\x02", "charge_per_unit": 200},
    ],
}

# ── 属性 6: unit_charge_passive(待生效费率表)──
charge.unit_charge_passive = {
    "charge_per_unit_scaling": {"commodity_scale": 0, "price_scale": 3},
    "commodity": {"target": None, "attribute_index": 0},
    "charge_tables": [
        {"index": b"\x03", "charge_per_unit": 300},
    ],
}

# ── 属性 7: unit_charge_activation_time(passive 激活时间)──
charge.unit_charge_activation_time = (2024, 7, 1, 0, 0, 0)

# ── 属性 8: period(计费周期,秒)──
charge.period = 3600

# ── 属性 9: charge_configuration(配置位掩码,可组合)──
charge.charge_configuration = dlms.ChargeConfiguration.CONTINUOUS_COLLECTION

# ── 属性 10: last_collection_time(上次回收时间)──
charge.last_collection_time = (2024, 6, 30, 23, 59, 59)

# ── 属性 11: last_collection_amount(上次回收金额)──
charge.last_collection_amount = 350

# ── 属性 12: total_amount_remaining(尚欠余额)──
charge.total_amount_remaining = 1200

# ── 属性 13: proportion(比例因子)──
charge.proportion = 100

# ── 方法处理:update_unit_charge(1) / activate(2) / collect(3) ──
def on_charge_action(self, event):
    if event.index == 1:   # update_unit_charge:passive 复制为 active
        self.unit_charge_active = self.unit_charge_passive
        print("[Charge] update_unit_charge")
    elif event.index == 2: # activate:立即激活 passive
        self.unit_charge_active = self.unit_charge_passive
        print("[Charge] activate")
    elif event.index == 3: # collect:执行一次计费回收
        # ...从 commodity.target 读取用量并按费率表计算...
        # self.total_amount_remaining += collected
        # self.last_collection_amount = collected
        print("[Charge] collect")
    elif event.index == 4: # update_last_collection_time
        self.last_collection_time = (2024, 7, 1, 0, 0, 0)
        print("[Charge] update_last_collection_time")
    elif event.index == 5: # update_total_amount_remaining
        # self.total_amount_remaining = ...
        print("[Charge] update_total_amount_remaining")
    elif event.index == 6: # set_total_amount_paid
        self.total_amount_paid = 0
        print("[Charge] set_total_amount_paid")
    return True

charge.on_before_action = on_charge_action

方法(均需通过 on_before_action 实现)
| 方法 | COSEM 方法 | 说明 |
|------|-----------|------|
| update_unit_charge | 1 | 将 passive 费率表复制为 active |
| activate | 2 | 立即激活 passive 费率表 |
| collect | 3 | 执行一次计费回收周期 |
| update_last_collection_time | 4 | 更新上次回收时间 |
| update_total_amount_remaining | 5 | 重新计算剩余欠款 |
| set_total_amount_paid | 6 | 重置已付金额计数 |

重要 :全部方法 经由 on_before_action 派发,需自行实现处理逻辑(C 层在 events.c 中统一置 e->handled=1 )。

枚举: ChargeType

计费回收方式(对应 DLMS_CHARGE_TYPE_* ),通过 dlms.ChargeType 访问:

常量 说明
ChargeType.CONSUMPTION_BASED_COLLECTION 0 按用量计费
ChargeType.TIME_BASED_COLLECTION 1 按时间计费
ChargeType.PAYMENT_EVENT_BASED_COLLECTION 2 按缴费事件计费

枚举: ChargeConfiguration

计费配置位掩码(对应 DLMS_CHARGE_CONFIGURATION_* ),通过 dlms.ChargeConfiguration 访问,可多值按位或:

常量 说明
ChargeConfiguration.NONE 0x00 无配置
ChargeConfiguration.PERCENTAGE_BASED_COLLECTION 0x01 按百分比计费
ChargeConfiguration.CONTINUOUS_COLLECTION 0x02 连续计费

unit_charge_active / unit_charge_passive 字典结构

两个费率表属性使用相同的 dict 结构:

{
    "charge_per_unit_scaling": {"commodity_scale": int, "price_scale": int},
    "commodity":               {"target": dlms_obj_or_none, "attribute_index": int},
    "charge_tables":           [{"index": bytes, "charge_per_unit": int}, ...]
}
类型 说明
charge_per_unit_scaling dict 计量刻度: commodity_scale (商品缩放,int8)、 price_scale (价格缩放,int8)
commodity dict 关联的商品对象: target (DLMS 对象引用,如能量寄存器,可 None )、 attribute_index (读取的属性编号)
charge_tables list[dict] 费率表条目: index (费率标识 bytes)、 charge_per_unit (单位费率 int16)

Python 属性一览

属性 类型 可写 说明
on_before_read Callable 读前钩子(继承自 CosemObject)
on_after_read Callable 读后钩子(继承自 CosemObject)
on_before_write Callable 写前钩子(继承自 CosemObject)
on_after_write Callable 写后钩子(继承自 CosemObject)
on_before_action Callable 动作前钩子( 所有方法的实现入口
on_after_action Callable 动作后钩子(继承自 CosemObject)
access_dict dict 实例级访问控制

访问控制

import dlms
from dlms import AccessMode, Authentication

# 构造函数中指定
charge = dlms.Charge("0.0.19.20.0.255",
    access={2: (AccessMode.READ_WRITE, Authentication.HIGH)})

# 或事后修改
charge.access_dict = {2: (AccessMode.READ_WRITE, Authentication.HIGH)}

典型用例

场景 示例
按用量计费 charge_type = ChargeType.CONSUMPTION_BASED_COLLECTION
按时间计费 charge_type = ChargeType.TIME_BASED_COLLECTION
阶梯费率 charge_tables 配置多档 index / charge_per_unit
周期回收 period = 3600 collect (方法 3)

Account

预付费(prepayment)计量中的 账户 对象,典型 OBIS 代码 0.0.19.0.0.255 。作为预付费业务的顶层控制器:维护付费模式与账户状态,关联其下的 Credit (信用)与 Charge (计费)对象,聚合可用信用( available_credit )与债务( aggregated_debt ),并通过 credit_charge_configurations / token_gateway_configurations 配置「信用 → 计费」「信用 → 令牌」的映射关系。一个 Account 可同时挂接多个 Credit / Charge ,配合 TokenGateway 完成充值 → 扣费 → 结算全流程。

Blue Book 属性

编号 Python 名称 类型 说明
1 logical_name octet-string(6) OBIS 代码,只读
2 payment_mode AccountPaymentMode 支付模式(CREDIT / PREPAYMENT)
3 account_status AccountStatus 账户状态
4 current_credit_in_use uint8 当前使用的信用索引
5 current_credit_status AccountCreditStatus 当前信用状态(位掩码)
6 available_credit int32 可用信用总额
7 amount_to_clear int32 需清偿金额
8 clearance_threshold int32 清偿阈值
9 aggregated_debt int32 累计债务
10 credit_references list[str] 关联的 Credit 对象的 OBIS 列表
11 charge_references list[str] 关联的 Charge 对象的 OBIS 列表
12 credit_charge_configurations list[dict] Credit↔Charge 映射配置
13 token_gateway_configurations list[dict] TokenGateway↔Credit 映射配置
14 account_activation_time tuple(6) 账户激活时间
15 account_closure_time tuple(6) 账户关闭时间
16 currency dict 货币信息 {name, scale, unit}
17 low_credit_threshold int32 低信用阈值
18 next_credit_available_threshold int32 下一信用可用阈值
19 max_provision uint16 最大预授权
20 max_provision_period int32 最大预授权周期
import dlms
from dlms import AccessMode, Authentication

account = dlms.Account("0.0.19.0.0.255")

# ── 属性 1: logical_name ──
print(account.logical_name)  # "0.0.19.0.0.255"

# ── 属性 2: payment_mode(付费模式)──
account.payment_mode = dlms.AccountPaymentMode.PREPAYMENT

# ── 属性 3: account_status(账户状态)──
account.account_status = dlms.AccountStatus.ACTIVE

# ── 属性 4: current_credit_in_use(当前使用的 Credit 索引)──
account.current_credit_in_use = 0

# ── 属性 5: current_credit_status(信用状态位掩码)──
account.current_credit_status = dlms.AccountCreditStatus.IN_CREDIT

# ── 属性 6: available_credit(可用信用)──
account.available_credit = 7500

# ── 属性 7: amount_to_clear(需清偿债务)──
account.amount_to_clear = 0

# ── 属性 8: clearance_threshold(清偿阈值)──
account.clearance_threshold = 100

# ── 属性 9: aggregated_debt(聚合债务)──
account.aggregated_debt = 0

# ── 属性 10: credit_references(关联 Credit 的 OBIS 列表)──
account.credit_references = ["0.0.19.10.0.255"]

# ── 属性 11: charge_references(关联 Charge 的 OBIS 列表)──
account.charge_references = ["0.0.19.20.0.255"]

# ── 属性 12: credit_charge_configurations(信用-计费关联配置)──
account.credit_charge_configurations = [
    {
        "credit_reference": "0.0.19.10.0.255",
        "charge_reference": "0.0.19.20.0.255",
        "collection_configuration": 1,
    },
]

# ── 属性 13: token_gateway_configurations(令牌网关配置)──
account.token_gateway_configurations = [
    {"credit_reference": "0.0.19.10.0.255", "token_proportion": 100},
]

# ── 属性 14: account_activation_time(账户激活时间)──
account.account_activation_time = (2024, 1, 1, 0, 0, 0)

# ── 属性 15: account_closure_time(账户关闭时间)──
account.account_closure_time = (2030, 12, 31, 23, 59, 59)

# ── 属性 16: currency(货币)──
account.currency = {"name": "EUR", "scale": -2, "unit": 8}

# ── 属性 17: low_credit_threshold(低信用告警阈值)──
account.low_credit_threshold = 500

# ── 属性 18: next_credit_available_threshold(下一信用可用阈值)──
account.next_credit_available_threshold = 1000

# ── 属性 19: max_provision(最大预充额度)──
account.max_provision = 2000

# ── 属性 20: max_provision_period(最大预充周期,秒)──
account.max_provision_period = 86400

# ── 方法处理:方法 1~18 全部派发至 on_before_action,C 层无派发 ──
# 方法编号与语义由业务逻辑约定(常见语义:activate / deactivate /
# update_credit / clear_debt / set_payment_mode 等),以下仅为示例。
def on_account_action(self, event):
    if event.index == 1:   # 例如 set_payment_mode:切换付费模式
        self.payment_mode = dlms.AccountPaymentMode.PREPAYMENT
        print("[Account] set_payment_mode -> PREPAYMENT")
    elif event.index == 2: # 例如 update_credit:从关联 Credit 对象同步可用信用
        self.available_credit = credit_obj.current_credit_amount
        print("[Account] update_credit available_credit={}".format(self.available_credit))
    elif event.index == 3: # 例如 clear_debt:清偿债务
        self.aggregated_debt = 0
        self.amount_to_clear = 0
        print("[Account] clear_debt")
    elif event.index == 4: # 例如 activate / deactivate:激活 / 关闭账户
        self.account_status = dlms.AccountStatus.ACTIVE
        print("[Account] activate")
    else:
        # 其余方法:保持 available_credit 与关联 Credit 同步
        self.available_credit = max(0, credit_obj.current_credit_amount)
        print("[Account] Action method={} available_credit={}".format(
              event.index, self.available_credit))
    return True

account.on_before_action = on_account_action

方法

支持 18 个动作方法 ,编号 1–18。应用须在 on_before_action 中实现全部 18 个方法。

编号 说明
1–18 由应用自行定义(如激活、充值、冻结、结算等)

构造函数

Account(logical_name: str, access: dict = None)
参数 类型 默认值 说明
logical_name str 必传 OBIS 代码字符串,如 "0.0.19.0.0.255"
access dict None 实例级权限

AccountStatus — 账户状态
| Python 名称 | 值 | 说明 |
|-------------|:--:|------|
| AccountStatus.NEW_INACTIVE_ACCOUNT | 1 | 新建未激活 |
| AccountStatus.ACTIVE | 2 | 已激活 |
| AccountStatus.CLOSED | 3 | 已关闭 |

AccountPaymentMode — 付费模式

Python 名称 说明
AccountPaymentMode.CREDIT 1 后付费(先消费后付费)
AccountPaymentMode.PREPAYMENT 2 预付费(先充值后消费)

AccountCreditStatus — 信用状态(位掩码,可位或组合)

Python 名称 说明
AccountCreditStatus.NONE 0x0 无特殊状态
AccountCreditStatus.IN_CREDIT 0x1 有可用信用
AccountCreditStatus.LOW_CREDIT 0x2 低信用
AccountCreditStatus.NEXT_CREDIT_ENABLED 0x4 下一信用已启用
AccountCreditStatus.NEXT_CREDIT_SELECTABLE 0x8 下一信用可选
AccountCreditStatus.CREDIT_REFERENCE_LIST 0x10 使用信用引用列表
AccountCreditStatus.SELECTABLE_CREDIT_IN_USE 0x20 可选信用正在使用
AccountCreditStatus.OUT_OF_CREDIT 0x40 信用耗尽
AccountCreditStatus.RESERVED 0x80 保留

取值见上方 AccountCreditStatus 枚举表(位掩码,可位或组合)。

Currency — 货币单位

Python 名称 说明
Currency.TIME 0 时间单位
Currency.CONSUMPTION 1 消费量(用量)单位
Currency.MONETARY 2 货币单位

Python 属性一览

属性 类型 可写 说明
logical_name bytes × OBIS 代码(6 字节)
payment_mode AccountPaymentMode 支付模式
account_status AccountStatus 账户状态
current_credit_in_use int 当前使用信用索引
current_credit_status AccountCreditStatus 当前信用状态位掩码
available_credit int 可用信用总额
amount_to_clear int 需清偿金额
clearance_threshold int 清偿阈值
aggregated_debt int 累计债务
credit_references list[str] Credit OBIS 引用列表
charge_references list[str] Charge OBIS 引用列表
credit_charge_configurations list[dict] Credit→Charge 映射列表
token_gateway_configurations list[dict] TokenGateway→Credit 映射列表
account_activation_time tuple(6) 激活时间
account_closure_time tuple(6) 关闭时间
currency dict 货币信息 {name, scale, unit}
low_credit_threshold int 低信用阈值
next_credit_available_threshold int 下一信用可用阈值
max_provision int 最大预授权
max_provision_period int 最大预授权周期
on_before_action Callable 动作前钩子

访问控制

import dlms
from dlms import AccessMode, Authentication


account = dlms.Account("0.0.19.0.0.255",access={6: (dlms.AccessMode.READ_WRITE, dlms.Authentication.HIGH)}

# 或事后修改
account.access_dict = {6: (dlms.AccessMode.READ_WRITE, dlms.Authentication.HIGH)}

其他辅助类


StatusMapping

状态映射 对象,典型 OBIS 代码 0.0.96.5.4.255 。将状态字( status_word )中的各个位映射到关联 COSEM 对象的条目( mapping_table ),用于把设备运行状态(如计量状态、报警位)以位图形式呈现给客户端。属性 2 为动态值(随设备状态变化),属性 3 为静态配置(由应用在初始化时设定)。

Blue Book 属性

编号 Python 名称 类型 说明
1 logical_name octet-string(6) OBIS 代码,只读
2 status_word tuple(2) (dlms_type, value) — 类型标签选择编码格式, value int bytes
3 mapping_table tuple(2) (ref_table_id, mapping) mapping 为单个起始条目整数或每比特条目索引列表

注意 :客户端 SET 请求会拒绝属性 2 和 3 的写入。需直接从 Python 更新值。

status_word 支持的类型标签

标签值 含义
4 BIT_STRING(位串)
6 UINT32(32 位无符号整数)
9 OCTET_STRING(八位位组串)
10 STRING(字符串)
12 STRING_UTF8(UTF-8 字符串)
17 UINT8(8 位无符号整数)
18 UINT16(16 位无符号整数 — 默认
21 UINT64(64 位无符号整数)

mapping_table 格式

  • 长-无符号选择 mapping int ):表示引用表中的起始条目索引
  • 数组选择 mapping list[int] ):显式指定每个比特位对应的条目索引列表

构造函数

StatusMapping(logical_name: str, access: dict = None)
参数 类型 默认值 说明
logical_name str 必传 OBIS 代码字符串
access dict None 实例级权限
import dlms

sm = dlms.StatusMapping("0.0.96.5.4.255",access={6: (dlms.AccessMode.READ_WRITE, dlms.Authentication.HIGH)})

# ── 属性 1: logical_name ──
print(sm.logical_name)  # "0.0.96.5.4.255"

# ── 属性 2: status_word(状态字,UINT16,全部位清零)──
sm.status_word = (18, 0x0000)

# ── 属性 3: mapping_table(映射表,8 位状态字逐位映射到条目 0~7)──
sm.mapping_table = (0, [0, 1, 2, 3, 4, 5, 6, 7])

# ── 运行时更新状态字(模拟某状态位置位)──
sm.status_word = (18, 0x0002)   # bit1 置位

# ── 方法处理:动作派发至 on_before_action ──
def on_sm_action(self, event):
    print("[StatusMapping] Action method={}".format(event.index))
    return True

sm.on_before_action = on_sm_action

Python 属性一览

属性 类型 可写 说明
on_before_read Callable 读前钩子(继承 CosemObject)
on_after_read Callable 读后钩子
on_before_write Callable 写前钩子
on_after_write Callable 写后钩子
on_before_action Callable 动作前钩子
on_after_action Callable 动作后钩子
access_dict dict 实例级访问控制

访问控制

import dlms
from dlms import AccessMode, Authentication


sm = dlms.StatusMapping("0.0.96.5.4.255",access={6: (dlms.AccessMode.READ_WRITE, dlms.Authentication.HIGH)})

# 或事后修改
sm.access_dict = {6: (dlms.AccessMode.READ_WRITE, dlms.Authentication.HIGH)}

Arbitrator

仲裁器 对象,典型 OBIS 代码 0.0.96.5.5.255 。解决多个参与者(actor)对同一组动作(action)的 并发请求冲突 :每个参与者拥有权限位集( permissions_table )与权重( weightings_table ),仲裁器根据参与者最近请求( most_recent_requests_table )与权重选出获胜动作,结果记录在 last_outcome 。常用于多主站/多控制源场景(如本地按键与远程指令争用同一执行器)。

Blue Book 属性

编号 Python 名称 类型 说明
1 logical_name octet-string(6) OBIS 代码,只读
2 actions list[tuple] (ScriptTable_or_None, selector) 列表
3 permissions_table list[bytes] 权限位集,每参与方一行
4 weightings_table list[list[int]] 权重表,每参与方×每动作的 uint16 权重
5 most_recent_requests_table list[bytes] 最近请求位集,每参与方一行
6 last_outcome uint8 上次仲裁结果(0–255)

方法(需通过 on_before_action 实现)

编号 名称 说明
1 push 提交请求 bytes(由应用实现仲裁逻辑)

构造函数

Arbitrator(logical_name: str, access: dict = None)
参数 类型 默认值 说明
logical_name str 必传 OBIS 代码字符串
access dict None 实例级权限

基本用法

import dlms

arb = dlms.Arbitrator("0.0.96.5.5.255", access={6: (dlms.AccessMode.READ_WRITE, dlms.Authentication.HIGH)})

# ── 属性 1: logical_name ──
print(arb.logical_name)  # "0.0.96.5.5.255"

# ── 属性 2: actions(动作列表,关联脚本表)──
# 先创建脚本表
disconnect_ctl = dlms.DisconnectControl("0.0.96.3.10.255")

action_close = dlms.ScriptAction(
    type=dlms.ScriptAction.Execute,
    target=disconnect_ctl,
    method=1,           # remote_disconnect
    parameter=0,
)
action_open = dlms.ScriptAction(
    type=dlms.ScriptAction.Execute,
    target=disconnect_ctl,
    method=2,           # remote_reconnect
    parameter=0,
)
script_table = dlms.ScriptTable("0.0.10.0.106.255")
script_table.add_script(id=1, actions=action_close)   # selector=1:关闭负载
script_table.add_script(id=3, actions=action_open)    # selector=3:打开负载

arb.actions = [
    (script_table, 1),   # 动作 0:执行脚本表 selector=1
    (None, 0),           # 动作 1:无脚本
    (script_table, 3),   # 动作 2:执行脚本表 selector=3
]

# ── 属性 3: permissions_table(权限位集:2 个参与者,各 4 位权限)──
arb.permissions_table = [b'\xF0', b'\x0F']

# ── 属性 4: weightings_table(权重表:2 参与者 × 3 动作)──
arb.weightings_table = [[10, 20, 30], [5, 15, 25]]

# ── 属性 5: most_recent_requests_table(最近请求位集)──
arb.most_recent_requests_table = [b'\x00', b'\x00']

# ── 属性 6: last_outcome(上次仲裁结果)──
arb.last_outcome = 0

# ── 方法处理:push(方法 1)──
def on_arb_action(self, event):
    if event.index == 1:   # push:接收参与者请求字节并执行仲裁
        # 业务逻辑:
        #   1. 解析请求字节,更新 most_recent_requests_table
        #   2. 结合 permissions_table 过滤无权限请求
        #   3. 按 weightings_table 加权比较,选出获胜动作
        #   4. self.last_outcome = winning_action_id
        #   5. 执行 self.actions[winning_action_id] 对应脚本
        print("[Arbitrator] push() invoked")
    return True

arb.on_before_action = on_arb_action

Python 属性一览

| 属性 | 类型 | 可写 | 说明 |
| on_before_read | Callable | √ | 读前钩子(继承自 CosemObject) |
| on_after_read | Callable | √ | 读后钩子(继承自 CosemObject) |
| on_before_write | Callable | √ | 写前钩子(继承自 CosemObject) |
| on_after_write | Callable | √ | 写后钩子(继承自 CosemObject) |
| on_before_action | Callable | √ | 动作前钩子( 方法 1 push 的实现入口 ) |
| on_after_action | Callable | √ | 动作后钩子(继承自 CosemObject) |
| access_dict | dict | √ | 实例级访问控制 |

import dlms
from dlms import AccessMode, Authentication


arb = dlms.Arbitrator("0.0.96.5.5.255", access={6: (dlms.AccessMode.READ_WRITE, dlms.Authentication.HIGH)})

# 或事后修改
arb.access_dict = {6: (dlms.AccessMode.READ_WRITE, dlms.Authentication.HIGH)}

DataProtection

数据保护 对象,典型 OBIS 代码 0.0.29.0.0.255 。为 DLMS 通信数据提供 密码学保护 :通过 required_protection 声明请求/响应必须满足的保护级别(认证、加密、数字签名),通过 protection_buffer 保存加密数据,通过 protection_object_list 声明受保护的对象列表,通过 protection_parameters_get / protection_parameters_set 配置读写操作的保护参数(保护类型、密钥类型等)。配合 protect() / unprotect() 方法可直接对数据块进行加密/解密。

本类在 Gurux 中为 stub cosem_getDataProtection() 返回 NOT_IMPLEMENTED assert(0) gxinvoke.c 对类 30 无动作派发)。 所有属性读取与动作派发均由 Python 回调驱动 protection_object_list / protection_parameters_get / protection_parameters_set 直接以 Python 对象形式存储在 Python 侧。

Blue Book 属性

编号 Python 名称 类型 说明
1 logical_name octet-string(6) OBIS 代码,只读
2 protection_buffer bytes 保护缓冲区(加密/认证后的数据)
3 required_protection RequiredProtection 所需保护类型位掩码
4 protection_object_list list[dict] 受保护对象列表(Python 侧存储)
5 protection_parameters_get list[dict] GET 操作的保护参数(Python 侧存储)
6 protection_parameters_set list[dict] SET 操作的保护参数(Python 侧存储)

构造函数

DataProtection(logical_name: str, access: dict = None)
参数 类型 默认值 说明
logical_name str 必传 OBIS 代码字符串
access dict None 实例级权限

方法

方法 说明
protect(plaintext_bytes) bytes 使用当前会话密码(AES-GCM)加密数据,自动更新 protection_buffer
unprotect(ciphertext_bytes) bytes 解密之前由 protect() 产生的密文包
deinit() 释放 C 堆内存

protect() / unprotect() 仅在启用 HIGH GMAC 加密会话时可用。无密码密钥时将抛出 ValueError

protection_object_list 字典结构

import dlms

dp = dlms.DataProtection("0.0.29.0.0.255", access={6: (dlms.AccessMode.READ_WRITE, dlms.Authentication.HIGH)})

# ── 属性 1: logical_name ──
print(dp.logical_name)  # "0.0.29.0.0.255"

# ── 属性 2: protection_buffer(保护缓冲)──
dp.protection_buffer = b'\x01\x02\x03\x04'

# ── 属性 3: protection_object_list(受保护对象列表)──
data = dlms.Data("1.0.1.8.0.255",nocopy=False,access={2: (dlms.AccessMode.READ_WRITE, dlms.Authentication.HIGH)})
dp.protection_object_list = [
    (data, 2, 0),   # (DLMS 对象, 属性编号, 数据下标)
]

# ── 属性 4: protection_parameters_get(GET 保护参数)──
dp.protection_parameters_get = [
    # (protection_type, id, originator, recipient, information, key_info)
    (dlms.ProtectionType.AUTHENTICATION, b'', b'', b'', b'',
     (dlms.DataProtectionKeyType.IDENTIFIED,
      dlms.IdentifiedKeyType.UNICAST_ENCRYPTION)),
]

# ── 属性 5: protection_parameters_set(SET 保护参数)──
dp.protection_parameters_set = []

# ── 属性 6: required_protection(必需保护位掩码)──
dp.required_protection = dlms.RequiredProtection.AUTHENTICATED_REQUEST

# ── 方法处理:动作派发至 on_before_action ──
def on_dp_action(self, event):
    print("[DataProtection] Action method={}".format(event.index))
    return True

dp.on_before_action = on_dp_action

# ── protect / unprotect(需加密会话,AES-GCM)──
# cipher = dp.protect(b'\x01\x02\x03\x04')   # 加密,同时写入 protection_buffer
# plain  = dp.unprotect(cipher)              # 解密

RequiredProtection — 必需保护(位掩码,可位或组合)

Python 名称 说明
RequiredProtection.NONE 0x0 无需保护
RequiredProtection.AUTHENTICATED_REQUEST 0x4 请求必须认证
RequiredProtection.ENCRYPTED_REQUEST 0x8 请求必须加密
RequiredProtection.DIGITALLY_SIGNED_REQUEST 0x10 请求必须数字签名
RequiredProtection.AUTHENTICATED_RESPONSE 0x20 响应必须认证
RequiredProtection.ENCRYPTED_RESPONSE 0x40 响应必须加密
RequiredProtection.DIGITALLY_SIGNED_RESPONSE 0x80 响应必须数字签名

ProtectionType — 保护类型

Python 名称 说明
ProtectionType.AUTHENTICATION 1 仅认证
ProtectionType.ENCRYPTION 2 仅加密
ProtectionType.AUTHENTICATION_ENCRYPTION 3 认证 + 加密

DataProtectionKeyType — 密钥类型

Python 名称 说明
DataProtectionKeyType.IDENTIFIED 0 预共享标识密钥(配 IdentifiedKeyType
DataProtectionKeyType.WRAPPED 1 包装(加密传输)密钥(配 WrappedKeyType + 密钥字节)
DataProtectionKeyType.AGREED 2 协商密钥(配参数 + 数据字节)

IdentifiedKeyType — 标识密钥子类型

Python 名称 说明
IdentifiedKeyType.UNICAST_ENCRYPTION 0 全局单播加密密钥
IdentifiedKeyType.BROADCAST_ENCRYPTION 1 全局广播加密密钥

WrappedKeyType — 包装密钥子类型

Python 名称 说明
WrappedKeyType.MASTER_KEY 0 主密钥

Python 属性一览

属性 类型 可写 说明
on_before_read Callable 读前钩子( 需实现复杂属性读取
on_before_action Callable 动作前钩子
on_after_read Callable 读后钩子
on_after_write Callable 写后钩子
on_before_write Callable 写前钩子
on_after_action Callable 动作后钩子
access_dict dict 实例级访问控制

访问权限

import dlms
from dlms import AccessMode, Authentication

dp = dlms.DataProtection("0.0.29.0.0.255", access={6: (dlms.AccessMode.READ_WRITE, dlms.Authentication.HIGH)})

# 或事后修改
dp.access_dict = {6: (dlms.AccessMode.READ_WRITE, dlms.Authentication.HIGH)}

SapAssignment

SAP(服务接入点)分配 对象,典型 OBIS 代码 0.0.41.0.0.255 。维护逻辑设备列表及其 SAP 地址( sap_id )与逻辑设备名(LDN)的映射关系。用于多逻辑设备通信场景:客户端通过 SAP 地址区分不同的逻辑设备。

Blue Book 属性

编号 Python 名称 类型 说明
1 logical_name octet-string(6) OBIS 代码,只读
2 sap_assignment_list list[tuple] [(sap_id, device_name), ...] device_name bytes

构造函数

SapAssignment(logical_name: str, sap_assignment_list: list = None)
import dlms

sap = dlms.SapAssignment(
    logical_name='0.0.41.0.0.255',
    sap_assignment_list=[(1, b'GRX0000000012345')],
)

# ── 属性 1: logical_name ──
print(sap.logical_name)  # "0.0.41.0.0.255"

# ── 属性 2: sap_assignment_list(SAP 分配列表)──
print(sap.sap_assignment_list)   # [(1, b'GRX0000000012345')]
sap.sap_assignment_list = [
    (1, b'GRX0000000012345'),
    (2, "GRX0000000012346"),
]

# ── 方法:connect_logical_device(新增/更新 SAP 分配,本地调用)──
sap.connect_logical_device(3, b'GRX0000000012347')


参数 类型 默认值 说明
logical_name str 必传 OBIS 代码字符串,如 "0.0.41.0.0.255"
sap_assignment_list list None 初始 SAP 分配列表,格式 [(sap_id, device_name), ...]

方法

方法 签名 说明
connect_logical_device (sap_id, device_name) 添加或更新单个 SAP 条目。若 sap_id 已存在则更新设备名称,否则新增
deinit () 释放 C 堆内存

Python 属性一览

属性 类型 可写 说明
on_before_read Callable 读前钩子(继承 CosemObject)
on_after_read Callable 读后钩子
on_before_write Callable 写前钩子
on_after_write Callable 写后钩子
on_before_action Callable 动作前钩子
on_after_action Callable 动作后钩子
access_dict dict 实例级访问控制

事件处理(Event Handling)

每个 COSEM 对象暴露六个回调属性,在客户端请求处理的不同阶段被调用。回调是普通的 Python 函数,直接在对象上赋值即可。


DLMSEvent — 事件上下文

DLMSEvent 是服务器为每个客户端请求创建的事件对象,传递给所有回调。

属性 类型 说明
index int 属性编号(GET/SET)或方法编号(ACTION),1-based
selector int 选择器类型: 0 =无, 1 =范围, 2 =条目
is_action bool True 为 ACTION(方法调用), False 为 GET/SET
selector_params dict / None 选择器参数(见下表)
parameters int / None ACTION 请求的方法参数

selector_params

selector 含义 字典内容
0 无选择器 None
1 范围选择器 {"from_time": int, "to_time": int}
2 条目选择器 {"from_entry": int, "to_entry": int}

辅助方法

obj.idx("attr_name")      # 属性名 → 索引,如 obj.idx('buffer') → 2
obj.attr_name(index)      # 索引 → 属性名,如 obj.attr_name(2) → 'buffer'

六大回调

所有回调签名为 handler(self, event) 。默认均为 None (禁用),赋值为可调用对象即启用。

回调 触发时机 用途
on_before_read GET 响应序列化前 刷新值、返回自定义数据、拒绝访问
on_after_read GET 响应发送后 日志、审计
on_before_write SET 请求应用前 验证、拒绝写入
on_after_write SET 请求应用后 同步硬件、记录变更
on_before_action ACTION 分发前 执行方法逻辑
on_after_action ACTION 分发后 日志、审计

on_before_read 返回值

返回值 效果
None / True 默认处理,序列化当前属性值
False 拒绝请求,客户端收到 Access Violation
list 替代默认值,将返回的列表编码后发送给客户端(用于 ProfileGeneric)

常见模式

读前刷新传感器值

import dlms

energy_reg = dlms.Register("1.0.1.8.0.255", scaler=-3)

def refresh_value(self, event):
    if event.index == self.idx('value'):
        self.value = read_energy_sensor()  # 应用自定义
    return True

energy_reg.on_before_read = refresh_value

写前校验

def validate_write(self, event):
    if event.index == 2:  # value
        # event 中可获取待写入值
        pass  # return False 可拒绝写入
    return True

data.on_before_write = validate_write

驱动硬件(ACTION)

def relay_handler(self, event):
    if event.index == 1:      # remote_disconnect
        gpio_relay.value(0)
    elif event.index == 2:    # remote_reconnect
        gpio_relay.value(1)
    return True               # 允许内存状态同步

disconnect_ctl.on_before_action = relay_handler

ProfileGeneric 从外部存储提供数据

def provide_buffer(self, event):
    if event.index != self.idx('buffer'):
        return True
    if event.selector == 1:
        t_from = event.selector_params['from_time']
        t_to   = event.selector_params['to_time']
        return load_flash_rows(t_from, t_to)  # 返回 list
    return load_flash_rows(None, None)

profile.on_before_read = provide_buffer

写后审计日志

def audit_write(self, event):
    attr = self.attr_name(event.index)
    now  = utime.localtime()
    msg  = "WRITE {}.{} at {}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}".format(
        self.logical_name, attr,
        now[0], now[1], now[2], now[3], now[4], now[5])
    append_audit_log(msg)

clock.on_after_write = audit_write

线程安全要点

  • 每个连接在独立的后台线程中运行,回调在所属连接的线程中执行
  • SerialConnection / OpticalConnection / MobileConnection 在 C 系统线程中运行
  • GenericConnection 在调用 connect() 的 Python 线程中运行
  • Helios RTOS 无 GIL,共享可变对象需用 _thread.allocate_lock() 保护
  • 不要在回调中调用 server.stop() ,会导致死锁

安全模型(Security)

DLMS/COSEM 定义了分层安全模型:访问控制按属性/方法实施,认证决定客户端的信任级别,加密保护传输中的 PDU 内容。


访问控制(Access Dictionaries)

每个 COSEM 对象构造函数接受可选的 access 参数。字典以 Authentication 级别为键,内层字典以属性索引(1-based)映射到 AccessMode 常量。

import dlms

reg = dlms.Register("1.0.1.8.0.255", access={
    dlms.Authentication.NONE: {2: dlms.AccessMode.READ},
    dlms.Authentication.HIGH: {3: dlms.AccessMode.AUTHENTICATED_WRITE},
})

AccessMode 常量

常量 含义
AccessMode.NONE 0 无访问权限(客户端不可见)
AccessMode.READ 1 允许 GET,拒绝 SET
AccessMode.WRITE 2 允许 SET,拒绝 GET
AccessMode.READ_WRITE 3 允许 GET 和 SET
AccessMode.AUTHENTICATED_READ 4 仅认证客户端可 GET
AccessMode.AUTHENTICATED_WRITE 5 仅认证客户端可 SET
AccessMode.AUTHENTICATED_READ_WRITE 6 GET 和 SET 均需认证

Authentication 常量

常量 说明
Authentication.NONE 0 公开,无需密码
Authentication.LOW 1 明文密码认证
Authentication.HIGH 2 挑战-响应(HLS)
Authentication.HIGH_MD5 3 挑战-响应 + MD5
Authentication.HIGH_SHA1 4 挑战-响应 + SHA-1
Authentication.HIGH_GMAC 5 AES-GCM 认证+可选加密
Authentication.HIGH_SHA256 6 挑战-响应 + SHA-256
Authentication.HIGH_ECDSA 7 ECDSA 认证

类级默认值

dlms.set_default_access() 可为整个类设置默认权限,避免在每个对象上重复编写。优先级: 实例 access_dict > 构造函数 access > set_default_access > 默认(无权限)

# 所有 Register 对象默认公开可读
dlms.set_default_access(dlms.Register, {
    dlms.Authentication.NONE: {2: dlms.AccessMode.READ},
})

# 个别对象可覆盖
sensitive_reg = dlms.Register("1.0.1.8.1.255")
sensitive_reg.access_dict = {
    dlms.Authentication.HIGH: {2: dlms.AccessMode.AUTHENTICATED_READ_WRITE},
}

认证机制

AssociationLogicalName.auth_mechanism 选择该关联所需的认证级别。

说明
'None' 公开关联,无密码。适用于初始化阶段的逻辑设备,配合 access_dict 限制敏感属性
'Low' 明文密码认证。客户端在 AA-open 握手中提交明文密码,服务器与 secret 比对。仅适合光口等物理安全接口
'High' 挑战-响应(HLS)。双方交换随机挑战,用共享密钥哈希后返回摘要。需要 SecuritySetup security_policy = SecurityPolicy.NOTHING
'HighGMac' AES-GCM 认证(可选加密通信)。需要配置 SecuritySetup 的系统标题、GUEK 和 GAK。所有 APDU 受完整性保护

SecuritySetup 配置

dlms.SecuritySetup 是接口类 64(OBIS 0.0.43.0.X.255 ),每个安全上下文创建一个对象。

关键属性

属性 类型 说明
security_policy int SecurityPolicy 值(见下表)
security_suite int 密码套件: 0 =AES-128 GCM, 1 =ECDH/AES, 2 =ECDH/AES 变体
server_system_title bytes 服务器 8 字节系统标题(标识符 3 字节 + 序列号 5 字节)
client_system_title bytes 客户端 8 字节系统标题
guek bytes Global Unicast Encryption Key(16 或 32 字节)
gak bytes Global Authentication Key(16 或 32 字节)
min_invocation_counter int 最小调用计数器,防重放攻击

SecurityPolicy 值

常量 含义
SecurityPolicy.NOTHING 0 无密码层保护(用于 High HLS)
SecurityPolicy.AUTHENTICATED 1 认证所有 APDU
SecurityPolicy.ENCRYPTED 2 加密所有 APDU
SecurityPolicy.AUTHENTICATED_ENCRYPTED 3 认证+加密所有 APDU

最小 HighGMac 配置

import dlms

sec = dlms.SecuritySetup("0.0.43.0.2.255")
sec.security_policy = dlms.SecurityPolicy.AUTHENTICATED_ENCRYPTED
sec.security_suite = 0
sec.server_system_title = b'GRX12345'
sec.client_system_title = b'GRX00001'
sec.guek = b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F'
sec.gak  = b'\xD0\xD1\xD2\xD3\xD4\xD5\xD6\xD7\xD8\xD9\xDA\xDB\xDC\xDD\xDE\xDF'

调用计数器持久化

GCM 调用计数器随每个加密帧递增。重启后必须恢复,否则客户端会拒绝重放帧。推荐方案:使用 nocopy=True Data 对象引用 SecuritySetup 属性 6。

import dlms

# nocopy=True 表示 .value 持有对 (sec, 6) 的实时引用
inv_ctr = dlms.Data("0.0.43.1.2.255", nocopy=True)
inv_ctr.value = (sec, 6)  # 指向 SecuritySetup 属性 6(调用计数器)
inv_ctr.access_dict = {dlms.Authentication.HIGH_GMAC: {2: dlms.AccessMode.READ}}

启动时,反序列化后将 sec.min_invocation_counter 设为恢复值加上安全余量(如 +100)。


密码处理

  • AssociationLogicalName.secret AssociationShortName.secret 接受 bytes
  • 生产环境建议从安全存储读取密码和密钥,而非硬编码在固件中
  • 切勿在日志或调试输出中打印 secret 或密钥值
  • 每台设备的密钥应从根密钥和设备序列号派生,而非全设备相同

KEK(Key Encryption Key)

当管理客户端可能使用 Global-Key-Transfer 服务时,需通过 dlms.set_kek() 设置 16 字节 KEK。KEK 必须保存在安全存储中。

import dlms
import SecureData

KEK_INDEX = 1
buf = bytearray(16)
length = SecureData.Read(KEK_INDEX, buf, 16)
if length != 16:
    raise RuntimeError("KEK not provisioned (SecureData index {})".format(KEK_INDEX))
dlms.set_kek(bytes(buf))

首次写入 KEK:

import SecureData

kek = b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F'
SecureData.Store(KEK_INDEX, kek, 16)

AssociationLogicalName 配置

接口类 15(OBIS 0.0.40.0.X.255 ),每个客户端关联创建一个对象。

关键属性

属性 类型 说明
auth_mechanism str 'None' / 'Low' / 'High' / 'HighGMac'
secret bytes / None Low 或 High 认证的共享密码; None 用于 None 和 HighGMac
objects list 该关联可访问的 COSEM 对象列表
clientSAP int 客户端 SAP 号(管理用 SAP 1,公开用 SAP 16)
security_setup SecuritySetup / None High 和 HighGMac 必需;None 和 Low 为 None
context DLMSContext / None 可选,控制协商的 PDU 大小和一致性块

DLMSContext

属性 说明
conformance Conformance 常量位掩码(默认协商)
maxReceivePduSize 最大接收 PDU 大小(字节;0=使用连接默认)
maxSendPduSize 最大发送 PDU 大小(字节;0=使用连接默认)
dlmsVersionNumber DLMS 版本(6=DLMS/COSEM Ed. 7+)

双关联示例

import dlms

# --- 公开只读关联 ---
assoc_pub = dlms.AssociationLogicalName("0.0.40.0.1.255")
assoc_pub.auth_mechanism = 'None'
assoc_pub.clientSAP = 16
assoc_pub.objects = [clock, energy_reg, profile]

# --- 管理关联(HighGMac)---
assoc_mgmt = dlms.AssociationLogicalName("0.0.40.0.2.255")
assoc_mgmt.auth_mechanism = 'HighGMac'
assoc_mgmt.clientSAP = 1
assoc_mgmt.objects = [clock, energy_reg, profile, inv_ctr, sec]
assoc_mgmt.security_setup = sec
assoc_mgmt.context = dlms.DLMSContext(dlmsVersionNumber=6)

# 两个关联对象都需通过 server.add_object() 注册
server.add_object(assoc_pub)
server.add_object(assoc_mgmt)

Short Name 关联(AssociationShortName)

接口类 12(OBIS 0.0.40.0.0.255 )。SN 关联使用 16 位短名引用属性,减少帧开销。仅支持 Low 认证。

import dlms

assoc_sn = dlms.AssociationShortName("0.0.40.0.0.255")
assoc_sn.secret = b"PASSword"
assoc_sn.objects = [clock, energy_reg]

server.add_object(assoc_sn)

conn = dlms.MobileConnection(
    tcp_udp_setup=tcp_udp,
    gprs_setup=gprs,
    gsm_diag=gsm,
    recv_buffer=recv_buf,
    use_logical_name=False,  # SN 模式
)
server.add_connection(conn)

SN 和 LN 关联可在同一服务器上共存,互不冲突。

安全模式总示例

服务器

import dlms
from dlms import Conformance, AccessMode, Authentication
import utime

try:
    import SecureData
except ImportError:
    SecureData = None   # 未开启模块时给提示,见 main()

# ======================= 配置区(非机密,可硬编码) =======================
UART_PORT     = 2                 # 串口连接所在 UART
HDLC_BAUD     = 9600              # HDLC 波特率
HDLC_DEV_ADDR = 0x10              # HDLC 设备地址
SERIAL_NUMBER = 12345             # 电表序列号
FLAG_ID       = "QCT"             # 厂商代码(3 字符)

# 演示便利开关:机密未预置时自动写入默认值(生产必须 False!)
AUTO_PROVISION = True

# SecureData 槽位规划(16 槽)
SLOT_KEK         = 1    # 16B 主密钥 KEK
SLOT_PWD_LOW     = 2    # LOW  密码(8B)
SLOT_PWD_HIGH    = 3    # HIGH 密码(8B)
SLOT_GMAC_SERVER = 4    # 8B  服务器系统标题
SLOT_GMAC_CLIENT = 5    # 8B  客户端系统标题
SLOT_GMAC_GUEK   = 6    # 16B 单播加密密钥
SLOT_GMAC_GAK    = 7    # 16B 认证密钥
# =====================================================================


# ======================= 1) SecureData 工具(机密处理) =======================
def _store(index, data):
    if SecureData is None:
        raise RuntimeError("SecureData 模块未启用")
    if SecureData.Store(index, data, len(data)) < 0:
        raise RuntimeError("SecureData.Store(slot=%d) failed" % index)


def _read_exact(index, expect_len, name):
    if SecureData is None:
        raise RuntimeError("SecureData 模块未启用")
    buf = bytearray(expect_len)
    n = SecureData.Read(index, buf, expect_len)
    if n != expect_len:
        raise RuntimeError("%s 未预置 (slot=%d, 读到 %d/%d)"
                           % (name, index, n, expect_len))
    return bytes(buf)


def provision_secrets():
    """产线/首次启动执行一次,把默认机密写入安全存储。
    注意:每台设备的密钥应从根密钥 + 序列号派生,不要全设备相同。"""
    _store(SLOT_KEK,         bytes([0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,
                                    0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E,0x0F]))
    _store(SLOT_PWD_LOW,     b"low12345")
    _store(SLOT_PWD_HIGH,    b"high12345")
    _store(SLOT_GMAC_SERVER, b"GRX12345")
    _store(SLOT_GMAC_CLIENT, b"GRX54321")
    _store(SLOT_GMAC_GUEK,   bytes([0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17,
                                    0x18,0x19,0x1A,0x1B,0x1C,0x1D,0x1E,0x1F]))
    _store(SLOT_GMAC_GAK,    bytes([0xD0,0xD1,0xD2,0xD3,0xD4,0xD5,0xD6,0xD7,
                                    0xD8,0xD9,0xDA,0xDB,0xDC,0xDD,0xDE,0xDF]))
    print("[provision] 机密已写入 SecureData(生产部署后请删除本调用)")


def load_secrets():
    """从安全存储读取全部机密并配置。绝不 print 这些值。"""
    kek = _read_exact(SLOT_KEK, 16, "KEK")
    dlms.set_kek(kek)                       # 全局 KEK,必须 server.run() 前调用

    pwd_low  = _read_exact(SLOT_PWD_LOW,  8, "LOW password")
    pwd_high = _read_exact(SLOT_PWD_HIGH, 8, "HIGH password")
    st_server = _read_exact(SLOT_GMAC_SERVER, 8, "server system title")
    st_client = _read_exact(SLOT_GMAC_CLIENT, 8, "client system title")
    guek = _read_exact(SLOT_GMAC_GUEK, 16, "GUEK")
    gak  = _read_exact(SLOT_GMAC_GAK,  16, "GAK")
    return dict(pwd_low=pwd_low, pwd_high=pwd_high,
                st_server=st_server, st_client=st_client, guek=guek, gak=gak)


# ======================= 2) 业务对象(按认证等级控制访问) =======================
FULL_CONF = (
    Conformance.BLOCK_TRANSFER_WITH_ACTION | Conformance.BLOCK_TRANSFER_WITH_SET_OR_WRITE |
    Conformance.BLOCK_TRANSFER_WITH_GET_OR_READ | Conformance.SET |
    Conformance.SELECTIVE_ACCESS | Conformance.ACTION |
    Conformance.MULTIPLE_REFERENCES | Conformance.GET
)


def build_business():
    """业务对象:energy/voltage/config/low_read/high_read/ldn。"""
    energy = dlms.Register(
        "1.0.1.8.0.255", 12345, scaler=1,
        access={Authentication.NONE: {2: AccessMode.READ, 3: AccessMode.READ},
                Authentication.HIGH: {2: AccessMode.READ_WRITE, 3: AccessMode.READ}},
    )
    voltage = dlms.Register(
        "1.0.32.7.0.255", 230, scaler=1,
        access={Authentication.NONE: {2: AccessMode.READ}},
    )
    config = dlms.Register(
        "1.0.25.1.0.255", 0, scaler=0,
        access={Authentication.NONE: {2: AccessMode.NONE},
                Authentication.HIGH: {2: AccessMode.READ_WRITE}},
    )
    low_read = dlms.Register(
        "1.0.11.1.0.255", 200, scaler=0,
        access={Authentication.NONE: {2: AccessMode.NONE},
                Authentication.LOW:  {2: AccessMode.READ},
                Authentication.HIGH: {2: AccessMode.READ}},
    )
    high_read = dlms.Register(
        "1.0.12.1.0.255", 300, scaler=0,
        access={Authentication.NONE: {2: AccessMode.NONE},
                Authentication.LOW:  {2: AccessMode.NONE},
                Authentication.HIGH: {2: AccessMode.READ}},
    )
    ldn = dlms.Data("0.0.42.0.0.255",
                    access={Authentication.NONE: {2: AccessMode.READ}})
    ldn.value = b"SN12345"
    return [energy, voltage, config, low_read, high_read, ldn]


# ======================= 3) SecuritySetup =======================
def build_security(sec):
    """创建密码认证 + GMAC 用的 SecuritySetup 对象。"""
    # HIGH 密码认证用的 SecuritySetup:仅密码,不做 GMac/加密
    sec_high = dlms.SecuritySetup("0.0.43.0.1.255")
    sec_high.security_policy = dlms.SecurityPolicy.NOTHING

    # High GMAC 认证/加密用的 SecuritySetup
    sec_gmac = dlms.SecuritySetup("0.0.43.0.2.255")
    sec_gmac.security_policy     = dlms.SecurityPolicy.AUTHENTICATED_ENCRYPTED
    sec_gmac.security_suite      = 0
    sec_gmac.server_system_title = sec["st_server"]
    sec_gmac.client_system_title = sec["st_client"]
    sec_gmac.guek                = sec["guek"]
    sec_gmac.gak                 = sec["gak"]
    return sec_high, sec_gmac


# ======================= 4) 关联对象(密码/安全挂载点) =======================
def build_assocs(sec, business, sec_high, sec_gmac):
    """None / Low / High / HighGMac 四级关联对象。"""
    def ctx():
        return dlms.DLMSContext(maxSendPduSize=128, maxReceivePduSize=128,
                                conformance=FULL_CONF)

    assoc_none = dlms.AssociationLogicalName("0.0.40.0.1.255")
    assoc_none.auth_mechanism = "None"
    assoc_none.clientSAP      = 0x10
    assoc_none.objects        = list(business)
    assoc_none.context        = ctx()

    assoc_low = dlms.AssociationLogicalName("0.0.40.0.2.255")
    assoc_low.auth_mechanism  = "Low"
    assoc_low.secret          = sec["pwd_low"]      # <- LOW 密码
    assoc_low.clientSAP       = 2
    assoc_low.objects         = list(business)
    assoc_low.security_setup  = sec_high
    assoc_low.context         = ctx()

    assoc_high = dlms.AssociationLogicalName("0.0.40.0.3.255")
    assoc_high.auth_mechanism = "High"
    assoc_high.secret         = sec["pwd_high"]     # <- HIGH 密码
    assoc_high.clientSAP      = 5
    assoc_high.objects        = list(business)
    assoc_high.security_setup = sec_high
    assoc_high.context        = ctx()

    assoc_gmac = dlms.AssociationLogicalName("0.0.40.0.4.255")
    assoc_gmac.auth_mechanism = "HighGMac"
    assoc_gmac.clientSAP      = 4
    assoc_gmac.objects        = list(business)
    assoc_gmac.security_setup = sec_gmac
    assoc_gmac.context        = ctx()

    return assoc_none, assoc_low, assoc_high, assoc_gmac


# ======================= 5) 服务器主流程 =======================
def build_server():
    """组装服务器并返回(含连接)。"""
    # 读取机密 + 设置 KEK
    try:
        sec = load_secrets()
    except RuntimeError:
        if not AUTO_PROVISION:
            raise
        print("[!] 机密未预置,演示模式自动写入默认值(生产请关闭 AUTO_PROVISION)")
        provision_secrets()
        sec = load_secrets()

    # SecuritySetup + 业务对象 + 关联对象
    business = build_business()
    sec_high, sec_gmac = build_security(sec)
    assocs = build_assocs(sec, business, sec_high, sec_gmac)

    # 连接:串口(HDLC)。光学/移动/通用连接同理换类型即可
    hdlc = dlms.IecHdlcSetup(
        "0.0.22.0.0.255",
        commSpeed=HDLC_BAUD, windowSizeRx=1, windowSizeTx=1,
        maxInfoLenTx=128, maxInfoLenRx=128, timeout=120, deviceAddr=HDLC_DEV_ADDR,
    )
    conn = dlms.SerialConnection(uart_port=UART_PORT, hdlc_setup=hdlc,
                                 use_logical_name=True)

    # 注册对象 + 连接
    server = dlms.Server(serial_number=SERIAL_NUMBER, flag_id=FLAG_ID)
    for obj in business + [sec_high, sec_gmac]:
        server.add_object(obj)
    for a in assocs:
        server.add_object(a)
    server.add_connection(conn)
    return server


def main():
    print("=" * 60)
    print("[Server] DLMS 安全服务器 Demo(密码 + GMAC + KEK)")
    print("=" * 60)
    if SecureData is None:
        print("[Error] SecureData 模块未启用(需 MICROPY_QPY_MODULE_SECUREDATA)")
        return -1

    server = build_server()
    server.run()
    print("[Server] 已启动,等待客户端(None/Low/High/HighGMac)接入...")
    try:
        while True:
            utime.sleep(1)
    except KeyboardInterrupt:
        server.stop()
        print("[Server] 已停止")
    return 0


if __name__ == "__main__":
    main()


客户端

import dlms
from dlms import Authentication

try:
    import SecureData
except ImportError:
    SecureData = None

# ======================= 配置区(非机密,可硬编码) =======================
UART_PORT     = 2                  # 客户端所在 UART(与服务器 UART2 对连)
HDLC_BAUD     = 9600               # 与服务器 IecHdlcSetup.commSpeed 一致
HDLC_DEV_ADDR = 0x10
SERVER_SERIAL = 12345              # 服务器序列号

# 演示便利开关:机密未预置时自动写入默认值(生产必须 False!)
AUTO_PROVISION = True

# SecureData 槽位规划(与服务器完全一致)
SLOT_KEK         = 1    # 16B 主密钥 KEK
SLOT_PWD_LOW     = 2    # LOW  密码(8B)
SLOT_PWD_HIGH    = 3    # HIGH 密码(8B)
SLOT_GMAC_SERVER = 4    # 8B  服务器系统标题
SLOT_GMAC_CLIENT = 5    # 8B  客户端系统标题
SLOT_GMAC_GUEK   = 6    # 16B 单播加密密钥
SLOT_GMAC_GAK    = 7    # 16B 认证密钥
# =====================================================================


# ======================= 1) SecureData 工具(与服务器同款) =======================
def _store(index, data):
    if SecureData is None:
        raise RuntimeError("SecureData 模块未启用")
    if SecureData.Store(index, data, len(data)) < 0:
        raise RuntimeError("SecureData.Store(slot=%d) failed" % index)


def _read_exact(index, expect_len, name):
    if SecureData is None:
        raise RuntimeError("SecureData 模块未启用")
    buf = bytearray(expect_len)
    n = SecureData.Read(index, buf, expect_len)
    if n != expect_len:
        raise RuntimeError("%s 未预置 (slot=%d, 读到 %d/%d)"
                           % (name, index, n, expect_len))
    return bytes(buf)


def provision_secrets():
    """产线/首次启动执行一次:客户端侧也写入同一套默认机密。"""
    _store(SLOT_KEK,         bytes([0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,
                                    0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E,0x0F]))
    _store(SLOT_PWD_LOW,     b"low12345")
    _store(SLOT_PWD_HIGH,    b"high12345")
    _store(SLOT_GMAC_SERVER, b"GRX12345")
    _store(SLOT_GMAC_CLIENT, b"GRX54321")
    _store(SLOT_GMAC_GUEK,   bytes([0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17,
                                    0x18,0x19,0x1A,0x1B,0x1C,0x1D,0x1E,0x1F]))
    _store(SLOT_GMAC_GAK,    bytes([0xD0,0xD1,0xD2,0xD3,0xD4,0xD5,0xD6,0xD7,
                                    0xD8,0xD9,0xDA,0xDB,0xDC,0xDD,0xDE,0xDF]))
    print("[provision] 客户端机密已写入 SecureData(生产部署后请删除)")


def load_secrets():
    """从安全存储读取客户端所需机密。绝不 print 这些值。"""
    return dict(
        pwd_low  = _read_exact(SLOT_PWD_LOW,  8, "LOW password"),
        pwd_high = _read_exact(SLOT_PWD_HIGH, 8, "HIGH password"),
        st_client = _read_exact(SLOT_GMAC_CLIENT, 8, "client system title"),
        guek = _read_exact(SLOT_GMAC_GUEK, 16, "GUEK"),
        gak  = _read_exact(SLOT_GMAC_GAK,  16, "GAK"),
    )


# ======================= 2) 串口连接(C 层驱动) =======================
def _build_serial_conn():
    """构造 C 层驱动的 SerialConnection(8N1 HDLC,客户端模式)。

    dlms_client_connect() 识别到 SerialConnection 后会自动:
      - 用 IecHdlcSetup.commSpeed 打开 UART(8N1)
      - 收 0x7E..0x7E HDLC 帧(按长度字段定位)
    无需 Python 开 UART / 解析帧。
    """
    hdlc = dlms.IecHdlcSetup("0.0.22.0.0.255",
                             commSpeed=HDLC_BAUD, deviceAddr=HDLC_DEV_ADDR)
    return dlms.SerialConnection(uart_port=UART_PORT, hdlc_setup=hdlc,
                                 use_logical_name=True)


# ======================= 3) 业务对象(与服务器 OBIS 一致) =======================
LN_ENERGY  = "1.0.1.8.0.255"
LN_VOLT    = "1.0.32.7.0.255"
LN_CONFIG  = "1.0.25.1.0.255"
LN_LOWREAD = "1.0.11.1.0.255"
LN_HIGHREAD = "1.0.12.1.0.255"


def _read_attr(client, ln, attr, name):
    obj = dlms.Register(ln, 0)
    try:
        client.read(obj, attr)
        print("[OK  ] {} attr{} = {}".format(name, attr, getattr(obj, "value", None)))
    except Exception as e:
        print("[FAIL] {} attr{} : {}".format(name, attr, e))


def _write_attr(client, ln, attr, value, name):
    obj = dlms.Register(ln, 0)
    obj.value = value
    try:
        client.write(obj, attr)
        print("[OK  ] {} attr{} 写入 {} 成功".format(name, attr, value))
    except Exception as e:
        print("[FAIL] {} attr{} 写入 : {}".format(name, attr, e))


# ======================= 4) HIGH 密码认证 Demo =======================
def demo_high(sec):
    print("--- HIGH 密码认证(HLS 挑战-响应) ---")
    conn = _build_serial_conn()
    try:
        client = dlms.Client(
            client_address=5,                              # 对应 assoc_high.clientSAP
            server_address=dlms.hdlc_server_address(SERVER_SERIAL),
            authentication=dlms.Authentication.HIGH,
            password=sec["pwd_high"].decode(),             # str,与 assoc_high.secret 一致
            security=0x00,                                 # 密码认证不加密
        )
        client.connect(conn)                               # C 层开 UART(8N1) + HDLC 收发
        print("[HIGH] 关联成功")
        _read_attr(client, LN_ENERGY, 2, "energy")
        _read_attr(client, LN_CONFIG, 2, "config")
        _write_attr(client, LN_CONFIG, 2, 999, "config")   # HIGH 可写
        try:
            client.disconnect()                            # C 层 deinit UART
        except Exception:
            pass
    except Exception as e:
        print("[HIGH] 失败: {}".format(e))


# ======================= 5) High GMAC 认证/加密 Demo =======================
def demo_gmac(sec):
    print("--- High GMAC 认证/加密 ---")
    conn = _build_serial_conn()
    try:
        client = dlms.Client(
            client_address=4,                              # 对应 assoc_gmac.clientSAP
            server_address=dlms.hdlc_server_address(SERVER_SERIAL),
            authentication=dlms.Authentication.HIGH_GMAC,
            system_title=sec["st_client"],                 # 8B,与服务器 client_system_title 一致
            authentication_key=sec["gak"],                 # 16B,与 sec_gmac.gak 一致
            block_cipher_key=sec["guek"],                  # 16B,与 sec_gmac.guek 一致
            security=0x30,                                 # AUTHENTICATION_ENCRYPTION
        )
        client.connect(conn)                               # C 层开 UART(8N1) + HDLC 收发
        print("[GMAC] 关联成功(GMac 认证 + 加密)")
        _read_attr(client, LN_ENERGY, 2, "energy")
        try:
            client.disconnect()                            # C 层 deinit UART
        except Exception:
            pass
    except Exception as e:
        print("[GMAC] 失败: {}".format(e))


# ======================= 6) 主流程 =======================
def main():
    print("=" * 60)
    print("[Client] DLMS 安全客户端 Demo(HIGH 密码 + High GMAC)")
    print("[Client] 服务器序列号 {}  UART{}  {} bps".format(
        SERVER_SERIAL, UART_PORT, HDLC_BAUD))
    print("=" * 60)
    if SecureData is None:
        print("[Error] SecureData 模块未启用(需 MICROPY_QPY_MODULE_SECUREDATA)")
        return -1

    try:
        sec = load_secrets()
    except RuntimeError:
        if not AUTO_PROVISION:
            raise
        print("[!] 客户端机密未预置,演示模式自动写入默认值(生产请关闭 AUTO_PROVISION)")
        provision_secrets()
        sec = load_secrets()

    demo_high(sec)
    print()
    demo_gmac(sec)
    return 0


if __name__ == "__main__":
    main()

连接(Connection)

连接表示 DLMS 客户端与服务器通信的物理或逻辑传输通道。每种连接类型封装不同的底层介质(UART、光口、蜂窝 UDP 或自定义 Python I/O),均通过 server.add_connection(conn) 添加到服务器。调用 server.start() 后,服务器为每个连接启动一个独立线程。


连接方式 直接必需对象 可选/间接对象 典型场景
SerialConnection IecHdlcSetup UART 上跑 HDLC
OpticalConnection LocalPortSetup + IecHdlcSetup Mode E 协商后跑 HDLC
MobileConnection TcpUdpSetup + GprsSetup + GsmDiagnostic + recv_buffer relay_tcp_setup 可选, IPv4Setup 常经 TcpUdpSetup 间接关联 蜂窝 TCP/UDP
GenericConnection ( HDLC ) IecHdlcSetup 自定义承载上的 HDLC
GenericConnection ( WRAPPER ) TcpUdpSetup IPv4Setup 间接 自定义承载上的 WRAPPER
GenericConnection ( HDLC_WITH_MODE_E ) IecHdlcSetup + LocalPortSetup 自定义承载上的 Mode E + HDLC

SerialConnection

串口连接。内部在 C 线程中管理 UART。适用于直连场景,无需自定义传输逻辑。

服务器示例

# -*- coding: utf-8 -*-
"""
DLMS 服务器端脚本(模组 A / 电表)- 统一 NONE/LOW/HIGH 认证版
==================================================================
通过 SerialConnection(HDLC over UART)对外提供 DLMS/COSEM 服务。
一个服务器同时注册三个关联对象,分别对应三种认证级别,
客户端按需选择连接

三种认证级别(客户端 SAP 各不相同,用于区分连接):
  * NONE (0x10): 无认证,直接关联
  * LOW  (0x20): 密码认证(LOW),AARE 阶段校验 assoc.secret
  * HIGH (0x30): HLS 挑战-响应认证,secret 作为挑战密钥

对象访问控制(利用 access_control.c 的级联语义):
  * energy / voltage / ext_energy / param / ldn:
      配置 {Authentication.NONE: {...}} —— 所有认证级别(含 LOW/HIGH)
      都能命中该配置,属性2 读写权限按配置生效。
  * config_reg (1.0.25.1.0.255):
      仅配置 {Authentication.HIGH: {2: READ_WRITE}} ——
      NONE / LOW 连接下级联匹配不到,落到 events.c 的 C 兜底(只读),
      写入被拒(READ_WRITE_DENIED);仅 HIGH 连接可写。

接线(模组 A  <->  模组 B):
    A.UART_TX  ----> B.UART_RX
    A.UART_RX  <---- B.UART_TX
    A.GND      ----  B.GND      (共地是必须的,否则电平无法判定)

关键点:
  * SerialConnection 的客户端侧强制使用 HDLC 帧格式,
    因此服务器端 interface_type 必须保持默认 HDLC(不要用 WRAPPER)。
  * 两端波特率(commSpeed)必须一致。
  * 客户端 client_address 必须等于对应关联对象的 clientSAP。
"""

import dlms
from dlms import Conformance
import utime

# ============================ 配置区 ============================
UART_PORT   = 2        # 模组 A 使用的 UART 口
BAUD        = 9600     # 波特率,必须与客户端一致
FLAG_ID     = "GRX"    # 厂商代码(3 字符)
SERIAL_NUM  = 12345    # 电表序列号(≤5 位,Python 模式由 set_serial_number 设置)
PASSWORD    = "Quectel"  # LOW/HIGH 认证密码(LOW 直接比对,HIGH 用作挑战密钥)

# 三种认证级别各自的客户端 SAP(必须互不相同,客户端按此选路)
SAP_NONE = 0x10    # NONE 认证关联对象
SAP_LOW  = 0x20    # LOW  认证关联对象
SAP_HIGH = 0x30    # HIGH 认证关联对象

# 服务器地址 = 序列号 % 10000 + 1000
#   Python 服务器模式:12345 -> 2345 + 1000 = 3345
SERVER_ADDR = SERIAL_NUM % 10000 + 1000   # = 3345,同时作为 IecHdlcSetup 设备地址
# ================================================================

# Python 服务器模式(CONFIG_DLMS_BUILTIN_SERVER 关闭):events.c 生效,
# svr_isTarget 读取 SRV_SERIAL_NUMBER(由本调用设置),
# 客户端 server_address 必须 == 该值 % 10000 + 1000。
dlms.set_serial_number(SERIAL_NUM)

# ============================ COSEM 对象 ============================
# 电能寄存器:属性2(value) 可读可写 —— 所有认证级别可写
energy = dlms.Register(
    "1.0.1.8.0.255",
    default_value=12345,
    scaler=1,
    access={
        dlms.Authentication.NONE: {
            2: dlms.AccessMode.READ_WRITE,   # value:客户端可写
            3: dlms.AccessMode.READ,         # scaler/unit
        }
    }
)

# 电压寄存器:只读 —— 所有认证级别只读
voltage = dlms.Register(
    "1.0.32.7.0.255",
    default_value=230,
    scaler=1,
    access={
        dlms.Authentication.NONE: {
            2: dlms.AccessMode.READ,
            3: dlms.AccessMode.READ,
        }
    }
)

# 高权限寄存器:仅 HIGH 认证可写 —— 演示"部分对象需要更高权限"
# 只配置 Authentication.HIGH 级,NONE/LOW 连接写会被拒(READ_WRITE_DENIED)。
config_reg = dlms.Register(
    "1.0.25.1.0.255",          # 费率/参数配置寄存器(示意)
    default_value=0,
    scaler=0,
    access={
        dlms.Authentication.HIGH: {
            2: dlms.AccessMode.READ_WRITE,   # value:仅 HIGH 可写
            3: dlms.AccessMode.READ,
        }
    }
)

# 扩展寄存器(ExtendedRegister,COSEM class 4):value 可读写
ext_energy = dlms.ExtendedRegister(
    "1.0.1.8.1.255",
    value=500,
    scaler=0,
    unit=dlms.Unit.ACTIVE_ENERGY,
    status=0,
    access={
        dlms.Authentication.NONE: {
            2: dlms.AccessMode.READ_WRITE,   # value:客户端可写
            3: dlms.AccessMode.READ,         # scaler/unit
            4: dlms.AccessMode.READ,         # status
            5: dlms.AccessMode.READ,         # capture_time
        }
    }
)
ext_energy.capture_time = (2026, 8, 20, 12, 0, 0)

# 参数对象:可读可写
param = dlms.Data(
    "0.0.1.1.0.255",
    access={dlms.Authentication.NONE: {2: dlms.AccessMode.READ_WRITE}}
)
param.value = 42

# 逻辑设备名 LDN:只读
ldn = dlms.Data(
    "0.0.42.0.0.255",
    access={dlms.Authentication.NONE: {2: dlms.AccessMode.READ}}
)
ldn.value = b"SN12345"

# ------------------------------------------------------------------
# 读取权限分级演示对象(仅演示 attr2 的读权限)
# ------------------------------------------------------------------

# 公开只读寄存器:NONE 认证即可读(所有认证级别都能读)
public_read_reg = dlms.Register(
    "1.0.10.1.0.255",          # 公开读数寄存器(示意)
    default_value=100,
    scaler=0,
    access={
        dlms.Authentication.NONE: {
            2: dlms.AccessMode.READ,   # NONE 可读(级联后 LOW/HIGH 也可读)
        }
    }
)

# LOW 级可读寄存器:只有 LOW(及以上) 认证才可读。
# 关键:必须显式把 NONE 级配置为 AccessMode.NONE(封死),否则 NONE 连接会
# 级联不到任何配置、落到 events.c 的 C 兜底(NONE 默认 READ,反而可读)。
low_read_reg = dlms.Register(
    "1.0.11.1.0.255",          # 需 LOW 权限的读数寄存器(示意)
    default_value=200,
    scaler=0,
    access={
        dlms.Authentication.NONE: {
            2: dlms.AccessMode.NONE,   # NONE 连接:不可读
        },
        dlms.Authentication.LOW: {
            2: dlms.AccessMode.READ,   # LOW 连接:可读
        },
        dlms.Authentication.HIGH: {
            2: dlms.AccessMode.READ,   # HIGH 连接:可读(级联命中 HIGH 级)
        },
    }
)

# HIGH 级可读寄存器:只有 HIGH 认证才可读。
# NONE / LOW 级都显式配置为 AccessMode.NONE 封死。
high_read_reg = dlms.Register(
    "1.0.12.1.0.255",          # 需 HIGH 权限的读数寄存器(示意)
    default_value=300,
    scaler=0,
    access={
        dlms.Authentication.NONE: {
            2: dlms.AccessMode.NONE,   # NONE 连接:不可读
        },
        dlms.Authentication.LOW: {
            2: dlms.AccessMode.NONE,   # LOW 连接:不可读
        },
        dlms.Authentication.HIGH: {
            2: dlms.AccessMode.READ,   # HIGH 连接:可读
        },
    }
)

# ------------------------------------------------------------------
# 类型级兜底访问控制(确保写权限一定生效)
# ------------------------------------------------------------------
dlms.set_default_access(
    dlms.Register,
    {
        dlms.Authentication.NONE: {
            2: dlms.AccessMode.READ_WRITE,
            3: dlms.AccessMode.READ,
        }
    }
)
dlms.set_default_access(
    dlms.Data,
    {
        dlms.Authentication.NONE: {
            2: dlms.AccessMode.READ_WRITE,
        }
    }
)

# ------------------ HDLC 链路层配置 ------------------
hdlc = dlms.IecHdlcSetup(
    "0.0.22.0.0.255",
    commSpeed=BAUD,
    windowSizeRx=1,
    windowSizeTx=1,
    maxInfoLenTx=128,
    maxInfoLenRx=128,
    timeout=120,
    deviceAddr=SERVER_ADDR,
)

# 服务器上注册的全部业务对象(关联对象各自持有自己的对象列表视图)
all_objects = [
    energy, voltage, config_reg,
    public_read_reg, low_read_reg, high_read_reg,
    param, ldn, hdlc, ext_energy,
]

# ------------------ 关联对象 1:NONE 认证 ------------------
assoc_none = dlms.AssociationLogicalName("0.0.40.0.1.255")
assoc_none.auth_mechanism = "None"
assoc_none.clientSAP = SAP_NONE
assoc_none.objects = list(all_objects)
assoc_none.context = dlms.DLMSContext(
    maxSendPduSize=128,
    maxReceivePduSize=128,
    conformance=Conformance.BLOCK_TRANSFER_WITH_ACTION | Conformance.BLOCK_TRANSFER_WITH_SET_OR_WRITE |
                Conformance.BLOCK_TRANSFER_WITH_GET_OR_READ | Conformance.SET |
                Conformance.SELECTIVE_ACCESS | Conformance.ACTION |
                Conformance.MULTIPLE_REFERENCES | Conformance.GET,
)

# ------------------ 关联对象 2:LOW 认证 ------------------
assoc_low = dlms.AssociationLogicalName("0.0.40.0.2.255")
assoc_low.auth_mechanism = "Low"
assoc_low.secret = b"Quectel"        # 密码,必须与 LOW 客户端 password 一致
assoc_low.clientSAP = SAP_LOW
assoc_low.objects = list(all_objects)
assoc_low.context = dlms.DLMSContext(
    maxSendPduSize=128,
    maxReceivePduSize=128,
    conformance=Conformance.BLOCK_TRANSFER_WITH_ACTION | Conformance.BLOCK_TRANSFER_WITH_SET_OR_WRITE |
                Conformance.BLOCK_TRANSFER_WITH_GET_OR_READ | Conformance.SET |
                Conformance.SELECTIVE_ACCESS | Conformance.ACTION |
                Conformance.MULTIPLE_REFERENCES | Conformance.GET,
)

# ------------------ 关联对象 3:HIGH 认证 ------------------
assoc_high = dlms.AssociationLogicalName("0.0.40.0.3.255")
assoc_high.auth_mechanism = "High"
assoc_high.secret = b"Quectel"       # HLS 挑战密钥,必须与 HIGH 客户端 password 一致
assoc_high.clientSAP = SAP_HIGH
assoc_high.objects = list(all_objects)
assoc_high.context = dlms.DLMSContext(
    maxSendPduSize=128,
    maxReceivePduSize=128,
    conformance=Conformance.BLOCK_TRANSFER_WITH_ACTION | Conformance.BLOCK_TRANSFER_WITH_SET_OR_WRITE |
                Conformance.BLOCK_TRANSFER_WITH_GET_OR_READ | Conformance.SET |
                Conformance.SELECTIVE_ACCESS | Conformance.ACTION |
                Conformance.MULTIPLE_REFERENCES | Conformance.GET,
)

# ============================ 服务器 ============================
server = dlms.Server(serial_number=SERIAL_NUM, flag_id=FLAG_ID)
for obj in all_objects + [assoc_none, assoc_low, assoc_high]:
    server.add_object(obj)

# SerialConnection:C 驱动 UART,服务器端自动监听并回包
serial_conn = dlms.SerialConnection(
    uart_port=UART_PORT,
    hdlc_setup=hdlc,
    flowcontrol=0,
    interface_type=dlms.InterfaceType.HDLC,  # 必须 HDLC(与客户端一致)
    use_logical_name=True,
)

server.add_connection(serial_conn)
server.run()   # 非阻塞,连接在后台线程中监听
print("[Server] DLMS server (NONE/LOW/HIGH) running on UART{} @ {} baud".format(UART_PORT, BAUD))
print("[Server] Serial num={}, Server addr={}, password={}".format(SERIAL_NUM, SERVER_ADDR, PASSWORD))
print("[Server] SAPs: NONE=0x{:02X} LOW=0x{:02X} HIGH=0x{:02X}".format(SAP_NONE, SAP_LOW, SAP_HIGH))
print("[Server] Objects: energy(1.0.1.8.0.255) voltage(1.0.32.7.0.255) config(HIGH-w) ext(1.0.1.8.1.255) param")
print("[Server] Read levels: public(1.0.10.1.0.255,NONE+) low(1.0.11.1.0.255,LOW+) high(1.0.12.1.0.255,HIGH)")

# ============================ 主循环 ============================
# 模拟电表周期采集,更新寄存器值(客户端可读到变化的电压)
try:
    seq = 0
    while True:
        seq += 1
        v = 228 + (seq % 7)          # 230/229/... 波动,模拟真实采样
        voltage.value = v
        print("[Server] sample: voltage={} V, energy={}".format(v, energy.value))
        utime.sleep(5)
except KeyboardInterrupt:
    server.stop()
    print("[Server] stopped")

客户端(NONE)

# -*- coding: utf-8 -*-
"""
DLMS 客户端脚本(模组 B / 集中器)- NONE 认证版
==================================================================
通过 SerialConnection(HDLC over UART)读取/写入服务器(模组 A),

本脚本使用 NONE 认证连接(client_address = 0x10,对应服务器 NONE 关联):
  * energy / ext_energy / param: 可读可写(服务器配置了 NONE 级 READ_WRITE)
  * voltage:                      只读
  * config_reg (1.0.25.1.0.255): 可读,写入会被拒绝(仅 HIGH 可写)

接线(模组 A  <->  模组 B):
    B.UART_RX  <---- A.UART_TX
    B.UART_TX  ----> A.UART_RX
    B.GND      ----  A.GND      (共地是必须的)

关键点:
  * client_address 必须等于对应认证级别关联对象的 clientSAP(0x10)。
  * server_address 必须等于 服务器序列号 % 10000 + 1000。
  * 波特率必须与服务器端一致(commSpeed)。
"""

import dlms
import utime

# ============================ 配置区 ============================
UART_PORT = 2        # 模组 B 使用的 UART 口
BAUD      = 9600     # 波特率,必须与服务器端一致

# 服务器端序列号 —— 必须与服务器脚本中的 SERIAL_NUM 一致!
SERVER_SERIAL = 12345

CLIENT_ADDR = 0x10   # NONE 认证关联对象的 clientSAP
# 服务器地址 = 序列号 % 10000 + 1000
SERVER_ADDR = SERVER_SERIAL % 10000 + 1000   # = 3345
AUTH        = dlms.Authentication.NONE       # NONE 认证

# 演示用对象的逻辑名(均为服务器上真实存在的对象)
LN_ENERGY = "1.0.1.8.0.255"    # 电能寄存器(Register,可写)
LN_VOLT   = "1.0.32.7.0.255"   # 电压寄存器(Register,只读)
LN_EXT    = "1.0.1.8.1.255"    # 扩展电能寄存器(ExtendedRegister,可写)
LN_PARAM  = "0.0.1.1.0.255"    # 参数 Data(可写)
LN_CONFIG = "1.0.25.1.0.255"   # 高权限寄存器(Register,仅 HIGH 认证可写)
LN_PUBLIC = "1.0.10.1.0.255"   # 公开只读寄存器(NONE+ 可读)
LN_LOWREAD = "1.0.11.1.0.255"  # LOW+ 可读寄存器(NONE 下不可读)
LN_HIGHREAD = "1.0.12.1.0.255" # HIGH 才可读寄存器(NONE/LOW 下不可读)
# ================================================================

# ------------------ HDLC 链路层配置(必须与服务器一致) ------------------
hdlc = dlms.IecHdlcSetup(
    "0.0.22.0.0.255",
    commSpeed=BAUD,
    windowSizeRx=1,
    windowSizeTx=1,
    maxInfoLenTx=128,
    maxInfoLenRx=128,
    timeout=120,
    deviceAddr=0x10,
)

serial_conn = dlms.SerialConnection(
    uart_port=UART_PORT,
    hdlc_setup=hdlc,
    flowcontrol=0,
    interface_type=dlms.InterfaceType.HDLC,  # 必须 HDLC
    use_logical_name=True,
)

client = dlms.Client(
    client_address=CLIENT_ADDR,
    server_address=SERVER_ADDR,
    authentication=AUTH,
    use_logical_name=True,
)


def read_attr(ln, attr):
    """读取单个属性,失败返回 None 并打印错误。"""
    try:
        val = client.read(ln, attr)
        print("[Read ] {} attr{} = {}".format(ln, attr, val))
        return val
    except Exception as e:
        print("[Read ] {} attr{} FAILED: {}".format(ln, attr, e))
        return None


def write_attr(ln, attr, value):
    """写入单个属性,返回是否成功。"""
    try:
        client.write(ln, attr, value)
        print("[Write] {} attr{} = {} OK".format(ln, attr, value))
        return True
    except Exception as e:
        print("[Write] {} attr{} = {} FAILED: {}".format(ln, attr, value, e))
        return False


def run_once():
    """单轮测试:读取所有对象 -> 写入 -> 回读验证。"""
    print("-" * 50)

    # 1) 读取
    volt = read_attr(LN_VOLT, 2)          # 电压寄存器值(只读)
    read_attr(LN_ENERGY, 2)               # 电能寄存器值
    read_attr(LN_EXT, 2)                  # 扩展电能寄存器值
    read_attr(LN_PARAM, 2)                # 参数

    # 2) 写电能寄存器(属性2 value)—— NONE 下可写
    new_energy = (volt or 0) + 1000
    write_attr(LN_ENERGY, 2, new_energy)

    # 3) 写扩展电能寄存器 + 参数对象 —— NONE 下可写
    write_attr(LN_EXT, 2, 888)
    write_attr(LN_PARAM, 2, 100)

    # 4) 高权限寄存器:NONE 连接下可读但不可写(期望 READ_WRITE_DENIED)
    read_attr(LN_CONFIG, 2)
    write_attr(LN_CONFIG, 2, 9999)

    # 5) 读取权限分级验证(NONE 连接)
    read_attr(LN_PUBLIC, 2)     # 期望:OK(NONE 可读)
    read_attr(LN_LOWREAD, 2)    # 期望:FAILED(仅 LOW+ 可读)
    read_attr(LN_HIGHREAD, 2)   # 期望:FAILED(仅 HIGH 可读)

    # 6) 回读验证
    read_attr(LN_ENERGY, 2)
    read_attr(LN_EXT, 2)
    read_attr(LN_PARAM, 2)


def main():
    print("[Client] Connecting to server via UART{} @ {} baud (NONE auth)...".format(UART_PORT, BAUD))
    print("[Client] client_addr=0x{:02X}, server_addr={}".format(CLIENT_ADDR, SERVER_ADDR))

    while True:
        try:
            client.connect(serial_conn)     # 阻塞直到关联(AARQ/AARE)完成
            print("[Client] connected & associated! (NONE auth)")
            run_once()
            break
        except Exception as e:
            print("[Client] connect/run failed: {}".format(e))
            utime.sleep(3)

    try:
        while True:
            utime.sleep(10)
            read_attr(LN_VOLT, 2)          # 周期性读取服务器采样的电压
            read_attr(LN_ENERGY, 2)
    except KeyboardInterrupt:
        pass
    finally:
        try:
            client.disconnect()
            print("[Client] disconnected")
        except Exception:
            pass


main()

客户端(LOW)

# -*- coding: utf-8 -*-
"""
DLMS 客户端脚本(模组 B / 集中器)- LOW 认证版
==================================================================
通过 SerialConnection(HDLC over UART)读取/写入服务器(模组 A)

本脚本使用 LOW 认证连接(client_address = 0x20,对应服务器 LOW 关联):
  * 密码认证:password 必须与服务器 assoc_low.secret 一致,
    否则 AARE 以 AUTHENTICATION_FAILURE 拒绝关联。
  * energy / ext_energy / param: 可读可写(服务器 NONE 级 READ_WRITE
    通过 access_control.c 级联语义在 LOW 连接下依然生效)
  * voltage:                      只读
  * config_reg (1.0.25.1.0.255): 可读,写入会被拒绝(仅 HIGH 可写)

接线(模组 A  <->  模组 B):
    B.UART_RX  <---- A.UART_TX
    B.UART_TX  ----> A.UART_RX
    B.GND      ----  A.GND      (共地是必须的)

关键点:
  * client_address 必须等于对应认证级别关联对象的 clientSAP(0x20)。
  * server_address 必须等于 服务器序列号 % 10000 + 1000。
  * 波特率必须与服务器端一致(commSpeed)。
"""

import dlms
import utime

# ============================ 配置区 ============================
UART_PORT = 2        # 模组 B 使用的 UART 口
BAUD      = 9600     # 波特率,必须与服务器端一致

# 服务器端序列号 —— 必须与服务器脚本中的 SERIAL_NUM 一致!
SERVER_SERIAL = 12345

CLIENT_ADDR = 0x20   # LOW 认证关联对象的 clientSAP(dlms_server.py 中 SAP_LOW)
# 服务器地址 = 序列号 % 10000 + 1000
SERVER_ADDR = SERVER_SERIAL % 10000 + 1000   # = 3345
AUTH        = dlms.Authentication.LOW        # LOW 认证(密码)
PASSWORD    = "Quectel"                      # 必须与服务器 assoc_low.secret 一致

# 演示用对象的逻辑名(均为服务器上真实存在的对象)
LN_ENERGY = "1.0.1.8.0.255"    # 电能寄存器(Register,可写)
LN_VOLT   = "1.0.32.7.0.255"   # 电压寄存器(Register,只读)
LN_EXT    = "1.0.1.8.1.255"    # 扩展电能寄存器(ExtendedRegister,可写)
LN_PARAM  = "0.0.1.1.0.255"    # 参数 Data(可写)
LN_CONFIG = "1.0.25.1.0.255"   # 高权限寄存器(Register,仅 HIGH 认证可写)
LN_PUBLIC = "1.0.10.1.0.255"   # 公开只读寄存器(NONE+ 可读)
LN_LOWREAD = "1.0.11.1.0.255"  # LOW+ 可读寄存器(NONE 下不可读)
LN_HIGHREAD = "1.0.12.1.0.255" # HIGH 才可读寄存器(NONE/LOW 下不可读)
# ================================================================

# ------------------ HDLC 链路层配置(必须与服务器一致) ------------------
hdlc = dlms.IecHdlcSetup(
    "0.0.22.0.0.255",
    commSpeed=BAUD,
    windowSizeRx=1,
    windowSizeTx=1,
    maxInfoLenTx=128,
    maxInfoLenRx=128,
    timeout=120,
    deviceAddr=0x10,
)

serial_conn = dlms.SerialConnection(
    uart_port=UART_PORT,
    hdlc_setup=hdlc,
    flowcontrol=0,
    interface_type=dlms.InterfaceType.HDLC,  # 必须 HDLC
    use_logical_name=True,
)

client = dlms.Client(
    client_address=CLIENT_ADDR,
    server_address=SERVER_ADDR,
    authentication=AUTH,
    password=PASSWORD,       # LOW 认证密码
    use_logical_name=True,
)


def read_attr(ln, attr):
    """读取单个属性,失败返回 None 并打印错误。"""
    try:
        val = client.read(ln, attr)
        print("[Read ] {} attr{} = {}".format(ln, attr, val))
        return val
    except Exception as e:
        print("[Read ] {} attr{} FAILED: {}".format(ln, attr, e))
        return None


def write_attr(ln, attr, value):
    """写入单个属性,返回是否成功。"""
    try:
        client.write(ln, attr, value)
        print("[Write] {} attr{} = {} OK".format(ln, attr, value))
        return True
    except Exception as e:
        print("[Write] {} attr{} = {} FAILED: {}".format(ln, attr, value, e))
        return False


def run_once():
    """单轮测试:读取所有对象 -> 写入 -> 回读验证。"""
    print("-" * 50)

    # 1) 读取
    volt = read_attr(LN_VOLT, 2)          # 电压寄存器值(只读)
    read_attr(LN_ENERGY, 2)               # 电能寄存器值
    read_attr(LN_EXT, 2)                  # 扩展电能寄存器值
    read_attr(LN_PARAM, 2)                # 参数

    # 2) 写电能寄存器(属性2 value)—— LOW 下可写
    new_energy = (volt or 0) + 1000
    write_attr(LN_ENERGY, 2, new_energy)

    # 3) 写扩展电能寄存器 + 参数对象 —— LOW 下可写
    write_attr(LN_EXT, 2, 888)
    write_attr(LN_PARAM, 2, 100)

    # 4) 高权限寄存器:LOW 连接下可读但不可写(期望 READ_WRITE_DENIED)
    read_attr(LN_CONFIG, 2)
    write_attr(LN_CONFIG, 2, 9999)

    # 5) 读取权限分级验证(LOW 连接)
    read_attr(LN_PUBLIC, 2)     # 期望:OK(NONE 可读)
    read_attr(LN_LOWREAD, 2)    # 期望:OK(LOW+ 可读)
    read_attr(LN_HIGHREAD, 2)   # 期望:FAILED(仅 HIGH 可读)

    # 6) 回读验证
    read_attr(LN_ENERGY, 2)
    read_attr(LN_EXT, 2)
    read_attr(LN_PARAM, 2)


def main():
    print("[Client] Connecting to server via UART{} @ {} baud (LOW auth)...".format(UART_PORT, BAUD))
    print("[Client] client_addr=0x{:02X}, server_addr={}, password={}".format(
        CLIENT_ADDR, SERVER_ADDR, PASSWORD))

    while True:
        try:
            client.connect(serial_conn)     # 阻塞直到关联(AARQ/AARE)完成
            print("[Client] connected & associated! (LOW auth)")
            run_once()
            break
        except Exception as e:
            print("[Client] connect/run failed: {}".format(e))
            utime.sleep(3)

    try:
        while True:
            utime.sleep(10)
            read_attr(LN_VOLT, 2)          # 周期性读取服务器采样的电压
            read_attr(LN_ENERGY, 2)
    except KeyboardInterrupt:
        pass
    finally:
        try:
            client.disconnect()
            print("[Client] disconnected")
        except Exception:
            pass


main()

客户端(HIGH)

# -*- coding: utf-8 -*-
"""
DLMS 客户端脚本(模组 B / 集中器)- HIGH 认证版
==================================================================
通过 SerialConnection(HDLC over UART)读取/写入服务器(模组 A)

本脚本使用 HIGH 认证连接(client_address = 0x30,对应服务器 HIGH 关联):
  * HLS 挑战-响应认证:client.c 在 connect() 中自动完成 AARQ + 挑战响应,
    password 作为挑战密钥,必须与服务器 assoc_high.secret 一致。
  * energy / ext_energy / param: 可读可写
  * voltage:                      只读
  * config_reg (1.0.25.1.0.255): 可读可写 —— 这是唯一能写该对象的认证级别
    (服务器仅配置了 Authentication.HIGH: {2: READ_WRITE})

接线(模组 A  <->  模组 B):
    B.UART_RX  <---- A.UART_TX
    B.UART_TX  ----> A.UART_RX
    B.GND      ----  A.GND      (共地是必须的)

关键点:
  * client_address 必须等于对应认证级别关联对象的 clientSAP(0x30)。
  * server_address 必须等于 服务器序列号 % 10000 + 1000。
  * 波特率必须与服务器端一致(commSpeed)。
"""

import dlms
import utime

# ============================ 配置区 ============================
UART_PORT = 2        # 模组 B 使用的 UART 口
BAUD      = 9600     # 波特率,必须与服务器端一致

# 服务器端序列号 —— 必须与服务器脚本中的 SERIAL_NUM 一致!
SERVER_SERIAL = 12345

CLIENT_ADDR = 0x30   # HIGH 认证关联对象的 clientSAP(dlms_server.py 中 SAP_HIGH)
# 服务器地址 = 序列号 % 10000 + 1000
SERVER_ADDR = SERVER_SERIAL % 10000 + 1000   # = 3345
AUTH        = dlms.Authentication.HIGH       # HIGH 认证(HLS 挑战-响应)
PASSWORD    = "Quectel"                      # 必须与服务器 assoc_high.secret 一致

# 演示用对象的逻辑名(均为服务器上真实存在的对象)
LN_ENERGY = "1.0.1.8.0.255"    # 电能寄存器(Register,可写)
LN_VOLT   = "1.0.32.7.0.255"   # 电压寄存器(Register,只读)
LN_EXT    = "1.0.1.8.1.255"    # 扩展电能寄存器(ExtendedRegister,可写)
LN_PARAM  = "0.0.1.1.0.255"    # 参数 Data(可写)
LN_CONFIG = "1.0.25.1.0.255"   # 高权限寄存器(Register,仅 HIGH 认证可写)
LN_PUBLIC = "1.0.10.1.0.255"   # 公开只读寄存器(NONE+ 可读)
LN_LOWREAD = "1.0.11.1.0.255"  # LOW+ 可读寄存器(NONE 下不可读)
LN_HIGHREAD = "1.0.12.1.0.255" # HIGH 才可读寄存器(NONE/LOW 下不可读)
# ================================================================

# ------------------ HDLC 链路层配置(必须与服务器一致) ------------------
hdlc = dlms.IecHdlcSetup(
    "0.0.22.0.0.255",
    commSpeed=BAUD,
    windowSizeRx=1,
    windowSizeTx=1,
    maxInfoLenTx=128,
    maxInfoLenRx=128,
    timeout=120,
    deviceAddr=0x10,
)

serial_conn = dlms.SerialConnection(
    uart_port=UART_PORT,
    hdlc_setup=hdlc,
    flowcontrol=0,
    interface_type=dlms.InterfaceType.HDLC,  # 必须 HDLC
    use_logical_name=True,
)

client = dlms.Client(
    client_address=CLIENT_ADDR,
    server_address=SERVER_ADDR,
    authentication=AUTH,
    password=PASSWORD,       # HIGH 认证挑战密钥
    use_logical_name=True,
)


def read_attr(ln, attr):
    """读取单个属性,失败返回 None 并打印错误。"""
    try:
        val = client.read(ln, attr)
        print("[Read ] {} attr{} = {}".format(ln, attr, val))
        return val
    except Exception as e:
        print("[Read ] {} attr{} FAILED: {}".format(ln, attr, e))
        return None


def write_attr(ln, attr, value):
    """写入单个属性,返回是否成功。"""
    try:
        client.write(ln, attr, value)
        print("[Write] {} attr{} = {} OK".format(ln, attr, value))
        return True
    except Exception as e:
        print("[Write] {} attr{} = {} FAILED: {}".format(ln, attr, value, e))
        return False


def run_once():
    """单轮测试:读取所有对象 -> 写入 -> 回读验证。"""
    print("-" * 50)

    # 1) 读取
    volt = read_attr(LN_VOLT, 2)          # 电压寄存器值(只读)
    read_attr(LN_ENERGY, 2)               # 电能寄存器值
    read_attr(LN_EXT, 2)                  # 扩展电能寄存器值
    read_attr(LN_PARAM, 2)                # 参数

    # 2) 写电能寄存器(属性2 value)—— HIGH 下可写
    new_energy = (volt or 0) + 1000
    write_attr(LN_ENERGY, 2, new_energy)

    # 3) 写扩展电能寄存器 + 参数对象 —— HIGH 下可写
    write_attr(LN_EXT, 2, 888)
    write_attr(LN_PARAM, 2, 100)

    # 4) 高权限寄存器:HIGH 连接下可读也可写(这是唯一的可写认证级别)
    read_attr(LN_CONFIG, 2)
    write_attr(LN_CONFIG, 2, 9999)

    # 5) 读取权限分级验证(HIGH 连接)
    read_attr(LN_PUBLIC, 2)     # 期望:OK(NONE 可读)
    read_attr(LN_LOWREAD, 2)    # 期望:OK(LOW+ 可读)
    read_attr(LN_HIGHREAD, 2)   # 期望:OK(仅 HIGH 可读)

    # 6) 回读验证(重点确认 config_reg 写入成功)
    read_attr(LN_ENERGY, 2)
    read_attr(LN_EXT, 2)
    read_attr(LN_PARAM, 2)
    read_attr(LN_CONFIG, 2)


def main():
    print("[Client] Connecting to server via UART{} @ {} baud (HIGH auth)...".format(UART_PORT, BAUD))
    print("[Client] client_addr=0x{:02X}, server_addr={}, password={}".format(
        CLIENT_ADDR, SERVER_ADDR, PASSWORD))

    while True:
        try:
            client.connect(serial_conn)     # 阻塞直到关联(AARQ/AARE + HLS)完成
            print("[Client] connected & associated! (HIGH auth)")
            run_once()
            break
        except Exception as e:
            print("[Client] connect/run failed: {}".format(e))
            utime.sleep(3)

    try:
        while True:
            utime.sleep(10)
            read_attr(LN_VOLT, 2)          # 周期性读取服务器采样的电压
            read_attr(LN_ENERGY, 2)
    except KeyboardInterrupt:
        pass
    finally:
        try:
            client.disconnect()
            print("[Client] disconnected")
        except Exception:
            pass


main()


构造函数参数

参数 类型 默认值 说明
uart_port int 必传 UART 端口号
hdlc_setup IecHdlcSetup 必传 HDLC 参数块
flowcontrol int 0 流控制: 0 =无, 1 =RTS/CTS
use_logical_name bool True LN 关联; False 为 SN 关联

OpticalConnection (暂不开发)

光口连接。实现 IEC 62056-21 Mode E 协商协议。

协商流程:

  1. 服务器在 300 bps (7E1) 等待客户端签到序列 /?...\r\n
  2. 服务器以 300 bps 回复标识字符串 /<FLAG><BAUD><MODE>\r\n
  3. 等待客户端 ACK 字节 ( 0x06 ) 确认波特率
  4. 双方切换至协商后的波特率 (8N1),继续 HDLC 帧通信

需要以下两个支持对象:

LocalPortSetup

接口类 19(OBIS 0.0.20.0.0.255 ),配置光口参数。

属性 类型 说明
default_mode int 光口协议模式( 0 =Mode E)
default_baud int 初始波特率(Mode E 必须为 300
proposed_baud int 协商后的目标波特率(通常 9600 19200
response_time int 签到与响应间的等待时间(毫秒,默认 1000
device_address bytes / None 设备标识(最多 6 字节)
password_1 bytes / None P1 级别(只读)光口密码
password_2 bytes / None P2 级别(读写)光口密码
password_5 bytes / None P5 级别(完全访问)光口密码
import dlms

local_port = dlms.LocalPortSetup(
    "0.0.20.0.0.255",
    default_mode=0,
    default_baud=300,
    proposed_baud=9600,
    response_time=200,
    device_address=b"MTR001",
    password_1=b"00000000",
    password_2=b"11111111",
    password_5=b"AAAAAAAA",
)

hdlc = dlms.IecHdlcSetup("0.0.22.0.0.255", commSpeed=9600, deviceAddr=0x10)

conn = dlms.OpticalConnection(
    uart_port=1,
    local_port_setup=local_port,
    hdlc_setup=hdlc,
)
server.add_connection(conn)

构造函数签名

OpticalConnection(uart_port, local_port_setup, hdlc_setup, *, use_logical_name=True)

MobileConnection

蜂窝网络连接。在后台线程中运行 C 管理 UDP 套接字。支持两种模式:

模式 条件 说明
直连模式 不提供 relay_tcp_setup 监听 tcp_udp_setup 定义的 UDP 端口,与知道设备公网 IP 的客户端直接通信
中继模式 提供 relay_tcp_setup 向 UDP 中继服务器注册,中继按 HDLC 地址路由;支持 connection.send(data) 主动推送

需要的支持对象

对象 作用
TcpUdpSetup 本地 UDP 端口(默认 4059 ),可关联 IPv4Setup 报告 IP
GprsSetup APN 配置
GsmDiagnostic 蜂窝信号质量属性
recv_buffer 预分配的 bytearray (推荐 4096 字节)
relay_tcp_setup (可选) 中继模式的第二组 TcpUdpSetup

构造函数签名

MobileConnection(tcp_udp_setup, gprs_setup, gsm_diag, recv_buffer,
                 relay_tcp_setup=None, *, use_logical_name=True)

服务器

# -*- coding: utf-8 -*-
"""
DLMS MobileConnection 服务器端 Demo —— 统一 NONE / LOW / HIGH 认证
==================================================================
传输:MobileConnection(UDP + HDLC 帧,IEC 62056-46),direct 模式(不经中继)。

配套客户端:
  client_address=0x10  NONE 认证
  client_address=2     LOW  认证
  client_address=5     HIGH 认证

地址约定:
  HDLC 服务器地址 = dlms.hdlc_server_address(SERIAL)
  客户端地址:NONE=0x10 / LOW=2 / HIGH=5(= 各关联对象 clientSAP)

注意:
  - IPv6 拨号后地址会变,脚本启动时自动从 dataCall 获取,无需手填。
  - 请确保模组已联网(必要时先 dataCall.activate(1))。
"""

import dlms
from dlms import Conformance
import utime

# ============================ 配置区 ============================
USE_IPV6   = True            # True=IPv6;False=IPv4(与客户端一致)
SERIAL     = 12345           # 电表序列号 -> HDLC 服务器地址
FLAG_ID    = "QCT"           # 厂商代码(3 字符)
APN        = "vipmobile"     # 运营商 APN
PIN_CODE   = 0               # SIM PIN(0 = 无)
LOCAL_PORT = 4059            # 本机 UDP 监听端口(direct 模式)

SERVER_IP   = "10.152.151.79"          # IPv4 模式:本机 IPv4
SERVER_IPV6 = "240E:453:DD7D:2479::1"  # IPv6 模式:兜底值,实际自动从 dataCall 获取

# 三种认证级别各自的客户端地址(与客户端 CLIENT_ADDRESS 一致)
CLIENT_ADDR_NONE = 0x10     # NONE 认证关联对象
CLIENT_ADDR_LOW  = 2        # LOW  认证关联对象
CLIENT_ADDR_HIGH = 5        # HIGH 认证关联对象
PASSWORD = "12345678"       # LOW/HIGH 认证密码(= 各 assoc.secret)
# ================================================================


def get_local_ipv6():
    """自动获取本机 IPv6(拨号会变)。"""
    try:
        import dataCall
        info = dataCall.getInfo(1, 1)  # (profile, ip_version, [state, recon, ip, dns1, dns2])
        if info[2][0] == 1 and info[2][2]:
            return info[2][2].upper()
    except Exception as e:
        print("[Net] 获取 IPv6 失败:{}".format(e))
    return None


# ============================ COSEM 业务对象 ============================
energy = dlms.Register(
    "1.0.1.8.0.255", default_value=12345, scaler=1,
    access={dlms.Authentication.NONE: {2: dlms.AccessMode.READ_WRITE, 3: dlms.AccessMode.READ}},
)

voltage = dlms.Register(
    "1.0.32.7.0.255", default_value=230, scaler=1,
    access={dlms.Authentication.NONE: {2: dlms.AccessMode.READ, 3: dlms.AccessMode.READ}},
)

config_reg = dlms.Register(
    "1.0.25.1.0.255", default_value=0, scaler=0,
    access={dlms.Authentication.HIGH: {2: dlms.AccessMode.READ_WRITE, 3: dlms.AccessMode.READ}},
)

public_read_reg = dlms.Register(
    "1.0.10.1.0.255", default_value=100, scaler=0,
    access={dlms.Authentication.NONE: {2: dlms.AccessMode.READ}},
)

low_read_reg = dlms.Register(
    "1.0.11.1.0.255", default_value=200, scaler=0,
    access={
        dlms.Authentication.NONE: {2: dlms.AccessMode.NONE},
        dlms.Authentication.LOW:  {2: dlms.AccessMode.READ},
        dlms.Authentication.HIGH: {2: dlms.AccessMode.READ},
    },
)

high_read_reg = dlms.Register(
    "1.0.12.1.0.255", default_value=300, scaler=0,
    access={
        dlms.Authentication.NONE: {2: dlms.AccessMode.NONE},
        dlms.Authentication.LOW:  {2: dlms.AccessMode.NONE},
        dlms.Authentication.HIGH: {2: dlms.AccessMode.READ},
    },
)

param = dlms.Data(
    "0.0.1.1.0.255",
    access={dlms.Authentication.NONE: {2: dlms.AccessMode.READ_WRITE}},
)
param.value = 42

ldn = dlms.Data(
    "0.0.42.0.0.255",
    access={dlms.Authentication.NONE: {2: dlms.AccessMode.READ}},
)
ldn.value = b"SN12345"

# 类型级兜底:未显式授权时 NONE 默认只读
dlms.set_default_access(dlms.Register, {dlms.Authentication.NONE: {2: dlms.AccessMode.READ, 3: dlms.AccessMode.READ}})
dlms.set_default_access(dlms.Data, {dlms.Authentication.NONE: {2: dlms.AccessMode.READ}})

business = [energy, voltage, config_reg, public_read_reg, low_read_reg, high_read_reg, param, ldn]


def build_network(local_ip):
    """创建网络对象:GprsSetup / IP / TcpUdpSetup / GsmDiagnostic。"""
    gprs = dlms.GprsSetup("0.1.25.0.0.255", apn=APN, pin_code=PIN_CODE)
    if USE_IPV6:
        ip_setup = dlms.IPv6Setup(
            "0.0.25.7.0.255",
            datalink_reference=gprs,
            address_config_mode=2,              # MANUAL
            unicast_ip_address=[local_ip],      # 本机 IPv6
            primary_dns_address="2001:4860:4860::8888",
            secondary_dns_address="2001:4860:4860::8844",
        )
    else:
        ip_setup = dlms.IPv4Setup(
            "0.0.25.1.0.255",
            datalink_reference=gprs,
            ip_address=local_ip,
            subnet_mask="255.255.255.0",
            gateway_ip_address="0.0.0.0",
            use_dhcp=False,
        )
    tcp_udp = dlms.TcpUdpSetup(
        "0.0.25.2.0.255",
        port=LOCAL_PORT,
        ip_reference=ip_setup,
        max_segment_size=1460,
        max_simultaneous_connections=1,
        inactivity_timeout=120,
    )
    gsm_diag = dlms.GsmDiagnostic("0.0.25.6.0.255")
    return gprs, ip_setup, tcp_udp, gsm_diag


def build_assocs(objects):
    """创建 3 个认证等级的关联对象。"""
    FULL_CONF = (
        Conformance.BLOCK_TRANSFER_WITH_ACTION | Conformance.BLOCK_TRANSFER_WITH_SET_OR_WRITE |
        Conformance.BLOCK_TRANSFER_WITH_GET_OR_READ | Conformance.SET |
        Conformance.SELECTIVE_ACCESS | Conformance.ACTION |
        Conformance.MULTIPLE_REFERENCES | Conformance.GET
    )

    assoc_none = dlms.AssociationLogicalName("0.0.40.0.1.255")
    assoc_none.auth_mechanism = "None"
    assoc_none.clientSAP = CLIENT_ADDR_NONE
    assoc_none.objects = list(objects)
    assoc_none.context = dlms.DLMSContext(maxSendPduSize=128, maxReceivePduSize=128, conformance=FULL_CONF)

    assoc_low = dlms.AssociationLogicalName("0.0.40.0.2.255")
    assoc_low.auth_mechanism = "Low"
    assoc_low.secret = b"12345678"
    assoc_low.clientSAP = CLIENT_ADDR_LOW
    assoc_low.objects = list(objects)
    assoc_low.context = dlms.DLMSContext(maxSendPduSize=128, maxReceivePduSize=128, conformance=FULL_CONF)

    assoc_high = dlms.AssociationLogicalName("0.0.40.0.3.255")
    assoc_high.auth_mechanism = "High"
    assoc_high.secret = b"12345678"
    assoc_high.clientSAP = CLIENT_ADDR_HIGH
    assoc_high.objects = list(objects)
    assoc_high.context = dlms.DLMSContext(maxSendPduSize=128, maxReceivePduSize=128, conformance=FULL_CONF)

    return assoc_none, assoc_low, assoc_high


def main():
    # 1. 确定本机 IP(IPv6 自动获取,拨号会变)
    local_ip = SERVER_IPV6 if USE_IPV6 else SERVER_IP
    if USE_IPV6:
        auto = get_local_ipv6()
        if auto:
            local_ip = auto

    print("=" * 60)
    print("[Server] MobileConnection (NONE/LOW/HIGH) {}".format("IPv6" if USE_IPV6 else "IPv4"))
    print("[Server] 本机 {} = {}".format("IPv6" if USE_IPV6 else "IP", local_ip))
    print("[Server] 端口 = {}  服务器地址 = {} (0x{:02X})".format(LOCAL_PORT, dlms.hdlc_server_address(SERIAL), dlms.hdlc_server_address(SERIAL)))
    print("=" * 60)

    # 2. 网络对象 + 业务对象
    gprs, ip_setup, tcp_udp, gsm_diag = build_network(local_ip)
    objs = list(business)

    # 3. Server + 注册对象
    server = dlms.Server(serial_number=SERIAL, flag_id=FLAG_ID)
    for obj in objs + [gprs, ip_setup, tcp_udp, gsm_diag]:
        server.add_object(obj)
    for assoc in build_assocs(objs):
        server.add_object(assoc)

    # 4. MobileConnection(direct 模式,UDP + HDLC)
    mobile = dlms.MobileConnection(
        tcp_udp_setup=tcp_udp,
        gprs_setup=gprs,
        gsm_diag=gsm_diag,
        recv_buffer=bytearray(4096),
        relay_tcp_setup=None,
    )
    mobile.on_connected = lambda: print("[Server] UDP 通道已建立")
    mobile.on_disconnected = lambda: print("[Server] UDP 通道断开")
    server.add_connection(mobile)

    # 5. 启动
    server.run()
    print("[Server] 已启动,等待客户端连接...")
    if USE_IPV6:
        print("[Server] 客户端配置:SERVER_IPV6={}  PORT={}  SERIAL={}".format(local_ip, LOCAL_PORT, SERIAL))
    else:
        print("[Server] 客户端配置:SERVER_IP={}  PORT={}  SERIAL={}".format(local_ip, LOCAL_PORT, SERIAL))

    try:
        while True:
            utime.sleep(1)
    except KeyboardInterrupt:
        server.stop()
        print("[Server] 已停止")


if __name__ == "__main__":
    main()

客户端(NONE)

# -*- coding: utf-8 -*-
"""
DLMS MobileConnection 客户端 Demo —— NONE 认证
==================================================================
传输:MobileConnection(UDP + HDLC 帧),direct 模式。

配套服务器:
  客户端地址 = 0x10 -> 服务器 NONE 关联对象(clientSAP=0x10)

预期(NONE 认证):
  可读写:energy(1.0.1.8.0.255)、voltage、param
  不可读:low_read(1.0.11.1.0.255)、high_read(1.0.12.1.0.255)
  不可写:config_reg(1.0.25.1.0.255,仅 HIGH)
"""

import dlms
import utime

# ============================ 配置区 ============================
USE_IPV6       = True            # 与服务器一致
SERVER_SERIAL  = 12345           # 服务器序列号
SERVER_IP      = "10.152.151.79"          # IPv4:服务器 IPv4
SERVER_IPV6    = "240E:453:DD7D:2479::1"  # IPv6:服务器 IPv6(从服务器打印复制)
SERVER_PORT    = 4059
APN            = "vipmobile"

CLIENT_ADDRESS = 0x10                       # NONE
AUTHENTICATION = dlms.Authentication.NONE
PASSWORD       = None
# ================================================================


def build_mobile():
    """创建客户端 MobileConnection(目标 = 服务器)。"""
    gprs = dlms.GprsSetup("0.1.25.0.0.255", apn=APN, pin_code=0)
    if USE_IPV6:
        target_ip = dlms.IPv6Setup(
            "0.0.25.7.0.255",
            datalink_reference=gprs,
            address_config_mode=2,
            unicast_ip_address=[SERVER_IPV6],   # 服务器 IPv6
        )
    else:
        target_ip = dlms.IPv4Setup(
            "0.0.25.1.0.255",
            datalink_reference=gprs,
            ip_address=SERVER_IP,
            use_dhcp=False,
        )
    tcp_udp = dlms.TcpUdpSetup("0.0.25.2.0.255", port=SERVER_PORT, ip_reference=target_ip)
    gsm_diag = dlms.GsmDiagnostic("0.0.25.6.0.255")
    return dlms.MobileConnection(
        tcp_udp_setup=tcp_udp,
        gprs_setup=gprs,
        gsm_diag=gsm_diag,
        recv_buffer=bytearray(4096),
        relay_tcp_setup=None,
    )


def read_attr(client, obj, attr, name):
    try:
        client.read(obj, attr)
        print("[OK  ] {} attr{} = {}".format(name, attr, getattr(obj, "value", None)))
    except Exception as e:
        print("[FAIL] {} attr{} : {}".format(name, attr, e))


def main():
    target = SERVER_IPV6 if USE_IPV6 else SERVER_IP
    print("=" * 60)
    print("[Client] MobileConnection NONE auth ({})".format("IPv6" if USE_IPV6 else "IPv4"))
    print("[Client] 目标 = {}:{}".format(target, SERVER_PORT))
    print("=" * 60)

    mobile = build_mobile()
    client = dlms.Client(
        client_address=CLIENT_ADDRESS,
        server_address=dlms.hdlc_server_address(SERVER_SERIAL),
        authentication=AUTHENTICATION,
        password=PASSWORD,
    )

    # 连接(服务器冷启动首次回包可能失败,重试几次兜底)
    ok = False
    for attempt in range(1, 4):
        try:
            client.connect(mobile)
            print("[Client] 已连接并完成 DLMS 关联")
            ok = True
            break
        except Exception as e:
            print("[Client] 第 {} 次连接失败: {}".format(attempt, e))
            utime.sleep(attempt * 2)
    if not ok:
        return -1

    # NONE 等级验证
    energy = dlms.Register("1.0.1.8.0.255", 0)
    voltage = dlms.Register("1.0.32.7.0.255", 0)
    low_read = dlms.Register("1.0.11.1.0.255", 0)
    high_read = dlms.Register("1.0.12.1.0.255", 0)
    config_reg = dlms.Register("1.0.25.1.0.255", 0)

    read_attr(client, energy, 2, "energy")
    read_attr(client, voltage, 2, "voltage")
    read_attr(client, low_read, 2, "low_read")     # 期望 FAIL(NONE 无权限)
    read_attr(client, high_read, 2, "high_read")   # 期望 FAIL
    try:
        config_reg.value = 999
        client.write(config_reg, 2)                # 期望 FAIL(仅 HIGH)
        print("[OK  ] config_reg 写成功(意外)")
    except Exception as e:
        print("[FAIL] config_reg 写 : {}".format(e))

    try:
        client.disconnect()
    except Exception:
        pass
    return 0


if __name__ == "__main__":
    main()

客户端(LOW)

# -*- coding: utf-8 -*-
"""
DLMS MobileConnection 客户端 Demo —— LOW 认证
==================================================================
传输:MobileConnection(UDP + HDLC 帧),direct 模式。

配套服务器:
  客户端地址 = 2 -> 服务器 LOW 关联对象(clientSAP=2),密码 = "12345678"

预期(LOW 认证):
  可读写:energy(1.0.1.8.0.255)、voltage、param
  可读  :low_read(1.0.11.1.0.255)
  不可读:high_read(1.0.12.1.0.255,仅 HIGH)
  不可写:config_reg(1.0.25.1.0.255,仅 HIGH)
"""

import dlms
import utime

# ============================ 配置区 ============================
USE_IPV6       = True            # 与服务器一致
SERVER_SERIAL  = 12345           # 服务器序列号
SERVER_IP      = "10.152.151.79"          # IPv4:服务器 IPv4
SERVER_IPV6    = "240E:453:DD7D:2479::1"  # IPv6:服务器 IPv6(从服务器打印复制)
SERVER_PORT    = 4059
APN            = "vipmobile"

CLIENT_ADDRESS = 2                          # LOW
AUTHENTICATION = dlms.Authentication.LOW
PASSWORD       = "12345678"
# ================================================================


def build_mobile():
    """创建客户端 MobileConnection(目标 = 服务器)。"""
    gprs = dlms.GprsSetup("0.1.25.0.0.255", apn=APN, pin_code=0)
    if USE_IPV6:
        target_ip = dlms.IPv6Setup(
            "0.0.25.7.0.255",
            datalink_reference=gprs,
            address_config_mode=2,
            unicast_ip_address=[SERVER_IPV6],   # 服务器 IPv6
        )
    else:
        target_ip = dlms.IPv4Setup(
            "0.0.25.1.0.255",
            datalink_reference=gprs,
            ip_address=SERVER_IP,
            use_dhcp=False,
        )
    tcp_udp = dlms.TcpUdpSetup("0.0.25.2.0.255", port=SERVER_PORT, ip_reference=target_ip)
    gsm_diag = dlms.GsmDiagnostic("0.0.25.6.0.255")
    return dlms.MobileConnection(
        tcp_udp_setup=tcp_udp,
        gprs_setup=gprs,
        gsm_diag=gsm_diag,
        recv_buffer=bytearray(4096),
        relay_tcp_setup=None,
    )


def read_attr(client, obj, attr, name):
    try:
        client.read(obj, attr)
        print("[OK  ] {} attr{} = {}".format(name, attr, getattr(obj, "value", None)))
    except Exception as e:
        print("[FAIL] {} attr{} : {}".format(name, attr, e))


def main():
    target = SERVER_IPV6 if USE_IPV6 else SERVER_IP
    print("=" * 60)
    print("[Client] MobileConnection LOW auth ({})".format("IPv6" if USE_IPV6 else "IPv4"))
    print("[Client] 目标 = {}:{}".format(target, SERVER_PORT))
    print("=" * 60)

    mobile = build_mobile()
    client = dlms.Client(
        client_address=CLIENT_ADDRESS,
        server_address=dlms.hdlc_server_address(SERVER_SERIAL),
        authentication=AUTHENTICATION,
        password=PASSWORD,
    )

    # 连接(服务器冷启动首次回包可能失败,重试几次兜底)
    ok = False
    for attempt in range(1, 4):
        try:
            client.connect(mobile)
            print("[Client] 已连接并完成 DLMS 关联")
            ok = True
            break
        except Exception as e:
            print("[Client] 第 {} 次连接失败: {}".format(attempt, e))
            utime.sleep(attempt * 2)
    if not ok:
        return -1

    # LOW 等级验证
    energy = dlms.Register("1.0.1.8.0.255", 0)
    voltage = dlms.Register("1.0.32.7.0.255", 0)
    low_read = dlms.Register("1.0.11.1.0.255", 0)
    high_read = dlms.Register("1.0.12.1.0.255", 0)
    config_reg = dlms.Register("1.0.25.1.0.255", 0)

    read_attr(client, energy, 2, "energy")
    read_attr(client, voltage, 2, "voltage")
    read_attr(client, low_read, 2, "low_read")     # 期望 OK(LOW 可读)
    read_attr(client, high_read, 2, "high_read")   # 期望 FAIL(仅 HIGH)
    try:
        config_reg.value = 999
        client.write(config_reg, 2)                # 期望 FAIL(仅 HIGH)
        print("[OK  ] config_reg 写成功(意外)")
    except Exception as e:
        print("[FAIL] config_reg 写 : {}".format(e))

    try:
        client.disconnect()
    except Exception:
        pass
    return 0


if __name__ == "__main__":
    main()

客户端(HIGH)

# -*- coding: utf-8 -*-
"""
DLMS MobileConnection 客户端 Demo —— HIGH 认证
==================================================================
传输:MobileConnection(UDP + HDLC 帧),direct 模式。

配套服务器:
  客户端地址 = 5 -> 服务器 HIGH 关联对象(clientSAP=5),密码 = "12345678"

预期(HIGH 认证,最高权限):
  可读写:energy(1.0.1.8.0.255)、voltage、param、config_reg(1.0.25.1.0.255)
  可读  :low_read(1.0.11.1.0.255)、high_read(1.0.12.1.0.255)
"""

import dlms
import utime

# ============================ 配置区 ============================
USE_IPV6       = True            # 与服务器一致
SERVER_SERIAL  = 12345           # 服务器序列号
SERVER_IP      = "10.152.151.79"          # IPv4:服务器 IPv4
SERVER_IPV6    = "240E:453:DD7D:2479::1"  # IPv6:服务器 IPv6(从服务器打印复制)
SERVER_PORT    = 4059
APN            = "vipmobile"

CLIENT_ADDRESS = 5                          # HIGH
AUTHENTICATION = dlms.Authentication.HIGH
PASSWORD       = "12345678"
# ================================================================


def build_mobile():
    """创建客户端 MobileConnection(目标 = 服务器)。"""
    gprs = dlms.GprsSetup("0.1.25.0.0.255", apn=APN, pin_code=0)
    if USE_IPV6:
        target_ip = dlms.IPv6Setup(
            "0.0.25.7.0.255",
            datalink_reference=gprs,
            address_config_mode=2,
            unicast_ip_address=[SERVER_IPV6],   # 服务器 IPv6
        )
    else:
        target_ip = dlms.IPv4Setup(
            "0.0.25.1.0.255",
            datalink_reference=gprs,
            ip_address=SERVER_IP,
            use_dhcp=False,
        )
    tcp_udp = dlms.TcpUdpSetup("0.0.25.2.0.255", port=SERVER_PORT, ip_reference=target_ip)
    gsm_diag = dlms.GsmDiagnostic("0.0.25.6.0.255")
    return dlms.MobileConnection(
        tcp_udp_setup=tcp_udp,
        gprs_setup=gprs,
        gsm_diag=gsm_diag,
        recv_buffer=bytearray(4096),
        relay_tcp_setup=None,
    )


def read_attr(client, obj, attr, name):
    try:
        client.read(obj, attr)
        print("[OK  ] {} attr{} = {}".format(name, attr, getattr(obj, "value", None)))
    except Exception as e:
        print("[FAIL] {} attr{} : {}".format(name, attr, e))


def main():
    target = SERVER_IPV6 if USE_IPV6 else SERVER_IP
    print("=" * 60)
    print("[Client] MobileConnection HIGH auth ({})".format("IPv6" if USE_IPV6 else "IPv4"))
    print("[Client] 目标 = {}:{}".format(target, SERVER_PORT))
    print("=" * 60)

    mobile = build_mobile()
    client = dlms.Client(
        client_address=CLIENT_ADDRESS,
        server_address=dlms.hdlc_server_address(SERVER_SERIAL),
        authentication=AUTHENTICATION,
        password=PASSWORD,
    )

    # 连接(服务器冷启动首次回包可能失败,重试几次兜底)
    ok = False
    for attempt in range(1, 4):
        try:
            client.connect(mobile)
            print("[Client] 已连接并完成 DLMS 关联")
            ok = True
            break
        except Exception as e:
            print("[Client] 第 {} 次连接失败: {}".format(attempt, e))
            utime.sleep(attempt * 2)
    if not ok:
        return -1

    # HIGH 等级验证(全部应成功)
    energy = dlms.Register("1.0.1.8.0.255", 0)
    voltage = dlms.Register("1.0.32.7.0.255", 0)
    low_read = dlms.Register("1.0.11.1.0.255", 0)
    high_read = dlms.Register("1.0.12.1.0.255", 0)
    config_reg = dlms.Register("1.0.25.1.0.255", 0)

    read_attr(client, energy, 2, "energy")
    read_attr(client, voltage, 2, "voltage")
    read_attr(client, low_read, 2, "low_read")     # 期望 OK
    read_attr(client, high_read, 2, "high_read")   # 期望 OK
    try:
        config_reg.value = 999
        client.write(config_reg, 2)                # 期望 OK(HIGH 可写)
        client.read(config_reg, 2)
        print("[OK  ] config_reg 写后读回 = {}".format(config_reg.value))
    except Exception as e:
        print("[FAIL] config_reg 写 : {}".format(e))

    try:
        client.disconnect()
    except Exception:
        pass
    return 0


if __name__ == "__main__":
    main()


GenericConnection

通用连接。适用于自定义传输场景(如特殊成帧的 UART、SPI、MQTT、G3-PLC 等)。

特性:

  • 不创建 C 线程,由 Python 驱动 I/O 循环
  • 通过 interface_type 选择成帧类型
  • 调用 conn.process_msg(data) 处理收到的数据,返回响应字节或 None

成帧类型

常量 适用场景 所需参数
InterfaceType.HDLC 字节流传输:UART、SPI、RS485 hdlc_setup
InterfaceType.WRAPPER 数据包传输:UDP、MQTT、G3-PLC tcp_udp_setup
InterfaceType.HDLC_WITH_MODE_E 光口手动 Mode E hdlc_setup + local_port_setup

构造函数签名

GenericConnection(interface_type, frame_size=1024, pdu_size=512,
                  *, hdlc_setup=None, tcp_udp_setup=None,
                  local_port_setup=None, use_logical_name=True)

因该模式可自定义传输模式,以下仅提供UART和TCP的示例

UART 模式
服务器

# -*- coding: utf-8 -*-
"""
DLMS 服务器端脚本(简洁版)- GenericConnection UART/HDLC,统一 NONE/LOW/HIGH 认证
==================================================================
  * NONE (clientSAP=0x10) / LOW (clientSAP=2) / HIGH (clientSAP=5)
"""

import dlms
from dlms import Conformance
import utime
import _thread

try:
    from machine import UART  # QuecPython / MicroPython
except ImportError:
    UART = None  # PC 上无法跑 UART 传输

# ============================ 配置区 ============================
UART_PORT   = 2        # 模组 A 使用的 UART 口
BAUD        = 19200    # 波特率,必须与客户端一致(Mk7 客户端为 19200)
FLAG_ID     = "GRX"    # 厂商代码(3 字符)
SERIAL_NUM  = 12345    # 电表序列号(Python 模式由 set_serial_number 设置)
PASSWORD    = "12345678"  # LOW/HIGH 认证密码(对齐 Mk7 LLS 的 -P 12345678)

# ---- HDLC 地址(与 Mk7 客户端脚本保持一致)----
SERVER_ADDRESS   = 144      # 电表(服务器)HDLC 地址(逻辑=1, 物理=16 -> 1*128+16)
HDLC_DEVICE_ADDR = 16       # 物理设备地址(GXDLMSDirector 中的 0x10)

# 三种认证级别各自的客户端 HDLC 地址(互不相同;对应客户端 CLIENT_ADDRESS)
CLIENT_ADDR_NONE = 0x10     # NONE 认证关联对象(本 SDK dlms_client_none.py 约定 0x10)
CLIENT_ADDR_LOW  = 2        # LOW  认证关联对象(对齐 Mk7 LLS: -c 2)
CLIENT_ADDR_HIGH = 5        # HIGH 认证关联对象(对齐 Mk7 HLS 的 -c 5 习惯)

LOG_SAMPLE = False          # True 时打印周期采样值(默认关闭,避免刷屏)
# ================================================================

# Python 服务器模式(CONFIG_DLMS_BUILTIN_SERVER 关闭):events.c 生效。
dlms.set_serial_number(SERIAL_NUM)

# ============================ COSEM 对象 ============================
energy = dlms.Register(
    "1.0.1.8.0.255",
    default_value=12345,
    scaler=1,
    access={
        dlms.Authentication.NONE: {
            2: dlms.AccessMode.READ_WRITE,   # value:客户端可写
            3: dlms.AccessMode.READ,         # scaler/unit
        }
    }
)

voltage = dlms.Register(
    "1.0.32.7.0.255",
    default_value=230,
    scaler=1,
    access={
        dlms.Authentication.NONE: {
            2: dlms.AccessMode.READ,
            3: dlms.AccessMode.READ,
        }
    }
)

# 高权限寄存器:仅 HIGH 认证可写
config_reg = dlms.Register(
    "1.0.25.1.0.255",          # 费率/参数配置寄存器(示意)
    default_value=0,
    scaler=0,
    access={
        dlms.Authentication.HIGH: {
            2: dlms.AccessMode.READ_WRITE,   # value:仅 HIGH 可写
            3: dlms.AccessMode.READ,
        }
    }
)

ext_energy = dlms.ExtendedRegister(
    "1.0.1.8.1.255",
    value=500,
    scaler=0,
    unit=dlms.Unit.ACTIVE_ENERGY,
    status=0,
    access={
        dlms.Authentication.NONE: {
            2: dlms.AccessMode.READ_WRITE,   # value:客户端可写
            3: dlms.AccessMode.READ,         # scaler/unit
            4: dlms.AccessMode.READ,         # status
            5: dlms.AccessMode.READ,         # capture_time
        }
    }
)
ext_energy.capture_time = (2026, 8, 20, 12, 0, 0)

param = dlms.Data(
    "0.0.1.1.0.255",
    access={dlms.Authentication.NONE: {2: dlms.AccessMode.READ_WRITE}}
)
param.value = 42

ldn = dlms.Data(
    "0.0.42.0.0.255",
    access={dlms.Authentication.NONE: {2: dlms.AccessMode.READ}}
)
ldn.value = b"SN12345"

public_read_reg = dlms.Register(
    "1.0.10.1.0.255",
    default_value=100,
    scaler=0,
    access={
        dlms.Authentication.NONE: {
            2: dlms.AccessMode.READ,
        }
    }
)

low_read_reg = dlms.Register(
    "1.0.11.1.0.255",
    default_value=200,
    scaler=0,
    access={
        dlms.Authentication.NONE: {
            2: dlms.AccessMode.NONE,   # NONE 连接:不可读
        },
        dlms.Authentication.LOW: {
            2: dlms.AccessMode.READ,   # LOW 连接:可读
        },
        dlms.Authentication.HIGH: {
            2: dlms.AccessMode.READ,   # HIGH 连接:可读
        },
    }
)

high_read_reg = dlms.Register(
    "1.0.12.1.0.255",
    default_value=300,
    scaler=0,
    access={
        dlms.Authentication.NONE: {
            2: dlms.AccessMode.NONE,
        },
        dlms.Authentication.LOW: {
            2: dlms.AccessMode.NONE,
        },
        dlms.Authentication.HIGH: {
            2: dlms.AccessMode.READ,
        },
    }
)

# 类型级兜底访问控制(确保写权限一定生效)
dlms.set_default_access(
    dlms.Register,
    {
        dlms.Authentication.NONE: {
            2: dlms.AccessMode.READ_WRITE,
            3: dlms.AccessMode.READ,
        }
    }
)
dlms.set_default_access(
    dlms.Data,
    {
        dlms.Authentication.NONE: {
            2: dlms.AccessMode.READ_WRITE,
        }
    }
)

# ------------------ HDLC 链路层配置 ------------------
hdlc = dlms.IecHdlcSetup(
    "0.0.22.0.0.255",
    commSpeed=BAUD,
    windowSizeRx=1,
    windowSizeTx=1,
    maxInfoLenTx=128,
    maxInfoLenRx=128,
    timeout=120,
    deviceAddr=HDLC_DEVICE_ADDR,   # 物理设备地址 0x10,与客户端 HDLC_DEVICE_ADDR 一致
)

all_objects = [
    energy, voltage, config_reg,
    public_read_reg, low_read_reg, high_read_reg,
    param, ldn, hdlc, ext_energy,
]

# 完整 conformance:GET/SET/ACTION + 块传输(HIGH 认证的 HLS 挑战-响应依赖 ACTION)
FULL_CONF = (
    Conformance.BLOCK_TRANSFER_WITH_ACTION | Conformance.BLOCK_TRANSFER_WITH_SET_OR_WRITE |
    Conformance.BLOCK_TRANSFER_WITH_GET_OR_READ | Conformance.SET |
    Conformance.SELECTIVE_ACCESS | Conformance.ACTION |
    Conformance.MULTIPLE_REFERENCES | Conformance.GET
)

# ------------------ 关联对象 1:NONE 认证 ------------------
assoc_none = dlms.AssociationLogicalName("0.0.40.0.1.255")
assoc_none.auth_mechanism = "None"
assoc_none.clientSAP = CLIENT_ADDR_NONE
assoc_none.objects = list(all_objects)
assoc_none.context = dlms.DLMSContext(
    maxSendPduSize=128,
    maxReceivePduSize=128,
    conformance=FULL_CONF,
)

# ------------------ 关联对象 2:LOW 认证 ------------------
assoc_low = dlms.AssociationLogicalName("0.0.40.0.2.255")
assoc_low.auth_mechanism = "Low"
assoc_low.secret = b"12345678"        # 密码,必须与 LOW 客户端 password 一致
assoc_low.clientSAP = CLIENT_ADDR_LOW
assoc_low.objects = list(all_objects)
assoc_low.context = dlms.DLMSContext(
    maxSendPduSize=128,
    maxReceivePduSize=128,
    conformance=FULL_CONF,
)

# ------------------ 关联对象 3:HIGH 认证 ------------------
assoc_high = dlms.AssociationLogicalName("0.0.40.0.3.255")
assoc_high.auth_mechanism = "High"
assoc_high.secret = b"12345678"       # HLS 挑战密钥,必须与 HIGH 客户端 password 一致
assoc_high.clientSAP = CLIENT_ADDR_HIGH
assoc_high.objects = list(all_objects)
assoc_high.context = dlms.DLMSContext(
    maxSendPduSize=128,
    maxReceivePduSize=128,
    conformance=FULL_CONF,
)

# ============================ 服务器 ============================
server = dlms.Server(serial_number=SERIAL_NUM, flag_id=FLAG_ID)
for obj in all_objects + [assoc_none, assoc_low, assoc_high]:
    server.add_object(obj)

generic_conn = dlms.GenericConnection(
    interface_type=dlms.InterfaceType.HDLC,  # 必须 HDLC(与客户端一致)
    frame_size=1024,
    pdu_size=512,
    hdlc_setup=hdlc,
    use_logical_name=True,
)

server.add_connection(generic_conn)
server.run()   # 设置 active registry + 注入对象;GenericConnection 不建 C 线程

print("[Server] DLMS server (NONE/LOW/HIGH) running via GenericConnection on UART{} @ {} baud".format(
    UART_PORT, BAUD))
print("[Server] Server addr={} (logical=1, physical={}), password={}".format(
    SERVER_ADDRESS, HDLC_DEVICE_ADDR, PASSWORD))


# ============================ Python UART 传输线程 ============================
def uart_loop(conn, uart_port, baud_rate):
    """后台线程:UART 读帧 -> process_msg -> 回包写回。"""
    try:
        if UART is None:
            print("[UART] machine.UART unavailable on PC")
            return
        uart = UART(uart_port, baud_rate, 8, 0, 1, 0)   # bits/parity/stop/flow
        conn.connect()    # process_msg 依赖 connected 状态
        print("[UART] Listening for DLMS frames on UART{} @ {} baud...".format(uart_port, baud_rate))
        buf = bytearray(1024)
        while True:
            n = uart.readinto(buf)
            if n and n > 0:
                resp = conn.process_msg(buf[:n])
                if resp:
                    uart.write(resp)
            utime.sleep(0.01)
    except Exception as e:
        print("[UART] Fatal error: {}".format(e))
    finally:
        try:
            conn.disconnect()
        except Exception:
            pass
        print("[UART] Thread stopped")


_thread.start_new_thread(uart_loop, (generic_conn, UART_PORT, BAUD))

# ============================ 主循环 ============================
# 模拟电表周期采集,更新寄存器值(客户端可读到变化的电压)
try:
    seq = 0
    while True:
        seq += 1
        v = 228 + (seq % 7)          # 230/229/... 波动,模拟真实采样
        voltage.value = v
        if LOG_SAMPLE:
            print("[Server] sample: voltage={} V, energy={}".format(v, energy.value))
        utime.sleep(5)
except KeyboardInterrupt:
    server.stop()
    print("[Server] stopped")

客户端(NONE)

# -*- coding: utf-8 -*-
"""
DLMS 客户端脚本(简洁版)- GenericConnection UART/HDLC,NONE 认证
==================================================================
  * client_address = 0x10 -> 服务器 NONE 关联对象(clientSAP=0x10)
  * server_address = 144  -> 服务器地址(逻辑=1, 物理=16)
  * 波特率 19200,与服务器一致
"""

import utime

try:
    from machine import UART  # QuecPython / MicroPython
except ImportError:
    UART = None  # PC 上无法跑 UART 传输

import dlms

# ============================ 配置区 ============================
UART_ID     = UART.UART2 if UART else 2   # 模组 B 使用的 UART 口
BAUDRATE    = 19200                       # 必须与服务器端一致
DATABITS    = 8
PARITY      = 0
STOPBITS    = 1

CLIENT_ADDRESS = 0x10          # 对应服务器 NONE 关联对象的 clientSAP
SERVER_ADDRESS = 144           # 电表(服务器)HDLC 地址(逻辑=1, 物理=16)
HDLC_DEVICE_ADDR = 16          # 物理设备地址(GXDLMSDirector 中的 0x10)

USE_LOGICAL_NAME = True
AUTHENTICATION   = dlms.Authentication.NONE   # NONE 认证:无密码
PASSWORD         = None
SECURITY         = 0x00
# ================================================================

# 演示对象(服务器 dlms_server_generic_lite.py 上注册)
LN_ENERGY  = "1.0.1.8.0.255"     # 电能寄存器(Register,所有级别可写)
LN_VOLT    = "1.0.32.7.0.255"    # 电压寄存器(Register,只读)
LN_CONFIG  = "1.0.25.1.0.255"    # 高权限寄存器(Register,仅 HIGH 可写)
LN_PUBLIC  = "1.0.10.1.0.255"    # 公开只读寄存器(NONE+ 可读)
LN_LOWREAD = "1.0.11.1.0.255"    # LOW+ 可读寄存器(NONE 下不可读)
LN_HIGHREAD = "1.0.12.1.0.255"   # HIGH 才可读寄存器(NONE 下不可读)


class UartTransport:
    """Drive HDLC frames over a QuecPython UART(帧级接收,无回显)。"""

    def __init__(self, uart_id, baudrate, databits, parity, stopbits):
        self._uart_id = uart_id
        self._baudrate = baudrate
        if UART is not None:
            self._uart = UART(uart_id, baudrate, databits, parity, stopbits, 0)
            self._uart.set_callback(None)  # 避免回调与 read() 竞争
        else:
            self._uart = None

    def open(self):
        pass

    def close(self):
        pass

    def send(self, data):
        if self._uart is None:
            raise RuntimeError("UART not available")
        return self._uart.write(data)

    def receive(self, timeout_ms):
        """读取恰好一帧 HDLC(0x7E ... 0x7E),按帧长字段收齐;超时返回 None。"""
        if self._uart is None:
            return None
        deadline = utime.ticks_ms() + timeout_ms
        buf = bytearray()

        # Phase 1: 扫描起始 0x7E 标志(跳过垃圾/回显字节)
        while utime.ticks_ms() < deadline:
            if self._uart.any() > 0:
                b = self._uart.read(1)
                if b and b[0] == 0x7E:
                    buf.append(0x7E)
                    break
            else:
                utime.sleep_ms(5)
        if not buf:
            return None

        # Phase 2: 读 2 字节帧头(帧类型 + 长度)
        while len(buf) < 3 and utime.ticks_ms() < deadline:
            if self._uart.any() > 0:
                b = self._uart.read(1)
                if b:
                    buf.append(b[0])
            else:
                utime.sleep_ms(5)
        if len(buf) < 3:
            return None

        frame_len = ((buf[1] & 0x07) << 8) | buf[2]
        if frame_len < 4:
            return None

        # Phase 3: 读帧体
        need = frame_len - 1
        while len(buf) < 3 + need and utime.ticks_ms() < deadline:
            if self._uart.any() > 0:
                b = self._uart.read(1)
                if b:
                    buf.append(b[0])
            else:
                utime.sleep_ms(5)
        if len(buf) < 3 + need:
            return None
        return bytes(buf)


def build_hdlc_setup(baudrate=None):
    return dlms.IecHdlcSetup(
        "0.0.22.0.0.255",
        commSpeed=baudrate if baudrate else BAUDRATE,
        windowSizeRx=1,
        windowSizeTx=1,
        maxInfoLenTx=128,
        maxInfoLenRx=128,
        timeout=10,
        deviceAddr=HDLC_DEVICE_ADDR,
    )


def build_connection(transport, baudrate=None):
    conn = dlms.GenericConnection(
        interface_type=dlms.InterfaceType.HDLC,
        frame_size=1024,
        pdu_size=1024,
        hdlc_setup=build_hdlc_setup(baudrate),
        use_logical_name=USE_LOGICAL_NAME,
    )
    conn.on_send = lambda data: transport.send(data)
    conn.on_receive = lambda timeout_ms: transport.receive(timeout_ms)
    return conn


def build_client():
    return dlms.Client(
        client_address=CLIENT_ADDRESS,
        server_address=SERVER_ADDRESS,
        authentication=AUTHENTICATION,
        password=PASSWORD,
        security=SECURITY,
        use_logical_name=USE_LOGICAL_NAME,
    )


def read_attr(client, ln, attr):
    try:
        val = client.read(ln, attr)
        print("[Read ] {} attr{} = {}".format(ln, attr, val))
        return val
    except Exception as e:
        print("[Read ] {} attr{} FAILED: {}".format(ln, attr, e))
        return None


def write_attr(client, ln, attr, value):
    try:
        client.write(ln, attr, value)
        print("[Write] {} attr{} = {} OK".format(ln, attr, value))
        return True
    except Exception as e:
        print("[Write] {} attr{} = {} FAILED: {}".format(ln, attr, value, e))
        return False


def run_once(client):
    """单轮测试:读 -> 权限验证 -> 写。"""
    # 1) NONE 可读对象
    read_attr(client, LN_VOLT, 2)
    read_attr(client, LN_ENERGY, 2)
    read_attr(client, LN_PUBLIC, 2)

    # 2) 权限分级验证:LOW+ / HIGH 对象应被拒
    read_attr(client, LN_LOWREAD, 2)   # 期望 FAILED(READ_WRITE_DENIED)
    read_attr(client, LN_HIGHREAD, 2)  # 期望 FAILED(READ_WRITE_DENIED)

    # 3) 写电能寄存器:NONE 下可写
    write_attr(client, LN_ENERGY, 2, 2222)

    # 4) 写高权限寄存器:仅 HIGH 可写,NONE 下应被拒
    write_attr(client, LN_CONFIG, 2, 9999)   # 期望 FAILED(READ_WRITE_DENIED)


def main():
    print("[Client] UART{} @ {} baud, NONE auth, client=0x{:02X}, server={}".format(
        UART_ID, BAUDRATE, CLIENT_ADDRESS, SERVER_ADDRESS))

    transport = UartTransport(UART_ID, BAUDRATE, DATABITS, PARITY, STOPBITS)
    client = build_client()

    while True:
        try:
            conn = build_connection(transport, BAUDRATE)
            client.connect(conn)     # SNRM (HDLC) + AARQ(NONE 直接关联)
            print("[Client] connected & associated! (NONE auth)")
            run_once(client)
            break
        except Exception as e:
            print("[Client] connect/run failed: {}".format(e))
            utime.sleep(3)

    try:
        while True:
            utime.sleep(10)
            read_attr(client, LN_VOLT, 2)     # 周期性读服务器采样的电压
            read_attr(client, LN_ENERGY, 2)
    except KeyboardInterrupt:
        pass
    finally:
        try:
            client.disconnect()
        except Exception:
            pass


if __name__ == "__main__":
    main()

客户端(LOW)

# -*- coding: utf-8 -*-
"""
DLMS 客户端脚本(简洁版)- GenericConnection UART/HDLC,LOW 认证
==================================================================
  * client_address = 2    -> 服务器 LOW 关联对象(clientSAP=2,对齐 Mk7 LLS -c 2)
  * server_address = 144  -> 服务器地址(逻辑=1, 物理=16)
  * 波特率 19200;LOW 认证:密码 "12345678"
"""

import utime

try:
    from machine import UART  # QuecPython / MicroPython
except ImportError:
    UART = None  # PC 上无法跑 UART 传输

import dlms

# ============================ 配置区 ============================
UART_ID     = UART.UART2 if UART else 2   # 模组 B 使用的 UART 口
BAUDRATE    = 19200                       # 必须与服务器端一致
DATABITS    = 8
PARITY      = 0
STOPBITS    = 1

CLIENT_ADDRESS = 2               # 对应服务器 LOW 关联对象的 clientSAP(Mk7 LLS -c 2)
SERVER_ADDRESS = 144             # 电表(服务器)HDLC 地址(逻辑=1, 物理=16)
HDLC_DEVICE_ADDR = 16            # 物理设备地址(GXDLMSDirector 中的 0x10)

USE_LOGICAL_NAME = True
AUTHENTICATION   = dlms.Authentication.LOW    # LOW 认证:密码方式
PASSWORD         = "12345678"                 # 必须与服务器 assoc_low.secret 一致
SECURITY         = 0x00
# ================================================================

# 演示对象(服务器 dlms_server_generic_lite.py 上注册)
LN_ENERGY  = "1.0.1.8.0.255"     # 电能寄存器(Register,所有级别可写)
LN_VOLT    = "1.0.32.7.0.255"    # 电压寄存器(Register,只读)
LN_CONFIG  = "1.0.25.1.0.255"    # 高权限寄存器(Register,仅 HIGH 可写)
LN_PUBLIC  = "1.0.10.1.0.255"    # 公开只读寄存器(NONE+ 可读)
LN_LOWREAD = "1.0.11.1.0.255"    # LOW+ 可读寄存器(LOW 可读)
LN_HIGHREAD = "1.0.12.1.0.255"   # HIGH 才可读寄存器(LOW 下不可读)


class UartTransport:
    """Drive HDLC frames over a QuecPython UART(帧级接收,无回显)。"""

    def __init__(self, uart_id, baudrate, databits, parity, stopbits):
        self._uart_id = uart_id
        self._baudrate = baudrate
        if UART is not None:
            self._uart = UART(uart_id, baudrate, databits, parity, stopbits, 0)
            self._uart.set_callback(None)  # 避免回调与 read() 竞争
        else:
            self._uart = None

    def open(self):
        pass

    def close(self):
        pass

    def send(self, data):
        if self._uart is None:
            raise RuntimeError("UART not available")
        return self._uart.write(data)

    def receive(self, timeout_ms):
        """读取恰好一帧 HDLC(0x7E ... 0x7E),按帧长字段收齐;超时返回 None。"""
        if self._uart is None:
            return None
        deadline = utime.ticks_ms() + timeout_ms
        buf = bytearray()

        # Phase 1: 扫描起始 0x7E 标志(跳过垃圾/回显字节)
        while utime.ticks_ms() < deadline:
            if self._uart.any() > 0:
                b = self._uart.read(1)
                if b and b[0] == 0x7E:
                    buf.append(0x7E)
                    break
            else:
                utime.sleep_ms(5)
        if not buf:
            return None

        # Phase 2: 读 2 字节帧头(帧类型 + 长度)
        while len(buf) < 3 and utime.ticks_ms() < deadline:
            if self._uart.any() > 0:
                b = self._uart.read(1)
                if b:
                    buf.append(b[0])
            else:
                utime.sleep_ms(5)
        if len(buf) < 3:
            return None

        frame_len = ((buf[1] & 0x07) << 8) | buf[2]
        if frame_len < 4:
            return None

        # Phase 3: 读帧体
        need = frame_len - 1
        while len(buf) < 3 + need and utime.ticks_ms() < deadline:
            if self._uart.any() > 0:
                b = self._uart.read(1)
                if b:
                    buf.append(b[0])
            else:
                utime.sleep_ms(5)
        if len(buf) < 3 + need:
            return None
        return bytes(buf)


def build_hdlc_setup(baudrate=None):
    return dlms.IecHdlcSetup(
        "0.0.22.0.0.255",
        commSpeed=baudrate if baudrate else BAUDRATE,
        windowSizeRx=1,
        windowSizeTx=1,
        maxInfoLenTx=128,
        maxInfoLenRx=128,
        timeout=10,
        deviceAddr=HDLC_DEVICE_ADDR,
    )


def build_connection(transport, baudrate=None):
    conn = dlms.GenericConnection(
        interface_type=dlms.InterfaceType.HDLC,
        frame_size=1024,
        pdu_size=1024,
        hdlc_setup=build_hdlc_setup(baudrate),
        use_logical_name=USE_LOGICAL_NAME,
    )
    conn.on_send = lambda data: transport.send(data)
    conn.on_receive = lambda timeout_ms: transport.receive(timeout_ms)
    return conn


def build_client():
    return dlms.Client(
        client_address=CLIENT_ADDRESS,
        server_address=SERVER_ADDRESS,
        authentication=AUTHENTICATION,
        password=PASSWORD,
        security=SECURITY,
        use_logical_name=USE_LOGICAL_NAME,
    )


def read_attr(client, ln, attr):
    try:
        val = client.read(ln, attr)
        print("[Read ] {} attr{} = {}".format(ln, attr, val))
        return val
    except Exception as e:
        print("[Read ] {} attr{} FAILED: {}".format(ln, attr, e))
        return None


def write_attr(client, ln, attr, value):
    try:
        client.write(ln, attr, value)
        print("[Write] {} attr{} = {} OK".format(ln, attr, value))
        return True
    except Exception as e:
        print("[Write] {} attr{} = {} FAILED: {}".format(ln, attr, value, e))
        return False


def run_once(client):
    """单轮测试:读 -> 权限验证 -> 写。"""
    # 1) LOW 可读对象
    read_attr(client, LN_VOLT, 2)
    read_attr(client, LN_ENERGY, 2)
    read_attr(client, LN_PUBLIC, 2)
    read_attr(client, LN_LOWREAD, 2)   # LOW 可读

    # 2) 权限分级验证:HIGH 对象应被拒
    read_attr(client, LN_HIGHREAD, 2)  # 期望 FAILED(READ_WRITE_DENIED)

    # 3) 写电能寄存器:LOW 下可写
    write_attr(client, LN_ENERGY, 2, 3333)

    # 4) 写高权限寄存器:仅 HIGH 可写,LOW 下应被拒
    write_attr(client, LN_CONFIG, 2, 9999)   # 期望 FAILED(READ_WRITE_DENIED)


def main():
    print("[Client] UART{} @ {} baud, LOW auth, client={}, server={}, pwd='{}'".format(
        UART_ID, BAUDRATE, CLIENT_ADDRESS, SERVER_ADDRESS, PASSWORD))

    transport = UartTransport(UART_ID, BAUDRATE, DATABITS, PARITY, STOPBITS)
    client = build_client()

    while True:
        try:
            conn = build_connection(transport, BAUDRATE)
            client.connect(conn)     # SNRM (HDLC) + AARQ(LOW:密码随 AARQ)
            print("[Client] connected & associated! (LOW auth)")
            run_once(client)
            break
        except Exception as e:
            print("[Client] connect/run failed: {}".format(e))
            utime.sleep(3)

    try:
        while True:
            utime.sleep(10)
            read_attr(client, LN_VOLT, 2)     # 周期性读服务器采样的电压
            read_attr(client, LN_ENERGY, 2)
    except KeyboardInterrupt:
        pass
    finally:
        try:
            client.disconnect()
        except Exception:
            pass


if __name__ == "__main__":
    main()

客户端(HIGH)

# -*- coding: utf-8 -*-
"""
DLMS 客户端脚本(简洁版)- GenericConnection UART/HDLC,HIGH 认证
==================================================================
  * client_address = 5    -> 服务器 HIGH 关联对象(clientSAP=5,对齐 Mk7 HLS -c 5)
  * server_address = 144  -> 服务器地址(逻辑=1, 物理=16)
  * 波特率 19200;HIGH 认证:HLS 挑战-响应,密码 "12345678"
"""

import utime

try:
    from machine import UART  # QuecPython / MicroPython
except ImportError:
    UART = None  # PC 上无法跑 UART 传输

import dlms

# ============================ 配置区 ============================
UART_ID     = UART.UART2 if UART else 2   # 模组 B 使用的 UART 口
BAUDRATE    = 19200                       # 必须与服务器端一致
DATABITS    = 8
PARITY      = 0
STOPBITS    = 1

CLIENT_ADDRESS = 5               # 对应服务器 HIGH 关联对象的 clientSAP(Mk7 HLS -c 5)
SERVER_ADDRESS = 144             # 电表(服务器)HDLC 地址(逻辑=1, 物理=16)
HDLC_DEVICE_ADDR = 16            # 物理设备地址(GXDLMSDirector 中的 0x10)

USE_LOGICAL_NAME = True
AUTHENTICATION   = dlms.Authentication.HIGH   # HIGH 认证:HLS 挑战-响应
PASSWORD         = "12345678"                 # 挑战密钥,必须与服务器 assoc_high.secret 一致
SECURITY         = 0x00
# ================================================================

# 演示对象(服务器 dlms_server_generic_lite.py 上注册)
LN_ENERGY  = "1.0.1.8.0.255"     # 电能寄存器(Register,所有级别可写)
LN_VOLT    = "1.0.32.7.0.255"    # 电压寄存器(Register,只读)
LN_CONFIG  = "1.0.25.1.0.255"    # 高权限寄存器(Register,仅 HIGH 可写)
LN_PUBLIC  = "1.0.10.1.0.255"    # 公开只读寄存器(NONE+ 可读)
LN_LOWREAD = "1.0.11.1.0.255"    # LOW+ 可读寄存器(HIGH 可读)
LN_HIGHREAD = "1.0.12.1.0.255"   # HIGH 才可读寄存器(HIGH 可读)


class UartTransport:
    """Drive HDLC frames over a QuecPython UART(帧级接收,无回显)。"""

    def __init__(self, uart_id, baudrate, databits, parity, stopbits):
        self._uart_id = uart_id
        self._baudrate = baudrate
        if UART is not None:
            self._uart = UART(uart_id, baudrate, databits, parity, stopbits, 0)
            self._uart.set_callback(None)  # 避免回调与 read() 竞争
        else:
            self._uart = None

    def open(self):
        pass

    def close(self):
        pass

    def send(self, data):
        if self._uart is None:
            raise RuntimeError("UART not available")
        return self._uart.write(data)

    def receive(self, timeout_ms):
        """读取恰好一帧 HDLC(0x7E ... 0x7E),按帧长字段收齐;超时返回 None。"""
        if self._uart is None:
            return None
        deadline = utime.ticks_ms() + timeout_ms
        buf = bytearray()

        # Phase 1: 扫描起始 0x7E 标志(跳过垃圾/回显字节)
        while utime.ticks_ms() < deadline:
            if self._uart.any() > 0:
                b = self._uart.read(1)
                if b and b[0] == 0x7E:
                    buf.append(0x7E)
                    break
            else:
                utime.sleep_ms(5)
        if not buf:
            return None

        # Phase 2: 读 2 字节帧头(帧类型 + 长度)
        while len(buf) < 3 and utime.ticks_ms() < deadline:
            if self._uart.any() > 0:
                b = self._uart.read(1)
                if b:
                    buf.append(b[0])
            else:
                utime.sleep_ms(5)
        if len(buf) < 3:
            return None

        frame_len = ((buf[1] & 0x07) << 8) | buf[2]
        if frame_len < 4:
            return None

        # Phase 3: 读帧体
        need = frame_len - 1
        while len(buf) < 3 + need and utime.ticks_ms() < deadline:
            if self._uart.any() > 0:
                b = self._uart.read(1)
                if b:
                    buf.append(b[0])
            else:
                utime.sleep_ms(5)
        if len(buf) < 3 + need:
            return None
        return bytes(buf)


def build_hdlc_setup(baudrate=None):
    return dlms.IecHdlcSetup(
        "0.0.22.0.0.255",
        commSpeed=baudrate if baudrate else BAUDRATE,
        windowSizeRx=1,
        windowSizeTx=1,
        maxInfoLenTx=128,
        maxInfoLenRx=128,
        timeout=10,
        deviceAddr=HDLC_DEVICE_ADDR,
    )


def build_connection(transport, baudrate=None):
    conn = dlms.GenericConnection(
        interface_type=dlms.InterfaceType.HDLC,
        frame_size=1024,
        pdu_size=1024,
        hdlc_setup=build_hdlc_setup(baudrate),
        use_logical_name=USE_LOGICAL_NAME,
    )
    conn.on_send = lambda data: transport.send(data)
    conn.on_receive = lambda timeout_ms: transport.receive(timeout_ms)
    return conn


def build_client():
    return dlms.Client(
        client_address=CLIENT_ADDRESS,
        server_address=SERVER_ADDRESS,
        authentication=AUTHENTICATION,
        password=PASSWORD,
        security=SECURITY,
        use_logical_name=USE_LOGICAL_NAME,
    )


def read_attr(client, ln, attr):
    try:
        val = client.read(ln, attr)
        print("[Read ] {} attr{} = {}".format(ln, attr, val))
        return val
    except Exception as e:
        print("[Read ] {} attr{} FAILED: {}".format(ln, attr, e))
        return None


def write_attr(client, ln, attr, value):
    try:
        client.write(ln, attr, value)
        print("[Write] {} attr{} = {} OK".format(ln, attr, value))
        return True
    except Exception as e:
        print("[Write] {} attr{} = {} FAILED: {}".format(ln, attr, value, e))
        return False


def run_once(client):
    """单轮测试:读 -> 权限验证 -> 写。"""
    # 1) 全部对象可读(HIGH 是最高权限)
    read_attr(client, LN_VOLT, 2)
    read_attr(client, LN_ENERGY, 2)
    read_attr(client, LN_PUBLIC, 2)
    read_attr(client, LN_LOWREAD, 2)
    read_attr(client, LN_HIGHREAD, 2)   # 仅 HIGH 可读
    read_attr(client, LN_CONFIG, 2)

    # 2) 写电能寄存器:HIGH 下可写
    write_attr(client, LN_ENERGY, 2, 4444)

    # 3) 写高权限寄存器:仅 HIGH 可写
    write_attr(client, LN_CONFIG, 2, 9999)   # 期望 OK
    read_attr(client, LN_CONFIG, 2)          # 回读验证


def main():
    print("[Client] UART{} @ {} baud, HIGH auth, client={}, server={}, pwd='{}'".format(
        UART_ID, BAUDRATE, CLIENT_ADDRESS, SERVER_ADDRESS, PASSWORD))

    transport = UartTransport(UART_ID, BAUDRATE, DATABITS, PARITY, STOPBITS)
    client = build_client()

    while True:
        try:
            conn = build_connection(transport, BAUDRATE)
            client.connect(conn)     # SNRM (HDLC) + AARQ + HLS(自动)
            print("[Client] connected & associated! (HIGH auth)")
            run_once(client)
            break
        except Exception as e:
            print("[Client] connect/run failed: {}".format(e))
            utime.sleep(3)

    try:
        while True:
            utime.sleep(10)
            read_attr(client, LN_VOLT, 2)     # 周期性读服务器采样的电压
            read_attr(client, LN_ENERGY, 2)
    except KeyboardInterrupt:
        pass
    finally:
        try:
            client.disconnect()
        except Exception:
            pass


if __name__ == "__main__":
    main()


TCP 模式
服务器

# -*- coding: utf-8 -*-
"""
DLMS 服务器端脚本(简洁版)- GenericConnection TCP/WRAPPER,统一 NONE/LOW/HIGH 认证
==================================================================
  * NONE (clientSAP=0x10) / LOW (clientSAP=2) / HIGH (clientSAP=5)
  * 配 _lite 客户端脚本使用(dlms_client_*_generic_tcp_lite.py)
"""

import dlms
from dlms import Conformance
import utime

try:
    import usocket as socket  # QuecPython / MicroPython
except ImportError:
    try:
        import socket  # 标准名兼容
    except ImportError:
        socket = None  # 无 socket 模块(纯 PC 环境)

# ============================ 配置区 ============================
TCP_PORT    = 4020    # TCP 端口,客户端必须一致
# IPv4/IPv6 开关(与客户端一致):False = IPv4;True = IPv6
USE_IPV6    = True
# 服务器监听地址:IPv4 填 SIM IPv4;IPv6 填模组自身 IPv6(拨号会变!)
# 绑定失败会自动回退 "0.0.0.0"(IPv4)/ "::"(IPv6,监听所有网卡)
SERVER_IP   = "240E:452:DDAB:928D::1"

FLAG_ID     = "GRX"    # 厂商代码(3 字符)
SERIAL_NUM  = 12345    # 电表序列号
PASSWORD    = "12345678"  # LOW/HIGH 认证密码

# ---- 地址(与客户端脚本保持一致)----
SERVER_ADDRESS   = 144      # 电表(服务器)地址(逻辑=1, 物理=16 -> 1*128+16)
HDLC_DEVICE_ADDR = 16       # 物理设备地址

# 三种认证级别各自的客户端地址(互不相同;对应客户端 CLIENT_ADDRESS)
CLIENT_ADDR_NONE = 0x10     # NONE 认证关联对象
CLIENT_ADDR_LOW  = 2        # LOW  认证关联对象
CLIENT_ADDR_HIGH = 5        # HIGH 认证关联对象

LOG_SAMPLE = True           # False 时不打印周期性采样值
# ================================================================

print("[Server] TCP:{}:{} addr={} pwd='{}' client NONE=0x{:02X} LOW={} HIGH={}".format(
    SERVER_IP, TCP_PORT, SERVER_ADDRESS, PASSWORD,
    CLIENT_ADDR_NONE, CLIENT_ADDR_LOW, CLIENT_ADDR_HIGH))

dlms.set_serial_number(SERIAL_NUM)

# ============================ COSEM 对象 ============================
energy = dlms.Register(
    "1.0.1.8.0.255",
    default_value=12345,
    scaler=1,
    access={
        dlms.Authentication.NONE: {
            2: dlms.AccessMode.READ_WRITE,
            3: dlms.AccessMode.READ,
        }
    }
)

voltage = dlms.Register(
    "1.0.32.7.0.255",
    default_value=230,
    scaler=1,
    access={
        dlms.Authentication.NONE: {
            2: dlms.AccessMode.READ,
            3: dlms.AccessMode.READ,
        }
    }
)

# 高权限寄存器:仅 HIGH 认证可写
config_reg = dlms.Register(
    "1.0.25.1.0.255",
    default_value=0,
    scaler=0,
    access={
        dlms.Authentication.HIGH: {
            2: dlms.AccessMode.READ_WRITE,
            3: dlms.AccessMode.READ,
        }
    }
)

ext_energy = dlms.ExtendedRegister(
    "1.0.1.8.1.255",
    value=500,
    scaler=0,
    unit=dlms.Unit.ACTIVE_ENERGY,
    status=0,
    access={
        dlms.Authentication.NONE: {
            2: dlms.AccessMode.READ_WRITE,
            3: dlms.AccessMode.READ,
            4: dlms.AccessMode.READ,
            5: dlms.AccessMode.READ,
        }
    }
)
ext_energy.capture_time = (2026, 8, 20, 12, 0, 0)

param = dlms.Data(
    "0.0.1.1.0.255",
    access={dlms.Authentication.NONE: {2: dlms.AccessMode.READ_WRITE}}
)
param.value = 42

ldn = dlms.Data(
    "0.0.42.0.0.255",
    access={dlms.Authentication.NONE: {2: dlms.AccessMode.READ}}
)
ldn.value = b"SN12345"

public_read_reg = dlms.Register(
    "1.0.10.1.0.255",
    default_value=100,
    scaler=0,
    access={
        dlms.Authentication.NONE: {2: dlms.AccessMode.READ},
    }
)

low_read_reg = dlms.Register(
    "1.0.11.1.0.255",
    default_value=200,
    scaler=0,
    access={
        dlms.Authentication.NONE: {2: dlms.AccessMode.NONE},
        dlms.Authentication.LOW:  {2: dlms.AccessMode.READ},
        dlms.Authentication.HIGH: {2: dlms.AccessMode.READ},
    }
)

high_read_reg = dlms.Register(
    "1.0.12.1.0.255",
    default_value=300,
    scaler=0,
    access={
        dlms.Authentication.NONE: {2: dlms.AccessMode.NONE},
        dlms.Authentication.LOW:  {2: dlms.AccessMode.NONE},
        dlms.Authentication.HIGH: {2: dlms.AccessMode.READ},
    }
)

# 类型级兜底访问控制
dlms.set_default_access(
    dlms.Register,
    {
        dlms.Authentication.NONE: {
            2: dlms.AccessMode.READ_WRITE,
            3: dlms.AccessMode.READ,
        }
    }
)
dlms.set_default_access(
    dlms.Data,
    {
        dlms.Authentication.NONE: {
            2: dlms.AccessMode.READ_WRITE,
        }
    }
)

# ------------------ 网络设置对象(TCP,与 USE_IPV6 一致) ------------------
if USE_IPV6:
    ip_setup = dlms.IPv6Setup(
        "0.0.25.7.0.255",
        address_config_mode=2,                    # MANUAL
        unicast_ip_address=[SERVER_IP],
        primary_dns_address="2001:4860:4860::8888",
        secondary_dns_address="2001:4860:4860::8844",
    )
else:
    ip_setup = dlms.IPv4Setup(
        "0.0.25.1.0.255",
        ip_address=SERVER_IP if SERVER_IP else "0.0.0.0",
        subnet_mask="255.255.255.0",
        gateway_ip_address="0.0.0.0",
        use_dhcp=(not SERVER_IP or SERVER_IP == "0.0.0.0"),
    )
tcp_udp = dlms.TcpUdpSetup(
    "0.0.25.2.0.255",
    port=TCP_PORT,
    ip_reference=ip_setup,
    max_segment_size=1460,
    max_simultaneous_connections=1,
    inactivity_timeout=120,
)

# ------------------ HDLC 链路层配置(对象用) ------------------
hdlc = dlms.IecHdlcSetup(
    "0.0.22.0.0.255",
    commSpeed=9600,
    windowSizeRx=1,
    windowSizeTx=1,
    maxInfoLenTx=128,
    maxInfoLenRx=128,
    timeout=120,
    deviceAddr=HDLC_DEVICE_ADDR,
)

all_objects = [
    energy, voltage, config_reg,
    public_read_reg, low_read_reg, high_read_reg,
    param, ldn, hdlc, ext_energy, ip_setup, tcp_udp,
]

# 完整 conformance(HIGH 的 HLS 需要 ACTION,长响应需要块传输)
FULL_CONF = (
    Conformance.BLOCK_TRANSFER_WITH_ACTION | Conformance.BLOCK_TRANSFER_WITH_SET_OR_WRITE |
    Conformance.BLOCK_TRANSFER_WITH_GET_OR_READ | Conformance.SET |
    Conformance.SELECTIVE_ACCESS | Conformance.ACTION |
    Conformance.MULTIPLE_REFERENCES | Conformance.GET
)

assoc_none = dlms.AssociationLogicalName("0.0.40.0.1.255")
assoc_none.auth_mechanism = "None"
assoc_none.clientSAP = CLIENT_ADDR_NONE
assoc_none.objects = list(all_objects)
assoc_none.context = dlms.DLMSContext(
    maxSendPduSize=128,
    maxReceivePduSize=128,
    conformance=FULL_CONF,
)

assoc_low = dlms.AssociationLogicalName("0.0.40.0.2.255")
assoc_low.auth_mechanism = "Low"
assoc_low.secret = b"12345678"
assoc_low.clientSAP = CLIENT_ADDR_LOW
assoc_low.objects = list(all_objects)
assoc_low.context = dlms.DLMSContext(
    maxSendPduSize=128,
    maxReceivePduSize=128,
    conformance=FULL_CONF,
)

assoc_high = dlms.AssociationLogicalName("0.0.40.0.3.255")
assoc_high.auth_mechanism = "High"
assoc_high.secret = b"12345678"
assoc_high.clientSAP = CLIENT_ADDR_HIGH
assoc_high.objects = list(all_objects)
assoc_high.context = dlms.DLMSContext(
    maxSendPduSize=128,
    maxReceivePduSize=128,
    conformance=FULL_CONF,
)

# ============================ 服务器 ============================
server = dlms.Server(serial_number=SERIAL_NUM, flag_id=FLAG_ID)
for obj in all_objects + [assoc_none, assoc_low, assoc_high]:
    server.add_object(obj)

generic_conn = dlms.GenericConnection(
    interface_type=dlms.InterfaceType.WRAPPER,  # TCP/IP WRAPPER
    frame_size=1024,
    pdu_size=512,
    tcp_udp_setup=tcp_udp,
    use_logical_name=True,
)

server.add_connection(generic_conn)
server.run()

print("[Server] DLMS server (NONE/LOW/HIGH) WRAPPER/TCP on port {}".format(TCP_PORT))
print("[Server] Server addr={}, password={}".format(SERVER_ADDRESS, PASSWORD))


# ============================ TCP 监听(单线程非阻塞轮询) ============================
_clients = []   # 每个元素: [sock, recv_buf(bytearray)]


def server_socket():
    """创建并绑定监听 socket(非阻塞)。支持 IPv4 / IPv6(由 USE_IPV6 决定)。"""
    if USE_IPV6:
        family   = socket.AF_INET6
        any_addr = "::"
        fam_name = "IPv6"
    else:
        family   = socket.AF_INET
        any_addr = "0.0.0.0"
        fam_name = "IPv4"
    srv = socket.socket(family, socket.SOCK_STREAM, socket.IPPROTO_TCP_SER)
    try:
        srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    except Exception:
        pass
    srv.setblocking(False)
    # IPv6:直接监听所有接口 [::];IPv4:先试 SERVER_IP,失败回退 0.0.0.0
    if USE_IPV6:
        srv.bind(("::", TCP_PORT))
    else:
        try:
            srv.bind((SERVER_IP, TCP_PORT))
        except Exception:
            try:
                srv.bind(("0.0.0.0", TCP_PORT))
            except Exception as e2:
                print("[Server] bind 0.0.0.0 failed: {}".format(e2))
                raise
    srv.listen(1)
    print("[Server] listening on {}:{} [{}]".format(
        "::" if USE_IPV6 else (SERVER_IP or "0.0.0.0"), TCP_PORT, fam_name))
    return srv


def pump_client(conn, entry):
    """非阻塞处理一个客户端连接:累积收帧 -> process_msg -> 回包。
    返回 False 表示连接已结束/出错,需要移除。"""
    sock, buf = entry
    try:
        while True:
            try:
                chunk = sock.recv(1024)
            except OSError:
                break   # 非阻塞:暂无数据
            if not chunk:
                print("[Server] client closed")
                sock.close()
                return False
            buf += chunk
            # 尝试解析出 1 个或多个完整 WRAPPER 帧
            while len(buf) >= 8:
                length = (buf[6] << 8) | buf[7]
                if length <= 0 or len(buf) < 8 + length:
                    break   # 帧未收齐,等待更多数据
                frame = bytes(buf[:8 + length])
                # QuecPython bytearray 不支持 del buf[:n],用切片重建
                buf = bytearray(buf[8 + length:])
                entry[1] = buf
                resp = conn.process_msg(frame)
                if resp:
                    sock.send(resp)
    except Exception as e:
        print("[Server] client error: {}".format(e))
        sock.close()
        return False
    return True


def poll_once(conn, srv):
    """轮询一次:accept 新客户端 + 处理现有客户端的所有待收数据。"""
    try:
        # 注意:QuecPython 的 accept() 返回 3 元组 (sock, ip_str, port),不是标准 2 元组!
        res = srv.accept()
        c = res[0]
        addr = res[1]
        c.setblocking(False)
        _clients.append([c, bytearray()])
        print("[Server] client connected: {}".format(addr))
    except OSError:
        pass   # 无新连接
    for entry in _clients[:]:
        if not pump_client(conn, entry):
            try:
                _clients.remove(entry)
            except Exception:
                pass


# 建立监听 socket(单线程轮询,不依赖 _thread 后台线程)
srv = server_socket()
generic_conn.connect()

# ============================ 主循环(TCP 轮询 + 采样) ============================
try:
    seq = 0
    while True:
        seq += 1
        poll_once(generic_conn, srv)         # 非阻塞处理 TCP 收发
        v = 228 + (seq % 7)                  # 模拟电压采样波动
        voltage.value = v
        if LOG_SAMPLE and seq % 10 == 0:     # 每 10 轮(约 2 秒)打印一次采样
            print("[Server] sample: voltage={} V, energy={}".format(v, energy.value))
        utime.sleep(0.2)                     # 短 sleep,让出 GIL 供 DLMS monitor 线程运行
except KeyboardInterrupt:
    server.stop()
    print("[Server] stopped")

客户端(NONE)

# -*- coding: utf-8 -*-
"""
DLMS 客户端脚本(简洁版)- GenericConnection TCP/WRAPPER,NONE 认证
==================================================================
  * client_address = 0x10 -> 服务器 NONE 关联对象(clientSAP=0x10)
  * server_address = 144  -> 服务器地址(逻辑=1, 物理=16)
"""

import utime

try:
    import usocket as socket  # QuecPython / MicroPython
except ImportError:
    try:
        import socket  # 标准名兼容
    except ImportError:
        socket = None  # 无 socket 模块

import dlms

# ============================ 配置区 ============================
# IPv4/IPv6 开关(与服务器一致)
USE_IPV6    = True        # False = IPv4;True = IPv6
SERVER_IP   = "240E:452:DDAB:928D::1"   # 模组 A 当前 IPv6
TCP_PORT    = 4020             # 必须与服务器一致

CLIENT_ADDRESS = 0x10           # 对应服务器 NONE 关联对象 clientSAP
SERVER_ADDRESS = 144            # 电表(服务器)地址(逻辑=1, 物理=16)
HDLC_DEVICE_ADDR = 16

USE_LOGICAL_NAME = True
AUTHENTICATION   = dlms.Authentication.NONE   # NONE 认证:无密码
PASSWORD         = None
SECURITY         = 0x00
# ================================================================

LN_ENERGY  = "1.0.1.8.0.255"
LN_VOLT    = "1.0.32.7.0.255"
LN_CONFIG  = "1.0.25.1.0.255"
LN_PUBLIC  = "1.0.10.1.0.255"
LN_LOWREAD = "1.0.11.1.0.255"
LN_HIGHREAD = "1.0.12.1.0.255"


def build_tcp_setup():
    # 按 IP 版本创建正确的 IP 设置对象(IPv6 用 IPv6Setup,IPv4 用 IPv4Setup)
    if USE_IPV6:
        ip_setup = dlms.IPv6Setup(
            "0.0.25.7.0.255",
            address_config_mode=2,               # MANUAL
            unicast_ip_address=[SERVER_IP],      # 模组 A 的 IPv6(对象数据与实际一致)
        )
    else:
        ip_setup = dlms.IPv4Setup(
            "0.0.25.1.0.255",
            ip_address=SERVER_IP,
            subnet_mask="255.255.255.0",
            use_dhcp=False,
        )
    tcp_udp = dlms.TcpUdpSetup(
        "0.0.25.2.0.255",
        port=TCP_PORT,
        ip_reference=ip_setup,
        max_segment_size=1460,
        max_simultaneous_connections=1,
        inactivity_timeout=120,
    )
    return ip_setup, tcp_udp


def _recv(sock, timeout_ms):
    """直接 recv 一段,超时返回 None(无 TX/RX 回显)。"""
    try:
        sock.settimeout(max(0.001, float(timeout_ms) / 1000.0))
        return sock.recv(4096)
    except Exception:
        return None


def build_connection(sock, tcp_udp):
    conn = dlms.GenericConnection(
        interface_type=dlms.InterfaceType.WRAPPER,   # TCP/IP WRAPPER
        frame_size=1024,
        pdu_size=1024,
        tcp_udp_setup=tcp_udp,
        use_logical_name=USE_LOGICAL_NAME,
    )
    conn.on_send = lambda data: sock.send(data)
    conn.on_receive = lambda timeout_ms: _recv(sock, timeout_ms)
    return conn


def build_client():
    return dlms.Client(
        client_address=CLIENT_ADDRESS,
        server_address=SERVER_ADDRESS,
        authentication=AUTHENTICATION,
        password=PASSWORD,
        security=SECURITY,
        use_logical_name=USE_LOGICAL_NAME,
    )


def read_attr(client, ln, attr):
    try:
        val = client.read(ln, attr)
        print("[Read ] {} attr{} = {}".format(ln, attr, val))
        return val
    except Exception as e:
        print("[Read ] {} attr{} FAILED: {}".format(ln, attr, e))
        return None


def write_attr(client, ln, attr, value):
    try:
        client.write(ln, attr, value)
        print("[Write] {} attr{} = {} OK".format(ln, attr, value))
        return True
    except Exception as e:
        print("[Write] {} attr{} = {} FAILED: {}".format(ln, attr, value, e))
        return False


def run_once(client):
    read_attr(client, LN_VOLT, 2)
    read_attr(client, LN_ENERGY, 2)
    read_attr(client, LN_PUBLIC, 2)
    # 权限验证:LOW+ / HIGH 对象应被拒
    read_attr(client, LN_LOWREAD, 2)    # 期望 FAILED
    read_attr(client, LN_HIGHREAD, 2)   # 期望 FAILED
    # 写
    write_attr(client, LN_ENERGY, 2, 2222)         # OK
    write_attr(client, LN_CONFIG, 2, 9999)         # 期望 FAILED



def main():
    ipver = "IPv6" if USE_IPV6 else "IPv4"
    print("[Client] TCP {}:{} [{}] NONE auth, client=0x{:02X}, server={}".format(
        SERVER_IP, TCP_PORT, ipver, CLIENT_ADDRESS, SERVER_ADDRESS))

    if socket is None:
        print("[Client] socket unavailable on PC")
        return
    client = build_client()

    while True:
        try:
            fam = socket.AF_INET6 if USE_IPV6 else socket.AF_INET
            sock = socket.socket(fam, socket.SOCK_STREAM)
            # 用 getaddrinfo 解析出 QuecPython 认可的 sockaddr(IPv6 为 4 元组)
            addr = socket.getaddrinfo(SERVER_IP, TCP_PORT, fam)[0][-1]
            sock.connect(addr)
            _, tcp_udp = build_tcp_setup()
            conn = build_connection(sock, tcp_udp)
            client.connect(conn)     # WRAPPER 无 SNRM,直接 AARQ
            print("[Client] connected & associated! (NONE auth)")
            run_once(client)
            break
        except Exception as e:
            print("[Client] connect/run failed: {}".format(e))
            utime.sleep(3)

    try:
        while True:
            utime.sleep(10)
            read_attr(client, LN_VOLT, 2)
            read_attr(client, LN_ENERGY, 2)
    except KeyboardInterrupt:
        pass
    finally:
        try:
            client.disconnect()
        except Exception:
            pass


if __name__ == "__main__":
    main()

客户端(LOW)

# -*- coding: utf-8 -*-
"""
DLMS 客户端脚本(简洁版)- GenericConnection TCP/WRAPPER,LOW 认证
==================================================================
  * client_address = 2    -> 服务器 LOW 关联对象(clientSAP=2)
  * server_address = 144  -> 服务器地址(逻辑=1, 物理=16)
  * LOW 认证:密码 "12345678"(AARQ 阶段比对,服务器 assoc_low.secret)
"""

import utime

try:
    import usocket as socket  # QuecPython / MicroPython
except ImportError:
    try:
        import socket  # 标准名兼容
    except ImportError:
        socket = None  # 无 socket 模块

import dlms

# ============================ 配置区 ============================
# IPv4/IPv6 开关(与服务器一致):False = IPv4;True = IPv6
USE_IPV6    = True
SERVER_IP   = "240E:452:DEBE:566B::1"
TCP_PORT    = 4020              # 必须与服务器一致

CLIENT_ADDRESS = 2              # 对应服务器 LOW 关联对象 clientSAP
SERVER_ADDRESS = 144            # 电表(服务器)地址(逻辑=1, 物理=16)
HDLC_DEVICE_ADDR = 16

USE_LOGICAL_NAME = True
AUTHENTICATION   = dlms.Authentication.LOW    # LOW 认证:密码方式
PASSWORD         = "12345678"                 # 必须与服务器 assoc_low.secret 一致
SECURITY         = 0x00
# ================================================================

LN_ENERGY  = "1.0.1.8.0.255"
LN_VOLT    = "1.0.32.7.0.255"
LN_CONFIG  = "1.0.25.1.0.255"
LN_PUBLIC  = "1.0.10.1.0.255"
LN_LOWREAD = "1.0.11.1.0.255"
LN_HIGHREAD = "1.0.12.1.0.255"


def read_wrapper_frame(sock, timeout_ms):
    """读取一个完整 WRAPPER 帧;超时/断开返回 None。"""
    sock.settimeout(timeout_ms / 1000.0)
    try:
        hdr = b""
        while len(hdr) < 8:
            chunk = sock.recv(8 - len(hdr))
            if not chunk:
                return None
            hdr += chunk
        length = (hdr[6] << 8) | hdr[7]
        body = b""
        while len(body) < length:
            chunk = sock.recv(length - len(body))
            if not chunk:
                return None
            body += chunk
        return hdr + body
    except OSError:
        return None


def build_tcp_setup():
    # 按 IP 版本创建正确的 IP 设置对象(IPv6 用 IPv6Setup,IPv4 用 IPv4Setup)
    if USE_IPV6:
        ip_setup = dlms.IPv6Setup(
            "0.0.25.7.0.255",
            address_config_mode=2,               # MANUAL
            unicast_ip_address=[SERVER_IP],      # 模组 A 的 IPv6(对象数据与实际一致)
        )
    else:
        ip_setup = dlms.IPv4Setup(
            "0.0.25.1.0.255",
            ip_address=SERVER_IP,
            subnet_mask="255.255.255.0",
            use_dhcp=False,
        )
    tcp_udp = dlms.TcpUdpSetup(
        "0.0.25.2.0.255",
        port=TCP_PORT,
        ip_reference=ip_setup,
        max_segment_size=1460,
        max_simultaneous_connections=1,
        inactivity_timeout=120,
    )
    return ip_setup, tcp_udp


def _recv(sock, timeout_ms):
    """直接 recv 一段,超时返回 None(无 TX/RX 回显)。"""
    try:
        sock.settimeout(max(0.001, float(timeout_ms) / 1000.0))
        return sock.recv(4096)
    except Exception:
        return None


def build_connection(sock, tcp_udp):
    conn = dlms.GenericConnection(
        interface_type=dlms.InterfaceType.WRAPPER,   # TCP/IP WRAPPER
        frame_size=1024,
        pdu_size=1024,
        tcp_udp_setup=tcp_udp,
        use_logical_name=USE_LOGICAL_NAME,
    )
    conn.on_send = lambda data: sock.send(data)
    conn.on_receive = lambda timeout_ms: _recv(sock, timeout_ms)
    return conn


def build_client():
    return dlms.Client(
        client_address=CLIENT_ADDRESS,
        server_address=SERVER_ADDRESS,
        authentication=AUTHENTICATION,
        password=PASSWORD,
        security=SECURITY,
        use_logical_name=USE_LOGICAL_NAME,
    )


def read_attr(client, ln, attr):
    try:
        val = client.read(ln, attr)
        print("[Read ] {} attr{} = {}".format(ln, attr, val))
        return val
    except Exception as e:
        print("[Read ] {} attr{} FAILED: {}".format(ln, attr, e))
        return None


def write_attr(client, ln, attr, value):
    try:
        client.write(ln, attr, value)
        print("[Write] {} attr{} = {} OK".format(ln, attr, value))
        return True
    except Exception as e:
        print("[Write] {} attr{} = {} FAILED: {}".format(ln, attr, value, e))
        return False


def run_once(client):
    read_attr(client, LN_VOLT, 2)
    read_attr(client, LN_ENERGY, 2)
    read_attr(client, LN_PUBLIC, 2)
    read_attr(client, LN_LOWREAD, 2)    # LOW 可读
    # 权限验证:HIGH 对象应被拒
    read_attr(client, LN_HIGHREAD, 2)   # 期望 FAILED
    # 写
    write_attr(client, LN_ENERGY, 2, 3333)         # OK
    write_attr(client, LN_CONFIG, 2, 9999)         # 期望 FAILED


def main():
    print("[Client] TCP {}:{} LOW auth, client={}, server={}, pwd='{}'".format(
        SERVER_IP, TCP_PORT, CLIENT_ADDRESS, SERVER_ADDRESS, PASSWORD))

    if socket is None:
        print("[Client] socket unavailable on PC")
        return
    client = build_client()

    while True:
        try:
            fam = socket.AF_INET6 if USE_IPV6 else socket.AF_INET
            sock = socket.socket(fam, socket.SOCK_STREAM)
            # 用 getaddrinfo 解析出 QuecPython 认可的 sockaddr(IPv6 为 4 元组)
            addr = socket.getaddrinfo(SERVER_IP, TCP_PORT, fam)[0][-1]
            sock.connect(addr)
            _, tcp_udp = build_tcp_setup()
            conn = build_connection(sock, tcp_udp)
            client.connect(conn)     # WRAPPER 无 SNRM,直接 AARQ
            print("[Client] connected & associated! (LOW auth)")
            run_once(client)
            break
        except Exception as e:
            print("[Client] connect/run failed: {}".format(e))
            utime.sleep(3)

    try:
        while True:
            utime.sleep(10)
            read_attr(client, LN_VOLT, 2)
            read_attr(client, LN_ENERGY, 2)
    except KeyboardInterrupt:
        pass
    finally:
        try:
            client.disconnect()
        except Exception:
            pass


if __name__ == "__main__":
    main()

客户端(HIGH)

# -*- coding: utf-8 -*-
"""
DLMS 客户端脚本(简洁版)- GenericConnection TCP/WRAPPER,HIGH 认证
==================================================================
  * client_address = 5    -> 服务器 HIGH 关联对象(clientSAP=5)
  * server_address = 144  -> 服务器地址(逻辑=1, 物理=16)
  * HIGH 认证:HLS 挑战-响应,密码 "12345678" 作为挑战密钥
"""

import utime

try:
    import usocket as socket  # QuecPython / MicroPython
except ImportError:
    try:
        import socket  # 标准名兼容
    except ImportError:
        socket = None  # 无 socket 模块

import dlms

# ============================ 配置区 ============================
# IPv4/IPv6 开关(与服务器一致)
USE_IPV6    = True        # False = IPv4;True = IPv6
SERVER_IP   = "240E:452:DEBE:566B::1"   # 模组 A 当前 IPv6
TCP_PORT    = 4020             # 必须与服务器一致

CLIENT_ADDRESS = 5              # 对应服务器 HIGH 关联对象 clientSAP
SERVER_ADDRESS = 144            # 电表(服务器)地址(逻辑=1, 物理=16)
HDLC_DEVICE_ADDR = 16

USE_LOGICAL_NAME = True
AUTHENTICATION   = dlms.Authentication.HIGH   # HIGH 认证:HLS 挑战-响应
PASSWORD         = "12345678"                 # 挑战密钥,必须与服务器 assoc_high.secret 一致
SECURITY         = 0x00
# ================================================================

LN_ENERGY  = "1.0.1.8.0.255"
LN_VOLT    = "1.0.32.7.0.255"
LN_CONFIG  = "1.0.25.1.0.255"
LN_PUBLIC  = "1.0.10.1.0.255"
LN_LOWREAD = "1.0.11.1.0.255"
LN_HIGHREAD = "1.0.12.1.0.255"


def read_wrapper_frame(sock, timeout_ms):
    """读取一个完整 WRAPPER 帧;超时/断开返回 None。"""
    sock.settimeout(timeout_ms / 1000.0)
    try:
        hdr = b""
        while len(hdr) < 8:
            chunk = sock.recv(8 - len(hdr))
            if not chunk:
                return None
            hdr += chunk
        length = (hdr[6] << 8) | hdr[7]
        body = b""
        while len(body) < length:
            chunk = sock.recv(length - len(body))
            if not chunk:
                return None
            body += chunk
        return hdr + body
    except OSError:
        return None


def build_tcp_setup():
    # 按 IP 版本创建正确的 IP 设置对象(IPv6 用 IPv6Setup,IPv4 用 IPv4Setup)
    if USE_IPV6:
        ip_setup = dlms.IPv6Setup(
            "0.0.25.7.0.255",
            address_config_mode=2,               # MANUAL
            unicast_ip_address=[SERVER_IP],      # 模组 A 的 IPv6(对象数据与实际一致)
        )
        print("[Client] TCP {}:{} [{}] HIGH auth, client={}, server={}, pwd='{}'".format(
            SERVER_IP, TCP_PORT, "IPv6", CLIENT_ADDRESS, SERVER_ADDRESS, PASSWORD))
    else:
        ip_setup = dlms.IPv4Setup(
            "0.0.25.1.0.255",
            ip_address=SERVER_IP,
            subnet_mask="255.255.255.0",
            use_dhcp=False,
        )
    tcp_udp = dlms.TcpUdpSetup(
        "0.0.25.2.0.255",
        port=TCP_PORT,
        ip_reference=ip_setup,
        max_segment_size=1460,
        max_simultaneous_connections=1,
        inactivity_timeout=120,
    )
    return ip_setup, tcp_udp


def _recv(sock, timeout_ms):
    """直接 recv 一段,超时返回 None(无 TX/RX 回显)。"""
    try:
        sock.settimeout(max(0.001, float(timeout_ms) / 1000.0))
        return sock.recv(4096)
    except Exception:
        return None


def build_connection(sock, tcp_udp):
    conn = dlms.GenericConnection(
        interface_type=dlms.InterfaceType.WRAPPER,   # TCP/IP WRAPPER
        frame_size=1024,
        pdu_size=1024,
        tcp_udp_setup=tcp_udp,
        use_logical_name=USE_LOGICAL_NAME,
    )
    conn.on_send = lambda data: sock.send(data)
    conn.on_receive = lambda timeout_ms: _recv(sock, timeout_ms)
    return conn


def build_client():
    return dlms.Client(
        client_address=CLIENT_ADDRESS,
        server_address=SERVER_ADDRESS,
        authentication=AUTHENTICATION,
        password=PASSWORD,
        security=SECURITY,
        use_logical_name=USE_LOGICAL_NAME,
    )


def read_attr(client, ln, attr):
    try:
        val = client.read(ln, attr)
        print("[Read ] {} attr{} = {}".format(ln, attr, val))
        return val
    except Exception as e:
        print("[Read ] {} attr{} FAILED: {}".format(ln, attr, e))
        return None


def write_attr(client, ln, attr, value):
    try:
        client.write(ln, attr, value)
        print("[Write] {} attr{} = {} OK".format(ln, attr, value))
        return True
    except Exception as e:
        print("[Write] {} attr{} = {} FAILED: {}".format(ln, attr, value, e))
        return False


def run_once(client):
    read_attr(client, LN_VOLT, 2)
    read_attr(client, LN_ENERGY, 2)
    read_attr(client, LN_PUBLIC, 2)
    read_attr(client, LN_LOWREAD, 2)
    read_attr(client, LN_HIGHREAD, 2)   # 仅 HIGH 可读
    read_attr(client, LN_CONFIG, 2)
    # 写
    write_attr(client, LN_ENERGY, 2, 4444)         # OK
    write_attr(client, LN_CONFIG, 2, 9999)         # OK(仅 HIGH 可写)
    read_attr(client, LN_CONFIG, 2)                # 回读验证


def main():
    ipver = "IPv6" if USE_IPV6 else "IPv4"
    print("[Client] TCP {}:{} [{}] HIGH auth, client={}, server={}, pwd='{}'".format(
        SERVER_IP, TCP_PORT, ipver, CLIENT_ADDRESS, SERVER_ADDRESS, PASSWORD))

    if socket is None:
        print("[Client] socket unavailable on PC")
        return
    client = build_client()

    while True:
        try:
            fam = socket.AF_INET6 if USE_IPV6 else socket.AF_INET
            sock = socket.socket(fam, socket.SOCK_STREAM)
            # 用 getaddrinfo 解析出 QuecPython 认可的 sockaddr(IPv6 为 4 元组)
            addr = socket.getaddrinfo(SERVER_IP, TCP_PORT, fam)[0][-1]
            sock.connect(addr)
            _, tcp_udp = build_tcp_setup()
            conn = build_connection(sock, tcp_udp)
            client.connect(conn)     # WRAPPER 无 SNRM,AARQ + HLS(自动)
            print("[Client] connected & associated! (HIGH auth)")
            run_once(client)
            break
        except Exception as e:
            print("[Client] connect/run failed: {}".format(e))
            utime.sleep(3)

    try:
        while True:
            utime.sleep(10)
            read_attr(client, LN_VOLT, 2)
            read_attr(client, LN_ENERGY, 2)
    except KeyboardInterrupt:
        pass
    finally:
        try:
            client.disconnect()
        except Exception:
            pass


if __name__ == "__main__":
    main()

连接生命周期回调

所有连接类型共享以下回调:

回调 触发时机
on_connected 物理链路建立(HDLC 链接 up、UDP 套接字绑定、或 GenericConnection.connect() 调用)
on_disconnected 链路断开(超时、传输错误、或 disconnect() 调用)
on_send 原始字节即将发送(C 驱动连接上为监控钩子; GenericConnection 客户端模式需接入发送)
on_receive 收到原始字节(功能同 on_send ,方向相反)
  • C 驱动连接( SerialConnection / OpticalConnection / MobileConnection )的回调在 C 监听线程中触发
  • GenericConnection 的回调在调用 connect() 的 Python 线程中触发
  • 保持回调简短,避免阻塞操作

服务器配置(Server)

本章介绍如何将 COSEM 对象、安全关联对象、连接和 Server 实例组装成一个运行中的 DLMS 服务器。


示例

最小骨架

import dlms
from dlms import Conformance
import utime

# ============================ 配置区 ============================
UART_PORT   = 2        # 模组 A 使用的 UART 口
BAUD        = 9600     # 波特率,必须与客户端一致
CLIENT_SAP  = 0x10     # 允许接入的客户端 SAP(None 认证关联)
FLAG_ID     = "GRX"    # 厂商代码(3 字符)
SERIAL_NUM  = 12345    # 电表序列号(≤5 位,Python 模式由 set_serial_number 设置)
# 服务器地址 = 序列号 % 10000 + 1000
#   Python 服务器模式:12345 -> 2345 + 1000 = 3345
#   (注意:不再是内置模式的 123456 -> 4456)
SERVER_ADDR = dlms.hdlc_server_address(SERIAL_NUM)   # = 3345,同时作为 IecHdlcSetup 设备地址
# ================================================================

# Python 服务器模式(CONFIG_DLMS_BUILTIN_SERVER 关闭):events.c 生效,
# svr_isTarget 读取 SRV_SERIAL_NUMBER(由本调用设置),
# 客户端 server_address 必须 == 该值 % 10000 + 1000。
dlms.set_serial_number(SERIAL_NUM)

# ============================ COSEM 对象 ============================
# 电能寄存器:属性2(value) 可读可写 —— 用于演示 client.write()
energy = dlms.Register(
    "1.0.1.8.0.255",
    default_value=12345,
    scaler=1,
    access={
        dlms.Authentication.NONE: {
            2: dlms.AccessMode.READ_WRITE,   # value:客户端可写
            3: dlms.AccessMode.READ,         # scaler/unit
        }
    }
)

# 电压寄存器:只读 —— 用于演示 client.read()
voltage = dlms.Register(
    "1.0.32.7.0.255",
    default_value=230,
    scaler=1,
    access={
        dlms.Authentication.NONE: {
            2: dlms.AccessMode.READ,
            3: dlms.AccessMode.READ,
        }
    }
)

# 扩展寄存器(ExtendedRegister,COSEM class 4):
#   value(2) / scaler-unit(3) / status(4) / capture_time(5)
# 带状态与采集时间,常用于最大需量、费率电量等。value 可读写。
ext_energy = dlms.ExtendedRegister(
    "1.0.1.8.1.255",
    value=500,
    scaler=0,
    unit=dlms.Unit.ACTIVE_ENERGY,
    status=0,
    access={
        dlms.Authentication.NONE: {
            2: dlms.AccessMode.READ_WRITE,   # value:客户端可写
            3: dlms.AccessMode.READ,         # scaler/unit
            4: dlms.AccessMode.READ,         # status
            5: dlms.AccessMode.READ,         # capture_time
        }
    }
)
# 设置采集时间(6 元组:年/月/日/时/分/秒)
ext_energy.capture_time = (2026, 8, 19, 12, 0, 0)

# 参数对象:可读可写
param = dlms.Data(
    "0.0.1.1.0.255",
    access={dlms.Authentication.NONE: {2: dlms.AccessMode.READ_WRITE}}
)
param.value = 42

# 逻辑设备名 LDN:只读
ldn = dlms.Data(
    "0.0.42.0.0.255",
    access={dlms.Authentication.NONE: {2: dlms.AccessMode.READ}}
)
ldn.value = b"SN12345"

# ------------------ HDLC 链路层配置 ------------------
hdlc = dlms.IecHdlcSetup(
    "0.0.22.0.0.255",
    commSpeed=BAUD,          # 直接写实际波特率数值
    windowSizeRx=1,
    windowSizeTx=1,
    maxInfoLenTx=128,
    maxInfoLenRx=128,
    timeout=120,             # 空闲超时(秒)
    deviceAddr=SERVER_ADDR,  # 与客户端 server_address 推导规则一致
)

# ------------------ 关联对象(None 认证) ------------------
assoc = dlms.AssociationLogicalName("0.0.40.0.1.255")
assoc.auth_mechanism = "None"
assoc.clientSAP = CLIENT_SAP
assoc.objects = [energy, voltage, param, ldn, hdlc, ext_energy]
assoc.context = dlms.DLMSContext(
    maxSendPduSize=128,
    maxReceivePduSize=128,
    # 包含 SET,客户端才能写入;若只读可去掉 SET
    conformance=Conformance.GET | Conformance.SET,
)

# ============================ 服务器 ============================
server = dlms.Server(serial_number=SERIAL_NUM, flag_id=FLAG_ID)
for obj in [energy, voltage, param, ldn, hdlc, ext_energy, assoc]:
    server.add_object(obj)

# SerialConnection:C 驱动 UART,服务器端自动监听并回包
serial_conn = dlms.SerialConnection(
    uart_port=UART_PORT,
    hdlc_setup=hdlc,
    flowcontrol=0,
    interface_type=dlms.InterfaceType.HDLC,  # 必须 HDLC(与客户端一致)
    use_logical_name=True,
)

server.add_connection(serial_conn)
server.run()   # 非阻塞,连接在后台线程中监听
print("[Server] DLMS server running on UART{} @ {} baud".format(UART_PORT, BAUD))
print("[Server] Serial num={}, Server addr={} (serial%10000+1000), Client SAP=0x{:02X}".format(
    SERIAL_NUM, SERVER_ADDR, CLIENT_SAP))
print("[Server] Objects: energy(1.0.1.8.0.255) voltage(1.0.32.7.0.255) param(0.0.1.1.0.255)")

# ============================ 主循环 ============================
# 模拟电表周期采集,更新寄存器值(客户端可读到变化的电压)
try:
    seq = 0
    while True:
        seq += 1
        v = 228 + (seq % 7)          # 230/229/... 波动,模拟真实采样
        voltage.value = v
        print("[Server] sample: voltage={} V".format(v))
        utime.sleep(5)
except KeyboardInterrupt:
    server.stop()
    print("[Server] stopped")



将对象组织到模块中

按子系统分组(能源、安全、网络、预付费等),便于条件编译。若某个板型不支持预付费,只需省略 setup_prepayment_objects() 调用,相关对象永远不会进入 add_object() 。启动序列的其他部分无需改动。


多连接

服务器最多支持 8 个 同时连接,每个在独立线程中运行。SN 和 LN 关联可在同一服务器上共存:

  • 一个 SerialConnection 使用 use_logical_name=True 服务 LN 客户端
  • 另一个 SerialConnection 使用 use_logical_name=False (不同 UART 端口)服务 SN 客户端
  • 一个 MobileConnection 可与两者同时运行

每个连接独占其 UART。两个 SerialConnection (或 GenericConnection )实例 不能共享 同一 UART 端口号。


事件代码和事件日志

server.set_event_code() 将 C 层的内部事件代码跟踪连接到两个 Python 对象,使 C 层检测到的事件代码变化自动反映到对象中,无需 Python 侧轮询。

import dlms

event_code = dlms.Data("0.0.96.11.0.255")
event_code.value = 0

event_log = dlms.ProfileGeneric(
    "0.0.99.98.0.255",
    capture_objects=[(clock, 2, 0), (event_code, 2, 0)],
    profile_entries=200,
)

server.set_event_code(event_code, event_log)

启动后,写入 event_code.value 且值发生变化时,服务器自动向 event_log 捕获一行:

event_code.value = 255  # "power fail" — 自动捕获到 event_log

server.set_event_code() 必须 server.run() 之前调用,之后调用无效。


RegisterMonitor 与后台监控

server.run() 启动后,服务器创建一个后台监控线程,每秒唤醒一次检查所有 RegisterMonitor 阈值。阈值被触发时执行对应的 ScriptTable 动作。

若应用更新了受监控寄存器的值并希望立即检查阈值(无需等待最多 1 秒),可调用 server.monitor()

energy_reg.value = read_energy_sensor()
server.monitor()  # 立即检查;若阈值被触发则执行 ScriptTable 动作
  • server.monitor() 返回已检查的连接数
  • 若监控返回错误则抛出 RuntimeError
  • 可从主应用线程安全调用,与后台监控线程并发执行

客户端(Client)

dlms.Client 提供同步接口,用于读取属性、写入属性和调用远程 DLMS/COSEM 服务器的方法。支持与服务器端相同的所有连接类型:串口、光口和蜂窝。所有客户端方法在通信失败时抛出 RuntimeError ,生产代码应将调用包裹在 try/except 中。


客户端地址和服务端地址

DLMS 会话由一对地址标识: 客户端地址 服务端地址

客户端地址 是 DLMS SAP,必须与目标服务器 AssociationLogicalName 上配置的 clientSAP 一致。常用约定值:

对应关联
16 ( 0x10 ) 公开(无认证)关联
18 ( 0x12 ) High(挑战-响应)认证关联
1 HighGMac(AES-GCM)关联

任何一致的值均可;上述仅匹配常见 DLMS 测试工具的默认值。

服务端地址 由设备序列号派生。HDLC 连接(串口和光口)的计算公式为 serial % 10000 + 1000 。辅助函数 dlms.hdlc_server_address(serial) 执行此计算:

import dlms

addr = dlms.hdlc_server_address(12345)  # 序列号 12345 → 服务端地址 3345

蜂窝中继连接同样适用此公式:中继服务器使用帧中嵌入的 HDLC 服务端地址将数据包路由到正确的设备。


构造客户端

Client 构造函数参数:

参数 类型 默认值 说明
client_address int 16 DLMS SAP
server_address int 1 HDLC 服务端地址
authentication int Authentication.NONE 认证级别常量
password str / None None Low 或 High 认证的密码;HighGMac 不需要
use_logical_name bool True True =LN 引用, False =SN 引用
system_title bytes / None None 客户端 8 字节系统标题(HighGMac 必需)
authentication_key bytes / None None GAK(HighGMac 必需)
block_cipher_key bytes / None None GUEK(HighGMac 必需)
security int 0 SecurityPolicy 值(HighGMac 必需)

公开(无认证)客户端

import dlms

client = dlms.Client(
    client_address=16,
    server_address=dlms.hdlc_server_address(12345),
)

HighGMac 客户端

import dlms

client = dlms.Client(
    client_address=1,
    server_address=dlms.hdlc_server_address(12345),
    authentication=dlms.Authentication.HIGH_GMAC,
    system_title=b'GRX00001',
    authentication_key=b'\xD0\xD1\xD2\xD3\xD4\xD5\xD6\xD7\xD8\xD9\xDA\xDB\xDC\xDD\xDE\xDF',
    block_cipher_key=b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F',
    security=dlms.SecurityPolicy.AUTHENTICATED_ENCRYPTED,
)

客户端最小骨架示例

import dlms
import utime

# ============================ 配置区 ============================
UART_PORT = 2        # 模组 B 使用的 UART 口
BAUD      = 9600     # 波特率,必须与服务器端一致

# 服务器端序列号 —— 必须与服务器脚本中的 SERIAL_NUM 一致!
# 当前配置为 Python 服务器模式(CONFIG_DLMS_BUILTIN_SERVER 关闭),
# 服务器地址由 dlms.set_serial_number(12345) 设置:
#   server_address = 12345 % 10000 + 1000 = 3345
SERVER_SERIAL = 12345

CLIENT_ADDR = 0x10   # 16,必须匹配服务器关联对象 clientSAP
# 服务器地址 = 序列号 % 10000 + 1000
#   Python 服务器 (CONFIG_DLMS_BUILTIN_SERVER 关闭,当前):
#     12345 -> 2345 + 1000 = 3345   ← 用这个
#   内置 C 服务器 (CONFIG_DLMS_BUILTIN_SERVER=y):
#     123456 -> 3456 + 1000 = 4456  (C 硬编码,Python 不可改)
SERVER_ADDR = dlms.hdlc_server_address(SERVER_SERIAL)
AUTH        = dlms.Authentication.NONE

# 演示用对象的逻辑名
LN_ENERGY  = "1.0.1.8.0.255"    # 电能寄存器(可写)
LN_VOLTAGE = "1.0.32.7.0.255"   # 电压寄存器(只读)
LN_PARAM   = "0.0.1.1.0.255"    # 参数 Data(可写)
LN_ext_energy = "1.0.1.8.1.255"
# ================================================================

# ------------------ HDLC 链路层配置(必须与服务器一致) ------------------
hdlc = dlms.IecHdlcSetup(
    "0.0.22.0.0.255",
    commSpeed=BAUD,
    windowSizeRx=1,
    windowSizeTx=1,
    maxInfoLenTx=128,
    maxInfoLenRx=128,
    timeout=120,
    deviceAddr=0x10,
)

serial_conn = dlms.SerialConnection(
    uart_port=UART_PORT,
    hdlc_setup=hdlc,
    flowcontrol=0,
    interface_type=dlms.InterfaceType.HDLC,  # 必须 HDLC
    use_logical_name=True,
)

client = dlms.Client(
    client_address=CLIENT_ADDR,
    server_address=SERVER_ADDR,
    authentication=AUTH,
    use_logical_name=True,
)


def read_attr(ln, attr):
    """读取单个属性,失败返回 None 并打印错误。"""
    try:
        val = client.read(ln, attr)
        print("[Read ] {} attr{} = {}".format(ln, attr, val))
        return val
    except Exception as e:
        print("[Read ] {} attr{} FAILED: {}".format(ln, attr, e))
        return None


def write_attr(ln, attr, value):
    """写入单个属性,返回是否成功。"""
    try:
        client.write(ln, attr, value)
        print("[Write] {} attr{} = {} OK".format(ln, attr, value))
        return True
    except Exception as e:
        print("[Write] {} attr{} = {} FAILED: {}".format(ln, attr, value, e))
        return False


def run_once():
    """单轮测试:读取所有对象 -> 写电能 -> 回读验证。"""
    print("-" * 50)

    # 1) 读取(简单模式:逻辑名 + 属性号)
    voltage = read_attr(LN_VOLTAGE, 2)      # 电压寄存器值
    read_attr(LN_ENERGY, 2)                 # 电能寄存器值
    read_attr(LN_ENERGY, 3)                 # scaler/unit
    read_attr(LN_PARAM, 2)                  # 参数

    # 2) 写入电能寄存器(属性2 value)
    new_energy = (voltage or 0) + 1000      # 演示:基于读到的电压拼一个值
    write_attr(LN_ENERGY, 2, new_energy)
    write_attr(LN_ext_energy, 2, new_energy)
    # 3) 写参数对象
    write_attr(LN_PARAM, 2, 100)

    # 4) 回读验证
    read_attr(LN_ENERGY, 2)
    read_attr(LN_PARAM, 2)


def main():
    print("[Client] Connecting to server via UART{} @ {} baud ...".format(UART_PORT, BAUD))
    print("[Client] client_addr=0x{:02X}, server_addr={}".format(CLIENT_ADDR, SERVER_ADDR))

    while True:
        try:
            client.connect(serial_conn)     # 阻塞直到关联(AARQ/AARE)完成
            print("[Client] connected & associated!")
            run_once()
            break
        except Exception as e:
            print("[Client] connect/run failed: {}".format(e))
            utime.sleep(3)

    try:
        while True:
            utime.sleep(10)
            read_attr(LN_VOLTAGE, 2)        # 周期性读取服务器采样的电压
            read_attr(LN_ENERGY, 2)
    except KeyboardInterrupt:
        pass
    finally:
        try:
            client.disconnect()
            print("[Client] disconnected")
        except Exception:
            pass


main()

会话状态属性

属性 说明
client.connected 物理传输打开后为 True
client.associated AARQ/AARE 握手成功后为 True ,DLMS 会话激活
client.currentAssociation 当前活动会话的 AssociationLogicalName 对象;未连接时为 None ,可用于检查协商的一致性块和 PDU 大小

client.disconnect() 发送 RLRQ(释放请求),等待 RLRE,然后关闭物理传输。


读写操作

所有读写方法均为同步:阻塞直到服务器响应或抛出 RuntimeError

单属性读取

import dlms

clock = dlms.Clock("0.0.1.0.0.255")
t = client.read(clock, clock.idx('time'))  # 读取属性 2 (time)
print("Time: {}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}".format(*t))

使用 obj.idx('attr_name') 代替裸整数,使意图清晰易懂。

批量读取

传递 None 作为属性索引,在一次请求中读取所有持久属性:

reg = dlms.Register("1.0.1.8.0.255", 0, scaler=-3)
client.read(reg, None)  # 读取 value 和 scaler
print("Energy: {} Wh".format(reg.value * (10 ** reg.scaler)))

单属性写入

client.write(clock, clock.idx('time_zone'), 60)  # 设置 UTC+1

批量写入

传递 None 将所有当前存储在对象上的属性写回服务器:

clock.time_zone = 120
client.write(clock, None)

方法调用

client.method(disconnect_ctl, 1)         # 调用 remote_disconnect(方法 1)
client.method(clock, 6, 30)              # shift_time(30 秒)

多属性批量读取

read_multiple 发送单个 Get-Request-With-List,按输入顺序返回值列表:

objs = [(clock, clock.idx('time')), (reg, reg.idx('value'))]
vals = client.read_multiple(objs)
print("Time: {}, Energy: {}".format(vals[0], vals[1]))

读取负荷曲线

client.read_profile(profile, start_index, count) ProfileGeneric 对象读取缓冲行。返回值是行列表,每行本身是一个列表,元素与 capture_objects 声明匹配。

import dlms

profile = dlms.ProfileGeneric(
    "1.0.99.1.0.255",
    capture_objects=[(clock, 2, 0), (reg, 2, 0)],
)

# 读取第 1–10 行
rows = client.read_profile(profile, start_index=1, count=10)
for row in rows:
    timestamp, energy = row[0], row[1]
    print("{}: {} Wh".format(timestamp, energy))

# 读取所有可用行
all_rows = client.read_profile(profile, start_index=1, count=0)

关联视图发现

当目标服务器未知或其对象列表可能变化时(如调试或互操作性测试),客户端可查询服务器的关联视图,而非本地构造对象。

get_object_info()

获取关联视图,返回 (class_id, version, logical_name_str) 元组列表:

info = client.get_object_info()
for class_id, version, ln in info:
    print("class={:3d}  ver={}  ln={}".format(class_id, version, ln))

get_objects()

执行关联视图查询,返回活的、带类型的 DLMS 对象列表。 dlms 模块中未实现的类 ID 会被静默跳过。

objects = client.get_objects()
print("Association contains {} objects".format(len(objects)))

# 找到第一个 Clock 并填充
for obj in objects:
    if isinstance(obj, dlms.Clock):
        client.read(obj)  # 批量读取所有持久属性
        print("Clock {}  time={}".format(obj.logical_name, obj.time))
        break

get_objects() 执行一次网络往返,应在每个会话中调用一次并复用结果。


蜂窝中继客户端

当服务器在 CGNAT 后使用 relay_tcp_setup 时,客户端也通过中继连接。中继根据帧中嵌入的 HDLC 服务端地址识别目标设备,因此客户端侧无需特殊配置,只需使用正确的服务端地址:

import dlms

relay_tcp = dlms.TcpUdpSetup("0.0.25.0.0.254", port=4060)
relay_tcp.ip_reference = ipv4_setup  # ipv4_setup.ipAddress = 中继服务器 IP

mobile = dlms.MobileConnection(
    tcp_udp_setup=dlms.TcpUdpSetup("0.0.25.0.0.255", port=4059),
    gprs_setup=gprs,
    gsm_diag=gsm,
    recv_buffer=bytearray(4096),
    relay_tcp_setup=relay_tcp,
)

client = dlms.Client(
    client_address=16,
    server_address=dlms.hdlc_server_address(12345),
)
client.connect(mobile)
value = client.read(reg, reg.idx('value'))
client.disconnect()

中继使用公式 serial % 10000 + 1000 将帧映射到已注册的设备。两个序列号在取模 10000 后结果相同的设备会发生冲突;分配序列号时应避免此情况。

持久化(Persistence)

嵌入式设备上的 DLMS 服务器必须在断电重启后保持对象状态。 dlms 模块提供 BinarySerializer 作为主要持久化机制,同时还有 JsonSerializer 和可自定义的 Serializer 基类。


BinarySerializer

每个 DLMS 对象保存为独立二进制文件,文件名来自 OBIS 代码(如 1.1.33.25.0.255.bin )。目录必须预先创建。

import dlms

ser = dlms.BinarySerializer("/usr/dlms")

核心方法

方法 说明
ser.save_all(server) 序列化服务器上所有已注册的对象
ser.load_all(server) 恢复所有对象; .bin 文件不存在时静默跳过(首次启动安全)
ser.save(obj) 保存单个对象
ser.load(obj) 恢复单个对象
ser.get_size(server) 返回已保存文件占用的总字节数(不修改文件)

典型启动流程

import dlms

# 创建对象
reg   = dlms.Register("1.0.1.8.0.255", 0, scaler=-3)
clock = dlms.Clock("0.0.1.0.0.255")

server = dlms.Server(serial_number=12345, flag_id="GRX")
server.add_object(reg)
server.add_object(clock)

# 恢复持久化状态(首次启动无效果)
ser = dlms.BinarySerializer("/usr/dlms")
ser.load_all(server)

# 启动服务器
dlms.set_serial_number(12345)
dlms.set_flag_id("GRX")
server.run()

序列化规则

BinarySerializer 根据属性标志决定序列化内容。可以用 obj.attrs() 查看实际会被保存的属性:

reg = dlms.Register("1.0.1.8.0.255", 0)
for index, name in reg.attrs():
    print("attr {}: {}".format(index, name))

自动排除的标志:

标志 说明
AttributeFlag.VOLATILE 持续变化的属性(如 Clock.time GsmDiagnostic.status ),自动排除
AttributeFlag.COMPLEX 结构化属性(如 ProfileGeneric.buffer ),自动排除,需自定持久化
只读属性 如 Unit/Scaler,同样自动排除

忽略特定属性

ser.ignore() 可从序列化中排除特定属性。可以按类排除(所有实例)或按实例排除。

import dlms

ser = dlms.BinarySerializer("/usr/dlms")

# 对所有 ProfileGeneric 对象跳过 buffer 属性(属性 2)
ser.ignore(dlms.ProfileGeneric, 2)

# 对特定 Register 跳过 scaler/unit(属性 3)
ser.ignore(my_special_reg, 3)

ProfileGeneric 缓冲区持久化

ProfileGeneric.buffer 因标记为 COMPLEX 被自动排除。推荐模式:新行捕获时追加到文件,而非一次性序列化整个缓冲区。

import dlms
import utime

BUFFER_PATH = "/usr/dlms/1.0.99.1.0.255.buf"

def _encode_row(ts, energy):
    return "{},{}\n".format(ts, energy).encode()

def on_capture(profile, event):
    if event.index == 2:  # capture
        with open(BUFFER_PATH, "ab") as f:
            f.write(_encode_row(utime.time(), energy_reg.value))
    return True

启动时按行计数恢复 entries_in_use

def _count_entries(path):
    count = 0
    try:
        with open(path, "rb") as f:
            while f.readline():
                count += 1
    except OSError:
        pass  # 文件尚不存在
    return count

注意 :切勿将大缓冲区文件整个加载到 Python 列表中计数或遍历。应逐行读取,否则可能超出堆内存。


JsonSerializer

JsonSerializer 定义在 dlms_serializer.py 中,使用 JSON 格式保存,便于调试查看。

from dlms_serializer import JsonSerializer

ser = JsonSerializer("/usr/dlms")
ser.save_all(server)

限制: 二进制属性(安全密钥、OCTET STRING 等)无法在标准 JSON 中无损表示。生产环境使用 BinarySerializer JsonSerializer 仅限开发和测试。


自定义序列化器

可继承 Serializer 基类实现自定义后端(EEPROM、SD 卡等)。

from dlms_serializer import Serializer

class EepromSerializer(Serializer):
    def save_all(self, server):
        for obj in server.object_registry:
            self.save(obj)

    def load_all(self, server):
        for obj in server.object_registry:
            self.load(obj)

    def save(self, obj):
        for index, name in obj.attrs():
            if self._is_ignored(obj, index):
                continue
            value = getattr(obj, name)
            self._dev.write(obj.logical_name, index, value)

    def load(self, obj):
        for index, name in obj.attrs():
            if self._is_ignored(obj, index):
                continue
            value = self._dev.read(obj.logical_name, index)
            if value is not None:
                setattr(obj, name, value)

    def _is_ignored(self, obj, index):
        for target, attr in self._ignored:
            if attr == index and (target is type(obj) or target is obj):
                return True
        return False

文件系统路径与存储

路径 说明
/usr/ 内部闪存,始终可用,适用于配置和小型对象状态
/bak/ 备份分区,由 FOTA/OTA 工具链管理, 不可写入
/ext/ SPI NOR 闪存(需硬件支持并通过 QPyCOM 启用)
/sd/ SD 卡,运行时挂载

目录需在首次保存前创建:

import uos

def _ensure_dirs():
    for path in ("/usr/dlms",):
        try:
            uos.mkdir(path)
        except OSError:
            pass  # 已存在

_ensure_dirs()

挂载 SD 卡(SPI,EC600N/EC800N 系列)

import uos

cdev = uos.VfsFat(1, 0, 4, 1)  # SPI port 1, mode 0, 13 MHz, CS=GPIO1
uos.mount(cdev, '/sd')

with open('/sd/test.txt', 'w+') as f:
    f.write('hello')
uos.listdir('/sd')

挂载 SD 卡(SDIO,EC600U/EC200U/EC200A 系列)

from uos import VfsSd
import uos

udev = VfsSd("sd_fs")
uos.mount(udev, '/sd')
udev.set_det(udev.GPIO10, 0)  # 可选:配置卡检测引脚

with open('/sd/dlms/lp.jsonl', 'a') as f:
    f.write('[1714000000,12345]\n')

容量警告: /usr/ 是内部闪存文件系统,与所有应用文件和固件脚本共享。保存大量 COSEM 对象(尤其是 ProfileGeneric 日志)可能耗尽空间。建议使用外部存储。

DLMS UDP 路由器(Router)

用途与 CGNAT 穿透

蜂窝模块由运营商 CGNAT 分配私有 IP 地址,头端系统无法向设备的 IP 发起入站连接。DLMS UDP 路由器作为一个公网 IP 的会合点解决此问题:

  • 设备向路由器发起 出站 UDP 连接并注册自身
  • 头端系统连接到路由器的独立端口
  • 路由器检查帧中嵌入的 HDLC 目标地址,在头端和设备间转发数据报

架构和地址映射

路由器监听两个独立的 UDP 套接字:

套接字 默认端口 连接方 用途
Board socket 4059 DLMS 服务器设备 设备注册、设备→客户端回复
Client socket 4060 头端/DLMS 客户端 客户端→设备请求

设备注册: 设备发送 BOARD:<serial>\n 作为第一个 UDP 数据报到端口 4059。例如设备序列号 METER-12345 发送:

BOARD:METER-12345\n

路由器从序列号中提取数字部分(去掉非数字字符),然后计算 HDLC 目标地址:

address = (numeric_serial % 10000) + 1000

METER-12345 的数字部分为 12345 ,因此 (12345 % 10000) + 1000 = 3345 。有效地址范围 1000–10999。

路由客户端数据报: 数据报到 4060 端口时,路由器解析帧头中的 HDLC 目标地址,查找已注册的匹配设备,转发数据报到该设备的 UDP 端点。

地址冲突: 两个序列号模 10000 结果相同的设备(如 12345 22345 )会被分配相同 HDLC 地址。路由器会记录警告并将流量转发给最近注册的设备。确保所有部署的序列号后四位唯一。


运行路由器

路由器是独立的 Python 3.8+ 包,位于 dlms_udp_router/

# 本地运行
cd dlms_udp_router
python -m pip install .
dlms-udp-router --host 0.0.0.0 --board-port 4059 --client-port 4060 --log-level INFO

# Docker
cd dlms_udp_router/docker
docker compose up --build

# systemd(Linux 服务器)
sudo cp dlms_udp_router/etc/dlms_udp_router.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now dlms_udp_router.service

环境变量配置

变量 默认值 说明
ROUTER_HOST 0.0.0.0 绑定地址
ROUTER_BOARD_PORT 4059 设备注册 UDP 端口
ROUTER_CLIENT_PORT 4060 DLMS 客户端 UDP 端口
ROUTER_LOG_LEVEL INFO 日志级别
ROUTER_SYSLOG_HOST (无) 远程 syslog 主机(UDP)
ROUTER_SYSLOG_PORT (无) 远程 syslog 端口

设备侧配置

设备通过 MobileConnection relay_tcp_setup 参数向路由器注册。 relay_tcp_setup 指向路由器的 board 端口(4059)。

import dlms

SERIAL    = 12345678
RELAY_IP  = "203.0.113.10"  # 路由器服务器公网 IP
RELAY_PORT = 4059            # 设备注册端口

tcp_udp = dlms.TcpUdpSetup("0.0.25.0.0.255", port=4059)

relay_setup = dlms.TcpUdpSetup("0.0.25.0.0.254", port=RELAY_PORT)
relay_setup.ipReference = ipv4_obj  # ipv4_obj.ipAddress = RELAY_IP

gprs     = dlms.GprsSetup("0.0.2.0.0.255")
gsm      = dlms.GsmDiagnostic("0.0.25.6.0.255")
recv_buf = bytearray(4096)

mobile = dlms.MobileConnection(
    tcp_udp_setup=tcp_udp,
    gprs_setup=gprs,
    gsm_diag=gsm,
    recv_buffer=recv_buf,
    relay_tcp_setup=relay_setup,
)
mobile.on_connected    = lambda: print("relay: connected")
mobile.on_disconnected = lambda: print("relay: disconnected")
server.add_connection(mobile)

server.run() 启动后,运行时在每次(重)连接时自动发送 BOARD:<serial>\n 注册数据报。

主动推送

中继模式下,设备可主动推送帧到当前连接的客户端:

mobile.send(raw_dlms_frame_bytes)

用于 PushSetup 通知。若中继连接未激活则抛出 RuntimeError


客户端侧使用

路由器对标准 DLMS/HDLC 流量透明。客户端发送正常 HDLC 请求帧到路由器的 client 端口(4060);路由器读取帧中的 HDLC 目标地址并转发给匹配的设备。

计算目标 HDLC 地址

import dlms

BOARD_SERIAL = 12345678
server_address = dlms.hdlc_server_address(BOARD_SERIAL % 10000)

QuecPython 客户端通过中继连接

import dlms

BOARD_SERIAL = 12345678
RELAY_IP     = "203.0.113.10"
CLIENT_PORT  = 4060

tcp_udp = dlms.TcpUdpSetup("0.0.25.0.0.255", port=4060)
relay   = dlms.TcpUdpSetup("0.0.25.0.0.254", port=CLIENT_PORT)
relay.ipReference = ipv4_obj  # ipv4_obj.ipAddress = RELAY_IP

gprs     = dlms.GprsSetup("0.0.2.0.0.255")
gsm      = dlms.GsmDiagnostic("0.0.25.6.0.255")
recv_buf = bytearray(4096)

mobile = dlms.MobileConnection(
    tcp_udp_setup=tcp_udp,
    gprs_setup=gprs,
    gsm_diag=gsm,
    recv_buffer=recv_buf,
    relay_tcp_setup=relay,
)

client = dlms.Client(
    client_address=16,
    server_address=dlms.hdlc_server_address(BOARD_SERIAL % 10000),
    conn=mobile,
)

PC/头端客户端

任何标准 DLMS 客户端库(如 Gurux DLMS.Net、gurux-dlms-python)均可与中继配合使用。将传输指向路由器的公网 IP 和 client 端口(4060)。出站帧中的 HDLC 目标地址必须等于 (BOARD_SERIAL % 10000) + 1000

防火墙注意: 确保路由器服务器上的 UDP 端口 4059 和 4060 已开放,入站(来自设备和客户端)和出站(回复)流量均需允许。

最佳实践(Best Practices)


内存管理

内存是 QuecPython 开发板上最受限的资源,通常可用堆仅 200–400 KB。

预分配接收缓冲区。 MobileConnection 需要构造时传入 recv_buffer bytearray。一次性分配,切勿重复创建:

recv_buf = bytearray(4096)  # 在模块级别分配,任何线程启动之前
mobile = dlms.MobileConnection(
    tcp_udp_setup=tcp_udp,
    gprs_setup=gprs,
    gsm_diag=gsm,
    recv_buffer=recv_buf,
)

避免在回调或循环内分配 bytearray 对象,重复分配会使堆碎片化,长期运行可能导致 MemoryError

对持有 C 端定时器的对象调用 deinit() 以下类分配了后台资源,在服务器关闭或对象不再需要时必须显式释放: ProfileGeneric PushSetup ActivityCalendar ScriptTable SingleActionSchedule 。不调用 deinit() 会泄漏定时器并阻止干净重启。

使用 BinarySerializer 持久化状态 ,而非内存结构。大型字典、历史读数列表或完整 ProfileGeneric 缓冲区应保存在文件系统上,而非 Python 堆中。按需读取请求所需的数据(参见第 9.4 节的逐行模式)。

切勿将完整捕获缓冲区加载到 Python 列表。 即使一个适中的 1440 条负荷曲线,一次全部反序列化也可能超出可用堆内存。在 on_before_read 中逐行从文件流式读取。


安全加固

部署前更改所有默认密钥。 运行时会初始化密码学密钥为已知测试值。使用这些默认值的设备可被知晓这些值的任何人读取或控制。必须更改的密钥:

  • GMAC_GUEK — Global Unicast Encryption Key(加密数据)
  • GMAC_GAK — Global Authentication Key(认证帧)
  • GMAC_KEK — Key Encryption Key(保护密钥更新包裹)

SecuritySetup 对象上设置:

security_setup.global_unicast_encryption_key = bytes.fromhex("YOUR_32_HEX_CHARS_GUEK")
security_setup.global_authentication_key     = bytes.fromhex("YOUR_32_HEX_CHARS_GAK")

KEK 通过 dlms.set_kek() 在启动时一次性传递给运行时。

将 KEK 存储在受保护存储中。 KEK 是最敏感的密钥,因为它在密钥更新过程中包裹所有其他密钥。它 不能 以字符串字面量出现在 config.py 中。使用 Quectel 的保护闪存分区或硬件安全元件。至少,将其存储在一个 USB 大容量存储模式不可访问的分区文件中。

对承载真实计量数据的连接使用 HighGMac 认证。 AuthenticationLevel.HIGH_GMAC 提供双向密码学认证和每帧重放保护。较低的认证级别(NONE、LOW)仅保留给对象列表经过严格限制的只读诊断关联。

将未认证访问限制为最小对象集。 任何 auth_mechanism = 'None' AssociationLogicalName 在无凭证情况下公开可读。其访问字典最多应暴露少量标识对象(如 clock、固件版本)。 切勿 在未认证关联中包含可写对象或安全相关对象。


线程安全

SerialConnection MobileConnection 在 C 管理的后台线程中运行 I/O 循环。COSEM 对象上注册的事件处理程序从这些线程调用。主 Python 线程同时运行应用循环。这会产生共享状态风险。

保持事件处理程序简短且非阻塞。 阻塞(sleep、等待锁、慢速文件系统写入)的事件处理程序会延迟连接线程,可能导致客户端超时。若需在事件响应中执行重要工作,设置标志并在主循环中处理:

_capture_pending = [False]

def on_before_action_capture(profile, event):
    if event.index == 2:
        _capture_pending[0] = True
    return True

# 在主应用循环中:
while True:
    if _capture_pending[0]:
        _capture_pending[0] = False
        # ... 在此处理慢速工作 ...
    utime.sleep_ms(100)

当处理程序和主循环都写入同一对象时,用锁保护共享状态。 MicroPython 的 _thread.allocate_lock() 提供简单互斥锁:

import _thread

_lock = _thread.allocate_lock()
_shared_value = [0]

def on_before_write(obj, event):
    with _lock:
        _shared_value[0] = event.value
    return True

# 在主循环中:
with _lock:
    v = _shared_value[0]

在紧循环中使用 utime.sleep_ms(0) 出让 CPU。 MicroPython 调度器对 Python 线程是协作式的。主循环迭代超过几毫秒不让出可能导致连接线程饥饿。在任何紧轮询循环中插入 utime.sleep_ms(0) (或一个小的正值)。


连接可靠性

通过 on_disconnected 响应断连。 每个连接类都暴露 on_disconnected 回调。注册一个至少记录事件的处理程序,并可选地重启受影响的连接或触发 modem 重置:

def _on_disconnected():
    print("connection lost; scheduling reconnect")
    _reconnect_pending[0] = True

mobile.on_disconnected = _on_disconnected

IecHdlcSetup 上设置 inactivity_timeout 以检测静默断连。 未发送 DISC 帧即消失的客户端会使服务器无限等待。设置非零的 inactivity_timeout (秒)使运行时在指定时间内无流量后关闭并重新打开连接:

hdlc = dlms.IecHdlcSetup(
    "0.0.22.0.0.255",
    commSpeed=9600,
    deviceAddr=0x10,
    inactivity_timeout=120,  # 静默 2 分钟后关闭
)

监控 GsmDiagnostic.status 以检查蜂窝链路。 在主循环中定期调用 gsm_diag.update() 并检查 gsm_diag.status 。值 1 (HOME_NETWORK)或 5 (ROAMING)表示注册激活;其他值表示设备不可达:

import utime

STATUS_REGISTERED = (1, 5)  # HOME_NETWORK, ROAMING

while True:
    gsm_diag.update()
    if gsm_diag.status not in STATUS_REGISTERED:
        print("network lost; status={}".format(gsm_diag.status))
        # 等待重新注册后再尝试重连
    utime.sleep_ms(60000)  # 每 60 秒检查一次

无硬件测试

DLMS 服务器逻辑的端到端测试通常需要物理开发板和 DLMS 客户端探头。 GenericConnection 提供了替代方案:由于其传输完全由 Python 驱动,可以在单个 Python 进程中构建回环,无需任何硬件即可执行请求-响应周期。

模式是创建一个带 GenericConnection 的服务器和一个客户端,将 on_send on_receive 钩子反向连接到 process_msg

import dlms

# 最小服务器
hdlc = dlms.IecHdlcSetup("0.0.22.0.0.255")
conn = dlms.GenericConnection(
    interface_type=dlms.InterfaceType.HDLC,
    hdlc_setup=hdlc,
)
server = dlms.Server(serial=99999, use_logical_name=True)
data_obj = dlms.Data("1.0.1.8.0.255")
data_obj.value = 42
server.add_object(data_obj)
server.add_connection(conn)
conn.connect()
server.run()

# 将帧直接发送到 process_msg 的客户端
client = dlms.Client(
    client_address=16,
    server_address=dlms.hdlc_server_address(99999),
    conn=conn,
)

def loopback_send(data):
    resp = conn.process_msg(data)
    if resp:
        conn.process_msg(resp)  # 将响应反馈回去

client.on_send = loopback_send
client.connect()
value = client.read(data_obj, 2)  # 读取属性 2
print(value)                       # 42
client.disconnect()

这种技术对于验证属性访问控制规则、确认 on_before_read / on_before_write 处理程序返回正确值,以及检查序列化往返都非常有用——全部在 PC 侧 Python 环境中完成。

客户端方法 connect read write action )是同步的,失败时抛出 RuntimeError 。在生产代码中,始终将它们包裹在 try/except 块中,以便优雅处理断连或意外响应,而非崩溃调用线程。


代码组织

大型部署受益于将应用拆分为集中的模块,而非将所有代码放在 main.py 中。 server_example/objects/ 目录展示了一种有效的布局:

config.py
    所有部署特定常量:OBIS 地址、数字序列号、APN、密码学密钥、端口号。
    这是不同开发板或客户的固件构建之间唯一不同的地方。

objects/data_objects.py
    Data、Register 和 ExtendedRegister 实例及其初始值。

objects/profile_objects.py
    ProfileGeneric 实例、捕获对象列表、缓冲区持久化辅助函数。

objects/security_objects.py
    SecuritySetup、AssociationLogicalName、AssociationShortName、访问字典。

objects/connection_objects.py
    IecHdlcSetup、TcpUdpSetup、GprsSetup、MobileConnection、SerialConnection。

main.py
    导入以上所有模块,调用 server.add_object() 和 server.add_connection(),
    调用 server.run(),然后进入应用循环。

启动时创建存储目录。 切勿假设 /usr/dlms/ /sd/dlms/ 已存在。在实例化任何序列化器之前的启动阶段调用一次辅助函数,可防止首次写入时出现 OSError

import uos

DIRS = ["/usr/dlms", "/usr/dlms/objects"]

def _ensure_dirs():
    for path in DIRS:
        try:
            uos.mkdir(path)
        except OSError:
            pass  # 已存在

_ensure_dirs()

保持 config.py 不含逻辑。 配置常量应为简单赋值。若某个值需要计算(如从序列号派生 HDLC 地址),计算一次——若开销小则在导入时计算,否则在 main.py 调用的 init() 函数中计算。避免 config.py 中出现依赖运行时状态的条件逻辑;该逻辑属于 main.py 或相关模块。

术语表(Glossary)

APDU(Application Protocol Data Unit) — DLMS 栈中最顶层的 PDU。APDU 携带 COSEM 服务请求或响应,并传递给传输层进行成帧。

Association(关联) — DLMS 客户端和服务器之间建立的逻辑会话。每个关联具有协商的认证级别、密码套件和一致性位集。在 dlms 模块中,关联由 AssociationLogicalName (LN 引用)和 AssociationShortName (SN 引用)对象表示。

Authentication(认证) — 验证连接客户端身份的过程。 dlms 模块支持的级别从 NONE (开放访问)到 HIGH_GMAC (AES-GCM 双向认证)。参见第 5 章。

BinarySerializer dlms 序列化辅助工具,将 COSEM 对象属性状态保存到设备文件系统上的二进制文件并恢复。标记为 AttributeFlag.COMPLEX (如 ProfileGeneric.buffer )的属性被排除。参见第 9 章。

CGNAT(Carrier-Grade NAT) — 移动运营商运行的网络地址转换层,为蜂窝设备分配私有 IP 地址,使设备无法接受直接入站 TCP 连接。DLMS UDP 路由器提供了绕过此限制的中继方案。参见第 10 章。

COSEM(Companion Specification for Energy Metering) — IEC 62056 标准的数据模型部分,定义了接口类目录及其属性。 dlms 模块中的每个 Python 类对应一个 COSEM 接口类。

Default behaviour(默认行为) — 当无事件处理程序拦截请求时,服务器 C 层自动执行的处理。对于 GET 请求意味着序列化当前属性值;对于 ACTION 请求意味着分发内置方法实现。事件处理程序可以观察或替换默认行为。参见第 4 章。

DLMS(Device Language Message Specification) — IEC 62056 标准的协议部分,定义 COSEM 对象如何序列化为 APDU 以及 APDU 如何传输。"DLMS" 通常非正式地指代 DLMS/COSEM 组合标准。

DLMSEvent — 传递给每个事件处理程序的上下文对象。携带当前请求的属性或方法索引、选择器类型、选择器参数和动作标志。参见第 4.1 节。

GAK(Global Authentication Key) — 16 或 32 字节的 AES 密钥,用于在 HighGMac 安全模式下计算 DLMS 帧的 GMAC 认证标签。服务器和客户端必须相同。

GenericConnection dlms 连接类,其传输 I/O 循环由 Python 编写。调用者打开物理通道,将接收到的字节传递给 process_msg() ,并将返回的字节写回通道。用于 UART、SPI、MQTT、G3-PLC 以及无需硬件的单元测试。参见第 6.4 节和第 11.5 节。

GUEK(Global Unicast Encryption Key) — 16 或 32 字节的 AES 密钥,用于在 HighGMac 安全模式下加密 DLMS APDU。有时写作 GMAC_GUEK。服务器和客户端必须相同。

HDLC(High-Level Data Link Control) — DLMS 在串行传输(RS-232、RS-485、光口)上使用的数据链路成帧层。提供寻址、成帧和流控制。HDLC 服务器地址由 dlms.hdlc_server_address() 从设备序列号派生。

HighGMac dlms 模块中最高的认证级别( Authentication.HIGH_GMAC )。使用 AES-GCM 提供每帧认证和可选加密。需要配置了 guek gak 密钥的 SecuritySetup 对象。参见第 5.2 节。

IEC 62056 — DLMS/COSEM 的国际标准系列。蓝皮书(IEC 62056-62)定义 COSEM 接口类;绿皮书(IEC 62056-5-3)定义安全;黄皮书涵盖一致性。

Interface class(接口类) — COSEM 中对对象类型的术语。每个接口类具有数字标识符(类 ID)并定义一组固定的属性和方法。例如, Register 是接口类 3, Clock 是接口类 8。

Invocation counter(调用计数器) — 每个 GMAC 保护帧中包含的单调递增的 32 位整数。服务器拒绝计数器不大于上次接受值的帧,防止重放攻击。计数器必须在断电重启间持久化。参见第 5.3 节。

KEK(Key Encryption Key) — 用于在密钥更新过程中包裹(加密)GUEK 和 GAK 的主密钥。在启动服务器前通过 dlms.set_kek() 设置。必须存储在设备的受保护存储中。参见第 5.4 节。

LN referencing(逻辑名引用) — COSEM 属性通过 OBIS 代码和属性索引标识的寻址模式。由 AssociationLogicalName 使用。这是 dlms 模块中的默认模式。

MobileConnection dlms 连接类,管理 C 驱动的蜂窝 UDP 套接字。支持直连模式(来自任何客户端的入站 UDP)和中继模式(向 DLMS UDP 路由器的出站注册)。需要预分配的 recv_buffer 。参见第 6.3 节和第 10.4 节。


OBIS code(对象标识系统) — 六组数字标识符 A.B.C.D.E.F ,唯一标识设备上的 COSEM 对象实例。每个 dlms 对象构造函数接受 OBIS 字符串作为其第一个参数。

PDU(Protocol Data Unit) — 特定协议层上的结构化数据单元。在 DLMS 应用层为 APDU;在 HDLC 层为 HDLC 帧。

ProfileGeneric — COSEM 接口类 7。存储历史时间序列数据(负荷曲线、事件日志)的标准对象。其 buffer 属性标记为 COMPLEX,必须与 BinarySerializer 分开持久化。参见第 3.6 节和第 9.4 节。
QuecPython — 嵌入 Quectel 蜂窝模组中的 MicroPython 1.13 运行时。 dlms 模块是该运行时的编译 C 扩展。

SAP(Service Access Point) — DLMS 系统中逻辑设备或客户端的数字标识符。标准管理客户端 SAP 为 16。SAP 到逻辑设备的映射保存在 SapAssignment 中。

SecuritySetup — COSEM 接口类 64。持有一个安全上下文的密码学密钥、安全策略和系统标题。每个使用 HighGMac 的关联需要一个 SecuritySetup 对象。参见第 5.3 节。

SN referencing(短名引用) — 使用 16 位短名代替 OBIS 代码的替代 COSEM 寻址模式。由 AssociationShortName 使用。在现代部署中较少见;LN 引用更受青睐。

System title(系统标题) — DLMS 实体的 8 字节标识符,用作 GCM 初始化向量的一部分。常规格式:3 字节 ASCII 标志标识符后跟 5 字节序列号。存储在 SecuritySetup.server_system_title client_system_title 中。

WRAPPER — 用于在 UDP 或 TCP 上承载 DLMS APDU 的薄成帧层(IEC 62056-47)。与 HDLC 不同,它不添加寻址;路由由 IP 层处理。通过 GenericConnection 上的 InterfaceType.WRAPPER 选择。