Compare commits
38 Commits
develop
...
feature/rt
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a4def045a | |||
| 0698640b26 | |||
| 1ddfb78c41 | |||
| 2d15995384 | |||
|
|
e852b7c640 | ||
|
|
15b4cbbe3e | ||
|
|
032ab5c46b | ||
| 1875b5e848 | |||
| 897507b97b | |||
|
|
40cee949a9 | ||
|
|
f68d548a2c | ||
|
|
2fd12b9361 | ||
|
|
e286c355ff | ||
|
|
cc5f3d0772 | ||
|
|
1e9e4b8680 | ||
|
|
6d0aa977fa | ||
|
|
1045e33d1a | ||
|
|
6d170eaed0 | ||
|
|
bedb2e3ef5 | ||
|
|
0acd7d31b8 | ||
|
|
063716d539 | ||
|
|
1996ef79d4 | ||
|
|
87d6f6073d | ||
|
|
cef333b56b | ||
|
|
21a6d2a8df | ||
|
|
db9d27b3a0 | ||
|
|
d250bfc671 | ||
| 7555c4c72b | |||
| d8456d165b | |||
|
|
983efb2d6b | ||
| 1191c17c35 | |||
|
|
f9b85cd2d5 | ||
|
|
a6f711f18d | ||
|
|
e6ad5a0a3d | ||
|
|
ae28807818 | ||
|
|
29af023a4f | ||
|
|
ee5bae1174 | ||
|
|
893fe1ea78 |
@ -17,7 +17,7 @@ public interface RtcmClient {
|
||||
@GetMapping(value = "/get_device_info")
|
||||
public HttpResp getDeviceInfo(@RequestParam(name = "deviceId") String deviceId, @RequestParam(name = "cmd") String cmd);
|
||||
@PostMapping("/device_param_changed")
|
||||
HttpResp deviceParamChanged(@RequestParam(name = "deviceId") String deviceId,@RequestParam(name = "oldParentId") String oldParentId,@RequestParam(name = "oldParentId1") String oldParentId1);
|
||||
HttpResp deviceParamChanged(@RequestParam(name = "deviceId") String deviceId,@RequestParam(name = "oldParentId") String oldParentId);
|
||||
|
||||
@PostMapping("/group_param_changed")
|
||||
HttpResp groupParamChanged();
|
||||
|
||||
@ -81,8 +81,6 @@ public class GnssDevice {
|
||||
// 参数改变
|
||||
private Integer change_flag = 0;
|
||||
|
||||
private String parentid1;
|
||||
|
||||
private String getBatteryConfigCmd(){
|
||||
return "d3130006"+ HexUtil.Int2HexString(Integer.parseInt(deviceid))+
|
||||
"55"+(has_battery?"01":"00");
|
||||
|
||||
@ -19,7 +19,8 @@ public class OpLogManager {
|
||||
public final static Short OP_OBJ_GROUP = 6;
|
||||
public final static Short OP_OBJ_WARNING = 7;
|
||||
public final static Short OP_OBJ_SYS = 8;
|
||||
public final static Short OP_OBJ_DEVICE_MSG = 9;
|
||||
public final static Short OP_OBJ_FWD_RECORD = 9;
|
||||
|
||||
|
||||
@Autowired
|
||||
OpLogMapper opLogMapper;
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
package com.imdroid.beidou_fwd.service;
|
||||
|
||||
import com.imdroid.common.util.ThreadManager;
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
@ -13,7 +12,6 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class TCPClient {
|
||||
private final Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||
@ -29,7 +27,7 @@ public class TCPClient {
|
||||
TCPListener listener;
|
||||
|
||||
public void start() {
|
||||
new Thread(this::connect, "forwarder tcp-client").start();
|
||||
//new Thread(this::connect, host+":"+port+" forwarder tcp-client").start();
|
||||
}
|
||||
|
||||
public void init(String dest_addr, int dest_port, TCPListener listener) {
|
||||
@ -54,7 +52,7 @@ public class TCPClient {
|
||||
}
|
||||
|
||||
public void connect() {
|
||||
logger.info("netty client starting");
|
||||
logger.info("{}:{} tcp connecting...",host,port);
|
||||
//启动客户端去连接服务器端
|
||||
try {
|
||||
ChannelFuture cf = bootstrap.connect(host, port);
|
||||
@ -62,17 +60,22 @@ public class TCPClient {
|
||||
@Override
|
||||
public void operationComplete(ChannelFuture future) throws Exception {
|
||||
if (!future.isSuccess()) {
|
||||
logger.info("{}:{} tcp connect failed. {}",host,port,future.cause().toString());
|
||||
//重连交给后端线程执行
|
||||
future.channel().eventLoop().schedule(() -> {
|
||||
logger.info("tcp client reconnect");
|
||||
/*future.channel().eventLoop().schedule(() -> {
|
||||
logger.info("{}:{} tcp client reconnect",host,port);
|
||||
try {
|
||||
connect();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}, 5000, TimeUnit.MILLISECONDS);
|
||||
}, 5000, TimeUnit.MILLISECONDS);*/
|
||||
} else {
|
||||
logger.info("tcp client start success!");
|
||||
/*future.channel().config().setWriteBufferWaterMark(new WriteBufferWaterMark(
|
||||
1024 * 1024, // low
|
||||
4 *1024*1024 // high
|
||||
));*/
|
||||
logger.info("{}:{} tcp client start success!",host,port);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -80,18 +83,35 @@ public class TCPClient {
|
||||
this.channel = cf.channel();
|
||||
this.channel.closeFuture().sync();
|
||||
} catch (Exception e) {
|
||||
logger.error("netty client error:", e);
|
||||
logger.error(host+":"+port+" tcp connect error:", e);
|
||||
}
|
||||
}
|
||||
|
||||
boolean tryReconnect() throws Exception{
|
||||
new Thread(this::connect, host+":"+port+" forwarder tcp-client").start();
|
||||
for(int i=0; i<20; i++){
|
||||
Thread.sleep(50);
|
||||
if(channel!=null && channel.isActive()) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void writeAndFlush(String json) {
|
||||
ByteBuf sendBuffer = Unpooled.buffer();
|
||||
sendBuffer.writeBytes(json.getBytes(StandardCharsets.UTF_8));
|
||||
//logger.info("send to {}: {}",host,json);
|
||||
if(channel==null || !channel.isActive()){
|
||||
try {
|
||||
if(!tryReconnect()) return;
|
||||
} catch (Exception e) {
|
||||
logger.error(e.toString());
|
||||
}
|
||||
}
|
||||
channel.writeAndFlush(sendBuffer).addListener(future -> {
|
||||
if (future.isSuccess()) {
|
||||
logger.info("send to tcp:"+host+" succeed.");
|
||||
} else {
|
||||
logger.info("send to tcp:"+host+" failed.");
|
||||
logger.info("send to tcp: {} failed. {}",host,future.cause().toString());
|
||||
if(listener!=null){
|
||||
listener.onMessage("failed");
|
||||
}
|
||||
@ -103,8 +123,8 @@ public class TCPClient {
|
||||
connectTime = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public void onDisconnect(){
|
||||
if(connectTime.isBefore(LocalDateTime.now().minusMinutes(1))) {
|
||||
public void onDisconnect(boolean isIdle){
|
||||
/*if(connectTime.isBefore(LocalDateTime.now().minusMinutes(1))) {
|
||||
connect();
|
||||
}
|
||||
else{
|
||||
@ -114,8 +134,8 @@ public class TCPClient {
|
||||
} catch (Exception e) {
|
||||
logger.error(e.toString());
|
||||
}
|
||||
},60, TimeUnit.SECONDS);
|
||||
}
|
||||
},isIdle?30:10, TimeUnit.SECONDS);
|
||||
}*/
|
||||
}
|
||||
|
||||
public void onMessage(String msg){
|
||||
|
||||
@ -40,13 +40,14 @@ public class TcpMessageHandler extends SimpleChannelInboundHandler<ByteBuf> {
|
||||
@Override
|
||||
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
|
||||
logger.info("tcp channel inactive");
|
||||
tcpClient.onDisconnect();
|
||||
tcpClient.onDisconnect(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
|
||||
logger.error("TcpMessageHandler error: {}", cause.toString());
|
||||
ctx.close();
|
||||
tcpClient.onDisconnect(false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -37,17 +37,17 @@ public class Forwarder {
|
||||
static boolean isFwdTableInit = true;//false;
|
||||
|
||||
@Autowired
|
||||
private GnssDeviceMapper deviceMapper;
|
||||
GnssDeviceMapper deviceMapper;
|
||||
|
||||
@Autowired
|
||||
private GnssCalcDataMapper gnssDataMapper;
|
||||
GnssCalcDataMapper gnssDataMapper;
|
||||
|
||||
@Autowired
|
||||
private FwdRecordMapper fwdRecordsMapper;
|
||||
FwdRecordMapper fwdRecordsMapper;
|
||||
@Autowired
|
||||
private ResendRecordMapper resendRecordMapper;
|
||||
ResendRecordMapper resendRecordMapper;
|
||||
@Autowired
|
||||
private GnssStatusMapper gnssStatusMapper;
|
||||
GnssStatusMapper gnssStatusMapper;
|
||||
|
||||
@Autowired
|
||||
GnssGroupFwdMapper fwdMapper;
|
||||
|
||||
@ -49,7 +49,7 @@ public class GXJKForwarder extends Forwarder {
|
||||
/**
|
||||
* 每半小时转发GNSS解算结果
|
||||
*/
|
||||
@Scheduled(cron = "0 0/10 * * * ?") // 每30分钟执行一次
|
||||
@Scheduled(cron = "0 0/30 * * * ?") // 每30分钟执行一次
|
||||
private void forwardGnss() {
|
||||
logger.debug("gxjk forwardGnss");
|
||||
if(mqttClient.isConnected()) forwardCurrentGnss();
|
||||
|
||||
@ -0,0 +1,75 @@
|
||||
package com.imdroid.beidou_fwd.task;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.imdroid.beidou_fwd.service.TCPClient;
|
||||
import com.imdroid.secapi.dto.ResendRecord;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
@Configuration
|
||||
@EnableScheduling
|
||||
public class GXJSForwarder extends GXXfzForwarder{
|
||||
private final String FORWARDER_NAME = "广西新发展";
|
||||
@Value("${xfz.server.host}")
|
||||
private String host;
|
||||
|
||||
@Value("${xfz.server.port}")
|
||||
private int port;
|
||||
private boolean enabled=true;
|
||||
|
||||
@PostConstruct
|
||||
void registerMe(){
|
||||
init(FORWARDER_NAME, "TCP "+host+":"+port,1,FWD_DEVICE_ID,30);
|
||||
xfzTcpClient = new TCPClient();
|
||||
xfzTcpClient.init(host, port,listener);
|
||||
if(!enabled) return;
|
||||
|
||||
xfzTcpClient.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 每半小时转发GNSS解算结果
|
||||
*/
|
||||
@Scheduled(cron = "0 0/30 * * * ?") // 每30分钟执行一次
|
||||
private void forwardGnss() {
|
||||
if(!enabled) return;
|
||||
logger.debug("gxjs forwardGnss");
|
||||
forwardCurrentGnss();
|
||||
}
|
||||
|
||||
@Override
|
||||
void forwardHistoryGnss(){
|
||||
// 1.从转发记录表里检索待补传记录时间表,含设备Id,时间段
|
||||
QueryWrapper<ResendRecord> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("fwd_group_id",fwdGroupId);
|
||||
queryWrapper.eq("state",ResendRecord.STATE_BREAK_POINT);
|
||||
queryWrapper.ge("createtime", LocalDateTime.now().minusDays(30));
|
||||
List<ResendRecord> resendRecordsList = resendRecordMapper.selectList(queryWrapper);
|
||||
|
||||
if(resendRecordsList!=null && resendRecordsList.size()>0){
|
||||
//修改状态
|
||||
UpdateWrapper<ResendRecord> updateWrapper = new UpdateWrapper<>();
|
||||
updateWrapper.eq("fwd_group_id",fwdGroupId);
|
||||
updateWrapper.eq("state",ResendRecord.STATE_BREAK_POINT);
|
||||
updateWrapper.ge("createtime", LocalDateTime.now().minusDays(30));
|
||||
updateWrapper.set("state",ResendRecord.STATE_FWDING);
|
||||
int updateNum = resendRecordMapper.update(null, updateWrapper);
|
||||
logger.debug("{} forward history records: {}, update {}",fwdGroupId, resendRecordsList.size(),updateNum);
|
||||
// 2.检索这个这个时间段的解算结果,如果有数据则单个终端转发,标志记录为已补传
|
||||
for(ResendRecord record:resendRecordsList){
|
||||
if(record.getProjectid()!=null)
|
||||
logger.debug("{} forward history {}",fwdGroupId, record.getProjectid());
|
||||
forwardBatchGnssRecords(record);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,7 @@ import com.imdroid.beidou_fwd.service.TCPClient;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
@ -11,20 +12,31 @@ import javax.annotation.PostConstruct;
|
||||
@Component
|
||||
@Configuration
|
||||
@EnableScheduling
|
||||
public class GXXfz2Forwarder extends GXXfzForwarder{
|
||||
private String FORWARDER_NAME = "广西路建";
|
||||
public class GXLJForwarder extends GXXfzForwarder{
|
||||
private final String FORWARDER_NAME = "广西路建";
|
||||
@Value("${gxlj.server.host}")
|
||||
private String host;
|
||||
|
||||
@Value("${gxlj.server.port}")
|
||||
private int port;
|
||||
private boolean enabled=true;
|
||||
|
||||
@PostConstruct
|
||||
@Override
|
||||
void registerMe(){
|
||||
init(FORWARDER_NAME, "TCP "+host+":"+port,6,FWD_DEVICE_ID,30);
|
||||
xfzTcpClient = new TCPClient();
|
||||
xfzTcpClient.init(host, port,listener);
|
||||
if(!enabled) return;
|
||||
xfzTcpClient.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 每半小时转发GNSS解算结果
|
||||
*/
|
||||
@Scheduled(cron = "0 0/30 * * * ?") // 每30分钟执行一次
|
||||
private void forwardGnss() {
|
||||
if(!enabled) return;
|
||||
logger.debug("gxlj forwardGnss");
|
||||
forwardCurrentGnss();
|
||||
}
|
||||
}
|
||||
@ -6,31 +6,15 @@ import com.imdroid.beidou_fwd.service.TCPListener;
|
||||
import com.imdroid.common.util.GsonUtil;
|
||||
import com.imdroid.common.util.NumberUtils;
|
||||
import com.imdroid.secapi.dto.GnssCalcData;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
@Configuration
|
||||
@EnableScheduling
|
||||
public class GXXfzForwarder extends Forwarder{
|
||||
private final String FORWARDER_NAME = "广西新发展";
|
||||
@Value("${xfz.server.host}")
|
||||
private String host;
|
||||
|
||||
@Value("${xfz.server.port}")
|
||||
private int port;
|
||||
|
||||
TCPClient xfzTcpClient;
|
||||
|
||||
static class XFZTCPListener implements TCPListener{
|
||||
class XFZTCPListener implements TCPListener{
|
||||
public static final int STATE_NO_ACK = 0;
|
||||
public static final int STATE_OK = 1;
|
||||
public static final int STATE_FAILED = 2;
|
||||
@ -51,36 +35,16 @@ public class GXXfzForwarder extends Forwarder{
|
||||
|
||||
@Override
|
||||
public void onMessage(String msg) {
|
||||
logger.info("{} client rx: {}",fwdGroupId, msg);
|
||||
if(msg.contains("succeed")) state = STATE_OK;
|
||||
else state = STATE_FAILED;
|
||||
}
|
||||
}
|
||||
XFZTCPListener listener = new XFZTCPListener();
|
||||
@PostConstruct
|
||||
void registerMe(){
|
||||
init(FORWARDER_NAME, "TCP "+host+":"+port,1,FWD_DEVICE_ID,30);
|
||||
xfzTcpClient = new TCPClient();
|
||||
xfzTcpClient.init(host, port,listener);
|
||||
xfzTcpClient.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 每半小时转发GNSS解算结果
|
||||
*/
|
||||
@Scheduled(cron = "0 0/30 * * * ?") // 每30分钟执行一次
|
||||
private void forwardGnss() {
|
||||
logger.debug("xfz forwardGnss");
|
||||
forwardCurrentGnss();
|
||||
}
|
||||
/*
|
||||
@Scheduled(cron = "0 0/10 * * * ?") // 每30分钟执行一次
|
||||
private void checkDevice() {
|
||||
//logger.debug("zny checkDevice");
|
||||
checkOfflineDevice("2345053","2350106","2350124");
|
||||
}*/
|
||||
|
||||
@Override
|
||||
int send(String projectId, List<GnssCalcData> records, LocalDateTime sentTime){
|
||||
int batchNum = 0;
|
||||
int sendNum = 0;
|
||||
if(records.size() == 0) return 0;
|
||||
|
||||
@ -104,27 +68,48 @@ public class GXXfzForwarder extends Forwarder{
|
||||
// 经纬度
|
||||
data.setDevLng(locationRecord.getR9250e());
|
||||
data.setDevLat(locationRecord.getR9250n());
|
||||
sendNum++;
|
||||
}
|
||||
// 发送
|
||||
batchNum++;
|
||||
|
||||
if(batchNum==20){
|
||||
String json = "#" + GsonUtil.toJson(xfzTcpMessage) + "!";
|
||||
logger.debug("project " + projectId + ": push calculation result to XFZ");
|
||||
//logger.debug("project {}: forwad {} gnss records to {}",projectId, dataList.size(),fwdGroupId);
|
||||
//logger.debug(json);
|
||||
try {
|
||||
listener.clear();
|
||||
xfzTcpClient.writeAndFlush(json);
|
||||
//等待应答
|
||||
if(checkResult()) sendNum += batchNum;
|
||||
} catch (Exception e1) {
|
||||
logger.error(e1.toString());
|
||||
}
|
||||
batchNum = 0;
|
||||
dataList.clear();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if(batchNum>0){
|
||||
String json = "#" + GsonUtil.toJson(xfzTcpMessage) + "!";
|
||||
logger.debug("project {}: forwad {} gnss records to {}",projectId, dataList.size(),fwdGroupId);
|
||||
logger.debug(json);
|
||||
try {
|
||||
listener.clear();
|
||||
xfzTcpClient.writeAndFlush(json);
|
||||
//等待应答
|
||||
if(!checkResult()) sendNum = 0;
|
||||
if(checkResult()) sendNum += batchNum;
|
||||
} catch (Exception e1) {
|
||||
sendNum = 0;
|
||||
e1.printStackTrace();
|
||||
logger.error(e1.toString());
|
||||
}
|
||||
dataList.clear();
|
||||
}
|
||||
return sendNum;
|
||||
}
|
||||
|
||||
boolean checkResult() throws InterruptedException {
|
||||
// 等待应答,最多等1s
|
||||
// 等待应答,最多等500ms
|
||||
for(int i=0; i<10; i++){
|
||||
Thread.sleep(100);
|
||||
Thread.sleep(50);
|
||||
if(listener.state == XFZTCPListener.STATE_OK) return true;
|
||||
else if(listener.state == XFZTCPListener.STATE_FAILED) return false;
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import com.imdroid.beidou_fwd.service.TCPClient;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
@ -12,20 +13,30 @@ import javax.annotation.PostConstruct;
|
||||
@Configuration
|
||||
@EnableScheduling
|
||||
public class GZBForwarder extends GXXfzForwarder{
|
||||
private String FORWARDER_NAME = "葛洲坝";
|
||||
private final String FORWARDER_NAME = "葛洲坝";
|
||||
@Value("${gzb.server.host}")
|
||||
private String host;
|
||||
|
||||
@Value("${gzb.server.port}")
|
||||
private int port;
|
||||
private boolean enabled=true;
|
||||
|
||||
@PostConstruct
|
||||
@Override
|
||||
void registerMe(){
|
||||
init(FORWARDER_NAME, "TCP "+host+":"+port,1,FWD_DEVICE_ID,30);
|
||||
xfzTcpClient = new TCPClient();
|
||||
xfzTcpClient.init(host, port,listener);
|
||||
if(!enabled) return;
|
||||
xfzTcpClient.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 每半小时转发GNSS解算结果
|
||||
*/
|
||||
@Scheduled(cron = "0 0/30 * * * ?") // 每30分钟执行一次
|
||||
private void forwardGnss() {
|
||||
if(!enabled) return;
|
||||
logger.debug("gzb forwardGnss");
|
||||
forwardCurrentGnss();
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,17 +28,18 @@ import java.util.List;
|
||||
@EnableScheduling
|
||||
public class GZYMQTTForwarder extends Forwarder {
|
||||
static final String FORWARDER_NAME = "贵州交勘院MQTT";
|
||||
@Value("${mqtt.server.brokerUrl}")
|
||||
@Value("${gzymqtt.server.brokerUrl}")
|
||||
private String brokerUrl;
|
||||
@Value("${mqtt.server.username}")
|
||||
@Value("${gzymqtt.server.username}")
|
||||
private String username;
|
||||
@Value("${mqtt.server.password}")
|
||||
@Value("${gzymqtt.server.password}")
|
||||
private String password;
|
||||
@Value("${mqtt.server.clientid}")
|
||||
@Value("${gzymqtt.server.clientid}")
|
||||
private String clientid;
|
||||
@Value("${mqtt.server.topic}")
|
||||
@Value("${gzymqtt.server.topic}")
|
||||
private String topic;
|
||||
|
||||
//@Value("${gzymqtt.server.enabled}")
|
||||
private boolean enabled=true;
|
||||
@Autowired
|
||||
GnssStatusMsgMapper statusMsgMapper;
|
||||
MQTTClient mqttClient;
|
||||
@ -47,6 +48,7 @@ public class GZYMQTTForwarder extends Forwarder {
|
||||
void registerMe() throws MqttException {
|
||||
init(FORWARDER_NAME, "MQTT "+brokerUrl,2,FWD_DEVICE_ALIAS_NAME,30);
|
||||
mqttClient = new MQTTClient(brokerUrl, username, password,clientid);
|
||||
if(!enabled) return;
|
||||
try{
|
||||
mqttClient.connect();
|
||||
}
|
||||
@ -61,6 +63,7 @@ public class GZYMQTTForwarder extends Forwarder {
|
||||
@Scheduled(cron = "0 0 0/1 * * ?") // 每小时执行一次
|
||||
//@Scheduled(cron = "0 0/5 * * * ?") // 每30分钟执行一次
|
||||
private void forwardGnss() {
|
||||
if(!enabled) return;
|
||||
logger.debug("gzy mqtt forwardGnss");
|
||||
if(mqttClient.isConnected()) {
|
||||
forwardCurrentGnss();
|
||||
@ -75,6 +78,11 @@ public class GZYMQTTForwarder extends Forwarder {
|
||||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
@Scheduled(cron = "0 0/5 * * * ?") // 每30分钟执行一次
|
||||
void forwardHistoryGnss() {
|
||||
super.forwardHistoryGnss();
|
||||
}*/
|
||||
|
||||
@Override
|
||||
int send(String projectId, List<GnssCalcData> records, LocalDateTime sentTime) {
|
||||
|
||||
@ -44,11 +44,14 @@ public class GNSSCalcFilterService {
|
||||
Integer xyfilterCycle = groupCalc.getFilter_hour();
|
||||
Integer zfilterCycle = groupCalc.getZfilter_hour();
|
||||
if(null == zfilterCycle) zfilterCycle = xyfilterCycle;
|
||||
if(groupCalc.getAuto_filter()) {
|
||||
VaryFilterCycle varyCycle = autoCycleDevices.get(deviceId);
|
||||
if (varyCycle != null) {
|
||||
xyfilterCycle = varyCycle.filterCycleHour;
|
||||
zfilterCycle = varyCycle.filterCycleHour;
|
||||
}
|
||||
}
|
||||
else autoCycleDevices.remove(deviceId);
|
||||
|
||||
// 平滑处理
|
||||
calcFilterLocation(newRecord, referPos, xyfilterCycle, zfilterCycle, groupCalc.getFilter_min_hour(),
|
||||
@ -166,7 +169,7 @@ public class GNSSCalcFilterService {
|
||||
newRecord.setAuxn(NumberUtils.scaleTwo(msumN / minCount));
|
||||
newRecord.setAuxd(NumberUtils.scaleTwo(msumD / minCount));
|
||||
//滤波窗口里的时间跨度超过滤波时间的2/3才认为稳定
|
||||
newRecord.setStabled(lastRecordTime.isBefore(newRecordTime.minusHours(filterCycleHour * 2 / 3)));
|
||||
newRecord.setStabled(lastRecordTime.isBefore(newRecordTime.minusHours(filterCycleHour * 1 / 2)));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -246,7 +249,7 @@ public class GNSSCalcFilterService {
|
||||
newRecord.setAuxn(NumberUtils.scaleTwo(msumN / minCount));
|
||||
newRecord.setAuxd(NumberUtils.scaleTwo(msumD / minCount));
|
||||
//滤波窗口里的时间跨度超过滤波时间的2/3才认为稳定
|
||||
newRecord.setStabled(lastRecordTime.isBefore(newRecordTime.minusHours(filterCycleHour * 2 / 3)));
|
||||
newRecord.setStabled(lastRecordTime.isBefore(newRecordTime.minusHours(filterCycleHour * 1 / 2)));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,25 +9,27 @@ import com.imdroid.sideslope.bd.Gga;
|
||||
import com.imdroid.sideslope.bd.Rtcm1005;
|
||||
import com.imdroid.sideslope.message.D331RtcmMessage;
|
||||
import com.imdroid.sideslope.ntrip.UdpNtripServer;
|
||||
import com.imdroid.sideslope.service.Device;
|
||||
import com.imdroid.sideslope.service.DeviceService;
|
||||
import com.imdroid.sideslope.ntrip.RtcmUdpForwarder;
|
||||
import com.imdroid.sideslope.server.DeviceChannel;
|
||||
import com.imdroid.sideslope.server.OnlineChannels;
|
||||
import com.imdroid.sideslope.service.DataPersistService;
|
||||
import com.imdroid.sideslope.bd.RtcmGgaUtil;
|
||||
import com.imdroid.sideslope.server.tcp.RtcmSpecificDeviceTcpServer;
|
||||
import com.imdroid.sideslope.service.Device;
|
||||
import com.imdroid.sideslope.service.DeviceService;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
|
||||
/**
|
||||
* @author Layton
|
||||
* @date 2023/2/2 20:49
|
||||
@ -37,15 +39,6 @@ public class D331RtcmMessageExecutor implements Executor<D331RtcmMessage, Void>
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||
|
||||
// 基站状态管理
|
||||
private static final Map<String, BaseStationStatus> baseStationStatusMap = new ConcurrentHashMap<>();
|
||||
|
||||
// 测站当前使用的基站映射
|
||||
private static final Map<String, String> deviceCurrentBaseMap = new ConcurrentHashMap<>();
|
||||
|
||||
// 基站切换监听器列表
|
||||
private static final List<BaseStationSwitchListener> switchListeners = new ArrayList<>();
|
||||
|
||||
@Autowired
|
||||
private DeviceService deviceService;
|
||||
@Autowired
|
||||
@ -54,136 +47,30 @@ public class D331RtcmMessageExecutor implements Executor<D331RtcmMessage, Void>
|
||||
private DataPersistService dataPersistService;
|
||||
@Autowired
|
||||
UdpNtripServer ntripServer;
|
||||
@Autowired
|
||||
RtcmUdpForwarder rtcmUdpForwarder;
|
||||
@Autowired
|
||||
RtcmSpecificDeviceTcpServer rtcmSpecificDeviceTcpServer;
|
||||
|
||||
// 添加一个成员变量用于追踪每个测站最后一次转发D300数据的时间
|
||||
private final Map<String, Long> lastD300ForwardTimeMap = new ConcurrentHashMap<>();
|
||||
private static final long D300_FORWARD_INTERVAL = 5000; // 5秒,单位毫秒
|
||||
|
||||
private static final Map<String, Long> deviceLastTime = new ConcurrentHashMap<>();
|
||||
private static final long LOG_INTERVAL = 10 * 60 * 1000L;
|
||||
|
||||
// 基站在线检查间隔 - 修改为秒
|
||||
private static final long BASE_STATION_CHECK_INTERVAL = 30; // 30秒
|
||||
private static final long BASE_STATION_OFFLINE_TIMEOUT = 5 * 60; // 5分钟,单位:秒
|
||||
|
||||
/**
|
||||
* 基站状态枚举
|
||||
*/
|
||||
public enum BaseStationStatusEnum {
|
||||
ONLINE,
|
||||
OFFLINE,
|
||||
SWITCHING
|
||||
}
|
||||
|
||||
/**
|
||||
* 基站状态信息
|
||||
*/
|
||||
public static class BaseStationStatus {
|
||||
private String baseStationId;
|
||||
private BaseStationStatusEnum status;
|
||||
private LocalDateTime lastActiveTime;
|
||||
private LocalDateTime statusChangeTime;
|
||||
private Set<String> servingDevices; // 当前服务的测站列表
|
||||
|
||||
public BaseStationStatus(String baseStationId) {
|
||||
this.baseStationId = baseStationId;
|
||||
this.status = BaseStationStatusEnum.OFFLINE;
|
||||
this.lastActiveTime = LocalDateTime.now();
|
||||
this.statusChangeTime = LocalDateTime.now();
|
||||
this.servingDevices = ConcurrentHashMap.newKeySet();
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public String getBaseStationId() { return baseStationId; }
|
||||
public BaseStationStatusEnum getStatus() { return status; }
|
||||
public void setStatus(BaseStationStatusEnum status) {
|
||||
if (this.status != status) {
|
||||
this.status = status;
|
||||
this.statusChangeTime = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
public LocalDateTime getLastActiveTime() { return lastActiveTime; }
|
||||
public void setLastActiveTime(LocalDateTime lastActiveTime) { this.lastActiveTime = lastActiveTime; }
|
||||
public LocalDateTime getStatusChangeTime() { return statusChangeTime; }
|
||||
public Set<String> getServingDevices() { return servingDevices; }
|
||||
}
|
||||
|
||||
/**
|
||||
* 基站切换监听器接口
|
||||
*/
|
||||
public interface BaseStationSwitchListener {
|
||||
void onBaseStationSwitch(String deviceId, String fromBaseId, String toBaseId, String reason);
|
||||
void onBaseStationStatusChange(String baseStationId, BaseStationStatusEnum oldStatus, BaseStationStatusEnum newStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认的基站切换监听器实现
|
||||
*/
|
||||
private class DefaultBaseStationSwitchListener implements BaseStationSwitchListener {
|
||||
@Override
|
||||
public void onBaseStationSwitch(String deviceId, String fromBaseId, String toBaseId, String reason) {
|
||||
logger.info("Base station switch - Device: {}, From: {} To: {}, Reason: {}",
|
||||
deviceId, fromBaseId, toBaseId, reason);
|
||||
|
||||
ThreadManager.getFixedThreadPool().submit(() -> {
|
||||
try {
|
||||
notifyBaseStationSwitch(deviceId, fromBaseId, toBaseId, reason);
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to send base station switch notification", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBaseStationStatusChange(String baseStationId, BaseStationStatusEnum oldStatus, BaseStationStatusEnum newStatus) {
|
||||
logger.info("Base station status change - BaseStation: {}, From: {} To: {}",
|
||||
baseStationId, oldStatus, newStatus);
|
||||
|
||||
ThreadManager.getFixedThreadPool().submit(() -> {
|
||||
try {
|
||||
notifyBaseStationStatusChange(baseStationId, oldStatus, newStatus);
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to send base station status notification", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化默认监听器
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
switchListeners.add(new DefaultBaseStationSwitchListener());
|
||||
logger.info("D331RtcmMessageExecutor initialized");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void execute(D331RtcmMessage message) {
|
||||
String id = message.getId();
|
||||
|
||||
// 更新基站状态
|
||||
updateBaseStationStatus(id);
|
||||
|
||||
// 补齐tenantId
|
||||
Device deviceBs = deviceService.findByDeviceId(id);
|
||||
if(deviceBs == null || deviceBs.getOpMode() == GnssDevice.OP_MODE_UNUSE || deviceBs.getOpMode() == GnssDevice.OP_MODE_CHECK) {
|
||||
return null;
|
||||
}
|
||||
if(deviceBs == null || deviceBs.getOpMode() == GnssDevice.OP_MODE_UNUSE) return null;
|
||||
|
||||
// 推送基站数据
|
||||
if(deviceBs.getOpMode() == GnssDevice.OP_MODE_USE) {
|
||||
byte[] forwardBytes = message.getSrcData();
|
||||
|
||||
// 获取使用该基站(包括作为主基站和备选基站)的所有测站
|
||||
List<Device> primaryDevices = deviceService.findByParentId(id);
|
||||
List<Device> backupDevices = deviceService.findByParentId1(id);
|
||||
|
||||
// 合并两个列表
|
||||
List<Device> allDevices = new ArrayList<>();
|
||||
allDevices.addAll(primaryDevices);
|
||||
allDevices.addAll(backupDevices);
|
||||
|
||||
// 要求快速转发,因此用缓存,不要每次都查数据库
|
||||
List<Device> deviceList = deviceService.findByParentId(id);
|
||||
//logger.debug("base station {} has {} rovers: ", message.getId(),deviceList.size());
|
||||
DeviceChannel deviceChannel = null;
|
||||
for (Device device : allDevices) {
|
||||
for (Device device : deviceList) {
|
||||
if (device.getOpMode() != GnssDevice.OP_MODE_USE) continue;
|
||||
if ((device.getModel()==GnssDevice.MODEL_G510) &&
|
||||
(device.getFixedNum()>100) && (device.getGnssSampleRate()>1)
|
||||
@ -192,24 +79,6 @@ public class D331RtcmMessageExecutor implements Executor<D331RtcmMessage, Void>
|
||||
continue;
|
||||
}
|
||||
String deviceId = device.getDeviceId();
|
||||
|
||||
// 智能基站选择和切换
|
||||
String selectedBaseId = selectOptimalBaseStation(device);
|
||||
|
||||
if (selectedBaseId == null || !selectedBaseId.equals(id)) {
|
||||
continue; // 当前基站不是该设备的最优选择
|
||||
}
|
||||
|
||||
// 验证兼容性(双重检查)
|
||||
if (!isBaseStationCompatibleWithDevice(selectedBaseId, device)) {
|
||||
logger.warn("Base station {} selected but not compatible with device {}, skipping",
|
||||
selectedBaseId, deviceId);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取要转发的数据(可能需要修改基站ID)
|
||||
byte[] deviceForwardBytes = prepareForwardData(forwardBytes, device, selectedBaseId);
|
||||
|
||||
// 获取设备通道并发送数据
|
||||
if(device.getDataChannelType() == Device.CHANNEL_TYPE_UDP) {
|
||||
deviceChannel = OnlineChannels.INSTANCE.getDataChannel(deviceId);
|
||||
@ -217,389 +86,149 @@ public class D331RtcmMessageExecutor implements Executor<D331RtcmMessage, Void>
|
||||
deviceChannel = OnlineChannels.INSTANCE.getConfigChannel(deviceId);
|
||||
}
|
||||
|
||||
// 根据基站和设备类型处理数据转发
|
||||
if (shouldForwardData(deviceBs, device)) {
|
||||
forwardDataToDevice(deviceChannel, deviceForwardBytes, device, deviceBs, id, deviceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理设备状态和统计信息
|
||||
processDeviceStatistics(deviceBs, message);
|
||||
|
||||
// 添加NTRIP处理
|
||||
byte[] srcdata = message.getSrcData();
|
||||
String rtcm = ByteUtil.bytesToHexString(srcdata);
|
||||
sendToNtrip(id, rtcm);
|
||||
|
||||
// 日志记录
|
||||
logMessageIfNeeded(deviceBs, message);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新基站状态
|
||||
*/
|
||||
private void updateBaseStationStatus(String baseStationId) {
|
||||
BaseStationStatus status = baseStationStatusMap.computeIfAbsent(baseStationId,
|
||||
k -> new BaseStationStatus(baseStationId));
|
||||
|
||||
BaseStationStatusEnum oldStatus = status.getStatus();
|
||||
status.setLastActiveTime(LocalDateTime.now());
|
||||
|
||||
if (oldStatus == BaseStationStatusEnum.OFFLINE) {
|
||||
status.setStatus(BaseStationStatusEnum.ONLINE);
|
||||
logger.info("Base station {} changed status from {} to {}", baseStationId, oldStatus, BaseStationStatusEnum.ONLINE);
|
||||
notifyStatusChange(baseStationId, oldStatus, BaseStationStatusEnum.ONLINE);
|
||||
|
||||
// 检查是否有设备需要切换回主基站
|
||||
checkAndSwitchBackToPrimary(baseStationId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并切换回主基站
|
||||
*/
|
||||
private void checkAndSwitchBackToPrimary(String primaryBaseId) {
|
||||
ThreadManager.getFixedThreadPool().submit(() -> {
|
||||
try {
|
||||
List<Device> devicesUsingPrimary = deviceService.findByParentId(primaryBaseId);
|
||||
|
||||
for (Device device : devicesUsingPrimary) {
|
||||
String currentBase = deviceCurrentBaseMap.get(device.getDeviceId());
|
||||
|
||||
if (currentBase != null && !currentBase.equals(primaryBaseId)) {
|
||||
// 设备当前使用备用基站,需要切换回主基站
|
||||
logger.info("Switching device {} back to primary base station {} from {}",
|
||||
device.getDeviceId(), primaryBaseId, currentBase);
|
||||
switchDeviceToBaseStation(device.getDeviceId(), primaryBaseId,
|
||||
"Primary base station back online");
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Error checking devices for primary base station switch back", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能选择最优基站
|
||||
*/
|
||||
private String selectOptimalBaseStation(Device device) {
|
||||
String primaryBaseId = device.getParentId();
|
||||
String backupBaseId = device.getParentId1();
|
||||
String deviceId = device.getDeviceId();
|
||||
|
||||
// 获取当前使用的基站
|
||||
String currentBaseId = deviceCurrentBaseMap.get(deviceId);
|
||||
|
||||
// 获取所有候选基站,按优先级排序
|
||||
List<String> candidateBases = getCandidateBaseStations(device);
|
||||
|
||||
for (String baseId : candidateBases) {
|
||||
// 检查基站是否在线
|
||||
boolean isOnline = isBaseStationOnline(baseId);
|
||||
|
||||
if (!isOnline) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查基站与设备的兼容性
|
||||
if (!isBaseStationCompatibleWithDevice(baseId, device)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 如果当前基站发生变化,记录切换
|
||||
if (currentBaseId != null && !currentBaseId.equals(baseId)) {
|
||||
switchDeviceToBaseStation(deviceId, baseId,
|
||||
String.format("Switch from %s to %s (priority/availability)", currentBaseId, baseId));
|
||||
} else if (currentBaseId == null) {
|
||||
// 首次连接
|
||||
deviceCurrentBaseMap.put(deviceId, baseId);
|
||||
BaseStationStatus status = baseStationStatusMap.get(baseId);
|
||||
if (status != null) {
|
||||
status.getServingDevices().add(deviceId);
|
||||
}
|
||||
}
|
||||
return baseId;
|
||||
}
|
||||
|
||||
logger.warn("No compatible and available base station found for device: {}", deviceId);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查基站与设备的兼容性
|
||||
*/
|
||||
private boolean isBaseStationCompatibleWithDevice(String baseStationId, Device device) {
|
||||
// 获取基站设备信息
|
||||
Device baseStation = deviceService.findByDeviceId(baseStationId);
|
||||
if (baseStation == null) {
|
||||
logger.warn("Base station {} not found", baseStationId);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查基站是否处于可用状态
|
||||
if (baseStation.getOpMode() != GnssDevice.OP_MODE_USE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 使用已有的兼容性判断逻辑
|
||||
return shouldForwardData(baseStation, device);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取候选基站列表(按优先级排序)
|
||||
*/
|
||||
private List<String> getCandidateBaseStations(Device device) {
|
||||
List<String> candidates = new ArrayList<>();
|
||||
|
||||
// 主基站优先级最高
|
||||
if (device.getParentId() != null) {
|
||||
candidates.add(device.getParentId());
|
||||
}
|
||||
|
||||
// 备用基站
|
||||
if (device.getParentId1() != null) {
|
||||
candidates.add(device.getParentId1());
|
||||
}
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换设备到指定基站
|
||||
*/
|
||||
private void switchDeviceToBaseStation(String deviceId, String newBaseId, String reason) {
|
||||
String oldBaseId = deviceCurrentBaseMap.get(deviceId);
|
||||
|
||||
logger.info("Executing base station switch for device {}: {} -> {}, Reason: {}",
|
||||
deviceId, oldBaseId, newBaseId, reason);
|
||||
|
||||
// 更新映射
|
||||
deviceCurrentBaseMap.put(deviceId, newBaseId);
|
||||
|
||||
// 更新基站服务列表
|
||||
if (oldBaseId != null) {
|
||||
BaseStationStatus oldStatus = baseStationStatusMap.get(oldBaseId);
|
||||
if (oldStatus != null) {
|
||||
oldStatus.getServingDevices().remove(deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
BaseStationStatus newStatus = baseStationStatusMap.get(newBaseId);
|
||||
if (newStatus != null) {
|
||||
newStatus.getServingDevices().add(deviceId);
|
||||
}
|
||||
|
||||
// 通知监听器
|
||||
for (BaseStationSwitchListener listener : switchListeners) {
|
||||
try {
|
||||
listener.onBaseStationSwitch(deviceId, oldBaseId, newBaseId, reason);
|
||||
} catch (Exception e) {
|
||||
logger.error("Error notifying base station switch listener", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 准备转发数据(可能需要修改基站ID)
|
||||
*/
|
||||
private byte[] prepareForwardData(byte[] originalData, Device device, String selectedBaseId) {
|
||||
String primaryBaseId = device.getParentId();
|
||||
|
||||
// 如果选择的基站不是主基站,需要修改RTCM数据中的基站ID
|
||||
if (primaryBaseId != null && !selectedBaseId.equals(primaryBaseId)) {
|
||||
try {
|
||||
byte[] modifyData = originalData.clone();
|
||||
// 修复:使用Long.parseLong而不是Integer.parseInt,因为基站ID可能很大
|
||||
long primaryBaseIdLong = Long.parseLong(primaryBaseId);
|
||||
String hexPrimaryBase = String.format("%06x", primaryBaseIdLong);
|
||||
|
||||
if (hexPrimaryBase.length() >= 6) {
|
||||
modifyData[5] = (byte) Integer.parseInt(hexPrimaryBase.substring(0, 2), 16);
|
||||
modifyData[6] = (byte) Integer.parseInt(hexPrimaryBase.substring(2, 4), 16);
|
||||
modifyData[7] = (byte) Integer.parseInt(hexPrimaryBase.substring(4, 6), 16);
|
||||
}
|
||||
|
||||
return modifyData;
|
||||
} catch (NumberFormatException e) {
|
||||
logger.error("Error parsing base station ID: {}", primaryBaseId, e);
|
||||
return originalData;
|
||||
}
|
||||
}
|
||||
|
||||
return originalData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否应该转发数据
|
||||
* 基站类型说明:
|
||||
* - model = 1: f9p基站,所有设备都可以接收
|
||||
* - model = 0: 博通基站,只有博通设备(model=0)和兼容设备(model=1)可以接收
|
||||
*/
|
||||
private boolean shouldForwardData(Device baseStation, Device device) {
|
||||
Short baseStationModel = baseStation.getModel();
|
||||
// 读取数据库中model字段,判断基站类型
|
||||
Short baseStationModel = deviceBs.getModel();
|
||||
// 如果model为null,使用默认值0
|
||||
if (baseStationModel == null) {
|
||||
baseStationModel = 0;
|
||||
}
|
||||
|
||||
Short deviceModel = device.getModel();
|
||||
if (deviceModel == null) {
|
||||
deviceModel = 0;
|
||||
logger.warn("Base station model is null for device: {}, using default value 0", id);
|
||||
}
|
||||
|
||||
if (baseStationModel == 1) {
|
||||
// f9p基站,所有设备都可以接收
|
||||
return true;
|
||||
// 基站类型为1,正常执行
|
||||
if(deviceChannel != null && deviceChannel.isOnline()) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("forward d331 rtcm from {} to device {}", id, deviceId);
|
||||
}
|
||||
ByteBuf buf = Unpooled.buffer();
|
||||
buf.writeBytes(forwardBytes);
|
||||
deviceChannel.writeAndFlush(buf);
|
||||
}
|
||||
} else if (baseStationModel == 0) {
|
||||
// 博通基站,只支持特定设备
|
||||
return (deviceModel == 0 || deviceModel == 1);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转发数据到设备
|
||||
*/
|
||||
private void forwardDataToDevice(DeviceChannel deviceChannel, byte[] forwardBytes,
|
||||
Device device, Device deviceBs, String baseStationId, String deviceId) {
|
||||
if (deviceChannel == null || !deviceChannel.isOnline()) {
|
||||
return;
|
||||
}
|
||||
//logger.info("Base station model is 0 for device: {}", deviceId);
|
||||
|
||||
Short deviceModel = device.getModel();
|
||||
// 如果model为null,使用默认值0
|
||||
if (deviceModel == null) {
|
||||
deviceModel = 0;
|
||||
//logger.warn("Device model is null for device: {}, using default value 0", deviceId);
|
||||
}
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("forward d331 rtcm from {} to device {}", baseStationId, deviceId);
|
||||
if(deviceModel == 0){
|
||||
// 测站类型为0,正常执行
|
||||
if(deviceId.startsWith("2307")){
|
||||
// 处理2307型号的测站
|
||||
forwardBytes[2] = (byte) (forwardBytes[2] & 0x07);//兼容不带序号的测站
|
||||
}
|
||||
// 对所有测站类型为0的设备执行转发
|
||||
if(deviceChannel != null && deviceChannel.isOnline()) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("forward d331 rtcm from {} to device {}", id, deviceId);
|
||||
}
|
||||
ByteBuf buf = Unpooled.buffer();
|
||||
buf.writeBytes(forwardBytes);
|
||||
deviceChannel.writeAndFlush(buf);
|
||||
}
|
||||
}
|
||||
else if(deviceModel == 1){
|
||||
//logger.info("Device model is 1 for device: {}", deviceId);
|
||||
|
||||
if(deviceChannel != null && deviceChannel.isOnline()) {
|
||||
//logger.info("Device channel is online for device: {}", deviceId);
|
||||
|
||||
ByteBuf buf = Unpooled.buffer();
|
||||
buf.writeBytes(forwardBytes);
|
||||
|
||||
// 特殊处理设备类型
|
||||
if (deviceModel == 0 && deviceId.startsWith("2307")) {
|
||||
// 处理2307型号的测站
|
||||
byte[] modifiedData = buf.array();
|
||||
modifiedData[2] = (byte) (modifiedData[2] & 0x07); // 兼容不带序号的测站
|
||||
} else if (deviceModel == 1) {
|
||||
// 处理类型1的设备,可能需要添加D300数据
|
||||
buf = handleDeviceType1(buf, device, deviceBs, deviceId);
|
||||
}
|
||||
|
||||
deviceChannel.writeAndFlush(buf);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理设备类型1的特殊逻辑
|
||||
*/
|
||||
private ByteBuf handleDeviceType1(ByteBuf buf, Device device, Device deviceBs, String deviceId) {
|
||||
// 检查是否满足10秒转发间隔,只有满足条件时才添加D300字符串
|
||||
long currentTime = System.currentTimeMillis();
|
||||
Long lastForwardTime = lastD300ForwardTimeMap.getOrDefault(deviceId, 0L);
|
||||
|
||||
if(currentTime - lastForwardTime >= D300_FORWARD_INTERVAL) {
|
||||
//logger.info("Adding D300 string for device: {}", deviceId);
|
||||
|
||||
// 获取当前buf中的数据
|
||||
byte[] originalData = buf.array();
|
||||
String originalHex = ByteUtil.bytesToHexString(originalData);
|
||||
|
||||
// 找到D300和D301的位置
|
||||
int d300Index = originalHex.indexOf("d300");
|
||||
int d301Index = originalHex.indexOf("d301");
|
||||
|
||||
// 确定插入位置:如果两个都存在,取位置靠前的;如果只存在一个,就用那个位置
|
||||
int insertIndex = -1;
|
||||
if (d300Index != -1 && d301Index != -1) {
|
||||
insertIndex = Math.min(d300Index, d301Index);
|
||||
// 两个都存在,取位置靠前的
|
||||
insertIndex = (d300Index < d301Index) ? d300Index : d301Index;
|
||||
//logger.info("Found both D300 and D301, D300 at {}, D301 at {}, will insert before position {}",
|
||||
/// d300Index, d301Index, insertIndex);
|
||||
} else if (d300Index != -1) {
|
||||
insertIndex = d300Index;
|
||||
//logger.info("Found D300 at position {}", d300Index);
|
||||
} else if (d301Index != -1) {
|
||||
insertIndex = d301Index;
|
||||
//logger.info("Found D301 at position {}", d301Index);
|
||||
}
|
||||
|
||||
|
||||
if (insertIndex != -1) {
|
||||
// 检查基站是否有有效的ECEF坐标
|
||||
Double ecefX = deviceBs.getEcefx();
|
||||
Double ecefY = deviceBs.getEcefy();
|
||||
Double ecefZ = deviceBs.getEcefz();
|
||||
|
||||
// 验证坐标有效性
|
||||
if (isValidEcefCoordinates(ecefX, ecefY, ecefZ)) {
|
||||
// 创建新的buf
|
||||
ByteBuf newBuf = Unpooled.buffer();
|
||||
// 写入D300/D301之前的数据
|
||||
newBuf.writeBytes(originalData, 0, insertIndex / 2);
|
||||
|
||||
// 使用f9p坐标生成1005消息,并插入
|
||||
double[] ecef =new double[3];
|
||||
ecef[0] = ecefX.doubleValue();
|
||||
ecef[1] = ecefY.doubleValue();
|
||||
ecef[2] = ecefZ.doubleValue();
|
||||
|
||||
ecef[0] = deviceBs.getEcefx();
|
||||
ecef[1] = deviceBs.getEcefy();
|
||||
ecef[2] = deviceBs.getEcefz();
|
||||
String rtcm1005 = Rtcm1005.generateRtcm1005Hex(ecef, 0);
|
||||
if (rtcm1005 != null && !rtcm1005.isEmpty()) {
|
||||
if (rtcm1005 != null) {
|
||||
// 写入RTCM 1005消息
|
||||
byte[] rtcm1005Bytes = ByteUtil.hexStringTobyte(rtcm1005);
|
||||
newBuf.writeBytes(rtcm1005Bytes);
|
||||
}
|
||||
|
||||
newBuf.writeBytes(originalData, insertIndex / 2, originalData.length - insertIndex / 2);
|
||||
buf = newBuf;
|
||||
// logger.info("Generated RTCM 1005 message for base station {}: {}", deviceBs.getDeviceId(), rtcm1005);
|
||||
} else {
|
||||
logger.warn("Base station {} has invalid ECEF coordinates, skipping RTCM 1005 generation for device {}",
|
||||
deviceBs.getDeviceId(), deviceId);
|
||||
}
|
||||
//logger.warn("Failed to generate RTCM 1005 message for base station: {}", deviceBs.getDeviceId());
|
||||
}
|
||||
|
||||
// 写入剩余的数据
|
||||
newBuf.writeBytes(originalData, insertIndex / 2, originalData.length - insertIndex / 2);
|
||||
|
||||
// 更新buf
|
||||
buf = newBuf;
|
||||
|
||||
// 添加日志,记录插入位置和完整数据
|
||||
// logger.info("Inserted RTCM 1005 message before position {}, complete data: {}",
|
||||
// insertIndex,
|
||||
// ByteUtil.bytesToHexString(buf.array()));
|
||||
}
|
||||
|
||||
// 更新最后转发时间
|
||||
lastD300ForwardTimeMap.put(deviceId, currentTime);
|
||||
|
||||
// 添加日志,记录测站转发的完整数据
|
||||
//logger.info("Forward data to device: {}, time: {}, complete data: {}",
|
||||
// deviceId,
|
||||
// LocalDateTime.now(),
|
||||
// ByteUtil.bytesToHexString(buf.array()));
|
||||
}
|
||||
|
||||
return buf;
|
||||
deviceChannel.writeAndFlush(buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理设备统计信息
|
||||
*/
|
||||
private void processDeviceStatistics(Device deviceBs, D331RtcmMessage message) {
|
||||
// 如果30分钟内收到不到d3f0和d3f2,则根据UDP最后一个报文触发状态更新和统计
|
||||
if(deviceBs.getD3xxbytes()>0){
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (deviceBs.getLastRxTime() != null &&
|
||||
deviceBs.getLastRxTime().isBefore(now.minusMinutes(1)) &&
|
||||
if(deviceBs.getLastRxTime().isBefore(now.minusMinutes(1)) &&
|
||||
(deviceBs.getLastD3f2Time() == null ||
|
||||
deviceBs.getLastD3f2Time().isBefore(now.minusMinutes(30)))) {
|
||||
// new cycle
|
||||
logger.info("device {} rx {} d331 in a cycle while not d3f0f2",deviceBs.getDeviceId(),deviceBs.getD3xxCount());
|
||||
|
||||
logger.info("device {} rx {} d331 in a cycle while not d3f0f2",
|
||||
deviceBs.getDeviceId(), deviceBs.getD3xxCount());
|
||||
|
||||
Device lastCycleDevice = createLastCycleDevice(deviceBs);
|
||||
deviceBs.clearStat();
|
||||
|
||||
ThreadManager.getFixedThreadPool().submit(() -> {
|
||||
try {
|
||||
beidouClient.onDeviceActive(deviceBs.getDeviceId(), deviceBs.getTenantId());
|
||||
} catch (Exception e) {
|
||||
logger.error("Error notifying device active", e);
|
||||
}
|
||||
dataPersistService.updateDeviceState(lastCycleDevice);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// update trx
|
||||
deviceBs.updateRx(message.getHeader(), message.getLen(), message.getPacketNum());
|
||||
|
||||
// update gga
|
||||
Gga gga = message.getGga();
|
||||
if (gga != null) {
|
||||
deviceBs.updateSatelitesNum(gga.getSatellitesInUsed());
|
||||
// 修复:正确处理Double类型的坐标
|
||||
deviceBs.setLatitude(gga.getLatitude());
|
||||
deviceBs.setLongitude(gga.getLongitude());
|
||||
deviceBs.setAltitude(gga.getAltitude());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建上一周期设备信息
|
||||
*/
|
||||
private Device createLastCycleDevice(Device deviceBs) {
|
||||
Device lastCycleDevice = new Device();
|
||||
lastCycleDevice.setDeviceId(deviceBs.getDeviceId());
|
||||
lastCycleDevice.setDeviceType(deviceBs.getDeviceType());
|
||||
@ -612,202 +241,80 @@ public class D331RtcmMessageExecutor implements Executor<D331RtcmMessage, Void>
|
||||
lastCycleDevice.setLastRxTime(deviceBs.getLastRxTime());
|
||||
lastCycleDevice.setLastValidCalcDataTime(deviceBs.getLastValidCalcDataTime());
|
||||
lastCycleDevice.setSatelitesInUse(deviceBs.getSatelitesInUse());
|
||||
return lastCycleDevice;
|
||||
deviceBs.clearStat();
|
||||
ThreadManager.getFixedThreadPool().submit(() -> {
|
||||
// 通知上线
|
||||
try {
|
||||
beidouClient.onDeviceActive(deviceBs.getDeviceId(), deviceBs.getTenantId());
|
||||
} catch (Exception e) {
|
||||
logger.error(e.toString());
|
||||
}
|
||||
dataPersistService.updateDeviceState(lastCycleDevice);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录日志(如果需要)
|
||||
*/
|
||||
private void logMessageIfNeeded(Device deviceBs, D331RtcmMessage message) {
|
||||
// update trx
|
||||
deviceBs.updateRx(message.getHeader(),message.getLen(),message.getPacketNum());
|
||||
// update gga
|
||||
Gga gga = message.getGga();
|
||||
if(gga != null) {
|
||||
deviceBs.updateSatelitesNum(gga.getSatellitesInUsed());
|
||||
deviceBs.setLatitude(gga.getLatitude());
|
||||
deviceBs.setLongitude(gga.getLongitude());
|
||||
deviceBs.setAltitude(gga.getAltitude());
|
||||
}
|
||||
|
||||
// 添加NTRIP处理
|
||||
byte[] srcdata = message.getSrcData();
|
||||
String rtcm = ByteUtil.bytesToHexString(srcdata);
|
||||
sendToNtrip(id, rtcm);
|
||||
|
||||
ThreadManager.getFixedThreadPool().submit(() -> {
|
||||
if (deviceBs.getLoggingmode() != null && deviceBs.getLoggingmode() == GnssDevice.LOGGING_MODE_FULL) {
|
||||
logger.info("receive {} d331 message: {}", message.getId(),
|
||||
DataTypeUtil.getHexString(message.getSrcData()));
|
||||
// 原始码流输出到日志文件 -- INFO 级别
|
||||
// 只有测站开了日志记录,或者消息来自基站,才将原码记录到日志文件
|
||||
if(deviceBs.getLoggingmode() == GnssDevice.LOGGING_MODE_FULL){
|
||||
logger.info("receive {} d331 message: {}", message.getId(), DataTypeUtil.getHexString(message.getSrcData()));
|
||||
}
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void sendToNtrip(String mountpoint, String hexData) {
|
||||
try {
|
||||
|
||||
// 将原始字节转换为16进制字符串用于RTCM提取
|
||||
//String hexData = ByteUtil.bytesToHexString(rawData);
|
||||
//System.out.println(hexData);
|
||||
|
||||
// 提取RTCM数据并发送到NtripServer,使用设备ID作为挂载点
|
||||
Optional.ofNullable(RtcmGgaUtil.getRtcms(hexData))
|
||||
.ifPresent(rtcm -> {
|
||||
//System.out.println("挂载点: " + mountpoint);
|
||||
//System.out.println("RTCM数据: " + rtcm);
|
||||
ntripServer.send(mountpoint, rtcm);
|
||||
|
||||
// 同时转发到12001端口
|
||||
rtcmUdpForwarder.forward(mountpoint, rtcm);
|
||||
|
||||
// 如果是特定设备的数据,则通过TCP服务器转发
|
||||
if (mountpoint.equals(rtcmSpecificDeviceTcpServer.getTargetDeviceId())) {
|
||||
for (String rtcmHex : rtcm) {
|
||||
byte[] rtcmData = ByteUtil.hexStringTobyte(rtcmHex);
|
||||
rtcmSpecificDeviceTcpServer.broadcastRtcmData(rtcmData);
|
||||
}
|
||||
logger.debug("Forwarded RTCM data from device {} to TCP server on port {}",
|
||||
mountpoint, rtcmSpecificDeviceTcpServer.getPort());
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
logger.error("处理NTRIP数据失败, 挂载点: {}, 错误: {}", mountpoint, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getMessageType() {
|
||||
return D331RtcmMessage.class;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送数据到NTRIP
|
||||
*/
|
||||
private void sendToNtrip(String mountpoint, String hexData) {
|
||||
try {
|
||||
Optional.ofNullable(RtcmGgaUtil.getRtcms(hexData))
|
||||
.ifPresent(rtcm -> ntripServer.send(mountpoint, rtcm));
|
||||
} catch (Exception e) {
|
||||
logger.error("处理NTRIP数据失败, 挂载点: {}, 错误: {}", mountpoint, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断基站是否在线
|
||||
*/
|
||||
private boolean isBaseStationOnline(String baseStationId) {
|
||||
if (baseStationId == null) return false;
|
||||
|
||||
BaseStationStatus status = baseStationStatusMap.get(baseStationId);
|
||||
if (status == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
return status.getLastActiveTime() != null &&
|
||||
status.getLastActiveTime().isAfter(now.minusSeconds(BASE_STATION_OFFLINE_TIMEOUT));
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断基站设备是否在线(兼容原有方法)
|
||||
*/
|
||||
private boolean isBaseStationOnline(Device baseStation) {
|
||||
if (baseStation == null) return false;
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
return baseStation.getLastRxTime() != null &&
|
||||
baseStation.getLastRxTime().isAfter(now.minusMinutes(5));
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知状态变化
|
||||
*/
|
||||
private void notifyStatusChange(String baseStationId, BaseStationStatusEnum oldStatus, BaseStationStatusEnum newStatus) {
|
||||
for (BaseStationSwitchListener listener : switchListeners) {
|
||||
try {
|
||||
listener.onBaseStationStatusChange(baseStationId, oldStatus, newStatus);
|
||||
} catch (Exception e) {
|
||||
logger.error("Error notifying base station status change listener", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知基站切换(可扩展为调用外部服务)
|
||||
*/
|
||||
private void notifyBaseStationSwitch(String deviceId, String fromBaseId, String toBaseId, String reason) {
|
||||
logger.info("Sending external notification for base station switch: Device={}, From={}, To={}, Reason={}",
|
||||
deviceId, fromBaseId, toBaseId, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知基站状态变化(可扩展为调用外部服务)
|
||||
*/
|
||||
private void notifyBaseStationStatusChange(String baseStationId, BaseStationStatusEnum oldStatus, BaseStationStatusEnum newStatus) {
|
||||
logger.info("Sending external notification for base station status change: BaseStation={}, From={}, To={}",
|
||||
baseStationId, oldStatus, newStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* 定期检查基站状态
|
||||
*/
|
||||
@Scheduled(fixedRate = 30000) // 30秒执行一次
|
||||
public void checkBaseStationStatus() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
for (BaseStationStatus status : baseStationStatusMap.values()) {
|
||||
if (status.getStatus() == BaseStationStatusEnum.ONLINE) {
|
||||
// 检查是否超时离线
|
||||
if (status.getLastActiveTime().isBefore(now.minusSeconds(BASE_STATION_OFFLINE_TIMEOUT))) {
|
||||
BaseStationStatusEnum oldStatus = status.getStatus();
|
||||
status.setStatus(BaseStationStatusEnum.OFFLINE);
|
||||
|
||||
logger.warn("Base station {} went offline (timeout). Last active: {}",
|
||||
status.getBaseStationId(), status.getLastActiveTime());
|
||||
|
||||
notifyStatusChange(status.getBaseStationId(), oldStatus, BaseStationStatusEnum.OFFLINE);
|
||||
|
||||
// 触发设备切换到备用基站
|
||||
triggerDeviceSwitchToBackup(status.getBaseStationId());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发设备切换到备用基站
|
||||
*/
|
||||
private void triggerDeviceSwitchToBackup(String offlineBaseId) {
|
||||
ThreadManager.getFixedThreadPool().submit(() -> {
|
||||
try {
|
||||
BaseStationStatus status = baseStationStatusMap.get(offlineBaseId);
|
||||
if (status != null) {
|
||||
Set<String> affectedDevices = new HashSet<>(status.getServingDevices());
|
||||
|
||||
for (String deviceId : affectedDevices) {
|
||||
// 查找设备信息并触发切换
|
||||
Device device = deviceService.findByDeviceId(deviceId);
|
||||
if (device != null) {
|
||||
String newBaseId = selectOptimalBaseStation(device);
|
||||
if (newBaseId != null && !newBaseId.equals(offlineBaseId)) {
|
||||
switchDeviceToBaseStation(deviceId, newBaseId,
|
||||
"Primary base station offline: " + offlineBaseId);
|
||||
} else {
|
||||
logger.warn("No suitable backup base station found for device {} (was using {})",
|
||||
deviceId, offlineBaseId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Error triggering device switch to backup base station", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加基站切换监听器
|
||||
*/
|
||||
public static void addBaseStationSwitchListener(BaseStationSwitchListener listener) {
|
||||
switchListeners.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除基站切换监听器
|
||||
*/
|
||||
public static void removeBaseStationSwitchListener(BaseStationSwitchListener listener) {
|
||||
switchListeners.remove(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取基站状态信息
|
||||
*/
|
||||
public static BaseStationStatus getBaseStationStatus(String baseStationId) {
|
||||
return baseStationStatusMap.get(baseStationId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有基站状态
|
||||
*/
|
||||
public static Map<String, BaseStationStatus> getAllBaseStationStatus() {
|
||||
return new HashMap<>(baseStationStatusMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备当前使用的基站
|
||||
*/
|
||||
public static String getDeviceCurrentBase(String deviceId) {
|
||||
return deviceCurrentBaseMap.get(deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证ECEF坐标是否有效
|
||||
*/
|
||||
private boolean isValidEcefCoordinates(Double x, Double y, Double z) {
|
||||
if (x == null || y == null || z == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否为零坐标
|
||||
if (x == 0.0 && y == 0.0 && z == 0.0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查坐标是否在地球表面合理范围内(大致6.3M到6.4M米)
|
||||
double distance = Math.sqrt(x * x + y * y + z * z);
|
||||
return distance >= 6300000 && distance <= 6400000;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
package com.imdroid.sideslope.ntrip;
|
||||
|
||||
import com.imdroid.common.util.ByteUtil;
|
||||
import com.imdroid.common.util.ThreadManager;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.InetAddress;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 将RTCM数据转发到指定UDP端口
|
||||
*/
|
||||
@Service
|
||||
public class RtcmUdpForwarder {
|
||||
private final Logger logger = LoggerFactory.getLogger(RtcmUdpForwarder.class);
|
||||
|
||||
@Value("${rtcm.forward.port:12001}")
|
||||
private Integer forwardPort;
|
||||
|
||||
private DatagramSocket socket;
|
||||
private DatagramPacket outPacket;
|
||||
|
||||
/**
|
||||
* 转发RTCM数据到UDP端口
|
||||
* @param mount 挂载点名称
|
||||
* @param hexRtcm 差分信息16进制字符串
|
||||
*/
|
||||
public void forward(String mount, String hexRtcm) {
|
||||
ThreadManager.getSingleThreadPool(RtcmUdpForwarder.class.getName()).execute(() -> {
|
||||
try {
|
||||
if (socket == null) socket = new DatagramSocket();
|
||||
if (outPacket == null) outPacket = new DatagramPacket(new byte[0], 0, InetAddress.getByName("localhost"), forwardPort);
|
||||
|
||||
// 转发原始RTCM数据,不添加挂载点信息
|
||||
byte[] rtcmData = ByteUtil.hexStringTobyte(hexRtcm);
|
||||
outPacket.setData(rtcmData);
|
||||
socket.send(outPacket);
|
||||
|
||||
logger.debug("Forwarded RTCM data from mountpoint {} to UDP port {}, data length: {}",
|
||||
mount, forwardPort, rtcmData.length);
|
||||
} catch (Exception e) {
|
||||
socket = null;
|
||||
outPacket = null;
|
||||
logger.error("Failed to forward RTCM data: {}", e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 转发多条RTCM数据到UDP端口
|
||||
* @param mount 挂载点名称
|
||||
* @param hexRtcmList 差分信息16进制字符串列表
|
||||
*/
|
||||
public void forward(String mount, List<String> hexRtcmList) {
|
||||
logger.debug("Forwarding {} RTCM messages from mountpoint {}", hexRtcmList.size(), mount);
|
||||
for (String hexRtcm : hexRtcmList) {
|
||||
forward(mount, hexRtcm);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,110 @@
|
||||
package com.imdroid.sideslope.server.tcp;
|
||||
|
||||
import io.netty.bootstrap.ServerBootstrap;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.ChannelOption;
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
/**
|
||||
* 特定设备RTCM数据TCP服务器
|
||||
*/
|
||||
@Component
|
||||
public class RtcmSpecificDeviceTcpServer implements ApplicationRunner {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(RtcmSpecificDeviceTcpServer.class);
|
||||
|
||||
@Value("${rtcm.specific.device.port:12002}")
|
||||
private Integer port;
|
||||
|
||||
@Value("${rtcm.specific.device.id:3530795}")
|
||||
private String targetDeviceId;
|
||||
|
||||
// 存储所有连接的客户端通道
|
||||
private final CopyOnWriteArrayList<SocketChannel> clientChannels = new CopyOnWriteArrayList<>();
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) throws Exception {
|
||||
new Thread(this::start0, "specific-device-tcp-server").start();
|
||||
}
|
||||
|
||||
private void start0() {
|
||||
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
|
||||
EventLoopGroup workerGroup = new NioEventLoopGroup();
|
||||
|
||||
try {
|
||||
ServerBootstrap bootstrap = new ServerBootstrap();
|
||||
bootstrap.group(bossGroup, workerGroup)
|
||||
.channel(NioServerSocketChannel.class)
|
||||
.option(ChannelOption.SO_BACKLOG, 128)
|
||||
.childOption(ChannelOption.SO_KEEPALIVE, true)
|
||||
.childHandler(new ChannelInitializer<SocketChannel>() {
|
||||
@Override
|
||||
protected void initChannel(SocketChannel ch) throws Exception {
|
||||
// 添加到客户端列表
|
||||
clientChannels.add(ch);
|
||||
|
||||
// 当连接关闭时,从列表中移除
|
||||
ch.closeFuture().addListener(future -> clientChannels.remove(ch));
|
||||
|
||||
logger.info("New client connected: {}", ch.remoteAddress());
|
||||
}
|
||||
});
|
||||
|
||||
ChannelFuture future = bootstrap.bind(port).sync();
|
||||
logger.info("Specific device TCP server started on port {} for device {}", port, targetDeviceId);
|
||||
|
||||
future.channel().closeFuture().sync();
|
||||
} catch (Exception e) {
|
||||
logger.error("Error starting Specific device TCP server at port {}", port, e);
|
||||
} finally {
|
||||
workerGroup.shutdownGracefully();
|
||||
bossGroup.shutdownGracefully();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向所有连接的客户端发送RTCM数据
|
||||
* @param rtcmData RTCM数据字节数组
|
||||
*/
|
||||
public void broadcastRtcmData(byte[] rtcmData) {
|
||||
if (clientChannels.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (SocketChannel channel : clientChannels) {
|
||||
if (channel.isActive()) {
|
||||
channel.writeAndFlush(io.netty.buffer.Unpooled.wrappedBuffer(rtcmData));
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("Broadcasted RTCM data to {} clients, data length: {}", clientChannels.size(), rtcmData.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取目标设备ID
|
||||
* @return 目标设备ID
|
||||
*/
|
||||
public String getTargetDeviceId() {
|
||||
return targetDeviceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务器端口
|
||||
* @return 服务器端口
|
||||
*/
|
||||
public Integer getPort() {
|
||||
return port;
|
||||
}
|
||||
}
|
||||
@ -74,6 +74,7 @@ public class Device {
|
||||
private BigDecimal used;
|
||||
|
||||
LocalDateTime lastRxTime;
|
||||
LocalDateTime lastD3f0f2Time;
|
||||
LocalDateTime lastD3f2Time;
|
||||
short noFixedAndFloatResult=0;
|
||||
|
||||
@ -89,15 +90,14 @@ public class Device {
|
||||
short abnormalD341Num = 0;
|
||||
byte cfgChannelType = CHANNEL_TYPE_TCP; // 0:TCP;1:DUP
|
||||
byte dataChannelType = CHANNEL_TYPE_UDP; // 0:TCP;1:DUP
|
||||
|
||||
private String parentId1;
|
||||
int lasQuality = 0;
|
||||
|
||||
public void updateRx(int head, int bytes,int count){
|
||||
lastRxHead = head;
|
||||
|
||||
switch (head){
|
||||
case 0xd3f0:
|
||||
//lastD3f0f2Time = LocalDateTime.now();
|
||||
lastD3f0f2Time = LocalDateTime.now();
|
||||
//clearStat();
|
||||
break;
|
||||
case 0xd3f2:
|
||||
@ -130,6 +130,7 @@ public class Device {
|
||||
}
|
||||
|
||||
public void updateCalcQuality(int quality){
|
||||
lasQuality = quality;
|
||||
if(b562AsCalc) {
|
||||
if (quality == UBXUtil.FIX_RESULT) fixedNum++;
|
||||
else if (quality == UBXUtil.FLOAT_RESULT) floatNum++;
|
||||
|
||||
@ -11,6 +11,4 @@ public interface DeviceService {
|
||||
Device findByDeviceId(String deviceId);
|
||||
|
||||
List<Device> findByParentId(String parentId);
|
||||
|
||||
List<Device> findByParentId1(String parentId1);
|
||||
}
|
||||
|
||||
@ -51,7 +51,6 @@ public class LocalDeviceServiceImpl implements DeviceService {
|
||||
device.setEcefy(gnssDevice.getEcefy());
|
||||
device.setEcefz(gnssDevice.getEcefz());
|
||||
device.setLoggingmode(gnssDevice.getLoggingmode());
|
||||
device.setParentId1(gnssDevice.getParentid1());
|
||||
return device;
|
||||
}
|
||||
|
||||
@ -66,19 +65,6 @@ public class LocalDeviceServiceImpl implements DeviceService {
|
||||
}
|
||||
return deviceList;
|
||||
}
|
||||
|
||||
public List<Device> getDeviceListFromDBByParentId1(String parentId1) {
|
||||
QueryWrapper<GnssDevice> query = new QueryWrapper();
|
||||
query.eq("parentid1", parentId1);
|
||||
List<GnssDevice> gnssDeviceList = gnssDeviceMapper.selectList(query);
|
||||
List<Device> deviceList = new ArrayList<>(gnssDeviceList.size());
|
||||
for (GnssDevice gnssDevice : gnssDeviceList) {
|
||||
Device device = findByDeviceId(gnssDevice.getDeviceid());
|
||||
if(device!=null) deviceList.add(device);
|
||||
}
|
||||
return deviceList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Device findByDeviceId(String deviceId) {
|
||||
Device device = deviceCache.getIfPresent(deviceId);
|
||||
@ -93,43 +79,23 @@ public class LocalDeviceServiceImpl implements DeviceService {
|
||||
|
||||
@Override
|
||||
public List<Device> findByParentId(String parentId) {
|
||||
String cacheKey = "PARENTID_" + parentId;
|
||||
List<Device> devices = subDeviceCache.getIfPresent(cacheKey);
|
||||
List<Device> devices = subDeviceCache.getIfPresent(parentId);
|
||||
if (devices == null) {
|
||||
devices = getDeviceListFromDBByParentId(parentId);
|
||||
if (devices != null) {
|
||||
subDeviceCache.put(cacheKey, devices);
|
||||
subDeviceCache.put(parentId, devices);
|
||||
}
|
||||
}
|
||||
return devices;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Device> findByParentId1(String parentId1) {
|
||||
String cacheKey = "PARENTID1_" + parentId1;
|
||||
List<Device> devices = subDeviceCache.getIfPresent(cacheKey);
|
||||
if (devices == null) {
|
||||
devices = getDeviceListFromDBByParentId1(parentId1);
|
||||
if (devices != null) {
|
||||
subDeviceCache.put(cacheKey, devices);
|
||||
}
|
||||
}
|
||||
return devices;
|
||||
}
|
||||
|
||||
public void refresh(String deviceId, String oldParentId, String oldParentId1){
|
||||
public void refresh(String deviceId, String oldParentId){
|
||||
Device device = deviceCache.getIfPresent(deviceId);
|
||||
if (device != null) {
|
||||
if(device.getParentId()!=null) {
|
||||
subDeviceCache.invalidate("PARENTID_" + device.getParentId());
|
||||
subDeviceCache.invalidate(device.getParentId());
|
||||
if(oldParentId!=null && !oldParentId.equals(device.getParentId())){
|
||||
subDeviceCache.invalidate("PARENTID_" + oldParentId);
|
||||
}
|
||||
}
|
||||
if(device.getParentId1()!=null) {
|
||||
subDeviceCache.invalidate("PARENTID1_" + device.getParentId1());
|
||||
if(oldParentId1!=null && !oldParentId1.equals(device.getParentId1())){
|
||||
subDeviceCache.invalidate("PARENTID1_" + oldParentId1);
|
||||
subDeviceCache.invalidate(oldParentId);
|
||||
}
|
||||
}
|
||||
deviceCache.invalidate(deviceId);
|
||||
|
||||
@ -154,9 +154,9 @@ public class ApiController {
|
||||
}
|
||||
|
||||
@PostMapping("/device_param_changed")
|
||||
public HttpResp deviceParamChanged(String deviceId, String oldParentId, String oldParentId1) {
|
||||
public HttpResp deviceParamChanged(String deviceId, String oldParentId) {
|
||||
// 更新设备缓存
|
||||
localDeviceService.refresh(deviceId, oldParentId, oldParentId1);
|
||||
localDeviceService.refresh(deviceId, oldParentId);
|
||||
calcService.refreshDeviceCalc(deviceId);
|
||||
|
||||
HttpResp resp = new HttpResp();
|
||||
|
||||
@ -287,7 +287,7 @@ public class GnssDeviceController extends BasicController{
|
||||
} else {
|
||||
// 更新组参数的关联个数
|
||||
updateBasicGroupAssociatedNum(device,old_device);
|
||||
rtcmClient.deviceParamChanged(device.getDeviceid(), old_device != null ? old_device.getParentid() : null, old_device != null ? old_device.getParentid1() : null);
|
||||
rtcmClient.deviceParamChanged(device.getDeviceid(), device.getParentid());
|
||||
return HttpResult.ok();
|
||||
}
|
||||
}
|
||||
@ -341,6 +341,65 @@ public class GnssDeviceController extends BasicController{
|
||||
return null;
|
||||
}
|
||||
|
||||
@PostMapping("/gnss/device/cont_loc")
|
||||
@ResponseBody
|
||||
public JSONObject contLoc(String deviceid,String createtime){
|
||||
JSONObject jsonObject = new JSONObject();
|
||||
|
||||
//old data
|
||||
String[] paras = createtime.split(",");
|
||||
String oldDeviceId;
|
||||
if(paras.length==2){
|
||||
oldDeviceId = paras[0];
|
||||
createtime = paras[1];
|
||||
}
|
||||
else {
|
||||
oldDeviceId = deviceid;
|
||||
createtime = paras[0];
|
||||
}
|
||||
QueryWrapper<GnssCalcData> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("deviceid",oldDeviceId);
|
||||
queryWrapper.eq("enabled",1);
|
||||
queryWrapper.eq("createtime",createtime);
|
||||
queryWrapper.last("limit 1");
|
||||
queryWrapper.isNotNull("rpose");
|
||||
GnssCalcData calcOldData = gnssCalcDataMapper.selectOne(queryWrapper);
|
||||
if(calcOldData==null) {
|
||||
jsonObject.put("code",1);
|
||||
jsonObject.put("msg","该时间无有效解算结果");
|
||||
return jsonObject;
|
||||
}
|
||||
|
||||
// new data
|
||||
QueryWrapper<GnssCalcData> queryWrapper1 = new QueryWrapper<>();
|
||||
queryWrapper1.eq("deviceid",deviceid);
|
||||
queryWrapper1.eq("enabled",1);
|
||||
queryWrapper1.orderByDesc("createtime");
|
||||
queryWrapper1.last("limit 1");
|
||||
queryWrapper1.isNotNull("rpose");
|
||||
GnssCalcData calcNewData = gnssCalcDataMapper.selectOne(queryWrapper1);
|
||||
if(calcNewData==null) {
|
||||
jsonObject.put("code",1);
|
||||
jsonObject.put("msg","无有效解算结果");
|
||||
return jsonObject;
|
||||
}
|
||||
|
||||
// 原来的初始值
|
||||
QueryWrapper<GnssDevice> deviceQueryWrapper = new QueryWrapper<>();
|
||||
deviceQueryWrapper.eq("deviceid",oldDeviceId);
|
||||
deviceQueryWrapper.last("limit 1");
|
||||
GnssDevice device = gnssDeviceMapper.selectOne(deviceQueryWrapper);
|
||||
if(device.getIpose()==null) device.setIpose(0.0);
|
||||
if(device.getIposn()==null) device.setIposn(0.0);
|
||||
if(device.getIposd()==null) device.setIposd(0.0);
|
||||
|
||||
jsonObject.put("code",0);
|
||||
jsonObject.put("ipose", NumberUtils.scaleTwo(calcNewData.getRpose()-calcOldData.getRpose()+device.getIpose()));
|
||||
jsonObject.put("iposn",NumberUtils.scaleTwo(calcNewData.getRposn()-calcOldData.getRposn()+device.getIposn()));
|
||||
jsonObject.put("iposd",NumberUtils.scaleTwo(calcNewData.getRposd()-calcOldData.getRposd()+device.getIposd()));
|
||||
return jsonObject;
|
||||
}
|
||||
|
||||
void updateBasicGroupAssociatedNum(GnssDevice newCfg, GnssDevice oldCfg){
|
||||
updateBasicGroupAssociatedNum(newCfg.getGroup_id());
|
||||
updateCalcGroupAssociatedNum(newCfg.getCalc_group_id());
|
||||
@ -409,7 +468,7 @@ public class GnssDeviceController extends BasicController{
|
||||
if (num == 0) {
|
||||
return HttpResult.failed();
|
||||
} else{
|
||||
rtcmClient.deviceParamChanged(del_id, null, null);
|
||||
rtcmClient.deviceParamChanged(del_id, null);
|
||||
return HttpResult.ok();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package com.imdroid.beidou.controller;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.imdroid.beidou.service.CommonExcelService;
|
||||
import com.imdroid.secapi.dto.GnssStatusMsg;
|
||||
@ -12,13 +11,12 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 状态消息 控制器
|
||||
@ -34,10 +32,6 @@ public class GnssMsgStatusController extends BasicController implements CommonEx
|
||||
GnssStatusMsgMapper statusMsgMapper;
|
||||
@Autowired
|
||||
OpLogManager opLogManager;
|
||||
|
||||
// 滑动窗口大小
|
||||
private static final int WINDOW_SIZE = 5;
|
||||
|
||||
@RequestMapping("/page/gnss_msg_status")
|
||||
public String gnssStatusMsg(Model m, HttpSession session) {
|
||||
initModel(m, session);
|
||||
@ -60,118 +54,6 @@ public class GnssMsgStatusController extends BasicController implements CommonEx
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取特定设备的数据,并应用滑动平均
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
* @param limit 获取条数,默认150条
|
||||
* @return 处理后的数据
|
||||
*/
|
||||
@RequestMapping("/gnss/msg/status/device_data")
|
||||
@ResponseBody
|
||||
public JSONObject getDeviceData(@RequestParam String deviceId,
|
||||
@RequestParam(required = false, defaultValue = "150") Integer limit) {
|
||||
// 查询指定设备的最近数据
|
||||
QueryWrapper<GnssStatusMsg> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("deviceid", deviceId)
|
||||
.orderByDesc("createtime")
|
||||
.last("limit " + limit);
|
||||
|
||||
List<GnssStatusMsg> dataList = statusMsgMapper.selectList(queryWrapper);
|
||||
// 反转列表,使其按时间升序排列
|
||||
Collections.reverse(dataList);
|
||||
|
||||
// 应用滑动平均
|
||||
List<GnssStatusMsg> smoothedData = applyMovingAverage(dataList, WINDOW_SIZE);
|
||||
|
||||
JSONObject result = new JSONObject();
|
||||
result.put("code", 0);
|
||||
result.put("msg", "");
|
||||
result.put("count", smoothedData.size());
|
||||
result.put("data", smoothedData);
|
||||
result.put("originalData", dataList);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用滑动平均算法
|
||||
*
|
||||
* @param dataList 原始数据列表
|
||||
* @param windowSize 窗口大小
|
||||
* @return 处理后的数据列表
|
||||
*/
|
||||
private List<GnssStatusMsg> applyMovingAverage(List<GnssStatusMsg> dataList, int windowSize) {
|
||||
if (dataList.size() <= 1) {
|
||||
return new ArrayList<>(dataList);
|
||||
}
|
||||
|
||||
List<GnssStatusMsg> result = new ArrayList<>(dataList.size());
|
||||
|
||||
// 初始窗口
|
||||
Deque<Float> rollWindow = new LinkedList<>();
|
||||
Deque<Float> pitchWindow = new LinkedList<>();
|
||||
Deque<Float> yawWindow = new LinkedList<>();
|
||||
|
||||
for (int i = 0; i < dataList.size(); i++) {
|
||||
GnssStatusMsg currentMsg = dataList.get(i);
|
||||
GnssStatusMsg smoothedMsg = new GnssStatusMsg();
|
||||
|
||||
// 复制基本属性
|
||||
smoothedMsg.setId(currentMsg.getId());
|
||||
smoothedMsg.setDeviceid(currentMsg.getDeviceid());
|
||||
smoothedMsg.setCreatetime(currentMsg.getCreatetime());
|
||||
smoothedMsg.setDevicetime(currentMsg.getDevicetime());
|
||||
smoothedMsg.setTenantid(currentMsg.getTenantid());
|
||||
smoothedMsg.setDtustate(currentMsg.getDtustate());
|
||||
smoothedMsg.setRssi(currentMsg.getRssi());
|
||||
smoothedMsg.setVoltage(currentMsg.getVoltage());
|
||||
smoothedMsg.setSolarvoltage(currentMsg.getSolarvoltage());
|
||||
smoothedMsg.setChargecurrency(currentMsg.getChargecurrency());
|
||||
smoothedMsg.setChargewatt(currentMsg.getChargewatt());
|
||||
smoothedMsg.setTemperature(currentMsg.getTemperature());
|
||||
smoothedMsg.setHumidity(currentMsg.getHumidity());
|
||||
|
||||
// 添加当前值到窗口
|
||||
if (currentMsg.getRoll() != null) rollWindow.addLast(currentMsg.getRoll());
|
||||
if (currentMsg.getPitch() != null) pitchWindow.addLast(currentMsg.getPitch());
|
||||
if (currentMsg.getYaw() != null) yawWindow.addLast(currentMsg.getYaw());
|
||||
|
||||
// 保持窗口大小
|
||||
if (rollWindow.size() > windowSize) rollWindow.removeFirst();
|
||||
if (pitchWindow.size() > windowSize) pitchWindow.removeFirst();
|
||||
if (yawWindow.size() > windowSize) yawWindow.removeFirst();
|
||||
|
||||
// 计算平均值
|
||||
if (!rollWindow.isEmpty()) {
|
||||
float rollSum = 0;
|
||||
for (Float val : rollWindow) rollSum += val;
|
||||
smoothedMsg.setRoll(rollSum / rollWindow.size());
|
||||
} else {
|
||||
smoothedMsg.setRoll(currentMsg.getRoll());
|
||||
}
|
||||
|
||||
if (!pitchWindow.isEmpty()) {
|
||||
float pitchSum = 0;
|
||||
for (Float val : pitchWindow) pitchSum += val;
|
||||
smoothedMsg.setPitch(pitchSum / pitchWindow.size());
|
||||
} else {
|
||||
smoothedMsg.setPitch(currentMsg.getPitch());
|
||||
}
|
||||
|
||||
if (!yawWindow.isEmpty()) {
|
||||
float yawSum = 0;
|
||||
for (Float val : yawWindow) yawSum += val;
|
||||
smoothedMsg.setYaw(yawSum / yawWindow.size());
|
||||
} else {
|
||||
smoothedMsg.setYaw(currentMsg.getYaw());
|
||||
}
|
||||
|
||||
result.add(smoothedMsg);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出excel
|
||||
*
|
||||
|
||||
@ -2,18 +2,16 @@ package com.imdroid.beidou.controller;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.imdroid.beidou.common.HttpResult;
|
||||
import com.imdroid.beidou.service.CommonExcelService;
|
||||
import com.imdroid.secapi.dto.GnssGroupFwd;
|
||||
import com.imdroid.secapi.dto.GnssGroupFwdMapper;
|
||||
import com.imdroid.secapi.dto.ResendRecord;
|
||||
import com.imdroid.secapi.dto.ResendRecordMapper;
|
||||
import com.imdroid.secapi.dto.*;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Controller
|
||||
@ -22,6 +20,10 @@ public class GnssResendController extends BasicController implements CommonExcel
|
||||
GnssGroupFwdMapper gnssGroupFwdMapper;
|
||||
@Autowired
|
||||
ResendRecordMapper resendRecordMapper;
|
||||
@Autowired
|
||||
TenantMapper tenantMapper;
|
||||
@Autowired
|
||||
OpLogManager opLogManager;
|
||||
|
||||
/********* 推送页面 *********/
|
||||
@RequestMapping("/page/resend_records")
|
||||
@ -33,6 +35,19 @@ public class GnssResendController extends BasicController implements CommonExcel
|
||||
|
||||
return "/page/resend_records";
|
||||
}
|
||||
@RequestMapping("/page/table/resend_record_add")
|
||||
public String gnssAddDev(Model m, HttpSession session) {
|
||||
initModel(m, session);
|
||||
//以下用于下拉框数据
|
||||
List<Tenant> tenants = tenantMapper.selectList(null);
|
||||
List<GnssGroupFwd> gnssGroupFwds = gnssGroupFwdMapper.selectList(null);
|
||||
|
||||
m.addAttribute("tenant_list", tenants);
|
||||
m.addAttribute("gnss_group_fwd_list", gnssGroupFwds);
|
||||
m.addAttribute("device", new GnssDevice());
|
||||
|
||||
return "/page/table/resend_record_add";
|
||||
}
|
||||
|
||||
/********* 推送数据 *********/
|
||||
@RequestMapping("/fwd/resend_records")
|
||||
@ -41,6 +56,40 @@ public class GnssResendController extends BasicController implements CommonExcel
|
||||
return this.pageList(session, page, limit, searchParams);
|
||||
}
|
||||
|
||||
@PostMapping("/fwd/resend_records/add")
|
||||
@ResponseBody
|
||||
public String update(HttpSession session, @RequestBody JSONObject object) throws Exception {
|
||||
// 从请求参数中创建对象
|
||||
ResendRecord resendRecord = JSONObject.toJavaObject(object,ResendRecord.class);
|
||||
resendRecord.setCreatetime(LocalDateTime.now());
|
||||
resendRecord.setState(ResendRecord.STATE_BREAK_POINT);
|
||||
int num = resendRecordMapper.insert(resendRecord);
|
||||
if (num == 0) {
|
||||
return HttpResult.failed();
|
||||
} else {
|
||||
opLogManager.addLog(getLoginUser(session),getTenantId(session),
|
||||
OpLogManager.OP_TYPE_ADD,
|
||||
OpLogManager.OP_OBJ_FWD_RECORD,
|
||||
"create new resend record");
|
||||
return HttpResult.ok();
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/fwd/resend_records/delete")
|
||||
@ResponseBody
|
||||
public String delete(HttpSession session, @RequestParam String del_id) throws Exception {
|
||||
int num = resendRecordMapper.deleteById(del_id);
|
||||
opLogManager.addLog(getLoginUser(session),getTenantId(session),
|
||||
OpLogManager.OP_TYPE_DEL,
|
||||
OpLogManager.OP_OBJ_FWD_RECORD,
|
||||
del_id + " deleted");
|
||||
if (num == 0) {
|
||||
return HttpResult.failed();
|
||||
} else{
|
||||
return HttpResult.ok();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取实体类的class
|
||||
*
|
||||
|
||||
@ -1,7 +1,3 @@
|
||||
/**
|
||||
* 坐标转换工具模块
|
||||
* 提供WGS84和GCJ-02坐标系之间的转换功能
|
||||
*/
|
||||
var CoordinateUtils = (function() {
|
||||
'use strict';
|
||||
|
||||
|
||||
@ -1,7 +1,3 @@
|
||||
/**
|
||||
* 设备标记管理模块
|
||||
* 负责设备标记的创建、分类和显示管理
|
||||
*/
|
||||
var DeviceMarkers = (function() {
|
||||
'use strict';
|
||||
|
||||
@ -281,11 +277,11 @@ var DeviceMarkers = (function() {
|
||||
|
||||
vectorSource.addFeature(myLocationFeature);
|
||||
|
||||
map.getView().animate({
|
||||
center: coordinates,
|
||||
zoom: 15,
|
||||
duration: 1000
|
||||
});
|
||||
// map.getView().animate({
|
||||
// center: coordinates,
|
||||
// zoom: 15,
|
||||
// duration: 1000
|
||||
// });
|
||||
},
|
||||
function(error) {
|
||||
console.error('获取位置失败:', error);
|
||||
@ -294,6 +290,50 @@ var DeviceMarkers = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
function locateMyLocation() {
|
||||
if (myLocationFeature) {
|
||||
var geometry = myLocationFeature.getGeometry();
|
||||
var coordinates = geometry.getCoordinates();
|
||||
|
||||
map.getView().animate({
|
||||
center: coordinates,
|
||||
zoom: 15,
|
||||
duration: 1000
|
||||
});
|
||||
return true;
|
||||
} else {
|
||||
if (navigator.geolocation) {
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
function(position) {
|
||||
var lat = position.coords.latitude;
|
||||
var lon = position.coords.longitude;
|
||||
|
||||
var currentMapType = getCurrentMapType();
|
||||
var coordinates = CoordinateUtils.getMapCoordinates(lat, lon, currentMapType);
|
||||
|
||||
if (myLocationFeature) {
|
||||
vectorSource.removeFeature(myLocationFeature);
|
||||
}
|
||||
|
||||
myLocationFeature = new ol.Feature({
|
||||
geometry: new ol.geom.Point(coordinates),
|
||||
isMyLocation: true
|
||||
});
|
||||
|
||||
vectorSource.addFeature(myLocationFeature);
|
||||
|
||||
map.getView().animate({
|
||||
center: coordinates,
|
||||
zoom: 15,
|
||||
duration: 1000
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
function startLocationUpdates() {
|
||||
if (navigator.geolocation) {
|
||||
@ -354,6 +394,7 @@ var DeviceMarkers = (function() {
|
||||
findDeviceById: findDeviceById,
|
||||
locateDevice: locateDevice,
|
||||
getMyLocation: getMyLocation,
|
||||
locateMyLocation: locateMyLocation,
|
||||
startLocationUpdates: startLocationUpdates,
|
||||
stopLocationUpdates: stopLocationUpdates,
|
||||
updateMyLocationForMapType: updateMyLocationForMapType,
|
||||
|
||||
@ -1,196 +1,126 @@
|
||||
/**
|
||||
* 设备总览主入口文件
|
||||
* 负责整个设备总览模块的初始化和全局函数暴露
|
||||
*/
|
||||
var DeviceOverview = (function() {
|
||||
'use strict';
|
||||
|
||||
// 全局变量(从原HTML中提取)
|
||||
var deviceList = [];
|
||||
|
||||
/**
|
||||
* 初始化设备总览模块
|
||||
* @param {Array} devices - 设备列表数据
|
||||
* @param {Object} options - 配置选项
|
||||
*/
|
||||
function init(devices, options) {
|
||||
deviceList = devices || [];
|
||||
|
||||
// 设置全局变量供其他模块使用
|
||||
window.deviceList = deviceList;
|
||||
window.userRole = options && options.role ? options.role : 'USER';
|
||||
|
||||
// 等待layui加载完成后初始化
|
||||
layui.use(['form'], function(){
|
||||
var form = layui.form;
|
||||
|
||||
// 绑定表单事件
|
||||
form.on('select(mapTypeNew)', function(data){
|
||||
MapCore.switchMapType(data.value);
|
||||
});
|
||||
|
||||
// 初始化地图核心
|
||||
MapCore.initialize(deviceList);
|
||||
|
||||
// 默认显示所有设备
|
||||
document.getElementById('warningFilter').value = 'all';
|
||||
SearchFilter.showAllDevices();
|
||||
});
|
||||
}
|
||||
|
||||
// 暴露给HTML使用的全局函数
|
||||
|
||||
/**
|
||||
* 地图类型变化处理
|
||||
*/
|
||||
function onMapTypeChange() {
|
||||
return MapCore.onMapTypeChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索设备
|
||||
*/
|
||||
function searchDeviceNew() {
|
||||
return MapCore.searchDeviceNew();
|
||||
}
|
||||
|
||||
/**
|
||||
* 告警过滤变化处理
|
||||
*/
|
||||
function onWarningFilterChange() {
|
||||
return MapCore.onWarningFilterChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换地图功能菜单
|
||||
*/
|
||||
function toggleMapFunctionsMenu() {
|
||||
return MapCore.toggleMapFunctionsMenu();
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换设备ID显示
|
||||
*/
|
||||
function toggleDeviceId() {
|
||||
return MapCore.toggleDeviceId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换集群显示
|
||||
*/
|
||||
function toggleCluster() {
|
||||
return MapCore.toggleCluster();
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换测距功能
|
||||
*/
|
||||
function toggleMeasureDistance() {
|
||||
return MeasureTools.toggleMeasureDistance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成测量
|
||||
*/
|
||||
function finishMeasuring() {
|
||||
return MeasureTools.finishMeasuring();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除测距
|
||||
*/
|
||||
function clearMeasure() {
|
||||
return MeasureTools.clearMeasure();
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换天气预报功能
|
||||
*/
|
||||
function toggleWeatherForecast() {
|
||||
return WeatherForecast.toggleWeatherForecast();
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭天气卡片
|
||||
*/
|
||||
function closeWeatherCard() {
|
||||
return WeatherForecast.closeWeatherCard();
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示上一个天气预报
|
||||
*/
|
||||
function showPrevForecast() {
|
||||
return WeatherForecast.showPrevForecast();
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示下一个天气预报
|
||||
*/
|
||||
function showNextForecast() {
|
||||
return WeatherForecast.showNextForecast();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询设备
|
||||
* @param {string} statusType - 状态类型
|
||||
*/
|
||||
function queryDevices(statusType) {
|
||||
return SearchFilter.queryDevices(statusType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 定位设备到地图
|
||||
* @param {string} deviceId - 设备ID
|
||||
* @param {number} latitude - 纬度
|
||||
* @param {number} longitude - 经度
|
||||
*/
|
||||
function locateDeviceOnMap(deviceId, latitude, longitude) {
|
||||
return SearchFilter.locateDeviceOnMap(deviceId, latitude, longitude);
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接定位设备
|
||||
* @param {string} deviceId - 设备ID
|
||||
*/
|
||||
function locateDeviceDirectly(deviceId) {
|
||||
return SearchFilter.locateDeviceDirectly(deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换地图类型
|
||||
* @param {string} mapType - 地图类型
|
||||
*/
|
||||
function switchMapType(mapType) {
|
||||
return MapCore.switchMapType(mapType);
|
||||
}
|
||||
|
||||
// 公开API
|
||||
function locateMyLocation() {
|
||||
return DeviceMarkers.locateMyLocation();
|
||||
}
|
||||
|
||||
function clearDeviceFilter() {
|
||||
return SearchFilter.clearDeviceFilter();
|
||||
}
|
||||
|
||||
return {
|
||||
init: init,
|
||||
|
||||
// 地图相关
|
||||
onMapTypeChange: onMapTypeChange,
|
||||
switchMapType: switchMapType,
|
||||
|
||||
// 搜索和过滤
|
||||
searchDeviceNew: searchDeviceNew,
|
||||
onWarningFilterChange: onWarningFilterChange,
|
||||
queryDevices: queryDevices,
|
||||
locateDeviceOnMap: locateDeviceOnMap,
|
||||
locateDeviceDirectly: locateDeviceDirectly,
|
||||
locateMyLocation: locateMyLocation,
|
||||
clearDeviceFilter: clearDeviceFilter,
|
||||
|
||||
// 地图功能
|
||||
toggleMapFunctionsMenu: toggleMapFunctionsMenu,
|
||||
toggleDeviceId: toggleDeviceId,
|
||||
toggleCluster: toggleCluster,
|
||||
|
||||
// 测距工具
|
||||
toggleMeasureDistance: toggleMeasureDistance,
|
||||
finishMeasuring: finishMeasuring,
|
||||
clearMeasure: clearMeasure,
|
||||
|
||||
// 天气预报
|
||||
toggleWeatherForecast: toggleWeatherForecast,
|
||||
closeWeatherCard: closeWeatherCard,
|
||||
showPrevForecast: showPrevForecast,
|
||||
@ -198,7 +128,6 @@ var DeviceOverview = (function() {
|
||||
};
|
||||
})();
|
||||
|
||||
// 将主要函数暴露到全局作用域,供HTML中的onclick等使用
|
||||
window.DeviceOverview = DeviceOverview;
|
||||
window.onMapTypeChange = DeviceOverview.onMapTypeChange;
|
||||
window.searchDeviceNew = DeviceOverview.searchDeviceNew;
|
||||
@ -216,3 +145,5 @@ window.showNextForecast = DeviceOverview.showNextForecast;
|
||||
window.queryDevices = DeviceOverview.queryDevices;
|
||||
window.locateDeviceOnMap = DeviceOverview.locateDeviceOnMap;
|
||||
window.locateDeviceDirectly = DeviceOverview.locateDeviceDirectly;
|
||||
window.locateMyLocation = DeviceOverview.locateMyLocation;
|
||||
window.clearDeviceFilter = DeviceOverview.clearDeviceFilter;
|
||||
@ -1,7 +1,3 @@
|
||||
/**
|
||||
* 地图核心模块
|
||||
* 负责地图初始化、事件处理和核心功能管理
|
||||
*/
|
||||
var MapCore = (function() {
|
||||
'use strict';
|
||||
|
||||
@ -92,9 +88,8 @@ var MapCore = (function() {
|
||||
|
||||
if (deviceList && deviceList.length > 0) {
|
||||
setCenterFromDevices(deviceList);
|
||||
}
|
||||
|
||||
DeviceMarkers.addDeviceMarkers(deviceList);
|
||||
}
|
||||
|
||||
DeviceMarkers.getMyLocation();
|
||||
DeviceMarkers.startLocationUpdates();
|
||||
@ -216,21 +211,13 @@ var MapCore = (function() {
|
||||
function setCenterFromDevices(deviceList) {
|
||||
if (!deviceList || deviceList.length === 0) return;
|
||||
|
||||
var minLat = deviceList[0].latitude;
|
||||
var maxLat = deviceList[0].latitude;
|
||||
var minLon = deviceList[0].longitude;
|
||||
var maxLon = deviceList[0].longitude;
|
||||
|
||||
for (var i = 1; i < deviceList.length; i++) {
|
||||
var device = deviceList[i];
|
||||
minLat = Math.min(minLat, device.latitude);
|
||||
maxLat = Math.max(maxLat, device.latitude);
|
||||
minLon = Math.min(minLon, device.longitude);
|
||||
maxLon = Math.max(maxLon, device.longitude);
|
||||
var sumLat = 0, sumLon = 0;
|
||||
for (var i = 0; i < deviceList.length; i++) {
|
||||
sumLat += deviceList[i].latitude;
|
||||
sumLon += deviceList[i].longitude;
|
||||
}
|
||||
|
||||
var centerLat = (minLat + maxLat) / 2;
|
||||
var centerLon = (minLon + maxLon) / 2;
|
||||
var centerLat = sumLat / deviceList.length;
|
||||
var centerLon = sumLon / deviceList.length;
|
||||
|
||||
map.getView().setCenter(ol.proj.fromLonLat([centerLon, centerLat]));
|
||||
}
|
||||
@ -241,15 +228,22 @@ var MapCore = (function() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存当前视图状态
|
||||
var currentCenter = map.getView().getCenter();
|
||||
var currentZoom = map.getView().getZoom();
|
||||
|
||||
map.removeLayer(currentBaseLayer);
|
||||
currentBaseLayer = MapLayers.getLayer(mapType);
|
||||
map.getLayers().insertAt(0, currentBaseLayer);
|
||||
|
||||
DeviceMarkers.updateMyLocationForMapType(mapType);
|
||||
|
||||
// 重新获取设备列表并添加标记
|
||||
var deviceList = window.deviceList || [];
|
||||
DeviceMarkers.addDeviceMarkers(deviceList);
|
||||
|
||||
// 恢复之前的视图状态
|
||||
map.getView().setCenter(currentCenter);
|
||||
map.getView().setZoom(currentZoom);
|
||||
}
|
||||
|
||||
function toggleDeviceId() {
|
||||
@ -281,10 +275,6 @@ var MapCore = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭地图功能菜单
|
||||
* @param {Event} event - 点击事件
|
||||
*/
|
||||
function closeMapFunctionsMenu(event) {
|
||||
var dropdown = document.getElementById('mapFunctionsMenu');
|
||||
var toggleBtn = document.querySelector('.dropdown-toggle');
|
||||
@ -295,9 +285,6 @@ var MapCore = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 地图类型变化处理
|
||||
*/
|
||||
function onMapTypeChange() {
|
||||
var mapType = document.getElementById('mapTypeSelectNew').value;
|
||||
switchMapType(mapType);
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
/**
|
||||
* 地图图层管理模块
|
||||
* 管理不同类型的地图图层(天地图、高德、谷歌等)
|
||||
*/
|
||||
var MapLayers = (function() {
|
||||
'use strict';
|
||||
|
||||
// 天地图 API 密钥 (fengyarnom@gmail.com)
|
||||
// 天地图的英文就是 TIANDITU 所以这里用拼音指代
|
||||
var TIANDITU_KEY = '0c260b8a094a4e0bc507808812cefdac';
|
||||
|
||||
function createTiandituTileLoadFunction() {
|
||||
|
||||
@ -1,7 +1,3 @@
|
||||
/**
|
||||
* 测距工具模块
|
||||
* 提供地图测距功能
|
||||
*/
|
||||
var MeasureTools = (function() {
|
||||
'use strict';
|
||||
|
||||
|
||||
@ -1,12 +1,10 @@
|
||||
/**
|
||||
* 搜索和过滤功能模块
|
||||
* 负责设备搜索、过滤和查询功能
|
||||
*/
|
||||
var SearchFilter = (function() {
|
||||
'use strict';
|
||||
|
||||
var currentSearchedDevice = null;
|
||||
var markerState = 3; // 1:all; 2:orange; 3:red
|
||||
var filteredDeviceIds = [];
|
||||
var isFilterActive = false;
|
||||
|
||||
function searchDevice(deviceId) {
|
||||
if (!deviceId || !deviceId.trim()) {
|
||||
@ -15,6 +13,13 @@ var SearchFilter = (function() {
|
||||
}
|
||||
|
||||
deviceId = deviceId.trim();
|
||||
|
||||
// 多设备
|
||||
if (deviceId.indexOf(',') !== -1) {
|
||||
return handleMultiDeviceSearch(deviceId);
|
||||
}
|
||||
|
||||
// 单设备
|
||||
currentSearchedDevice = deviceId;
|
||||
|
||||
var success = DeviceMarkers.locateDevice(deviceId);
|
||||
@ -41,6 +46,126 @@ var SearchFilter = (function() {
|
||||
return success;
|
||||
}
|
||||
|
||||
function handleMultiDeviceSearch(deviceIdsString) {
|
||||
var deviceIds = deviceIdsString.split(',').map(function(id) {
|
||||
return id.trim();
|
||||
}).filter(function(id) {
|
||||
return id !== '';
|
||||
});
|
||||
|
||||
if (deviceIds.length === 0) {
|
||||
clearSearch();
|
||||
return false;
|
||||
}
|
||||
|
||||
filteredDeviceIds = deviceIds;
|
||||
|
||||
var foundDevices = filterDevicesByIds(deviceIds);
|
||||
|
||||
if (foundDevices.length > 0) {
|
||||
showFilterStatus(deviceIds.length);
|
||||
|
||||
isFilterActive = true;
|
||||
|
||||
var firstDevice = foundDevices[0];
|
||||
var geometry = firstDevice.getGeometry();
|
||||
var coordinates = geometry.getCoordinates();
|
||||
|
||||
if (MapCore.getMap()) {
|
||||
MapCore.getMap().getView().animate({
|
||||
center: coordinates,
|
||||
zoom: 12,
|
||||
duration: 1000
|
||||
});
|
||||
}
|
||||
|
||||
if (window.layer && typeof window.layer.msg === 'function') {
|
||||
window.layer.msg('已过滤显示 ' + foundDevices.length + ' 个设备');
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
if (window.layer && typeof window.layer.msg === 'function') {
|
||||
window.layer.msg('未找到任何匹配的设备');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function filterDevicesByIds(deviceIds) {
|
||||
if (!DeviceMarkers.getAllFeatures) return [];
|
||||
|
||||
var allFeatures = DeviceMarkers.getAllFeatures();
|
||||
var foundDevices = [];
|
||||
var vectorSource = MapCore.getVectorSource();
|
||||
|
||||
if (!vectorSource) return [];
|
||||
|
||||
var myLocationFeature = DeviceMarkers.getMyLocationFeature();
|
||||
|
||||
vectorSource.clear();
|
||||
|
||||
if (myLocationFeature) {
|
||||
vectorSource.addFeature(myLocationFeature);
|
||||
}
|
||||
|
||||
for (var i = 0; i < allFeatures.length; i++) {
|
||||
var feature = allFeatures[i];
|
||||
var deviceInfo = feature.get('deviceInfo');
|
||||
|
||||
if (deviceInfo) {
|
||||
var deviceId = String(deviceInfo.deviceid).trim();
|
||||
|
||||
for (var j = 0; j < deviceIds.length; j++) {
|
||||
var searchId = String(deviceIds[j]).trim();
|
||||
|
||||
if (deviceId === searchId ||
|
||||
deviceId.indexOf(searchId) !== -1 ||
|
||||
deviceId.replace(/\s+/g, '') === searchId.replace(/\s+/g, '')) {
|
||||
vectorSource.addFeature(feature);
|
||||
foundDevices.push(feature);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return foundDevices;
|
||||
}
|
||||
|
||||
function showFilterStatus(count) {
|
||||
var filterStatus = document.getElementById('filterStatus');
|
||||
|
||||
if (filterStatus) {
|
||||
filterStatus.innerHTML =
|
||||
'<span style="color: #1aa094; font-weight: bold; font-size: 12px;">定位中</span>' +
|
||||
'<button class="toolbar-btn" onclick="clearDeviceFilter()" style="background: #f56565; margin-left: 6px;">' +
|
||||
'<i class="layui-icon layui-icon-close" style="font-size: 12px;"></i>' +
|
||||
'</button>';
|
||||
|
||||
filterStatus.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
|
||||
function hideFilterStatus() {
|
||||
var filterStatus = document.getElementById('filterStatus');
|
||||
if (filterStatus) {
|
||||
filterStatus.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function clearDeviceFilter() {
|
||||
isFilterActive = false;
|
||||
filteredDeviceIds = [];
|
||||
hideFilterStatus();
|
||||
|
||||
applyCurrentFilter();
|
||||
|
||||
if (window.layer && typeof window.layer.msg === 'function') {
|
||||
window.layer.msg('已清除设备过滤');
|
||||
}
|
||||
}
|
||||
|
||||
function clearSearch() {
|
||||
currentSearchedDevice = null;
|
||||
var searchInput = document.getElementById('deviceSearchNew');
|
||||
@ -48,9 +173,13 @@ var SearchFilter = (function() {
|
||||
searchInput.value = '';
|
||||
}
|
||||
|
||||
if (isFilterActive) {
|
||||
clearDeviceFilter();
|
||||
} else {
|
||||
// 恢复到当前的过滤状态
|
||||
applyCurrentFilter();
|
||||
}
|
||||
}
|
||||
|
||||
function applyCurrentFilter() {
|
||||
switch(markerState) {
|
||||
@ -170,6 +299,14 @@ var SearchFilter = (function() {
|
||||
markerState = state;
|
||||
}
|
||||
|
||||
function isFilterModeActive() {
|
||||
return isFilterActive;
|
||||
}
|
||||
|
||||
function getFilteredDeviceIds() {
|
||||
return filteredDeviceIds;
|
||||
}
|
||||
|
||||
return {
|
||||
searchDevice: searchDevice,
|
||||
clearSearch: clearSearch,
|
||||
@ -182,6 +319,9 @@ var SearchFilter = (function() {
|
||||
locateDeviceDirectly: locateDeviceDirectly,
|
||||
getCurrentSearchedDevice: getCurrentSearchedDevice,
|
||||
getMarkerState: getMarkerState,
|
||||
setMarkerState: setMarkerState
|
||||
setMarkerState: setMarkerState,
|
||||
clearDeviceFilter: clearDeviceFilter,
|
||||
isFilterModeActive: isFilterModeActive,
|
||||
getFilteredDeviceIds: getFilteredDeviceIds
|
||||
};
|
||||
})();
|
||||
@ -1,7 +1,6 @@
|
||||
var WeatherForecast = (function() {
|
||||
'use strict';
|
||||
|
||||
var weatherApiKey = 'Uxh4IdMuAvhSiBnsf4UUDVGF4e3YAp2B';
|
||||
var weatherEnabled = false;
|
||||
var weatherData = null;
|
||||
var currentForecastIndex = 0;
|
||||
@ -81,7 +80,6 @@ var WeatherForecast = (function() {
|
||||
contentElement.innerHTML =
|
||||
'<div class="weather-loading">' +
|
||||
'<i class="layui-icon layui-icon-loading layui-icon-anim-rotate"></i>' +
|
||||
'<p>请确保网络可访问 Windy API 服务</p>' +
|
||||
'<p>正在获取天气预测数据...</p>' +
|
||||
'</div>';
|
||||
}
|
||||
@ -140,23 +138,19 @@ var WeatherForecast = (function() {
|
||||
}
|
||||
|
||||
function fetchWeatherData(lat, lon) {
|
||||
var requestBody = {
|
||||
"lat": parseFloat(lat.toFixed(2)),
|
||||
"lon": parseFloat(lon.toFixed(2)),
|
||||
"model": "gfs",
|
||||
"parameters": ["temp", "wind", "precip", "pressure", "rh", "windGust"],
|
||||
"levels": ["surface"],
|
||||
"key": weatherApiKey,
|
||||
"hours": 72
|
||||
};
|
||||
var url = 'https://api.open-meteo.com/v1/forecast?' +
|
||||
'latitude=' + lat.toFixed(4) +
|
||||
'&longitude=' + lon.toFixed(4) +
|
||||
'¤t=temperature_2m,wind_speed_10m,wind_direction_10m,relative_humidity_2m,surface_pressure' +
|
||||
'&hourly=temperature_2m,relative_humidity_2m,wind_speed_10m,wind_direction_10m,precipitation,surface_pressure,wind_gusts_10m' +
|
||||
'&forecast_days=3' +
|
||||
'&timezone=auto';
|
||||
|
||||
fetch('https://api.windy.com/api/point-forecast/v2', {
|
||||
method: 'POST',
|
||||
fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(requestBody)
|
||||
}
|
||||
})
|
||||
.then(function(response) {
|
||||
if (!response.ok) {
|
||||
@ -165,7 +159,7 @@ var WeatherForecast = (function() {
|
||||
return response.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
weatherData = data;
|
||||
weatherData = transformOpenMeteoData(data);
|
||||
|
||||
var currentTime = new Date().getTime();
|
||||
var closestIndex = 0;
|
||||
@ -203,6 +197,40 @@ var WeatherForecast = (function() {
|
||||
});
|
||||
}
|
||||
|
||||
function transformOpenMeteoData(data) {
|
||||
var transformed = {
|
||||
ts: [],
|
||||
'temp-surface': [],
|
||||
'wind-speed': [],
|
||||
'wind-direction': [],
|
||||
'precipitation': [],
|
||||
'rh-surface': [],
|
||||
'pressure-surface': [],
|
||||
'gust-surface': []
|
||||
};
|
||||
|
||||
if (!data.hourly || !data.hourly.time) {
|
||||
return transformed;
|
||||
}
|
||||
|
||||
for (var i = 0; i < data.hourly.time.length; i++) {
|
||||
transformed.ts.push(new Date(data.hourly.time[i]).getTime());
|
||||
|
||||
transformed['temp-surface'].push(data.hourly.temperature_2m[i]);
|
||||
|
||||
var windSpeedMs = data.hourly.wind_speed_10m[i] ? data.hourly.wind_speed_10m[i] / 3.6 : null;
|
||||
transformed['wind-speed'].push(windSpeedMs);
|
||||
transformed['wind-direction'].push(data.hourly.wind_direction_10m[i]);
|
||||
transformed['precipitation'].push(data.hourly.precipitation[i]);
|
||||
transformed['rh-surface'].push(data.hourly.relative_humidity_2m[i]);
|
||||
transformed['pressure-surface'].push(data.hourly.surface_pressure[i]);
|
||||
var gustMs = data.hourly.wind_gusts_10m[i] ? data.hourly.wind_gusts_10m[i] / 3.6 : null;
|
||||
transformed['gust-surface'].push(gustMs);
|
||||
}
|
||||
|
||||
return transformed;
|
||||
}
|
||||
|
||||
function displayCurrentForecast() {
|
||||
if (!weatherData || !weatherData.ts || weatherData.ts.length === 0) {
|
||||
displayWeatherError('无可用的天气预测数据');
|
||||
@ -213,22 +241,22 @@ var WeatherForecast = (function() {
|
||||
var forecastHtml = '<div class="weather-forecast-item"><div class="weather-param-grid">';
|
||||
|
||||
if (weatherData['temp-surface'] && weatherData['temp-surface'][i] !== null) {
|
||||
var temp = (weatherData['temp-surface'][i] - 273.15).toFixed(1);
|
||||
var temp = weatherData['temp-surface'][i].toFixed(1);
|
||||
forecastHtml += createWeatherParam('温度', temp + '°C');
|
||||
}
|
||||
|
||||
if (weatherData['wind_u-surface'] && weatherData['wind_v-surface'] &&
|
||||
weatherData['wind_u-surface'][i] !== null && weatherData['wind_v-surface'][i] !== null) {
|
||||
var windU = weatherData['wind_u-surface'][i];
|
||||
var windV = weatherData['wind_v-surface'][i];
|
||||
var windSpeed = Math.sqrt(windU * windU + windV * windV).toFixed(1);
|
||||
var windDir = getWindDirection(windU, windV);
|
||||
if (weatherData['wind-speed'] && weatherData['wind-speed'][i] !== null) {
|
||||
var windSpeed = weatherData['wind-speed'][i].toFixed(1);
|
||||
forecastHtml += createWeatherParam('风速', windSpeed + ' m/s');
|
||||
}
|
||||
|
||||
if (weatherData['wind-direction'] && weatherData['wind-direction'][i] !== null) {
|
||||
var windDir = getWindDirectionFromDegrees(weatherData['wind-direction'][i]);
|
||||
forecastHtml += createWeatherParam('风向', windDir);
|
||||
}
|
||||
|
||||
if (weatherData['past3hprecip-surface'] && weatherData['past3hprecip-surface'][i] !== null) {
|
||||
var precip = weatherData['past3hprecip-surface'][i].toFixed(1);
|
||||
if (weatherData['precipitation'] && weatherData['precipitation'][i] !== null) {
|
||||
var precip = weatherData['precipitation'][i].toFixed(1);
|
||||
forecastHtml += createWeatherParam('降水', precip + ' mm');
|
||||
}
|
||||
|
||||
@ -238,7 +266,7 @@ var WeatherForecast = (function() {
|
||||
}
|
||||
|
||||
if (weatherData['pressure-surface'] && weatherData['pressure-surface'][i] !== null) {
|
||||
var pressure = (weatherData['pressure-surface'][i] / 100).toFixed(0);
|
||||
var pressure = weatherData['pressure-surface'][i].toFixed(0);
|
||||
forecastHtml += createWeatherParam('气压', pressure + ' hPa');
|
||||
}
|
||||
|
||||
@ -291,6 +319,14 @@ var WeatherForecast = (function() {
|
||||
return directions[index];
|
||||
}
|
||||
|
||||
function getWindDirectionFromDegrees(degrees) {
|
||||
if (degrees === null || degrees === undefined) return '无风';
|
||||
|
||||
var directions = ['北', '东北', '东', '东南', '南', '西南', '西', '西北'];
|
||||
var index = Math.round(degrees / 45) % 8;
|
||||
return directions[index];
|
||||
}
|
||||
|
||||
function isEnabled() {
|
||||
return weatherEnabled;
|
||||
}
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
<!--由于需要一些特定的页面效果,
|
||||
导致无法使用layui提供的样式,所以这里我还是自己写css,页面主要的 js 在 static/js/device-overview 库中,它是通过暴露方法让main JS 获得方法的-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
@ -12,6 +15,7 @@
|
||||
<link rel="stylesheet" href="../css/public.css" media="all">
|
||||
<link rel="stylesheet" href="../css/ol.css">
|
||||
<!-- 谷歌地图的API接口,这个访问没有天地图的次数多,所以这边限制只有管理员能使用 -->
|
||||
<!-- 账户是 fengyarnom@gmail.com-->
|
||||
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyBEBxB5n9vikPHbfNMzo_dzneXJi77YqjE" async defer></script>
|
||||
<style>
|
||||
.top-panel {
|
||||
@ -522,11 +526,9 @@
|
||||
|
||||
/* 测量状态指示器样式 */
|
||||
#measureStatus {
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
border: 1px solid rgba(72, 187, 120, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 6px 12px;
|
||||
box-shadow: 0 2px 8px rgba(72, 187, 120, 0.2);
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
@ -537,6 +539,20 @@
|
||||
min-width: 24px;
|
||||
}
|
||||
|
||||
#filterStatus {
|
||||
border: 1px solid rgba(26, 160, 148, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 6px 12px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
#filterStatus .toolbar-btn {
|
||||
padding: 2px 6px;
|
||||
height: 24px;
|
||||
min-width: 24px;
|
||||
}
|
||||
|
||||
.weather-forecast-card {
|
||||
position: absolute;
|
||||
top: 55%;
|
||||
@ -788,6 +804,14 @@
|
||||
justify-content: center;
|
||||
padding: 6px 0px;
|
||||
}
|
||||
|
||||
#filterStatus {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 6px 0px;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -847,7 +871,7 @@
|
||||
<div id="weatherForecastCard" class="weather-forecast-card" style="display: none;" th:if="${role=='SUPER_ADMIN'}">
|
||||
<div class="weather-card-header">
|
||||
<h3 class="weather-card-title">
|
||||
Windy 天气预测
|
||||
Open-Meteo 天气预测
|
||||
</h3>
|
||||
<button class="weather-close-btn" onclick="closeWeatherCard()">×</button>
|
||||
</div>
|
||||
@ -892,7 +916,7 @@
|
||||
<div class="toolbar-divider"></div>
|
||||
|
||||
<div class="toolbar-item">
|
||||
<input type="text" id="deviceSearchNew" class="toolbar-input" placeholder="搜索设备编号">
|
||||
<input type="text" id="deviceSearchNew" class="toolbar-input" placeholder="设备编号">
|
||||
<button class="toolbar-btn" onclick="searchDeviceNew()">搜索</button>
|
||||
</div>
|
||||
|
||||
@ -931,20 +955,23 @@
|
||||
</div>
|
||||
<div class="dropdown-divider"></div>
|
||||
<div class="dropdown-group">
|
||||
<div class="dropdown-group-title">测量工具</div>
|
||||
<div class="dropdown-group-title">定位工具</div>
|
||||
<div class="dropdown-item" onclick="toggleMeasureDistance()">
|
||||
测距
|
||||
</div>
|
||||
<div class="dropdown-item" onclick="clearMeasure()">
|
||||
清除测距
|
||||
</div>
|
||||
<div class="dropdown-item" onclick="locateMyLocation()">
|
||||
当前位置
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown-divider"></div>
|
||||
<div class="dropdown-group" th:if="${role=='SUPER_ADMIN'}">
|
||||
<div class="dropdown-group-title">Windy 天气预测</div>
|
||||
<div class="dropdown-group-title">Open-Meteo 天气预测</div>
|
||||
<div class="dropdown-item">
|
||||
<label class="switch-label">
|
||||
<span>启用天气预测(需网络被代理)</span>
|
||||
<span>启用天气预测</span>
|
||||
<input type="checkbox" id="enableWeatherSwitch" onchange="toggleWeatherForecast()">
|
||||
<span class="switch-slider"></span>
|
||||
</label>
|
||||
@ -955,11 +982,14 @@
|
||||
</div>
|
||||
|
||||
<div id="measureStatus" class="toolbar-item" style="display: none;">
|
||||
<span style="color: #48bb78; font-weight: bold; font-size: 12px;">测量中</span>
|
||||
<span style="color: #1aa094; font-weight: bold; font-size: 12px;">测量中</span>
|
||||
<button class="toolbar-btn" onclick="finishMeasuring()" style="background: #f56565; margin-left: 6px;">
|
||||
<i class="layui-icon layui-icon-close" style="font-size: 12px;"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="filterStatus" class="toolbar-item" style="display: none;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -970,12 +1000,10 @@
|
||||
|
||||
</div>
|
||||
|
||||
<!-- JavaScript 依赖 -->
|
||||
<script src="../js/ol.js"></script>
|
||||
<script src="../lib/jquery-3.4.1/jquery-3.4.1.min.js" charset="utf-8"></script>
|
||||
<script src="../lib/layui-v2.6.3/layui.js" charset="utf-8"></script>
|
||||
|
||||
<!-- 设备总览模块化JavaScript文件 -->
|
||||
<script src="../js/device-overview-module/coordinate-utils.js"></script>
|
||||
<script src="../js/device-overview-module/map-layers.js"></script>
|
||||
<script src="../js/device-overview-module/device-markers.js"></script>
|
||||
@ -998,15 +1026,12 @@
|
||||
/*[/]*/
|
||||
];
|
||||
|
||||
// 获取用户角色
|
||||
var userRole = /*[[${role}]]*/ 'USER';
|
||||
|
||||
// 初始化设备总览模块
|
||||
DeviceOverview.init(deviceList, {
|
||||
role: userRole
|
||||
});
|
||||
|
||||
// 暴露全局函数供HTML调用
|
||||
window.onMapTypeChange = DeviceOverview.onMapTypeChange;
|
||||
window.searchDeviceNew = DeviceOverview.searchDeviceNew;
|
||||
window.onWarningFilterChange = DeviceOverview.onWarningFilterChange;
|
||||
@ -1024,6 +1049,7 @@
|
||||
window.locateDeviceOnMap = DeviceOverview.locateDeviceOnMap;
|
||||
window.locateDeviceDirectly = DeviceOverview.locateDeviceDirectly;
|
||||
window.switchMapType = DeviceOverview.switchMapType;
|
||||
window.locateMyLocation = DeviceOverview.locateMyLocation;
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
@ -144,7 +144,6 @@
|
||||
{field: 'group_id', title: '基本参数组', width: 60, sort: true},
|
||||
{field: 'calc_group_id', title: '解算参数组', width: 60, sort: true},
|
||||
{field: 'parentid', title: '基站编号', width: 80, sort: true},
|
||||
{field: 'parentid1', title: '备用基站', width: 80, sort: true},
|
||||
{field: 'tenantname', title: '所属组织', width: 120},
|
||||
{field: 'fwd_group_id', title: '推送组', width: 80},
|
||||
{field: 'fwd_group_id2', title: '推送2', width: 80},
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">设备号</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" name="s_deviceid" id="deviceid" autocomplete="off" class="layui-input">
|
||||
<input type="text" name="s_deviceid" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
@ -44,7 +44,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<button type="submit" class="layui-btn layui-btn-primary" lay-submit lay-filter="data-search-btn"><i class="layui-icon"></i> 搜 索</button>
|
||||
<button type="submit" class="layui-btn layui-btn-primary" lay-submit lay-filter="data-search-btn"><i class="layui-icon"></i> 搜 索</button>
|
||||
<button type="submit" class="layui-btn layui-btn-primary" lay-submit lay-filter="data-export-btn"><i class="layui-icon"></i>导出</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -52,43 +52,24 @@
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div class="layui-tab layui-tab-card" lay-filter="data-tab">
|
||||
<ul class="layui-tab-title">
|
||||
<li class="layui-this">数据表格</li>
|
||||
<li>姿态曲线</li>
|
||||
</ul>
|
||||
<div class="layui-tab-content">
|
||||
<div class="layui-tab-item layui-show">
|
||||
<table class="layui-hide" id="currentTableId" lay-filter="currentTableFilter"></table>
|
||||
</div>
|
||||
<div class="layui-tab-item">
|
||||
<div id="echarts-attitude" style="min-height:500px;padding: 10px"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<script src="../js/lay-module/echarts/echartsTheme.js" charset="utf-8"></script>
|
||||
<script src="../js/lay-module/echarts/echarts.js" charset="utf-8"></script>
|
||||
<script src="../lib/layui-v2.6.3/layui.js" charset="utf-8"></script>
|
||||
<script th:inline="javascript">
|
||||
layui.use(['form', 'table', 'laydate', 'element'], function () {
|
||||
layui.use(['form', 'table', 'laydate'], function () {
|
||||
var $ = layui.$;
|
||||
var form = layui.form,
|
||||
table = layui.table,
|
||||
laydate = layui.laydate,
|
||||
element = layui.element;
|
||||
|
||||
var echartsAttitude = echarts.init(document.getElementById('echarts-attitude'), 'walden');
|
||||
var chartDataLoaded = false;
|
||||
laydate = layui.laydate;
|
||||
|
||||
var cfg_cols = [
|
||||
{field: 'deviceid', title: '设备号', width: 100},
|
||||
{field: 'createtime', title: '上报时间', templet: "<div>{{layui.util.toDateString(d.createtime, 'yyyy-MM-dd HH:mm:ss')}}</div>"},
|
||||
{field: 'roll', title: 'roll', templet: "<div>{{d.roll.toFixed(2)}}</div>"},
|
||||
{field: 'pitch', title: 'pitch', templet: "<div>{{d.pitch.toFixed(2)}}</div>"},
|
||||
{field: 'yaw', title: 'yaw', templet: "<div>{{d.yaw.toFixed(2)}}</div>"},
|
||||
{field: 'roll', title: 'roll', templet: "<div>{{d.roll.toFixed(3)}}</div>"},
|
||||
{field: 'pitch', title: 'pitch', templet: "<div>{{d.pitch.toFixed(3)}}</div>"},
|
||||
{field: 'yaw', title: 'yaw', templet: "<div>{{d.yaw.toFixed(3)}}</div>"},
|
||||
{field: 'rssi', title: '信号强度'},
|
||||
{field: 'voltage', title: '电压(mV)'},
|
||||
{field: 'solarvoltage', title: '光伏电压(mV)'},
|
||||
@ -97,7 +78,9 @@
|
||||
{field: 'temperature', title: '温度(°C)'},
|
||||
{field: 'humidity', title: '湿度(%)'}
|
||||
];
|
||||
|
||||
/**
|
||||
* 初始化表单,要加上,不然刷新部分组件可能会不加载
|
||||
*/
|
||||
form.render();
|
||||
|
||||
laydate.render({
|
||||
@ -112,7 +95,7 @@
|
||||
table.render({
|
||||
elem: '#currentTableId',
|
||||
url: '/gnss/msg/status/list',
|
||||
toolbar: '#toolbarDemo',
|
||||
toolbar: '#toolbarDemo',//开启头部工具栏
|
||||
defaultToolbar: ['filter'],
|
||||
cols: [
|
||||
cfg_cols
|
||||
@ -123,9 +106,11 @@
|
||||
skin: 'line'
|
||||
});
|
||||
|
||||
// 监听搜索操作
|
||||
form.on('submit(data-search-btn)', function (data) {
|
||||
var result = JSON.stringify(data.field);
|
||||
|
||||
//执行搜索重载
|
||||
table.reload('currentTableId', {
|
||||
page: {
|
||||
curr: 1
|
||||
@ -138,6 +123,7 @@
|
||||
return false;
|
||||
});
|
||||
|
||||
// 监听导出操作
|
||||
form.on('submit(data-export-btn)', function (data) {
|
||||
var result = $('#searchFrm').serialize();
|
||||
var u = "/gnss/msg/status/export?" + result;
|
||||
@ -145,173 +131,6 @@
|
||||
return false;
|
||||
});
|
||||
|
||||
element.on('tab(data-tab)', function(data){
|
||||
if (data.index == 1) {
|
||||
echartsAttitude.resize();
|
||||
|
||||
if (!chartDataLoaded) {
|
||||
var deviceId = $('#deviceid').val();
|
||||
if (deviceId) {
|
||||
loadChartData(deviceId);
|
||||
} else {
|
||||
layer.msg('请先在搜索框中输入设备号');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function loadChartData(deviceId) {
|
||||
$.ajax({
|
||||
url: '/gnss/msg/status/device_data',
|
||||
type: 'GET',
|
||||
data: {
|
||||
deviceId: deviceId
|
||||
},
|
||||
success: function(res) {
|
||||
if (res.code === 0 && res.data) {
|
||||
showChart(res.data, res.originalData);
|
||||
chartDataLoaded = true;
|
||||
} else {
|
||||
layer.msg('获取数据失败');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
layer.msg('获取数据失败');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showChart(smoothedData, originalData) {
|
||||
var t = [];
|
||||
var roll = [];
|
||||
var pitch = [];
|
||||
var yaw = [];
|
||||
var smoothRoll = [];
|
||||
var smoothPitch = [];
|
||||
var smoothYaw = [];
|
||||
|
||||
for (var i = 0; i < originalData.length; i++) {
|
||||
t.push(originalData[i].createtime);
|
||||
roll.push(originalData[i].roll);
|
||||
pitch.push(originalData[i].pitch);
|
||||
yaw.push(originalData[i].yaw);
|
||||
}
|
||||
|
||||
for (var i = 0; i < smoothedData.length; i++) {
|
||||
smoothRoll.push(smoothedData[i].roll);
|
||||
smoothPitch.push(smoothedData[i].pitch);
|
||||
smoothYaw.push(smoothedData[i].yaw);
|
||||
}
|
||||
|
||||
var option = {
|
||||
title: {
|
||||
text: '姿态数据曲线'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
label: {
|
||||
backgroundColor: '#6a7985'
|
||||
}
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['Roll', 'Pitch', 'Yaw', '平滑Roll', '平滑Pitch', '平滑Yaw']
|
||||
},
|
||||
toolbox: {
|
||||
feature: {
|
||||
saveAsImage: {}
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: t,
|
||||
axisLabel: {
|
||||
formatter: function(value) {
|
||||
var date = new Date(value);
|
||||
return date.getFullYear() + '-' +
|
||||
(date.getMonth() + 1).toString().padStart(2, '0') + '-' +
|
||||
date.getDate().toString().padStart(2, '0') + ' ' +
|
||||
date.getHours().toString().padStart(2, '0') + ':' +
|
||||
date.getMinutes().toString().padStart(2, '0') + ':' +
|
||||
date.getSeconds().toString().padStart(2, '0');
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value'
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: 'Roll',
|
||||
type: 'line',
|
||||
data: roll,
|
||||
lineStyle: {
|
||||
width: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Pitch',
|
||||
type: 'line',
|
||||
data: pitch,
|
||||
lineStyle: {
|
||||
width: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Yaw',
|
||||
type: 'line',
|
||||
data: yaw,
|
||||
lineStyle: {
|
||||
width: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '平滑Roll',
|
||||
type: 'line',
|
||||
data: smoothRoll,
|
||||
lineStyle: {
|
||||
width: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '平滑Pitch',
|
||||
type: 'line',
|
||||
data: smoothPitch,
|
||||
lineStyle: {
|
||||
width: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '平滑Yaw',
|
||||
type: 'line',
|
||||
data: smoothYaw,
|
||||
lineStyle: {
|
||||
width: 2
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
echartsAttitude.setOption(option);
|
||||
|
||||
window.onresize = function() {
|
||||
echartsAttitude.resize();
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@ -53,16 +53,38 @@
|
||||
</fieldset>
|
||||
|
||||
<table class="layui-hide" id="forwardParaTableId" lay-filter="forwardParaTableFilter"></table>
|
||||
<script type="text/html" id="currentTableBar">
|
||||
<a class="layui-btn layui-btn-xs layui-btn-danger data-count-delete" lay-event="delete">删除</a>
|
||||
</script>
|
||||
<script type="text/html" id="toolbarTop">
|
||||
<div class="layui-btn-container" th:if="${role=='SUPER_ADMIN'}">
|
||||
<button class="layui-btn layui-btn-normal layui-btn-sm data-add-btn" lay-event="add">添加</button>
|
||||
</div>
|
||||
</script>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<script src="../lib/layui-v2.6.3/layui.js" charset="utf-8"></script>
|
||||
<script th:inline="none">
|
||||
<script th:inline="javascript">
|
||||
layui.use(['form', 'table'], function () {
|
||||
var $ = layui.$,
|
||||
form = layui.form,
|
||||
table = layui.table,
|
||||
laydate = layui.laydate;
|
||||
var cfg_cols = [
|
||||
{field: 'projectid', title: '项目号', sort: true},
|
||||
{field: 'deviceid', title: '设备号'},
|
||||
{field: 'createtime', title: '推送时间', templet: "<div>{{layui.util.toDateString(d.createtime, 'yyyy-MM-dd HH:mm:ss')}}</div>"},
|
||||
{field: 'starttime', title: '数据开始时间', templet: "<div>{{layui.util.toDateString(d.starttime, 'yyyy-MM-dd HH:mm:ss')}}</div>"},
|
||||
{field: 'endtime', title: '数据结束时间', templet: "<div>{{layui.util.toDateString(d.endtime, 'yyyy-MM-dd HH:mm:ss')}}</div>"},
|
||||
{field: 'fwd_group_id', title: '推送组'},
|
||||
{field: 'state', title: '状态',templet: '#stateTrans'},
|
||||
{title: '操作', toolbar: '#currentTableBar', fixed: "right", width: 120}
|
||||
];
|
||||
if([[${role}]] != "SUPER_ADMIN") {
|
||||
cfg_cols[7].hide = true;
|
||||
}
|
||||
|
||||
laydate.render({
|
||||
elem: '#ID-laydate-start-date1',
|
||||
type: 'datetime'
|
||||
@ -85,17 +107,11 @@
|
||||
table.render({
|
||||
elem: '#forwardParaTableId',
|
||||
url: '/fwd/resend_records',
|
||||
toolbar: '#toolbarTable',
|
||||
toolbar: '#toolbarTop',
|
||||
defaultToolbar: ['filter'],
|
||||
cols: [[
|
||||
{field: 'projectid', title: '项目号', sort: true},
|
||||
{field: 'deviceid', title: '设备号'},
|
||||
{field: 'createtime', title: '推送时间', templet: "<div>{{layui.util.toDateString(d.createtime, 'yyyy-MM-dd HH:mm:ss')}}</div>"},
|
||||
{field: 'starttime', title: '数据开始时间', templet: "<div>{{layui.util.toDateString(d.starttime, 'yyyy-MM-dd HH:mm:ss')}}</div>"},
|
||||
{field: 'endtime', title: '数据结束时间', templet: "<div>{{layui.util.toDateString(d.endtime, 'yyyy-MM-dd HH:mm:ss')}}</div>"},
|
||||
{field: 'fwd_group_id', title: '推送组'},
|
||||
{field: 'state', title: '状态',templet: '#stateTrans'}
|
||||
]],
|
||||
cols: [
|
||||
cfg_cols
|
||||
],
|
||||
limits: [10, 20, 50, 100, 150],
|
||||
limit: 10,
|
||||
page: true,
|
||||
@ -119,8 +135,50 @@
|
||||
return false;
|
||||
});
|
||||
|
||||
table.on('toolbar(forwardParaTableFilter)', function (obj) {
|
||||
if (obj.event === 'add') { // 监听添加操作
|
||||
var index = layer.open({
|
||||
title: '添加补传记录',
|
||||
type: 2,
|
||||
shade: 0.2,
|
||||
maxmin:true,
|
||||
shadeClose: true,
|
||||
area: ['100%', '100%'],
|
||||
content: '../page/table/resend_record_add'
|
||||
});
|
||||
$(window).on("resize", function () {
|
||||
layer.full(index);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
table.on('tool(forwardParaTableFilter)', function (obj) {
|
||||
var data = obj.data;
|
||||
if (obj.event === 'delete') {
|
||||
layer.confirm('确定删除?', function(index){
|
||||
$.ajax({
|
||||
type:"POST",
|
||||
url:"/fwd/resend_records/delete",
|
||||
data:{
|
||||
'del_id':data.id
|
||||
},
|
||||
success: function (data) {
|
||||
table.reload('forwardParaTableId');
|
||||
},
|
||||
error: function () {
|
||||
console.log("ajax error");
|
||||
}
|
||||
});
|
||||
layer.close(index);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
function onUpdated(){
|
||||
layui.table.reload('forwardParaTableId');
|
||||
}
|
||||
</script>
|
||||
|
||||
<script type="text/html" id="stateTrans">
|
||||
@ -129,9 +187,9 @@
|
||||
{{# } else if(d.state == 1){ }}
|
||||
<span class="layui-badge layui-bg-red">推送失败</span>
|
||||
{{# } else if(d.state == 2){ }}
|
||||
<span class="layui-badge layui-bg-red">断点补传</span>
|
||||
<span class="layui-badge layui-bg-orange">断点补传</span>
|
||||
{{# } else { }}
|
||||
<span class="layui-badge layui-bg-orange">推送中</span>
|
||||
<span class="layui-badge layui-bg-blue">推送中</span>
|
||||
{{# } }}
|
||||
</script>
|
||||
|
||||
|
||||
@ -89,12 +89,6 @@
|
||||
<input type="number" name="parentid" id="parentid" th:field="*{parentid}" placeholder="请输入关联基准站编号" value="" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline" >
|
||||
<label class="layui-form-label">备选基站</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="number" name="parentid1" id="parentid1" th:field="*{parentid1}" placeholder="请输入备选基准站编号" value="" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item" id="ecef_div">
|
||||
<div class="layui-inline">
|
||||
@ -236,8 +230,9 @@
|
||||
<hr th:if="${role=='SUPER_ADMIN'}">
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block" style="float:right" >
|
||||
<button class="layui-btn layui-btn-normal" lay-submit lay-filter="saveBtn">确认保存</button>
|
||||
<button class="layui-btn layui-btn-normal" lay-submit lay-filter="saveBtn">保存</button>
|
||||
<button class="layui-btn layui-btn-normal" lay-submit lay-filter="initLocBtn">取初值</button>
|
||||
<button class="layui-btn layui-btn-normal" lay-submit lay-filter="contBtn">续数据</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -292,6 +287,47 @@
|
||||
return false;
|
||||
});
|
||||
|
||||
form.on('submit(contBtn)', function (data) {
|
||||
// 弹出输入框
|
||||
layer.prompt({
|
||||
title: '请输入接续的设备编号和日期时间,用逗号隔开', // 弹窗标题
|
||||
formType: 0, // 0-文本输入框
|
||||
maxlength: 50, // 最大输入长度
|
||||
value: '', // 初始值
|
||||
btn: ['确定', '取消'] // 按钮组
|
||||
}, function(value, index) { // 确定按钮回调
|
||||
// 处理输入结果
|
||||
if(value) {
|
||||
$.ajax({
|
||||
type:"POST",
|
||||
url:"/gnss/device/cont_loc",
|
||||
data:{
|
||||
'deviceid':$('#deviceid').val(),
|
||||
'createtime':value
|
||||
},
|
||||
success: function (result) {
|
||||
if(result.code == 0) {
|
||||
$('#ipose').val(result.ipose);
|
||||
$('#iposn').val(result.iposn);
|
||||
$('#iposd').val(result.iposd);
|
||||
}
|
||||
else{
|
||||
layer.alert(result.msg);
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
console.log("ajax error");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
layer.msg('输入内容不能为空!', {icon: 2});
|
||||
}
|
||||
// 关闭当前弹窗
|
||||
layer.close(index);
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
form.on('select(device_type)', function (data) {
|
||||
setEcefEditor();
|
||||
});
|
||||
@ -326,6 +362,7 @@
|
||||
}
|
||||
layui.form.render();
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
@ -0,0 +1,136 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>补传数据</title>
|
||||
<meta name="renderer" content="webkit">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<link rel="stylesheet" href="../../lib/layui-v2.6.3/css/layui.css" media="all">
|
||||
<link rel="stylesheet" href="../../css/public.css" media="all">
|
||||
<style>
|
||||
body {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="layui-form layuimini-form">
|
||||
<input type="hidden" name="id" id="id">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">所属部门</label>
|
||||
<div class="layui-input-inline">
|
||||
<select name="tenantid" id="tenantid" lay-search="" lay-filter="tenant">
|
||||
<option value="">全部</option>
|
||||
<option th:each="item : ${tenant_list}" th:text="${item.name}" th:value="${item.id}"></option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">所属项目</label>
|
||||
<div class="layui-input-inline">
|
||||
<select name="project_id" id="projectid" lay-search="" lay-filter="project">
|
||||
<option value="">全部</option>
|
||||
<option th:each="item : ${project_list}" th:text="${item.project_id}" th:value="${item.project_id}"></option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">设备编号</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" name="deviceid" id="deviceid" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label required">推送组</label>
|
||||
<div class="layui-input-inline">
|
||||
<select name="fwd_group_id" id="fwd_group_id" lay-verify="required" lay-reqtext="不能为空" lay-search="">
|
||||
<option th:each="item : ${gnss_group_fwd_list}" th:text="${item.name}" th:value="${item.name}"></option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label required">时间段</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" name="starttime" autocomplete="off" lay-verify="required" lay-reqtext="不能为空" id="ID-laydate-start-date" class="layui-input" placeholder="开始日期">
|
||||
</div>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" name="endtime" autocomplete="off" lay-verify="required" lay-reqtext="不能为空" id="ID-laydate-end-date" class="layui-input" placeholder="结束日期">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<button class="layui-btn layui-btn-normal" lay-submit lay-filter="saveBtn">确认保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="../../lib/layui-v2.6.3/layui.js" charset="utf-8"></script>
|
||||
<script>
|
||||
layui.use(['form'], function () {
|
||||
var form = layui.form,
|
||||
$ = layui.$,
|
||||
laydate = layui.laydate;
|
||||
var iframeIndex = parent.layer.getFrameIndex(window.name);
|
||||
|
||||
laydate.render({
|
||||
elem: '#ID-laydate-start-date',
|
||||
type: 'datetime'
|
||||
});
|
||||
laydate.render({
|
||||
elem: '#ID-laydate-end-date',
|
||||
type: 'datetime'
|
||||
});
|
||||
// 所属部门下拉框改变,修改项目下拉框
|
||||
form.on('select(tenant)', function (data) {
|
||||
console.log(data.value);
|
||||
$.ajax({
|
||||
type:"GET",
|
||||
url:"/gnss/device/q_project",
|
||||
data:{
|
||||
'tenantid':data.value
|
||||
},
|
||||
success: function(result) {
|
||||
$('#projectid').empty();
|
||||
$('#projectid').append(new Option("全部", ""));
|
||||
$('#deviceid').empty();
|
||||
//console.log(result);
|
||||
$.each(result.data, function (index, item) {
|
||||
$('#projectid')
|
||||
.append(new Option(item.project_id, item.project_id));
|
||||
});
|
||||
layui.form.render("select");
|
||||
},
|
||||
error: function () {
|
||||
console.log("ajax error");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
//监听提交
|
||||
form.on('submit(saveBtn)', function (data) {
|
||||
$.ajax({
|
||||
type:"POST",
|
||||
url:"/fwd/resend_records/add",
|
||||
contentType: "application/json;charset=UTF-8",
|
||||
data: JSON.stringify(data.field),
|
||||
success: function (result) {
|
||||
parent.onUpdated();
|
||||
parent.layer.close(iframeIndex);
|
||||
},
|
||||
error: function () {
|
||||
console.log("ajax error");
|
||||
parent.layer.close(iframeIndex);
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@ -3,12 +3,10 @@ package com.imdroid.ntripproxy.service;
|
||||
public class Ntrip2Channels {
|
||||
final private String localHost="127.0.0.1";
|
||||
final private int localPort=9903;
|
||||
// 将远程主机改为本地,端口改为12000
|
||||
final private String remoteHost="127.0.0.1";
|
||||
final private String remoteHost="8.134.185.53";
|
||||
//final private String remoteHost="100.91.37.6";
|
||||
//final private String remoteHost="47.107.50.52";
|
||||
//final private String remoteHost="8.134.185.53";
|
||||
final private int remotePort=12000;
|
||||
final private int remotePort=9903;
|
||||
|
||||
public static final Ntrip2Channels INSTANCE = new Ntrip2Channels();
|
||||
|
||||
|
||||
@ -1,185 +0,0 @@
|
||||
package com.imdroid.ntripproxy.service;
|
||||
|
||||
import com.imdroid.common.util.DataTypeUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.*;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.SelectionKey;
|
||||
import java.nio.channels.Selector;
|
||||
import java.nio.channels.ServerSocketChannel;
|
||||
import java.nio.channels.SocketChannel;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/**
|
||||
* RTCM数据过滤服务器
|
||||
* 监听UDP 12000端口,接收RTCM数据
|
||||
* 开启TCP 12002端口,转发指定deviceId的RTCM数据
|
||||
*/
|
||||
@Component
|
||||
public class RtcmFilterServer implements ApplicationRunner {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(RtcmFilterServer.class);
|
||||
|
||||
// 配置参数
|
||||
private static final int UDP_PORT = 12000;
|
||||
private static final int TCP_PORT = 12002;
|
||||
private static final String TARGET_DEVICE_ID = "3530795";
|
||||
private static final int BUFFER_SIZE = 4096;
|
||||
|
||||
// TCP客户端连接列表
|
||||
private final List<SocketChannel> tcpClients = new CopyOnWriteArrayList<>();
|
||||
|
||||
// 线程池
|
||||
private final ExecutorService executorService = Executors.newFixedThreadPool(2);
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) {
|
||||
// 启动UDP监听服务
|
||||
executorService.submit(this::startUdpServer);
|
||||
|
||||
// 启动TCP服务器
|
||||
executorService.submit(this::startTcpServer);
|
||||
|
||||
logger.info("RTCM过滤服务已启动 - UDP监听端口:{}, TCP服务端口:{}, 目标设备ID:{}",
|
||||
UDP_PORT, TCP_PORT, TARGET_DEVICE_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动UDP服务器监听12000端口
|
||||
*/
|
||||
private void startUdpServer() {
|
||||
try (DatagramSocket socket = new DatagramSocket(UDP_PORT)) {
|
||||
logger.info("UDP服务已启动,监听端口: {}", UDP_PORT);
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
|
||||
while (true) {
|
||||
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
|
||||
socket.receive(packet);
|
||||
|
||||
// 解析数据包
|
||||
byte[] data = new byte[packet.getLength()];
|
||||
System.arraycopy(packet.getData(), packet.getOffset(), data, 0, packet.getLength());
|
||||
|
||||
// 提取设备ID
|
||||
String deviceId = extractDeviceId(data);
|
||||
|
||||
// 如果是目标设备ID,则转发到TCP客户端
|
||||
if (TARGET_DEVICE_ID.equals(deviceId)) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("接收到目标设备 {} 的RTCM数据,长度: {}", deviceId, data.length);
|
||||
}
|
||||
forwardToTcpClients(data);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error("UDP服务异常: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动TCP服务器监听12002端口
|
||||
*/
|
||||
private void startTcpServer() {
|
||||
try {
|
||||
Selector selector = Selector.open();
|
||||
ServerSocketChannel serverChannel = ServerSocketChannel.open();
|
||||
serverChannel.configureBlocking(false);
|
||||
serverChannel.socket().bind(new InetSocketAddress(TCP_PORT));
|
||||
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
|
||||
|
||||
logger.info("TCP服务已启动,监听端口: {}", TCP_PORT);
|
||||
|
||||
while (true) {
|
||||
selector.select();
|
||||
Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
|
||||
|
||||
while (keys.hasNext()) {
|
||||
SelectionKey key = keys.next();
|
||||
keys.remove();
|
||||
|
||||
if (!key.isValid()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key.isAcceptable()) {
|
||||
acceptConnection(selector, serverChannel);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error("TCP服务异常: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 接受新的TCP客户端连接
|
||||
*/
|
||||
private void acceptConnection(Selector selector, ServerSocketChannel serverChannel) throws IOException {
|
||||
SocketChannel clientChannel = serverChannel.accept();
|
||||
clientChannel.configureBlocking(false);
|
||||
clientChannel.register(selector, SelectionKey.OP_READ);
|
||||
|
||||
tcpClients.add(clientChannel);
|
||||
logger.info("新的TCP客户端已连接: {}", clientChannel.getRemoteAddress());
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数据包中提取设备ID
|
||||
*/
|
||||
private String extractDeviceId(byte[] data) {
|
||||
// 按照NtripMessage格式解析,设备ID从第4字节开始,4字节无符号整型
|
||||
if (data.length >= 8) {
|
||||
ByteBuffer buffer = ByteBuffer.wrap(data, 4, 4);
|
||||
long deviceId = buffer.getInt() & 0xFFFFFFFFL; // 转为无符号整型
|
||||
return String.valueOf(deviceId);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 将数据转发到所有TCP客户端
|
||||
*/
|
||||
private void forwardToTcpClients(byte[] data) {
|
||||
if (tcpClients.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<SocketChannel> disconnectedClients = new ArrayList<>();
|
||||
|
||||
for (SocketChannel client : tcpClients) {
|
||||
try {
|
||||
if (client.isOpen()) {
|
||||
ByteBuffer buffer = ByteBuffer.wrap(data);
|
||||
client.write(buffer);
|
||||
} else {
|
||||
disconnectedClients.add(client);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.warn("向TCP客户端发送数据失败: {}", e.getMessage());
|
||||
disconnectedClients.add(client);
|
||||
}
|
||||
}
|
||||
|
||||
// 移除断开连接的客户端
|
||||
for (SocketChannel client : disconnectedClients) {
|
||||
tcpClients.remove(client);
|
||||
try {
|
||||
client.close();
|
||||
logger.info("已关闭断开连接的TCP客户端");
|
||||
} catch (IOException e) {
|
||||
logger.error("关闭TCP客户端异常: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user