├── .github └── workflows │ └── python-app.yml ├── .gitignore ├── LICENSE ├── README.md ├── main.py ├── main.spec ├── make.bat ├── requirements.txt ├── snapshots └── Snipaste_UI.png └── upnptool ├── __init__.py ├── logic.py ├── note.txt ├── smalltool1.py ├── smalltool1.tcl └── smalltool1_support.py /.github/workflows/python-app.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: Python application 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | build: 17 | runs-on: windows-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Set up Python 3.12 22 | uses: actions/setup-python@v5 23 | with: 24 | python-version: "3.12" 25 | - name: Install dependencies 26 | run: | 27 | python -m pip install --upgrade pip 28 | pip install -r requirements.txt 29 | - name: Build Dists 30 | run: | 31 | pyinstaller --noconfirm main.spec 32 | 33 | - name: Upload a Build Artifact 34 | uses: actions/upload-artifact@v4 35 | 36 | with: 37 | # A file, directory or wildcard pattern that describes what to upload 38 | path: ./dist/upnptool.zip 39 | retention-days: 3 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | 93 | # Rope project settings 94 | .ropeproject 95 | 96 | # mkdocs documentation 97 | /site 98 | 99 | .idea 100 | *.0 101 | *.csv 102 | *.json 103 | 104 | *.tcl# 105 | backups/ 106 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | 3 | 4 | # UpnpTool 5 | 6 | Upnp Manage Tool for local home network 7 | 8 | 9 | ## Features 10 | 11 | 1. Query UPnP Root Server. 12 | 2. List/Add/Delete NAT(port map) control over UPnP . 13 | 14 | 15 | ## Snapshot 16 | 17 | ![ui](./snapshots/Snipaste_UI.png) 18 | 19 | Writen by Python 3.10 20 | 21 | 22 | ## Development 23 | 24 | 1. UI code Generator: use the software [PAGE - Python Automatic GUI Generator ](https://page.sourceforge.net/) to create and edit WinForm style window, and generate `.py, _support.py`. 25 | 2. Binary Pack: use pyinstaller to build distributed executable files. we could just click `make.bat` to build project. 26 | 27 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # @FileName :main.py 4 | # @Time :2023/8/22 22:00 5 | 6 | 7 | import logging 8 | from upnptool.smalltool1_support import main 9 | 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | if __name__ == '__main__': 14 | main() 15 | -------------------------------------------------------------------------------- /main.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | import shutil 3 | import zipfile 4 | 5 | block_cipher = None 6 | 7 | 8 | a = Analysis( 9 | ['main.py'], 10 | pathex=[], 11 | binaries=[], 12 | datas=[], 13 | hiddenimports=[], 14 | hookspath=[], 15 | hooksconfig={}, 16 | runtime_hooks=[], 17 | excludes=[], 18 | win_no_prefer_redirects=False, 19 | win_private_assemblies=False, 20 | cipher=block_cipher, 21 | noarchive=False, 22 | ) 23 | pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) 24 | 25 | exe = EXE( 26 | pyz, 27 | a.scripts, 28 | [], 29 | exclude_binaries=True, 30 | name='upnptool', 31 | debug=False, 32 | bootloader_ignore_signals=False, 33 | strip=False, 34 | upx=False, 35 | console=False, 36 | disable_windowed_traceback=False, 37 | argv_emulation=False, 38 | target_arch=None, 39 | codesign_identity=None, 40 | entitlements_file=None, 41 | ) 42 | coll = COLLECT( 43 | exe, 44 | a.binaries, 45 | a.zipfiles, 46 | a.datas, 47 | strip=False, 48 | upx=False, 49 | upx_exclude=[], 50 | name='main', 51 | ) 52 | 53 | 54 | shutil.make_archive('./dist/upnptool', 'zip', './dist/main') 55 | 56 | -------------------------------------------------------------------------------- /make.bat: -------------------------------------------------------------------------------- 1 | 2 | @echo off 3 | 4 | pyinstaller --noconfirm .\main.spec 5 | 6 | 7 | rem build higher compressed archive file [*.7z] for upload. 8 | set p7z= "%programfiles%/7-zip/7z.exe" 9 | if exist %p7z% ( 10 | %p7z% a -mx9 -y ./dist/upnptool.7z ./dist/main 11 | ) 12 | 13 | echo %date% %time% 14 | echo build and dist finished. 15 | 16 | @echo on 17 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | upnpclient==1.0.3 2 | pyinstaller==5.13.0 3 | -------------------------------------------------------------------------------- /snapshots/Snipaste_UI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/6769/UpnpTool/2c1bf61d45b349c2b8730f7a9ce245756fe1b12c/snapshots/Snipaste_UI.png -------------------------------------------------------------------------------- /upnptool/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # @FileName :__init__.py 4 | # @Time :2023/8/22 21:58 5 | 6 | 7 | import logging 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | if __name__ == '__main__': 12 | pass 13 | -------------------------------------------------------------------------------- /upnptool/logic.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import logging 3 | import ctypes 4 | import functools 5 | import tkinter as tk 6 | import tkinter.ttk as ttk 7 | from . import smalltool1 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | def hdpi(root): 13 | # 告诉操作系统使用程序自身的dpi适配 14 | ctypes.windll.shcore.SetProcessDpiAwareness(1) 15 | # 获取屏幕的缩放因子 16 | ScaleFactor = ctypes.windll.shcore.GetScaleFactorForDevice(0) 17 | # 设置程序缩放 18 | root.tk.call('tk', 'scaling', ScaleFactor / 75) 19 | 20 | 21 | def initgui(w: smalltool1.Toplevel1): 22 | h = socket.gethostname() 23 | w.remotehostvar.set('0.0.0.0') 24 | w.localhostvar.set(socket.gethostbyname(h)) 25 | w.descriptvar.set('your msg') 26 | w.timeoutvar.set('0') 27 | # 激活cross listbox 多选 28 | w.Scrolledlistbox_portmaps.config(exportselection=0) 29 | w.Scrolledlistbox_devices.configure(exportselection=0) 30 | 31 | bind_logger_output(w.Scrolledtext_log) 32 | 33 | 34 | class TextOutHandler(logging.Handler): 35 | """""" 36 | 37 | def __init__(self, *args, **kwargs): 38 | """""" 39 | if 'textbox' in kwargs: 40 | self.textbox: smalltool1.ScrolledText = kwargs.pop('textbox') 41 | if not self.textbox: 42 | logger.error('bind error: %s', self.__name__) 43 | 44 | super().__init__(*args, **kwargs) 45 | 46 | def emit(self, record: logging.LogRecord) -> None: 47 | """""" 48 | msg = self.format(record) 49 | self.textbox.configure(state=tk.NORMAL) 50 | self.textbox.insert(tk.END, msg) 51 | self.textbox.insert(tk.END, '\n') 52 | self.textbox.see(tk.END) 53 | self.textbox.configure(state=tk.DISABLED) 54 | 55 | 56 | def bind_logger_output(textbox): 57 | log = logging.getLogger(__package__) 58 | hd = TextOutHandler(textbox=textbox) 59 | log.setLevel(logging.DEBUG) 60 | fmt = logging.Formatter('%(asctime)s [%(levelname)s]:\t%(message)s', datefmt=logging.Formatter.default_time_format) 61 | hd.setFormatter(fmt) 62 | log.addHandler(hd) 63 | std = logging.StreamHandler() 64 | std.setFormatter(fmt) 65 | log.addHandler(std) 66 | 67 | log.info('Welcome.') 68 | log.debug('UPnP Tool Created @6767') 69 | 70 | 71 | def protector(func): 72 | """装饰器保护器""" 73 | 74 | @functools.wraps(func) 75 | def decorated(*args, **kwargs): 76 | try: 77 | return func(*args, **kwargs) 78 | except Exception as e: 79 | logger.exception('%s %s', type(e).__name__, e) 80 | 81 | return decorated 82 | -------------------------------------------------------------------------------- /upnptool/note.txt: -------------------------------------------------------------------------------- 1 | 2 | https://pypi.org/project/upnpclient/ 3 | 4 | 5 | 6 | d.WANIPConn1.AddPortMapping( 7 | NewRemoteHost='0.0.0.0', 8 | NewExternalPort=13899, 9 | NewProtocol='UDP', 10 | NewInternalPort=13899, 11 | NewInternalClient='192.168.1.6', 12 | NewEnabled='1', 13 | NewPortMappingDescription='Testing', 14 | NewLeaseDuration=100) 15 | 16 | 17 | d.WANIPConn1.DeletePortMapping(NewRemoteHost='0.0.0.0', 18 | NewExternalPort=13899, 19 | NewProtocol='UDP', 20 | NewInternalPort=13899, 21 | NewInternalClient='192.168.1.6', 22 | NewEnabled='1', 23 | NewPortMappingDescription='Testing' 24 | ) -------------------------------------------------------------------------------- /upnptool/smalltool1.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # GUI module generated by PAGE version 7.6 5 | # in conjunction with Tcl version 8.6 6 | # Aug 22, 2023 11:10:38 PM CST platform: Windows NT 7 | 8 | import sys 9 | import tkinter as tk 10 | import tkinter.ttk as ttk 11 | from tkinter.constants import * 12 | import os.path 13 | 14 | _script = sys.argv[0] 15 | _location = os.path.dirname(_script) 16 | 17 | from . import smalltool1_support 18 | 19 | _bgcolor = '#d9d9d9' # X11 color: 'gray85' 20 | _fgcolor = '#000000' # X11 color: 'black' 21 | _compcolor = 'gray40' # X11 color: #666666 22 | _ana1color = '#c3c3c3' # Closest X11 color: 'gray76' 23 | _ana2color = 'beige' # X11 color: #f5f5dc 24 | _tabfg1 = 'black' 25 | _tabfg2 = 'black' 26 | _tabbg1 = 'grey75' 27 | _tabbg2 = 'grey89' 28 | _bgmode = 'light' 29 | 30 | _style_code_ran = 0 31 | def _style_code(): 32 | global _style_code_ran 33 | if _style_code_ran: 34 | return 35 | style = ttk.Style() 36 | if sys.platform == "win32": 37 | style.theme_use('winnative') 38 | style.configure('.',background=_bgcolor) 39 | style.configure('.',foreground=_fgcolor) 40 | style.configure('.',font='TkDefaultFont') 41 | style.map('.',background = 42 | [('selected', _compcolor), ('active',_ana2color)]) 43 | if _bgmode == 'dark': 44 | style.map('.',foreground = 45 | [('selected', 'white'), ('active','white')]) 46 | else: 47 | style.map('.',foreground = 48 | [('selected', 'black'), ('active','black')]) 49 | style.configure('Vertical.TScrollbar', background=_bgcolor, 50 | arrowcolor= _fgcolor) 51 | style.configure('Horizontal.TScrollbar', background=_bgcolor, 52 | arrowcolor= _fgcolor) 53 | _style_code_ran = 1 54 | 55 | class Toplevel1: 56 | def __init__(self, top=None): 57 | '''This class configures and populates the toplevel window. 58 | top is the toplevel containing window.''' 59 | 60 | top.geometry("755x403+828+257") 61 | top.minsize(152, 1) 62 | top.maxsize(2052, 1133) 63 | top.resizable(1, 1) 64 | top.title("UPnP Manager") 65 | top.configure(background="#d9d9d9") 66 | top.configure(highlightbackground="#d9d9d9") 67 | top.configure(highlightcolor="black") 68 | 69 | self.top = top 70 | self.localhostvar = tk.StringVar() 71 | self.localportvar = tk.StringVar() 72 | self.remotehostvar = tk.StringVar() 73 | self.remoteportvar = tk.StringVar() 74 | self.timeoutvar = tk.StringVar() 75 | self.comboboxtype = tk.StringVar() 76 | self.descriptvar = tk.StringVar() 77 | 78 | _style_code() 79 | self.Scrolledtext_log = ScrolledText(self.top) 80 | self.Scrolledtext_log.place(relx=0.03, rely=0.737, relheight=0.226 81 | , relwidth=0.943) 82 | self.Scrolledtext_log.configure(background="white") 83 | self.Scrolledtext_log.configure(font="-family {Consolas} -size 9") 84 | self.Scrolledtext_log.configure(foreground="black") 85 | self.Scrolledtext_log.configure(highlightbackground="#d9d9d9") 86 | self.Scrolledtext_log.configure(highlightcolor="black") 87 | self.Scrolledtext_log.configure(insertbackground="black") 88 | self.Scrolledtext_log.configure(insertborderwidth="3") 89 | self.Scrolledtext_log.configure(selectbackground="#c4c4c4") 90 | self.Scrolledtext_log.configure(selectforeground="black") 91 | self.Scrolledtext_log.configure(wrap="none") 92 | self.TFrame1 = ttk.Frame(self.top) 93 | self.TFrame1.place(relx=0.371, rely=0.025, relheight=0.697 94 | , relwidth=0.289) 95 | self.TFrame1.configure(relief='groove') 96 | self.TFrame1.configure(borderwidth="2") 97 | self.TFrame1.configure(relief="groove") 98 | self.TButton_addportmap = ttk.Button(self.TFrame1) 99 | self.TButton_addportmap.place(relx=0.046, rely=0.854, height=27 100 | , width=187) 101 | self.TButton_addportmap.configure(command=smalltool1_support.add_portmap) 102 | self.TButton_addportmap.configure(takefocus="") 103 | self.TButton_addportmap.configure(text='''AddPortMap''') 104 | self.TButton_addportmap.configure(compound='left') 105 | self.TLabel1 = ttk.Label(self.TFrame1) 106 | self.TLabel1.place(relx=0.032, rely=0.036, height=36, width=85) 107 | self.TLabel1.configure(background="#d9d9d9") 108 | self.TLabel1.configure(foreground="#000000") 109 | self.TLabel1.configure(font="TkDefaultFont") 110 | self.TLabel1.configure(relief="flat") 111 | self.TLabel1.configure(anchor='w') 112 | self.TLabel1.configure(justify='left') 113 | self.TLabel1.configure(text='''LocalHost''') 114 | self.TLabel1.configure(compound='left') 115 | self.TEntry_localhost = ttk.Entry(self.TFrame1) 116 | self.TEntry_localhost.place(relx=0.413, rely=0.075, relheight=0.082 117 | , relwidth=0.56) 118 | self.TEntry_localhost.configure(textvariable=self.localhostvar) 119 | self.TEntry_localhost.configure(takefocus="") 120 | self.TEntry_localhost.configure(cursor="ibeam") 121 | self.TEntry_localhost_tooltip = \ 122 | ToolTip(self.TEntry_localhost, '''device who use port''') 123 | 124 | self.TLabel2 = ttk.Label(self.TFrame1) 125 | self.TLabel2.place(relx=0.037, rely=0.146, height=36, width=75) 126 | self.TLabel2.configure(background="#d9d9d9") 127 | self.TLabel2.configure(foreground="#000000") 128 | self.TLabel2.configure(font="TkDefaultFont") 129 | self.TLabel2.configure(relief="flat") 130 | self.TLabel2.configure(anchor='w') 131 | self.TLabel2.configure(justify='left') 132 | self.TLabel2.configure(text='''LocalPort''') 133 | self.TLabel2.configure(compound='left') 134 | self.TLabel3 = ttk.Label(self.TFrame1) 135 | self.TLabel3.place(relx=0.032, rely=0.498, height=36, width=105) 136 | self.TLabel3.configure(background="#d9d9d9") 137 | self.TLabel3.configure(foreground="#000000") 138 | self.TLabel3.configure(font="TkDefaultFont") 139 | self.TLabel3.configure(relief="flat") 140 | self.TLabel3.configure(anchor='w') 141 | self.TLabel3.configure(justify='left') 142 | self.TLabel3.configure(text='''RemotePort''') 143 | self.TLabel3.configure(compound='left') 144 | self.TEntry_localport = ttk.Entry(self.TFrame1) 145 | self.TEntry_localport.place(relx=0.413, rely=0.178, relheight=0.085 146 | , relwidth=0.56) 147 | self.TEntry_localport.configure(textvariable=self.localportvar) 148 | self.TEntry_localport.configure(takefocus="") 149 | self.TEntry_localport.configure(cursor="fleur") 150 | self.TEntry_localport_tooltip = \ 151 | ToolTip(self.TEntry_localport, '''1-65535''') 152 | 153 | self.TEntry_remotehost = ttk.Entry(self.TFrame1) 154 | self.TEntry_remotehost.place(relx=0.55, rely=0.427, relheight=0.082 155 | , relwidth=0.422) 156 | self.TEntry_remotehost.configure(textvariable=self.remotehostvar) 157 | self.TEntry_remotehost.configure(takefocus="") 158 | self.TEntry_remotehost.configure(cursor="fleur") 159 | self.TEntry_remotehost_tooltip = \ 160 | ToolTip(self.TEntry_remotehost, '''route listening''') 161 | 162 | self.TLabel4 = ttk.Label(self.TFrame1) 163 | self.TLabel4.place(relx=0.037, rely=0.427, height=21, width=102) 164 | self.TLabel4.configure(background="#d9d9d9") 165 | self.TLabel4.configure(foreground="#000000") 166 | self.TLabel4.configure(font="TkDefaultFont") 167 | self.TLabel4.configure(relief="flat") 168 | self.TLabel4.configure(anchor='w') 169 | self.TLabel4.configure(justify='left') 170 | self.TLabel4.configure(text='''RemoteHost''') 171 | self.TLabel4.configure(compound='left') 172 | self.TEntry_remoteport = ttk.Entry(self.TFrame1) 173 | self.TEntry_remoteport.place(relx=0.55, rely=0.534, relheight=0.085 174 | , relwidth=0.422) 175 | self.TEntry_remoteport.configure(textvariable=self.remoteportvar) 176 | self.TEntry_remoteport.configure(takefocus="") 177 | self.TEntry_remoteport.configure(cursor="ibeam") 178 | self.TEntry_remoteport_tooltip = \ 179 | ToolTip(self.TEntry_remoteport, '''1-65535''') 180 | 181 | self.TEntry_timeout = ttk.Entry(self.TFrame1) 182 | self.TEntry_timeout.place(relx=0.55, rely=0.641, relheight=0.085 183 | , relwidth=0.422) 184 | self.TEntry_timeout.configure(textvariable=self.timeoutvar) 185 | self.TEntry_timeout.configure(takefocus="") 186 | self.TEntry_timeout.configure(cursor="ibeam") 187 | self.TEntry_timeout_tooltip = \ 188 | ToolTip(self.TEntry_timeout, '''≥0''') 189 | 190 | self.TLabel5 = ttk.Label(self.TFrame1) 191 | self.TLabel5.place(relx=0.032, rely=0.605, height=32, width=71) 192 | self.TLabel5.configure(background="#d9d9d9") 193 | self.TLabel5.configure(foreground="#000000") 194 | self.TLabel5.configure(font="TkDefaultFont") 195 | self.TLabel5.configure(relief="flat") 196 | self.TLabel5.configure(anchor='w') 197 | self.TLabel5.configure(justify='left') 198 | self.TLabel5.configure(text='''Timeout''') 199 | self.TLabel5.configure(compound='left') 200 | self.TCombobox_type = ttk.Combobox(self.TFrame1) 201 | self.TCombobox_type.place(relx=0.413, rely=0.747, relheight=0.085 202 | , relwidth=0.56) 203 | self.value_list = ['TCP','UDP',] 204 | self.TCombobox_type.configure(values=self.value_list) 205 | self.TCombobox_type.configure(textvariable=self.comboboxtype) 206 | self.TCombobox_type.configure(takefocus="") 207 | self.TCombobox_type_tooltip = \ 208 | ToolTip(self.TCombobox_type, '''select TCP/UDP''') 209 | 210 | self.TLabel6 = ttk.Label(self.TFrame1) 211 | self.TLabel6.place(relx=0.046, rely=0.712, height=31, width=51) 212 | self.TLabel6.configure(background="#d9d9d9") 213 | self.TLabel6.configure(foreground="#000000") 214 | self.TLabel6.configure(font="TkDefaultFont") 215 | self.TLabel6.configure(relief="flat") 216 | self.TLabel6.configure(anchor='w') 217 | self.TLabel6.configure(justify='left') 218 | self.TLabel6.configure(text='''Type''') 219 | self.TLabel6.configure(compound='left') 220 | self.TEntry_description = ttk.Entry(self.TFrame1) 221 | self.TEntry_description.place(relx=0.413, rely=0.285, relheight=0.1 222 | , relwidth=0.56) 223 | self.TEntry_description.configure(textvariable=self.descriptvar) 224 | self.TEntry_description.configure(takefocus="") 225 | self.TEntry_description.configure(cursor="ibeam") 226 | self.TLabel7 = ttk.Label(self.TFrame1) 227 | self.TLabel7.place(relx=0.018, rely=0.285, height=21, width=83) 228 | self.TLabel7.configure(background="#d9d9d9") 229 | self.TLabel7.configure(foreground="#000000") 230 | self.TLabel7.configure(font="-family {Microsoft YaHei UI} -size 9") 231 | self.TLabel7.configure(relief="flat") 232 | self.TLabel7.configure(anchor='w') 233 | self.TLabel7.configure(justify='left') 234 | self.TLabel7.configure(text='''Description''') 235 | self.TLabel7.configure(compound='left') 236 | self.TFrame2 = ttk.Frame(self.top) 237 | self.TFrame2.place(relx=0.03, rely=0.025, relheight=0.697 238 | , relwidth=0.331) 239 | self.TFrame2.configure(relief='groove') 240 | self.TFrame2.configure(borderwidth="2") 241 | self.TFrame2.configure(relief="groove") 242 | self.TButton_querydevice = ttk.Button(self.TFrame2) 243 | self.TButton_querydevice.place(relx=0.048, rely=0.036, height=27 244 | , width=217) 245 | self.TButton_querydevice.configure(command=smalltool1_support.query_devices) 246 | self.TButton_querydevice.configure(takefocus="") 247 | self.TButton_querydevice.configure(text='''Query''') 248 | self.TButton_querydevice.configure(compound='left') 249 | self.Scrolledlistbox_devices = ScrolledListBox(self.TFrame2) 250 | self.Scrolledlistbox_devices.place(relx=0.04, rely=0.142, relheight=0.804 251 | , relwidth=0.884) 252 | self.Scrolledlistbox_devices.configure(background="white") 253 | self.Scrolledlistbox_devices.configure(cursor="xterm") 254 | self.Scrolledlistbox_devices.configure(disabledforeground="#a3a3a3") 255 | self.Scrolledlistbox_devices.configure(font="TkFixedFont") 256 | self.Scrolledlistbox_devices.configure(foreground="black") 257 | self.Scrolledlistbox_devices.configure(highlightbackground="#d9d9d9") 258 | self.Scrolledlistbox_devices.configure(highlightcolor="#d9d9d9") 259 | self.Scrolledlistbox_devices.configure(selectbackground="#c4c4c4") 260 | self.Scrolledlistbox_devices.configure(selectforeground="black") 261 | self.TFrame3 = ttk.Frame(self.top) 262 | self.TFrame3.place(relx=0.67, rely=0.025, relheight=0.697 263 | , relwidth=0.302) 264 | self.TFrame3.configure(relief='groove') 265 | self.TFrame3.configure(borderwidth="2") 266 | self.TFrame3.configure(relief="groove") 267 | self.TButton_deleteport = ttk.Button(self.TFrame3) 268 | self.TButton_deleteport.place(relx=0.044, rely=0.854, height=27 269 | , width=197) 270 | self.TButton_deleteport.configure(command=smalltool1_support.delete_portmap) 271 | self.TButton_deleteport.configure(takefocus="") 272 | self.TButton_deleteport.configure(text='''DeletePortMap''') 273 | self.TButton_deleteport.configure(compound='left') 274 | self.Scrolledlistbox_portmaps = ScrolledListBox(self.TFrame3, selectmode=MULTIPLE) 275 | self.Scrolledlistbox_portmaps.place(relx=0.066, rely=0.142 276 | , relheight=0.69, relwidth=0.851) 277 | self.Scrolledlistbox_portmaps.configure(background="white") 278 | self.Scrolledlistbox_portmaps.configure(cursor="xterm") 279 | self.Scrolledlistbox_portmaps.configure(disabledforeground="#a3a3a3") 280 | self.Scrolledlistbox_portmaps.configure(font="TkFixedFont") 281 | self.Scrolledlistbox_portmaps.configure(foreground="black") 282 | self.Scrolledlistbox_portmaps.configure(highlightbackground="#d9d9d9") 283 | self.Scrolledlistbox_portmaps.configure(highlightcolor="#d9d9d9") 284 | self.Scrolledlistbox_portmaps.configure(selectbackground="#c4c4c4") 285 | self.Scrolledlistbox_portmaps.configure(selectforeground="black") 286 | self.TButton_queryport = ttk.Button(self.TFrame3) 287 | self.TButton_queryport.place(relx=0.066, rely=0.036, height=27 288 | , width=187) 289 | self.TButton_queryport.configure(command=smalltool1_support.list_portmaps) 290 | self.TButton_queryport.configure(takefocus="") 291 | self.TButton_queryport.configure(text='''ListPortMap''') 292 | self.TButton_queryport.configure(compound='left') 293 | 294 | from time import time, localtime, strftime 295 | class ToolTip(tk.Toplevel): 296 | """ Provides a ToolTip widget for Tkinter. """ 297 | def __init__(self, wdgt, msg=None, msgFunc=None, delay=0.5, 298 | follow=True): 299 | self.wdgt = wdgt 300 | self.parent = self.wdgt.master 301 | tk.Toplevel.__init__(self, self.parent, bg='black', padx=1, pady=1) 302 | self.withdraw() 303 | self.overrideredirect(True) 304 | self.msgVar = tk.StringVar() 305 | if msg is None: 306 | self.msgVar.set('No message provided') 307 | else: 308 | self.msgVar.set(msg) 309 | self.msgFunc = msgFunc 310 | self.delay = delay 311 | self.follow = follow 312 | self.visible = 0 313 | self.lastMotion = 0 314 | self.msg = tk.Message(self, textvariable=self.msgVar, bg=_bgcolor, 315 | fg=_fgcolor, font="TkDefaultFont", 316 | aspect=1000) 317 | self.msg.grid() 318 | self.wdgt.bind('', self.spawn, '+') 319 | self.wdgt.bind('', self.hide, '+') 320 | self.wdgt.bind('', self.move, '+') 321 | def spawn(self, event=None): 322 | self.visible = 1 323 | self.after(int(self.delay * 1000), self.show) 324 | def show(self): 325 | if self.visible == 1 and time() - self.lastMotion > self.delay: 326 | self.visible = 2 327 | if self.visible == 2: 328 | self.deiconify() 329 | def move(self, event): 330 | self.lastMotion = time() 331 | if self.follow is False: 332 | self.withdraw() 333 | self.visible = 1 334 | self.geometry('+%i+%i' % (event.x_root + 20, event.y_root - 10)) 335 | try: 336 | self.msgVar.set(self.msgFunc()) 337 | except: 338 | pass 339 | self.after(int(self.delay * 1000), self.show) 340 | def hide(self, event=None): 341 | self.visible = 0 342 | self.withdraw() 343 | def update(self, msg): 344 | self.msgVar.set(msg) 345 | def configure(self, **kwargs): 346 | backgroundset = False 347 | foregroundset = False 348 | # Get the current tooltip text just in case the user doesn't provide any. 349 | current_text = self.msgVar.get() 350 | # to clear the tooltip text, use the .update method 351 | if 'debug' in kwargs.keys(): 352 | debug = kwargs.pop('debug', False) 353 | if debug: 354 | for key, value in kwargs.items(): 355 | print(f'key: {key} - value: {value}') 356 | if 'background' in kwargs.keys(): 357 | background = kwargs.pop('background') 358 | backgroundset = True 359 | if 'bg' in kwargs.keys(): 360 | background = kwargs.pop('bg') 361 | backgroundset = True 362 | if 'foreground' in kwargs.keys(): 363 | foreground = kwargs.pop('foreground') 364 | foregroundset = True 365 | if 'fg' in kwargs.keys(): 366 | foreground = kwargs.pop('fg') 367 | foregroundset = True 368 | 369 | fontd = kwargs.pop('font', None) 370 | if 'text' in kwargs.keys(): 371 | text = kwargs.pop('text') 372 | if (text == '') or (text == "\n"): 373 | text = current_text 374 | else: 375 | self.msgVar.set(text) 376 | reliefd = kwargs.pop('relief', 'flat') 377 | justifyd = kwargs.pop('justify', 'left') 378 | padxd = kwargs.pop('padx', 1) 379 | padyd = kwargs.pop('pady', 1) 380 | borderwidthd = kwargs.pop('borderwidth', 2) 381 | wid = self.msg # The message widget which is the actual tooltip 382 | if backgroundset: 383 | wid.config(bg=background) 384 | if foregroundset: 385 | wid.config(fg=foreground) 386 | wid.config(font=fontd) 387 | wid.config(borderwidth=borderwidthd) 388 | wid.config(relief=reliefd) 389 | wid.config(justify=justifyd) 390 | wid.config(padx=padxd) 391 | wid.config(pady=padyd) 392 | # End of Class ToolTip 393 | 394 | # The following code is added to facilitate the Scrolled widgets you specified. 395 | class AutoScroll(object): 396 | '''Configure the scrollbars for a widget.''' 397 | def __init__(self, master): 398 | # Rozen. Added the try-except clauses so that this class 399 | # could be used for scrolled entry widget for which vertical 400 | # scrolling is not supported. 5/7/14. 401 | try: 402 | vsb = ttk.Scrollbar(master, orient='vertical', command=self.yview) 403 | except: 404 | pass 405 | hsb = ttk.Scrollbar(master, orient='horizontal', command=self.xview) 406 | try: 407 | self.configure(yscrollcommand=self._autoscroll(vsb)) 408 | except: 409 | pass 410 | self.configure(xscrollcommand=self._autoscroll(hsb)) 411 | self.grid(column=0, row=0, sticky='nsew') 412 | try: 413 | vsb.grid(column=1, row=0, sticky='ns') 414 | except: 415 | pass 416 | hsb.grid(column=0, row=1, sticky='ew') 417 | master.grid_columnconfigure(0, weight=1) 418 | master.grid_rowconfigure(0, weight=1) 419 | # Copy geometry methods of master (taken from ScrolledText.py) 420 | methods = tk.Pack.__dict__.keys() | tk.Grid.__dict__.keys() \ 421 | | tk.Place.__dict__.keys() 422 | for meth in methods: 423 | if meth[0] != '_' and meth not in ('config', 'configure'): 424 | setattr(self, meth, getattr(master, meth)) 425 | 426 | @staticmethod 427 | def _autoscroll(sbar): 428 | '''Hide and show scrollbar as needed.''' 429 | def wrapped(first, last): 430 | first, last = float(first), float(last) 431 | if first <= 0 and last >= 1: 432 | sbar.grid_remove() 433 | else: 434 | sbar.grid() 435 | sbar.set(first, last) 436 | return wrapped 437 | 438 | def __str__(self): 439 | return str(self.master) 440 | 441 | def _create_container(func): 442 | '''Creates a ttk Frame with a given master, and use this new frame to 443 | place the scrollbars and the widget.''' 444 | def wrapped(cls, master, **kw): 445 | container = ttk.Frame(master) 446 | container.bind('', lambda e: _bound_to_mousewheel(e, container)) 447 | container.bind('', lambda e: _unbound_to_mousewheel(e, container)) 448 | return func(cls, container, **kw) 449 | return wrapped 450 | 451 | class ScrolledText(AutoScroll, tk.Text): 452 | '''A standard Tkinter Text widget with scrollbars that will 453 | automatically show/hide as needed.''' 454 | @_create_container 455 | def __init__(self, master, **kw): 456 | tk.Text.__init__(self, master, **kw) 457 | AutoScroll.__init__(self, master) 458 | 459 | class ScrolledListBox(AutoScroll, tk.Listbox): 460 | '''A standard Tkinter Listbox widget with scrollbars that will 461 | automatically show/hide as needed.''' 462 | @_create_container 463 | def __init__(self, master, **kw): 464 | tk.Listbox.__init__(self, master, **kw) 465 | AutoScroll.__init__(self, master) 466 | def size_(self): 467 | sz = tk.Listbox.size(self) 468 | return sz 469 | 470 | import platform 471 | def _bound_to_mousewheel(event, widget): 472 | child = widget.winfo_children()[0] 473 | if platform.system() == 'Windows' or platform.system() == 'Darwin': 474 | child.bind_all('', lambda e: _on_mousewheel(e, child)) 475 | child.bind_all('', lambda e: _on_shiftmouse(e, child)) 476 | else: 477 | child.bind_all('', lambda e: _on_mousewheel(e, child)) 478 | child.bind_all('', lambda e: _on_mousewheel(e, child)) 479 | child.bind_all('', lambda e: _on_shiftmouse(e, child)) 480 | child.bind_all('', lambda e: _on_shiftmouse(e, child)) 481 | 482 | def _unbound_to_mousewheel(event, widget): 483 | if platform.system() == 'Windows' or platform.system() == 'Darwin': 484 | widget.unbind_all('') 485 | widget.unbind_all('') 486 | else: 487 | widget.unbind_all('') 488 | widget.unbind_all('') 489 | widget.unbind_all('') 490 | widget.unbind_all('') 491 | 492 | def _on_mousewheel(event, widget): 493 | if platform.system() == 'Windows': 494 | widget.yview_scroll(-1*int(event.delta/120),'units') 495 | elif platform.system() == 'Darwin': 496 | widget.yview_scroll(-1*int(event.delta),'units') 497 | else: 498 | if event.num == 4: 499 | widget.yview_scroll(-1, 'units') 500 | elif event.num == 5: 501 | widget.yview_scroll(1, 'units') 502 | 503 | def _on_shiftmouse(event, widget): 504 | if platform.system() == 'Windows': 505 | widget.xview_scroll(-1*int(event.delta/120), 'units') 506 | elif platform.system() == 'Darwin': 507 | widget.xview_scroll(-1*int(event.delta), 'units') 508 | else: 509 | if event.num == 4: 510 | widget.xview_scroll(-1, 'units') 511 | elif event.num == 5: 512 | widget.xview_scroll(1, 'units') 513 | def start_up(): 514 | smalltool1_support.main() 515 | 516 | if __name__ == '__main__': 517 | smalltool1_support.main() 518 | 519 | 520 | 521 | 522 | -------------------------------------------------------------------------------- /upnptool/smalltool1.tcl: -------------------------------------------------------------------------------- 1 | ############################################################################# 2 | # Generated by PAGE version 7.6 3 | # in conjunction with Tcl version 8.6 4 | # Aug 22, 2023 11:11:40 PM CST platform: Windows NT 5 | set vTcl(timestamp) "" 6 | if {![info exists vTcl(borrow)]} { 7 | ::vTcl::MessageBox -title Error -message "You must open project files from within PAGE." 8 | exit} 9 | 10 | 11 | set image_list { 12 | } 13 | vTcl:create_project_images $image_list ;# In image.tcl 14 | 15 | if {!$vTcl(borrow) && !$vTcl(template)} { 16 | 17 | set vTcl(actual_gui_font_dft_desc) TkDefaultFont 18 | set vTcl(actual_gui_font_dft_name) TkDefaultFont 19 | set vTcl(actual_gui_font_text_desc) TkTextFont 20 | set vTcl(actual_gui_font_text_name) TkTextFont 21 | set vTcl(actual_gui_font_fixed_desc) TkFixedFont 22 | set vTcl(actual_gui_font_fixed_name) TkFixedFont 23 | set vTcl(actual_gui_font_menu_desc) TkMenuFont 24 | set vTcl(actual_gui_font_menu_name) TkMenuFont 25 | set vTcl(actual_gui_font_tooltip_desc) TkDefaultFont 26 | set vTcl(actual_gui_font_tooltip_name) TkDefaultFont 27 | set vTcl(actual_gui_font_treeview_desc) TkDefaultFont 28 | set vTcl(actual_gui_font_treeview_name) TkDefaultFont 29 | ########################################### 30 | set vTcl(actual_gui_bg) #d9d9d9 31 | set vTcl(actual_gui_fg) #000000 32 | set vTcl(actual_gui_analog) #ececec 33 | set vTcl(actual_gui_menu_analog) #ececec 34 | set vTcl(actual_gui_menu_bg) #d9d9d9 35 | set vTcl(actual_gui_menu_fg) #000000 36 | set vTcl(complement_color) gray40 37 | set vTcl(analog_color_p) #c3c3c3 38 | set vTcl(analog_color_m) beige 39 | set vTcl(tabfg1) black 40 | set vTcl(tabfg2) black 41 | set vTcl(actual_gui_menu_active_bg) #ececec 42 | set vTcl(actual_gui_menu_active_fg) #000000 43 | ########################################### 44 | set vTcl(pr,autoalias) 1 45 | set vTcl(pr,relative_placement) 1 46 | set vTcl(mode) Relative 47 | } 48 | 49 | 50 | 51 | 52 | proc vTclWindow.top1 {base} { 53 | global vTcl 54 | if {$base == ""} { 55 | set base .top1 56 | } 57 | if {[winfo exists $base]} { 58 | wm deiconify $base; return 59 | } 60 | set top $base 61 | set target $base 62 | ################### 63 | # CREATING WIDGETS 64 | ################### 65 | vTcl::widgets::core::toplevel::createCmd $top -class Toplevel \ 66 | -background $vTcl(actual_gui_bg) \ 67 | -highlightbackground $vTcl(actual_gui_bg) -highlightcolor black 68 | wm focusmodel $top passive 69 | wm geometry $top 755x403+994+296 70 | update 71 | # set in toplevel.wgt. 72 | global vTcl 73 | global img_list 74 | set vTcl(save,dflt,origin) 0 75 | wm maxsize $top 2052 1133 76 | wm minsize $top 152 1 77 | wm overrideredirect $top 0 78 | wm resizable $top 1 1 79 | wm deiconify $top 80 | set toptitle "UPnP Manager" 81 | wm title $top $toptitle 82 | namespace eval ::widgets::${top}::ClassOption {} 83 | set ::widgets::${top}::ClassOption(-toptitle) $toptitle 84 | vTcl:DefineAlias "$top" "Toplevel1" vTcl:Toplevel:WidgetProc "" 1 85 | set vTcl(real_top) {} 86 | vTcl::widgets::ttk::scrolledtext::CreateCmd "$top.scr54" \ 87 | -borderwidth 2 -relief groove -background $vTcl(actual_gui_bg) \ 88 | -height 75 -highlightbackground $vTcl(actual_gui_bg) \ 89 | -highlightcolor black -width 125 90 | vTcl:DefineAlias "$top.scr54" "Scrolledtext_log" vTcl:WidgetProc "Toplevel1" 1 91 | ### SPOT dump_widget_opt A 92 | 93 | $top.scr54.01 configure -background white \ 94 | -font "-family {Consolas} -size 9" \ 95 | -foreground black \ 96 | -height 3 \ 97 | -highlightbackground #d9d9d9 \ 98 | -highlightcolor black \ 99 | -insertbackground black \ 100 | -insertborderwidth 3 \ 101 | -selectbackground #c4c4c4 \ 102 | -selectforeground black \ 103 | -width 10 \ 104 | -wrap none 105 | ttk::style configure TFrame -background $vTcl(actual_gui_bg) 106 | ttk::frame "$top.tFr57" \ 107 | -borderwidth 2 -relief groove -width 218 -height 281 108 | vTcl:DefineAlias "$top.tFr57" "TFrame1" vTcl:WidgetProc "Toplevel1" 1 109 | ### SPOT dump_widget_opt A 110 | set site_3_0 $top.tFr57 111 | ttk::style configure TButton -background $vTcl(actual_gui_bg) 112 | ttk::style configure TButton -foreground $vTcl(actual_gui_fg) 113 | ttk::style configure TButton -font "$vTcl(actual_gui_font_dft_desc)" 114 | ttk::button "$site_3_0.tBu61" \ 115 | -command "add_portmap" -takefocus {} -text "AddPortMap" \ 116 | -compound left 117 | vTcl:DefineAlias "$site_3_0.tBu61" "TButton_addportmap" vTcl:WidgetProc "Toplevel1" 1 118 | ### SPOT dump_widget_opt A 119 | ttk::label "$site_3_0.tLa66" \ 120 | -background $vTcl(actual_gui_bg) -foreground $vTcl(actual_gui_fg) \ 121 | -font "TkDefaultFont" -relief flat -anchor w -justify left \ 122 | -text "LocalHost" -compound left 123 | vTcl:DefineAlias "$site_3_0.tLa66" "TLabel1" vTcl:WidgetProc "Toplevel1" 1 124 | ### SPOT dump_widget_opt A 125 | ttk::entry "$site_3_0.tEn67" \ 126 | -font "TkTextFont" -textvariable "localhostvar" -foreground {} \ 127 | -background {} -takefocus {} -cursor ibeam 128 | vTcl:DefineAlias "$site_3_0.tEn67" "TEntry_localhost" vTcl:WidgetProc "Toplevel1" 1 129 | ### SPOT dump_widget_opt A 130 | bind $site_3_0.tEn67 <> { 131 | set ::vTcl::balloon::%W {device who use port} 132 | } 133 | ttk::label "$site_3_0.tLa68" \ 134 | -background $vTcl(actual_gui_bg) -foreground $vTcl(actual_gui_fg) \ 135 | -font "TkDefaultFont" -relief flat -anchor w -justify left \ 136 | -text "LocalPort" -compound left 137 | vTcl:DefineAlias "$site_3_0.tLa68" "TLabel2" vTcl:WidgetProc "Toplevel1" 1 138 | ### SPOT dump_widget_opt A 139 | ttk::label "$site_3_0.tLa69" \ 140 | -background $vTcl(actual_gui_bg) -foreground $vTcl(actual_gui_fg) \ 141 | -font "TkDefaultFont" -relief flat -anchor w -justify left \ 142 | -text "RemotePort" -compound left 143 | vTcl:DefineAlias "$site_3_0.tLa69" "TLabel3" vTcl:WidgetProc "Toplevel1" 1 144 | ### SPOT dump_widget_opt A 145 | ttk::entry "$site_3_0.tEn47" \ 146 | -font "TkTextFont" -textvariable "localportvar" -foreground {} \ 147 | -background {} -takefocus {} -cursor fleur 148 | vTcl:DefineAlias "$site_3_0.tEn47" "TEntry_localport" vTcl:WidgetProc "Toplevel1" 1 149 | ### SPOT dump_widget_opt A 150 | bind $site_3_0.tEn47 <> { 151 | set ::vTcl::balloon::%W {1-65535} 152 | } 153 | ttk::entry "$site_3_0.tEn48" \ 154 | -font "TkTextFont" -textvariable "remotehostvar" -foreground {} \ 155 | -background {} -takefocus {} -cursor fleur 156 | vTcl:DefineAlias "$site_3_0.tEn48" "TEntry_remotehost" vTcl:WidgetProc "Toplevel1" 1 157 | ### SPOT dump_widget_opt A 158 | bind $site_3_0.tEn48 <> { 159 | set ::vTcl::balloon::%W {route listening} 160 | } 161 | ttk::label "$site_3_0.tLa49" \ 162 | -background $vTcl(actual_gui_bg) -foreground $vTcl(actual_gui_fg) \ 163 | -font "TkDefaultFont" -relief flat -anchor w -justify left \ 164 | -text "RemoteHost" -compound left 165 | vTcl:DefineAlias "$site_3_0.tLa49" "TLabel4" vTcl:WidgetProc "Toplevel1" 1 166 | ### SPOT dump_widget_opt A 167 | ttk::entry "$site_3_0.tEn50" \ 168 | -font "TkTextFont" -textvariable "remoteportvar" -foreground {} \ 169 | -background {} -takefocus {} -cursor ibeam 170 | vTcl:DefineAlias "$site_3_0.tEn50" "TEntry_remoteport" vTcl:WidgetProc "Toplevel1" 1 171 | ### SPOT dump_widget_opt A 172 | bind $site_3_0.tEn50 <> { 173 | set ::vTcl::balloon::%W {1-65535} 174 | } 175 | ttk::entry "$site_3_0.tEn49" \ 176 | -font "TkTextFont" -textvariable "timeoutvar" -foreground {} \ 177 | -background {} -takefocus {} -cursor ibeam 178 | vTcl:DefineAlias "$site_3_0.tEn49" "TEntry_timeout" vTcl:WidgetProc "Toplevel1" 1 179 | ### SPOT dump_widget_opt A 180 | bind $site_3_0.tEn49 <> { 181 | set ::vTcl::balloon::%W {≥0} 182 | } 183 | ttk::label "$site_3_0.tLa50" \ 184 | -background $vTcl(actual_gui_bg) -foreground $vTcl(actual_gui_fg) \ 185 | -font "TkDefaultFont" -relief flat -anchor w -justify left \ 186 | -text "Timeout" -compound left 187 | vTcl:DefineAlias "$site_3_0.tLa50" "TLabel5" vTcl:WidgetProc "Toplevel1" 1 188 | ### SPOT dump_widget_opt A 189 | ttk::combobox "$site_3_0.tCo51" \ 190 | -values "TCP UDP" -font "TkTextFont" -textvariable "comboboxtype" \ 191 | -foreground {} -background {} -takefocus {} 192 | vTcl:DefineAlias "$site_3_0.tCo51" "TCombobox_type" vTcl:WidgetProc "Toplevel1" 1 193 | ### SPOT dump_widget_opt A 194 | bind $site_3_0.tCo51 <> { 195 | set ::vTcl::balloon::%W {select TCP/UDP} 196 | } 197 | ttk::label "$site_3_0.tLa52" \ 198 | -background $vTcl(actual_gui_bg) -foreground $vTcl(actual_gui_fg) \ 199 | -font "TkDefaultFont" -relief flat -anchor w -justify left \ 200 | -text "Type" -compound left 201 | vTcl:DefineAlias "$site_3_0.tLa52" "TLabel6" vTcl:WidgetProc "Toplevel1" 1 202 | ### SPOT dump_widget_opt A 203 | ttk::entry "$site_3_0.tEn53" \ 204 | -font "TkTextFont" -textvariable "descriptvar" -foreground {} \ 205 | -background {} -takefocus {} -cursor ibeam 206 | vTcl:DefineAlias "$site_3_0.tEn53" "TEntry_description" vTcl:WidgetProc "Toplevel1" 1 207 | ### SPOT dump_widget_opt A 208 | ttk::label "$site_3_0.tLa54" \ 209 | -background $vTcl(actual_gui_bg) -foreground $vTcl(actual_gui_fg) \ 210 | -font "-family {Microsoft YaHei UI} -size 9 -weight normal -slant roman -underline 0 -overstrike 0" \ 211 | -relief flat -anchor w -justify left -text "Description" \ 212 | -compound left 213 | vTcl:DefineAlias "$site_3_0.tLa54" "TLabel7" vTcl:WidgetProc "Toplevel1" 1 214 | ### SPOT dump_widget_opt A 215 | place $site_3_0.tBu61 \ 216 | -in $site_3_0 -x 0 -relx 0.048 -y 0 -rely 0.854 -width 187 \ 217 | -relwidth 0 -height 27 -relheight 0 -anchor nw -bordermode ignore 218 | place $site_3_0.tLa66 \ 219 | -in $site_3_0 -x 0 -relx 0.032 -y 0 -rely 0.036 -width 0 \ 220 | -relwidth 0.39 -height 0 -relheight 0.128 -anchor nw \ 221 | -bordermode ignore 222 | place $site_3_0.tEn67 \ 223 | -in $site_3_0 -x 0 -relx 0.413 -y 0 -rely 0.075 -width 0 \ 224 | -relwidth 0.56 -height 0 -relheight 0.082 -anchor nw \ 225 | -bordermode ignore 226 | place $site_3_0.tLa68 \ 227 | -in $site_3_0 -x 0 -relx 0.038 -y 0 -rely 0.146 -width 0 \ 228 | -relwidth 0.341 -height 0 -relheight 0.128 -anchor nw \ 229 | -bordermode ignore 230 | place $site_3_0.tLa69 \ 231 | -in $site_3_0 -x 0 -relx 0.032 -y 0 -rely 0.498 -width 0 \ 232 | -relwidth 0.482 -height 0 -relheight 0.128 -anchor nw \ 233 | -bordermode ignore 234 | place $site_3_0.tEn47 \ 235 | -in $site_3_0 -x 0 -relx 0.413 -y 0 -rely 0.178 -width 0 \ 236 | -relwidth 0.56 -height 0 -relheight 0.085 -anchor nw \ 237 | -bordermode ignore 238 | place $site_3_0.tEn48 \ 239 | -in $site_3_0 -x 0 -relx 0.55 -y 0 -rely 0.427 -width 92 -relwidth 0 \ 240 | -height 23 -relheight 0 -anchor nw -bordermode ignore 241 | place $site_3_0.tLa49 \ 242 | -in $site_3_0 -x 0 -relx 0.037 -y 0 -rely 0.427 -width 0 \ 243 | -relwidth 0.468 -height 0 -relheight 0.075 -anchor nw \ 244 | -bordermode ignore 245 | place $site_3_0.tEn50 \ 246 | -in $site_3_0 -x 0 -relx 0.55 -y 0 -rely 0.534 -width 92 -relwidth 0 \ 247 | -height 24 -relheight 0 -anchor nw -bordermode ignore 248 | place $site_3_0.tEn49 \ 249 | -in $site_3_0 -x 0 -relx 0.55 -y 0 -rely 0.641 -width 92 -relwidth 0 \ 250 | -height 24 -relheight 0 -anchor nw -bordermode ignore 251 | place $site_3_0.tLa50 \ 252 | -in $site_3_0 -x 0 -relx 0.032 -y 0 -rely 0.605 -width 0 \ 253 | -relwidth 0.326 -height 0 -relheight 0.114 -anchor nw \ 254 | -bordermode ignore 255 | place $site_3_0.tCo51 \ 256 | -in $site_3_0 -x 0 -relx 0.413 -y 0 -rely 0.747 -width 0 \ 257 | -relwidth 0.56 -height 0 -relheight 0.085 -anchor nw \ 258 | -bordermode ignore 259 | place $site_3_0.tLa52 \ 260 | -in $site_3_0 -x 0 -relx 0.046 -y 0 -rely 0.712 -width 0 \ 261 | -relwidth 0.234 -height 0 -relheight 0.11 -anchor nw \ 262 | -bordermode ignore 263 | place $site_3_0.tEn53 \ 264 | -in $site_3_0 -x 0 -relx 0.413 -y 0 -rely 0.285 -width 0 \ 265 | -relwidth 0.56 -height 0 -relheight 0.1 -anchor nw -bordermode ignore 266 | place $site_3_0.tLa54 \ 267 | -in $site_3_0 -x 0 -relx 0.018 -y 0 -rely 0.285 -width 0 \ 268 | -relwidth 0.381 -height 0 -relheight 0.075 -anchor nw \ 269 | -bordermode ignore 270 | ttk::style configure TFrame -background $vTcl(actual_gui_bg) 271 | ttk::frame "$top.tFr59" \ 272 | -borderwidth 2 -relief groove -width 215 -height 275 273 | vTcl:DefineAlias "$top.tFr59" "TFrame2" vTcl:WidgetProc "Toplevel1" 1 274 | ### SPOT dump_widget_opt A 275 | set site_3_0 $top.tFr59 276 | ttk::style configure TButton -background $vTcl(actual_gui_bg) 277 | ttk::style configure TButton -foreground $vTcl(actual_gui_fg) 278 | ttk::style configure TButton -font "$vTcl(actual_gui_font_dft_desc)" 279 | ttk::button "$site_3_0.tBu60" \ 280 | -command "query_devices" -takefocus {} -text "Query" -compound left 281 | vTcl:DefineAlias "$site_3_0.tBu60" "TButton_querydevice" vTcl:WidgetProc "Toplevel1" 1 282 | ### SPOT dump_widget_opt A 283 | vTcl::widgets::ttk::scrolledlistbox::CreateCmd "$site_3_0.scr62" \ 284 | -background $vTcl(actual_gui_bg) -height 75 \ 285 | -highlightbackground $vTcl(actual_gui_bg) -highlightcolor black \ 286 | -width 125 287 | vTcl:DefineAlias "$site_3_0.scr62" "Scrolledlistbox_devices" vTcl:WidgetProc "Toplevel1" 1 288 | ### SPOT dump_widget_opt A 289 | 290 | $site_3_0.scr62.01 configure -background white \ 291 | -cursor xterm \ 292 | -disabledforeground #a3a3a3 \ 293 | -font TkFixedFont \ 294 | -foreground black \ 295 | -height 3 \ 296 | -highlightbackground #d9d9d9 \ 297 | -highlightcolor #d9d9d9 \ 298 | -selectbackground #c4c4c4 \ 299 | -selectforeground black \ 300 | -width 10 301 | place $site_3_0.tBu60 \ 302 | -in $site_3_0 -x 0 -relx 0.048 -y 0 -rely 0.036 -width 217 \ 303 | -relwidth 0 -height 27 -relheight 0 -anchor nw -bordermode ignore 304 | place $site_3_0.scr62 \ 305 | -in $site_3_0 -x 0 -relx 0.04 -y 0 -rely 0.142 -width 0 \ 306 | -relwidth 0.884 -height 0 -relheight 0.804 -anchor nw \ 307 | -bordermode ignore 308 | ttk::style configure TFrame -background $vTcl(actual_gui_bg) 309 | ttk::frame "$top.tFr63" \ 310 | -borderwidth 2 -relief groove -width 228 -height 281 311 | vTcl:DefineAlias "$top.tFr63" "TFrame3" vTcl:WidgetProc "Toplevel1" 1 312 | ### SPOT dump_widget_opt A 313 | set site_3_0 $top.tFr63 314 | ttk::style configure TButton -background $vTcl(actual_gui_bg) 315 | ttk::style configure TButton -foreground $vTcl(actual_gui_fg) 316 | ttk::style configure TButton -font "$vTcl(actual_gui_font_dft_desc)" 317 | ttk::button "$site_3_0.tBu64" \ 318 | -command "delete_portmap" -takefocus {} -text "DeletePortMap" \ 319 | -compound left 320 | vTcl:DefineAlias "$site_3_0.tBu64" "TButton_deleteport" vTcl:WidgetProc "Toplevel1" 1 321 | ### SPOT dump_widget_opt A 322 | vTcl::widgets::ttk::scrolledlistbox::CreateCmd "$site_3_0.scr47" \ 323 | -background $vTcl(actual_gui_bg) -height 75 \ 324 | -highlightbackground $vTcl(actual_gui_bg) -highlightcolor black \ 325 | -width 125 326 | vTcl:DefineAlias "$site_3_0.scr47" "Scrolledlistbox_portmaps" vTcl:WidgetProc "Toplevel1" 1 327 | ### SPOT dump_widget_opt A 328 | 329 | $site_3_0.scr47.01 configure -background white \ 330 | -cursor xterm \ 331 | -disabledforeground #a3a3a3 \ 332 | -font TkFixedFont \ 333 | -foreground black \ 334 | -height 3 \ 335 | -highlightbackground #d9d9d9 \ 336 | -highlightcolor #d9d9d9 \ 337 | -selectbackground #c4c4c4 \ 338 | -selectforeground black \ 339 | -width 10 340 | ttk::style configure TButton -background $vTcl(actual_gui_bg) 341 | ttk::style configure TButton -foreground $vTcl(actual_gui_fg) 342 | ttk::style configure TButton -font "$vTcl(actual_gui_font_dft_desc)" 343 | ttk::button "$site_3_0.tBu48" \ 344 | -command "list_portmaps" -takefocus {} -text "ListPortMap" \ 345 | -compound left 346 | vTcl:DefineAlias "$site_3_0.tBu48" "TButton_queryport" vTcl:WidgetProc "Toplevel1" 1 347 | ### SPOT dump_widget_opt A 348 | place $site_3_0.tBu64 \ 349 | -in $site_3_0 -x 0 -relx 0.044 -y 0 -rely 0.854 -width 197 \ 350 | -relwidth 0 -height 27 -relheight 0 -anchor nw -bordermode ignore 351 | place $site_3_0.scr47 \ 352 | -in $site_3_0 -x 0 -relx 0.066 -y 0 -rely 0.142 -width 0 \ 353 | -relwidth 0.851 -height 0 -relheight 0.69 -anchor nw \ 354 | -bordermode ignore 355 | place $site_3_0.tBu48 \ 356 | -in $site_3_0 -x 0 -relx 0.066 -y 0 -rely 0.036 -width 187 \ 357 | -relwidth 0 -height 27 -relheight 0 -anchor nw -bordermode ignore 358 | ################### 359 | # SETTING GEOMETRY 360 | ################### 361 | place $top.scr54 \ 362 | -in $top -x 0 -relx 0.031 -y 0 -rely 0.736 -width 0 -relwidth 0.943 \ 363 | -height 0 -relheight 0.226 -anchor nw -bordermode ignore 364 | place $top.tFr57 \ 365 | -in $top -x 0 -relx 0.371 -y 0 -rely 0.025 -width 0 -relwidth 0.289 \ 366 | -height 0 -relheight 0.697 -anchor nw -bordermode ignore 367 | place $top.tFr59 \ 368 | -in $top -x 0 -relx 0.031 -y 0 -rely 0.025 -width 0 -relwidth 0.33 \ 369 | -height 0 -relheight 0.698 -anchor nw -bordermode ignore 370 | place $top.tFr63 \ 371 | -in $top -x 0 -relx 0.67 -y 0 -rely 0.025 -width 0 -relwidth 0.302 \ 372 | -height 0 -relheight 0.697 -anchor nw -bordermode ignore 373 | 374 | vTcl:FireEvent $base <> 375 | } 376 | 377 | proc 36 {args} {return 1} 378 | 379 | 380 | Window show . 381 | set btop1 "" 382 | if {$vTcl(borrow)} { 383 | set btop1 .bor[expr int([expr rand() * 100])] 384 | while {[lsearch $btop1 $vTcl(tops)] != -1} { 385 | set btop1 .bor[expr int([expr rand() * 100])] 386 | } 387 | } 388 | set vTcl(btop) $btop1 389 | Window show .top1 $btop1 390 | if {$vTcl(borrow)} { 391 | $btop1 configure -background plum 392 | } 393 | 394 | -------------------------------------------------------------------------------- /upnptool/smalltool1_support.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Support module generated by PAGE version 7.6 5 | # in conjunction with Tcl version 8.6 6 | 7 | 8 | import sys 9 | import threading 10 | import tkinter as tk 11 | from tkinter.constants import * 12 | import logging 13 | from typing import Optional 14 | 15 | import upnpclient 16 | 17 | from . import logic 18 | 19 | _debug = True # False to eliminate debug printing from callback functions. 20 | logger = logging.getLogger(__name__) 21 | 22 | 23 | def main(*args): 24 | '''Main entry point for the application.''' 25 | from . import smalltool1 26 | global root 27 | root = tk.Tk() 28 | root.protocol('WM_DELETE_WINDOW', root.destroy) 29 | # Creates a toplevel widget. 30 | global _top1, _w1 31 | _top1 = root 32 | logic.hdpi(root) 33 | _w1 = smalltool1.Toplevel1(_top1) 34 | logic.initgui(_w1) 35 | 36 | root.mainloop() 37 | 38 | 39 | devices = [] 40 | 41 | 42 | @logic.protector 43 | def query_devices(*args): 44 | logger.info('start query devices on LAN') 45 | 46 | # _w1.TButton_querydevice.config(state=DISABLED) 47 | @logic.protector 48 | def _inner(): 49 | _w1.TButton_querydevice.configure(state=tk.DISABLED) 50 | 51 | try: 52 | global devices 53 | devices = upnpclient.discover() 54 | _w1.Scrolledlistbox_devices.delete(0, END) 55 | for device in devices: 56 | _w1.Scrolledlistbox_devices.insert(END, device.friendly_name) 57 | logger.info('device: %s, %s', device, device.device_name) 58 | logger.info('discover devices: %d', len(devices)) 59 | except Exception as e: 60 | logger.error(e) 61 | finally: 62 | _w1.TButton_querydevice.configure(state=tk.NORMAL) 63 | 64 | # _w1.TButton_querydevice.config(state=NORMAL) 65 | 66 | t = threading.Thread(target=_inner) 67 | t.start() 68 | 69 | 70 | @logic.protector 71 | def add_portmap(*args): 72 | d = _load_device() 73 | if not d: 74 | return 75 | try: 76 | r = d.WANIPConn1.AddPortMapping( 77 | NewRemoteHost=_w1.remotehostvar.get(), 78 | NewExternalPort=_w1.remoteportvar.get(), 79 | NewProtocol=_w1.comboboxtype.get(), 80 | NewInternalPort=_w1.localportvar.get(), 81 | NewInternalClient=_w1.localhostvar.get(), 82 | NewEnabled='1', 83 | NewPortMappingDescription=_w1.descriptvar.get(), 84 | NewLeaseDuration=_w1.timeoutvar.get()) 85 | except upnpclient.ValidationError as e: 86 | logger.error('参数填写错误,无法创建端口映射') 87 | return 88 | 89 | logger.debug('add port: %s', r) 90 | logger.info('端口映射添加成功') 91 | 92 | list_portmaps(*args) 93 | 94 | 95 | @logic.protector 96 | def delete_portmap(*args): 97 | d = _load_device() 98 | if not d: 99 | return 100 | cr: tuple = _w1.Scrolledlistbox_portmaps.curselection() 101 | if not cr: 102 | logger.warning('端口未选中!') 103 | return 104 | 105 | logger.debug('delete selected ports index: %s', cr) 106 | crl = list(cr) 107 | crl.reverse() 108 | for i in crl: 109 | v = d.WANIPConn1.GetGenericPortMappingEntry(NewPortMappingIndex=i) 110 | d.WANIPConn1.DeletePortMapping(**v) 111 | 112 | logger.info('refresh ports...') 113 | list_portmaps(*args) 114 | 115 | 116 | def _load_device() -> Optional[upnpclient.upnp.Device]: 117 | """""" 118 | cr = _w1.Scrolledlistbox_devices.curselection() 119 | if not cr: 120 | logger.warning('设备未选中!') 121 | return 122 | logger.debug('device selected: %s', cr) 123 | d = devices[cr[0]] 124 | if not hasattr(d, 'WANIPConn1'): 125 | logger.error('不支持UPnP的设备,换一个吧') 126 | return 127 | return d 128 | 129 | 130 | @logic.protector 131 | def list_portmaps(*args): 132 | """ 133 | 134 | In [1]: d.WANIPConn1.GetGenericPortMappingEntry(NewPortMappingIndex=3) 135 | Out[1]: 136 | {'NewRemoteHost': None, 137 | 'NewExternalPort': 54321, 138 | 'NewProtocol': 'UDP', 139 | 'NewInternalPort': 12345, 140 | 'NewInternalClient': '192.168.1.2', 141 | 'NewEnabled': True, 142 | 'NewPortMappingDescription': 'MiniTP SDK', 143 | 'NewLeaseDuration': 0} 144 | """ 145 | d = _load_device() 146 | if not d: 147 | return 148 | 149 | st = d.WANIPConn1.GetStatusInfo() 150 | nat = d.WANIPConn1.GetNATRSIPStatus() 151 | oip = d.WANIPConn1.GetExternalIPAddress() 152 | logger.info('GetStatusInfo: %s', st) 153 | logger.info('GetNATRSIPStatus: %s', nat) 154 | logger.info('GetExternalIPAddress: %s', oip) 155 | _w1.Scrolledlistbox_portmaps.delete(0, END) 156 | 157 | try: 158 | # 枚举更新端口映射列表, Entry start from 0 to last item. 159 | for i in range(2 ** 16): 160 | v = d.WANIPConn1.GetGenericPortMappingEntry(NewPortMappingIndex=i) 161 | line = '[{NewPortMappingDescription}] {NewProtocol}://{NewInternalClient}:{NewInternalPort} <-> {NewExternalPort}'.format_map( 162 | v) 163 | _w1.Scrolledlistbox_portmaps.insert(END, line) 164 | except upnpclient.soap.SOAPError as e: 165 | pass 166 | 167 | 168 | if __name__ == '__main__': 169 | import smalltool1 170 | 171 | logging.basicConfig(level=logging.DEBUG, datefmt='%Y-%m-%d %H:%M:%S', 172 | format='%(asctime)s [%(levelname)s]:%(filename)s %(message)s', ) 173 | smalltool1.start_up() 174 | --------------------------------------------------------------------------------