├── .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 |  [](https://pypi.python.org/pypi/comtool/)   
7 |
8 | [](https://github.com/Neutree/COMTool/releases) [](https://pypi.org/project/COMTool/) [](https://sourceforge.net/projects/comtool)
9 |
10 |
11 | 跨平台开源串口调试助手,使用 python 编写
12 |
13 | --------
14 |
15 | | Windows | Linux | Raspberry Pi | macOS |
16 | | ------- | ----- | ------------ | ----- |
17 | |  |  |  |  |
18 |
19 |
20 | | 白色主题 | 黑色主题 | 协议插件 | TCP/UDP | 终端 | 图表绘制 |
21 | | ------ | ------- | ------- | ------- | ------ | -------- |
22 | |  |  |  |  |  |  |
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 |  
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 |
--------------------------------------------------------------------------------