目的
记录并分享一个用 python 实现上位机功能的思路与模板。将通过三个方面分享:
- Python 自定义简易通信规约,并实现 Server 端
- Python 实现 Client 端
- PyQt5 绘制上位机界面,通过上位机作为 Client 访问 Server 端
本章将用 Python 实现 Client 端。
简易的日志模块
利用 python 自带的 logging 模块实现 client 端的日志记录。日志保存路径为 C:\ProgramData\pyHost。
import logging
import datetime
import sys
import os
#region LOGGING
LOGPATH = 'C:\\ProgramData\\pyHost'
if not os.path.exists(LOGPATH):
os.makedirs(LOGPATH)
timeStamp = datetime.datetime.strftime(datetime.datetime.now(), '%Y_%m_%d_%H_%M_%S')
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
fileHandler = logging.FileHandler(f"{LOGPATH}\\log_{timeStamp}.log")
consoleHandler = logging.StreamHandler(sys.stdout)
formatter = logging.Formatter('%(asctime)s : %(name)s : %(funcName)s : %(levelname)s : %(message)s')
fileHandler.setFormatter(formatter)
consoleHandler.setFormatter(formatter)
logger.addHandler(fileHandler)
logger.addHandler(consoleHandler)
#endregion
如果日志目录不存在,则创建日志目录。每次运行的日志保存为 log_[日志创建的时间戳]。在保存日志到文件的同时,我们也希望在终端有日志打印,因此 logger 中添加了 fileHandler 和 consoleHandler。
Client (host) 端 API
在 Client 端,我们希望能收发 dummyCom 正确的报文,并解析数据。dummyCom 规约见上一篇博文。
import socket
from traceback import format_exc
from dummyCom.dummyCom import dummyComStack, bprint
#region HOSTAPI
class hostAPI(object):
def __init__(self, *comParams):
# define a communication channel
# TCP communication is taken in the template
self.channel = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.channel.settimeout(3)
self.stack = dummyComStack()
try:
self.channel.connect(comParams)
logger.debug(f'HOST connected: {comParams}')
except Exception as err:
logger.debug(f'HOST cannot connect: {comParams}')
logger.debug(f'Exeption: {err}')
logger.debug(f'{format_exc}')
self.channel = None
def parse(self, msg: bytes):
'''
@params message
@return function_code, address, data
'''
return self.stack.parse_msg(msg)
def generate_message(self, fc: int, addr: int, data: int):
'''
@params function_code, address, data
@return message
'''
return self.stack.generate_msg(fc, addr, data)
def send_message(self, msg: bytes):
'''
@params message
@return send_res
'''
try:
self.channel.sendall(msg) # type:ignore
logger.debug(f'[send --->] {bprint(msg)}')
return True
except Exception as err:
logger.debug(f'[error :] {err}')
logger.debug(f'\n[ERR INFO:]\n{format_exc()}\n[END OF ERROR]\n')
return None
def recv_message(self):
'''
@return message
'''
try:
rMsg = self.channel.recv(1024)
logger.debug(f'[recv <---] {bprint(rMsg)}')
return rMsg # type:ignore
except Exception as err:
logger.debug(f'[error :] {err}')
logger.debug(f'\n[ERR INFO:]\n{format_exc()}\n[END OF ERROR]\n')
return None
#endregion
在日志中引入 traceback.format_exc() 可以更好的记录代码运行过程中出现的报错。
hostAPI 初始化的时候,会创建一个 TCP 连接到 server 端。 其中 comParams 参数为 (ip, port)。
hostAPI 中的 parse, generate_message, 分别调用 dummyCom protocol stack 中定义的接口。
hostAPI 的 send_message,将给定的 bytes 发送到 server。
hosAPI 的 recv_message,从 TCP 连接收至多 1024 个 bytes。
运行与调试
先在 Terminal 中启动 Server 端
cd ./dummyCom
python ./dummyCom.py
再另启一个 Terminal,运行 Python 命令行,调用 hostAPI 命令:
python
首先创建一个 client 连接到 127.0.0.1:2333。我们想读取地址为 15 存储的数值,因此先生成一个 readMsg,读 (FC=0x03) 地址 15。将 readMsg 发送给 Server 之后,再读取收到的报文。从 logger 记录的 receive message: 23 83 00 0F 00 0F 91 32 可以看到 地址 15 存储的数值为 15。
在 C:\ProgramData\pyHost 下查看 log:
下一篇将通过 PyQt5 实现 hostAPI 的 GUI。
附:hostAPI (without GUI)源码
'''
@auther: echen.hu
@email: echen.hu@163.com
This script defines a template for a host machine, which
can communicate with client machines through some kind of
protocol over ethernet, serial etc.
'''
import logging
import datetime
import socket
import sys
import os
from traceback import format_exc
from dummyCom.dummyCom import dummyComStack, bprint
#region LOGGING
LOGPATH = 'C:\\ProgramData\\pyHost'
if not os.path.exists(LOGPATH):
os.makedirs(LOGPATH)
timeStamp = datetime.datetime.strftime(datetime.datetime.now(), '%Y_%m_%d_%H_%M_%S')
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
fileHandler = logging.FileHandler(f"{LOGPATH}\\log_{timeStamp}.log")
consoleHandler = logging.StreamHandler(sys.stdout)
formatter = logging.Formatter('%(asctime)s : %(name)s : %(funcName)s : %(levelname)s : %(message)s')
fileHandler.setFormatter(formatter)
consoleHandler.setFormatter(formatter)
logger.addHandler(fileHandler)
logger.addHandler(consoleHandler)
#endregion
#region HOSTAPI
class hostAPI(object):
def __init__(self, *comParams):
# define a communication channel
# TCP communication is taken in the template
self.channel = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.channel.settimeout(3)
self.stack = dummyComStack()
try:
self.channel.connect(comParams)
logger.debug(f'HOST connected: {comParams}')
except Exception as err:
logger.debug(f'HOST cannot connect: {comParams}')
logger.debug(f'Exeption: {err}')
logger.debug(f'{format_exc}')
self.channel = None
def parse(self, msg: bytes):
'''
@params message
@return function_code, address, data
'''
return self.stack.parse_msg(msg)
def generate_message(self, fc: int, addr: int, data: int):
'''
@params function_code, address, data
@return message
'''
return self.stack.generate_msg(fc, addr, data)
def send_message(self, msg: bytes):
'''
@params message
@return send_res
'''
try:
self.channel.sendall(msg) # type:ignore
logger.debug(f'[send --->] {bprint(msg)}')
return True
except Exception as err:
logger.debug(f'[error :] {err}')
logger.debug(f'\n[ERR INFO:]\n{format_exc()}\n[END OF ERROR]\n')
return None
def recv_message(self):
'''
@return message
'''
try:
rMsg = self.channel.recv(1024)
logger.debug(f'[recv <---] {bprint(rMsg)}')
return rMsg # type:ignore
except Exception as err:
logger.debug(f'[error :] {err}')
logger.debug(f'\n[ERR INFO:]\n{format_exc()}\n[END OF ERROR]\n')
return None
#endregion