前言
其实就是一次直接对接公司的物联平台服务,通过snack3解析物联平台定义的物模型,做设备的上下行。灵活接入各类设备,真香。
一、物联网大致架构
- 物联网并不难,平台架构基本能力是要能做到接入兼容、协议转换、数据存储、上下行稳定
- 接入方式:直接对协议、云对云
- 最终的物联平台提供的就是服务:主要是用户saas化、数据隔离、物模型管理(协议解析后的产物,物联公司的好坏就看对外暴露的物模型能力)
二、物联服务
物联平台首先肯定是要注册一个账号的,剩下的就是对接上下行。方式就有几种,如下:
1.透传方式
理解就是实际接口是设备、设备云提供的,上下行只是做数据的转发、留痕,类似中继。也是就是如果你的网络跟设备在一个网络实际就可以直接对它发指令,收上报数据。(大致这个意思,这不是今天分享的方向)
2.物联平台对外暴露物模型
业务系统上下行对接都依赖物模型,物联平台只对物模型上暴露的内容负责解析。重点来了,物模型对外暴露一般就是一个json串,上面有定义上行的字段,值域解释,下行服务定义,下行指令入参、入参值域。下面就示例一个近期对接的除湿机物模型:
物模型:
这个物模型json文件,以设备类型编码命名放在项目静态文件目录里。
实际上这里还可以改进用表记录,同时增加版本号的支持。当然放静态文件目录里也可以支持版本号,我这里这次没有考虑版本的支持。
{
"profile": {
"device_category": "以太网数据转换器",
"device_unit": "DTUA-ETH-02-0013",
"version": "1.0"
},
"properties": [
{
"dataType": {
"specs": {},
"type": "string"
},
"identifier": "register",
"name": "注册包"
},
{
"dataType": {
"specs": [
{
"dataType": {
"specs": {},
"type": "string"
},
"name": "室内温度故障",
"identifier": "indoor_temp_falut"
},
{
"dataType": {
"specs": {},
"type": "string"
},
"name": "新风温度故障",
"identifier": "air_temp_falut"
},
{
"dataType": {
"specs": {},
"type": "string"
},
"name": "冷凝温度故障",
"identifier": "condensationr_tempr_falut"
},
{
"dataType": {
"specs": {},
"type": "string"
},
"name": "电源故障",
"identifier": "power_falut"
},
{
"dataType": {
"specs": {},
"type": "string"
},
"name": "送风机过载",
"identifier": "blower_fault"
},
{
"dataType": {
"specs": {},
"type": "string"
},
"name": "压缩机1高压",
"identifier": "c1_high_pressure_falut"
},
{
"dataType": {
"specs": {},
"type": "string"
},
"name": "压缩机1低压",
"identifier": "c1_low_pressure_falut"
},
{
"dataType": {
"specs": {},
"type": "string"
},
"name": "压缩机2高压",
"identifier": "c2_high_pressure_falut"
},
{
"dataType": {
"specs": {},
"type": "string"
},
"name": "压缩机2低压",
"identifier": "c2_low_pressure_falut"
},
{
"dataType": {
"specs": {},
"type": "string"
},
"name": "电加热保护",
"identifier": "electric_heating_fault"
},
{
"dataType": {
"specs": {},
"type": "string"
},
"name": "失风报警",
"identifier": "lose_wind_fault"
},
{
"dataType": {
"specs": {},
"type": "string"
},
"name": "滤网报警",
"identifier": "strainer_fault"
},
{
"dataType": {
"specs": {},
"type": "string"
},
"name": "火灾报警",
"identifier": "fire_fault"
}
],
"type": "struct"
},
"identifier": "fault_reason",
"name": "故障原因"
},
{
"dataType": {
"specs": {
"0": "无故障",
"1": "有故障"
},
"type": "bool"
},
"identifier": "fault_status",
"name": "故障状态"
},
{
"dataType": {
"specs": {
"0": "不复位",
"1": "复位"
},
"type": "bool"
},
"name": "故障复位",
"identifier": "fault_reset"
},
{
"dataType": {
"specs": {
"0": "关机",
"1": "开机"
},
"type": "bool"
},
"name": "开关机",
"identifier": "switch_state"
},
{
"dataType": {
"specs": {
"0": "本地",
"1": "远程"
},
"type": "bool"
},
"name": "机组控制模式",
"identifier": "control_mode"
},
{
"dataType": {
"specs": {
"unit": "℃",
"min": "18",
"max": "32"
},
"type": "double"
},
"name": "设定温度",
"identifier": "set_temp"
},
{
"dataType": {
"specs": {
"unit": "℃"
},
"type": "double"
},
"name": "室内温度",
"identifier": "indoor_temp"
}
],
"services": [
{
"name": "设置机组控制方式",
"identifier": "set_control_mode",
"ackTimeout": null,
"inputs": [
{
"dataType": {
"specs": {
"0": "本地",
"1": "远程"
},
"type": "bool"
},
"name": "机组控制模式",
"identifier": "control_mode"
}
],
"ackType": 0,
"dataType": {
"specs": {}
},
"isPooling": 0
},
{
"ackTimeout": null,
"inputs": [
{
"dataType": {
"specs": {
"0": "不复位",
"1": "复位"
},
"type": "bool"
},
"name": "故障复位",
"identifier": "fault_reset"
}
],
"name": "故障复位",
"identifier": "set_fault_reset",
"ackType": 0,
"dataType": {
"specs": {}
}
},
{
"ackTimeout": null,
"inputs": [
{
"dataType": {
"specs": {
"0": "关机",
"1": "开机"
},
"type": "bool"
},
"name": "开关机",
"identifier": "switch_state"
}
],
"name": "设置开关机",
"identifier": "set_switch_state",
"ackType": 0,
"dataType": {
"specs": {}
},
"filterOpen": 1
},
{
"ackTimeout": null,
"inputs": [
{
"dataType": {
"specs": {
"unit": "℃",
"min": "18",
"max": "32"
},
"type": "double"
},
"name": "设定温度",
"identifier": "set_temp"
}
],
"name": "设置温度",
"identifier": "set_work_temp",
"ackType": 0,
"dataType": {
"specs": {}
},
"filterOpen": 1
},
{
"ackTimeout": null,
"inputs": [
{
"dataType": {
"specs": {
"1": "轮询数据"
},
"type": "enum",
"default": "1"
},
"name": "命令参数",
"identifier": "command"
}
],
"name": "获取故障信息",
"identifier": "get_fault_information",
"ackType": 0,
"isPooling": 1,
"poolingPeriod": 30,
"dataType": {
"specs": {}
}
},
{
"ackTimeout": null,
"inputs": [
{
"dataType": {
"specs": {
"1": "轮询数据"
},
"type": "enum",
"default": "1"
},
"name": "命令参数",
"identifier": "command"
}
],
"name": "获取室内温度",
"identifier": "get_indoor_temp",
"ackType": 0,
"isPooling": 1,
"poolingPeriod": 30,
"dataType": {
"specs": {}
}
},
{
"ackTimeout": null,
"inputs": [
{
"dataType": {
"specs": {
"1": "轮询数据"
},
"type": "enum",
"default": "1"
},
"name": "命令参数",
"identifier": "command"
}
],
"name": "获取开关机状态",
"identifier": "get_switch_state",
"ackType": 0,
"dataType": {
"specs": {}
},
"isPooling": 1,
"poolingPeriod": 30
},
{
"ackTimeout": null,
"inputs": [
{
"dataType": {
"specs": {
"1": "轮询数据"
},
"type": "enum",
"default": "1"
},
"name": "命令参数",
"identifier": "command"
}
],
"name": "获取设定温度",
"identifier": "get_set_temp",
"ackType": 0,
"isPooling": 1,
"poolingPeriod": 30,
"dataType": {
"specs": {}
}
}
]
}
**properties:**上行字段
**services:**下行服务
重点来了
上神器snack3,用于指令上下行。下面讲几个核心用例:
- 根据设备获取下行指令
public Result<?> getCommandByDeviceInfo(DeviceVo deviceVo, String lang) {
Integer id = deviceVo.getDeviceInfoDto().getId();
String deviceTypeNo = deviceVo.getDeviceTypeNo();
if (id == null && StringUtils.isBlank(deviceTypeNo)) {
return ResultGenerator.genFailResult("设备ID和设备类型编码不能同时为空");
}
if (id != null) {
DeviceInfo deviceInfo = deviceInfoMapper.selectByPrimaryKey(id);
if (deviceInfo == null) {
return ResultGenerator.genFailResult("设备信息不存在");
}
deviceTypeNo = deviceInfo.getDeviceTypeNo();
}
JSONArray jsonArr = null;
//获取物模型json串
//TODO 如果用表记录,这里可以查表,同时增加从设备信息获取物模型版本号查库找到物模型json字符串
String thingsJsonUrl = "/things/" + deviceTypeNo + ".json";
try {
String thingsJsonStr = ResourcesUtil.getResourceAsString(thingsJsonUrl);
//snack3出马,加载json字符串
ONode jsonONode = ONode.loadStr(thingsJsonStr);
if (jsonONode.isNull()) {
return ResultGenerator.genFailResult("指令集配置异常");
} else {
Boolean filterOpen = deviceVo.getFilterOpen();
String commandONodeStr = null;
if (filterOpen) {
//过滤查询物模型json串上设置的对外暴露的下行服务,语法挺有意思的,是不是有点熟悉?
commandONodeStr = jsonONode.select("$.services[?(filterOpen == 1)]").getString();
} else {
commandONodeStr = jsonONode.select("$.services").getString();
}
if (StringUtils.isNotBlank(commandONodeStr) && JSONUtil.isJsonArray(commandONodeStr)) {
jsonArr = JSONUtil.parseArray(commandONodeStr);
} else {
return ResultGenerator.genFailResult("指令集配置异常");
}
}
} catch (Exception e) {
return ResultGenerator.genFailResult("获取设备下行指令集异常,原因:" + e.getMessage());
}
return ResultGenerator.genSuccessResult(jsonArr);
}
返回的这些指令,前端基本可以根据类型、用不同组件展示,再也不是写死硬编码了。
- 上行数据的熟悉可用于规则引擎配置,这里就不多说了。
- 再说snack3的有趣使用
private void airFaultExport(DeviceVo deviceVo, List<JSONObject> list, HttpServletResponse response) {
List<DeviceAirRecordExport> exports = new ArrayList<>();
if (CollectionUtil.isNotEmpty(list)) {
list.stream().forEach(c -> {
//snack3解析bean,获取子属性值到新的对象
ONode node = ONode.load(c);
DeviceAirRecordExport export = new DeviceAirRecordExport();
export.setDeviceNo(node.select("$.deviceInfo.deviceNo").getString());
export.setName(node.select("$.deviceInfo.name").getString());
export.setInstallAddress(node.select("$.deviceInfo.installAddress").getString());
export.setFaultReason(node.select("$.faultReason").getString());
export.setRecordTime(node.select("$.recordTime").getDate());
exports.add(export);
});
}
Workbook workbook = DefaultExcelBuilder.of(DeviceAirRecordExport.class).build(exports);
AttachmentExportUtil.export(workbook, "设备故障记录", response);
}
对象里有子对象的bean再也不用一层层get、判断null,然后再取字段熟悉值了。
3.最后奉上snack3的集成使用
1、pom.xml增加依赖
<!-- snack3支持 -->
<dependency>
<groupId>org.noear</groupId>
<artifactId>snack3</artifactId>
<version>3.2.38</version>
</dependency>
2、任何地方直接
ONode开用,就这么简单,至于具体语法,可以去snack3的码云看,有微信、QQ群哦,这里我就不贴了,免得说做广告
总结
- 物联能力强弱就看对外暴露的方式
- 热爱物联网的欢迎留言交流,自己创业入行的欢迎来洽谈合作
- snack3真香
- snack3遇上对外暴露物模型json串真是福音,基本可以做到全部不固化硬编码,投入最小的成本就可以入行物联网。剩下的就是专心搞自己的销售,业务接入就这么简单。
希望能启发大家,uping