目录
- 鱼塘卡口和工地门口,拉线方案为什么废了
- 这篇只会碰到这几项能力
- 轮询、官方 App、一上来写原生,户外为什么扛不住
- 动手:从创建应用到本地出画
- 画面出来之后,再订掉线和回放
- 联调会踩的坑
- 真正省下的不是再买一块太阳能板
鱼塘卡口和工地门口,拉线方案为什么废了
以前户外安防的做法很朴素:找电工拉 220V,再找弱电铺网线,镜头挂上,保安盯官方 App。门口有配电箱、机房就在五十米外时,这套还能交差。
点位一出围墙就废了。鱼塘对岸没电杆,工地大门临时围挡每周挪一次,林班卡口更没有网线可挖。再买十台网线枪机,现场还是「没网没电」。太阳能板加 4G 卡能让机器自己活着,但画面还停在安装师傅的私人 App 里。
缺的不是再买一台相机,而是用现行 OpenAPI:先 unBindDeviceInfo 核这台机是不是走 SIMCard、4G 是否已经 online,再 bindDevice 进开发者资产,用 bindDeviceLive 把 HLS 接到你自己的值班台。
读完按这个顺序交差:
- 在开放平台创建应用,拿到
appId/appSecret。 - 签名自测通过,能拿到
accessToken。 unBindDeviceInfo读回wifiConfigMode含SIMCard,且status为online或sleep。checkDeviceBindOrNot后bindDevice,isMine=true。bindDeviceLive出 HLS,本地/preview能播、封面图能打开。- 抽一张卡或断供电,台账里对应点位变成
offline,不要把sleep当成掉线。
户外没市电、没网线。本文用现行 OpenAPI 的
unBindDeviceInfo核SIMCard上线,再bindDeviceLive把画面接到本地值班页,带可运行 Node.js,验到/preview出画为止。
需要用到的这几项能力
查询见设备查询说明,添加见设备添加说明,直播见设备直播说明。
unBindDeviceInfo:绑之前看平台支不支持、设备在不在、配网模式里有没有SIMCard。status是online/offline/sleep/upgrading。checkDeviceBindOrNot+bindDevice:先看isBind/isMine,再绑进开发者账号。code按机型传底部安全码或改过的设备密码。listDeviceDetailsByIds/deviceOnline:绑完后复核在离线。列表用英文枚举;deviceOnline的onLine是0/1/3/4,4是休眠。bindDeviceLive+getLiveStreamInfo:创建设备源直播,再取 HLS。没创建过,后一个接口拿不到地址。deviceStatus里的sleep:4G 太阳能常见省电态,不是掉线。能力集里有CloseDormant才谈closeDormant,以该机返回为准。accessToken有效期 3 天,报TK1002再刷,不要每个请求都申。
你负责把太阳能板朝向、SIM 卡套餐和点位表绑进自己的值班页,开放平台负责核蜂窝是否上线、把 HLS 吐给你。
太阳能 + 电池 + 4G/5G SIM
│
▼
设备自己上蜂窝(wifiConfigMode 含 SIMCard)
│
▼
unBindDeviceInfo 核 exist / online|sleep
│
▼
bindDevice 入开发者账号
│
▼
bindDeviceLive → 你的 /preview 出画
轮询、官方 App、一上来写原生,户外为什么扛不住
轮询适合对台账、对在离线,前提是设备已经在同一个开发者账号下,并且你分得清 sleep 和 offline。机还在安装师傅私人号里,你轮的是空账。
只靠官方 App,红点停在包工头手机里。卡欠费、电池见底、天线被雨布捂住,值班室要第二天对表才知道。户外要的是自己的页能出画、掉线能进自己的系统,不是再下一个看监控的 App。
一上来写原生更没必要。项目部已经有 Web 值班台或钉钉,缺的是 HTTP 把 4G 点位收进来,不是第三个客户端。
动手:从创建应用到本地出画
部署只是开胃菜。真正过关的是:卡插上、板子朝南,本地 /preview 能播这路户外画面。画面没出来之前,不要去订回调、不要去取回放。
第一步:创建应用,先填户外点位表
打开 乐橙开放平台 注册并创建应用。控制台「我的应用 - 应用信息」能看到 appId / appSecret 再往下做。没有这两样,后面签名都会废。
先不要写接口。现场先让设备自己活着:太阳能板朝南、电池有电、4G/5G 卡有流量且已激活。官方 App 里能预览,再谈绑定。wifiTransferMode 里的 5Ghz 是 Wi-Fi 频段,不是 5G 蜂窝;蜂窝配网看 SIMCard。板子朝北、天线捂在雨布里,开放平台帮不了你把太阳搬过来。
IMOU_APP_ID=lcdxxxxxxxxx
IMOU_APP_SECRET=你的密钥
PORT=8080
# 户外点位(先一张表,再填进环境变量)
SITE_POND=鱼塘对岸序列号
SITE_GATE=工地门口序列号
DEVICE_CODE_POND=底部8位安全码或改过的设备密码
DEVICE_CODE_GATE=底部8位安全码或改过的设备密码
# sites.json 填写模板(对齐写字段)
{
"鱼塘对岸序列号": {
"name": "鱼塘对岸",
"power": "solar",
"network": "4G",
"channelId": "0"
},
"工地门口序列号": {
"name": "工地门口",
"power": "solar",
"network": "5G",
"channelId": "0"
}
}
成功:控制台能看到应用;点位表每行都有序列号、供电、卡类型,官方 App 已能预览。
兜底:App 里都是离线,先查卡费、电池电压、天线,不要先改签名。控制台都没有应用,不要往下抄代码。
第二步:签名壳,先和文档案例对齐
请求走 https://openapi.lechange.cn/openapi/{method},壳子是 system + params + id。签名按开发规范:time:{time},nonce:{nonce},appSecret:{appSecret} 做 MD5 小写 32 位。time 和服务器误差不能超过 5 分钟,否则 SN1002;nonce 5 分钟内不能重复,否则 SN1005。
// openapi-client.js
const crypto = require('crypto');
const { v4: uuidv4 } = require('uuid');
const OPENAPI_BASE = 'https://openapi.lechange.cn/openapi';
function calcSign(time, nonce, appSecret) {
const raw = `time:${time},nonce:${nonce},appSecret:${appSecret}`;
return crypto.createHash('md5').update(raw, 'utf8').digest('hex');
}
async function callOpenApi(method, appId, appSecret, params = {}) {
const time = Math.floor(Date.now() / 1000);
const nonce = uuidv4();
const body = {
system: { ver: '1.0', appId, time, nonce, sign: calcSign(time, nonce, appSecret) },
id: uuidv4(),
params,
};
const res = await fetch(`${OPENAPI_BASE}/${method}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const json = await res.json();
if (!json.result || json.result.code !== '0') {
const msg = json.result ? `${json.result.code} ${json.result.msg}` : JSON.stringify(json);
throw new Error(`OpenAPI ${method} failed: ${msg}`);
}
return json.result.data;
}
module.exports = { callOpenApi, calcSign };
成功:calcSign(1706511734, 'f5a1ae2d-c09c-4d39-a744-83a5c2c653c2', 'test123456789test123456789') 得到 fd37b62889e4757c58b8f3bf05fb9976;再调 accessToken 能打印 token 和 expireTime。
兜底:对不上标准案例,先别查业务接口,多半是字符串拼错。TK1002 再刷 token。签名算法若控制台提示不一致,以现行开发规范为准,不要混网上过时示例。
第三步:先 unBindDeviceInfo,再谈绑定
这篇核蜂窝的接口是 unBindDeviceInfo。绑之前打这一枪:support=true、deviceExist=exist、wifiConfigMode 里能看到 SIMCard。status=offline 先别绑,机还没从蜂窝爬上来。
token=上一步拿到的 accessToken
deviceId=鱼塘对岸序列号
status=sleep 不是失败。太阳能 4G 为了省电会睡,和卡没费、电池空了的 offline 不是一回事。wifiConfigMode 是逗号分隔的配对模式列表,缺 SIMCard 就不要按 4G 方案往下做,以该机返回为准。
成功:控制台打印 wifiConfigMode、status、catalog、ability。至少一台是 online 或 sleep,且模式里有 SIMCard。
兜底:notExist 或 support=false,先核对序列号和机型是否在现行平台支持范围内。一直 offline,回到现场查卡和供电。
第四步:核绑定,再一台一台 bindDevice
先 checkDeviceBindOrNot:isBind=false 再绑;isMine=true 已经是你的;isBind=true 且 isMine=false 在别人账上,先解绑或走交接。入账接口是 bindDevice。
token=管理员 accessToken
deviceId=鱼塘对岸序列号
code=底部8位安全码或改过的设备密码
未改密且标签没有 8 位安全码,按文档把 code 传空。一批机不要写循环闷头绑,先抽鱼塘、门口各一台。
应用开发写明:部分新出厂或升级后的设备,可能无法只靠 HTTP bindDevice 完成绑定,要结合客户端 SDK。联调绑失败、核绑定又显示未在本账号,先走这条说明,不要改签名。
成功:bindDevice 返回 code=0;再查一次,isBind=true 且 isMine=true。再用 listDeviceDetailsByIds 能看到 deviceVersion 和 deviceStatus。
兜底:已经是别人的资产,先让现场从原账号解绑。新机 HTTP 绑不上,按应用开发说明改 SDK 路径。
第五步:bindDeviceLive,本地页播出画
这篇出画接口是 bindDeviceLive。4G 流量金贵,第一次用 streamId=1(标清辅码流)。已经创建过就不要反复创,改打 getLiveStreamInfo。直播地址对外等于公开画面,只给值班内网。
// outdoor-bringup.js
require('dotenv').config();
const { callOpenApi } = require('./openapi-client');
async function bringUp(deviceId, code, channelId = '0') {
const appId = process.env.IMOU_APP_ID;
const appSecret = process.env.IMOU_APP_SECRET;
const token = (await callOpenApi('accessToken', appId, appSecret, {})).accessToken;
const info = await callOpenApi('unBindDeviceInfo', appId, appSecret, { token, deviceId });
console.log('[unBindDeviceInfo]', {
support: info.support,
deviceExist: info.deviceExist,
status: info.status,
wifiConfigMode: info.wifiConfigMode,
catalog: info.catalog,
});
if (!String(info.wifiConfigMode || '').includes('SIMCard')) {
throw new Error('wifiConfigMode 无 SIMCard,不要按 4G 方案硬绑');
}
const bind = await callOpenApi('checkDeviceBindOrNot', appId, appSecret, { token, deviceId });
if (bind.isBind && !bind.isMine) throw new Error('isMine=false,先从原账号解绑');
if (!bind.isMine) {
await callOpenApi('bindDevice', appId, appSecret, { token, deviceId, code });
}
let live;
try {
live = await callOpenApi('bindDeviceLive', appId, appSecret, {
token,
deviceId,
channelId,
streamId: 1,
});
} catch (e) {
live = await callOpenApi('getLiveStreamInfo', appId, appSecret, { token, deviceId, channelId });
}
const hls = (live.streams || []).map((s) => s.hls).filter(Boolean);
console.log('[live]', { liveStatus: live.liveStatus, hls, cover: (live.streams || [])[0] && (live.streams || [])[0].coverUrl });
return { token, info, live };
}
module.exports = { bringUp };
成功:日志里出现 hls 地址,coverUrl 能在浏览器打开一张封面。
兜底:getLiveStreamInfo 为空,说明还没创建成功,先看 bindDeviceLive 的返回,不要先写播放器。sleep 时出不了画,等它醒或按该机能力集看是否支持 closeDormant,不要拿门锁用的唤醒接口套到枪机上。
第六步:接收服务,/preview 出了才算闭环
开放平台吐 HLS,你的服务收成值班页。这是接收侧,不要和签名、绑定揉在一节。
// preview-server.js
require('dotenv').config();
const express = require('express');
const { callOpenApi } = require('./openapi-client');
const SITES = require('./sites.json');
async function token() {
return (await callOpenApi('accessToken', process.env.IMOU_APP_ID, process.env.IMOU_APP_SECRET, {})).accessToken;
}
const app = express();
app.get('/health', (_req, res) => res.status(200).send('ok'));
app.get('/preview', async (req, res) => {
const deviceId = req.query.deviceId || Object.keys(SITES)[0];
const channelId = (SITES[deviceId] && SITES[deviceId].channelId) || '0';
const tk = await token();
const detail = await callOpenApi('listDeviceDetailsByIds', process.env.IMOU_APP_ID, process.env.IMOU_APP_SECRET, {
token: tk,
deviceList: [{ deviceId, channelId: [channelId] }],
});
const live = await callOpenApi('getLiveStreamInfo', process.env.IMOU_APP_ID, process.env.IMOU_APP_SECRET, {
token: tk,
deviceId,
channelId,
});
const device = (detail.deviceList || [])[0] || {};
res.json({
name: (SITES[deviceId] && SITES[deviceId].name) || deviceId,
deviceVersion: device.deviceVersion,
deviceStatus: device.deviceStatus,
streams: live.streams || [],
});
});
app.listen(process.env.PORT || 8080);
deviceList 按listDeviceDetailsByIds样例传:deviceId + channelId 数组。联调以返回里的 deviceVersion 为准。
按勾,不要跳步。
/health返回ok。GET /preview?deviceId=鱼塘对岸序列号能看到deviceVersion、deviceStatus、streams[].hls。- 浏览器打开一条 HTTPS 的
hls(status为0),画面是现场,不是黑场。 - 抽卡或拔电池,再刷
/preview,该行变成offline;板子还在、机在睡,应是sleep,不要打电话。
成功:上面四条都对得上。这就是「值班台出画了」。
兜底:有地址播不出来,先换 streams 里带 https 的那条,并确认本机网络能访问直播域名。单台复核可用 deviceOnline,不要和列表英文枚举混着比。
画面出来之后,再订掉线和回放
最小闭环已经成立,才加第二层。仍用同一套现行 OpenAPI,不要另起一套协议。
卡欠费、电池见底要进值班群:用 setMessageCallback 订 deviceStatus,getMessageCallback 读回同一条 HTTPS。上下线体的 id 固定是 -1,格式见事件消息格式定义。4G 短抖动可以给 offline 加两三分钟确认窗;sleep 不要进这扇窗。
事后取证:同一天时段用 queryCloudRecords 查云录像片段(queryRange 最大 100),再用 createDeviceRecordHls 生成回放地址。beginTime / endTime 格式是 yyyy-MM-dd HH:mm:ss,文档写明不支持跨天。点位没开通云存储、也没本地卡,就不要硬查。
告警进工单、遮挡进群,是下一篇的事。这篇停在「自己的页能出画,抽卡状态会变」。
联调会踩的坑
| 现象 | 多半是什么 | 先做什么 |
|---|---|---|
unBindDeviceInfo 一直 offline | 卡没激活 / 没费 / 电池空 | 官方 App 能预览再打接口 |
wifiConfigMode 没有 SIMCard | 这台不是蜂窝配网 | 不要按 4G 方案硬绑 |
把 5Ghz 当成 5G 网 | 那是 Wi-Fi 频段 | 蜂窝只看 SIMCard |
| HTTP 绑失败 | 新机 / 新固件不走纯 HTTP 绑定 | 看应用开发的 SDK 说明 |
getLiveStreamInfo 是空的 | 还没 bindDeviceLive | 先创建,再查 |
| 有地址、黑场 | 机在 sleep,或播了 HTTP 地址 | 等唤醒;换 HTTPS 的 hls |
把 sleep 当成掉线打电话 | 太阳能 4G 省电 | 和 offline 分开看 |
| 签名对不上标准案例 | 原始串拼错或混了过时算法 | 先对齐 MD5 案例,再谈业务 |
真正省下的不是再买一块太阳能板
这篇省下的不是多一路镜头、多一块板子,而是值班室不用盯安装师傅的 App,才能回答「鱼塘和对面卡口现在有没有画、掉的是没电还是没卡」。
只在官方 App 里看自家院子、出事了有人盯手机的散户,不必在这里把签名和直播页搭起来。已经有线有电、只差统一台账的园区,去看 bindDevice + listDeviceDetailsByPage 那条线就够了。
appSecret、设备密码只放环境变量。直播地址当密钥管。日志里的序列号对外部打码。接口以现行文档为准,不要混已标注不再维护的栏目。