202602 海康客制潜伏举升车 — 3T顶升AGV开发实践
基于海康 RCSlite 控制器,开发客制化 3T 潜伏举升车。通过自定义脚本控制液压顶升机构(油泵电机 + 电磁阀),实现货架举升与放下动作,配合 RCS-Lite 调度平台完成搬运任务闭环。
项目概述
本项目使用海康控制器搭建 3T 潜伏举升 AGV,上装机构为液压顶升盘,通过 DO 控制油泵电机和上升/下降电磁阀,DI 读取上下限位传感器状态。属于 RCSlite 文档中客制类潜伏举升(套壳) 方案,即套用海康标准潜伏车的 AGV 协议,每个动作对应一个执行脚本。
核心硬件 IO 映射:
| 信号 | 类型 | 名称 | 说明 |
|---|---|---|---|
| 油泵电机 | DO | PUMP_DO_NAME | 液压泵启停 |
| 上升电磁阀 | DO | LIFT_UP_DO_NAME | 控制顶升上升 |
| 下降电磁阀 | DO | LIFT_DOWN_DO_NAME | 控制顶升下降 |
| 上限位传感器 | DI | UP_LIMIT_DI_NAME | 到达上限位 |
| 下限位传感器 | DI | DOWN_LIMIT_DI_NAME | 到达下限位 |
平台端配置(RCS-Lite)
1. AMR 车型添加
在 RCS-Lite 系统模型中添加 AMR 类型,选择潜伏系列,配置车辆尺寸。本项目中 3T 车型参数:长度 2500mm、宽度 1200mm、旋转直径 2773mm,开启全向模式。

2. 能力集上报开关
客制车型项目中,需要在运营管理 → 服务管理 → RCS 能力集 → 本地配置中,开启 「amr参数设置信任车上报」 开关。该功能使平台信任小车上报的能力集参数,客制化车型必须开启。

3. 任务模板配置
任务模板选择潜伏系列中的搬运货架模板,流程为:移动 → 举升 → 移动 → 放下。该任务模板会在执行举升/放下步骤时触发车端绑定的取货/放货脚本。

客制潜伏车不支持控制举升盘方向,在有客制潜伏车运行的地图中,各点属性及线属性禁止配置货架角度,否则会引发规划库告警或小车指令异常。
车端配置(MapStudioPro)
使用 MapStudioPro 完成车型参数配置和脚本绑定。在基础配置 → 客制车参数中,选择客制车类型为 「类搬运车」,并绑定对应脚本:
- 取货脚本:
bj3t_load(举升动作) - 放货脚本:
bj3t_unload(下降动作) - 强制举升下降脚本:
bj3t_force_up_down

核心脚本开发
脚本开发在 RoboDesigner 中完成,左侧可以看到任务脚本和虚拟机构的目录结构。

顶升执行器(bj3t_lift)
bj3t_lift 是底层执行器类,封装了液压顶升机构的 IO 控制逻辑。通过 execute_with_timeout 实现带超时检测的动作控制,同时在每个检测周期内检查急停/碰撞状态。
class bj3t_lift(Actuator):
def __init__(self, robot: SecDevInterface):
super().__init__()
self.robot = robot
self.robot_operator = ParamOperator(robot, __file__, None)
self.pump_do_name = self.robot_operator.get_value_in_struct_data("PUMP_DO_NAME")
self.lift_down_do_name = self.robot_operator.get_value_in_struct_data("LIFT_DOWN_DO_NAME")
self.lift_up_do_name = self.robot_operator.get_value_in_struct_data("LIFT_UP_DO_NAME")
self.down_limit_di_name = self.robot_operator.get_value_in_struct_data("DOWN_LIMIT_DI_NAME")
self.up_limit_di_name = self.robot_operator.get_value_in_struct_data("UP_LIMIT_DI_NAME")
顶升上升控制 — 先检查是否已在上限位,然后关闭下降阀、打开上升阀和油泵,每 50ms 轮询上限位传感器,10 秒超时:
def lift_up(self, robot: SecDevInterface = None):
"""顶升上升控制"""
up_limit = self.get_di_value(robot, self.up_limit_di_name)
if up_limit == 1:
robot.log("已在上限位,无需上升")
return
def action_func(**kwargs):
robot.log("开始顶升上升")
self.set_do_value(robot, self.lift_down_do_name, 0) # 先确保下降阀关闭
self.set_do_value(robot, self.lift_up_do_name, 1)
self.set_do_value(robot, self.pump_do_name, 1)
return True
def check_func(**kwargs):
if self.check_emergency(robot):
raise Exception("急停或碰撞触发")
return self.get_di_value(robot, self.up_limit_di_name)
def cleanup_func(**kwargs):
robot.log("执行清理:关闭泵与电磁阀")
self.stop(robot)
result, last_value, elapsed = self.execute_with_timeout(
robot=robot,
action_func=action_func,
check_func=check_func,
timeout=10.0,
check_interval=0.05,
target_value=1,
cleanup_func=cleanup_func,
)
if result in [TimeoutResult.TIMEOUT, TimeoutResult.VALUE_TIMEOUT]:
raise Exception(f"顶升上升超时: {result.value}, 最后状态={last_value}, 耗时={elapsed:.2f}秒")
elif result == TimeoutResult.FAILED:
raise Exception(f"顶升上升失败: 最后状态={last_value}, 耗时={elapsed:.2f}秒")
self.stop(robot)
停止与安全 — 任何异常或动作完成后,统一关闭所有 DO 输出:
def stop(self, robot: SecDevInterface = None):
"""停止所有操作"""
try:
self.set_do_value(robot, self.pump_do_name, 0)
self.set_do_value(robot, self.lift_up_do_name, 0)
self.set_do_value(robot, self.lift_down_do_name, 0)
robot.log("停止完成")
except Exception as e:
robot.log(f"停止失败: {e}")
取货脚本(bj3t_load)
bj3t_load 是任务调度绑定的取货入口脚本,从任务参数中获取 pod_type 和 pod_str,调用 bj3t_lift.lift_up() 执行举升,并更新动作状态供调度平台感知。
class Business(TemplateBusiness):
def __init__(self, robot: SecDevInterface, params):
super().__init__()
robot.log(f"params: {params}")
self.lift = bj3t_lift(robot)
self.lift.check_status(robot)
self.robot_operator = ParamOperator(robot, __file__, params)
self.pod_type = self.robot_operator.get_value_in_task_data("pod_type")
self.pod_str = self.robot_operator.get_value_in_task_data("pod_str")
def execute(self, robot: SecDevInterface, args):
try:
robot.log(f"开始顶升 - pod_type: {self.pod_type}, pod_str: {self.pod_str}")
self.lift.lift_up(robot)
Status(robot, "").set_action_status({"pod_type": self.pod_type, "lift_status": 1})
robot.log("顶升完成")
return BusinessStatus.FINISHED
except Exception as e:
robot.log(f"顶升失败: {e}")
Status(robot, "").set_action_status({"pod_type": self.pod_type, "lift_status": 0})
return BusinessStatus.FAILED
ParamOperator 用于从任务参数和结构化配置中读取值。get_value_in_task_data 读取调度平台下发的运行时参数,get_value_in_struct_data 读取 RoboDesigner 中配置的机构参数(如 IO 名称映射)。
脚本架构总结
bj3t_load (取货脚本) bj3t_unload (放货脚本)
│ │
▼ ▼
bj3t_lift.lift_up() bj3t_lift.lift_down()
│ │
▼ ▼
┌─────────────────────────────────────────┐
│ bj3t_lift (执行器) │
│ ┌─────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 油泵电机 │ │ 上升电磁阀│ │ 下降电磁阀│ │
│ │ (DO) │ │ (DO) │ │ (DO) │ │
│ └─────────┘ └──────────┘ └──────────┘ │
│ ┌───────────┐ ┌───────────┐ │
│ │ 上限位传感器│ │ 下限位传感器│ │
│ │ (DI) │ │ (DI) │ │
│ └───────────┘ └───────────┘ │
└─────────────────────────────────────────┘
关键配置要点
- 平台侧:AMR 类型选潜伏系列,开启能力集信任车上报,任务模板使用搬运货架
- 车端侧:客制车类型选「类搬运车」,绑定取货/放货/强制举升脚本
- PDA 侧:ClientView 中应用类型选
PDA(RCS-Lite),发布后校验appId、服务地址和服务管理中的IP白名单 - 脚本侧:
execute_with_timeout统一封装超时与安全检测,每个动作完成后必须调用stop()关闭所有 IO - 安全机制:每个检测周期内调用
check_emergency检查急停和碰撞状态,异常时立即抛出异常并在上层try-except中执行停止操作
PDA 配置(ClientView)
项目现场除了平台端调度和车端脚本之外,还需要通过 ClientView 交付 PDA 页面,用于现场人员下发任务、查看页面联调效果和登录移动端进行操作。这个环节的关键不在页面本身,而在于服务地址、appId、发布路径和白名单必须同时打通。
1. ClientView 安装与登录
最短可执行流程是先安装 ClientView,浏览器访问 http://IP:9195/clientview,使用默认账号 admin / Hik@1234 登录。登录后先创建项目,再继续配置服务、应用和布局。
在应用管理中新增应用时,应用类型必须选择 PDA(RCS-Lite)。应用创建完成后,要记住该应用的 appId,后续 PDA 页面访问路径和 app.json 校验都要用到它。
2. 项目、服务与应用配置
ClientView 的配置顺序建议固定为:项目 -> 服务 -> 应用 -> 布局 -> 菜单。其中最容易出错的是服务地址,标准建议按版本区分:
| RCS-Lite 版本 | 服务地址 |
|---|---|
| V1.5.1 及之后 | http://IP:18182/rcms |
服务地址按版本填错时,最典型的现象是页面能打开、接口却不可用。这类问题很容易在联调时误判成前端按钮异常,实际根因通常是 ClientView 服务配置指向了错误的 RCS-Lite 接口地址。
服务配置完成后,需要把 PDA 页面实际调用的接口绑定到服务中,例如任务下发页面调用 genAgvSchedulingTask,货架绑定页面调用 bindPodAndBerth。接口没绑全时,页面可以打开,但按钮点击后不会成功下发任务。

3. 布局页面与表单映射
布局决定 PDA 页面长什么样,菜单决定 PDA 首页显示哪些入口。本项目中单独做了一个“任务下发”页面,页面上有两个输入框,分别对应起点和终点,按钮点击后按模板参数组装请求体并调用 RCS-Lite 接口。

页面对应的请求体如下:
{
"reqCode": this.reqCode,
"taskTyp": this.taskTyp,
"positionCodePath": [
{
"positionCode": this.positionCode1,
"type": "00"
},
{
"positionCode": this.positionCode2,
"type": "00"
}
]
}
字段含义可以按页面控件直接理解:
| 参数名 | 值 | 含义 |
|---|---|---|
reqCode | ${Date.now()} | 请求唯一编号,用于本次任务请求去重和追踪 |
taskTyp | W1 | 任务类型编码,决定平台按哪种业务类型解析本次任务 |
positionCode1 | #startPoint | 起点点位编码 |
positionCode2 | #endPoint | 终点点位编码 |
也就是说,这个页面本质上就是把“起点/终点”两个输入项映射成 positionCodePath 数组中的两个节点,再交给 genAgvSchedulingTask 接口。若后续页面改成“取货位/放货位”命名,映射关系仍然不变,只是业务语义从起终点切换为取放货点。
ClientView 自带预览功能,适合先检查页面布局:

ClientView 预览地址不能替代正式发布地址。现场最容易误判的是页面在 ClientView 里能预览,但点击之后功能失效,认为配置地址错误;只有导出 app.zip 并导入 RCS-Lite nginx 后,页面资源才真正落到服务目录中。
正确的做法使用应该直接访问 http://IP:18182/clientview/apps/rcs-lite-pda/index.html?appId=应用编号 这个正式发布地址进行联调测试。
这个项目是:http://192.168.111.47:18182/clientview/apps/rcs-lite-pda/index.html?appId=20260317150709222#/
上传新包后要强制刷新缓存。PDA 或 PC 浏览器往往还会命中旧资源,表现为页面明明改了但现场看不到更新。这个问题最直接的处理方式就是执行 Ctrl+F5 强制刷新。

4. 发布到 RCS-Lite 与 PDA 接入
页面在 ClientView 中保存完成后,并不等于现场 PDA 已经可用。标准发布流程是:
- 在 ClientView 中导出
app.zip - 在 RCS-Lite 的 nginx 服务中执行导入
- 校验
app.json中的host、port、appId - 在 PDA 中填写连接参数并登录

在 RCS-Lite 中导入 PDA 页面包时,可以直接进入 运营管理 -> 服务管理,在 nginx 服务右侧点击 更多 -> 导入,选择 ClientView 导出的 app.zip 后确认打开。只有这一步完成后,页面资源才会真正发布到 RCS-Lite 的 ClientView 访问目录。

如果现场版本较低,不能从界面导入,则需要手工把导出的文件解压到 RcsLite/nginx/html/clientview/app 目录,并确认 app.json 中服务器地址填写正确。这里的 host 和 appId 是 PDA 能否打开正确页面的关键字段。

PDA 端参数建议按下面填写:
- 协议:
http - 服务器:RCS-Lite 服务器 IP
- 端口:按版本填写
18182或旧版本对应端口 - 页面路径:
/clientview/apps/rcs-lite-pda/index.html?appId=应用编号 - 用户名:通常使用
admin - 密码:与 RCS-Lite 主平台登录密码一致,不是 ClientView 默认密码

此外,PDA 登录前还需要在 运营管理 -> 服务管理 中编辑相关服务,在 IP白名单 中逐条添加 PDA 或调试电脑的 IP 地址并保存。该白名单不支持按网段一次性放行,多台设备仍需逐台录入,否则 PDA 会出现登录失败或页面空白。
白名单漏配会导致 PDA 无法登录。只要服务管理里的 IP白名单 没把设备 IP 加进去,账号密码填对了也进不去,而且不支持按网段统一放行。

参数填对并完成发布后,PDA 实际访问的页面路径中会带上 appId,这时才算真正进入现场可用状态。
PDA 章节的排查顺序建议固定为:先看服务地址,再看 appId,再看 nginx 是否已导入,最后检查服务管理中的 IP白名单 和浏览器缓存。按这个顺序排查,比直接改页面配置更快定位问题。