# ElectricMeter **Repository Path**: dustin-wei/electric-meter ## Basic Information - **Project Name**: ElectricMeter - **Description**: No description available - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2023-11-30 - **Last Updated**: 2024-07-12 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # ElectricMeter Project QuecPython电表项目,支持DLMS和RFC1662协议。采用QFrame框架研发。 ## QFrame 框架的设计思路 ### 设计模式 由于业务模块之间可能存在交互耦合,框架设计中,业务模块之间通信采用**星型结构设计**,如下图所示: 下图Meditor为一个中介对象(通常被框架设计成**Application**对象),各个业务对象之间通过Application对象通信,这种设计被称之为**中介模式**。 ![](./docs/media/星型结构.png) **QFrame** 拟采用星型结构的设计模式,不同业务以**应用拓展**方式存在,而各应用拓展之间的交互一律通过中心对象 **Application** 调用。 ### 应用对象 一个基于 QuecPython 的应用必须有一个协调各种外设的中心调用对象。 在 QFrame 中,中心调用对象是一个 `Application` 类的实例。每个 QFrame 应用必须创建一个该类的实例,并且把模块的名称传递给该实例。应用参数也是通过该对象配置。 下面的代码: ```python from usr.qframe import Application # init application instance app = Application(__name__) # read settings from json file app.config.from_json('/usr/dev.json') # app.config is a python dict, you can use to update settings as below: app.config.update( { "UART": { "port":2, "baudrate":115200, "bytesize":8, "parity":0, "stopbits":1, "flowctl":0 } } ) ``` ### 应用拓展 应用拓展指的是为 QFrame 应用增加业务功能的模块/包, 一个基于 QuecPython 平台的应用可能存在多种业务模块,QFrame 将每种业务约定成应用拓展的形式存在,这意味着您在开发业务的时候,需要将业务模块以应用拓展的形式封装(具体封装形式见后续讲解)。 #### 使用拓展 至此,我们已了解到应用拓展即为具体业务在 QFrame 中的表现形式。 我们在编写应用拓展时,一般来说,扩展从 `app.config` 获取其自身的配置并在初始化时传递给应用实例。 例如,一个名为 "gnss" 的拓展(一个设备的外置gnss,通过串口与设备通信,该应用拓展需要从应用配置中读取串口配置参数)。 那么。该拓展的使用可以分为两个部分: 1. 应用拓展定义 2. 应用拓展初始化。 ##### 应用拓展的定义 以下是拓展定义的抽象基类。 ```python class AppExtensionABC(object): """Abstract Application Extension Class""" def __init__(self, name, app=None): self.name = name # extension name if app: self.init_app(app) def init_app(self, app): # register into app, then, you can use `app.{extesion.name}` to get current extension instance app.append_extesion(self) # TODO: do something else raise NotImplementedError def load(self): # loading extension functions, this method will be called in `app.mainloop` raise NotImplementedError ``` 初始化方法 `__init__` 中我们需要传入 `Application` 应用程序对象,如果传入,则即刻调用 `init_app` 来完成拓展的初始化动作。亦可不传入应用对象直接实例化拓展对象,后续再手动调用 `init_app` 来完成初始化。 ##### 应用拓展的使用 当我们按照约定定义好拓展应用后,可以按如下方式,加载拓展应用对象。 ```python # STYLE 1 # give app when create the extension object app = Application(__name__) ext = ExtensionClass(app) # STYLE2 # using `init_app` method ext = ExtensionClass() ext.init_app(app) ``` ## 系统初始化流程 系统流程初始化步骤: 1. 实例化应用对象 2. 导入配置json文件 3. 初始化各应用拓展组件(此步骤会将各个应用拓展注册进主应用对象中,方便各拓展之间通信) 4. 检测网路(此步骤会阻塞等待网络就绪,若等待超时则尝试cfun切换以恢复网络) 5. 加载应用拓展,并启动相关服务(用户可自定义实现) 6. 系统进入正常运行状态(默认开启sim卡和网络检测,若出现掉网情况,会自行尝试cfun切换以恢复网络) 流程图示: ![](./docs/media/init.png) ## 系统框图 ![](./docs/media/system.png) 概述:App应用层及其各类组件负责处理业务逻辑;模组接收服务端下发指令通过串口透传给设备(会将下行数据打包为1662协议再转发);设备上行0x2100协议报文经模组解包后会将消息体数据透传给服务器;设备其他上行报文模组内进行处理并应答(根据实际业务需求实现)。 ## UML ![](./docs/media/UML.png) ## 应用入口 `demo.py`作为应用主脚本,提供工厂函数`create_app`传入配置路径来初始化应用和加载各拓展。其中,主要应用拓展功能又三大类`rfc1662resolver`(1662协议解析)、`client`(tcp客户端)和`uart`(串口读写),它们分别都被注册进主应用Application中方便互相协作。 入口脚本代码如下: ```python import checkNet from usr.qframe import Application from usr.business import rfc1662resolver, client, uart PROJECT_NAME = "QuecPython_Framework_DEMO" PROJECT_VERSION = "1.0.0" def poweron_print_once(): checknet = checkNet.CheckNetwork( PROJECT_NAME, PROJECT_VERSION, ) checknet.poweron_print_once() def create_app(name='DTU', config_path='/code/dev.json'): # initialize Application _app = Application(name) # read settings from json file _app.config.from_json(config_path) # init rfc1662resolver extension rfc1662resolver.init_app(_app) # init uart extension uart.init_app(_app) # init tcp client extension client.init_app(_app) return _app # create app with `create_app` factory function app = create_app() if __name__ == '__main__': poweron_print_once() # loading all extensions app.mainloop() ``` ### 业务应用拓展(Extension) 应用拓展指的是为 QFrame 应用增加业务功能的模块/包, 一个基于 QuecPython 平台的应用可能存在多种业务模块,QFrame 将每种业务约定成应用拓展的形式存在,这意味着您在开发业务的时候,需要将业务模块以应用拓展的形式封装(具体封装形式见后续讲解)。 在`business.py`模块中定义了三类拓展对象,分别是`rfc1662resolver`,`client`和`uart`,功能分别是: - ``rfc1662resolver``:负责解析和组件1662协议报文,(`RFC1662ProtocolResolver`实例对象)。 - `client`:tcp客户端(`BusinessClient`实例对象),负责与tcp服务器上下行交互。 - `uart`:串口客户端(`UartBusiness`实例对象),负责串口读写。 #### 1662协议组件 定义模块:`protocol.py`,中关键类定义。 ##### 类`RFC1662ProtocolResolver` 该类是一个应用拓展类,是RFC1662协议数据的解析器,用于解析处理业务中传输的RFC1662协议数据,对该类数据进行解包组包的功能。 有如下方法: - `resolve(msg)` - 功能:处理一个1662协议消息。行为是通过解析消息的protocol(可以理解为协议报文的id),根据该protocol从注册表中查找该消息的处理函数,如果找到则调用函数处理,否则抛出`ValueError`异常。如何注册处理函数见装饰器函数`register`。 - 参数:接收唯一`msg`(一个`RFC1662Protocol`对象,该类是1662协议的封装类,见后续介绍)。 - 返回值:None - 异常:若传入`msg`未能在注册表中查找到处理函数,则抛出`ValueError`异常。 - `register(protocol)` - 功能:是一个装饰器函数,用于注册一个protocol的处理函数。 - 参数:接收唯一参数`protocol`,可以理解为1662协议的报文id。 - 返回值:原函数 - `tcp_to_meter_packet(data)`,静态方法 - 功能:将字节数据data,组包成透传1662数据包(0x2100),即tcp透传给表计的数据帧 - 参数:data,字节串 - 返回值:0x2100协议包字节串 - 异常:无 - `module_to_meter_packet(data)`,静态方法 - 功能:组1662协议数据包(0x2200),即模块主动发向表计的数据帧 - 参数:data是一个列表,`[get/set, id, data]`,其中: - `get/set`:`COSEM.GET/COSEM.SET`,分别对应值为`0xC0/0xC1` - `id`:功能命令字 - `data`:字节串 - 返回值:0x2200协议包字节串 应用示例: ```python # we have inited a RFC1662ProtocolResolver object in `business.py` module # import `rfc1662resolver` from code.business import rfc1662resolver # decorate with protocol 0x2100 @rfc1662resolver.register(0x2100) def handle2100(msg): """when get a 0x2100 message,this function will be called""" pass ``` ##### 类RFC1662Protocol 该类是1662协议的具体实现,包括解包和组包。该类对象是一个完整1662协议包的封装形式。主要方法如: - `build_rfc_0x2100`:组0x2100协议包,返回字节,等同于`RFC1662ProtocolResolver.tcp_to_meter_packet` - `build_rfc_0x2200`:组0x2200协议包,返回字节,等同于`RFC1662ProtocolResolver.module_to_meter_packet` - `build`:类方法,用于解析一帧协议包,返回RFC1662Protocol对象。 - `replay_get`:应答get指令, 判断成功失败 - `replay_set`:应答set指令 - `reply_event`:应答event信息 ##### TCP客户端组件 ###### 基类`TcpClient` 定义在`qframe/builtins/clients.py`模块中。 开放用户接口`recv_callback`,用户通过重写该方法,实现对tcp下行数据的业务处理。该类同时实现了tcp客户端的重连操作。 开放用户接口`send`,用户可使用该方法发送上行数据。 ```python class TcpClient(object): # ... def recv_callback(self, data): raise NotImplementedError('you must implement this method to handle data received by tcp.') def send(self, data): # TODO: uplink data method pass ``` ###### 子类`BusinessClient` `BusinessClient`通过重写`recv_callback`方法,实现透传数据。 ```python class BusinessClient(TcpClient): def recv_callback(self, data): # recv tcp data and send to uart data = RFC1662Protocol.build_rfc_0x2100(data) CurrentApp().uart.write(data) ``` > 注:`CurrentApp()`获取当前应用程序对象,通过访问`uart`属性获取注册的`uart`对象,在通过write方法,将tcp服务器消息通过串口透传。 ##### 串口服务组件 ###### 基类`Uart` 定义在`qframe/builtins/uart.py`模块中。 开放用户接口`recv_callback`,用户通过重写该方法,实现对uart数据的业务处理。该方法会在串口读线程中被调用,当串口有数据时候,会通过该方法将数据交由用户处理。 开放用户接口`write`,用户通过该方法可以向串口写数据。 ```python class Uart(object): # ... def recv_callback(self, data): raise NotImplementedError('you must implement this method to handle data received from device.') def write(self, data): # TODO: write data to uart pass ``` ###### 子类`UartBusiness` `UartBusiness`通过重写`recv_callback`方法,实现对串口接收数据的业务处理。 ```python class UartBusiness(Uart): def recv_callback(self, data): # parse 1662 protocol data pass ``` > 在子类`UartBusiness`的`recv_callback`方法中解析1662协议报文,构建消息对象后,通过`rfc1662resolver.resolve`方法分发消息处理业务。 ## 组件交互时序图 ```mermaid sequenceDiagram Title: extensions communication process participant uart as Uart participant protocol as RFC1662Protocol participant resolver as RFC1662ProtocolResolver participant client as Client uart -->> protocol: request from meter protocol -->> resolver: build message resolver -->> resolver: handle business resolver -->> protocol: build response message protocol -->> uart: response to meter resolver -->> client: tcp data post through ``` ## 编写业务功能 定义模块:`business.py`。 在该模块中,全局唯一一个`rfc1662resolver`解析器,用于注册指定类型的消息处理函数。如下示例注册0x2100协议透传处理函数。 ```python # >>>>>>>>>> handle rfc1662 message received from uart <<<<<<<<<< @rfc1662resolver.register(0x2100) def handle2100(msg): """post data received to cloud""" # message body bytes data = msg.info().request_data() if data: # post data to tcp server by `client` extension register in Application CurrentApp().client.send(data) ```