├── .github └── workflows │ ├── pack.yml │ ├── pack_linux.yml │ ├── release_linux.yml │ ├── release_macos.yml │ └── release_windows.yml ├── .gitignore ├── COMTool ├── Combobox.py ├── Main.py ├── __init__.py ├── assets │ ├── RaspberryPiScreenshot.png │ ├── arrow-down.png │ ├── arrow-left-white.png │ ├── arrow-left.png │ ├── arrow-right-white.png │ ├── arrow-right.png │ ├── close.png │ ├── donate_alipay.jpg │ ├── donate_wechat.jpg │ ├── help-white.png │ ├── help.png │ ├── language-white.png │ ├── language.png │ ├── left.png │ ├── logo.icns │ ├── logo.ico │ ├── logo.png │ ├── logo2.png │ ├── qss │ │ ├── style-dark.qss │ │ └── style-light.qss │ ├── right.png │ ├── screenshot_V1.0.png │ ├── screenshot_V1.3.png │ ├── screenshot_V1.4_night.png │ ├── screenshot_V1.7.png │ ├── screenshot_graph.png │ ├── screenshot_macos.jpg │ ├── screenshot_protocol_v2.3.png │ ├── screenshot_terminal.png │ ├── screenshot_v2.png │ ├── screenshot_v2_white.png │ ├── skin-white.png │ ├── skin.png │ └── tcp_udp.png ├── autoUpdate.py ├── babel.cfg ├── conn │ ├── __init__.py │ ├── base.py │ ├── conn_serial.py │ ├── conn_ssh.py │ ├── conn_tcp_udp.py │ └── test_tcp_udp.py ├── helpAbout.py ├── i18n.py ├── locales │ ├── en │ │ └── LC_MESSAGES │ │ │ └── messages.po │ ├── ja │ │ └── LC_MESSAGES │ │ │ └── messages.po │ ├── messages.pot │ ├── zh_CN │ │ └── LC_MESSAGES │ │ │ └── messages.po │ └── zh_TW │ │ └── LC_MESSAGES │ │ └── messages.po ├── logger.py ├── main2.py ├── parameters.py ├── pluginItems.py ├── plugins │ ├── __init__.py │ ├── base.py │ ├── crc.py │ ├── dbg.py │ ├── graph.py │ ├── graph_protocol.py │ ├── graph_widget_metasenselite.py │ ├── graph_widgets.py │ ├── graph_widgets_base.py │ ├── myplugin.py │ ├── myplugin2 │ │ ├── README.md │ │ ├── comtool_plugin_myplugin2 │ │ │ ├── __init__.py │ │ │ ├── locales │ │ │ │ ├── en │ │ │ │ │ └── LC_MESSAGES │ │ │ │ │ │ └── messages.po │ │ │ │ ├── ja │ │ │ │ │ └── LC_MESSAGES │ │ │ │ │ │ └── messages.po │ │ │ │ ├── messages.pot │ │ │ │ ├── zh_CN │ │ │ │ │ └── LC_MESSAGES │ │ │ │ │ │ └── messages.po │ │ │ │ └── zh_TW │ │ │ │ │ └── LC_MESSAGES │ │ │ │ │ └── messages.po │ │ │ ├── myplugin2.py │ │ │ └── plugin_i18n.py │ │ └── setup.py │ ├── protocol.py │ ├── protocols.py │ └── terminal.py ├── protocols │ └── maix-smart.py ├── qta_icon_browser.py ├── settings.py ├── test.py ├── utils.py ├── utils_ui.py ├── version.py ├── wave.py ├── widgets.py └── win32_utils.py ├── LICENSE ├── MANIFEST.in ├── Pipfile ├── README.MD ├── README_ZH.MD ├── cxsetup.py ├── docs ├── dev.md ├── plugins.md └── plugins_zh.md ├── pack.py ├── requirements.txt ├── setup.cfg ├── setup.py └── tool ├── comtool.desktop ├── send_curve_demo.py └── test.sh /.github/workflows/pack.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: pack-win-mac 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ master ] 12 | # Allows you to run this workflow manually from the Actions tab 13 | workflow_dispatch: 14 | 15 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 16 | jobs: 17 | # This workflow contains a single job called "build" 18 | build: 19 | name: test pack task 20 | # The type of runner that the job will run on 21 | strategy: 22 | matrix: 23 | os: ["windows-latest", "macos-latest", "macos-13"] 24 | python-version: ["3.8", "3.9", "3.10"] # must use str, not int, or 3.10 will be recognized as 3.1 25 | runs-on: ${{ matrix.os }} 26 | # Steps represent a sequence of tasks that will be executed as part of the job 27 | steps: 28 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 29 | - name: checkout code from github 30 | uses: actions/checkout@v2 31 | 32 | - name: Set up Python 33 | uses: actions/setup-python@v2 34 | with: 35 | python-version: ${{ matrix.python-version }} 36 | 37 | # Runs a set of commands using the runners shell 38 | - name: test pack 39 | run: | 40 | pip3 install -r requirements.txt 41 | pip3 install pyinstaller wheel pyinstaller-hooks-contrib 42 | python setup.py sdist bdist_wheel 43 | python pack.py 44 | -------------------------------------------------------------------------------- /.github/workflows/pack_linux.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: pack-linux 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ master ] 12 | # Allows you to run this workflow manually from the Actions tab 13 | workflow_dispatch: 14 | 15 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 16 | jobs: 17 | # This workflow contains a single job called "build" 18 | build: 19 | name: test pack task 20 | # The type of runner that the job will run on 21 | strategy: 22 | matrix: 23 | os: ["ubuntu-latest", "ubuntu-20.04"] 24 | python-version: ["3.8", "3.9", "3.10"] # must use str, not int, or 3.10 will be recognized as 3.1 25 | runs-on: ${{ matrix.os }} 26 | # Steps represent a sequence of tasks that will be executed as part of the job 27 | steps: 28 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 29 | - name: checkout code from github 30 | uses: actions/checkout@v2 31 | 32 | - name: Set up Python 33 | uses: actions/setup-python@v2 34 | with: 35 | python-version: ${{ matrix.python-version }} 36 | 37 | # Runs a set of commands using the runners shell 38 | - name: test pack 39 | run: | 40 | python --version 41 | export QT_DEBUG_PLUGINS=1 42 | sudo apt-get update 43 | DEBIAN_FRONTEND=noninteractive sudo apt-get install -y --no-install-recommends \ 44 | xvfb \ 45 | x11-utils \ 46 | libxkbcommon-x11-0 \ 47 | libxcb-icccm4 \ 48 | libxcb-image0 \ 49 | libxcb-keysyms1 \ 50 | libxcb-randr0 \ 51 | libxcb-render-util0 \ 52 | libxcb-xkb1 \ 53 | libegl1-mesa \ 54 | libxcb-xinerama0 \ 55 | libglib2.0-0 \ 56 | libopengl0 57 | pip3 install -r requirements.txt 58 | pip3 install -U pyinstaller wheel pyinstaller-hooks-contrib 59 | python setup.py sdist bdist_wheel 60 | xvfb-run python pack.py 61 | -------------------------------------------------------------------------------- /.github/workflows/release_linux.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: release for linux 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | release: 9 | types: [published] 10 | # Allows you to run this workflow manually from the Actions tab 11 | workflow_dispatch: 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | # This workflow contains a single job called "build" 16 | build: 17 | name: release and upload assets task 18 | # The type of runner that the job will run on 19 | strategy: 20 | matrix: 21 | python-version: ["3.9"] # must use str, not int, or 3.10 will be recognized as 3.1 22 | os: ["ubuntu-latest", "ubuntu-20.04"] 23 | runs-on: ${{ matrix.os }} 24 | # Steps represent a sequence of tasks that will be executed as part of the job 25 | steps: 26 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 27 | - name: checkout code from github 28 | uses: actions/checkout@v2 29 | 30 | - name: Set up Python 31 | uses: actions/setup-python@v2 32 | with: 33 | python-version: ${{ matrix.python-version }} 34 | 35 | # Runs a set of commands using the runners shell 36 | - name: pack 37 | id: pack 38 | run: | 39 | python --version 40 | export QT_DEBUG_PLUGINS=1 41 | sudo apt-get update 42 | DEBIAN_FRONTEND=noninteractive sudo apt-get install -y --no-install-recommends \ 43 | xvfb \ 44 | x11-utils \ 45 | libxkbcommon-x11-0 \ 46 | libxcb-icccm4 \ 47 | libxcb-image0 \ 48 | libxcb-keysyms1 \ 49 | libxcb-randr0 \ 50 | libxcb-render-util0 \ 51 | libxcb-xkb1 \ 52 | libegl1-mesa \ 53 | libxcb-xinerama0 \ 54 | libglib2.0-0 \ 55 | libopengl0 56 | pip3 install -r requirements.txt 57 | pip3 install pyinstaller wheel 58 | python setup.py sdist bdist_wheel 59 | xvfb-run python pack.py 60 | release_path=`python pack.py ${{ matrix.os }}` 61 | echo $release_path 62 | release_name=`echo $release_path | awk -F"/" '{print $NF}'` 63 | echo ::set-output name=release_path::$release_path 64 | echo ::set-output name=release_name::$release_name 65 | - name: Upload to release 66 | uses: svenstaro/upload-release-action@v2 67 | with: 68 | file: ${{ steps.pack.outputs.release_path }} 69 | asset_name: ${{ steps.pack.outputs.release_name }} 70 | tag: ${{ github.ref }} 71 | repo_token: ${{ secrets.GITHUB_TOKEN }} 72 | 73 | -------------------------------------------------------------------------------- /.github/workflows/release_macos.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: release for macos 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | release: 9 | types: [published] 10 | # Allows you to run this workflow manually from the Actions tab 11 | workflow_dispatch: 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | # This workflow contains a single job called "build" 16 | build: 17 | name: release and upload assets task 18 | # The type of runner that the job will run on 19 | strategy: 20 | matrix: 21 | python-version: ["3.9"] # must use str, not int, or 3.10 will be recognized as 3.1 22 | os: ["macos-latest", "macos-13"] 23 | runs-on: ${{ matrix.os }} 24 | # Steps represent a sequence of tasks that will be executed as part of the job 25 | steps: 26 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 27 | - name: checkout code from github 28 | uses: actions/checkout@v2 29 | 30 | - name: Set up Python 31 | uses: actions/setup-python@v2 32 | with: 33 | python-version: ${{ matrix.python-version }} 34 | 35 | # Runs a set of commands using the runners shell 36 | - name: pack 37 | id: pack 38 | run: | 39 | python --version 40 | pip3 install -r requirements.txt 41 | pip3 install pyinstaller wheel pyinstaller-hooks-contrib 42 | python setup.py sdist bdist_wheel 43 | python pack.py 44 | release_path=`python pack.py ${{ matrix.os }}` 45 | echo $release_path 46 | release_name=`echo $release_path | awk -F"/" '{print $NF}'` 47 | echo ::set-output name=release_path::$release_path 48 | echo ::set-output name=release_name::$release_name 49 | - name: Upload to release 50 | uses: svenstaro/upload-release-action@v2 51 | with: 52 | file: ${{ steps.pack.outputs.release_path }} 53 | asset_name: ${{ steps.pack.outputs.release_name }} 54 | tag: ${{ github.ref }} 55 | repo_token: ${{ secrets.GITHUB_TOKEN }} 56 | 57 | -------------------------------------------------------------------------------- /.github/workflows/release_windows.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: release windows 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | release: 9 | types: [published] 10 | # Allows you to run this workflow manually from the Actions tab 11 | workflow_dispatch: 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | # This workflow contains a single job called "build" 16 | build: 17 | name: release and upload assets task 18 | # The type of runner that the job will run on 19 | strategy: 20 | matrix: 21 | python-version: ["3.9"] # must use str, not int, or 3.10 will be recognized as 3.1 22 | os: [windows-latest] 23 | runs-on: ${{ matrix.os }} 24 | # Steps represent a sequence of tasks that will be executed as part of the job 25 | steps: 26 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 27 | - name: checkout code from github 28 | uses: actions/checkout@v2 29 | 30 | - name: Set up Python 31 | uses: actions/setup-python@v2 32 | with: 33 | python-version: ${{ matrix.python-version }} 34 | 35 | # Runs a set of commands using the runners shell 36 | - name: pack 37 | id: pack 38 | run: | 39 | python --version 40 | pip3 install -r requirements.txt 41 | pip3 install pyinstaller wheel pyinstaller-hooks-contrib 42 | python setup.py sdist bdist_wheel 43 | python pack.py 44 | $release_path = python pack.py ${{ matrix.os }} 45 | echo "::set-output name=release_path::$release_path" 46 | - name: Upload to release 47 | uses: svenstaro/upload-release-action@v2 48 | with: 49 | file: ${{ steps.pack.outputs.release_path }} 50 | asset_name: ${{ steps.pack.outputs.release_path }} 51 | tag: ${{ github.ref }} 52 | repo_token: ${{ secrets.GITHUB_TOKEN }} 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .vscode 3 | .idea 4 | *.config 5 | 6 | bin 7 | dist 8 | build 9 | *.egg-info 10 | *.pyc 11 | 12 | *.spec 13 | .DS_Store 14 | 15 | venv 16 | config.json 17 | 18 | *.mo 19 | comtool.log 20 | comtool.*.json 21 | *.tar.xz 22 | *.dmg 23 | *.zip 24 | *.log 25 | config.*.json 26 | comtool_*_v*.* 27 | -------------------------------------------------------------------------------- /COMTool/Combobox.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import QComboBox,QListView 2 | from PyQt5.QtCore import pyqtSignal 3 | 4 | 5 | class ComboBox(QComboBox): 6 | clicked = pyqtSignal() 7 | # popupAboutToBeShown = pyqtSignal() 8 | 9 | def __init__(self): 10 | QComboBox.__init__(self) 11 | listView = QListView() 12 | listView.executeDelayedItemsLayout() 13 | self.setView(listView) 14 | 15 | def mouseReleaseEvent(self, QMouseEvent): 16 | self.showItems() 17 | 18 | def showPopup(self): 19 | # self.popupAboutToBeShown.emit() 20 | # prevent show popup, manually call it in mouse release event 21 | pass 22 | 23 | def _showPopup(self): 24 | max_w = 0 25 | for i in range(self.count()): 26 | w = self.view().sizeHintForColumn(i) 27 | if w > max_w: 28 | max_w = w 29 | self.view().setMinimumWidth(max_w + 50) 30 | super(ComboBox, self).showPopup() 31 | 32 | def showItems(self): 33 | self._showPopup() 34 | 35 | def mousePressEvent(self, QMouseEvent): 36 | self.clicked.emit() 37 | 38 | 39 | -------------------------------------------------------------------------------- /COMTool/Main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | try: 4 | from main2 import main 5 | from parameters import log 6 | except Exception: 7 | from COMTool.main2 import main 8 | from COMTool.parameters import log 9 | 10 | def restart_program(): 11 | ''' 12 | restart program, not return 13 | ''' 14 | python = sys.executable 15 | log.i("Restarting program, comand: {} {} {}".format(python, python, *sys.argv)) 16 | os.execl(python, python, * sys.argv) 17 | 18 | if __name__ == '__main__': 19 | while 1: 20 | ret = main() 21 | if not ret is None: 22 | break 23 | restart_program() 24 | print("-- program exit, code:", ret) 25 | sys.exit(ret) 26 | -------------------------------------------------------------------------------- /COMTool/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from . import version 3 | from .version import __version__ 4 | 5 | -------------------------------------------------------------------------------- /COMTool/assets/RaspberryPiScreenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neutree/COMTool/63fa45cca9bff2072ac43b9766e31f1983756fa3/COMTool/assets/RaspberryPiScreenshot.png -------------------------------------------------------------------------------- /COMTool/assets/arrow-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neutree/COMTool/63fa45cca9bff2072ac43b9766e31f1983756fa3/COMTool/assets/arrow-down.png -------------------------------------------------------------------------------- /COMTool/assets/arrow-left-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neutree/COMTool/63fa45cca9bff2072ac43b9766e31f1983756fa3/COMTool/assets/arrow-left-white.png -------------------------------------------------------------------------------- /COMTool/assets/arrow-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neutree/COMTool/63fa45cca9bff2072ac43b9766e31f1983756fa3/COMTool/assets/arrow-left.png -------------------------------------------------------------------------------- /COMTool/assets/arrow-right-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neutree/COMTool/63fa45cca9bff2072ac43b9766e31f1983756fa3/COMTool/assets/arrow-right-white.png -------------------------------------------------------------------------------- /COMTool/assets/arrow-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neutree/COMTool/63fa45cca9bff2072ac43b9766e31f1983756fa3/COMTool/assets/arrow-right.png -------------------------------------------------------------------------------- /COMTool/assets/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neutree/COMTool/63fa45cca9bff2072ac43b9766e31f1983756fa3/COMTool/assets/close.png -------------------------------------------------------------------------------- /COMTool/assets/donate_alipay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neutree/COMTool/63fa45cca9bff2072ac43b9766e31f1983756fa3/COMTool/assets/donate_alipay.jpg -------------------------------------------------------------------------------- /COMTool/assets/donate_wechat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neutree/COMTool/63fa45cca9bff2072ac43b9766e31f1983756fa3/COMTool/assets/donate_wechat.jpg -------------------------------------------------------------------------------- /COMTool/assets/help-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neutree/COMTool/63fa45cca9bff2072ac43b9766e31f1983756fa3/COMTool/assets/help-white.png -------------------------------------------------------------------------------- /COMTool/assets/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neutree/COMTool/63fa45cca9bff2072ac43b9766e31f1983756fa3/COMTool/assets/help.png -------------------------------------------------------------------------------- /COMTool/assets/language-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neutree/COMTool/63fa45cca9bff2072ac43b9766e31f1983756fa3/COMTool/assets/language-white.png -------------------------------------------------------------------------------- /COMTool/assets/language.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neutree/COMTool/63fa45cca9bff2072ac43b9766e31f1983756fa3/COMTool/assets/language.png -------------------------------------------------------------------------------- /COMTool/assets/left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neutree/COMTool/63fa45cca9bff2072ac43b9766e31f1983756fa3/COMTool/assets/left.png -------------------------------------------------------------------------------- /COMTool/assets/logo.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neutree/COMTool/63fa45cca9bff2072ac43b9766e31f1983756fa3/COMTool/assets/logo.icns -------------------------------------------------------------------------------- /COMTool/assets/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neutree/COMTool/63fa45cca9bff2072ac43b9766e31f1983756fa3/COMTool/assets/logo.ico -------------------------------------------------------------------------------- /COMTool/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neutree/COMTool/63fa45cca9bff2072ac43b9766e31f1983756fa3/COMTool/assets/logo.png -------------------------------------------------------------------------------- /COMTool/assets/logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neutree/COMTool/63fa45cca9bff2072ac43b9766e31f1983756fa3/COMTool/assets/logo2.png -------------------------------------------------------------------------------- /COMTool/assets/qss/style-dark.qss: -------------------------------------------------------------------------------- 1 | .warning { 2 | background: #fb8c00; 3 | color: #0772ca; 4 | } 5 | 6 | MainWindow { 7 | background-color:#212121; 8 | color:#bcbcbd; 9 | min-width:100px; 10 | } 11 | QWidget { 12 | background-color:#212121; 13 | } 14 | QMessageBox { 15 | background-color:#212121; 16 | } 17 | QWidget { 18 | color:#bcbcbd; 19 | font-family: "Microsoft YaHei",Tahoma,Optima,"Trebuchet MS"; 20 | } 21 | .TitleBar { 22 | background-color: #212121; 23 | color: white; 24 | } 25 | .TitleBar QPushButton { 26 | border: none; 27 | border-radius: 0; 28 | min-width: 35; 29 | min-height: 35; 30 | } 31 | .TitleBar QPushButton:hover { 32 | border: none; 33 | border-radius: 0; 34 | } 35 | .TitleBar .icon{ 36 | margin-left: 5; 37 | background-color: transparent; 38 | } 39 | .TitleBar .title{ 40 | margin-left: 5; 41 | color: white; 42 | background-color: transparent; 43 | } 44 | .TitleBar .top{ 45 | margin-left: 5; 46 | color: white; 47 | border-radius: 20; 48 | background-color: transparent; 49 | } 50 | .TitleBar .top:hover{ 51 | background-color: #273b4e; 52 | } 53 | .TitleBar .topActive{ 54 | margin-left: 5; 55 | color: white; 56 | border-radius: 20; 57 | background-color: #273b4e; 58 | } 59 | .TitleBar .min{ 60 | background-color: transparent; 61 | color: white; 62 | } 63 | .TitleBar .max{ 64 | background-color: transparent; 65 | color: white;} 66 | .TitleBar .close{ 67 | background-color: transparent; 68 | color: white; 69 | } 70 | .TitleBar .min:hover { 71 | background-color: #2ba13e; 72 | } 73 | .TitleBar .max:hover { 74 | background-color: #cf9001; 75 | } 76 | .TitleBar .close:hover { 77 | background-color: #df2f25; 78 | } 79 | .menuItem { 80 | min-height:27px; 81 | height:27px; 82 | min-width:27px; 83 | width:27px; 84 | border-radius: 5px; 85 | background-color: transparent; 86 | } 87 | .menuItem:hover { 88 | background-color: transparent; 89 | } 90 | #menuItem1 { 91 | margin-right:10px; 92 | border-image: url("$DataPath/assets/arrow-left.png") 93 | } 94 | #menuItem1:hover { 95 | border-image: url("$DataPath/assets/arrow-left-white.png") 96 | } 97 | #menuItem2 { 98 | margin-right:10px; 99 | min-height:27px; 100 | min-width:27px; 101 | border-image: url("$DataPath/assets/skin.png") 102 | } 103 | #menuItem2:hover { 104 | border-image: url("$DataPath/assets/skin-white.png") 105 | } 106 | #menuItem3 { 107 | min-height:27px; 108 | min-width:27px; 109 | border-image: url("$DataPath/assets/help.png") 110 | } 111 | #menuItem3:hover { 112 | border-image: url("$DataPath/assets/help-white.png") 113 | } 114 | #menuItemLang { 115 | margin-right:10px; 116 | min-height:27px; 117 | min-width:27px; 118 | border-image: url("$DataPath/assets/language.png") 119 | } 120 | #menuItemLang:hover { 121 | border-image: url("$DataPath/assets/language-white.png") 122 | } 123 | #menuItem4 { 124 | min-height:27px; 125 | min-width:27px; 126 | margin-right:0px; 127 | border-image: url("$DataPath/assets/arrow-right.png") 128 | } 129 | #menuItem4:hover { 130 | border-image: url("$DataPath/assets/arrow-right-white.png") 131 | } 132 | 133 | .settingWidget QComboBox{ 134 | width:60px; 135 | min-width:60px; 136 | } 137 | QComboBox { 138 | border: 1px solid #272727; 139 | border-radius: 5px; 140 | padding: 1px 1px 1px 3px; 141 | min-height:25px; 142 | min-width:60px; 143 | } 144 | 145 | QComboBox:editable { 146 | background: #3a3a3a; 147 | } 148 | 149 | QComboBox:disabled { 150 | color:gray; 151 | } 152 | 153 | QComboBox:!editable, QComboBox::drop-down:editable { 154 | background: #3a3a3a; 155 | } 156 | 157 | /* QComboBox gets the "on" state when the popup is open */ 158 | QComboBox:!editable:on, QComboBox::drop-down:editable:on { 159 | background: #3a3a3a; 160 | } 161 | 162 | QComboBox:on { /* shift the text when the popup opens */ 163 | padding-top: 2px; 164 | padding-left: 2px; 165 | } 166 | 167 | QComboBox::drop-down { 168 | subcontrol-origin: padding; 169 | subcontrol-position: top right; 170 | width: 18px; 171 | 172 | border-left-width: 0px; 173 | border-left-color: #524a4a; 174 | border-left-style: solid; /* just a single line */ 175 | border-top-right-radius: 5px; /* same radius as the QComboBox */ 176 | border-bottom-right-radius: 5px; 177 | } 178 | 179 | QComboBox::down-arrow { 180 | image:url($DataPath/assets/arrow-down.png); 181 | } 182 | 183 | QComboBox::down-arrow:on { /* shift the arrow when popup is open */ 184 | top: 1px; 185 | left: 1px; 186 | } 187 | QComboBox::hover{ 188 | border: 2px solid #26a2ff; 189 | } 190 | QComboBox QAbstractItemView { 191 | border: 2px solid #3a3a3a; 192 | selection-background-color: #26a2ff; 193 | selection-color:#bcbcbd; 194 | /* min-width:400px; 195 | min-height:10em; */ 196 | } 197 | QComboBox QAbstractItemView::item{ 198 | min-height:3em; 199 | } 200 | 201 | 202 | 203 | QGroupBox { 204 | border: 2px solid #2b2b2b; 205 | border-radius: 5px; 206 | padding:0px; 207 | margin-top: 2ex; /* leave space at the top for the title */ 208 | } 209 | 210 | QGroupBox::title { 211 | subcontrol-origin: margin; 212 | subcontrol-position: top left; /* position at the top center */ 213 | padding: 0px 1px; 214 | } 215 | 216 | 217 | QPushButton { 218 | font-family:Tahoma,Optima,"Trebuchet MS"; 219 | border: 2px solid #0865b1; 220 | border-radius: 5px; 221 | background-color: #0865b1; 222 | min-width: 80px; 223 | height:30px; 224 | min-height:25px; 225 | } 226 | 227 | QPushButton:disabled { 228 | border: 2px solid #2e2d2d; 229 | background-color: #2e2d2d; 230 | color: #838383; 231 | } 232 | .TitleBar QPushButton:disabled { 233 | border: none; 234 | background-color: #2e2d2d; 235 | } 236 | 237 | 238 | QPushButton:hover { 239 | background-color: #0f88eb; 240 | border: 2px solid #0f88eb; 241 | color:white; 242 | } 243 | QPushButton:pressed { 244 | background-color: #044174; 245 | } 246 | 247 | QPushButton:flat { 248 | border: none; /* no border for a flat push button */ 249 | } 250 | 251 | QPushButton:default { 252 | border-color: #0772ca; /* make the default button prominent */ 253 | } 254 | .deleteBtn { 255 | min-width: 10px; 256 | min-height: 10px; 257 | width: 20px; 258 | height: 20px; 259 | background: #b94545; 260 | border-radius: 12px; 261 | border: 2px solid #b94545; 262 | } 263 | .deleteBtn:hover { 264 | border-radius: 12px; 265 | background: #861818; 266 | border: 2px solid #861818; 267 | } 268 | .smallBtn { 269 | min-width: 30px; 270 | } 271 | .smallBtn2 { 272 | min-width: 24px; 273 | min-height: 24px; 274 | width: 24px; 275 | height: 24px; 276 | } 277 | .smallBtn3 { 278 | min-width: 24px; 279 | min-height: 24px; 280 | max-width: 24px; 281 | max-height: 24px; 282 | width: 24px; 283 | height: 24px; 284 | } 285 | .bigBtn { 286 | min-height: 100px; 287 | } 288 | 289 | .remark { 290 | min-width: 10px; 291 | min-height: 10px; 292 | height: 20px; 293 | } 294 | 295 | QTextEdit, QPlainTextEdit, QListView { 296 | background-color: #3a3a3a; 297 | border: 2px solid #3a3a3a; 298 | border-radius: 5px; 299 | background-attachment: scroll; 300 | } 301 | 302 | QLineEdit { 303 | border: 1px solid #3a3a3a; 304 | border-radius: 5px; 305 | padding: 0 8px; 306 | background: #3a3a3a; 307 | selection-background-color: #373277; 308 | height: 30px; 309 | } 310 | .smallInput { 311 | height: 18px; 312 | } 313 | 314 | QCheckBox { 315 | } 316 | QCheckBox:disabled { 317 | color: gray; 318 | } 319 | QCheckBox::indicator { 320 | width: 13px; 321 | height: 13px; 322 | } 323 | 324 | QCheckBox::indicator:unchecked { 325 | background-color: #6b6a6a; 326 | } 327 | 328 | QCheckBox::indicator:checked { 329 | background-color: #008000; 330 | } 331 | 332 | QRadioButton::indicator { 333 | width: 13px; 334 | height: 13px; 335 | } 336 | 337 | QRadioButton::indicator:unchecked { 338 | border-radius:6px; 339 | background-color: #6b6a6a; 340 | } 341 | 342 | QRadioButton::indicator::checked { 343 | border-radius:6px; 344 | background-color: #7777cc; 345 | } 346 | 347 | QToolTip { 348 | border: 0px solid #0772ca; 349 | padding: 5px; 350 | color: white; 351 | background: #7777cc; 352 | } 353 | 354 | .statusBar { 355 | border:none; 356 | max-height: 30; 357 | } 358 | 359 | QScrollBar { 360 | border: none; 361 | background: #3a3a3a; 362 | border-radius: 5px; 363 | } 364 | QScrollBar:vertical { 365 | width: 10px; 366 | } 367 | QScrollBar:horizontal { 368 | height: 10px; 369 | } 370 | QScrollBar::handle { 371 | background: #212121; 372 | border-radius: 5px; 373 | } 374 | QScrollBar::add-line { 375 | border: none; 376 | } 377 | QScrollBar::sub-line { 378 | border: none; 379 | } 380 | QScrollBar::up-arrow, QScrollBar::down-arrow { 381 | border: none; 382 | background-color: #3a3a3a; 383 | } 384 | QScrollBar::add-page, QScrollBar::sub-page { 385 | background: none; 386 | background-color: #3a3a3a; 387 | } 388 | .scrollbar2, .scrollbar2 QScrollBar { 389 | background: #212121; 390 | } 391 | .scrollbar2::handle, .scrollbar2 QScrollBar::handle { 392 | background: #3a3a3a; 393 | } 394 | .scrollbar2::add-page, .scrollbar2::sub-page, .scrollbar2 QScrollBar::add-page, .scrollbar2 QScrollBar::sub-page { 395 | background-color: #212121; 396 | } 397 | QScrollArea { 398 | border: none; 399 | padding: 0; 400 | margin: 0; 401 | } 402 | 403 | 404 | QTabWidget::pane { /* The tab widget frame */ 405 | /* border-top: 2px solid #e6e6e6; */ 406 | } 407 | 408 | QTabWidget::tab-bar { 409 | /* left: 5px; /* move to the right by 5px */ 410 | alignment: left; 411 | } 412 | 413 | QTabBar::close-button { 414 | subcontrol-position: right; 415 | border-radius: 7px; 416 | background: #474747; 417 | image: url("$DataPath/assets/close.png") 418 | } 419 | QTabBar::close-button:hover { 420 | background: #8b1c1c; 421 | image: url("$DataPath/assets/close.png") 422 | } 423 | QTabBar QToolButton { 424 | border-radius: 3px; 425 | background-color: #0865b1; 426 | } 427 | QTabBar QToolButton:hover { 428 | background-color: #114977; 429 | } 430 | QTabBar QToolButton:disabled { 431 | background-color: #242b31; 432 | } 433 | QTabBar QToolButton::right-arrow { /* the arrow mark in the tool buttons */ 434 | image: url("$DataPath/assets/right.png") 435 | } 436 | 437 | QTabBar QToolButton::left-arrow { 438 | image: url("$DataPath/assets/left.png") 439 | } 440 | 441 | /* Style the tab using the tab sub-control. Note that 442 | it reads QTabBar _not_ QTabWidget */ 443 | QTabBar::tab { 444 | color: #bcbcbd; 445 | background: #3a3a3a; 446 | border: 2px solid #3a3a3a; 447 | border-bottom: none; 448 | border-top-left-radius: 4px; 449 | border-top-right-radius: 4px; 450 | min-width: 80px; 451 | min-height: 34px; 452 | padding-left: 2ex; 453 | padding-right: 2ex; 454 | } 455 | 456 | QTabBar::tab:selected, QTabBar::tab:hover { 457 | color: #bcbcbd; 458 | border-top-color: #0663af; 459 | border-left-color: #0663af; 460 | border-right-color: #0663af; 461 | background-color: #0865b1; 462 | } 463 | 464 | QTabBar::tab:!selected { 465 | margin-top: 2px; /* make non-selected tabs look smaller */ 466 | } 467 | 468 | /* make use of negative margins for overlapping tabs */ 469 | QTabBar::tab:selected { 470 | /* expand/overlap to the left and right by 4px */ 471 | margin-left: -4px; 472 | margin-right: -4px; 473 | } 474 | 475 | QTabBar::tab:first:selected { 476 | margin-left: 0; /* the first selected tab has nothing to overlap with on the left */ 477 | } 478 | 479 | QTabBar::tab:last:selected { 480 | margin-right: 0; /* the last selected tab has nothing to overlap with on the right */ 481 | } 482 | 483 | QTabBar::tab:only-one { 484 | margin: 0; /* if there is only one tab, we don't want overlapping margins */ 485 | } 486 | 487 | .helpList, QListWidget { 488 | border: none; 489 | outline: none; 490 | } 491 | 492 | .helpList::item, QListWidget::item { 493 | border: none; 494 | height: 40px; 495 | width: 100px 496 | } 497 | .helpList { 498 | min-width: 100px; 499 | } 500 | QListWidget::item:selected { 501 | color: #1787e4; 502 | border-right: 5px solid #0865b1; 503 | background-color: #1c364b; 504 | } 505 | QListWidget::item:hover { 506 | color: #1787e4; 507 | background-color: #1c364b; 508 | } 509 | .helpList::item:selected { 510 | color: #009688; 511 | border-right: 5px solid #009688; 512 | background-color: #253836; 513 | } 514 | .helpList::item:hover { 515 | color: #009688; 516 | background-color: #253836; 517 | } 518 | 519 | .graphBtn { 520 | max-height: 120px; 521 | } 522 | -------------------------------------------------------------------------------- /COMTool/assets/qss/style-light.qss: -------------------------------------------------------------------------------- 1 | /* @font-face { 2 | font-family: "Josefin Sans"; 3 | src: url("$DataPath/assets/fonts/JosefinSans-Regular.ttf"); 4 | } */ 5 | /* bug 自定义字体发虚, 在py代码中addApplicationFont不会发虚 */ 6 | 7 | .warning { 8 | background: #fb8c00; 9 | color: #0772ca; 10 | } 11 | 12 | MainWindow { 13 | background-color:#f5f5f5; 14 | color: #464444; 15 | min-width:100px; 16 | } 17 | QWidget { 18 | background-color:#f5f5f5; 19 | color:#6b6b6b; 20 | font-family: "Microsoft YaHei",Tahoma,Optima,"Trebuchet MS"; 21 | } 22 | QMessageBox { 23 | background-color:#f5f5f5; 24 | } 25 | .TitleBar { 26 | background-color: #0b1722; 27 | color: white; 28 | } 29 | .TitleBar QPushButton { 30 | border: none; 31 | border-radius: 0; 32 | min-width: 35; 33 | min-height: 35; 34 | } 35 | .TitleBar QPushButton:hover { 36 | border: none; 37 | border-radius: 0; 38 | } 39 | .TitleBar .icon{ 40 | margin-left: 5; 41 | background-color: transparent; 42 | } 43 | .TitleBar .title{ 44 | margin-left: 5; 45 | color: white; 46 | background-color: transparent; 47 | } 48 | .TitleBar .top{ 49 | margin-left: 5; 50 | color: white; 51 | border-radius: 20; 52 | background-color: transparent; 53 | } 54 | .TitleBar .top:hover{ 55 | background-color: #273b4e; 56 | } 57 | .TitleBar .topActive{ 58 | margin-left: 5; 59 | color: white; 60 | border-radius: 20; 61 | background-color: #273b4e; 62 | } 63 | .TitleBar .min{ 64 | background-color: transparent; 65 | color: white; 66 | } 67 | .TitleBar .max{ 68 | background-color: transparent; 69 | color: white; 70 | } 71 | .TitleBar .close{ 72 | background-color: transparent; 73 | color: white; 74 | } 75 | .TitleBar .min:hover { 76 | background-color: #2ba13e; 77 | } 78 | .TitleBar .max:hover { 79 | background-color: #cf9001; 80 | } 81 | .TitleBar .close:hover { 82 | background-color: #df2f25; 83 | } 84 | .menuItem { 85 | min-height:27px; 86 | height:27px; 87 | min-width:27px; 88 | width:27px; 89 | border-radius: 5px; 90 | background-color: transparent; 91 | } 92 | .menuItem:hover { 93 | min-height:27px; 94 | height:27px; 95 | min-width:27px; 96 | width:27px; 97 | border-radius: 5px; 98 | background-color: transparent; 99 | } 100 | #menuItem1 { 101 | min-height:27px; 102 | height:27px; 103 | min-width:27px; 104 | width:27px; 105 | border-radius: 5px; 106 | margin-right:10px; 107 | background-color: #f5f5f5; 108 | border-image: url("$DataPath/assets/arrow-left.png") 109 | } 110 | #menuItem1:hover { 111 | border-image: url("$DataPath/assets/arrow-left-white.png") 112 | } 113 | #menuItem2 { 114 | min-height:27px; 115 | height:27px; 116 | min-width:27px; 117 | width:27px; 118 | border-radius: 5px; 119 | margin-right:10px; 120 | background-color: #0b1722; 121 | border-image: url("$DataPath/assets/skin.png") 122 | } 123 | #menuItem2:hover { 124 | background-color: #0b1722; 125 | border-image: url("$DataPath/assets/skin-white.png") 126 | } 127 | #menuItemLang { 128 | margin-right:10px; 129 | min-height:27px; 130 | min-width:27px; 131 | border-image: url("$DataPath/assets/language.png") 132 | } 133 | #menuItemLang:hover { 134 | border-image: url("$DataPath/assets/language-white.png") 135 | } 136 | #menuItem3 { 137 | min-height:27px; 138 | height:27px; 139 | min-width:27px; 140 | width:27px; 141 | border-radius: 5px; 142 | margin-right:10px; 143 | background-color: #0b1722; 144 | border-image: url("$DataPath/assets/help.png") 145 | } 146 | #menuItem3:hover { 147 | background-color: #0b1722; 148 | border-image: url("$DataPath/assets/help-white.png") 149 | } 150 | #menuItem4 { 151 | min-height:27px; 152 | height:27px; 153 | min-width:27px; 154 | width:27px; 155 | border-radius: 5px; 156 | margin-right:0px; 157 | background-color: #f5f5f5; 158 | border-image: url("$DataPath/assets/arrow-right.png") 159 | } 160 | #menuItem4:hover { 161 | margin-right:0px; 162 | border-image: url("$DataPath/assets/arrow-right-white.png") 163 | } 164 | 165 | .settingWidget QComboBox{ 166 | width:60px; 167 | min-width:60px; 168 | } 169 | QComboBox { 170 | border: 2px solid #dde4ec; 171 | border-radius: 5px; 172 | padding: 1px 10px 1px 3px; 173 | min-height:25px; 174 | min-width:60px; 175 | } 176 | 177 | QComboBox:editable { 178 | background: #f5f5f5; 179 | } 180 | 181 | QComboBox:!editable, QComboBox::drop-down:editable { 182 | background: #f5f5f5; 183 | } 184 | 185 | /* QComboBox gets the "on" state when the popup is open */ 186 | QComboBox:!editable:on, QComboBox::drop-down:editable:on { 187 | background: #f5f5f5; 188 | } 189 | 190 | QComboBox:on { /* shift the text when the popup opens */ 191 | padding-top: 2px; 192 | padding-left: 2px; 193 | } 194 | 195 | QComboBox::drop-down { 196 | subcontrol-origin: padding; 197 | subcontrol-position: top right; 198 | width: 18px; 199 | border-left-width: 0px; 200 | border-left-color: #b9b9b9; 201 | border-left-style: solid; /* just a single line */ 202 | border-top-right-radius: 5px; /* same radius as the QComboBox */ 203 | border-bottom-right-radius: 5px; 204 | } 205 | 206 | QComboBox::down-arrow { 207 | image:url($DataPath/assets/arrow-down.png); 208 | } 209 | 210 | QComboBox::down-arrow:on { /* shift the arrow when popup is open */ 211 | top: 1px; 212 | left: 1px; 213 | } 214 | QComboBox::hover{ 215 | border: 2px solid #26a2ff; 216 | } 217 | QComboBox QAbstractItemView { 218 | border: 2px solid #f5f5f5; 219 | selection-background-color: #26a2ff; 220 | selection-color:#f5f5f5; 221 | /* min-width:400px; */ 222 | min-height:4em; 223 | } 224 | QComboBox QAbstractItemView::item{ 225 | min-height:3em; 226 | } 227 | 228 | 229 | 230 | QGroupBox { 231 | border: 2px solid #e6e6e6; 232 | border-radius: 5px; 233 | padding:0px; 234 | margin-top: 2ex; /* leave space at the top for the title */ 235 | } 236 | QGroupBox::title { 237 | subcontrol-origin: margin; 238 | subcontrol-position: top left; /* position at the top center */ 239 | padding: 0px 1px; 240 | } 241 | 242 | 243 | QPushButton { 244 | border: 2px solid #0f88eb; 245 | border-radius: 5px; 246 | background-color: #0f88eb; 247 | min-width: 80px; 248 | height:30px; 249 | min-height:25px; 250 | color:white; 251 | } 252 | 253 | QPushButton:disabled { 254 | border: 2px solid #7bc4ff; 255 | background-color: #7bc4ff; 256 | } 257 | .TitleBar QPushButton:disabled { 258 | border: none; 259 | background-color: #203549; 260 | } 261 | 262 | QPushButton:hover { 263 | background-color: #0772ca; 264 | border: 2px solid #0772ca; 265 | border-radius: 5px; 266 | min-height:25px; 267 | color:white; 268 | } 269 | QPushButton:pressed { 270 | background-color: #045292; 271 | } 272 | 273 | QPushButton:flat { 274 | border: none; /* no border for a flat push button */ 275 | } 276 | 277 | QPushButton:default { 278 | border-color: #0772ca; /* make the default button prominent */ 279 | } 280 | .deleteBtn { 281 | min-width: 10px; 282 | min-height: 10px; 283 | width: 20px; 284 | height: 20px; 285 | background: #cf5050; 286 | border-radius: 12px; 287 | border: 2px solid #cf5050; 288 | } 289 | .deleteBtn:hover { 290 | border-radius: 12px; 291 | background: #b61313; 292 | border: 2px solid #b61313; 293 | } 294 | .smallBtn { 295 | min-width: 30px; 296 | } 297 | .smallBtn2 { 298 | min-width: 24px; 299 | min-height: 24px; 300 | width: 24px; 301 | height: 24px; 302 | } 303 | .smallBtn3 { 304 | min-width: 24px; 305 | min-height: 24px; 306 | max-width: 24px; 307 | max-height: 24px; 308 | width: 24px; 309 | height: 24px; 310 | } 311 | .bigBtn { 312 | min-height: 100px; 313 | } 314 | 315 | .remark { 316 | min-width: 10px; 317 | min-height: 10px; 318 | height: 20px; 319 | } 320 | 321 | QTextEdit, QPlainTextEdit, QListView { 322 | background-color: #f5f5f5; 323 | border: 2px solid #e6e6e6; 324 | border-radius: 5px; 325 | background-attachment: scroll; 326 | } 327 | 328 | QLineEdit { 329 | border: 2px solid #e6e6e6; 330 | border-radius: 5px; 331 | background: #f5f5f5; 332 | selection-background-color: #5951ca; 333 | height: 30px; 334 | } 335 | .smallInput { 336 | height: 18px; 337 | } 338 | 339 | QCheckBox { 340 | } 341 | 342 | QCheckBox:disabled { 343 | color: #cecaca; 344 | } 345 | 346 | QCheckBox::indicator { 347 | width: 13px; 348 | height: 13px; 349 | } 350 | 351 | QCheckBox::indicator:unchecked { 352 | background-color: #cecaca; 353 | } 354 | 355 | QCheckBox::indicator:checked { 356 | background-color: #008000; 357 | } 358 | QRadioButton { 359 | text-align: center; 360 | } 361 | QRadioButton::indicator { 362 | width: 13px; 363 | height: 13px; 364 | } 365 | 366 | QRadioButton::indicator:unchecked { 367 | border-radius:6px; 368 | background-color: #cecaca; 369 | } 370 | 371 | QRadioButton::indicator::checked { 372 | border-radius:6px; 373 | background-color: #7777cc; 374 | } 375 | 376 | QToolTip { 377 | border: 0px solid #0772ca; 378 | padding: 5px; 379 | color: white; 380 | background: #7777cc; 381 | } 382 | 383 | .statusBar { 384 | border:none; 385 | max-height: 30; 386 | } 387 | 388 | 389 | QScrollBar { 390 | border: none; 391 | background: #f5f5f5; 392 | border-radius: 5px; 393 | } 394 | QScrollBar:vertical { 395 | width: 10px; 396 | } 397 | QScrollBar:horizontal { 398 | height: 10px; 399 | } 400 | QScrollBar::handle { 401 | background: #cfcfcf; 402 | border-radius: 5px; 403 | } 404 | QScrollBar::add-line { 405 | border: none; 406 | } 407 | QScrollBar::sub-line { 408 | border: none; 409 | } 410 | QScrollBar::up-arrow, QScrollBar::down-arrow { 411 | border: none; 412 | background-color: #f5f5f5; 413 | } 414 | QScrollBar::add-page, QScrollBar::sub-page { 415 | background-color: #f5f5f5; 416 | } 417 | 418 | QScrollArea { 419 | border: none; 420 | padding: 0; 421 | margin: 0; 422 | } 423 | 424 | QTabWidget::pane { /* The tab widget frame */ 425 | /* border-top: 2px solid #e6e6e6; */ 426 | } 427 | 428 | QTabWidget::tab-bar { 429 | /* left: 5px; /* move to the right by 5px */ 430 | alignment: left; 431 | } 432 | QTabBar::close-button { 433 | subcontrol-position: right; 434 | border-radius: 7px; 435 | background: #79b0e0; 436 | image: url("$DataPath/assets/close.png") 437 | } 438 | QTabBar::close-button:hover { 439 | background: #cc2626; 440 | image: url("$DataPath/assets/close.png") 441 | } 442 | QTabBar QToolButton { 443 | border-radius: 3px; 444 | background-color: #0f88eb; 445 | } 446 | QTabBar QToolButton:hover { 447 | background-color: #0e538b; 448 | } 449 | QTabBar QToolButton:disabled { 450 | background-color: #cecece; 451 | } 452 | QTabBar QToolButton::right-arrow { /* the arrow mark in the tool buttons */ 453 | image: url("$DataPath/assets/right.png") 454 | } 455 | 456 | QTabBar QToolButton::left-arrow { 457 | image: url("$DataPath/assets/left.png") 458 | } 459 | /* Style the tab using the tab sub-control. Note that 460 | it reads QTabBar _not_ QTabWidget */ 461 | QTabBar::tab { 462 | color: #0f88eb; 463 | background: #e6e6e6; 464 | border: 2px solid #e6e6e6; 465 | border-bottom: none; 466 | border-top-left-radius: 4px; 467 | border-top-right-radius: 4px; 468 | min-width: 80px; 469 | min-height: 34px; 470 | padding-left: 2ex; 471 | padding-right: 2ex; 472 | } 473 | 474 | QTabBar::tab:selected, QTabBar::tab:hover { 475 | color: white; 476 | border-top-color: #0073d1; 477 | border-left-color: #0073d1; 478 | border-right-color: #0073d1; 479 | background-color: #0f88eb; 480 | } 481 | 482 | QTabBar::tab:!selected { 483 | margin-top: 2px; /* make non-selected tabs look smaller */ 484 | } 485 | 486 | /* make use of negative margins for overlapping tabs */ 487 | QTabBar::tab:selected { 488 | /* expand/overlap to the left and right by 4px */ 489 | margin-left: -4px; 490 | margin-right: -4px; 491 | } 492 | 493 | QTabBar::tab:first:selected { 494 | margin-left: 0; /* the first selected tab has nothing to overlap with on the left */ 495 | } 496 | 497 | QTabBar::tab:last:selected { 498 | margin-right: 0; /* the last selected tab has nothing to overlap with on the right */ 499 | } 500 | 501 | QTabBar::tab:only-one { 502 | margin: 0; /* if there is only one tab, we don't want overlapping margins */ 503 | } 504 | 505 | .helpList, QListWidget { 506 | border: none; 507 | outline: none; 508 | } 509 | 510 | .helpList::item, QListWidget::item { 511 | border: none; 512 | height: 40px; 513 | width: 100px 514 | } 515 | .helpList { 516 | min-width: 100px; 517 | } 518 | QListWidget::item:selected { 519 | color: #0f88eb; 520 | border-right: 5px solid #0f88eb; 521 | background-color: #b3ddff; 522 | } 523 | QListWidget::item:hover { 524 | color: #0f88eb; 525 | background-color: #b3ddff; 526 | } 527 | .helpList::item:selected { 528 | color: #009688; 529 | border-right: 5px solid #009688; 530 | background-color: #eafdfa; 531 | } 532 | .helpList::item:hover { 533 | color: #009688; 534 | background-color: #eafdfa; 535 | } 536 | 537 | .graphBtn { 538 | max-height: 120px; 539 | } -------------------------------------------------------------------------------- /COMTool/assets/right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neutree/COMTool/63fa45cca9bff2072ac43b9766e31f1983756fa3/COMTool/assets/right.png -------------------------------------------------------------------------------- /COMTool/assets/screenshot_V1.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neutree/COMTool/63fa45cca9bff2072ac43b9766e31f1983756fa3/COMTool/assets/screenshot_V1.0.png -------------------------------------------------------------------------------- /COMTool/assets/screenshot_V1.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neutree/COMTool/63fa45cca9bff2072ac43b9766e31f1983756fa3/COMTool/assets/screenshot_V1.3.png -------------------------------------------------------------------------------- /COMTool/assets/screenshot_V1.4_night.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neutree/COMTool/63fa45cca9bff2072ac43b9766e31f1983756fa3/COMTool/assets/screenshot_V1.4_night.png -------------------------------------------------------------------------------- /COMTool/assets/screenshot_V1.7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neutree/COMTool/63fa45cca9bff2072ac43b9766e31f1983756fa3/COMTool/assets/screenshot_V1.7.png -------------------------------------------------------------------------------- /COMTool/assets/screenshot_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neutree/COMTool/63fa45cca9bff2072ac43b9766e31f1983756fa3/COMTool/assets/screenshot_graph.png -------------------------------------------------------------------------------- /COMTool/assets/screenshot_macos.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neutree/COMTool/63fa45cca9bff2072ac43b9766e31f1983756fa3/COMTool/assets/screenshot_macos.jpg -------------------------------------------------------------------------------- /COMTool/assets/screenshot_protocol_v2.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neutree/COMTool/63fa45cca9bff2072ac43b9766e31f1983756fa3/COMTool/assets/screenshot_protocol_v2.3.png -------------------------------------------------------------------------------- /COMTool/assets/screenshot_terminal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neutree/COMTool/63fa45cca9bff2072ac43b9766e31f1983756fa3/COMTool/assets/screenshot_terminal.png -------------------------------------------------------------------------------- /COMTool/assets/screenshot_v2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neutree/COMTool/63fa45cca9bff2072ac43b9766e31f1983756fa3/COMTool/assets/screenshot_v2.png -------------------------------------------------------------------------------- /COMTool/assets/screenshot_v2_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neutree/COMTool/63fa45cca9bff2072ac43b9766e31f1983756fa3/COMTool/assets/screenshot_v2_white.png -------------------------------------------------------------------------------- /COMTool/assets/skin-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neutree/COMTool/63fa45cca9bff2072ac43b9766e31f1983756fa3/COMTool/assets/skin-white.png -------------------------------------------------------------------------------- /COMTool/assets/skin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neutree/COMTool/63fa45cca9bff2072ac43b9766e31f1983756fa3/COMTool/assets/skin.png -------------------------------------------------------------------------------- /COMTool/assets/tcp_udp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neutree/COMTool/63fa45cca9bff2072ac43b9766e31f1983756fa3/COMTool/assets/tcp_udp.png -------------------------------------------------------------------------------- /COMTool/autoUpdate.py: -------------------------------------------------------------------------------- 1 | try: 2 | import version 3 | import parameters 4 | except ImportError: 5 | from COMTool import version, parameters 6 | import platform 7 | 8 | log = parameters.log 9 | 10 | class AutoUpdate: 11 | updateUrl = "https://github.com/Neutree/COMTool/releases" 12 | releaseApiUrl = "https://api.github.com/repos/Neutree/COMTool/releases" 13 | releaseApiUrl2 = "https://neucrack.com/comtool_update" 14 | def detectNewVersion(self): 15 | need, v = self.checkUpdate_neucrack() # github api may change, but this will not 16 | if not v: 17 | log.i("get version info from neucrack fail, now get from github") 18 | need , v = self.checkUpdate_github() 19 | return need, v 20 | 21 | def checkUpdate_github(self): 22 | import requests, json 23 | latest = version.Version() 24 | try: 25 | page = requests.get(self.releaseApiUrl) 26 | if page.status_code != 200: 27 | log.i("request {} fail, check update fail!".format(self.releaseApiUrl)) 28 | return False, None 29 | releases = json.loads(page.content) 30 | releasesInfo = [] 31 | for release in releases: 32 | if release["prerelease"] or release["draft"]: 33 | continue 34 | tag = release["tag_name"] 35 | name = release["name"] 36 | body = release["body"] 37 | ver = self.decodeTag(tag, name, body) 38 | releasesInfo.append([ver, ver.major * 100 + ver.minor * 10 + ver.dev]) 39 | releasesInfo = sorted(releasesInfo, key=lambda x:x[1], reverse=True) 40 | latest = releasesInfo[0][0] 41 | if self.needUpdate(latest): 42 | return True, latest 43 | except Exception as e: 44 | import traceback 45 | traceback.print_exc() 46 | return False, None 47 | log.i("Already latest version!") 48 | return False, latest 49 | 50 | def checkUpdate_neucrack(self): 51 | import requests, json 52 | latest = version.Version() 53 | try: 54 | headers = { 55 | "User-Agent": f"comtool_v{version.major}.{version.minor}.{version.dev}-{platform.platform()}" 56 | } 57 | page = requests.post(self.releaseApiUrl2, headers=headers) 58 | if page.status_code != 200: 59 | log.i("request {} fail, check update fail!".format(self.releaseApiUrl)) 60 | return False, None 61 | release = json.loads(page.content) 62 | latest.load_dict(release) 63 | if self.needUpdate(latest): 64 | return True, latest 65 | except Exception as e: 66 | import traceback 67 | traceback.print_exc() 68 | return False, None 69 | log.i("Already latest version!") 70 | return False, latest 71 | 72 | def decodeTag(self, tag, name, body): 73 | # v1.7.9 74 | tag = tag[1:].split(".") 75 | return version.Version(int(tag[0]), int(tag[1]), int(tag[2]) if len(tag) > 2 else 0, name, body) 76 | 77 | def needUpdate(self, ver): 78 | if ver.major * 10 + ver.minor > version.major * 10 + version.minor: 79 | return True 80 | return False 81 | 82 | def OpenBrowser(self): 83 | import webbrowser 84 | webbrowser.open(self.updateUrl, new=0, autoraise=True) 85 | return 86 | 87 | if __name__ == "__main__": 88 | update = AutoUpdate() 89 | needUpdate, latest = update.detectNewVersion() 90 | print(needUpdate, latest) 91 | -------------------------------------------------------------------------------- /COMTool/babel.cfg: -------------------------------------------------------------------------------- 1 | [python: **.py] 2 | -------------------------------------------------------------------------------- /COMTool/conn/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import ConnectionStatus 2 | from .conn_serial import Serial 3 | from .conn_tcp_udp import TCP_UDP 4 | from .conn_ssh import SSH 5 | 6 | conns = [Serial, TCP_UDP, SSH] 7 | -------------------------------------------------------------------------------- /COMTool/conn/base.py: -------------------------------------------------------------------------------- 1 | 2 | from PyQt5.QtCore import pyqtSignal 3 | from PyQt5.QtCore import QObject 4 | from enum import Enum 5 | 6 | 7 | class ConnectionStatus(Enum): 8 | CLOSED = 0 9 | CONNECTED = 1 10 | LOSE = 2 11 | CONNECTING = 3 12 | 13 | 14 | class COMM(QObject): 15 | ''' 16 | call sequence: 17 | onInit 18 | onWidget 19 | onUiInitDone 20 | isConnected or getConnStatus 21 | send 22 | getConfig 23 | onDel 24 | ''' 25 | onReceived = lambda self, data:None # data: bytes 26 | onConnectionStatus = pyqtSignal(ConnectionStatus, str) # connected, msg 27 | hintSignal = pyqtSignal(str, str, str) # hintSignal.emit(type(error, warning, info), title, msg) 28 | configGlobal = {} 29 | id = "" 30 | name = "" 31 | 32 | def __init__(self) -> None: 33 | super().__init__() 34 | if (not self.id) or not self.name: 35 | raise ValueError(f"var id of {self} should be set") 36 | 37 | def onInit(self, config): 38 | ''' 39 | init params, DO NOT take too long time in this func 40 | ''' 41 | pass 42 | 43 | def onWidget(self): 44 | ''' 45 | this method runs in UI thread, do not block too long 46 | ''' 47 | raise NotImplementedError() 48 | 49 | def onUiInitDone(self): 50 | ''' 51 | UI init done, you can update your widget here 52 | this method runs in UI thread, do not block too long 53 | ''' 54 | 55 | def send(self, data : bytes): 56 | raise NotImplementedError() 57 | 58 | def isConnected(self): 59 | raise NotImplementedError() 60 | 61 | def getConnStatus(self): 62 | raise NotImplementedError() 63 | 64 | def disconnect(self): 65 | raise NotImplementedError() 66 | 67 | def getConfig(self): 68 | ''' 69 | get config, dict type 70 | this method runs in UI thread, do not block too long 71 | ''' 72 | return {} 73 | 74 | def ctrl(self, k, v): 75 | pass 76 | 77 | def onDel(self): 78 | ''' 79 | del all things and wait all thread to exit 80 | ''' 81 | pass -------------------------------------------------------------------------------- /COMTool/conn/test_tcp_udp.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "..") 3 | sys.path.insert(0, path) 4 | 5 | from conn_tcp_udp import TCP_UDP 6 | 7 | if __name__ == "__main__": 8 | from PyQt5.QtWidgets import QApplication 9 | from base import ConnectionStatus 10 | 11 | app = QApplication(sys.argv) 12 | 13 | conn = TCP_UDP() 14 | conn.onInit({}) 15 | 16 | class Event(): 17 | def __init__(self, conn) -> None: 18 | self.conn = conn 19 | 20 | def onReceived(self, data): 21 | print("-- received:", data) 22 | # self.conn.send(data) 23 | 24 | def onConnection(self, status, msg): 25 | print("-- onConnection:", status, msg) 26 | if status == ConnectionStatus.CONNECTED: 27 | print("== send data") 28 | self.conn.send('''GET http://example.com HTTP/1.1 29 | Accept-Language: zh-cn 30 | User-Agent: comtool 31 | Host: example.com:80 32 | Connection: close 33 | 34 | '''.encode()) 35 | 36 | def onHint(self, level, title, msg): 37 | print(level, title, msg) 38 | 39 | event = Event(conn) 40 | 41 | conn.onReceived = event.onReceived 42 | conn.onConnectionStatus.connect(event.onConnection) 43 | conn.hintSignal.connect(event.onHint) 44 | window = conn.onWidget() 45 | conn.onUiInitDone() 46 | window.show() 47 | ret = app.exec_() 48 | -------------------------------------------------------------------------------- /COMTool/helpAbout.py: -------------------------------------------------------------------------------- 1 | import sys 2 | try: 3 | import parameters 4 | from i18n import _ 5 | import version 6 | except ImportError: 7 | from COMTool import parameters 8 | from COMTool.i18n import _ 9 | from COMTool import version 10 | 11 | import os 12 | import PyQt5.QtCore 13 | from PyQt5.QtWidgets import QWidget, QLabel, QVBoxLayout 14 | import time 15 | 16 | 17 | def HelpInfo(): 18 | return '''\ 19 |

{}


20 | v{}

21 | {} + {}
22 | {}: {}
23 | {}: {}
24 |
25 |
{}
26 | 27 | 28 |
29 | {}: LGPL-3.0
30 | {}
31 | {} Github, {} releases {}
32 | {} issues
33 | {}: 566531359

34 | {}: neucrack.com/donate

35 | '''.format( 36 | parameters.appName, 37 | version.__version__, 38 | '{}{}.{}'.format(sys.implementation.name, sys.implementation.version.major, sys.implementation.version.minor), 39 | 'PyQt{}'.format(PyQt5.QtCore.QT_VERSION_STR), 40 | _("Config path"), 41 | parameters.configFilePath, 42 | _("Old config backup in"), 43 | os.path.dirname(parameters.configFilePath,), 44 | _('COMTool is a Open source project create by'), 45 | '{}/{}'.format(parameters.dataPath, parameters.appLogo2), 46 | _("License"), 47 | _('Welcome to improve it together and add plugins'), 48 | _('See more details on'), 49 | _("and get latest version at"), 50 | _("page"), 51 | _("Have problem? see"), 52 | _("QQ group for plugin development discussion"), 53 | _("You can buy me half a cup of coffee if this software helpes you"), 54 | os.path.join(parameters.assetsDir, "donate_wechat.jpg"), 55 | os.path.join(parameters.assetsDir, "donate_alipay.jpg") 56 | ) 57 | -------------------------------------------------------------------------------- /COMTool/i18n.py: -------------------------------------------------------------------------------- 1 | import os 2 | import gettext 3 | import babel 4 | from collections import OrderedDict 5 | 6 | locales=["en", "zh_CN", "zh_TW", "ja"] 7 | 8 | 9 | root_dir = os.path.abspath(os.path.dirname(__file__)) 10 | locale = "en" 11 | 12 | tr = lambda x:x 13 | 14 | def _(text): 15 | return tr(text) 16 | 17 | def set_locale(locale_in): 18 | global locale, tr, root_dir 19 | print("-- set locale to", locale_in) 20 | locale = locale_in 21 | locales_path = os.path.join(root_dir, 'locales') 22 | if not os.path.exists(locales_path): # for pyinstaller pack 23 | locales_path = os.path.join(os.path.dirname(root_dir), 'locales') 24 | # check translate binary file 25 | mo_path = os.path.join(locales_path, "en", "LC_MESSAGES", "messages.mo") 26 | if not os.path.exists(mo_path): 27 | main("finish") 28 | lang = gettext.translation('messages', localedir=locales_path, languages=[locale]) 29 | tr = lang.gettext 30 | 31 | def get_languages(): 32 | languages = OrderedDict() 33 | for locale in locales: 34 | obj = babel.Locale.parse(locale) 35 | languages[locale] = obj.language_name + (" " + obj.script_name if obj.script_name else "") 36 | return languages 37 | 38 | def extract(src_path, config_file_path, out_path): 39 | from distutils.errors import DistutilsOptionError 40 | from babel.messages.frontend import extract_messages 41 | cmdinst = extract_messages() 42 | cmdinst.initialize_options() 43 | cmdinst.mapping_file = config_file_path 44 | cmdinst.output_file = out_path 45 | cmdinst.input_paths = src_path 46 | try: 47 | cmdinst.ensure_finalized() 48 | cmdinst.run() 49 | except DistutilsOptionError as err: 50 | raise err 51 | 52 | def init(template_path, out_dir, locale, domain="messages"): 53 | from distutils.errors import DistutilsOptionError 54 | from babel.messages.frontend import init_catalog 55 | cmdinst = init_catalog() 56 | cmdinst.initialize_options() 57 | cmdinst.input_file = template_path 58 | cmdinst.output_dir = out_dir 59 | cmdinst.locale = locale 60 | cmdinst.domain = domain 61 | try: 62 | cmdinst.ensure_finalized() 63 | cmdinst.run() 64 | except DistutilsOptionError as err: 65 | raise err 66 | 67 | def update(template_path, out_dir, locale, domain="messages"): 68 | from distutils.errors import DistutilsOptionError 69 | from babel.messages.frontend import update_catalog 70 | cmdinst = update_catalog() 71 | cmdinst.initialize_options() 72 | cmdinst.input_file = template_path 73 | cmdinst.output_dir = out_dir 74 | cmdinst.locale = locale 75 | cmdinst.domain = domain 76 | try: 77 | cmdinst.ensure_finalized() 78 | cmdinst.run() 79 | except DistutilsOptionError as err: 80 | raise err 81 | 82 | def compile(translate_dir, locale, domain="messages"): 83 | from distutils.errors import DistutilsOptionError 84 | from babel.messages.frontend import compile_catalog 85 | cmdinst = compile_catalog() 86 | cmdinst.initialize_options() 87 | cmdinst.directory = translate_dir 88 | cmdinst.locale = locale 89 | cmdinst.domain = domain 90 | try: 91 | cmdinst.ensure_finalized() 92 | cmdinst.run() 93 | except DistutilsOptionError as err: 94 | raise err 95 | 96 | 97 | 98 | def main(cmd, path=None): 99 | global root_dir 100 | babel_cfg_path = os.path.join(root_dir, "babel.cfg") 101 | if path: 102 | if os.path.exists(path): 103 | root_dir = os.path.abspath(path) 104 | if os.path.exists(os.path.join(root_dir, "babel.cfg")): 105 | babel_cfg_path = os.path.join(root_dir, "babel.cfg") 106 | else: 107 | print("path {} not exists".format(path)) 108 | return 109 | 110 | cwd = os.getcwd() 111 | os.chdir(root_dir) 112 | if cmd == "prepare": 113 | print("== translate locales: {} ==".format(locales)) 114 | print("-- extract keys from files") 115 | if not os.path.exists("locales"): 116 | os.makedirs("locales") 117 | # os.system("pybabel extract -F babel.cfg -o locales/messages.pot ./") 118 | extract("./", babel_cfg_path, "locales/messages.pot") 119 | for locale in locales: 120 | print("-- generate {} po files from pot files".format(locale)) 121 | if os.path.exists('locales/{}/LC_MESSAGES/messages.po'.format(locale)): 122 | print("-- file already exits, only update") 123 | # "pybabel update -i locales/messages.pot -d locales -l {}".format(locale) 124 | update("locales/messages.pot", "locales", locale) 125 | else: 126 | print("-- file not exits, now create") 127 | # "pybabel init -i locales/messages.pot -d locales -l {}".format(locale) 128 | init("locales/messages.pot", "locales", locale) 129 | elif cmd == "finish": 130 | print("== translate locales: {} ==".format(locales)) 131 | for locale in locales: 132 | print("-- generate {} mo file from po files".format(locale)) 133 | # "pybabel compile -d locales -l {}".format(locale) 134 | compile("locales", locale) 135 | os.chdir(cwd) 136 | 137 | 138 | def cli_main(): 139 | import argparse 140 | parser = argparse.ArgumentParser("tranlate tool") 141 | parser.add_argument("-p", "--path", default="", help="path to the root of plugin") 142 | parser.add_argument("cmd", type=str, choices=["prepare", "finish"]) 143 | args = parser.parse_args() 144 | main(args.cmd, args.path) 145 | 146 | if __name__ == "__main__": 147 | cli_main() 148 | -------------------------------------------------------------------------------- /COMTool/logger.py: -------------------------------------------------------------------------------- 1 | ''' 2 | logger wrapper based oh logging 3 | 4 | @author neucrack 5 | @license MIT copyright 2020-2021 neucrack CZD666666@gmail.com 6 | ''' 7 | 8 | 9 | 10 | import logging 11 | import coloredlogs 12 | import sys 13 | 14 | class Logger: 15 | ''' 16 | use logging module to record log to console or file 17 | ''' 18 | def __init__(self, level="d", stdout = True, file_path=None, 19 | fmt = '%(asctime)s - [%(levelname)s] - %(message)s', 20 | logger_name = "logger"): 21 | self.log = logging.getLogger(logger_name) 22 | formatter=logging.Formatter(fmt=fmt) 23 | level_ = logging.INFO 24 | if level == "i": 25 | level_ = logging.INFO 26 | elif level == "w": 27 | level_ = logging.WARNING 28 | elif level == "e": 29 | level_ = logging.ERROR 30 | # terminal output 31 | coloredlogs.DEFAULT_FIELD_STYLES = {'asctime': {'color': 'green'}, 'hostname': {'color': 'magenta'}, 32 | 'levelname': {'color': 'green', 'bold': True}, 'request_id': {'color': 'yellow'}, 33 | 'name': {'color': 'blue'}, 'programname': {'color': 'cyan'}, 34 | 'processName': {'color': 'magenta'}, 35 | 'threadName': {'color': 'magenta'}, 36 | 'filename': {'color': 'white'}, 37 | 'lineno': {'color': 'white'}} 38 | level_styles = { 39 | 'debug': { 40 | 'color': "white" 41 | }, 42 | 'info': { 43 | 'color': "green" 44 | }, 45 | 'warn': { 46 | 'color': "yellow" 47 | }, 48 | 'error': { 49 | 'color': "red" 50 | } 51 | } 52 | 53 | coloredlogs.install(level=level_, fmt=fmt, level_styles=level_styles) 54 | self.log.setLevel(level_) 55 | # sh = logging.StreamHandler() 56 | # sh.setFormatter(formatter) 57 | # sh.setLevel(level_) 58 | # self.log.addHandler(sh) 59 | # file output 60 | if not stdout: 61 | self.log.propagate = False 62 | if file_path: 63 | fh = logging.FileHandler(file_path, mode="a", encoding="utf-8") 64 | fh.setFormatter(formatter) 65 | fh.setLevel(level_) 66 | self.log.addHandler(fh) 67 | 68 | def d(self, *args): 69 | out = "" 70 | for arg in args: 71 | out += " " + str(arg) 72 | self.log.debug(out) 73 | 74 | def i(self, *args): 75 | out = "" 76 | for arg in args: 77 | out += " " + str(arg) 78 | self.log.info(out) 79 | 80 | def w(self, *args): 81 | out = "" 82 | for arg in args: 83 | out += " " + str(arg) 84 | self.log.warning(out) 85 | 86 | def e(self, *args): 87 | out = "" 88 | for arg in args: 89 | out += " " + str(arg) 90 | self.log.error(out) 91 | 92 | class Fake_Logger: 93 | ''' 94 | use logging module to record log to console or file 95 | ''' 96 | def __init__(self, level="d", file_path=None, fmt = '%(asctime)s - [%(levelname)s]: %(message)s'): 97 | pass 98 | 99 | def d(self, *args): 100 | print(args) 101 | 102 | def i(self, *args): 103 | print(args) 104 | 105 | def w(self, *args): 106 | print(args) 107 | 108 | def e(self, *args): 109 | print(args) 110 | 111 | 112 | if __name__ == "__main__": 113 | import os 114 | log = Logger(file_path=os.path.join(os.path.dirname(os.path.abspath(__file__)), "test.log")) 115 | log.d("debug", "hello") 116 | log.i("info:", 1) 117 | log.w("warning") 118 | log.e("error") 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /COMTool/parameters.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | import sys, os 3 | from datetime import datetime 4 | import json 5 | 6 | try: 7 | from i18n import _, set_locale 8 | from logger import Logger 9 | except ImportError: 10 | from COMTool.i18n import _, set_locale 11 | from COMTool.logger import Logger 12 | 13 | appName = "COMTool" 14 | appIcon = "assets/logo.png" 15 | appLogo = "assets/logo.png" 16 | appLogo2 = "assets/logo2.png" 17 | dataPath = os.path.abspath(os.path.dirname(__file__)).replace("\\", "/") # replace \ to / for qss usage, qss only support / 18 | assetsDir = os.path.join(dataPath, "assets").replace("\\", "/") 19 | if not os.path.exists(assetsDir): # for pyinstaller pack 20 | dataPath = os.path.dirname(dataPath) 21 | assetsDir = os.path.join(dataPath, "assets").replace("\\", "/") 22 | 23 | defaultBaudrates = [9600, 19200, 38400, 57600, 74880, 115200, 921600, 1000000, 1500000, 2000000, 4500000] 24 | encodings = ["ASCII", "UTF-8", "UTF-16", "GBK", "GB2312", "GB18030"] 25 | customSendItemHeight = 40 26 | 27 | author = "Neucrack" 28 | 29 | def get_config_path(configFileName): 30 | configFilePath = configFileName 31 | try: 32 | configFilePath = os.path.join(configFileDir, configFileName) 33 | if not os.path.exists(configFileDir): 34 | os.makedirs(configFileDir) 35 | except: 36 | pass 37 | return configFilePath 38 | 39 | configFileName="config.json" 40 | configFilePath=configFileName 41 | 42 | if sys.platform.startswith('linux') or sys.platform.startswith('darwin') or sys.platform.startswith('freebsd'): 43 | configFileDir = os.path.join(os.getenv("HOME"), ".config/comtool") 44 | configFilePath = get_config_path(configFileName) 45 | else: 46 | configFileDir = os.path.abspath(os.getcwd()) 47 | configFilePath = os.path.join(configFileDir, configFileName) 48 | 49 | logPath = os.path.join(configFileDir, "run.log") 50 | log = Logger(file_path=logPath) 51 | log.i("Config path:", configFilePath) 52 | log.i("Log path:", logPath) 53 | 54 | 55 | class Parameters: 56 | config = { 57 | "version": 3, 58 | "skin": "light", 59 | "locale": "en", 60 | "encoding": "UTF-8", 61 | "skipVersion": None, 62 | "connId": "serial", 63 | "pluginsInfo": { # enabled plugins ID 64 | "external": { 65 | # "myplugin2": { 66 | # # "package": "myplugin", # package installed as a python package 67 | # "path": "E:\main\projects\COMTool\COMTool\plugins\myplugin2\myplugin2.py" 68 | # } 69 | } 70 | }, 71 | "activeItem": "dbg-1", 72 | "currItem": None, 73 | "items": [ 74 | # { 75 | # "name": "dbg-1", 76 | # "pluginId": "dbg", 77 | # "config": { 78 | # "conns": { 79 | # "currConn": "serial", 80 | # "serial": { 81 | # } 82 | # }, 83 | # "plugin": { 84 | 85 | # } 86 | # } 87 | # } 88 | ] 89 | } 90 | 91 | def save(self, path): 92 | path = os.path.abspath(path) 93 | if not os.path.exists(os.path.dirname(path)): 94 | os.makedirs(os.path.dirname(path)) 95 | with open(path, "w", encoding="utf-8") as f: 96 | json.dump(self.config, f, indent=4, ensure_ascii=False) 97 | 98 | def load(self, path): 99 | if not os.path.exists(path): 100 | return 101 | with open(path, encoding="utf-8") as f: 102 | config = json.load(f) 103 | if "version" in config and config["version"] == self.config["version"]: 104 | self.config = config 105 | else: # for old config, just backup 106 | t = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") 107 | old_path = "{}.bak.{}.json".format(path, t) 108 | log.w("Old config file, backup to", old_path) 109 | shutil.copyfile(path, old_path) 110 | return 111 | 112 | def __getitem__(self, idx): 113 | return self.config[idx] 114 | 115 | def __setitem__(self, idx, v): 116 | self.config[idx] = v 117 | 118 | def __str__(self) -> str: 119 | return json.dumps(self.config) 120 | 121 | 122 | strStyleShowHideButtonLeft = ''' 123 | QPushButton { 124 | border-image: url("$DataPath/assets/arrow-left.png") 125 | } 126 | QPushButton:hover { 127 | border-image: url("$DataPath/assets/arrow-left-white.png") 128 | }''' 129 | 130 | strStyleShowHideButtonRight = ''' 131 | QPushButton { 132 | border-image: url("$DataPath/assets/arrow-right.png") 133 | } 134 | QPushButton:hover { 135 | border-image: url("$DataPath/assets/arrow-right-white.png") 136 | }''' 137 | 138 | styleForCode = { 139 | "light":{ 140 | "iconColor": "white", 141 | "iconSelectorColor": "#929599" 142 | }, 143 | "dark":{ 144 | "iconColor": "#bcbcbd", 145 | "iconSelectorColor": "#bcbcbd" 146 | } 147 | } 148 | 149 | -------------------------------------------------------------------------------- /COMTool/pluginItems.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import pyqtSignal, Qt, QRect, QMargins 2 | from PyQt5.QtWidgets import (QApplication, QWidget,QPushButton,QMessageBox,QDesktopWidget,QMainWindow, 3 | QVBoxLayout,QHBoxLayout,QGridLayout,QTextEdit,QLabel,QRadioButton,QCheckBox, 4 | QLineEdit,QGroupBox,QSplitter,QFileDialog, QScrollArea, QTabWidget, QMenu, QSplashScreen) 5 | from PyQt5.QtGui import QIcon,QFont,QTextCursor,QPixmap,QColor, QCloseEvent 6 | import threading 7 | import time 8 | import os 9 | import json 10 | 11 | try: 12 | from i18n import _ 13 | from Combobox import ComboBox 14 | from conn import ConnectionStatus, conns 15 | from parameters import log, configFilePath 16 | except ImportError: 17 | from COMTool.Combobox import ComboBox 18 | from COMTool.conn import ConnectionStatus, conns 19 | from COMTool.i18n import _ 20 | from COMTool.parameters import log, configFilePath 21 | 22 | class PluginItem: 23 | # display name 24 | name = '' 25 | widget = None 26 | def __init__(self, name, pluginClass, 27 | connClasses, connsConfigs, 28 | globalConfig, itemConfig, 29 | hintSignal, reloadWindowSignal, 30 | connCallback): 31 | ''' 32 | item show name, e.g. dbg-1 33 | ''' 34 | self.reloadWindowSignal = reloadWindowSignal 35 | self.hintSignal = hintSignal 36 | self.name = name 37 | self.connClasses = connClasses 38 | self.connsConfigs = connsConfigs 39 | self.currConnWidget = None 40 | self.currConnIdx = 0 41 | self.sendProcess = None 42 | self.dataToSend = [] 43 | self.fileToSend = [] 44 | # widgets 45 | self.settingWidget = None 46 | self.mainWidget = None 47 | self.functionalWidget = None 48 | # init plugin 49 | self.plugin = pluginClass() 50 | self.plugin.configGlobal = globalConfig 51 | self.plugin.send = self.sendData 52 | self.plugin.ctrlConn = self.ctrlConn 53 | self.plugin.hintSignal = self.hintSignal 54 | self.plugin.reloadWindowSignal = self.reloadWindowSignal 55 | self.plugin.connCallbak = connCallback 56 | self.plugin.onInit(config=itemConfig) 57 | if not "version" in itemConfig: 58 | raise Exception("{} {}".format(_("version not found in config of plugin:"), self.plugin.id)) 59 | # conn 60 | self.isAddConn = self.plugin.onIsAddConnWidget() 61 | if self.isAddConn: 62 | self.conns, self.connWidgets = self.newConnWidgets() 63 | # frame 64 | self.widget = self.newFrame(self.isAddConn) 65 | self.uiLoadConfigs() 66 | self.initEvent() 67 | 68 | def newConnWidgets(self): 69 | connsWidgets = [] 70 | conns = [] 71 | for Conn in self.connClasses: 72 | # conn.onInit() 73 | conn = Conn() 74 | conns.append(conn) 75 | if conn.id in self.connsConfigs: 76 | connConfig = self.connsConfigs[conn.id] 77 | else: 78 | connConfig = {} 79 | self.connsConfigs[conn.id] = connConfig 80 | conn.onInit(connConfig) 81 | widget = conn.onWidget() 82 | conn.onUiInitDone() 83 | connsWidgets.append(widget) 84 | return conns, connsWidgets 85 | 86 | def newFrame(self, isAddConn): 87 | wrapper = QWidget() 88 | wrapperLayout = QVBoxLayout() 89 | wrapperLayout.setContentsMargins(0, 0, 0, 0) 90 | widget = QSplitter(Qt.Horizontal) 91 | widget.setProperty("class", "contentWrapper") 92 | statusBar = self.plugin.onWidgetStatusBar(wrapper) 93 | wrapper.setLayout(wrapperLayout) 94 | wrapperLayout.addWidget(widget) 95 | if not statusBar is None: 96 | wrapperLayout.addWidget(statusBar) 97 | # widgets settings 98 | self.settingWidget = QWidget() 99 | self.settingWidget.setProperty("class","settingWidget") 100 | settingLayout = QVBoxLayout() 101 | self.settingWidget.setLayout(settingLayout) 102 | # get connection settings widgets 103 | if isAddConn: 104 | connSettingsGroupBox = QGroupBox(_("Connection")) 105 | layout = QVBoxLayout() 106 | connSettingsGroupBox.setLayout(layout) 107 | self.connSelectCommbox = ComboBox() 108 | for conn in self.conns: 109 | self.connSelectCommbox.addItem(conn.name) 110 | layout.addWidget(self.connSelectCommbox) 111 | layout.setContentsMargins(1, 6, 0, 0) 112 | self.connsParent = QWidget() 113 | layout2 = QVBoxLayout() 114 | layout2.setContentsMargins(0, 0, 0, 0) 115 | self.connsParent.setLayout(layout2) 116 | layout.addWidget(self.connsParent) 117 | settingLayout.addWidget(connSettingsGroupBox) 118 | # get settings widgets 119 | subSettingWidget = self.plugin.onWidgetSettings(widget) 120 | if not subSettingWidget is None: 121 | settingLayout.addWidget(subSettingWidget) 122 | settingLayout.addStretch() 123 | # widgets main 124 | self.mainWidget = self.plugin.onWidgetMain(widget) 125 | # widgets functional 126 | self.functionalWidget = QWidget() 127 | layout3 = QVBoxLayout() 128 | self.functionalWidget.setLayout(layout3) 129 | loadConfigBtn = QPushButton(_("Load config")) 130 | shareConfigBtn = QPushButton(_("Share config")) 131 | layout3.addWidget(loadConfigBtn) 132 | layout3.addWidget(shareConfigBtn) 133 | loadConfigBtn.clicked.connect(lambda : self.selectLoadfile()) 134 | shareConfigBtn.clicked.connect(lambda : self.selectSharefile()) 135 | pluginFuncWidget = self.plugin.onWidgetFunctional(widget) 136 | if not pluginFuncWidget is None: 137 | layout3.addWidget(pluginFuncWidget) 138 | layout3.addStretch() 139 | # add to frame 140 | widget.addWidget(self.settingWidget) 141 | widget.addWidget(self.mainWidget) 142 | widget.addWidget(self.functionalWidget) 143 | widget.setStretchFactor(0, 1) 144 | widget.setStretchFactor(1, 2) 145 | widget.setStretchFactor(2, 1) 146 | self.functionalWidget.hide() 147 | # UI init done 148 | self.plugin.onUiInitDone() 149 | return wrapper 150 | 151 | # event 152 | def selectSharefile(self): 153 | oldPath = os.getcwd() 154 | fileName_choose, filetype = QFileDialog.getSaveFileName(self.functionalWidget, 155 | _("Select file"), 156 | os.path.join(oldPath, f"comtool.{self.name}.json"), 157 | _("json file (*.json);;config file (*.conf);;All Files (*)")) 158 | if fileName_choose != "": 159 | with open(fileName_choose, "w", encoding="utf-8") as f: 160 | for item in self.plugin.configGlobal["items"]: 161 | if item["name"] == self.name: 162 | json.dump(item, f, indent=4, ensure_ascii=False) 163 | break 164 | 165 | def selectLoadfile(self): 166 | oldPath = os.getcwd() 167 | fileName_choose, filetype = QFileDialog.getOpenFileName(self.functionalWidget, 168 | _("Select file"), 169 | oldPath, 170 | _("json file (*.json);;config file (*.conf);;All Files (*)")) 171 | if fileName_choose != "": 172 | with open(fileName_choose, "r", encoding="utf-8") as f: 173 | config = json.load( f) 174 | if "pluginsInfo" in config: # global config file 175 | self.hintSignal.emit("error", _("Error"), _("Not support load global config file, you can copy config file mannually to " + configFilePath)) 176 | return 177 | if config["pluginId"] != self.plugin.id: 178 | self.hintSignal.emit("error", _("Error"), _("Config is not for this plugin, config is for plugin:") + " " + config["pluginId"]) 179 | return 180 | if config["config"]["plugin"]["version"] != self.plugin.config["version"]: 181 | self.hintSignal.emit("warning", _("Warning"), "{} {}, {}: {}, {}: {}".format( 182 | _("Config version not same, plugin config version:"), config["config"]["plugin"]["version"], 183 | _("now"), self.plugin.config["version"], 184 | _("this maybe lead to some problem, if happened, please remove it manually from config file"), 185 | configFilePath)) 186 | return 187 | self.oldConnConfigs = self.connsConfigs.copy() 188 | self.oldPluginConfigs = self.plugin.config.copy() 189 | self.connsConfigs.clear() 190 | self.plugin.config.clear() 191 | for k, v in config["config"]["conns"].items(): 192 | self.connsConfigs[k] = v 193 | for k, v in config["config"]["plugin"].items(): 194 | self.plugin.config[k] = v 195 | def onClose(ok): 196 | if not ok: 197 | self.connsConfigs.clear() 198 | self.connsConfigs.update(self.oldConnConfigs) 199 | self.plugin.config.clear() 200 | self.plugin.config.update(self.oldPluginConfigs) 201 | self.reloadWindowSignal.emit("", _("Restart to load config?"), onClose) 202 | 203 | def _setConn(self, idx): 204 | if not self.isAddConn: 205 | return 206 | if self.currConnWidget: 207 | self.currConnWidget.setParent(None) 208 | self.conns[self.currConnIdx].onReceived = lambda x:None 209 | self.conns[self.currConnIdx].onConnectionStatus.disconnect(self.onConnStatus) 210 | self.currConnWidget = self.connWidgets[idx] 211 | self.connsParent.layout().addWidget(self.currConnWidget) 212 | self.conns[idx].onReceived = self.onReceived 213 | self.plugin.isConnected = self.conns[idx].isConnected 214 | self.plugin.getConnStatus = self.conns[idx].getConnStatus 215 | self.conns[idx].onConnectionStatus.connect(self.onConnStatus) 216 | self.connsConfigs["currConn"] = self.conns[idx].id 217 | self.currConnIdx = idx 218 | 219 | 220 | def uiLoadConfigs(self): 221 | if self.isAddConn: 222 | loadedIdx = 0 223 | if "currConn" in self.connsConfigs: 224 | for idx, conn in enumerate(self.conns): 225 | if conn.id == self.connsConfigs["currConn"]: 226 | loadedIdx = idx 227 | self.connSelectCommbox.setCurrentIndex(loadedIdx) 228 | self._setConn(loadedIdx) 229 | 230 | def initEvent(self): 231 | if self.isAddConn: 232 | self.connSelectCommbox.currentIndexChanged.connect(self.onConnChanged) 233 | 234 | def onConnChanged(self, idx): 235 | self.conns[self.currConnIdx].disconnect() 236 | self._setConn(idx) 237 | self.onConnStatus(ConnectionStatus.CLOSED, _("Change connection, auto close old connection")) 238 | 239 | def ctrlConn(self, k, v): 240 | if self.isAddConn: 241 | self.conns[self.currConnIdx].ctrl(k, v) 242 | 243 | def onConnStatus(self, status:ConnectionStatus, msg): 244 | self.plugin.onConnChanged(status, msg) 245 | if self.sendProcess is None: 246 | self.sendProcess = threading.Thread(target=self.sendDataProcess) 247 | self.sendProcess.setDaemon(True) 248 | self.sendProcess.start() 249 | 250 | def sendData(self, data_bytes=None, file_path=None, callback=lambda ok, msg, length, path:None): 251 | if data_bytes: 252 | self.dataToSend.insert(0, (data_bytes, callback)) 253 | if file_path: 254 | self.fileToSend.insert(0, (file_path, callback)) 255 | 256 | def onReceived(self, data): 257 | self.plugin.onReceived(data) 258 | 259 | def onKeyPressEvent(self, e): 260 | self.plugin.onKeyPressEvent(e) 261 | 262 | def onKeyReleaseEvent(self, e): 263 | self.plugin.onKeyReleaseEvent(e) 264 | 265 | def sendDataProcess(self): 266 | self.receiveProgressStop = False 267 | while not self.receiveProgressStop: 268 | try: 269 | if not self.conns[self.currConnIdx].isConnected(): 270 | time.sleep(0.001) 271 | continue 272 | while len(self.dataToSend) > 0: 273 | data, callback = self.dataToSend.pop() 274 | self.conns[self.currConnIdx].send(data) 275 | callback(True, "", len(data), "") 276 | while len(self.fileToSend) > 0: 277 | file_path, callback = self.fileToSend.pop() 278 | ok = False 279 | length = 0 280 | if file_path and os.path.exists(file_path): 281 | data = None 282 | try: 283 | with open(file_path, "rb") as f: 284 | data = f.read() 285 | except Exception as e: 286 | self.hintSignal.emit("error", _("Error"), _("Open file failed!") + "\n%s\n%s" %(file_path, str(e))) 287 | if data: 288 | self.conns[self.currConnIdx].send(data) 289 | length = len(data) 290 | ok = True 291 | callback(ok, "", length, file_path) 292 | time.sleep(0.001) 293 | except Exception as e: 294 | import traceback 295 | exc = traceback.format_exc() 296 | log.e(exc) 297 | if 'multiple access' in str(e): 298 | self.hintSignal.emit("error", _("Error"), "device disconnected or multiple access on port?") 299 | continue 300 | 301 | def onDel(self): 302 | self.receiveProgressStop = True 303 | if self.isAddConn: 304 | for conn in self.conns: 305 | conn.onDel() 306 | self.plugin.onDel() 307 | 308 | -------------------------------------------------------------------------------- /COMTool/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | from . import dbg 2 | from . import protocol 3 | from .import terminal 4 | from . import graph 5 | # from . import myplugin 6 | 7 | pluginClasses = [dbg.Plugin, protocol.Plugin, terminal.Plugin, graph.Plugin] 8 | # pluginClasses.append(myplugin.Plugin) 9 | 10 | builtinPlugins = {} 11 | for c in pluginClasses: 12 | builtinPlugins[c.id] = c 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /COMTool/plugins/base.py: -------------------------------------------------------------------------------- 1 | 2 | from PyQt5.QtCore import QObject, Qt 3 | from PyQt5.QtWidgets import (QApplication, QWidget,QPushButton,QMessageBox,QDesktopWidget,QMainWindow, 4 | QVBoxLayout,QHBoxLayout,QGridLayout,QTextEdit,QLabel,QRadioButton,QCheckBox, 5 | QLineEdit,QGroupBox,QSplitter,QFileDialog, QScrollArea) 6 | try: 7 | from Combobox import ComboBox 8 | from i18n import _ 9 | import utils, parameters 10 | from conn.base import ConnectionStatus 11 | from widgets import statusBar 12 | except ImportError: 13 | from COMTool import utils, parameters 14 | from COMTool.i18n import _ 15 | from COMTool.Combobox import ComboBox 16 | from COMTool.conn.base import ConnectionStatus 17 | from COMTool.widgets import statusBar 18 | 19 | class Plugin_Base(QObject): 20 | ''' 21 | call sequence: 22 | set vars like hintSignal, hintSignal 23 | onInit 24 | onWidget 25 | onUiInitDone 26 | onActive 27 | onConnChanged -> connCallbak # in UI thread 28 | send # can call in UI thread directly 29 | onReceived # in receive thread 30 | onDel 31 | ''' 32 | # vars set by caller 33 | isConnected = lambda o: False 34 | getConnStatus = lambda o: ConnectionStatus.CLOSED 35 | connCallbak = lambda status,msg:None 36 | send = lambda o,x,y:None # send(data_bytes=None, file_path=None, callback=lambda ok,msg:None), can call in UI thread directly 37 | ctrlConn = lambda o,k,v:None # call ctrl func of connection 38 | hintSignal = None # hintSignal.emit(type(error, warning, info), title, msg) 39 | reloadWindowSignal = None # reloadWindowSignal.emit(title, msg, callback(close or not)), reload window to load new configs 40 | configGlobal = {} 41 | # other vars 42 | connParent = "main" # parent id 43 | connChilds = [] # children ids 44 | id = "" 45 | name = "" 46 | 47 | enabled = False # user enabled this plugin 48 | active = False # using this plugin 49 | 50 | help = None # help info, can be str or QWidget 51 | 52 | def __init__(self): 53 | super().__init__() 54 | if not self.id: 55 | raise ValueError(f"var id of Plugin {self} should be set") 56 | 57 | def onInit(self, config): 58 | ''' 59 | init params, DO NOT take too long time in this func 60 | @config dict type, just change this var's content, 61 | when program exit, this config will be auto save to config file 62 | ''' 63 | self.config = config 64 | default = { 65 | "version": 1, 66 | } 67 | for k in default: 68 | if not k in self.config: 69 | self.config[k] = default[k] 70 | 71 | def onDel(self): 72 | pass 73 | 74 | def onConnChanged(self, status:ConnectionStatus, msg:str): 75 | ''' 76 | call in UI thread, be carefully!! 77 | ''' 78 | if status == ConnectionStatus.CONNECTED: 79 | self.statusBar.setMsg("info", '{} {}'.format(_("Connected"), msg)) 80 | elif status == ConnectionStatus.CLOSED: 81 | self.statusBar.setMsg("info", '{} {}'.format(_("Closed"), msg)) 82 | elif status == ConnectionStatus.CONNECTING: 83 | self.statusBar.setMsg("info", '{} {}'.format(_("Connecting"), msg)) 84 | elif status == ConnectionStatus.LOSE: 85 | self.statusBar.setMsg("warning", '{} {}'.format(_("Connection lose"), msg)) 86 | else: 87 | self.statusBar.setMsg("warning", msg) 88 | self.connCallbak(self, status, msg) 89 | 90 | def onIsAddConnWidget(self): 91 | ''' 92 | Auto add connection widget to the left or not, default is True. 93 | Override this method to return False if you don't want to add connection widget. 94 | ''' 95 | return True 96 | 97 | def onWidgetMain(self, parent): 98 | ''' 99 | main widget, just return a QWidget object 100 | ''' 101 | raise NotImplementedError() 102 | 103 | def onWidgetSettings(self, parent): 104 | ''' 105 | setting widget, just return a QWidget object or None 106 | ''' 107 | return None 108 | 109 | def onWidgetFunctional(self, parent): 110 | ''' 111 | functional widget, just return a QWidget object or None 112 | ''' 113 | return None 114 | 115 | def onWidgetStatusBar(self, parent): 116 | self.statusBar = statusBar(rxTxCount=False) 117 | return self.statusBar 118 | 119 | def onReceived(self, data : bytes): 120 | ''' 121 | call in receive thread, not UI thread 122 | ''' 123 | for plugin in self.connChilds: 124 | plugin.onReceived(data) 125 | 126 | def sendData(self, data:bytes): 127 | ''' 128 | send data, chidren call send will invoke this function 129 | if you send data in this plugin, you can directly call self.send 130 | ''' 131 | self.send(data) 132 | 133 | def onKeyPressEvent(self, event): 134 | pass 135 | 136 | def onKeyReleaseEvent(self, event): 137 | pass 138 | 139 | def onUiInitDone(self): 140 | ''' 141 | UI init done, you can update your widget here 142 | this method runs in UI thread, do not block too long 143 | ''' 144 | pass 145 | 146 | def onActive(self): 147 | ''' 148 | plugin active 149 | ''' 150 | pass 151 | 152 | def bindVar(self, uiObj, varObj, varName: str, vtype=None, vErrorMsg="", checkVar=lambda v:v, invert = False, emptyDefault = None): 153 | objType = type(uiObj) 154 | if objType == QCheckBox: 155 | v = uiObj.isChecked() 156 | varObj[varName] = v if not invert else not v 157 | return 158 | elif objType == QLineEdit: 159 | v = uiObj.text() 160 | if v == "" and emptyDefault is not None: 161 | v = emptyDefault 162 | elif objType == ComboBox: 163 | varObj[varName] = uiObj.currentText() 164 | return 165 | elif objType == QRadioButton: 166 | v = uiObj.isChecked() 167 | varObj[varName] = v if not invert else not v 168 | return 169 | else: 170 | raise Exception("not support this object") 171 | if vtype: 172 | try: 173 | v = vtype(v) 174 | except Exception: 175 | uiObj.setText(str(varObj[varName])) 176 | self.hintSignal.emit("error", _("Error"), vErrorMsg) 177 | return 178 | try: 179 | v = checkVar(v) 180 | except Exception as e: 181 | self.hintSignal.emit("error", _("Error"), str(e)) 182 | return 183 | varObj[varName] = v 184 | 185 | def parseSendData(self, data:str, encoding, usrCRLF=False, isHexStr=False, escape=False): 186 | if not data: 187 | return b'' 188 | if usrCRLF: 189 | data = data.replace("\n", "\r\n") 190 | if isHexStr: 191 | if usrCRLF: 192 | data = data.replace("\r\n", " ") 193 | else: 194 | data = data.replace("\n", " ") 195 | data = utils.hex_str_to_bytes(data) 196 | if data == -1: 197 | self.hintSignal.emit("error", _("Error"), _("Format error, should be like 00 01 02 03")) 198 | return b'' 199 | else: 200 | if not escape: 201 | data = data.encode(encoding,"ignore") 202 | else: # '11234abcd\n123你好\r\n\thello\x00\x01\x02' 203 | try: 204 | data = utils.str_to_bytes(data, escape=True, encoding=encoding) 205 | except Exception as e: 206 | self.hintSignal.emit("error", _("Error"), _("Escape is on, but escape error:") + str(e)) 207 | return b'' 208 | return data 209 | 210 | def decodeReceivedData(self, data:bytes, encoding, isHexStr = False, escape=False): 211 | if isHexStr: 212 | data = utils.bytes_to_hex_str(data) 213 | elif escape: 214 | data = str(data)[2:-1] # b'1234\x01' => "b'1234\\x01'" =>"1234\\x01" 215 | else: 216 | data = data.decode(encoding=encoding, errors="ignore") 217 | return data 218 | -------------------------------------------------------------------------------- /COMTool/plugins/crc.py: -------------------------------------------------------------------------------- 1 | 2 | auchCRCHi = [ 3 | 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 4 | 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 5 | 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 6 | 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 7 | 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 8 | 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 9 | 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 10 | 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 11 | 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 12 | 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 13 | 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 14 | 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 15 | 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 16 | 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 17 | 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 18 | 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 19 | 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 20 | 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 21 | 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 22 | 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 23 | 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 24 | 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 25 | 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 26 | 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 27 | 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 28 | 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40 29 | ] 30 | auchCRCLo = [ 31 | 0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06, 32 | 0x07, 0xC7, 0x05, 0xC5, 0xC4, 0x04, 0xCC, 0x0C, 0x0D, 0xCD, 33 | 0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09, 34 | 0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A, 35 | 0x1E, 0xDE, 0xDF, 0x1F, 0xDD, 0x1D, 0x1C, 0xDC, 0x14, 0xD4, 36 | 0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3, 37 | 0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3, 38 | 0xF2, 0x32, 0x36, 0xF6, 0xF7, 0x37, 0xF5, 0x35, 0x34, 0xF4, 39 | 0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A, 40 | 0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8, 0xE9, 0x29, 41 | 0xEB, 0x2B, 0x2A, 0xEA, 0xEE, 0x2E, 0x2F, 0xEF, 0x2D, 0xED, 42 | 0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26, 43 | 0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60, 44 | 0x61, 0xA1, 0x63, 0xA3, 0xA2, 0x62, 0x66, 0xA6, 0xA7, 0x67, 45 | 0xA5, 0x65, 0x64, 0xA4, 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F, 46 | 0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68, 47 | 0x78, 0xB8, 0xB9, 0x79, 0xBB, 0x7B, 0x7A, 0xBA, 0xBE, 0x7E, 48 | 0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5, 49 | 0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71, 50 | 0x70, 0xB0, 0x50, 0x90, 0x91, 0x51, 0x93, 0x53, 0x52, 0x92, 51 | 0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C, 52 | 0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B, 53 | 0x99, 0x59, 0x58, 0x98, 0x88, 0x48, 0x49, 0x89, 0x4B, 0x8B, 54 | 0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C, 55 | 0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42, 56 | 0x43, 0x83, 0x41, 0x81, 0x80, 0x40 57 | ] 58 | 59 | def crc16(array, crc=0x0000): 60 | ''' 61 | crc16 IBM if crc is 0x0000 62 | crc16 MODBUS if crc is 0xffff 63 | ''' 64 | crchi = crc >> 8 65 | crclo = crc & 0xff 66 | for i in range(0, len(array)): 67 | crcindex = crchi ^ array[i] 68 | crchi = crclo ^ auchCRCHi[crcindex] 69 | crclo = auchCRCLo[crcindex] 70 | return crclo << 8 | crchi 71 | 72 | -------------------------------------------------------------------------------- /COMTool/plugins/graph.py: -------------------------------------------------------------------------------- 1 | 2 | import enum 3 | from PyQt5.QtCore import QObject, Qt 4 | from PyQt5.QtWidgets import (QApplication, QWidget,QPushButton,QMessageBox,QDesktopWidget,QMainWindow, 5 | QVBoxLayout,QHBoxLayout,QGridLayout,QTextEdit,QLabel,QRadioButton,QCheckBox, 6 | QLineEdit,QGroupBox,QSplitter,QFileDialog, QScrollArea, QListWidget) 7 | try: 8 | from .base import Plugin_Base 9 | from Combobox import ComboBox 10 | from i18n import _ 11 | import utils, parameters 12 | from conn.base import ConnectionStatus 13 | from widgets import statusBar 14 | from plugins.graph_widgets import graphWidgets 15 | except ImportError: 16 | from COMTool import utils, parameters 17 | from COMTool.i18n import _ 18 | from COMTool.Combobox import ComboBox 19 | from COMTool.conn.base import ConnectionStatus 20 | from COMTool.widgets import statusBar 21 | from COMTool.plugins.graph_widgets import graphWidgets 22 | from COMTool.plugins.base import Plugin_Base 23 | 24 | 25 | class Plugin(Plugin_Base): 26 | ''' 27 | call sequence: 28 | set vars like hintSignal, hintSignal 29 | onInit 30 | onWidget 31 | onUiInitDone 32 | onActive 33 | send 34 | onReceived 35 | onDel 36 | ''' 37 | # vars set by caller 38 | isConnected = lambda o: False 39 | send = lambda o,x,y:None # send(data_bytes=None, file_path=None, callback=lambda ok,msg:None), can call in UI thread directly 40 | ctrlConn = lambda o,k,v:None # call ctrl func of connection 41 | hintSignal = None # hintSignal.emit(type(error, warning, info), title, msg) 42 | reloadWindowSignal = None # reloadWindowSignal.emit(title, msg, callback(close or not)), reload window to load new configs 43 | configGlobal = {} 44 | # other vars 45 | connParent = "main" # parent id 46 | connChilds = [] # children ids 47 | id = "graph" 48 | name = _("Graph") 49 | 50 | enabled = False # user enabled this plugin 51 | active = False # using this plugin 52 | 53 | help = '{}

{}

Python


{}

{}
{}

{}


C/C++


{}
'.format( 54 | _("Double click graph item to add a graph widget"), _("line chart plot protocol:"), 55 | ''' 56 | from COMTool.plugins import graph_protocol 57 | 58 | # For ASCII protocol("binary protocol" not checked) 59 | frame = graph_protocol.plot_pack(name, x, y, binary = False) 60 | 61 | # For binary protocol("binary protocol" checked) 62 | frame = graph_protocol.plot_pack(name, x, y, header= b'\\xAA\\xCC\\xEE\\xBB') 63 | ''', 64 | _("Full demo see:"), 65 | 'https://github.com/Neutree/COMTool/tree/master/tool/send_curve_demo.py', 66 | _("Install comtool by pip install comtool first"), 67 | ''' 68 | 69 | /*******'''+ _('For ASCII protocol("binary protocol" not checked)') + ''' *******/ 70 | /** 71 | * $[line name],[x],[y]<,checksum>\\n 72 | * ''' + _('"$" means start of frame, end with "\\n" "," means separator') + ''', 73 | * ''' + _('checksum is optional, checksum is sum of all bytes in frame except ",checksum".') + ''' 74 | * ''' + _('[x] is optional') + ''' 75 | * ''' + _('e.g.') + ''' 76 | * "$roll,2.0\\n" 77 | * "$roll,1.0,2.0\\n" 78 | * "$pitch,1.0,2.0\\r\\n" 79 | * "$pitch,1.0,2.0,179\\n" (179 = sum(b"$pitch,1.0,2.0") % 256) 80 | */ 81 | int plot_pack_ascii(uint8_t *buff, int buff_len, const char *name, float x, float y) 82 | { 83 | snprintf(buff, buff_len, "$%s,%f,%f", name, x, y); 84 | //snprintf(buff, buff_len, "$%s,%f", name, y); 85 | // add checksum 86 | int sum = 0; 87 | for (int i = 0; i < strlen(buff); i++) 88 | { 89 | sum += buff[i]; 90 | } 91 | snprintf(buff + strlen(buff), buff_len - strlen(buff), ",%d\\n", sum & 0xFF); 92 | return strlen(buff); 93 | } 94 | 95 | uint8_t buff[64]; 96 | double x = 1.0, y = 2.0; 97 | int len = plot_pack_ascii(buff, sizeof(buff), "data1", x, y); 98 | send_bytes(buff, len); 99 | /*****************************************************************/ 100 | 101 | 102 | /******* ''' + _('For binary protocol("binary protocol" checked)') + ''' *******/ 103 | int plot_pack_binary(uint8_t *buff, int buff_len, 104 | uint8_t *header, int header_len, 105 | char *name, 106 | double x, double y) 107 | { 108 | uint8_t len = (uint8_t)strlen(name); 109 | int actual_len = header_len + 1 + len + 8 + 8 + 1; 110 | assert(actual_len <= buff_len); 111 | 112 | memcpy(buff, header, header_len); 113 | buff[header_len] = len; 114 | memcpy(buff + 5, name, len); 115 | memcpy(buff + 5 + len, &x, 8); 116 | memcpy(buff + 5 + len + 8, &y, 8); 117 | int sum = 0; 118 | for (int i = 0; i < header_len+1+len+8+8; i++) 119 | { 120 | sum += buff[i]; 121 | } 122 | buff[header_len+1+len+8+8] = (uint8_t)(sum & 0xff); 123 | return header_len+1+len+8+8+1; 124 | } 125 | 126 | uint8_t buff[64]; 127 | uint8_t header[] = {0xAA, 0xCC, 0xEE, 0xBB}; 128 | double x = 1.0, y = 2.0; 129 | int len = plot_pack_binary(buff, sizeof(buff), header, sizeof(header), "data1", x, y); 130 | send_bytes(buff, len); 131 | /*****************************************************************/ 132 | 133 | 134 | ''') 135 | 136 | def __init__(self): 137 | super().__init__() 138 | if not self.id: 139 | raise ValueError(f"var id of Plugin {self} should be set") 140 | 141 | def onInit(self, config): 142 | ''' 143 | init params, DO NOT take too long time in this func 144 | @config dict type, just change this var's content, 145 | when program exit, this config will be auto save to config file 146 | ''' 147 | self.config = config 148 | default = { 149 | "version": 1, 150 | "graphWidgets": [ 151 | # { 152 | # "id": "plot", 153 | # "config": {} 154 | # } 155 | ] 156 | } 157 | for k in default: 158 | if not k in self.config: 159 | self.config[k] = default[k] 160 | self.widgets = [] 161 | 162 | def onDel(self): 163 | pass 164 | 165 | def onWidgetMain(self, parent): 166 | ''' 167 | main widget, just return a QWidget object 168 | ''' 169 | widget = QWidget() 170 | widget.setProperty("class", "scrollbar2") 171 | layout = QVBoxLayout(widget) 172 | scroll = QScrollArea() 173 | scroll.setWidgetResizable(True) 174 | scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) 175 | scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn) 176 | layout.addWidget(scroll) 177 | widget2 = QWidget() 178 | scroll.setWidget(widget2) 179 | self.widgetsLayout = QVBoxLayout() 180 | widget2.setLayout(self.widgetsLayout) 181 | widget.resize(600, 400) 182 | # load graph widgets 183 | for item in self.config["graphWidgets"]: 184 | if not item["id"] in graphWidgets: 185 | continue 186 | c = graphWidgets[item["id"]] 187 | w = c(hintSignal = self.hintSignal, rmCallback = self.rmWidgetFromMain, send=self.sendData, config=item["config"]) 188 | self.widgets.append(w) 189 | self.widgetsLayout.addWidget(w) 190 | return widget 191 | 192 | def onWidgetSettings(self, parent): 193 | ''' 194 | setting widget, just return a QWidget object or None 195 | ''' 196 | itemList = QListWidget() 197 | for k,v in graphWidgets.items(): 198 | itemList.addItem(k) 199 | itemList.setToolTip(_("Double click to add a graph widget")) 200 | itemList.setCurrentRow(0) 201 | itemList.itemDoubleClicked.connect(self.addWidgetToMain) 202 | return itemList 203 | 204 | def addWidgetToMain(self, item): 205 | for k, c in graphWidgets.items(): 206 | if k == item.text(): 207 | config = { 208 | "id": c.id, 209 | "config": {} 210 | } 211 | w = c(hintSignal = self.hintSignal, rmCallback = self.rmWidgetFromMain, send=self.sendData, config=config["config"]) 212 | self.widgets.append(w) 213 | self.widgetsLayout.addWidget(w) 214 | self.config["graphWidgets"].append(config) 215 | 216 | def rmWidgetFromMain(self, widget): 217 | self.widgetsLayout.removeWidget(widget) 218 | for item in self.config["graphWidgets"]: 219 | if id(item["config"]) == id(widget.config): 220 | self.config["graphWidgets"].remove(item) 221 | break 222 | widget.deleteLater() 223 | self.widgets.remove(widget) 224 | 225 | def onWidgetFunctional(self, parent): 226 | ''' 227 | functional widget, just return a QWidget object or None 228 | ''' 229 | button = QPushButton(_("Clear count")) 230 | button.clicked.connect(self.clearCount) 231 | return button 232 | 233 | def onWidgetStatusBar(self, parent): 234 | self.statusBar = statusBar(rxTxCount=True) 235 | return self.statusBar 236 | 237 | def clearCount(self): 238 | self.statusBar.clear() 239 | 240 | def onReceived(self, data : bytes): 241 | ''' 242 | call in receive thread, not UI thread 243 | ''' 244 | self.statusBar.addRx(len(data)) 245 | for w in self.widgets: 246 | w.onData(data) 247 | 248 | def sendData(self, data:bytes): 249 | ''' 250 | send data, chidren call send will invoke this function 251 | if you send data in this plugin, you can directly call self.send 252 | ''' 253 | self.send(data, callback=self.onSent) 254 | 255 | def onSent(self, ok, msg, length, path): 256 | if ok: 257 | self.statusBar.addTx(length) 258 | else: 259 | self.hintSignal.emit("error", _("Error"), _("Send data failed!") + " " + msg) 260 | 261 | def onKeyPressEvent(self, event): 262 | for w in self.widgets: 263 | w.onKeyPressEvent(event) 264 | 265 | def onKeyReleaseEvent(self, event): 266 | for w in self.widgets: 267 | w.onKeyReleaseEvent(event) 268 | 269 | def onUiInitDone(self): 270 | ''' 271 | UI init done, you can update your widget here 272 | this method runs in UI thread, do not block too long 273 | ''' 274 | pass 275 | 276 | def onActive(self): 277 | ''' 278 | plugin active 279 | ''' 280 | pass 281 | -------------------------------------------------------------------------------- /COMTool/plugins/graph_protocol.py: -------------------------------------------------------------------------------- 1 | from struct import pack, unpack 2 | 3 | def plot_pack(name:str, x:float, y:float, header= b'\xAA\xCC\xEE\xBB', binary = True): 4 | name = name.encode() 5 | if binary: 6 | f = header 7 | f+= pack("B", len(name )) 8 | f+=name 9 | f += pack("d", x) 10 | f += pack("d", y) 11 | f += pack("B", sum(f)%256) 12 | else: 13 | f = "${},{},{}".format(name, x, y).encode() 14 | checksum = sum(f) & 0xFF 15 | f += b',{:d}\n'.format(checksum) 16 | return f 17 | -------------------------------------------------------------------------------- /COMTool/plugins/graph_widgets_base.py: -------------------------------------------------------------------------------- 1 | 2 | from PyQt5.QtWidgets import (QWidget) 3 | 4 | class Graph_Widget_Base(QWidget): 5 | def __init__(self, parent=None, hintSignal=lambda type, title, msg: None, rmCallback=lambda widget: None, send=lambda x: None, config=None, defaultConfig=None): 6 | QWidget.__init__(self, parent) 7 | self.hintSignal = hintSignal 8 | self.rmCallback = rmCallback 9 | self.send = send 10 | if config is None: 11 | config = {} 12 | if not defaultConfig: 13 | defaultConfig = {} 14 | self.config = config 15 | for k in defaultConfig: 16 | if not k in self.config: 17 | self.config[k] = defaultConfig[k] 18 | 19 | def onData(self, data: bytes): 20 | pass 21 | 22 | def onKeyPressEvent(self, event): 23 | pass 24 | 25 | def onKeyReleaseEvent(self, event): 26 | pass 27 | 28 | -------------------------------------------------------------------------------- /COMTool/plugins/myplugin.py: -------------------------------------------------------------------------------- 1 | ''' 2 | @brief This is an example plugin, can receive and send data 3 | @author 4 | @date 5 | @license LGPL-3.0 6 | ''' 7 | from PyQt5.QtWidgets import QWidget, QVBoxLayout, QTextEdit, QPushButton, QLineEdit 8 | from PyQt5.QtCore import pyqtSignal 9 | from PyQt5.QtGui import QFont, QTextCursor 10 | 11 | try: 12 | from plugins.base import Plugin_Base 13 | from conn import ConnectionStatus 14 | from i18n import _ 15 | except ImportError: 16 | from COMTool.plugins.base import Plugin_Base 17 | from COMTool.i18n import _ 18 | from COMTool.conn import ConnectionStatus 19 | 20 | class Plugin(Plugin_Base): 21 | id = "myplugin" 22 | name = _("my plugin") 23 | updateSignal = pyqtSignal(str, str) 24 | 25 | def onConnChanged(self, status:ConnectionStatus, msg:str): 26 | super().onConnChanged(status, msg) 27 | print("-- connection changed: {}, msg: {}".format(status, msg)) 28 | 29 | def onWidgetMain(self, parent): 30 | ''' 31 | main widget, just return a QWidget object 32 | ''' 33 | self.widget = QWidget() 34 | layout = QVBoxLayout() 35 | # receive widget 36 | self.receiveArea = QTextEdit("") 37 | font = QFont('Menlo,Consolas,Bitstream Vera Sans Mono,Courier New,monospace, Microsoft YaHei', 10) 38 | self.receiveArea.setFont(font) 39 | self.receiveArea.setLineWrapMode(QTextEdit.NoWrap) 40 | # send input widget 41 | self.input = QTextEdit() 42 | self.input.setAcceptRichText(False) 43 | # send button 44 | self.button = QPushButton(_("Send")) 45 | # add widgets 46 | layout.addWidget(self.receiveArea) 47 | layout.addWidget(self.input) 48 | layout.addWidget(self.button) 49 | self.widget.setLayout(layout) 50 | # event 51 | self.button.clicked.connect(self.buttonSend) 52 | self.updateSignal.connect(self.updateUI) 53 | return self.widget 54 | 55 | def buttonSend(self): 56 | ''' 57 | UI thread 58 | ''' 59 | data = self.input.toPlainText() 60 | if not data: 61 | # to pop up a warning window 62 | self.hintSignal.emit("error", _("Error"), _("Input data first please") ) 63 | return 64 | dataBytes = data.encode(self.configGlobal["encoding"]) 65 | self.send(dataBytes) 66 | 67 | def updateUI(self, dataType, data): 68 | ''' 69 | UI thread 70 | ''' 71 | if dataType == "receive": 72 | self.receiveArea.moveCursor(QTextCursor.End) 73 | self.receiveArea.insertPlainText(data) 74 | 75 | def onReceived(self, data : bytes): 76 | ''' 77 | call in receive thread, not UI thread 78 | ''' 79 | super().onReceived(data) 80 | # decode data 81 | dataStr = data.decode(self.configGlobal["encoding"]) 82 | # DO NOT set seld.receiveBox here for all UI operation should be in UI thread, 83 | # instead, set self.receiveBox in UI thread, we can use signal to send data to UI thread 84 | self.updateSignal.emit("receive", dataStr) 85 | -------------------------------------------------------------------------------- /COMTool/plugins/myplugin2/README.md: -------------------------------------------------------------------------------- 1 | Plugin demo 2 | ==== 3 | 4 | 5 | to use this plugin, just install in this directory: 6 | ``` 7 | by pip install . 8 | ``` 9 | 10 | 11 | -------------------------------------------------------------------------------- /COMTool/plugins/myplugin2/comtool_plugin_myplugin2/__init__.py: -------------------------------------------------------------------------------- 1 | from .myplugin2 import Plugin 2 | 3 | -------------------------------------------------------------------------------- /COMTool/plugins/myplugin2/comtool_plugin_myplugin2/locales/en/LC_MESSAGES/messages.po: -------------------------------------------------------------------------------- 1 | # English translations for PROJECT. 2 | # Copyright (C) 2022 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2022. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: PROJECT VERSION\n" 9 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 10 | "POT-Creation-Date: 2022-04-05 19:24+0800\n" 11 | "PO-Revision-Date: 2022-04-05 19:24+0800\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language: en\n" 14 | "Language-Team: en \n" 15 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.9.1\n" 20 | 21 | #: myplugin2.py:26 22 | msgid "my plugin2" 23 | msgstr "" 24 | 25 | #: myplugin2.py:47 26 | msgid "Send" 27 | msgstr "" 28 | 29 | #: myplugin2.py:65 30 | msgid "Error" 31 | msgstr "" 32 | 33 | #: myplugin2.py:65 34 | msgid "Input data first please" 35 | msgstr "" 36 | 37 | -------------------------------------------------------------------------------- /COMTool/plugins/myplugin2/comtool_plugin_myplugin2/locales/ja/LC_MESSAGES/messages.po: -------------------------------------------------------------------------------- 1 | # Japanese translations for PROJECT. 2 | # Copyright (C) 2022 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2022. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: PROJECT VERSION\n" 9 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 10 | "POT-Creation-Date: 2022-04-05 19:24+0800\n" 11 | "PO-Revision-Date: 2022-04-05 19:24+0800\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language: ja\n" 14 | "Language-Team: ja \n" 15 | "Plural-Forms: nplurals=1; plural=0\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.9.1\n" 20 | 21 | #: myplugin2.py:26 22 | msgid "my plugin2" 23 | msgstr "" 24 | 25 | #: myplugin2.py:47 26 | msgid "Send" 27 | msgstr "" 28 | 29 | #: myplugin2.py:65 30 | msgid "Error" 31 | msgstr "" 32 | 33 | #: myplugin2.py:65 34 | msgid "Input data first please" 35 | msgstr "" 36 | 37 | -------------------------------------------------------------------------------- /COMTool/plugins/myplugin2/comtool_plugin_myplugin2/locales/messages.pot: -------------------------------------------------------------------------------- 1 | # Translations template for PROJECT. 2 | # Copyright (C) 2022 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2022. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PROJECT VERSION\n" 10 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 11 | "POT-Creation-Date: 2022-04-05 19:24+0800\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Generated-By: Babel 2.9.1\n" 19 | 20 | #: myplugin2.py:26 21 | msgid "my plugin2" 22 | msgstr "" 23 | 24 | #: myplugin2.py:47 25 | msgid "Send" 26 | msgstr "" 27 | 28 | #: myplugin2.py:65 29 | msgid "Error" 30 | msgstr "" 31 | 32 | #: myplugin2.py:65 33 | msgid "Input data first please" 34 | msgstr "" 35 | 36 | -------------------------------------------------------------------------------- /COMTool/plugins/myplugin2/comtool_plugin_myplugin2/locales/zh_CN/LC_MESSAGES/messages.po: -------------------------------------------------------------------------------- 1 | # Chinese (Simplified, China) translations for PROJECT. 2 | # Copyright (C) 2022 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2022. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: PROJECT VERSION\n" 9 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 10 | "POT-Creation-Date: 2022-04-05 19:24+0800\n" 11 | "PO-Revision-Date: 2022-04-05 19:24+0800\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language: zh_Hans_CN\n" 14 | "Language-Team: zh_Hans_CN \n" 15 | "Plural-Forms: nplurals=1; plural=0\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.9.1\n" 20 | 21 | #: myplugin2.py:26 22 | msgid "my plugin2" 23 | msgstr "我的插件2" 24 | 25 | #: myplugin2.py:47 26 | msgid "Send" 27 | msgstr "发送" 28 | 29 | #: myplugin2.py:65 30 | msgid "Error" 31 | msgstr "错误" 32 | 33 | #: myplugin2.py:65 34 | msgid "Input data first please" 35 | msgstr "请先输入数据" 36 | 37 | -------------------------------------------------------------------------------- /COMTool/plugins/myplugin2/comtool_plugin_myplugin2/locales/zh_TW/LC_MESSAGES/messages.po: -------------------------------------------------------------------------------- 1 | # Chinese (Traditional, Taiwan) translations for PROJECT. 2 | # Copyright (C) 2022 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2022. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: PROJECT VERSION\n" 9 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 10 | "POT-Creation-Date: 2022-04-05 19:24+0800\n" 11 | "PO-Revision-Date: 2022-04-05 19:24+0800\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language: zh_Hant_TW\n" 14 | "Language-Team: zh_Hant_TW \n" 15 | "Plural-Forms: nplurals=1; plural=0\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.9.1\n" 20 | 21 | #: myplugin2.py:26 22 | msgid "my plugin2" 23 | msgstr "" 24 | 25 | #: myplugin2.py:47 26 | msgid "Send" 27 | msgstr "" 28 | 29 | #: myplugin2.py:65 30 | msgid "Error" 31 | msgstr "" 32 | 33 | #: myplugin2.py:65 34 | msgid "Input data first please" 35 | msgstr "" 36 | 37 | -------------------------------------------------------------------------------- /COMTool/plugins/myplugin2/comtool_plugin_myplugin2/myplugin2.py: -------------------------------------------------------------------------------- 1 | ''' 2 | @brief This is an example plugin, can receive and send data 3 | @author 4 | @date 5 | @license LGPL-3.0 6 | ''' 7 | from PyQt5.QtWidgets import QWidget, QVBoxLayout, QTextEdit, QPushButton, QLineEdit 8 | from PyQt5.QtCore import pyqtSignal 9 | from PyQt5.QtGui import QFont, QTextCursor 10 | 11 | try: 12 | from plugins.base import Plugin_Base 13 | from conn import ConnectionStatus 14 | except ImportError: 15 | from COMTool.plugins.base import Plugin_Base 16 | from COMTool.conn import ConnectionStatus 17 | 18 | # i18n for this plugin 19 | # to use translation, use `comtool-i18n -p COMTool/plugins/myplugin2/comtool_plugin_myplugin2 prepare` first 20 | # and translate file in locales dir 21 | # use `comtool-i18n -p COMTool/plugins/myplugin2/comtool_plugin_myplugin2 finish` to generate translate binary files(*.mo) 22 | # `comtool-i18n` command from comtool, or you can run source `python COMTool/i18n.py -p COMTool/plugins/myplugin2/comtool_plugin_myplugin2 prepare` 23 | try: 24 | from .plugin_i18n import _ 25 | except Exception: 26 | from plugin_i18n import _ 27 | 28 | class Plugin(Plugin_Base): 29 | id = "myplugin2" 30 | name = _("my plugin2") 31 | updateSignal = pyqtSignal(str, str) 32 | 33 | def onConnChanged(self, status:ConnectionStatus, msg:str): 34 | super().onConnChanged(status, msg) 35 | print("-- connection changed: {}, msg: {}".format(status, msg)) 36 | 37 | def onWidgetMain(self, parent): 38 | ''' 39 | main widget, just return a QWidget object 40 | ''' 41 | self.widget = QWidget() 42 | layout = QVBoxLayout() 43 | # receive widget 44 | self.receiveArea = QTextEdit("") 45 | font = QFont('Menlo,Consolas,Bitstream Vera Sans Mono,Courier New,monospace, Microsoft YaHei', 10) 46 | self.receiveArea.setFont(font) 47 | self.receiveArea.setLineWrapMode(QTextEdit.NoWrap) 48 | # send input widget 49 | self.input = QTextEdit() 50 | self.input.setAcceptRichText(False) 51 | # send button 52 | self.button = QPushButton(_("Send")) 53 | # add widgets 54 | layout.addWidget(self.receiveArea) 55 | layout.addWidget(self.input) 56 | layout.addWidget(self.button) 57 | self.widget.setLayout(layout) 58 | # event 59 | self.button.clicked.connect(self.buttonSend) 60 | self.updateSignal.connect(self.updateUI) 61 | return self.widget 62 | 63 | def buttonSend(self): 64 | ''' 65 | UI thread 66 | ''' 67 | data = self.input.toPlainText() 68 | if not data: 69 | # to pop up a warning window 70 | self.hintSignal.emit("error", _("Error"), _("Input data first please") ) 71 | return 72 | dataBytes = data.encode(self.configGlobal["encoding"]) 73 | self.send(dataBytes) 74 | 75 | def updateUI(self, dataType, data): 76 | ''' 77 | UI thread 78 | ''' 79 | if dataType == "receive": 80 | self.receiveArea.moveCursor(QTextCursor.End) 81 | self.receiveArea.insertPlainText(data) 82 | 83 | def onReceived(self, data : bytes): 84 | ''' 85 | call in receive thread, not UI thread 86 | ''' 87 | super().onReceived(data) 88 | # decode data 89 | dataStr = data.decode(self.configGlobal["encoding"]) 90 | # DO NOT set seld.receiveBox here for all UI operation should be in UI thread, 91 | # instead, set self.receiveBox in UI thread, we can use signal to send data to UI thread 92 | self.updateSignal.emit("receive", dataStr) 93 | -------------------------------------------------------------------------------- /COMTool/plugins/myplugin2/comtool_plugin_myplugin2/plugin_i18n.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | try: 4 | from i18n import locales, main 5 | from parameters import log 6 | except Exception: 7 | from COMTool.i18n import locales, main 8 | from COMTool.parameters import log 9 | 10 | 11 | import gettext 12 | 13 | currDir = os.path.abspath(os.path.dirname(__file__)) 14 | localesDir = os.path.join(currDir, 'locales') 15 | mo_path = os.path.join(localesDir, "en", "LC_MESSAGES", "messages.mo") 16 | # detect if no translate binary files, generate 17 | if not os.path.exists(mo_path): 18 | main("finish", path = currDir) 19 | 20 | try: 21 | lang = gettext.translation('messages', localedir=localesDir, languages=locales) 22 | lang.install() 23 | _ = lang.gettext 24 | except Exception as e: 25 | msg = "can not find plugin i18n files in {}".format(currDir) 26 | log.e(msg) 27 | raise Exception(msg) 28 | 29 | -------------------------------------------------------------------------------- /COMTool/plugins/myplugin2/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup,find_packages 2 | from codecs import open 3 | from os import path 4 | import os 5 | from COMTool import helpAbout, parameters, i18n, version 6 | import platform 7 | 8 | here = os.path.abspath(path.abspath(path.dirname(__file__))) 9 | packageDir = os.path.join(here, "comtool_plugin_myplugin2") 10 | 11 | # update translate 12 | i18n.main("finish", path = packageDir) 13 | 14 | # Get the long description from the README file 15 | with open(path.join(here, 'README.MD'), encoding='utf-8') as f: 16 | long_description = f.read() 17 | 18 | systemPlatform = platform.platform() 19 | installRequires = [] 20 | 21 | setup( 22 | name='comtool-plugin-myplugin2', 23 | 24 | # Versions should comply with PEP440. For a discussion on single-sourcing 25 | # the version across setup.py and the project code, see 26 | # https://packaging.python.org/en/latest/single_source_version.html 27 | version="v1.0.0", 28 | 29 | # Author details 30 | author='Neucrack', 31 | author_email='czd666666@gmail.com', 32 | 33 | description='plugin demo for comtool', 34 | long_description=long_description, 35 | long_description_content_type="text/markdown", 36 | 37 | # The project's main homepage. 38 | url='https://github.com/Neutree/COMTool', 39 | 40 | # Choose your license 41 | license='LGPL-3.0', 42 | 43 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 44 | classifiers=[ 45 | # How mature is this project? Common values are 46 | # 3 - Alpha 47 | # 4 - Beta 48 | # 5 - Production/Stable 49 | 'Development Status :: 5 - Production/Stable', 50 | 51 | # Indicate who your project is intended for 52 | 'Intended Audience :: Developers', 53 | 'Topic :: Software Development :: Embedded Systems', 54 | 'Topic :: Software Development :: Debuggers', 55 | 56 | # Pick your license as you wish (should match "license" above) 57 | 'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)', 58 | 59 | # Specify the Python versions you support here. In particular, ensure 60 | # that you indicate whether you support Python 2, Python 3 or both. 61 | 'Programming Language :: Python :: 3', 62 | 'Programming Language :: Python :: 3.8', 63 | 'Programming Language :: Python :: 3.9', 64 | 'Programming Language :: Python :: 3.10', 65 | ], 66 | 67 | # What does your project relate to? 68 | keywords='comtool plugin', 69 | 70 | # You can just specify the packages manually here if your project is 71 | # simple. Or you can use find_packages(). 72 | packages=find_packages(), 73 | 74 | # Alternatively, if you want to distribute just a my_module.py, uncomment 75 | # this: 76 | # py_modules=["my_module"], 77 | 78 | # List run-time dependencies here. These will be installed by pip when 79 | # your project is installed. For an analysis of "install_requires" vs pip's 80 | # requirements files see: 81 | # https://packaging.python.org/en/latest/requirements.html 82 | install_requires=installRequires, 83 | 84 | # List additional groups of dependencies here (e.g. development 85 | # dependencies). You can install these using the following syntax, 86 | # for example: 87 | # $ pip install -e .[dev,test] 88 | extras_require={ 89 | # 'dev': ['check-manifest'], 90 | # 'test': ['coverage'], 91 | }, 92 | 93 | # If there are data files included in your packages that need to be 94 | # installed, specify them here. If using Python 2.6 or less, then these 95 | # have to be included in MANIFEST.in as well. 96 | package_data={ 97 | 'COMTool': ["locales/*/*/*.?o"], 98 | }, 99 | 100 | # Although 'package_data' is the preferred approach, in some case you may 101 | # need to place data files outside of your packages. See: 102 | # http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files # noqa 103 | # In this case, 'data_file' will be installed into '/my_data' 104 | data_files=[ 105 | ("",["README.MD"]) 106 | ], 107 | 108 | # To provide executable scripts, use entry points in preference to the 109 | # "scripts" keyword. Entry points provide cross-platform support and allow 110 | # pip to create the appropriate form of executable for the target platform. 111 | entry_points={ 112 | # 'console_scripts': [ 113 | # 'gui_scripts': [ 114 | # 'comtool=COMTool.Main:main', 115 | # ], 116 | }, 117 | ) 118 | 119 | -------------------------------------------------------------------------------- /COMTool/plugins/protocols.py: -------------------------------------------------------------------------------- 1 | try: 2 | import parameters 3 | except ImportError: 4 | from COMTool import parameters 5 | import os 6 | 7 | protocols_dir = os.path.join(parameters.dataPath, "protocols") 8 | 9 | default = ''' 10 | def decode(data:bytes) -> bytes: 11 | return data 12 | 13 | def encode(data:bytes) -> bytes: 14 | return data 15 | ''' 16 | 17 | add_crc16 = ''' 18 | 19 | def decode(data:bytes) -> bytes: 20 | return data 21 | 22 | def encode(data:bytes) -> bytes: 23 | crc_bytes = pack(" bytes: 29 | return data 30 | 31 | def encode(data:bytes) -> bytes: 32 | return data + bytes([sum(data) % 256]) 33 | ''' 34 | 35 | 36 | defaultProtocols = { 37 | "default": default, 38 | "add_crc16": add_crc16, 39 | "add_sum": add_sum, 40 | } 41 | ignoreList = ["maix-smart"] 42 | 43 | for file in os.listdir(protocols_dir): 44 | name, ext = os.path.splitext(file) 45 | if name in ignoreList: 46 | continue 47 | if ext.endswith(".py"): 48 | with open(os.path.join(protocols_dir, file)) as f: 49 | code = f.read() 50 | defaultProtocols[name] = code 51 | 52 | 53 | -------------------------------------------------------------------------------- /COMTool/qta_icon_browser.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from qtpy import QtCore, QtGui, QtWidgets 4 | 5 | import qtawesome 6 | 7 | 8 | # TODO: Set icon colour and copy code with color kwarg 9 | 10 | VIEW_COLUMNS = 5 11 | AUTO_SEARCH_TIMEOUT = 500 12 | ALL_COLLECTIONS = 'All' 13 | 14 | 15 | class IconBrowser(QtWidgets.QDialog): 16 | """ 17 | A small browser window that allows the user to search through all icons from 18 | the available version of QtAwesome. You can also copy the name and python 19 | code for the currently selected icon. 20 | """ 21 | 22 | def __init__(self, parent = None, 23 | dialog = False, 24 | title = 'QtAwesome Icon Browser', 25 | btnOkName = 'OK', 26 | btnCopyName = 'Copy Name', 27 | color = "gray" 28 | ): 29 | super().__init__(parent) 30 | 31 | self.dialog = dialog 32 | self.selected = "" 33 | self.color = color 34 | 35 | self.setMinimumSize(400, 600) 36 | self.setWindowTitle(title) 37 | if dialog: 38 | self.setWindowModality(QtCore.Qt.WindowModal) 39 | 40 | qtawesome._instance() 41 | fontMaps = qtawesome._resource['iconic'].charmap 42 | 43 | iconNames = [] 44 | for fontCollection, fontData in fontMaps.items(): 45 | for iconName in fontData: 46 | iconNames.append('%s.%s' % (fontCollection, iconName)) 47 | 48 | self._filterTimer = QtCore.QTimer(self) 49 | self._filterTimer.setSingleShot(True) 50 | self._filterTimer.setInterval(AUTO_SEARCH_TIMEOUT) 51 | self._filterTimer.timeout.connect(self._updateFilter) 52 | 53 | model = IconModel(self.color) 54 | model.setStringList(sorted(iconNames)) 55 | 56 | self._proxyModel = QtCore.QSortFilterProxyModel() 57 | self._proxyModel.setSourceModel(model) 58 | self._proxyModel.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) 59 | 60 | self._listView = IconListView(self) 61 | self._listView.setUniformItemSizes(True) 62 | self._listView.setViewMode(QtWidgets.QListView.IconMode) 63 | self._listView.setModel(self._proxyModel) 64 | self._listView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) 65 | if dialog: 66 | self._listView.doubleClicked.connect(self._selectIconText) 67 | else: 68 | self._listView.doubleClicked.connect(self._copyIconText) 69 | self._listView.selectionModel().selectionChanged.connect(self._updateNameField) 70 | 71 | self._lineEdit = QtWidgets.QLineEdit(self) 72 | self._lineEdit.setAlignment(QtCore.Qt.AlignCenter) 73 | self._lineEdit.textChanged.connect(self._triggerDelayedUpdate) 74 | self._lineEdit.returnPressed.connect(self._triggerImmediateUpdate) 75 | 76 | self._comboBox = QtWidgets.QComboBox(self) 77 | self._comboBox.setMinimumWidth(75) 78 | self._comboBox.currentIndexChanged.connect(self._triggerImmediateUpdate) 79 | self._comboBox.addItems([ALL_COLLECTIONS] + sorted(fontMaps.keys())) 80 | 81 | lyt = QtWidgets.QHBoxLayout() 82 | lyt.setContentsMargins(0, 0, 0, 0) 83 | lyt.addWidget(self._comboBox) 84 | lyt.addWidget(self._lineEdit) 85 | self._combo_style = QtWidgets.QComboBox(self) 86 | self._combo_style.addItems([ 87 | qtawesome.styles.DEFAULT_DARK_PALETTE, 88 | qtawesome.styles.DEFAULT_LIGHT_PALETTE]) 89 | self._combo_style.currentTextChanged.connect(self._updateStyle) 90 | lyt.addWidget(self._combo_style) 91 | 92 | searchBarFrame = QtWidgets.QFrame(self) 93 | searchBarFrame.setLayout(lyt) 94 | 95 | self._nameField = QtWidgets.QLineEdit(self) 96 | self._nameField.setAlignment(QtCore.Qt.AlignCenter) 97 | self._nameField.setReadOnly(True) 98 | 99 | 100 | if self.dialog: 101 | self._copyButton = QtWidgets.QPushButton(btnOkName, self) 102 | self._copyButton.clicked.connect(self._selectIconText) 103 | else: 104 | self._copyButton = QtWidgets.QPushButton(btnCopyName, self) 105 | self._copyButton.clicked.connect(self._copyIconText) 106 | 107 | lyt = QtWidgets.QVBoxLayout() 108 | lyt.addWidget(searchBarFrame) 109 | lyt.addWidget(self._listView) 110 | lyt.addWidget(self._nameField) 111 | lyt.addWidget(self._copyButton) 112 | 113 | self.setLayout(lyt) 114 | 115 | self.setTabOrder(self._comboBox, self._lineEdit) 116 | self.setTabOrder(self._lineEdit, self._combo_style) 117 | self.setTabOrder(self._combo_style, self._listView) 118 | self.setTabOrder(self._listView, self._nameField) 119 | self.setTabOrder(self._nameField, self._copyButton) 120 | self.setTabOrder(self._copyButton, self._comboBox) 121 | 122 | QtWidgets.QShortcut( 123 | QtGui.QKeySequence(QtCore.Qt.Key_Return), 124 | self, 125 | self._copyIconText, 126 | ) 127 | QtWidgets.QShortcut( 128 | QtGui.QKeySequence("Ctrl+F"), 129 | self, 130 | self._lineEdit.setFocus, 131 | ) 132 | 133 | self._lineEdit.setFocus() 134 | 135 | geo = self.geometry() 136 | 137 | # QApplication.desktop() has been removed in Qt 6. 138 | # Instead, QGuiApplication.screenAt(QPoint) is supported 139 | # in Qt 5.10 or later. 140 | try: 141 | screen = QtGui.QGuiApplication.screenAt(QtGui.QCursor.pos()) 142 | centerPoint = screen.geometry().center() 143 | except AttributeError: 144 | desktop = QtWidgets.QApplication.desktop() 145 | screen = desktop.screenNumber(desktop.cursor().pos()) 146 | centerPoint = desktop.screenGeometry(screen).center() 147 | 148 | geo.moveCenter(centerPoint) 149 | self.setGeometry(geo) 150 | 151 | def _updateStyle(self, text: str): 152 | _app = QtWidgets.QApplication.instance() 153 | if text == qtawesome.styles.DEFAULT_DARK_PALETTE: 154 | qtawesome.reset_cache() 155 | qtawesome.dark(_app) 156 | else: 157 | qtawesome.reset_cache() 158 | qtawesome.light(_app) 159 | 160 | def _updateFilter(self): 161 | """ 162 | Update the string used for filtering in the proxy model with the 163 | current text from the line edit. 164 | """ 165 | reString = "" 166 | 167 | group = self._comboBox.currentText() 168 | if group != ALL_COLLECTIONS: 169 | reString += r"^%s\." % group 170 | 171 | searchTerm = self._lineEdit.text() 172 | if searchTerm: 173 | reString += ".*%s.*$" % searchTerm 174 | 175 | # QSortFilterProxyModel.setFilterRegExp has been removed in Qt 6. 176 | # Instead, QSortFilterProxyModel.setFilterRegularExpression is 177 | # supported in Qt 5.12 or later. 178 | try: 179 | self._proxyModel.setFilterRegularExpression(reString) 180 | except AttributeError: 181 | self._proxyModel.setFilterRegExp(reString) 182 | 183 | def _triggerDelayedUpdate(self): 184 | """ 185 | Reset the timer used for committing the search term to the proxy model. 186 | """ 187 | self._filterTimer.stop() 188 | self._filterTimer.start() 189 | 190 | def _triggerImmediateUpdate(self): 191 | """ 192 | Stop the timer used for committing the search term and update the 193 | proxy model immediately. 194 | """ 195 | self._filterTimer.stop() 196 | self._updateFilter() 197 | 198 | def _copyIconText(self): 199 | """ 200 | Copy the name of the currently selected icon to the clipboard. 201 | """ 202 | indexes = self._listView.selectedIndexes() 203 | if not indexes: 204 | return 205 | 206 | clipboard = QtWidgets.QApplication.instance().clipboard() 207 | clipboard.setText(indexes[0].data()) 208 | 209 | def _selectIconText(self): 210 | """ 211 | Select this button, close widget and return value 212 | """ 213 | indexes = self._listView.selectedIndexes() 214 | if not indexes: 215 | return 216 | 217 | self.selected = indexes[0].data() 218 | self.close() 219 | 220 | def exec(self): 221 | super().exec() 222 | return self.selected 223 | 224 | def _updateNameField(self): 225 | """ 226 | Update field to the name of the currently selected icon. 227 | """ 228 | indexes = self._listView.selectedIndexes() 229 | if not indexes: 230 | self._nameField.setText("") 231 | else: 232 | self._nameField.setText(indexes[0].data()) 233 | 234 | 235 | class IconListView(QtWidgets.QListView): 236 | """ 237 | A QListView that scales it's grid size to ensure the same number of 238 | columns are always drawn. 239 | """ 240 | 241 | def __init__(self, parent=None): 242 | super().__init__(parent) 243 | self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) 244 | 245 | def resizeEvent(self, event): 246 | """ 247 | Re-implemented to re-calculate the grid size to provide scaling icons 248 | Parameters 249 | ---------- 250 | event : QtCore.QEvent 251 | """ 252 | width = self.viewport().width() - 30 253 | # The minus 30 above ensures we don't end up with an item width that 254 | # can't be drawn the expected number of times across the view without 255 | # being wrapped. Without this, the view can flicker during resize 256 | tileWidth = width / VIEW_COLUMNS 257 | iconWidth = int(tileWidth * 0.8) 258 | # tileWidth needs to be an integer for setGridSize 259 | tileWidth = int(tileWidth) 260 | 261 | self.setGridSize(QtCore.QSize(tileWidth, tileWidth)) 262 | self.setIconSize(QtCore.QSize(iconWidth, iconWidth)) 263 | 264 | return super().resizeEvent(event) 265 | 266 | 267 | class IconModel(QtCore.QStringListModel): 268 | 269 | def __init__(self, color): 270 | super().__init__() 271 | self.color = color 272 | 273 | def flags(self, index): 274 | return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable 275 | 276 | def data(self, index, role): 277 | """ 278 | Re-implemented to return the icon for the current index. 279 | Parameters 280 | ---------- 281 | index : QtCore.QModelIndex 282 | role : int 283 | Returns 284 | ------- 285 | Any 286 | """ 287 | if role == QtCore.Qt.DecorationRole: 288 | iconString = self.data(index, role=QtCore.Qt.DisplayRole) 289 | return qtawesome.icon(iconString, color = self.color) 290 | return super().data(index, role) 291 | 292 | def selectIcon(parent = None, title = 'QtAwesome Icon Browser', btnName = 'OK', color = "gray"): 293 | browser = IconBrowser(parent, dialog = True, title=title, btnOkName= btnName, color=color) 294 | browser.show() 295 | selectedIcon = browser.exec() 296 | return selectedIcon 297 | 298 | def run(dialog = False): 299 | """ 300 | Start the IconBrowser and block until the process exits. 301 | """ 302 | app = QtWidgets.QApplication([]) 303 | qtawesome.dark(app) 304 | 305 | if dialog: 306 | return selectIcon() 307 | 308 | browser = IconBrowser() 309 | browser.show() 310 | 311 | sys.exit(app.exec_()) 312 | 313 | 314 | if __name__ == '__main__': 315 | dialog = False 316 | if len(sys.argv) > 1: 317 | dialog = sys.argv[1].lower() in ["true", "1"] 318 | ret = run(dialog = dialog) 319 | print("ret: ", ret) 320 | 321 | -------------------------------------------------------------------------------- /COMTool/settings.py: -------------------------------------------------------------------------------- 1 | 2 | from PyQt5.QtCore import pyqtSignal 3 | from PyQt5.QtWidgets import (QApplication, QWidget,QToolTip,QPushButton,QMessageBox,QDesktopWidget,QMainWindow, 4 | QVBoxLayout,QHBoxLayout,QGridLayout,QTextEdit,QLabel,QRadioButton,QCheckBox, 5 | QLineEdit,QGroupBox,QSplitter) 6 | 7 | 8 | class Settings(QWidget): 9 | 10 | closed = pyqtSignal() 11 | updatedisTextRawSignal = pyqtSignal(str) 12 | buffer = "" 13 | 14 | def __init__(self,parent = None): 15 | super().__init__(parent) 16 | self.init() 17 | self.initEvent() 18 | self.show() 19 | 20 | def __del__(self): 21 | pass 22 | 23 | def init(self): 24 | self.resize(500,400) 25 | self.mainLayout = QVBoxLayout() 26 | self.setLayout(self.mainLayout) 27 | self.disTextRaw = QLabel("0000") 28 | self.mainLayout.addWidget(self.disTextRaw) 29 | 30 | def initEvent(self): 31 | pass 32 | 33 | def closeEvent(self, event): 34 | self.closed.emit() 35 | event.accept() 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /COMTool/test.py: -------------------------------------------------------------------------------- 1 | # import unittest,sys 2 | # from COMTool import Main,helpAbout 3 | # 4 | # class COMTest(unittest.TestCase): 5 | # 6 | # def setUp(self): 7 | # print("setup") 8 | # 9 | # def tearDown(self): 10 | # print("teardown") 11 | # 12 | # def test_1(self): 13 | # print("test",sys.prefix) 14 | # Main.main() 15 | # 16 | # if __name__=="__main__": 17 | # unittest.main() 18 | # 19 | 20 | 21 | # from PyQt5.QtCore import pyqtSignal,Qt 22 | # from PyQt5.QtWidgets import (QApplication, QWidget,QToolTip,QPushButton,QMessageBox,QDesktopWidget,QMainWindow, 23 | # QVBoxLayout,QHBoxLayout,QGridLayout,QTextEdit,QLabel,QRadioButton,QCheckBox, 24 | # QLineEdit,QGroupBox,QSplitter) 25 | # from PyQt5.QtWidgets import QComboBox,QListView 26 | # from PyQt5.QtGui import QIcon,QFont,QTextCursor,QPixmap 27 | # from PyQt5.QtCore import pyqtSignal 28 | # import sys 29 | 30 | # class MyClass(object): 31 | # def __init__(self, arg): 32 | # super(MyClass, self).__init__() 33 | # self.arg = arg 34 | 35 | # class myWindow(QWidget): 36 | # def __init__(self, parent=None): 37 | # super(myWindow, self).__init__(parent) 38 | 39 | # self.comboBox = QComboBox(self) 40 | # self.comboBox.addItems([str(x) for x in range(3)]) 41 | 42 | # self.myObject=MyClass(self ) 43 | 44 | # slotLambda = lambda: self.indexChanged_lambda(self.myObject) 45 | # self.comboBox.currentIndexChanged.connect(slotLambda) 46 | 47 | # # @QtCore.pyqtSlot(str) 48 | # def indexChanged_lambda(self, obj): 49 | # print('lambda:', type(obj), obj.arg.comboBox.currentText()) 50 | 51 | # if __name__ == "__main__": 52 | # app = QApplication(sys.argv) 53 | # app.setApplicationName('myApp') 54 | # dialog = myWindow() 55 | # dialog.show() 56 | # sys.exit(app.exec_()) 57 | 58 | 59 | 60 | import re 61 | class A: 62 | def __init__(self) -> None: 63 | self.lastColor = None 64 | 65 | def _getColorByfmt(self, fmt:bytes): 66 | colors = { 67 | b"0": "#000000", 68 | b"31": "#ff0000", 69 | b"32": "#008000", 70 | b"33": "#ffff00" 71 | } 72 | fmt = fmt[2:-1].split(b";") 73 | if len(fmt) == 1: 74 | color = colors[b"0"] 75 | else: 76 | style, color = fmt 77 | color = colors[color] 78 | return color 79 | 80 | def _texSplitByColor(self, text:bytes): 81 | if not self.lastColor: 82 | self.lastColor = "#000000" 83 | colorFmt = re.findall(rb'\x1b\[.*?m', text) 84 | colorStrs = [] 85 | if colorFmt: 86 | p = 0 87 | for fmt in colorFmt: 88 | idx = text[p:].index(fmt) 89 | if idx != 0: 90 | colorStrs.append((self.lastColor, text[p:p+idx])) 91 | p += idx 92 | self.lastColor = self._getColorByfmt(fmt) 93 | p += len(fmt) 94 | if p != len(text): 95 | colorStrs.append((self.lastColor, text[p:])) 96 | else: 97 | colorStrs = [(self.lastColor, text)] 98 | return colorStrs 99 | 100 | text = b'\x1b[0;32mI (1092) esp_qcloud_prov: Scan this QR code from the Wechat for Provisioning.\x1b[0m' 101 | text2 = b'\x1b[0;32mI (1092) esp_qcloud_prov: Scan this QR code from the Wechat for Provisioning.\x1b[0mProvisioning' 102 | 103 | a = A() 104 | text = a._texSplitByColor(text) 105 | print(text) 106 | print(a._texSplitByColor(text2)) 107 | -------------------------------------------------------------------------------- /COMTool/utils.py: -------------------------------------------------------------------------------- 1 | 2 | from datetime import datetime 3 | import binascii 4 | import re 5 | 6 | def datetime_format_ms(dt): 7 | res = dt.strftime("%Y-%m-%d %H:%M:%S") 8 | return '{}.{:03d}'.format(res, int(round(dt.microsecond/1000))) 9 | 10 | def hexlify(bs, sed=b' '): 11 | tmp = b'%02X' + sed.encode() 12 | return b''.join([tmp % b for b in bs]) 13 | 14 | def bytes_to_hex_str(strB : bytes) -> str: 15 | strHex = binascii.b2a_hex(strB).upper() 16 | return re.sub(r"(?<=\w)(?=(?:\w\w)+$)", " ", strHex.decode())+" " 17 | 18 | def hex_str_to_bytes(hexString : str) -> bytes: 19 | dataList = hexString.split(" ") 20 | j = 0 21 | for i in dataList: 22 | if len(i) > 2: 23 | return -1 24 | elif len(i) == 1: 25 | dataList[j] = "0" + i 26 | j += 1 27 | data = "".join(dataList) 28 | try: 29 | data = bytes.fromhex(data) 30 | except Exception: 31 | return -1 32 | return data 33 | 34 | def str_to_bytes(data:str, escape=False, encoding="utf-8"): 35 | if not escape: 36 | return data.encodeing(encoding) 37 | final = b"" 38 | p = 0 39 | escapes = { 40 | "a": (b'\a', 2), 41 | "b": (b'\b', 2), 42 | "f": (b'\f', 2), 43 | "n": (b'\n', 2), 44 | "r": (b'\r', 2), 45 | "t": (b'\t', 2), 46 | "v": (b'\v', 2), 47 | "\\": (b'\\', 2), 48 | "\'": (b"'", 2), 49 | '\"': (b'"', 2), 50 | } 51 | octstr = ["0", "1", "2", "3", "4", "5", "6", "7"] 52 | while 1: 53 | idx = data[p:].find("\\") 54 | if idx < 0: 55 | final += data[p:].encode(encoding, "ignore") 56 | break 57 | final += data[p : p + idx].encode(encoding, "ignore") 58 | p += idx 59 | e = data[p+1] 60 | if e in escapes: 61 | r = escapes[e][0] 62 | p += escapes[e][1] 63 | elif e == "x": # \x01 64 | try: 65 | r = bytes([int(data[p+2 : p+4], base=16)]) 66 | p += 4 67 | except Exception: 68 | msg = "Escape is on, but escape error:" + data[p : p+4] 69 | raise Exception(msg) 70 | elif e in octstr and len(data) > (p+2) and data[p+2] in octstr: # \dd or \ddd e.g. \001 71 | try: 72 | twoOct = False 73 | if len(data) > (p+3) and data[p+3] in octstr: # \ddd 74 | try: 75 | r = bytes([int(data[p+1 : p+4], base=8)]) 76 | p += 4 77 | except Exception: 78 | twoOct = True 79 | else: 80 | twoOct = True 81 | if twoOct: 82 | r = bytes([int(data[p+1 : p+3], base=8)]) 83 | p += 3 84 | except Exception as e: 85 | msg = "Escape is on, but escape error:" + data[p : p+4] 86 | raise Exception(msg) 87 | else: 88 | r = data[p: p+2].encode(encoding, "ignore") 89 | p += 2 90 | final += r 91 | return final 92 | 93 | 94 | def can_draw(ucs4cp): 95 | return 0x2500 <= ucs4cp and ucs4cp <= 0x259F 96 | 97 | 98 | -------------------------------------------------------------------------------- /COMTool/utils_ui.py: -------------------------------------------------------------------------------- 1 | try: 2 | from Combobox import ComboBox 3 | from i18n import _ 4 | import utils, parameters 5 | from parameters import dataPath 6 | except ImportError: 7 | from COMTool import utils, parameters 8 | from COMTool.i18n import _ 9 | from COMTool.Combobox import ComboBox 10 | from COMTool.parameters import dataPath 11 | 12 | from PyQt5.QtWidgets import QWidget 13 | from PyQt5.QtGui import QIcon 14 | import qtawesome as qta # https://github.com/spyder-ide/qtawesome 15 | import os 16 | 17 | _buttonIcons = {} 18 | _skin = "light" 19 | 20 | def get_skins(): 21 | qss_path = os.path.join(dataPath, "assets", "qss") 22 | names = os.listdir(qss_path) 23 | styles = [] 24 | for name in names: 25 | if name.startswith("style-"): 26 | styles.append(name[6:-4]) 27 | return styles 28 | 29 | def setSkin(skin): 30 | global _skin, _buttonIcons 31 | 32 | if skin == _skin: 33 | return 34 | delete = [] 35 | for btn in _buttonIcons: 36 | if type(btn.parent()) == None: 37 | delete.append(btn) 38 | continue 39 | icon, colorVar = _buttonIcons[btn] 40 | color = parameters.styleForCode[skin][colorVar] 41 | btn.setIcon(qta.icon(icon, color=color)) 42 | for btn in delete: 43 | _buttonIcons.pop(btn) 44 | _skin = skin 45 | 46 | def setButtonIcon(button, icon : str, colorVar = "iconColor"): 47 | ''' 48 | @colorVar set in parameters.styleForCode 49 | ''' 50 | global _skin, _buttonIcons 51 | 52 | iconColor = parameters.styleForCode[_skin][colorVar] 53 | _buttonIcons[button] = [icon, colorVar] 54 | button.setIcon(qta.icon(icon, color=iconColor)) 55 | 56 | def clearButtonIcon(button): 57 | global _skin, _buttonIcons 58 | if button in _buttonIcons: 59 | button.setIcon(QIcon()) 60 | _buttonIcons.pop(button) 61 | 62 | def getStyleVar(var): 63 | global _skin, _buttonIcons 64 | 65 | return parameters.styleForCode[_skin][var] 66 | 67 | def updateStyle(parent, widget): 68 | parent.style().unpolish(widget) 69 | parent.style().polish(widget) 70 | parent.update() -------------------------------------------------------------------------------- /COMTool/version.py: -------------------------------------------------------------------------------- 1 | 2 | major = 3 3 | minor = 4 4 | dev = 1 5 | 6 | __version__ = "{}.{}.{}".format(major, minor, dev) 7 | 8 | class Version: 9 | def __init__(self, major=major, minor=minor, dev=dev, name="", desc=""): 10 | self.major = major 11 | self.minor = minor 12 | self.dev = dev 13 | self.name = name 14 | self.desc = desc 15 | 16 | def dump_dict(self): 17 | ret = { 18 | "major": self.major, 19 | "minor": self.minor, 20 | "dev": self.dev, 21 | "name": self.name, 22 | "desc": self.desc 23 | } 24 | return ret 25 | 26 | def load_dict(self, obj): 27 | self.major = obj['major'] 28 | self.minor = obj['minor'] 29 | self.dev= obj['dev'] 30 | self.name = obj['name'] 31 | self.desc = obj['desc'] 32 | 33 | def int(self): 34 | return self.major * 100 + self.minor * 10 + self.dev 35 | 36 | def __str__(self): 37 | return 'v{}.{}.{}, {}: {}'.format(self.major, self.minor, self.dev, self.name, self.desc) 38 | -------------------------------------------------------------------------------- /COMTool/wave.py: -------------------------------------------------------------------------------- 1 | 2 | from PyQt5.QtCore import pyqtSignal 3 | from PyQt5.QtWidgets import (QApplication, QWidget,QToolTip,QPushButton,QMessageBox,QDesktopWidget,QMainWindow, 4 | QVBoxLayout,QHBoxLayout,QGridLayout,QTextEdit,QLabel,QRadioButton,QCheckBox, 5 | QLineEdit,QGroupBox,QSplitter) 6 | 7 | 8 | class Wave(QWidget): 9 | 10 | closed = pyqtSignal() 11 | updatedisTextRawSignal = pyqtSignal(str) 12 | buffer = "" 13 | 14 | def __init__(self,parent = None): 15 | super(Wave,self).__init__(parent) 16 | self.init() 17 | self.initEvent() 18 | self.show() 19 | 20 | def __del__(self): 21 | pass 22 | 23 | def init(self): 24 | self.resize(500,400) 25 | self.mainLayout = QVBoxLayout() 26 | self.setLayout(self.mainLayout) 27 | self.disTextRaw = QLabel("0000") 28 | self.mainLayout.addWidget(self.disTextRaw) 29 | 30 | def initEvent(self): 31 | self.updatedisTextRawSignal.connect(self.updateTextRaw) 32 | 33 | def displayData(self,bytes): 34 | try: 35 | self.buffer += bytes.decode("utf-8") 36 | end = self.buffer.index("\r\n") 37 | frame = self.buffer[0:end] 38 | self.buffer = self.buffer[end+2:] 39 | self.updatedisTextRawSignal.emit(frame) 40 | # print("==========\n",frame,",",self.buffer,"\n==========") 41 | 42 | except Exception: 43 | pass 44 | 45 | def closeEvent(self, event): 46 | self.closed.emit() 47 | event.accept() 48 | 49 | def updateTextRaw(self,frame): 50 | self.disTextRaw.setText(frame) 51 | 52 | -------------------------------------------------------------------------------- /COMTool/win32_utils.py: -------------------------------------------------------------------------------- 1 | 2 | from ctypes import WinDLL, c_bool, c_int, c_longlong, POINTER, byref, Structure 3 | from ctypes.wintypes import DWORD, HWND, LONG, LPCVOID 4 | from enum import Enum 5 | 6 | 7 | class DWMNCRENDERINGPOLICY(Enum): 8 | DWMNCRP_USEWINDOWSTYLE = 0 9 | DWMNCRP_DISABLED = 1 10 | DWMNCRP_ENABLED = 2 11 | DWMNCRP_LAS = 3 12 | 13 | class DWMWINDOWATTRIBUTE(Enum): 14 | DWMWA_NCRENDERING_ENABLED = 1 15 | DWMWA_NCRENDERING_POLICY = 2 16 | DWMWA_TRANSITIONS_FORCEDISABLED = 3 17 | DWMWA_ALLOW_NCPAINT = 4 18 | DWMWA_CAPTION_BUTTON_BOUNDS = 5 19 | DWMWA_NONCLIENT_RTL_LAYOUT = 6 20 | DWMWA_FORCE_ICONIC_REPRESENTATION = 7 21 | DWMWA_FLIP3D_POLICY = 8 22 | DWMWA_EXTENDED_FRAME_BOUNDS = 9 23 | DWMWA_HAS_ICONIC_BITMAP = 10 24 | DWMWA_DISALLOW_PEEK = 11 25 | DWMWA_EXCLUDED_FROM_PEEK = 12 26 | DWMWA_CLOAK = 13 27 | DWMWA_CLOAKED = 14 28 | DWMWA_FREEZE_REPRESENTATION = 25 29 | DWMWA_LAST = 16 30 | 31 | class GWL(Enum): 32 | GWL_EXSTYLE = -20 33 | # Retrieves the extended window styles. 34 | GWL_HINSTANCE = -6 35 | # Retrieves a handle to the application instance. 36 | GWL_HWNDPARENT = -8 37 | # Retrieves a handle to the parent window, if any. 38 | GWL_ID = -12 39 | # Retrieves the identifier of the window. 40 | GWL_STYLE = -16 41 | # Retrieves the window styles. 42 | GWL_USERDATA = -21 43 | # Retrieves the user data associated with the window. This data is intended for use by the application that created the window. Its value is initially zero. 44 | GWL_WNDPROC = -4 45 | 46 | class WINDOW_STYLE(Enum): 47 | ''' 48 | windows window style enumerate 49 | ''' 50 | WS_BORDER = 0x00800000 51 | WS_CAPTION = 0x00C00000 52 | WS_CHILD = 0x40000000 53 | WS_CHILDWINDOW = 0x40000000 54 | WS_CLIPCHILDREN = 0x02000000 55 | WS_CLIPSIBLINGS = 0x04000000 56 | WS_DISABLED = 0x08000000 57 | WS_DLGFRAME = 0x00400000 58 | WS_GROUP = 0x00020000 59 | WS_HSCROLL = 0x00100000 60 | WS_ICONIC = 0x20000000 61 | WS_MAXIMIZE = 0x01000000 62 | WS_MAXIMIZEBOX = 0x00010000 63 | WS_MINIMIZE = 0x20000000 64 | WS_MINIMIZEBOX = 0x00020000 65 | WS_OVERLAPPED = 0x00000000 66 | WS_OVERLAPPEDWINDOW = 0x00CF0000 67 | WS_POPUP = 0x80000000 68 | WS_POPUPWINDOW = 0x80880000 69 | WS_SIZEBOX = 0x00040000 70 | WS_SYSMENU = 0x00080000 71 | WS_TABSTOP = 0x00010000 72 | WS_THICKFRAME = 0x00040000 73 | WS_TILED = 0x00000000 74 | WS_TILEDWINDOW = 0x00CF0000 75 | WS_VISIBLE = 0x10000000 76 | WS_VSCROLL = 0x00200000 77 | 78 | class MARGINS(Structure): 79 | _fields_ = [ 80 | ("cxLeftWidth", c_int), 81 | ("cxRightWidth", c_int), 82 | ("cyTopHeight", c_int), 83 | ("cyBottomHeight", c_int), 84 | ] 85 | 86 | def addShadowEffect(hWnd): 87 | dwmapi = WinDLL("dwmapi") 88 | DwmExtendFrameIntoClientArea = dwmapi.DwmExtendFrameIntoClientArea 89 | DwmSetWindowAttribute = dwmapi.DwmSetWindowAttribute 90 | DwmExtendFrameIntoClientArea.restype = LONG 91 | DwmSetWindowAttribute.restype = LONG 92 | DwmSetWindowAttribute.argtypes = [c_int, DWORD, LPCVOID, DWORD] 93 | DwmExtendFrameIntoClientArea.argtypes = [c_int, POINTER(MARGINS)] 94 | hWnd = int(hWnd) 95 | DwmSetWindowAttribute( 96 | hWnd, 97 | DWMWINDOWATTRIBUTE.DWMWA_NCRENDERING_POLICY.value, 98 | byref(c_int(DWMNCRENDERINGPOLICY.DWMNCRP_ENABLED.value)), 99 | 4, 100 | ) 101 | margins = MARGINS(-1, -1, -1, -1) 102 | DwmExtendFrameIntoClientArea(hWnd, byref(margins)) 103 | # set auto maxium window 104 | # TODO: how to make maiximumble when move window to the edge of screen ? 105 | # winuser = WinDLL("user32") 106 | # userGetWindowLong = winuser.GetWindowLongA 107 | # style = userGetWindowLong(hWnd, GWL.GWL_STYLE.value) 108 | # userSetWindowLongPtr = winuser.SetWindowLongPtrA 109 | # userSetWindowLongPtr(hWnd, GWL.GWL_STYLE.value, 110 | # style | WINDOW_STYLE.WS_THICKFRAME.value | WINDOW_STYLE.WS_MAXIMIZEBOX.value ) 111 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2016-2022 Neucrack CZD666666@gmail.com 5 | 6 | 7 | Everyone is permitted to copy and distribute verbatim copies 8 | of this license document, but changing it is not allowed. 9 | 10 | 11 | This version of the GNU Lesser General Public License incorporates 12 | the terms and conditions of version 3 of the GNU General Public 13 | License, supplemented by the additional permissions listed below. 14 | 15 | 0. Additional Definitions. 16 | 17 | As used herein, "this License" refers to version 3 of the GNU Lesser 18 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 19 | General Public License. 20 | 21 | "The Library" refers to a covered work governed by this License, 22 | other than an Application or a Combined Work as defined below. 23 | 24 | An "Application" is any work that makes use of an interface provided 25 | by the Library, but which is not otherwise based on the Library. 26 | Defining a subclass of a class defined by the Library is deemed a mode 27 | of using an interface provided by the Library. 28 | 29 | A "Combined Work" is a work produced by combining or linking an 30 | Application with the Library. The particular version of the Library 31 | with which the Combined Work was made is also called the "Linked 32 | Version". 33 | 34 | The "Minimal Corresponding Source" for a Combined Work means the 35 | Corresponding Source for the Combined Work, excluding any source code 36 | for portions of the Combined Work that, considered in isolation, are 37 | based on the Application, and not on the Linked Version. 38 | 39 | The "Corresponding Application Code" for a Combined Work means the 40 | object code and/or source code for the Application, including any data 41 | and utility programs needed for reproducing the Combined Work from the 42 | Application, but excluding the System Libraries of the Combined Work. 43 | 44 | 1. Exception to Section 3 of the GNU GPL. 45 | 46 | You may convey a covered work under sections 3 and 4 of this License 47 | without being bound by section 3 of the GNU GPL. 48 | 49 | 2. Conveying Modified Versions. 50 | 51 | If you modify a copy of the Library, and, in your modifications, a 52 | facility refers to a function or data to be supplied by an Application 53 | that uses the facility (other than as an argument passed when the 54 | facility is invoked), then you may convey a copy of the modified 55 | version: 56 | 57 | a) under this License, provided that you make a good faith effort to 58 | ensure that, in the event an Application does not supply the 59 | function or data, the facility still operates, and performs 60 | whatever part of its purpose remains meaningful, or 61 | 62 | b) under the GNU GPL, with none of the additional permissions of 63 | this License applicable to that copy. 64 | 65 | 3. Object Code Incorporating Material from Library Header Files. 66 | 67 | The object code form of an Application may incorporate material from 68 | a header file that is part of the Library. You may convey such object 69 | code under terms of your choice, provided that, if the incorporated 70 | material is not limited to numerical parameters, data structure 71 | layouts and accessors, or small macros, inline functions and templates 72 | (ten or fewer lines in length), you do both of the following: 73 | 74 | a) Give prominent notice with each copy of the object code that the 75 | Library is used in it and that the Library and its use are 76 | covered by this License. 77 | 78 | b) Accompany the object code with a copy of the GNU GPL and this license 79 | document. 80 | 81 | 4. Combined Works. 82 | 83 | You may convey a Combined Work under terms of your choice that, 84 | taken together, effectively do not restrict modification of the 85 | portions of the Library contained in the Combined Work and reverse 86 | engineering for debugging such modifications, if you also do each of 87 | the following: 88 | 89 | a) Give prominent notice with each copy of the Combined Work that 90 | the Library is used in it and that the Library and its use are 91 | covered by this License. 92 | 93 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 94 | document. 95 | 96 | c) For a Combined Work that displays copyright notices during 97 | execution, include the copyright notice for the Library among 98 | these notices, as well as a reference directing the user to the 99 | copies of the GNU GPL and this license document. 100 | 101 | d) Do one of the following: 102 | 103 | 0) Convey the Minimal Corresponding Source under the terms of this 104 | License, and the Corresponding Application Code in a form 105 | suitable for, and under terms that permit, the user to 106 | recombine or relink the Application with a modified version of 107 | the Linked Version to produce a modified Combined Work, in the 108 | manner specified by section 6 of the GNU GPL for conveying 109 | Corresponding Source. 110 | 111 | 1) Use a suitable shared library mechanism for linking with the 112 | Library. A suitable mechanism is one that (a) uses at run time 113 | a copy of the Library already present on the user's computer 114 | system, and (b) will operate properly with a modified version 115 | of the Library that is interface-compatible with the Linked 116 | Version. 117 | 118 | e) Provide Installation Information, but only if you would otherwise 119 | be required to provide such information under section 6 of the 120 | GNU GPL, and only to the extent that such information is 121 | necessary to install and execute a modified version of the 122 | Combined Work produced by recombining or relinking thethe 123 | Application with a modified version of the Linked Version. (If 124 | you use option 4d0, the Installation Information must accompany 125 | the Minimal Corresponding Source and Corresponding Application 126 | Code. If you use option 4d1, you must provide the Installation 127 | Information in the manner specified by section 6 of the GNU GPL 128 | for conveying Corresponding Source.) 129 | 130 | 5. Combined Libraries. 131 | 132 | You may place library facilities that are a work based on the 133 | Library side by side in a single library together with other library 134 | facilities that are not Applications and are not covered by this 135 | License, and convey such a combined library under terms of your 136 | choice, if you do both of the following: 137 | 138 | a) Accompany the combined library with a copy of the same work based 139 | on the Library, uncombined with any other library facilities, 140 | conveyed under the terms of this License. 141 | 142 | b) Give prominent notice with the combined library that part of it 143 | is a work based on the Library, and explaining where to find the 144 | accompanying uncombined form of the same work. 145 | 146 | 6. Revised Versions of the GNU Lesser General Public License. 147 | 148 | The Free Software Foundation may publish revised and/or new versions 149 | of the GNU Lesser General Public License from time to time. Such new 150 | versions will be similar in spirit to the present version, but may 151 | differ in detail to address new problems or concerns. 152 | 153 | Each version is given a distinguishing version number. If the 154 | Library as you received it specifies that a certain numbered version 155 | of the GNU Lesser General Public License "or any later version" 156 | applies to it, you have the option of following the terms and 157 | conditions either of that published version or of any later version 158 | published by the Free Software Foundation. If the Library as you 159 | received it does not specify a version number of the GNU Lesser 160 | General Public License, you may choose any version of the GNU Lesser 161 | General Public License ever published by the Free Software Foundation. 162 | 163 | If the Library as you received it specifies that a proxy can decide 164 | whether future versions of the GNU Lesser General Public License shall 165 | apply, that proxy's public statement of acceptance of any version is 166 | permanent authorization for you to choose that version for the 167 | Library. 168 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.MD 2 | include LICENSE 3 | include setup.cfg 4 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | pyserial = ">=3.5" 8 | requests = ">=2.25.1" 9 | PyQt5 = ">=5.15.6" 10 | Babel = ">=2.9.1" 11 | QtAwesome = "*" 12 | 13 | [dev-packages] 14 | 15 | [requires] 16 | python_version = "3.7" 17 | -------------------------------------------------------------------------------- /README_ZH.MD: -------------------------------------------------------------------------------- 1 | COMTool 2 | ======== 3 | 4 | [English](./README.MD) | 中文 5 | 6 | ![GitHub](https://img.shields.io/github/license/neutree/comtool) [![PyPI](https://img.shields.io/pypi/v/comtool.svg)](https://pypi.python.org/pypi/comtool/) ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/neutree/comtool/pack.yml?branch=master) ![GitHub repo size](https://img.shields.io/github/repo-size/neutree/comtool) ![GitHub Repo stars](https://img.shields.io/github/stars/neutree/comtool?style=social) 7 | 8 | [![GitHub all releases](https://img.shields.io/github/downloads/neutree/comtool/total?label=release%20downloads)](https://github.com/Neutree/COMTool/releases) [![PyPI - Downloads](https://img.shields.io/pypi/dm/comtool?label=pypi%20downloads)](https://pypi.org/project/COMTool/) [![SourceForge](https://img.shields.io/sourceforge/dt/comtool?label=sourceforge%20downloads)](https://sourceforge.net/projects/comtool) 9 | 10 | 11 | 跨平台开源串口调试助手,使用 python 编写 12 | 13 | -------- 14 | 15 | | Windows | Linux | Raspberry Pi | macOS | 16 | | ------- | ----- | ------------ | ----- | 17 | | ![comtool Windows screenshot](./COMTool/assets/screenshot_v2_white.png) | ![comtool linux screenshot](./COMTool/assets/screenshot_V1.4_night.png) | ![comtool Raspberry Pi screenshot](./COMTool/assets/RaspberryPiScreenshot.png) | ![](./COMTool/assets/screenshot_macos.jpg) | 18 | 19 | 20 | | 白色主题 | 黑色主题 | 协议插件 | TCP/UDP | 终端 | 图表绘制 | 21 | | ------ | ------- | ------- | ------- | ------ | -------- | 22 | | ![comtool white theme](./COMTool/assets/screenshot_v2_white.png) | ![comtool dark theme](./COMTool/assets/screenshot_v2.png) | ![comtool protocol plugin](./COMTool/assets/screenshot_protocol_v2.3.png) | ![tcp udp plugin](./COMTool/assets/tcp_udp.png) | ![terminal](./COMTool/assets/screenshot_terminal.png) | ![plugin graph](./COMTool/assets/screenshot_graph.png) | 23 | 24 | 25 | > 截图可能不是最新的版本, 最新的版本只会更好用更好看 26 | 27 | ## 特性 28 | 29 | - [x] 跨平台 (Windows, Linux, macOS, Raspberry Pi)(使用 python 编写,只要你的平台支持 python) 30 | - [x] 可靠,界面不会卡死 31 | - [x] 多语言支持 32 | - [x] 多主题支持,支持自定义主题 33 | - [x] 多种字符编码格式支持,比如 `ASII,GBK(Chinese),UTF-8,UTF-16` 等 34 | - [x] 自动保存设置(退出保存) 35 | - [x] 多种连接方式支持,同时支持编写连接插件 36 | - [x] 串口 支持 37 | - [x] 串口自动检测,支持记住上次使用的串口号 38 | - [x] 串口断线自动重连 39 | - [x] 波特率(随意设置)、校验、停止位、流控等设置支持 40 | - [x] `rts` 和 `dtr` 手动控制 41 | - [x] TCP/UDP 支持,包括客户端和服务端模式支持 42 | - [x] SSH 客户端支持 43 | - [x] 插件支持(插件开发请看[docs/plugins_zh.md](./docs/plugins_zh.md))),内置插件如下: 44 | - [x] 调试插件,基本收发数据调试 45 | - [x] 基础收发功能(字符(ASCII) 和 十六进制(HEX)) 46 | - [x] 收发计数 47 | - [x] 清空接收缓冲区支持 48 | - [x] 自动换行 49 | - [x] 定时发送 50 | - [x] 发送记录保存和再次选中发送 51 | - [x] 自定义常用发送内容,一键发送 52 | - [x] 两种常用换行符CR LF(\r\n) 和 LF(\n) 支持 53 | - [x] 快捷键比如 Ctrl+Enter 发送数据 54 | - [x] 转义字符支持,比如 `\r \n \t \x` 等 55 | - [x] 收发记录,以及添加时间戳和记录到文件功能 56 | - [x] 发送文件 57 | - [x] `unix` 终端风格颜色支持,比如`\x1b[33;42mhello\x1b[0mhello2` 58 | - [x] 协议插件,可自定义收发协议 59 | - [x] 自定义协议编解码支持 60 | - [x] 自定义快捷键发送 61 | - [x] 终端插件, 基本终端功能 62 | - [x] 图表插件 63 | - [x] 支持动态添加图表控件,添加你需要的控件 64 | - [x] 实时显示折线图,支持自定义协议头(支持转移符) 65 | - [x] 自定义按钮来发送数据,支持自定义快捷键 66 | 67 | ## 安装 68 | 69 | 有两种安装方式: 70 | 71 | * [下载二进制文件并运行](#安装可执行程序(无需安装,直接执行)): 适合 Windows 或 macOS,以及简单使用的用户 72 | * [以 python 包方式安装(源码安装)](#以-python-包形式安装): 适合 Linux 用户, 以及需要使用插件的用户,或者熟悉 python 的用户 73 | 74 | ## 安装可执行程序(无需安装,直接执行) 75 | 76 | ### Windows 77 | 78 | * 在 [release](https://github.com/Neutree/COMTool/releases) 或 [sourceforge](https://sourceforge.net/projects/comtool/files/) 下载最新的可执行文件 79 | * 解压`.zip`文件,点击`comtool.exe`运行 80 | > 另外你也可以使用 scoop 安装, 由 [StudentWeis](https://github.com/Neutree/COMTool/issues/50) 维护 81 | > ``` 82 | > scoop bucket add Nightly https://github.com/StudentWeis/Nightly 83 | > scoop install comtool 84 | > ``` 85 | 86 | ### Linux 87 | 88 | 89 | Linux版本太多,我们只为ubuntu编译二进制。 90 | 其他发行版请[从 pypi 或源码安装](#以-python-包形式安装)。 91 | 如果你有什么好的跨平台打包想法,比如 flatpak 或 appimage,你可以贡献一个 PR 或添加一个 issue 来告诉我如何可以做到 92 | 93 | > Arch Linux 及其衍生版本可以通过 AUR 仓库在线安装:(目前由 [taotieren](https://github.com/Neutree/COMTool/issues/44) 维护): 94 | > ```bash 95 | > # 发行版 96 | > yay -S python-comtool 97 | > # 开发版 98 | > yay -S python-comtool-git 99 | > ``` 100 | 101 | * 在 [release](https://github.com/Neutree/COMTool/releases) 页面或 [sourceforge](https://sourceforge.net/projects/comtool/files/) 下载最新版本 102 | * 如果不想使用`sudo`命令自动软件,则需要将当前用户添加到`dialout`组 103 | ```shell 104 | sudo usermod -a -G dialout $USER 105 | grep 'dialout' /etc/group 106 | reboot #must reboot to take effect 107 | ``` 108 | 109 | * 解压`.zip`文件,双击`comtool`运行 110 | 111 | ### 树莓派 112 | 113 | 114 | 打开终端,先用包管理器安装依赖: 115 | 116 | ```shell 117 | sudo apt install git python3-pyqt5 python3-numpy 118 | ``` 119 | 120 | > 先用包管理器安装 pyqt5 numpy 等包更容易安装。 121 | > 后面如果 `pip` 安装过程中某个包遇到了错误,也可以先尝试用系统自带的包管理器安装对应的包。 122 | > 找到包名的技巧就是用`sudo apt-cache search 包名 | grep 包名` 来搜索包名,然后安装 123 | 124 | 然后用 `pip` 安装剩下的包: 125 | ``` 126 | git clone https://github.com/Neutree/COMTool.git --depth=1 127 | cd COMTool 128 | pip3 install . --verbose 129 | # 或者 130 | # python setup.py bdist_wheel 131 | # sudo pip3 install dist/COMTool-*.*.*-py3-none-any.whl --verbose 132 | ``` 133 | 134 | * 如果不想使用`sudo`命令自动软件,则需要将当前用户添加到`dialout`组 135 | ```shell 136 | sudo usermod -a -G dialout $USER 137 | grep 'dialout' /etc/group 138 | reboot #must reboot to take effect 139 | ``` 140 | 141 | 然后通过命令启动 142 | ``` 143 | comtool 144 | ``` 145 | 146 | ### macOS 147 | 148 | * 在 [release](https://github.com/Neutree/COMTool/releases) 页面或 [sourceforge](https://sourceforge.net/projects/comtool/files/) 下载最新版本 149 | * 安装 dmg 包 150 | 151 | 如果你想同时打开多个`comtool`,只需要右键 dock 栏图标,选择`新建窗口`即可。 152 | 153 | 另外也可以打开终端并输入 154 | ``` 155 | open -n /Application/comtool.app 156 | ``` 157 | 或者 158 | ``` 159 | cd /Applicatioin/comtool.app/Contents/MacOS 160 | ./comtool 161 | ``` 162 | 163 | > 因为程序没有开发者签名,所以第一次打开时会警告,需要到`设置 -> 安全和隐私 -> 通用` 看到提示`comtool` 点击 `仍要打开`即可 164 | 165 | 166 | ## Windows Defender 显示 comtool 可执行程序是恶意软件? 167 | 168 | 如果你的软件是从[这里](https://github.com/Neutree/COMTool/releases)下载的,没关系,这是[打包产生的问题](https://github.com/pyinstaller/pyinstaller/issues/4852),所有的源码和打包脚本都在这里,连打包过程都是用`github action`完全自动化,没有人手动打包。 169 | 170 | 如果你仍然担心,只需下载源代码,然后使用 `python`运行或自己打包。 171 | 172 | 当然,如果你找到更好的打包方式,请来 `issue` 告诉我们。 173 | 174 | 175 | ## 以 python 包形式安装 176 | 177 | 对于开发者,或者没有你的平台的预编译软件, 可以使用这种方式安装 178 | 179 | * 先安装 `Python3` 180 | * 如果是 `windows` 或 `macOS`:[下载 python3](https://www.python.org/downloads/) 181 | * 如果 `linux`: 比如`ubuntu`, `sudo apt install python3 python3-pip`, macOS `brew install python3 python3-pip` 182 | 183 | * 确保你有`pip` 184 | ```shell 185 | pip3 --version 186 | # 或者 187 | pip --version 188 | ``` 189 | 190 | 如果没有这个命令,安装 191 | ```shell 192 | python3 -m ensurepip 193 | ``` 194 | 195 | * 然后从 `pypi` 安装: 196 | ```shell 197 | pip3 install comtool 198 | comtool 199 | ``` 200 | 201 | 在国内,为了下载速度更快, 你可以用 `tuna` 镜像更快地下载: 202 | ```shell 203 | pip install -i https://pypi.tuna.tsinghua.edu.cn/simple comtool 204 | ``` 205 | 206 | * 也可以直接从 `github` 安装 207 | ``` 208 | pip3 install git+https://github.com/Neutree/COMTool 209 | ``` 210 | 211 | * 或者你也可以下载源码,然后从源码安装 212 | * 下载源码,[在网页下载](https://github.com/Neutree/COMTool) 或 `git clone https://github.com/Neutree/COMTool.git` 213 | * 安装 214 | ``` 215 | cd COMTool 216 | pip install . 217 | ``` 218 | 或者自己构建 `wheel` 可执行文件 219 | ``` 220 | pip3 install wheel 221 | python setup.py bdist_wheel 222 | pip install dist/COMTool-*.*.*-py3-none-any.whl 223 | comtool 224 | ``` 225 | 226 | * 如果 `pip` 安装过程中遇到了错误,比如最容易出错的`pyqt5`,也可以先尝试用系统自带的包管理器安装对应的包,比如 227 | ``` 228 | sudo apt install python3-pyqt5 python3-numpy 229 | ``` 230 | > 要知道包名的技巧就是用`sudo apt-cache search 包名 | grep 包名` 来搜索包名,然后安装 231 | 232 | 233 | * 如果不想使用`sudo`命令自动软件,则需要将当前用户添加到`dialout`组 234 | ```shell 235 | sudo usermod -a -G dialout $USER 236 | grep 'dialout' /etc/group 237 | reboot #must reboot to take effect 238 | ``` 239 | 240 | ## Linux 手动添加程序图标到开始菜单 241 | 242 | * 复制 [tool/comtool.desktop](tool/comtool.desktop) 文件到`/usr/share/applications`目录(可能需要 `root` 权限) 243 | * 修改`/usr/share/applications/comtool.desktop`,替换里面的图标路径 `Icon=/usr/local/COMTool/assets/logo.ico` 为实际的[图标](COMTool/assets/logo.ico)路径或者你喜欢的图标,保存即可 244 | * 在开始菜单里面就可以找到 comtool 应用了 245 | 246 | ## 打包成可执行文件 247 | 248 | 249 | ```shell 250 | pip3 install pyinstaller 251 | python pack.py 252 | cd dist 253 | ls 254 | ``` 255 | 256 | > 打包前最好创建一个虚拟环境,这样打包出来的可执行文件会小很多 257 | > `pip install virtualenv` 258 | > `virtualenv venv` 259 | > `source venv/bin/activate` # linux 260 | > `venv/Scripts/activate` # windows 261 | > 如果遇到 `因为在此系统上禁止运行脚本`, 可以临时允许当前终端执行脚本 `Set-ExecutionPolicy -Scope Process -ExecutionPolicy RemoteSigned` 262 | > 然后`pip install pyinstaller`,`python pack.py`。 263 | 264 | ## 开发 265 | 266 | 1. 安装 `python(>=3.8)`和`pip3` 267 | 268 | Linux: 269 | ``` 270 | sudo apt install python3 python3-pip 271 | ``` 272 | 273 | Windows: 274 | [下载 python3](https://www.python.org/downloads/) 275 | 276 | 2. 安装`pyserial`和`PyQt5`等包(在[requirements.txt](requirements.txt)中列出) 277 | ``` 278 | cd COMTool 279 | pip3 install -r requirements.txt 280 | ``` 281 | 282 | 在树莓派上,可以通过 `apt` 命令安装 `python3-pyqt5`: 283 | ``` 284 | sudo pip3 install --upgrade pyserial 285 | sudo apt install python3-pyqt5 286 | ``` 287 | 288 | 3. 克隆项目 289 | ``` 290 | git clone https://github.com/Neutree/COMTool.git 291 | ``` 292 | 293 | 4. 撸码、解决错误或添加新的特性 294 | 295 | 推荐使用 `PyCharm` IDE 或 `vscode` 开始 296 | 297 | 运行方法: 298 | 需要先生成翻译所需要的二进制文件(`.mo`) 299 | 300 | ``` 301 | python COMTool/i18n.py finish 302 | ``` 303 | 304 | 然后执行主程序即可 305 | 306 | ``` 307 | python COMTool/Main.py 308 | ``` 309 | 310 | 5. 创建合并请求 311 | 312 | ## 快速编写你自己的插件 313 | 314 | 参考 [docs/plugins.md](./docs/plugins_zh.md) 315 | 316 | ## 添加翻译 317 | 318 | * 先安装环境(`requirments.txt`中的`python pip`包) 319 | ```shell 320 | apt install python3 python3-pip 321 | pip3 install -r requirements.txt 322 | ``` 323 | 324 | * 如果你需要添加新语言,否则跳过此步骤 325 | 326 | 在 [i18n.py](./COMTool/i18n.py) 中添加语言 327 | ``` 328 | locales=["en", "zh_CN", "zh_TW", "ja"] 329 | ``` 330 | 将你的语言附加到此列表中,可以在 [此处](https://www.science.co.il/language/Locale-codes.php) 或 [wikipedia](https://en.wikipedia.org/wiki/Language_localisation),例如`zh_CN`表示中国大陆,对应的语言是简体汉字,`zh_TW`表示中国台湾,语言是繁体字,你也可以只用`zh`来使用中文简体字 331 | 332 | * 生成翻译文件 333 | 334 | ```shell 335 | python i18n.py prepare 336 | ``` 337 | 338 | 此命令将在 `locales` 文件夹中生成 `.po` 文件 339 | 340 | * 手动翻译 341 | 342 | 然后翻译`.po`文件,这是一个叫`gettext`的标准翻译文件格式,可以直接手动改文件,也可以利用网上的工具 343 | 344 | * 生成二进制翻译文件 345 | 346 | 为了让程序读得更快,文本文件`.po`应该转换成二进制文件`.mo`,运行命令: 347 | ```shell 348 | python i18n.py finish 349 | ``` 350 | 然后你可以看到`locales//LC_MESSAGES/messages.mo`文件 351 | 352 | * 测试 353 | 354 | 运行应用程序,你会看到新的翻译 355 | 356 | * 合并请求 357 | 358 | 创建 PR 以将你的更改合并到 [comtool 仓库](https://github.com/Neutree/COMTool) 359 | 360 | ## 自定义主题 361 | 362 | 在源码或者二进制程序目录下的`assets/qss`目录中,从`style-dark.qss`或者`style-light.qss`复制一个文件,文件名为`style-xxx.qss`,这里`xxx`就是主题的名字,这样软件里就能检测到这个主题了。 363 | 然后根据你的喜好修改`qss`文件即可, `qss`和`css`语法类似,不过支持得不是很完全,`css`语法能不能用以实际效果为准哈哈。 364 | 欢迎提交主题代码(PR) 365 | > 另外软件没有为主题刻意优化过,class 和 id 可能都是随手写的,所以不保证未来的代码能完全兼容现在的 qss。 366 | 367 | 368 | ## 问题和意见 369 | 370 | 创建 [issue](https://github.com/Neutree/COMTool/issues/new) 371 | 372 | 373 | ## 开源协议 374 | 375 | [LGPL-3.0 许可证](LICENSE) 376 | 377 | 以库的方式使用了以下开源项目: 378 | 379 | * [PyQt5](https://www.riverbankcomputing.com/software/pyqt/): [GNU GPL v3](https://www.riverbankcomputing.com/software/pyqt/) 380 | * [pyserial](https://github.com/pyserial/pyserial): [BSD-3-Clause](https://github.com/pyserial/pyserial/blob/master/LICENSE.txt) 381 | * [requests](https://github.com/psf/requests): [Apache 2.0](https://github.com/psf/requests/blob/main/LICENSE) 382 | * [Babel](https://github.com/python-babel/babel): [BSD](https://github.com/python-babel/babel/blob/master/LICENSE) 383 | * [qtawesome](https://github.com/spyder-ide/qtawesome): [MIT](https://github.com/spyder-ide/qtawesome/blob/master/LICENSE.txt) 384 | * [pyte](https://github.com/selectel/pyte): [LGPL 3.0](https://github.com/selectel/pyte/blob/master/LICENSE) 385 | * [paramiko](https://github.com/paramiko/paramiko): [LGPL 2.1](https://github.com/paramiko/paramiko/blob/main/LICENSE) 386 | * [pyperclip](https://github.com/asweigart/pyperclip): [BSD-3-Clause](https://github.com/asweigart/pyperclip/blob/master/LICENSE.txt) 387 | 388 | ## 赞赏 389 | 390 | 如果项目帮助到你了,可以请作者喝杯下午茶~ 391 | 392 | ![](./COMTool/assets/donate_wechat.jpg) ![](./COMTool/assets/donate_alipay.jpg) 393 | 394 | -------------------------------------------------------------------------------- /cxsetup.py: -------------------------------------------------------------------------------- 1 | from cx_Freeze import setup,Executable 2 | from codecs import open 3 | from os import path 4 | from COMTool import parameters,helpAbout 5 | import sys 6 | import traceback 7 | import msilib 8 | 9 | here = path.abspath(path.dirname(__file__)) 10 | 11 | # Get the long description from the README file 12 | with open(path.join(here, 'README.md'), encoding='utf-8') as f: 13 | long_description = f.read() 14 | 15 | # Dependencies are automatically detected, but it might need fine tuning. 16 | 17 | #中文需要显式用gbk方式编码 18 | product_name = parameters.appName.encode('gbk') 19 | unproduct_name = (parameters.strUninstallApp).encode('gbk') 20 | product_desc = (parameters.appName+" V"+str(helpAbout.versionMajor)+"."+str(helpAbout.versionMinor)).encode("gbk") 21 | 22 | #uuid叫通用唯一识别码,后面再卸载快捷方式中要用到 23 | product_code = msilib.gen_uuid() 24 | #主程序手动命名 25 | target_name= 'comtool.exe' 26 | 27 | 28 | build_exe_options = { 29 | "include_files":["README.MD","LICENSE"], 30 | #包含外围的ini、jpg文件,以及data目录下所有文件,以上所有的文件路径都是相对于cxsetup.py的路径。 31 | "packages": [], #包含用到的包 32 | "includes": [], 33 | "excludes": ["unittest"], #提出wx里tkinter包 34 | "path": sys.path, #指定上述的寻找路径 35 | # "icon": "assets/logo.ico" #指定ico文件 36 | }; 37 | 38 | #快捷方式表,这里定义了三个快捷方式 39 | shortcut_table = [ 40 | 41 | #1、桌面快捷方式 42 | ("DesktopShortcut", # Shortcut 43 | "DesktopFolder", # Directory_ ,必须在Directory表中 44 | product_name, # Name 45 | "TARGETDIR", # Component_,必须在Component表中 46 | "[TARGETDIR]"+target_name, # Target 47 | None, # Arguments 48 | product_desc, # Description 49 | None, # Hotkey 50 | None, # Icon 51 | None, # IconIndex 52 | None, # ShowCmd 53 | 'TARGETDIR' # WkDir 54 | ), 55 | 56 | #2、开始菜单快捷方式 57 | ("StartupShortcut", # Shortcut 58 | "MenuDir", # Directory_ 59 | product_name, # Name 60 | "TARGETDIR", # Component_ 61 | "[TARGETDIR]"+target_name, # Target 62 | None, # Arguments 63 | product_desc, # Description 64 | None, # Hotkey 65 | None, # Icon 66 | None, # IconIndex 67 | None, # ShowCmd 68 | 'TARGETDIR' # WkDir 69 | ), 70 | 71 | #3、程序卸载快捷方式 72 | ("UniShortcut", # Shortcut 73 | "MenuDir", # Directory_ 74 | unproduct_name, # Name 75 | "TARGETDIR", # Component_ 76 | "[System64Folder]msiexec.exe", # Target 77 | r"/x"+product_code, # Arguments 78 | product_desc, # Description 79 | None, # Hotkey 80 | None, # Icon 81 | None, # IconIndex 82 | None, # ShowCmd 83 | 'TARGETDIR' # WkDir 84 | ) 85 | ] 86 | 87 | 88 | #手动建设的目录,在这里定义。 89 | ''' 90 | 自定义目录说明: 91 | ============== 92 | 1、3个字段分别为 Directory,Directory_Parent,DefaultDir 93 | 2、字段1指目录名,可以随意命名,并在后面直接使用 94 | 3、字段2是指字段1的上级目录,上级目录本身也是需要预先定义,除了某些系统自动定义的目录,譬如桌面快捷方式中使用DesktopFolder 95 | 参考网址 https://msdn.microsoft.com/en-us/library/aa372452(v=vs.85).aspx 96 | ''' 97 | directories = [ 98 | ( "ProgramMenuFolder","TARGETDIR","." ), 99 | ( "MenuDir", "ProgramMenuFolder", product_name) 100 | ] 101 | 102 | # Now create the table dictionary 103 | # 也可把directories放到data里。 104 | ''' 105 | 快捷方式说明: 106 | ============ 107 | 1、windows的msi安装包文件,本身都带一个install database,包含很多表(用一个Orca软件可以看到)。 108 | 2、下面的 Directory、Shortcut都是msi数据库中的表,所以冒号前面的名字是固定的(貌似大小写是区分的)。 109 | 3、data节点其实是扩展很多自定义的东西,譬如前面的directories的配置,其实cxfreeze中代码的内容之一,就是把相关配置数据写入到msi数据库的对应表中 110 | 参考网址:https://msdn.microsoft.com/en-us/library/aa367441(v=vs.85).aspx 111 | ''' 112 | msi_data = {#"Directory":directories , 113 | "Shortcut": shortcut_table 114 | } 115 | 116 | # Change some default MSI options and specify the use of the above defined tables 117 | #注意product_code是我扩展的,现有的官网cx_freeze不支持该参数,为此简单修改了cx_freeze包的代码,后面贴上修改的代码。 118 | bdist_msi_options = { 'data': msi_data, 119 | 'upgrade_code': '{9f21e33d-48f7-cf34-33e9-efcfd80eed10}', 120 | 'add_to_path': False, 121 | 'directories': directories, 122 | 'product_code': product_code, 123 | 'initial_target_dir': r'[ProgramFilesFolder]\%s' % (product_name)} 124 | 125 | 126 | # GUI applications require a different base on Windows (the default is for a 127 | # console application). 128 | base = None; 129 | if sys.platform == "win32": 130 | base = "Win32GUI" 131 | 132 | #简易方式定义快捷方式,放到Executeable()里。 133 | #shortcutName = "AppName", 134 | #shortcutDir = "ProgramMenuFolder" 135 | setup( name = parameters.appName, 136 | author=parameters.author, 137 | version = str(helpAbout.versionMajor)+"."+str(helpAbout.versionMinor)+"."+str(helpAbout.versionDev), 138 | description = product_desc.decode('gbk'), 139 | options = {"build_exe": build_exe_options, 140 | "bdist_msi": bdist_msi_options}, 141 | executables = [Executable("COMTool/Main.py", 142 | targetName= target_name, 143 | base=base, 144 | icon= r"assets/logo.ico") 145 | ]) 146 | 147 | -------------------------------------------------------------------------------- /docs/dev.md: -------------------------------------------------------------------------------- 1 | 开发日记( development notes) 2 | ====== 3 | 4 | 分功能将开发时的思路和方法记录在这里,如果有开发者想参与开发或者基于这个项目修改,可以先看本文档 5 | 6 | 如果你贡献了代码,也欢迎你把你贡献的功能模块的思路记录在这里 7 | 8 | 9 | ## 协议插件中的快捷键发送 10 | 11 | 给 key mode 按钮定义了一个 eventFilter, 拦截所有按钮按下和抬起按键的操作 12 | > 代码为`protocol.py`中的`ModeButtonEventFilter`类 13 | 14 | 15 | 16 | ## 国际化支持 17 | 18 | 程序执行时先从文件加载配置,设置语言,再初始化其它内容,目的是保证在代码任何地方都能通过 i18n 模块正确地获取到翻译 19 | 20 | 21 | ## TCP UDP 连接支持 22 | 23 | 使用模块化(/插件化)开发, 在[conn/conn_tcp_udp.py](../conn/conn_tcp_udp.py) 中定义了这个类,可以直接执行 [conn/test_tcp_udp.py](../conn/test_tcp_udp.py) 测试模块 24 | 25 | ## 终端 26 | 27 | 使用了 `paramiko` 作为 `ssh`连接的后端,解析接收到的数据(`VT100`格式)使用了`pyte`, 根据`pyte`获取到的输出使用一个`pixmap`进行绘制,然后将`pixmap`刷到`QWidget`上实现显示 28 | 29 | 这里需要注意的是关于刷新和绘制,为了同步数据以及防止界面卡死: 30 | 接收和解析以及绘制`pixmap`都在`onReceived`函数中进行,这个函数在接收线程中执行,没在`UI`线程中执行,所以在绘制`pixmap`过程中不要直接调用任何直接操作界面的方法,所有操作都对这个`pixmap`进行操作,等绘制完成后再用`update()`函数通知`UI`线程,在`paintEvent`函数中将`pixmap`画到`widget`上 31 | 32 | 33 | ## ~~不同插件继承数据~~(已废弃此方案) 34 | 35 | 每个插件都可以使用连接,即用户在界面点击连接后,当前的插件就可以通过`send`和`onReceived`函数发送和接收数据了 36 | 37 | 另外也支持继承关系,比如`protocol`插件继承自`bdg`插件(即`protocol`插件的`connParent`为`dbg`),当我们使用`protocol`插件收发数据时,接收到数据会先发给`dbg`,然后`dbg`在`onReceived`函数中转发给`connChilds`即`protocol`插件,`protocol`发送数据时会先转发给`dbg`插件的`sendData`。 38 | 39 | 需要注意的是这个功能是一个试验性的功能,目前只支持两级,比如这里的`dbg`插件的`connParent`是`main`, `protocol`的`connParent`是`dbg`,**不可以**再有继承于`protocol`的插件了,因为是试验性功能,如果要支持更多层级继承需要修改代码使用递归实现 40 | 41 | 另外也需要注意,这里在`protocol`收发消息会经过`dbg`,但是在`dbg`收发不会转发给子插件也就是`protocol` 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /docs/plugins.md: -------------------------------------------------------------------------------- 1 | COMTool plugins doc 2 | ======= 3 | 4 | 5 | Before you start developing plugins, you need to know how to run the source code. See the [README.md](../README.MD) for the development introduction. 6 | 7 | 8 | ## Add plugin and integrated to COMTool as default plugin 9 | 10 | * Go to `COMTool/plugins` dir 11 | * Create a plugin file like `myplugin.py` 12 | * Write a class in `myplugin.py`, and inherit from `.base.Plugin_Base`, and name must be `Plugin` e.g. 13 | ```python 14 | from .base import Plugin_Base 15 | 16 | class Plugin(Plugin_Base): 17 | pass 18 | ``` 19 | * Edit your plugin class, implement variables and functions inherit from `Plugin_Base`, and you can use translate in your code by function `_`, e.g. `_("hello")`. 20 | Example plugin see [myplugin.py](../COMTool/plugins/myplugin.py) 21 | 22 | * Add plugin in [COMTool/plugins/__init__.py](../COMTool/plugins/__init__.py) to enable plugin 23 | ```python 24 | from . import myplugin 25 | plugins = [..., myplugin.Plugin] 26 | ``` 27 | 28 | * OK, all works done! Just run program to see our plugin 29 | 30 | ``` 31 | python COMTool/Main.py 32 | ``` 33 | 34 | ## Write an external plugin, can load by COMTool at any time 35 | 36 | * Create a project dir and a `myplugin.py` file (any name is ok) 37 | * Wite a plugin the as we said in [section one](#Add-plugin-and-integrated-to-COMTool-as-default-plugin), emample file see [myplugin.py](../COMTool/plugins/myplugin.py), and the plugin class' name must be `Plugin` 38 | * Bootup COMTool, click plugins list and select add new plugin 39 | 40 | And there's some points should be pay attention: 41 | * When load plugin, the plugin's path will be insert to the start of `sys.path`, so you should name your files carefully, it's recommended to name all files to be likely `comtool_plugin_xxx.py` 42 | * Comtool binary executable packed with `pyinstaller`, only packed some package comtool used, so if your plugin contain some special package, there's some resolution: 43 | * Just keep your plugin clean, no special package import 44 | * Or copy them to dir root dir of binary executable file too 45 | * Or use comtool program install as python package(installed by pip) 46 | 47 | 48 | ## Write an external plugin as a python package, can auto load by COMTool 49 | 50 | Create a python package, example: [COMTool/plugins/myplugin2](../COMTool/plugins/myplugin2) 51 | 52 | * Package name must be `comtool_plugin_xxx` 53 | * Build package with `python setup.py sdist bdist_wheel` 54 | * You can upload your package to `pypi.org`, by `twine upload ./dist/*` 55 | * Then user can install your package by `pip install comtool-plugin-xxx`, then comtool will automatically load plugin 56 | 57 | ## I18n of plugin 58 | 59 | If you want to let your plugin support i18n(internationalization): 60 | * Just like [COMTool/plugins/myplugin2](../COMTool/plugins/myplugin2) did, create a `plugin_18n.py` to define `_` function, and use it in your plugin like `_("Hello")` 61 | * Use `comtool-i18n -p prepare` first, e.g. `comtool-i18n -p COMTool/plugins/myplugin2/comtool_plugin_myplugin2 prepare` to find strings need to translate in your code automatically, this will generate `messages.pot` and `po` files in `locales` directory 62 | * Translate `po` files manually 63 | * Run `comtool-i18n -p finish` to generate `mo` files in `locales` directory 64 | * `setup.py` should include translate binary files (`*.mo`) to package data, so user can use these translation 65 | 66 | 67 | ## Add connection plugin 68 | 69 | Connection plugin just the same as the plugins, have an base class `COMM` in [COMTool/conn/__init__.py](../COMTool/conn/__init__.py), just: 70 | * Create a new file in `COMTool/conn` directory like `conn_serial.py`, `conn_ssh.py`, `conn_tcp_udp.py`, and implement the methods of `COMM` class'. 71 | * Add your connection class to [COMTool/conn/__init__.py](../COMTool/conn/__init__.py) 72 | 73 | * That's all, run COMTool 74 | 75 | ``` 76 | python COMTool/Main.py 77 | ``` 78 | 79 | 80 | -------------------------------------------------------------------------------- /docs/plugins_zh.md: -------------------------------------------------------------------------------- 1 | COMTool 插件文档 2 | ======= 3 | 4 | 在学会开发插件前, 需要了解如何将源码跑起来,看项目[README_ZH.md](../README_ZH.MD) 开发介绍 5 | 6 | 7 | ## 添加插件并集成到 COMTool 作为默认插件 8 | 9 | * 去 `COMTool/plugins` 目录 10 | * 创建一个插件文件,如 `myplugin.py` 11 | * 写一个类,继承自 `.base.Plugin_Base`,并命名为 `Plugin`,例如 12 | ```python 13 | from .base import Plugin_Base 14 | 15 | class Plugin(Plugin_Base): 16 | pass 17 | ``` 18 | * 编辑您的插件类,实现继承自 `Plugin_Base` 的变量和函数,并使用函数 `_` 在您的代码中使用翻译,例如 `_("hello")`。 19 | 插件参见 [myplugin.py](../COMTool/plugins/myplugin.py) 20 | 21 | * 在 [COMTool/plugins/__init__.py](../COMTool/plugins/__init__.py) 添加插件,以启用插件 22 | ```python 23 | from . import myplugin 24 | plugins = [..., myplugin.Plugin] 25 | ``` 26 | 27 | * OK,所有工作都完成!只需运行程序就可以看到我们的插件 28 | 29 | ``` 30 | python COMTool/Main.py 31 | ``` 32 | 33 | 34 | ## 编写一个外部插件,可以通过 COMTool 在任何时候加载 35 | 36 | * 创建一个项目目录和一个 `myplugin.py` 文件(任何名字都可以) 37 | * 写一个插件像我们在 [section one](#Add-plugin-and-integrated-to-COMTool-as-default-plugin) 说的那样,示例文件参见 [myplugin.py](../COMTool/plugins/myplugin.py),插件类的名字必须是 `Plugin` 38 | * 启动 COMTool,点击插件列表并选择添加新插件 39 | 40 | 有几个点需要注意 41 | * 加载插件时,插件的路径将被插入 `sys.path` 的开头,所以文件名的命名需要格外小心,建议所有文件命名为可能的 `comtool_plugin_xxx.py` 42 | * Comtool 可执行文件使用了 `pyinstaller` 打包, 只会将使用到了包打包进去,所以如果你的插件包含一些特殊的包,解决方法如下: 43 | * 保持插件的纯净,不要包含特殊包 44 | * 或者将它们拷贝到可执行文件的根目录 45 | * 或者使用 COMTool 程序安装为 python 包(通过 pip 安装) 46 | 47 | 48 | ## 编写一个 python 包作为插件,可以被 COMTool 自动加载 49 | 50 | 创建一个 python 包, 比如: [COMTOOL/plugins/myplugin2](../COMTool/plugins/myplugin2) 51 | 52 | * 包名必须是 `comtool_plugin_xxx` 53 | * 构建包使用 `python setup.py sdist bdist_wheel` 54 | * 你可以将包上传到 `pypi.org`,使用 `twine upload ./dist/*` 55 | * 然后用户使用 `pip install comtool-plugin-xxx` 安装包,启动软件时 COMTool 将自动加载插件 56 | 57 | 58 | ## 插件 i18n (国际化/翻译) 59 | 60 | 如果你想要让你的插件支持国际化(i18n): 61 | * 正如 [COMTool/plugins/myplugin2](../COMTool/plugins/myplugin2) 那样,创建一个 `plugin_18n.py` 来定义 `_` 函数,并在你的插件中使用它,如 `_("Hello")` 62 | * 用 `comtool-i18n -p prepare` 命令首先准备翻译,比如 `comtool-i18n -p COMTool/plugins/myplugin2/comtool_plugin_myplugin2 prepare` 将自动在你的代码中找到需要翻译的字符串,并生成 `messages.pot` 和 `po` 文件 63 | * 手动翻译 `po` 文件 64 | * 执行 `comtool-i18n -p finish` 命令生成 `mo` 文件 65 | * `setup.py` 应该包含翻译二进制文件(`*.mo`)到`package data`,这样用户才可以使用这些翻译 66 | 67 | 68 | ## 添加连接插件 69 | 70 | 新的连接插件, 和普通插件类似, 有一个基类 `COMM`,在 [COMTool/conn/__init__.py](../COMTool/conn/__init__.py), 只需要: 71 | * 在`COMTool/conn` 创建一个新文件, 如 `conn_serial.py`, `conn_ssh.py`, `conn_tcp_udp.py`, 并实现 `COMM` 类 72 | * 将你的连接类添加到 [COMTool/conn/__init__.py](../COMTool/conn/__init__.py) 73 | * 到此就可以使用了,执行 COMTool 就可以看到新的连接了 74 | ``` 75 | python COMTool/Main.py 76 | ``` 77 | 78 | -------------------------------------------------------------------------------- /pack.py: -------------------------------------------------------------------------------- 1 | import os, sys, shutil 2 | sys.path.insert(1,"./COMTool/") 3 | from COMTool import version, i18n 4 | import zipfile 5 | import shutil 6 | import re 7 | 8 | 9 | if sys.version_info < (3, 7): 10 | print("only support python >= 3.7, but now is {}".format(sys.version_info)) 11 | sys.exit(1) 12 | 13 | # when execute packed executable program(./dist/comtool) warning missing package, add here to resolve 14 | hidden_imports = [ 15 | # "pyqtgraph.graphicsItems.PlotItem.plotConfigTemplate_pyqt5", # fixed in latest pyinstaller-hooks-contrib 16 | # "pyqtgraph.graphicsItems.ViewBox.axisCtrlTemplate_pyqt5", 17 | # "pyqtgraph.imageview.ImageViewTemplate_pyqt5", 18 | "babel.numbers" 19 | ] 20 | 21 | 22 | linux_out = "comtool_ubuntu_v{}.tar.xz".format(version.__version__) 23 | macos_out = "comtool_macos_v{}.dmg".format(version.__version__) 24 | windows_out = "comtool_windows_v{}.7z".format(version.__version__) 25 | 26 | def zip(out, path): 27 | out = os.path.abspath(out) 28 | cwd = os.getcwd() 29 | os.chdir(os.path.dirname(path)) 30 | with zipfile.ZipFile(out,'w', zipfile.ZIP_DEFLATED) as target: 31 | for i in os.walk(os.path.basename(path)): 32 | for n in i[2]: 33 | target.write(os.path.join(i[0],n)) 34 | os.chdir(cwd) 35 | 36 | def zip_7z(out, path): 37 | out = os.path.abspath(out) 38 | cwd = os.getcwd() 39 | os.chdir(os.path.dirname(path)) 40 | ret = os.system(f"7z a -t7z -mx=9 {out} {os.path.basename(path)}") 41 | if ret != 0: 42 | raise Exception("7z compress failed") 43 | os.chdir(cwd) 44 | 45 | def upadte_spec_bundle(spec_path, items = {}, plist_items={}): 46 | with open(spec_path) as f: 47 | spec = f.read() 48 | def BUNDLE(*args, **kw_args): 49 | kw_args.update(items) 50 | if "info_plist" in kw_args: 51 | kw_args["info_plist"].update(plist_items) 52 | else: 53 | kw_args["info_plist"] = plist_items 54 | bundle_str_args = "" 55 | for arg in args: 56 | if type(arg) == str and arg != "exe" and arg != "coll": 57 | bundle_str_args += f'"{arg}", \n' 58 | else: 59 | bundle_str_args += f'{arg}, \n' 60 | for k, v in kw_args.items(): 61 | if type(v) == str: 62 | bundle_str_args += f'{k}="{v}",\n' 63 | else: 64 | bundle_str_args += f'{k}={v},\n' 65 | return bundle_str_args 66 | 67 | match = re.findall(r'BUNDLE\((.*?)\)', spec, flags=re.MULTILINE|re.DOTALL) 68 | if len(match) <= 0: 69 | raise Exception("no BUNDLE found in spec, please check code") 70 | code =f'app = BUNDLE({match[0]})' 71 | vars = { 72 | "BUNDLE": BUNDLE, 73 | "exe": "exe", 74 | "coll": "coll" 75 | } 76 | exec(code, vars) 77 | final_str = vars["app"] 78 | 79 | def re_replace(c): 80 | print(c[0]) 81 | return f'BUNDLE({final_str})' 82 | 83 | final_str = re.sub(r'BUNDLE\((.*)\)', re_replace, spec, flags=re.I|re.MULTILINE|re.DOTALL) 84 | print(final_str) 85 | 86 | with open(spec_path, "w") as f: 87 | f.write(spec) 88 | 89 | def pack(): 90 | # update translate 91 | i18n.main("finish") 92 | 93 | if os.path.exists("COMTool/__pycache__"): 94 | shutil.rmtree("COMTool/__pycache__") 95 | 96 | hidden_imports_str = "" 97 | for item in hidden_imports: 98 | hidden_imports_str += f'--hidden-import {item} ' 99 | if sys.platform.startswith("win32"): 100 | cmd = f'pyinstaller {hidden_imports_str} -p "COMTool" --add-data="COMTool/assets;assets" --add-data="COMTool/locales;locales" --add-data="COMTool/protocols;protocols" --add-data="README.MD;./" --add-data="README_ZH.MD;./" -i="COMTool/assets/logo.ico" -w COMTool/Main.py -n comtool' 101 | elif sys.platform.startswith("darwin"): 102 | # macos not case insensitive, so can not contain comtool file and COMTool dir, so we copy to binary root dir 103 | cmd = f'pyi-makespec {hidden_imports_str} -p "COMTool" --add-data="COMTool/assets:assets" --add-data="COMTool/locales:locales" --add-data="COMTool/protocols:protocols" --add-data="README_ZH.MD:./" --add-data="README.MD:./" -i="COMTool/assets/logo.icns" -w COMTool/Main.py -n comtool' 104 | ret = os.system(cmd) 105 | if ret != 0: 106 | raise Exception("pack failed") 107 | print("-- update bundle for macos build") 108 | upadte_spec_bundle("comtool.spec", 109 | items = { 110 | "version": version.__version__ 111 | }, 112 | plist_items = { 113 | "LSMultipleInstancesProhibited": False, 114 | "CFBundleShortVersionString": version.__version__ 115 | }) # enable multi instance support 116 | print("-- update bundle for macos build complete") 117 | cmd = 'pyinstaller comtool.spec' 118 | else: 119 | cmd = f'pyinstaller {hidden_imports_str} -p "COMTool" --add-data="COMTool/assets:assets" --add-data="COMTool/locales:locales" --add-data="COMTool/protocols:protocols" --add-data="README.MD:./" --add-data="README_ZH.MD:./" -i="COMTool/assets/logo.ico" -w COMTool/Main.py -n comtool' 120 | 121 | print("-- execute:", cmd) 122 | ret = os.system(cmd) 123 | if ret != 0: 124 | raise Exception("pack failed") 125 | 126 | if sys.platform.startswith("darwin"): 127 | if os.path.exists("./dist/comtool 0.0.0.dmg"): 128 | os.remove("./dist/comtool 0.0.0.dmg") 129 | ret = os.system('npm install --global create-dmg') 130 | if ret != 0: 131 | raise Exception("pack failed") 132 | ret = os.system('create-dmg ./dist/comtool.app ./dist') 133 | # not check ret, for create-dmg no certifacate will cause fail too, if generate fail 134 | # the next copy command will fail 135 | print("files in dist dir:", os.listdir("dist")) 136 | shutil.copyfile("./dist/comtool 0.0.0.dmg", macos_out) 137 | elif sys.platform.startswith("win32"): 138 | # zip(windows_out, "dist/comtool") 139 | zip_7z(windows_out, "dist/comtool") 140 | else: 141 | cmd = "cd dist && tar -Jcf {} comtool/ && mv {} ../ && cd ..".format(linux_out, linux_out) 142 | ret = os.system(cmd) 143 | if ret != 0: 144 | raise Exception("pack failed") 145 | 146 | if __name__ == "__main__": 147 | if len(sys.argv) > 1: 148 | os_name = sys.argv[1] 149 | if os_name.startswith("ubuntu"): 150 | if os_name != "ubuntu-latest": 151 | linux_out_new = linux_out.replace("ubuntu", os_name.replace("-", "_")) 152 | os.rename(linux_out, linux_out_new) 153 | linux_out = linux_out_new 154 | print(linux_out) 155 | elif os_name.startswith("windows"): 156 | if os_name != "windows-latest": 157 | windows_out_new = windows_out.replace("windows", os_name.replace("-", "_")) 158 | os.rename(windows_out, windows_out_new) 159 | windows_out = windows_out_new 160 | print(windows_out) 161 | elif os_name.startswith("macos"): 162 | macos_version = os_name.split("-")[1] 163 | if macos_version.isdigit() and int(macos_version) < 14: 164 | macos_out_new = macos_out.replace("macos", "macos_x64") 165 | else: # github actions macos-latest is using M1 chip 166 | macos_out_new = macos_out.replace("macos", "macos_arm64") 167 | os.rename(macos_out, macos_out_new) 168 | macos_out = macos_out_new 169 | print(macos_out) 170 | else: 171 | sys.exit(1) 172 | else: 173 | pack() 174 | 175 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PyQt5>=5.15.6 2 | pyserial>=3.5 3 | requests>=2.25.1 4 | Babel>=2.9.1 5 | qtawesome>=1.1.1,<=1.3.1 6 | paramiko 7 | pyte 8 | pyperclip 9 | coloredlogs 10 | pyqtgraph 11 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | # This flag says that the code is written to work on both Python 2 and Python 3 | # 3. If at all possible, it is good practice to do this. If you cannot, you 4 | # will need to generate wheels for each Python version that you support. 5 | # universal=0 6 | [metadata] 7 | license_file=LICENSE 8 | 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup,find_packages 2 | from codecs import open 3 | from os import path 4 | import os 5 | from COMTool import version 6 | import platform 7 | 8 | here = path.abspath(path.dirname(__file__)) 9 | 10 | # Get the long description from the README file 11 | with open(path.join(here, 'README.MD'), encoding='utf-8') as f: 12 | long_description = f.read() 13 | 14 | systemPlatform = platform.platform() 15 | if platform.python_version_tuple()[0] != "3": 16 | raise Exception("python3 is required, but python{} is used, use `pip3 install` or `python3 -m pip install` command instead".format(platform.python_version())) 17 | 18 | if "Linux" in systemPlatform and "arm" in systemPlatform : 19 | print("\n\nplatform is arm linux: It's recommended to install some packages by `sudo apt install`, for example: `sudo apt install python3-pyqt5 python3-numpy") 20 | print("And if some package download or install failed, you can download the wheel file and install by `pip install ****.whl` mannually first\n\n") 21 | ret = os.system("sudo pip3 install --upgrade pyserial requests Babel qtawesome paramiko pyte pyperclip coloredlogs pyqtgraph") 22 | if ret != 0: 23 | raise Exception("install packages failed") 24 | installRequires = [] 25 | else: 26 | installRequires = ['pyqt5>=5', 27 | 'pyserial>=3.4', 28 | 'requests', 29 | 'Babel', 30 | 'qtawesome>=1.1.1,<=1.3.1', 31 | 'paramiko', 32 | 'pyte', 33 | 'pyperclip', 34 | 'coloredlogs', 35 | 'pyqtgraph' 36 | ] 37 | 38 | setup( 39 | name='COMTool', 40 | 41 | # Versions should comply with PEP440. For a discussion on single-sourcing 42 | # the version across setup.py and the project code, see 43 | # https://packaging.python.org/en/latest/single_source_version.html 44 | version=version.__version__, 45 | 46 | # Author details 47 | author='Neucrack', 48 | author_email='czd666666@gmail.com', 49 | 50 | description='Cross platform serial debug assistant with GUI', 51 | long_description=long_description, 52 | long_description_content_type="text/markdown", 53 | 54 | # The project's main homepage. 55 | url='https://github.com/Neutree/COMTool', 56 | 57 | 58 | 59 | # Choose your license 60 | license='LGPL-3.0', 61 | 62 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 63 | classifiers=[ 64 | # How mature is this project? Common values are 65 | # 3 - Alpha 66 | # 4 - Beta 67 | # 5 - Production/Stable 68 | 'Development Status :: 5 - Production/Stable', 69 | 70 | # Indicate who your project is intended for 71 | 'Intended Audience :: Developers', 72 | 'Topic :: Software Development :: Embedded Systems', 73 | 'Topic :: Software Development :: Debuggers', 74 | 75 | # Pick your license as you wish (should match "license" above) 76 | 'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)', 77 | 78 | # Specify the Python versions you support here. In particular, ensure 79 | # that you indicate whether you support Python 2, Python 3 or both. 80 | 'Programming Language :: Python :: 3', 81 | 'Programming Language :: Python :: 3.8', 82 | 'Programming Language :: Python :: 3.9', 83 | 'Programming Language :: Python :: 3.10', 84 | ], 85 | 86 | # What does your project relate to? 87 | keywords='Serial Debug Tool Assistant ', 88 | 89 | # You can just specify the packages manually here if your project is 90 | # simple. Or you can use find_packages(). 91 | packages=find_packages(), 92 | 93 | # Alternatively, if you want to distribute just a my_module.py, uncomment 94 | # this: 95 | # py_modules=["my_module"], 96 | 97 | # List run-time dependencies here. These will be installed by pip when 98 | # your project is installed. For an analysis of "install_requires" vs pip's 99 | # requirements files see: 100 | # https://packaging.python.org/en/latest/requirements.html 101 | install_requires=installRequires, 102 | 103 | # List additional groups of dependencies here (e.g. development 104 | # dependencies). You can install these using the following syntax, 105 | # for example: 106 | # $ pip install -e .[dev,test] 107 | extras_require={ 108 | # 'dev': ['check-manifest'], 109 | # 'test': ['coverage'], 110 | }, 111 | 112 | # If there are data files included in your packages that need to be 113 | # installed, specify them here. If using Python 2.6 or less, then these 114 | # have to be included in MANIFEST.in as well. 115 | package_data={ 116 | 'COMTool': ['assets/*', "assets/qss/*", "locales/*/*/*.?o", "protocols/*"], 117 | }, 118 | 119 | # Although 'package_data' is the preferred approach, in some case you may 120 | # need to place data files outside of your packages. See: 121 | # http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files # noqa 122 | # In this case, 'data_file' will be installed into '/my_data' 123 | data_files=[ 124 | ("",["LICENSE","README.MD"]) 125 | ], 126 | 127 | # To provide executable scripts, use entry points in preference to the 128 | # "scripts" keyword. Entry points provide cross-platform support and allow 129 | # pip to create the appropriate form of executable for the target platform. 130 | entry_points={ 131 | 'console_scripts': [ 132 | 'comtool-i18n=COMTool.i18n:cli_main', 133 | ], 134 | 'gui_scripts': [ 135 | 'comtool=COMTool.Main:main', 136 | ], 137 | }, 138 | ) 139 | 140 | -------------------------------------------------------------------------------- /tool/comtool.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Version=2.3 3 | Type=Application 4 | Name=comtool 5 | Comment=comtool serial communication debug tool 6 | Exec=comtool %U 7 | Icon=/usr/local/COMTool/assets/logo.ico 8 | Categories=Development;Utility; 9 | Terminal=false 10 | StartupNotify=true 11 | Actions=new-window; 12 | 13 | [Desktop Action new-window] 14 | Name=New Window 15 | Name[zh_CN]=新建窗口 16 | Name[zh_TW]=開新視窗 17 | Name[ja]=新規ウインドウ 18 | Exec=comtool %U 19 | 20 | -------------------------------------------------------------------------------- /tool/send_curve_demo.py: -------------------------------------------------------------------------------- 1 | ''' 2 | execute `pip install comtool --upgrade` first 3 | Then run a TCP server on COMTool 4 | Finally run this script to connect the server and send data 5 | ''' 6 | from COMTool.plugins import graph_protocol 7 | import math 8 | import socket 9 | import time 10 | 11 | class Conn: 12 | def __init__(self, addr, port): 13 | self.addr = addr 14 | self.port = port 15 | self.sock = None 16 | # connect tcp server 17 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 18 | self.sock.connect((self.addr, self.port)) 19 | 20 | def send(self, data): 21 | self.sock.send(data) 22 | 23 | if __name__ == "__main__": 24 | conn = Conn("127.0.0.1", 2345) 25 | 26 | count = 0 27 | while 1: 28 | # x belong to [0, 2pi] 29 | x = count * 2 * math.pi / 100 30 | y = math.sin(x) 31 | frame1 = graph_protocol.plot_pack("data1", x, y, header= b'\xAA\xCC\xEE\xBB') 32 | y = math.pow(math.cos(x), 2) 33 | frame2 = graph_protocol.plot_pack("data2", x, y, header= b'\xAA\xCC\xEE\xBB') 34 | conn.send(frame1) 35 | conn.send(frame2) 36 | count += 1 37 | time.sleep(0.1) 38 | -------------------------------------------------------------------------------- /tool/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # set -e 4 | # set -x 5 | 6 | python setup.py sdist bdist_wheel 7 | python pack.py 8 | --------------------------------------------------------------------------------