资讯动态

COMTool 开发笔记精读:快捷键发送、国际化、TCP/UDP 连接与终端渲染的实现剖析

发布时间:2026/10/14 0:41:26 来源:尧图企业网站定制
开发工具嵌入式串口调试【免费下载链接】COMToolCross platform communicate assistant(Serial/network/terminal tool) 跨平台 串口调试助手 网络调试助手 终端工具 linux windows mac Raspberry Pi 支持插件和二次开发项目地址https://gitcode.com/gh_mirrors/co/COMTool点击查看免费下载COMTool 是一款跨平台的通信调试助手支持串口、TCP/UDP 网络调试与 SSH 终端并采用插件化架构。本文基于仓库中的开发笔记 docs/dev.md逐条还原其记录的核心功能思路协议插件的快捷键发送机制、国际化i18n初始化顺序、TCP/UDP 连接模块的模块化设计、基于 pyte 的终端渲染线程模型以及已废弃的插件间数据继承方案并结合源码给出每个功能的关键实现位置与配置参数说明。读完后你可以理解 COMTool 各核心模块的底层设计并具备参与二次开发或编写插件所需的技术背景。一、协议插件中的快捷键发送ModeButtonEventFilter 事件拦截docs/dev.md 记录的第一个功能是协议插件protocol的快捷键发送。官方思路只有一句话给 Key mode 按钮定义了一个 eventFilter拦截所有按键按下和抬起操作对应实现是 protocol.py 中的ModeButtonEventFilter类。从源码看这套机制由三个部分协作完成事件拦截器ModeButtonEventFilterprotocol.pyclass ModeButtonEventFilter(QObject): def __init__(self, keyPressCb, keyReleaseCb) - None: super().__init__() self.keyPressCb keyPressCb self.keyReleaseCb keyReleaseCb def eventFilter(self, obj, evt): if evt.type() QEvent.KeyPress: # prevent default key events self.keyPressCb(evt) return True elif evt.type() QEvent.KeyRelease: self.keyReleaseCb(evt) return True return False它对KeyPress和KeyRelease事件全部返回True吞掉事件防止默认按键行为并分发给两个回调。承载拦截器的按钮ModeButtonprotocol.py继承QPushButton在构造函数中installEventFilter(eventFilter)并额外提供onFocusIn/onFocusOut两个 pyqtSignal。在onWidgetMain中可以看到它与拦截器的绑定方式protocol.pyself.keyBtneventFilter self.ModeButtonEventFilter( self.onModeBtnKeyPressEvent, self.onModeBtnKeyReleaseEvent) self.keyModeBtn self.ModeButton(_(Key mode), self.keyBtneventFilter)组合键匹配逻辑onModeBtnKeyPressEventprotocol.py把每次按下的键值event.key()即 Qt 的 key code 整数依次压入self.pressedKeys然后与config[customSendItems]中每个发送项的shortcut列表做逐位比较一旦当前按住序列与某条 shortcut 完全一致就调用sendCustomItem(item)发出该条自定义数据。onModeBtnKeyReleaseEvent负责把抬起的键从pressedKeys中移除从而支持先按 Esc 再按 Left这类多键组合。shortcut 的数据结构保存在插件配置里默认值可见onInitprotocol.py例如shortcut: [[16777216, Esc]]——第一个元素是 Qt key code第二个元素是用于 Key mode 下按钮文字展示的键名。用户可在界面上通过EditRemarDialog为每条自定义发送项编辑 shortcutprotocol.py。值得注意的细节是keyModeTuggleprotocol.pyKey mode 按钮通过 focusIn/focusOut 来开关快捷键模式而keyModeClickTime用于在 0.2 秒内抑制刚获得焦点就点了一下导致的误切换退出 key mode 时会清空pressedKeys注释中说明这是为了避免 ALTTAB 切换窗口时残留按键状态造成的 bug。二、国际化支持先加载配置与语言再初始化其它内容docs/dev.md 对国际化的描述是程序执行时先从文件加载配置设置语言再初始化其它内容目的是保证在代码任何地方都能通过 i18n 模块正确地获取到翻译。 这句话点出了整个 i18n 方案的关键语言必须在任何业务代码执行前就绪。2.1 启动顺序证据main2.py 中的加载时序main2.py 的模块级代码完整印证了这一顺序# Init lanuage first to ensure use function _ works correctly try: import parameters import i18n except ImportError: from COMTool import parameters from COMTool import i18n def loadConfig(): paramObj parameters.Parameters() paramObj.load(parameters.configFilePath) return paramObj log parameters.log log.i(loading config from, parameters.configFilePath) programConfig loadConfig() log.i(loading config complete) i18n.set_locale(programConfig[locale])即导入parameters→ 从配置文件Linux/macOS 下为~/.config/comtool/config.json见 parameters.py读取locale配置项 → 调用i18n.set_locale(locale)→ 之后才import其余业务模块helpAbout、Combobox、plugins等。由于各模块在导入时就会执行如name _(protocol)protocol.py这样的模块级翻译调用这个时序保证了模块导入期拿到的就已经是翻译后的文本。2.2 i18n 模块的核心实现i18n.py 基于 Python 标准库gettextbabel支持四种语言locales[en, zh_CN, zh_TW, ja] locale en tr lambda x:x def _(text): return tr(text)_()就是全局翻译函数未初始化时是恒等函数透传英文原文。set_localei18n.py负责加载编译后的.mo二进制文件并把tr替换为lang.gettextdef set_locale(locale_in): global locale, tr, root_dir locale locale_in locales_path os.path.join(root_dir, locales) if not os.path.exists(locales_path): # for pyinstaller pack locales_path os.path.join(os.path.dirname(root_dir), locales) # check translate binary file mo_path os.path.join(locales_path, en, LC_MESSAGES, messages.mo) if not os.path.exists(mo_path): main(finish) lang gettext.translation(messages, localedirlocales_path, languages[locale]) tr lang.gettext其中mo_path不存在时会自动调用main(finish)现场编译——这保证了即使仓库里只有.po源文件本仓库 COMTool/locales 目录下存放的正是messages.po与messages.pot模板程序首次运行时也能自动编译出.mo同时兼容 PyInstaller 打包后的目录结构。2.3 翻译工具链prepare / finish 两个命令i18n.py 的main(cmd, path)封装了 babel 的消息处理流程并提供了命令行入口cli_main-p/--path可指向插件工程根目录prepare执行extract按 COMTool/babel.cfg内容为[python: **.py]从源码提取所有待翻译串到locales/messages.pot再对每个 locale 执行update已存在.po或init新建finish对每个 locale 执行compile把.po编译为LC_MESSAGES/messages.mo。对应函数extract/init/update/compile见 i18n.py它们均是对babel.messages.frontend中extract_messages、init_catalog、update_catalog、compile_catalog的封装。插件工程同样适用外部插件目录如 COMTool/plugins/myplugin2/comtool_plugin_myplugin2自带完整locales/结构说明该工具链支持按-p 插件根目录单独运行。运行期语言切换见 main2.py 的onLanguageChanged更新config[locale]、调用i18n.set_locale(locale)后弹窗询问是否立即重启——由于大量文本在模块导入时已求值重启由 COMTool/Main.py 中main()返回None触发restart_program()用os.execl重拉进程是让全部文本切换到新语言的完整方案。三、TCP/UDP 连接支持模块化插件化连接类设计docs/dev.md 第三条写道使用模块化/插件化开发在 conn/conn_tcp_udp.py 中定义了这个类可以直接执行 conn/test_tcp_udp.py 测试模块。 这条笔记对应的是把网络连接从 UI 中剥离、做成独立可测模块的架构决策。3.1 COMM 基类连接模块的统一契约所有连接模块串口、TCP/UDP、SSH都继承 conn/base.py 的COMM(QObject)其约定了固定调用序列与能力接口call sequence: onInit # 初始化参数禁止耗时 onWidget # 返回连接配置 QWidgetUI 线程勿阻塞 onUiInitDone # UI 初始化完成回填配置 isConnected / getConnStatus send # send(data: bytes) getConfig # 返回 dict 型配置 onDel # 清理资源、等待线程退出COMM还定义了三个关键信号/回调onReceived接收数据回调运行在接收线程、onConnectionStatus pyqtSignal(ConnectionStatus, str)与hintSignal pyqtSignal(str, str, str)。连接状态枚举同样定义在此文件conn/base.pyclass ConnectionStatus(Enum): CLOSED 0 CONNECTED 1 LOSE 2 CONNECTING 3conn/init.py 把Serial、TCP_UDP、SSH统一导出为conns列表主窗口按此列表为每个标签页实例化全部连接类型由用户在设置区下拉框中切换见 pluginItems.py 的newConnWidgets。3.2 TCP_UDP 类配置参数与默认值TCP_UDP类定义于 conn/conn_tcp_udp.pyonInit中给出的完整配置项与默认值如下配置项类型默认值含义protocolstrtcp协议类型tcp或udpmodestrclient模式client客户端或server服务端监听target[str, list][127.0.0.1:2345, [127.0.0.1:2345]]目标地址[0]为当前值[1]为历史列表自动记忆portint2345server/UDP 模式绑定的本地端口auto_reconnectboolFalse断线后是否自动重连auto_reconnect_intervalfloat1.0重连间隔秒配置缺失时会按default字典补齐conn_tcp_udp.pywidgetConfMap则把每个配置项映射到对应的控件onUiInitDone遍历回填conn_tcp_udp.py。3.3 连接建立四种组合分支点击 OPEN/CLOSE 按钮后openCloseSerialconn_tcp_udp.py并不在主线程直接操作 socket而是启动一个 daemon 线程执行openCloseSerialProcess用self.busy标志防止重复点击。openCloseSerialProcessconn_tcp_udp.py按protocol × mode分四个分支TCP clientsocket.socket()→connect(target)目标由checkTarget解析支持全角冒号、http://前缀等容错conn_tcp_udp.py→ 记录本地getsockname()回显到 Local 输入框 → 启动receiveDataProcess接收线程TCP serversetsockopt(SO_REUSEADDR)→bind((0.0.0.0, port))→listen(100)→ 启动waitClientsProcess线程。该线程以settimeout(0.06)的非阻塞方式轮询accept()从而能在主状态切为CLOSED后及时退出conn_tcp_udp.py每接受一个客户端就为其单独开一个receiveDataProcess线程并通过updateClientsSignal把ip:port加入客户端下拉框实现广播给所有客户端 / 指定单个客户端的发送选择serverModeSelectedClient为None表示全部UDPsocket.socket(typesocket.SOCK_DGRAM)→bind((0.0.0.0, port))→ 接收线程用recvfrom(4096)获取数据与来源地址发送走sendto(data, target)conn_tcp_udp.py任何异常都会关闭 socket、把状态置为CLOSED并经hintSignal弹出 Connect Failed 提示。接收线程receiveDataProcessconn_tcp_udp.py中还有两个值得注意的工程细节批量投递conn.settimeout(0.1)数据先累积进buffer满足 flush 条件无新数据超过 1ms才整体回调self.onReceived(buffer)降低回调频率TCP 中recv返回b视为对端 FIN 关闭抛出 Closed by peer自动重连当连接异常且auto_reconnect开启且非 server 模式时进入waitingReconnect循环按auto_reconnect_interval间隔重建 socket重连期间状态显示为LOSE。3.4 独立测试不依赖主程序的模块验证docs/dev.md 提到可直接执行 conn/test_tcp_udp.py 测试该模块。这个测试脚本test_tcp_udp.py展示了COMM契约的最小宿主写法conn TCP_UDP() conn.onInit({}) class Event(): def onReceived(self, data): print(-- received:, data) def onConnection(self, status, msg): if status ConnectionStatus.CONNECTED: self.conn.send(GET http://example.com HTTP/1.1 ....encode()) event Event(conn) conn.onReceived event.onReceived conn.onConnectionStatus.connect(event.onConnection) conn.hintSignal.connect(event.onHint) window conn.onWidget() conn.onUiInitDone() window.show()它按onInit → onWidget → onUiInitDone的顺序驱动连接对象仅注入onReceived/onConnectionStatus/hintSignal三个回调就能把 TCP_UDP 的配置界面单独拉起来运行——这正是模块化/插件化设计带来的可测试性连接模块不需要知道主窗口、插件系统的任何细节。四、终端paramiko pyte pixmap 的线程安全渲染docs/dev.md 的终端一节指出技术选型为paramiko作为 SSH 后端、pyte解析 VT100 格式数据、用pixmap绘制后刷到QWidget。并且特别强调了刷新/绘制的线程约束接收和解析以及绘制 pixmap 都在 onReceived 函数中进行这个函数在接收线程中执行……所有操作都对这个 pixmap 进行操作等绘制完成后再用 update() 函数通知 UI 线程在 paintEvent 函数中将 pixmap 画到 widget 上。4.1 SSH 后端paramiko 封装conn/conn_ssh.py 的SSH_CONN类封装了 paramikoclient.connect(host, port, username, password, key_filename)支持密码或私钥文件登录AutoAddPolicy处理未知主机指纹随后open_session()→get_pty(termlinux, width, height)→invoke_shell()获得交互式 shell。resize(w, h)通过channel.resize_pty实现 PTY 尺寸同步——这正是 terminal.py 中resizeConnOutput在窗口尺寸变化时调用self.ctrlConn(resize, (w, h))的落点。SSH 连接的接收循环结构与 TCP 的receiveDataProcess基本同构buffer 累积 1ms 空闲批量投递 可选自动重连见 conn_ssh.py。4.2 前后端分离Terminal_Backend 与 Terminal_Frontendplugins/terminal.py 把终端拆成两部分Terminal_Backendterminal.py持有pyte.HistoryScreen(60, 20, history9999, ratio0.05)启用 LNM 模式的带历史回卷屏和pyte.ByteStreamfeed(data)把原始字节喂给 stream 完成 VT100 解析resize同步屏幕行列数。它是纯 Python 对象可以被接收线程安全调用Terminal_FrontendQWidget负责像素级绘制。构造时创建self.pixmap QPixmap(self.width(), self.height())terminal.py源码注释明确写道pixmap, all operation on this pixmap in receive thread, then invoke UI thread to paint this pixmap on main widget与 docs/dev.md 的描述完全一致。4.3 接收线程内的完整链路onReceived → 绘制 → update()Terminal_Frontend.onReceivedterminal.py就是文档所说在接收线程执行的入口def onReceived(self, data: bytes): in receive thread, not UI thread lock to make sure receive thread not change screen when check changes here self.updatePixmapLock.acquire() self.backend.feed(data) self.updatePixmap() self.updatePixmapLock.release()三步都在接收线程内完成并用updatePixmapLock保护防止与 UI 侧的定时检查timerEventterminal.py并发修改屏幕状态。updatePixmapterminal.py只有两类触发条件screen.dirty非空pyte 记录的本帧变化行或光标位置变化命中则走增量重绘paint_part_pixmap——只重绘screen.dirty中的行terminal.py并画光标最后调用 Qt 的self.update()请求重绘。paintEventterminal.py运行在 UI 线程只做一件事painter.drawPixmap(0, 0, self.pixmap)把接收线程已经画好的位图盖到 widget 上。由于 pixmap 是双缓冲位图跨线程写入 UI 线程读取的模式避免了在接收线程里直接操作 QWidgetQt 不允许导致崩溃或界面卡死这正是 docs/dev.md 中那段提醒的落地实现。围绕这套渲染模型Terminal_Frontend还实现了若干配套能力均可在源码中验证方向键/功能键的 xterm 转义序列映射表keymapterminal.py、基于wcswidth的中文宽度光标定位paint_cursorterminal.py、通过HistoryScreen的prev_page/next_page配合滚动条实现的回卷onScrolled/wheelEvent、以及左右键配合的选区复制/粘贴pyperclip。插件层terminal.py 的Plugin类则做接线onWidgetMain返回带滚动条的Scroll_TerminalonReceived把数据转给self.widget.onReceived(data)并转发给connChilds连接成功后手动触发一次resizeEvent以设置初始 PTY 尺寸并聚焦终端terminal.py。五、插件间继承数据已废弃方案的机制解析docs/dev.md 最后一节以删除线标题标注不同插件继承数据已废弃此方案但完整记录了该试验性机制的语义且从源码结构看对应代码至今仍保留在仓库中理解它对读懂现有数据流很有帮助。5.1 数据流向connParent / connChilds每个插件基类Plugin_Baseplugins/base.py声明了两个类属性connParent main # parent id connChilds [] # children ids其中dbg、terminal、graph的connParent都是main而protocol的connParent是dbgprotocol.py——这正是文档举例的 protocol 插件继承自 dbg 插件。按文档描述语义是双向的接收方向使用protocol插件收数据时连接层收到的数据会先到达父链上的dbgdbg在自己的onReceived中再转发给connChilds即protocol。对应实现即 base.py 中基类的默认onReceived遍历self.connChilds逐一回调protocol的覆写版本也保留了同样的转发protocol.py发送方向protocol发送数据时先转发给dbg的sendData再出网/出串口。文档同时明确了两条限制值得摘录保留这是试验性功能目前只支持两级如main → dbg → protocol不可以再有继承于protocol的插件若要支持更多层级需要改用递归实现转发是单向的在 protocol 收发消息会经过 dbg但是在 dbg 收发不会转发给子插件也就是 protocol。5.2 连接回调如何注入插件理解每个插件都可以使用连接这句话需要看 pluginItems.py 中的PluginItem它在初始化时把宿主能力注入插件实例pluginItems.pyself.plugin pluginClass() self.plugin.configGlobal globalConfig self.plugin.send self.sendData self.plugin.ctrlConn self.ctrlConn self.plugin.hintSignal self.hintSignal ... self.plugin.onInit(configitemConfig)即Plugin_Base中那些类级占位send lambda o,x,y:None等见 base.py会在实例化后被真实函数替换。切换连接类型时_setConnpluginItems.py会把当前连接的onReceived指到PluginItem.onReceived→ 再调plugin.onReceived(data)并同步isConnected/getConnStatus实际发送则由后台线程sendDataProcesspluginItems.py从dataToSend队列取数据调用conns[currConnIdx].send(data)插件 UI 线程只需入队即可。这与 docs/dev.md 用户在界面点击连接后当前的插件就可以通过 send 和 onReceived 函数发送和接收数据了 的描述一一对应。六、给参与开发者的几点建议结合 docs/dev.md 开头如果有开发者想参与开发或者基于这个项目修改可以先看本文档的定位从当前源码结构可以归纳出几条实践要点新增连接类型继承 conn/base.py 的COMM实现onWidget/send/isConnected/getConnStatus并在 conn/init.py 的conns列表中注册onReceived回调与状态信号由框架统一接线新增功能插件继承 plugins/base.py 的Plugin_Base实现onWidgetMain等生命周期方法在 plugins/init.py 的pluginClasses中注册外部插件还支持以comtool_plugin_xxx命名的 Python 包形式被自动发现见 main2.py 的loadPluginsInfoList涉及界面文本的代码一律通过_()取词翻译更新流程走i18n的prepare/finish工具链在onReceived中禁止直接操作 QWidget要么像 protocol 那样用pyqtSignalshowReceiveDataSignal把文本投递回 UI 线程要么像终端那样跨线程只写 pixmap、用update()请求 UI 线程重绘。上述四条分别对应 docs/dev.md 记录的四项功能与一处废弃设计其实现依据可分别在 COMTool/plugins/protocol.py、COMTool/i18n.py、COMTool/conn/conn_tcp_udp.py、COMTool/plugins/terminal.py 及 COMTool/plugins/base.py 中查证。赞分享开发工具嵌入式串口调试【免费下载链接】COMToolCross platform communicate assistant(Serial/network/terminal tool) 跨平台 串口调试助手 网络调试助手 终端工具 linux windows mac Raspberry Pi 支持插件和二次开发项目地址https://gitcode.com/gh_mirrors/co/COMTool点击查看免费下载相关推荐如何实现前端国际化完整指南与最佳实践如何实现前端国际化完整指南与最佳实践 前端国际化i18n是现代 Web 应用不可或缺的功能尤其在全球化背景下让产品支持多语言和多地区文化习惯已成为刚需文档技术博客教程COMTool协议窗口发送记录功能的技术实现探讨COMTool协议窗口发送记录功能的技术实现探讨 在串口通信工具COMTool的使用过程中协议窗口的发送功能目前缺乏记录选项这给开发者调试和追踪通信过程带来开发工具嵌入式串口调试Video2X 指南3 步免费完成 AI 视频超分与补帧Video2X 指南3 步免费完成 AI 视频超分与补帧 糊片为什么放大救不回来 放大不会让细节凭空出现低分辨率文件里丢掉的画面任何播放器的调节都找不回来音视频视频处理图像处理深度学习创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑