Python包制作 - 以DLT645-2007通讯规约库为例

部署运行你感兴趣的模型镜像

Python包制作 - 以DLT645-2007通讯规约库为例

概述

本报告以实际项目中的DLT645协议库为例,详细介绍Python安装包的制作过程。DLT645是一个电能表通信协议的Python实现库,支持TCP和RTU通信方式,是一个功能完整的实际项目案例。

Python包管理简介

包管理工具发展历程

Python包管理经历了从传统的setup.py到现代的pyproject.toml的演进:

  1. 传统方式: 使用setup.py + setuptools
  2. 现代方式: 使用pyproject.toml + build工具
  3. 混合方式: 同时提供两种配置以保证兼容性

核心概念

  • 源码包(sdist): 包含源代码的压缩包(.tar.gz)
  • 轮子包(wheel): 预编译的二进制包(.whl)
  • MANIFEST.in: 控制源码包中包含的非Python文件

项目结构设计

以DLT645项目为例,展示一个标准的Python包项目结构:

dlt645/
├── src/                    # 源代码目录
│   ├── __init__.py        # 包初始化文件
│   ├── common/            # 公共模块
│   ├── model/             # 数据模型
│   ├── protocol/          # 协议实现
│   ├── service/           # 服务层
│   │   ├── clientsvc/     # 客户端服务
│   │   └── serversvc/     # 服务端服务
│   └── transport/         # 传输层
│       ├── client/        # 客户端传输
│       └── server/        # 服务端传输
├── config/                # 配置文件目录
│   ├── energy_types.json
│   ├── demand_types.json
│   └── variable_types.json
├── test/                  # 测试目录
├── examples.py           # 使用示例
├── setup.py              # 传统配置文件
├── pyproject.toml        # 现代配置文件
├── MANIFEST.in           # 文件包含规则
├── build.sh              # 构建脚本
├── README.md             # 项目说明
└── requirements.txt      # 依赖列表

关键设计原则

  1. 模块化设计: 按功能分层组织代码
  2. 配置分离: 将配置文件独立存放
  3. 测试覆盖: 提供完整的测试用例
  4. 文档完善: 包含详细的使用说明和示例

配置文件详解

1. pyproject.toml(推荐的现代方式)

[build-system]
requires = ["setuptools>=45", "wheel", "setuptools-scm"]
build-backend = "setuptools.build_meta"

[project]
name = "dlt645"
version = "1.0.0"
description = "DLT645协议Python实现库"
readme = "README.md"
requires-python = ">=3.7"
license = {text = "Apache License 2.0"}
authors = [
    {name = "Chen Dongyu", email = "1755696012@qq.com"}
]
keywords = ["dlt645", "protocol", "communication", "energy", "meter"]
classifiers = [
    "Development Status :: 4 - Beta",
    "Intended Audience :: Developers",
    "Topic :: Communications",
    "Programming Language :: Python :: 3",
    "License :: OSI Approved :: Apache Software License",
]
dependencies = [
    "loguru>=0.5.0",
    "pyserial>=3.4",
]

[project.optional-dependencies]
dev = [
    "pytest>=6.0",
    "pytest-cov>=2.0",
    "black>=21.0",
]

[tool.setuptools]
packages = ["dlt645"]
package-dir = {"dlt645" = "src"}
include-package-data = true

[tool.setuptools.package-data]
"dlt645" = ["config/*.json"]
配置说明:
  • build-system: 定义构建系统和依赖
  • project: 项目基本信息和元数据
  • dependencies: 运行时依赖
  • optional-dependencies: 可选依赖(如开发工具)
  • tool.setuptools: setuptools特定配置

2. setup.py(兼容传统方式)

from setuptools import setup, find_packages
import os

def read_readme():
    if os.path.exists("README.md"):
        with open("README.md", "r", encoding="utf-8") as f:
            return f.read()
    return "DLT645协议Python实现库"

setup(
    name="dlt645",
    version="1.0.0",
    author="Chen Dongyu",
    author_email="1755696012@qq.com",
    description="DLT645协议Python实现库",
    long_description=read_readme(),
    long_description_content_type="text/markdown",
    
    # 包结构配置
    packages=["dlt645", "dlt645.common", "dlt645.model", ...],
    package_dir={"dlt645": "src"},
    
    # 数据文件包含
    package_data={
        "dlt645": ["config/*.json"],
    },
    include_package_data=True,
    
    # 依赖管理
    install_requires=[
        "loguru>=0.5.0",
        "pyserial>=3.4",
    ],
    
    extras_require={
        "dev": [
            "pytest>=6.0",
            "pytest-cov>=2.0",
        ],
    },
    
    python_requires=">=3.7",
)

3. MANIFEST.in(文件包含控制)

include README.md
include pyproject.toml
include setup.py
recursive-include config *.json
recursive-include src *.py
recursive-include test *.py
global-exclude *.pyc
global-exclude __pycache__
global-exclude .DS_Store

构建脚本编写

自动化构建脚本(build.sh)

DLT645项目的构建脚本提供了完整的自动化构建流程:

#!/bin/bash
# 核心功能函数

# 清理构建文件
clean_build() {
    print_info "清理构建文件..."
    rm -rf build/
    rm -rf dist/
    rm -rf *.egg-info/
    find . -name "*.pyc" -delete
    find . -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
}

# 检查依赖
check_dependencies() {
    python -c "import setuptools" 2>/dev/null || pip install setuptools
    python -c "import wheel" 2>/dev/null || pip install wheel
    python -c "import build" 2>/dev/null || pip install build
}

# 构建包
build_package() {
    if command -v python -m build &> /dev/null; then
        python -m build  # 现代方式
    else
        python setup.py sdist bdist_wheel  # 传统方式
    fi
}

# 测试安装
test_install() {
    temp_env="temp_test_env"
    python -m venv "$temp_env"
    source "$temp_env/bin/activate"
    pip install dist/*.whl
    python -c "import dlt645; print('包导入成功')"
    deactivate
    rm -rf "$temp_env"
}

构建脚本的使用方式

# 完整构建(包含测试)
./build.sh build

# 快速构建(跳过测试)
./build.sh quick

# 仅清理
./build.sh clean

# 完整构建+安装测试
./build.sh all

本地安装

# 从wheel文件安装
pip install dist/dlt645-1.0.0-py3-none-any.whl

# 从源码包安装
pip install dist/dlt645-1.0.0.tar.gz

# 开发模式安装(可编辑安装)
pip install -e .

在项目中引用的例子

1. 在EMS模拟设备中的使用

在实际的EMS模拟设备项目中,DLT645包被引用的方式:

# 在 src/device/device.py 中的引用
from dlt645.service.serversvc.server_service import (
    MeterServerService, 
    new_tcp_server, 
    new_rtu_server
)

class Device:
    def __init__(self):
        self.dlt645_server = None
    
    def start_dlt645_server(self, config):
        """启动DLT645服务器"""
        if config['protocol_type'] == 'TCP':
            self.dlt645_server = new_tcp_server(
                config['ip'], 
                config['port'], 
                config['timeout']
            )
        elif config['protocol_type'] == 'RTU':
            self.dlt645_server = new_rtu_server(
                config['port'],
                config['data_bits'],
                config['stop_bits'],
                config['baud_rate'],
                config['parity'],
                config['timeout']
            )
        
        # 设置设备数据
        self.setup_device_data()
        
        # 启动服务
        self.dlt645_server.server.start()
    
    def setup_device_data(self):
        """设置设备数据"""
        # 设置电能量数据
        self.dlt645_server.set_00(0x00000000, 12345.67)  # 总有功电能
        self.dlt645_server.set_00(0x00010000, 10000.50)  # 正向有功电能
        
        # 设置变量数据
        self.dlt645_server.set_02(0x02010100, 220.5)     # A相电压
        self.dlt645_server.set_02(0x02020100, 15.6)      # A相电流

2. 客户端使用示例

# 客户端连接示例
from dlt645 import MeterClientService

def create_dlt645_client(config):
    """创建DLT645客户端"""
    if config['connection_type'] == 'TCP':
        client = MeterClientService.new_tcp_client(
            config['host'], 
            config['port'], 
            config['timeout']
        )
    else:
        client = MeterClientService.new_rtu_client(
            port=config['serial_port'],
            baudrate=config['baud_rate'],
            databits=config['data_bits'],
            stopbits=config['stop_bits'],
            parity=config['parity'],
            timeout=config['timeout']
        )
    
    # 设置设备地址
    client.set_address(bytes.fromhex(config['device_address']))
    
    return client

def read_meter_data(client):
    """读取电表数据"""
    data_points = [
        (0x00000000, "总有功电能"),
        (0x00010000, "正向有功电能"),
        (0x02010100, "A相电压"),
        (0x02020100, "A相电流"),
    ]
    
    results = {}
    for di, description in data_points:
        try:
            if di & 0xFF000000 == 0x00000000:  # 电能量数据
                data = client.read_01(di)
            elif di & 0xFF000000 == 0x02000000:  # 变量数据
                data = client.read_03(di)
            
            if data:
                results[description] = data.value
                print(f"{description}: {data.value}")
            else:
                print(f"{description}: 读取失败")
                
        except Exception as e:
            print(f"读取{description}时发生错误: {e}")
    
    return results

3. 配置文件的使用

# 在项目配置中引用DLT645配置
import json
import os

def load_dlt645_config():
    """加载DLT645配置"""
    config_dir = os.path.join(os.path.dirname(__file__), 'config')
    
    configs = {}
    config_files = [
        'energy_types.json',
        'demand_types.json', 
        'variable_types.json'
    ]
    
    for config_file in config_files:
        config_path = os.path.join(config_dir, config_file)
        if os.path.exists(config_path):
            with open(config_path, 'r', encoding='utf-8') as f:
                configs[config_file.replace('.json', '')] = json.load(f)
    
    return configs

# 使用配置文件
dlt645_configs = load_dlt645_config()
energy_types = dlt645_configs.get('energy_types', {})

5. 完整的应用示例

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
DLT645协议在EMS系统中的应用示例
"""

import time
import threading
from dlt645 import new_tcp_server, MeterClientService

class EMSDevice:
    """EMS设备类,集成DLT645协议"""
    
    def __init__(self, device_config):
        self.config = device_config
        self.dlt645_server = None
        self.is_running = False
        
    def start_dlt645_service(self):
        """启动DLT645服务"""
        # 创建服务器
        self.dlt645_server = new_tcp_server(
            self.config['dlt645']['ip'],
            self.config['dlt645']['port'],
            self.config['dlt645']['timeout']
        )
        
        # 注册设备
        device_addr = bytearray.fromhex(self.config['dlt645']['device_address'])
        self.dlt645_server.register_device(device_addr)
        
        # 初始化数据
        self.init_meter_data()
        
        # 启动服务器
        self.dlt645_server.server.start()
        self.is_running = True
        
        print(f"DLT645服务已启动: {self.config['dlt645']['ip']}:{self.config['dlt645']['port']}")
        
    def init_meter_data(self):
        """初始化电表数据"""
        # 电能量数据
        energy_data = self.config.get('energy_data', {})
        for di_hex, value in energy_data.items():
            di = int(di_hex, 16)
            self.dlt645_server.set_00(di, value)
            
        # 变量数据
        variable_data = self.config.get('variable_data', {})
        for di_hex, value in variable_data.items():
            di = int(di_hex, 16)
            self.dlt645_server.set_02(di, value)
    
    def update_real_time_data(self, data_updates):
        """更新实时数据"""
        if not self.is_running:
            return
            
        for data_type, updates in data_updates.items():
            for di_hex, value in updates.items():
                di = int(di_hex, 16)
                if data_type == 'energy':
                    self.dlt645_server.set_00(di, value)
                elif data_type == 'variable':
                    self.dlt645_server.set_02(di, value)

# 使用示例
if __name__ == "__main__":
    # 设备配置
    device_config = {
        'dlt645': {
            'ip': '127.0.0.1',
            'port': 8021,
            'timeout': 30,
            'device_address': '010203040506'
        },
        'energy_data': {
            '0x00000000': 12345.67,  # 总有功电能
            '0x00010000': 10000.50,  # 正向有功电能
        },
        'variable_data': {
            '0x02010100': 220.5,     # A相电压
            '0x02020100': 15.6,      # A相电流
        }
    }
    
    # 创建设备实例
    ems_device = EMSDevice(device_config)
    
    # 启动DLT645服务
    ems_device.start_dlt645_service()
    
    # 模拟数据更新
    def simulate_data_updates():
        """模拟数据更新"""
        import random
        while True:
            updates = {
                'variable': {
                    '0x02010100': 220.0 + random.uniform(-5, 5),  # A相电压波动
                    '0x02020100': 15.0 + random.uniform(-2, 2),   # A相电流波动
                }
            }
            ems_device.update_real_time_data(updates)
            time.sleep(5)  # 每5秒更新一次
    
    # 启动数据更新线程
    update_thread = threading.Thread(target=simulate_data_updates, daemon=True)
    update_thread.start()
    
    # 保持主程序运行
    try:
        print("EMS设备运行中,按Ctrl+C退出...")
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        print("\n设备已停止")

6.嵌入模拟设备后效果

  • 在界面上设置随机数据

    在这里插入图片描述

  • 查看实时数据进行对比

    在这里插入图片描述

  • 验证通讯报文是否正确

    在这里插入图片描述

最佳实践与总结

最佳实践

  1. 项目结构标准化

    • 使用标准的目录结构
    • 分离源码、测试、配置和文档
    • 提供清晰的包层次结构
  2. 配置文件管理

    • 同时提供setup.py和pyproject.toml以保证兼容性
    • 使用MANIFEST.in精确控制包含的文件
    • 合理设置依赖版本范围
  3. 自动化构建

    • 编写完整的构建脚本
    • 包含测试、清理、验证等步骤
    • 提供多种构建选项
  4. 版本管理

    • 使用语义化版本控制
    • 维护CHANGELOG记录变更
    • 考虑向后兼容性
  5. 文档完善

    • 提供详细的README
    • 包含使用示例和API文档
    • 说明安装和依赖要求

常见问题与解决方案

  1. 导入路径问题

    # 在__init__.py中正确设置导入
    from .service.serversvc.server_service import MeterServerService
    from .service.clientsvc.client_service import MeterClientService
    
  2. 数据文件包含

    # 在setup.py中包含配置文件
    package_data={
        "dlt645": ["config/*.json"],
    }
    
  3. 依赖版本冲突

    # 使用合理的版本范围
    install_requires=[
        "loguru>=0.5.0,<1.0.0",
        "pyserial>=3.4,<4.0",
    ]
    

您可能感兴趣的与本文相关的镜像

Python3.10

Python3.10

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

MC皮蛋侠客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值