樊春春 1 жил өмнө
commit
7f9f86b6ec
69 өөрчлөгдсөн 4067 нэмэгдсэн , 0 устгасан
  1. 114 0
      .gitignore
  2. BIN
      assets/background.jpeg
  3. BIN
      assets/bitbug_favicon.ico
  4. BIN
      assets/ic_launch.png
  5. BIN
      assets/icon/三级上电.jpeg
  6. BIN
      assets/icon/下载.jpg
  7. BIN
      assets/icon/下载2.jpg
  8. BIN
      assets/icon/下载3.jpeg
  9. BIN
      assets/icon/参数设置.jpg
  10. BIN
      assets/icon/实时曲线.png
  11. BIN
      assets/icon/密码.jpg
  12. BIN
      assets/icon/心跳.jpeg
  13. BIN
      assets/icon/数据保存.jpg
  14. BIN
      assets/icon/文件.jpg
  15. BIN
      assets/icon/断路器.jpeg
  16. BIN
      assets/icon/用户管理.jpg
  17. BIN
      assets/icon/继电器开关.jpg
  18. BIN
      assets/icon/设置2.jpg
  19. BIN
      assets/icon/读取参数.png
  20. BIN
      assets/icon/配置参数.jpeg
  21. BIN
      assets/icon/铁电.jpg
  22. 153 0
      build_pyd.py
  23. BIN
      config/ControlCANx86/ControlCAN.dll
  24. BIN
      config/ControlCANx86/kerneldlls/CAN232.dll
  25. BIN
      config/ControlCANx86/kerneldlls/CANETE.dll
  26. BIN
      config/ControlCANx86/kerneldlls/CANET_TCP.dll
  27. BIN
      config/ControlCANx86/kerneldlls/PC104C2.dll
  28. BIN
      config/ControlCANx86/kerneldlls/PC104CAN.dll
  29. BIN
      config/ControlCANx86/kerneldlls/PCI5121.dll
  30. BIN
      config/ControlCANx86/kerneldlls/gisadll.dll
  31. BIN
      config/ControlCANx86/kerneldlls/gpcidll.dll
  32. BIN
      config/ControlCANx86/kerneldlls/isa5420.dll
  33. 20 0
      config/ControlCANx86/kerneldlls/kerneldll.ini
  34. BIN
      config/ControlCANx86/kerneldlls/usbcan.dll
  35. 0 0
      controller/__init__.py
  36. 354 0
      controller/pcs_ctl.py
  37. 32 0
      controller/pcs_status_ctl.py
  38. 35 0
      pcs_upper.py
  39. 12 0
      requirements.txt
  40. 0 0
      ui/__init__.py
  41. 419 0
      ui/home.py
  42. 0 0
      ui/own/__init__.py
  43. 21 0
      ui/own/frame_theme.py
  44. 257 0
      ui/own/led.py
  45. 87 0
      ui/own/palette_theme.py
  46. 90 0
      ui/own/switch_button.py
  47. 57 0
      ui/statusbar.py
  48. 0 0
      utils/__init__.py
  49. 209 0
      utils/can.py
  50. 41 0
      utils/com.py
  51. 32 0
      utils/delay.py
  52. 25 0
      utils/globalvar.py
  53. 43 0
      utils/hex_bit.py
  54. 28 0
      utils/log_signal.py
  55. 6 0
      utils/modbus/__init__.py
  56. 34 0
      utils/modbus/defines.py
  57. 80 0
      utils/modbus/exceptions.py
  58. 101 0
      utils/modbus/hooks.py
  59. 987 0
      utils/modbus/modbus.py
  60. 248 0
      utils/modbus/modbus_rtu.py
  61. 238 0
      utils/modbus/utils.py
  62. 19 0
      utils/qssloader.py
  63. 66 0
      utils/qt.py
  64. 27 0
      utils/resource.py
  65. 0 0
      widget/__init__.py
  66. 70 0
      widget/pcs_home.py
  67. 21 0
      widget/pcs_status_bar.py
  68. 0 0
      worker/__init__.py
  69. 141 0
      worker/pcs_work.py

+ 114 - 0
.gitignore

@@ -0,0 +1,114 @@
+# Mac
+.DS_Store
+*/.DS_Store
+
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# pyenv
+.python-version
+
+# celery beat schedule file
+celerybeat-schedule
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# vscode 
+.vscode
+.vscode/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+
+*.csv
+**/Thumbs.db

BIN
assets/background.jpeg


BIN
assets/bitbug_favicon.ico


BIN
assets/ic_launch.png


BIN
assets/icon/三级上电.jpeg


BIN
assets/icon/下载.jpg


BIN
assets/icon/下载2.jpg


BIN
assets/icon/下载3.jpeg


BIN
assets/icon/参数设置.jpg


BIN
assets/icon/实时曲线.png


BIN
assets/icon/密码.jpg


BIN
assets/icon/心跳.jpeg


BIN
assets/icon/数据保存.jpg


BIN
assets/icon/文件.jpg


BIN
assets/icon/断路器.jpeg


BIN
assets/icon/用户管理.jpg


BIN
assets/icon/继电器开关.jpg


BIN
assets/icon/设置2.jpg


BIN
assets/icon/读取参数.png


BIN
assets/icon/配置参数.jpeg


BIN
assets/icon/铁电.jpg


+ 153 - 0
build_pyd.py

@@ -0,0 +1,153 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+'''
+@文件    :setup.py
+@时间    :2022/02/21 17:00:46
+@作者    :None
+@版本    :1.0
+@说明    :源码编译文件
+'''
+
+
+import sys
+import os
+import shutil
+import time
+from distutils.core import setup
+from Cython.Build import cythonize
+
+# import zipextimporter
+
+start_time = time.time()
+curr_dir = os.path.abspath('.')
+parent_path = sys.argv[1] if len(sys.argv) > 1 else ""
+setup_file = __file__.replace('/', '')
+build_dir = "build"
+build_tmp_dir = build_dir + "/temp"
+
+s = "# cython: language_level=3"
+
+"""
+获取py文件的路径
+:param base_path: 根路径
+:param parent_path: 父路径
+:param excepts: 排除文件
+:return: py文件的迭代器
+"""
+
+
+def get_py(base_path=os.path.abspath('.'), parent_path='', name='', excepts=(), copyOther=False, delC=False):
+
+    full_path = os.path.join(base_path, parent_path, name)
+    for filename in os.listdir(full_path):
+        full_filename = os.path.join(full_path, filename)
+        if os.path.isdir(full_filename) and filename != build_dir and not filename.startswith('.'):
+            for f in get_py(base_path, os.path.join(parent_path, name), filename, excepts, copyOther, delC):
+                yield f
+        elif os.path.isfile(full_filename):
+            ext = os.path.splitext(filename)[1]
+            if ext == ".c":
+                if delC and os.stat(full_filename).st_mtime > start_time:
+                    os.remove(full_filename)
+            elif full_filename not in excepts and os.path.splitext(filename)[1] not in ('.pyc', '.pyx'):
+                if os.path.splitext(filename)[1] in ('.py', '.pyx') and not filename.startswith('__'):
+                    path = os.path.join(parent_path, name, filename)
+                    yield path
+        else:
+            pass
+
+
+def pack_pyd():
+    # 获取py列表
+    module_list = list(get_py(base_path=curr_dir, parent_path=parent_path, excepts=(setup_file,)))
+    try:
+        setup(
+            ext_modules=cythonize(module_list, language_level="3"),
+            script_args=["build_ext", "-b", build_dir, "-t", build_tmp_dir],
+        )
+    except Exception as ex:
+        print("error! ", str(ex))
+    else:
+        module_list = list(get_py(base_path=curr_dir, parent_path=parent_path, excepts=(setup_file,), copyOther=True))
+
+    module_list = list(get_py(base_path=curr_dir, parent_path=parent_path, excepts=(setup_file,), delC=True))
+    if os.path.exists(build_tmp_dir):
+        shutil.rmtree(build_tmp_dir)
+
+    print("complate! time:", time.time() - start_time, 's')
+
+
+"""
+删除编译过程中生成的.c文件
+:param path:
+:param excepts:
+:return:
+"""
+
+
+def delete_c(path='.', excepts=(setup_file,)):
+
+    dirs = os.listdir(path)
+    for dir in dirs:
+        new_dir = os.path.join(path, dir)
+        if os.path.isfile(new_dir):
+            ext = os.path.splitext(new_dir)[1]
+            if ext == '.c':
+                os.remove(new_dir)
+        elif os.path.isdir(new_dir):
+            delete_c(new_dir)
+
+
+if __name__ == '__main__':
+    try:
+        pack_pyd()
+    except Exception as e:
+        print(str(e))
+    finally:
+        delete_c()
+
+# #!/usr/bin/env python
+# # -*- coding:utf-8 -*-
+# # @time: 2021/5/26 14:17
+# # @File: create_pyd_file.py
+# import os
+# import shutil
+# import time
+# import sys
+
+
+# def func(path):
+#     folder_path = os.path.dirname(path)
+#     file_path = os.path.split(path)[1]
+#     os.chdir(folder_path)
+#     with open('setup.py', 'w') as f:
+#         f.write('from setuptools import setup\n')
+#         f.write('from Cython.Build import cythonize\n')
+#         f.write('setup(\n')
+#         f.write("name='test',\n")
+#         f.write("ext_modules=cythonize('%s')\n" % file_path)
+#         f.write(")\n")
+#     os.system('python setup.py build_ext --inplace')
+#     filename = file_path.split('.py')[0]
+#     time.sleep(2)
+#     # 这里的cp37-win_amd64需要注意一下,这个是依据python解释器类型以及windows版本生成的,建议是单个生成一个pyd文件然后相应修改一下
+#     os.rename('%s\\%s.cp38-win_64.pyd' % (folder_path, filename), '%s\\%s.pyd' % (folder_path, filename))
+#     # 这个是删除py源文件,测试的时候可以先注释掉查看效果
+#     os.remove('%s.c' % filename)
+#     build_folder_path = os.path.join(folder_path, 'build')
+#     # 删除掉生成的build文件夹
+#     shutil.rmtree(build_folder_path)
+#     os.remove('setup.py')
+#     os.remove(file_path)
+
+
+# def get_all_file(path):
+#     for root, dirs, files in os.walk(path):
+#         for name in files:
+#             if name.endswith(".py"):
+#                 file_path = os.path.join(root, name)
+#                 func(file_path)
+
+
+# paths = sys.argv[1]
+# get_all_file(paths)

BIN
config/ControlCANx86/ControlCAN.dll


BIN
config/ControlCANx86/kerneldlls/CAN232.dll


BIN
config/ControlCANx86/kerneldlls/CANETE.dll


BIN
config/ControlCANx86/kerneldlls/CANET_TCP.dll


BIN
config/ControlCANx86/kerneldlls/PC104C2.dll


BIN
config/ControlCANx86/kerneldlls/PC104CAN.dll


BIN
config/ControlCANx86/kerneldlls/PCI5121.dll


BIN
config/ControlCANx86/kerneldlls/gisadll.dll


BIN
config/ControlCANx86/kerneldlls/gpcidll.dll


BIN
config/ControlCANx86/kerneldlls/isa5420.dll


+ 20 - 0
config/ControlCANx86/kerneldlls/kerneldll.ini

@@ -0,0 +1,20 @@
+[KERNELDLL]
+COUNT=18
+1=PCI5121.dll
+2=PCI9810B.dll
+3=USBCAN.dll
+4=USBCAN.dll
+5=PCI9820B.dll
+6=CAN232.dll
+7=PCI5121.dll
+8=CANLite.dll
+9=ISA9620B.dll
+10=ISA5420.dll
+11=PC104CAN.dll
+12=CANETE.dll
+13=DNP9810B.dll
+14=PCI9840B.dll
+15=PC104C2.dll
+16=PCI9820I.dll
+17=CANET_TCP.dll
+18=pec9920.dll

BIN
config/ControlCANx86/kerneldlls/usbcan.dll


+ 0 - 0
controller/__init__.py


+ 354 - 0
controller/pcs_ctl.py

@@ -0,0 +1,354 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+'''
+@文件    :bms_pcs_ctl.py
+@时间    :2022/02/21 16:56:26
+@作者    :None
+@版本    :1.0
+@说明    :主页面 CTL
+'''
+
+from sys import argv
+from utils.globalvar import SD
+from utils.qt import QApplication, QThread, QTimer
+from controller.pcs_status_ctl import PcsStatusControll
+from widget.pcs_home import Win_Pcs_Home
+from worker.pcs_work import PcsCanReceived, PcsComWork
+import serial.tools.list_ports
+import utils.modbus.defines as cst
+
+
+class PcsControll:
+    def __init__(self):
+        self._app = QApplication(argv)
+        self._view = Win_Pcs_Home()
+        self.init()
+
+    def init(self):
+        self.port_check()
+        self.pcs_status = PcsStatusControll()
+        self._view.setStatusBar(self.pcs_status._view.statusbar)
+        self._view.interface_signal.connect(self.interface_chose)
+        self._view.connect_signal.connect(self.pcs_connect)
+        self._view.disconnect_signal.connect(self.pcs_disconnect)
+        self._view.btn_all_on.clicked.connect(self._relay_all_on)
+        self._view.btn_all_off.clicked.connect(self._relay_all_off)
+        self._view.btn_cls_on.clicked.connect(self._relay_on)
+        self._view.btn_cls_off.clicked.connect(self._relay_off)
+        # self.timer = QTimer()
+        # self.timer.timeout.connect(self.com_work)
+
+    def _relay_all_on(self):
+        if SD.CAN_ON_OFF:
+            if self._view.cb_interface.currentIndex() == 0:
+                SD.CAN_CONTROL.send(0x182B003F, [1, 0, 0, 0, 0, 0, 0, 0], extern_flag=True)
+            elif self._view.cb_interface.currentIndex() == 1:
+                SD.COM_CONTROL.send(1, cst.WRITE_MULTIPLE_REGISTERS, 0x1900, 0x01)
+            else:
+                return
+
+    def _relay_all_off(self):
+        if SD.CAN_ON_OFF:
+            if self._view.cb_interface.currentIndex() == 0:
+                SD.CAN_CONTROL.send(0x182B003F, [0, 0, 0, 0, 0, 0, 0, 0], extern_flag=True)
+            elif self._view.cb_interface.currentIndex() == 1:
+                SD.COM_CONTROL.send(1, cst.WRITE_MULTIPLE_REGISTERS, 0x1900, 0x00)
+            else:
+                return
+
+    def _relay_on(self):
+        if SD.CAN_ON_OFF:
+            if self._view.cb_interface.currentIndex() == 0:
+                SD.CAN_CONTROL.send(0x182B003F, [0xff, 1, self._view.cb_cls.currentIndex() + 1, 0, 0, 0, 0, 0], extern_flag=True)
+            elif self._view.cb_interface.currentIndex() == 1:
+                SD.COM_CONTROL.send(1, cst.WRITE_MULTIPLE_REGISTERS, 0x1901, 1 << self._view.cb_cls.currentIndex())
+            else:
+                return
+
+    def _relay_off(self):
+        if SD.CAN_ON_OFF:
+            if self._view.cb_interface.currentIndex() == 0:
+                SD.CAN_CONTROL.send(0x182B003F, [0xff, 0, self._view.cb_cls.currentIndex() + 1, 0, 0, 0, 0, 0], extern_flag=True)
+            elif self._view.cb_interface.currentIndex() == 1:
+                SD.COM_CONTROL.send(1, cst.WRITE_MULTIPLE_REGISTERS, 0x1901, (~ (1 << self._view.cb_cls.currentIndex())) & 0xFFFF)
+            else:
+                return
+
+    def com_work(self):
+        ret = SD.COM_CONTROL.send(1, cst.READ_HOLDING_REGISTERS, 0x0100, 16)
+        print(list(ret))
+
+    def port_check(self):
+        com_port = {}
+        port_list = list(serial.tools.list_ports.comports())
+        self._view.cb_com_interface_port.clear()
+
+        for port in port_list:
+            com_port["%s" % port_list[0]] = "%s" % port[1]
+            self._view.cb_com_interface_port.addItem(port[0])
+
+    def pcs_connect(self):
+        if self._view.cb_interface.currentIndex() == 0:
+            self.pcs_can_connect()
+        elif self._view.cb_interface.currentIndex() == 1:
+            self.pcs_com_connect()
+        else:
+            return
+
+    def pcs_disconnect(self):
+        if self._view.cb_interface.currentIndex() == 0:
+            self.pcs_can_disconnect()
+        elif self._view.cb_interface.currentIndex() == 1:
+            self.pcs_com_disconnect()
+        else:
+            return
+
+    def pcs_can_connect(self):
+        self._can_index_var = self._view.cb_can_interface_index.currentText()
+        self._can_chanel_var = self._view.cb_can_interface_channel.currentText()
+        self._can_baudrate_var = self._view.cb_can_interface_baudrate.currentText()
+
+        can_index = int(self._can_index_var)
+        can_channel = int(self._can_chanel_var)
+        can_baudrate = self._can_baudrate_var
+
+        SD.CAN_CONTROL.set_can_board(can_index, can_channel, can_baudrate)
+        try:
+            if SD.CAN_CONTROL.open_device():
+                SD.CAN_ON_OFF = SD.CAN_CONTROL.init_can(0x00000000, 0xFFFFFFFF)
+
+            if SD.CAN_ON_OFF:
+                self.pcs_can_connected()
+                self._view.groupbox_can_interface.setDisabled(True)
+                self._view.groupbox_interface.setDisabled(True)
+            else:
+                self._view.can_connect_error()
+        except AttributeError:
+            self._view.no_can_device()
+
+    def pcs_can_disconnect(self):
+        if SD.CAN_ON_OFF:
+            SD.CAN_ON_OFF = False
+            SD.CAN_CONTROL.clear_buffer()
+            SD.CAN_CONTROL.close_can()
+
+        if self.pcs_can_received_thread.isRunning():
+            self.pcs_can_received_thread.quit()
+            self.pcs_can_received_thread.wait()
+
+        if self.pcs_can_received_thread.isFinished():
+            del self.pcs_can_received
+            del self.pcs_can_received_thread
+
+        self._view.groupbox_can_interface.setDisabled(False)
+        self._view.groupbox_interface.setDisabled(False)
+        self.pcs_data_recovery()
+        self.pcs_status.pcs_disconnect()
+
+    def pcs_com_disconnect(self):
+        if SD.CAN_ON_OFF:
+            SD.CAN_ON_OFF = False
+
+        if self.pcs_com_thread.isRunning():
+            self.pcs_com_thread.quit()
+            self.pcs_com_thread.wait()
+
+        if self.pcs_com_thread.isFinished():
+            del self.pcs_com_work
+            del self.pcs_com_thread
+
+        # self.timer.stop()
+
+        self._view.groupbox_com_interface.setDisabled(False)
+        self._view.groupbox_interface.setDisabled(False)
+        self.pcs_data_recovery()
+        self.pcs_status.pcs_disconnect()
+
+    def pcs_com_connect(self):
+        self._port = self._view.cb_com_interface_port.currentText()
+        self._baudrate = int(self._view.cb_com_interface_baudrate.currentText())
+        self._bytesize = int(self._view.cb_com_interface_word_length.currentText())
+        self._stopbits = int(self._view.cb_com_interface_stop.currentText())
+
+        if self._view.cb_com_interface_parity.currentIndex() == 0:
+            self._parity = 'N'
+        elif self._view.cb_com_interface_parity.currentIndex() == 1:
+            self._parity = 'E'
+        elif self._view.cb_com_interface_parity.currentIndex() == 2:
+            self._parity = 'O'
+        else:
+            self._parity = 'M'
+
+        SD.COM_CONTROL.set_com(self._port, self._baudrate, self._bytesize, self._parity, self._stopbits)
+        SD.CAN_ON_OFF = SD.COM_CONTROL.open_device()
+
+        if SD.CAN_ON_OFF:
+            self.pcs_com_connected()
+            # self.timer.start(1000)
+            self._view.groupbox_com_interface.setDisabled(True)
+            self._view.groupbox_interface.setDisabled(True)
+        else:
+            self._view.no_com_device()
+
+    def pcs_com_connected(self):
+        # Com线程类
+        self.pcs_com_thread = QThread()
+        self.pcs_com_work = PcsComWork()
+        self.pcs_com_work.moveToThread(self.pcs_com_thread)
+        self.pcs_com_thread.finished.connect(self.pcs_com_work.deleteLater)
+        self.pcs_com_thread.started.connect(self.pcs_com_work.work)
+
+        self.pcs_com_work.show_00_com_signal.connect(self._pcs_00_com_signal)
+        self.pcs_com_work.show_01_com_signal.connect(self._pcs_01_com_signal)
+        self.pcs_com_work.show_02_com_signal.connect(self._pcs_02_com_signal)
+        self.pcs_com_work.show_03_com_signal.connect(self._pcs_03_com_signal)
+        self.pcs_com_work.show_04_com_signal.connect(self._pcs_04_com_signal)
+        self.pcs_com_work.show_05_com_signal.connect(self._pcs_05_com_signal)
+        self.pcs_com_work.show_06_com_signal.connect(self._pcs_06_com_signal)
+        self.pcs_com_work.show_07_com_signal.connect(self._pcs_07_com_signal)
+        self.pcs_com_work.show_08_com_signal.connect(self._pcs_08_com_signal)
+        self.pcs_com_work.show_09_com_signal.connect(self._pcs_09_com_signal)
+        self.pcs_com_work.show_0A_com_signal.connect(self._pcs_0A_com_signal)
+        self.pcs_com_work.show_0B_com_signal.connect(self._pcs_0B_com_signal)
+        self.pcs_com_work.show_0C_com_signal.connect(self._pcs_0C_com_signal)
+        self.pcs_com_work.show_0D_com_signal.connect(self._pcs_0D_com_signal)
+        self.pcs_com_work.show_0E_com_signal.connect(self._pcs_0E_com_signal)
+        self.pcs_com_work.show_0F_com_signal.connect(self._pcs_0F_com_signal)
+
+        self.pcs_status.pcs_connect()
+        # 启动线程
+        self.pcs_com_thread.start()
+
+    def _pcs_00_com_signal(self, data):
+        print(hex(data))
+
+    def _pcs_01_com_signal(self, data):
+        print(hex(data))
+
+    def _pcs_02_com_signal(self, data):
+        self._view.edt_bms_volt.setText(str(data * 0.1))
+
+    def _pcs_03_com_signal(self, data):
+        self._view.edt_bms_cur.setText(str(data * 0.1 - 2000))
+
+    def _pcs_04_com_signal(self, data):
+        self._view.edt_soc.setText(str(data * 0.1))
+
+    def _pcs_05_com_signal(self, data):
+        self._view.edt_bms_soh.setText(str(data * 0.1))
+
+    def _pcs_06_com_signal(self, data):
+        self._view.edt_bms_chg_cur_max.setText(str(data * 0.1))
+
+    def _pcs_07_com_signal(self, data):
+        self._view.edt_bms_dischg_cur_max.setText(str(data * 0.1))
+
+    def _pcs_08_com_signal(self, data):
+        self._view.edt_bms_chg_volt_high.setText(str(data * 0.1))
+
+    def _pcs_09_com_signal(self, data):
+        self._view.edt_bms_dischg_volt_high.setText(str(data * 0.1))
+
+    def _pcs_0A_com_signal(self, data):
+        self._view.edt_bms_chg_power_real.setText(str(data * 0.1))
+
+    def _pcs_0B_com_signal(self, data):
+        self._view.edt_bms_dischg_power_real.setText(str(data * 0.1))
+
+    def _pcs_0C_com_signal(self, data):
+        self._view.edt_bms_high_volt.setText(str(data * 0.1))
+
+    def _pcs_0D_com_signal(self, data):
+        self._view.edt_bms_low_volt.setText(str(data * 0.1))
+
+    def _pcs_0E_com_signal(self, data):
+        self._view.edt_bms_dischg_power_max.setText(str(data * 0.1))
+
+    def _pcs_0F_com_signal(self, data):
+        self._view.edt_bms_chg_power_max.setText(str(data * 0.1))
+
+    def pcs_can_connected(self):
+        # CAN数据接收类
+        self.pcs_can_received_thread = QThread()
+        self.pcs_can_received = PcsCanReceived()
+        self.pcs_can_received.moveToThread(self.pcs_can_received_thread)
+        self.pcs_can_received_thread.finished.connect(self.pcs_can_received.deleteLater)
+        self.pcs_can_received_thread.started.connect(self.pcs_can_received.received)
+
+        self.pcs_can_received.show_90_signal.connect(self._90_signal)
+        self.pcs_can_received.show_91_signal.connect(self._91_signal)
+        self.pcs_can_received.show_92_signal.connect(self._92_signal)
+        self.pcs_can_received.show_93_signal.connect(self._93_signal)
+
+        self.pcs_status.pcs_connect()
+        # 启动线程
+        self.pcs_can_received_thread.start()
+
+    def _90_signal(self, data):
+        self._view.edt_soc.setText(str(round(data[0] * 0.4, 1)))
+        self._view.led_bms_ready.set_status(data[1])
+        if data[2] == 0:
+            self._view.edt_bms_chg_enabled.setText("允充允放")
+        elif data[2] == 1:
+            self._view.edt_bms_chg_enabled.setText("允充禁放")
+        elif data[2] == 2:
+            self._view.edt_bms_chg_enabled.setText("禁充允放")
+        elif data[2] == 3:
+            self._view.edt_bms_chg_enabled.setText("禁充禁放")
+        self._view.edt_bms_stop.setText("")
+        self._view.led_bms_fault.set_status(2 if data[4] == 1 else 0)
+        self._view.edt_bms_soh.setText(str(round(data[5] * 0.4, 1)))
+        self._view.edt_bms_high_temp.setText(str(data[6] - 40))
+        self._view.edt_bms_low_temp.setText(str(data[7] - 40))
+
+    def _91_signal(self, data):
+        self._view.edt_bms_dischg_power_max.setText(str(round((data[1] << 8 | data[0]) * 0.1, 1)))
+        self._view.edt_bms_chg_power_max.setText(str(round((data[3] << 8 | data[2]) * 0.1, 1)))
+        self._view.edt_bms_chg_cur_max.setText(str(round((data[5] << 8 | data[4]) * 0.1 - 3200, 1)))
+        self._view.edt_bms_dischg_cur_max.setText(str(round((data[7] << 8 | data[6]) * 0.1 - 3200, 1)))
+
+    def _92_signal(self, data):
+        self._view.edt_bms_high_volt.setText(str(round((data[1] << 8 | data[0]) * 0.001, 3)))
+        self._view.edt_bms_low_volt.setText(str(round((data[3] << 8 | data[2]) * 0.001, 3)))
+        self._view.edt_bms_chg_volt_high.setText(str(round((data[5] << 8 | data[4]) * 0.1, 1)))
+        self._view.edt_bms_dischg_volt_high.setText(str(round((data[7] << 8 | data[6]) * 0.1, 1)))
+
+    def _93_signal(self, data):
+        self._view.edt_bms_volt.setText(str(round((data[1] << 8 | data[0]) * 0.1, 2)))
+        self._view.edt_bms_cur.setText(str(round((data[3] << 8 | data[2]) * 0.1 - 3200, 2)))
+        self._view.edt_bms_chg_power_real.setText(str(round((data[5] << 8 | data[4]) * 0.1, 1)))
+        self._view.edt_bms_dischg_power_real.setText(str(round((data[7] << 8 | data[6]) * 0.1, 1)))
+
+    def run(self):
+        self._view.show()
+        return self._app.exec_()
+
+    def interface_chose(self, current_interface):
+        if current_interface == 1:
+            self._view.groupbox_can_interface.hide()
+            self._view.groupbox_com_interface.show()
+        else:
+            self._view.groupbox_com_interface.hide()
+            self._view.groupbox_can_interface.show()
+
+    def pcs_data_recovery(self):
+        self._view.edt_soc.setText("")
+        self._view.led_bms_ready.set_status(0)
+        self._view.edt_bms_chg_enabled.setText("")
+        self._view.edt_bms_stop.setText("")
+        self._view.led_bms_fault.set_status(0)
+        self._view.edt_bms_soh.setText("")
+        self._view.edt_bms_high_temp.setText("")
+        self._view.edt_bms_low_temp.setText("")
+        self._view.edt_bms_dischg_power_max.setText("")
+        self._view.edt_bms_chg_power_max.setText("")
+        self._view.edt_bms_chg_cur_max.setText("")
+        self._view.edt_bms_dischg_cur_max.setText("")
+        self._view.edt_bms_high_volt.setText("")
+        self._view.edt_bms_low_volt.setText("")
+        self._view.edt_bms_chg_volt_high.setText("")
+        self._view.edt_bms_dischg_volt_high.setText("")
+        self._view.edt_bms_volt.setText("")
+        self._view.edt_bms_cur.setText("")
+        self._view.edt_bms_chg_power_real.setText("")
+        self._view.edt_bms_dischg_power_real.setText("")

+ 32 - 0
controller/pcs_status_ctl.py

@@ -0,0 +1,32 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+'''
+@文件    :bms_status_ctl.py
+@时间    :2022/02/21 16:57:56
+@作者    :None
+@版本    :1.0
+@说明    : 状态栏 CTL
+'''
+
+
+from widget.pcs_status_bar import Win_Pcs_Status_Bar
+from utils.globalvar import SD
+
+
+class PcsStatusControll:
+    def __init__(self):
+        self._view = Win_Pcs_Status_Bar()
+
+    def pcs_connect(self):
+        self._view.label_pcs_connect.setText("已连接")
+        self._view.label_pcs_send.setText("发送参数中")
+        self._view.label_pcs_received.setText("接收数据中")
+
+    def pcs_disconnect(self):
+        if SD.CAN_ON_OFF is False:
+            self._view.label_pcs_send.setText("发送未启动")
+            self._view.label_pcs_received.setText("接收未启动")
+            self._view.label_pcs_connect.setText("未连接")
+
+    def run(self):
+        self._view.show()

+ 35 - 0
pcs_upper.py

@@ -0,0 +1,35 @@
+"""
+@文件    :PcsUpper.py
+@时间    :2021/12/08 16:32:57
+@作者    :None
+@版本    :1.0
+@说明    :PcsUpper程序主入口
+"""
+
+
+from sys import exit
+from controller.pcs_ctl import PcsControll
+from utils.globalvar import SD
+from utils.qt import QApplication, QCoreApplication, Qt, QStyleFactory
+
+
+def pcs_start():
+
+    # 适配分辨率
+    if hasattr(Qt, 'AA_EnableHighDpiScaling'):
+        QCoreApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
+
+    if hasattr(Qt, 'AA_UseHighDpiPixmaps'):
+        QCoreApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
+        SD.HIGH_DPI = 0
+    else:
+        SD.HIGH_DPI = 1
+    main_connrtoller = PcsControll()
+
+    QApplication.setStyle(QStyleFactory.create("windows"))
+
+    exit(main_connrtoller.run())
+
+
+if __name__ == "__main__":
+    pcs_start()

+ 12 - 0
requirements.txt

@@ -0,0 +1,12 @@
+#
+# This file is autogenerated by pip-compile
+# To update, run:
+#
+#    pip-compile requirements.txt
+#
+
+
+PyQt5
+PyQtChart
+PySide
+pymodbus_tk

+ 0 - 0
ui/__init__.py


+ 419 - 0
ui/home.py

@@ -0,0 +1,419 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+'''
+@文件    :home.py
+@时间    :2022/01/22 09:53:31
+@作者    :None
+@版本    :1.0
+@说明    :主页面
+'''
+
+
+from ui.own.frame_theme import MyFrame
+from ui.own.switch_button import SwitchButton
+from ui.own.led import Led
+from ui.own.palette_theme import set_my_palette
+from utils.qt import Qt, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QLabel, QGroupBox, QLineEdit, QPushButton, QSizePolicy, QComboBox, QSize, QCoreApplication, QMetaObject
+
+
+class UiPcsHomePage(object):
+    def setupUi(self, main):
+        main.resize(650, 780)
+        main.setMinimumSize(650, 780)
+
+        set_my_palette(main)
+
+        self.central_widget = QWidget(main)
+        self.main_layout = QHBoxLayout(self.central_widget)
+        self.main_layout.setContentsMargins(0, 0, 0, 0)
+        self.main_layout.setSpacing(0)
+
+        # 主窗口左侧布局
+        self.left_widget = QWidget(self.central_widget)
+        # 主窗口左侧采用垂直布局
+        self.left_layout = QVBoxLayout(self.left_widget)
+        self.left_layout.setContentsMargins(0, 0, 0, 0)
+        self.left_layout.setSpacing(0)
+
+        # 数据接口
+        self.interface_widget = MyFrame(self.left_widget)
+        self.interface_hlayout = QHBoxLayout(self.interface_widget)
+        self.interface_hlayout.setContentsMargins(0, 0, 0, 0)
+        self.interface_hlayout.setSpacing(0)
+        self.groupbox_interface = QGroupBox(self.interface_widget)
+        self.interface_vlayout = QVBoxLayout(self.groupbox_interface)
+        self.cb_interface = QComboBox(self.groupbox_interface)
+        self.cb_interface.addItems(["CAN通讯", "串口通讯"])
+        self.interface_vlayout.addWidget(self.cb_interface)
+        self.interface_hlayout.addWidget(self.groupbox_interface)
+        self.left_layout.addWidget(self.interface_widget)
+
+        # CAN通讯配置
+        self.can_interface_widget = MyFrame(self.left_widget)
+        self.can_interface_hlayout = QHBoxLayout(self.can_interface_widget)
+        self.can_interface_hlayout.setContentsMargins(0, 0, 0, 0)
+        self.can_interface_hlayout.setSpacing(0)
+        self.groupbox_can_interface = QGroupBox(self.interface_widget)
+        self.can_interface_glayout = QGridLayout(self.groupbox_can_interface)
+
+        self.lb_can_interface_channel = QLabel(self.groupbox_can_interface)
+        self.lb_can_interface_channel.setText("设备索引号:")
+        self.can_interface_glayout.addWidget(self.lb_can_interface_channel, 0, 0)
+        self.cb_can_interface_channel = QComboBox(self.groupbox_can_interface)
+        self.cb_can_interface_channel.addItems([str(i) for i in range(9)])
+        self.can_interface_glayout.addWidget(self.cb_can_interface_channel, 0, 1)
+
+        self.lb_can_interface_index = QLabel(self.groupbox_can_interface)
+        self.lb_can_interface_index.setText("设备通道号:")
+        self.can_interface_glayout.addWidget(self.lb_can_interface_index, 1, 0)
+        self.cb_can_interface_index = QComboBox(self.groupbox_can_interface)
+        self.cb_can_interface_index.addItems(["0", "1"])
+        self.can_interface_glayout.addWidget(self.cb_can_interface_index, 1, 1)
+
+        self.lb_can_interface_baudrate = QLabel(self.groupbox_can_interface)
+        self.lb_can_interface_baudrate.setText("CAN波特率:")
+        self.can_interface_glayout.addWidget(self.lb_can_interface_baudrate, 2, 0)
+        self.cb_can_interface_baudrate = QComboBox(self.groupbox_can_interface)
+        self.cb_can_interface_baudrate.addItems(["125Kbps", "250Kbps", "500Kbps"])
+        self.cb_can_interface_baudrate.setCurrentText("250Kbps")
+        self.can_interface_glayout.addWidget(self.cb_can_interface_baudrate, 2, 1)
+
+        self.can_interface_hlayout.addWidget(self.groupbox_can_interface)
+        self.left_layout.addWidget(self.can_interface_widget)
+
+        # 串口通讯配置
+        self.com_interface_widget = MyFrame(self.left_widget)
+        self.com_interface_hlayout = QHBoxLayout(self.com_interface_widget)
+        self.com_interface_hlayout.setContentsMargins(0, 0, 0, 0)
+        self.com_interface_hlayout.setSpacing(0)
+        self.groupbox_com_interface = QGroupBox(self.interface_widget)
+        self.com_interface_glayout = QGridLayout(self.groupbox_com_interface)
+
+        self.lb_com_interface_port = QLabel(self.groupbox_com_interface)
+        self.lb_com_interface_port.setText("端口号:")
+        self.com_interface_glayout.addWidget(self.lb_com_interface_port, 0, 0)
+        self.cb_com_interface_port = QComboBox(self.groupbox_com_interface)
+        self.cb_com_interface_port.addItems([str(i + 1) for i in range(9)])
+        self.com_interface_glayout.addWidget(self.cb_com_interface_port, 0, 1)
+
+        self.lb_com_interface_baudrate = QLabel(self.groupbox_com_interface)
+        self.lb_com_interface_baudrate.setText("波特率:")
+        self.com_interface_glayout.addWidget(self.lb_com_interface_baudrate, 1, 0)
+        self.cb_com_interface_baudrate = QComboBox(self.groupbox_com_interface)
+        self.cb_com_interface_baudrate.addItems(["9600", "115200"])
+        self.com_interface_glayout.addWidget(self.cb_com_interface_baudrate, 1, 1)
+
+        self.lb_com_interface_parity = QLabel(self.groupbox_com_interface)
+        self.lb_com_interface_parity.setText("校验位:")
+        self.com_interface_glayout.addWidget(self.lb_com_interface_parity, 2, 0)
+        self.cb_com_interface_parity = QComboBox(self.groupbox_com_interface)
+        self.cb_com_interface_parity.addItems(["无校验", "偶校验", "奇校验"])
+        self.com_interface_glayout.addWidget(self.cb_com_interface_parity, 2, 1)
+
+        self.lb_com_interface_word_length = QLabel(self.groupbox_com_interface)
+        self.lb_com_interface_word_length.setText("数据位:")
+        self.com_interface_glayout.addWidget(self.lb_com_interface_word_length, 3, 0)
+        self.cb_com_interface_word_length = QComboBox(self.groupbox_com_interface)
+        self.cb_com_interface_word_length.addItems(["8", "9"])
+        self.com_interface_glayout.addWidget(self.cb_com_interface_word_length, 3, 1)
+
+        self.lb_com_interface_stop = QLabel(self.groupbox_com_interface)
+        self.lb_com_interface_stop.setText("停止位:")
+        self.com_interface_glayout.addWidget(self.lb_com_interface_stop, 4, 0)
+        self.cb_com_interface_stop = QComboBox(self.groupbox_com_interface)
+        self.cb_com_interface_stop.addItems(["1", "1.5", "2"])
+        self.com_interface_glayout.addWidget(self.cb_com_interface_stop, 4, 1)
+        self.groupbox_com_interface.hide()
+        self.com_interface_hlayout.addWidget(self.groupbox_com_interface)
+        self.left_layout.addWidget(self.com_interface_widget)
+
+        # BMS连接开关
+        self.pcs_connect_widget = MyFrame(self.left_widget)
+        self.pcs_connect_layout = QHBoxLayout(self.pcs_connect_widget)
+        self.pcs_connect_layout.setContentsMargins(0, 0, 0, 0)
+        self.pcs_connect_layout.setSpacing(0)
+        self.groupbox_pcs_connect = QGroupBox(self.pcs_connect_widget)
+        self.bms_connect_hlayout = QHBoxLayout(self.groupbox_pcs_connect)
+        self.pcs_switch = SwitchButton(self.groupbox_pcs_connect)
+        size_policy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
+        size_policy.setHorizontalStretch(0)
+        size_policy.setVerticalStretch(0)
+        size_policy.setHeightForWidth(self.pcs_switch.sizePolicy().hasHeightForWidth())
+        self.pcs_switch.setSizePolicy(size_policy)
+        self.pcs_switch.setMinimumSize(QSize(70, 30))
+        self.bms_connect_hlayout.addWidget(self.pcs_switch)
+        self.pcs_connect_layout.addWidget(self.groupbox_pcs_connect)
+        self.left_layout.addWidget(self.pcs_connect_widget)
+
+        # 其他信息
+        self.other_widget = MyFrame(self.left_widget)
+        self.other_layout = QHBoxLayout(self.other_widget)
+        self.other_layout.setContentsMargins(0, 0, 0, 0)
+        self.other_layout.setSpacing(0)
+        self.groupbox_other = QGroupBox(self.other_widget)
+        size_policy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding)
+        size_policy.setHorizontalStretch(0)
+        size_policy.setVerticalStretch(0)
+        size_policy.setHeightForWidth(self.groupbox_other.sizePolicy().hasHeightForWidth())
+        self.groupbox_other.setSizePolicy(size_policy)
+        self.other_layout.addWidget(self.groupbox_other)
+        self.left_layout.addWidget(self.other_widget)
+
+        self.main_layout.addWidget(self.left_widget, stretch=1)
+
+        self.right_widget = QWidget(self.central_widget)
+        # 主窗口右侧采用垂直布局
+        self.right_layout = QVBoxLayout(self.right_widget)
+        self.right_layout.setContentsMargins(0, 0, 0, 0)
+        self.right_layout.setSpacing(0)
+
+        # 数据接口
+        self.important_data_widget = MyFrame(self.right_widget)
+        self.important_data_hlayout = QHBoxLayout(self.important_data_widget)
+        self.important_data_hlayout.setContentsMargins(0, 0, 0, 0)
+        self.important_data_hlayout.setSpacing(0)
+        self.groupbox_important_data = QGroupBox(self.important_data_widget)
+        self.groupbox_important_data.setTitle("实时数据")
+        self.important_data_glayout = QGridLayout(self.groupbox_important_data)
+
+        self.lb_param = QLabel(self.groupbox_important_data)
+        self.lb_param.setText("参数")
+        self.important_data_glayout.addWidget(self.lb_param, 0, 0, 1, 1, Qt.AlignCenter)
+        # self.lb_param_address = QLabel(self.groupbox_important_data)
+        # self.lb_param_address.setText("地址")
+        # self.important_data_glayout.addWidget(self.lb_param_address, 0, 1, 1, 1, Qt.AlignCenter)
+        self.lb_param_value = QLabel(self.groupbox_important_data)
+        self.lb_param_value.setText("值")
+        self.important_data_glayout.addWidget(self.lb_param_value, 0, 2, 1, 1, Qt.AlignCenter)
+        # 当前荷电状态(SOC)
+        self.lb_soc = QLabel(self.groupbox_important_data)
+        self.lb_soc.setText("当前荷电状态 [%]")
+        self.important_data_glayout.addWidget(self.lb_soc, 1, 0, 1, 1, Qt.AlignCenter)
+        # self.edt_soc_address = QLineEdit(self.groupbox_important_data)
+        # self.edt_soc_address.setText("1800")
+        # self.important_data_glayout.addWidget(self.edt_soc_address, 1, 1, 1, 1, Qt.AlignCenter)
+        self.edt_soc = QLineEdit(self.groupbox_important_data)
+        self.edt_soc.setReadOnly(True)
+        self.important_data_glayout.addWidget(self.edt_soc, 1, 2, 1, 1, Qt.AlignCenter)
+        # BMS是否已准备好
+        self.lb_bms_ready = QLabel(self.groupbox_important_data)
+        self.lb_bms_ready.setText("BMS就绪状态")
+        self.important_data_glayout.addWidget(self.lb_bms_ready, 2, 0, 1, 1, Qt.AlignCenter)
+        # self.edt_bms_ready_address = QLineEdit(self.groupbox_important_data)
+        # self.edt_bms_ready_address.setText("1801")
+        # self.important_data_glayout.addWidget(self.edt_bms_ready_address, 2, 1, 1, 1, Qt.AlignCenter)
+        self.led_bms_ready = Led(self.groupbox_important_data)
+        self.important_data_glayout.addWidget(self.led_bms_ready, 2, 2, 1, 1, Qt.AlignCenter)
+        # 充放电使能标志
+        self.lb_bms_chg_enabled = QLabel(self.groupbox_important_data)
+        self.lb_bms_chg_enabled.setText("充放电使能标志")
+        self.important_data_glayout.addWidget(self.lb_bms_chg_enabled, 3, 0, 1, 1, Qt.AlignCenter)
+        # self.edt_bms_chg_enabled_address = QLineEdit(self.groupbox_important_data)
+        # self.edt_bms_chg_enabled_address.setText("1802")
+        # self.important_data_glayout.addWidget(self.edt_bms_chg_enabled_address, 3, 1, 1, 1, Qt.AlignCenter)
+        self.edt_bms_chg_enabled = QLineEdit(self.groupbox_important_data)
+        self.edt_bms_chg_enabled.setReadOnly(True)
+        self.important_data_glayout.addWidget(self.edt_bms_chg_enabled, 3, 2, 1, 1, Qt.AlignCenter)
+        # BMS停机指令
+        self.lb_bms_stop = QLabel(self.groupbox_important_data)
+        self.lb_bms_stop.setText("BMS停机指令")
+        self.important_data_glayout.addWidget(self.lb_bms_stop, 4, 0, 1, 1, Qt.AlignCenter)
+        # self.edt_bms_stop_address = QLineEdit(self.groupbox_important_data)
+        # self.edt_bms_stop_address.setText("1803")
+        # self.important_data_glayout.addWidget(self.edt_bms_stop_address, 4, 1, 1, 1, Qt.AlignCenter)
+        self.edt_bms_stop = QLineEdit(self.groupbox_important_data)
+        self.edt_bms_stop.setReadOnly(True)
+        self.important_data_glayout.addWidget(self.edt_bms_stop, 4, 2, 1, 1, Qt.AlignCenter)
+        # BMS故障标志
+        self.lb_bms_fault = QLabel(self.groupbox_important_data)
+        self.lb_bms_fault.setText("BMS故障标志")
+        self.important_data_glayout.addWidget(self.lb_bms_fault, 5, 0, 1, 1, Qt.AlignCenter)
+        # self.edt_bms_fault_address = QLineEdit(self.groupbox_important_data)
+        # self.important_data_glayout.addWidget(self.edt_bms_fault_address, 5, 1, 1, 1, Qt.AlignCenter)
+        self.led_bms_fault = Led(self.groupbox_important_data)
+        self.important_data_glayout.addWidget(self.led_bms_fault, 5, 2, 1, 1, Qt.AlignCenter)
+        # 电池堆SOH
+        self.lb_bms_soh = QLabel(self.groupbox_important_data)
+        self.lb_bms_soh.setText("电池堆SOH [%]")
+        self.important_data_glayout.addWidget(self.lb_bms_soh, 6, 0, 1, 1, Qt.AlignCenter)
+        # self.edt_bms_soh_address = QLineEdit(self.groupbox_important_data)
+        # self.important_data_glayout.addWidget(self.edt_bms_soh_address, 6, 1, 1, 1, Qt.AlignCenter)
+        self.edt_bms_soh = QLineEdit(self.groupbox_important_data)
+        self.edt_bms_soh.setReadOnly(True)
+        self.important_data_glayout.addWidget(self.edt_bms_soh, 6, 2, 1, 1, Qt.AlignCenter)
+        # 最高单体温度值
+        self.lb_bms_high_temp = QLabel(self.groupbox_important_data)
+        self.lb_bms_high_temp.setText("最高单体温度值 [℃]")
+        self.important_data_glayout.addWidget(self.lb_bms_high_temp, 7, 0, 1, 1, Qt.AlignCenter)
+        # self.edt_bms_high_temp_address = QLineEdit(self.groupbox_important_data)
+        # self.important_data_glayout.addWidget(self.edt_bms_high_temp_address, 7, 1, 1, 1, Qt.AlignCenter)
+        self.edt_bms_high_temp = QLineEdit(self.groupbox_important_data)
+        self.edt_bms_high_temp.setReadOnly(True)
+        self.important_data_glayout.addWidget(self.edt_bms_high_temp, 7, 2, 1, 1, Qt.AlignCenter)
+        # 最低单体温度值
+        self.lb_bms_low_temp = QLabel(self.groupbox_important_data)
+        self.lb_bms_low_temp.setText("最低单体温度值 [℃]")
+        self.important_data_glayout.addWidget(self.lb_bms_low_temp, 8, 0, 1, 1, Qt.AlignCenter)
+        # self.edt_bms_low_temp_address = QLineEdit(self.groupbox_important_data)
+        # self.important_data_glayout.addWidget(self.edt_bms_low_temp_address, 8, 1, 1, 1, Qt.AlignCenter)
+        self.edt_bms_low_temp = QLineEdit(self.groupbox_important_data)
+        self.edt_bms_low_temp.setReadOnly(True)
+        self.important_data_glayout.addWidget(self.edt_bms_low_temp, 8, 2, 1, 1, Qt.AlignCenter)
+        # 重要信息2 BAMS->EMS(上位机)
+        # 当前最大允许放电功率
+        self.lb_bms_dischg_power_max = QLabel(self.groupbox_important_data)
+        self.lb_bms_dischg_power_max.setText("当前最大允许放电功率 [kW]")
+        self.important_data_glayout.addWidget(self.lb_bms_dischg_power_max, 9, 0, 1, 1, Qt.AlignCenter)
+        # self.edt_bms_dischg_power_max_address = QLineEdit(self.groupbox_important_data)
+        # self.important_data_glayout.addWidget(self.edt_bms_dischg_power_max_address, 9, 1, 1, 1, Qt.AlignCenter)
+        self.edt_bms_dischg_power_max = QLineEdit(self.groupbox_important_data)
+        self.edt_bms_dischg_power_max.setReadOnly(True)
+        self.important_data_glayout.addWidget(self.edt_bms_dischg_power_max, 9, 2, 1, 1, Qt.AlignCenter)
+        # 当前最大允许充电功率
+        self.lb_bms_chg_power_max = QLabel(self.groupbox_important_data)
+        self.lb_bms_chg_power_max.setText("当前最大允许充电功率 [kW]")
+        self.important_data_glayout.addWidget(self.lb_bms_chg_power_max, 10, 0, 1, 1, Qt.AlignCenter)
+        # self.edt_bms_chg_power_max_address = QLineEdit(self.groupbox_important_data)
+        # self.important_data_glayout.addWidget(self.edt_bms_chg_power_max_address, 10, 1, 1, 1, Qt.AlignCenter)
+        self.edt_bms_chg_power_max = QLineEdit(self.groupbox_important_data)
+        self.edt_bms_chg_power_max.setReadOnly(True)
+        self.important_data_glayout.addWidget(self.edt_bms_chg_power_max, 10, 2, 1, 1, Qt.AlignCenter)
+        # 当前最大允许充电电流
+        self.lb_bms_chg_cur_max = QLabel(self.groupbox_important_data)
+        self.lb_bms_chg_cur_max.setText("当前最大允许充电电流 [A]")
+        self.important_data_glayout.addWidget(self.lb_bms_chg_cur_max, 11, 0, 1, 1, Qt.AlignCenter)
+        # self.edt_bms_chg_cur_max_address = QLineEdit(self.groupbox_important_data)
+        # self.important_data_glayout.addWidget(self.edt_bms_chg_cur_max_address, 11, 1, 1, 1, Qt.AlignCenter)
+        self.edt_bms_chg_cur_max = QLineEdit(self.groupbox_important_data)
+        self.edt_bms_chg_cur_max.setReadOnly(True)
+        self.important_data_glayout.addWidget(self.edt_bms_chg_cur_max, 11, 2, 1, 1, Qt.AlignCenter)
+        # 当前最大允许放电电流
+        self.lb_bms_dischg_cur_max = QLabel(self.groupbox_important_data)
+        self.lb_bms_dischg_cur_max.setText("当前最大允许放电电流 [A]")
+        self.important_data_glayout.addWidget(self.lb_bms_dischg_cur_max, 12, 0, 1, 1, Qt.AlignCenter)
+        # self.edt_bms_dischg_cur_max_address = QLineEdit(self.groupbox_important_data)
+        # self.important_data_glayout.addWidget(self.edt_bms_dischg_cur_max_address, 12, 1, 1, 1, Qt.AlignCenter)
+        self.edt_bms_dischg_cur_max = QLineEdit(self.groupbox_important_data)
+        self.edt_bms_dischg_cur_max.setReadOnly(True)
+        self.important_data_glayout.addWidget(self.edt_bms_dischg_cur_max, 12, 2, 1, 1, Qt.AlignCenter)
+        # 重要信息3 BAMS->EMS(上位机)
+        # 最高单体电压值
+        self.lb_bms_high_volt = QLabel(self.groupbox_important_data)
+        self.lb_bms_high_volt.setText("最高单体电压值 [V]")
+        self.important_data_glayout.addWidget(self.lb_bms_high_volt, 13, 0, 1, 1, Qt.AlignCenter)
+        # self.edt_bms_high_volt_address = QLineEdit(self.groupbox_important_data)
+        # self.important_data_glayout.addWidget(self.edt_bms_high_volt_address, 13, 1, 1, 1, Qt.AlignCenter)
+        self.edt_bms_high_volt = QLineEdit(self.groupbox_important_data)
+        self.edt_bms_high_volt.setReadOnly(True)
+        self.important_data_glayout.addWidget(self.edt_bms_high_volt, 13, 2, 1, 1, Qt.AlignCenter)
+        # 最低单体电压值
+        self.lb_bms_low_volt = QLabel(self.groupbox_important_data)
+        self.lb_bms_low_volt.setText("最低单体电压值 [V]")
+        self.important_data_glayout.addWidget(self.lb_bms_low_volt, 14, 0, 1, 1, Qt.AlignCenter)
+        # self.edt_bms_low_volt_address = QLineEdit(self.groupbox_important_data)
+        # self.important_data_glayout.addWidget(self.edt_bms_low_volt_address, 14, 1, 1, 1, Qt.AlignCenter)
+        self.edt_bms_low_volt = QLineEdit(self.groupbox_important_data)
+        self.edt_bms_low_volt.setReadOnly(True)
+        self.important_data_glayout.addWidget(self.edt_bms_low_volt, 14, 2, 1, 1, Qt.AlignCenter)
+        # 最大允许充电电压
+        self.lb_bms_chg_volt_high = QLabel(self.groupbox_important_data)
+        self.lb_bms_chg_volt_high.setText("最大允许充电电压 [V]")
+        self.important_data_glayout.addWidget(self.lb_bms_chg_volt_high, 15, 0, 1, 1, Qt.AlignCenter)
+        # self.edt_bms_chg_volt_high_address = QLineEdit(self.groupbox_important_data)
+        # self.important_data_glayout.addWidget(self.edt_bms_chg_volt_high_address, 15, 1, 1, 1, Qt.AlignCenter)
+        self.edt_bms_chg_volt_high = QLineEdit(self.groupbox_important_data)
+        self.edt_bms_chg_volt_high.setReadOnly(True)
+        self.important_data_glayout.addWidget(self.edt_bms_chg_volt_high, 15, 2, 1, 1, Qt.AlignCenter)
+        # 最大允许放电电压
+        self.lb_bms_dischg_volt_high = QLabel(self.groupbox_important_data)
+        self.lb_bms_dischg_volt_high.setText("最大允许放电电压 [V]")
+        self.important_data_glayout.addWidget(self.lb_bms_dischg_volt_high, 16, 0, 1, 1, Qt.AlignCenter)
+        # self.edt_bms_dischg_volt_high_address = QLineEdit(self.groupbox_important_data)
+        # self.important_data_glayout.addWidget(self.edt_bms_dischg_volt_high_address, 16, 1, 1, 1, Qt.AlignCenter)
+        self.edt_bms_dischg_volt_high = QLineEdit(self.groupbox_important_data)
+        self.edt_bms_dischg_volt_high.setReadOnly(True)
+        self.important_data_glayout.addWidget(self.edt_bms_dischg_volt_high, 16, 2, 1, 1, Qt.AlignCenter)
+        # 重要信息4 BAMS->EMS(上位机)
+        # 电池堆实时总电压
+        self.lb_bms_volt = QLabel(self.groupbox_important_data)
+        self.lb_bms_volt.setText("电池堆实时总电压 [V]")
+        self.important_data_glayout.addWidget(self.lb_bms_volt, 17, 0, 1, 1, Qt.AlignCenter)
+        # self.edt_bms_volt_address = QLineEdit(self.groupbox_important_data)
+        # self.important_data_glayout.addWidget(self.edt_bms_volt_address, 17, 1, 1, 1, Qt.AlignCenter)
+        self.edt_bms_volt = QLineEdit(self.groupbox_important_data)
+        self.edt_bms_volt.setReadOnly(True)
+        self.important_data_glayout.addWidget(self.edt_bms_volt, 17, 2, 1, 1, Qt.AlignCenter)
+        # 电池堆实时总电流
+        self.lb_bms_cur = QLabel(self.groupbox_important_data)
+        self.lb_bms_cur.setText("电池堆实时总电流 [A]")
+        self.important_data_glayout.addWidget(self.lb_bms_cur, 18, 0, 1, 1, Qt.AlignCenter)
+        # self.edt_bms_cur_address = QLineEdit(self.groupbox_important_data)
+        # self.important_data_glayout.addWidget(self.edt_bms_cur_address, 18, 1, 1, 1, Qt.AlignCenter)
+        self.edt_bms_cur = QLineEdit(self.groupbox_important_data)
+        self.edt_bms_cur.setReadOnly(True)
+        self.important_data_glayout.addWidget(self.edt_bms_cur, 18, 2, 1, 1, Qt.AlignCenter)
+        # 当前可充电能量
+        self.lb_bms_chg_power_real = QLabel(self.groupbox_important_data)
+        self.lb_bms_chg_power_real.setText("当前可充电能量 [kWh]")
+        self.important_data_glayout.addWidget(self.lb_bms_chg_power_real, 19, 0, 1, 1, Qt.AlignCenter)
+        # self.edt_bms_chg_power_real_address = QLineEdit(self.groupbox_important_data)
+        # self.important_data_glayout.addWidget(self.edt_bms_chg_power_real_address, 19, 1, 1, 1, Qt.AlignCenter)
+        self.edt_bms_chg_power_real = QLineEdit(self.groupbox_important_data)
+        self.edt_bms_chg_power_real.setReadOnly(True)
+        self.important_data_glayout.addWidget(self.edt_bms_chg_power_real, 19, 2, 1, 1, Qt.AlignCenter)
+        # 当前可放电能量
+        self.lb_bms_dischg_power_real = QLabel(self.groupbox_important_data)
+        self.lb_bms_dischg_power_real.setText("当前可放电能量 [kWh]")
+        self.important_data_glayout.addWidget(self.lb_bms_dischg_power_real, 20, 0, 1, 1, Qt.AlignCenter)
+        # self.edt_bms_dischg_power_real_address = QLineEdit(self.groupbox_important_data)
+        # self.important_data_glayout.addWidget(self.edt_bms_dischg_power_real_address, 20, 1, 1, 1, Qt.AlignCenter)
+        self.edt_bms_dischg_power_real = QLineEdit(self.groupbox_important_data)
+        self.edt_bms_dischg_power_real.setReadOnly(True)
+        self.important_data_glayout.addWidget(self.edt_bms_dischg_power_real, 20, 2, 1, 1, Qt.AlignCenter)
+
+        self.important_data_hlayout.addWidget(self.groupbox_important_data)
+        self.right_layout.addWidget(self.important_data_widget)
+
+        # 数据接口
+        self.relay_widget = MyFrame(self.right_widget)
+        self.relay_vlayout = QVBoxLayout(self.relay_widget)
+        self.relay_vlayout.setContentsMargins(0, 0, 0, 0)
+        self.relay_vlayout.setSpacing(0)
+        self.groupbox_relay = QGroupBox(self.relay_widget)
+        self.groupbox_relay.setTitle("PCS 控制指令")
+        self.relay_glayout = QGridLayout(self.groupbox_relay)
+
+        self.btn_all_on = QPushButton(self.groupbox_relay)
+        self.btn_all_on.setText("一键闭合")
+        self.relay_glayout.addWidget(self.btn_all_on, 0, 2, 1, 1, Qt.AlignCenter)
+        self.btn_all_off = QPushButton(self.groupbox_relay)
+        self.btn_all_off.setText("一键断开")
+        self.relay_glayout.addWidget(self.btn_all_off, 1, 2, 1, 1, Qt.AlignCenter)
+
+        self.lb_cls = QLabel(self.groupbox_relay)
+        self.lb_cls.setText("簇号:")
+        self.relay_glayout.addWidget(self.lb_cls, 0, 0, 1, 1, Qt.AlignCenter)
+        self.cb_cls = QComboBox(self.groupbox_relay)
+        self.cb_cls.addItems([str(i + 1) for i in range(16)])
+        self.relay_glayout.addWidget(self.cb_cls, 0, 1, 1, 1)
+        self.btn_cls_on = QPushButton(self.groupbox_relay)
+        self.btn_cls_on.setText("闭合")
+        self.relay_glayout.addWidget(self.btn_cls_on, 1, 0, 1, 1, Qt.AlignCenter)
+        self.btn_cls_off = QPushButton(self.groupbox_relay)
+        self.btn_cls_off.setText("断开")
+        self.relay_glayout.addWidget(self.btn_cls_off, 1, 1, 1, 1, Qt.AlignCenter)
+
+        self.relay_vlayout.addWidget(self.groupbox_relay)
+        self.right_layout.addWidget(self.relay_widget)
+        self.main_layout.addWidget(self.right_widget, stretch=2)
+
+        main.setCentralWidget(self.central_widget)
+
+        self.retranslateUi(main)
+        QMetaObject.connectSlotsByName(main)
+
+    def retranslateUi(self, MainWindow):
+        _translate = QCoreApplication.translate
+        MainWindow.setWindowTitle(_translate("MainWindow", "PCS模拟器"))
+        self.groupbox_interface.setTitle(_translate("MainWindow", "数据接口"))
+        # self.groupbox_pcs_connect.setTitle(_translate("MainWindow", "BMS开关"))

+ 0 - 0
ui/own/__init__.py


+ 21 - 0
ui/own/frame_theme.py

@@ -0,0 +1,21 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+'''
+@文件    :frame_theme.py
+@时间    :2022/01/22 10:04:48
+@作者    :None
+@版本    :1.0
+@说明    :Frame控件主题
+'''
+
+
+from utils.qt import QFrame
+
+
+class MyFrame(QFrame):
+    def __init__(self, parent=None):
+        super(MyFrame, self).__init__(parent)
+        self.setFrameStyle(QFrame.Panel | QFrame.Raised)
+        # 设置外线宽度
+        self.setLineWidth(3)
+        self.setMidLineWidth(3)

+ 257 - 0
ui/own/led.py

@@ -0,0 +1,257 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+'''
+@文件    :led.py
+@时间    :2022/01/14 09:31:20
+@作者    :None
+@版本    :2.0
+@说明    :LED灯
+'''
+
+
+# from numpy import array, uint8
+from utils.qt import QApplication, QColor, QSize, QPushButton
+
+
+class Led(QPushButton):
+    black = QColor(0x00, 0x00, 0x00)
+    white = QColor(0xff, 0xff, 0xff)
+    blue = QColor(0x73, 0xce, 0xf4)
+    green = QColor(0xad, 0xff, 0x2f)
+    orange = QColor(0xff, 0xa5, 0x00)
+    purple = QColor(0xaf, 0x00, 0xff)
+    red = QColor(0xf4, 0x37, 0x53)
+    yellow = QColor(0xff, 0xff, 0x00)
+
+    capsule = 1
+    circle = 2
+    rectangle = 3
+
+    def __init__(self, parent, first_color=green, second_color=red, third_color=yellow, off_color=black, shape=circle):
+        super().__init__()
+
+        self._qss = 'QPushButton {{ \
+                                   border: 3px solid lightgray; \
+                                   border-radius: {}px; \
+                                   background-color: \
+                                       QLinearGradient( \
+                                           y1: 0, y2: 1, \
+                                           stop: 0 white, \
+                                           stop: 0.2 #{}, \
+                                           stop: 0.8 #{}, \
+                                           stop: 1 #{} \
+                                       ); \
+                                 }}'
+        self._first_qss = ''
+        self._second_qss = ''
+        self._third_qss = ''
+        self._off_qss = ''
+
+        self._status = 0
+        self._end_radius = 0
+
+        # Properties that will trigger changes on qss.
+        self.__first_color = None
+        self.__second_color = None
+        self.__third_color = None
+        self.__off_color = None
+        self.__shape = None
+        self.__height = 0
+
+        self._first_color = first_color
+        self._second_color = second_color
+        self._third_color = third_color
+        self._off_color = off_color
+        self._shape = shape
+        self._height = self.sizeHint().height()
+
+    # =================================================== Reimplemented Methods
+    def sizeHint(self):
+        res_h = QApplication.desktop().screenGeometry().height()
+        # res_w = QApplication.desktop().screenGeometry().width()
+
+        # res_w, res_h = pyautogui.size()  # Available resolution geometry
+        if self._shape == Led.capsule:
+            base_w = 50
+            base_h = 30
+        elif self._shape == Led.circle:
+            base_w = 30
+            base_h = 30
+        elif self._shape == Led.rectangle:
+            base_w = 40
+            base_h = 30
+
+        width = int(base_w * res_h / 1080)
+        height = int(base_h * res_h / 1080)
+        return QSize(width, height)
+
+    def resizeEvent(self, event):
+        self._height = self.size().height()
+        QPushButton.resizeEvent(self, event)
+
+    def setFixedSize(self, width, height):
+        self._height = height
+        if self._shape == Led.circle:
+            QPushButton.setFixedSize(self, height, height)
+        else:
+            QPushButton.setFixedSize(self, width, height)
+
+    # ============================================================== Properties
+    # 一级故障
+    @property
+    def _first_color(self):
+        return self.__first_color
+
+    @_first_color.setter
+    def _first_color(self, color):
+        self.__first_color = color
+        self._update_first_qss()
+
+    @_first_color.deleter
+    def _first_color(self):
+        del self.__first_color
+
+    # 二级故障
+    @property
+    def _second_color(self):
+        return self.__second_color
+
+    @_second_color.setter
+    def _second_color(self, color):
+        self.__second_color = color
+        self._update_second_qss()
+
+    @_second_color.deleter
+    def _second_color(self):
+        del self.__second_color
+
+    # 三级故障
+    @property
+    def _third_color(self):
+        return self.__third_color
+
+    @_third_color.setter
+    def _third_color(self, color):
+        self.__third_color = color
+        self._update_third_qss()
+
+    @_third_color.deleter
+    def _third_color(self):
+        del self.__third_color
+
+    # 无故障
+    @property
+    def _off_color(self):
+        return self.__off_color
+
+    @_off_color.setter
+    def _off_color(self, color):
+        self.__off_color = color
+        self._update_off_qss()
+
+    @_off_color.deleter
+    def _off_color(self):
+        del self.__off_color
+
+    @property
+    def _shape(self):
+        return self.__shape
+
+    @_shape.setter
+    def _shape(self, shape):
+        self.__shape = shape
+        self._update_end_radius()
+        self._update_first_qss()
+        self._update_second_qss()
+        self._update_third_qss()
+        self._update_off_qss()
+        self.set_status(self._status)
+
+    @_shape.deleter
+    def _shape(self):
+        del self.__shape
+
+    @property
+    def _height(self):
+        return self.__height
+
+    @_height.setter
+    def _height(self, height):
+        self.__height = height
+        self._update_end_radius()
+        self._update_first_qss()
+        self._update_second_qss()
+        self._update_third_qss()
+        self._update_off_qss()
+        self.set_status(self._status)
+
+    @_height.deleter
+    def _height(self):
+        del self.__height
+
+    # ================================================================= Methods
+    def _update_first_qss(self):
+        color, grad = self._get_gradient(self.__first_color)
+        self._first_qss = self._qss.format(self._end_radius, grad, color, color)
+
+    def _update_second_qss(self):
+        color, grad = self._get_gradient(self.__second_color)
+        self._second_qss = self._qss.format(self._end_radius, grad, color, color)
+
+    def _update_third_qss(self):
+        color, grad = self._get_gradient(self.__third_color)
+        self._third_qss = self._qss.format(self._end_radius, grad, color, color)
+
+    def _update_off_qss(self):
+        color, grad = self._get_gradient(self.__off_color)
+        self._off_qss = self._qss.format(self._end_radius, grad, color, color)
+
+    def _get_gradient(self, color):
+        grad = QColor(int((self.white.red() - color.red()) / 2) + color.red(), int((self.white.green() - color.green()) / 2) + color.green(), int((self.white.blue() - color.blue()) / 2) + color.blue())
+        grad = '{:02X}{:02X}{:02X}'.format(grad.red(), grad.green(), grad.blue())
+        color = '{:02X}{:02X}{:02X}'.format(color.red(), color.green(), color.blue())
+        return color, grad
+
+    def _update_end_radius(self):
+        if self.__shape == Led.rectangle:
+            self._end_radius = int(self.__height / 10)
+        else:
+            self._end_radius = int(self.__height / 2)
+
+    def _toggle_first(self):
+        self.setStyleSheet(self._first_qss)
+
+    def _toggle_second(self):
+        self.setStyleSheet(self._second_qss)
+
+    def _toggle_third(self):
+        self.setStyleSheet(self._third_qss)
+
+    def _toggle_off(self):
+        self.setStyleSheet(self._off_qss)
+
+    def set_first_color(self, color):
+        self._first_color = color
+
+    def set_second_color(self, color):
+        self._second_color = color
+
+    def set_third_color(self, color):
+        self._third_color = color
+
+    def set_off_color(self, color):
+        self._off_color = color
+
+    def set_shape(self, shape):
+        self._shape = shape
+
+    def set_status(self, status):
+        self._status = status
+        if self._status == 0:
+            self._toggle_off()
+        elif self._status == 1:
+            self._toggle_first()
+        elif self._status == 2:
+            self._toggle_second()
+        else:
+            self._toggle_third()

+ 87 - 0
ui/own/palette_theme.py

@@ -0,0 +1,87 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+'''
+@文件    :palette_theme.py
+@时间    :2022/01/15 19:42:22
+@作者    :None
+@版本    :1.0
+@说明    :主题
+'''
+
+import os
+from utils.globalvar import SD
+from utils.resource import resource_path
+from utils.qt import QPalette, QFont, QBrush, QPixmap, QColor, QIcon
+
+THEME = 0
+
+
+def set_my_palette(self):
+
+    icon = QIcon()
+    icon.addPixmap(QPixmap(resource_path(os.path.join("assets", "ic_launch.png"))), QIcon.Normal, QIcon.Off)
+    self.setWindowIcon(icon)
+
+    self.setAutoFillBackground(True)
+    palette = QPalette()
+
+    if THEME:
+        # 背景图片
+        palette.setBrush(QPalette.Window, QBrush(QPixmap(resource_path(os.path.join("assets", "background.jpeg")))))
+        # 背景字体
+        palette.setColor(QPalette.WindowText, QColor(255, 255, 255))
+        palette.setColor(QPalette.ButtonText, QColor(255, 255, 255))
+        palette.setColor(QPalette.Button, QColor("#1A1A1A"))
+        palette.setColor(QPalette.Base, QColor("#1A1A1A"))
+        palette.setColor(QPalette.Text, QColor(255, 255, 255))
+        palette.setColor(QPalette.ToolTipBase, QColor(255, 255, 255))
+
+    self.setPalette(palette)
+    # 定义字体样式
+    font = QFont('Microsoft YaHei')
+    if SD.HIGH_DPI == 1:
+        font.setPixelSize(17)
+    self.setFont(font)
+
+
+def set_sure_background(self):
+    palette = self.palette()
+    palette.setColor(QPalette.Button, QColor("#99CC99"))
+    self.setPalette(palette)
+    self.setAutoFillBackground(True)
+    self.setFlat(True)
+
+
+def set_edt_bg(self, color):
+    palette = self.palette()
+    palette.setColor(QPalette.Base, color)
+    self.setPalette(palette)
+    self.setAutoFillBackground(True)
+
+
+def set_edt_bg_white(self):
+    palette = self.palette()
+    palette.setColor(QPalette.Base, QColor("#FFFFFF"))
+    self.setPalette(palette)
+    self.setAutoFillBackground(True)
+
+
+def set_tab_bg(self):
+    palette = QPalette()
+    palette.setColor(QPalette.Active, QPalette.Button, QColor(0, 0, 255))
+    self.setPalette(palette)
+    self.setAutoFillBackground(True)
+
+
+def set_table_header_bg(self):
+    palette = QPalette()
+    palette.setColor(QPalette.Button, QColor("#FFA500"))
+    self.setPalette(palette)
+    self.setAutoFillBackground(True)
+
+
+def set_table_line_bg(self):
+    palette = QPalette()
+    palette.setColor(QPalette.Active, QPalette.ColorRole, QColor("#FFA500"))
+    self.setPalette(palette)
+    self.setAutoFillBackground(True)

+ 90 - 0
ui/own/switch_button.py

@@ -0,0 +1,90 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+'''
+@文件    :switch_button.py
+@时间    :2022/01/22 10:05:50
+@作者    :None
+@版本    :1.0
+@说明    :连接开关
+'''
+
+
+from utils.qt import QWidget, Signal, Qt, QRect, QPainter, QFont, QColor, QBrush, QPen
+
+
+class SwitchButton(QWidget):
+
+    """自定义Switch按钮"""
+    # 信号
+    checkedChanged = Signal(bool)
+
+    def __init__(self, parent=None):
+        super(SwitchButton, self).__init__(parent)
+
+        # 设置无边框和背景透明
+        self.setWindowFlags(self.windowFlags() | Qt.FramelessWindowHint)
+        self.setAttribute(Qt.WA_TranslucentBackground)
+
+        self.resize(70, 30)
+        self.state = False  # 按钮状态:True表示开,False表示关
+
+    def mousePressEvent(self, event):
+        """鼠标点击事件:用于切换按钮状态"""
+        super(SwitchButton, self).mousePressEvent(event)
+
+        # self.state = False if self.state else True
+        self.state = not self.state
+        # 发射信号
+        self.checkedChanged.emit(self.state)
+
+        self.update()
+
+    def paintEvent(self, event):
+        """绘制按钮"""
+        super(SwitchButton, self).paintEvent(event)
+
+        # 创建绘制器并设置抗锯齿和图片流畅转换
+        painter = QPainter(self)
+        painter.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform)
+
+        # 定义字体样式
+        font = QFont('Microsoft YaHei')
+        font.setPixelSize(14)
+        painter.setFont(font)
+
+        # 开关为开的状态
+        if self.state:
+            # 绘制背景
+            painter.setPen(Qt.NoPen)
+            # brush = QBrush(QColor('#969696'))
+            brush = QBrush(QColor('#006400'))
+            painter.setBrush(brush)
+            painter.drawRoundedRect(0, 0, self.width(), self.height(), self.height() // 2, self.height() // 2)
+
+            # 绘制圆圈
+            painter.setPen(Qt.NoPen)
+            brush.setColor(QColor('#ffffff'))
+            painter.setBrush(brush)
+            painter.drawRoundedRect(43, 3, 24, 24, 12, 12)
+
+            # 绘制文本
+            painter.setPen(QPen(QColor('#ffffff')))
+            painter.setBrush(Qt.NoBrush)
+            painter.drawText(QRect(18, 4, 50, 20), Qt.AlignLeft, '开')
+        # 开关为关的状态
+        else:
+            # 绘制背景
+            painter.setPen(Qt.NoPen)
+            brush = QBrush(QColor('#FFFFFF'))
+            painter.setBrush(brush)
+            painter.drawRoundedRect(0, 0, self.width(), self.height(), self.height() // 2, self.height() // 2)
+
+            # 绘制圆圈
+            pen = QPen(QColor('#999999'))
+            pen.setWidth(1)
+            painter.setPen(pen)
+            painter.drawRoundedRect(3, 3, 24, 24, 12, 12)
+
+            # 绘制文本
+            painter.setBrush(Qt.NoBrush)
+            painter.drawText(QRect(38, 4, 50, 20), Qt.AlignLeft, '关')

+ 57 - 0
ui/statusbar.py

@@ -0,0 +1,57 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+'''
+@文件    :statusbar.py
+@时间    :2022/01/22 09:55:34
+@作者    :None
+@版本    :1.0
+@说明    :状态栏页面
+'''
+
+
+from utils.qt import QStatusBar, QLabel, QFrame, QMetaObject, QCoreApplication
+
+
+class UiStatusBar(object):
+    def setupUi(self, pcs_status):
+        self.statusbar = QStatusBar()
+        # pcs连接状态
+        self.label_pcs_connect = QLabel()
+        self.label_pcs_connect.setFrameStyle(QFrame.Panel | QFrame.Raised)
+        self.label_pcs_connect.setLineWidth(3)
+        self.label_pcs_connect.setMidLineWidth(3)
+
+        # pcs发送状态
+        self.label_pcs_send = QLabel()
+        self.label_pcs_send.setFrameStyle(QFrame.Panel | QFrame.Raised)
+        self.label_pcs_send.setLineWidth(3)
+        self.label_pcs_send.setMidLineWidth(3)
+
+        # pcs发送状态
+        self.label_pcs_received = QLabel()
+        self.label_pcs_received.setFrameStyle(QFrame.Panel | QFrame.Raised)
+        self.label_pcs_received.setLineWidth(3)
+        self.label_pcs_received.setMidLineWidth(3)
+
+        # pcs软件版本号
+        self.label_pcs_version = QLabel()
+        self.label_pcs_version.setFrameStyle(QFrame.Panel | QFrame.Raised)
+        self.label_pcs_version.setLineWidth(3)
+        self.label_pcs_version.setMidLineWidth(3)
+
+        # 往状态栏中添加组件(stretch应该是拉伸组件宽度)
+        self.statusbar.addWidget(self.label_pcs_connect, stretch=0)
+        self.statusbar.addWidget(self.label_pcs_send, stretch=0)
+        self.statusbar.addWidget(self.label_pcs_received, stretch=0)
+        self.statusbar.addPermanentWidget(self.label_pcs_version, stretch=0)
+
+        self.statusbar.setSizeGripEnabled(False)
+
+        self.retranslateUi(pcs_status)
+        QMetaObject.connectSlotsByName(pcs_status)
+
+    def retranslateUi(self, pcs_status):
+        _translate = QCoreApplication.translate
+        self.label_pcs_connect.setText(_translate("pcs_status", "未连接"))
+        self.label_pcs_send.setText(_translate("pcs_status", "发送未启动"))
+        self.label_pcs_received.setText(_translate("pcs_status", "接收未启动"))

+ 0 - 0
utils/__init__.py


+ 209 - 0
utils/can.py

@@ -0,0 +1,209 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+'''
+@文件    :canConnect.py
+@时间    :2021/12/09 13:47:58
+@作者    :None
+@版本    :1.0
+@说明    :CAN连接驱动
+'''
+
+
+from ctypes.util import find_library
+from sys import platform
+import ctypes
+from ctypes import c_ubyte, c_uint
+from utils.log_signal import LogSignal
+from utils.resource import resource_path, resource_path_extend
+
+PYINSTALLER = 1
+if PYINSTALLER:
+    if platform == "win64":
+        _CanDLLName = resource_path("config\\ControlCANx64\\ControlCAN.dll")
+        ZLGCAN = ctypes.windll.LoadLibrary(_CanDLLName)
+    elif platform == "win32":
+        _CanDLLName = resource_path("config\\ControlCANx86\\ControlCAN.dll")
+        ZLGCAN = ctypes.windll.LoadLibrary(_CanDLLName)
+    elif platform == "linux":
+        _CanDLLName = resource_path("config/linux/libusbcan.so")
+        ZLGCAN = ctypes.cdll.LoadLibrary(_CanDLLName)
+    else:
+        ZLGCAN = ctypes.cdll.LoadLibrary(find_library("libPCBUSB.dylib"))
+else:
+    if platform == "win64":
+        resource_path_extend("config")
+        ZLGCAN = ctypes.windll.LoadLibrary("ControlCAN.dll")
+    elif platform == "win32":
+        resource_path_extend("config")
+        ZLGCAN = ctypes.windll.LoadLibrary("ControlCAN.dll")
+    else:
+        ZLGCAN = ""
+
+ubyte_array = c_ubyte * 8
+ubyte_3array = c_ubyte * 3
+
+# can type
+CANTYPE = {
+    'USBCAN-I': 3,
+    'USBCAN-II': 4,
+}
+
+# can mode
+NORMAL_MODE = 0
+LISTEN_MODE = 1
+
+# filter type
+SINGLE_FILTER = 0
+DOUBLE_FILTER = 1
+
+# status
+STATUS_OK = 1
+
+# sendtype
+SEND_NORMAL = 0
+SEND_SINGLE = 1
+SELF_SEND_RECV = 2
+SELF_SEND_RECV_SINGLE = 3
+
+
+class VCI_INIT_CONFIG(ctypes.Structure):
+    _fields_ = [("AccCode", c_uint),  # 验收码。SJA1000的帧过滤验收码。对经过屏蔽码过滤为“有关位”进行匹配,全部匹配成功后,此帧可以被接收。
+                # 屏蔽码。SJA1000的帧过滤屏蔽码。对接收的CAN帧ID进行过滤,对应位为0的是“有关位”,对应位为1的是“无关位”。屏蔽码推荐设置为0xFFFFFFFF,即全部接收。
+                ("AccMask", c_uint),
+                # 保留
+                ("Reserved", c_uint),
+                # 滤波方式
+                ("Filter", c_ubyte),
+                # 波特率定时器 0
+                ("Timing0", c_ubyte),
+                # 波特率定时器 1
+                ("Timing1", c_ubyte),
+                # 模式。=0表示正常模式(相当于正常节点),=1表示只听模式(只接收,不影响总线),=2表示自发自收模式(环回模式)。
+                ("Mode", c_ubyte)
+                ]
+
+
+# VCI_CAN_OBJ结构体是CAN帧结构体,即1个结构体表示一个帧的数据结构。在发送函数VCI_Transmit和接收函数VCI_Receive中,被用来传送CAN信息帧。
+class VCI_CAN_OBJ(ctypes.Structure):
+    _fields_ = [("ID", c_uint),  # 帧ID。32位变量,数据格式为靠右对齐
+                # 设备接收到某一帧的时间标识。时间标示从CAN卡上电开始计时,计时单位为0.1ms。
+                ("TimeStamp", c_uint),
+                # 是否使用时间标识,为1时TimeStamp有效,TimeFlag和TimeStamp只在此帧为接收帧时有意义。
+                ("TimeFlag", c_ubyte),
+                # 发送帧类型。=0时为正常发送(发送失败会自动重发,重发时间为4秒,4秒内没有发出则取消);=1时为单次发送(只发送一次,发送失败不会自动重发,总线只产生一帧数据);其它值无效。
+                ("SendType", c_ubyte),
+                # 是否是远程帧。=0时为为数据帧,=1时为远程帧(数据段空)。
+                ("RemoteFlag", c_ubyte),
+                # 是否是扩展帧。=0时为标准帧(11位ID),=1时为扩展帧(29位ID)。
+                ("ExternFlag", c_ubyte),
+                # 数据长度 DLC (<=8),即CAN帧Data有几个字节。约束了后面Data[8]中的有效字节
+                ("DataLen", c_ubyte),
+                ("Data", c_ubyte * 8),
+                # CAN帧的数据。由于CAN规定了最大是8个字节,所以这里预留了8个字节的空间,受DataLen约束。如DataLen定义为3,即Data[0]、Data[1]、Data[2]是有效的
+                ("Reserved", c_ubyte * 3)  # 系统保留
+                ]
+
+
+class PVCI_ERR_INFO(ctypes.Structure):
+    _fields_ = [("ErrorCode", c_uint),
+                ("PassiveErrData", c_ubyte * 3),
+                ("ArLostErrData", c_ubyte)
+                ]
+
+
+baudRateConfig = {
+    '5Kbps': {'time0': 0xBF, 'time1': 0xFF},
+    '10Kbps': {'time0': 0x31, 'time1': 0x1C},
+    '20Kbps': {'time0': 0x18, 'time1': 0x1C},
+    '40Kbps': {'time0': 0x87, 'time1': 0xFF},
+    '50Kbps': {'time0': 0x09, 'time1': 0x1C},
+    '80Kbps': {'time0': 0x83, 'time1': 0xFF},
+    '100Kbps': {'time0': 0x04, 'time1': 0x1C},
+    '125Kbps': {'time0': 0x03, 'time1': 0x1C},
+    '200Kbps': {'time0': 0x81, 'time1': 0xFA},
+    '250Kbps': {'time0': 0x01, 'time1': 0x1C},
+    '400Kbps': {'time0': 0x80, 'time1': 0xFA},
+    '500Kbps': {'time0': 0x00, 'time1': 0x1C},
+    '666Kbps': {'time0': 0x80, 'time1': 0xB6},
+    '800Kbps': {'time0': 0x00, 'time1': 0x16},
+    '1000Kbps': {'time0': 0x00, 'time1': 0x14},
+}
+
+
+class MessageDeal:
+    def __init__(self):
+        self.canType = CANTYPE['USBCAN-II']
+
+    def set_can_board(self, canIndex, canChannel, canBaudrate):
+        self.canIndex = canIndex
+        self.canChannel = canChannel
+        self.canBaudrate = canBaudrate
+
+    def open_device(self):
+        ret = ZLGCAN.VCI_OpenDevice(self.canType, self.canChannel, self.canChannel)
+        if ret != STATUS_OK:
+            # LogSignal.print_log_signal().log_emit('打开CAN卡失败: {}'.format(str(self.canChannel)) if SD.SYSTEM_LANGUAGE == 0 else 'CAN Device Error: {}'.format(str(self.canChannel)))
+            return False
+        return True
+
+    def init_can(self, accCode, accMask):
+        # 初始化通道
+        _vci_initconfig = VCI_INIT_CONFIG(accCode, accMask, 0, DOUBLE_FILTER,
+                                          baudRateConfig[self.canBaudrate]['time0'],
+                                          baudRateConfig[self.canBaudrate]['time1'],
+                                          NORMAL_MODE)
+        ret = ZLGCAN.VCI_InitCAN(self.canType, self.canIndex, self.canChannel, ctypes.byref(_vci_initconfig))
+        if ret != STATUS_OK:
+            # LogSignal.print_log_signal().log_emit('初始化CAN卡失败: {}'.format(str(self.canChannel)) if SD.SYSTEM_LANGUAGE == 0 else 'CAN Open Error: {}'.format(str(self.canChannel)))
+            return False
+
+        ret = ZLGCAN.VCI_StartCAN(self.canType, self.canIndex, self.canChannel)
+        if ret != STATUS_OK:
+            # LogSignal.print_log_signal().log_emit('启动CAN卡失败: {}'.format(str(self.canChannel))if SD.SYSTEM_LANGUAGE == 0 else 'CAN Start Error: {}'.format(str(self.canChannel)))
+            return False
+        return True
+
+    def get_undeal_number(self):
+        return ZLGCAN.VCI_GetReceiveNum(self.canType, self.canIndex, self.canChannel)
+
+    def receive(self, number=1):
+        objs = (VCI_CAN_OBJ * number)()
+        ret = ZLGCAN.VCI_Receive(self.canType, self.canIndex, self.canChannel, ctypes.byref(objs), number, 10)
+        if ret == 0xFFFFFFFF:
+            return None
+        else:
+            return objs[:ret]
+
+    def send(self, ID, data, remote_flag=False, extern_flag=False, data_len=8):
+        vci_can_obj = VCI_CAN_OBJ()
+        vci_can_obj.ID = ID
+        vci_can_obj.SendType = SEND_NORMAL
+        if remote_flag:
+            vci_can_obj.RemoteFlag = 1
+        else:
+            vci_can_obj.RemoteFlag = 0
+        if extern_flag:
+            vci_can_obj.ExternFlag = 1
+        else:
+            vci_can_obj.ExternFlag = 0
+        vci_can_obj.DataLen = data_len
+        if len(data) < 8:
+            data += (8 - len(data)) * [0]
+        vci_can_obj.Data = (c_ubyte * 8)(data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7])
+        ret = ZLGCAN.VCI_Transmit(self.canType, self.canIndex, self.canChannel, ctypes.byref(vci_can_obj), 1)
+        if ret != STATUS_OK:
+            LogSignal.print_log_signal().log_emit("CAN Send Error!")
+            return False
+        else:
+            return True
+
+    def read_err_info(self):
+        errInfo = PVCI_ERR_INFO(0, ubyte_3array(0, 0, 0), 0)
+        ZLGCAN.VCI_ReadErrInfo(self.canType, self.canIndex, self.canChannel, ctypes.byref(errInfo))
+        LogSignal.print_log_signal().log_emit(errInfo.ErrorCode, errInfo.PassiveErrData[0], errInfo.PassiveErrData[1], errInfo.PassiveErrData[2], errInfo.ArLostErrData)
+
+    def clear_buffer(self):
+        return ZLGCAN.VCI_ClearBuffer(self.canType, self.canIndex, self.canChannel)
+
+    def close_can(self):
+        return ZLGCAN.VCI_CloseDevice(self.canType, self.canIndex)

+ 41 - 0
utils/com.py

@@ -0,0 +1,41 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+'''
+@文件    :com.py
+@时间    :2022/11/14 17:28:23
+@作者    :None
+@版本    :1.0
+@说明    :modbus 485驱动
+'''
+
+import serial.tools.list_ports
+import serial
+from serial.serialutil import SerialException
+from utils.modbus import modbus_rtu
+
+
+class ComMaster:
+    def __init__(self):
+        super(ComMaster, self).__init__()
+        self.master = None
+
+    def set_com(self, port, baudrate, bytesize, parity, stopbits):
+        self.port = port
+        self.baudrate = baudrate
+        self.bytesize = bytesize
+        self.parity = parity
+        self.stopbits = stopbits
+
+    def open_device(self):
+        try:
+            self.master = modbus_rtu.RtuMaster(serial.Serial(port=self.port, baudrate=self.baudrate, bytesize=self.bytesize, parity=self.parity, stopbits=self.stopbits, xonxoff=0))
+            self.master.set_timeout(1)
+            self.master.set_verbose(True)
+            return True
+        except SerialException:
+            return False
+
+    def send(self, slave, function_code, starting_address, byte_count):
+        ret = self.master.execute(slave, function_code, starting_address, byte_count)
+        print(list(ret))
+        return ret

+ 32 - 0
utils/delay.py

@@ -0,0 +1,32 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+'''
+@文件    :delay.py
+@时间    :2022/02/17 15:37:43
+@作者    :None
+@版本    :1.0
+@说明    :精确到ms延迟函数
+'''
+
+
+import sys
+import ctypes
+from time import sleep
+
+
+def m_delay(ms):
+    if sys.platform == "darwin" or sys.platform == "linux":
+        sleep(ms / 1000)
+    elif sys.platform == "win32" or sys.platform == "win64":
+        stop_value = ctypes.c_longlong(0)
+        start_value = ctypes.c_longlong(0)
+        freq = ctypes.c_longlong(0)
+        n = 0
+        ctypes.windll.kernel32.QueryPerformanceFrequency(ctypes.byref(freq))
+        count = ms * freq.value / 1000
+        ctypes.windll.kernel32.QueryPerformanceCounter(ctypes.byref(start_value))
+        while n < count:
+            ctypes.windll.kernel32.QueryPerformanceCounter(ctypes.byref(stop_value))
+            n = stop_value.value - start_value.value
+    else:
+        sleep(ms / 1000)

+ 25 - 0
utils/globalvar.py

@@ -0,0 +1,25 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+'''
+@文件    :globalvar.py
+@时间    :2022/01/22 10:11:23
+@作者    :None
+@版本    :1.0
+@说明    :全局变量
+'''
+
+from utils.can import MessageDeal
+from utils.com import ComMaster
+
+
+class SD:
+    # 一二级BMS Can开关,Can开关开启后开始接收数据
+    CAN_ON_OFF = False
+
+    # 加载CAN连接驱动
+    CAN_CONTROL = MessageDeal()
+
+    # 加载串口连接驱动
+    COM_CONTROL = ComMaster()
+
+    HIGH_DPI = 0

+ 43 - 0
utils/hex_bit.py

@@ -0,0 +1,43 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+'''
+@文件    :hexBit.py
+@时间    :2021/12/09 13:48:58
+@作者    :None
+@版本    :1.0
+@说明    :获取位的值
+'''
+
+
+def a_bit(data, i):
+    if i == 1:
+        return data & 0x01
+    if i == 2:
+        return (data & 0x02) >> 1
+    if i == 3:
+        return (data & 0x04) >> 2
+    if i == 4:
+        return (data & 0x08) >> 3
+    if i == 5:
+        return (data & 0x10) >> 4
+    if i == 6:
+        return (data & 0x20) >> 5
+    if i == 7:
+        return (data & 0x40) >> 6
+    if i == 8:
+        return (data & 0x80) >> 7
+
+
+def hex_bcd(data):
+    s = data // 16
+    g = data % 16
+    c = s * 10 + g
+    return str(c)
+
+
+def bcd_hex(data):
+    int_data = int(data)
+    s = int_data // 10
+    g = int_data % 10
+    c = s * 16 + g
+    return c

+ 28 - 0
utils/log_signal.py

@@ -0,0 +1,28 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+'''
+@文件    :log_signal.py
+@时间    :2022/02/17 15:37:11
+@作者    :None
+@版本    :1.0
+@说明    : 日志打印实例/单例模式
+'''
+
+from utils.qt import Signal, QObject
+
+
+class LogSignal(QObject):
+    instance = None
+    signal = Signal(str)
+
+    @classmethod
+    def print_log_signal(cls):
+        if cls.instance:
+            return cls.instance
+        else:
+            obj = cls()
+            cls.instance = obj
+            return cls.instance
+
+    def log_emit(self, data):
+        self.signal.emit(data)

+ 6 - 0
utils/modbus/__init__.py

@@ -0,0 +1,6 @@
+import logging
+
+LOGGER = logging.getLogger("modbus")
+
+
+# VERSION = '1.1.2'

+ 34 - 0
utils/modbus/defines.py

@@ -0,0 +1,34 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+
+# modbus exception codes
+ILLEGAL_FUNCTION = 1
+ILLEGAL_DATA_ADDRESS = 2
+ILLEGAL_DATA_VALUE = 3
+SLAVE_DEVICE_FAILURE = 4
+COMMAND_ACKNOWLEDGE = 5
+SLAVE_DEVICE_BUSY = 6
+MEMORY_PARITY_ERROR = 8
+
+# supported modbus functions
+RAW = 0
+READ_COILS = 1
+READ_DISCRETE_INPUTS = 2
+READ_HOLDING_REGISTERS = 3
+READ_INPUT_REGISTERS = 4
+WRITE_SINGLE_COIL = 5
+WRITE_SINGLE_REGISTER = 6
+READ_EXCEPTION_STATUS = 7
+DIAGNOSTIC = 8
+REPORT_SLAVE_ID = 17
+WRITE_MULTIPLE_COILS = 15
+WRITE_MULTIPLE_REGISTERS = 16
+READ_FILE_RECORD = 20
+READ_WRITE_MULTIPLE_REGISTERS = 23
+DEVICE_INFO = 43
+
+# supported block types
+COILS = 1
+DISCRETE_INPUTS = 2
+HOLDING_REGISTERS = 3
+ANALOG_INPUTS = 4

+ 80 - 0
utils/modbus/exceptions.py

@@ -0,0 +1,80 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+
+class ModbusError(Exception):
+    """Exception raised when the modbus slave returns an error"""
+
+    def __init__(self, exception_code, value=""):
+        """constructor: set the exception code returned by the slave"""
+        if not value:
+            value = "Modbus Error: Exception code = %d" % (exception_code)
+        Exception.__init__(self, value)
+        self._exception_code = exception_code
+
+    def get_exception_code(self):
+        """return the exception code returned by the slave (see defines)"""
+        return self._exception_code
+
+
+class ModbusFunctionNotSupportedError(Exception):
+    """
+    Exception raised when calling a modbus function not supported by modbus_tk
+    """
+    pass
+
+
+class DuplicatedKeyError(Exception):
+    """
+    Exception raised when trying to add an object with a key that is already
+    used for another object
+    """
+    pass
+
+
+class MissingKeyError(Exception):
+    """
+    Exception raised when trying to get an object with a key that doesn't exist
+    """
+    pass
+
+
+class InvalidModbusBlockError(Exception):
+    """Exception raised when a modbus block is not valid"""
+    pass
+
+
+class InvalidArgumentError(Exception):
+    """
+    Exception raised when one argument of a function doesn't meet
+    what is expected
+    """
+    pass
+
+
+class OverlapModbusBlockError(Exception):
+    """
+    Exception raised when adding modbus block on a memory address
+    range already in use
+    """
+    pass
+
+
+class OutOfModbusBlockError(Exception):
+    """Exception raised when accessing out of a modbus block"""
+    pass
+
+
+class ModbusInvalidResponseError(Exception):
+    """
+    Exception raised when the response sent by the slave doesn't fit
+    with the expected format
+    """
+    pass
+
+
+class ModbusInvalidRequestError(Exception):
+    """
+    Exception raised when the request by the master doesn't fit
+    with the expected format
+    """
+    pass

+ 101 - 0
utils/modbus/hooks.py

@@ -0,0 +1,101 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+
+
+from __future__ import with_statement
+import threading
+
+_LOCK = threading.RLock()
+_HOOKS = {}
+
+
+def install_hook(name, fct):
+    """
+    Install one of the following hook
+
+    modbus_rtu.RtuMaster.before_open((master,))
+    modbus_rtu.RtuMaster.after_close((master,)
+    modbus_rtu.RtuMaster.before_send((master, request)) returns modified request or None
+    modbus_rtu.RtuMaster.after_recv((master, response)) returns modified response or None
+
+    modbus_rtu.RtuServer.before_close((server, ))
+    modbus_rtu.RtuServer.after_close((server, ))
+    modbus_rtu.RtuServer.before_open((server, ))
+    modbus_rtu.RtuServer.after_open(((server, ))
+    modbus_rtu.RtuServer.after_read((server, request)) returns modified request or None
+    modbus_rtu.RtuServer.before_write((server, response))  returns modified response or None
+    modbus_rtu.RtuServer.after_write((server, response))
+    modbus_rtu.RtuServer.on_error((server, excpt))
+
+    modbus_tcp.TcpMaster.before_connect((master, ))
+    modbus_tcp.TcpMaster.after_connect((master, ))
+    modbus_tcp.TcpMaster.before_close((master, ))
+    modbus_tcp.TcpMaster.after_close((master, ))
+    modbus_tcp.TcpMaster.before_send((master, request))
+    modbus_tcp.TcpServer.after_send((master, request))
+    modbus_tcp.TcpMaster.after_recv((master, response))
+
+
+    modbus_tcp.TcpServer.on_connect((server, client, address))
+    modbus_tcp.TcpServer.on_disconnect((server, sock))
+    modbus_tcp.TcpServer.after_recv((server, sock, request)) returns modified request or None
+    modbus_tcp.TcpServer.before_send((server, sock, response)) returns modified response or None
+    modbus_tcp.TcpServer.on_error((server, sock, excpt))
+
+    modbus_rtu_over_tcp.RtuOverTcpMaster.after_recv((master, response))
+
+    modbus.Master.before_send((master, request)) returns modified request or None
+    modbus.Master.after_send((master))
+    modbus.Master.after_recv((master, response)) returns modified response or None
+
+    modbus.Slave.handle_request((slave, request_pdu)) returns modified response or None
+    modbus.Slave.handle_write_multiple_coils_request((slave, request_pdu))
+    modbus.Slave.handle_write_multiple_registers_request((slave, request_pdu)) returns modified response or None
+    modbus.Slave.handle_write_single_register_request((slave, request_pdu)) returns modified response or None
+    modbus.Slave.handle_write_single_coil_request((slave, request_pdu)) returns modified response or None
+    modbus.Slave.handle_read_input_registers_request((slave, request_pdu)) returns modified response or None
+    modbus.Slave.handle_read_holding_registers_request((slave, request_pdu)) returns modified response or None
+    modbus.Slave.handle_read_discrete_inputs_request((slave, request_pdu)) returns modified response or None
+    modbus.Slave.handle_read_coils_request((slave, request_pdu)) returns modified response or None
+    modbus.Slave.handle_read_write_multiple_registers_request((slave, request_pdu)) returns modified response or None
+    modbus.Slave.handle_read_exception_status_request((slave, request_pdu)) returns modified response or None
+
+    modbus.Slave.on_handle_broadcast((slave, response_pdu)) returns modified response or None
+    modbus.Slave.on_exception((slave, function_code, excpt))
+
+
+    modbus.Databank.on_error((db, excpt, request_pdu))
+
+    modbus.ModbusBlock.setitem((self, slice, value))
+
+    modbus.Server.before_handle_request((server, request)) returns modified request or None
+    modbus.Server.after_handle_request((server, response)) returns modified response or None
+    modbus.Server.on_exception((server, excpt))
+    """
+    with _LOCK:
+        try:
+            _HOOKS[name].append(fct)
+        except KeyError:
+            _HOOKS[name] = [fct]
+
+
+def uninstall_hook(name, fct=None):
+    """remove the function from the hooks"""
+    with _LOCK:
+        if fct:
+            _HOOKS[name].remove(fct)
+        else:
+            del _HOOKS[name][:]
+
+
+def call_hooks(name, args):
+    """call the function associated with the hook and pass the given args"""
+    with _LOCK:
+        try:
+            for fct in _HOOKS[name]:
+                retval = fct(args)
+                if retval is not None:
+                    return retval
+        except KeyError:
+            pass
+        return None

+ 987 - 0
utils/modbus/modbus.py

@@ -0,0 +1,987 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+
+from __future__ import with_statement
+
+import struct
+import threading
+import re
+
+from utils.modbus import LOGGER
+from utils.modbus import defines
+from utils.modbus.exceptions import (
+    ModbusError, ModbusFunctionNotSupportedError, DuplicatedKeyError, MissingKeyError, InvalidModbusBlockError,
+    InvalidArgumentError, OverlapModbusBlockError, OutOfModbusBlockError, ModbusInvalidResponseError,
+    ModbusInvalidRequestError
+)
+from utils.modbus.hooks import call_hooks
+from utils.modbus.utils import threadsafe_function, get_log_buffer
+
+
+class Query(object):
+    def __init__(self):
+        pass
+
+    def build_request(self, pdu, slave):
+        raise NotImplementedError()
+
+    def parse_response(self, response):
+        raise NotImplementedError()
+
+    def parse_request(self, request):
+        raise NotImplementedError()
+
+    def build_response(self, response_pdu):
+        raise NotImplementedError()
+
+
+class Master(object):
+    def __init__(self, timeout_in_sec, hooks=None):
+        self._timeout = timeout_in_sec
+        self._verbose = False
+        self._is_opened = False
+
+    def __del__(self):
+        self.close()
+
+    def set_verbose(self, verbose):
+        self._verbose = verbose
+
+    def open(self):
+        if not self._is_opened:
+            self._do_open()
+            self._is_opened = True
+
+    def close(self):
+        if self._is_opened:
+            ret = self._do_close()
+            if ret:
+                self._is_opened = False
+
+    def _do_open(self):
+        raise NotImplementedError()
+
+    def _do_close(self):
+        raise NotImplementedError()
+
+    def _send(self, buf):
+        raise NotImplementedError()
+
+    def _recv(self, expected_length):
+        raise NotImplementedError()
+
+    def _make_query(self):
+        raise NotImplementedError()
+
+    @threadsafe_function
+    def execute(
+        self, slave, function_code, starting_address, quantity_of_x=0, output_value=0, data_format="",
+        expected_length=-1, write_starting_address_fc23=0, number_file=None, pdu=""
+    ):
+        """
+        Execute a modbus query and returns the data part of the answer as a tuple
+        The returned tuple depends on the query function code. see modbus protocol
+        specification for details
+        data_format makes possible to extract the data like defined in the
+        struct python module documentation
+        For function Read_File_Record
+        starting_address, quantity_of_x, number_file must be tuple ()
+        of one long (by the number of requested sub_seq)
+        the result will be
+        ((sub _ seq_0 _ data), (sub_seq_1_data),... (sub_seq_N_data)).
+        """
+
+        is_read_function = False
+        nb_of_digits = 0
+        if number_file is None:
+            number_file = tuple()
+
+        # open the connection if it is not already done
+        self.open()
+
+        # Build the modbus pdu and the format of the expected data.
+        # It depends of function code. see modbus specifications for details.
+        if function_code == defines.READ_COILS or function_code == defines.READ_DISCRETE_INPUTS:
+            is_read_function = True
+            pdu = struct.pack(">BHH", function_code, starting_address, quantity_of_x)
+            byte_count = quantity_of_x // 8
+            if (quantity_of_x % 8) > 0:
+                byte_count += 1
+            nb_of_digits = quantity_of_x
+            if not data_format:
+                data_format = ">" + (byte_count * "B")
+            if expected_length < 0:
+                # No length was specified and calculated length can be used:
+                # slave + func + bytcodeLen + bytecode + crc1 + crc2
+                expected_length = byte_count + 5
+
+        elif function_code == defines.READ_INPUT_REGISTERS or function_code == defines.READ_HOLDING_REGISTERS:
+            is_read_function = True
+            pdu = struct.pack(">BHH", function_code, starting_address, quantity_of_x)
+            if not data_format:
+                data_format = ">" + (quantity_of_x * "H")
+            if expected_length < 0:
+                # No length was specified and calculated length can be used:
+                # slave + func + bytcodeLen + bytecode x 2 + crc1 + crc2
+                expected_length = 2 * quantity_of_x + 5
+
+        elif function_code == defines.READ_FILE_RECORD:
+            is_read_function = True
+            if (
+                isinstance(number_file, tuple)
+                and isinstance(starting_address, tuple)
+                and isinstance(quantity_of_x, tuple)
+                and len(number_file) == len(starting_address) == len(quantity_of_x) > 0
+            ):
+                count_seq = len(number_file)
+            else:
+                raise ModbusInvalidRequestError(
+                    'For function READ_FILE_RECORD param'
+                    'starting_address, quantity_of_x, number_file must be tuple()'
+                    'of one length > 0 (by the number of requested sub_seq)'
+                )
+            pdu = struct.pack(">BB", function_code, count_seq * 7) + b''.join(map(lambda zip_param: struct.pack(">BHHH", *zip_param), zip(count_seq * (6, ), number_file, starting_address, quantity_of_x)))
+            if not data_format:
+                data_format = ">BB" + 'BB'.join(map(lambda x: x * 'H', quantity_of_x))
+            if expected_length < 0:
+                # No length was specified and calculated length can be used:
+                # slave + func + bytcodeLen + (byteLenSubReq+byteref+bytecode[] x 2)*countSubReq + crc1 + crc2
+                expected_length = 2 * sum(quantity_of_x) + 2 * count_seq + 5
+
+        elif (function_code == defines.WRITE_SINGLE_COIL) or (function_code == defines.WRITE_SINGLE_REGISTER):
+            if function_code == defines.WRITE_SINGLE_COIL:
+                if output_value != 0:
+                    output_value = 0xff00
+                fmt = ">BHH"
+            else:
+                fmt = ">BH" + ("H" if output_value >= 0 else "h")
+            pdu = struct.pack(fmt, function_code, starting_address, output_value)
+            if not data_format:
+                data_format = ">HH"
+            if expected_length < 0:
+                # No length was specified and calculated length can be used:
+                # slave + func + adress1 + adress2 + value1+value2 + crc1 + crc2
+                expected_length = 8
+
+        elif function_code == defines.WRITE_MULTIPLE_COILS:
+            byte_count = len(output_value) // 8
+            if (len(output_value) % 8) > 0:
+                byte_count += 1
+            pdu = struct.pack(">BHHB", function_code, starting_address, len(output_value), byte_count)
+            i, byte_value = 0, 0
+            for j in output_value:
+                if j > 0:
+                    byte_value += pow(2, i)
+                if i == 7:
+                    pdu += struct.pack(">B", byte_value)
+                    i, byte_value = 0, 0
+                else:
+                    i += 1
+            if i > 0:
+                pdu += struct.pack(">B", byte_value)
+            if not data_format:
+                data_format = ">HH"
+            if expected_length < 0:
+                # No length was specified and calculated length can be used:
+                # slave + func + adress1 + adress2 + outputQuant1 + outputQuant2 + crc1 + crc2
+                expected_length = 8
+
+        elif function_code == defines.WRITE_MULTIPLE_REGISTERS:
+            if output_value and data_format:
+                byte_count = struct.calcsize(data_format)
+            else:
+                byte_count = 2 * len(output_value)
+            pdu = struct.pack(">BHHB", function_code, starting_address, byte_count // 2, byte_count)
+            if output_value and data_format:
+                pdu += struct.pack(data_format, *output_value)
+            else:
+                for j in output_value:
+                    fmt = "H" if j >= 0 else "h"
+                    pdu += struct.pack(">" + fmt, j)
+            # data_format is now used to process response which is always 2 registers:
+            #   1) data address of first register, 2) number of registers written
+            data_format = ">HH"
+            if expected_length < 0:
+                # No length was specified and calculated length can be used:
+                # slave + func + adress1 + adress2 + outputQuant1 + outputQuant2 + crc1 + crc2
+                expected_length = 8
+
+        elif function_code == defines.READ_EXCEPTION_STATUS:
+            pdu = struct.pack(">B", function_code)
+            data_format = ">B"
+            if expected_length < 0:
+                # No length was specified and calculated length can be used:
+                expected_length = 5
+
+        elif function_code == defines.DIAGNOSTIC:
+            # SubFuncCode  are in starting_address
+            pdu = struct.pack(">BH", function_code, starting_address)
+            if len(output_value) > 0:
+                for j in output_value:
+                    # copy data in pdu
+                    pdu += struct.pack(">B", j)
+                if not data_format:
+                    data_format = ">" + (len(output_value) * "B")
+                if expected_length < 0:
+                    # No length was specified and calculated length can be used:
+                    # slave + func + SubFunc1 + SubFunc2 + Data + crc1 + crc2
+                    expected_length = len(output_value) + 6
+
+        elif function_code == defines.READ_WRITE_MULTIPLE_REGISTERS:
+            is_read_function = True
+            byte_count = 2 * len(output_value)
+            pdu = struct.pack(
+                ">BHHHHB",
+                function_code, starting_address, quantity_of_x, write_starting_address_fc23,
+                len(output_value), byte_count
+            )
+            for j in output_value:
+                fmt = "H" if j >= 0 else "h"
+                # copy data in pdu
+                pdu += struct.pack(">" + fmt, j)
+            if not data_format:
+                data_format = ">" + (quantity_of_x * "H")
+            if expected_length < 0:
+                # No length was specified and calculated length can be used:
+                # slave + func + bytcodeLen + bytecode x 2 + crc1 + crc2
+                expected_length = 2 * quantity_of_x + 5
+
+        elif function_code == defines.RAW:
+            # caller has to set arguments "pdu", "expected_length", and "data_format"
+            pass
+
+        elif function_code == defines.DEVICE_INFO:
+            # is_read_function = True
+            mei_type = 0x0E
+            pdu = struct.pack(
+                ">BBBB",
+                # function_code = 43 (0x2B)
+                # MEI Type = 0x0E (Read Device Identification)
+                # output_value[0] = Read Device ID code
+                # output_value[1] = Object Id
+                function_code, mei_type, output_value[0], output_value[1]
+            )
+
+        else:
+            raise ModbusFunctionNotSupportedError("The {0} function code is not supported. ".format(function_code))
+
+        # instantiate a query which implements the MAC (TCP or RTU) part of the protocol
+        query = self._make_query()
+
+        # add the mac part of the protocol to the request
+        request = query.build_request(pdu, slave)
+
+        # send the request to the slave
+        retval = call_hooks("modbus.Master.before_send", (self, request))
+        if retval is not None:
+            request = retval
+        if self._verbose:
+            LOGGER.debug(get_log_buffer("-> ", request))
+        self._send(request)
+
+        call_hooks("modbus.Master.after_send", (self, ))
+
+        if slave != 0:
+            # receive the data from the slave
+            response = self._recv(expected_length)
+            retval = call_hooks("modbus.Master.after_recv", (self, response))
+            if retval is not None:
+                response = retval
+            if self._verbose:
+                LOGGER.debug(get_log_buffer("<- ", response))
+
+            # extract the pdu part of the response
+            response_pdu = query.parse_response(response)
+
+            # analyze the received data
+            (return_code, byte_2) = struct.unpack(">BB", response_pdu[0:2])
+
+            if return_code > 0x80:
+                # the slave has returned an error
+                exception_code = byte_2
+                raise ModbusError(exception_code)
+            else:
+                if is_read_function:
+                    # get the values returned by the reading function
+                    byte_count = byte_2
+                    data = response_pdu[2:]
+                    if byte_count != len(data):
+                        # the byte count in the pdu is invalid
+                        raise ModbusInvalidResponseError(
+                            "Byte count is {0} while actual number of bytes is {1}. ".format(byte_count, len(data))
+                        )
+                elif function_code == defines.DEVICE_INFO:
+                    data = response_pdu[1:]
+                    data_format = ">" + (len(data) * "B")
+                else:
+                    # returns what is returned by the slave after a writing function
+                    data = response_pdu[1:]
+
+                # returns the data as a tuple according to the data_format
+                # (calculated based on the function or user-defined)
+                if (re.match("[>]?[sp]?", data_format)):
+                    # result = data.decode()
+                    result = data
+                else:
+                    result = struct.unpack(data_format, data)
+                if nb_of_digits > 0:
+                    digits = []
+                    for byte_val in result:
+                        for i in range(8):
+                            if len(digits) >= nb_of_digits:
+                                break
+                            digits.append(byte_val % 2)
+                            byte_val = byte_val >> 1
+                    result = tuple(digits)
+                if function_code == defines.READ_FILE_RECORD:
+                    sub_seq = list()
+                    ptr = 0
+                    while ptr < len(result):
+                        sub_seq += ((ptr + 2, ptr + 2 + result[ptr] // 2), )
+                        ptr += result[ptr] // 2 + 2
+                    result = tuple(map(lambda sub_seq_x: result[sub_seq_x[0]:sub_seq_x[1]], sub_seq))
+                return result
+
+    def set_timeout(self, timeout_in_sec):
+        """Defines a timeout on the MAC layer"""
+        self._timeout = timeout_in_sec
+
+    def get_timeout(self):
+        """Gets the current value of the MAC layer timeout"""
+        return self._timeout
+
+
+class ModbusBlock(object):
+    def __init__(self, starting_address, size, name=''):
+        self.starting_address = starting_address
+        self._data = [0] * size
+        self.size = len(self._data)
+
+    def is_in(self, starting_address, size):
+        if starting_address > self.starting_address:
+            return (self.starting_address + self.size) > starting_address
+        elif starting_address < self.starting_address:
+            return (starting_address + size) > self.starting_address
+        return True
+
+    def __getitem__(self, item):
+        return self._data.__getitem__(item)
+
+    def __setitem__(self, item, value):
+        call_hooks("modbus.ModbusBlock.setitem", (self, item, value))
+        return self._data.__setitem__(item, value)
+
+
+class Slave(object):
+    def __init__(self, slave_id, unsigned=True, memory=None):
+        self._id = slave_id
+        self.unsigned = unsigned
+        self._blocks = {}
+
+        if memory is None:
+            self._memory = {
+                defines.COILS: [],
+                defines.DISCRETE_INPUTS: [],
+                defines.HOLDING_REGISTERS: [],
+                defines.ANALOG_INPUTS: [],
+            }
+        else:
+            self._memory = memory
+
+        self._data_lock = threading.RLock()
+
+        self._fn_code_map = {
+            defines.READ_COILS: self._read_coils,
+            defines.READ_DISCRETE_INPUTS: self._read_discrete_inputs,
+            defines.READ_INPUT_REGISTERS: self._read_input_registers,
+            defines.READ_HOLDING_REGISTERS: self._read_holding_registers,
+            defines.READ_EXCEPTION_STATUS: self._read_exception_status,
+            defines.WRITE_SINGLE_COIL: self._write_single_coil,
+            defines.WRITE_SINGLE_REGISTER: self._write_single_register,
+            defines.WRITE_MULTIPLE_COILS: self._write_multiple_coils,
+            defines.WRITE_MULTIPLE_REGISTERS: self._write_multiple_registers,
+            defines.READ_WRITE_MULTIPLE_REGISTERS: self._read_write_multiple_registers,
+        }
+
+    def _get_block_and_offset(self, block_type, address, length):
+        for block in self._memory[block_type]:
+            if address >= block.starting_address:
+                offset = address - block.starting_address
+                if block.size >= offset + length:
+                    return block, offset
+        raise ModbusError(defines.ILLEGAL_DATA_ADDRESS)
+
+    def _read_digital(self, block_type, request_pdu):
+        (starting_address, quantity_of_x) = struct.unpack(">HH", request_pdu[1:5])
+
+        if (quantity_of_x <= 0) or (quantity_of_x > 2000):
+            # maximum allowed size is 2000 bits in one reading
+            raise ModbusError(defines.ILLEGAL_DATA_VALUE)
+
+        block, offset = self._get_block_and_offset(block_type, starting_address, quantity_of_x)
+
+        values = block[offset: offset + quantity_of_x]
+
+        # pack bits in bytes
+        byte_count = quantity_of_x // 8
+        if (quantity_of_x % 8) > 0:
+            byte_count += 1
+
+        # write the response header
+        response = struct.pack(">B", byte_count)
+
+        i, byte_value = 0, 0
+        for coil in values:
+            if coil:
+                byte_value += (1 << i)
+            if i >= 7:
+                # write the values of 8 bits in a byte
+                response += struct.pack(">B", byte_value)
+                # reset the counters
+                i, byte_value = 0, 0
+            else:
+                i += 1
+
+        # if there is remaining bits: add one more byte with their values
+        if i > 0:
+            fmt = "B" if self.unsigned else "b"
+            response += struct.pack(">" + fmt, byte_value)
+        return response
+
+    def _read_coils(self, request_pdu):
+        """handle read coils modbus function"""
+        call_hooks("modbus.Slave.handle_read_coils_request", (self, request_pdu))
+        return self._read_digital(defines.COILS, request_pdu)
+
+    def _read_discrete_inputs(self, request_pdu):
+        """handle read discrete inputs modbus function"""
+        call_hooks("modbus.Slave.handle_read_discrete_inputs_request", (self, request_pdu))
+        return self._read_digital(defines.DISCRETE_INPUTS, request_pdu)
+
+    def _read_registers(self, block_type, request_pdu):
+        """read the value of holding and input registers"""
+        (starting_address, quantity_of_x) = struct.unpack(">HH", request_pdu[1:5])
+
+        if (quantity_of_x <= 0) or (quantity_of_x > 125):
+            # maximum allowed size is 125 registers in one reading
+            LOGGER.debug("quantity_of_x is %d", quantity_of_x)
+            raise ModbusError(defines.ILLEGAL_DATA_VALUE)
+
+        # look for the block corresponding to the request
+        block, offset = self._get_block_and_offset(block_type, starting_address, quantity_of_x)
+
+        # get the values
+        values = block[offset: offset + quantity_of_x]
+
+        # write the response header
+        response = struct.pack(">B", 2 * quantity_of_x)
+        # add the values of every register on 2 bytes
+        for reg in values:
+            fmt = "H" if self.unsigned else "h"
+            response += struct.pack(">" + fmt, reg)
+        return response
+
+    def _read_holding_registers(self, request_pdu):
+        """handle read coils modbus function"""
+        call_hooks("modbus.Slave.handle_read_holding_registers_request", (self, request_pdu))
+        return self._read_registers(defines.HOLDING_REGISTERS, request_pdu)
+
+    def _read_input_registers(self, request_pdu):
+        """handle read coils modbus function"""
+        call_hooks("modbus.Slave.handle_read_input_registers_request", (self, request_pdu))
+        return self._read_registers(defines.ANALOG_INPUTS, request_pdu)
+
+    def _read_exception_status(self, request_pdu):
+        """handle read exception status modbus function"""
+        call_hooks("modbus.Slave.handle_read_exception_status_request", (self, request_pdu))
+        response = struct.pack(">B", 0)
+        return response
+
+    def _read_write_multiple_registers(self, request_pdu):
+        """execute modbus function 23"""
+        call_hooks("modbus.Slave.handle_read_write_multiple_registers_request", (self, request_pdu))
+        # get the starting address and the number of items from the request pdu
+        (starting_read_address, quantity_of_x_to_read, starting_write_address, quantity_of_x_to_write, byte_count_to_write) = struct.unpack(">HHHHB", request_pdu[1:10])
+
+        # read part
+        if (quantity_of_x_to_read <= 0) or (quantity_of_x_to_read > 125):
+            # maximum allowed size is 125 registers in one reading
+            LOGGER.debug("quantity_of_x_to_read is %d", quantity_of_x_to_read)
+            raise ModbusError(defines.ILLEGAL_DATA_VALUE)
+
+        # look for the block corresponding to the request
+        block, offset = self._get_block_and_offset(defines.HOLDING_REGISTERS, starting_read_address, quantity_of_x_to_read)
+
+        # get the values
+        values = block[offset: offset + quantity_of_x_to_read]
+        # write the response header
+        response = struct.pack(">B", 2 * quantity_of_x_to_read)
+        # add the values of every register on 2 bytes
+        for reg in values:
+            fmt = "H" if self.unsigned else "h"
+            response += struct.pack(">" + fmt, reg)
+
+        # write part
+        if (quantity_of_x_to_write <= 0) or (quantity_of_x_to_write > 123) or (byte_count_to_write != (quantity_of_x_to_write * 2)):
+            # maximum allowed size is 123 registers in one reading
+            raise ModbusError(defines.ILLEGAL_DATA_VALUE)
+
+        # look for the block corresponding to the request
+        block, offset = self._get_block_and_offset(defines.HOLDING_REGISTERS, starting_write_address, quantity_of_x_to_write)
+
+        count = 0
+        for i in range(quantity_of_x_to_write):
+            count += 1
+            fmt = "H" if self.unsigned else "h"
+            block[offset + i] = struct.unpack(">" + fmt, request_pdu[10 + 2 * i:12 + 2 * i])[0]
+
+        return response
+
+    def _write_multiple_registers(self, request_pdu):
+        """execute modbus function 16"""
+        call_hooks("modbus.Slave.handle_write_multiple_registers_request", (self, request_pdu))
+        # get the starting address and the number of items from the request pdu
+        (starting_address, quantity_of_x, byte_count) = struct.unpack(">HHB", request_pdu[1:6])
+
+        if (quantity_of_x <= 0) or (quantity_of_x > 123) or (byte_count != (quantity_of_x * 2)):
+            # maximum allowed size is 123 registers in one reading
+            raise ModbusError(defines.ILLEGAL_DATA_VALUE)
+
+        # look for the block corresponding to the request
+        block, offset = self._get_block_and_offset(defines.HOLDING_REGISTERS, starting_address, quantity_of_x)
+
+        count = 0
+        for i in range(quantity_of_x):
+            count += 1
+            fmt = "H" if self.unsigned else "h"
+            block[offset + i] = struct.unpack(">" + fmt, request_pdu[6 + 2 * i: 8 + 2 * i])[0]
+
+        return struct.pack(">HH", starting_address, count)
+
+    def _write_multiple_coils(self, request_pdu):
+        """execute modbus function 15"""
+        call_hooks("modbus.Slave.handle_write_multiple_coils_request", (self, request_pdu))
+        # get the starting address and the number of items from the request pdu
+        (starting_address, quantity_of_x, byte_count) = struct.unpack(">HHB", request_pdu[1:6])
+
+        expected_byte_count = quantity_of_x // 8
+        if (quantity_of_x % 8) > 0:
+            expected_byte_count += 1
+
+        if (quantity_of_x <= 0) or (quantity_of_x > 1968) or (byte_count != expected_byte_count):
+            # maximum allowed size is 1968 coils
+            raise ModbusError(defines.ILLEGAL_DATA_VALUE)
+
+        # look for the block corresponding to the request
+        block, offset = self._get_block_and_offset(defines.COILS, starting_address, quantity_of_x)
+
+        count = 0
+        for i in range(byte_count):
+            if count >= quantity_of_x:
+                break
+            fmt = "B" if self.unsigned else "b"
+            (byte_value, ) = struct.unpack(">" + fmt, request_pdu[6 + i: 7 + i])
+            for j in range(8):
+                if count >= quantity_of_x:
+                    break
+
+                if byte_value & (1 << j):
+                    block[offset + i * 8 + j] = 1
+                else:
+                    block[offset + i * 8 + j] = 0
+
+                count += 1
+        return struct.pack(">HH", starting_address, count)
+
+    def _write_single_register(self, request_pdu):
+        """execute modbus function 6"""
+        call_hooks("modbus.Slave.handle_write_single_register_request", (self, request_pdu))
+
+        fmt = "H" if self.unsigned else "h"
+        (data_address, value) = struct.unpack(">H" + fmt, request_pdu[1:5])
+        block, offset = self._get_block_and_offset(defines.HOLDING_REGISTERS, data_address, 1)
+        block[offset] = value
+        # returns echo of the command
+        return request_pdu[1:]
+
+    def _write_single_coil(self, request_pdu):
+        """execute modbus function 5"""
+
+        call_hooks("modbus.Slave.handle_write_single_coil_request", (self, request_pdu))
+        (data_address, value) = struct.unpack(">HH", request_pdu[1:5])
+        block, offset = self._get_block_and_offset(defines.COILS, data_address, 1)
+        if value == 0:
+            block[offset] = 0
+        elif value == 0xff00:
+            block[offset] = 1
+        else:
+            raise ModbusError(defines.ILLEGAL_DATA_VALUE)
+        # returns echo of the command
+        return request_pdu[1:]
+
+    def handle_request(self, request_pdu, broadcast=False):
+        """
+        parse the request pdu, makes the corresponding action
+        and returns the response pdu
+        """
+        # thread-safe
+        with self._data_lock:
+            try:
+                retval = call_hooks("modbus.Slave.handle_request", (self, request_pdu))
+                if retval is not None:
+                    return retval
+
+                # get the function code
+                (function_code, ) = struct.unpack(">B", request_pdu[0:1])
+
+                # check if the function code is valid. If not returns error response
+                if function_code not in self._fn_code_map:
+                    raise ModbusError(defines.ILLEGAL_FUNCTION)
+
+                # if read query is broadcasted raises an error
+                cant_be_broadcasted = (
+                    defines.READ_COILS,
+                    defines.READ_DISCRETE_INPUTS,
+                    defines.READ_INPUT_REGISTERS,
+                    defines.READ_HOLDING_REGISTERS
+                )
+                if broadcast and (function_code in cant_be_broadcasted):
+                    raise ModbusInvalidRequestError("Function %d can not be broadcasted" % function_code)
+
+                # execute the corresponding function
+                response_pdu = self._fn_code_map[function_code](request_pdu)
+                if response_pdu:
+                    if broadcast:
+                        call_hooks("modbus.Slave.on_handle_broadcast", (self, response_pdu))
+                        LOGGER.debug("broadcast: %s", get_log_buffer("!!", response_pdu))
+                        return ""
+                    else:
+                        return struct.pack(">B", function_code) + response_pdu
+                raise Exception("No response for function %d" % function_code)
+
+            except ModbusError as excpt:
+                LOGGER.debug(str(excpt))
+                call_hooks("modbus.Slave.on_exception", (self, function_code, excpt))
+                return struct.pack(">BB", function_code + 128, excpt.get_exception_code())
+
+    def add_block(self, block_name, block_type, starting_address, size):
+        """Add a new block identified by its name"""
+        # thread-safe
+        with self._data_lock:
+            if size <= 0:
+                raise InvalidArgumentError("size must be a positive number")
+
+            if starting_address < 0:
+                raise InvalidArgumentError("starting address must be zero or positive number")
+
+            if block_name in self._blocks:
+                raise DuplicatedKeyError("Block {0} already exists. ".format(block_name))
+
+            if block_type not in self._memory:
+                raise InvalidModbusBlockError("Invalid block type {0}".format(block_type))
+
+            # check that the new block doesn't overlap an existing block
+            # it means that only 1 block per type must correspond to a given address
+            # for example: it must not have 2 holding registers at address 100
+            index = 0
+            for i in range(len(self._memory[block_type])):
+                block = self._memory[block_type][i]
+                if block.is_in(starting_address, size):
+                    raise OverlapModbusBlockError(
+                        "Overlap block at {0} size {1}".format(block.starting_address, block.size)
+                    )
+                if block.starting_address > starting_address:
+                    index = i
+                    break
+
+            # if the block is ok: register it
+            self._blocks[block_name] = (block_type, starting_address)
+            # add it in the 'per type' shortcut
+            self._memory[block_type].insert(index, ModbusBlock(starting_address, size, block_name))
+
+    def remove_block(self, block_name):
+        """
+        Remove the block with the given name.
+        Raise an exception if not found
+        """
+        # thread safe
+        with self._data_lock:
+            block = self._get_block(block_name)
+
+            # the block has been found: remove it from the shortcut
+            block_type = self._blocks.pop(block_name)[0]
+            self._memory[block_type].remove(block)
+
+    def remove_all_blocks(self):
+        """
+        Remove all the blocks
+        """
+        # thread safe
+        with self._data_lock:
+            self._blocks.clear()
+            for key in self._memory:
+                self._memory[key] = []
+
+    def _get_block(self, block_name):
+        """Find a block by its name and raise and exception if not found"""
+        if block_name not in self._blocks:
+            raise MissingKeyError("block {0} not found".format(block_name))
+        (block_type, starting_address) = self._blocks[block_name]
+        for block in self._memory[block_type]:
+            if block.starting_address == starting_address:
+                return block
+        raise Exception("Bug?: the block {0} is not registered properly in memory".format(block_name))
+
+    def set_values(self, block_name, address, values):
+        """
+        Set the values of the items at the given address
+        If values is a list or a tuple, the value of every item is written
+        If values is a number, only one value is written
+        """
+        # thread safe
+        with self._data_lock:
+            block = self._get_block(block_name)
+
+            # the block has been found
+            # check that it doesn't write out of the block
+            offset = address - block.starting_address
+
+            size = 1
+            if isinstance(values, list) or isinstance(values, tuple):
+                size = len(values)
+
+            if (offset < 0) or ((offset + size) > block.size):
+                raise OutOfModbusBlockError(
+                    "address {0} size {1} is out of block {2}".format(address, size, block_name)
+                )
+
+            # if Ok: write the values
+            if isinstance(values, list) or isinstance(values, tuple):
+                block[offset: offset + len(values)] = values
+            else:
+                block[offset] = values
+
+    def get_values(self, block_name, address, size=1):
+        """
+        return the values of n items at the given address of the given block
+        """
+        # thread safe
+        with self._data_lock:
+            block = self._get_block(block_name)
+
+            # the block has been found
+            # check that it doesn't write out of the block
+            offset = address - block.starting_address
+
+            if (offset < 0) or ((offset + size) > block.size):
+                raise OutOfModbusBlockError(
+                    "address {0} size {1} is out of block {2}".format(address, size, block_name)
+                )
+
+            # returns the values
+            if size == 1:
+                return tuple([block[offset], ])
+            else:
+                return tuple(block[offset: offset + size])
+
+
+class Databank(object):
+    """A databank is a shared place containing the data of all slaves"""
+
+    def __init__(self, error_on_missing_slave=True):
+        """Constructor"""
+        # the map of slaves by ids
+        self._slaves = {}
+        # protect access to the map of slaves
+        self._lock = threading.RLock()
+        self.error_on_missing_slave = error_on_missing_slave
+
+    def add_slave(self, slave_id, unsigned=True, memory=None):
+        """Add a new slave with the given id"""
+        with self._lock:
+            if (slave_id <= 0) or (slave_id > 255):
+                raise Exception("Invalid slave id {0}".format(slave_id))
+            if slave_id not in self._slaves:
+                self._slaves[slave_id] = Slave(slave_id, unsigned, memory)
+                return self._slaves[slave_id]
+            else:
+                raise DuplicatedKeyError("Slave {0} already exists".format(slave_id))
+
+    def get_slave(self, slave_id):
+        """Get the slave with the given id"""
+        with self._lock:
+            if slave_id in self._slaves:
+                return self._slaves[slave_id]
+            else:
+                raise MissingKeyError("Slave {0} doesn't exist".format(slave_id))
+
+    def remove_slave(self, slave_id):
+        """Remove the slave with the given id"""
+        with self._lock:
+            if slave_id in self._slaves:
+                self._slaves.pop(slave_id)
+            else:
+                raise MissingKeyError("Slave {0} already exists".format(slave_id))
+
+    def remove_all_slaves(self):
+        """clean the list of slaves"""
+        with self._lock:
+            self._slaves.clear()
+
+    def handle_request(self, query, request):
+        """
+        when a request is received, handle it and returns the response pdu
+        """
+        request_pdu = ""
+        try:
+            # extract the pdu and the slave id
+            (slave_id, request_pdu) = query.parse_request(request)
+
+            # get the slave and let him executes the action
+            if slave_id == 0:
+                # broadcast
+                for key in self._slaves:
+                    self._slaves[key].handle_request(request_pdu, broadcast=True)
+                return
+            else:
+                try:
+                    slave = self.get_slave(slave_id)
+                except MissingKeyError:
+                    if self.error_on_missing_slave:
+                        raise
+                    else:
+                        return ""
+
+                response_pdu = slave.handle_request(request_pdu)
+                # make the full response
+                response = query.build_response(response_pdu)
+                return response
+        except ModbusInvalidRequestError as excpt:
+            # Request is invalid, do not send any response
+            LOGGER.error("invalid request: " + str(excpt))
+            return ""
+        except MissingKeyError as excpt:
+            # No slave with this ID in server, do not send any response
+            LOGGER.error("handle request failed: " + str(excpt))
+            return ""
+        except Exception as excpt:
+            call_hooks("modbus.Databank.on_error", (self, excpt, request_pdu))
+            LOGGER.error("handle request failed: " + str(excpt))
+
+        # If the request was not handled correctly, return a server error response
+        func_code = 1
+        if len(request_pdu) > 0:
+            (func_code, ) = struct.unpack(">B", request_pdu[0:1])
+
+        return struct.pack(">BB", func_code + 0x80, defines.SLAVE_DEVICE_FAILURE)
+
+
+class Server(object):
+    """
+    This class owns several slaves and defines an interface
+    to be implemented for a TCP or RTU server
+    """
+
+    def __init__(self, databank=None):
+        """Constructor"""
+        # never use a mutable type as default argument
+        self._databank = databank if databank else Databank()
+        self._verbose = False
+        self._thread = None
+        self._go = None
+        self._make_thread()
+
+    def _do_init(self):
+        """executed before the server starts: to be overridden"""
+        pass
+
+    def _do_exit(self):
+        """executed after the server stops: to be overridden"""
+        pass
+
+    def _do_run(self):
+        """main function of the server: to be overridden"""
+        pass
+
+    def _make_thread(self):
+        """create the main thread of the server"""
+        self._thread = threading.Thread(target=Server._run_server, args=(self,))
+        self._go = threading.Event()
+
+    def set_verbose(self, verbose):
+        """if verbose is true the sent and received packets will be logged"""
+        self._verbose = verbose
+
+    def get_db(self):
+        """returns the databank"""
+        return self._databank
+
+    def add_slave(self, slave_id, unsigned=True, memory=None):
+        """add slave to the server"""
+        return self._databank.add_slave(slave_id, unsigned, memory)
+
+    def get_slave(self, slave_id):
+        """get the slave with the given id"""
+        return self._databank.get_slave(slave_id)
+
+    def remove_slave(self, slave_id):
+        """remove the slave with the given id"""
+        self._databank.remove_slave(slave_id)
+
+    def remove_all_slaves(self):
+        """remove the slave with the given id"""
+        self._databank.remove_all_slaves()
+
+    def _make_query(self):
+        """
+        Returns an instance of a Query subclass implementing
+        the MAC layer protocol
+        """
+        raise NotImplementedError()
+
+    def start(self):
+        """Start the server. It will handle request"""
+        self._go.set()
+        self._thread.start()
+
+    def stop(self):
+        """stop the server. It doesn't handle request anymore"""
+        if self._thread.is_alive():
+            self._go.clear()
+            self._thread.join()
+
+    def _run_server(self):
+        """main function of the main thread"""
+        try:
+            self._do_init()
+            while self._go.isSet():
+                self._do_run()
+            LOGGER.debug("%s has stopped", self.__class__)
+            self._do_exit()
+        except Exception as excpt:
+            LOGGER.error("server error: %s", str(excpt))
+            call_hooks("modbus.Server.on_exception", (self, excpt))
+        # make possible to rerun in future
+        self._make_thread()
+
+    def _handle(self, request):
+        """handle a received sentence"""
+
+        if self._verbose:
+            LOGGER.debug(get_log_buffer("-->", request))
+
+        # gets a query for analyzing the request
+        query = self._make_query()
+
+        retval = call_hooks("modbus.Server.before_handle_request", (self, request))
+        if retval:
+            request = retval
+
+        response = self._databank.handle_request(query, request)
+        retval = call_hooks("modbus.Server.after_handle_request", (self, response))
+        if retval:
+            response = retval
+
+        if response and self._verbose:
+            LOGGER.debug(get_log_buffer("<--", response))
+        return response

+ 248 - 0
utils/modbus/modbus_rtu.py

@@ -0,0 +1,248 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+
+
+import struct
+import time
+
+from utils.modbus import LOGGER
+from utils.modbus.modbus import (
+    Databank, Query, Master, Server,
+    InvalidArgumentError, ModbusInvalidResponseError, ModbusInvalidRequestError
+)
+from utils.modbus.hooks import call_hooks
+from utils.modbus import utils
+
+
+class RtuQuery(Query):
+    def __init__(self):
+        super(RtuQuery, self).__init__()
+        self._request_address = 0
+        self._response_address = 0
+
+    def build_request(self, pdu, slave):
+        self._request_address = slave
+        if (self._request_address < 0) or (self._request_address > 255):
+            raise InvalidArgumentError("Invalid address {0}".format(self._request_address))
+        data = struct.pack(">B", self._request_address) + pdu
+        crc = struct.pack(">H", utils.calculate_crc(data))
+        return data + crc
+
+    def parse_response(self, response):
+        if len(response) < 3:
+            raise ModbusInvalidResponseError("Response length is invalid {0}".format(len(response)))
+
+        (self._response_address, ) = struct.unpack(">B", response[0:1])
+
+        if self._request_address != self._response_address:
+            raise ModbusInvalidResponseError(
+                "Response address {0} is different from request address {1}".format(
+                    self._response_address, self._request_address
+                )
+            )
+
+        (crc, ) = struct.unpack(">H", response[-2:])
+
+        if crc != utils.calculate_crc(response[:-2]):
+            raise ModbusInvalidResponseError("Invalid CRC in response")
+
+        return response[1:-2]
+
+    def parse_request(self, request):
+        if len(request) < 3:
+            raise ModbusInvalidRequestError("Request length is invalid {0}".format(len(request)))
+
+        (self._request_address, ) = struct.unpack(">B", request[0:1])
+
+        (crc, ) = struct.unpack(">H", request[-2:])
+        if crc != utils.calculate_crc(request[:-2]):
+            raise ModbusInvalidRequestError("Invalid CRC in request")
+
+        return self._request_address, request[1:-2]
+
+    def build_response(self, response_pdu):
+        self._response_address = self._request_address
+        data = struct.pack(">B", self._response_address) + response_pdu
+        crc = struct.pack(">H", utils.calculate_crc(data))
+        return data + crc
+
+
+class RtuMaster(Master):
+    def __init__(self, serial, interchar_multiplier=1.5, interframe_multiplier=3.5, t0=None):
+        self._serial = serial
+        self.use_sw_timeout = False
+        LOGGER.debug("RtuMaster %s is %s", self._serial.name, "opened" if self._serial.is_open else "closed")
+        super(RtuMaster, self).__init__(self._serial.timeout)
+
+        if t0:
+            self._t0 = t0
+        else:
+            self._t0 = utils.calculate_rtu_inter_char(self._serial.baudrate)
+        self._serial.inter_byte_timeout = interchar_multiplier * self._t0
+        self.set_timeout(interframe_multiplier * self._t0)
+        self.handle_local_echo = False
+
+    def _do_open(self):
+        if not self._serial.is_open:
+            call_hooks("modbus_rtu.RtuMaster.before_open", (self, ))
+            self._serial.open()
+
+    def _do_close(self):
+        if self._serial.is_open:
+            self._serial.close()
+            call_hooks("modbus_rtu.RtuMaster.after_close", (self, ))
+            return True
+
+    def set_timeout(self, timeout_in_sec, use_sw_timeout=False):
+        Master.set_timeout(self, timeout_in_sec)
+        self._serial.timeout = timeout_in_sec
+        self.use_sw_timeout = use_sw_timeout
+
+    def _send(self, request):
+        retval = call_hooks("modbus_rtu.RtuMaster.before_send", (self, request))
+        if retval is not None:
+            request = retval
+
+        self._serial.reset_input_buffer()
+        self._serial.reset_output_buffer()
+
+        self._serial.write(request)
+        self._serial.flush()
+
+        if self.handle_local_echo:
+            self._serial.read(len(request))
+
+    def _recv(self, expected_length=-1):
+        response = utils.to_data("")
+        start_time = time.time() if self.use_sw_timeout else 0
+        readed_len = 0
+        while True:
+            if self._serial.timeout:
+                read_bytes = self._serial.read(expected_length - readed_len if (expected_length - readed_len) > 0 else 1)
+            else:
+                read_bytes = self._serial.read(expected_length if expected_length > 0 else 1)
+            if self.use_sw_timeout:
+                read_duration = time.time() - start_time
+            else:
+                read_duration = 0
+            if (not read_bytes) or (read_duration > self._serial.timeout):
+                break
+            response += read_bytes
+            if expected_length >= 0 and len(response) >= expected_length:
+                break
+            readed_len += len(read_bytes)
+
+        retval = call_hooks("modbus_rtu.RtuMaster.after_recv", (self, response))
+        if retval is not None:
+            return retval
+        return response
+
+    def _make_query(self):
+        return RtuQuery()
+
+
+class RtuServer(Server):
+    _timeout = 0
+
+    def __init__(self, serial, databank=None, error_on_missing_slave=True, **kwargs):
+        interframe_multiplier = kwargs.pop('interframe_multiplier', 3.5)
+        interchar_multiplier = kwargs.pop('interchar_multiplier', 1.5)
+
+        databank = databank if databank else Databank(error_on_missing_slave=error_on_missing_slave)
+        super(RtuServer, self).__init__(databank)
+
+        self._serial = serial
+        LOGGER.debug("RtuServer %s is %s", self._serial.name, "opened" if self._serial.is_open else "closed")
+
+        self._t0 = utils.calculate_rtu_inter_char(self._serial.baudrate)
+        self._serial.inter_byte_timeout = interchar_multiplier * self._t0
+        self.set_timeout(interframe_multiplier * self._t0)
+
+        self._block_on_first_byte = False
+
+    def close(self):
+        if self._serial.is_open:
+            call_hooks("modbus_rtu.RtuServer.before_close", (self, ))
+            self._serial.close()
+            call_hooks("modbus_rtu.RtuServer.after_close", (self, ))
+
+    def set_timeout(self, timeout):
+        self._timeout = timeout
+        self._serial.timeout = timeout
+
+    def get_timeout(self):
+        return self._timeout
+
+    def __del__(self):
+        self.close()
+
+    def _make_query(self):
+        return RtuQuery()
+
+    def start(self):
+        self._block_on_first_byte = True
+        super(RtuServer, self).start()
+
+    def stop(self):
+        self._block_on_first_byte = False
+        if self._serial.is_open:
+            self._serial.cancel_read()
+        super(RtuServer, self).stop()
+
+    def _do_init(self):
+        if not self._serial.is_open:
+            call_hooks("modbus_rtu.RtuServer.before_open", (self, ))
+            self._serial.open()
+            call_hooks("modbus_rtu.RtuServer.after_open", (self, ))
+
+    def _do_exit(self):
+        self.close()
+
+    def _do_run(self):
+        try:
+            request = utils.to_data('')
+            if self._block_on_first_byte:
+                self._serial.timeout = None
+                try:
+                    read_bytes = self._serial.read(1)
+                    request += read_bytes
+                except Exception as e:
+                    self._serial.close()
+                    self._serial.open()
+                self._serial.timeout = self._timeout
+
+            while True:
+                try:
+                    read_bytes = self._serial.read(128)
+                    if not read_bytes:
+                        break
+                except Exception as e:
+                    self._serial.close()
+                    self._serial.open()
+                    break
+                request += read_bytes
+
+            if request:
+                retval = call_hooks("modbus_rtu.RtuServer.after_read", (self, request))
+                if retval is not None:
+                    request = retval
+
+                response = self._handle(request)
+
+                retval = call_hooks("modbus_rtu.RtuServer.before_write", (self, response))
+                if retval is not None:
+                    response = retval
+
+                if response:
+                    if self._serial.in_waiting > 0:
+                        LOGGER.warning("Not sending response because there is new request pending")
+                    else:
+                        self._serial.write(response)
+                        self._serial.flush()
+                        time.sleep(self.get_timeout())
+
+                call_hooks("modbus_rtu.RtuServer.after_write", (self, response))
+
+        except Exception as excpt:
+            LOGGER.error("Error while handling request, Exception occurred: %s", excpt)
+            call_hooks("modbus_rtu.RtuServer.on_error", (self, excpt))

+ 238 - 0
utils/modbus/utils.py

@@ -0,0 +1,238 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+"""
+ Modbus TestKit: Implementation of Modbus protocol in python
+
+ (C)2009 - Luc Jean - [email protected]
+ (C)2009 - Apidev - http://www.apidev.fr
+
+ This is distributed under GNU LGPL license, see license.txt
+"""
+from __future__ import print_function
+
+import sys
+import threading
+import logging
+import socket
+import select
+from utils.modbus import LOGGER
+
+PY2 = sys.version_info[0] == 2
+PY3 = sys.version_info[0] == 3
+
+
+def threadsafe_function(fcn):
+    """decorator making sure that the decorated function is thread safe"""
+    lock = threading.RLock()
+
+    def new(*args, **kwargs):
+        """Lock and call the decorated function
+
+           Unless kwargs['threadsafe'] == False
+        """
+        threadsafe = kwargs.pop('threadsafe', True)
+        if threadsafe:
+            lock.acquire()
+        try:
+            ret = fcn(*args, **kwargs)
+        except Exception as excpt:
+            raise excpt
+        finally:
+            if threadsafe:
+                lock.release()
+        return ret
+    return new
+
+
+def flush_socket(socks, lim=0):
+    """remove the data present on the socket"""
+    input_socks = [socks]
+    cnt = 0
+    while True:
+        i_socks = select.select(input_socks, input_socks, input_socks, 0.0)[0]
+        if len(i_socks) == 0:
+            break
+        for sock in i_socks:
+            sock.recv(1024)
+        if lim > 0:
+            cnt += 1
+            if cnt >= lim:
+                # avoid infinite loop due to loss of connection
+                raise Exception("flush_socket: maximum number of iterations reached")
+
+
+def get_log_buffer(prefix, buff):
+    """Format binary data into a string for debug purpose"""
+    log = prefix
+    for i in buff:
+        log += str(ord(i) if PY2 else i) + "-"
+    return log[:-1]
+
+
+class ConsoleHandler(logging.Handler):
+    """This class is a logger handler. It prints on the console"""
+
+    def __init__(self):
+        """Constructor"""
+        logging.Handler.__init__(self)
+
+    def emit(self, record):
+        """format and print the record on the console"""
+        print(self.format(record))
+
+
+class LogitHandler(logging.Handler):
+    """This class is a logger handler. It send to a udp socket"""
+
+    def __init__(self, dest):
+        """Constructor"""
+        logging.Handler.__init__(self)
+        self._dest = dest
+        self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+
+    def emit(self, record):
+        """format and send the record over udp"""
+        data = self.format(record) + "\r\n"
+        if PY3:
+            data = to_data(data)
+        self._sock.sendto(data, self._dest)
+
+
+class DummyHandler(logging.Handler):
+    """This class is a logger handler. It doesn't do anything"""
+
+    def __init__(self):
+        """Constructor"""
+        super(DummyHandler, self).__init__()
+
+    def emit(self, record):
+        """do nothing with the given record"""
+        pass
+
+
+def create_logger(name="dummy", level=logging.DEBUG, record_format=None):
+    """Create a logger according to the given settings"""
+    if record_format is None:
+        record_format = "%(asctime)s\t%(levelname)s\t%(module)s.%(funcName)s\t%(threadName)s\t%(message)s"
+
+    logger = logging.getLogger("modbus_tk")
+    logger.setLevel(level)
+    formatter = logging.Formatter(record_format)
+    if name == "udp":
+        log_handler = LogitHandler(("127.0.0.1", 1975))
+    elif name == "console":
+        log_handler = ConsoleHandler()
+    elif name == "dummy":
+        log_handler = DummyHandler()
+    else:
+        raise Exception("Unknown handler %s" % name)
+    log_handler.setFormatter(formatter)
+    logger.addHandler(log_handler)
+    return logger
+
+
+def swap_bytes(word_val):
+    """swap lsb and msb of a word"""
+    msb = (word_val >> 8) & 0xFF
+    lsb = word_val & 0xFF
+    return (lsb << 8) + msb
+
+
+def calculate_crc(data):
+    """Calculate the CRC16 of a datagram"""
+    CRC16table = (
+        0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241,
+        0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440,
+        0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40,
+        0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841,
+        0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40,
+        0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41,
+        0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641,
+        0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040,
+        0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240,
+        0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441,
+        0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41,
+        0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840,
+        0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41,
+        0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40,
+        0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640,
+        0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041,
+        0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240,
+        0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441,
+        0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41,
+        0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840,
+        0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41,
+        0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40,
+        0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640,
+        0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041,
+        0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241,
+        0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440,
+        0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40,
+        0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841,
+        0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40,
+        0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41,
+        0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641,
+        0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040
+    )
+    crc = 0xFFFF
+    if PY2:
+        for c in data:
+            crc = (crc >> 8) ^ CRC16table[(ord(c) ^ crc) & 0xFF]
+    else:
+        for c in data:
+            crc = (crc >> 8) ^ CRC16table[((c) ^ crc) & 0xFF]
+    return swap_bytes(crc)
+
+
+def calculate_rtu_inter_char(baudrate):
+    """calculates the interchar delay from the baudrate"""
+    if baudrate <= 19200:
+        return 11.0 / baudrate
+    else:
+        return 0.0005
+
+
+class WorkerThread(object):
+    """
+    A thread which is running an almost-ever loop
+    It can be stopped by calling the stop function
+    """
+
+    def __init__(self, main_fct, args=(), init_fct=None, exit_fct=None):
+        """Constructor"""
+        self._fcts = [init_fct, main_fct, exit_fct]
+        self._args = args
+        self._thread = threading.Thread(target=WorkerThread._run, args=(self,))
+        self._go = threading.Event()
+
+    def start(self):
+        """Start the thread"""
+        self._go.set()
+        self._thread.start()
+
+    def stop(self):
+        """stop the thread"""
+        if self._thread.is_alive():
+            self._go.clear()
+            self._thread.join()
+
+    def _run(self):
+        """main function of the thread execute _main_fct until stop is called"""
+        # pylint: disable=broad-except
+        try:
+            if self._fcts[0]:
+                self._fcts[0](*self._args)
+            while self._go.isSet():
+                self._fcts[1](*self._args)
+        except Exception as excpt:
+            LOGGER.error("error: %s", str(excpt))
+        finally:
+            if self._fcts[2]:
+                self._fcts[2](*self._args)
+
+
+def to_data(string_data):
+    if PY2:
+        return string_data
+    else:
+        return bytearray(string_data, 'ascii')

+ 19 - 0
utils/qssloader.py

@@ -0,0 +1,19 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+"""
+@文件    :qssloader.py
+@时间    :2022/01/07 20:04:32
+@作者    :None
+@版本    :1.0
+@说明    :主题美化
+"""
+
+
+class QSSLoader:
+    def __init__(self):
+        pass
+
+    @staticmethod
+    def read_qss_file(qss_file_name):
+        with open(qss_file_name, "r", encoding="UTF-8") as file:
+            return file.read()

+ 66 - 0
utils/qt.py

@@ -0,0 +1,66 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+'''
+@文件    :qt.py
+@时间    :2022/02/21 16:48:46
+@作者    :None
+@版本    :1.0
+@说明    : pyqt和pyside2兼容包
+'''
+
+
+QT_CHOSE = 0
+if QT_CHOSE:
+    # PySide2
+    from PySide2 import (  # noqa F401
+        QtGui, QtWidgets, QtCore
+    )
+    from PySide2.QtWidgets import (  # noqa F401
+        QMainWindow, QDesktopWidget, QStyleFactory, QWidget, QFrame,
+        QApplication, QSizePolicy, QHBoxLayout, QVBoxLayout, QGridLayout,
+        QLabel, QPushButton, QLineEdit, QProgressBar, QComboBox, QLCDNumber,
+        QMessageBox, QFileDialog, QPlainTextEdit, QStatusBar, QGroupBox, QTabBar,
+        QTabWidget, QTableWidget, QAbstractItemView, QCheckBox, QHeaderView,
+        QStyle, QStyleOptionButton, QTableWidgetItem, QSpacerItem
+    )
+    from PySide2.QtGui import (  # noqa F401
+        QGuiApplication, QPalette, QPainter, QColor, QPen,
+        QLinearGradient, QFont, QBrush, QPixmap, QIcon, QRegExpValidator, QKeySequence
+    )
+    from PySide2.QtCore import (  # noqa F401
+        QObject, QCoreApplication, QStandardPaths, QMetaObject, Qt,
+        QPoint, QRect, QRectF, QPointF, QDateTime, QTimer, QSize,
+        QThread, QMutex, QSemaphore, QRegExp, QModelIndex, Signal, Slot
+    )
+    from PySide2.QtCharts import QtCharts
+    QChartView = QtCharts.QChartView
+    QChart = QtCharts.QChart
+    QLineSeries = QtCharts.QLineSeries
+    QCategoryAxis = QtCharts.QCategoryAxis
+    QValueAxis = QtCharts.QValueAxis
+    QLegendMarker = QtCharts.QLegendMarker
+else:
+    # PyQt5
+    from PyQt5 import (  # noqa F401
+        QtGui, QtWidgets, QtCore, QtChart
+    )
+    from PyQt5.QtWidgets import (  # noqa F401
+        QMainWindow, QDesktopWidget, QStyleFactory, QWidget, QFrame,
+        QApplication, QSizePolicy, QHBoxLayout, QVBoxLayout, QGridLayout,
+        QLabel, QPushButton, QLineEdit, QProgressBar, QComboBox, QLCDNumber,
+        QMessageBox, QFileDialog, QPlainTextEdit, QStatusBar, QGroupBox, QTabBar,
+        QTabWidget, QTableWidget, QAbstractItemView, QCheckBox, QHeaderView,
+        QStyle, QStyleOptionButton, QTableWidgetItem, QSpacerItem
+    )
+    from PyQt5.QtGui import (  # noqa F401
+        QGuiApplication, QPalette, QPainter, QColor, QPen,
+        QLinearGradient, QFont, QBrush, QPixmap, QIcon, QRegExpValidator, QKeySequence
+    )
+    from PyQt5.QtCore import (  # noqa F401
+        QObject, QCoreApplication, QStandardPaths, QMetaObject, Qt, QRect, QRectF,
+        QPoint, QPointF, QDateTime, QTimer, QSize,
+        QThread, QMutex, QSemaphore, QRegExp, QModelIndex, pyqtSignal as Signal, pyqtSlot as Slot
+    )
+    from PyQt5.QtChart import (  # noqa F401
+        QChartView, QLineSeries, QChart, QCategoryAxis, QValueAxis, QLegendMarker
+    )

+ 27 - 0
utils/resource.py

@@ -0,0 +1,27 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+'''
+@文件    :resourcePath.py
+@时间    :2021/12/09 13:49:58
+@作者    :None
+@版本    :1.0
+@说明    :资源文件路径配置
+'''
+
+
+from os import path
+import sys
+from sys import path as spath
+
+
+def resource_path(relative_path):
+    try:
+        base_path = sys._MEIPASS
+    except Exception:
+        base_path = path.abspath(".")
+    return path.join(base_path, relative_path)
+
+
+def resource_path_extend(relative_path):
+    spath.append(path.abspath(relative_path))
+    print(sys.path)

+ 0 - 0
widget/__init__.py


+ 70 - 0
widget/pcs_home.py

@@ -0,0 +1,70 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+'''
+@文件    :bms_home.py
+@时间    :2021/12/09 13:33:56
+@作者    :None
+@版本    :1.0
+@说明    :主页面逻辑
+'''
+
+
+from ui.home import UiPcsHomePage
+from utils.qt import QMainWindow, Qt, QStyle, QMessageBox, QGuiApplication, Signal
+
+
+class Win_Pcs_Home(UiPcsHomePage, QMainWindow):
+    # 跳转连接页面
+    interface_signal = Signal(int)
+    connect_signal = Signal()
+    disconnect_signal = Signal()
+
+    def __init__(self, parent=None):
+        super(Win_Pcs_Home, self).__init__(parent)
+        self.setupUi(self)
+
+        self.cb_interface.currentIndexChanged.connect(self._interface_chose)
+        self.pcs_switch.checkedChanged.connect(self._get_state)
+
+    # 居中显示
+
+    def center(self):
+        # 新方法
+        self.setGeometry(
+            QStyle.alignedRect(
+                Qt.LeftToRight,
+                Qt.AlignCenter,
+                self.size(),
+                QGuiApplication.primaryScreen().availableGeometry(),
+            ),
+        )
+
+    def _interface_chose(self):
+        self.interface_signal.emit(self.cb_interface.currentIndex())
+
+    # BMS连接
+    def _get_state(self, checked):
+        if checked:
+            self.connect_signal.emit()
+        else:
+            self.disconnect_signal.emit()
+
+    def can_connect_error(self):
+        QMessageBox.critical(self, "失败!", "CAN 连接失败!")
+        self.pcs_switch.state = False
+        return
+
+    def no_can_device(self):
+        QMessageBox.critical(self, "失败!", "无CAN 设备!")
+        self.pcs_switch.state = False
+        return
+
+    def com_connect_error(self):
+        QMessageBox.critical(self, "失败!", "串口连接失败!")
+        self.pcs_switch.state = False
+        return
+
+    def no_com_device(self):
+        QMessageBox.critical(self, "失败!", "无法连接此串口设备!")
+        self.pcs_switch.state = False
+        return

+ 21 - 0
widget/pcs_status_bar.py

@@ -0,0 +1,21 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+'''
+@文件    :bmu_data.py
+@时间    :2022/01/03 01:57:32
+@作者    :None
+@版本    :1.0
+@说明    :一级主页
+'''
+
+
+from ui.own.frame_theme import MyFrame
+from ui.statusbar import UiStatusBar
+
+
+class Win_Pcs_Status_Bar(UiStatusBar, MyFrame):
+    def __init__(self, parent=None):
+        super(Win_Pcs_Status_Bar, self).__init__(parent)
+        self.setupUi(self)
+
+        self.label_pcs_version.setText("Version:v1.0.0")

+ 0 - 0
worker/__init__.py


+ 141 - 0
worker/pcs_work.py

@@ -0,0 +1,141 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+'''
+@文件    :ccuWork.py
+@时间    :2021/12/09 08:31:48
+@作者    :None
+@版本    :1.0
+@说明    :二级BMS工作线程
+'''
+
+from utils.globalvar import SD
+from utils.qt import QObject, Signal
+import utils.modbus.defines as cst
+from utils.delay import m_delay
+
+
+class PcsCanReceived(QObject):
+
+    show_90_signal = Signal(object)
+    show_91_signal = Signal(object)
+    show_92_signal = Signal(object)
+    show_93_signal = Signal(object)
+
+    def __init__(self):
+        super(PcsCanReceived, self).__init__()
+
+    # 接收数据
+    def received(self):
+        while SD.CAN_ON_OFF:
+            restNum = SD.CAN_CONTROL.get_undeal_number()
+            if restNum <= 0:
+                continue
+            revRet = SD.CAN_CONTROL.receive(restNum)
+            for i in revRet:
+                if int(i.RemoteFlag) != 0:
+                    continue
+
+                if i.ID == 0x18903F00:
+                    self.show_90_signal.emit(i.Data)
+                elif i.ID == 0x18913F00:
+                    self.show_91_signal.emit(i.Data)
+                elif i.ID == 0x18923F00:
+                    self.show_92_signal.emit(i.Data)
+                elif i.ID == 0x18933F00:
+                    self.show_93_signal.emit(i.Data)
+                else:
+                    continue
+
+
+class PcsComWork(QObject):
+
+    show_00_com_signal = Signal(int)
+    show_01_com_signal = Signal(int)
+    show_02_com_signal = Signal(int)
+    show_03_com_signal = Signal(int)
+    show_04_com_signal = Signal(int)
+    show_05_com_signal = Signal(int)
+    show_06_com_signal = Signal(int)
+    show_07_com_signal = Signal(int)
+    show_08_com_signal = Signal(int)
+    show_09_com_signal = Signal(int)
+    show_0A_com_signal = Signal(int)
+    show_0B_com_signal = Signal(int)
+    show_0C_com_signal = Signal(int)
+    show_0D_com_signal = Signal(int)
+    show_0E_com_signal = Signal(int)
+    show_0F_com_signal = Signal(int)
+
+    def __init__(self):
+        super(PcsComWork, self).__init__()
+
+    # 接收数据
+    def work(self):
+        while SD.CAN_ON_OFF:
+            ret = SD.COM_CONTROL.send(1, cst.READ_HOLDING_REGISTERS, 0x0100, 1)
+            self.show_00_com_signal.emit(ret[1] << 8 | ret[0])
+            m_delay(10)
+
+            ret = SD.COM_CONTROL.send(1, cst.READ_HOLDING_REGISTERS, 0x0101, 1)
+            self.show_01_com_signal.emit(ret[1] << 8 | ret[0])
+            m_delay(10)
+
+            ret = SD.COM_CONTROL.send(1, cst.READ_HOLDING_REGISTERS, 0x0102, 1)
+            self.show_02_com_signal.emit(ret[1] << 8 | ret[0])
+            m_delay(10)
+
+            ret = SD.COM_CONTROL.send(1, cst.READ_HOLDING_REGISTERS, 0x0103, 1)
+            self.show_03_com_signal.emit(ret[1] << 8 | ret[0])
+            m_delay(10)
+
+            ret = SD.COM_CONTROL.send(1, cst.READ_HOLDING_REGISTERS, 0x0104, 1)
+            self.show_04_com_signal.emit(ret[1] << 8 | ret[0])
+            m_delay(10)
+
+            ret = SD.COM_CONTROL.send(1, cst.READ_HOLDING_REGISTERS, 0x0105, 1)
+            self.show_05_com_signal.emit(ret[1] << 8 | ret[0])
+            m_delay(10)
+
+            ret = SD.COM_CONTROL.send(1, cst.READ_HOLDING_REGISTERS, 0x0106, 1)
+            self.show_06_com_signal.emit(ret[1] << 8 | ret[0])
+            m_delay(10)
+
+            ret = SD.COM_CONTROL.send(1, cst.READ_HOLDING_REGISTERS, 0x0107, 1)
+            self.show_07_com_signal.emit(ret[1] << 8 | ret[0])
+            m_delay(10)
+
+            ret = SD.COM_CONTROL.send(1, cst.READ_HOLDING_REGISTERS, 0x0108, 1)
+            self.show_08_com_signal.emit(ret[1] << 8 | ret[0])
+            m_delay(10)
+
+            ret = SD.COM_CONTROL.send(1, cst.READ_HOLDING_REGISTERS, 0x0109, 1)
+            self.show_09_com_signal.emit(ret[1] << 8 | ret[0])
+            m_delay(10)
+
+            ret = SD.COM_CONTROL.send(1, cst.READ_HOLDING_REGISTERS, 0x010A, 1)
+            self.show_0A_com_signal.emit(ret[1] << 8 | ret[0])
+            m_delay(10)
+
+            ret = SD.COM_CONTROL.send(1, cst.READ_HOLDING_REGISTERS, 0x010B, 5)
+            self.show_0B_com_signal.emit(ret[1] << 8 | ret[0])
+            self.show_0C_com_signal.emit(ret[3] << 8 | ret[2])
+            self.show_0D_com_signal.emit(ret[5] << 8 | ret[4])
+            self.show_0E_com_signal.emit(ret[7] << 8 | ret[6])
+            self.show_0F_com_signal.emit(ret[9] << 8 | ret[8])
+            m_delay(10)
+
+            # ret = SD.COM_CONTROL.send(1, cst.READ_HOLDING_REGISTERS, 0x010C, 1)
+            # self.show_0C_com_signal.emit(ret[1] << 8 | ret[0])
+            # m_delay(10)
+
+            # ret = SD.COM_CONTROL.send(1, cst.READ_HOLDING_REGISTERS, 0x010D, 1)
+            # self.show_0D_com_signal.emit(ret[1] << 8 | ret[0])
+            # m_delay(10)
+
+            # ret = SD.COM_CONTROL.send(1, cst.READ_HOLDING_REGISTERS, 0x010E, 1)
+            # self.show_0E_com_signal.emit(ret[1] << 8 | ret[0])
+            # m_delay(10)
+
+            # ret = SD.COM_CONTROL.send(1, cst.READ_HOLDING_REGISTERS, 0x010F, 1)
+            # self.show_0F_com_signal.emit(ret[1] << 8 | ret[0])
+            # m_delay(10)