202508 海康机器人叉车与物流大师WMS对接
海康 RCS-2000 4.3.2 调度叉车,物流大师 WMS 管理库存。现场人员用海康 ClientView 做的 PDA 完成收货、上架、质检和库存查询,PDA 页面脚本直接调用物流大师的接口。
入库流程
本文只介绍入库到上架这一段,以及质检和库存查询:
- 收货:PDA 扫站点地码、托盘码,逐条添加物料明细后提交,WMS 把载具绑定到站点并把物料装进载具。
- 上架:PDA 选择起点巷道,下发叉车入库任务
PF-FMR-INBOUND,叉车把托盘从码头送到货架。 - 质检:PDA 扫仓位号,提交合格或不合格。
- 库存查询:PDA 按物料码查询库位、数量、质检状态和入库时间。
WMS 中可以在库位可视化里看到货架上每个仓位的占用情况:

PDA 基础配置
使用 ClientView 2.1.1 版本。
添加项目
路径:系统管理 → 添加
添加服务
添加 RCS 服务,服务路径为:http://192.168.100.88/
添加请求头:键:X-LR-REQUEST-ID,值:${String(new Date().getTime())}。值写错会导致登录失败。
海康系统默认只开放 https 访问。PDA 页面走 https 时,调用物流大师的 http 接口(http://192.168.100.88:8000/...)会有问题。
解决方法:登录机器人平台运维管理系统(OPS),进入 机器人平台运维管理系统 → 流量网关服务(unify-ingress),在"基础信息"中开启 http 访问。开启后按钮显示为"关闭http访问"。

添加应用
选择应用 PDA(ClientView 和 RCS 配合使用,选择 PDA 标准版),添加扩展脚本:
this.onLogin = async (info) => {
try {
// 校验参数
if (!info.username || !info.password) {
throw {
message: this.$t('login.error.need')
}
}
const data1 = await this.$get('/unify/basic/api/unify/challengeCode?username=' + info.username + '&clientType=pda&purpose=0')
const cData = data1.data
if (!cData.success) {
throw cData
}
const pas1 = sha256(info.password + cData.data.salt)
const param = {
username: info.username,
password: sha256(pas1 + cData.data.codeId), // 需要对密码做处理
clientType: 'pda',
authId: cData.data.authId,
captchaCode: ''
} // 调用登录接口
const {
data
} = await this.$post('/unify/basic/api/unify/login', param)
if (data.success) {
// 更新token
mutations.updateToken(data)
// 登录成功之后跳转至首页
this.$router.push('/home')
} else {
throw data
}
} catch (e) {
this.loginTip = e.message || this.$t('login.error.unknown')
}
}
上下文
| 属性名 | 属性值 | 类型 |
|---|---|---|
| LOGOUT_METHOD | RCS.logout | String |
页面脚本的写法
每个 PDA 页面的脚本都写在"页面脚本"里,以 this.xxx = () => {} 的形式定义方法,再在按钮的"点击"事件中选择该方法:
this.form['#xxx']:读取表单元件的值,#xxx为元件标识;this.$component('#xxx'):获取元件实例,可读写值($value)或设置列表数据($param('data', rows));this.$model('key')读取、this.$model('key', value)写入:在脚本中暂存数据;- 请求头都要带
X-lr-request-id; - 物流大师接口失败时返回 HTTP 400 和
{code, message},脚本在catch中取err.response.data.message提示。
this.$model 与 元件.$param('data', value) 都能存一组数据,区别如下:
this.$model('key', value) | 元件.$param('data', value) | |
|---|---|---|
| 作用对象 | 页面脚本,按 key 存取 | 某个元件,例如列表 #receiptList |
| 作用 | 暂存数据,页面上看不到 | 设置列表显示的内容 |
收货

页面元件:站点 #siteCode、载具 #carrierCode、物料码 #materialCode、数量 #quantity、收货明细列表 #receiptList。
"添加明细"按钮调用 addReceiptItem:先在后端确认物料存在,再追加到明细列表。
const BASE_URL = 'http://192.168.100.88:8000/rcs/rtas/web/schedule'
const headers = () => ({ 'X-lr-request-id': new Date().getTime() })
const pickMessage = (data, fallback) => (data && data.message) || fallback
const isOk = (data) => data.success || data.code === '0'
this.addReceiptItem = async () => {
const materialInput = this.$component('#materialCode')
const quantityInput = this.$component('#quantity')
const receiptList = this.$component('#receiptList')
const materialCode = String(materialInput.$value() ?? '').trim()
const quantity = Number(quantityInput.$value())
if (!materialCode) return this.$tip('请先扫描或输入物料码')
if (!Number.isSafeInteger(quantity) || quantity <= 0) return this.$tip('数量必须是大于 0 的整数')
const rows = this.$model('receiptItems') || []
if (rows.some(row => row.materialCode === materialCode)) return this.$tip('该物料已存在,请修改已有明细的数量')
// 后端确认物料存在;不存在时返回 HTTP 400
try {
const res = await this.$post(`${BASE_URL}/materials/check`, { materialCode }, { headers: headers() })
if (!isOk(res.data)) return this.$tip(pickMessage(res.data, `物料 ${materialCode} 校验失败`))
} catch (err) {
return this.$tip(pickMessage(err.response && err.response.data, err.message))
}
// 追加明细并刷新列表
const nextRows = [...rows, { materialCode, quantity }]
this.$model('receiptItems', nextRows)
receiptList.$param('data', nextRows)
materialInput.$value('')
quantityInput.$value(1)
}
"提交"按钮调用 submitReceipt:把站点、载具和全部明细一起提交。成功后清空明细,站点保留,方便同一个收货口连续收多托。
this.submitReceipt = async () => {
const siteCode = String(this.$component('#siteCode').$value() ?? '').trim()
const carrierInput = this.$component('#carrierCode')
const carrierCode = String(carrierInput.$value() ?? '').trim()
const items = this.$model('receiptItems') || []
if (!siteCode) return this.$tip('请先扫描站点码')
if (!carrierCode) return this.$tip('请先扫描托盘码')
if (items.length === 0) return this.$tip('请先添加收货明细')
try {
const res = await this.$post(`${BASE_URL}/receipt`, {
siteCode,
carrierCode,
items: items.map(row => ({ materialCode: row.materialCode, quantity: row.quantity }))
}, { headers: headers() })
if (!isOk(res.data)) return this.$tip(pickMessage(res.data, '收货失败'))
// 开始下一单
this.$model('receiptItems', [])
this.$component('#receiptList').$param('data', [])
carrierInput.$value('')
this.$tip('收货成功', 'success')
} catch (err) {
// 失败时不清空,改完可以直接重新提交
this.$tip(pickMessage(err.response && err.response.data, err.message))
}
}
上架

"叉车码头->货架"页面只需选择起点巷道 #startCode,下发叉车入库任务 PF-FMR-INBOUND:
this.onSubmit = () => {
const params = {
targetRoute: [{
type: 'STACK',
code: this.form['#startCode'],
seq: 0
}],
initPriority: 10,
interrupt: 0,
taskType: 'PF-FMR-INBOUND'
}
const headers = { 'X-lr-request-id': new Date().getTime() }
this.$post('http://192.168.100.88:8000/rcs/rtas/web/schedule/submit', params, { headers }).then((res) => {
if (res.data.success || res.data.code === '0') {
this.$tip(this.$text('下发成功'), 'success')
this.clear()
} else {
this.$tip(res.data.message)
}
})
}
质检

扫描仓位号 #storageBinCode,选择质检状态 #qualified 后提交。质检结果必须明确选择,漏选时不发请求,避免后端把库存当成不合格冻结。
this.inspection = () => {
const raw = this.form['#qualified']
let qualified = null
if (raw === true || raw === 'true' || raw === 1 || raw === '1') qualified = true
if (raw === false || raw === 'false' || raw === 0 || raw === '0') qualified = false
if (qualified === null) {
this.$tip(this.$text('请选择质检结果'))
return
}
const params = {
storageBinCode: this.form['#storageBinCode'],
qualified
}
const headers = { 'X-lr-request-id': new Date().getTime() }
this.$post('http://192.168.100.88:8000/rcs/rtas/web/schedule/inspection', params, { headers })
.then((res) => {
if (res.data.success || res.data.code === '0') {
this.$tip(this.$text('下发成功'), 'success')
this.clear()
} else {
this.$tip(res.data.message)
}
})
.catch((err) => {
const data = err.response && err.response.data
this.$tip((data && data.message) || err.message)
})
}
库存查询

按物料码 #materialCode 查询库存,方法返回库存数组:
this.queryList = async () => {
const materialCode = this.form['#materialCode']
if (!materialCode) {
this.$tip(this.$text('请先扫描或输入物料码'))
return []
}
const headers = { 'X-lr-request-id': new Date().getTime() }
try {
const res = await this.$post('http://192.168.100.88:8000/rcs/rtas/web/schedule/stocks',
{ materialCode }, { headers })
if (!(res.data.success || res.data.code === '0')) {
this.$tip(res.data.message)
return []
}
const result = res.data.data
if (result == null) return []
return Array.isArray(result) ? result : [result]
} catch (err) {
const data = err.response && err.response.data
this.$tip((data && data.message) || err.message)
return []
}
}
"查询"按钮的点击事件中先调用 queryList,再添加一个"数据联动":源字段选"入参"(即上一步方法的返回值),目标字段选列表元件的 data,查询结果就会显示到列表中。

其他页面:直接调用海康 RCS
下面两个页面不经过物流大师,直接调用海康 RCS 的接口。
任务下发
this.onSubmit = () => {
const params = {
targetRoute: [{
type: 'SITE',
code: this.form['#startCode'],
seq: 0
},
{
type: 'SITE',
code: this.form['#endCode'],
seq: 1
}
],
taskType: 'PF-LMR-COMMON'
}
const priority = this.form['#initPriority']
params.initPriority = priority ? parseInt(priority) : 1
const headers = {'X-lr-request-id': new Date().getTime()}
this.$post('https://192.168.100.88/rcs/rtas/web/schedule/submit', params, {headers}).then((res) => {
if (res.data.success || res.data.code === '0') {
this.$tip(this.$text('下发成功'), 'success')
this.clear()
} else {
this.$tip(res.data.message)
}
})
}
#startCode和#endCode为起点、终点输入框的元件标识;#initPriority为优先级输入框的元件标识。
绑定解绑

// 绑定载具
this.bindCarrier = () => {
const params = {
carrierCode: this.form['#carrierCode'],
siteCode: this.form['#siteCode'],
bindType: 'BIND'
}
const headers = {'X-lr-request-id': new Date().getTime()}
this.$post('https://192.168.100.88/rcs/rtas/web/schedule/bindCarrierSite', params, {headers}).then((res) => {
if (res.data.success || res.data.code === '0') {
this.$tip(this.$text('下发成功'), 'success')
this.clear()
} else {
this.$tip(res.data.message)
}
})
}
// 解绑载具:参数相同,bindType 改为 'UNBIND'
按钮事件 → 添加脚本方法 → 选择写好的函数:

发布
获取应用 ID 和项目 ID,拼接一个页面 URL 如下:
http://192.168.100.88/clientview/apps/base-pda/index.html?appId=20251119133240866&projectId=20251107102233759#/login
在浏览器访问该地址,如果能访问到,说明 URL 拼接成功。
将前面 IP 地址部分拿掉,把下面的路径配置到 PDA 登录页面右上角的加载路径中:
/clientview/apps/base-pda/index.html?appId=20251119133240866&projectId=20251107102233759#/login
现场问题:放货时误报障碍物
现象
叉车送货进入储位放货时,任务中断并提示:
2D激光检测到放货储位点附近存在障碍物,无法继续执行放货任务,传感器坐标(-422 225 63),障碍物高度区间175-175
但现场检查货架储位,里面并没有障碍物。
原因
叉车开启了放货静态检测功能:放货前检测目标储位内是否已存在物体,检测到就停止放货。该功能默认关闭,对应 XML 字段 b_put_obs(0:关闭;1:开启)。
解决方法
在 MapStudioPro 中关闭放货静态检测:
- 用 MapStudioPro 登录叉车 12622,切到"专家模式"。
- 进入 叉车特有配置 → 功能参数,找到"放货静态检测",把开关关掉。
- 点"保存"。