Browse Source

把中农云的推送接口从center-controller移过来

修改面积计算方式
master
zhangyeguang 6 months ago
parent
commit
4f0f838972
  1. 16
      README.md
  2. 6
      social/pom.xml
  3. 44
      social/src/main/java/com/jiagutech/ams/controller/JobController.java
  4. 48
      social/src/main/java/com/jiagutech/ams/controller/MessageDataController.java
  5. 82
      social/src/main/java/com/jiagutech/ams/listener/JobFinishListener.java
  6. 15
      social/src/main/java/com/jiagutech/ams/model/TrackImageMapping.java
  7. 2
      social/src/main/java/com/jiagutech/ams/model/TrackItem.java
  8. 22
      social/src/main/java/com/jiagutech/ams/model/TrackMapping.java
  9. 20
      social/src/main/java/com/jiagutech/ams/model/ZhongNongTrack.java
  10. 21
      social/src/main/java/com/jiagutech/ams/model/request/MachImage.java
  11. 29
      social/src/main/java/com/jiagutech/ams/model/request/RealTimeData.java
  12. 41
      social/src/main/java/com/jiagutech/ams/model/request/WorkRecord.java
  13. 27
      social/src/main/java/com/jiagutech/ams/rest/TrackRestClient.java
  14. 75
      social/src/main/java/com/jiagutech/ams/rest/ZhongNongRestClient.java
  15. 58
      social/src/main/java/com/jiagutech/ams/scheduled/JobScheduled.java
  16. 2
      social/src/main/java/com/jiagutech/ams/service/JobService.java
  17. 6
      social/src/main/java/com/jiagutech/ams/service/JobServiceImpl.java
  18. 16
      social/src/main/java/com/jiagutech/ams/service/MessageDataService.java
  19. 72
      social/src/main/java/com/jiagutech/ams/service/MessageDataServiceImpl.java
  20. 22
      social/src/main/java/com/jiagutech/ams/utils/FlightTrackerUtils.java
  21. 264
      social/src/main/java/com/jiagutech/ams/utils/HuaweiObs.java
  22. 104
      social/src/main/java/com/jiagutech/ams/utils/TrackUtil.java
  23. 6
      web/pom.xml
  24. 2
      web/src/main/java/com/jiagutech/ams/common/RestTemplateInterceptor.java
  25. 30
      web/src/main/java/com/jiagutech/ams/config/ObsConfig.java
  26. 3
      web/src/main/java/com/jiagutech/ams/config/SaPermissionImpl.java
  27. 2
      web/src/main/resources/application-dev.yml
  28. 2
      web/src/main/resources/application-prod.yml
  29. 16
      web/src/main/resources/application.yml

16
README.md

@ -1,2 +1,18 @@
# ams-social
- 框架/依赖说明
- JDK17
- SpringBoot 3.1.7
- sa-token 登陆认证
- mapstruct、orika 对象拷贝
- mybatis-plus 数据持久化
- easy-excel excel导入导出
- hikari 数据库连接池
- openapi 注解生成接口文档
- rabbitmq 消息队列
- redis 缓存
- actuator/prometheus 监控
- huawei obs 对象存储
- fastjson2/gson/jackson json
- maven-pmd-plugin 静态代码检查插件
- 功能说明

6
social/pom.xml

@ -104,7 +104,11 @@
<artifactId>mapstruct-processor</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.huaweicloud</groupId>
<artifactId>esdk-obs-java</artifactId>
<version>3.20.6.1</version>
</dependency>
</dependencies>
</project>

44
social/src/main/java/com/jiagutech/ams/controller/JobController.java

@ -3,27 +3,31 @@ package com.jiagutech.ams.controller;
import cn.dev33.satoken.annotation.SaCheckRole;
import cn.dev33.satoken.annotation.SaMode;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.map.MapUtil;
import com.jiagutech.ams.listener.JobFinishListener;
import com.jiagutech.ams.model.TrackImageVO;
import com.jiagutech.ams.model.TrackItem;
import com.jiagutech.ams.model.common.PageRequest;
import com.jiagutech.ams.model.common.PageResult;
import com.jiagutech.ams.model.common.R;
import com.jiagutech.ams.model.dto.JobTypeDTO;
import com.jiagutech.ams.model.dto.TrackImageDTO;
import com.jiagutech.ams.model.request.JobCreateRequest;
import com.jiagutech.ams.model.request.JobPageRequest;
import com.jiagutech.ams.model.request.JobStatusRequest;
import com.jiagutech.ams.model.response.JobCreateResponse;
import com.jiagutech.ams.model.response.JobItem;
import com.jiagutech.ams.model.response.JobTypeItem;
import com.jiagutech.ams.service.JobService;
import com.jiagutech.ams.utils.TrackUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.apache.commons.collections4.MapUtils;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @ClassName JobController
@ -39,6 +43,8 @@ import java.util.List;
public class JobController {
private final JobService jobService;
private final JobFinishListener jobFinishListener;
@GetMapping("/types")
@Operation(summary = "作业类型列表")
public R<List<JobTypeDTO>> getJobTypes() {
@ -100,5 +106,39 @@ public class JobController {
return R.ok();
}
@GetMapping("/tracksByZhongnong")
@Operation(summary = "从中农云获取作业轨迹列表")
public R<List<TrackItem>> getTracksByZhongnongList(@RequestParam("deviceId") String deviceId,
@RequestParam long startTime,
@RequestParam long endTime) {
return R.ok(jobFinishListener.getTracks(deviceId, startTime, endTime));
}
@GetMapping("/countAreaByZhongnong/{jobId}")
@Operation(summary = "从中农云获取轨迹计算面积")
public R<Void> countAreaByZhongnong(@PathVariable("jobId") Long jobId) {
jobFinishListener.refreshJobArea(jobId);
return R.ok();
}
@GetMapping("/calcAreaByZhongnong")
@Operation(summary = "重新计算面积")
public R<Map> calcAreaByZhongnong(@RequestParam("deviceId") String deviceId,
@RequestParam long startTime,
@RequestParam long endTime,
@RequestParam float breadth) {
List<TrackItem> tracks = jobService.getTracks(deviceId, startTime, endTime);
List<TrackItem> tracks1 = jobFinishListener.getTracks(deviceId, startTime, endTime);
float noFilter = TrackUtil.calcAreaByBlockDataNoFilter(tracks,breadth);
float byFilter = TrackUtil.calcAreaByBlockData(tracks);
float zhongnongArea = TrackUtil.calcAreaByBlockDataNoFilter(tracks1,breadth);
Map<String, Float> areas = new HashMap<>(4);
areas.put("noFilter", noFilter);
areas.put("byFilter", byFilter);
areas.put("zhongnongArea", zhongnongArea);
return R.ok(areas);
}
}

48
social/src/main/java/com/jiagutech/ams/controller/MessageDataController.java

@ -0,0 +1,48 @@
package com.jiagutech.ams.controller;
import com.alibaba.fastjson2.JSONObject;
import com.jiagutech.ams.model.common.R;
import com.jiagutech.ams.model.request.MachImage;
import com.jiagutech.ams.model.request.RealTimeData;
import com.jiagutech.ams.model.request.WorkRecord;
import com.jiagutech.ams.service.MessageDataService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/MessageDataService")
public class MessageDataController {
private final MessageDataService messageDataService;
@PostMapping("/machineryRealtimeData")
public R addRealTimeData(@RequestBody List<RealTimeData> realTimeDatas) {
//if (checkSign(request,response)) {
log.info("RealTimeData:{}", JSONObject.toJSONString(realTimeDatas));
messageDataService.addRealTimeData(realTimeDatas);
return R.ok("success");
}
@PostMapping(value = "/machineryImage", consumes = "multipart/form-data")
public R machineryImage(@ModelAttribute MachImage machImage) {
log.info("workRecord:{}", machImage.getVehicleId());
messageDataService.machineryImage(machImage);
return R.ok("success");
}
@PostMapping("/machineryAreaDataList")
public R addWorkData(@RequestBody List<WorkRecord> workRecords) {
log.info("WorkRecord:{}", JSONObject.toJSONString(workRecords));
messageDataService.addWorkData(workRecords);
return R.ok("success");
}
}

82
social/src/main/java/com/jiagutech/ams/listener/JobFinishListener.java

@ -5,21 +5,28 @@ import com.jiagutech.ams.event.JobFinishEvent;
import com.jiagutech.ams.mapper.DeviceMapper;
import com.jiagutech.ams.mapper.JobMapper;
import com.jiagutech.ams.model.TrackItem;
import com.jiagutech.ams.model.TrackMapping;
import com.jiagutech.ams.model.ZhongNongTrack;
import com.jiagutech.ams.model.dto.DeviceDTO;
import com.jiagutech.ams.model.dto.JobDTO;
import com.jiagutech.ams.rest.TrackRestClient;
import com.jiagutech.ams.rest.ZhongNongRestClient;
import com.jiagutech.ams.scheduled.JobScheduled;
import com.jiagutech.ams.service.JobService;
import com.jiagutech.ams.utils.FlightTrackerUtils;
import com.jiagutech.ams.utils.TrackUtil;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Hex;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
@ -36,11 +43,18 @@ import java.util.concurrent.TimeUnit;
@Component
@RequiredArgsConstructor
public class JobFinishListener {
private final JobService jobService;
private final JobMapper jobMapper;
private final DeviceMapper deviceMapper;
private final ScheduledExecutorService taskScheduler = Executors.newScheduledThreadPool(1);
private final JobScheduled jobScheduled;
private final TrackRestClient trackRestClient;
private final JobService jobService;
@Value("${zhong-nong.client.key_id}")
private String keyId;
@Value("${zhong-nong.client.secret_key}")
private String secretKey;
@Async
@EventListener(JobFinishEvent.class)
public void finishJob(JobFinishEvent event) {
@ -53,12 +67,66 @@ public class JobFinishListener {
Instant instant2 = Instant.ofEpochMilli(endTime);
Duration duration = Duration.between(instant1, instant2);
jobDTO.setDuration((int) duration.toSeconds());
jobMapper.updateById(jobDTO);
jobScheduled.handleArea(jobDTO, 0);
taskScheduler.schedule(() -> handleArea(jobDTO), 30, TimeUnit.SECONDS);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public List<TrackItem> getTracks(String deviceId, long startTime, long endTime) {
List<TrackItem> trackList = getTracksFromZhongNong(deviceId, startTime, endTime);
log.info("tracks length={}", trackList.size());
return trackList;
}
public void refreshJobArea(long jobId) {
JobDTO jobDTO = jobMapper.selectById(jobId);
if ((jobDTO.getDuration()==null ||jobDTO.getDuration() == 0) && jobDTO.getStartTime() != null && jobDTO.getEndTime() != null) {
Instant instant1 = Instant.ofEpochMilli(jobDTO.getStartTime());
Instant instant2 = Instant.ofEpochMilli(jobDTO.getEndTime());
Duration duration = Duration.between(instant1, instant2);
jobDTO.setDuration((int) duration.toSeconds());
}
handleArea(jobDTO);
}
public void handleArea(JobDTO jobDTO) {
log.info("job:{},去获取轨迹计算作业面积", jobDTO.getId());
DeviceDTO deviceDTO = deviceMapper.selectById(jobDTO.getDeviceId());
log.info("从td-engine中获取轨迹");
List<TrackItem> tracks = jobService.getTracks(deviceDTO.getBoxNum(), jobDTO.getStartTime(), jobDTO.getEndTime());
if (CollectionUtil.isEmpty(tracks)) {
log.info("从中农云中获取轨迹");
tracks = getTracksFromZhongNong(deviceDTO.getBoxNum(), jobDTO.getStartTime(), jobDTO.getEndTime());
}
jobDTO.setArea(TrackUtil.calcAreaByBlockData(tracks));
TrackItem trackItem = tracks.get(tracks.size() - 1);
jobDTO.setLat(trackItem.getLat()).setLng(trackItem.getLng());
deviceDTO.setLat(trackItem.getLat()).setLng(trackItem.getLng()).setUpdateTime(trackItem.getTimestamp());
deviceMapper.updateById(deviceDTO);
jobMapper.updateById(jobDTO);
}
private List<TrackItem> getTracksFromZhongNong(String vehicleId, Long startTime, Long endTime) {
List<ZhongNongTrack> tracks = trackRestClient.getZhongNongTracks(keyId, secretKey, vehicleId, startTime, endTime);
log.info("中农云获取到的轨迹数量:{}", tracks != null ? tracks.size() : 0);
TrackMapping mapping = TrackMapping.INSTANCE;
return mapping.convertToTrackItemsByZhongNongTracks(tracks);
}
@SneakyThrows
private String signature(String keyId, long timestamp, String secretKey) {
MessageDigest md = MessageDigest.getInstance("MD5");
String str = "keyId=" + keyId + "&timestamp=" + timestamp + "&secretKey=" + secretKey;
byte[] digest = md.digest(str.getBytes(StandardCharsets.UTF_8));
return Hex.encodeHexString(digest);
}
}

15
social/src/main/java/com/jiagutech/ams/model/TrackImageMapping.java

@ -0,0 +1,15 @@
package com.jiagutech.ams.model;
import com.jiagutech.ams.model.request.MachImage;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper
public interface TrackImageMapping {
TrackImageMapping INTANCE = Mappers.getMapper(TrackImageMapping.class);
TrackImageVO convertByMachImage(MachImage machImage);
}

2
social/src/main/java/com/jiagutech/ams/model/TrackItem.java

@ -26,6 +26,8 @@ public class TrackItem {
private Float seeding;
private Double breadth;
private String imageUrl;

22
social/src/main/java/com/jiagutech/ams/model/TrackMapping.java

@ -4,8 +4,14 @@ import com.jiagutech.ams.model.dto.TrackImageDTO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.Named;
import org.mapstruct.factory.Mappers;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.List;
@Mapper
public interface TrackMapping {
@ -20,5 +26,21 @@ public interface TrackMapping {
TrackItem convertToTrackItemByLocus(Locus track);
@Mappings(value = {
@Mapping(target = "timestamp", source = "sendTime",qualifiedByName = "timeStrToLong"),
@Mapping(target = "lng", source = "longitude"),
@Mapping(target = "lat", source = "latitude")
})
TrackItem convertToTrackItemByZhongNongTrack(ZhongNongTrack track);
List<TrackItem> convertToTrackItemsByZhongNongTracks(List<ZhongNongTrack> zhongNongTrack);
@Named("timeStrToLong")
default Long timeStrToLong(String timeStr) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime localDateTime = LocalDateTime.parse(timeStr, formatter);
return localDateTime.toInstant(ZoneOffset.ofHours(8)).toEpochMilli(); // 使用UTC+8的时区偏移量
}
List<TrackItem> convertToTrackItemsByLocus(List<Locus> locusList);
}

20
social/src/main/java/com/jiagutech/ams/model/ZhongNongTrack.java

@ -0,0 +1,20 @@
package com.jiagutech.ams.model;
import lombok.Data;
@Data
public class ZhongNongTrack {
private String simNo;
private String sendTime;
private Float velocity;
private Double latitude;
private Double longitude;
private Integer direction;
}

21
social/src/main/java/com/jiagutech/ams/model/request/MachImage.java

@ -0,0 +1,21 @@
package com.jiagutech.ams.model.request;
import lombok.Data;
import org.springframework.web.multipart.MultipartFile;
@Data
public class MachImage {
// 设备Id
private String vehicleId;
// 时间戳
private long timestamp;
// 纬度,最多保留7位小数(CGCS2000 坐标系)
private double lat;
// 经度,最多保留7位小数(CGCS2000 坐标系)
private double lng;
private MultipartFile image;
private String imageUrl;
}

29
social/src/main/java/com/jiagutech/ams/model/request/RealTimeData.java

@ -0,0 +1,29 @@
package com.jiagutech.ams.model.request;
import lombok.Data;
@Data
public class RealTimeData {
// 设备Id
private String vehicleId;
// 时间戳
private long timestamp;
// 纬度,最多保留7位小数(CGCS2000 坐标系)
private double lat;
// 经度,最多保留7位小数(CGCS2000 坐标系)
private double lng;
// 状态[1位数字](0=不工作,1=工作,3=合格)
private int status;
// 幅宽(米)[浮点数,最多保留两位小数]
private Double breadth; // 可选字段
// 速度(千米/小时<km/h>)[浮点数,最多保留一位小数]
private Double velocity; // 可选字段
// 耕深(厘米)[浮点数,最多保留一位小数]
private Double deep; // 可选字段
// 流量(脉冲值)[浮点数,最多保留一位小数]
// 流量(脉冲值)[浮点数,最多保留一位小数]
private Double flow; // 可选字段
// 播种速度 粒/s转对应实体类
private Double seeding; // 可选字段
}

41
social/src/main/java/com/jiagutech/ams/model/request/WorkRecord.java

@ -0,0 +1,41 @@
package com.jiagutech.ams.model.request;
import com.jiagutech.ams.model.Locus;
import lombok.Data;
import java.util.List;
// 主要的作业记录类
@Data
public class WorkRecord {
// 达标面积(亩)
private Double qualifiedWorkArea;
// 重复作业面积(亩)
private Double repeatWorkArea;
// 作业面积(亩)
private Double workArea;
// 开始时间,毫秒时间戳
private Long startAt;
// 结束时间,毫秒时间戳
private Long endAt;
// 设备Id
private String vehicleId;
// 作业地点经度
private Double longitude;
// 作业地点纬度
private Double latitude;
// 耕深(厘米)
private Double avgDeep;
// 合格率
private Double qualifiedRate;
// 行驶里程(米)
private Double distance;
// 作业类型,编码见附表1
private Long module;
// 总流量(单位:L)
private Double sumFlow;
// 总播种量 (单位:粒)
private Integer seedingNumber;
// 地块轨迹
private List<Locus> locus;
}

27
social/src/main/java/com/jiagutech/ams/rest/TrackRestClient.java

@ -3,6 +3,7 @@ package com.jiagutech.ams.rest;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.TypeReference;
import com.jiagutech.ams.model.Locus;
import com.jiagutech.ams.model.ZhongNongTrack;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
@ -28,7 +29,7 @@ public class TrackRestClient {
@Value("${center-track.client.url}")
private String CenterTrackUrl;
private String centerBaseUrl;
public List<Locus> getTractorByDroneIdAndTime(String droneid, Long startTime, Long endTime) {
@ -36,9 +37,29 @@ public class TrackRestClient {
map.put("droneid", droneid);
map.put("startTime", startTime);
map.put("endTime", endTime);
String res = restTemplate.getForObject(CenterTrackUrl + "?droneid={droneid}&start={startTime}&end={endTime}", String.class, map);
return JSON.parseObject(res, new TypeReference<List<Locus>>() {});
String res = restTemplate.getForObject(centerBaseUrl + "/track?droneid={droneid}&start={startTime}&end={endTime}", String.class, map);
return JSON.parseObject(res, new TypeReference<List<Locus>>() {
});
}
public String saveTractor(List<Locus> tracks) {
String s = JSON.toJSONString(tracks);
log.info("传参==>" + s);
String result = restTemplate.postForObject(centerBaseUrl + "/track/saveTractor", s, String.class);
return result;
}
public List<ZhongNongTrack> getZhongNongTracks(String keyId, String secretKey, String droneId, Long startTime, Long endTime) {
Map<String, Object> map = new HashMap<>();
map.put("droneId", droneId);
map.put("startTime", startTime);
map.put("endTime", endTime);
map.put("keyId", keyId);
map.put("secretKey", secretKey);
String res = restTemplate.getForObject(centerBaseUrl + "/zhongnong/tracks?keyId={keyId}&secretKey={secretKey}&droneId={droneId}&start={startTime}&end={endTime}", String.class, map);
return JSON.parseObject(res, new TypeReference<List<ZhongNongTrack>>() {
});
}
}

75
social/src/main/java/com/jiagutech/ams/rest/ZhongNongRestClient.java

@ -0,0 +1,75 @@
package com.jiagutech.ams.rest;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.jiagutech.ams.model.ZhongNongTrack;
import io.swagger.v3.core.util.Json;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List;
@Slf4j
@Component
@RequiredArgsConstructor
public class ZhongNongRestClient {
private final RestTemplate restTemplate;
@Value("${zhong-nong.client.base_url}")
private String baseUrl;
public String getAccessToken(String keyId, String signature, long timestamp) {
// 设置请求头和请求体
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
JSONObject body = new JSONObject();
body.put("keyId", keyId);
body.put("signature", signature);
body.put("timestamp", timestamp);
HttpEntity<String> requestEntity = new HttpEntity<>(body.toJSONString(), headers);
ResponseEntity<String> response = restTemplate.postForEntity(baseUrl + "/ThirdParty/sso", requestEntity, String.class);
if (response.getStatusCode().is2xxSuccessful()) {
return response.getBody();
} else {
log.error("获取token失败:{}", response);
return null;
}
}
public List<ZhongNongTrack> getTracks(String accessToken, String simNo, long startTime, long endTime) {
// 设置请求头和请求体
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("concrete-token-id", accessToken);
JSONObject body = new JSONObject();
body.put("simNo", simNo);
DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
.withZone(ZoneId.of("Asia/Shanghai"));
body.put("startTime", df.format(Instant.ofEpochMilli(startTime)));
body.put("endTime", df.format(Instant.ofEpochMilli(endTime)));
log.info(" get tracks params :{}", body);
HttpEntity<String> requestEntity = new HttpEntity<>(body.toJSONString(), headers);
ResponseEntity<String> response = restTemplate.postForEntity(baseUrl + "/MessageDataService/vehicleLocusList", requestEntity, String.class);
if (response.getStatusCode().is2xxSuccessful()) {
return JSON.parseArray(response.getBody(), ZhongNongTrack.class);
} else {
log.error("获取轨迹失败:{}", response);
return null;
}
}
}

58
social/src/main/java/com/jiagutech/ams/scheduled/JobScheduled.java

@ -6,24 +6,20 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.jiagutech.ams.mapper.DeviceMapper;
import com.jiagutech.ams.mapper.JobMapper;
import com.jiagutech.ams.model.TrackItem;
import com.jiagutech.ams.model.common.PageRequest;
import com.jiagutech.ams.model.common.PageResult;
import com.jiagutech.ams.model.dto.DeviceDTO;
import com.jiagutech.ams.model.dto.JobDTO;
import com.jiagutech.ams.model.request.JobPageRequest;
import com.jiagutech.ams.model.response.JobItem;
import com.jiagutech.ams.service.JobService;
import com.jiagutech.ams.utils.FlightTrackerUtils;
import com.jiagutech.ams.utils.TrackUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.*;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@Slf4j
@Component
@ -32,7 +28,6 @@ public class JobScheduled {
private final JobService jobService;
private final JobMapper jobMapper;
private final DeviceMapper deviceMapper;
private final ScheduledExecutorService taskScheduler = Executors.newScheduledThreadPool(1);
@Scheduled(cron = "0 20 0 * * ?")
public void cronTask() {
@ -52,10 +47,6 @@ public class JobScheduled {
queryWrapper.eq("area", 0);
List<JobDTO> jobDTOS = jobMapper.selectList(queryWrapper);
for (JobDTO jobDTO : jobDTOS) {
Long startTime = jobDTO.getStartTime();
Long endTime = System.currentTimeMillis();
Instant instant1 = Instant.ofEpochMilli(startTime);
Instant instant2 = Instant.ofEpochMilli(endTime);
List<TrackItem> trackList = jobService.getTrackList(jobDTO.getId());
if (CollectionUtil.isEmpty(trackList)) {
continue;
@ -66,25 +57,8 @@ public class JobScheduled {
}
public void handleArea(JobDTO jobDTO, int retryCount) {
log.info("job:{}去获取轨迹计算作业面积,当前重试次数:{}", jobDTO.getId(), retryCount);
List<TrackItem> trackList = jobService.getTrackList(jobDTO.getId());
if (CollectionUtil.isNotEmpty(trackList)) {
update(jobDTO, trackList);
} else {
if (retryCount < 3) {
int[] retryDelays = {60, 120, 300};
int delay = retryDelays[retryCount];
taskScheduler.schedule(() -> handleArea(jobDTO, retryCount + 1), delay, TimeUnit.SECONDS);
} else {
log.info("job:{}达到最大重试次数,面积计算未成功,终止任务", jobDTO.getId());
}
}
}
private void update(JobDTO jobDTO, List<TrackItem> trackList) {
jobDTO.setArea(calculateArea(trackList));
jobDTO.setArea(TrackUtil.calcAreaByBlockData(trackList));
TrackItem trackItem = trackList.get(trackList.size() - 1);
jobDTO.setLat(trackItem.getLat()).setLng(trackItem.getLng());
DeviceDTO deviceDTO = new DeviceDTO().setId(jobDTO.getDeviceId()).setLat(trackItem.getLat()).setLng(trackItem.getLng()).setUpdateTime(trackItem.getTimestamp());
@ -93,24 +67,4 @@ public class JobScheduled {
}
private float calculateArea(List<TrackItem> trackList) {
try {
if (CollectionUtil.isEmpty(trackList)) {
return 0.0f;
}
List<FlightTrackerUtils.PointTemp> pointTemps = trackList.stream().map(item -> {
LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(item.getTimestamp()), ZoneId.systemDefault());
int speed = Math.round(item.getVelocity() * 5 / 18);
return new FlightTrackerUtils.PointTemp(localDateTime, item.getLat(), item.getLng(), speed);
}).toList();
if (CollectionUtil.isEmpty(pointTemps)) {
return 0.0f;
}
return (float) FlightTrackerUtils.calculateArea(pointTemps);
} catch (Exception e) {
log.error("calculate area error", e);
return 0.0f;
}
}
}

2
social/src/main/java/com/jiagutech/ams/service/JobService.java

@ -34,4 +34,6 @@ public interface JobService {
void saveJobImages(TrackImageVO trackImageDTO);
List<TrackItem> getTracks(String droneId,long startTime,long endTime);
}

6
social/src/main/java/com/jiagutech/ams/service/JobServiceImpl.java

@ -229,4 +229,10 @@ public class JobServiceImpl implements JobService {
}
return queryWrapper;
}
@Override
public List<TrackItem> getTracks(String droneId, long startTime, long endTime) {
List<Locus> tractorByDroneIdAndTime = trackRestClient.getTractorByDroneIdAndTime(droneId, startTime, endTime);
return TrackMapping.INSTANCE.convertToTrackItemsByLocus(tractorByDroneIdAndTime);
}
}

16
social/src/main/java/com/jiagutech/ams/service/MessageDataService.java

@ -0,0 +1,16 @@
package com.jiagutech.ams.service;
import com.jiagutech.ams.model.request.MachImage;
import com.jiagutech.ams.model.request.RealTimeData;
import com.jiagutech.ams.model.request.WorkRecord;
import java.util.List;
public interface MessageDataService {
void addRealTimeData(List<RealTimeData> realTimeDatas);
void machineryImage(MachImage machImage);
void addWorkData(List<WorkRecord> workRecords);
}

72
social/src/main/java/com/jiagutech/ams/service/MessageDataServiceImpl.java

@ -0,0 +1,72 @@
package com.jiagutech.ams.service;
import com.alibaba.fastjson2.JSONObject;
import com.jiagutech.ams.model.Locus;
import com.jiagutech.ams.model.TrackDataJG;
import com.jiagutech.ams.model.TrackImageMapping;
import com.jiagutech.ams.model.UavSortie;
import com.jiagutech.ams.model.request.MachImage;
import com.jiagutech.ams.model.request.RealTimeData;
import com.jiagutech.ams.model.request.WorkRecord;
import com.jiagutech.ams.rest.TrackRestClient;
import com.jiagutech.ams.utils.HuaweiObs;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
@RequiredArgsConstructor
public class MessageDataServiceImpl implements MessageDataService {
private final StringRedisTemplate stringRedisTemplate;
private final HuaweiObs huaweiObs;
private final JobService jobService;
private final TrackRestClient trackRestClient;
public void addRealTimeData(List<RealTimeData> realTimeDatas) {
for (RealTimeData realTimeData : realTimeDatas) {
TrackDataJG uavTrackDataRVO = new TrackDataJG();
uavTrackDataRVO.setLat(realTimeData.getLat());
uavTrackDataRVO.setLng(realTimeData.getLng());
uavTrackDataRVO.setDroneId(realTimeData.getVehicleId());
uavTrackDataRVO.setHvel(Float.parseFloat(String.valueOf(realTimeData.getVelocity())));
uavTrackDataRVO.setTimeStamp(realTimeData.getTimestamp());
String s = JSONObject.toJSONString(uavTrackDataRVO);
stringRedisTemplate.opsForValue().set("mtrack:" + realTimeData.getVehicleId(), s, 2, TimeUnit.MINUTES);
}
}
@SneakyThrows
public void machineryImage(MachImage machImage) {
String upload = huaweiObs.upload(machImage.getImage());
if (upload != null) {
log.info("保存图片到ams,图片链接:{}", upload);
machImage.setImageUrl(upload);
machImage.setImage(null);
TrackImageMapping mapping = TrackImageMapping.INTANCE;
jobService.saveJobImages(mapping.convertByMachImage(machImage));
}
}
@Override
public void addWorkData(List<WorkRecord> workRecords) {
for (WorkRecord workRecord : workRecords) {
List<Locus> locus = workRecord.getLocus();
//轨迹插入
if (locus.size() > 0) {
trackRestClient.saveTractor(locus);
}
}
}
}

22
social/src/main/java/com/jiagutech/ams/utils/FlightTrackerUtils.java

@ -7,10 +7,7 @@ import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;
import org.locationtech.jts.geom.*;
import org.locationtech.proj4j.BasicCoordinateTransform;
import org.locationtech.proj4j.CRSFactory;
import org.locationtech.proj4j.CoordinateReferenceSystem;
import org.locationtech.proj4j.ProjCoordinate;
import org.locationtech.proj4j.*;
import java.io.FileReader;
@ -364,5 +361,22 @@ public class FlightTrackerUtils {
System.out.println(sorties.get(i).toJson());
}
}
public static ProjCoordinate CGS2000ToWGS84(double x, double y) {
// 定义坐标转换器
CoordinateTransformFactory ctFactory = new CoordinateTransformFactory();
// 定义源和目标投影
CRSFactory crsFactory = new CRSFactory();
CoordinateReferenceSystem sourceCRS = crsFactory.createFromName("EPSG:4547"); // 原始坐标系
CoordinateReferenceSystem targetCRS = crsFactory.createFromName("EPSG:4326"); // 目标坐标系
// 创建转换器
CoordinateTransform transform = ctFactory.createTransform(sourceCRS, targetCRS);
// 执行坐标转换
ProjCoordinate srcCoord = new ProjCoordinate(x, y);
ProjCoordinate targetCoord = new ProjCoordinate();
transform.transform(srcCoord, targetCoord);
// 4. 输出转换后的正常经纬度坐标
return targetCoord;
}
}

264
social/src/main/java/com/jiagutech/ams/utils/HuaweiObs.java

@ -0,0 +1,264 @@
package com.jiagutech.ams.utils;
import cn.hutool.core.util.IdUtil;
import com.alibaba.fastjson2.JSON;
import com.obs.services.ObsClient;
import com.obs.services.model.*;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;
/**
* @author Lenovo
*/
@Slf4j
@Component
public class HuaweiObs {
//Access Key Id
//private String ak = "Access Key Id";
//Secret Access Key
//private String sk = "Secret Access Key";
//桶名称
private String bucketName = "jg-iot";
// 终端节点访问Endpoint
//private String endpoint = "Endpoint";
// 文件目录
private String prifix = "/farm";
// 访问域名 在域名后面或文件目录前加“/”
private String path = "/farm/";
@Resource
private ObsClient obsClient;
/**
* 文件上传
*
* @param file
* @return
* @throws IOException
*/
public String upload(MultipartFile file) throws IOException {
//ObsClient obsClient = null;
Calendar cal = Calendar.getInstance();
int year = cal.get(Calendar.YEAR);
int month = cal.get(Calendar.MONTH);
int day = cal.get(Calendar.DATE);
String fileName = IdUtil.simpleUUID() + file.getOriginalFilename().substring(file.getOriginalFilename().indexOf("."));
String objectName = prifix + "/" + year + "/" + month + "/" + day + "/" + fileName;
//obsClient = new ObsClient(ak, sk, endpoint);
PutObjectResult response = obsClient.putObject(bucketName, objectName, file.getInputStream());
log.info(JSON.toJSONString(response));
// 可选:调用成功后,记录调用成功的HTTP状态码和服务端请求ID
int statusCode = response.getStatusCode();
if (200 == statusCode) {
return response.getObjectUrl();
}
return null;
}
/**
* 上传文件--流式
*
* @param fileName 上传文件名称
* @param is 文件流
* @return
*/
public String uploadFile(String fileName, InputStream is) throws IOException {
//ObsClient obsClient = null;
try {
Calendar cal = Calendar.getInstance();
int year = cal.get(Calendar.YEAR);
int month = cal.get(Calendar.MONTH);
int day = cal.get(Calendar.DATE);
String objectName = prifix + "/" + year + "/" + month + "/" + day + "/" + fileName;
//obsClient = new ObsClient(ak, sk, endpoint);
HeaderResponse response = obsClient.putObject(bucketName, objectName, is);
log.info(JSON.toJSONString(response));
// 可选:调用成功后,记录调用成功的HTTP状态码和服务端请求ID
int statusCode = response.getStatusCode();
if (200 == statusCode) {
String objectUrl = path + objectName;
return objectUrl;
}
} finally {
obsClient.close();
}
return null;
}
/**
* 上传文件--字节数组
*
* @param fileName 上传文件名称
* @param fileType 文件路径
* @param is 文件流
* @return
*/
public boolean uploadFileByte(String fileName, FileType fileType, byte[] is) {
//ObsClient obsClient = null;
try {
String objectName = fileType.getType().concat("/").concat(fileName);
//obsClient = new ObsClient(ak, sk, endpoint);
HeaderResponse response = obsClient.putObject(bucketName, objectName, new ByteArrayInputStream(is));
// 可选:调用成功后,记录调用成功的HTTP状态码和服务端请求ID
int statusCode = response.getStatusCode();
if (200 == statusCode) {
return true;
}
obsClient.close();
} catch (IOException e) {
log.info("文件上传失败:{}", e.getMessage(), e);
}
return false;
}
/**
* 下载文件
*
* @param fileName 文件名称
* @param fileType 文件路径
* @return
*/
public String getDownloadUrl(String fileName, FileType fileType) {
//ObsClient obsClient = null;
//obsClient = new ObsClient(ak, sk, endpoint);
// URL有效期,3600秒.5分钟
long expireSeconds = 3600L;
String objectName = fileType.getType().concat("/").concat(fileName);
TemporarySignatureRequest request = new TemporarySignatureRequest(HttpMethodEnum.GET, expireSeconds);
request.setBucketName(bucketName);
request.setObjectKey(objectName);
TemporarySignatureResponse response = obsClient.createTemporarySignature(request);
return response.getSignedUrl();
}
/**
* 获取上传地址
*
* @param fileName 文件名称
* @param fileType 文件路径
* @return
*/
public String getUploadUrl(String fileName, FileType fileType) {
try {
// 创建ObsClient实例
//ObsClient obsClient = new ObsClient(ak, sk, endpoint);
// URL有效期,3600秒
long expireSeconds = 3600L;
Map<String, String> headers = new HashMap<String, String>();
headers.put("Content-Type", "application/octet-stream");
String objectName = fileType.getType().concat("/").concat(fileName);
TemporarySignatureRequest request = new TemporarySignatureRequest(HttpMethodEnum.PUT, expireSeconds);
request.setBucketName(bucketName);
request.setObjectKey(objectName);
request.setHeaders(headers);
TemporarySignatureResponse response = obsClient.createTemporarySignature(request);
return response.getSignedUrl();
} catch (Exception e) {
log.error("获取上传地址异常:{}", e.getMessage(), e);
}
return null;
}
/**
* 下载文件返回字节数组
*
* @param fileName 文件名称
* @param fileType 文件路径
* @return
*/
public byte[] downloadFile(String fileName, FileType fileType) {
try {
InputStream inputStream = downloadFileInputStream(fileName, fileType);
byte[] bytes = IOUtils.toByteArray(inputStream);
return bytes;
} catch (Exception e) {
log.error("下载文件异常:{}", e.getMessage(), e);
}
return null;
}
/**
* 上传视频
*
* @param fileName 文件名称
* @param fileType 文件路径
* @return
*/
public boolean uploadFileVideo(String fileName, FileType fileType, InputStream is) {
try {
String objectName = fileType.getType().concat("/").concat(fileName);
//ObsClient obsClient = new ObsClient(ak, sk, endpoint);
// 添加 ContentType (添加后可在浏览器中直接浏览,而非下载链接)
obsClient.putObject(bucketName, objectName, is);
obsClient.close();
return true;
} catch (Exception e) {
log.error("上传视频文件异常:{}", e.getMessage(), e);
}
return false;
}
/**
* 下载文件返回流式
*
* @param fileName 文件名称
* @param fileType 文件路径
* @return
*/
public InputStream downloadFileInputStream(String fileName, FileType fileType) {
try {
String objectName = fileType.getType().concat("/").concat(fileName);
// 用户拿到STS临时凭证后,通过其中的安全令牌(SecurityToken)和临时访问密钥(AccessKeyId和AccessKeySecret)生成OSSClient。
//ObsClient obsClient = new ObsClient(ak, sk, endpoint);
ObsObject obsObject = obsClient.getObject(bucketName, objectName);
obsClient.close();
return obsObject.getObjectContent();
} catch (Exception e) {
log.error("下载文件异常:{}", e.getMessage(), e);
}
return null;
}
public enum FileType {
TEST("TEST", "测试");
private String type;
private String desc;
FileType(String type, String desc) {
this.type = type;
this.desc = desc;
}
public String getType() {
return type;
}
public String getDesc() {
return desc;
}
}
}

104
social/src/main/java/com/jiagutech/ams/utils/TrackUtil.java

@ -0,0 +1,104 @@
package com.jiagutech.ams.utils;
import cn.hutool.core.collection.CollectionUtil;
import com.jiagutech.ams.model.TrackItem;
import lombok.extern.slf4j.Slf4j;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
@Slf4j
public class TrackUtil {
public static float calculateArea(List<TrackItem> trackList) {
try {
if (CollectionUtil.isEmpty(trackList)) {
return 0.0f;
}
List<FlightTrackerUtils.PointTemp> pointTemps = trackList.stream().map(item -> {
LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(item.getTimestamp()), ZoneId.systemDefault());
int speed = Math.round(item.getVelocity() * 5 / 18);
return new FlightTrackerUtils.PointTemp(localDateTime, item.getLat(), item.getLng(), speed);
}).toList();
if (CollectionUtil.isEmpty(pointTemps)) {
return 0.0f;
}
return (float) FlightTrackerUtils.calculateArea(pointTemps);
} catch (Exception e) {
log.error("calculate area error", e);
return 0.0f;
}
}
private static final double EARTH_RADIUS = 6371000;
// 计算总作业面积
public static double calculateArea(List<double[]> coordinates, double width) {
double totalDistance = 0.0;
// 累加轨迹点之间的距离
for (int i = 1; i < coordinates.size(); i++) {
double[] start = coordinates.get(i - 1);
double[] end = coordinates.get(i);
totalDistance += haversineDistance(start[1], start[0], end[1], end[0]);
}
// 作业面积 = 作业宽度 * 总距离 * 作业效率系数
return width * totalDistance * 0.7 * 0.0015; // Convert to mu (亩)
}
public static double haversineDistance(double lat1, double lon1, double lat2, double lon2) {
// 将纬度和经度从度转换为弧度
double lat1Rad = Math.toRadians(lat1);
double lon1Rad = Math.toRadians(lon1);
double lat2Rad = Math.toRadians(lat2);
double lon2Rad = Math.toRadians(lon2);
// 计算差值
double deltaLat = lat2Rad - lat1Rad;
double deltaLon = lon2Rad - lon1Rad;
// 应用Haversine公式
double a = Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2)
+ Math.cos(lat1Rad) * Math.cos(lat2Rad)
* Math.sin(deltaLon / 2) * Math.sin(deltaLon / 2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
// 返回距离(米)
return EARTH_RADIUS * c;
}
// // Haversine公式计算两点之间的距离
// public static double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
// double dLat = Math.toRadians(lat2 - lat1);
// double dLon = Math.toRadians(lon2 - lon1);
// double a = Math.sin(dLat / 2) * Math.sin(dLat / 2)
// + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
// * Math.sin(dLon / 2) * Math.sin(dLon / 2);
// double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
// return EARTH_RADIUS * c;
// }
public static float calcAreaByBlockData(List<TrackItem> pointTemps) {
AtomicReference<Double> breadth = new AtomicReference<>((double) 0);
List<double[]> coordinates = pointTemps.stream()
.filter(p -> p.getVelocity() > 0)
.filter(p -> p.getBreadth()!=null && p.getBreadth() > 0)
.map(item -> {
breadth.set(item.getBreadth());
return new double[]{item.getLng(), item.getLat()};
}).toList();
return (float) calculateArea(coordinates, breadth.get());
}
public static float calcAreaByBlockDataNoFilter(List<TrackItem> pointTemps,double breadth) {
List<double[]> coordinates = pointTemps.stream()
.map(item -> new double[]{item.getLng(), item.getLat()}).toList();
return (float) calculateArea(coordinates, breadth);
}
}

6
web/pom.xml

@ -111,6 +111,12 @@
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
<groupId>com.huaweicloud</groupId>
<artifactId>esdk-obs-java</artifactId>
<version>3.20.6.1</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<finalName>${project.parent.artifactId}</finalName>

2
web/src/main/java/com/jiagutech/ams/common/RestTemplateInterceptor.java

@ -19,7 +19,7 @@ public class RestTemplateInterceptor implements ClientHttpRequestInterceptor {
// 打印出请求的详细信息
log.info("URI: {}", request.getURI());
if (body != null && body.length > 0) {
log.debug("Request Body: {}", new String(body, StandardCharsets.UTF_8));
log.info("Request Body: {}", new String(body, StandardCharsets.UTF_8));
}
// 执行请求并获取响应

30
web/src/main/java/com/jiagutech/ams/config/ObsConfig.java

@ -0,0 +1,30 @@
package com.jiagutech.ams.config;
import com.obs.services.ObsClient;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Data
@Configuration
@NoArgsConstructor
@ConfigurationProperties(prefix = "huawei.obs")
public class ObsConfig {
private String endPoint;
private String ak;
private String sk;
private String bucketName;
private String pathPrefix;
@Bean
public ObsClient obsClient() {
ObsClient obsClient= new ObsClient(ak, sk, endPoint);
return obsClient;
}
}

3
web/src/main/java/com/jiagutech/ams/config/SaPermissionImpl.java

@ -1,12 +1,9 @@
package com.jiagutech.ams.config;
import cn.dev33.satoken.stp.StpInterface;
import cn.dev33.satoken.stp.StpUtil;
import com.jiagutech.ams.constant.UserConstants;
import com.jiagutech.ams.model.LoginUser;
import com.jiagutech.ams.utils.LoginUtil;
import java.util.ArrayList;
import java.util.List;

2
web/src/main/resources/application-dev.yml

@ -23,5 +23,5 @@ spring:
center-track:
client:
url: http://localhost:11003/iot/farm/track
url: http://localhost:11003/iot/farm

2
web/src/main/resources/application-prod.yml

@ -24,5 +24,5 @@ spring:
center-track:
client:
url: http://192.168.0.17:11003/iot/farm/track
url: http://192.168.0.6:11003/iot/farm

16
web/src/main/resources/application.yml

@ -107,3 +107,19 @@ info:
java: 17
author: zhangyeguang
description: 贵州农机监管平台-合作社模块
zhong-nong:
client:
base_url: http://47.104.95.74:8080/eop
key_id: 59B9D06F2CD5E50C9F5EFFACDBEB46DE
secret_key: 366466118BBB799F12479CFB4DCC9AB4
huawei:
obs:
end-point: "obs.cn-east-2.myhuaweicloud.com"
ak: "NMUAWG3TN50OSJESCIRV"
sk: "qG0VFwhiTAtiCuUaMAxJL3zsusp3W4GHs7THR9pC"
bucket-name: iot
path-prefix: farm
Loading…
Cancel
Save