- sample代码架构
- sample代码实现
- 开发注意事项
4.7.1 sample代码架构
固件升级功能只在机场上支持,Pilot2上云不支持。sample对固件的功能实现主要包含如下步骤:
- 前端上传固件离线升级包
- 机场上报设备固件属性
- 前端点击固件升级,将请求发送给后端,创建固件升级任务;后端收到前端请求后,将固件升级消息下发给机场,机场对升级消息进行相应。
- 固件升级是一个持续的动作,机场会通过thing/product/{gateway_sn}/events上报固件升级进度,云端收到进度通知,并会调用websocket通知前端。
4.7.2 sample代码实现
1. 用户从设备下载页面下载固件离线升级包,以机场固件包为例,开发者需要从机场下载页面下载离线固件升级包。并在前端页面将固件升级包上传到后端。
2. 机场在固件版本发生变化(如机场开机、机场固件升级等)时,会通过thing/product/{device_sn}/state主题向云端上报固件版本号firmware_version。
所有mqtt消息都会经过InboundMessageRouter#determineTargetChannels方法,在该方法中找到通过DeviceTopicEnum#find方法找到消息对应的主题,如果是 thing/product/{device_sn}/state 主题的消息会被选择到ChannelName.INBOUND_STATE通道中。
public class InboundMessageRouter extends AbstractMessageRouter {
@Override
@Router(inputChannel = ChannelName.INBOUND)
protected Collection<MessageChannel> determineTargetChannels(Message<?> message) {
// 获取消息头
MessageHeaders headers = message.getHeaders();
String topic = headers.get(MqttHeaders.RECEIVED_TOPIC).toString();
byte[] payload = (byte[])message.getPayload();
log.debug("received topic :{} \t payload :{}", topic, new String(payload));
// 根据消息topic不同,将消息转发到不同的channel中。
// thing/product/{device_sn}/state主题消息会被转发到ChannelName.INBOUND_STATE通道中。
DeviceTopicEnum topicEnum = DeviceTopicEnum.find(topic);
MessageChannel bean = (MessageChannel) SpringBeanUtils.getBean(topicEnum.getBeanName());
return Collections.singleton(bean);
}
}
上报固件版本号的消息被转发到ChannelName.INBOUND_STATE中后,通过
public IntegrationFlow stateDataRouterFlow() {
return IntegrationFlows
.from(ChannelName.INBOUND_STATE)
.transform(Message.class, source -> {
// 将上报的state消息根据消息内容转换为Java DTO。
try {
TopicStateRequest response = Common.getObjectMapper().readValue((byte[]) source.getPayload(), new TypeReference<TopicStateRequest>() {});
String topic = String.valueOf(source.getHeaders().get(MqttHeaders.RECEIVED_TOPIC));
String from = topic.substring((THING_MODEL_PRE + PRODUCT).length(), topic.indexOf(STATE_SUF));
return response.setFrom(from)
// 根据消息的gateway_sn和消息内容将直播消息转换为对应的Java DTO。
.setData(Common.getObjectMapper().convertValue(response.getData(), getTypeReference(response.getGateway(), response.getData())));
} catch (IOException e) {
throw new CloudSDKException(e);
}
}, null)
// 根据消息内容序列化后的DTO class类型将消息提交给不同的channel进行处理
// 上报固件版本号的消息交给ChannelName.INBOUND_STATE_DOCK_FIRMWARE_VERSION通道处理
.<TopicStateRequest, StateDataKeyEnum>route(response -> StateDataKeyEnum.find(response.getData().getClass()),
mapping -> Arrays.stream(StateDataKeyEnum.values()).forEach(key -> mapping.channelMapping(key, key.getChannelName())))
.get();
}
private Class getTypeReference(String gatewaySn, Object data) {
Set<String> keys = ((Map<String, Object>) data).keySet();
// 根据gateway_sn查找上线时的缓存,从缓存中获取当前上报state消息的设备是RC还是Dock
switch (SDKManager.getDeviceSDK(gatewaySn).getType()) {
case RC:
return RcStateDataKeyEnum.find(keys).getClassType();
case DOCK:
return DockStateDataKeyEnum.find(keys).getClassType();
default:
throw new CloudSDKException(CloudSDKErrorEnum.WRONG_DATA, "Unexpected value: " + SDKManager.getDeviceSDK(gatewaySn).getType());
}
}
ChannelName.INBOUND_STATE_DOCK_FIRMWARE_VERSION通道中的消息会被交给sdk包中的AbstractDeviceService#dockFirmwareVersionUpdate方法进行处理,sdk包中的默认实现是抛出异常,开发者需要定义AbstractDeviceService类的实现类,实现dockFirmwareVersionUpdate方法,处理固件版本号上报的逻辑,示例代码的实现为SDKDeviceService#dockFirmwareVersionUpdate:
@Override
public void dockFirmwareVersionUpdate(TopicStateRequest<DockFirmwareVersion> request, MessageHeaders headers) {
// 上报的固件版本号为空,直接返回
if (!StringUtils.hasText(request.getData().getFirmwareVersion())) {
return;
}
DeviceDTO device = DeviceDTO.builder()
.deviceSn(request.getFrom())
.firmwareVersion(request.getData().getFirmwareVersion())
// 固件是否需要一致性升级
.firmwareStatus(request.getData().getNeedCompatibleStatus() ?
DeviceFirmwareStatusEnum.UNKNOWN : DeviceFirmwareStatusEnum.CONSISTENT_UPGRADE)
.build();
// 调用deviceService#updateDevice更新固件升级状态
boolean isUpd = deviceService.updateDevice(device);
if (!isUpd) {
log.error("Data update of firmware version failed. SN: {}", request.getFrom());
}
}
deviceService#updateDevice:
@Override
public Boolean updateDevice(DeviceDTO deviceDTO) {
// 将设备的固件版本号保存进数据库
int update = mapper.update(this.deviceDTO2Entity(deviceDTO),
new LambdaUpdateWrapper<DeviceEntity>().eq(DeviceEntity::getDeviceSn, deviceDTO.getDeviceSn()));
return update > 0;
}
3. 前端通过Post ${url.manage.prefix}${url.manage.version}/devices/{workspace_id}/devices/ota创建固件升级任务,对应后端请求接口为DeviceController#createOtaJob:
@PostMapping("/{workspace_id}/devices/ota")
public HttpResultResponse createOtaJob(@PathVariable("workspace_id") String workspaceId,
@RequestBody List<DeviceFirmwareUpgradeDTO> upgradeDTOS) {
// 调用deviceService#createDeviceOtaJob处理
return deviceService.createDeviceOtaJob(workspaceId, upgradeDTOS);
}
deviceService#createDeviceOtaJob:
@Override
public HttpResultResponse createDeviceOtaJob(String workspaceId, List<DeviceFirmwareUpgradeDTO> upgradeDTOS) {
// 查找固件版本是否存在,不存在直接报错
List<OtaCreateDevice> deviceOtaFirmwares = deviceFirmwareService.getDeviceOtaFirmware(workspaceId, upgradeDTOS);
if (deviceOtaFirmwares.isEmpty()) {
return HttpResultResponse.error();
}
// 检查设备是否在线
Optional<DeviceDTO> deviceOpt = deviceRedisService.getDeviceOnline(deviceOtaFirmwares.get(0).getSn());
if (deviceOpt.isEmpty()) {
throw new RuntimeException("Device is offline.");
}
// 检查机场是否可以进行固件升级,如机场需要在空闲状态下才能进行固件升级等
DeviceDTO device = deviceOpt.get();
String gatewaySn = DeviceDomainEnum.DOCK == device.getDomain() ? device.getDeviceSn() : device.getParentSn();
checkOtaConditions(gatewaySn);
// 调用sdk包封装好的接口abstractFirmwareService#otaCreate,下发固件升级任务
TopicServicesResponse<ServicesReplyData<OtaCreateResponse>> response = abstractFirmwareService.otaCreate(
SDKManager.getDeviceSDK(gatewaySn), new OtaCreateRequest().setDevices(deviceOtaFirmwares));
ServicesReplyData<OtaCreateResponse> serviceReply = response.getData();
String bid = response.getBid();
if (!serviceReply.getResult().isSuccess()) {
return HttpResultResponse.error(serviceReply.getResult());
}
deviceOtaFirmwares.forEach(deviceOta -> deviceRedisService.setFirmwareUpgrading(deviceOta.getSn(),
EventsReceiver.<OtaProgress>builder().bid(bid).sn(deviceOta.getSn()).build()));
return HttpResultResponse.success();
}
abstractFirmwareService#otaCreate:
public TopicServicesResponse<ServicesReplyData<OtaCreateResponse>> otaCreate(GatewayManager gateway, OtaCreateRequest request) {
// 下发mqtt消息给设备,创建固件升级任务
return servicesPublish.publish(
new TypeReference<OtaCreateResponse>() {},
gateway.getGatewaySn(),
FirmwareMethodEnum.OTA_CREATE.getMethod(),
request);
}
4. 设备上报固件升级进度。机场会通过thing/product/{gateway_sn}/events主题持续上报云端固件升级进度。所有mqtt消息都会经过InboundMessageRouter#determineTargetChannels方法,在该方法中找到通过DeviceTopicEnum#find方法找到消息对应的主题,如果是 thing/product/{device_sn}/events 主题的消息会被选择到ChannelName.INBOUND_EVENTS通道中。
public class InboundMessageRouter extends AbstractMessageRouter {
@Override
@Router(inputChannel = ChannelName.INBOUND)
protected Collection<MessageChannel> determineTargetChannels(Message<?> message) {
// 获取消息头
MessageHeaders headers = message.getHeaders();
String topic = headers.get(MqttHeaders.RECEIVED_TOPIC).toString();
byte[] payload = (byte[])message.getPayload();
log.debug("received topic :{} \t payload :{}", topic, new String(payload));
// 根据消息topic不同,将消息转发到不同的channel中。
// thing/product/{gateway_sn}/events会被转发到ChannelName.INBOUND_EVENTS通道中。
DeviceTopicEnum topicEnum = DeviceTopicEnum.find(topic);
MessageChannel bean = (MessageChannel) SpringBeanUtils.getBean(topicEnum.getBeanName());
return Collections.singleton(bean);
}
}
消息被转发到ChannelName.INBOUND_EVENTS中后,通过EventsRouter#stateDataRouterFlow方法中定义的流程处理
@Bean
public IntegrationFlow eventsMethodRouterFlow() {
return IntegrationFlows
// 处理ChannelName.INBOUND_EVENT中的消息
.from(ChannelName.INBOUND_EVENTS)
.transform(Message.class, source -> {
try {
// 将上报的消息序列化为Request实体
TopicEventsRequest data = Common.getObjectMapper().readValue((byte[]) source.getPayload(), TopicEventsRequest.class);
String topic = String.valueOf(source.getHeaders().get(MqttHeaders.RECEIVED_TOPIC));
// 设置上报消息的sn
return data.setFrom(topic.substring((THING_MODEL_PRE + PRODUCT).length(), topic.indexOf(EVENTS_SUF)))
.setData(Common.getObjectMapper().convertValue(data.getData(), EventsMethodEnum.find(data.getMethod()).getClassType()));
} catch (IOException e) {
throw new CloudSDKException(e);
}
}, null)
.<TopicEventsRequest, EventsMethodEnum>route(
// 固件升级进度上报的消息,经过该方法会将消息推送给ChannelName.INBOUND_EVENTS_OTA_PROGRESS通道处理
response -> EventsMethodEnum.find(response.getMethod()),
mapping -> Arrays.stream(EventsMethodEnum.values()).forEach(
methodEnum -> mapping.channelMapping(methodEnum, methodEnum.getChannelName())))
.get();
}
ChannelName.INBOUND_EVENTS_OTA_PROGRESS的消息会交给AbstractFirmwareService#otaProgress处理,在sdk中默认实现为抛出异常信息,开发者需要定义AbstractFirmwareService的实现类,实现otaProgress方法。示例代码中有默认实现:DeviceFirmwareServiceImpl#otaProgress
@Override
public TopicEventsResponse<MqttReply> otaProgress(TopicEventsRequest<EventsDataRequest<OtaProgress>> request, MessageHeaders headers) {
// 获取gateway_sn
String sn = request.getGateway();
EventsReceiver<OtaProgress> eventsReceiver = new EventsReceiver<OtaProgress>()
.setBid(request.getBid())
.setOutput(request.getData().getOutput())
.setResult(request.getData().getResult());
log.info("SN: {}, {} ===> Upgrading progress: {}",
sn, request.getMethod(), eventsReceiver.getOutput().getProgress());
// 如果固件升级没有成功,打印报错日志
if (!eventsReceiver.getResult().isSuccess()) {
log.error("SN: {}, {} ===> Error: {}", sn, request.getMethod(), eventsReceiver.getResult());
}
// 检查设备是否在线,没有在线,直接返回
Optional<DeviceDTO> deviceOpt = deviceRedisService.getDeviceOnline(sn);
if (deviceOpt.isEmpty()) {
return null;
}
OtaProgressStatusEnum statusEnum = eventsReceiver.getOutput().getStatus();
DeviceDTO device = deviceOpt.get();
// 通过websocket协议,将固件升级消息推送给前端。
handleProgress(device.getWorkspaceId(), sn, eventsReceiver, statusEnum.isEnd());
handleProgress(device.getWorkspaceId(), device.getChildDeviceSn(), eventsReceiver, statusEnum.isEnd());
return new TopicEventsResponse<MqttReply>().setData(MqttReply.success());
}
4.7.3 开发注意事项
1. 固件升级目前需要注意关闭4G,并保证飞行器开机后进行升级。
2. 机场和飞机固件某些版本有放回滚策略,升级报错,请检查是否触发放回滚。可以使用DJI Assistant2手动升级查看是否报错。
3. 固件升级接口也可实现固件降级的逻辑。
评论
1 条评论
机场固件是不是已经包含了飞行器的固件?
上传机场固件的话,接口有个 device_name 参数是要传DeviceNameEnum里面的所有的值吗?
不是的话,官网 M30 飞机没有提供固件下载。机场固件上传后一直提示需要一致性更新,但是机场固件版本又是一致的。
请登录写评论。