This commit is contained in:
2026-06-17 09:14:42 +08:00
parent 3879cd0067
commit a68a670ecb
27 changed files with 1053 additions and 392 deletions

View File

@@ -5,6 +5,8 @@ import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.maibu.annotation.Excel;
import com.maibu.core.domain.BaseDO;
import com.maibu.influxdb.MowerRealTimeData;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@@ -220,9 +222,8 @@ public class Device extends BaseDO {
@TableField(exist = false)
private DeviceRunningStatusHistory lastRunningStatus;
public DeviceRunningStatusHistory getLastRunningStatus() {
return lastRunningStatus != null ? lastRunningStatus : new DeviceRunningStatusHistory();
}
@TableField(exist = false)
private MowerRealTimeData lastMowerRealTimeData;
@TableField(exist = false)
private Map<Integer,Integer> statusStatistics;

View File

@@ -6,11 +6,10 @@ import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import com.maibu.core.domain.OrgBaseDO;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName(value = "iot_work_record", autoResultMap = true)

View File

@@ -0,0 +1,10 @@
package com.maibu.core.business.device;
import lombok.Data;
@Data
public class DeviceStatusDetail {
private String name;
private String value;
private String unit;
}

View File

@@ -98,6 +98,8 @@ public class NettyDevice extends Connector {
siteMemory.removeDevicePlanExecute(task.getDeviceId());
task = null;
currentTaskId = null;
currentPoint = null;
locationQueue.clear();
executingTask = false;
return finishId;
}
@@ -113,6 +115,7 @@ public class NettyDevice extends Connector {
siteMemory.removeDevicePlanExecute(task.getDeviceId());
task = null;
currentTaskId = null;
currentPoint = null;
locationQueue.clear();
executingTask = false;
}
@@ -152,8 +155,9 @@ public class NettyDevice extends Connector {
task.setTaskStaus(DeviceTaskStaus.EXECUTING);
executingTask = true;
sendPathCommand((byte) 0x01, (short) 3, 0.00d, 0.00d, (short) 0);
Thread.sleep(1000);
sendPathCommand((byte) 0x01, (short) 1, currentPoint.getLat(), currentPoint.getLng(), (short) 0);
// Thread.sleep(1000);
// sendPathCommand((byte) 0x01, (short) 1, currentPoint.getLat(),
// currentPoint.getLng(), (short) 0);
// sendNextPoint();
return true;
} else {

View File

@@ -1,29 +0,0 @@
package com.maibu.influxdb;
import com.maibu.influxdb.inter.InfluxField;
import com.maibu.influxdb.inter.InfluxTag;
import com.maibu.influxdb.inter.InfluxTime;
import com.maibu.influxdb.inter.InfluxMeasurement;
import lombok.Data;
import java.time.Instant;
//@Measurement(name = "device_data")
@Data
@InfluxMeasurement("device_data")
public class DeviceData {
// @Column(tag = true)
@InfluxTag
private String deviceId;
@InfluxField
private Double temperature;
@InfluxField
private Double voltage;
@InfluxTime
private Instant time;
}

View File

@@ -0,0 +1,155 @@
package com.maibu.influxdb;
import java.time.Instant;
import java.util.List;
import org.springframework.util.CollectionUtils;
import com.influxdb.query.FluxRecord;
import com.maibu.influxdb.inter.InfluxField;
import com.maibu.influxdb.inter.InfluxMeasurement;
import com.maibu.influxdb.inter.InfluxTag;
import com.maibu.influxdb.inter.InfluxTime;
import lombok.Data;
//@Measurement(name = "device_data")
@Data
@InfluxMeasurement("mower_realTime_data")
public class MowerRealTimeData {
// @Column(tag = true)
@InfluxTag
private String deviceId;
@InfluxField
private String voltage;// 电压
@InfluxField
private String leftTargetSpeed; // 左轮目标速度
@InfluxField
private String rightTargetSpeed;// 右轮目标速度
@InfluxField
private String leftMeasureSpeed;// 左轮测量速度
@InfluxField
private String rightMeasureSpeed;// 右轮测量速度
@InfluxField
private String leftCurrent;// 左轮电流
@InfluxField
private String rightCurrent;// 右轮电流
@InfluxField
private String leftMotorTemp;// 左轮温度
@InfluxField
private String rightMotorTemp;// 右轮温度
@InfluxField
private String chipTemp;// 芯片温度
@InfluxField
private String yaw;// 偏航角
@InfluxField
private String pitch;// 仰俯角
@InfluxField
private String roll;// 翻滚角
@InfluxField
private String satelliteCnt;// 跟踪的卫星数
@InfluxField
private String qual;// 定位质量
@InfluxField
private String headingStatus;// 航向角状态
@InfluxField
private String latitude;// 纬度
@InfluxField
private String longitude;// 经度
@InfluxField
private String cuttingSpeed;// 割刀速度
@InfluxField
private String controlMode;// 控制模式
@InfluxField
private String battery;// 电池电量
@InfluxField
private String workingArea;// 作业面积
@InfluxField
private String obstacleSign;// 障碍物标志位
@InfluxTime
private Instant time;
public static MowerRealTimeData transferLatestInfluxData(FluxRecord record) {
if (record == null || record.getValue() == null) {
return null;
}
MowerRealTimeData data = new MowerRealTimeData();
data.setDeviceId((String) record.getValueByKey("deviceId"));
data.setVoltage((String) record.getValueByKey("voltage"));
data.setLeftTargetSpeed((String) record.getValueByKey("leftTargetSpeed"));
data.setRightTargetSpeed((String) record.getValueByKey("rightTargetSpeed"));
data.setLeftMeasureSpeed((String) record.getValueByKey("leftMeasureSpeed"));
data.setRightMeasureSpeed((String) record.getValueByKey("rightMeasureSpeed"));
data.setLeftMotorTemp((String) record.getValueByKey("leftMotorTemp"));
data.setRightMotorTemp((String) record.getValueByKey("rightMotorTemp"));
data.setLeftCurrent((String) record.getValueByKey("leftCurrent"));
data.setRightCurrent((String) record.getValueByKey("rightCurrent"));
data.setChipTemp((String) record.getValueByKey("chipTemp"));
data.setYaw((String) record.getValueByKey("yaw"));
data.setPitch((String) record.getValueByKey("pitch"));
data.setRoll((String) record.getValueByKey("roll"));
data.setSatelliteCnt((String) record.getValueByKey("satelliteCnt"));
data.setQual((String) record.getValueByKey("qual"));
data.setHeadingStatus((String) record.getValueByKey("headingStatus"));
data.setLatitude((String) record.getValueByKey("latitude"));
data.setLongitude((String) record.getValueByKey("longitude"));
data.setCuttingSpeed((String) record.getValueByKey("cuttingSpeed"));
data.setControlMode((String) record.getValueByKey("controlMode"));
data.setBattery((String) record.getValueByKey("battery"));
data.setWorkingArea((String) record.getValueByKey("workingArea"));
data.setObstacleSign((String) record.getValueByKey("obstacleSign"));
return data;
}
}

View File

@@ -1,65 +0,0 @@
package com.maibu.influxdb;
import com.influxdb.query.FluxRecord;
import com.influxdb.query.FluxTable;
import com.maibu.influxdb.util.InfluxDBUtil;
import com.maibu.influxdb.util.InfluxSqlBuilder;
import org.springframework.beans.factory.annotation.Value;
import java.time.Instant;
import java.util.List;
import java.util.Map;
public class test {
@Value("${ioTDA.serverIp}")
private String serverIp;
// @Value("${ioTDA.deviceId}")
// private String deviceId;
@Value("${ioTDA.secret}")
private String secret;
@Value("${ioTDA.serviceId}")
private String serviceId;
public static void main(String[] args) {
String url = "http://localhost:8086";
String token = "maibu-token";
String org = "maibu";
String bucket = "device_data";
//
// InfluxDBClient client = InfluxDBClientFactory.create(url, token.toCharArray(), org, bucket);
//
//
// WriteApiBlocking writeApi = client.getWriteApiBlocking();
InfluxDBUtil dbUtil = new InfluxDBUtil(url,token,org,bucket);
DeviceData data = new DeviceData();
data.setDeviceId("AGV_001");
data.setTemperature(36.522);
data.setVoltage(48.222);
data.setTime(Instant.now());
String line = InfluxSqlBuilder.buildLine(data);
dbUtil.write(line);
Map<String, String> map = InfluxSqlBuilder.buildTagMap(data,DeviceData::getDeviceId);
String flux = InfluxSqlBuilder.buildQuery(bucket,bucket,10,map,"temperature");
List<FluxTable> tables = dbUtil.query(flux);
for (FluxTable table : tables) {
for (FluxRecord record : table.getRecords()) {
System.out.println(record.getValue());
}
}
}
}

View File

@@ -14,15 +14,15 @@ public class InfluxDBUtil {
private InfluxDBClient client;
private WriteApi writeApi;
private String bucket;
private String org;
public String bucket;
public String org;
public InfluxDBUtil(String url, String token, String org, String bucket) {
this.client = InfluxDBClientFactory.create(url, token.toCharArray(), org, bucket);
this.bucket = bucket;
this.org = org;
// ✅ 异步批量写入(核心)
// 异步批量写入(核心)
this.writeApi = client.makeWriteApi(
WriteOptions.builder()
.batchSize(5000) // 每5000条写一次
@@ -32,20 +32,20 @@ public class InfluxDBUtil {
.build()
);
// ✅ 监听写入成功
// 监听写入成功
this.writeApi.listenEvents(WriteSuccessEvent.class, event -> {
// 可关闭或打debug日志
// System.out.println("写入成功");
});
// ✅ 监听异常
// 监听异常
this.writeApi.listenEvents(WriteErrorEvent.class, event -> {
System.err.println("InfluxDB写入失败: " + event.getThrowable().getMessage());
});
}
// =========================
// ✅ 批量写入(推荐 Line Protocol)
// 批量写入(推荐 Line Protocol)
// =========================
public void writeBatch(List<String> lines) {
for (String line : lines) {
@@ -54,14 +54,14 @@ public class InfluxDBUtil {
}
// =========================
// ✅ 单条写入
// 单条写入
// =========================
public void write(String line) {
writeApi.writeRecord(bucket, org, WritePrecision.MS, line);
}
// =========================
// ✅ 查询(返回原始结果)
// 查询(返回原始结果)
// =========================
public List<FluxTable> query(String flux) {
QueryApi queryApi = client.getQueryApi();
@@ -69,7 +69,7 @@ public class InfluxDBUtil {
}
// =========================
// ✅ 查询(转List<Map>)
// 查询(转List<Map>)
// =========================
public List<String> queryValues(String flux) {
List<String> resultList = new ArrayList<>();
@@ -86,14 +86,14 @@ public class InfluxDBUtil {
}
// =========================
// ✅ 手动flush(重要)
// 手动flush(重要)
// =========================
public void flush() {
writeApi.flush();
}
// =========================
// ✅ 关闭资源(必须)
// 关闭资源(必须)
// =========================
public void close() {
try {

View File

@@ -6,6 +6,7 @@ import com.maibu.influxdb.inter.InfluxTime;
import com.maibu.influxdb.inter.InfluxMeasurement;
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.StringJoiner;
@@ -13,6 +14,7 @@ import java.util.StringJoiner;
public class InfluxSqlBuilder {
/**
* 插入语句拼接
*
* @param obj
* @return
*/
@@ -41,7 +43,8 @@ public class InfluxSqlBuilder {
field.setAccessible(true);
Object value = field.get(obj);
if (value == null) continue;
if (value == null)
continue;
String key = escapeKey(field.getName());
@@ -89,49 +92,130 @@ public class InfluxSqlBuilder {
}
}
// =========================
// ✅ 构建 Flux 查询
// =========================
// public static String buildQuery(
// String bucket,
// String measurement,
// long startHoursAgo,
// Map<String, String> tagFilters,
// String field) {
// StringBuilder flux = new StringBuilder();
// flux.append("from(bucket:\"").append(bucket).append("\")")
// .append(" |> range(start: -").append(startHoursAgo).append("h)")
// .append(" |> filter(fn: (r) => r._measurement == \"")
// .append(measurement).append("\")");
// // tag过滤
// if (tagFilters != null && !tagFilters.isEmpty()) {
// for (Map.Entry<String, String> entry : tagFilters.entrySet()) {
// flux.append(" |> filter(fn: (r) => r.")
// .append(entry.getKey())
// .append(" == \"")
// .append(entry.getValue())
// .append("\")");
// }
// }
// // field过滤
// if (field != null) {
// flux.append(" |> filter(fn: (r) => r._field == \"")
// .append(field)
// .append("\")");
// }
// // 按时间倒序
// flux.append(" |> sort(columns:[\"_time\"], desc:true)");
// return flux.toString();
// }
public static String buildQuery(
String bucket,
String measurement,
long startSecondsAgo,
Map<String, String> tagFilters,
String field
) {
long startHoursAgo,
Map<String, ? extends Collection<String>> tagFilters,
String field,
boolean latestPerDevice,
String groupColumn) {
StringBuilder flux = new StringBuilder();
flux.append("from(bucket:\"").append(bucket).append("\")")
.append(" |> range(start: -").append(startSecondsAgo).append("h)")
flux.append("from(bucket:\"")
.append(bucket)
.append("\")")
.append(" |> range(start: -")
.append(startHoursAgo)
.append("h)")
.append(" |> filter(fn: (r) => r._measurement == \"")
.append(measurement).append("\")");
.append(measurement)
.append("\")");
// tag过滤
// Tag过滤
if (tagFilters != null && !tagFilters.isEmpty()) {
for (Map.Entry<String, String> entry : tagFilters.entrySet()) {
flux.append(" |> filter(fn: (r) => r.")
.append(entry.getKey())
.append(" == \"")
.append(entry.getValue())
.append("\")");
for (Map.Entry<String, ? extends Collection<String>> entry : tagFilters.entrySet()) {
Collection<String> values = entry.getValue();
if (values == null || values.isEmpty()) {
continue;
}
flux.append(" |> filter(fn: (r) => ");
boolean first = true;
for (String value : values) {
if (!first) {
flux.append(" or ");
}
flux.append("r.")
.append(entry.getKey())
.append(" == \"")
.append(value)
.append("\"");
first = false;
}
flux.append(")");
}
}
// field过滤
if (field != null) {
// Field过滤
if (field != null && !field.isEmpty()) {
flux.append(" |> filter(fn: (r) => r._field == \"")
.append(field)
.append("\")");
}
// 每个设备最新一条
if (latestPerDevice) {
if (groupColumn == null || groupColumn.isEmpty()) {
groupColumn = "deviceId";
}
flux.append(" |> group(columns:[\"")
.append(groupColumn)
.append("\"])")
.append(" |> last()");
} else {
flux.append(" |> sort(columns:[\"_time\"], desc:true)");
}
return flux.toString();
}
/**
* 类的某些属性转换成map
*
* @param obj
* @param functions
* @return
@@ -139,8 +223,7 @@ public class InfluxSqlBuilder {
*/
public static <T> Map<String, String> buildTagMap(
T obj,
SFunction<T, ?>... functions
) {
SFunction<T, ?>... functions) {
Map<String, String> map = new HashMap<>();
try {
@@ -190,5 +273,4 @@ public class InfluxSqlBuilder {
return value.toString(); // double/float
}
}

View File

@@ -4,6 +4,8 @@ import java.beans.Introspector;
import java.lang.invoke.SerializedLambda;
import java.lang.reflect.Method;
import com.maibu.influxdb.inter.InfluxMeasurement;
public class LambdaUtils {
public static <T> String getFieldName(SFunction<T, ?> fn) {
@@ -26,4 +28,16 @@ public class LambdaUtils {
throw new RuntimeException("解析字段失败", e);
}
}
public static String getMeasurement(Class<?> clazz) {
InfluxMeasurement annotation = clazz.getAnnotation(InfluxMeasurement.class);
if (annotation == null) {
throw new IllegalArgumentException(
clazz.getName() + " 未配置 @InfluxMeasurement");
}
return annotation.value();
}
}

View File

@@ -1,30 +1,26 @@
package com.maibu.memory;
import com.maibu.core.business.Device;
import com.maibu.core.business.IoTCommonDevice;
import com.maibu.core.business.IoTCommonProduct;
import com.maibu.core.business.uav.IotUAVDevice;
import lombok.Data;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import com.maibu.core.business.Device;
import com.maibu.core.business.IoTCommonDevice;
import com.maibu.core.business.IoTCommonProduct;
import com.maibu.influxdb.util.InfluxDBUtil;
import com.maibu.mqtt.MqttClientUtil;
import lombok.Data;
@Component
@Data
public class GlobalMemory {
// orgId - siteId - deviceId
// public static ConcurrentHashMap<Long, IoTCommonDevice> unRegisterCommonDeviceMap = new ConcurrentHashMap<>();
//
// public static ConcurrentHashMap<Long, Device> unRegisterDeviceMap = new ConcurrentHashMap<>();
// orgId - siteId - deviceId
public static ConcurrentHashMap<String, IoTCommonDevice> unRegisterCommonDeviceMap = new ConcurrentHashMap<>();
@@ -35,6 +31,9 @@ public class GlobalMemory {
public static ConcurrentHashMap<Long, IoTCommonProduct> productMap = new ConcurrentHashMap<>();
public static InfluxDBUtil influxDBUtil;
public static MqttClientUtil mqttClientUtil;
public static SiteMemory getSiteMemory(Long orgId, Long siteId) {
if (orgId != null && siteId != null) {
@@ -56,11 +55,12 @@ public class GlobalMemory {
/**
* 新增场站内存
*
* @param orgId
* @param siteId
* @param siteMemory
*/
//todo 初始化
// todo 初始化
public static void addSiteMemory(Long orgId, Long siteId, SiteMemory siteMemory) {
if (orgId != null && siteId != null) {
@@ -75,6 +75,7 @@ public class GlobalMemory {
/**
* 删除
*
* @param orgId
* @param siteId
*/

View File

@@ -1,22 +1,23 @@
package com.maibu.memory;
import cn.hutool.core.util.ObjectUtil;
import com.maibu.core.business.*;
import com.maibu.core.business.device.Connector;
import com.maibu.core.business.device.NettyDevice;
import com.maibu.core.enums.ConnectorStatus;
import com.maibu.core.enums.ConnectorType;
import io.netty.channel.Channel;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import com.maibu.core.business.Device;
import com.maibu.core.business.DevicePlan;
import com.maibu.core.business.DevicePlanTask;
import com.maibu.core.business.ErrorIdentificationStandard;
import com.maibu.core.business.IoTCommonDevice;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
@Component
@Data

View File

@@ -0,0 +1,135 @@
package com.maibu.mqtt;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
@Slf4j
public class MqttClientUtil {
private final MqttClient client;
public MqttClientUtil(
String broker,
String clientId,
String username,
String password) throws MqttException {
this.client = new MqttClient(
broker,
clientId,
new MemoryPersistence());
MqttConnectOptions options = new MqttConnectOptions();
options.setCleanSession(false);
options.setAutomaticReconnect(true);
options.setConnectionTimeout(10);
options.setKeepAliveInterval(60);
if (username != null) {
options.setUserName(username);
}
if (password != null) {
options.setPassword(password.toCharArray());
}
client.connect(options);
log.info("MQTT连接成功: {}", broker);
}
/**
* 订阅
*/
public void subscribe(String topic) throws MqttException {
client.subscribe(topic);
log.info("订阅成功: {}", topic);
}
/**
* 订阅并监听
*/
public void subscribe(
String topic,
IMqttMessageListener listener)
throws MqttException {
client.subscribe(topic, listener);
log.info("订阅成功: {}", topic);
}
/**
* 发布消息
*/
public void publish(
String topic,
String payload)
throws MqttException {
publish(topic, payload, 1);
}
/**
* 发布消息
*/
public void publish(
String topic,
String payload,
int qos)
throws MqttException {
MqttMessage message =
new MqttMessage(payload.getBytes());
message.setQos(qos);
client.publish(topic, message);
}
/**
* 设置全局监听器
*/
public void setCallback() {
client.setCallback(new MqttCallback() {
@Override
public void connectionLost(Throwable cause) {
log.error("MQTT断开连接", cause);
}
@Override
public void messageArrived(
String topic,
MqttMessage message) {
log.info(
"收到消息 topic={} payload={}",
topic,
new String(message.getPayload()));
}
@Override
public void deliveryComplete(
IMqttDeliveryToken token) {
log.debug("消息发送成功");
}
});
}
public boolean isConnected() {
return client.isConnected();
}
public void disconnect() throws MqttException {
client.disconnect();
client.close();
}
}

View File

@@ -0,0 +1,10 @@
package com.maibu.mqtt;
public class MqttTopic {
public static final String DEVICE_STATUS_TOPIC = "device/%s/realTimeMessage"; // 实时消息
public static final String DEVICE_TASK_STATUS_TOPIC = "task/%s/status"; // 任务状态 完成、取消、暂停、继续
public static final String DEVICE_TASK_ARRIVE_TOPIC = "task/%s/arrive"; // 任务到达
}