OpenPLC Editor v4 端口接口契约

📅 2026/6/29 17:52:58
OpenPLC Editor v4 端口接口契约
OpenPLC Editor 采用端口与适配器六边形架构使其共享的 React UI 与平台特定机制完全解耦。端口接口是契约层——纯 TypeScriptinterface定义声明了 UI能做什么而不规定如何做。每个端口代表一个内聚的领域功能编译、调试、项目管理等且每个方法要么返回一个带类型的结果对象要么返回一个用于事件订阅的Unsubscribe清理函数。这种设计使得相同的组件树能够在 Electron通过 IPC 桥接或浏览器通过 HTTP/WebRTC中保持不变地运行适配器在应用启动时接入具体的实现。架构定位端口层位于两个边界之间共享 UIReact 组件、hooks、Zustand stores向内依赖于端口类型而适配器实现则向外依赖于端口类型来满足契约。这种反转确保了 UI 既不知道 Electron 的window.bridge也不知道 REST 端点——它只知道端口的方法签名和返回类型。PlatformProviderReact context 在应用根部注入了一个具体的PlatformPorts对象。组件随后通过usePlatform()hook 或便捷 hookuseCompiler()、useRuntime()等访问端口。缺失的 provider 会立即触发运行时错误从而在开发阶段而非生产环境中捕获接线错误。PlatformPorts 聚合对象所有端口接口被组装成一个由PlatformProvider通过 React context 分发的单一PlatformPorts对象。必需端口在所有平台上均存在可选端口标有?仅在其平台支持时才可用。端口必需领域主要关注点CompilerPort✅编译PLC 编译流水线 (XML → ST → C → binary)RuntimePort✅运行时远程 PLC 运行时通信 (启动/停止/状态)DebuggerPort✅调试基于 Modbus 的调试协议 (变量读/写/强制)SimulatorPort✅仿真内置 AVR 仿真器 (基于 avr8js 的 ATmega2560)ProjectPort✅项目项目生命周期 (创建、打开、保存、POU 管理)DevicePort✅硬件开发板/硬件发现与配置OrchestratorPort✅云端Orchestrator 发现与设备列表SystemPort✅平台应用级服务 (存储、链接、日志)WindowPort✅窗口原生窗口管理AcceleratorPort✅快捷键键盘快捷键与应用级操作ThemePort✅主题主题检测与切换VersionControlPort✅VCSGit 版本控制操作NavigationPort✅导航深度链接路由与搜索EsiPort❌EtherCATESI 仓库管理AIPort❌AIAI 辅助编码 (补全、聊天、遥测)编辑器平台通过导入适配器工厂函数并将每个函数连接到 Electron IPC 桥接来组装其editorPorts对象。运行时状态IP 地址、项目路径通过闭包设置器而非构造函数参数注入从而使适配器的创建与运行时配置解耦。核心端口契约CompilerPort编译流水线被抽象为流式操作UI 使用结构化的项目数据和目标开发板调用compileProgram()然后在每个流水线阶段接收CompileProgressEvent回调。适配器决定是通过 IPC 调用本地二进制文件xml2st、iec2c、arduino-cli还是调用远程 HTTP 端点——UI 完全察觉不到其中的差异。方法返回值用途compileProgram(args, onProgress)PromiseCompileResult完整流水线XML → ST → C → binarycompileForDebug(args, onProgress)PromiseDebugCompileResult带符号的调试模式编译exportProjectXml(args)PromiseResult{message: string}导出为 IEC 61131-3 XMLonCompileOutput?(callback)Unsubscribe订阅 stdout/stderr 流CompileProgramArgs类型携带projectData、boardTarget、projectPath以及如isSimulator和compileOnly等可选标志。CompileProgressEvent类型使用可区分的stage字段xml | st | c | glue | arduino | done | error以便 UI 可以渲染特定阶段的进度指示器。RuntimePort运行时通信抽象了与远程 OpenPLC 运行时交互的完整生命周期身份验证、PLC 控制、日志检索和程序上传。连接详情IP 地址、JWT 令牌由各适配器内部管理——UI 仅调用逻辑操作。方法返回值用途login(params)PromiseLoginResult身份验证接收 JWTcreateUser(params)Promise{success, error?}首次创建用户getUsersInfo()PromiseUsersInfoResult检查运行时是否包含用户getStatus(includeStats?)PromiseRuntimeStatusResultPLC 状态 可选计时信息startPlc()Promise{success, error?}启动 PLC 程序stopPlc()Promise{success, error?}停止 PLC 程序getLogs(minId?)PromiseRuntimeLogsResult运行时日志getSerialPorts()Promise{success, ports?}可用的串口getCompilationStatus()PromiseCompilationStatusResult正在进行的编译状态clearCredentials()Promise{success}登出 / 清除已存储的凭据uploadProgram?(data)Promise{success, error?}上传已编译的程序onTokenRefreshed?(cb)UnsubscribeJWT 自动续期事件DebuggerPort调试协议使用自定义的 Modbus 功能码0x41–0x45进行 PLC 变量操作但端口隐藏了所有传输细节。无论适配器是通过 TCP、RTU、WebSocket、WebRTC 还是仿真器虚拟串口进行路由UI 看到的都是相同的接口。方法返回值用途connect()Promise{success, error?}连接到调试目标disconnect()Promise{success}断开连接getVariablesList(indexes)PromiseDebugVariableResult按调试索引批量读取setVariable(index, force, buf?)PromiseDebugSetResult写入或强制变量verifyMd5(expectedMd5)PromiseMd5VerifyResult验证程序完整性readProgramMd5(path, board)Promise{success, md5?}读取已编译 ST 的 MD5readDebugFile(path, board)Promise{success, content?}读取变量映射文件onDisconnected(cb)Unsubscribe连接丢失事件isConnected()boolean连接状态检查DebugVariableResult.needsReconnect标志指示 UI 在重试读取之前重新建立调试连接——这是基于 Modbus 调试协议独有的模式用于优雅地处理传输层断开连接。SimulatorPort内置 AVR 仿真器封装了avr8js0.20.0模拟器。编辑器和 Web 适配器共享相同的仿真核心但在固件加载上有所不同编辑器适配器接收文件路径通过 IPC 从磁盘读取而 Web 适配器接收十六进制内容字符串来自编译器 API。loadFirmware(hexPathOrContent)方法的单一string参数兼顾了这两种情况。方法返回值用途loadFirmware(hexPathOrContent)Promise{success, error?}加载并启动仿真器stop()Promise{success}停止仿真器isRunning()boolean运行状态onStopped(cb)Unsubscribe仿真器停止事件connectDebugger()Promisevoid将调试器桥接到仿真器disconnectDebugger()void清理调试桥接ProjectPort项目生命周期是方法最丰富的端口反映了跨文件系统编辑器和云Web存储管理 PLC 项目的复杂性。该端口处理项目的增删改查、POU 文件操作、文件监听以及最近项目追踪。方法返回值用途createProject(params)PromiseProjectResponse新建项目openProject()PromiseProjectResponse平台文件选择器openProjectByPath(path)PromiseProjectResponse按标识符打开saveProject(params)Promise{success, error?}保存完整项目saveFile(path, content)Promise{success, error?}保存单个文件createPou(params)Promise{success, data?}新建 POU 文件deletePou(path)Promise{success, error?}删除 POUrenamePou(params)Promise{success, data?}重命名 POUpickPath()Promise{success, path?}目录选择器getRecentProjects()PromiseRecentProject[]最近项目列表readFileContent(path)Promise{success, content?}读取文件内容watchFile?(path)Promise{success, error?}启动文件监听unwatchFile?(path)Promise{success}停止监听unwatchAll?()Promise{success}停止所有监听器onFileExternalChange?(cb)Unsubscribe外部变更事件ProjectResponse类型返回一个结构化有效载荷包含meta名称、类型、路径、projectDataPLC 程序数据、deviceConfiguration和devicePinMapping——这是 UI 通过单次调用填充项目状态所需的全部信息。辅助端口契约DevicePort硬件发现抽象了本地 HAL JSON 文件 arduino-cli编辑器与捆绑的hals.json orchestrator APIWeb之间的差异。开发板预览图片也有所不同编辑器从本地resources/目录加载而 Web 返回捆绑资源的 URL。方法返回值用途getAvailableBoards()PromiseMapstring, BoardInfo所有开发板及其规格/引脚getCommunicationPorts()PromiseCommunicationPort[]串口/通信端口refreshBoards()PromiseArray{board, version}重新扫描开发板列表refreshCommunicationPorts()PromiseCommunicationPort[]重新扫描端口getPreviewImage(name)Promisestring开发板预览图 (base64 或 URL)SystemPort平台服务对持久化键值存储electron-storevslocalStorage、外部链接打开shell.openExternalvswindow.open、系统信息和诊断日志提供了轻量级的抽象。方法返回值用途getSystemInfo()PromiseSystemInfo操作系统、架构、暗色模式、窗口状态getStoreValue(key)Promiseunknown读取持久化值setStoreValue(key, value)void写入持久化值openExternalLink(url)Promise{success}在默认浏览器中打开log(level, message)void诊断日志WindowPort原生窗口管理采用渐进式空操作模式每个方法在编辑器适配器中都有有意义的实现但在 Web 适配器中会优雅降级例如close()触发beforeunloadreload()调用window.location.reload()而minimize()/maximize()/hide()/quit()变为空操作。方法返回值用途minimize()void最小化窗口maximize()void最大化/还原close()void关闭窗口hide()void最小化到托盘reload()void重新加载应用quit()void完全退出rebuildMenu()void重建原生菜单onCloseRequested(cb)Unsubscribe关闭/退出请求事件onMaximizedChanged?(cb)Unsubscribe最大化状态切换AcceleratorPort键盘快捷键遵循事件订阅模型而非命令式分发。UI 为命名操作onSaveProject、onUndo等注册回调适配器将这些回调映射到特定平台的机制——在编辑器中是 Electron 菜单加速器在 Web 中是浏览器keydown监听器。所有订阅都返回Unsubscribe函数用于清理。类别方法项目onCreateProject,onOpenProject,onOpenRecent,onSaveProject,onSaveFile,onCloseProject,onExportProject编辑器onCloseTab,onDeleteFile,onFindInProject,onUndo,onRedo视图onSwitchPerspective,onAboutThemePort主题管理提供了响应式和命令式两种模式onThemeChanged订阅操作系统级别或用户发起的更改而setTheme/toggleTheme则显式控制活动主题。编辑器适配器监听 Electron 的nativeThemeIPC 事件Web 适配器使用window.matchMedia((prefers-color-scheme: dark))。方法返回值用途getCurrentTheme()ThemeVariant读取当前主题setTheme(theme)void显式设置主题toggleTheme()void切换亮色/暗色onThemeChanged(cb)Unsubscribe主题更改事件平台能力除了行为端口PlatformCapabilities接口还提供了功能切换标志允许 UI 有条件地渲染或启用功能而无需进行运行时平台检测。每个适配器提供自己的能力实例——Electron 应用对应EDITOR_CAPABILITIES浏览器应用对应WEB_CAPABILITIES。能力编辑器Web类别hasNativeWindowControls✅❌窗口与外观hasNativeMenu✅❌窗口与外观hasNativeFileDialogs✅❌窗口与外观hasAuthentication❌✅身份验证hasLocalSerialPorts✅❌设备与硬件hasOrchestratorDevices❌✅设备与硬件hasWebRTC❌✅设备与硬件hasInProcessSimulator✅✅仿真器hasLocalFilesystem✅❌项目管理hasProjectExport✅❌项目管理hasAboutDialog✅❌项目管理hasPythonLSP✅❌编辑器功能hasUndoRedoHistory✅❌编辑器功能hasFileWatcher✅❌编辑器功能hasAIAssistant❌✅AI 功能hasProxiedRuntimeConnection❌✅运行时连接hasDirectProgramUpload❌✅运行时连接能力是分支 UI 逻辑的推荐方式——推荐使用if (capabilities.hasLocalFilesystem)而不是if (platform electron)。这使分支保持声明式且可测试并使得通过组合能力标志来添加新平台变得轻而易举而不是在各组件中散布平台检查。共享领域类型所有端口接口共享一个定义在types.ts中的通用类型词汇表。这些类型与平台无关代表了编辑器和 Web 适配器必须达成一致的领域概念。最重要的模式包括结果包装器— 可能失败的操作返回ResultT这是一个可区分联合类型{ success: true } T | { success: false; error: string }。这强制调用者在访问结果数据之前检查success消除了未处理异常的反模式。取消订阅模式— 事件订阅返回() void清理函数遵循 React effect 模式。这确保了组件在卸载时始终清理监听器防止在长时间运行的编辑器会话中出现内存泄漏。PLC 领域模型— 类型层次结构涵盖了PLCProjectData→PLCPou→PLCVariable→PLCVariableType外加用于资源配置的PLCTask/PLCInstance以及带有 Modbus RTU/TCP 子配置的DeviceConfiguration还有PLCDataType结构体、枚举、数组。使用模式在 React 组件中使用端口的典型生命周期遵循三个步骤通过 hook 访问端口使用带类型的参数调用方法以及处理可区分的结果。// 1. 通过便捷 hook 访问 const compiler useCompiler() const capabilities useCapabilities() // 2. 使用带类型的参数调用 const result await compiler.compileProgram( { projectData, boardTarget: arduino_uno, projectPath: /projects/my-plc }, (event) setProgress(event.stage, event.progress) ) // 3. 处理可区分的结果 if (result.success) { navigateToHex(result.hexPath) } else { showError(result.error) }PlatformProvider在应用根部完成接入——Electron 构建对应editorPorts浏览器构建对应webPorts——通过 context 使整个端口集对所有组件可用而无需属性透传。