边缘应用开发手册
1 基本概念说明
本文提到的边缘应用,是指部署在物业管理一体机的边缘应用,是基于IoT的领域服务对接方案实现的边缘应用。领域服务对接方案里有服务模型定义和数据模型定义,下面介绍一些相关的基本概念:
服务模型:
是一组可提供完整业务功能的HTTP/HTTPS接口。边缘应用开发者可以根据对应的场景需求和业务需求进行这套接口定义。
服务提供方:
服务提供方即是可以提供服务模型里定义的服务功能的应用。服务提供方即可以是云端应用,也可以是边缘端应用
服务依赖方:
服务依赖方即是使用服务模型里定义的服务功能的应用。服务依赖方也可以是云端应用或边缘端应用
数据模型:
使用场景:本地系统/设备上报的消息可以基于数据模型进行定义,例如人脸通行事件
通过数据模型和IoT数据总线机制,可实现应用间的数据信息流转
IoT平台提供了对数据进行增删改查的4个API,以及HTTP2方式的消息订阅机制
(数据提供方)应用可以通过数据添加的API接口,将本地系统或设备的消息送入数据总线。
(数据消费方)应用可以通过查询数据模型的API接口获取消息,也可以通过数据变更消息订阅方式获取消息。
数据模型支持图片文件上传,例如人脸通行事件的人脸照片
物业管理一体机框架:
物业管理一体机是基于K8s框架实现的,底层是EdgeBox底座,之上是基于docker的各种应用和服务。
LE组件是基于docker的服务程序,里面包含了支持各种设备接入驱动,例如门禁驱动,车行驱动,EBA设备驱动等
边缘应用也是基于docker的应用程序。边缘应用可通过编译出的jar包,打包成镜像,然后通过IoT云端平台将应用下发到指定的物业管理一体机。边缘应用的启动入口,可以通过应用jar包里的docker file指定。
2 整体架构
下面将介绍边缘端适配器应用在整体架构里的位置以及上下游模块的关系,便于更好的理解边缘应用开发方法
2.1 核心模块功能说明
云端应用:即SaaS应用,一般由ISV提供,是服务模型依赖方,负责服务模型的调用和数据订阅。
物联网云平台:即IoT云端平台。在领域服务对接方案里,与边缘端核心服务一起提供服务总线和数据总线框架服务。
适配器应用:即运行在物业管理一体机的边缘应用,是服务模型的提供方。该应用一方面接受云端应用通过IoT平台的服务模型调用,然后将调用转换成本地系统支持的接口调用,另外一方面该应用接受本地系统的事件上报,然后通过边缘数据总线,将消息安装数据模型格式要求,上传到IoT云端平台
本地系统:即项目现场的本地各种系统,例如立方停车场系统。
2.2 核心流程说明
基于边缘适配器应用的领域服务对接方案,核心流程包括两个:至上而下的服务模型调用,至下而上的数据上报。下面分别说明。下面的序号,与框架图中的序号一一对应。
服务调用:
(1)云端SaaS应用调用服务模型提供的HTTP服务。 发起服务调用时,需提供项目Appkey, 路径名称(path)里需包含服务模型ID+接口方法名称。
(2)边缘适配器应用,侦听到对应的服务调用后,进行适当的适配转换,再调用本地系统提供的HTTP服务
(3)本地系统有一套对外开放的HTTP服务。侦听到来自边缘适配器应用调用后,完成相应的功能并将结果返回。
数据上报:
(4)本地系统发生事件,可以将事件内容通过HTTP接口发送给边缘适配器应用。接口的URL可以双方(适配器应用/本地系统)约定。
(5)边缘适配器应用侦听到事件请求后,可以将事件内容转化成数据模型要求的格式,然后根据(边缘)IoT平台提供的数据插入接口,上报数据到边缘数据总线,然后内部流转到云端数据总线。
(6)云端SaaS应用,可以通过IoT平台提供的查询数据模型的API接口获取数据,也可以通过数据变更消息订阅方式获取数据。
3 边缘应用开发指导
这里介绍的边缘应用,是服务提供方应用。
3.1 边缘应用对接服务模型
边缘应用侦听到对应的服务调用后,进行适当的适配转换,再调用本地系统提供的HTTP服务。
边缘应用对接服务模型,可以参考IoT公开链接:服务总线:服务提供的开发示例。
3.2 边缘应用对接数据模型
具体对接可参考IoT公开链接:边缘应用数据总线对接。
这里只补充说明一些需要关注的内容:
通过本地系统环境变量获取appkey and appSecrect:
public static final String appkey =System.getenv("iot.hosting.appKey");
public static final StringappSecret =System.getenv("iot.hosting.appSecret");
//边缘端数据模型服务路由
private static finalStringDATA_EDGE_PATH =System.getenv("iot.hosting.api.domain");
请求参数:
请求参数里的modelId,就是对应的数据模型Id。
request.putParam("modelId","value1");
对于边缘端应用,下面两个请求参数可以忽略掉
request.putParam("scopeId","value2");
request.putParam("appId","value3");
上传文件数据模型接口说明:
下面链接文档提供的接口只是获得了需要上传的文件名称和URL: 获取文件上传地址接口
如返回接口示例:
{
"id":"6fr2c332-c1db-417c-aa15-8c5trg3r5d92",
"code":200,
"message":null,
"localizedMsg":null,
"data":{
"fileName":"5269712352e5.jpg",
"URL":"https://xxxxx.xxx.xx.com/xxx/file/5269712352e5.jpg?Expires=1557902379&OSSAccessKeyId=xxxx&Signature=xxxx"
}
开发者还需将真正需要上传的文件,上传到接口返回值里指定的fileName和URL,示例代码如下:
// response为上传文件数据模型接口的返回
result = new String(response.getBody(), "UTF-8");
UploadResult uploadResult = JSON.parseObject(result, UploadResult.class);
data nameAndPath = uploadResult.getData();
String url = nameAndPath.getUrl();
HttpClient httpClient = HttpClients.createDefault();
HttpResponse response;
HttpPut put = new HttpPut(url);
//fileBytes:为准备要上传的图片文件
HttpEntity reqEntity = EntityBuilder.create().setBinary(fileByte).build();
put.setEntity(reqEntity);
response = httpClient.execute(put);
4 服务依赖方应用开发指导
服务依赖方,例如云端SaaS应用,需要进行服务调用和订阅数据。这里也是列出需要特别关注的点。详细内容请参考:附录1 参考链接:1:边缘应用服务总线对接;3:服务总线
4.1 APPkey and AppSecrect
云端SaaS应用访问服务模型时,请求参数里的Appkey and AppSecrect, 可以从物联网应用服务平台的项目详情的开发配置里查看得到,如下图所示:
4.2 下载文件数据模型接口
可以参考边缘应用数据总线对接的下载文件数据模型接口:获取文件下载地址接口。
需要注意下面几点:
a: Appkey and Appsecrect:
请参考4.1 里的说明获取
b: filename:
入参列表里缺失了filename,实际上是需要的:
request.putParam("fileName","value1");
filename是从订阅的数据模型消息里获取到的
c: scopeid:
scopeid就是项目id。在物联网应用服务平台里选择打开某个项目后,浏览器地址栏里projectId就是scopeid,如下图所示:
下载文件数据模型接口返回参数里,提供了需下载的文件URL:
获取下载的图片路径的demo:
public static void main(String[] args) throws UnsupportedEncodingException {
IoTApiClientBuilderParams ioTApiClientBuilderParams =
new IoTApiClientBuilderParams();
ioTApiClientBuilderParams.setAppKey("333464769");
ioTApiClientBuilderParams.setAppSecret("532ecf2cb6554d46b195e6d240aaf30d");
SyncApiClient syncClient = new SyncApiClient(ioTApiClientBuilderParams);
IoTApiRequest request = new IoTApiRequest();
//设置api的版本
request.setApiVer("0.0.1");
request.setId("42423423");
request.putParam("fileName", "03d5999cb80b4cff94cfb8eec1723e54.jpg");
request.putParam("scopeId", "a124LsoSscaXN9qb"); //项目id
request.putParam("modelId", "iot_park_pass_record");
request.putParam("attrName", "plateNumberImage");
ApiResponse response = syncClient.postBody("api.link.aliyun.com",
"/data/model/data/download", request, true);
System.out.println("response code = " + response.getCode()
+ " response = " + new String(response.getBody(), "UTF-8"));
}
{
"id": "6fr2c332-c1db-417c-aa15-8c5trg3r5d92",
"code": 200,
"message": null,
"localizedMsg": null,
"data": {
"url": "https://xxxxx.xxx.xx.com/xxx/file/5269712352e5.jpg?Expires=1557902379&OSSAccessKeyId=uyedjYL******&Signature=sotMFFIq4RP%2BWJSDScE8SxvO******"
}
}
开发者还需要根据这个URL,进行文件下载操作。
4.3 数据查询/数据订阅
前面提到过,云端应用可以通过IoT平台提供的查询数据模型的API接口获取数据,也可以通过数据变更消息订阅方式获取数据。
可以分别参考下面内容实现,只是注意Appkey and Appsecrect需要参考4.1 里的说明得到
数据查询:请参考:查询数据接口
数据订阅:请参考:数据变更消息订阅
5 边缘端应用自测
5.1 物联网应用服务平台应用调试接口
应用通过AIoT开放平台创建部署成功后,可以基于平台的应用调试接口,进行服务接口的调用验证:
进入应用管理,点击需调试的应用。注意这个应用需要处于已发布状态,如下图所示
点击实例管理-测试:
点击服务提供测试,选择一个要调试验证的接口,点击右侧的进入接口调试界面
接口请求里,可以根据输入参数要求补充相关参数,然后点击发送,然后查看接口返回信息,如下图所示:
5.2 模拟本地系统事件上报:Postman
前置条件:
PC机安装了Postman软件
物业管理一体机部署了适配器应用
PC机需要与物业管理一体机在相同的局域网内
模拟事件上报:
下面示例,以《停车场系统领域模型V3.1-数据模型定义》-车辆通行为例
路径:物业一体机ip:port/具体路径
请求Body内容示例如下:
{
"carCode": "浙A5Cxxx",
"inTime": "2016-10-18 16:44:44",
"passTime": "2016-10-28 16:44:44",
"parkID": "88",
"inOrOut": "1",
"GUID": "134589c1d68d44d38dcb7f084b9cf8a1",
"channelID": "1",
"channelName": "北大门出口",
"imagePath": "https://ss1.bdstatic.com\\70cFuXSh_xxx\it\\u=3854694535,624476780&fm=11&gp=0.jpg"
}
5.3 模拟本地服务调用:Postman
前置条件:
PC机安装了Postman软件
物业管理一体机部署了适配器应用
PC机需要与物业管理一体机在相同的局域网内
本地部署了相应的系统,例如部署了xxx停车系统
模拟服务调用:
下面示例,以《停车场系统领域服务V3.1-服务模型定义》-1.1 查询停车场信息为例
路径: 物业一体机IP:port/服务模型ID/服务接口path
特别注意Header内容:定义 Content-Type 为 application/octet-stream
请求Body内容示例如下:
{
"id":"UniqueRequestId",
"version":"1.0",
"request":{
"apiVer":"1.0"
},
"params":{
}
}
Postman示例截图如下。其中192.168.1.40是物业一体机的内网IP地址,需根据实际情况填写,10060是固定的端口号。
6 示例代码
6.1 边缘端适配器应用对接服务模型
以车辆加入安全黑/白名单服务为例
1、入口层
package com.aliyun.iotx.parkinglot.adapter.web.servicemodelcontroller;
import com.alibaba.fastjson.JSON;
/**
* 黑白名单
*/
@Slf4j
@RestController
@RequestMapping(value = "/iotx_parking_service_model", method = RequestMethod.POST)
public class BlackWhiteListController {
@Autowired
private BlackWhiteListService blackWhiteListService;
/**
* 7.1车辆加入安全黑/白名单
* @param request
* @return
*/
@RequestMapping(value = "/parkingLotVehicleListAdd")
public IoTxResult vehicleAddList(HttpServletRequest request) throws Exception {
String json="";
json= new String(readInputStream(request.getInputStream()),"UTF-8");
//将json数据解析成vo对象接收
BWListVo bwListVo = JSON.parseObject(json, BWListVo.class);
BlackWhiteListVo blackWhiteList = bwListVo.getParams();
//自己业务的处理逻辑
blackWhiteListService.vehicleAddList(blackWhiteList);
IoTxResult<Object> ioTxResult = new IoTxResult<>();
ioTxResult.setData(null);
IoTxResultUtils.ioTxResultSet(ParkingLotAdapterEnum.SUCCDESS_VEHICLEADDLIST, ioTxResult);
return ioTxResult;
}
}
/**
依赖类:
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
class BWListVo implements Serializable {
private static final long serialVersionUID = -8067179280515471493L;
/**
* request里的全局唯一id透传
*/
private String id;
/**
* 请求协议版本
*/
private String version;
private Map<String, Object> request;
private BlackWhiteListVo params;
}
/**
* 车辆加入安全
* 黑白名单入参
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
@ValidateBean
class BlackWhiteListVo implements Serializable {
private static final long serialVersionUID = 831783475630914****L;
@NotBlank(message = "parkingLotId不能为空")
private String parkingLotId;
@JsonFormat(with = ACCEPT_SINGLE_VALUE_AS_ARRAY)
@EachValidate(constraint = NotBlank.class,message = "车牌号不能为空,不能有空字符串")
private List<String> plateNumber;
@JsonFormat(with = ACCEPT_SINGLE_VALUE_AS_ARRAY)
private List<String> areaId;
@NotBlank(message = "type不能为空")
private String type;
private String effectiveDate;
private String expiryDate;
}
6.2 边缘端适配器应用对接数据模型
以车辆通行数据模型为例:
车辆通行接口入口
package com.aliyun.iotx.parkinglot.adapter.web.datamodelcontroller;
import com.alibaba.cloudapi.sdk.model.ApiResponse;
/**
* 数据模型
* 车辆通行
*/
@Slf4j
@RestController
public class IoTParkPassRecordController {
@Autowired
private IoTParkPassRecordService parkPassRecordService;
/**
* 上传进出记录
* 图片地址格式
* @param inOutRecordVo
* @return
*/
@RequestMapping(value = "/reportInAndOutRecord", method = RequestMethod.POST)
public LFResultVo inOutRecordReport(@RequestBody InOutRecordVo inOutRecordVo) {
String newUrl = UrlFormatUtil.change(inOutRecordVo.getImagePath());
inOutRecordVo.setImagePath(newUrl);
ApiResponse apiResponse = parkPassRecordService.inOutRecordReport(inOutRecordVo);
LFResultVo lfResultVo = new LFResultVo();
if (apiResponse.getCode() == 200) {
lfResultVo.setResCode(0);
lfResultVo.setResMsg("数据上报成功");
} else {
lfResultVo.setResCode(1);
lfResultVo.setResMsg("数据上报失败");
}
return lfResultVo;
}
}
可以参考边缘应用数据总线对接
边缘应用数据总线参考使用:
public class BlackWhiteList {
/**
* 车辆通行上传
* @param request
* @return
*/
public static void main(String[] args) {
IoTApiRequest ioTApiRequest = new IoTApiRequest();
String uuid = UUID.randomUUID().toString();
String uuidOne = uuid.replace("-", "");
ioTApiRequest.setId(uuidOne);
ioTApiRequest.setApiVer("1.0");
ioTApiRequest.putParam("modelId", "iot_park_pass_record");
JSONObject properties = new JSONObject();
properties.put("direction", "获取入参的对应的值");
properties.put("openType", "获取入参的对应的值");
properties.put("plateNumber","获取入参的对应的值");
//首先获取到要上传的文件名和上传的路径
ApiResponse response = getUploadFileNameAndPath();
String result = null;
try {
result = new String(response.getBody(), "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
//将JSON字符串转化成对象
UploadResult uploadResult = JSON.parseObject(result, UploadResult.class);
data nameAndPath = uploadResult.getData();
properties.put("plateNumberImage", nameAndPath.getFileName());
//根据底层停车系统上报的文件路径下载文件
String lfFileUrl = inOutRecordVo.getImagePath();
//下载后文件的名字
String name = nameAndPath.getFileName();
//文件保存路径
String savePath = fsp.getSavePathOne();
if(!StringUtil.isEmpty(lfFileUrl)){
String url = FileUtil.getURL(lfFileUrl);
FileUtil.downLoadFromUrl(url,name,savePath);
}
//根据dop接口获取到的path将文件上传
//上传文件的路径
String url = nameAndPath.getUrl();
//要上传的文件
File file = new File(savePath + name);
if(file.exists()){
byte[] fileBytes = FileUtil.getFileBytes(file);
boolean b1 = FileUtil.uploadFile(url, fileBytes);
if (!b1) {
log.info("文件上传失败");
throw new ParkingLotAdapterException(502, "文件上传失败", "File upload failed");
}
}
//文件上传成功后,清除本地缓存的文件
FileUtil.deleteFile(savePath, name);
properties.put("typePermission", "获取入参的对应的值");
properties.put("plateColor", "获取入参的对应的值");
properties.put("plateType", "获取入参的对应的值");
properties.put("vehicleColor", "获取入参的对应的值");
properties.put("vehicleType", "获取入参的对应的值");
properties.put("barrierId", "获取入参的对应的值");
if (StringUtils.isNotBlank(inOutRecordVo.getChannelName())) {
properties.put("barrierName", "获取入参的对应的值");
}
properties.put("parkingLotId","获取入参的对应的值");
properties.put("areaId", "未知");
properties.put("orderNumber", "获取入参的对应的值");
properties.put("recordId", "未知");
//数据上报的时间
Date date = new Date();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String eventTime = dateFormat.format(date);
properties.put("eventTime",eventTime);
ioTApiRequest.putParam("properties", properties);
ApiResponse apiResponse = syncApiClient.postBody(host, path, ioTApiRequest, "https".equalsIgnoreCase(schema));
}
public static ApiResponse getUploadFileNameAndPath() {
IoTApiRequest request = new IoTApiRequest();
//设置api的版本
request.setApiVer("0.0.1");
// 接口参数
String uuid = UUID.randomUUID().toString();
request.setId(uuid.replace("-", ""));
JSONObject param = new JSONObject();
param.put("appId", "应用id");
param.put("version", "应用版本");
request.putParam("properties", param);
//这个参数对应于数据模型中需要上传文件的字段
request.putParam("attrName", "plateNumberImage");
//这个参数对应于数据模型的模型id
request.putParam("modelId", "iot_park_pass_record");
request.putParam("fileType", "文件类型");
request.putParam("version", "版本);
request.putParam("fileSize", "文件大小");
try {
ApiResponse apiResponse = syncApiClient.postBody(DATA_EDGE_PATH,
"/data/model/data/upload", request, false);
return apiResponse;
} catch (IOException e) {
log.info("上传文件获取文件名和上传路径接口出现异常:{}", e.getMessage());
}
return null;
}
}
pom依赖:
<dependency>
<groupId>com.aliyun.iotx</groupId>
<artifactId>iotx-api-gateway-client</artifactId>
</dependency>
<dependency>
<groupId>com.aliyun.iotx</groupId>
<artifactId>common-base</artifactId>
</dependency>
6.3 云端应用
服务模型调用示例代码:
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import com.alibaba.cloudapi.sdk.model.ApiResponse;
*/
public class RequestDemo {
public static void main(String[] args) throws UnsupportedEncodingException {
postRequestDemo();
}
public static void postRequestDemo() throws UnsupportedEncodingException {
IoTApiClientBuilderParams builderParams = new IoTApiClientBuilderParams();
builderParams.setAppKey("xxxxxxx");
builderParams.setAppSecret("xxxxxx");
SyncApiClient syncClient = new SyncApiClient(builderParams);
IoTApiRequest request = new IoTApiRequest();
//设置api的版本
request.setApiVer("1.0");
request.setId("42423423");
//请求参数域名、path、request
String host = "service-mesh.api-iot.cn-shanghai.aliyuncs.com";
String path = "/iotx_parking_service_model/parkingLotInfoGet";
ApiResponse response = syncClient.postBody(host, path, request);
System.out.println(
"response code = " + response.getCode() + " response content = " + new String(response.getBody(),
"utf-8"));
}
}
数据模型订阅消费示例代码:
public class Subscribe {
public static void main(String[] args) throws InterruptedException {
EventBus eventBus = new EventBus("li");
//注册所有的订阅
eventBus.register(new HelloEventListener());
String appKey = "您的AppKey";
String appSecret = "您的Secret";
String endpoint = String.format("https://%s.iot-as-http2.cn-shanghai.aliyuncs.com:443", appKey);
Profile profile = Profile.getAppKeyProfile(endpoint, appKey, appSecret);
MessageClient client = MessageClientFactory.messageClient(profile);
MessageCallback messageCallback = new MessageCallback() {
@Override
public Action consume(MessageToken messageToken) {
Message m = messageToken.getMessage();
System.out.println("receive : " + new String(messageToken.getMessage().getPayload()));
JSONObject jsonObject = JSON.parseObject(new String(messageToken.getMessage().getPayload()));
JSONArray dataIds = jsonObject.getJSONArray("dataIds");
eventBus.post(dataIds);
return MessageCallback.Action.CommitSuccess;
}
};
String topic = String.format("/sys/appkey/%s/dop/model/data/change", appKey);
client.setMessageListener(topic, messageCallback);
client.connect(messageToken -> {
System.out.println(messageToken.getMessage());
return MessageCallback.Action.CommitSuccess;
});
}
}
public class HelloEventListener {
@Subscribe
public void listen(JSONArray dataIds) {
System.out.println("receive3 msg:" + dataIds);
List<Long> objects = dataIds.toJavaList(Long.class);
System.out.println(objects);
for (Long object : objects) {
try {
getData(String.valueOf(object));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
}
public static void getData(String id) throws UnsupportedEncodingException {
IoTApiClientBuilderParams ioTApiClientBuilderParams =
new IoTApiClientBuilderParams();
ioTApiClientBuilderParams.setAppKey("您的AppKey");
ioTApiClientBuilderParams.setAppSecret("您的Secret");
SyncApiClient syncClient = new SyncApiClient(ioTApiClientBuilderParams);
IoTApiRequest request = new IoTApiRequest();
request.setApiVer("0.0.3");
request.putParam("modelId", "iot_park_pass_record");
List<String> returnFields = Lists.newArrayList("plateNumber");
request.putParam("returnFields", returnFields);
request.putParam("conditions", JSON.parseArray("[{\"fieldName\": \"id\",\"operate\": \"eq\",\"value\": " + id + "}]"));
ApiResponse response = syncClient.postBody("api.link.aliyun.com",
"/data/model/data/query", request, true);
System.out.println("response code = " + response.getCode()
+ " response = " + new String(response.getBody(), "UTF-8"));
}
}
pom依赖:
<dependency>
<groupId>com.aliyun.openservices</groupId>
<artifactId>iot-client-message</artifactId>
<version>1.1.3</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>3.7.1</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.11</version>
</dependency>
监听后的返回结果示例:
getData方法返回的结果:就可以得到刚刚上传的数据了。
重复消费的问题:
本地客户端的业务代码处理消息的时间过长,大于等于30s。服务端认为你消费失败了,就会向客户端重复推送数据。
附录 如何查看边缘应用日志
应用开发完成并部署到物业一体机进行调试,可以登录到物业一体机,查看对应的应用日志:
进入应用管理,点击需调试的适配器应用。注意这个应用需要处于已发布状态,如下图所示
选择实例管理-管理
选择节点运维
点击SSH终端,选择容器(一般只有一个,点击选择即可):
选择完容器,将自动登录到物业一体机。当前目录即保持有该应用的日志