项目概览

RemoteCtrl 是一套已经用于生产现场的工业设备远程控制网关。它部署在现场工控机上,为操作人员提供命令行和 Web 两种入口,并在入口与底层设备之间统一处理命令解析、状态读取、并发控制、故障恢复、日志审计和服务生命周期。

当前项目面向一套包含双工业机器人、双 Z 轴、PLC 和整机控制逻辑的设备。控制链路分为两类:

  • 通过 PLC 和 Modbus TCP 完成整机、机器人常用动作和 Z 轴位置控制;
  • 通过机器人厂商提供的 C# SDK 直连控制器,完成更细粒度的运动、状态和脚本操作。

“已用于生产”描述的是这套网关及其现场控制链路已经实际投入使用。下文列出的高级 SDK 能力来自当前稳定源码;除非另有说明,不代表每一项高级能力都已在所有现场工艺中启用。

项目背景与目标

工业设备通常已经具备 PLC、机器人控制器和本地操作界面,但远程诊断和受控操作仍会遇到几个工程问题:

  • 不同设备暴露的协议、状态模型和错误形式并不一致;
  • 多个操作入口同时写 PLC 时,可能出现命令交错和不可预测的运动;
  • 远程连接中断后,设备状态、触发信号和操作结果必须能够被追踪;
  • 现场计算机往往无法稳定访问互联网,安装和升级不能依赖在线下载;
  • 控制能力必须受到网络边界、身份验证、状态机和物理安全回路的共同约束。

项目的目标不是把设备接口简单暴露到网络,而是在远程入口和现场设备之间增加一层可审计、可恢复、可部署的控制网关:

  1. 为操作员提供一致的命令和 Web 操作模型;
  2. 串行化 PLC 写入,减少并发控制冲突;
  3. 分离 PLC 间接控制与厂商 SDK 直控能力;
  4. 持续采集设备状态,并在通信中断后自动恢复;
  5. 将核心服务、Web 面板和依赖打包为可离线交付的 Windows 服务;
  6. 保留足够的日志与验证证据,支持现场排查和版本更新。

匿名化与公开边界

本案例保留了系统中可复用的架构和工程方法,但不会公开以下内容:

  • 客户、项目现场、设备品牌和具体产品型号;
  • 现场 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 随后进入恢复流程:

  1. 将系统状态更新为报警,阻止部分新动作;
  2. 使用指数退避执行一组快速重试;
  3. 快速重试耗尽后进入较慢的持续重试;
  4. 连接恢复后由轮询工作器重新读取真实状态。

慢速重试不会永久放弃,因此现场网络或 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 服务。两个服务配置为开机自动启动,并在异常退出后自动恢复。

更新脚本执行以下流程:

  1. 检测现有安装目录和服务状态;
  2. 校验新安装包的关键文件;
  3. 停止 Web 控制台和核心服务;
  4. 创建带时间戳的旧版本备份;
  5. 覆盖程序文件,同时保留现场配置和环境文件;
  6. 更新必要的 Python 依赖;
  7. 重新启动服务并由操作员执行健康检查。

当前备份支持人工恢复,但还没有形成带版本索引、自动健康检查和一键回滚的完整发布系统。

构建与测试验证

案例编写前对稳定源码重新执行了 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、机器人控制器和安全回路上的硬件在环验证。

后续路线

后续工作按风险优先级推进:

  1. 将核心 API 和 Web 面板收敛到内部接口,增加统一登录、角色权限、CSRF 防护和 Web 操作审计;
  2. 固化 OpenSSH ForceCommand、VPN 和防火墙边界,移除未使用的内置 SSH 占位依赖;
  3. 为脉冲/长按增加取消与异常清零保障,并补充设备侧信号超时;
  4. 增加 PLC 与 SDK 两条控制路径的所有权仲裁和互锁;
  5. 统一 .NET 目标框架,锁定 NuGet 与 Python 依赖并处理安全公告;
  6. 补齐 Web/API、队列、驱动、轮询、重连和审计测试;
  7. 建立包含单元测试、离线包构建和静态安全检查的 CI;
  8. 建立可识别版本、自动健康检查、保留策略和一键回滚的发布流程;
  9. 将 PLC 与机器人硬件在环回归纳入正式发版验收。

项目复盘

RemoteCtrl 的核心价值不只是“远程发命令”,而是把多入口、多协议和多设备之间的控制责任放进一套可解释的工程边界中:

  • 入口层负责连接、认证和协议适配;
  • 应用服务负责状态机和业务参数;
  • PLC 队列负责间接控制的执行顺序;
  • 分侧锁负责机器人 SDK 的并发隔离;
  • 后台工作器负责状态同步和故障恢复;
  • 部署工具负责把开发成果变成现场可运行的长期服务。

生产使用证明了这条整体链路具备实际价值,也暴露出工业远程控制必须长期面对的问题:网络边界、身份与审计、控制权仲裁、异常清理和可回滚发布都不能靠单个功能点解决。下一阶段的重点不是继续增加命令数量,而是进一步收紧安全边界并提高验证和发布的可重复性。