first commit

This commit is contained in:
zj
2026-02-04 21:16:33 +08:00
commit 1104e7dc13
36 changed files with 18789 additions and 0 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -0,0 +1,133 @@
('E:\\pythonProject\\TCPUDP\\build\\网络设备控制器\\PYZ-00.pyz',
[('__future__', 'e:\\python38\\lib\\__future__.py', 'PYMODULE'),
('_compat_pickle', 'e:\\python38\\lib\\_compat_pickle.py', 'PYMODULE'),
('_compression', 'e:\\python38\\lib\\_compression.py', 'PYMODULE'),
('_py_abc', 'e:\\python38\\lib\\_py_abc.py', 'PYMODULE'),
('_strptime', 'e:\\python38\\lib\\_strptime.py', 'PYMODULE'),
('_threading_local', 'e:\\python38\\lib\\_threading_local.py', 'PYMODULE'),
('argparse', 'e:\\python38\\lib\\argparse.py', 'PYMODULE'),
('base64', 'e:\\python38\\lib\\base64.py', 'PYMODULE'),
('bisect', 'e:\\python38\\lib\\bisect.py', 'PYMODULE'),
('bz2', 'e:\\python38\\lib\\bz2.py', 'PYMODULE'),
('calendar', 'e:\\python38\\lib\\calendar.py', 'PYMODULE'),
('configparser', 'e:\\python38\\lib\\configparser.py', 'PYMODULE'),
('contextlib', 'e:\\python38\\lib\\contextlib.py', 'PYMODULE'),
('copy', 'e:\\python38\\lib\\copy.py', 'PYMODULE'),
('csv', 'e:\\python38\\lib\\csv.py', 'PYMODULE'),
('ctypes', 'e:\\python38\\lib\\ctypes\\__init__.py', 'PYMODULE'),
('ctypes._aix', 'e:\\python38\\lib\\ctypes\\_aix.py', 'PYMODULE'),
('ctypes._endian', 'e:\\python38\\lib\\ctypes\\_endian.py', 'PYMODULE'),
('ctypes.macholib',
'e:\\python38\\lib\\ctypes\\macholib\\__init__.py',
'PYMODULE'),
('ctypes.macholib.dyld',
'e:\\python38\\lib\\ctypes\\macholib\\dyld.py',
'PYMODULE'),
('ctypes.macholib.dylib',
'e:\\python38\\lib\\ctypes\\macholib\\dylib.py',
'PYMODULE'),
('ctypes.macholib.framework',
'e:\\python38\\lib\\ctypes\\macholib\\framework.py',
'PYMODULE'),
('ctypes.util', 'e:\\python38\\lib\\ctypes\\util.py', 'PYMODULE'),
('datetime', 'e:\\python38\\lib\\datetime.py', 'PYMODULE'),
('email', 'e:\\python38\\lib\\email\\__init__.py', 'PYMODULE'),
('email._encoded_words',
'e:\\python38\\lib\\email\\_encoded_words.py',
'PYMODULE'),
('email._header_value_parser',
'e:\\python38\\lib\\email\\_header_value_parser.py',
'PYMODULE'),
('email._parseaddr', 'e:\\python38\\lib\\email\\_parseaddr.py', 'PYMODULE'),
('email._policybase', 'e:\\python38\\lib\\email\\_policybase.py', 'PYMODULE'),
('email.base64mime', 'e:\\python38\\lib\\email\\base64mime.py', 'PYMODULE'),
('email.charset', 'e:\\python38\\lib\\email\\charset.py', 'PYMODULE'),
('email.contentmanager',
'e:\\python38\\lib\\email\\contentmanager.py',
'PYMODULE'),
('email.encoders', 'e:\\python38\\lib\\email\\encoders.py', 'PYMODULE'),
('email.errors', 'e:\\python38\\lib\\email\\errors.py', 'PYMODULE'),
('email.feedparser', 'e:\\python38\\lib\\email\\feedparser.py', 'PYMODULE'),
('email.generator', 'e:\\python38\\lib\\email\\generator.py', 'PYMODULE'),
('email.header', 'e:\\python38\\lib\\email\\header.py', 'PYMODULE'),
('email.headerregistry',
'e:\\python38\\lib\\email\\headerregistry.py',
'PYMODULE'),
('email.iterators', 'e:\\python38\\lib\\email\\iterators.py', 'PYMODULE'),
('email.message', 'e:\\python38\\lib\\email\\message.py', 'PYMODULE'),
('email.parser', 'e:\\python38\\lib\\email\\parser.py', 'PYMODULE'),
('email.policy', 'e:\\python38\\lib\\email\\policy.py', 'PYMODULE'),
('email.quoprimime', 'e:\\python38\\lib\\email\\quoprimime.py', 'PYMODULE'),
('email.utils', 'e:\\python38\\lib\\email\\utils.py', 'PYMODULE'),
('fnmatch', 'e:\\python38\\lib\\fnmatch.py', 'PYMODULE'),
('getopt', 'e:\\python38\\lib\\getopt.py', 'PYMODULE'),
('gettext', 'e:\\python38\\lib\\gettext.py', 'PYMODULE'),
('gzip', 'e:\\python38\\lib\\gzip.py', 'PYMODULE'),
('hashlib', 'e:\\python38\\lib\\hashlib.py', 'PYMODULE'),
('importlib', 'e:\\python38\\lib\\importlib\\__init__.py', 'PYMODULE'),
('importlib._bootstrap',
'e:\\python38\\lib\\importlib\\_bootstrap.py',
'PYMODULE'),
('importlib._bootstrap_external',
'e:\\python38\\lib\\importlib\\_bootstrap_external.py',
'PYMODULE'),
('importlib.abc', 'e:\\python38\\lib\\importlib\\abc.py', 'PYMODULE'),
('importlib.machinery',
'e:\\python38\\lib\\importlib\\machinery.py',
'PYMODULE'),
('importlib.metadata',
'e:\\python38\\lib\\importlib\\metadata.py',
'PYMODULE'),
('importlib.util', 'e:\\python38\\lib\\importlib\\util.py', 'PYMODULE'),
('json', 'e:\\python38\\lib\\json\\__init__.py', 'PYMODULE'),
('json.decoder', 'e:\\python38\\lib\\json\\decoder.py', 'PYMODULE'),
('json.encoder', 'e:\\python38\\lib\\json\\encoder.py', 'PYMODULE'),
('json.scanner', 'e:\\python38\\lib\\json\\scanner.py', 'PYMODULE'),
('logging', 'e:\\python38\\lib\\logging\\__init__.py', 'PYMODULE'),
('lzma', 'e:\\python38\\lib\\lzma.py', 'PYMODULE'),
('netbios',
'e:\\python38\\lib\\site-packages\\win32\\lib\\netbios.py',
'PYMODULE'),
('network_manager',
'E:\\pythonProject\\TCPUDP\\network_manager.py',
'PYMODULE'),
('optparse', 'e:\\python38\\lib\\optparse.py', 'PYMODULE'),
('pathlib', 'e:\\python38\\lib\\pathlib.py', 'PYMODULE'),
('pickle', 'e:\\python38\\lib\\pickle.py', 'PYMODULE'),
('platform', 'e:\\python38\\lib\\platform.py', 'PYMODULE'),
('pprint', 'e:\\python38\\lib\\pprint.py', 'PYMODULE'),
('py_compile', 'e:\\python38\\lib\\py_compile.py', 'PYMODULE'),
('quopri', 'e:\\python38\\lib\\quopri.py', 'PYMODULE'),
('random', 'e:\\python38\\lib\\random.py', 'PYMODULE'),
('scheduler_app', 'E:\\pythonProject\\TCPUDP\\scheduler_app.py', 'PYMODULE'),
('selectors', 'e:\\python38\\lib\\selectors.py', 'PYMODULE'),
('settings_app', 'E:\\pythonProject\\TCPUDP\\settings_app.py', 'PYMODULE'),
('shutil', 'e:\\python38\\lib\\shutil.py', 'PYMODULE'),
('signal', 'e:\\python38\\lib\\signal.py', 'PYMODULE'),
('socket', 'e:\\python38\\lib\\socket.py', 'PYMODULE'),
('string', 'e:\\python38\\lib\\string.py', 'PYMODULE'),
('stringprep', 'e:\\python38\\lib\\stringprep.py', 'PYMODULE'),
('subprocess', 'e:\\python38\\lib\\subprocess.py', 'PYMODULE'),
('tarfile', 'e:\\python38\\lib\\tarfile.py', 'PYMODULE'),
('tempfile', 'e:\\python38\\lib\\tempfile.py', 'PYMODULE'),
('textwrap', 'e:\\python38\\lib\\textwrap.py', 'PYMODULE'),
('threading', 'e:\\python38\\lib\\threading.py', 'PYMODULE'),
('tkinter', 'e:\\python38\\lib\\tkinter\\__init__.py', 'PYMODULE'),
('tkinter.commondialog',
'e:\\python38\\lib\\tkinter\\commondialog.py',
'PYMODULE'),
('tkinter.constants', 'e:\\python38\\lib\\tkinter\\constants.py', 'PYMODULE'),
('tkinter.messagebox',
'e:\\python38\\lib\\tkinter\\messagebox.py',
'PYMODULE'),
('tkinter.ttk', 'e:\\python38\\lib\\tkinter\\ttk.py', 'PYMODULE'),
('token', 'e:\\python38\\lib\\token.py', 'PYMODULE'),
('tokenize', 'e:\\python38\\lib\\tokenize.py', 'PYMODULE'),
('tracemalloc', 'e:\\python38\\lib\\tracemalloc.py', 'PYMODULE'),
('typing', 'e:\\python38\\lib\\typing.py', 'PYMODULE'),
('urllib', 'e:\\python38\\lib\\urllib\\__init__.py', 'PYMODULE'),
('urllib.parse', 'e:\\python38\\lib\\urllib\\parse.py', 'PYMODULE'),
('utils', 'E:\\pythonProject\\TCPUDP\\utils.py', 'PYMODULE'),
('uu', 'e:\\python38\\lib\\uu.py', 'PYMODULE'),
('uuid', 'e:\\python38\\lib\\uuid.py', 'PYMODULE'),
('zipfile', 'e:\\python38\\lib\\zipfile.py', 'PYMODULE')])

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,30 @@
This file lists modules PyInstaller was not able to find. This does not
necessarily mean this module is required for running your program. Python and
Python 3rd-party packages include a lot of conditional or optional modules. For
example the module 'ntpath' only exists on Windows, whereas the module
'posixpath' only exists on Posix systems.
Types if import:
* top-level: imported at the top-level - look at these first
* conditional: imported within an if-statement
* delayed: imported within a function
* optional: imported within a try-except-statement
IMPORTANT: Do NOT post this list to the issue-tracker. Use it as a basis for
tracking down the missing module yourself. Thanks!
missing module named org - imported by copy (optional)
missing module named pwd - imported by posixpath (delayed, conditional), shutil (optional), tarfile (optional), pathlib (delayed, conditional, optional)
missing module named 'org.python' - imported by pickle (optional)
missing module named _frozen_importlib_external - imported by importlib._bootstrap (delayed), importlib (optional), importlib.abc (optional)
excluded module named _frozen_importlib - imported by importlib (optional), importlib.abc (optional)
missing module named posix - imported by os (conditional, optional), shutil (conditional)
missing module named resource - imported by posix (top-level)
missing module named grp - imported by shutil (optional), tarfile (optional), pathlib (delayed)
missing module named _uuid - imported by uuid (optional)
missing module named _posixsubprocess - imported by subprocess (optional)
missing module named vms_lib - imported by platform (delayed, conditional, optional)
missing module named 'java.lang' - imported by platform (delayed, optional)
missing module named java - imported by platform (delayed)
missing module named _winreg - imported by platform (delayed, optional)

File diff suppressed because it is too large Load Diff

Binary file not shown.

242
config.json Normal file
View File

@@ -0,0 +1,242 @@
{
"devices": [
{
"id": "85ce7dab-22a3-4d39-b783-268eb037cd88",
"name": "test1tcp",
"type": "test",
"protocol": "TCP",
"address": "127.0.0.1",
"port": 19987,
"keep_alive": true,
"commands": [
{
"name": "on1",
"data": "on1",
"type": "text"
},
{
"name": "on2",
"data": "00100000080011",
"type": "hex"
},
{
"name": "2",
"data": "2",
"type": "text"
},
{
"name": "3",
"data": "3",
"type": "text"
},
{
"name": "4",
"data": "4",
"type": "text"
},
{
"name": "5",
"data": "5",
"type": "text"
},
{
"name": "6",
"data": "6",
"type": "text"
},
{
"name": "7",
"data": "7",
"type": "text"
},
{
"name": "8",
"data": "8",
"type": "text"
},
{
"name": "111",
"data": "111",
"type": "text"
},
{
"name": "1111",
"data": "1111",
"type": "text"
},
{
"name": "11111",
"data": "11111",
"type": "text"
},
{
"name": "22",
"data": "22",
"type": "text"
},
{
"name": "222",
"data": "222",
"type": "text"
},
{
"name": "333",
"data": "333",
"type": "text"
},
{
"name": "444",
"data": "444",
"type": "text"
}
],
"timeout": 5
},
{
"id": "e1e6e141-0d41-4251-8d63-786f3485061a",
"name": "zm",
"type": "zm",
"protocol": "UDP",
"address": "127.0.0.1",
"port": 9128,
"keep_alive": false,
"commands": [
{
"name": "on1",
"data": "setr=1111",
"type": "text"
}
],
"timeout": 5
},
{
"id": "77c8ea68-f3c0-4e71-8922-0f23909ee8f9",
"name": "test2",
"type": "test2",
"protocol": "TCP",
"address": "1.1.1.1",
"port": 2,
"keep_alive": false,
"commands": [
{
"name": "1",
"data": "1",
"type": "text"
}
],
"timeout": 5
},
{
"id": "51f60dc8-5cbd-41d2-8233-077d206c77b5",
"name": "test3",
"type": "test",
"protocol": "TCP",
"address": "1.1.1.1",
"port": 3,
"keep_alive": false,
"commands": [
{
"name": "2",
"data": "2",
"type": "text"
}
],
"timeout": 5
},
{
"id": "441b23c7-cd24-45f0-914c-8c65e076b056",
"name": "test4",
"type": "test",
"protocol": "TCP",
"address": "1.1.1.1",
"port": 4,
"keep_alive": false,
"commands": [
{
"name": "4",
"data": "4",
"type": "text"
}
],
"timeout": 5
}
],
"scheduled_tasks": [
{
"id": "a07f035f-dc44-4fca-be9a-6f9399b526f1",
"name": "test1",
"schedule_type": "once",
"trigger": {
"hour": 1,
"minute": 41,
"second": 0,
"year": 2026,
"month": 1,
"day": 28
},
"actions": [
{
"device_id": "85ce7dab-22a3-4d39-b783-268eb037cd88",
"command_index": 0,
"delay_ms": 0
}
],
"enabled": true,
"last_run_time": "2026-01-28T01:41:00.285071",
"next_run_time": null
},
{
"id": "6d19bd9a-1815-485b-96ee-0c722f26a0be",
"name": "新任务",
"schedule_type": "once",
"trigger": {
"hour": 4,
"minute": 31,
"second": 0,
"year": 2026,
"month": 1,
"day": 28
},
"actions": [
{
"device_id": "85ce7dab-22a3-4d39-b783-268eb037cd88",
"command_index": 0,
"delay_ms": 0
},
{
"device_id": "85ce7dab-22a3-4d39-b783-268eb037cd88",
"command_index": 1,
"delay_ms": 500
},
{
"device_id": "85ce7dab-22a3-4d39-b783-268eb037cd88",
"command_index": 2,
"delay_ms": 500
},
{
"device_id": "85ce7dab-22a3-4d39-b783-268eb037cd88",
"command_index": 3,
"delay_ms": 500
},
{
"device_id": "85ce7dab-22a3-4d39-b783-268eb037cd88",
"command_index": 4,
"delay_ms": 500
},
{
"device_id": "85ce7dab-22a3-4d39-b783-268eb037cd88",
"command_index": 5,
"delay_ms": 500
},
{
"device_id": "85ce7dab-22a3-4d39-b783-268eb037cd88",
"command_index": 6,
"delay_ms": 500
}
],
"enabled": true,
"last_run_time": "2026-01-28T04:31:00.158597",
"next_run_time": null
}
]
}

4
dist/config.json vendored Normal file
View File

@@ -0,0 +1,4 @@
{
"devices": [],
"scheduled_tasks": []
}

9
dist/logs/app_log.log vendored Normal file
View File

@@ -0,0 +1,9 @@
2026-01-28 04:39:25,911 - INFO - 日志系统已初始化。
2026-01-28 04:39:25,948 - WARNING - 未找到 config.json将创建新配置。
2026-01-28 04:39:25,949 - INFO - 配置保存到 config.json 成功。
2026-01-28 04:39:36,880 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 04:39:36,880 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 04:39:36,882 - INFO - 配置保存到 config.json 成功。
2026-01-28 04:39:36,895 - INFO - 配置保存到 config.json 成功。
2026-01-28 04:39:42,568 - INFO - 定时任务管理窗口已关闭。
2026-01-28 04:39:42,569 - INFO - 定时任务管理窗口已关闭。

BIN
dist/网络设备控制器.exe vendored Normal file

Binary file not shown.

808
logs/app_log.log Normal file
View File

@@ -0,0 +1,808 @@
2026-01-27 22:50:10,759 - INFO - 日志系统已初始化。
2026-01-27 22:50:10,788 - INFO - 从 config.json 加载配置成功。
2026-01-27 22:50:10,790 - INFO - 配置保存到 config.json 成功。
2026-01-27 22:50:25,319 - INFO - 配置保存到 config.json 成功。
2026-01-27 22:50:25,319 - INFO - 配置保存到 config.json 成功。
2026-01-27 22:50:25,320 - INFO - 定时任务 'test1' (ID: a07f035f...) 已保存。
2026-01-27 22:50:26,688 - INFO - 配置保存到 config.json 成功。
2026-01-27 22:50:26,689 - INFO - 配置保存到 config.json 成功。
2026-01-27 22:51:00,310 - INFO - !!! 任务 'test1' 触发执行 !!!
2026-01-27 22:51:00,310 - INFO - 调度器: 任务 'test1' 到期,准备执行。
2026-01-27 22:51:00,310 - INFO - 调度器: 任务 'test1' 到期,准备执行。
2026-01-27 22:51:00,312 - INFO - 配置保存到 config.json 成功。
2026-01-27 22:51:00,361 - INFO - 定时任务 'test1':开始执行指令序列...
2026-01-27 22:51:00,361 - INFO - 定时任务 'test1':开始执行指令序列...
2026-01-27 22:51:00,361 - INFO - 定时任务 'test1':执行指令 'on1' 到设备 'zm'...
2026-01-27 22:51:00,365 - INFO - 定时任务 'test1':执行指令 'on1' 到设备 'zm'...
2026-01-27 22:51:00,365 - INFO - [调度任务] UDP指令 'on1' 已发送到设备 'zm'。
2026-01-27 22:51:00,365 - INFO - 定时任务 'test1':指令 'on1' 成功,响应: UDP command sent (no response expected)
2026-01-27 22:51:00,366 - INFO - 定时任务 'test1':指令 'on1' 成功,响应: UDP command sent (no response expected)
2026-01-27 22:51:00,366 - INFO - 定时任务 'test1':执行指令 'on1' 到设备 'acs'...
2026-01-27 22:51:00,367 - INFO - 定时任务 'test1':执行指令 'on1' 到设备 'acs'...
2026-01-27 22:51:05,974 - ERROR - [调度任务] 与设备 'acs' (192.168.8.16:502) 通信超时。
2026-01-27 22:51:05,976 - ERROR - 定时任务 'test1':指令 'on1' 执行失败: 与设备 'acs' (192.168.8.16:502) 通信超时。
2026-01-27 22:51:05,977 - ERROR - 定时任务 'test1':指令 'on1' 执行失败: 与设备 'acs' (192.168.8.16:502) 通信超时。
2026-01-27 22:51:05,978 - INFO - 定时任务 'test1':指令序列执行完毕。
2026-01-27 22:51:05,979 - INFO - 定时任务 'test1':指令序列执行完毕。
2026-01-27 22:51:18,157 - WARNING - 准备发送指令 'on1' 到设备 'zm'...
2026-01-27 22:51:18,157 - INFO - 准备发送指令 'on1' 到设备 'zm'...
2026-01-27 22:51:18,158 - INFO - UDP指令 'on1' 已发送到设备 'zm'。
2026-01-27 22:51:22,074 - WARNING - 准备发送指令 'on1' 到设备 'acs'...
2026-01-27 22:51:22,074 - INFO - 准备发送指令 'on1' 到设备 'acs'...
2026-01-27 22:51:35,427 - INFO - 配置保存到 config.json 成功。
2026-01-27 22:51:35,428 - INFO - 配置保存到 config.json 成功。
2026-01-27 22:51:36,030 - ERROR - 与设备 'acs' (192.168.8.16:502) 通信超时。
2026-01-27 22:51:43,153 - INFO - 配置保存到 config.json 成功。
2026-01-27 22:51:43,154 - INFO - 配置保存到 config.json 成功。
2026-01-27 22:51:49,671 - INFO - 配置保存到 config.json 成功。
2026-01-27 22:51:49,672 - INFO - 配置保存到 config.json 成功。
2026-01-28 00:54:03,131 - INFO - 日志系统已初始化。
2026-01-28 00:54:03,194 - INFO - 从 config.json 加载配置成功。
2026-01-28 00:54:03,196 - INFO - 配置保存到 config.json 成功。
2026-01-28 00:54:13,480 - INFO - 配置保存到 config.json 成功。
2026-01-28 00:54:13,481 - INFO - 配置保存到 config.json 成功。
2026-01-28 00:55:59,413 - INFO - 日志系统已初始化。
2026-01-28 00:55:59,456 - INFO - 从 config.json 加载配置成功。
2026-01-28 00:55:59,457 - INFO - 配置保存到 config.json 成功。
2026-01-28 00:56:58,630 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 00:56:58,630 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 00:56:58,631 - INFO - 配置保存到 config.json 成功。
2026-01-28 00:56:58,679 - INFO - 配置保存到 config.json 成功。
2026-01-28 00:57:33,175 - INFO - 配置保存到 config.json 成功。
2026-01-28 00:57:33,175 - INFO - 配置保存到 config.json 成功。
2026-01-28 00:57:33,176 - INFO - 定时任务 'test1' (ID: a07f035f...) 已保存。
2026-01-28 00:57:38,937 - INFO - 配置保存到 config.json 成功。
2026-01-28 00:57:38,938 - INFO - 配置保存到 config.json 成功。
2026-01-28 00:58:00,210 - INFO - !!! 任务 'test1' 触发执行 !!!
2026-01-28 00:58:00,211 - INFO - 调度器: 任务 'test1' 到期,准备执行。
2026-01-28 00:58:00,211 - INFO - 调度器: 任务 'test1' 到期,准备执行。
2026-01-28 00:58:00,218 - INFO - 配置保存到 config.json 成功。
2026-01-28 00:58:00,261 - INFO - 定时任务 'test1':开始执行指令序列...
2026-01-28 00:58:00,262 - INFO - 定时任务 'test1':开始执行指令序列...
2026-01-28 00:58:00,262 - INFO - 定时任务 'test1':执行指令 'on1' 到设备 'zm'...
2026-01-28 00:58:00,266 - INFO - 定时任务 'test1':执行指令 'on1' 到设备 'zm'...
2026-01-28 00:58:00,266 - INFO - [调度任务] UDP指令 'on1' 已发送到设备 'zm'。
2026-01-28 00:58:00,267 - INFO - 定时任务 'test1':指令 'on1' 成功,响应: UDP command sent (no response expected)
2026-01-28 00:58:00,270 - INFO - 定时任务 'test1':指令 'on1' 成功,响应: UDP command sent (no response expected)
2026-01-28 00:58:00,270 - INFO - 定时任务 'test1':执行指令 'on1' 到设备 'acs'...
2026-01-28 00:58:00,273 - INFO - 定时任务 'test1':执行指令 'on1' 到设备 'acs'...
2026-01-28 00:58:09,923 - ERROR - [调度任务] 与设备 'acs' (192.168.8.16:502) 通信超时。
2026-01-28 00:58:09,924 - ERROR - 定时任务 'test1':指令 'on1' 执行失败: 与设备 'acs' (192.168.8.16:502) 通信超时。
2026-01-28 00:58:09,928 - ERROR - 定时任务 'test1':指令 'on1' 执行失败: 与设备 'acs' (192.168.8.16:502) 通信超时。
2026-01-28 00:58:09,928 - INFO - 定时任务 'test1':指令之间延迟 200 毫秒。
2026-01-28 00:58:09,931 - INFO - 定时任务 'test1':指令之间延迟 200 毫秒。
2026-01-28 00:58:10,132 - INFO - 定时任务 'test1':执行指令 'on1' 到设备 'zm'...
2026-01-28 00:58:10,132 - INFO - 定时任务 'test1':执行指令 'on1' 到设备 'zm'...
2026-01-28 00:58:10,132 - INFO - [调度任务] UDP指令 'on1' 已发送到设备 'zm'。
2026-01-28 00:58:10,134 - INFO - 定时任务 'test1':指令 'on1' 成功,响应: UDP command sent (no response expected)
2026-01-28 00:58:10,136 - INFO - 定时任务 'test1':指令 'on1' 成功,响应: UDP command sent (no response expected)
2026-01-28 00:58:10,136 - INFO - 定时任务 'test1':指令序列执行完毕。
2026-01-28 00:58:10,138 - INFO - 定时任务 'test1':指令序列执行完毕。
2026-01-28 00:58:52,370 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 00:58:52,370 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 00:58:52,371 - INFO - 配置保存到 config.json 成功。
2026-01-28 00:58:52,422 - INFO - 配置保存到 config.json 成功。
2026-01-28 00:59:00,844 - INFO - 配置保存到 config.json 成功。
2026-01-28 00:59:00,844 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:01:14,994 - INFO - 日志系统已初始化。
2026-01-28 01:01:15,026 - INFO - 从 config.json 加载配置成功。
2026-01-28 01:01:15,028 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:02:51,577 - INFO - 新设备 'test1tcp' (ID: 85ce7dab...) 已添加。
2026-01-28 01:02:51,577 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:02:51,577 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:02:51,578 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:02:51,644 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:03:00,060 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:03:00,060 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:03:00,061 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:03:00,130 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:03:02,981 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:03:02,981 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:03:02,982 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:03:03,050 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:03:06,959 - WARNING - 准备发送指令 'on1' 到设备 'test1tcp'...
2026-01-28 01:03:06,959 - INFO - 准备发送指令 'on1' 到设备 'test1tcp'...
2026-01-28 01:03:15,362 - ERROR - 发送指令 'on1' 到设备 'test1tcp' 时发生未知错误: 'ip'
2026-01-28 01:04:47,076 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:04:47,076 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:04:47,077 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:04:47,153 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:06:50,403 - INFO - 日志系统已初始化。
2026-01-28 01:06:50,437 - INFO - 从 config.json 加载配置成功。
2026-01-28 01:06:50,438 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:08:01,020 - WARNING - 准备发送指令 'on1' 到设备 'test1tcp'...
2026-01-28 01:08:01,020 - INFO - 准备发送指令 'on1' 到设备 'test1tcp'...
2026-01-28 01:08:05,695 - ERROR - 发送指令 'on1' 到设备 'test1tcp' 时发生未知错误: 'ip'
2026-01-28 01:09:39,366 - INFO - 日志系统已初始化。
2026-01-28 01:09:39,409 - INFO - 从 config.json 加载配置成功。
2026-01-28 01:09:39,410 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:10:26,526 - WARNING - 准备发送指令 'on1' 到设备 'test1tcp'...
2026-01-28 01:10:26,526 - INFO - 准备发送指令 'on1' 到设备 'test1tcp'...
2026-01-28 01:10:26,748 - INFO - 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 'on1': b'on1'
2026-01-28 01:10:26,749 - INFO - 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: 'on1'
2026-01-28 01:10:36,102 - WARNING - 准备发送指令 'on2' 到设备 'test1tcp'...
2026-01-28 01:10:36,102 - INFO - 准备发送指令 'on2' 到设备 'test1tcp'...
2026-01-28 01:10:36,758 - INFO - 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 'on2': b'\x00\x10\x00\x00\x08\x00\x11'
2026-01-28 01:10:36,759 - INFO - 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: '\x00\x10\x00\x00\x08\x00\x11'
2026-01-28 01:11:51,650 - INFO - 新设备 't2' (ID: 84e0f5b3...) 已添加。
2026-01-28 01:11:51,650 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:11:51,650 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:11:51,651 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:11:51,737 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:11:56,557 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:11:56,557 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:11:56,558 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:11:56,649 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:11:58,729 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:11:58,730 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:11:58,730 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:11:58,833 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:12:23,924 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:12:23,924 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:12:23,926 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:12:24,012 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:12:31,047 - INFO - 已关闭设备 84e0f5b3-15a2-4daa-835f-db7e0d524245 的保持连接。
2026-01-28 01:12:31,047 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:12:31,047 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:12:31,048 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:12:31,121 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:12:32,521 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:12:32,521 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:12:32,522 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:12:32,596 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:12:56,121 - WARNING - 准备发送指令 'on1' 到设备 'test1tcp'...
2026-01-28 01:12:56,121 - INFO - 准备发送指令 'on1' 到设备 'test1tcp'...
2026-01-28 01:12:56,122 - INFO - 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 'on1': b'on1'
2026-01-28 01:12:56,123 - INFO - 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: 'on1'
2026-01-28 01:13:09,272 - INFO - 设备 'test1tcp' (ID: 85ce7dab...) 已更新。
2026-01-28 01:13:09,272 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:13:09,272 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:13:09,273 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:13:09,346 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:13:16,665 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:13:16,665 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:13:16,666 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:13:16,737 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:13:50,667 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:13:50,668 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:13:50,668 - INFO - 定时任务 'test1' (ID: a07f035f...) 已保存。
2026-01-28 01:13:51,807 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:13:51,808 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:14:00,068 - INFO - !!! 任务 'test1' 触发执行 !!!
2026-01-28 01:14:00,068 - INFO - 调度器: 任务 'test1' 到期,准备执行。
2026-01-28 01:14:00,068 - INFO - 调度器: 任务 'test1' 到期,准备执行。
2026-01-28 01:14:00,074 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:14:00,119 - INFO - 定时任务 'test1':开始执行指令序列...
2026-01-28 01:14:00,119 - INFO - 定时任务 'test1':开始执行指令序列...
2026-01-28 01:14:00,120 - INFO - 定时任务 'test1':执行指令 'on1' 到设备 'test1tcp'...
2026-01-28 01:14:00,124 - INFO - 定时任务 'test1':执行指令 'on1' 到设备 'test1tcp'...
2026-01-28 01:14:00,125 - INFO - [调度任务] 成功连接到设备 'test1tcp' (127.0.0.1:19987)。
2026-01-28 01:14:00,129 - INFO - [调度任务] 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 'on1': b'on1'
2026-01-28 01:14:00,131 - INFO - [调度任务] 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: 'on1'
2026-01-28 01:14:00,133 - INFO - [调度任务] 已关闭与设备 'test1tcp' (127.0.0.1:19987) 的非保持连接。
2026-01-28 01:14:00,134 - INFO - 定时任务 'test1':指令 'on1' 成功,响应: on1
2026-01-28 01:14:00,137 - INFO - 定时任务 'test1':指令 'on1' 成功,响应: on1
2026-01-28 01:14:00,137 - INFO - 定时任务 'test1':指令序列执行完毕。
2026-01-28 01:14:00,139 - INFO - 定时任务 'test1':指令序列执行完毕。
2026-01-28 01:14:22,822 - INFO - 设备 'test1tcp' (ID: 85ce7dab...) 已更新。
2026-01-28 01:14:22,822 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:14:22,822 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:14:22,823 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:14:22,993 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:14:25,137 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:14:25,137 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:14:25,138 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:14:25,212 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:14:30,617 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:14:30,618 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:14:30,618 - INFO - 定时任务 'test1' (ID: a07f035f...) 已保存。
2026-01-28 01:14:34,235 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:14:34,236 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:16:00,076 - INFO - !!! 任务 'test1' 触发执行 !!!
2026-01-28 01:16:00,076 - INFO - 调度器: 任务 'test1' 到期,准备执行。
2026-01-28 01:16:00,076 - INFO - 调度器: 任务 'test1' 到期,准备执行。
2026-01-28 01:16:00,079 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:16:00,127 - INFO - 定时任务 'test1':开始执行指令序列...
2026-01-28 01:16:00,127 - INFO - 定时任务 'test1':开始执行指令序列...
2026-01-28 01:16:00,127 - INFO - 定时任务 'test1':执行指令 'on1' 到设备 'test1tcp'...
2026-01-28 01:16:00,129 - INFO - 定时任务 'test1':执行指令 'on1' 到设备 'test1tcp'...
2026-01-28 01:16:00,129 - INFO - [调度任务] 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 'on1': b'on1'
2026-01-28 01:16:00,130 - INFO - [调度任务] 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: 'on1'
2026-01-28 01:16:00,132 - INFO - 定时任务 'test1':指令 'on1' 成功,响应: on1
2026-01-28 01:16:00,136 - INFO - 定时任务 'test1':指令 'on1' 成功,响应: on1
2026-01-28 01:16:00,136 - INFO - 定时任务 'test1':指令序列执行完毕。
2026-01-28 01:16:00,137 - INFO - 定时任务 'test1':指令序列执行完毕。
2026-01-28 01:19:29,403 - INFO - 日志系统已初始化。
2026-01-28 01:19:29,443 - INFO - 从 config.json 加载配置成功。
2026-01-28 01:19:29,444 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:19:47,470 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:19:47,471 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:19:47,471 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:19:47,539 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:19:51,521 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:19:51,521 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:19:51,522 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:19:51,558 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:19:53,829 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:19:53,829 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:19:53,830 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:19:53,858 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:20:17,940 - INFO - 设备 'test1tcp' (ID: 85ce7dab...) 已更新。
2026-01-28 01:20:17,941 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:20:17,941 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:20:17,941 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:20:17,970 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:21:22,620 - INFO - 日志系统已初始化。
2026-01-28 01:21:22,664 - INFO - 从 config.json 加载配置成功。
2026-01-28 01:21:22,665 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:24:18,576 - INFO - 日志系统已初始化。
2026-01-28 01:24:18,621 - INFO - 从 config.json 加载配置成功。
2026-01-28 01:24:18,622 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:24:51,390 - WARNING - 准备发送指令 'on1' 到设备 'test1tcp'...
2026-01-28 01:24:51,390 - INFO - 准备发送指令 'on1' 到设备 'test1tcp'...
2026-01-28 01:24:51,391 - INFO - 成功连接到设备 'test1tcp' (127.0.0.1:19987)。
2026-01-28 01:24:51,394 - INFO - 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 'on1': b'on1'
2026-01-28 01:24:51,394 - INFO - 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: 'on1'
2026-01-28 01:24:51,398 - INFO - 已关闭与设备 'test1tcp' (127.0.0.1:19987) 的非保持连接。
2026-01-28 01:32:08,075 - INFO - 日志系统已初始化。
2026-01-28 01:32:08,120 - INFO - 从 config.json 加载配置成功。
2026-01-28 01:32:08,121 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:38:07,029 - INFO - 日志系统已初始化。
2026-01-28 01:38:07,068 - INFO - 从 config.json 加载配置成功。
2026-01-28 01:38:07,069 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:38:19,674 - WARNING - 准备发送指令 'on1' 到设备 'test1tcp'...
2026-01-28 01:38:19,674 - INFO - 准备发送指令 'on1' 到设备 'test1tcp'...
2026-01-28 01:38:19,677 - INFO - 成功连接到设备 'test1tcp' (127.0.0.1:19987)。
2026-01-28 01:38:19,677 - INFO - 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 'on1': b'on1'
2026-01-28 01:38:19,679 - INFO - 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: 'on1'
2026-01-28 01:38:19,680 - INFO - 已关闭与设备 'test1tcp' (127.0.0.1:19987) 的非保持连接。
2026-01-28 01:38:44,653 - WARNING - 准备发送指令 'on2' 到设备 'test1tcp'...
2026-01-28 01:38:44,653 - INFO - 准备发送指令 'on2' 到设备 'test1tcp'...
2026-01-28 01:38:44,665 - INFO - 成功连接到设备 'test1tcp' (127.0.0.1:19987)。
2026-01-28 01:38:44,666 - INFO - 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 'on2': b'\x00\x10\x00\x00\x08\x00\x11'
2026-01-28 01:38:44,667 - INFO - 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: '\x00\x10\x00\x00\x08\x00\x11'
2026-01-28 01:38:44,668 - INFO - 已关闭与设备 'test1tcp' (127.0.0.1:19987) 的非保持连接。
2026-01-28 01:38:58,771 - WARNING - 准备发送指令 'on2' 到设备 'test1tcp'...
2026-01-28 01:38:58,771 - INFO - 准备发送指令 'on2' 到设备 'test1tcp'...
2026-01-28 01:38:58,772 - INFO - 成功连接到设备 'test1tcp' (127.0.0.1:19987)。
2026-01-28 01:38:58,773 - INFO - 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 'on2': b'\x00\x10\x00\x00\x08\x00\x11'
2026-01-28 01:38:58,773 - INFO - 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: '\x00\x10\x00\x00\x08\x00\x11'
2026-01-28 01:38:58,775 - INFO - 已关闭与设备 'test1tcp' (127.0.0.1:19987) 的非保持连接。
2026-01-28 01:39:01,589 - WARNING - 准备发送指令 'on1' 到设备 'test1tcp'...
2026-01-28 01:39:01,589 - INFO - 准备发送指令 'on1' 到设备 'test1tcp'...
2026-01-28 01:39:01,602 - INFO - 成功连接到设备 'test1tcp' (127.0.0.1:19987)。
2026-01-28 01:39:01,603 - INFO - 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 'on1': b'on1'
2026-01-28 01:39:01,605 - INFO - 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: 'on1'
2026-01-28 01:39:01,606 - INFO - 已关闭与设备 'test1tcp' (127.0.0.1:19987) 的非保持连接。
2026-01-28 01:39:08,462 - WARNING - 准备发送指令 'on2' 到设备 'test1tcp'...
2026-01-28 01:39:08,462 - INFO - 准备发送指令 'on2' 到设备 'test1tcp'...
2026-01-28 01:39:08,464 - INFO - 成功连接到设备 'test1tcp' (127.0.0.1:19987)。
2026-01-28 01:39:08,465 - INFO - 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 'on2': b'\x00\x10\x00\x00\x08\x00\x11'
2026-01-28 01:39:08,466 - INFO - 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: '\x00\x10\x00\x00\x08\x00\x11'
2026-01-28 01:39:08,468 - INFO - 已关闭与设备 'test1tcp' (127.0.0.1:19987) 的非保持连接。
2026-01-28 01:39:27,829 - INFO - 设备 'test1tcp' (ID: 85ce7dab...) 已更新。
2026-01-28 01:39:27,830 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:39:27,830 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:39:27,831 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:39:27,858 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:39:29,487 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:39:29,487 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:39:29,488 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:39:29,520 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:39:45,519 - WARNING - 准备发送指令 'on1' 到设备 'test1tcp'...
2026-01-28 01:39:45,519 - INFO - 准备发送指令 'on1' 到设备 'test1tcp'...
2026-01-28 01:39:45,520 - INFO - 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 'on1': b'on1'
2026-01-28 01:39:45,521 - INFO - 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: 'on1'
2026-01-28 01:39:49,536 - WARNING - 准备发送指令 'on2' 到设备 'test1tcp'...
2026-01-28 01:39:49,536 - INFO - 准备发送指令 'on2' 到设备 'test1tcp'...
2026-01-28 01:39:49,537 - INFO - 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 'on2': b'\x00\x10\x00\x00\x08\x00\x11'
2026-01-28 01:39:49,538 - INFO - 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: '\x00\x10\x00\x00\x08\x00\x11'
2026-01-28 01:39:53,051 - WARNING - 准备发送指令 'on1' 到设备 'test1tcp'...
2026-01-28 01:39:53,051 - INFO - 准备发送指令 'on1' 到设备 'test1tcp'...
2026-01-28 01:39:53,052 - INFO - 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 'on1': b'on1'
2026-01-28 01:39:53,054 - INFO - 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: 'on1'
2026-01-28 01:39:56,339 - WARNING - 准备发送指令 'on2' 到设备 'test1tcp'...
2026-01-28 01:39:56,339 - INFO - 准备发送指令 'on2' 到设备 'test1tcp'...
2026-01-28 01:39:56,340 - INFO - 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 'on2': b'\x00\x10\x00\x00\x08\x00\x11'
2026-01-28 01:39:56,342 - INFO - 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: '\x00\x10\x00\x00\x08\x00\x11'
2026-01-28 01:40:02,296 - WARNING - 准备发送指令 'on1' 到设备 'test1tcp'...
2026-01-28 01:40:02,296 - INFO - 准备发送指令 'on1' 到设备 'test1tcp'...
2026-01-28 01:40:02,297 - INFO - 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 'on1': b'on1'
2026-01-28 01:40:02,299 - INFO - 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: 'on1'
2026-01-28 01:40:18,353 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:40:18,354 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:40:18,354 - INFO - 定时任务 'test1' (ID: a07f035f...) 已保存。
2026-01-28 01:40:19,977 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:40:19,978 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:41:00,285 - INFO - !!! 任务 'test1' 触发执行 !!!
2026-01-28 01:41:00,285 - INFO - 调度器: 任务 'test1' 到期,准备执行。
2026-01-28 01:41:00,285 - INFO - 调度器: 任务 'test1' 到期,准备执行。
2026-01-28 01:41:00,288 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:41:00,335 - INFO - 定时任务 'test1':开始执行指令序列...
2026-01-28 01:41:00,336 - INFO - 定时任务 'test1':开始执行指令序列...
2026-01-28 01:41:00,336 - INFO - 定时任务 'test1':执行指令 'on1' 到设备 'test1tcp'...
2026-01-28 01:41:00,337 - INFO - 定时任务 'test1':执行指令 'on1' 到设备 'test1tcp'...
2026-01-28 01:41:00,338 - INFO - [调度任务] 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 'on1': b'on1'
2026-01-28 01:41:00,339 - INFO - [调度任务] 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: 'on1'
2026-01-28 01:41:00,340 - INFO - 定时任务 'test1':指令 'on1' 成功,响应: on1
2026-01-28 01:41:00,342 - INFO - 定时任务 'test1':指令 'on1' 成功,响应: on1
2026-01-28 01:41:00,342 - INFO - 定时任务 'test1':指令序列执行完毕。
2026-01-28 01:41:00,343 - INFO - 定时任务 'test1':指令序列执行完毕。
2026-01-28 01:42:48,594 - INFO - 新设备 'zm' (ID: e1e6e141...) 已添加。
2026-01-28 01:42:48,594 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:42:48,594 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:42:48,595 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:42:48,638 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:43:56,609 - INFO - 日志系统已初始化。
2026-01-28 01:43:56,653 - INFO - 从 config.json 加载配置成功。
2026-01-28 01:43:56,655 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:44:39,596 - INFO - 新设备 'zm22' (ID: ceded2c1...) 已添加。
2026-01-28 01:44:39,596 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:44:39,596 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:44:39,597 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:44:39,657 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:47:39,380 - INFO - 日志系统已初始化。
2026-01-28 01:47:39,412 - INFO - 从 config.json 加载配置成功。
2026-01-28 01:47:39,414 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:47:53,435 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:47:53,436 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:47:53,436 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:47:53,494 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:48:12,446 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:48:12,446 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:48:12,447 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:48:12,493 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:48:23,925 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:48:23,926 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:48:29,672 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:48:29,673 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:48:44,486 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:48:44,487 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:50:02,800 - INFO - 日志系统已初始化。
2026-01-28 01:50:02,843 - INFO - 从 config.json 加载配置成功。
2026-01-28 01:50:02,844 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:50:20,197 - INFO - 定时任务管理窗口已关闭。
2026-01-28 01:50:20,197 - INFO - 定时任务管理窗口已关闭。
2026-01-28 01:50:36,059 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:50:36,060 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:50:36,060 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:50:36,104 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:50:37,928 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:50:37,928 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:50:37,930 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:50:37,978 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:50:39,993 - INFO - 定时任务管理窗口已关闭。
2026-01-28 01:50:39,993 - INFO - 定时任务管理窗口已关闭。
2026-01-28 01:53:59,197 - INFO - 日志系统已初始化。
2026-01-28 01:53:59,229 - INFO - 从 config.json 加载配置成功。
2026-01-28 01:53:59,230 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:56:21,563 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:56:21,563 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:56:21,564 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:56:21,611 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:56:22,445 - INFO - 已关闭设备 85ce7dab-22a3-4d39-b783-268eb037cd88 的保持连接。
2026-01-28 01:56:31,123 - INFO - 日志系统已初始化。
2026-01-28 01:56:31,197 - INFO - 从 config.json 加载配置成功。
2026-01-28 01:56:31,198 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:56:53,987 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:56:53,987 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:56:53,988 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:56:54,035 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:56:54,992 - INFO - 已关闭设备 85ce7dab-22a3-4d39-b783-268eb037cd88 的保持连接。
2026-01-28 01:57:00,443 - INFO - 日志系统已初始化。
2026-01-28 01:57:00,485 - INFO - 从 config.json 加载配置成功。
2026-01-28 01:57:00,486 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:57:17,451 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:57:17,451 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:57:17,452 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:57:17,500 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:57:18,193 - INFO - 已关闭设备 85ce7dab-22a3-4d39-b783-268eb037cd88 的保持连接。
2026-01-28 01:57:24,086 - INFO - 日志系统已初始化。
2026-01-28 01:57:24,134 - INFO - 从 config.json 加载配置成功。
2026-01-28 01:57:24,135 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:58:02,243 - INFO - 新设备 '1' (ID: f2a8f914...) 已添加。
2026-01-28 01:58:02,243 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:58:02,243 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:58:02,244 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:58:02,305 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:58:20,122 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:58:20,122 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:58:20,123 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:58:20,181 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:58:22,023 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:58:22,023 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 01:58:22,024 - INFO - 配置保存到 config.json 成功。
2026-01-28 01:58:22,071 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:12:29,039 - INFO - 定时任务管理窗口已关闭。
2026-01-28 03:12:29,039 - INFO - 定时任务管理窗口已关闭。
2026-01-28 03:12:33,254 - WARNING - 准备发送指令 'on1' 到设备 'test1tcp'...
2026-01-28 03:12:33,255 - INFO - 准备发送指令 'on1' 到设备 'test1tcp'...
2026-01-28 03:12:33,256 - INFO - 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 'on1': b'on1'
2026-01-28 03:12:33,261 - INFO - 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: 'on1'
2026-01-28 03:12:38,453 - WARNING - 准备发送指令 'on1' 到设备 'test1tcp'...
2026-01-28 03:12:38,453 - INFO - 准备发送指令 'on1' 到设备 'test1tcp'...
2026-01-28 03:12:38,454 - INFO - 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 'on1': b'on1'
2026-01-28 03:12:38,455 - INFO - 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: 'on1'
2026-01-28 03:12:39,604 - WARNING - 准备发送指令 'on1' 到设备 'test1tcp'...
2026-01-28 03:12:39,604 - INFO - 准备发送指令 'on1' 到设备 'test1tcp'...
2026-01-28 03:12:39,605 - INFO - 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 'on1': b'on1'
2026-01-28 03:12:39,607 - INFO - 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: 'on1'
2026-01-28 03:12:40,440 - WARNING - 准备发送指令 'on2' 到设备 'test1tcp'...
2026-01-28 03:12:40,440 - INFO - 准备发送指令 'on2' 到设备 'test1tcp'...
2026-01-28 03:12:40,441 - INFO - 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 'on2': b'\x00\x10\x00\x00\x08\x00\x11'
2026-01-28 03:12:40,442 - INFO - 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: '\x00\x10\x00\x00\x08\x00\x11'
2026-01-28 03:12:41,197 - WARNING - 准备发送指令 'on2' 到设备 'test1tcp'...
2026-01-28 03:12:41,197 - INFO - 准备发送指令 'on2' 到设备 'test1tcp'...
2026-01-28 03:12:41,198 - INFO - 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 'on2': b'\x00\x10\x00\x00\x08\x00\x11'
2026-01-28 03:12:41,200 - INFO - 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: '\x00\x10\x00\x00\x08\x00\x11'
2026-01-28 03:12:41,727 - WARNING - 准备发送指令 'on2' 到设备 'test1tcp'...
2026-01-28 03:12:41,727 - INFO - 准备发送指令 'on2' 到设备 'test1tcp'...
2026-01-28 03:12:41,728 - INFO - 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 'on2': b'\x00\x10\x00\x00\x08\x00\x11'
2026-01-28 03:12:41,730 - INFO - 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: '\x00\x10\x00\x00\x08\x00\x11'
2026-01-28 03:12:44,904 - WARNING - 准备发送指令 'on1' 到设备 'zm'...
2026-01-28 03:12:44,904 - INFO - 准备发送指令 'on1' 到设备 'zm'...
2026-01-28 03:12:44,905 - INFO - UDP指令 'on1' 已发送到设备 'zm'。
2026-01-28 03:12:53,391 - INFO - 设备 'test1tcp' (ID: 85ce7dab...) 已更新。
2026-01-28 03:12:53,391 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 03:12:53,391 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 03:12:53,392 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:12:53,445 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:12:56,446 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 03:12:56,446 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 03:12:56,447 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:12:56,495 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:13:00,950 - WARNING - 准备发送指令 'on1' 到设备 'test1tcp'...
2026-01-28 03:13:00,950 - INFO - 准备发送指令 'on1' 到设备 'test1tcp'...
2026-01-28 03:13:00,952 - INFO - 成功连接到设备 'test1tcp' (127.0.0.1:19987)。
2026-01-28 03:13:00,952 - INFO - 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 'on1': b'on1'
2026-01-28 03:13:00,954 - INFO - 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: 'on1'
2026-01-28 03:13:00,955 - INFO - 已关闭与设备 'test1tcp' (127.0.0.1:19987) 的非保持连接。
2026-01-28 03:13:07,213 - WARNING - 准备发送指令 'on2' 到设备 'test1tcp'...
2026-01-28 03:13:07,213 - INFO - 准备发送指令 'on2' 到设备 'test1tcp'...
2026-01-28 03:13:07,214 - INFO - 成功连接到设备 'test1tcp' (127.0.0.1:19987)。
2026-01-28 03:13:07,216 - INFO - 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 'on2': b'\x00\x10\x00\x00\x08\x00\x11'
2026-01-28 03:13:07,218 - INFO - 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: '\x00\x10\x00\x00\x08\x00\x11'
2026-01-28 03:13:07,219 - INFO - 已关闭与设备 'test1tcp' (127.0.0.1:19987) 的非保持连接。
2026-01-28 03:13:14,030 - INFO - 已关闭设备 85ce7dab-22a3-4d39-b783-268eb037cd88 的保持连接。
2026-01-28 03:13:58,395 - INFO - 日志系统已初始化。
2026-01-28 03:13:58,435 - INFO - 从 config.json 加载配置成功。
2026-01-28 03:13:58,437 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:16:07,419 - INFO - 日志系统已初始化。
2026-01-28 03:16:07,454 - INFO - 从 config.json 加载配置成功。
2026-01-28 03:16:07,455 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:16:13,324 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 03:16:13,324 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 03:16:13,325 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:16:13,372 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:16:36,860 - INFO - 日志系统已初始化。
2026-01-28 03:16:36,904 - INFO - 从 config.json 加载配置成功。
2026-01-28 03:16:36,905 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:17:02,880 - INFO - 新设备 'test2' (ID: 77c8ea68...) 已添加。
2026-01-28 03:17:02,880 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 03:17:02,880 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 03:17:02,881 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:17:02,940 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:17:19,072 - INFO - 新设备 'test3' (ID: 51f60dc8...) 已添加。
2026-01-28 03:17:19,072 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 03:17:19,072 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 03:17:19,073 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:17:19,151 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:17:35,914 - INFO - 新设备 'test4' (ID: 441b23c7...) 已添加。
2026-01-28 03:17:35,914 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 03:17:35,914 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 03:17:35,915 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:17:36,010 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:17:37,981 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 03:17:37,982 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 03:17:37,983 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:17:38,091 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:18:42,786 - INFO - 设备 'test1tcp' (ID: 85ce7dab...) 已更新。
2026-01-28 03:18:42,786 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 03:18:42,786 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 03:18:42,788 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:18:42,906 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:18:44,853 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 03:18:44,853 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 03:18:44,854 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:18:45,027 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:22:11,240 - INFO - 日志系统已初始化。
2026-01-28 03:22:11,286 - INFO - 从 config.json 加载配置成功。
2026-01-28 03:22:11,287 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:36:12,929 - INFO - 日志系统已初始化。
2026-01-28 03:36:12,975 - INFO - 从 config.json 加载配置成功。
2026-01-28 03:36:12,977 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:36:28,734 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 03:36:28,734 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 03:36:28,735 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:36:28,792 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:36:31,496 - INFO - 定时任务管理窗口已关闭。
2026-01-28 03:36:31,496 - INFO - 定时任务管理窗口已关闭。
2026-01-28 03:36:39,764 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:36:39,764 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:37:54,544 - INFO - 日志系统已初始化。
2026-01-28 03:37:54,585 - INFO - 从 config.json 加载配置成功。
2026-01-28 03:37:54,586 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:41:15,328 - INFO - 日志系统已初始化。
2026-01-28 03:41:15,364 - INFO - 从 config.json 加载配置成功。
2026-01-28 03:41:15,366 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:43:08,334 - INFO - 日志系统已初始化。
2026-01-28 03:43:08,380 - INFO - 从 config.json 加载配置成功。
2026-01-28 03:43:08,382 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:45:19,143 - INFO - 日志系统已初始化。
2026-01-28 03:45:19,211 - INFO - 从 config.json 加载配置成功。
2026-01-28 03:45:19,213 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:48:16,772 - INFO - 日志系统已初始化。
2026-01-28 03:48:17,109 - INFO - 从 config.json 加载配置成功。
2026-01-28 03:48:17,112 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:49:00,463 - INFO - 日志系统已初始化。
2026-01-28 03:49:00,777 - INFO - 从 config.json 加载配置成功。
2026-01-28 03:49:00,778 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:49:22,122 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 03:49:22,122 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 03:49:22,123 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:49:22,256 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:51:39,426 - INFO - 日志系统已初始化。
2026-01-28 03:51:39,603 - INFO - 从 config.json 加载配置成功。
2026-01-28 03:51:39,604 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:53:34,599 - INFO - 日志系统已初始化。
2026-01-28 03:53:34,921 - INFO - 从 config.json 加载配置成功。
2026-01-28 03:53:34,923 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:54:32,439 - INFO - 日志系统已初始化。
2026-01-28 03:54:32,641 - INFO - 从 config.json 加载配置成功。
2026-01-28 03:54:32,643 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:56:34,416 - INFO - 日志系统已初始化。
2026-01-28 03:56:54,119 - INFO - 日志系统已初始化。
2026-01-28 03:56:54,266 - INFO - 从 config.json 加载配置成功。
2026-01-28 03:56:54,267 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:58:36,116 - INFO - 日志系统已初始化。
2026-01-28 03:58:36,274 - INFO - 从 config.json 加载配置成功。
2026-01-28 03:58:36,275 - INFO - 配置保存到 config.json 成功。
2026-01-28 03:59:11,587 - INFO - 日志系统已初始化。
2026-01-28 03:59:11,751 - INFO - 从 config.json 加载配置成功。
2026-01-28 03:59:11,752 - INFO - 配置保存到 config.json 成功。
2026-01-28 04:03:14,157 - INFO - 日志系统已初始化。
2026-01-28 04:03:14,379 - INFO - 从 config.json 加载配置成功。
2026-01-28 04:03:14,381 - INFO - 配置保存到 config.json 成功。
2026-01-28 04:04:36,809 - INFO - 日志系统已初始化。
2026-01-28 04:04:36,983 - INFO - 从 config.json 加载配置成功。
2026-01-28 04:04:36,984 - INFO - 配置保存到 config.json 成功。
2026-01-28 04:04:54,393 - INFO - 日志系统已初始化。
2026-01-28 04:04:54,539 - INFO - 从 config.json 加载配置成功。
2026-01-28 04:04:54,540 - INFO - 配置保存到 config.json 成功。
2026-01-28 04:04:58,583 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 04:04:58,583 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 04:04:58,584 - INFO - 配置保存到 config.json 成功。
2026-01-28 04:04:58,885 - INFO - 配置保存到 config.json 成功。
2026-01-28 04:05:07,309 - INFO - 日志系统已初始化。
2026-01-28 04:05:07,604 - INFO - 从 config.json 加载配置成功。
2026-01-28 04:05:07,606 - INFO - 配置保存到 config.json 成功。
2026-01-28 04:08:39,782 - INFO - 日志系统已初始化。
2026-01-28 04:08:39,830 - INFO - 从 config.json 加载配置成功。
2026-01-28 04:08:39,831 - INFO - 配置保存到 config.json 成功。
2026-01-28 04:09:13,554 - WARNING - 准备发送指令 '4' 到设备 'test1tcp'...
2026-01-28 04:09:13,554 - INFO - 准备发送指令 '4' 到设备 'test1tcp'...
2026-01-28 04:09:13,557 - INFO - 成功连接到设备 'test1tcp' (127.0.0.1:19987)。
2026-01-28 04:09:13,561 - INFO - 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 '4': b'4'
2026-01-28 04:09:13,562 - INFO - 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: '4'
2026-01-28 04:09:13,565 - INFO - 已关闭与设备 'test1tcp' (127.0.0.1:19987) 的非保持连接。
2026-01-28 04:09:21,706 - WARNING - 准备发送指令 '7' 到设备 'test1tcp'...
2026-01-28 04:09:21,706 - INFO - 准备发送指令 '7' 到设备 'test1tcp'...
2026-01-28 04:09:21,710 - INFO - 成功连接到设备 'test1tcp' (127.0.0.1:19987)。
2026-01-28 04:09:21,742 - INFO - 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 '7': b'7'
2026-01-28 04:09:21,743 - INFO - 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: '7'
2026-01-28 04:09:21,745 - INFO - 已关闭与设备 'test1tcp' (127.0.0.1:19987) 的非保持连接。
2026-01-28 04:09:26,967 - WARNING - 准备发送指令 '6' 到设备 'test1tcp'...
2026-01-28 04:09:26,967 - INFO - 准备发送指令 '6' 到设备 'test1tcp'...
2026-01-28 04:09:26,984 - INFO - 成功连接到设备 'test1tcp' (127.0.0.1:19987)。
2026-01-28 04:09:26,993 - INFO - 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 '6': b'6'
2026-01-28 04:09:26,997 - INFO - 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: '6'
2026-01-28 04:09:27,002 - INFO - 已关闭与设备 'test1tcp' (127.0.0.1:19987) 的非保持连接。
2026-01-28 04:09:29,234 - WARNING - 准备发送指令 'on2' 到设备 'test1tcp'...
2026-01-28 04:09:29,235 - INFO - 准备发送指令 'on2' 到设备 'test1tcp'...
2026-01-28 04:09:29,257 - INFO - 成功连接到设备 'test1tcp' (127.0.0.1:19987)。
2026-01-28 04:09:29,261 - INFO - 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 'on2': b'\x00\x10\x00\x00\x08\x00\x11'
2026-01-28 04:09:29,263 - INFO - 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: '\x00\x10\x00\x00\x08\x00\x11'
2026-01-28 04:09:29,264 - INFO - 已关闭与设备 'test1tcp' (127.0.0.1:19987) 的非保持连接。
2026-01-28 04:09:31,466 - WARNING - 准备发送指令 'on1' 到设备 'test1tcp'...
2026-01-28 04:09:31,466 - INFO - 准备发送指令 'on1' 到设备 'test1tcp'...
2026-01-28 04:09:31,468 - INFO - 成功连接到设备 'test1tcp' (127.0.0.1:19987)。
2026-01-28 04:09:31,468 - INFO - 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 'on1': b'on1'
2026-01-28 04:09:31,471 - INFO - 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: 'on1'
2026-01-28 04:09:31,473 - INFO - 已关闭与设备 'test1tcp' (127.0.0.1:19987) 的非保持连接。
2026-01-28 04:10:21,226 - INFO - 设备 'test1tcp' (ID: 85ce7dab...) 已更新。
2026-01-28 04:10:21,226 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 04:10:21,226 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 04:10:21,227 - INFO - 配置保存到 config.json 成功。
2026-01-28 04:10:22,085 - INFO - 配置保存到 config.json 成功。
2026-01-28 04:10:26,766 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 04:10:26,766 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 04:10:26,767 - INFO - 配置保存到 config.json 成功。
2026-01-28 04:10:27,281 - INFO - 配置保存到 config.json 成功。
2026-01-28 04:11:00,379 - INFO - 日志系统已初始化。
2026-01-28 04:11:00,432 - INFO - 从 config.json 加载配置成功。
2026-01-28 04:11:00,434 - INFO - 配置保存到 config.json 成功。
2026-01-28 04:12:24,686 - INFO - 日志系统已初始化。
2026-01-28 04:12:24,731 - INFO - 从 config.json 加载配置成功。
2026-01-28 04:12:24,732 - INFO - 配置保存到 config.json 成功。
2026-01-28 04:12:44,474 - INFO - 日志系统已初始化。
2026-01-28 04:12:44,534 - INFO - 从 config.json 加载配置成功。
2026-01-28 04:12:44,536 - INFO - 配置保存到 config.json 成功。
2026-01-28 04:12:56,006 - WARNING - 准备发送指令 '8' 到设备 'test1tcp'...
2026-01-28 04:12:56,006 - INFO - 准备发送指令 '8' 到设备 'test1tcp'...
2026-01-28 04:12:56,025 - INFO - 成功连接到设备 'test1tcp' (127.0.0.1:19987)。
2026-01-28 04:12:56,027 - INFO - 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 '8': b'8'
2026-01-28 04:12:56,029 - INFO - 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: '8'
2026-01-28 04:12:56,032 - INFO - 已关闭与设备 'test1tcp' (127.0.0.1:19987) 的非保持连接。
2026-01-28 04:12:57,535 - WARNING - 准备发送指令 '222' 到设备 'test1tcp'...
2026-01-28 04:12:57,535 - INFO - 准备发送指令 '222' 到设备 'test1tcp'...
2026-01-28 04:12:57,554 - INFO - 成功连接到设备 'test1tcp' (127.0.0.1:19987)。
2026-01-28 04:12:57,555 - INFO - 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 '222': b'222'
2026-01-28 04:12:57,557 - INFO - 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: '222'
2026-01-28 04:12:57,560 - INFO - 已关闭与设备 'test1tcp' (127.0.0.1:19987) 的非保持连接。
2026-01-28 04:13:09,231 - INFO - 设备 'test1tcp' (ID: 85ce7dab...) 已更新。
2026-01-28 04:13:09,231 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 04:13:09,231 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 04:13:09,232 - INFO - 配置保存到 config.json 成功。
2026-01-28 04:13:10,084 - INFO - 配置保存到 config.json 成功。
2026-01-28 04:13:17,468 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 04:13:17,468 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 04:13:17,469 - INFO - 配置保存到 config.json 成功。
2026-01-28 04:13:18,139 - INFO - 配置保存到 config.json 成功。
2026-01-28 04:13:19,614 - WARNING - 准备发送指令 '11111' 到设备 'test1tcp'...
2026-01-28 04:13:19,614 - INFO - 准备发送指令 '11111' 到设备 'test1tcp'...
2026-01-28 04:13:19,615 - INFO - 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 '11111': b'11111'
2026-01-28 04:13:19,616 - INFO - 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: '11111'
2026-01-28 04:13:20,677 - WARNING - 准备发送指令 '6' 到设备 'test1tcp'...
2026-01-28 04:13:20,677 - INFO - 准备发送指令 '6' 到设备 'test1tcp'...
2026-01-28 04:13:20,678 - INFO - 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 '6': b'6'
2026-01-28 04:13:20,679 - INFO - 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: '6'
2026-01-28 04:13:23,533 - WARNING - 准备发送指令 '7' 到设备 'test1tcp'...
2026-01-28 04:13:23,534 - INFO - 准备发送指令 '7' 到设备 'test1tcp'...
2026-01-28 04:13:23,534 - INFO - 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 '7': b'7'
2026-01-28 04:13:23,536 - INFO - 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: '7'
2026-01-28 04:13:24,284 - WARNING - 准备发送指令 '222' 到设备 'test1tcp'...
2026-01-28 04:13:24,285 - INFO - 准备发送指令 '222' 到设备 'test1tcp'...
2026-01-28 04:13:24,285 - INFO - 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 '222': b'222'
2026-01-28 04:13:24,287 - INFO - 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: '222'
2026-01-28 04:13:25,223 - WARNING - 准备发送指令 '3' 到设备 'test1tcp'...
2026-01-28 04:13:25,224 - INFO - 准备发送指令 '3' 到设备 'test1tcp'...
2026-01-28 04:13:25,224 - INFO - 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 '3': b'3'
2026-01-28 04:13:25,239 - INFO - 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: '3'
2026-01-28 04:13:25,706 - WARNING - 准备发送指令 'on2' 到设备 'test1tcp'...
2026-01-28 04:13:25,706 - INFO - 准备发送指令 'on2' 到设备 'test1tcp'...
2026-01-28 04:13:25,706 - INFO - 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 'on2': b'\x00\x10\x00\x00\x08\x00\x11'
2026-01-28 04:13:25,709 - INFO - 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: '\x00\x10\x00\x00\x08\x00\x11'
2026-01-28 04:13:26,392 - WARNING - 准备发送指令 'on1' 到设备 'zm'...
2026-01-28 04:13:26,393 - INFO - 准备发送指令 'on1' 到设备 'zm'...
2026-01-28 04:13:26,393 - INFO - UDP指令 'on1' 已发送到设备 'zm'。
2026-01-28 04:13:29,630 - WARNING - 准备发送指令 '2' 到设备 'test3'...
2026-01-28 04:13:29,631 - INFO - 准备发送指令 '2' 到设备 'test3'...
2026-01-28 04:13:30,454 - WARNING - 准备发送指令 '1' 到设备 'test2'...
2026-01-28 04:13:30,454 - INFO - 准备发送指令 '1' 到设备 'test2'...
2026-01-28 04:13:33,227 - WARNING - 准备发送指令 '4' 到设备 'test4'...
2026-01-28 04:13:33,228 - INFO - 准备发送指令 '4' 到设备 'test4'...
2026-01-28 04:13:34,638 - ERROR - 与设备 'test3' (1.1.1.1:3) 通信超时。
2026-01-28 04:13:34,643 - INFO - 已关闭与设备 'test3' (1.1.1.1:3) 的非保持连接。
2026-01-28 04:13:35,468 - ERROR - 与设备 'test2' (1.1.1.1:2) 通信超时。
2026-01-28 04:13:35,473 - INFO - 已关闭与设备 'test2' (1.1.1.1:2) 的非保持连接。
2026-01-28 04:13:35,658 - WARNING - 准备发送指令 '7' 到设备 'test1tcp'...
2026-01-28 04:13:35,659 - INFO - 准备发送指令 '7' 到设备 'test1tcp'...
2026-01-28 04:13:35,659 - INFO - 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 '7': b'7'
2026-01-28 04:13:35,661 - INFO - 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: '7'
2026-01-28 04:13:38,236 - ERROR - 与设备 'test4' (1.1.1.1:4) 通信超时。
2026-01-28 04:13:38,237 - INFO - 已关闭与设备 'test4' (1.1.1.1:4) 的非保持连接。
2026-01-28 04:20:33,664 - INFO - 定时任务管理窗口已关闭。
2026-01-28 04:20:33,664 - INFO - 定时任务管理窗口已关闭。
2026-01-28 04:20:34,482 - INFO - 已关闭设备 85ce7dab-22a3-4d39-b783-268eb037cd88 的保持连接。
2026-01-28 04:20:37,622 - INFO - 日志系统已初始化。
2026-01-28 04:20:37,668 - INFO - 从 config.json 加载配置成功。
2026-01-28 04:20:37,669 - INFO - 配置保存到 config.json 成功。
2026-01-28 04:21:49,416 - INFO - 定时任务管理窗口已关闭。
2026-01-28 04:21:49,417 - INFO - 定时任务管理窗口已关闭。
2026-01-28 04:21:50,961 - INFO - 已关闭设备 85ce7dab-22a3-4d39-b783-268eb037cd88 的保持连接。
2026-01-28 04:21:52,090 - INFO - 日志系统已初始化。
2026-01-28 04:21:52,144 - INFO - 从 config.json 加载配置成功。
2026-01-28 04:21:52,145 - INFO - 配置保存到 config.json 成功。
2026-01-28 04:22:06,368 - INFO - 定时任务管理窗口已关闭。
2026-01-28 04:22:06,369 - INFO - 定时任务管理窗口已关闭。
2026-01-28 04:22:07,363 - INFO - 已关闭设备 85ce7dab-22a3-4d39-b783-268eb037cd88 的保持连接。
2026-01-28 04:26:01,023 - INFO - 日志系统已初始化。
2026-01-28 04:26:01,067 - INFO - 从 config.json 加载配置成功。
2026-01-28 04:26:01,068 - INFO - 配置保存到 config.json 成功。
2026-01-28 04:28:27,202 - INFO - 定时任务管理窗口已关闭。
2026-01-28 04:28:27,204 - INFO - 定时任务管理窗口已关闭。
2026-01-28 04:28:28,992 - INFO - 已关闭设备 85ce7dab-22a3-4d39-b783-268eb037cd88 的保持连接。
2026-01-28 04:28:30,342 - INFO - 日志系统已初始化。
2026-01-28 04:28:30,384 - INFO - 从 config.json 加载配置成功。
2026-01-28 04:28:30,385 - INFO - 配置保存到 config.json 成功。
2026-01-28 04:28:39,432 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 04:28:39,432 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 04:28:39,433 - INFO - 配置保存到 config.json 成功。
2026-01-28 04:28:39,988 - INFO - 配置保存到 config.json 成功。
2026-01-28 04:29:49,330 - INFO - 配置保存到 config.json 成功。
2026-01-28 04:29:49,331 - INFO - 配置保存到 config.json 成功。
2026-01-28 04:29:49,338 - INFO - 定时任务 '新任务' (ID: 6d19bd9a...) 已保存。
2026-01-28 04:30:00,063 - INFO - 配置保存到 config.json 成功。
2026-01-28 04:30:00,064 - INFO - 配置保存到 config.json 成功。
2026-01-28 04:30:12,855 - INFO - 配置保存到 config.json 成功。
2026-01-28 04:30:12,857 - INFO - 配置保存到 config.json 成功。
2026-01-28 04:30:12,857 - INFO - 定时任务 '新任务' (ID: 6d19bd9a...) 已保存。
2026-01-28 04:30:13,864 - INFO - 配置保存到 config.json 成功。
2026-01-28 04:30:13,865 - INFO - 配置保存到 config.json 成功。
2026-01-28 04:31:00,158 - INFO - !!! 任务 '新任务' 触发执行 !!!
2026-01-28 04:31:00,158 - INFO - 调度器: 任务 '新任务' 到期,准备执行。
2026-01-28 04:31:00,158 - INFO - 调度器: 任务 '新任务' 到期,准备执行。
2026-01-28 04:31:00,162 - INFO - 配置保存到 config.json 成功。
2026-01-28 04:31:00,209 - INFO - 定时任务 '新任务':开始执行指令序列...
2026-01-28 04:31:00,210 - INFO - 定时任务 '新任务':开始执行指令序列...
2026-01-28 04:31:00,210 - INFO - 定时任务 '新任务':执行指令 'on1' 到设备 'test1tcp'...
2026-01-28 04:31:00,212 - INFO - 定时任务 '新任务':执行指令 'on1' 到设备 'test1tcp'...
2026-01-28 04:31:00,213 - INFO - [调度任务] 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 'on1': b'on1'
2026-01-28 04:31:00,216 - INFO - [调度任务] 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: 'on1'
2026-01-28 04:31:00,218 - INFO - 定时任务 '新任务':指令 'on1' 成功,响应: on1
2026-01-28 04:31:00,221 - INFO - 定时任务 '新任务':指令 'on1' 成功,响应: on1
2026-01-28 04:31:00,221 - INFO - 定时任务 '新任务':执行指令 'on2' 到设备 'test1tcp'...
2026-01-28 04:31:00,222 - INFO - 定时任务 '新任务':执行指令 'on2' 到设备 'test1tcp'...
2026-01-28 04:31:00,223 - INFO - [调度任务] 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 'on2': b'\x00\x10\x00\x00\x08\x00\x11'
2026-01-28 04:31:00,224 - INFO - [调度任务] 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: '\x00\x10\x00\x00\x08\x00\x11'
2026-01-28 04:31:00,226 - INFO - 定时任务 '新任务':指令 'on2' 成功,响应: 
2026-01-28 04:31:00,227 - INFO - 定时任务 '新任务':指令 'on2' 成功,响应: 
2026-01-28 04:31:00,228 - INFO - 定时任务 '新任务':指令之间延迟 500 毫秒。
2026-01-28 04:31:00,237 - INFO - 定时任务 '新任务':指令之间延迟 500 毫秒。
2026-01-28 04:31:00,737 - INFO - 定时任务 '新任务':执行指令 '2' 到设备 'test1tcp'...
2026-01-28 04:31:00,738 - INFO - 定时任务 '新任务':执行指令 '2' 到设备 'test1tcp'...
2026-01-28 04:31:00,738 - INFO - [调度任务] 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 '2': b'2'
2026-01-28 04:31:00,740 - INFO - [调度任务] 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: '2'
2026-01-28 04:31:00,741 - INFO - 定时任务 '新任务':指令 '2' 成功,响应: 2
2026-01-28 04:31:00,743 - INFO - 定时任务 '新任务':指令 '2' 成功,响应: 2
2026-01-28 04:31:00,743 - INFO - 定时任务 '新任务':指令之间延迟 500 毫秒。
2026-01-28 04:31:00,745 - INFO - 定时任务 '新任务':指令之间延迟 500 毫秒。
2026-01-28 04:31:01,246 - INFO - 定时任务 '新任务':执行指令 '3' 到设备 'test1tcp'...
2026-01-28 04:31:01,246 - INFO - 定时任务 '新任务':执行指令 '3' 到设备 'test1tcp'...
2026-01-28 04:31:01,247 - INFO - [调度任务] 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 '3': b'3'
2026-01-28 04:31:01,249 - INFO - [调度任务] 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: '3'
2026-01-28 04:31:01,251 - INFO - 定时任务 '新任务':指令 '3' 成功,响应: 3
2026-01-28 04:31:01,253 - INFO - 定时任务 '新任务':指令 '3' 成功,响应: 3
2026-01-28 04:31:01,253 - INFO - 定时任务 '新任务':指令之间延迟 500 毫秒。
2026-01-28 04:31:01,255 - INFO - 定时任务 '新任务':指令之间延迟 500 毫秒。
2026-01-28 04:31:01,755 - INFO - 定时任务 '新任务':执行指令 '4' 到设备 'test1tcp'...
2026-01-28 04:31:01,756 - INFO - 定时任务 '新任务':执行指令 '4' 到设备 'test1tcp'...
2026-01-28 04:31:01,756 - INFO - [调度任务] 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 '4': b'4'
2026-01-28 04:31:01,759 - INFO - [调度任务] 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: '4'
2026-01-28 04:31:01,760 - INFO - 定时任务 '新任务':指令 '4' 成功,响应: 4
2026-01-28 04:31:01,762 - INFO - 定时任务 '新任务':指令 '4' 成功,响应: 4
2026-01-28 04:31:01,762 - INFO - 定时任务 '新任务':指令之间延迟 500 毫秒。
2026-01-28 04:31:01,769 - INFO - 定时任务 '新任务':指令之间延迟 500 毫秒。
2026-01-28 04:31:02,269 - INFO - 定时任务 '新任务':执行指令 '5' 到设备 'test1tcp'...
2026-01-28 04:31:02,270 - INFO - 定时任务 '新任务':执行指令 '5' 到设备 'test1tcp'...
2026-01-28 04:31:02,270 - INFO - [调度任务] 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 '5': b'5'
2026-01-28 04:31:02,272 - INFO - [调度任务] 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: '5'
2026-01-28 04:31:02,273 - INFO - 定时任务 '新任务':指令 '5' 成功,响应: 5
2026-01-28 04:31:02,274 - INFO - 定时任务 '新任务':指令 '5' 成功,响应: 5
2026-01-28 04:31:02,274 - INFO - 定时任务 '新任务':指令之间延迟 500 毫秒。
2026-01-28 04:31:02,276 - INFO - 定时任务 '新任务':指令之间延迟 500 毫秒。
2026-01-28 04:31:02,776 - INFO - 定时任务 '新任务':执行指令 '6' 到设备 'test1tcp'...
2026-01-28 04:31:02,777 - INFO - 定时任务 '新任务':执行指令 '6' 到设备 'test1tcp'...
2026-01-28 04:31:02,777 - INFO - [调度任务] 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 '6': b'6'
2026-01-28 04:31:02,779 - INFO - [调度任务] 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: '6'
2026-01-28 04:31:02,781 - INFO - 定时任务 '新任务':指令 '6' 成功,响应: 6
2026-01-28 04:31:02,783 - INFO - 定时任务 '新任务':指令 '6' 成功,响应: 6
2026-01-28 04:31:02,783 - INFO - 定时任务 '新任务':指令序列执行完毕。
2026-01-28 04:31:02,785 - INFO - 定时任务 '新任务':指令序列执行完毕。
2026-01-28 04:31:42,828 - WARNING - 准备发送指令 'on2' 到设备 'test1tcp'...
2026-01-28 04:31:42,828 - INFO - 准备发送指令 'on2' 到设备 'test1tcp'...
2026-01-28 04:31:42,829 - INFO - 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 'on2': b'\x00\x10\x00\x00\x08\x00\x11'
2026-01-28 04:31:42,832 - INFO - 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: '\x00\x10\x00\x00\x08\x00\x11'
2026-01-28 04:31:47,237 - WARNING - 准备发送指令 '7' 到设备 'test1tcp'...
2026-01-28 04:31:47,237 - INFO - 准备发送指令 '7' 到设备 'test1tcp'...
2026-01-28 04:31:47,238 - INFO - 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 '7': b'7'
2026-01-28 04:31:47,240 - INFO - 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: '7'
2026-01-28 04:31:48,231 - WARNING - 准备发送指令 '22' 到设备 'test1tcp'...
2026-01-28 04:31:48,231 - INFO - 准备发送指令 '22' 到设备 'test1tcp'...
2026-01-28 04:31:48,233 - INFO - 发送到设备 'test1tcp' (127.0.0.1:19987) 指令 '22': b'22'
2026-01-28 04:31:48,234 - INFO - 从设备 'test1tcp' (127.0.0.1:19987) 收到响应: '22'
2026-01-28 04:32:17,637 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 04:32:17,637 - INFO - 设置已保存,正在刷新主界面...
2026-01-28 04:32:17,638 - INFO - 配置保存到 config.json 成功。
2026-01-28 04:32:18,120 - INFO - 配置保存到 config.json 成功。
2026-01-28 04:32:43,421 - INFO - 已关闭设备 85ce7dab-22a3-4d39-b783-268eb037cd88 的保持连接。

911
main_control_app.py Normal file
View File

@@ -0,0 +1,911 @@
import tkinter as tk
from tkinter import ttk, messagebox
import json
import os
import sys
import threading
import time
from functools import partial
from datetime import datetime, timedelta
import calendar
import uuid
import logging
# 导入 NetworkManager 和 SettingsApp
from network_manager import NetworkManager
from settings_app import SettingsApp
from scheduler_app import SchedulerApp
# --- 修正点:从 utils.py 导入 center_toplevel_window ---
from utils import center_toplevel_window
# --- 新增:定义应用程序名称,用于创建用户配置目录下的子文件夹 ---
APP_NAME = "DeviceControlApp"
class MainControlApp(tk.Tk):
def __init__(self):
super().__init__()
self.title("设备控制台")
self.geometry("800x600")
# config_file 和 log_dir 将在 _setup_user_dirs 中设置
self.config_file = None
self.log_dir = None
self.devices = []
self.scheduled_tasks = []
self.device_ui_elements = {}
self.scheduler_window = None # 用于跟踪 SchedulerApp 实例
# --- 新增:在设置日志系统之前,先确定用户配置目录 ---
self._setup_user_dirs()
self._setup_logging() # 设置日志系统,现在会使用 self.log_dir
self.network_manager = NetworkManager(
log_callback=self.log_message,
connection_status_callback=self.update_main_connection_status,
ip_status_callback=self._update_ip_status_dot,
tcp_status_callback=self._update_tcp_status_dot,
app_logger=self.app_logger # <--- 修改点1将 app_logger 传递给 NetworkManager
)
self.create_widgets()
self.load_config()
self.refresh_device_panels()
self.connection_check_interval = 5000 # 5秒
self._start_periodic_connection_check()
self._scheduler_check_id = None
self._start_scheduler_check() # 首次启动调度器检查
self.protocol("WM_DELETE_WINDOW", self.on_closing)
def resource_path(self, relative_path):
"""获取资源文件的绝对路径,兼容 PyInstaller 打包"""
try:
base_path = sys._MEIPASS
except Exception:
base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)
# --- 新增:设置用户特定配置和日志目录的方法 ---
def _setup_user_dirs(self):
"""
根据操作系统确定用户特定的配置和日志目录。
Windows: %APPDATA%\APP_NAME
macOS: ~/Library/Application Support/APP_NAME
Linux: ~/.config/APP_NAME (遵循 XDG Base Directory Specification)
"""
base_dir = None
if sys.platform == "win32":
# Windows: %APPDATA% (通常是 C:\Users\<User>\AppData\Roaming)
base_dir = os.path.join(os.getenv('APPDATA'), APP_NAME)
elif sys.platform == "darwin":
# macOS: ~/Library/Application Support/
base_dir = os.path.join(os.path.expanduser('~/Library/Application Support'), APP_NAME)
elif sys.platform.startswith("linux"):
# Linux: 优先使用 XDG_CONFIG_HOME否则默认 ~/.config
xdg_config_home = os.getenv('XDG_CONFIG_HOME')
if xdg_config_home:
base_dir = os.path.join(xdg_config_home, APP_NAME)
else:
base_dir = os.path.join(os.path.expanduser('~/.config'), APP_NAME)
else:
# 对于其他或未知操作系统,回退到当前工作目录下的子文件夹
base_dir = os.path.join(os.path.abspath('.'), APP_NAME + "_data")
# 确保基目录存在
os.makedirs(base_dir, exist_ok=True)
self.config_file = os.path.join(base_dir, "config.json")
self.log_dir = os.path.join(base_dir, "logs") # 日志文件放在应用数据目录下的 logs 子文件夹
# 确保日志目录存在
os.makedirs(self.log_dir, exist_ok=True)
def _setup_logging(self):
"""配置日志系统,包括文件日志和控制台日志(如果需要)"""
log_file_path = os.path.join(self.log_dir, "app_log.log")
self.app_logger = logging.getLogger("DeviceControlApp")
self.app_logger.setLevel(logging.DEBUG)
if not self.app_logger.handlers:
file_handler = logging.FileHandler(log_file_path, encoding='utf-8')
file_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
file_handler.setFormatter(file_formatter)
self.app_logger.addHandler(file_handler)
# 如果需要控制台输出,可以取消注释以下行
# console_handler = logging.StreamHandler(sys.stdout)
# console_handler.setFormatter(file_formatter)
# self.app_logger.addHandler(console_handler)
self.app_logger.info("日志系统已初始化。")
self.app_logger.info(f"配置文件路径: {self.config_file}")
self.app_logger.info(f"日志文件目录: {self.log_dir}")
def create_widgets(self):
self.main_frame = ttk.Frame(self, padding="10")
self.main_frame.pack(fill=tk.BOTH, expand=True)
top_buttons_frame = ttk.Frame(self.main_frame)
top_buttons_frame.pack(side=tk.TOP, anchor=tk.NW, fill=tk.X, pady=5)
self.settings_btn = ttk.Button(top_buttons_frame, text="设置", command=self.open_settings)
self.settings_btn.pack(side=tk.LEFT, padx=5)
self.scheduler_btn = ttk.Button(top_buttons_frame, text="定时任务", command=self.open_scheduler)
self.scheduler_btn.pack(side=tk.LEFT, padx=5)
# 新增:调度器状态显示标签
self.scheduler_status_var = tk.StringVar(value="调度器: 未启动")
self.scheduler_status_label = ttk.Label(top_buttons_frame, textvariable=self.scheduler_status_var,
font=("Arial", 9, "italic"), foreground="gray")
self.scheduler_status_label.pack(side=tk.RIGHT, padx=10)
self.device_panels_frame = ttk.Frame(self.main_frame)
self.device_panels_frame.pack(fill=tk.BOTH, expand=True, pady=10) # 此帧将扩展以填充主框架的剩余空间
# 创建一个容器帧来管理 canvas 和滚动条的布局
canvas_container = ttk.Frame(self.device_panels_frame)
canvas_container.pack(fill=tk.BOTH, expand=True) # 此容器帧将填充 device_panels_frame
self.canvas = tk.Canvas(canvas_container)
self.v_scrollbar = ttk.Scrollbar(canvas_container, orient="vertical", command=self.canvas.yview)
self.h_scrollbar = ttk.Scrollbar(canvas_container, orient="horizontal", command=self.canvas.xview)
# 先打包滚动条
self.v_scrollbar.pack(side="right", fill="y")
self.h_scrollbar.pack(side="bottom", fill="x")
# 再打包 canvas使其填充剩余空间
self.canvas.pack(side="left", fill="both", expand=True)
self.scrollable_frame = ttk.Frame(self.canvas)
self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw", tags="scrollable_frame_window")
self.canvas.configure(yscrollcommand=self.v_scrollbar.set, xscrollcommand=self.h_scrollbar.set)
self.scrollable_frame.bind(
"<Configure>",
self._on_scrollable_frame_configure
)
# 绑定鼠标滚轮事件到 canvas 和 scrollable_frame
self.canvas.bind("<MouseWheel>", self._on_mouse_wheel)
self.canvas.bind("<Button-4>", self._on_mouse_wheel) # For Linux/macOS
self.canvas.bind("<Button-5>", self._on_mouse_wheel) # For Linux/macOS
self.scrollable_frame.bind("<MouseWheel>", self._on_mouse_wheel)
self.scrollable_frame.bind("<Button-4>", self._on_mouse_wheel) # For Linux/macOS
self.scrollable_frame.bind("<Button-5>", self._on_mouse_wheel) # For Linux/macOS
self.log_frame = ttk.Frame(self.main_frame, padding="5", relief=tk.GROOVE, borderwidth=2)
self.log_frame.pack(fill=tk.X, pady=5) # 移除 expand=True让其只占据必要高度并填充横向空间
ttk.Label(self.log_frame, text="日志和响应", font=("Arial", 10, "bold")).pack(anchor=tk.NW)
# 增加日志文本框的高度
# 创建一个子框架来包含 Text 控件和 Scrollbar
log_text_frame = ttk.Frame(self.log_frame)
log_text_frame.pack(fill=tk.BOTH, expand=True, pady=(5,0))
self.log_text = tk.Text(log_text_frame, height=15, state=tk.DISABLED, wrap=tk.WORD)
self.log_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
log_scrollbar = ttk.Scrollbar(log_text_frame, orient=tk.VERTICAL, command=self.log_text.yview)
log_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.log_text.config(yscrollcommand=log_scrollbar.set)
def _on_scrollable_frame_configure(self, event):
"""当 scrollable_frame 大小改变时,更新 canvas 的 scrollregion 和 create_window 的大小"""
# 更新 scrollregion包括横向和纵向
bbox = self.canvas.bbox("all")
if bbox:
# 添加一些内边距
self.canvas.configure(scrollregion=(bbox[0]-10, bbox[1]-10, bbox[2]+10, bbox[3]+10))
# 确保滚动区域的宽度与 canvas 相同,避免不必要的横向滚动
canvas_width = self.canvas.winfo_width()
if canvas_width > 1: # 确保canvas已经初始化
self.canvas.itemconfig("scrollable_frame_window", width=canvas_width)
def load_config(self):
"""从 JSON 文件加载配置"""
if os.path.exists(self.config_file):
try:
with open(self.config_file, 'r', encoding='utf-8') as f:
config_data = json.load(f)
self.devices = config_data.get("devices", [])
self.scheduled_tasks = config_data.get("scheduled_tasks", [])
self.log_message(f"{self.config_file} 加载配置成功。")
except json.JSONDecodeError:
self.log_message(f"错误:{self.config_file} 文件格式不正确。", "red")
self.devices = []
self.scheduled_tasks = []
except Exception as e:
self.log_message(f"错误:加载配置失败 - {e}", "red")
self.devices = []
self.scheduled_tasks = []
else:
self.log_message(f"未找到 {self.config_file},将创建新配置。", "orange")
self.devices = []
self.scheduled_tasks = []
for device in self.devices:
if "id" not in device:
device["id"] = str(uuid.uuid4())
# --- 修正点:为可能缺失的设备信息字段添加默认值 ---
if "name" not in device:
device["name"] = "未知设备"
if "type" not in device:
device["type"] = "通用"
if "protocol" not in device:
device["protocol"] = "TCP" # 默认使用TCP
# --- 修正点:迁移旧的 'ip' 字段到 'address' ---
if "ip" in device and "address" not in device:
device["address"] = device["ip"]
del device["ip"] # 移除旧的 'ip' 字段
elif "address" not in device:
device["address"] = "127.0.0.1" # 默认本地回环地址
# --- 修正点结束 ---
if "port" not in device:
device["port"] = 0 # 默认端口
# --- 修正点结束 ---
if "keep_alive" not in device:
device["keep_alive"] = False
if "timeout" not in device:
device["timeout"] = 5
for command in device.get("commands", []):
if "data" not in command:
command["data"] = command.get("command_data", "")
if "type" not in command:
command["type"] = "text"
if "command_data" in command:
del command["command_data"]
new_scheduled_tasks = []
for task in self.scheduled_tasks:
if "schedule_type" not in task:
self.log_message(f"迁移旧格式定时任务 (ID: {task.get('id', 'N/A')})", "orange")
migrated_task = {
"id": task.get("id", str(uuid.uuid4())),
"name": f"旧任务_{task.get('id', str(uuid.uuid4()))[:4]}",
"schedule_type": "daily",
"trigger": {
"hour": int(task["time"].split(":")[0]),
"minute": int(task["time"].split(":")[1]),
"second": 0
},
"actions": [],
"enabled": task.get("enabled", True),
"last_run_time": None,
"next_run_time": None
}
device_obj = None
if 0 <= task.get("device_index", -1) < len(self.devices):
device_obj = self.devices[task["device_index"]]
if device_obj and task.get("command_indices"):
for cmd_idx in task["command_indices"]:
migrated_task["actions"].append({
"device_id": device_obj["id"],
"command_index": cmd_idx,
"delay_ms": int(task.get("delay_between_commands", 0) * 1000)
})
new_scheduled_tasks.append(migrated_task)
else:
if task.get("last_run_time") and isinstance(task["last_run_time"], str):
try:
task["last_run_time"] = datetime.fromisoformat(task["last_run_time"])
except ValueError:
task["last_run_time"] = None
if task.get("next_run_time") and isinstance(task["next_run_time"], str):
try:
task["next_run_time"] = datetime.fromisoformat(task["next_run_time"])
except ValueError:
task["next_run_time"] = None
new_scheduled_tasks.append(task)
self.scheduled_tasks = new_scheduled_tasks
self.app_logger.debug(f"Loaded {len(self.scheduled_tasks)} scheduled tasks from config.")
for task in self.scheduled_tasks:
self.app_logger.debug(f" Task ID: {task.get('id')}, Name: {task.get('name')}, Type: {task.get('schedule_type')}, Next Run: {task.get('next_run_time')}")
self._recalculate_all_next_run_times()
def save_config(self):
"""将当前配置保存到 JSON 文件"""
serializable_scheduled_tasks = []
for task in self.scheduled_tasks:
temp_task = task.copy()
if isinstance(temp_task.get("last_run_time"), datetime):
temp_task["last_run_time"] = temp_task["last_run_time"].isoformat()
if isinstance(temp_task.get("next_run_time"), datetime):
temp_task["next_run_time"] = temp_task["next_run_time"].isoformat()
serializable_scheduled_tasks.append(temp_task)
config_data = {
"devices": self.devices,
"scheduled_tasks": serializable_scheduled_tasks
}
try:
with open(self.config_file, 'w', encoding='utf-8') as f:
json.dump(config_data, f, indent=4, ensure_ascii=False)
self.log_message(f"配置保存到 {self.config_file} 成功。", "blue")
self.app_logger.debug(f"Config saved to {self.config_file}.")
except Exception as e:
self.log_message(f"错误:保存配置失败 - {e}", "red")
self.app_logger.error(f"Failed to save config - {e}")
def refresh_device_panels(self):
"""清除并重新创建所有设备面板"""
for widget in self.scrollable_frame.winfo_children():
widget.destroy()
self.device_ui_elements.clear()
# 确保至少有一个设备面板,即使设备列表为空
if not self.devices:
empty_frame = ttk.Frame(self.scrollable_frame, padding="10", relief=tk.RIDGE, borderwidth=2)
empty_frame.pack(fill=tk.X, expand=True, pady=5, padx=5)
ttk.Label(empty_frame, text="暂无设备配置", font=("Arial", 10)).pack(pady=10)
else:
for i, device in enumerate(self.devices):
self._create_device_panel(i, device)
self._set_device_command_buttons_state(i, tk.NORMAL)
self.scrollable_frame.update_idletasks()
# 更新滚动区域
bbox = self.canvas.bbox("all")
if bbox:
self.canvas.config(scrollregion=bbox)
def _create_device_panel(self, device_index, device_info):
"""为单个设备创建UI面板"""
panel_frame = ttk.Frame(self.scrollable_frame, padding="10", relief=tk.RIDGE, borderwidth=2)
panel_frame.pack(fill=tk.X, expand=True, pady=5, padx=5)
# 添加一个背景色以便调试
panel_frame.configure(style="DevicePanel.TFrame")
style = ttk.Style()
style.configure("DevicePanel.TFrame", background="lightgray")
# 绑定滚轮事件到 panel_frame 及其所有子组件
panel_frame.bind("<MouseWheel>", self._on_mouse_wheel)
panel_frame.bind("<Button-4>", self._on_mouse_wheel)
panel_frame.bind("<Button-5>", self._on_mouse_wheel)
# 递归绑定子组件的滚轮事件
def bind_children_mouse_wheel(widget):
for child in widget.winfo_children():
child.bind("<MouseWheel>", self._on_mouse_wheel)
child.bind("<Button-4>", self._on_mouse_wheel)
child.bind("<Button-5>", self._on_mouse_wheel)
bind_children_mouse_wheel(child)
bind_children_mouse_wheel(panel_frame)
# 创建设备信息行(名称和状态指示灯)
info_frame = ttk.Frame(panel_frame)
info_frame.pack(fill=tk.X, pady=(0, 10))
device_name_label = ttk.Label(info_frame, text=device_info["name"], font=("Arial", 12, "bold"))
device_name_label.pack(side=tk.LEFT, padx=5)
themed_bg = ttk.Style().lookup("TFrame", "background")
# 添加设备状态指示灯
ip_status_canvas = tk.Canvas(info_frame, width=15, height=15, bg=themed_bg, highlightthickness=0)
ip_status_canvas.pack(side=tk.LEFT, padx=5)
ip_dot_id = ip_status_canvas.create_oval(2, 2, 13, 13, fill="gray", outline="gray")
tcp_status_canvas = tk.Canvas(info_frame, width=15, height=15, bg=themed_bg, highlightthickness=0)
tcp_status_canvas.pack(side=tk.LEFT, padx=5)
tcp_dot_id = tcp_status_canvas.create_oval(2, 2, 13, 13, fill="gray", outline="gray")
self.device_ui_elements[device_index] = {
"ip_status_canvas": ip_status_canvas,
"ip_dot_id": ip_dot_id,
"tcp_status_canvas": tcp_status_canvas,
"tcp_dot_id": tcp_dot_id,
"command_buttons": [],
"is_sending": False
}
# 创建命令按钮容器,使用网格布局来换行显示按钮
commands_frame = ttk.Frame(panel_frame)
commands_frame.pack(fill=tk.X, expand=True)
# 添加命令按钮使用网格布局每行最多显示5个按钮
commands = device_info.get("commands", [])
for cmd_index, command in enumerate(commands):
btn = ttk.Button(commands_frame, text=command["name"],
command=partial(self._send_command_from_button, device_index, cmd_index))
# 计算网格位置每行5个按钮
row = cmd_index // 5
col = cmd_index % 5
btn.grid(row=row, column=col, padx=5, pady=2, sticky="w")
self.device_ui_elements[device_index]["command_buttons"].append(btn)
# 配置网格列权重,使按钮能够正确对齐
for i in range(5):
commands_frame.columnconfigure(i, weight=1)
# 强制更新布局
panel_frame.update_idletasks()
def _send_command_from_button(self, device_index, command_index):
"""处理指令按钮点击事件,实现设备内互斥"""
device = self.devices[device_index]
command = device["commands"][command_index]
if self.device_ui_elements[device_index]["is_sending"]:
self.log_message(f"设备 '{device['name']}' 正在发送指令,请稍候。", "orange")
self.app_logger.info(f"设备 '{device['name']}' 正在发送指令,请稍候。")
return
self.log_message(f"准备发送指令 '{command['name']}' 到设备 '{device['name']}'...", "orange")
self.app_logger.info(f"准备发送指令 '{command['name']}' 到设备 '{device['name']}'...")
self.device_ui_elements[device_index]["is_sending"] = True
self._set_device_command_buttons_state(device_index, tk.DISABLED)
def re_enable_device_buttons():
"""发送完成后重新启用当前设备的指令按钮"""
self.device_ui_elements[device_index]["is_sending"] = False
self._set_device_command_buttons_state(device_index, tk.NORMAL)
self.network_manager.send_command_async(
device_index,
device,
command,
lambda: self.after(0, re_enable_device_buttons)
)
def _set_device_command_buttons_state(self, device_index, state):
"""设置指定设备的所有指令按钮的状态"""
if device_index in self.device_ui_elements:
for btn in self.device_ui_elements[device_index]["command_buttons"]:
btn.config(state=state)
def _on_mouse_wheel(self, event):
"""处理鼠标滚轮事件,实现滚动"""
if event.delta: # Windows
self.canvas.yview_scroll(int(-1*(event.delta/120)), "units")
elif event.num == 4: # Linux/macOS scroll up
self.canvas.yview_scroll(-1, "units")
elif event.num == 5: # Linux/macOS scroll down
self.canvas.yview_scroll(1, "units")
def _draw_dot(self, canvas, dot_id, color):
"""在画布上绘制指定颜色的圆点"""
canvas.itemconfig(dot_id, fill=color, outline=color)
def _update_ip_status_dot(self, device_index, is_connected):
"""更新 IP 状态点颜色"""
if device_index in self.device_ui_elements:
canvas = self.device_ui_elements[device_index]["ip_status_canvas"]
dot_id = self.device_ui_elements[device_index]["ip_dot_id"]
color = "green" if is_connected else "red"
self.after(0, lambda: self._draw_dot(canvas, dot_id, color))
def _update_tcp_status_dot(self, device_index, is_connected):
"""更新 TCP 连接状态点颜色"""
if device_index in self.device_ui_elements:
canvas = self.device_ui_elements[device_index]["tcp_status_canvas"]
dot_id = self.device_ui_elements[device_index]["tcp_dot_id"]
color = "green" if is_connected else "red"
self.after(0, lambda: self._draw_dot(canvas, dot_id, color))
def _start_periodic_connection_check(self):
"""启动周期性连接检查"""
if not hasattr(self, '_periodic_check_id'):
self._periodic_check_id = None
if self._periodic_check_id:
self.after_cancel(self._periodic_check_id)
self._check_all_connections_periodic()
self._periodic_check_id = self.after(self.connection_check_interval, self._start_periodic_connection_check)
def _check_all_connections_periodic(self):
"""
检查所有设备的 IP 可达性和 TCP 连接状态。
优化:对于 keep_alive 的 TCP 设备,只进行一次 TCP 连接状态检查。
"""
for i, device in enumerate(self.devices):
if device["protocol"] == "TCP":
if device.get("keep_alive", False):
# 对于保持连接的 TCP 设备,通过检查 TCP 连接来判断可达性
self.network_manager.check_tcp_connection_async(i, device)
# Note: check_tcp_connection_async 内部会同时更新 IP 和 TCP 状态点
else:
# 对于非保持连接的 TCP 设备,进行 IP 可达性检查
self.network_manager.check_ip_reachability_async(i, device)
# TCP 状态点应始终显示为未连接
self._update_tcp_status_dot(i, False)
elif device["protocol"] == "UDP":
# 对于 UDP 设备,使用 UDP 方式检查可达性
self.network_manager.check_udp_reachability_async(i, device)
# TCP 状态点应始终显示为未连接
self._update_tcp_status_dot(i, False)
def open_settings(self):
"""打开设置窗口"""
settings_window = SettingsApp(self, on_save_callback=self.on_settings_saved, devices_data=self.devices)
# --- 移除此行center_toplevel_window(settings_window, self) ---
# SettingsApp 内部会自行居中
def on_settings_saved(self, updated_devices):
"""当设置窗口保存配置后,由回调函数调用"""
self.log_message("设置已保存,正在刷新主界面...", "blue")
self.app_logger.info("设置已保存,正在刷新主界面...")
self.devices = updated_devices
self.save_config()
self.refresh_device_panels()
self._start_periodic_connection_check()
self._recalculate_all_next_run_times()
self._start_scheduler_check() # 确保调度器检查也重新启动以反映新配置
def open_scheduler(self):
"""打开定时任务管理窗口,确保只有一个实例"""
if self.scheduler_window is None or not self.scheduler_window.winfo_exists():
self.scheduler_window = SchedulerApp(self, self.devices, self.scheduled_tasks, self.save_config, self.log_message)
# --- 移除此行center_toplevel_window(self.scheduler_window, self) ---
# SchedulerApp 内部会自行居中
self.scheduler_window.protocol("WM_DELETE_WINDOW", self._on_scheduler_closing)
else:
self.scheduler_window.lift() # 将现有窗口带到前面
def _on_scheduler_closing(self):
"""处理 SchedulerApp 关闭事件"""
if self.scheduler_window:
self.scheduler_window.destroy()
self.scheduler_window = None
self.log_message("定时任务管理窗口已关闭。", "blue")
self.app_logger.info("定时任务管理窗口已关闭。")
def _start_scheduler_check(self):
"""启动周期性检查定时任务"""
if self._scheduler_check_id:
self.after_cancel(self._scheduler_check_id)
self._check_schedules() # 立即执行一次检查
self._scheduler_check_id = self.after(1000, self._start_scheduler_check)
def refresh_scheduler_display(self):
"""
供 SchedulerApp 调用,强制刷新主界面的调度器状态显示。
"""
self.app_logger.debug(f"refresh_scheduler_display called at {datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')}")
self._check_schedules() # 立即触发一次调度器检查以更新UI
def _recalculate_all_next_run_times(self):
"""在配置加载或设备更新后,重新计算所有任务的下次运行时间"""
now = datetime.now()
self.app_logger.debug(f"Recalculating all next run times at {now.strftime('%Y-%m-%d %H:%M:%S.%f')}")
for task in self.scheduled_tasks:
task_name = task.get('name', task['id'][:8])
old_next_run = task.get("next_run_time")
task["next_run_time"] = self._calculate_next_run_time(task, now)
self.app_logger.debug(f" Task '{task_name}': Old next run: {old_next_run}, New next run: {task['next_run_time']}")
self.save_config()
def _check_schedules(self):
"""检查是否有定时任务到期"""
now = datetime.now()
self.app_logger.debug(f"\n--- 调度器检查 @ {now.strftime('%Y-%m-%d %H:%M:%S.%f')} ---")
next_task_summary = ""
min_next_run_time = datetime.max
tasks_to_trigger = [] # Collect tasks to trigger to avoid modifying list while iterating
for task in self.scheduled_tasks:
task_name = task.get('name', task['id'][:8])
self.app_logger.debug(f" 检查任务 '{task_name}' (启用: {task.get('enabled', True)})")
if not task.get("enabled", True):
self.app_logger.debug(f" 任务 '{task_name}' 已禁用,跳过。")
continue
current_next_run_time = task.get("next_run_time")
if current_next_run_time:
self.app_logger.debug(f" 任务 '{task_name}' 当前下次运行时间: {current_next_run_time.strftime('%Y-%m-%d %H:%M:%S.%f')}")
# Check if it's time to trigger this task
# Allow a small window for execution, e.g., within the last second
if current_next_run_time <= now < (current_next_run_time + timedelta(seconds=1)):
self.app_logger.info(f" !!! 任务 '{task_name}' 触发执行 !!!")
tasks_to_trigger.append((task, now))
# Update next_task_summary based on the *current* next_run_time
if current_next_run_time < min_next_run_time:
min_next_run_time = current_next_run_time
next_task_summary = f"'{task_name}' @ {min_next_run_time.strftime('%H:%M:%S')}"
else:
# If next_run_time is None, try to calculate it for the first time or if it was cleared
self.app_logger.debug(f" 任务 '{task_name}' next_run_time 为 None尝试计算。")
new_next_run = self._calculate_next_run_time(task, now)
if new_next_run:
task["next_run_time"] = new_next_run
self.app_logger.debug(f" 任务 '{task_name}' 首次计算下次运行时间: {new_next_run.strftime('%Y-%m-%d %H:%M:%S.%f')}")
# Also check if this newly calculated time is due right now
if new_next_run <= now < (new_next_run + timedelta(seconds=1)):
self.app_logger.info(f" !!! 任务 '{task_name}' (首次计算后) 触发执行 !!!")
tasks_to_trigger.append((task, now))
if new_next_run < min_next_run_time:
min_next_run_time = new_next_run
next_task_summary = f"'{task_name}' @ {min_next_run_time.strftime('%H:%M:%S')}"
else:
self.app_logger.debug(f" 任务 '{task_name}' 无法计算下次运行时间。")
# Now trigger the collected tasks
for task, trigger_time in tasks_to_trigger:
self.log_message(f"调度器: 任务 '{task.get('name', task['id'][:8])}' 到期,准备执行。", "blue")
self.app_logger.info(f"调度器: 任务 '{task.get('name', task['id'][:8])}' 到期,准备执行。")
threading.Thread(target=self._execute_scheduled_commands, args=(task, trigger_time), name=f"SchedulerTask-{task.get('name', task['id'][:8])}").start()
# Immediately update the task's last_run_time and recalculate next_run_time
# This is crucial for 'once' tasks to prevent re-triggering
self.after(0, lambda t=task, c_now=trigger_time: self._update_scheduled_task_after_execution(t, c_now))
# Update scheduler status label
self.scheduler_status_var.set(
f"调度器: 上次检查 {now.strftime('%H:%M:%S')} | 下个任务: {next_task_summary}"
)
def _update_scheduled_task_after_execution(self, task, execution_time):
"""
在主线程中更新定时任务的 last_run_time 和 next_run_time。
"""
task_name = task.get("name", task["id"][:8])
self.app_logger.debug(f"_update_scheduled_task_after_execution for task '{task_name}' called at {datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')}.")
task["last_run_time"] = execution_time
task["next_run_time"] = self._calculate_next_run_time(task, execution_time + timedelta(seconds=1))
self.save_config()
# 如果定时任务管理窗口是打开的,刷新其显示
if self.scheduler_window and self.scheduler_window.winfo_exists():
self.scheduler_window._update_schedule_display()
def _calculate_next_run_time(self, task, reference_time):
"""
根据任务的调度类型和触发器计算下次运行时间。
reference_time: datetime对象作为计算下次运行时间的基准。
"""
schedule_type = task["schedule_type"]
trigger = task["trigger"]
next_run = None
if schedule_type == "once":
try:
target_time = datetime(
trigger["year"], trigger["month"], trigger["day"],
trigger["hour"], trigger["minute"], trigger["second"]
)
self.app_logger.debug(f" _calculate_next_run_time for 'once' task '{task.get('name', task['id'][:8])}': target_time={target_time}, reference_time={reference_time}, last_run_time={task.get('last_run_time')}")
if task.get("last_run_time") is None and target_time >= reference_time:
next_run = target_time
except ValueError:
self.app_logger.warning(f"任务 '{task.get('name', task['id'][:8])}' 的一次性触发时间无效。")
self.log_message(f"任务 '{task.get('name', task['id'][:8])}' 的一次性触发时间无效。", "red")
elif schedule_type == "daily":
target_hour = trigger["hour"]
target_minute = trigger["minute"]
target_second = trigger["second"]
# Start with today's target time
potential_run_time = reference_time.replace(
hour=target_hour,
minute=target_minute,
second=target_second,
microsecond=0
)
# If target time is in the past, set it for tomorrow
if potential_run_time <= reference_time:
potential_run_time += timedelta(days=1)
next_run = potential_run_time
elif schedule_type == "weekly":
target_hour = trigger["hour"]
target_minute = trigger["minute"]
target_second = trigger["second"]
days_of_week = sorted(trigger["days_of_week"]) # 0=Mon, 6=Sun
min_next_run = None
for day_of_week_target in days_of_week:
# Calculate days ahead to reach the target day of week
days_ahead = (day_of_week_target - reference_time.weekday() + 7) % 7
potential_run_time = (reference_time + timedelta(days=days_ahead)).replace(
hour=target_hour, minute=target_minute, second=target_second, microsecond=0
)
# If it's the same day of week, but the time has passed, move to next week
if days_ahead == 0 and potential_run_time <= reference_time:
potential_run_time += timedelta(weeks=1)
if min_next_run is None or potential_run_time < min_next_run:
min_next_run = potential_run_time
next_run = min_next_run
elif schedule_type == "monthly":
target_hour = trigger["hour"]
target_minute = trigger["minute"]
target_second = trigger["second"]
current_year = reference_time.year
current_month = reference_time.month
potential_runs = []
# Check current month and next month
for month_offset in range(2):
check_year = current_year
check_month = current_month + month_offset
if check_month > 12:
check_month -= 12
check_year += 1
if "month_day" in trigger:
target_day = trigger["month_day"]
try:
# Ensure target_day is valid for the month
if target_day > calendar.monthrange(check_year, check_month)[1]:
continue # Skip if day is invalid for this month (e.g., Feb 30)
potential_date = datetime(check_year, check_month, target_day,
target_hour, target_minute, target_second, microsecond=0)
if potential_date > reference_time:
potential_runs.append(potential_date)
except ValueError:
pass # Date might be invalid (e.g., Feb 30)
elif "week_of_month" in trigger:
target_week_of_month = trigger["week_of_month"]
target_day_of_week = trigger["day_of_week"]
count = 0
for day in range(1, calendar.monthrange(check_year, check_month)[1] + 1):
d = datetime(check_year, check_month, day)
if d.weekday() == target_day_of_week:
count += 1
if count == target_week_of_month:
potential_date = d.replace(
hour=target_hour, minute=target_minute, second=target_second, microsecond=0
)
if potential_date > reference_time:
potential_runs.append(potential_date)
break
if potential_runs:
next_run = min(potential_runs)
else:
next_run = None
self.app_logger.debug(f" _calculate_next_run_time for '{task.get('name', task['id'][:8])}' returns: {next_run}")
return next_run
def _execute_scheduled_commands(self, task, trigger_time):
"""
执行定时任务中的指令序列。
此方法在后台线程中运行。
"""
task_name = task.get("name", task["id"][:8])
self.app_logger.debug(f"[SchedulerThread-{threading.current_thread().name}] _execute_scheduled_commands for task '{task_name}' STARTED @ {datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')}")
time.sleep(0.05) # Give UI a moment to update log message
self.log_message(f"调度器: 任务 '{task_name}':开始执行指令序列...", "blue")
self.app_logger.info(f"调度器: 任务 '{task_name}':开始执行指令序列...")
try:
for i, action in enumerate(task["actions"]):
device_id = action["device_id"]
command_index = action["command_index"]
# 获取当前指令的延迟时间
delay_ms = action.get("delay_ms", 0)
device = next((d for d in self.devices if d["id"] == device_id), None)
if not device:
self.log_message(f"定时任务 '{task_name}' 失败: 设备ID '{device_id[:8]}...' 无效或不存在。", "red")
self.app_logger.error(f"定时任务 '{task_name}' 失败: 设备ID '{device_id[:8]}...' 无效或不存在。")
continue
if not (0 <= command_index < len(device["commands"])):
self.log_message(f"定时任务 '{task_name}' 失败: 设备 '{device['name']}' 的指令索引 {command_index} 无效。", "red")
self.app_logger.error(f"定时任务 '{task_name}' 失败: 设备 '{device['name']}' 的指令索引 {command_index} 无效。")
continue
command = device["commands"][command_index]
# 如果当前指令有延迟,则先等待
if delay_ms > 0:
self.log_message(f"定时任务 '{task_name}':指令 '{command['name']}' 将延迟 {delay_ms} 毫秒后执行。", "gray")
self.app_logger.info(f"定时任务 '{task_name}':指令 '{command['name']}' 将延迟 {delay_ms} 毫秒后执行。")
time.sleep(delay_ms / 1000.0)
self.log_message(f"定时任务 '{task_name}':执行指令 '{command['name']}' 到设备 '{device['name']}'...", "blue")
self.app_logger.info(f"定时任务 '{task_name}':执行指令 '{command['name']}' 到设备 '{device['name']}'...")
try:
response = self.network_manager.send_command_sync_for_scheduler(device, command, is_scheduled_task=True)
self.log_message(f"定时任务 '{task_name}':指令 '{command['name']}' 成功,响应: {response}", "green")
self.app_logger.info(f"定时任务 '{task_name}':指令 '{command['name']}' 成功,响应: {response}")
except Exception as e:
self.log_message(f"定时任务 '{task_name}':指令 '{command['name']}' 执行失败: {e}", "red")
self.app_logger.error(f"定时任务 '{task_name}':指令 '{command['name']}' 执行失败: {e}")
self.log_message(f"定时任务 '{task_name}':指令序列执行完毕。", "blue")
self.app_logger.info(f"定时任务 '{task_name}':指令序列执行完毕。")
except Exception as e:
self.log_message(f"定时任务 '{task_name}' 执行过程中发生未预期错误: {e}", "red")
self.app_logger.critical(f"定时任务 '{task_name}' 执行过程中发生未预期错误: {e}")
finally:
self.app_logger.debug(f"[SchedulerThread-{threading.current_thread().name}] _execute_scheduled_commands for task '{task_name}' FINISHED @ {datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')}")
def update_main_connection_status(self, status, color="black"):
"""MainControlApp 的连接状态更新回调,这里仅用于日志"""
self.log_message(f"全局连接状态更新: {status}", color)
self.app_logger.info(f"全局连接状态更新: {status}")
def log_message(self, message, color="black"):
"""向日志文本框添加消息 (在 UI 线程中执行) 并写入文件"""
timestamp_str = datetime.now().strftime("[%Y-%m-%d %H:%M:%S]")
ui_message = f"{timestamp_str} {message}"
# 这里的日志级别判断保持不变,因为它控制的是 UI 显示的颜色和 app_logger 的级别
if color == "red":
self.app_logger.error(message)
elif color == "orange":
self.app_logger.warning(message)
elif color == "blue":
self.app_logger.info(message)
elif color == "green":
self.app_logger.info(message)
elif color == "gray": # UI日志中灰色通常是调试信息
self.app_logger.debug(message)
else:
self.app_logger.info(message)
self.after(0, lambda: self._log_message_safe(ui_message, color))
def _log_message_safe(self, message, color):
"""安全的 UI 更新函数,确保在主线程中被调用"""
self.log_text.config(state=tk.NORMAL)
self.log_text.insert(tk.END, message + "\n", color)
self.log_text.tag_config("red", foreground="red")
self.log_text.tag_config("green", foreground="green")
self.log_text.tag_config("blue", foreground="blue")
self.log_text.tag_config("orange", foreground="orange")
self.log_text.tag_config("gray", foreground="gray")
self.log_text.see(tk.END)
self.log_text.config(state=tk.DISABLED)
def on_closing(self):
"""窗口关闭时保存配置并关闭所有活动套接字"""
self.network_manager.close_all_sockets()
if hasattr(self, '_periodic_check_id') and self._periodic_check_id:
self.after_cancel(self._periodic_check_id)
if hasattr(self, '_scheduler_check_id') and self._scheduler_check_id:
self.after_cancel(self._scheduler_check_id)
self.destroy()
if __name__ == "__main__":
app = MainControlApp()
app.mainloop()

335
network_manager.py Normal file
View File

@@ -0,0 +1,335 @@
import socket
import threading
import time
import sys
# import subprocess # <--- 移除 subprocess 模块导入
import logging
class NetworkManager:
def __init__(self, log_callback, connection_status_callback, ip_status_callback, tcp_status_callback, app_logger):
self.log_callback = log_callback
self.connection_status_callback = connection_status_callback
self.ip_status_callback = ip_status_callback
self.tcp_status_callback = tcp_status_callback
self.app_logger = app_logger # 保存 app_logger 实例
self.sockets = {} # Dictionary to hold persistent sockets if keep_alive is true
self.socket_lock = threading.Lock() # Lock for managing self.sockets
def _prepare_command_data(self, command):
# Placeholder for actual command data preparation
if command["type"] == "hex":
return bytes.fromhex(command["data"].replace(" ", ""))
else: # Default to text
return command["data"].encode('utf-8')
def _log_with_prefix(self, message, color, is_scheduled_task):
prefix = "[调度任务] " if is_scheduled_task else ""
full_message = prefix + message
# 调用 UI 的 log_callback
self.log_callback(full_message, color)
# 根据颜色将消息发送到 app_logger 的对应级别
if color == "red":
self.app_logger.error(full_message)
elif color == "orange":
self.app_logger.warning(full_message)
elif color == "blue" or color == "green":
self.app_logger.info(full_message)
elif color == "gray":
self.app_logger.debug(full_message) # 'gray' 通常用于更详细的内部信息,映射到 DEBUG
else:
self.app_logger.info(full_message) # 默认使用 INFO
def _connect_tcp_and_send(self, device, command, is_scheduled_task=False):
# This method handles connection, sending, and receiving for TCP
# It's called by send_command_async and send_command_sync_for_scheduler
sock = None
response_data = b""
device_id = device["id"]
self.app_logger.debug(f"[NetworkManager] _connect_tcp_and_send called for device '{device['name']}', scheduled: {is_scheduled_task}")
try:
# Check for existing persistent connection if keep_alive is true
if device.get("keep_alive"):
with self.socket_lock:
sock = self.sockets.get(device_id)
if sock:
# Try to use existing socket, check if it's still open
try:
sock.send(b'') # Send a dummy byte to check connection
self.app_logger.debug(f"[NetworkManager] Using existing keep-alive socket for '{device['name']}'.")
except (socket.error, BrokenPipeError):
self._log_with_prefix(f"设备 '{device['name']}' ({device['address']}:{device['port']}) 的保持连接已断开,重新连接。", "orange", is_scheduled_task)
self.app_logger.debug(f"[NetworkManager] Keep-alive for '{device['name']}' broken, re-establishing.")
sock.close()
sock = None
if not sock:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(device.get("timeout", 5))
sock.connect((device["address"], device["port"]))
self._log_with_prefix(f"成功建立与设备 '{device['name']}' ({device['address']}:{device['port']}) 的保持连接。", "green", is_scheduled_task)
self.app_logger.debug(f"[NetworkManager] Established new keep-alive socket for '{device['name']}'.")
self.sockets[device_id] = sock
else:
# Non-persistent connection: create new socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(device.get("timeout", 5))
sock.connect((device["address"], device["port"]))
self._log_with_prefix(f"成功连接到设备 '{device['name']}' ({device['address']}:{device['port']})。", "green", is_scheduled_task)
self.app_logger.debug(f"[NetworkManager] Established non-persistent connection for '{device['name']}'.")
data_to_send = self._prepare_command_data(command)
sock.sendall(data_to_send)
self._log_with_prefix(f"发送到设备 '{device['name']}' ({device['address']}:{device['port']}) 指令 '{command['name']}': {data_to_send!r}", "gray", is_scheduled_task)
self.app_logger.debug(f"[NetworkManager] Sent data to '{device['name']}': {data_to_send!r}")
# Receive response
response_data = sock.recv(1024)
response_str = response_data.decode('utf-8', errors='ignore').strip()
self._log_with_prefix(f"从设备 '{device['name']}' ({device['address']}:{device['port']}) 收到响应: {response_str!r}", "gray", is_scheduled_task)
self.app_logger.debug(f"[NetworkManager] Received response from '{device['name']}': {response_str!r}")
return response_str
except socket.timeout:
error_msg = f"与设备 '{device['name']}' ({device['address']}:{device['port']}) 通信超时。"
self._log_with_prefix(error_msg, "red", is_scheduled_task)
self.app_logger.error(f"[NetworkManager] {error_msg}")
raise ConnectionError(error_msg)
except (socket.error, ConnectionRefusedError, OSError) as e:
error_msg = f"与设备 '{device['name']}' ({device['address']}:{device['port']}) 连接或通信失败: {e}"
self_log_with_prefix(error_msg, "red", is_scheduled_task)
self.app_logger.error(f"[NetworkManager] {error_msg}")
raise ConnectionError(error_msg)
except Exception as e:
error_msg = f"发送指令 '{command['name']}' 到设备 '{device['name']}' 时发生未知错误: {e}"
self._log_with_prefix(error_msg, "red", is_scheduled_task)
self.app_logger.error(f"[NetworkManager] {error_msg}")
raise RuntimeError(error_msg)
finally:
if sock and not device.get("keep_alive"):
sock.close()
self._log_with_prefix(f"已关闭与设备 '{device['name']}' ({device['address']}:{device['port']}) 的非保持连接。", "gray", is_scheduled_task)
self.app_logger.debug(f"[NetworkManager] Closed non-persistent connection for '{device['name']}'.")
def send_command_async(self, device_index, device, command, on_finish_callback=None):
def _send_task():
self.app_logger.debug(f"[NetworkManager] send_command_async: _send_task started for '{device['name']}'.")
try:
if device["protocol"] == "TCP":
self._connect_tcp_and_send(device, command, is_scheduled_task=False) # Not a scheduled task
elif device["protocol"] == "UDP":
# UDP send logic (simplified, assuming no response needed for async)
# For UDP, you'd typically create a socket, sendto, and then close it.
# No explicit 'connection' is made.
try:
udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
data_to_send = self._prepare_command_data(command)
udp_sock.sendto(data_to_send, (device["address"], device["port"]))
self._log_with_prefix(f"UDP指令 '{command['name']}' 已发送到设备 '{device['name']}' ({device['address']}:{device['port']})。", "green", False)
self.app_logger.debug(f"[NetworkManager] UDP command '{command['name']}' sent to '{device['name']}' (async).")
except Exception as e:
self._log_with_prefix(f"UDP指令 '{command['name']}' 发送失败到设备 '{device['name']}': {e}", "red", False)
self.app_logger.error(f"[NetworkManager] UDP command '{command['name']}' send failed to '{device['name']}': {e}")
finally:
if udp_sock:
udp_sock.close()
else:
self._log_with_prefix(f"不支持的协议 '{device['protocol']}'", "red", False)
self.app_logger.error(f"[NetworkManager] Unsupported protocol '{device['protocol']}' (async).")
except Exception as e:
# Errors are already logged by _connect_tcp_and_send or specific UDP logic
self.app_logger.error(f"[NetworkManager] send_command_async error for '{device['name']}': {e}")
finally:
if on_finish_callback:
on_finish_callback()
self.app_logger.debug(f"[NetworkManager] send_command_async: _send_task finished for '{device['name']}'.")
threading.Thread(target=_send_task).start()
def send_command_sync_for_scheduler(self, device, command, is_scheduled_task=True):
"""
为调度器同步发送指令,并返回响应。
此方法在后台线程中被调用。
"""
self.app_logger.debug(f"[NetworkManager] send_command_sync_for_scheduler called for '{device['name']}', command '{command['name']}'.")
if device["protocol"] == "TCP":
return self._connect_tcp_and_send(device, command, is_scheduled_task=is_scheduled_task)
elif device["protocol"] == "UDP":
# UDP logic for scheduler (simplified)
try:
udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
data_to_send = self._prepare_command_data(command)
udp_sock.sendto(data_to_send, (device["address"], device["port"]))
self._log_with_prefix(f"UDP指令 '{command['name']}' 已发送到设备 '{device['name']}' ({device['address']}:{device['port']})。", "green", is_scheduled_task)
self.app_logger.debug(f"[NetworkManager] UDP command '{command['name']}' sent to '{device['name']}' (scheduled).")
return "UDP command sent (no response expected)" # UDP typically doesn't have a direct response
except Exception as e:
self._log_with_prefix(f"UDP指令 '{command['name']}' 发送失败到设备 '{device['name']}': {e}", "red", is_scheduled_task)
self.app_logger.error(f"[NetworkManager] UDP command '{command['name']}' send failed to '{device['name']}': {e}")
raise RuntimeError(f"UDP command send failed: {e}")
finally:
if udp_sock:
udp_sock.close()
else:
self._log_with_prefix(f"不支持的协议 '{device['protocol']}'", "red", is_scheduled_task)
self.app_logger.error(f"[NetworkManager] Unsupported protocol '{device['protocol']}' (scheduled).")
raise ValueError(f"不支持的协议: {device['protocol']}")
def check_ip_reachability_async(self, device_index, device):
"""
使用 socket 尝试 TCP 连接来检查 IP 可达性。
此方法在后台线程中运行。
"""
def _check():
is_reachable = False
sock = None
target_address = device["address"]
target_port = device["port"] # 使用设备配置的端口进行检查
self.app_logger.debug(f"[NetworkManager] Starting IP reachability check for '{device['name']}' ({target_address}:{target_port}).")
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(1) # 设置一个短的超时时间例如1秒
# connect_ex() 方法尝试连接如果成功返回0否则返回错误码
result_code = sock.connect_ex((target_address, target_port))
if result_code == 0:
is_reachable = True
self.app_logger.debug(f"[NetworkManager] IP reachable: TCP connect successful for '{device['name']}' ({target_address}:{target_port}).")
else:
self.app_logger.debug(f"[NetworkManager] IP not reachable: TCP connect failed for '{device['name']}' ({target_address}:{target_port}) with error code {result_code}.")
except socket.timeout:
self.app_logger.debug(f"[NetworkManager] IP not reachable: TCP connect timeout for '{device['name']}' ({target_address}:{target_port}).")
except socket.error as e:
self.app_logger.debug(f"[NetworkManager] IP not reachable: TCP connect error for '{device['name']}' ({target_address}:{target_port}): {e}")
except Exception as e:
self.app_logger.error(f"[NetworkManager] Unexpected error during TCP reachability check for '{device['name']}': {e}")
finally:
if sock:
sock.close() # 确保关闭套接字
self.ip_status_callback(device_index, is_reachable)
self.app_logger.debug(f"[NetworkManager] IP reachability check for '{device['name']}' ({device['address']}:{device['port']}) finished. Status: {'Reachable' if is_reachable else 'Unreachable'}")
threading.Thread(target=_check).start()
def check_tcp_connection_async(self, device_index, device):
"""
检查 TCP 保持连接的状态。
如果设备是 keep_alive且 socket 存在,尝试发送空数据。
如果 socket 不存在,则尝试建立新的 keep_alive 连接。
此方法在后台线程中运行。
"""
def _check():
is_connected = False
sock_to_close = None # 用于临时创建的 socket
device_id = device["id"]
self.app_logger.debug(f"[NetworkManager] Starting TCP connection check for '{device['name']}' (keep_alive: {device.get('keep_alive')}).")
try:
if device.get("keep_alive"):
with self.socket_lock:
sock = self.sockets.get(device_id)
if sock:
try:
sock.send(b'') # Try sending dummy data to check connection
is_connected = True
self.app_logger.debug(f"[NetworkManager] TCP check: Keep-alive socket for '{device['name']}' is active.")
except (socket.error, BrokenPipeError):
self.app_logger.debug(f"[NetworkManager] TCP check: Keep-alive socket for '{device['name']}' broken, closing and removing.")
sock.close()
del self.sockets[device_id]
is_connected = False
else:
# No persistent socket yet, try to establish one to check
self.app_logger.debug(f"[NetworkManager] TCP check: No existing keep-alive for '{device['name']}', trying to establish new one.")
new_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
new_sock.settimeout(device.get("timeout", 5))
new_sock.connect((device["address"], device["port"]))
self.sockets[device_id] = new_sock # Store it for future use
is_connected = True
self.app_logger.debug(f"[NetworkManager] TCP check: New keep-alive socket established for '{device['name']}'.")
sock_to_close = None # Don't close, it's now persistent
else:
# 如果 keep_alive 是 False我们不在此处进行 TCP 连接检查,因为它是非保持连接。
# IP 可达性检查(通过 TCP 连接尝试)已经足够。
self.app_logger.debug(f"[NetworkManager] TCP check: Device '{device['name']}' is not keep-alive, skipping detailed TCP connection check.")
is_connected = False # 明确设置为 False因为没有保持连接
sock_to_close = None
except Exception as e:
self.app_logger.error(f"[NetworkManager] TCP connection check error for '{device['name']}': {e}")
is_connected = False
finally:
if sock_to_close: # 如果是临时创建的 socket 且未被存储为 persistent
sock_to_close.close()
self.tcp_status_callback(device_index, is_connected)
# 同时更新 IP 状态点,因为 TCP 连接成功必然意味着 IP 可达
self.ip_status_callback(device_index, is_connected) # <--- 新增:根据 TCP 状态更新 IP 状态点
threading.Thread(target=_check).start()
def check_udp_reachability_async(self, device_index, device):
"""
尝试发送一个UDP数据包来检查UDP端口的可达性。
注意:这只能确认数据包是否能发送出去,不能确认远程服务是否在监听。
"""
def _check():
is_reachable = False
sock = None
target_address = device["address"]
target_port = device["port"]
self.app_logger.debug(f"[NetworkManager] Starting UDP reachability check for '{device['name']}' ({target_address}:{target_port}).")
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(1) # 短暂超时
# 尝试发送一个空数据包或一个小的"心跳"数据包
sock.sendto(b'', (target_address, target_port))
is_reachable = True # 如果sendto没有抛出异常我们认为IP可达
self.app_logger.debug(f"[NetworkManager] UDP send successful for '{device['name']}' ({target_address}:{target_port}).")
except socket.timeout:
self.app_logger.debug(f"[NetworkManager] UDP send timeout for '{device['name']}' ({target_address}:{target_port}).")
except socket.error as e:
# 常见的错误可能是"Network is unreachable"或"Host is down"
self.app_logger.debug(f"[NetworkManager] UDP send failed for '{device['name']}' ({target_address}:{target_port}): {e}")
except Exception as e:
self.app_logger.error(f"[NetworkManager] Unexpected error during UDP reachability check for '{device['name']}': {e}")
finally:
if sock:
sock.close()
self.ip_status_callback(device_index, is_reachable)
self.app_logger.debug(f"[NetworkManager] UDP reachability check for '{device['name']}' ({device['address']}:{device['port']}) finished. Status: {'Reachable' if is_reachable else 'Unreachable'}")
threading.Thread(target=_check).start()
def close_connection_for_device(self, device):
"""关闭指定设备的保持连接"""
device_id = device["id"]
with self.socket_lock:
if device_id in self.sockets:
try:
self.sockets[device_id].close()
self.log_callback(f"已关闭设备 {device_id} 的保持连接。", "gray")
self.app_logger.debug(f"[NetworkManager] Closed keep-alive socket for device {device_id}.")
except Exception as e:
self.log_callback(f"关闭设备 {device_id} 的保持连接时出错: {e}", "red")
self.app_logger.error(f"[NetworkManager] Error closing keep-alive socket for device {device_id}: {e}")
finally:
del self.sockets[device_id]
def close_all_sockets(self):
with self.socket_lock:
for device_id, sock in self.sockets.items():
try:
sock.close()
self.log_callback(f"已关闭设备 {device_id} 的保持连接。", "gray")
self.app_logger.debug(f"[NetworkManager] Closed keep-alive socket for device {device_id}.")
except Exception as e:
self.log_callback(f"关闭设备 {device_id} 的保持连接时出错: {e}", "red")
self.app_logger.error(f"[NetworkManager] Error closing keep-alive socket for device {device_id}: {e}")
self.sockets.clear()

676
scheduler_app.py Normal file
View File

@@ -0,0 +1,676 @@
import tkinter as tk
from tkinter import ttk, messagebox
import json
from datetime import datetime, timedelta
import calendar
import uuid
from utils import center_toplevel_window
class SchedulerApp(tk.Toplevel):
def __init__(self, master, devices_data, scheduled_tasks, save_config_callback, log_message_callback):
super().__init__(master)
self.master = master # master在这里是MainControlApp的实例
self.title("定时任务管理")
self.geometry("800x600")
self.devices = devices_data
self.scheduled_tasks = scheduled_tasks
self.save_config_callback = save_config_callback
self.log_message_callback = log_message_callback
self.create_widgets()
self._update_schedule_display() # Initial display of tasks
# --- 修正点:在所有部件创建并加载数据后,再居中窗口 ---
center_toplevel_window(self, master)
# --- 修正点结束 ---
self.protocol("WM_DELETE_WINDOW", self.on_closing)
def create_widgets(self):
# Frame for buttons
button_frame = ttk.Frame(self)
button_frame.pack(side=tk.TOP, fill=tk.X, padx=10, pady=5)
ttk.Button(button_frame, text="添加任务", command=self.add_task).pack(side=tk.LEFT, padx=5)
ttk.Button(button_frame, text="保存并关闭", command=self.on_closing).pack(side=tk.RIGHT, padx=5)
# Frame for task list
task_list_frame = ttk.Frame(self)
task_list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
self.task_tree = ttk.Treeview(task_list_frame, columns=("Name", "Type", "Trigger", "Actions", "Enabled", "Last Run", "Next Run"), show="headings")
self.task_tree.heading("Name", text="任务名称")
self.task_tree.heading("Type", text="类型")
self.task_tree.heading("Trigger", text="触发条件")
self.task_tree.heading("Actions", text="执行指令")
self.task_tree.heading("Enabled", text="启用")
self.task_tree.heading("Last Run", text="上次运行")
self.task_tree.heading("Next Run", text="下次运行")
self.task_tree.column("Name", width=120, anchor=tk.W)
self.task_tree.column("Type", width=80, anchor=tk.CENTER)
self.task_tree.column("Trigger", width=150, anchor=tk.W)
self.task_tree.column("Actions", width=150, anchor=tk.W)
self.task_tree.column("Enabled", width=50, anchor=tk.CENTER)
self.task_tree.column("Last Run", width=120, anchor=tk.CENTER)
self.task_tree.column("Next Run", width=120, anchor=tk.CENTER)
self.task_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar = ttk.Scrollbar(task_list_frame, orient="vertical", command=self.task_tree.yview)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.task_tree.config(yscrollcommand=scrollbar.set)
self.task_tree.bind("<Double-1>", self.edit_selected_task)
self.task_tree.bind("<<TreeviewSelect>>", self.on_task_select)
# Context menu for tasks
self.context_menu = tk.Menu(self, tearoff=0)
self.context_menu.add_command(label="编辑任务", command=self.edit_selected_task)
self.context_menu.add_command(label="删除任务", command=self.delete_selected_task)
self.context_menu.add_command(label="启用/禁用任务", command=self.toggle_task_enabled)
self.task_tree.bind("<Button-3>", self.show_context_menu)
def on_task_select(self, event):
# This function is called when a task is selected, useful if you want to
# enable/disable buttons based on selection.
pass
def show_context_menu(self, event):
try:
self.task_tree.selection_set(self.task_tree.identify_row(event.y))
self.context_menu.tk_popup(event.x_root, event.y_root)
finally:
self.context_menu.grab_release()
def _update_schedule_display(self):
"""刷新任务列表显示"""
for i in self.task_tree.get_children():
self.task_tree.delete(i)
for task in self.scheduled_tasks:
task_name = task.get("name", "无名称")
# 调度类型显示为中文
schedule_type_display = {
"once": "一次性",
"daily": "每天",
"weekly": "每周",
"monthly": "每月"
}.get(task["schedule_type"], "未知")
trigger_str = self._get_trigger_string(task)
actions_str = self._get_actions_string(task)
enabled_str = "" if task["enabled"] else ""
last_run_str = task["last_run_time"].strftime("%Y-%m-%d %H:%M:%S") if task["last_run_time"] else "N/A"
next_run_str = task["next_run_time"].strftime("%Y-%m-%d %H:%M:%S") if task["next_run_time"] else "N/A"
self.task_tree.insert("", tk.END, iid=task["id"],
values=(task_name, schedule_type_display, trigger_str, actions_str, enabled_str, last_run_str, next_run_str))
def _get_trigger_string(self, task):
trigger = task["trigger"]
if task["schedule_type"] == "once":
return f"{trigger['year']}-{trigger['month']:02d}-{trigger['day']:02d} {trigger['hour']:02d}:{trigger['minute']:02d}:{trigger['second']:02d}"
elif task["schedule_type"] == "daily":
return f"每天 {trigger['hour']:02d}:{trigger['minute']:02d}:{trigger['second']:02d}"
elif task["schedule_type"] == "weekly":
day_names = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
days = ", ".join([day_names[d] for d in sorted(trigger["days_of_week"])])
return f"{days} {trigger['hour']:02d}:{trigger['minute']:02d}:{trigger['second']:02d}"
elif task["schedule_type"] == "monthly":
if "month_day" in trigger:
return f"每月 {trigger['month_day']}{trigger['hour']:02d}:{trigger['minute']:02d}:{trigger['second']:02d}"
elif "week_of_month" in trigger:
week_of_month_map = {1: "第一个", 2: "第二个", 3: "第三个", 4: "第四个", -1: "最后一个"}
day_names = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
return f"每月 {week_of_month_map.get(trigger['week_of_month'], '')}{day_names[trigger['day_of_week']]} {trigger['hour']:02d}:{trigger['minute']:02d}:{trigger['second']:02d}"
return "N/A"
def _get_actions_string(self, task):
if not task["actions"]:
return "无指令"
action_names = []
for action in task["actions"]:
device = next((d for d in self.devices if d["id"] == action["device_id"]), None)
if device and 0 <= action["command_index"] < len(device["commands"]):
cmd_name = device["commands"][action["command_index"]]["name"]
action_names.append(f"{device['name']}:{cmd_name}")
else:
action_names.append("未知设备/指令")
return ", ".join(action_names)
def add_task(self):
"""打开一个新窗口以添加或编辑任务"""
self.TaskEditor(self, None) # self是SchedulerApp实例作为TaskEditor的master_app
def edit_selected_task(self, event=None):
"""编辑选中的任务"""
selected_item = self.task_tree.focus()
if selected_item:
task_id = selected_item
task = next((t for t in self.scheduled_tasks if t["id"] == task_id), None)
if task:
self.TaskEditor(self, task)
else:
messagebox.showerror("错误", "未找到选中的任务。")
else:
messagebox.showwarning("提示", "请选择一个任务进行编辑。")
def delete_selected_task(self):
"""删除选中的任务"""
selected_item = self.task_tree.focus()
if selected_item:
if messagebox.askyesno("确认删除", "确定要删除选中的任务吗?"):
task_id = selected_item
self.scheduled_tasks[:] = [t for t in self.scheduled_tasks if t["id"] != task_id]
self.save_config_callback()
self.master._recalculate_all_next_run_times() # 重新计算所有任务的下次运行时间
self.master.refresh_scheduler_display() # 立即刷新主界面的调度器状态
self._update_schedule_display()
self.log_message_callback(f"定时任务 (ID: {task_id[:8]}...) 已删除。", "blue")
else:
messagebox.showwarning("提示", "请选择一个任务进行删除。")
def toggle_task_enabled(self):
"""切换选中任务的启用/禁用状态"""
selected_item = self.task_tree.focus()
if selected_item:
task_id = selected_item
task = next((t for t in self.scheduled_tasks if t["id"] == task_id), None)
if task:
task["enabled"] = not task["enabled"]
self.save_config_callback()
self.master._recalculate_all_next_run_times() # 重新计算所有任务的下次运行时间
self.master.refresh_scheduler_display() # 立即刷新主界面的调度器状态
self._update_schedule_display()
self.log_message_callback(f"定时任务 '{task.get('name', task_id[:8])}' (ID: {task_id[:8]}...) 已更新。", "blue")
else:
messagebox.showwarning("提示", "请选择一个任务进行操作。")
def on_closing(self):
"""关闭窗口时保存配置"""
self.save_config_callback()
self.master._recalculate_all_next_run_times() # 重新计算所有任务的下次运行时间
self.master.refresh_scheduler_display() # 立即刷新主界面的调度器状态
self.destroy()
class TaskEditor(tk.Toplevel):
def __init__(self, master_app, task_data=None):
super().__init__(master_app)
self.master_app = master_app # master_app在这里是SchedulerApp的实例
self.title("编辑定时任务" if task_data else "添加定时任务")
self.geometry("500x700")
self.task_data = task_data if task_data else self._create_new_task_template()
self.original_task_id = self.task_data["id"] # Store original ID for updates
# 定义调度类型的中英文映射
self.schedule_type_map = {
"once": "一次性",
"daily": "每天",
"weekly": "每周",
"monthly": "每月"
}
self.schedule_type_reverse_map = {v: k for k, v in self.schedule_type_map.items()}
self.create_widgets()
self._load_task_data()
# --- 修正点:在所有部件创建并加载数据后,再居中窗口 ---
center_toplevel_window(self, master_app)
# --- 修正点结束 ---
self.transient(master_app) # Make this window a transient window of the master
self.grab_set() # Grab all events until this window is destroyed
self.wait_window(self) # Wait until this window is destroyed
def _create_new_task_template(self):
now = datetime.now()
return {
"id": str(uuid.uuid4()),
"name": "新任务",
"schedule_type": "daily", # 保持英文
"trigger": {"hour": now.hour, "minute": now.minute + 1, "second": 0}, # Default to 1 min from now
"actions": [],
"enabled": True,
"last_run_time": None,
"next_run_time": None
}
def create_widgets(self):
main_frame = ttk.Frame(self, padding="10")
main_frame.pack(fill=tk.BOTH, expand=True)
# Task Name
ttk.Label(main_frame, text="任务名称:").grid(row=0, column=0, sticky=tk.W, pady=2)
self.name_entry = ttk.Entry(main_frame)
self.name_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), pady=2)
# Schedule Type
ttk.Label(main_frame, text="调度类型:").grid(row=1, column=0, sticky=tk.W, pady=2)
# 内部变量,存储英文键 (用于JSON)
self.schedule_type_internal_var = tk.StringVar(self)
# 显示变量,存储中文文本 (用于OptionMenu)
self.schedule_type_display_var = tk.StringVar(self)
# 初始化显示变量,默认显示“每天”
self.schedule_type_display_var.set(self.schedule_type_map.get(self.task_data["schedule_type"], "每天"))
self.schedule_type_optionmenu = ttk.OptionMenu(main_frame, self.schedule_type_display_var,
self.schedule_type_display_var.get(), # 默认显示值
*self.schedule_type_map.values(), # 选项为中文显示文本
command=self._on_schedule_type_change)
self.schedule_type_optionmenu.grid(row=1, column=1, sticky=(tk.W, tk.E), pady=2)
# Trigger Frame
self.trigger_frame = ttk.LabelFrame(main_frame, text="触发条件", padding="5")
self.trigger_frame.grid(row=2, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=5)
# 初始创建触发条件部件,此时 schedule_type_internal_var 尚未完全加载,但在 _load_task_data 会再次调用
self._create_trigger_widgets(self.trigger_frame)
# Actions Frame
self.actions_frame = ttk.LabelFrame(main_frame, text="执行指令", padding="5")
self.actions_frame.grid(row=3, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=5)
self._create_actions_widgets(self.actions_frame)
# Enabled Checkbutton
self.enabled_var = tk.BooleanVar(self)
self.enabled_checkbutton = ttk.Checkbutton(main_frame, text="启用任务", variable=self.enabled_var)
self.enabled_checkbutton.grid(row=4, column=0, columnspan=2, sticky=tk.W, pady=5)
# Save/Cancel Buttons
button_frame = ttk.Frame(main_frame)
button_frame.grid(row=5, column=0, columnspan=2, sticky=tk.E, pady=10)
ttk.Button(button_frame, text="保存", command=self._save_task).pack(side=tk.LEFT, padx=5)
ttk.Button(button_frame, text="取消", command=self.destroy).pack(side=tk.LEFT, padx=5)
main_frame.columnconfigure(1, weight=1)
def _create_trigger_widgets(self, parent_frame):
# Clear existing trigger widgets
for widget in parent_frame.winfo_children():
widget.destroy()
# Time inputs (common to all)
ttk.Label(parent_frame, text="时间 (HH:MM:SS):").grid(row=0, column=0, sticky=tk.W, pady=2)
self.hour_var = tk.StringVar(self)
self.minute_var = tk.StringVar(self)
self.second_var = tk.StringVar(self)
self.hour_spinbox = ttk.Spinbox(parent_frame, from_=0, to=23, wrap=True, width=3, format="%02.0f", textvariable=self.hour_var)
self.minute_spinbox = ttk.Spinbox(parent_frame, from_=0, to=59, wrap=True, width=3, format="%02.0f", textvariable=self.minute_var)
self.second_spinbox = ttk.Spinbox(parent_frame, from_=0, to=59, wrap=True, width=3, format="%02.0f", textvariable=self.second_var)
self.hour_spinbox.grid(row=0, column=1, sticky=tk.W, padx=(0,2))
ttk.Label(parent_frame, text=":").grid(row=0, column=2, sticky=tk.W)
self.minute_spinbox.grid(row=0, column=3, sticky=tk.W, padx=(0,2))
ttk.Label(parent_frame, text=":").grid(row=0, column=4, sticky=tk.W)
self.second_spinbox.grid(row=0, column=5, sticky=tk.W)
# 使用内部变量获取调度类型
schedule_type = self.schedule_type_internal_var.get()
if schedule_type == "once":
ttk.Label(parent_frame, text="日期 (YYYY-MM-DD):").grid(row=1, column=0, sticky=tk.W, pady=2)
self.year_var = tk.StringVar(self)
self.month_var = tk.StringVar(self)
self.day_var = tk.StringVar(self)
# --- 新增:如果当前是新建任务且调度类型为“一次性”,则自动填充为当前日期 ---
# 检查 self.task_data["trigger"] 中是否已包含日期信息,如果未包含,则说明是新选择“一次性”类型
if "year" not in self.task_data["trigger"]:
now = datetime.now()
self.year_var.set(str(now.year))
self.month_var.set(f"{now.month:02d}")
self.day_var.set(f"{now.day:02d}")
# --- 新增结束 ---
self.year_entry = ttk.Entry(parent_frame, width=5, textvariable=self.year_var)
self.month_entry = ttk.Entry(parent_frame, width=3, textvariable=self.month_var)
self.day_entry = ttk.Entry(parent_frame, width=3, textvariable=self.day_var)
self.year_entry.grid(row=1, column=1, sticky=tk.W, padx=(0,2))
ttk.Label(parent_frame, text="-").grid(row=1, column=2, sticky=tk.W)
self.month_entry.grid(row=1, column=3, sticky=tk.W, padx=(0,2))
ttk.Label(parent_frame, text="-").grid(row=1, column=4, sticky=tk.W)
self.day_entry.grid(row=1, column=5, sticky=tk.W)
elif schedule_type == "weekly":
ttk.Label(parent_frame, text="选择星期:").grid(row=1, column=0, sticky=tk.W, pady=2)
self.days_of_week_vars = []
day_names = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
for i, day_name in enumerate(day_names):
var = tk.BooleanVar(self)
cb = ttk.Checkbutton(parent_frame, text=day_name, variable=var)
cb.grid(row=1 + (i // 4), column=1 + (i % 4), sticky=tk.W, padx=2)
self.days_of_week_vars.append((i, var)) # Store (day_index, BooleanVar)
elif schedule_type == "monthly":
self.monthly_type_var = tk.StringVar(self)
self.monthly_type_var.set("month_day") # Default to specific day of month
ttk.Radiobutton(parent_frame, text="每月指定日期", variable=self.monthly_type_var, value="month_day", command=lambda: self._on_monthly_type_change(parent_frame)).grid(row=1, column=0, columnspan=3, sticky=tk.W)
ttk.Radiobutton(parent_frame, text="每月第N个星期X", variable=self.monthly_type_var, value="week_of_month", command=lambda: self._on_monthly_type_change(parent_frame)).grid(row=2, column=0, columnspan=3, sticky=tk.W)
self.monthly_specific_day_frame = ttk.Frame(parent_frame)
self.monthly_specific_day_frame.grid(row=1, column=3, columnspan=3, sticky=(tk.W, tk.E))
ttk.Label(self.monthly_specific_day_frame, text="日期:").pack(side=tk.LEFT)
self.month_day_var = tk.StringVar(self)
self.month_day_spinbox = ttk.Spinbox(self.monthly_specific_day_frame, from_=1, to=31, wrap=True, width=3, textvariable=self.month_day_var)
self.month_day_spinbox.pack(side=tk.LEFT, padx=2)
self.monthly_week_day_frame = ttk.Frame(parent_frame)
self.monthly_week_day_frame.grid(row=2, column=3, columnspan=3, sticky=(tk.W, tk.E))
ttk.Label(self.monthly_week_day_frame, text="").pack(side=tk.LEFT)
self.week_of_month_var = tk.StringVar(self)
self.week_of_month_optionmenu = ttk.OptionMenu(self.monthly_week_day_frame, self.week_of_month_var, "第一个", "第一个", "第二个", "第三个", "第四个", "最后一个")
self.week_of_month_optionmenu.pack(side=tk.LEFT, padx=2)
ttk.Label(self.monthly_week_day_frame, text="").pack(side=tk.LEFT)
self.day_of_week_var = tk.StringVar(self)
self.day_of_week_optionmenu = ttk.OptionMenu(self.monthly_week_day_frame, self.day_of_week_var, "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日")
self.day_of_week_optionmenu.pack(side=tk.LEFT, padx=2)
self._on_monthly_type_change(parent_frame) # Initialize visibility
def _on_schedule_type_change(self, selected_display_type):
# 将选中的中文显示文本映射回英文内部类型
internal_type = self.schedule_type_reverse_map.get(selected_display_type, "daily")
self.schedule_type_internal_var.set(internal_type) # 更新内部变量
# 根据内部类型重新创建触发条件部件
self._create_trigger_widgets(self.trigger_frame)
self._load_trigger_data() # 重新加载触发条件数据以填充新创建的部件
def _on_monthly_type_change(self, parent_frame):
if self.monthly_type_var.get() == "month_day":
self.monthly_specific_day_frame.grid(row=1, column=3, columnspan=3, sticky=(tk.W, tk.E))
self.monthly_week_day_frame.grid_forget()
else:
self.monthly_specific_day_frame.grid_forget()
self.monthly_week_day_frame.grid(row=2, column=3, columnspan=3, sticky=(tk.W, tk.E))
def _create_actions_widgets(self, parent_frame):
# Clear existing action widgets
for widget in parent_frame.winfo_children():
widget.destroy()
self.action_entries = [] # List to hold (device_dropdown, command_dropdown, delay_entry) for each action
# Add Action Button
ttk.Button(parent_frame, text="添加指令", command=self._add_action_row).pack(pady=5)
# Scrollable frame for actions
self.action_canvas = tk.Canvas(parent_frame)
self.action_scrollbar = ttk.Scrollbar(parent_frame, orient="vertical", command=self.action_canvas.yview)
self.action_scrollable_frame = ttk.Frame(self.action_canvas)
self.action_scrollable_frame.bind(
"<Configure>",
lambda e: self.action_canvas.configure(
scrollregion=self.action_canvas.bbox("all")
)
)
self.action_canvas.create_window((0, 0), window=self.action_scrollable_frame, anchor="nw")
self.action_canvas.configure(yscrollcommand=self.action_scrollbar.set)
self.action_canvas.pack(side="left", fill="both", expand=True)
self.action_scrollbar.pack(side="right", fill="y")
# Initial action rows if any
if self.task_data["actions"]:
for action in self.task_data["actions"]:
self._add_action_row(action)
def _add_action_row(self, action_data=None):
row_frame = ttk.Frame(self.action_scrollable_frame, padding=5, relief=tk.RIDGE, borderwidth=1)
row_frame.pack(fill=tk.X, pady=2)
# Device selection
ttk.Label(row_frame, text="设备:").pack(side=tk.LEFT)
device_names = [d["name"] for d in self.master_app.devices]
device_var = tk.StringVar(self)
# --- 修正点1: 确保 lambda 捕获正确的 command_dropdown 和 command_var ---
# 先创建 command_dropdown 和 command_var再创建 device_dropdown
command_var = tk.StringVar(self)
command_dropdown = ttk.OptionMenu(row_frame, command_var, "无指令") # Placeholder
device_dropdown = ttk.OptionMenu(row_frame, device_var, device_names[0] if device_names else "无设备", *device_names,
command=lambda selected_device_name, dv=device_var, cdd=command_dropdown, cv=command_var: self._on_device_select(selected_device_name, dv, cdd, cv))
device_dropdown.pack(side=tk.LEFT, padx=5)
# Command selection (will be updated based on device)
ttk.Label(row_frame, text="指令:").pack(side=tk.LEFT)
command_dropdown.pack(side=tk.LEFT, padx=5) # 修正点2: 放置 command_dropdown
# Delay input
ttk.Label(row_frame, text="延迟(ms):").pack(side=tk.LEFT)
delay_var = tk.StringVar(self)
delay_entry = ttk.Entry(row_frame, width=5, textvariable=delay_var)
delay_entry.pack(side=tk.LEFT, padx=5)
delay_var.set("0") # Default delay
# Delete button
delete_btn = ttk.Button(row_frame, text="X", command=lambda: self._delete_action_row(row_frame))
delete_btn.pack(side=tk.RIGHT)
self.action_entries.append((row_frame, device_var, device_dropdown, command_var, command_dropdown, delay_var))
# Initialize dropdowns if action_data is provided (editing existing task)
if action_data:
device = next((d for d in self.master_app.devices if d["id"] == action_data["device_id"]), None)
if device:
device_var.set(device["name"])
self._on_device_select(device["name"], device_var, command_dropdown, command_var)
if 0 <= action_data["command_index"] < len(device["commands"]):
command_var.set(device["commands"][action_data["command_index"]]["name"])
delay_var.set(str(action_data.get("delay_ms", 0)))
else: # For new rows, set default device if available
if device_names:
device_var.set(device_names[0])
self._on_device_select(device_names[0], device_var, command_dropdown, command_var)
self.action_scrollable_frame.update_idletasks()
self.action_canvas.config(scrollregion=self.action_canvas.bbox("all"))
def _delete_action_row(self, row_frame):
for i, (frame, _, _, _, _, _) in enumerate(self.action_entries):
if frame == row_frame:
self.action_entries.pop(i)
row_frame.destroy()
break
self.action_scrollable_frame.update_idletasks()
self.action_canvas.config(scrollregion=self.action_canvas.bbox("all"))
def _on_device_select(self, selected_device_name, device_var, command_dropdown, command_var):
device = next((d for d in self.master_app.devices if d["name"] == selected_device_name), None)
if device:
commands = device.get("commands", [])
command_names = [cmd["name"] for cmd in commands]
# Update the command dropdown
menu = command_dropdown["menu"]
menu.delete(0, "end")
if command_names:
for cmd_name in command_names:
menu.add_command(label=cmd_name, command=tk._setit(command_var, cmd_name))
command_var.set(command_names[0]) # Set default to first command
else:
menu.add_command(label="无指令", command=tk._setit(command_var, "无指令"))
command_var.set("无指令")
else:
menu = command_dropdown["menu"]
menu.delete(0, "end")
menu.add_command(label="无指令", command=tk._setit(command_var, "无指令"))
command_var.set("无指令")
def _load_task_data(self):
self.name_entry.delete(0, tk.END)
self.name_entry.insert(0, self.task_data["name"])
# 设置内部变量 (英文)
self.schedule_type_internal_var.set(self.task_data["schedule_type"])
# 设置显示变量 (中文)
self.schedule_type_display_var.set(self.schedule_type_map.get(self.schedule_type_internal_var.get(), "每天"))
# 触发更新逻辑,这会调用 _create_trigger_widgets 和 _load_trigger_data
self._on_schedule_type_change(self.schedule_type_display_var.get())
self.enabled_var.set(self.task_data["enabled"])
def _load_trigger_data(self):
trigger = self.task_data["trigger"]
self.hour_var.set(f"{trigger['hour']:02d}")
self.minute_var.set(f"{trigger['minute']:02d}")
self.second_var.set(f"{trigger['second']:02d}")
# 使用内部变量获取调度类型
if self.schedule_type_internal_var.get() == "once":
# 只有当 task_data["trigger"] 中有日期信息时才加载,否则保持 _create_trigger_widgets 中设置的当前日期
if "year" in trigger:
self.year_var.set(str(trigger["year"]))
self.month_var.set(f"{trigger['month']:02d}")
self.day_var.set(f"{trigger['day']:02d}")
elif self.schedule_type_internal_var.get() == "weekly":
if hasattr(self, 'days_of_week_vars'):
for i, var in self.days_of_week_vars:
var.set(i in trigger["days_of_week"])
elif self.schedule_type_internal_var.get() == "monthly":
if "month_day" in trigger:
self.monthly_type_var.set("month_day")
self.month_day_var.set(str(trigger["month_day"]))
elif "week_of_month" in trigger:
self.monthly_type_var.set("week_of_month")
week_of_month_map = {1: "第一个", 2: "第二个", 3: "第三个", 4: "第四个", -1: "最后一个"}
day_names = ["星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"]
self.week_of_month_var.set(week_of_month_map.get(trigger["week_of_month"], "第一个"))
self.day_of_week_var.set(day_names[trigger["day_of_week"]])
self._on_monthly_type_change(self.trigger_frame) # Update visibility after loading
def _save_task(self):
try:
# Basic validation
name = self.name_entry.get().strip()
if not name:
messagebox.showerror("错误", "任务名称不能为空。")
return
hour = int(self.hour_var.get())
minute = int(self.minute_var.get())
second = int(self.second_var.get())
if not (0 <= hour <= 23 and 0 <= minute <= 59 and 0 <= second <= 59):
messagebox.showerror("错误", "时间格式不正确。小时(0-23), 分钟(0-59), 秒(0-59)。")
return
new_trigger = {"hour": hour, "minute": minute, "second": second}
# 从内部变量获取调度类型 (英文)
schedule_type = self.schedule_type_internal_var.get()
if schedule_type == "once":
year = int(self.year_var.get())
month = int(self.month_var.get())
day = int(self.day_var.get())
try:
datetime(year, month, day) # Validate date
except ValueError:
messagebox.showerror("错误", "一次性任务日期无效。")
return
new_trigger.update({"year": year, "month": month, "day": day})
elif schedule_type == "weekly":
selected_days = [idx for idx, var in self.days_of_week_vars if var.get()]
if not selected_days:
messagebox.showerror("错误", "每周任务至少需要选择一天。")
return
new_trigger["days_of_week"] = selected_days
elif schedule_type == "monthly":
if self.monthly_type_var.get() == "month_day":
month_day = int(self.month_day_var.get())
if not (1 <= month_day <= 31):
messagebox.showerror("错误", "每月指定日期必须在1-31之间。")
return
new_trigger["month_day"] = month_day
else: # week_of_month
week_of_month_map_rev = {"第一个": 1, "第二个": 2, "第三个": 3, "第四个": 4, "最后一个": -1}
day_names_rev = {"星期一": 0, "星期二": 1, "星期三": 2, "星期四": 3, "星期五": 4, "星期六": 5, "星期日": 6}
week_of_month = week_of_month_map_rev.get(self.week_of_month_var.get())
day_of_week = day_names_rev.get(self.day_of_week_var.get())
if week_of_month is None or day_of_week is None:
messagebox.showerror("错误", "每月第N个星期X任务的设置无效。")
return
new_trigger["week_of_month"] = week_of_month
new_trigger["day_of_week"] = day_of_week
new_actions = []
for _, device_var, _, command_var, _, delay_var in self.action_entries:
selected_device_name = device_var.get()
selected_command_name = command_var.get()
delay = int(delay_var.get())
if selected_device_name == "无设备" or selected_command_name == "无指令":
messagebox.showerror("错误", "指令列表中有未选择设备或指令的项。")
return
if delay < 0:
messagebox.showerror("错误", "指令延迟不能为负数。")
return
device = next((d for d in self.master_app.devices if d["name"] == selected_device_name), None)
if not device:
messagebox.showerror("错误", f"设备 '{selected_device_name}' 未找到。")
return
command_index = -1
for i, cmd in enumerate(device["commands"]):
if cmd["name"] == selected_command_name:
command_index = i
break
if command_index == -1:
messagebox.showerror("错误", f"指令 '{selected_command_name}' 在设备 '{selected_device_name}' 中未找到。")
return
new_actions.append({
"device_id": device["id"],
"command_index": command_index,
"delay_ms": delay
})
if not new_actions:
messagebox.showerror("错误", "请至少添加一个执行指令。")
return
# Update or add task
self.task_data.update({
"name": name,
"schedule_type": schedule_type, # 保存英文键
"trigger": new_trigger,
"actions": new_actions,
"enabled": self.enabled_var.get()
})
# Clear last_run_time and next_run_time for re-calculation
self.task_data["last_run_time"] = None
self.task_data["next_run_time"] = None
# If it's a new task, add it to the list
if self.original_task_id not in [t["id"] for t in self.master_app.scheduled_tasks]:
self.master_app.scheduled_tasks.append(self.task_data)
self.master_app.save_config_callback()
# 修正此处:通过 master_app.master 访问 MainControlApp 的方法
self.master_app.master._recalculate_all_next_run_times() # 重新计算所有任务的下次运行时间
self.master_app.master.refresh_scheduler_display() # 立即刷新主界面的调度器状态
self.master_app._update_schedule_display() # Refresh the list in SchedulerApp
self.master_app.log_message_callback(f"定时任务 '{name}' (ID: {self.task_data['id'][:8]}...) 已保存。", "blue")
self.destroy()
except ValueError as e:
messagebox.showerror("输入错误", f"请检查输入值是否正确: {e}")
except Exception as e:
messagebox.showerror("错误", f"保存任务时发生未知错误: {e}")

379
settings_app.py Normal file
View File

@@ -0,0 +1,379 @@
import tkinter as tk
from tkinter import ttk, messagebox
import json
import uuid
import re # 用于验证IP地址和端口
from utils import center_toplevel_window
class SettingsApp(tk.Toplevel):
def __init__(self, master, on_save_callback, devices_data):
super().__init__(master)
self.master = master
self.on_save_callback = on_save_callback
self.original_devices_data = json.loads(json.dumps(devices_data)) # 深拷贝一份原始数据
self.devices = devices_data # 引用主应用中的设备列表
self.title("设备设置")
self.geometry("800x600")
self.create_widgets()
self.load_devices_to_treeview()
# --- 修正点:在所有部件创建并加载数据后,再居中窗口 ---
center_toplevel_window(self, master)
# --- 修正点结束 ---
self.protocol("WM_DELETE_WINDOW", self.on_closing)
# 使 SettingsApp 成为模态窗口
self.transient(master)
self.grab_set()
self.wait_window(self)
def create_widgets(self):
# Top buttons frame
top_buttons_frame = ttk.Frame(self)
top_buttons_frame.pack(side=tk.TOP, fill=tk.X, padx=10, pady=5)
ttk.Button(top_buttons_frame, text="添加设备", command=self.add_device).pack(side=tk.LEFT, padx=5)
ttk.Button(top_buttons_frame, text="编辑设备", command=self.edit_selected_device).pack(side=tk.LEFT, padx=5)
ttk.Button(top_buttons_frame, text="删除设备", command=self.delete_selected_device).pack(side=tk.LEFT, padx=5)
ttk.Button(top_buttons_frame, text="保存并关闭", command=self.on_closing).pack(side=tk.RIGHT, padx=5)
# Device list Treeview
self.device_tree = ttk.Treeview(self, columns=("Name", "Type", "Protocol", "Address", "Port", "Keep Alive"), show="headings")
self.device_tree.heading("Name", text="设备名称")
self.device_tree.heading("Type", text="设备类型")
self.device_tree.heading("Protocol", text="协议")
self.device_tree.heading("Address", text="地址")
self.device_tree.heading("Port", text="端口")
self.device_tree.heading("Keep Alive", text="保持连接")
self.device_tree.column("Name", width=120)
self.device_tree.column("Type", width=100)
self.device_tree.column("Protocol", width=80)
self.device_tree.column("Address", width=120)
self.device_tree.column("Port", width=60)
self.device_tree.column("Keep Alive", width=80, anchor=tk.CENTER)
self.device_tree.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
scrollbar = ttk.Scrollbar(self, orient="vertical", command=self.device_tree.yview)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.device_tree.config(yscrollcommand=scrollbar.set)
self.device_tree.bind("<Double-1>", self.edit_selected_device)
def load_devices_to_treeview(self):
"""将设备数据加载到 Treeview"""
for i in self.device_tree.get_children():
self.device_tree.delete(i)
for device in self.devices:
self.device_tree.insert("", tk.END, iid=device["id"],
values=(device["name"], device["type"], device["protocol"],
device["address"], device["port"], "" if device["keep_alive"] else ""))
def add_device(self):
"""打开一个新窗口以添加设备"""
self.DeviceEditor(self, None)
def edit_selected_device(self, event=None):
"""编辑选中的设备"""
selected_item = self.device_tree.focus()
if selected_item:
device_id = selected_item
device = next((d for d in self.devices if d["id"] == device_id), None)
if device:
self.DeviceEditor(self, device)
else:
messagebox.showerror("错误", "未找到选中的设备。")
else:
messagebox.showwarning("提示", "请选择一个设备进行编辑。")
def delete_selected_device(self):
"""删除选中的设备"""
selected_item = self.device_tree.focus()
if selected_item:
if messagebox.askyesno("确认删除", "确定要删除选中的设备吗?", parent=self):
device_id = selected_item
self.devices[:] = [d for d in self.devices if d["id"] != device_id]
self.on_save_callback(self.devices) # 通知主应用保存
self.load_devices_to_treeview()
else:
messagebox.showwarning("提示", "请选择一个设备进行删除。", parent=self)
def on_closing(self):
"""关闭窗口时保存配置"""
# 检查是否有设备被删除,通知 network_manager 关闭连接
original_device_ids = {d["id"] for d in self.original_devices_data}
current_device_ids = {d["id"] for d in self.devices}
deleted_device_ids = original_device_ids - current_device_ids
for device_id in deleted_device_ids:
# 找到对应的原始设备信息,以便关闭连接
deleted_device = next((d for d in self.original_devices_data if d["id"] == device_id), None)
if deleted_device and deleted_device.get("keep_alive"):
self.master.network_manager.close_connection_for_device(deleted_device)
self.on_save_callback(self.devices)
self.destroy()
class DeviceEditor(tk.Toplevel):
def __init__(self, master_app, device_data=None):
super().__init__(master_app)
self.master_app = master_app # SettingsApp instance
self.device_data = device_data # Existing device data or None for new device
self.title("编辑设备" if device_data else "添加设备")
self.geometry("470x650") # 增加高度以确保所有控件可见
self.create_widgets()
if self.device_data:
self._load_device_data()
# --- 修正点:在所有部件创建并加载数据后,再居中窗口 ---
center_toplevel_window(self, master_app)
# --- 修正点结束 ---
self.transient(master_app)
self.grab_set()
self.wait_window(self)
def create_widgets(self):
main_frame = ttk.Frame(self, padding="10")
main_frame.pack(fill=tk.BOTH, expand=True)
# Device Name
ttk.Label(main_frame, text="设备名称:").grid(row=0, column=0, sticky=tk.W, pady=2)
self.name_entry = ttk.Entry(main_frame)
self.name_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), pady=2)
# Device Type
ttk.Label(main_frame, text="设备类型:").grid(row=1, column=0, sticky=tk.W, pady=2)
self.type_var = tk.StringVar(self)
self.type_entry = ttk.Entry(main_frame, textvariable=self.type_var)
self.type_entry.grid(row=1, column=1, sticky=(tk.W, tk.E), pady=2)
# Protocol
ttk.Label(main_frame, text="协议:").grid(row=2, column=0, sticky=tk.W, pady=2)
self.protocol_var = tk.StringVar(self)
self.protocol_optionmenu = ttk.OptionMenu(main_frame, self.protocol_var, "TCP", "TCP", "UDP")
self.protocol_optionmenu.grid(row=2, column=1, sticky=(tk.W, tk.E), pady=2)
# Address
ttk.Label(main_frame, text="地址 (IP/域名):").grid(row=3, column=0, sticky=tk.W, pady=2)
self.address_entry = ttk.Entry(main_frame)
self.address_entry.grid(row=3, column=1, sticky=(tk.W, tk.E), pady=2)
# Port
ttk.Label(main_frame, text="端口:").grid(row=4, column=0, sticky=tk.W, pady=2)
self.port_entry = ttk.Entry(main_frame)
self.port_entry.grid(row=4, column=1, sticky=(tk.W, tk.E), pady=2)
# Keep Alive
self.keep_alive_var = tk.BooleanVar(self)
self.keep_alive_checkbutton = ttk.Checkbutton(main_frame, text="保持连接", variable=self.keep_alive_var)
self.keep_alive_checkbutton.grid(row=5, column=0, columnspan=2, sticky=tk.W, pady=5)
# Commands Frame
self.commands_frame = ttk.LabelFrame(main_frame, text="指令列表", padding="5")
self.commands_frame.grid(row=6, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), pady=5)
self._create_commands_widgets(self.commands_frame)
# Save/Cancel Buttons
button_frame = ttk.Frame(main_frame)
button_frame.grid(row=7, column=0, columnspan=2, sticky=tk.E, pady=10)
ttk.Button(button_frame, text="保存", command=self._save_device).pack(side=tk.LEFT, padx=5)
ttk.Button(button_frame, text="取消", command=self.destroy).pack(side=tk.LEFT, padx=5)
main_frame.columnconfigure(1, weight=1)
main_frame.rowconfigure(6, weight=1) # 允许 commands_frame 垂直扩展
main_frame.rowconfigure(7, weight=0) # 确保按钮行不会扩展
def _create_commands_widgets(self, parent_frame):
# Clear existing command widgets
for widget in parent_frame.winfo_children():
widget.destroy()
self.command_entries = [] # List to hold (name_entry, data_entry, type_var, type_optionmenu) for each command
# Add Command Button
ttk.Button(parent_frame, text="添加指令", command=self._add_command_row).pack(pady=5)
# Scrollable frame for commands
self.command_canvas = tk.Canvas(parent_frame)
self.command_scrollbar = ttk.Scrollbar(parent_frame, orient="vertical", command=self.command_canvas.yview)
self.command_scrollable_frame = ttk.Frame(self.command_canvas)
# Bind the canvas's configure event to resize the inner frame
self.command_canvas.bind('<Configure>', self._on_command_canvas_resize)
self.command_canvas.create_window((0, 0), window=self.command_scrollable_frame, anchor="nw", tags="scrollable_frame_window")
self.command_canvas.configure(yscrollcommand=self.command_scrollbar.set)
self.command_canvas.pack(side="left", fill="both", expand=True)
self.command_scrollbar.pack(side="right", fill="y")
# Initial command rows if any
if self.device_data and self.device_data.get("commands"):
for command in self.device_data["commands"]:
self._add_command_row(command)
def _on_command_canvas_resize(self, event):
# Update the width of the scrollable frame to match the canvas width
self.command_canvas.itemconfig("scrollable_frame_window", width=event.width)
# Update scrollregion
self.command_canvas.configure(scrollregion=self.command_canvas.bbox("all"))
def _add_command_row(self, command_data=None):
row_frame = ttk.Frame(self.command_scrollable_frame, padding=5, relief=tk.RIDGE, borderwidth=1)
row_frame.pack(fill=tk.X, expand=True, pady=2) # Make row_frame expand horizontally
# Command Name
ttk.Label(row_frame, text="名称:").pack(side=tk.LEFT)
name_entry = ttk.Entry(row_frame, width=10)
name_entry.pack(side=tk.LEFT, padx=2)
# Command Data
ttk.Label(row_frame, text="数据:").pack(side=tk.LEFT)
data_entry = ttk.Entry(row_frame, width=20) # Keep initial width
data_entry.pack(side=tk.LEFT, padx=2, fill=tk.X, expand=True) # Allow data_entry to expand
# Command Type
ttk.Label(row_frame, text="类型:").pack(side=tk.LEFT)
type_var = tk.StringVar(self)
type_optionmenu = ttk.OptionMenu(row_frame, type_var, "text", "text", "hex")
type_optionmenu.pack(side=tk.LEFT, padx=2)
# Delete button
delete_btn = ttk.Button(row_frame, text="X", command=lambda: self._delete_command_row(row_frame))
delete_btn.pack(side=tk.RIGHT)
self.command_entries.append((row_frame, name_entry, data_entry, type_var, type_optionmenu))
if command_data:
name_entry.insert(0, command_data.get("name", ""))
data_entry.insert(0, command_data.get("data", ""))
type_var.set(command_data.get("type", "text"))
self.command_scrollable_frame.update_idletasks()
self.command_canvas.config(scrollregion=self.command_canvas.bbox("all"))
def _delete_command_row(self, row_frame):
for i, (frame, _, _, _, _) in enumerate(self.command_entries):
if frame == row_frame:
self.command_entries.pop(i)
row_frame.destroy()
break
self.command_scrollable_frame.update_idletasks()
self.command_canvas.config(scrollregion=self.command_canvas.bbox("all"))
def _load_device_data(self):
self.name_entry.insert(0, self.device_data["name"])
self.type_var.set(self.device_data["type"])
self.address_entry.insert(0, self.device_data["address"])
self.port_entry.insert(0, str(self.device_data["port"]))
self.protocol_var.set(self.device_data["protocol"])
self.keep_alive_var.set(self.device_data["keep_alive"])
def _save_device(self):
name = self.name_entry.get().strip()
device_type = self.type_var.get().strip()
protocol = self.protocol_var.get()
address = self.address_entry.get().strip()
port_str = self.port_entry.get().strip()
keep_alive = self.keep_alive_var.get()
# Validation
if not name:
messagebox.showerror("错误", "设备名称不能为空。")
return
if not device_type:
messagebox.showerror("错误", "设备类型不能为空。")
return
if not address:
messagebox.showerror("错误", "地址不能为空。")
return
if not port_str:
messagebox.showerror("错误", "端口不能为空。")
return
try:
port = int(port_str)
if not (1 <= port <= 65535):
raise ValueError
except ValueError:
messagebox.showerror("错误", "端口号必须是1到65535之间的整数。")
return
# Basic IP/Domain validation
if not re.match(r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$|^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,6}$", address):
messagebox.showerror("错误", "地址格式不正确 (IP地址或域名)。")
return
commands = []
for _, name_entry, data_entry, type_var, _ in self.command_entries:
cmd_name = name_entry.get().strip()
cmd_data = data_entry.get().strip()
cmd_type = type_var.get()
if not cmd_name:
messagebox.showerror("错误", "指令名称不能为空。")
return
if not cmd_data:
messagebox.showerror("错误", "指令数据不能为空。")
return
if cmd_type == "hex":
try:
bytes.fromhex(cmd_data) # Validate hex string
except ValueError:
messagebox.showerror("错误", f"指令 '{cmd_name}' 的十六进制数据格式不正确。")
return
commands.append({
"name": cmd_name,
"data": cmd_data,
"type": cmd_type
})
if not commands:
messagebox.showerror("错误", "请至少添加一个指令。")
return
# Update existing device or add new one
if self.device_data:
# Find the device by ID and update it
for i, d in enumerate(self.master_app.devices):
if d["id"] == self.device_data["id"]:
self.master_app.devices[i].update({
"name": name,
"type": device_type,
"protocol": protocol,
"address": address,
"port": port,
"keep_alive": keep_alive,
"commands": commands
})
self.master_app.master.log_message(f"设备 '{name}' (ID: {self.device_data['id'][:8]}...) 已更新。", "blue")
break
else:
# Add new device
new_device = {
"id": str(uuid.uuid4()),
"name": name,
"type": device_type,
"protocol": protocol,
"address": address,
"port": port,
"keep_alive": keep_alive,
"commands": commands
}
self.master_app.devices.append(new_device)
self.master_app.master.log_message(f"新设备 '{name}' (ID: {new_device['id'][:8]}...) 已添加。", "blue")
self.master_app.on_save_callback(self.master_app.devices) # 通知 SettingsApp 保存并刷新
self.master_app.load_devices_to_treeview() # 刷新设备列表显示
self.destroy() # Close the editor window

27
utils.py Normal file
View File

@@ -0,0 +1,27 @@
import tkinter as tk
def center_toplevel_window(toplevel_window, parent_window):
"""
将一个 Toplevel 窗口相对于其父窗口居中。
:param toplevel_window: 要居中的 Toplevel 窗口实例。
:param parent_window: 父窗口实例 (通常是 tk.Tk 或另一个 Toplevel)。
"""
toplevel_window.update_idletasks() # 确保窗口已经渲染,以便获取正确的尺寸
# 获取父窗口的几何信息
parent_width = parent_window.winfo_width()
parent_height = parent_window.winfo_height()
parent_x = parent_window.winfo_x()
parent_y = parent_window.winfo_y()
# 获取 Toplevel 窗口自身的几何信息
window_width = toplevel_window.winfo_width()
window_height = toplevel_window.winfo_height()
# 计算新的 x, y 坐标,使 Toplevel 窗口居中
x = parent_x + (parent_width // 2) - (window_width // 2)
y = parent_y + (parent_height // 2) - (window_height // 2)
# 设置 Toplevel 窗口的几何位置
toplevel_window.geometry(f"+{x}+{y}")

View File

@@ -0,0 +1,38 @@
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['main_control_app.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='网络设备控制器',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)