跳到主要内容

202602 海康客制潜伏举升车 — 3T顶升AGV开发实践

· 阅读需 12 分钟

基于海康 RCSlite 控制器,开发客制化 3T 潜伏举升车。通过自定义脚本控制液压顶升机构(油泵电机 + 电磁阀),实现货架举升与放下动作,配合 RCS-Lite 调度平台完成搬运任务闭环。

项目概述

本项目使用海康控制器搭建 3T 潜伏举升 AGV,上装机构为液压顶升盘,通过 DO 控制油泵电机和上升/下降电磁阀,DI 读取上下限位传感器状态。属于 RCSlite 文档中客制类潜伏举升(套壳) 方案,即套用海康标准潜伏车的 AGV 协议,每个动作对应一个执行脚本。

核心硬件 IO 映射:

信号类型名称说明
油泵电机DOPUMP_DO_NAME液压泵启停
上升电磁阀DOLIFT_UP_DO_NAME控制顶升上升
下降电磁阀DOLIFT_DOWN_DO_NAME控制顶升下降
上限位传感器DIUP_LIMIT_DI_NAME到达上限位
下限位传感器DIDOWN_LIMIT_DI_NAME到达下限位

平台端配置(RCS-Lite)

1. AMR 车型添加

在 RCS-Lite 系统模型中添加 AMR 类型,选择潜伏系列,配置车辆尺寸。本项目中 3T 车型参数:长度 2500mm、宽度 1200mm、旋转直径 2773mm,开启全向模式。

RCS-Lite AMR 类型配置

2. 能力集上报开关

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

RCS 能力集配置 — 信任车上报

3. 任务模板配置

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

RCS-Lite 任务模板配置

信息

客制潜伏车不支持控制举升盘方向,在有客制潜伏车运行的地图中,各点属性及线属性禁止配置货架角度,否则会引发规划库告警或小车指令异常。

车端配置(MapStudioPro)

使用 MapStudioPro 完成车型参数配置和脚本绑定。在基础配置 → 客制车参数中,选择客制车类型为 「类搬运车」,并绑定对应脚本:

  • 取货脚本bj3t_load(举升动作)
  • 放货脚本bj3t_unload(下降动作)
  • 强制举升下降脚本bj3t_force_up_down

MapStudioPro 客制车参数与脚本绑定

核心脚本开发

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

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_typepod_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) │ │
│ └───────────┘ └───────────┘ │
└─────────────────────────────────────────┘

关键配置要点

  1. 平台侧:AMR 类型选潜伏系列,开启能力集信任车上报,任务模板使用搬运货架
  2. 车端侧:客制车类型选「类搬运车」,绑定取货/放货/强制举升脚本
  3. PDA 侧:ClientView 中应用类型选 PDA(RCS-Lite),发布后校验 appId、服务地址和服务管理中的 IP白名单
  4. 脚本侧execute_with_timeout 统一封装超时与安全检测,每个动作完成后必须调用 stop() 关闭所有 IO
  5. 安全机制:每个检测周期内调用 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。接口没绑全时,页面可以打开,但按钮点击后不会成功下发任务。

ClientView 服务配置与接口绑定

3. 布局页面与表单映射

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

ClientView 任务页面与请求参数映射

页面对应的请求体如下:

{
"reqCode": this.reqCode,
"taskTyp": this.taskTyp,
"positionCodePath": [
{
"positionCode": this.positionCode1,
"type": "00"
},
{
"positionCode": this.positionCode2,
"type": "00"
}
]
}

字段含义可以按页面控件直接理解:

参数名含义
reqCode${Date.now()}请求唯一编号,用于本次任务请求去重和追踪
taskTypW1任务类型编码,决定平台按哪种业务类型解析本次任务
positionCode1#startPoint起点点位编码
positionCode2#endPoint终点点位编码

也就是说,这个页面本质上就是把“起点/终点”两个输入项映射成 positionCodePath 数组中的两个节点,再交给 genAgvSchedulingTask 接口。若后续页面改成“取货位/放货位”命名,映射关系仍然不变,只是业务语义从起终点切换为取放货点。

ClientView 自带预览功能,适合先检查页面布局:

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 强制刷新。

PDA 实际访问页面

4. 发布到 RCS-Lite 与 PDA 接入

页面在 ClientView 中保存完成后,并不等于现场 PDA 已经可用。标准发布流程是:

  1. 在 ClientView 中导出 app.zip
  2. 在 RCS-Lite 的 nginx 服务中执行导入
  3. 校验 app.json 中的 hostportappId
  4. 在 PDA 中填写连接参数并登录

ClientView 导出应用包

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

RCS-Lite nginx 导入 ClientView 应用包

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

app.json 中的 host 与 appId 配置

PDA 端参数建议按下面填写:

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

PDA 端页面路径配置

此外,PDA 登录前还需要在 运营管理 -> 服务管理 中编辑相关服务,在 IP白名单 中逐条添加 PDA 或调试电脑的 IP 地址并保存。该白名单不支持按网段一次性放行,多台设备仍需逐台录入,否则 PDA 会出现登录失败或页面空白。

注意

白名单漏配会导致 PDA 无法登录。只要服务管理里的 IP白名单 没把设备 IP 加进去,账号密码填对了也进不去,而且不支持按网段统一放行。

服务管理中的 IP 白名单配置

参数填对并完成发布后,PDA 实际访问的页面路径中会带上 appId,这时才算真正进入现场可用状态。

提示

PDA 章节的排查顺序建议固定为:先看服务地址,再看 appId,再看 nginx 是否已导入,最后检查服务管理中的 IP白名单 和浏览器缓存。按这个顺序排查,比直接改页面配置更快定位问题。