项目概览
RemoteCtrl 是一套已经用于生产现场的工业设备远程控制网关。它部署在现场工控机上,为操作人员提供命令行和 Web 两种入口,并在入口与底层设备之间统一处理命令解析、状态读取、并发控制、故障恢复、日志审计和服务生命周期。
当前项目面向一套包含双工业机器人、双 Z 轴、PLC 和整机控制逻辑的设备。控制链路分为两类:
- 通过 PLC 和 Modbus TCP 完成整机、机器人常用动作和 Z 轴位置控制;
- 通过机器人厂商提供的 C# SDK 直连控制器,完成更细粒度的运动、状态和脚本操作。
“已用于生产”描述的是这套网关及其现场控制链路已经实际投入使用。下文列出的高级 SDK 能力来自当前稳定源码;除非另有说明,不代表每一项高级能力都已在所有现场工艺中启用。
项目背景与目标
工业设备通常已经具备 PLC、机器人控制器和本地操作界面,但远程诊断和受控操作仍会遇到几个工程问题:
- 不同设备暴露的协议、状态模型和错误形式并不一致;
- 多个操作入口同时写 PLC 时,可能出现命令交错和不可预测的运动;
- 远程连接中断后,设备状态、触发信号和操作结果必须能够被追踪;
- 现场计算机往往无法稳定访问互联网,安装和升级不能依赖在线下载;
- 控制能力必须受到网络边界、身份验证、状态机和物理安全回路的共同约束。
项目的目标不是把设备接口简单暴露到网络,而是在远程入口和现场设备之间增加一层可审计、可恢复、可部署的控制网关:
- 为操作员提供一致的命令和 Web 操作模型;
- 串行化 PLC 写入,减少并发控制冲突;
- 分离 PLC 间接控制与厂商 SDK 直控能力;
- 持续采集设备状态,并在通信中断后自动恢复;
- 将核心服务、Web 面板和依赖打包为可离线交付的 Windows 服务;
- 保留足够的日志与验证证据,支持现场排查和版本更新。
匿名化与公开边界
本案例保留了系统中可复用的架构和工程方法,但不会公开以下内容:
- 客户、项目现场、设备品牌和具体产品型号;
- 现场 IP、端口、账号、密码、密钥和 VPN 配置;
- PLC 寄存器地址、真实设备拓扑、工艺参数和程序编号;
- 厂商 SDK 文件、Native 依赖、配置文件和完整命令参考;
- 生产节拍、产量、利用率、故障率或远程距离等业务指标。
文中的结构图和示例均使用抽象名称,目的是说明软件设计,而不是还原现场网络。
技术栈与运行约束
| 层次 | 技术 | 主要职责 |
|---|---|---|
| 核心宿主 | .NET 8、C#、ASP.NET Core | 依赖注入、后台服务、REST API 和统一生命周期 |
| PLC 通信 | Modbus TCP、FluentModbus | 寄存器读写、浮点编解码、连接状态管理 |
| 机器人直控 | 厂商 C# SDK | 运动、点动、状态、力数据和脚本操作 |
| 命令入口 | Telnet 兼容 TCP、Windows OpenSSH | 受控命令行访问与加密转发 |
| Web 控制台 | Python、FastAPI、Jinja2、httpx | 状态展示、控制表单、配置和日志查看 |
| 日志 | NLog、结构化审计日志 | 运行日志、连接记录和命令记录 |
| 服务管理 | NSSM、PowerShell | 安装、启停、更新、备份和开机自启 |
| 测试 | xUnit、NSubstitute、FluentAssertions | 命令解析、服务状态机和会话行为验证 |
核心服务以 .NET 8 自包含程序交付,测试项目当前使用 .NET 9。Web 控制台需要 Python 运行环境,但依赖会提前下载为离线 wheels,不要求现场计算机访问公共包仓库。
总体架构
┌────────────────────── 操作入口 ──────────────────────┐
│ │
│ Web 浏览器 远程命令行客户端 │
│ │ │ │
│ ▼ ▼ │
│ WebUICtrl 系统 OpenSSH │
│ FastAPI / Jinja2 ForceCommand │
│ │ HTTP │ 本机转发 │
└──────┼───────────────────────────────────┼───────────┘
│ ▼
│ TelnetTransport + 认证
│ │
▼ ▼
┌────────────────── RemoteCtrl .NET 8 宿主 ─────────────┐
│ Minimal API CommandParser │
│ └──────────────┬───────────────┘ │
│ ▼ │
│ System / Robot / Axis / RobotSdk Services │
│ │ │ │
│ ▼ ▼ │
│ PLC 优先级命令队列 分侧 SDK 锁 │
│ 单消费者串行执行 左右设备可并行 │
│ │ │ │
│ ▼ ▼ │
│ Modbus TCP Driver 厂商机器人 SDK │
│ │ │ │
│ PollingWorker / ReconnectWorker / 状态缓存 │
└──────────────┼───────────────────────┼─────────────────┘
▼ ▼
PLC 双机器人控制器
│
双机器人 / 双 Z 轴 / 整机逻辑
两个入口最终复用同一组应用服务。这样可以避免 Web 面板和命令行各自实现一套控制规则,也让状态机、参数校验、队列和错误处理保持一致。
统一宿主与依赖装配
核心服务使用 ASP.NET Core WebApplication 同时承载 REST API 和后台工作器。依赖注入容器负责装配:
- PLC 驱动和命令队列;
- 整机、机器人、Z 轴和机器人 SDK 服务;
- 命令解析器、会话跟踪、IP 速率限制和审计日志;
- PLC 重连、命令消费、状态轮询、Telnet 和 SSH 后台服务。
这种装配方式让 Kestrel、设备连接和后台循环共享同一个启动、取消与关闭过程。独立的 PLC 地址映射文件是必需配置,缺失时服务会直接启动失败,避免在地址为空或默认值错误时继续写入设备。
两条远程操作入口
Web 控制台链路
WebUICtrl 是一个轻量 FastAPI 应用,通过 httpx 调用核心服务的 Minimal API:
浏览器表单或状态轮询
→ FastAPI 路由校验
→ RemoteCtrl REST API
→ 应用服务
→ PLC 队列或机器人 SDK
面板展示整机状态、左右机器人状态和左右 Z 轴位置,并提供初始化、启停、复位、模式切换、机器人常用控制和位置动作。配置页只读取经过筛选的运行参数,日志页展示最近的运行记录。
Web 后端无法访问核心 API 时,首页不会把旧值伪装成实时状态,而是显示断开状态并禁用相应数据区域。代理请求统一设置超时,并将下游 HTTP 错误或连接失败转换为明确的上游响应。
加密命令行链路
核心服务实现 Telnet 兼容的文本命令入口,负责用户名验证、失败次数限制、字符编码、命令帮助、会话跟踪、IP 速率限制和操作审计。命令按层级路由到整机、机器人、Z 轴或 SDK 直控服务,并返回稳定的成功、参数错误、未连接或 SDK 错误结果。
项目内的 SshTransport 目前只是架构占位,并没有提供可投入生产的 SSH 服务端。现场加密入口由 Windows OpenSSH 提供,通过受限账户和 ForceCommand 将会话转发到本机 Telnet 入口:
远程 SSH 会话
→ Windows OpenSSH 身份验证
→ ForceCommand
→ 本机 Telnet 命令入口
→ CommandParser
因此,生产架构不会把 SSH.NET 客户端库误当作完整的 SSH 服务端,也不会把明文 Telnet 直接作为公网访问方案。
PLC 间接控制
整机、机器人常用动作和 Z 轴位置控制通过 PLC 完成。应用层服务负责把业务动作转换为寄存器写入或脉冲信号,底层驱动只处理 Modbus 通信。
| 服务 | 公开语义 | 设备侧行为 |
|---|---|---|
| SystemService | 初始化、启动、停止、复位、手自动切换 | 写入整机控制信号并维护状态机 |
| RobotService | 启停、暂停、复位、使能、速度、程序与循环参数 | 通过 PLC 间接控制左右机器人 |
| AxisService | 移动到指定取料位置、读取当前位置 | 控制左右 Z 轴预设位置 |
应用服务会检查参数范围和部分状态条件。例如,系统未初始化或处于报警时会拒绝启动;机器人未使能时会拒绝开始动作。这里的软件校验属于附加保护,不能替代急停、安全门、驱动器限位和 PLC 安全逻辑。
单消费者优先级队列
所有 PLC 写入先进入 PlcCommandQueue。队列使用普通和高优先级两个 Channel,由单个消费者执行:
多个 Telnet / Web 会话
│
├── 普通命令 ─────┐
└── 停止命令 ──┐ │
▼ ▼
高优先级 / 普通队列
│
▼
单消费者执行
│
▼
Modbus TCP
消费者每次优先排空高优先级命令,整机停止和停止按钮释放都走高优先级通道。调用方不仅等待“成功入队”,还会等待命令实际执行完毕;底层异常会通过结果任务返回给原调用者。
高优先级只表示软件队列中的执行次序,不是安全等级认证,也不等同于物理急停回路。
Modbus 串行访问与数据转换
Modbus 驱动使用 SemaphoreSlim 保护连接、读取和写入,确保同一连接上只有一个在途操作。驱动统一处理:
- 连接与断开;
- 单寄存器和批量寄存器读写;
- 两寄存器浮点数的字节序转换;
- 读写异常后的断线标记;
- 取消和资源释放。
机器人状态采用批量读取,减少轮询周期内的往返次数;Z 轴位置也以连续寄存器一次读取。地址映射集中在独立 JSON 配置中,业务服务不散落现场地址常量。
状态采集与断线恢复
PollingWorker 使用不同周期分别采集高频机器人/轴状态和低频整机状态,并把结果写入应用服务的内存状态缓存。Web 和命令行读取缓存时不会直接触发一轮完整的设备扫描。
状态采集包含:
- 左右机器人速度、笛卡尔位置、姿态和启停/使能状态;
- 左右 Z 轴当前位置与多个预设位置;
- 整机运行状态和模式。
单次轮询失败会写入警告日志,但不会让整个后台服务退出。PLC 读写异常会把连接标记为断开,PlcReconnectWorker 随后进入恢复流程:
- 将系统状态更新为报警,阻止部分新动作;
- 使用指数退避执行一组快速重试;
- 快速重试耗尽后进入较慢的持续重试;
- 连接恢复后由轮询工作器重新读取真实状态。
慢速重试不会永久放弃,因此现场网络或 PLC 恢复后不必仅为了重连而重启整个服务。
机器人 SDK 直控
机器人直控服务绕过 PLC,直接调用厂商 C# SDK。当前源码实现的能力包括:
| 能力分组 | 已实现能力 |
|---|---|
| 连接与准备 | 连接、断开、上电、使能、复位、回零和速度设置 |
| 基础运动 | 关节运动、直线运动和停止 |
| 相对运动 | 单关节相对移动、用户/工具坐标系直线偏移 |
| 点动 | 关节点动、笛卡尔点动和点动停止 |
| 状态读取 | 控制器状态、笛卡尔位姿、关节位置和力数据 |
| 程序管理 | 脚本列表、切换、启动、暂停、继续和停止 |
| 双设备操作 | 同一动作并发下发到左右机器人 |
左右机器人各自拥有独立的 SDK 实例、连接状态、速度状态和 SemaphoreSlim。同一机器人的 SDK 调用会被串行化,左右机器人之间则允许并行执行。双设备命令会分别调用左右服务并汇总结果,避免用一把全局锁把互不相关的设备串在一起。
输入层会校验速度百分比、轴号、方向、距离、位姿参数和命令结构。SDK 返回的原始错误码会保留在稳定响应中并写入日志,便于结合厂商手册排查,而不是只返回笼统的“调用失败”。
PLC 间接控制和 SDK 直控是两条独立路径。现场流程需要明确同一时刻由哪条路径拥有设备控制权;当前实现尚未提供覆盖两条路径的统一仲裁器。
会话、限流与审计
命令行入口允许多个已认证会话同时在线,SessionTracker 为每个会话保存短 ID、入口类型、用户名、客户端地址和连接时间。PLC 间接控制由命令队列串行化,SDK 直控则由分侧锁保护。
IpRateLimiter 在连接建立和认证阶段限制单个来源的尝试频率;Telnet 认证有最大失败次数。AuditLogger 单独记录:
- 认证成功与失败;
- 客户端来源和会话生命周期;
- 命令文本及响应;
- PLC 报警事件。
当前审计重点覆盖命令行入口。Web API 没有等价的用户身份上下文,因此 Web 操作不能形成同等粒度的“谁执行了什么”审计链,这是后续认证改造必须补齐的部分。
配置与现场适配
核心服务通过配置绑定装配 PLC、Telnet、SSH、API、速率限制和机器人 SDK 参数,PLC 地址映射单独保存。Web 控制台通过环境变量指定核心 API 地址。
配置的设计目标是让同一套程序适配不同现场,但公开案例不包含任何真实配置。实际交付时至少需要完成:
- 设备地址、通信参数和 PLC 地址表校验;
- 机器人坐标系、速度和脚本配置确认;
- 命令行用户和 OpenSSH 受限账户配置;
- 防火墙、VPN 和允许访问的管理终端确认;
- 日志路径、保留周期和磁盘空间确认。
配置变更属于受控发布的一部分,不能把开发环境默认值直接带入生产。
离线构建与 Windows 服务交付
现场环境可能无法访问 NuGet 或 Python 包仓库,因此项目提供离线安装包构建链路。联网构建机生成的 ZIP 包包含:
RemoteCtrl 离线包
├── 自包含的 .NET 核心服务
├── WebUICtrl Python 源码与模板
├── Python 离线 wheels
├── NSSM 服务管理工具
├── OpenSSH / 本机转发所需工具
├── 安装、更新与管理脚本
└── 包版本元数据和现场说明
安装脚本创建 Python 虚拟环境、离线安装依赖,并通过 NSSM 注册核心服务和 Web 控制台两个 Windows 服务。两个服务配置为开机自动启动,并在异常退出后自动恢复。
更新脚本执行以下流程:
- 检测现有安装目录和服务状态;
- 校验新安装包的关键文件;
- 停止 Web 控制台和核心服务;
- 创建带时间戳的旧版本备份;
- 覆盖程序文件,同时保留现场配置和环境文件;
- 更新必要的 Python 依赖;
- 重新启动服务并由操作员执行健康检查。
当前备份支持人工恢复,但还没有形成带版本索引、自动健康检查和一键回滚的完整发布系统。
构建与测试验证
案例编写前对稳定源码重新执行了 Release 验证:
| 验证项 | 当前结果 |
|---|---|
| .NET Release 构建 | 0 error;存在已记录的依赖警告 |
| xUnit 测试 | 196 项通过 |
| 核心服务目标框架 | .NET 8 |
| 测试目标框架 | .NET 9 |
现有自动化测试主要覆盖:
- 命令解析、帮助树和参数映射;
- Telnet 会话输入、字符编码和响应;
- 整机状态机和 PLC 控制服务;
- 机器人 PLC 间接控制;
- SDK 配置默认值、相对运动参数和命令分发。
仓库还保留了离线部署链路的验证记录,覆盖离线包构建、目录/ZIP 安装、版本更新、配置保留、服务启停以及 API/Web 首页健康检查。这些记录证明部署脚本曾完成完整演练,但它们不是持续运行的 CI,也不能替代当前版本的现场验收。
生产使用结果
根据项目实际使用情况,可以确认:
- 核心控制网关已经用于生产现场;
- 操作人员可以通过受控命令入口和 Web 面板查看设备状态并执行允许的操作;
- PLC 间接控制、机器人直控、状态轮询、断线恢复和双服务部署形成了完整运行链路;
- 离线安装与更新方式适应了现场网络受限的交付条件;
- 日志和命令响应为远程诊断提供了统一证据入口。
本案例不公布客户、产线和业务数据,也不使用未经记录的数字评价性能或可靠性。
已知边界与风险
网络与身份
- REST API 和 Web 控制台当前缺少应用层认证,并默认监听所有网卡;现阶段必须依赖防火墙、现场隔离网络或 VPN 控制访问范围。
- Telnet 是明文协议,只应作为本机 SSH 转发目标或在明确受控的网络中使用。
- 内置
SshTransport尚未真正实现 SSH 服务端;生产入口依赖系统 OpenSSH 和 ForceCommand。 - 配置、日志和会话查询接口可能暴露运行信息,需要在认证改造中一并收敛。
运动与并发
- 脉冲和长按逻辑在写入高电平后,如果取消、延时失败或通信中断,没有
finally保证再次写零,需要增加失败清理与设备侧超时保护。 - 普通 PLC 命令队列当前没有容量上限和负载丢弃策略,异常流量下可能积压。
- PLC 间接控制与 SDK 直控没有统一所有权仲裁,需要靠操作流程和现场安全逻辑避免同时控制同一设备。
- 软件高优先级停止不是安全功能,不能替代硬件急停和安全 PLC。
依赖、测试与发布
- 项目声明的 FluentModbus 版本当前会解析到另一个兼容版本,构建可用但依赖不可完全复现。
- 现有 SSH.NET 包版本存在已知高危安全公告,而生产 SSH 又不依赖其占位实现,应移除无用引用或升级并重新验证。
- WebUI、Minimal API、PLC 驱动、轮询/重连、队列和会话管理缺少充分的自动化覆盖。
- Python 依赖使用范围版本而不是完整锁文件;离线 wheels 固化了单次交付,却没有提供长期可复现的依赖清单。
- 仓库没有持续集成工作流,当前验证仍依赖开发机和人工发布检查。
- Mock 与单元测试无法替代真实 PLC、机器人控制器和安全回路上的硬件在环验证。
后续路线
后续工作按风险优先级推进:
- 将核心 API 和 Web 面板收敛到内部接口,增加统一登录、角色权限、CSRF 防护和 Web 操作审计;
- 固化 OpenSSH ForceCommand、VPN 和防火墙边界,移除未使用的内置 SSH 占位依赖;
- 为脉冲/长按增加取消与异常清零保障,并补充设备侧信号超时;
- 增加 PLC 与 SDK 两条控制路径的所有权仲裁和互锁;
- 统一 .NET 目标框架,锁定 NuGet 与 Python 依赖并处理安全公告;
- 补齐 Web/API、队列、驱动、轮询、重连和审计测试;
- 建立包含单元测试、离线包构建和静态安全检查的 CI;
- 建立可识别版本、自动健康检查、保留策略和一键回滚的发布流程;
- 将 PLC 与机器人硬件在环回归纳入正式发版验收。
项目复盘
RemoteCtrl 的核心价值不只是“远程发命令”,而是把多入口、多协议和多设备之间的控制责任放进一套可解释的工程边界中:
- 入口层负责连接、认证和协议适配;
- 应用服务负责状态机和业务参数;
- PLC 队列负责间接控制的执行顺序;
- 分侧锁负责机器人 SDK 的并发隔离;
- 后台工作器负责状态同步和故障恢复;
- 部署工具负责把开发成果变成现场可运行的长期服务。
生产使用证明了这条整体链路具备实际价值,也暴露出工业远程控制必须长期面对的问题:网络边界、身份与审计、控制权仲裁、异常清理和可回滚发布都不能靠单个功能点解决。下一阶段的重点不是继续增加命令数量,而是进一步收紧安全边界并提高验证和发布的可重复性。