Compare commits

...

89 Commits

Author SHA1 Message Date
01427e230e feat: rtksrv v1
# Conflicts:
#	sec-ntrip-proxy/src/main/java/com/imdroid/ntripproxy/service/Ntrip2Channels.java
2025-07-28 17:52:55 +08:00
b6629696d6 Merge pull request 'fix: 删除调试日志,修复f9p生产1005的错误' (#18) from feature/backup-basestation into develop
Reviewed-on: #18
2025-07-09 00:31:10 -07:00
fengyarnom
c8678bc0a0 fix: 删除调试日志,修复f9p生产1005的错误 2025-07-09 15:31:25 +08:00
9d54bdcb1a Merge remote-tracking branch 'gitea/feature/backup-basestation' into develop 2025-07-08 18:02:54 +08:00
bc89ccca79 fix: 修正因 stash 添加的字符错误 2025-07-08 17:46:08 +08:00
efe8787d53 fix: 修正因 stash 添加的字符错误 2025-07-08 17:41:33 +08:00
zms
e7c6b36f6d 完善基站切换 2025-07-08 17:21:55 +08:00
zms
9913b8a732 完善基站切换 2025-07-08 17:20:41 +08:00
4ceac73519 Merge pull request '修复切换基站bug' (#17) from feature/backup-basestation into develop
Reviewed-on: #17
2025-07-07 19:40:27 -07:00
702c6a87f4 feat: 优化页面效果 2025-07-04 22:00:01 +08:00
3e99132ad7 feat: 优化页面效果 2025-07-04 21:56:54 +08:00
efaf5fe8f4 feat: 优化页面效果 2025-07-04 21:48:26 +08:00
6ecc833a2c feat: 优化页面效果 2025-07-04 21:09:34 +08:00
e4ec7a13eb feat: 优化页面效果 2025-07-04 19:48:37 +08:00
94b91c1290 feat: 优化页面效果 2025-07-04 17:53:24 +08:00
0d88e161d9 feat: 优化页面效果 2025-07-04 17:44:11 +08:00
d694ea8835 feat: 优化页面效果 2025-07-04 16:51:16 +08:00
c79c868c41 feat: 优化页面效果 2025-07-04 16:39:48 +08:00
1c4633881d feat: 优化页面效果 2025-07-04 15:00:31 +08:00
5fef8eec51 feat: 优化页面效果 2025-07-04 14:12:46 +08:00
3cbeba9832 feat: 适配手机端 2025-07-04 13:49:13 +08:00
c0d25ef0e7 feat: 适配手机端 2025-07-04 13:32:58 +08:00
c0ba5fc4b9 feat: 适配手机端 2025-07-04 13:29:50 +08:00
71c56c6eb3 feat: 新增谷歌地图 2025-07-04 12:15:03 +08:00
5f83223d9a feat: 优化页面 2025-07-04 11:46:02 +08:00
1336bb16e6 feat: 新增测距和比例尺 2025-07-04 11:11:35 +08:00
a70852fdc9 feat: 新增测距 2025-07-04 09:37:25 +08:00
4425fcd7e6 feat: 优化页面 2025-07-04 09:09:23 +08:00
a4cf6130c3 feat: 优化页面 2025-07-03 14:49:13 +08:00
zms
e256cd5e68 修复切换基站bug 2025-07-01 09:45:58 +08:00
fengyarnom
3a503701af feat: 新增姿态平滑 2025-06-30 16:02:33 +08:00
fengyarnom
cbb1a22161 merge: 合并主分支 2025-06-30 14:48:50 +08:00
fengyarnom
281c11b1e7 feat: 新增测距功能
- 新增测距功能
- 优化 device_overview.html 页面的JS代码
2025-06-30 14:44:41 +08:00
fengyarnom
0d21c8c6f3 feature: 优先使用地形图 2025-06-30 10:18:17 +08:00
fengyarnom
16c659a42d feature: 新增地形图 2025-06-30 10:14:31 +08:00
weidong
d62777bc91 1、优化推送逻辑 2025-06-28 14:00:17 +08:00
15b14a94f6 Merge pull request '修改使用备用基站的ID' (#16) from feature/backup-basestation into develop
Reviewed-on: #16
2025-06-26 20:55:55 -07:00
zms
a580eb204a 修改使用备用基站的ID 2025-06-27 11:53:19 +08:00
weidong
eed05660ab 1、如果博通基站D341统计为0,则断电重启GNSS模块 2025-06-25 10:04:43 +08:00
fengyarnom
9f5fa31bb1 fix: 修改一些BUG 2025-06-25 09:20:36 +08:00
fengyarnom
40813356a6 fix: 修改一些BUG 2025-06-24 19:34:21 +08:00
8115e54fcc Merge pull request '添加备用基站' (#15) from feature/backup-basestation into develop
Reviewed-on: #15
2025-06-23 02:51:35 -07:00
weidong
689f6cf360 1、连续无固定解改为每秒发一次冷启动,连续发10次
2、MQTT推送优化
2025-06-23 14:43:21 +08:00
weidong
30f8b822e8 1、连续无固定解改为每秒发一次冷启动,连续发10次 2025-06-23 14:36:01 +08:00
weidong
af1517b350 1、修改中南院MQTT日期时间格式 2025-06-21 08:26:47 +08:00
zms
e272988ada 添加备用基站 2025-06-20 19:41:54 +08:00
weidong
d6d3213f72 1、优化MQTT初始连接 2025-06-20 19:12:13 +08:00
fengyarnom
137603cddb fix: 新增ol库 2025-06-20 19:00:04 +08:00
weidong
c7b1438d02 1、优化MQTT初始连接 2025-06-20 18:58:56 +08:00
feedaaabb4 revert: 1293f89 2025-06-20 18:51:12 +08:00
weidong
57de48e899 1、中南院MQTT推送,时间到毫秒 2025-06-20 16:20:40 +08:00
weidong
108bf78bfe 1、中南院MQTT推送,时间到毫秒 2025-06-20 16:20:18 +08:00
weidong
d53d2510df 1、增加中南院MQTT推送 2025-06-20 10:30:41 +08:00
fengyarnom
4a394849a1 Merge branch 'feature/beidou' into develop 2025-06-20 10:14:10 +08:00
fengyarnom
425e4b158e fix: 修复备选基站编号显示问题 2025-06-20 10:13:12 +08:00
fengyarnom
b6418ffeae feature: 添加备选基站 2025-06-20 10:05:58 +08:00
zms
1293f891db feature: 添加备用基站 2025-06-20 17:43:59 +08:00
fengyarnom
aac2bcbfd5 fix: 新增一些新的地图源 2025-06-20 09:56:52 +08:00
weidong
6495661e1f 1、增加中南院MQTT推送 2025-06-20 09:28:10 +08:00
weidong
e33ff7aa71 1、推送打印改为debug级别
2、增加中南院MQTT推送
2025-06-20 09:09:46 +08:00
weidong
528003e39e 1、F9P固定1s推送 2025-06-15 22:14:03 +08:00
weidong
0d3e977f76 1、F9P固定1s推送 2025-06-15 21:33:44 +08:00
weidong
66f283a8a4 1、增加版本管理
2、只有固定解大于100个时,才允许间隔推送
2025-06-15 21:25:33 +08:00
fengyarnom
705b004d62 fix: 地图源加载失败可自动切换到备选地图源 2025-06-13 22:59:34 +08:00
fengyarnom
4ced686ed2 feat: 更新本地 JS 库 2025-06-13 18:45:36 +08:00
fengyarnom
3e15355609 Merge remote-tracking branch 'gitea/develop' into feature/beidou 2025-06-13 18:15:55 +08:00
fengyarnom
499037fca6 Merge remote-tracking branch 'gitea/develop' into develop 2025-06-13 18:09:23 +08:00
fengyarnom
4570a8f9b3 fix: 修改转发D300的时间 2025-06-13 18:08:51 +08:00
fengyarnom
702b83ed25 fix: 适配手机显示 2025-06-13 18:00:08 +08:00
694783cc23 Merge pull request 'develop' (#13) from develop into feature/beidou
Reviewed-on: #13
2025-06-13 01:27:55 -07:00
b7dcfc40ac Merge pull request 'feature/ui' (#12) from feature/ui into develop
Reviewed-on: #12
2025-06-13 01:27:30 -07:00
fengyarnom
316c89e8f3 feat: 新增聚合显示功能 2025-06-13 16:55:46 +08:00
fengyarnom
81032c8d9d fix: 缩小地图定位图标 2025-06-13 11:11:54 +08:00
fengyarnom
7224ece179 Merge branch 'master' into feature/beidou 2025-06-12 18:11:01 +08:00
weidong
1e78245b5f 1、自动将6和7开头的设备设置为带电池 2025-06-12 18:07:09 +08:00
fengyarnom
0b3ff272c5 feat: 更新地图与设备管理功能
主要功能:
- 地图设备定位:支持输入设备号直接定位到地图位置
- 地图底图更换:替换为奥维地图源,使用天地图和高德地图
- 自动更新位置:用户位置10分钟自动更新,便于现场维护
- 统计优化:首页显示未推送设备统计
- SIM卡管理增强:新增一些增强的功能
- 命令行优化:常用指令支持置底操作
- F9P基站兼容博通测站:为F9P基站添加RTCM1005
2025-06-11 17:45:43 +08:00
fengyarnom
7d6d9b29b2 fix: 注释日志打印 2025-06-11 17:19:11 +08:00
fengyarnom
427b38c8f1 fix: 修正一下bug 2025-06-11 16:56:54 +08:00
0b9bda229f Merge pull request 'fix: 修正一下bug' (#10) from feature/ui into develop
Reviewed-on: #10
2025-06-11 01:28:09 -07:00
a220fbb1ef Merge pull request 'feat: 增加按imei查询' (#9) from feature/add_imei_search into develop
Reviewed-on: #9
2025-06-11 01:13:20 -07:00
weidong
5deeb3c70b 1、增加电池参数配置
2、增加版本管理页面
2025-06-10 10:38:02 +08:00
weidong
0864ca38c9 1、增加工作周期参数合法性检查 2025-06-01 10:42:21 +08:00
weidong
48f32c8d2b 1、连续无固定解,发F9P断电 2025-05-30 19:14:43 +08:00
weidong
83528f0cc4 1、连续无固定解,发F9P冷启动改发初始化 2025-05-29 20:28:58 +08:00
weidong
c9337e0777 1、如果光伏板电压低于电池电压3V,强制把充电电流和积分电量改为0显示 2025-05-28 11:59:50 +08:00
weidong
c82f87c3dc 1、回退参数 2025-05-27 08:40:34 +08:00
weidong
f90c61c676 1、回退参数 2025-05-26 20:09:24 +08:00
weidong
65b02c3e90 1、回退参数 2025-05-26 09:40:49 +08:00
weidong
cdd5599ea9 1、推送消息统一等20ms间隔 2025-05-25 21:13:25 +08:00
67 changed files with 6965 additions and 1094 deletions

View File

@ -18,6 +18,7 @@
<module>sec-ntrip-proxy</module> <module>sec-ntrip-proxy</module>
<module>sec-exapi</module> <module>sec-exapi</module>
<module>sec-beidou-ehm</module> <module>sec-beidou-ehm</module>
<module>sec-vermgr</module>
</modules> </modules>
<properties> <properties>

View File

@ -28,6 +28,7 @@ beidou-fwd 9906 解算结果推送
ntrip-proxy 9910 11001(外) ntrip服务器代理 ntrip-proxy 9910 11001(外) ntrip服务器代理
beidou-exapi 9908(外) API beidou-exapi 9908(外) API
beidou-ehm 9912 健康检查、SIM卡检查 beidou-ehm 9912 健康检查、SIM卡检查
vermgr 9914 9916(外) 版本管理服务
2024-9 2024-9
算法: 算法:
@ -55,6 +56,17 @@ beidou-ehm 9912 健康检查、SIM卡检查
2健康检查增加连续无有效解的时长之和统计 2健康检查增加连续无有效解的时长之和统计
3批量升级 3批量升级
a在设备页面增加勾选框和“准备升级”、“完成升级”按钮 a在系统管理里增加一页固件升级页面包括固件列表和设备列表
b点击“准备升级”把模式改为“待升级”同时发连接版本服务器指令 1固件列表固件名、固件大小、创建时间。每次升级只能单选一个固件
c点击“完成升级”把模式改为“正常” 2设备列表所属组织、设备号、项目、桩号、版本号、使用状态正常、维护、停用、当前状态工作、休眠、离线、最近一次版本升级时间
3设备可多选筛选条件所属组织、设备号、项目、桩号、版本号
b升级过程
1WEB服务选择固件->筛选设备->勾选设备
2WEB服务点击“升级”系统检查所勾选的设备批次与所选固件是否一致如果有不一致弹出“固件不匹配”提示窗口结束升级
3WEB服务向版本服务发升级命令UpgradeCmd(deviceList)版本服务如果应答UpgradeAck则把待升级的设备改为“维护”状态发连接服务器指令升级按钮变灰否则提示“版本服务未启动”的提示结束升级
4版本服务当收到WEB服务发来的升级命令UpgradeCmd(deviceList)回应答UpgradeAck为每个待升级设备启动30s升级定时器
5版本服务当收到TCP连接响应则向对端发版本查询指令开始版本传输流程升级过程向WEB服务发送进度指示UpgradeInd(deviceid,progress)每次收到ACK则刷新定时器。
6版本服务当设备升级完毕或升级定时器超时结束升级流程向WEB服务发升级完成指示UpgradeCompleteInd(deviceid,result)。当所有设备升级结束再给WEB发一个全部升级完成的指示UpgradeCompleteInd(all)
7WEB服务当收到全部升级结束指示按钮变正常升级设备状态改为正常
8WEB服务保存升级记录
c增加一页固件升级记录表包括所属组织、设备号、项目、桩号、升级时间、升级固件、是否成功

View File

@ -41,4 +41,10 @@ public interface BeidouClient {
@RequestParam(name = "tenantId") Integer tenantId, @RequestParam(name = "tenantId") Integer tenantId,
@RequestParam(name = "uploadTime") LocalDateTime uploadTime); @RequestParam(name = "uploadTime") LocalDateTime uploadTime);
@PostMapping("/upgrade_progress")
String onUpgradeProgress(@RequestParam(name = "deviceId") String deviceId,
@RequestParam(name = "msg") String msg);
@PostMapping("/upgrade_complete")
String onUpgradeComplete();
} }

View File

@ -17,7 +17,7 @@ public interface RtcmClient {
@GetMapping(value = "/get_device_info") @GetMapping(value = "/get_device_info")
public HttpResp getDeviceInfo(@RequestParam(name = "deviceId") String deviceId, @RequestParam(name = "cmd") String cmd); public HttpResp getDeviceInfo(@RequestParam(name = "deviceId") String deviceId, @RequestParam(name = "cmd") String cmd);
@PostMapping("/device_param_changed") @PostMapping("/device_param_changed")
HttpResp deviceParamChanged(@RequestParam(name = "deviceId") String deviceId,@RequestParam(name = "oldParentId") String oldParentId); HttpResp deviceParamChanged(@RequestParam(name = "deviceId") String deviceId,@RequestParam(name = "oldParentId") String oldParentId,@RequestParam(name = "oldParentId1") String oldParentId1);
@PostMapping("/group_param_changed") @PostMapping("/group_param_changed")
HttpResp groupParamChanged(); HttpResp groupParamChanged();

View File

@ -0,0 +1,11 @@
package com.imdroid.secapi.client;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(name="VersionClient",url = "http://localhost:9916/ver_mgr")
public interface VersionClient {
@PostMapping("/upgrade_cmd")
HttpResp upgrade(@RequestParam(name = "deviceList") String deviceList, @RequestParam(name = "verFile") String verFile);
}

View File

@ -3,9 +3,13 @@ package com.imdroid.secapi.dto;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.imdroid.common.util.ByteUtil;
import com.imdroid.common.util.HexUtil;
import lombok.Data; import lombok.Data;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
/** /**
* GNSS设备配置数据 * GNSS设备配置数据
@ -21,6 +25,7 @@ public class GnssDevice {
public static final short OP_MODE_USE = 0; public static final short OP_MODE_USE = 0;
public static final short OP_MODE_CHECK = 1; public static final short OP_MODE_CHECK = 1;
public static final short OP_MODE_UNUSE = 2; public static final short OP_MODE_UNUSE = 2;
public static final short OP_MODE_UPGRADING = 3;
public static final short MODEL_G505 = 0; //F9P public static final short MODEL_G505 = 0; //F9P
public static final short MODEL_G510 = 1; //博通 public static final short MODEL_G510 = 1; //博通
@ -28,6 +33,9 @@ public class GnssDevice {
public static final short LOGGING_MODE_SIMPLE = 0; // 精简模式 仅D3F0和D3F2 public static final short LOGGING_MODE_SIMPLE = 0; // 精简模式 仅D3F0和D3F2
public static final short LOGGING_MODE_FULL = 1; // 完整模式 public static final short LOGGING_MODE_FULL = 1; // 完整模式
public static final int PARA_MASK_HAS_BATTERY = 0x01;
public static final int PARA_MASK_VOLTAGE_FACTOR = 0x02;
@TableId(value = "id", type = IdType.AUTO) @TableId(value = "id", type = IdType.AUTO)
private Long id; private Long id;
private Integer tenantid; private Integer tenantid;
@ -48,6 +56,8 @@ public class GnssDevice {
private Integer calc_group_id = 1; private Integer calc_group_id = 1;
private String fwd_group_id; private String fwd_group_id;
private String fwd_group_id2; private String fwd_group_id2;
private Boolean has_battery;
private Byte voltage_factor; //电压测量分压比
private Boolean syn; //组参数是否同步 private Boolean syn; //组参数是否同步
private String pictures; private String pictures;
private Double ipose; //初始位置 private Double ipose; //初始位置
@ -68,4 +78,46 @@ public class GnssDevice {
private String remark; private String remark;
private String iccid; private String iccid;
// 参数改变
private Integer change_flag = 0;
private String parentid1;
private String getBatteryConfigCmd(){
return "d3130006"+ HexUtil.Int2HexString(Integer.parseInt(deviceid))+
"55"+(has_battery?"01":"00");
}
private String getVoltageFactorConfigCmd(){
return "d3130006"+ HexUtil.Int2HexString(Integer.parseInt(deviceid))+
"52"+HexUtil.Byte2HexString(voltage_factor);
}
public List<String> getConfigCmd(){
List<String> cmdList = new ArrayList<>();
if((change_flag&PARA_MASK_HAS_BATTERY) !=0 ){
cmdList.add(getBatteryConfigCmd());
}
if((change_flag&PARA_MASK_VOLTAGE_FACTOR) !=0 ){
cmdList.add(getVoltageFactorConfigCmd());
}
return cmdList;
}
public boolean clearChangeFlag(String ack){
boolean result = false;
if(change_flag!=0) {
byte[] ackBytes = ByteUtil.hexStringTobyte(ack);
if(ackBytes[ackBytes.length-1] == 1) {
if (ackBytes[8] == 0x55) {
change_flag &= ~PARA_MASK_HAS_BATTERY;
result = true;
} else if (ackBytes[8] == 0x52) {
change_flag &= ~PARA_MASK_VOLTAGE_FACTOR;
result = true;
}
}
}
return result;
}
} }

View File

@ -28,7 +28,7 @@ public class GnssGroup implements Serializable {
Short gnss_sample_s; Short gnss_sample_s;
public String getConfigCmd(GnssDevice device){ public String getConfigCmd(GnssDevice device){
String cmd = "D3110009"; String cmd = "D311000a";
cmd += HexUtil.Int2HexString(Integer.parseInt(device.getDeviceid())) cmd += HexUtil.Int2HexString(Integer.parseInt(device.getDeviceid()))
+HexUtil.Byte2HexString((byte) work_cycle.intValue()) +HexUtil.Byte2HexString((byte) work_cycle.intValue())
+HexUtil.Byte2HexString((byte) active_time.intValue()) +HexUtil.Byte2HexString((byte) active_time.intValue())

View File

@ -0,0 +1,15 @@
package com.imdroid.secapi.dto;
import lombok.Data;
@Data
public class UpgradeState {
static final public byte STATE_DISCONNECTED = 0;
static final public byte STATE_INFO_ACQUIRING = 1;
static final public byte STATE_UPGRADING = 2;
static final public byte STATE_UPGRADED = 3;
String deviceId;
byte progress=0;
byte state=STATE_DISCONNECTED; //0:未连接1获取信息2:升级中3:升级完成
byte code=0;
}

View File

@ -0,0 +1,19 @@
package com.imdroid.beidou_fwd.entity;
import lombok.Data;
/**
* MQTT数据推送 api
*
* @author LiGang
*/
@Data
public class ZNYMQTTData {
private String timestamp;
// gnss数据
private double rpose;
private double rposn;
private double rposu;
}

View File

@ -41,6 +41,10 @@ public class MQTTClient {
client.connect(options); client.connect(options);
} }
public boolean isConnected(){
return client.isConnected();
}
public boolean publish(String topic, String message) { public boolean publish(String topic, String message) {
if(!client.isConnected()){ if(!client.isConnected()){
logger.info("mqtt disconnected"); logger.info("mqtt disconnected");

View File

@ -62,7 +62,7 @@ public class Forwarder {
* @param fwdNameType推送设备名deviceid或fwdid或projectid-sector-name * @param fwdNameType推送设备名deviceid或fwdid或projectid-sector-name
* @param cycle发送周期 * @param cycle发送周期
*/ */
void init(String fwdGroupId, String desc, Integer tenantId, byte fwdNameType, int cycle){ public void init(String fwdGroupId, String desc, Integer tenantId, byte fwdNameType, int cycle){
this.fwdGroupId = fwdGroupId; this.fwdGroupId = fwdGroupId;
this.description = desc; this.description = desc;
this.tenantId = tenantId; this.tenantId = tenantId;
@ -90,10 +90,18 @@ public class Forwarder {
} }
} }
public void forwardCurrentGnss() {
if(forwardGnssRecords(LocalDateTime.now(), null)){
// 推送失败记录补发记录
ThreadManager.getScheduledThreadPool().schedule(() -> {
this.forwardHistoryGnss();
},100, TimeUnit.SECONDS);
}
}
/*** /***
* 推送指定企业设备ID时间的GNSS记录查找指定时间前cycle分钟的有效数据推送 * 推送指定企业设备ID时间的GNSS记录查找指定时间前cycle分钟的有效数据推送
* @param sendTime要推送的记录的时间 * @param sendTime要推送的记录的时间
* @param resendRecord重发记录
*/ */
private boolean forwardGnssRecords(LocalDateTime sendTime, ResendRecord resendRecord) { private boolean forwardGnssRecords(LocalDateTime sendTime, ResendRecord resendRecord) {
@ -101,24 +109,45 @@ public class Forwarder {
String beginTime = sendTime.minusMinutes(fwdCycleMinutes).format(formatter); String beginTime = sendTime.minusMinutes(fwdCycleMinutes).format(formatter);
// 查找属于指定推送组的设备列表 // 查找属于指定推送组的设备列表
/* QueryWrapper<GnssDevice> queryWrapper = new QueryWrapper<>(); List<GnssDeviceJoin> gnssDeviceList;
if(deviceId != null){ MPJQueryWrapper jquery = null;
queryWrapper.eq("deviceid", deviceId);
} if(resendRecord!=null && resendRecord.getDeviceid()!=null){
queryWrapper.eq("fwd_group_id", fwdGroupId) jquery = new MPJQueryWrapper<GnssDevice> ()
.or()
.eq("fwd_group_id2", fwdGroupId);
List<GnssDevice> gnssDeviceList = deviceMapper.selectList(queryWrapper);
*/
MPJQueryWrapper jquery = new MPJQueryWrapper<GnssDevice> ()
.selectAll(GnssDevice.class) .selectAll(GnssDevice.class)
.select("d.latitude as latitude") .select("d.latitude as latitude")
.select("d.longitude as longitude") .select("d.longitude as longitude")
.leftJoin("gnssstatus d on t.deviceid = d.deviceid") .leftJoin("gnssstatus d on t.deviceid = d.deviceid")
.eq("fwd_group_id", fwdGroupId) .and(warpper->warpper.eq("fwd_group_id", fwdGroupId)
.or() .or()
.eq("fwd_group_id2", fwdGroupId); .eq("fwd_group_id2", fwdGroupId)).
List<GnssDeviceJoin> gnssDeviceList = deviceMapper.selectJoinList(GnssDeviceJoin.class, jquery); eq("deviceid", resendRecord.getDeviceid());
}
else if(resendRecord!=null && resendRecord.getProjectid()!=null){
jquery = new MPJQueryWrapper<GnssDevice> ()
.selectAll(GnssDevice.class)
.select("d.latitude as latitude")
.select("d.longitude as longitude")
.leftJoin("gnssstatus d on t.deviceid = d.deviceid")
.and(warpper->warpper.eq("fwd_group_id", fwdGroupId)
.or()
.eq("fwd_group_id2", fwdGroupId))
.and(warpper->warpper.eq("project_id",resendRecord.getProjectid())
.or()
.eq("project2_id",resendRecord.getProjectid()));
}
else{
jquery = new MPJQueryWrapper<GnssDevice> ()
.selectAll(GnssDevice.class)
.select("d.latitude as latitude")
.select("d.longitude as longitude")
.leftJoin("gnssstatus d on t.deviceid = d.deviceid")
.and(warpper->warpper.eq("fwd_group_id", fwdGroupId)
.or()
.eq("fwd_group_id2", fwdGroupId));
}
gnssDeviceList = deviceMapper.selectJoinList(GnssDeviceJoin.class, jquery);
logger.debug("candidate fwd devices {}", gnssDeviceList.size());
// 查询最近半小时的GNSS记录 // 查询最近半小时的GNSS记录
QueryWrapper<GnssCalcData> gnssQueryWrapper = new QueryWrapper<>(); QueryWrapper<GnssCalcData> gnssQueryWrapper = new QueryWrapper<>();
@ -131,7 +160,9 @@ public class Forwarder {
if(resendRecord != null && resendRecord.getDeviceid()!=null){ if(resendRecord != null && resendRecord.getDeviceid()!=null){
gnssQueryWrapper.eq("deviceid", resendRecord.getDeviceid()); gnssQueryWrapper.eq("deviceid", resendRecord.getDeviceid());
} }
List<GnssCalcData> locationRecords = gnssDataMapper.selectList(gnssQueryWrapper); List<GnssCalcData> locationRecords = gnssDataMapper.selectList(gnssQueryWrapper);
logger.debug("candidate fwd records {}", locationRecords.size());
if(locationRecords.size() == 0) return false; if(locationRecords.size() == 0) return false;
// 构造按项目id分类的GNSS记录 // 构造按项目id分类的GNSS记录
@ -216,15 +247,15 @@ public class Forwarder {
fwdResult = false; fwdResult = false;
fwdRecord.setResult(FwdRecord.RESULT_FAILED); fwdRecord.setResult(FwdRecord.RESULT_FAILED);
// 新增重发记录 // 新增重发记录
resendRecord = new ResendRecord(); ResendRecord resendRecord1 = new ResendRecord();
resendRecord.setProjectid(projectId); resendRecord1.setProjectid(projectId);
resendRecord.setTenantid(tenantId); resendRecord1.setTenantid(tenantId);
resendRecord.setCreatetime(LocalDateTime.now()); resendRecord1.setCreatetime(LocalDateTime.now());
resendRecord.setStarttime(sendTime); resendRecord1.setStarttime(sendTime);
resendRecord.setEndtime(sendTime); resendRecord1.setEndtime(sendTime);
resendRecord.setFwd_group_id(fwdGroupId); resendRecord1.setFwd_group_id(fwdGroupId);
resendRecord.setState(ResendRecord.STATE_FWD_FAILED); resendRecord1.setState(ResendRecord.STATE_FWD_FAILED);
resendRecordMapper.insert(resendRecord); resendRecordMapper.insert(resendRecord1);
} }
fwdRecordsMapper.insert(fwdRecord); fwdRecordsMapper.insert(fwdRecord);
} }
@ -239,14 +270,6 @@ public class Forwarder {
return fwdResult; return fwdResult;
} }
void forwardCurrentGnss() {
if(forwardGnssRecords(LocalDateTime.now(),null)) {
// 推送失败记录补发记录
ThreadManager.getScheduledThreadPool().schedule(() -> {
this.forwardHistoryGnss();
},100, TimeUnit.SECONDS);
}
}
void forwardHistoryGnss() { void forwardHistoryGnss() {
// 1.从转发记录表里检索待补传记录时间表含设备Id时间段 // 1.从转发记录表里检索待补传记录时间表含设备Id时间段
@ -257,7 +280,7 @@ public class Forwarder {
queryWrapper.ge("createtime", LocalDateTime.now().minusDays(30)); queryWrapper.ge("createtime", LocalDateTime.now().minusDays(30));
List<ResendRecord> resendRecordsList = resendRecordMapper.selectList(queryWrapper); List<ResendRecord> resendRecordsList = resendRecordMapper.selectList(queryWrapper);
if(resendRecordsList!=null){ if(resendRecordsList!=null && resendRecordsList.size()>0){
//修改状态 //修改状态
UpdateWrapper<ResendRecord> updateWrapper = new UpdateWrapper<>(); UpdateWrapper<ResendRecord> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("fwd_group_id",fwdGroupId); updateWrapper.eq("fwd_group_id",fwdGroupId);
@ -266,11 +289,11 @@ public class Forwarder {
updateWrapper.ge("createtime", LocalDateTime.now().minusDays(30)); updateWrapper.ge("createtime", LocalDateTime.now().minusDays(30));
updateWrapper.set("state",ResendRecord.STATE_FWDING); updateWrapper.set("state",ResendRecord.STATE_FWDING);
int updateNum = resendRecordMapper.update(null, updateWrapper); int updateNum = resendRecordMapper.update(null, updateWrapper);
logger.info("{} forward history records: {}, update {}",fwdGroupId, resendRecordsList.size(),updateNum); logger.debug("{} forward history records: {}, update {}",fwdGroupId, resendRecordsList.size(),updateNum);
// 2.检索这个这个时间段的解算结果如果有数据则单个终端转发标志记录为已补传 // 2.检索这个这个时间段的解算结果如果有数据则单个终端转发标志记录为已补传
for(ResendRecord record:resendRecordsList){ for(ResendRecord record:resendRecordsList){
if(record.getProjectid()!=null) if(record.getProjectid()!=null)
logger.info("{} forward history {}",fwdGroupId, record.getProjectid()); logger.debug("{} forward history {}",fwdGroupId, record.getProjectid());
forwardBatchGnssRecords(record); forwardBatchGnssRecords(record);
} }
} }
@ -404,7 +427,7 @@ public class Forwarder {
tranData.setDeviceType(2); tranData.setDeviceType(2);
tranData.setDeviceSn(locationRecord.getDeviceid()); tranData.setDeviceSn(locationRecord.getDeviceid());
String json = GsonUtil.toJson(tranData); String json = GsonUtil.toJson(tranData);
logger.info("forward to GZY mqtt: {}",json); logger.debug("forward to GZY mqtt: {}",json);
sendNum++; sendNum++;
} }

View File

@ -38,16 +38,29 @@ public class GXJKForwarder extends Forwarder {
void registerMe() throws MqttException { void registerMe() throws MqttException {
init(FORWARDER_NAME, "MQTT "+brokerUrl,7,FWD_DEVICE_NAME,10); init(FORWARDER_NAME, "MQTT "+brokerUrl,7,FWD_DEVICE_NAME,10);
mqttClient = new MQTTClient(brokerUrl, username, password,clientid); mqttClient = new MQTTClient(brokerUrl, username, password,clientid);
try{
mqttClient.connect(); mqttClient.connect();
} }
catch (Exception e){
logger.error("gxjk mqtt connect failed: {}",e.toString());
}
}
/** /**
* 每半小时转发GNSS解算结果 * 每半小时转发GNSS解算结果
*/ */
@Scheduled(cron = "0 0/10 * * * ?") // 每30分钟执行一次 @Scheduled(cron = "0 0/10 * * * ?") // 每30分钟执行一次
private void forwardGnss() { private void forwardGnss() {
logger.info("gxjk forwardGnss"); logger.debug("gxjk forwardGnss");
forwardCurrentGnss(); if(mqttClient.isConnected()) forwardCurrentGnss();
else{
try{
mqttClient.connect();
}
catch (Exception e){
logger.error("gxjk mqtt connect failed: {}",e.toString());
}
}
} }
@Override @Override
@ -76,8 +89,8 @@ public class GXJKForwarder extends Forwarder {
sendNum++; sendNum++;
} }
String json = "#" + GsonUtil.toJson(sendData) + "!"; String json = "#" + GsonUtil.toJson(sendData) + "!";
logger.info("project " + projectId + ": push calculation result to GXJK"); logger.debug("project " + projectId + ": push calculation result to GXJK");
logger.info(json); logger.debug(json);
// /slope/项目号-all/gnss/all/publish // /slope/项目号-all/gnss/all/publish
String topic = "/slope/"+projectId+"-all/gnss/all/publish"; String topic = "/slope/"+projectId+"-all/gnss/all/publish";
try { try {

View File

@ -21,7 +21,7 @@ import java.util.List;
@Configuration @Configuration
@EnableScheduling @EnableScheduling
public class GXXfzForwarder extends Forwarder{ public class GXXfzForwarder extends Forwarder{
private String FORWARDER_NAME = "广西新发展"; private final String FORWARDER_NAME = "广西新发展";
@Value("${xfz.server.host}") @Value("${xfz.server.host}")
private String host; private String host;
@ -69,13 +69,13 @@ public class GXXfzForwarder extends Forwarder{
*/ */
@Scheduled(cron = "0 0/30 * * * ?") // 每30分钟执行一次 @Scheduled(cron = "0 0/30 * * * ?") // 每30分钟执行一次
private void forwardGnss() { private void forwardGnss() {
logger.info("xfz forwardGnss"); logger.debug("xfz forwardGnss");
forwardCurrentGnss(); forwardCurrentGnss();
} }
/* /*
@Scheduled(cron = "0 0/10 * * * ?") // 每30分钟执行一次 @Scheduled(cron = "0 0/10 * * * ?") // 每30分钟执行一次
private void checkDevice() { private void checkDevice() {
//logger.info("zny checkDevice"); //logger.debug("zny checkDevice");
checkOfflineDevice("2345053","2350106","2350124"); checkOfflineDevice("2345053","2350106","2350124");
}*/ }*/
@ -107,8 +107,8 @@ public class GXXfzForwarder extends Forwarder{
sendNum++; sendNum++;
} }
String json = "#" + GsonUtil.toJson(xfzTcpMessage) + "!"; String json = "#" + GsonUtil.toJson(xfzTcpMessage) + "!";
logger.info("project " + projectId + ": push calculation result to XFZ"); logger.debug("project " + projectId + ": push calculation result to XFZ");
logger.info(json); logger.debug(json);
try { try {
listener.clear(); listener.clear();
xfzTcpClient.writeAndFlush(json); xfzTcpClient.writeAndFlush(json);

View File

@ -40,7 +40,7 @@ public class GZYForwarder extends Forwarder{
@Scheduled(cron = "0 0 0/1 * * ?") // 每小时执行一次 @Scheduled(cron = "0 0 0/1 * * ?") // 每小时执行一次
// @Scheduled(cron = "0 0/30 * * * ?") // 每30分钟执行一次 // @Scheduled(cron = "0 0/30 * * * ?") // 每30分钟执行一次
private void forwardGnss() { private void forwardGnss() {
logger.info("gzy UDP forwardGnss"); logger.debug("gzy UDP forwardGnss");
forwardCurrentGnss(); forwardCurrentGnss();
} }
@ -62,8 +62,8 @@ public class GZYForwarder extends Forwarder{
tranData.setZ(d); tranData.setZ(d);
gzyData.setTranData(tranData); gzyData.setTranData(tranData);
String msg = "JGKJ" + GsonUtil.toJson(gzyData) + "#!"; String msg = "JGKJ" + GsonUtil.toJson(gzyData) + "#!";
logger.info("forward to GZY"); logger.debug("forward to GZY");
logger.info(msg); logger.debug(msg);
udpClient.sendMessage(msg); udpClient.sendMessage(msg);
sendNum++; sendNum++;
} }

View File

@ -47,8 +47,13 @@ public class GZYMQTTForwarder extends Forwarder {
void registerMe() throws MqttException { void registerMe() throws MqttException {
init(FORWARDER_NAME, "MQTT "+brokerUrl,2,FWD_DEVICE_ALIAS_NAME,30); init(FORWARDER_NAME, "MQTT "+brokerUrl,2,FWD_DEVICE_ALIAS_NAME,30);
mqttClient = new MQTTClient(brokerUrl, username, password,clientid); mqttClient = new MQTTClient(brokerUrl, username, password,clientid);
try{
mqttClient.connect(); mqttClient.connect();
} }
catch (Exception e){
logger.error("gzy mqtt connect failed: {}",e.toString());
}
}
/** /**
* 每半小时转发GNSS解算结果 * 每半小时转发GNSS解算结果
@ -56,10 +61,20 @@ public class GZYMQTTForwarder extends Forwarder {
@Scheduled(cron = "0 0 0/1 * * ?") // 每小时执行一次 @Scheduled(cron = "0 0 0/1 * * ?") // 每小时执行一次
//@Scheduled(cron = "0 0/5 * * * ?") // 每30分钟执行一次 //@Scheduled(cron = "0 0/5 * * * ?") // 每30分钟执行一次
private void forwardGnss() { private void forwardGnss() {
logger.info("gzy mqtt forwardGnss"); logger.debug("gzy mqtt forwardGnss");
if(mqttClient.isConnected()) {
forwardCurrentGnss(); forwardCurrentGnss();
//forwardAngleData(); //forwardAngleData();
} }
else{
try{
mqttClient.connect();
}
catch (Exception e){
logger.error("gzy mqtt connect failed: {}",e.toString());
}
}
}
@Override @Override
int send(String projectId, List<GnssCalcData> records, LocalDateTime sentTime) { int send(String projectId, List<GnssCalcData> records, LocalDateTime sentTime) {
@ -77,7 +92,7 @@ public class GZYMQTTForwarder extends Forwarder {
tranData.setDeviceType(2); tranData.setDeviceType(2);
tranData.setDeviceSn(locationRecord.getDeviceid()); tranData.setDeviceSn(locationRecord.getDeviceid());
String json = GsonUtil.toJson(tranData); String json = GsonUtil.toJson(tranData);
logger.info("forward to GZY mqtt: {}",json); logger.debug("forward to GZY mqtt: {}",json);
try { try {
if(!mqttClient.publish(topic, json)) break; if(!mqttClient.publish(topic, json)) break;
Thread.sleep(50); Thread.sleep(50);
@ -116,7 +131,7 @@ public class GZYMQTTForwarder extends Forwarder {
tranData.setAngleAz(NumberUtils.scale((double) az,2)); tranData.setAngleAz(NumberUtils.scale((double) az,2));
String json = GsonUtil.toJson(tranData); String json = GsonUtil.toJson(tranData);
logger.info("forward to GZY mqtt angles: {}",json); logger.debug("forward to GZY mqtt angles: {}",json);
try { try {
if(!mqttClient.publish(topic, json)) break; if(!mqttClient.publish(topic, json)) break;
Thread.sleep(10); Thread.sleep(10);
@ -125,7 +140,7 @@ public class GZYMQTTForwarder extends Forwarder {
} }
sendNum++; sendNum++;
} }
logger.info("total number of angles sent to GZY: {}",sendNum); logger.debug("total number of angles sent to GZY: {}",sendNum);
} }

View File

@ -55,7 +55,7 @@ public class KingMaForwarder extends Forwarder{
@Scheduled(cron = "0 0 0/1 * * ?") // 每小时执行一次 @Scheduled(cron = "0 0 0/1 * * ?") // 每小时执行一次
//@Scheduled(cron = "0 0/5 * * * ?") // 每30分钟执行一次 //@Scheduled(cron = "0 0/5 * * * ?") // 每30分钟执行一次
private void forwardGnss() { private void forwardGnss() {
logger.info("kingma forwardGnss"); logger.debug("kingma forwardGnss");
forwardCurrentGnss(); forwardCurrentGnss();
} }
@ -88,12 +88,12 @@ public class KingMaForwarder extends Forwarder{
if(header == null || nowTime.isAfter(lastTokenTime.plusMinutes(59))){ if(header == null || nowTime.isAfter(lastTokenTime.plusMinutes(59))){
try { try {
if (!updateToken()) { if (!updateToken()) {
logger.info("update token failed!"); logger.debug("update token failed!");
return 0; return 0;
} }
} }
catch (Exception e){ catch (Exception e){
logger.info("update token failed!"); logger.debug("update token failed!");
return 0; return 0;
} }
lastTokenTime = nowTime; lastTokenTime = nowTime;
@ -135,10 +135,10 @@ public class KingMaForwarder extends Forwarder{
sendNum++; sendNum++;
} }
String json = GsonUtil.toJson(dataList); String json = GsonUtil.toJson(dataList);
logger.info(json); logger.debug(json);
String result = HttpUtils.postJson(data_host,header,json); String result = HttpUtils.postJson(data_host,header,json);
logger.info("project " + projectId + ": push calculation result to Kingma"); logger.debug("project " + projectId + ": push calculation result to Kingma");
logger.info("result: "+result); logger.debug("result: "+result);
JSONObject obj = (JSONObject) JSONObject.parse(result); JSONObject obj = (JSONObject) JSONObject.parse(result);
String msg = obj.getString("message"); String msg = obj.getString("message");
if(msg.equals("Success")) return sendNum; if(msg.equals("Success")) return sendNum;

View File

@ -75,7 +75,7 @@ public class SaasForwarder extends Forwarder{
@Scheduled(cron = "0 20,50 * * * ?") // 每30分钟执行一次 @Scheduled(cron = "0 20,50 * * * ?") // 每30分钟执行一次
private void forwardGnss() { private void forwardGnss() {
if(enabled) { if(enabled) {
logger.info("saas forwardGnss"); logger.debug("saas forwardGnss");
forwardCurrentGnss(); forwardCurrentGnss();
} }
} }
@ -107,8 +107,8 @@ public class SaasForwarder extends Forwarder{
sendNum++; sendNum++;
} }
String json = "#" + GsonUtil.toJson(tcpMessage) + "!"; String json = "#" + GsonUtil.toJson(tcpMessage) + "!";
logger.info("project " + projectId + ": push calculation result to SAAS"); logger.debug("project " + projectId + ": push calculation result to SAAS");
logger.info(json); logger.debug(json);
try { try {
listener.clear(); listener.clear();
tcpClient.writeAndFlush(json); tcpClient.writeAndFlush(json);

View File

@ -34,14 +34,14 @@ public class ZNYForwarder extends Forwarder{
*/ */
@Scheduled(cron = "0 0/10 * * * ?") // 每30分钟执行一次 @Scheduled(cron = "0 0/10 * * * ?") // 每30分钟执行一次
private void checkDevice() { private void checkDevice() {
//logger.info("zny checkDevice"); //logger.debug("zny checkDevice");
//checkNoDataDevice("2345078","2345065","2345073"); //checkNoDataDevice("2345078","2345065","2345073");
//checkNoDataDevice("2345085","2345068","2345075"); //checkNoDataDevice("2345085","2345068","2345075");
} }
@Scheduled(cron = "0 0/30 * * * ?") // 每30分钟执行一次 @Scheduled(cron = "0 0/30 * * * ?") // 每30分钟执行一次
private void forwardGnss() { private void forwardGnss() {
logger.info("zny forwardGnss"); logger.debug("zny forwardGnss");
forwardCurrentGnss(); forwardCurrentGnss();
} }
@ -60,12 +60,12 @@ public class ZNYForwarder extends Forwarder{
sendNum++; sendNum++;
} }
String json = GsonUtil.toJson(defoData); String json = GsonUtil.toJson(defoData);
logger.info("发送数据到武汉中南设计院平台:{}", json); logger.debug("发送数据到武汉中南设计院平台:{}", json);
//return sendNum; //return sendNum;
try { try {
String result = HttpUtils.postJson(data_host, json); String result = HttpUtils.postJson(data_host, json);
logger.info("发送数据到武汉中南设计院平台返回结果:{}", result); logger.debug("发送数据到武汉中南设计院平台返回结果:{}", result);
JSONObject obj = (JSONObject) JSONObject.parse(result); JSONObject obj = (JSONObject) JSONObject.parse(result);
String msg = obj.getString("msg"); String msg = obj.getString("msg");
if (msg.contains("成功")) return sendNum; if (msg.contains("成功")) return sendNum;

View File

@ -34,12 +34,12 @@ public class ZNYForwarder2 extends Forwarder{
*/ */
@Scheduled(cron = "0 0/30 * * * ?") // 每30分钟执行一次 @Scheduled(cron = "0 0/30 * * * ?") // 每30分钟执行一次
private void forwardGnss() { private void forwardGnss() {
logger.info("zny2 forwardGnss"); logger.debug("zny2 forwardGnss");
forwardCurrentGnss(); forwardCurrentGnss();
} }
@Scheduled(cron = "0 0/10 * * * ?") // 每30分钟执行一次 @Scheduled(cron = "0 0/10 * * * ?") // 每30分钟执行一次
private void checkDevice() { private void checkDevice() {
//logger.info("zny checkDevice"); //logger.debug("zny checkDevice");
//checkOfflineDevice("2419374","2410188","2410194"); //checkOfflineDevice("2419374","2410188","2410194");
//checkOfflineDevice("2419350","2410232","2410232"); //checkOfflineDevice("2419350","2410232","2410232");
//checkOfflineDevice("2345074","2345065","2345089"); //checkOfflineDevice("2345074","2345065","2345089");
@ -59,12 +59,12 @@ public class ZNYForwarder2 extends Forwarder{
sendNum++; sendNum++;
} }
String json = GsonUtil.toJson(defoData); String json = GsonUtil.toJson(defoData);
logger.info("发送数据到武汉中南设计院2.0平台:{}", json); logger.debug("发送数据到武汉中南设计院2.0平台:{}", json);
//return sendNum; //return sendNum;
try { try {
String result = HttpUtils.postJson(data_host, json); String result = HttpUtils.postJson(data_host, json);
logger.info("武汉中南设计院2.0平台返回结果:{}", result); logger.debug("武汉中南设计院2.0平台返回结果:{}", result);
JSONObject obj = (JSONObject) JSONObject.parse(result); JSONObject obj = (JSONObject) JSONObject.parse(result);
String msg = obj.getString("msg"); String msg = obj.getString("msg");
if (msg.contains("成功")) return sendNum; if (msg.contains("成功")) return sendNum;

View File

@ -0,0 +1,108 @@
package com.imdroid.beidou_fwd.task;
import com.imdroid.beidou_fwd.entity.ZNYMQTTData;
import com.imdroid.beidou_fwd.service.MQTTClient;
import com.imdroid.common.util.Des3Utils;
import com.imdroid.common.util.GsonUtil;
import com.imdroid.common.util.NumberUtils;
import com.imdroid.secapi.dto.GnssCalcData;
import com.imdroid.secapi.dto.GnssStatusMsgMapper;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.springframework.beans.factory.annotation.Autowired;
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.time.format.DateTimeFormatter;
import java.util.List;
@Component
@Configuration
@EnableScheduling
public class ZNYMQTTForwarder extends Forwarder {
static final String FORWARDER_NAME = "中南院MQTT";
final DateTimeFormatter formatterMs = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
static byte[] key = new byte[] { (byte) 0x8c, (byte) 0xc7, 0x2b, 0x05, 0x70, 0x5d, 0x5c, 0x46, (byte) 0xf4, 0x12, (byte) 0xaf, (byte) 0x8c, (byte) 0xbe,
(byte) 0xd5, 0x5a, (byte) 0xad, (byte) 0x8c, (byte) 0xc7, 0x2b, 0x05, 0x70, 0x5d, 0x5c, 0x46 };
static byte[] iv = new byte[] { 0x66, 0x7b, 0x02, (byte) 0xa8, 0x5c, 0x61, (byte) 0xc7, (byte) 0x86 };
@Value("${zny.mqtt.brokerUrl}")
private String brokerUrl;
@Value("${zny.mqtt.username}")
private String username;
@Value("${zny.mqtt.password}")
private String password;
@Value("${zny.mqtt.clientid}")
private String clientid;
@Autowired
GnssStatusMsgMapper statusMsgMapper;
MQTTClient mqttClient;
@PostConstruct
void registerMe() throws MqttException {
init(FORWARDER_NAME, "MQTT "+brokerUrl,4,FWD_DEVICE_ALIAS_NAME,30);
mqttClient = new MQTTClient(brokerUrl, username, password,clientid);
try{
mqttClient.connect();
}
catch (Exception e){
logger.error("zny mqtt connect failed: {}",e.toString());
}
}
/**
* 每半小时转发GNSS解算结果
*/
//@Scheduled(cron = "0 0 0/1 * * ?") // 每小时执行一次
@Scheduled(cron = "0 0/30 * * * ?") // 每30分钟执行一次
private void forwardGnss() {
logger.debug("zny mqtt forwardGnss");
if(mqttClient.isConnected()) forwardCurrentGnss();
else{
try{
mqttClient.connect();
}
catch (Exception e){
logger.error("zny mqtt connect failed: {}",e.toString());
}
}
}
@Override
int send(String projectId, List<GnssCalcData> records, LocalDateTime sentTime) {
int sendNum = 0;
for (GnssCalcData locationRecord : records) {
ZNYMQTTData tranData = new ZNYMQTTData();
tranData.setTimestamp(locationRecord.getCreatetime().format(formatterMs));
double n = NumberUtils.scale(locationRecord.getRposn(), 2);
double e = NumberUtils.scale(locationRecord.getRpose(), 2);
double d = NumberUtils.scale(locationRecord.getRposd(), 2);
tranData.setRposn(n);
tranData.setRpose(e);
tranData.setRposu(d);
String json = GsonUtil.toJson(tranData);
String cryptJson = Des3Utils.encrypt(json,key,iv);
logger.info("forward to ZNY mqtt: {}, {}",json, cryptJson);
String topic = "/GnssData/"+locationRecord.getDeviceid()+"/Increment";
try {
if(!mqttClient.publish(topic, cryptJson)) break;
Thread.sleep(20);
} catch (Exception e1) {
e1.printStackTrace();
}
sendNum++;
}
return sendNum;
}
}

View File

@ -60,3 +60,9 @@ gxjk.mqtt.server.clientid = GXJK_client
sass.server.host = 127.0.0.1 sass.server.host = 127.0.0.1
sass.server.port = 9933 sass.server.port = 9933
sass.server.enabled = false sass.server.enabled = false
zny.mqtt.brokerUrl=tcp://42.194.196.91:51883
zny.mqtt.username=testuser
zny.mqtt.password=Test@20b39#
zny.mqtt.clientid = ZNY_client
zny.mqtt.topic = /GnssData/testgnss/Increment

View File

@ -21,13 +21,13 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.scheduling.annotation.Scheduled;
import javax.annotation.PostConstruct;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.time.Duration;
import java.util.Map; import java.util.*;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
/** /**
* @author Layton * @author Layton
* @date 2023/2/2 20:49 * @date 2023/2/2 20:49
@ -37,6 +37,15 @@ public class D331RtcmMessageExecutor implements Executor<D331RtcmMessage, Void>
private final Logger logger = LoggerFactory.getLogger(this.getClass()); 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 @Autowired
private DeviceService deviceService; private DeviceService deviceService;
@Autowired @Autowired
@ -48,30 +57,159 @@ public class D331RtcmMessageExecutor implements Executor<D331RtcmMessage, Void>
// 添加一个成员变量用于追踪每个测站最后一次转发D300数据的时间 // 添加一个成员变量用于追踪每个测站最后一次转发D300数据的时间
private final Map<String, Long> lastD300ForwardTimeMap = new ConcurrentHashMap<>(); private final Map<String, Long> lastD300ForwardTimeMap = new ConcurrentHashMap<>();
private static final long D300_FORWARD_INTERVAL = 10000; // 10秒单位毫秒 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 @Override
public Void execute(D331RtcmMessage message) { public Void execute(D331RtcmMessage message) {
String id = message.getId(); String id = message.getId();
// 更新基站状态
updateBaseStationStatus(id);
// 补齐tenantId // 补齐tenantId
Device deviceBs = deviceService.findByDeviceId(id); Device deviceBs = deviceService.findByDeviceId(id);
if(deviceBs == null || deviceBs.getOpMode() == GnssDevice.OP_MODE_UNUSE) return null; if(deviceBs == null || deviceBs.getOpMode() == GnssDevice.OP_MODE_UNUSE || deviceBs.getOpMode() == GnssDevice.OP_MODE_CHECK) {
return null;
}
// 推送基站数据 // 推送基站数据
if(deviceBs.getOpMode() == GnssDevice.OP_MODE_USE) { if(deviceBs.getOpMode() == GnssDevice.OP_MODE_USE) {
byte[] forwardBytes = message.getSrcData(); byte[] forwardBytes = message.getSrcData();
// 要求快速转发因此用缓存不要每次都查数据库
List<Device> deviceList = deviceService.findByParentId(id); // 获取使用该基站(包括作为主基站和备选基站)的所有测站
//logger.debug("base station {} has {} rovers: ", message.getId(),deviceList.size()); List<Device> primaryDevices = deviceService.findByParentId(id);
List<Device> backupDevices = deviceService.findByParentId1(id);
// 合并两个列表
List<Device> allDevices = new ArrayList<>();
allDevices.addAll(primaryDevices);
allDevices.addAll(backupDevices);
DeviceChannel deviceChannel = null; DeviceChannel deviceChannel = null;
for (Device device : deviceList) { for (Device device : allDevices) {
if (device.getOpMode() != GnssDevice.OP_MODE_USE) continue; if (device.getOpMode() != GnssDevice.OP_MODE_USE) continue;
if (device.getFixedNum()>0 && device.getGnssSampleRate()>1 if ((device.getModel()==GnssDevice.MODEL_G510) &&
(device.getFixedNum()>100) && (device.getGnssSampleRate()>1)
&& (deviceBs.getD3xxCount()%device.getGnssSampleRate()) != 0) { && (deviceBs.getD3xxCount()%device.getGnssSampleRate()) != 0) {
//if(!UBXUtil.has1005(forwardBytes)) continue; //1005必推 //if(!UBXUtil.has1005(forwardBytes)) continue; //1005必推
continue; continue;
} }
String deviceId = device.getDeviceId(); 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) { if(device.getDataChannelType() == Device.CHANNEL_TYPE_UDP) {
deviceChannel = OnlineChannels.INSTANCE.getDataChannel(deviceId); deviceChannel = OnlineChannels.INSTANCE.getDataChannel(deviceId);
@ -79,149 +217,389 @@ public class D331RtcmMessageExecutor implements Executor<D331RtcmMessage, Void>
deviceChannel = OnlineChannels.INSTANCE.getConfigChannel(deviceId); deviceChannel = OnlineChannels.INSTANCE.getConfigChannel(deviceId);
} }
// 读取数据库中model字段判断基站类型 // 根据基站和设备类型处理数据转发
Short baseStationModel = deviceBs.getModel(); if (shouldForwardData(deviceBs, device)) {
// 如果model为null使用默认值0 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();
if (baseStationModel == null) { if (baseStationModel == null) {
baseStationModel = 0; baseStationModel = 0;
logger.warn("Base station model is null for device: {}, using default value 0", id); }
Short deviceModel = device.getModel();
if (deviceModel == null) {
deviceModel = 0;
} }
if (baseStationModel == 1) { if (baseStationModel == 1) {
// 基站类型为1正常执行 // f9p基站所有设备都可以接收
if(deviceChannel != null && deviceChannel.isOnline()) { return true;
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) { } else if (baseStationModel == 0) {
//logger.info("Base station model is 0 for device: {}", deviceId); // 博通基站只支持特定设备
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;
}
Short deviceModel = device.getModel(); Short deviceModel = device.getModel();
// 如果model为null使用默认值0
if (deviceModel == null) { if (deviceModel == null) {
deviceModel = 0; deviceModel = 0;
//logger.warn("Device model is null for device: {}, using default value 0", 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()) { if (logger.isDebugEnabled()) {
logger.debug("forward d331 rtcm from {} to device {}", id, deviceId); logger.debug("forward d331 rtcm from {} to device {}", baseStationId, deviceId);
} }
ByteBuf buf = Unpooled.buffer(); ByteBuf buf = Unpooled.buffer();
buf.writeBytes(forwardBytes); 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); 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); * 处理设备类型1的特殊逻辑
*/
ByteBuf buf = Unpooled.buffer(); private ByteBuf handleDeviceType1(ByteBuf buf, Device device, Device deviceBs, String deviceId) {
buf.writeBytes(forwardBytes);
// 检查是否满足10秒转发间隔只有满足条件时才添加D300字符串
long currentTime = System.currentTimeMillis(); long currentTime = System.currentTimeMillis();
Long lastForwardTime = lastD300ForwardTimeMap.getOrDefault(deviceId, 0L); Long lastForwardTime = lastD300ForwardTimeMap.getOrDefault(deviceId, 0L);
if (currentTime - lastForwardTime >= D300_FORWARD_INTERVAL) { if (currentTime - lastForwardTime >= D300_FORWARD_INTERVAL) {
//logger.info("Adding D300 string for device: {}", deviceId);
// 获取当前buf中的数据
byte[] originalData = buf.array(); byte[] originalData = buf.array();
String originalHex = ByteUtil.bytesToHexString(originalData); String originalHex = ByteUtil.bytesToHexString(originalData);
// 找到D300和D301的位置
int d300Index = originalHex.indexOf("d300"); int d300Index = originalHex.indexOf("d300");
int d301Index = originalHex.indexOf("d301"); int d301Index = originalHex.indexOf("d301");
// 确定插入位置如果两个都存在取位置靠前的如果只存在一个就用那个位置
int insertIndex = -1; int insertIndex = -1;
if (d300Index != -1 && d301Index != -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) { } else if (d300Index != -1) {
insertIndex = d300Index; insertIndex = d300Index;
//logger.info("Found D300 at position {}", d300Index);
} else if (d301Index != -1) { } else if (d301Index != -1) {
insertIndex = d301Index; insertIndex = d301Index;
//logger.info("Found D301 at position {}", d301Index);
} }
if (insertIndex != -1) { if (insertIndex != -1) {
// 创建新的buf // 检查基站是否有有效的ECEF坐标
Double ecefX = deviceBs.getEcefx();
Double ecefY = deviceBs.getEcefy();
Double ecefZ = deviceBs.getEcefz();
// 验证坐标有效性
if (isValidEcefCoordinates(ecefX, ecefY, ecefZ)) {
ByteBuf newBuf = Unpooled.buffer(); ByteBuf newBuf = Unpooled.buffer();
// 写入D300/D301之前的数据
newBuf.writeBytes(originalData, 0, insertIndex / 2); newBuf.writeBytes(originalData, 0, insertIndex / 2);
// 使用f9p坐标生成1005消息并插入
double[] ecef = new double[3]; double[] ecef = new double[3];
ecef[0] = deviceBs.getEcefx(); ecef[0] = ecefX.doubleValue();
ecef[1] = deviceBs.getEcefy(); ecef[1] = ecefY.doubleValue();
ecef[2] = deviceBs.getEcefz(); ecef[2] = ecefZ.doubleValue();
String rtcm1005 = Rtcm1005.generateRtcm1005Hex(ecef, 0); String rtcm1005 = Rtcm1005.generateRtcm1005Hex(ecef, 0);
if (rtcm1005 != null) { if (rtcm1005 != null && !rtcm1005.isEmpty()) {
// 写入RTCM 1005消息
byte[] rtcm1005Bytes = ByteUtil.hexStringTobyte(rtcm1005); byte[] rtcm1005Bytes = ByteUtil.hexStringTobyte(rtcm1005);
newBuf.writeBytes(rtcm1005Bytes); newBuf.writeBytes(rtcm1005Bytes);
logger.info("Generated RTCM 1005 message for base station {}: {}", deviceBs.getDeviceId(), rtcm1005);
} else {
//logger.warn("Failed to generate RTCM 1005 message for base station: {}", deviceBs.getDeviceId());
} }
// 写入剩余的数据
newBuf.writeBytes(originalData, insertIndex / 2, originalData.length - insertIndex / 2); newBuf.writeBytes(originalData, insertIndex / 2, originalData.length - insertIndex / 2);
// 更新buf
buf = newBuf; buf = newBuf;
} else {
// 添加日志记录插入位置和完整数据 logger.warn("Base station {} has invalid ECEF coordinates, skipping RTCM 1005 generation for device {}",
// logger.info("Inserted RTCM 1005 message before position {}, complete data: {}", deviceBs.getDeviceId(), deviceId);
// insertIndex, }
// ByteUtil.bytesToHexString(buf.array()));
} }
// 更新最后转发时间
lastD300ForwardTimeMap.put(deviceId, currentTime); lastD300ForwardTimeMap.put(deviceId, currentTime);
// 添加日志记录测站转发的完整数据
//logger.info("Forward data to device: {}, time: {}, complete data: {}",
// deviceId,
// LocalDateTime.now(),
// ByteUtil.bytesToHexString(buf.array()));
} }
deviceChannel.writeAndFlush(buf); return buf;
}
}
}
}
} }
// 如果30分钟内收到不到d3f0和d3f2则根据UDP最后一个报文触发状态更新和统计 /**
* 处理设备统计信息
*/
private void processDeviceStatistics(Device deviceBs, D331RtcmMessage message) {
if (deviceBs.getD3xxbytes() > 0) { if (deviceBs.getD3xxbytes() > 0) {
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
if(deviceBs.getLastRxTime().isBefore(now.minusMinutes(1)) && if (deviceBs.getLastRxTime() != null &&
deviceBs.getLastRxTime().isBefore(now.minusMinutes(1)) &&
(deviceBs.getLastD3f2Time() == null || (deviceBs.getLastD3f2Time() == null ||
deviceBs.getLastD3f2Time().isBefore(now.minusMinutes(30)))) { 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(); Device lastCycleDevice = new Device();
lastCycleDevice.setDeviceId(deviceBs.getDeviceId()); lastCycleDevice.setDeviceId(deviceBs.getDeviceId());
lastCycleDevice.setDeviceType(deviceBs.getDeviceType()); lastCycleDevice.setDeviceType(deviceBs.getDeviceType());
@ -234,67 +612,202 @@ public class D331RtcmMessageExecutor implements Executor<D331RtcmMessage, Void>
lastCycleDevice.setLastRxTime(deviceBs.getLastRxTime()); lastCycleDevice.setLastRxTime(deviceBs.getLastRxTime());
lastCycleDevice.setLastValidCalcDataTime(deviceBs.getLastValidCalcDataTime()); lastCycleDevice.setLastValidCalcDataTime(deviceBs.getLastValidCalcDataTime());
lastCycleDevice.setSatelitesInUse(deviceBs.getSatelitesInUse()); lastCycleDevice.setSatelitesInUse(deviceBs.getSatelitesInUse());
deviceBs.clearStat(); return lastCycleDevice;
}
/**
* 记录日志如果需要
*/
private void logMessageIfNeeded(Device deviceBs, D331RtcmMessage message) {
ThreadManager.getFixedThreadPool().submit(() -> { ThreadManager.getFixedThreadPool().submit(() -> {
// 通知上线 if (deviceBs.getLoggingmode() != null && deviceBs.getLoggingmode() == GnssDevice.LOGGING_MODE_FULL) {
try { logger.info("receive {} d331 message: {}", message.getId(),
beidouClient.onDeviceActive(deviceBs.getDeviceId(), deviceBs.getTenantId()); DataTypeUtil.getHexString(message.getSrcData()));
} catch (Exception e) {
logger.error(e.toString());
}
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());
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(() -> {
// 原始码流输出到日志文件 -- 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);
});
} catch (Exception e) {
logger.error("处理NTRIP数据失败, 挂载点: {}, 错误: {}", mountpoint, e.getMessage());
}
} }
@Override @Override
public Class<?> getMessageType() { public Class<?> getMessageType() {
return D331RtcmMessage.class; 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;
}
} }

View File

@ -32,6 +32,9 @@ import java.util.TimerTask;
public class D3F0SelfCheckMessageExecutor implements Executor<D3F0SelfCheckMessage, Void> { public class D3F0SelfCheckMessageExecutor implements Executor<D3F0SelfCheckMessage, Void> {
private final Logger logger = LoggerFactory.getLogger(this.getClass()); private final Logger logger = LoggerFactory.getLogger(this.getClass());
final String F9PInitCmd = "f9";
final String GNSSPowerOff = "25 00";
final String GNSSPowerOn = "25 01";
final String F9PColdStartCmd = "b56206040400000002001068"; final String F9PColdStartCmd = "b56206040400000002001068";
final String BTCloseMneaCmd = "424be8e1020a00040000ffff"; final String BTCloseMneaCmd = "424be8e1020a00040000ffff";
final String BTOpenGGACmd = "424b852c0208000400000001"; final String BTOpenGGACmd = "424b852c0208000400000001";
@ -81,6 +84,11 @@ public class D3F0SelfCheckMessageExecutor implements Executor<D3F0SelfCheckMessa
checkAndResetBTGnss(device); checkAndResetBTGnss(device);
} }
} }
else{
if(device.getModel() == GnssDevice.MODEL_G510){
checkAndResetBTBsGnss(device);
}
}
// 通知beidou服务设备上线这里会触发参数同步 // 通知beidou服务设备上线这里会触发参数同步
GnssStatus lastGnssStatus = dataPersistService.getDeviceState(message.getId()); GnssStatus lastGnssStatus = dataPersistService.getDeviceState(message.getId());
try { try {
@ -172,9 +180,18 @@ public class D3F0SelfCheckMessageExecutor implements Executor<D3F0SelfCheckMessa
String sendCmd = Integer.toHexString(msgType) + HexUtil.Short2HexString(len)+ String sendCmd = Integer.toHexString(msgType) + HexUtil.Short2HexString(len)+
HexUtil.Int2HexString(Integer.parseInt(device.getDeviceId()))+gnssCmd; HexUtil.Int2HexString(Integer.parseInt(device.getDeviceId()))+gnssCmd;
rtcmClient.config(device.getDeviceId(), sendCmd); rtcmClient.config(device.getDeviceId(), sendCmd);
saveMsg(device.getDeviceId(), device.getTenantId(),0xD310, sendCmd, true); saveMsg(device.getDeviceId(), device.getTenantId(),msgType, sendCmd, true);
} }
//example: d31300050080F2B3f9
void sendDebugCommand(Device device, String cmd){
int msgType = 0xD313;
short len = (short) (cmd.length()/2+4);
String sendCmd = Integer.toHexString(msgType) + HexUtil.Short2HexString(len)+
HexUtil.Int2HexString(Integer.parseInt(device.getDeviceId()))+cmd;
rtcmClient.config(device.getDeviceId(), sendCmd);
saveMsg(device.getDeviceId(), device.getTenantId(),msgType, sendCmd, true);
}
void sendD3F1Ack(Device device){ void sendD3F1Ack(Device device){
int flag = 0xD3F1; int flag = 0xD3F1;
short len = (short) (4); short len = (short) (4);
@ -196,14 +213,22 @@ public class D3F0SelfCheckMessageExecutor implements Executor<D3F0SelfCheckMessa
} }
} }
private void checkAndResetBTBsGnss(Device device){
if(device.getD341Count() < 50){
startBTBsResetTask(device);
}
}
void startF9PColdStartTask(Device device){ void startF9PColdStartTask(Device device){
Timer timer = new Timer(); Timer timer = new Timer();
for(int delay=1; delay<=10; delay++) {
timer.schedule(new TimerTask() { timer.schedule(new TimerTask() {
@Override @Override
public void run() { public void run() {
sendGnssCommand(device, F9PColdStartCmd); sendGnssCommand(device, F9PColdStartCmd);
} }
},1000); }, delay*1500);
}
} }
void startBTHotStartTask(Device device){ void startBTHotStartTask(Device device){
@ -216,6 +241,22 @@ public class D3F0SelfCheckMessageExecutor implements Executor<D3F0SelfCheckMessa
},1000); },1000);
} }
void startBTBsResetTask(Device device){
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
sendGnssCommand(device, GNSSPowerOff);
}
},1000);
timer.schedule(new TimerTask() {
@Override
public void run() {
sendGnssCommand(device, GNSSPowerOn);
}
},11000);
}
void startBTResetTask(Device device){ void startBTResetTask(Device device){
Timer timer = new Timer(); Timer timer = new Timer();
timer.schedule(new TimerTask() { timer.schedule(new TimerTask() {
@ -255,4 +296,6 @@ public class D3F0SelfCheckMessageExecutor implements Executor<D3F0SelfCheckMessa
} }
},16000); },16000);
} }
} }

View File

@ -90,6 +90,8 @@ public class Device {
byte cfgChannelType = CHANNEL_TYPE_TCP; // 0:TCP;1:DUP byte cfgChannelType = CHANNEL_TYPE_TCP; // 0:TCP;1:DUP
byte dataChannelType = CHANNEL_TYPE_UDP; // 0:TCP;1:DUP byte dataChannelType = CHANNEL_TYPE_UDP; // 0:TCP;1:DUP
private String parentId1;
public void updateRx(int head, int bytes,int count){ public void updateRx(int head, int bytes,int count){
lastRxHead = head; lastRxHead = head;

View File

@ -11,4 +11,6 @@ public interface DeviceService {
Device findByDeviceId(String deviceId); Device findByDeviceId(String deviceId);
List<Device> findByParentId(String parentId); List<Device> findByParentId(String parentId);
List<Device> findByParentId1(String parentId1);
} }

View File

@ -51,6 +51,7 @@ public class LocalDeviceServiceImpl implements DeviceService {
device.setEcefy(gnssDevice.getEcefy()); device.setEcefy(gnssDevice.getEcefy());
device.setEcefz(gnssDevice.getEcefz()); device.setEcefz(gnssDevice.getEcefz());
device.setLoggingmode(gnssDevice.getLoggingmode()); device.setLoggingmode(gnssDevice.getLoggingmode());
device.setParentId1(gnssDevice.getParentid1());
return device; return device;
} }
@ -65,6 +66,19 @@ public class LocalDeviceServiceImpl implements DeviceService {
} }
return deviceList; 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 @Override
public Device findByDeviceId(String deviceId) { public Device findByDeviceId(String deviceId) {
Device device = deviceCache.getIfPresent(deviceId); Device device = deviceCache.getIfPresent(deviceId);
@ -79,23 +93,43 @@ public class LocalDeviceServiceImpl implements DeviceService {
@Override @Override
public List<Device> findByParentId(String parentId) { public List<Device> findByParentId(String parentId) {
List<Device> devices = subDeviceCache.getIfPresent(parentId); String cacheKey = "PARENTID_" + parentId;
List<Device> devices = subDeviceCache.getIfPresent(cacheKey);
if (devices == null) { if (devices == null) {
devices = getDeviceListFromDBByParentId(parentId); devices = getDeviceListFromDBByParentId(parentId);
if (devices != null) { if (devices != null) {
subDeviceCache.put(parentId, devices); subDeviceCache.put(cacheKey, devices);
} }
} }
return devices; return devices;
} }
public void refresh(String deviceId, String oldParentId){ @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){
Device device = deviceCache.getIfPresent(deviceId); Device device = deviceCache.getIfPresent(deviceId);
if (device != null) { if (device != null) {
if(device.getParentId()!=null) { if(device.getParentId()!=null) {
subDeviceCache.invalidate(device.getParentId()); subDeviceCache.invalidate("PARENTID_" + device.getParentId());
if(oldParentId!=null && !oldParentId.equals(device.getParentId())){ if(oldParentId!=null && !oldParentId.equals(device.getParentId())){
subDeviceCache.invalidate(oldParentId); subDeviceCache.invalidate("PARENTID_" + oldParentId);
}
}
if(device.getParentId1()!=null) {
subDeviceCache.invalidate("PARENTID1_" + device.getParentId1());
if(oldParentId1!=null && !oldParentId1.equals(device.getParentId1())){
subDeviceCache.invalidate("PARENTID1_" + oldParentId1);
} }
} }
deviceCache.invalidate(deviceId); deviceCache.invalidate(deviceId);

View File

@ -154,9 +154,9 @@ public class ApiController {
} }
@PostMapping("/device_param_changed") @PostMapping("/device_param_changed")
public HttpResp deviceParamChanged(String deviceId, String oldParentId) { public HttpResp deviceParamChanged(String deviceId, String oldParentId, String oldParentId1) {
// 更新设备缓存 // 更新设备缓存
localDeviceService.refresh(deviceId, oldParentId); localDeviceService.refresh(deviceId, oldParentId, oldParentId1);
calcService.refreshDeviceCalc(deviceId); calcService.refreshDeviceCalc(deviceId);
HttpResp resp = new HttpResp(); HttpResp resp = new HttpResp();

View File

@ -12,10 +12,12 @@ import org.springframework.web.bind.annotation.ResponseBody;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List;
@Controller @Controller
public class APIController extends BasicController { public class APIController extends BasicController {
SimpleDateFormat dateFormat = new SimpleDateFormat("yy-MM-dd HH:mm:ss"); SimpleDateFormat dateFormat = new SimpleDateFormat("yy-MM-dd HH:mm:ss");
final String verServerConnectCmd = "AT+QIOPEN=1,2,\"TCP\",\"iot.im-droid.com\",7665,0,1";
@Autowired @Autowired
RtcmClient rtcmClient; RtcmClient rtcmClient;
@Autowired @Autowired
@ -48,10 +50,15 @@ public class APIController extends BasicController{
device.setSyn(true); device.setSyn(true);
deviceMapper.updateById(device); deviceMapper.updateById(device);
} }
} } else if (msgType == 0xd312) {
else if(msgType == 0xd312){
// 工作周期一致性检查 // 工作周期一致性检查
checkWorkCycle(device, configAck); checkWorkCycle(device, configAck);
} else if (msgType == 0xd313) {
if(device.getChange_flag()!=0) {
if(device.clearChangeFlag(configAck)){
deviceMapper.updateById(device);
}
}
} }
// 保存, debug 01 02上来的原始数据不保存 // 保存, debug 01 02上来的原始数据不保存
@ -69,17 +76,15 @@ public class APIController extends BasicController{
// 检查是否需要更新 ICCID // 检查是否需要更新 ICCID
updateICCID(device, dtuAck); updateICCID(device, dtuAck);
} } else if (msgType == 0xd313 && configAck.length() >= 90) {
else if(msgType == 0xd313&&configAck.length()>=90){
//转成字符串 //转成字符串
String dtuAck = configAck.substring(26 * 2); String dtuAck = configAck.substring(26 * 2);
rxInfo += configAck + "(" + HexUtil.HexString2String(dtuAck) + ")"; rxInfo += configAck + "(" + HexUtil.HexString2String(dtuAck) + ")";
} } else {
else{
rxInfo += configAck; rxInfo += configAck;
} }
WebSocketServer.sendMessageToAll(rxInfo); CmdLineWebSocketServer.sendMessageToAll(rxInfo);
return null; return null;
} }
@ -153,7 +158,6 @@ public class APIController extends BasicController{
// 保存 // 保存
saveMsg(deviceId, tenantId, 0xD312, uploadCmd, true); saveMsg(deviceId, tenantId, 0xD312, uploadCmd, true);
return null; return null;
} }
@ -177,6 +181,13 @@ public class APIController extends BasicController{
} }
} }
} }
List<String> deviceCmds = device.getConfigCmd();
for(String deviceCmd:deviceCmds){
rtcmClient.config(deviceId, deviceCmd);
// 保存
saveMsg(deviceId, tenantId, 0xd312, deviceCmd, true);
}
// 检查有没有待发送的指令 // 检查有没有待发送的指令
QueryWrapper<DeviceCacheCmd> queryWrapper = new QueryWrapper<>(); QueryWrapper<DeviceCacheCmd> queryWrapper = new QueryWrapper<>();
@ -192,8 +203,10 @@ public class APIController extends BasicController{
// 保存 // 保存
saveMsg(deviceId, tenantId, cacheCmd.getMsgtype(), cacheCmd.getCmd(), true); saveMsg(deviceId, tenantId, cacheCmd.getMsgtype(), cacheCmd.getCmd(), true);
} }
// 设备上线后检查是否需要重新查询更新 ICCID
// 检查iccid
checkAndAskICCID(device); checkAndAskICCID(device);
return null; return null;
} }
@ -280,4 +293,39 @@ public class APIController extends BasicController{
deviceMapper.updateById(device); deviceMapper.updateById(device);
} }
} }
/****** upgrade operations *******/
@PostMapping(value = "/api/upgrade_ack")
@ResponseBody
public String onUpgradeAck() {
//发连接服务器指令
QueryWrapper<GnssDevice> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("opmode", GnssDevice.OP_MODE_UPGRADING);
List<GnssDevice> gnssDeviceList = deviceMapper.selectList(queryWrapper);
for (GnssDevice device : gnssDeviceList) {
short len = (short) (verServerConnectCmd.length() + 5);
String sendCmd = "d31a" + HexUtil.Short2HexString(len) +
HexUtil.Int2HexString(Integer.parseInt(device.getDeviceid())) +
"01" + HexUtil.String2HexString(verServerConnectCmd);
rtcmClient.config(device.getDeviceid(), sendCmd);
}
//界面的升级按钮变灰
UpgradeWebSocketServer.sendMessageToAll("on upgrade ack");
return null;
}
@PostMapping(value = "/api/upgrading_ind")
@ResponseBody
public String onUpgradingInd(String deviceId, Byte progress) {
UpgradeWebSocketServer.sendMessageToAll("on upgrade ind " + deviceId + "," + progress);
return null;
}
@PostMapping(value = "/api/upgrade_complete_ind")
@ResponseBody
public String onUpgradeCompleteInd(String deviceId, Byte code) {
UpgradeWebSocketServer.sendMessageToAll("on upgrade complete ind " + deviceId + "," + code);
// 保存升级记录
return null;
}
} }

View File

@ -12,13 +12,13 @@ import java.util.concurrent.ConcurrentHashMap;
//单例 //单例
@ServerEndpoint(value = "/websocket",configurator = WebSocketConfig.class) @ServerEndpoint(value = "/websocket/cmdline",configurator = WebSocketConfig.class)
@Component @Component
@Slf4j @Slf4j
public class WebSocketServer { public class CmdLineWebSocketServer {
// thread safety counter // thread safety counter
// thread safety set to hold websocket objects // thread safety set to hold websocket objects
private static Map<String,Session> webSocketSet = new ConcurrentHashMap<>(); private static final Map<String,Session> webSocketSet = new ConcurrentHashMap<>();
@OnOpen @OnOpen
public void onOpen(Session session){ public void onOpen(Session session){

View File

@ -109,6 +109,13 @@ public class GnssDeviceController extends BasicController{
tenants = tenantMapper.selectList(queryWrapper); tenants = tenantMapper.selectList(queryWrapper);
} }
GnssDevice device = gnssDeviceMapper.selectById(id); GnssDevice device = gnssDeviceMapper.selectById(id);
if(device.getDeviceid().startsWith("6") || device.getDeviceid().startsWith("7")){
if(!device.getHas_battery()){
device.setHas_battery(true);
device.setChange_flag(GnssDevice.PARA_MASK_HAS_BATTERY);
gnssDeviceMapper.updateById(device);
}
}
m.addAttribute("tenant_list", tenants); m.addAttribute("tenant_list", tenants);
m.addAttribute("gnss_group_list", gnssGroups); m.addAttribute("gnss_group_list", gnssGroups);
m.addAttribute("gnss_group_calc_list", gnssGroupCalcs); m.addAttribute("gnss_group_calc_list", gnssGroupCalcs);
@ -180,6 +187,17 @@ public class GnssDeviceController extends BasicController{
if (opmode != null && opmode != QUERY_ALL) { if (opmode != null && opmode != QUERY_ALL) {
queryWrapper.eq("opmode", opmode); queryWrapper.eq("opmode", opmode);
} }
Integer hasBattery = search.getInteger("has_battery");
if(hasBattery != null && hasBattery != QUERY_ALL){
if(hasBattery == 1) {
queryWrapper.and(wrapper -> wrapper.eq("has_battery", 1)
.or().likeRight("deviceid","6")
.or().likeRight("deviceid","7"));
}
else{
queryWrapper.eq("has_battery", 0);
}
}
//推送状 //推送状
Integer fwd = search.getInteger("fwd_group_id"); Integer fwd = search.getInteger("fwd_group_id");
if (fwd != null && fwd != QUERY_ALL) { if (fwd != null && fwd != QUERY_ALL) {
@ -245,6 +263,12 @@ public class GnssDeviceController extends BasicController{
if(!old_device.getGroup_id().equals(device.getGroup_id())){ if(!old_device.getGroup_id().equals(device.getGroup_id())){
device.setSyn(false); device.setSyn(false);
} }
if(diff.contains("has_battery")){
device.setChange_flag(GnssDevice.PARA_MASK_HAS_BATTERY);
}
if(diff.contains("voltage_factor")){
device.setChange_flag(GnssDevice.PARA_MASK_VOLTAGE_FACTOR);
}
num = gnssDeviceMapper.updateById(device); num = gnssDeviceMapper.updateById(device);
} }
} }
@ -263,7 +287,7 @@ public class GnssDeviceController extends BasicController{
} else { } else {
// 更新组参数的关联个数 // 更新组参数的关联个数
updateBasicGroupAssociatedNum(device,old_device); updateBasicGroupAssociatedNum(device,old_device);
rtcmClient.deviceParamChanged(device.getDeviceid(), device.getParentid()); rtcmClient.deviceParamChanged(device.getDeviceid(), old_device != null ? old_device.getParentid() : null, old_device != null ? old_device.getParentid1() : null);
return HttpResult.ok(); return HttpResult.ok();
} }
} }
@ -385,7 +409,7 @@ public class GnssDeviceController extends BasicController{
if (num == 0) { if (num == 0) {
return HttpResult.failed(); return HttpResult.failed();
} else{ } else{
rtcmClient.deviceParamChanged(del_id, null); rtcmClient.deviceParamChanged(del_id, null, null);
return HttpResult.ok(); return HttpResult.ok();
} }
} }

View File

@ -63,9 +63,13 @@ public class GnssGroupController extends BasicController {
@PostMapping("/gnss/group/update") @PostMapping("/gnss/group/update")
@ResponseBody @ResponseBody
public String update(HttpSession session, @RequestBody JSONObject object) throws Exception { public HttpResult update(HttpSession session, @RequestBody JSONObject object) throws Exception {
int num = 0; int num = 0;
GnssGroup group = JSONObject.toJavaObject(object,GnssGroup.class); GnssGroup group = JSONObject.toJavaObject(object,GnssGroup.class);
if(group.getActive_time()+ group.getActive_offset()>= group.getWork_cycle()){
return HttpResult.fail("参数组合错误!");
}
GnssGroup oldGroup = gnssGroupMapper.selectById(group.getId()); GnssGroup oldGroup = gnssGroupMapper.selectById(group.getId());
// no changed // no changed
@ -90,12 +94,12 @@ public class GnssGroupController extends BasicController {
group.getName()); group.getName());
} }
if (num == 0) { if (num == 0) {
return HttpResult.failed(); return HttpResult.fail("更新数据失败");
} else{ } else{
// 更新所有的device同步标志 // 更新所有的device同步标志
deviceMapper.setSynFlagByGroupId(group.getId()); deviceMapper.setSynFlagByGroupId(group.getId());
rtcmClient.groupParamChanged(); rtcmClient.groupParamChanged();
return HttpResult.ok(); return HttpResult.success("更新成功");
} }
} }

View File

@ -1,6 +1,7 @@
package com.imdroid.beidou.controller; package com.imdroid.beidou.controller;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.imdroid.beidou.service.CommonExcelService; import com.imdroid.beidou.service.CommonExcelService;
import com.imdroid.secapi.dto.GnssStatusMsg; import com.imdroid.secapi.dto.GnssStatusMsg;
@ -11,11 +12,13 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSession;
import java.util.*;
/** /**
* 状态消息 控制器 * 状态消息 控制器
@ -31,6 +34,10 @@ public class GnssMsgStatusController extends BasicController implements CommonEx
GnssStatusMsgMapper statusMsgMapper; GnssStatusMsgMapper statusMsgMapper;
@Autowired @Autowired
OpLogManager opLogManager; OpLogManager opLogManager;
// 滑动窗口大小
private static final int WINDOW_SIZE = 5;
@RequestMapping("/page/gnss_msg_status") @RequestMapping("/page/gnss_msg_status")
public String gnssStatusMsg(Model m, HttpSession session) { public String gnssStatusMsg(Model m, HttpSession session) {
initModel(m, session); initModel(m, session);
@ -40,7 +47,129 @@ public class GnssMsgStatusController extends BasicController implements CommonEx
@RequestMapping("/gnss/msg/status/list") @RequestMapping("/gnss/msg/status/list")
@ResponseBody @ResponseBody
public JSONObject listStatusMsg(HttpSession session, Integer page, Integer limit, String searchParams) { public JSONObject listStatusMsg(HttpSession session, Integer page, Integer limit, String searchParams) {
return this.pageList(session, page, limit, searchParams); JSONObject obj = this.pageList(session, page, limit, searchParams);
List<GnssStatusMsg> msgList = (List<GnssStatusMsg>) obj.get("data");
for(GnssStatusMsg msg:msgList){
if(msg.getChargecurrency()!=null && msg.getSolarvoltage()!=null){
if(msg.getVoltage()>msg.getSolarvoltage()+3000){
msg.setChargecurrency(0);
msg.setChargewatt(0.0F);
}
}
}
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;
} }
/** /**

View File

@ -0,0 +1,71 @@
package com.imdroid.beidou.controller;
import com.imdroid.beidou.config.WebSocketConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpSession;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
//单例
@ServerEndpoint(value = "/websocket/upgrade",configurator = WebSocketConfig.class)
@Component
@Slf4j
public class UpgradeWebSocketServer{
// thread safety counter
// thread safety set to hold websocket objects
private static final Map<String, Session> webSocketSet = new ConcurrentHashMap<>();
@OnOpen
public void onOpen(Session session){
HttpSession httpSession= (HttpSession) session.getUserProperties().get(HttpSession.class.getName());
webSocketSet.put(httpSession.getId(), session);
log.info("new websocket opened, total socket num: "+ webSocketSet.size());
}
@OnClose
public void onClose(Session session){
HttpSession httpSession= (HttpSession) session.getUserProperties().get(HttpSession.class.getName());
if(httpSession!=null) {
webSocketSet.remove(httpSession.getId());
}
log.info("websocket closed, total socket num: "+ webSocketSet.size());
}
@OnMessage
public void onMessage(String msg, Session session){
//log.info("websocket: "+msg);
}
@OnError
public void onError(Session session, Throwable error){
error.printStackTrace();
}
public static void sendMessageToAll(String msg){
try {
for (Session item : webSocketSet.values()) {
item.getBasicRemote().sendText(msg);
}
}
catch (Exception e){
log.error("websocket send msg error:", e);
}
}
public static void sendMessage(HttpSession httpSession, String msg) {
Session session = webSocketSet.get(httpSession.getId());
if(session != null){
try {
session.getBasicRemote().sendText(msg);
}
catch (Exception e){
log.error("websocket send msg error:", e);
}
}
}
}

View File

@ -0,0 +1,267 @@
package com.imdroid.beidou.controller;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.imdroid.beidou.common.HttpResult;
import com.imdroid.common.util.HexUtil;
import com.imdroid.secapi.client.HttpResp;
import com.imdroid.secapi.client.RtcmClient;
import com.imdroid.secapi.client.VersionClient;
import com.imdroid.secapi.dto.*;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpSession;
import java.io.File;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@Controller
public class VersionController extends BasicController{
@Value("${version.path}")
private String version_path;
@Autowired
GnssDeviceMapper gnssDeviceMapper;
@Autowired
private GnssStatusMapper gnssStatusMapper;
@Autowired
RtcmClient rtcmClient;
@Autowired
OpLogManager opLogManager;
@Autowired
TenantMapper tenantMapper;
@Autowired
VersionClient versionClient;
@Value("${ver_server.connect.cmd}")
String verServerConnectCmd;
@Data
public class FirmwareInfo{
String name;
long length;
long create_time;
public FirmwareInfo(String n, long l, long t){
name = n;
length = l;
create_time = t;
}
}
// 正在升级的设备mapdeviceId <-> UpgradeInfo(高字节升级结果低字节进度)
final Map<String, Short> upgradingDeviceList = new ConcurrentHashMap<>();
final Map<String, String> deviceSNtoFirmwareMap=new HashMap<>();
@PostConstruct
public void init(){
deviceSNtoFirmwareMap.put("2353","2353");
deviceSNtoFirmwareMap.put("2410","2410");
deviceSNtoFirmwareMap.put("2412","2410");
deviceSNtoFirmwareMap.put("2415","2415");
deviceSNtoFirmwareMap.put("2419","2415");
deviceSNtoFirmwareMap.put("2421","2421");
deviceSNtoFirmwareMap.put("2435","2421");
deviceSNtoFirmwareMap.put("2439","2439");
deviceSNtoFirmwareMap.put("2445","2439");
deviceSNtoFirmwareMap.put("8450","8450");
deviceSNtoFirmwareMap.put("8507","8450");
deviceSNtoFirmwareMap.put("8512","8450");
deviceSNtoFirmwareMap.put("8513","8450");
deviceSNtoFirmwareMap.put("8516","8515");
deviceSNtoFirmwareMap.put("8522","8515");
deviceSNtoFirmwareMap.put("9450","9450");
deviceSNtoFirmwareMap.put("9513","9450");
deviceSNtoFirmwareMap.put("9522","9515");
deviceSNtoFirmwareMap.put("6516","6515");
deviceSNtoFirmwareMap.put("6521","6515");
deviceSNtoFirmwareMap.put("6522","6515");
}
/**** 推送页面 *****/
@RequestMapping("/sys/ver_mgr")
public String gnssDevCfg(Model m, HttpSession session) {
initModel(m, session);
List<Tenant> tenants = tenantMapper.selectList(null);
m.addAttribute("tenant_list", tenants);
return "/page/gnss_ver_mgr";
}
/**** 推送数据 *****/
@RequestMapping("/sys/ver_mgr/firmware_list")
@ResponseBody
public JSONObject listFirmware(int page, int limit) {
List<FirmwareInfo> firmwareInfoList = new ArrayList();
int total = getFirmwareList(page, limit,firmwareInfoList);
//用PageInfo对结果进行包装,传入连续显示的页数
JSONObject jsonObject = new JSONObject();
jsonObject.put("code", 0);
jsonObject.put("msg", "");
jsonObject.put("count", total);
jsonObject.put("data", firmwareInfoList);
return jsonObject;
}
@RequestMapping("/sys/ver_mgr/gnss_device_list")
@ResponseBody
public JSONObject list(HttpSession session, int page, int limit, String searchParams) {
Page<GnssDevice> pageable = new Page<>(page, limit);
QueryWrapper<GnssDevice> queryWrapper = new QueryWrapper<>();
queryWrapper.orderByAsc("deviceid");
// 条件查询
if(searchParams != null) {
JSONObject search = (JSONObject) JSONObject.parse(searchParams);
//设备号
String deviceid = search.getString("deviceid");
if (StringUtils.hasText(deviceid)) {
queryWrapper.like("deviceid", deviceid);
}
//项目号
String project_id = search.getString("project_id");
if (StringUtils.hasText(project_id)) {
queryWrapper.like("project_id", project_id);
}
//所属组织
String tenantname = search.getString("tenantname");
if (StringUtils.hasText(tenantname)) {
if(tenantname.startsWith("非SAAS"))
queryWrapper.ne("tenantname",Tenant.SAAS_PROVIDER_NAME);
else queryWrapper.like("tenantname", tenantname);
}
//桩号
String sector = search.getString("sector");
if (StringUtils.hasText(sector)) {
queryWrapper.like("sector", sector);
}
//设备类型
Integer devicetype = search.getInteger("devicetype");
if (devicetype != null && devicetype != QUERY_ALL) {
queryWrapper.eq("devicetype", devicetype);
}
//版本
String appver = search.getString("appver");
if (StringUtils.hasText(appver)) {
queryWrapper.like("appver", appver);
}
//备注
String remark = search.getString("remark");
if (StringUtils.hasText(remark)) {
queryWrapper.like("remark", remark);
}
//使用状态
Integer opmode = search.getInteger("opmode");
if (opmode != null && opmode != QUERY_ALL) {
queryWrapper.eq("opmode", opmode);
}
}
IPage<GnssDevice> cs = gnssDeviceMapper.selectPage(pageable, queryWrapper);
JSONObject jsonObject = new JSONObject();
jsonObject.put("code", 0);
jsonObject.put("msg", "");
jsonObject.put("count", cs.getTotal());
jsonObject.put("data", cs.getRecords());
return jsonObject;
}
@PostMapping("/sys/ver_mgr/upgrade")
@ResponseBody
public HttpResult upgradeApp(@RequestParam String firmware, @RequestParam String id_list) throws Exception {
HttpResp rsp;
if(firmware!=null && !firmware.equals("undefined")) {
String[] deviceList = id_list.split(";");
//请求版本服务升级
try {
String invalidId = checkUpgradeList(deviceList, firmware);
if(invalidId==null) {
rsp = versionClient.upgrade(id_list, firmware);
}
else return HttpResult.fail("设备型号和固件不匹配:"+invalidId);
//System.out.println(id_list);
}
catch (Exception e){
return HttpResult.fail("版本服务未启动");
}
if(rsp.getCode() == HttpResp.HTTP_RSP_OK){
//设置设备的使用状态为升级下发连接版本服务器指令
startUpgrade(deviceList);
return HttpResult.success("开始升级");
}
else{
return HttpResult.fail(rsp.getResponseMessage());
}
}
return HttpResult.fail("非法固件");
}
int getFirmwareList(int page, int limit, List firmwareInfo){
File folder = new File(version_path);
File[] files = folder.listFiles((file) -> file.getName().endsWith(".bin"));
if(files==null) return 0;
Arrays.sort(files, new Comparator<File>() {
public int compare(File f1, File f2) {
long diff = f1.lastModified() - f2.lastModified();
if (diff > 0)
return -1;
else if (diff == 0)
return 0;
else
return 1;//如果 if 中修改为 返回-1 同时此处修改为返回 1 排序就会是递减
}
public boolean equals(Object obj) {
return true;
}
});
int count = 0;
for(File file:files){
if((int)count/limit+1 == page) {
firmwareInfo.add(new FirmwareInfo(file.getName(), file.length(), file.lastModified()));
}
count++;
}
return files.length;
}
String checkUpgradeList(String[] deviceList, String firmware){
for(String deviceId:deviceList){
String snHigh = deviceId.substring(0,4);
if(!firmware.contains(deviceSNtoFirmwareMap.get(snHigh))){
return deviceId;
}
}
return null;
}
void startUpgrade(String[] deviceList){
for(String deviceId:deviceList){
UpdateWrapper<GnssDevice> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("deviceid",deviceId).set("opmode",GnssDevice.OP_MODE_UPGRADING);
gnssDeviceMapper.update(null, updateWrapper);
rtcmClient.config(deviceId,getConnectVerServerCmd(deviceId));
}
}
String getConnectVerServerCmd(String deviceId){
short len = (short) (verServerConnectCmd.length() + 5);
return "d31a" + HexUtil.Short2HexString(len)+
HexUtil.Int2HexString(Integer.parseInt(deviceId))+
"01"+HexUtil.String2HexString(verServerConnectCmd);
}
}

View File

@ -199,8 +199,12 @@ public interface CommonExcelService<T, R> {
* @param paraValue 查询参数值 * @param paraValue 查询参数值
*/ */
default void setQueryWrapper(AbstractWrapper queryWrapper, String paraName, Object paraValue) { default void setQueryWrapper(AbstractWrapper queryWrapper, String paraName, Object paraValue) {
// null
if (paraName.startsWith("NULL") && StringUtils.hasText((String)paraValue)) {
addNullQueryWrapper(queryWrapper, paraName, paraValue);
}
// String // String
if (paraName.startsWith("s") && StringUtils.hasText((String)paraValue)) { else if (paraName.startsWith("s") && StringUtils.hasText((String)paraValue)) {
addStringQueryWrapper(queryWrapper, paraName, paraValue); addStringQueryWrapper(queryWrapper, paraName, paraValue);
} }
// Number,-1表示全部 // Number,-1表示全部
@ -221,6 +225,17 @@ public interface CommonExcelService<T, R> {
return paraName.substring(paraName.indexOf("_") + 1); return paraName.substring(paraName.indexOf("_") + 1);
} }
default void addNullQueryWrapper(AbstractWrapper queryWrapper, String paraName, Object paraValue) {
String column = getColumn(paraName);
String value = paraValue.toString();
if (value.equals("0")) {
queryWrapper.isNull(column);
}
else if(value.equals("1")){
queryWrapper.isNotNull(column);
}
}
default void addStringQueryWrapper(AbstractWrapper queryWrapper, String paraName, Object paraValue) { default void addStringQueryWrapper(AbstractWrapper queryWrapper, String paraName, Object paraValue) {
String column = getColumn(paraName); String column = getColumn(paraName);
String value = paraValue.toString(); String value = paraValue.toString();

View File

@ -69,6 +69,9 @@ CREATE TABLE IF NOT EXISTS `gnssdevices` (
`model` smallint DEFAULT 0, `model` smallint DEFAULT 0,
`loggingmode` smallint DEFAULT 0 COMMENT '日志模式: 0-精简模式(仅D3F0和D3F2), 1-完整模式', `loggingmode` smallint DEFAULT 0 COMMENT '日志模式: 0-精简模式(仅D3F0和D3F2), 1-完整模式',
`iccid` VARCHAR(36) DEFAULT NULL COMMENT 'ICCID号唯一', `iccid` VARCHAR(36) DEFAULT NULL COMMENT 'ICCID号唯一',
`has_battery` bit(1) DEFAULT 0 COMMENT '是否内置电池',
`change_flag` int DEFAULT 0 COMMENT '参数改变标识',
`voltage_factor` tinyint DEFAULT NULL COMMENT '分压比',
PRIMARY KEY (`id`) PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View File

@ -24,7 +24,7 @@
{ {
"title": "健康检查报告", "title": "健康检查报告",
"href": "page/gnss_ehm", "href": "page/gnss_ehm",
"icon": "fa fa-tachometer", "icon": "fa fa-heartbeat",
"target": "_self" "target": "_self"
}, },
{ {
@ -212,6 +212,12 @@
"href": "sys/apikey", "href": "sys/apikey",
"icon": "fa fa-key", "icon": "fa fa-key",
"target": "_self" "target": "_self"
},
{
"title": "版本管理",
"href": "sys/ver_mgr",
"icon": "fa fa-file-code-o",
"target": "_self"
} }
] ]
} }

View File

@ -0,0 +1,354 @@
:root,
:host {
--ol-background-color: white;
--ol-accent-background-color: #F5F5F5;
--ol-subtle-background-color: rgba(128, 128, 128, 0.25);
--ol-partial-background-color: rgba(255, 255, 255, 0.75);
--ol-foreground-color: #333333;
--ol-subtle-foreground-color: #666666;
--ol-brand-color: #00AAFF;
}
.ol-box {
box-sizing: border-box;
border-radius: 2px;
border: 1.5px solid var(--ol-background-color);
background-color: var(--ol-partial-background-color);
}
.ol-mouse-position {
top: 8px;
right: 8px;
position: absolute;
}
.ol-scale-line {
background: var(--ol-partial-background-color);
border-radius: 4px;
bottom: 8px;
left: 8px;
padding: 2px;
position: absolute;
}
.ol-scale-line-inner {
border: 1px solid var(--ol-subtle-foreground-color);
border-top: none;
color: var(--ol-foreground-color);
font-size: 10px;
text-align: center;
margin: 1px;
will-change: contents, width;
transition: all 0.25s;
}
.ol-scale-bar {
position: absolute;
bottom: 8px;
left: 8px;
}
.ol-scale-bar-inner {
display: flex;
}
.ol-scale-step-marker {
width: 1px;
height: 15px;
background-color: var(--ol-foreground-color);
float: right;
z-index: 10;
}
.ol-scale-step-text {
position: absolute;
bottom: -5px;
font-size: 10px;
z-index: 11;
color: var(--ol-foreground-color);
text-shadow: -1.5px 0 var(--ol-partial-background-color), 0 1.5px var(--ol-partial-background-color), 1.5px 0 var(--ol-partial-background-color), 0 -1.5px var(--ol-partial-background-color);
}
.ol-scale-text {
position: absolute;
font-size: 12px;
text-align: center;
bottom: 25px;
color: var(--ol-foreground-color);
text-shadow: -1.5px 0 var(--ol-partial-background-color), 0 1.5px var(--ol-partial-background-color), 1.5px 0 var(--ol-partial-background-color), 0 -1.5px var(--ol-partial-background-color);
}
.ol-scale-singlebar {
position: relative;
height: 10px;
z-index: 9;
box-sizing: border-box;
border: 1px solid var(--ol-foreground-color);
}
.ol-scale-singlebar-even {
background-color: var(--ol-subtle-foreground-color);
}
.ol-scale-singlebar-odd {
background-color: var(--ol-background-color);
}
.ol-unsupported {
display: none;
}
.ol-viewport,
.ol-unselectable {
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
.ol-viewport canvas {
all: unset;
overflow: hidden;
}
.ol-viewport {
touch-action: pan-x pan-y;
}
.ol-selectable {
-webkit-touch-callout: default;
-webkit-user-select: text;
-moz-user-select: text;
user-select: text;
}
.ol-grabbing {
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
.ol-grab {
cursor: move;
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.ol-control {
position: absolute;
background-color: var(--ol-subtle-background-color);
border-radius: 4px;
}
.ol-zoom {
top: .5em;
left: .5em;
}
.ol-rotate {
top: .5em;
right: .5em;
transition: opacity .25s linear, visibility 0s linear;
}
.ol-rotate.ol-hidden {
opacity: 0;
visibility: hidden;
transition: opacity .25s linear, visibility 0s linear .25s;
}
.ol-zoom-extent {
top: 4.643em;
left: .5em;
}
.ol-full-screen {
right: .5em;
top: .5em;
}
.ol-control button {
display: block;
margin: 1px;
padding: 0;
color: var(--ol-subtle-foreground-color);
font-weight: bold;
text-decoration: none;
font-size: inherit;
text-align: center;
height: 1.375em;
width: 1.375em;
line-height: .4em;
background-color: var(--ol-background-color);
border: none;
border-radius: 2px;
}
.ol-control button::-moz-focus-inner {
border: none;
padding: 0;
}
.ol-zoom-extent button {
line-height: 1.4em;
}
.ol-compass {
display: block;
font-weight: normal;
will-change: transform;
}
.ol-touch .ol-control button {
font-size: 1.5em;
}
.ol-touch .ol-zoom-extent {
top: 5.5em;
}
.ol-control button:hover,
.ol-control button:focus {
text-decoration: none;
outline: 1px solid var(--ol-subtle-foreground-color);
color: var(--ol-foreground-color);
}
.ol-zoom .ol-zoom-in {
border-radius: 2px 2px 0 0;
}
.ol-zoom .ol-zoom-out {
border-radius: 0 0 2px 2px;
}
.ol-attribution {
text-align: right;
bottom: .5em;
right: .5em;
max-width: calc(100% - 1.3em);
display: flex;
flex-flow: row-reverse;
align-items: center;
}
.ol-attribution a {
color: var(--ol-subtle-foreground-color);
text-decoration: none;
}
.ol-attribution ul {
margin: 0;
padding: 1px .5em;
color: var(--ol-foreground-color);
text-shadow: 0 0 2px var(--ol-background-color);
font-size: 12px;
}
.ol-attribution li {
display: inline;
list-style: none;
}
.ol-attribution li:not(:last-child):after {
content: " ";
}
.ol-attribution img {
max-height: 2em;
max-width: inherit;
vertical-align: middle;
}
.ol-attribution button {
flex-shrink: 0;
}
.ol-attribution.ol-collapsed ul {
display: none;
}
.ol-attribution:not(.ol-collapsed) {
background: var(--ol-partial-background-color);
}
.ol-attribution.ol-uncollapsible {
bottom: 0;
right: 0;
border-radius: 4px 0 0;
}
.ol-attribution.ol-uncollapsible img {
margin-top: -.2em;
max-height: 1.6em;
}
.ol-attribution.ol-uncollapsible button {
display: none;
}
.ol-zoomslider {
top: 4.5em;
left: .5em;
height: 200px;
}
.ol-zoomslider button {
position: relative;
height: 10px;
}
.ol-touch .ol-zoomslider {
top: 5.5em;
}
.ol-overviewmap {
left: 0.5em;
bottom: 0.5em;
}
.ol-overviewmap.ol-uncollapsible {
bottom: 0;
left: 0;
border-radius: 0 4px 0 0;
}
.ol-overviewmap .ol-overviewmap-map,
.ol-overviewmap button {
display: block;
}
.ol-overviewmap .ol-overviewmap-map {
border: 1px solid var(--ol-subtle-foreground-color);
height: 150px;
width: 150px;
}
.ol-overviewmap:not(.ol-collapsed) button {
bottom: 0;
left: 0;
position: absolute;
}
.ol-overviewmap.ol-collapsed .ol-overviewmap-map,
.ol-overviewmap.ol-uncollapsible button {
display: none;
}
.ol-overviewmap:not(.ol-collapsed) {
background: var(--ol-subtle-background-color);
}
.ol-overviewmap-box {
border: 1.5px dotted var(--ol-subtle-foreground-color);
}
.ol-overviewmap .ol-overviewmap-box:hover {
cursor: move;
}
.ol-overviewmap .ol-viewport:hover {
cursor: pointer;
}

View File

@ -0,0 +1,83 @@
/**
* 坐标转换工具模块
* 提供WGS84和GCJ-02坐标系之间的转换功能
*/
var CoordinateUtils = (function() {
'use strict';
var pi = 3.14159265358979324;
var a = 6378245.0;
var ee = 0.00669342162296594323;
/**
* 判断是否在国内不在国内则不做偏移
*/
function outOfChina(lon, lat) {
if ((lon < 72.004 || lon > 137.8347) && (lat < 0.8293 || lat > 55.8271)) {
return true;
} else {
return false;
}
}
function transformLat(x, y) {
var ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x));
ret += (20.0 * Math.sin(6.0 * x * pi) + 20.0 * Math.sin(2.0 * x * pi)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(y * pi) + 40.0 * Math.sin(y / 3.0 * pi)) * 2.0 / 3.0;
ret += (160.0 * Math.sin(y / 12.0 * pi) + 320 * Math.sin(y * pi / 30.0)) * 2.0 / 3.0;
return ret;
}
function transformLon(x, y) {
var ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x));
ret += (20.0 * Math.sin(6.0 * x * pi) + 20.0 * Math.sin(2.0 * x * pi)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(x * pi) + 40.0 * Math.sin(x / 3.0 * pi)) * 2.0 / 3.0;
ret += (150.0 * Math.sin(x / 12.0 * pi) + 300.0 * Math.sin(x / 30.0 * pi)) * 2.0 / 3.0;
return ret;
}
function transform(wgLat, wgLon) {
var mars_point = {lon: 0, lat: 0};
if (outOfChina(wgLon, wgLat)) {
mars_point.lat = wgLat;
mars_point.lon = wgLon;
return mars_point;
}
var dLat = transformLat(wgLon - 105.0, wgLat - 35.0);
var dLon = transformLon(wgLon - 105.0, wgLat - 35.0);
var radLat = wgLat / 180.0 * pi;
var magic = Math.sin(radLat);
magic = 1 - ee * magic * magic;
var sqrtMagic = Math.sqrt(magic);
dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * pi);
dLon = (dLon * 180.0) / (a / sqrtMagic * Math.cos(radLat) * pi);
mars_point.lat = wgLat + dLat;
mars_point.lon = wgLon + dLon;
return mars_point;
}
function getMapCoordinates(lat, lon, mapType) {
var coordinates;
if (mapType === 'amap' || mapType === 'amap_satellite') {
// 高德地图 WGS84 转换为 GCJ-02
var gcjCoord = transform(lat, lon);
coordinates = ol.proj.fromLonLat([gcjCoord.lon, gcjCoord.lat]);
} else if (mapType.startsWith('google_')) {
// Google地图使用WGS84坐标系
coordinates = ol.proj.fromLonLat([lon, lat]);
} else {
// 天地图 CGCS2000与WGS84实质一样
coordinates = ol.proj.fromLonLat([lon, lat]);
}
return coordinates;
}
return {
transform: transform,
getMapCoordinates: getMapCoordinates,
outOfChina: outOfChina
};
})();

View File

@ -0,0 +1,369 @@
/**
* 设备标记管理模块
* 负责设备标记的创建分类和显示管理
*/
var DeviceMarkers = (function() {
'use strict';
var greenFeatures = [];
var orangeFeatures = [];
var redFeatures = [];
var allFeatures = [];
var myLocationFeature = null;
var myLocationInterval = null;
var showDeviceId = true;
var minZoomForLabels = 4;
var map = null;
var vectorSource = null;
var vectorLayer = null;
function init(mapInstance, vectorSourceInstance, vectorLayerInstance) {
map = mapInstance;
vectorSource = vectorSourceInstance;
vectorLayer = vectorLayerInstance;
}
function createDeviceStyle(feature) {
if (feature.get('isMyLocation')) {
return new ol.style.Style({
image: new ol.style.Icon({
anchor: [0.5, 1],
src: '../images/loc_blue.png',
scale: 0.7
})
});
}
var deviceInfo = feature.get('deviceInfo');
if (!deviceInfo) return null;
var iconSrc;
var color = '#000';
var isHovered = feature.get('hovered') === true;
var scale = isHovered ? 0.85 : 0.7;
// 根据告警级别选择图标
if (deviceInfo.warning == 2) {
iconSrc = '../images/loc1_red.png';
} else if (deviceInfo.warning == 1) {
iconSrc = '../images/loc1_orange.png';
} else {
iconSrc = '../images/loc1_green.png';
}
var style = new ol.style.Style({
image: new ol.style.Icon({
anchor: [0.5, 1],
src: iconSrc,
scale: scale
})
});
// 根据缩放级别和设置决定是否显示设备ID
if (showDeviceId && map && map.getView().getZoom() >= minZoomForLabels) {
style.setText(new ol.style.Text({
text: deviceInfo.deviceid,
offsetY: -30,
fill: new ol.style.Fill({
color: isHovered ? '#1aa094' : color
}),
stroke: new ol.style.Stroke({
color: '#fff',
width: isHovered ? 3 : 2
}),
font: isHovered ? 'bold 12px Arial' : '12px Arial'
}));
}
return style;
}
function addDeviceMarkers(deviceList) {
if (!vectorSource || !deviceList) return;
var savedMyLocationFeature = myLocationFeature;
vectorSource.clear();
greenFeatures = [];
orangeFeatures = [];
redFeatures = [];
allFeatures = [];
for (var i = 0; i < deviceList.length; i++) {
var device = deviceList[i];
var currentMapType = getCurrentMapType();
var mapCoordinates = CoordinateUtils.getMapCoordinates(
device.latitude,
device.longitude,
currentMapType
);
var feature = new ol.Feature({
geometry: new ol.geom.Point(mapCoordinates),
deviceInfo: device
});
// 按告警级别分类
if (device.warning == 2) {
redFeatures.push(feature);
} else if (device.warning == 1) {
orangeFeatures.push(feature);
} else {
greenFeatures.push(feature);
}
allFeatures.push(feature);
vectorSource.addFeature(feature);
}
if (savedMyLocationFeature) {
vectorSource.addFeature(savedMyLocationFeature);
}
// 强制更新样式
if (vectorLayer) {
vectorLayer.changed();
}
}
function getCurrentMapType() {
var mapTypeSelect = document.getElementById('mapTypeSelectNew');
return mapTypeSelect ? mapTypeSelect.value : 'tianditu_satellite';
}
function showAllDevices() {
if (!vectorSource) return;
var savedMyLocationFeature = myLocationFeature;
vectorSource.clear();
if (savedMyLocationFeature) {
vectorSource.addFeature(savedMyLocationFeature);
}
for (var i = 0; i < allFeatures.length; i++) {
vectorSource.addFeature(allFeatures[i]);
}
}
function showWarning1Devices() {
if (!vectorSource) return;
var savedMyLocationFeature = myLocationFeature;
vectorSource.clear();
if (savedMyLocationFeature) {
vectorSource.addFeature(savedMyLocationFeature);
}
for (var i = 0; i < allFeatures.length; i++) {
vectorSource.addFeature(allFeatures[i]);
}
hideGreenFeatures();
hideRedFeatures();
}
function showWarning2Devices() {
if (!vectorSource) return;
var savedMyLocationFeature = myLocationFeature;
vectorSource.clear();
if (savedMyLocationFeature) {
vectorSource.addFeature(savedMyLocationFeature);
}
for (var i = 0; i < allFeatures.length; i++) {
vectorSource.addFeature(allFeatures[i]);
}
hideGreenFeatures();
hideOrangeFeatures();
}
function hideGreenFeatures() {
for (var i = 0; i < greenFeatures.length; i++) {
vectorSource.removeFeature(greenFeatures[i]);
}
}
function hideOrangeFeatures() {
for (var i = 0; i < orangeFeatures.length; i++) {
vectorSource.removeFeature(orangeFeatures[i]);
}
}
function hideRedFeatures() {
for (var i = 0; i < redFeatures.length; i++) {
vectorSource.removeFeature(redFeatures[i]);
}
}
function findDeviceById(deviceId) {
// console.log('搜索设备:', deviceId, '(类型:', typeof deviceId, ')');
// console.log('当前设备总数:', allFeatures.length);
var searchTerm = String(deviceId).trim();
for (var i = 0; i < allFeatures.length; i++) {
var feature = allFeatures[i];
var deviceInfo = feature.get('deviceInfo');
if (deviceInfo) {
var currentDeviceId = String(deviceInfo.deviceid);
// console.log('检查设备:', currentDeviceId, '(类型:', typeof deviceInfo.deviceid, ')');
if (currentDeviceId === searchTerm) {
// console.log('精确匹配找到设备:', currentDeviceId);
return feature;
}
if (currentDeviceId.indexOf(searchTerm) !== -1) {
// console.log('部分匹配找到设备:', currentDeviceId);
return feature;
}
if (currentDeviceId.replace(/\s+/g, '') === searchTerm.replace(/\s+/g, '')) {
// console.log('去除空格后匹配找到设备:', currentDeviceId);
return feature;
}
}
}
console.log('未找到设备:', deviceId);
return null;
}
function locateDevice(deviceId) {
var feature = findDeviceById(deviceId);
if (feature && map) {
var geometry = feature.getGeometry();
var coordinates = geometry.getCoordinates();
map.getView().animate({
center: coordinates,
zoom: Math.max(map.getView().getZoom(), 15),
duration: 1000
});
return true;
}
return false;
}
/**
* 获取我的位置
*/
function getMyLocation() {
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
});
},
function(error) {
console.error('获取位置失败:', error);
}
);
}
}
function startLocationUpdates() {
if (navigator.geolocation) {
myLocationInterval = setInterval(function() {
getMyLocation();
}, 30000);
}
}
function stopLocationUpdates() {
if (myLocationInterval) {
clearInterval(myLocationInterval);
myLocationInterval = null;
}
}
function updateMyLocationForMapType(mapType) {
if (myLocationFeature) {
var geometry = myLocationFeature.getGeometry();
var coordinates = geometry.getCoordinates();
var lonLat = ol.proj.toLonLat(coordinates);
var newCoordinates = CoordinateUtils.getMapCoordinates(lonLat[1], lonLat[0], mapType);
myLocationFeature.setGeometry(new ol.geom.Point(newCoordinates));
}
}
function setShowDeviceId(show) {
showDeviceId = show;
if (vectorLayer) {
vectorLayer.changed();
}
}
function getDeviceStats() {
return {
total: allFeatures.length,
green: greenFeatures.length,
orange: orangeFeatures.length,
red: redFeatures.length
};
}
return {
init: init,
createDeviceStyle: createDeviceStyle,
addDeviceMarkers: addDeviceMarkers,
showAllDevices: showAllDevices,
showWarning1Devices: showWarning1Devices,
showWarning2Devices: showWarning2Devices,
hideGreenFeatures: hideGreenFeatures,
hideOrangeFeatures: hideOrangeFeatures,
hideRedFeatures: hideRedFeatures,
findDeviceById: findDeviceById,
locateDevice: locateDevice,
getMyLocation: getMyLocation,
startLocationUpdates: startLocationUpdates,
stopLocationUpdates: stopLocationUpdates,
updateMyLocationForMapType: updateMyLocationForMapType,
setShowDeviceId: setShowDeviceId,
getDeviceStats: getDeviceStats,
getAllFeatures: function() { return allFeatures; },
getGreenFeatures: function() { return greenFeatures; },
getOrangeFeatures: function() { return orangeFeatures; },
getRedFeatures: function() { return redFeatures; },
getMyLocationFeature: function() { return myLocationFeature; }
};
})();

View File

@ -0,0 +1,218 @@
/**
* 设备总览主入口文件
* 负责整个设备总览模块的初始化和全局函数暴露
*/
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
return {
init: init,
// 地图相关
onMapTypeChange: onMapTypeChange,
switchMapType: switchMapType,
// 搜索和过滤
searchDeviceNew: searchDeviceNew,
onWarningFilterChange: onWarningFilterChange,
queryDevices: queryDevices,
locateDeviceOnMap: locateDeviceOnMap,
locateDeviceDirectly: locateDeviceDirectly,
// 地图功能
toggleMapFunctionsMenu: toggleMapFunctionsMenu,
toggleDeviceId: toggleDeviceId,
toggleCluster: toggleCluster,
// 测距工具
toggleMeasureDistance: toggleMeasureDistance,
finishMeasuring: finishMeasuring,
clearMeasure: clearMeasure,
// 天气预报
toggleWeatherForecast: toggleWeatherForecast,
closeWeatherCard: closeWeatherCard,
showPrevForecast: showPrevForecast,
showNextForecast: showNextForecast
};
})();
// 将主要函数暴露到全局作用域供HTML中的onclick等使用
window.DeviceOverview = DeviceOverview;
window.onMapTypeChange = DeviceOverview.onMapTypeChange;
window.searchDeviceNew = DeviceOverview.searchDeviceNew;
window.onWarningFilterChange = DeviceOverview.onWarningFilterChange;
window.toggleMapFunctionsMenu = DeviceOverview.toggleMapFunctionsMenu;
window.toggleDeviceId = DeviceOverview.toggleDeviceId;
window.toggleCluster = DeviceOverview.toggleCluster;
window.toggleMeasureDistance = DeviceOverview.toggleMeasureDistance;
window.finishMeasuring = DeviceOverview.finishMeasuring;
window.clearMeasure = DeviceOverview.clearMeasure;
window.toggleWeatherForecast = DeviceOverview.toggleWeatherForecast;
window.closeWeatherCard = DeviceOverview.closeWeatherCard;
window.showPrevForecast = DeviceOverview.showPrevForecast;
window.showNextForecast = DeviceOverview.showNextForecast;
window.queryDevices = DeviceOverview.queryDevices;
window.locateDeviceOnMap = DeviceOverview.locateDeviceOnMap;
window.locateDeviceDirectly = DeviceOverview.locateDeviceDirectly;

View File

@ -0,0 +1,360 @@
/**
* 地图核心模块
* 负责地图初始化事件处理和核心功能管理
*/
var MapCore = (function() {
'use strict';
var map = null;
var vectorSource = null;
var vectorLayer = null;
var clusterSource = null;
var clusterLayer = null;
var currentBaseLayer = null;
var showDeviceId = true;
var showCluster = true;
var minZoomForLabels = 4;
var maxZoomForClustering = 8;
var hoveredFeature = null;
function initialize(deviceList) {
// 创建矢量数据源和图层
vectorSource = new ol.source.Vector();
vectorLayer = new ol.layer.Vector({
source: vectorSource,
style: function(feature) {
return DeviceMarkers.createDeviceStyle(feature);
}
});
// 创建集群数据源和图层
clusterSource = new ol.source.Cluster({
distance: 40,
source: vectorSource
});
clusterLayer = new ol.layer.Vector({
source: clusterSource,
style: function(feature) {
var size = feature.get('features').length;
var style = new ol.style.Style({
image: new ol.style.Circle({
radius: 15,
fill: new ol.style.Fill({
color: '#3399CC'
})
}),
text: new ol.style.Text({
text: size.toString(),
fill: new ol.style.Fill({
color: '#fff'
})
})
});
return style;
}
});
var initialMapType = document.getElementById('mapTypeSelectNew').value || 'tianditu_satellite';
currentBaseLayer = MapLayers.getLayer(initialMapType);
map = new ol.Map({
target: 'map-container',
layers: [
currentBaseLayer,
clusterLayer,
vectorLayer
],
view: new ol.View({
center: ol.proj.fromLonLat([116.404, 39.915]),
zoom: 7
})
});
DeviceMarkers.init(map, vectorSource, vectorLayer);
MeasureTools.init(map);
WeatherForecast.init();
var scaleLineControl = new ol.control.ScaleLine();
map.addControl(scaleLineControl);
var initialZoom = map.getView().getZoom();
updateLayerVisibility(initialZoom);
map.getView().on('change:resolution', function() {
var zoom = map.getView().getZoom();
updateLayerVisibility(zoom);
vectorLayer.changed();
});
bindMouseEvents();
if (deviceList && deviceList.length > 0) {
setCenterFromDevices(deviceList);
}
DeviceMarkers.addDeviceMarkers(deviceList);
DeviceMarkers.getMyLocation();
DeviceMarkers.startLocationUpdates();
document.getElementById('showDeviceIdSwitch').checked = showDeviceId;
document.getElementById('showClusterSwitch').checked = showCluster;
}
function updateLayerVisibility(zoom) {
if (showCluster) {
if (zoom >= maxZoomForClustering) {
clusterLayer.setVisible(false);
vectorLayer.setVisible(true);
} else {
clusterLayer.setVisible(true);
vectorLayer.setVisible(false);
}
} else {
clusterLayer.setVisible(false);
vectorLayer.setVisible(true);
}
}
function bindMouseEvents() {
map.on('pointermove', function(evt) {
if (evt.dragging) {
return;
}
var pixel = map.getEventPixel(evt.originalEvent);
var hit = map.hasFeatureAtPixel(pixel);
map.getTargetElement().style.cursor = hit ? 'pointer' : '';
var feature = map.forEachFeatureAtPixel(pixel, function(feature) {
return feature;
});
if (hoveredFeature && hoveredFeature !== feature) {
hoveredFeature.set('hovered', false);
}
if (feature) {
// 处理集群
var features = feature.get('features');
if (features && features.length === 1) {
features[0].set('hovered', true);
hoveredFeature = features[0];
} else if (!features && feature.get('deviceInfo')) {
feature.set('hovered', true);
hoveredFeature = feature;
}
}
vectorLayer.changed();
});
map.on('click', function(evt) {
var feature = map.forEachFeatureAtPixel(evt.pixel, function(feature) {
return feature;
});
if (feature) {
var features = feature.get('features');
if (features && features.length > 1) {
// 集群点击,扩展视图
var extent = vectorSource.getExtent();
map.getView().fit(extent, {
padding: [50, 50, 50, 50],
duration: 1000
});
} else if (features && features.length === 1) {
// 单个设备点击
var deviceInfo = features[0].get('deviceInfo');
if (deviceInfo) {
showDeviceInfo(deviceInfo);
// 如果天气预测开启,显示天气卡片
if (WeatherForecast.isEnabled()) {
WeatherForecast.showWeatherForecast(deviceInfo);
}
}
} else if (feature.get('deviceInfo')) {
var deviceInfo = feature.get('deviceInfo');
showDeviceInfo(deviceInfo);
// 如果天气预测开启,显示天气卡片
if (WeatherForecast.isEnabled()) {
WeatherForecast.showWeatherForecast(deviceInfo);
}
}
}
});
}
function showDeviceInfo(deviceInfo) {
var statusText = '';
if (deviceInfo.warning === 2) {
statusText = '严重告警';
} else if (deviceInfo.warning === 1) {
statusText = '一般告警';
} else {
statusText = '正常';
}
var infoMsg = ' 设备: ' + deviceInfo.deviceid +
' | 状态: ' + statusText +
' | 坐标: ' + deviceInfo.latitude.toFixed(4) + ', ' + deviceInfo.longitude.toFixed(4);
if (window.layer && typeof window.layer.msg === 'function') {
window.layer.msg(infoMsg, {
time: 3000,
area: ['auto', 'auto'],
offset: 'auto'
});
}
}
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 centerLat = (minLat + maxLat) / 2;
var centerLon = (minLon + maxLon) / 2;
map.getView().setCenter(ol.proj.fromLonLat([centerLon, centerLat]));
}
function switchMapType(mapType) {
if (!MapLayers.hasLayer(mapType)) {
console.error('未知的地图类型:', mapType);
return;
}
map.removeLayer(currentBaseLayer);
currentBaseLayer = MapLayers.getLayer(mapType);
map.getLayers().insertAt(0, currentBaseLayer);
DeviceMarkers.updateMyLocationForMapType(mapType);
// 重新获取设备列表并添加标记
var deviceList = window.deviceList || [];
DeviceMarkers.addDeviceMarkers(deviceList);
}
function toggleDeviceId() {
showDeviceId = document.getElementById('showDeviceIdSwitch').checked;
DeviceMarkers.setShowDeviceId(showDeviceId);
if (window.layer && typeof window.layer.msg === 'function') {
window.layer.msg(showDeviceId ? '已显示设备信息' : '已隐藏设备信息');
}
}
function toggleCluster() {
showCluster = document.getElementById('showClusterSwitch').checked;
var zoom = map.getView().getZoom();
updateLayerVisibility(zoom);
if (window.layer && typeof window.layer.msg === 'function') {
window.layer.msg(showCluster ? '已启用集群显示' : '已禁用集群显示');
}
}
function toggleMapFunctionsMenu() {
var menu = document.getElementById('mapFunctionsMenu');
if (menu) {
menu.classList.toggle('show');
if (menu.classList.contains('show')) {
document.addEventListener('click', closeMapFunctionsMenu);
}
}
}
/**
* 关闭地图功能菜单
* @param {Event} event - 点击事件
*/
function closeMapFunctionsMenu(event) {
var dropdown = document.getElementById('mapFunctionsMenu');
var toggleBtn = document.querySelector('.dropdown-toggle');
if (dropdown && !dropdown.contains(event.target) && !toggleBtn.contains(event.target)) {
dropdown.classList.remove('show');
document.removeEventListener('click', closeMapFunctionsMenu);
}
}
/**
* 地图类型变化处理
*/
function onMapTypeChange() {
var mapType = document.getElementById('mapTypeSelectNew').value;
switchMapType(mapType);
}
function onWarningFilterChange() {
var filterValue = document.getElementById('warningFilter').value;
switch(filterValue) {
case 'all':
SearchFilter.showAllDevices();
break;
case 'warning1':
SearchFilter.showWarning1Devices();
break;
case 'warning2':
SearchFilter.showWarning2Devices();
break;
}
}
function searchDeviceNew() {
var searchInput = document.getElementById('deviceSearchNew');
if (searchInput) {
var deviceId = searchInput.value.trim();
if (deviceId) {
SearchFilter.searchDevice(deviceId);
} else {
if (window.layer && typeof window.layer.msg === 'function') {
window.layer.msg('请输入设备编号');
}
}
}
}
function getMap() {
return map;
}
function getVectorSource() {
return vectorSource;
}
function getVectorLayer() {
return vectorLayer;
}
return {
initialize: initialize,
switchMapType: switchMapType,
toggleDeviceId: toggleDeviceId,
toggleCluster: toggleCluster,
toggleMapFunctionsMenu: toggleMapFunctionsMenu,
onMapTypeChange: onMapTypeChange,
onWarningFilterChange: onWarningFilterChange,
searchDeviceNew: searchDeviceNew,
getMap: getMap,
getVectorSource: getVectorSource,
getVectorLayer: getVectorLayer
};
})();

View File

@ -0,0 +1,179 @@
/**
* 地图图层管理模块
* 管理不同类型的地图图层天地图高德谷歌等
*/
var MapLayers = (function() {
'use strict';
// 天地图 API 密钥 fengyarnom@gmail.com
var TIANDITU_KEY = '0c260b8a094a4e0bc507808812cefdac';
function createTiandituTileLoadFunction() {
return function(imageTile, src) {
imageTile.getImage().src = src;
imageTile.getImage().onerror = function() {
// 天地图加载失败时切换到高德地图
var mapTypeSelect = document.getElementById('mapTypeSelectNew');
if(mapTypeSelect && mapTypeSelect.value.startsWith('tianditu_')) {
mapTypeSelect.value = 'amap';
if (window.DeviceOverview && typeof window.DeviceOverview.switchMapType === 'function') {
window.DeviceOverview.switchMapType('amap');
}
if (window.layer && typeof window.layer.msg === 'function') {
window.layer.msg('天地图加载失败,已自动切换到高德地图');
}
if (window.layui && window.layui.form) {
window.layui.form.render('select');
}
}
};
};
}
var mapLayers = {
// 高德地图
amap: new ol.layer.Tile({
source: new ol.source.XYZ({
url: 'https://webrd0{1-4}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}'
})
}),
// 高德卫星图
amap_satellite: new ol.layer.Group({
layers: [
new ol.layer.Tile({
source: new ol.source.XYZ({
url: 'https://webst0{1-4}.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}'
})
}),
new ol.layer.Tile({
source: new ol.source.XYZ({
url: 'https://webst0{1-4}.is.autonavi.com/appmaptile?style=8&x={x}&y={y}&z={z}'
})
})
]
}),
// 谷歌卫星图
google_satellite: new ol.layer.Tile({
source: new ol.source.XYZ({
url: 'https://mt{0-3}.google.com/vt/lyrs=s&x={x}&y={y}&z={z}',
crossOrigin: 'anonymous'
})
}),
// 谷歌地形图
google_terrain: new ol.layer.Tile({
source: new ol.source.XYZ({
url: 'https://mt{0-3}.google.com/vt/lyrs=p&x={x}&y={y}&z={z}',
crossOrigin: 'anonymous'
})
}),
// 谷歌道路图
google_roadmap: new ol.layer.Tile({
source: new ol.source.XYZ({
url: 'https://mt{0-3}.google.com/vt/lyrs=m&x={x}&y={y}&z={z}',
crossOrigin: 'anonymous'
})
}),
// 谷歌混合图
google_hybrid: new ol.layer.Tile({
source: new ol.source.XYZ({
url: 'https://mt{0-3}.google.com/vt/lyrs=y&x={x}&y={y}&z={z}',
crossOrigin: 'anonymous'
})
}),
// 天地图卫星影像
tianditu_satellite: new ol.layer.Group({
layers: [
new ol.layer.Tile({
source: new ol.source.XYZ({
url: 'https://t{0-7}.tianditu.gov.cn/img_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=' + TIANDITU_KEY,
tileLoadFunction: createTiandituTileLoadFunction()
})
}),
new ol.layer.Tile({
source: new ol.source.XYZ({
url: 'https://t{0-7}.tianditu.gov.cn/cia_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cia&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=' + TIANDITU_KEY,
tileLoadFunction: createTiandituTileLoadFunction()
})
})
]
}),
// 天地图矢量图
tianditu_normal: new ol.layer.Group({
layers: [
new ol.layer.Tile({
source: new ol.source.XYZ({
url: 'https://t{0-7}.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=' + TIANDITU_KEY,
tileLoadFunction: createTiandituTileLoadFunction()
})
}),
new ol.layer.Tile({
source: new ol.source.XYZ({
url: 'https://t{0-7}.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=' + TIANDITU_KEY,
tileLoadFunction: createTiandituTileLoadFunction()
})
})
]
}),
// 天地图地形图
tianditu_terrain: new ol.layer.Group({
layers: [
new ol.layer.Tile({
source: new ol.source.XYZ({
url: 'https://t{0-7}.tianditu.gov.cn/ter_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=ter&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=' + TIANDITU_KEY,
tileLoadFunction: createTiandituTileLoadFunction()
})
})
]
}),
// 天地图地形混合图
tianditu_terrain_hybrid: new ol.layer.Group({
layers: [
new ol.layer.Tile({
source: new ol.source.XYZ({
url: 'https://t{0-7}.tianditu.gov.cn/ter_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=ter&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=' + TIANDITU_KEY,
tileLoadFunction: createTiandituTileLoadFunction()
})
}),
new ol.layer.Tile({
source: new ol.source.XYZ({
url: 'https://t{0-7}.tianditu.gov.cn/cta_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cta&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=' + TIANDITU_KEY,
tileLoadFunction: createTiandituTileLoadFunction()
})
})
]
})
};
function getLayer(mapType) {
return mapLayers[mapType];
}
function getAllLayers() {
return mapLayers;
}
function hasLayer(mapType) {
return mapLayers.hasOwnProperty(mapType);
}
return {
getLayer: getLayer,
getAllLayers: getAllLayers,
hasLayer: hasLayer
};
})();

View File

@ -0,0 +1,303 @@
/**
* 测距工具模块
* 提供地图测距功能
*/
var MeasureTools = (function() {
'use strict';
var measureActive = false;
var measureDraw = null;
var measureSource = null;
var measureLayer = null;
var measureTooltips = [];
var measureFeatures = [];
var currentMeasureTooltips = [];
var currentSketch = null;
var currentListener = null;
var segmentTooltips = [];
var map = null;
function init(mapInstance) {
map = mapInstance;
// 测距专用图层
measureSource = new ol.source.Vector();
measureLayer = new ol.layer.Vector({
source: measureSource,
style: new ol.style.Style({
fill: new ol.style.Fill({ color: 'rgba(255,255,255,0.2)' }),
stroke: new ol.style.Stroke({ color: '#ffcc33', width: 2 }),
image: new ol.style.RegularShape({
points: 4,
radius: 8,
radius2: 0,
angle: Math.PI / 4,
stroke: new ol.style.Stroke({ color: '#ed8936', width: 2 })
})
})
});
if (map) {
map.addLayer(measureLayer);
}
}
function toggleMeasureDistance() {
if (measureActive) {
deactivateMeasure();
if (window.layer && typeof window.layer.msg === 'function') {
window.layer.msg('测距功能已关闭');
}
} else {
activateMeasure();
if (window.layer && typeof window.layer.msg === 'function') {
window.layer.msg('点击左键添加距离节点,点击右键结束测量 :)');
}
}
var menu = document.getElementById('mapFunctionsMenu');
if (menu) {
menu.classList.remove('show');
}
}
function finishMeasuring() {
if (measureActive && currentSketch && measureDraw) {
measureDraw.finishDrawing();
}
}
function activateMeasure() {
if (!map || !measureSource) return;
measureActive = true;
// 显示测量状态指示器
var measureStatus = document.getElementById('measureStatus');
if (measureStatus) {
measureStatus.style.display = 'flex';
}
measureDraw = new ol.interaction.Draw({
source: measureSource,
type: 'LineString',
style: new ol.style.Style({
fill: new ol.style.Fill({ color: 'rgba(255,255,255,0.2)' }),
stroke: new ol.style.Stroke({ color: '#ffcc33', width: 2 }),
image: new ol.style.RegularShape({
points: 4,
radius: 8,
radius2: 0,
angle: Math.PI / 4,
stroke: new ol.style.Stroke({ color: '#ed8936', width: 2 })
})
})
});
map.addInteraction(measureDraw);
map.getViewport().addEventListener('contextmenu', function(e) {
if (measureActive && currentSketch) {
e.preventDefault();
measureDraw.finishDrawing();
}
});
measureDraw.on('drawstart', function(evt) {
currentSketch = evt.feature;
currentMeasureTooltips = [];
segmentTooltips = [];
currentListener = currentSketch.getGeometry().on('change', function(e) {
var geom = e.target;
var coords = geom.getCoordinates();
clearTemporaryTooltips();
clearSegmentTooltips();
// 计算并显示每个节点的距离
var total = 0;
for (var i = 0; i < coords.length; i++) {
if (i === 0) {
// 起点
var startTooltip = createMeasureTooltip(coords[0], '起点');
currentMeasureTooltips.push(startTooltip);
} else {
// 计算段距离
var seg = new ol.geom.LineString([coords[i-1], coords[i]]);
var segmentLength = ol.sphere.getLength(seg);
total += segmentLength;
// 显示每个节点的累计距离
var output = formatLength(total);
var tooltip = createMeasureTooltip(coords[i], output);
segmentTooltips.push(tooltip);
if (i === coords.length - 1) {
currentMeasureTooltips.push(tooltip);
}
}
}
});
});
// 绘制结束
measureDraw.on('drawend', function(evt) {
var coords = evt.feature.getGeometry().getCoordinates();
clearTemporaryTooltips();
clearSegmentTooltips();
var total = 0;
for (var i = 0; i < coords.length; i++) {
if (i === 0) {
var startTooltip = createMeasureTooltip(coords[0], '起点', true);
measureTooltips.push(startTooltip);
} else {
var seg = new ol.geom.LineString([coords[i-1], coords[i]]);
total += ol.sphere.getLength(seg);
var output = formatLength(total);
var tooltip = createMeasureTooltip(coords[i], output, true);
measureTooltips.push(tooltip);
}
}
measureFeatures.push(evt.feature);
if (currentListener) {
ol.Observable.unByKey(currentListener);
}
currentSketch = null;
currentListener = null;
var measureStatus = document.getElementById('measureStatus');
if (measureStatus) {
measureStatus.style.display = 'none';
}
map.removeInteraction(measureDraw);
measureActive = false;
if (window.layer && typeof window.layer.msg === 'function') {
window.layer.msg('测量完成');
}
});
}
function deactivateMeasure() {
measureActive = false;
var measureStatus = document.getElementById('measureStatus');
if (measureStatus) {
measureStatus.style.display = 'none';
}
if (measureDraw && map) {
map.removeInteraction(measureDraw);
measureDraw = null;
}
if (currentListener) {
ol.Observable.unByKey(currentListener);
currentListener = null;
}
currentSketch = null;
clearTemporaryTooltips();
clearSegmentTooltips();
}
function createMeasureTooltip(coord, text, isStatic) {
var elem = document.createElement('div');
elem.className = isStatic ? 'ol-tooltip ol-tooltip-static' : 'ol-tooltip ol-tooltip-measure';
elem.innerHTML = text;
var overlay = new ol.Overlay({
element: elem,
offset: [0, -15],
positioning: 'bottom-center'
});
overlay.setPosition(coord);
if (map) {
map.addOverlay(overlay);
}
return overlay;
}
function formatLength(length) {
if (length > 1000) {
return (Math.round(length / 100) / 10) + ' km';
} else {
return (Math.round(length * 10) / 10) + ' m';
}
}
function clearAllMeasureTooltips() {
if (!map) return;
for (var i = 0; i < measureTooltips.length; i++) {
map.removeOverlay(measureTooltips[i]);
}
measureTooltips = [];
clearTemporaryTooltips();
}
function clearTemporaryTooltips() {
if (!map) return;
for (var i = 0; i < currentMeasureTooltips.length; i++) {
map.removeOverlay(currentMeasureTooltips[i]);
}
currentMeasureTooltips = [];
}
function clearSegmentTooltips() {
if (!map) return;
for (var i = 0; i < segmentTooltips.length; i++) {
map.removeOverlay(segmentTooltips[i]);
}
segmentTooltips = [];
}
function clearMeasure() {
if (measureSource) {
measureSource.clear();
}
clearAllMeasureTooltips();
measureFeatures = [];
if (window.layer && typeof window.layer.msg === 'function') {
window.layer.msg('测距标记已清除');
}
// 关闭地图功能菜单
var menu = document.getElementById('mapFunctionsMenu');
if (menu) {
menu.classList.remove('show');
}
}
function isActive() {
return measureActive;
}
function getMeasureCount() {
return measureFeatures.length;
}
return {
init: init,
toggleMeasureDistance: toggleMeasureDistance,
finishMeasuring: finishMeasuring,
clearMeasure: clearMeasure,
isActive: isActive,
getMeasureCount: getMeasureCount
};
})();

View File

@ -0,0 +1,187 @@
/**
* 搜索和过滤功能模块
* 负责设备搜索过滤和查询功能
*/
var SearchFilter = (function() {
'use strict';
var currentSearchedDevice = null;
var markerState = 3; // 1:all; 2:orange; 3:red
function searchDevice(deviceId) {
if (!deviceId || !deviceId.trim()) {
clearSearch();
return false;
}
deviceId = deviceId.trim();
currentSearchedDevice = deviceId;
var success = DeviceMarkers.locateDevice(deviceId);
if (success) {
if (window.layer && typeof window.layer.msg === 'function') {
window.layer.msg('已定位到设备: ' + deviceId);
}
// 获取设备信息并显示天气预测(如果启用)
var targetFeature = DeviceMarkers.findDeviceById(deviceId);
if (targetFeature && window.WeatherForecast && window.WeatherForecast.isEnabled()) {
var deviceInfo = targetFeature.get('deviceInfo');
if (deviceInfo) {
window.WeatherForecast.showWeatherForecast(deviceInfo);
}
}
} else {
if (window.layer && typeof window.layer.msg === 'function') {
window.layer.msg('未找到设备: ' + deviceId);
}
}
return success;
}
function clearSearch() {
currentSearchedDevice = null;
var searchInput = document.getElementById('deviceSearchNew');
if (searchInput) {
searchInput.value = '';
}
// 恢复到当前的过滤状态
applyCurrentFilter();
}
function applyCurrentFilter() {
switch(markerState) {
case 1:
showAllDevices();
break;
case 2:
showWarning1Devices();
break;
case 3:
showWarning2Devices();
break;
}
}
function showAllDevices() {
currentSearchedDevice = null;
clearSearchInput();
DeviceMarkers.showAllDevices();
markerState = 1;
updateFilterSelect('all');
}
function showWarning1Devices() {
currentSearchedDevice = null;
clearSearchInput();
DeviceMarkers.showWarning1Devices();
markerState = 2;
updateFilterSelect('warning1');
}
function showWarning2Devices() {
currentSearchedDevice = null;
clearSearchInput();
DeviceMarkers.showWarning2Devices();
markerState = 3;
updateFilterSelect('warning2');
}
function clearSearchInput() {
var searchInput = document.getElementById('deviceSearchNew');
if (searchInput) {
searchInput.value = '';
}
}
function updateFilterSelect(filterValue) {
var filterSelect = document.getElementById('warningFilter');
if (filterSelect) {
filterSelect.value = filterValue;
}
}
function filterDevicesByStatus(statusType) {
switch(statusType) {
case 'warning1':
showWarning1Devices();
break;
case 'warning2':
showWarning2Devices();
break;
case 'offline':
case 'no_fwd':
case 'nofixed':
case 'nogga':
// 对于这些状态,显示所有设备,让用户在弹窗中选择
showAllDevices();
break;
default:
showAllDevices();
break;
}
}
function queryDevices(statusType) {
filterDevicesByStatus(statusType);
// 打开设备列表弹窗
if (window.layer && typeof window.layer.open === 'function') {
var index = window.layer.open({
title: '设备列表',
type: 2,
shade: 0.2,
maxmin: true,
shadeClose: true,
anim: 2,
offset: 'rb',
area: ['100%', '50%'],
content: '../page/gnss_q_status?query=' + statusType,
});
}
}
function locateDeviceOnMap(deviceId, latitude, longitude) {
currentSearchedDevice = deviceId;
var success = DeviceMarkers.locateDevice(deviceId);
if (success && window.layer && typeof window.layer.closeAll === 'function') {
window.layer.closeAll();
}
return success;
}
function locateDeviceDirectly(deviceId) {
currentSearchedDevice = deviceId;
return DeviceMarkers.locateDevice(deviceId);
}
function getCurrentSearchedDevice() {
return currentSearchedDevice;
}
function getMarkerState() {
return markerState;
}
function setMarkerState(state) {
markerState = state;
}
return {
searchDevice: searchDevice,
clearSearch: clearSearch,
showAllDevices: showAllDevices,
showWarning1Devices: showWarning1Devices,
showWarning2Devices: showWarning2Devices,
filterDevicesByStatus: filterDevicesByStatus,
queryDevices: queryDevices,
locateDeviceOnMap: locateDeviceOnMap,
locateDeviceDirectly: locateDeviceDirectly,
getCurrentSearchedDevice: getCurrentSearchedDevice,
getMarkerState: getMarkerState,
setMarkerState: setMarkerState
};
})();

View File

@ -0,0 +1,307 @@
var WeatherForecast = (function() {
'use strict';
var weatherApiKey = 'Uxh4IdMuAvhSiBnsf4UUDVGF4e3YAp2B';
var weatherEnabled = false;
var weatherData = null;
var currentForecastIndex = 0;
var currentWeatherDevice = null;
function init() {
// 天气预报模块初始化完成
}
function toggleWeatherForecast() {
var role = window.userRole || 'USER';
if (role !== 'SUPER_ADMIN') {
if (window.layer && typeof window.layer.msg === 'function') {
window.layer.msg('您没有权限使用此功能');
}
return;
}
var enableSwitch = document.getElementById('enableWeatherSwitch');
weatherEnabled = enableSwitch ? enableSwitch.checked : false;
if (weatherEnabled) {
if (window.layer && typeof window.layer.msg === 'function') {
window.layer.msg(
'搜索设备或点击地图设备图标即可自动查询天气预测',
{time: 3000, area: ['300px', '80px']}
);
}
} else {
if (window.layer && typeof window.layer.msg === 'function') {
window.layer.msg('天气预测功能已关闭');
}
closeWeatherCard();
}
var menu = document.getElementById('mapFunctionsMenu');
if (menu) {
menu.classList.remove('show');
}
}
function showWeatherForecast(deviceInfo) {
currentWeatherDevice = deviceInfo;
weatherData = null;
var role = window.userRole || 'USER';
if (role !== 'SUPER_ADMIN') {
return;
}
if (!weatherEnabled) {
if (window.layer && typeof window.layer.msg === 'function') {
window.layer.msg('天气预测功能未启用', {time: 2000});
}
return;
}
var deviceIdElement = document.getElementById('weatherDeviceId');
var deviceCoordsElement = document.getElementById('weatherDeviceCoords');
if (deviceIdElement) {
deviceIdElement.textContent = '设备: ' + deviceInfo.deviceid;
}
if (deviceCoordsElement) {
deviceCoordsElement.textContent =
'坐标: ' + deviceInfo.latitude.toFixed(4) + ', ' + deviceInfo.longitude.toFixed(4);
}
var weatherCard = document.getElementById('weatherForecastCard');
if (weatherCard) {
weatherCard.style.display = 'block';
}
var contentElement = document.getElementById('weatherForecastContent');
if (contentElement) {
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>';
}
var prevBtn = document.getElementById('prevForecast');
var nextBtn = document.getElementById('nextForecast');
var timeDisplay = document.getElementById('forecastTimeDisplay');
if (prevBtn) prevBtn.disabled = true;
if (nextBtn) nextBtn.disabled = true;
if (timeDisplay) timeDisplay.textContent = '--:--';
fetchWeatherData(deviceInfo.latitude, deviceInfo.longitude);
}
function closeWeatherCard() {
var weatherCard = document.getElementById('weatherForecastCard');
if (weatherCard) {
weatherCard.style.display = 'none';
}
currentWeatherDevice = null;
weatherData = null;
}
function showPrevForecast() {
if (!weatherData || currentForecastIndex <= 0) return;
currentForecastIndex--;
displayCurrentForecast();
updateForecastNavigation();
}
function showNextForecast() {
if (!weatherData || !weatherData.ts || currentForecastIndex >= weatherData.ts.length - 1) return;
currentForecastIndex++;
displayCurrentForecast();
updateForecastNavigation();
}
function updateForecastNavigation() {
if (!weatherData || !weatherData.ts) return;
var prevBtn = document.getElementById('prevForecast');
var nextBtn = document.getElementById('nextForecast');
var timeDisplay = document.getElementById('forecastTimeDisplay');
if (prevBtn) prevBtn.disabled = (currentForecastIndex <= 0);
if (nextBtn) nextBtn.disabled = (currentForecastIndex >= weatherData.ts.length - 1);
if (timeDisplay) {
var timestamp = weatherData.ts[currentForecastIndex];
var date = new Date(timestamp);
timeDisplay.textContent = formatDateTime(date);
}
}
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
};
fetch('https://api.windy.com/api/point-forecast/v2', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(requestBody)
})
.then(function(response) {
if (!response.ok) {
throw new Error('网络响应状态: ' + response.status);
}
return response.json();
})
.then(function(data) {
weatherData = data;
var currentTime = new Date().getTime();
var closestIndex = 0;
var futureIndex = -1;
if (weatherData.ts && weatherData.ts.length > 0) {
for (var i = 0; i < weatherData.ts.length; i++) {
if (weatherData.ts[i] > currentTime) {
futureIndex = i;
break;
}
}
if (futureIndex >= 0) {
closestIndex = futureIndex;
} else {
var smallestDiff = Number.MAX_VALUE;
for (var i = 0; i < weatherData.ts.length; i++) {
var diff = Math.abs(weatherData.ts[i] - currentTime);
if (diff < smallestDiff) {
smallestDiff = diff;
closestIndex = i;
}
}
}
}
currentForecastIndex = closestIndex;
displayCurrentForecast();
updateForecastNavigation();
})
.catch(function(error) {
console.error('天气数据获取失败:', error);
displayWeatherError('获取天气数据失败: ' + error.message);
});
}
function displayCurrentForecast() {
if (!weatherData || !weatherData.ts || weatherData.ts.length === 0) {
displayWeatherError('无可用的天气预测数据');
return;
}
var i = currentForecastIndex;
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);
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);
forecastHtml += createWeatherParam('风速', windSpeed + ' m/s');
forecastHtml += createWeatherParam('风向', windDir);
}
if (weatherData['past3hprecip-surface'] && weatherData['past3hprecip-surface'][i] !== null) {
var precip = weatherData['past3hprecip-surface'][i].toFixed(1);
forecastHtml += createWeatherParam('降水', precip + ' mm');
}
if (weatherData['rh-surface'] && weatherData['rh-surface'][i] !== null) {
var humidity = weatherData['rh-surface'][i].toFixed(0);
forecastHtml += createWeatherParam('湿度', humidity + '%');
}
if (weatherData['pressure-surface'] && weatherData['pressure-surface'][i] !== null) {
var pressure = (weatherData['pressure-surface'][i] / 100).toFixed(0);
forecastHtml += createWeatherParam('气压', pressure + ' hPa');
}
if (weatherData['gust-surface'] && weatherData['gust-surface'][i] !== null) {
var gust = weatherData['gust-surface'][i].toFixed(1);
forecastHtml += createWeatherParam('阵风', gust + ' m/s');
}
forecastHtml += '</div></div>';
var contentElement = document.getElementById('weatherForecastContent');
if (contentElement) {
contentElement.innerHTML = forecastHtml;
}
}
function displayWeatherError(message) {
var contentElement = document.getElementById('weatherForecastContent');
if (contentElement) {
contentElement.innerHTML =
'<div class="weather-error">' +
'<i class="layui-icon layui-icon-close"></i>' +
'<p>' + message + '</p>' +
'</div>';
}
}
function createWeatherParam(label, value) {
return '<div class="weather-param">' +
'<span class="weather-param-label">' + label + '</span>' +
'<span class="weather-param-value">' + value + '</span>' +
'</div>';
}
function formatDateTime(date) {
var month = (date.getMonth() + 1).toString().padStart(2, '0');
var day = date.getDate().toString().padStart(2, '0');
var hours = date.getHours().toString().padStart(2, '0');
var minutes = date.getMinutes().toString().padStart(2, '0');
return month + '-' + day + ' ' + hours + ':' + minutes;
}
function getWindDirection(u, v) {
var angle = Math.atan2(-u, -v) * 180 / Math.PI;
angle = (angle + 360) % 360;
var directions = ['北', '东北', '东', '东南', '南', '西南', '西', '西北'];
var index = Math.round(angle / 45) % 8;
return directions[index];
}
function isEnabled() {
return weatherEnabled;
}
return {
init: init,
toggleWeatherForecast: toggleWeatherForecast,
showWeatherForecast: showWeatherForecast,
closeWeatherCard: closeWeatherCard,
showPrevForecast: showPrevForecast,
showNextForecast: showNextForecast,
isEnabled: isEnabled
};
})();

File diff suppressed because one or more lines are too long

View File

@ -219,7 +219,7 @@
//建立webSocket连接 //建立webSocket连接
var webSocktPath; var webSocktPath;
webSocktPath = (basePath+"/websocket").replace("http","ws"); webSocktPath = (basePath+"/websocket/cmdline").replace("http","ws");
websocket = new WebSocket(webSocktPath); websocket = new WebSocket(webSocktPath);
//打开webSokcet连接时回调该函数 //打开webSokcet连接时回调该函数

View File

@ -96,6 +96,16 @@
</div> </div>
</div> </div>
<div class="layui-inline"> <div class="layui-inline">
<label class="layui-form-label">内置电池</label>
<div class="layui-input-inline">
<select name="has_battery" id="has_battery" lay-verify="required" lay-search="">
<option value="-1">全部</option>
<option value="0"></option>
<option value="1"></option>
</select>
</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>
</div> </div>
</div> </div>
@ -134,6 +144,7 @@
{field: 'group_id', title: '基本参数组', width: 60, sort: true}, {field: 'group_id', title: '基本参数组', width: 60, sort: true},
{field: 'calc_group_id', title: '解算参数组', width: 60, sort: true}, {field: 'calc_group_id', title: '解算参数组', width: 60, sort: true},
{field: 'parentid', title: '基站编号', width: 80, sort: true}, {field: 'parentid', title: '基站编号', width: 80, sort: true},
{field: 'parentid1', title: '备用基站', width: 80, sort: true},
{field: 'tenantname', title: '所属组织', width: 120}, {field: 'tenantname', title: '所属组织', width: 120},
{field: 'fwd_group_id', title: '推送组', width: 80}, {field: 'fwd_group_id', title: '推送组', width: 80},
{field: 'fwd_group_id2', title: '推送2', width: 80}, {field: 'fwd_group_id2', title: '推送2', width: 80},
@ -141,6 +152,7 @@
{field: 'syn', title: '参数同步', width: 80,templet: '#synTrans'}, {field: 'syn', title: '参数同步', width: 80,templet: '#synTrans'},
{field: 'model', title: '型号', width: 80,templet: "<div>{{d.model==0?'G505':'G510'}}</div>"}, {field: 'model', title: '型号', width: 80,templet: "<div>{{d.model==0?'G505':'G510'}}</div>"},
{field: 'appver', title: '固件版本', width: 80}, {field: 'appver', title: '固件版本', width: 80},
{field: 'voltage_factor', title: '分压系数', width: 60},
{field: 'imei', title: 'IMEI', width: 100}, {field: 'imei', title: 'IMEI', width: 100},
{title: '操作', toolbar: '#currentTableBar', fixed: "right", width: 120} {title: '操作', toolbar: '#currentTableBar', fixed: "right", width: 120}
]; ];

View File

@ -21,7 +21,17 @@
<div class="layui-inline"> <div class="layui-inline">
<label class="layui-form-label">设备号</label> <label class="layui-form-label">设备号</label>
<div class="layui-input-inline"> <div class="layui-input-inline">
<input type="text" name="s_deviceid" autocomplete="off" class="layui-input"> <input type="text" name="s_deviceid" id="deviceid" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">内置电池</label>
<div class="layui-input-inline">
<select name="NULL_solarvoltage" id="NULL_solarvoltage" lay-verify="required" lay-search="">
<option value="-1">全部</option>
<option value="0"></option>
<option value="1"></option>
</select>
</div> </div>
</div> </div>
<div class="layui-inline"> <div class="layui-inline">
@ -34,7 +44,7 @@
</div> </div>
</div> </div>
<div class="layui-inline"> <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">&#xe67d;</i>导出</button> <button type="submit" class="layui-btn layui-btn-primary" lay-submit lay-filter="data-export-btn"><i class="layui-icon">&#xe67d;</i>导出</button>
</div> </div>
</div> </div>
@ -42,17 +52,36 @@
</div> </div>
</fieldset> </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> <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>
</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 src="../lib/layui-v2.6.3/layui.js" charset="utf-8"></script>
<script th:inline="javascript"> <script th:inline="javascript">
layui.use(['form', 'table', 'laydate'], function () { layui.use(['form', 'table', 'laydate', 'element'], function () {
var $ = layui.$; var $ = layui.$;
var form = layui.form, var form = layui.form,
table = layui.table, table = layui.table,
laydate = layui.laydate; laydate = layui.laydate,
element = layui.element;
var echartsAttitude = echarts.init(document.getElementById('echarts-attitude'), 'walden');
var chartDataLoaded = false;
var cfg_cols = [ var cfg_cols = [
{field: 'deviceid', title: '设备号', width: 100}, {field: 'deviceid', title: '设备号', width: 100},
@ -68,9 +97,7 @@
{field: 'temperature', title: '温度(°C)'}, {field: 'temperature', title: '温度(°C)'},
{field: 'humidity', title: '湿度(%)'} {field: 'humidity', title: '湿度(%)'}
]; ];
/**
* 初始化表单,要加上,不然刷新部分组件可能会不加载
*/
form.render(); form.render();
laydate.render({ laydate.render({
@ -85,7 +112,7 @@
table.render({ table.render({
elem: '#currentTableId', elem: '#currentTableId',
url: '/gnss/msg/status/list', url: '/gnss/msg/status/list',
toolbar: '#toolbarDemo',//开启头部工具栏 toolbar: '#toolbarDemo',
defaultToolbar: ['filter'], defaultToolbar: ['filter'],
cols: [ cols: [
cfg_cols cfg_cols
@ -96,11 +123,9 @@
skin: 'line' skin: 'line'
}); });
// 监听搜索操作
form.on('submit(data-search-btn)', function (data) { form.on('submit(data-search-btn)', function (data) {
var result = JSON.stringify(data.field); var result = JSON.stringify(data.field);
//执行搜索重载
table.reload('currentTableId', { table.reload('currentTableId', {
page: { page: {
curr: 1 curr: 1
@ -113,7 +138,6 @@
return false; return false;
}); });
// 监听导出操作
form.on('submit(data-export-btn)', function (data) { form.on('submit(data-export-btn)', function (data) {
var result = $('#searchFrm').serialize(); var result = $('#searchFrm').serialize();
var u = "/gnss/msg/status/export?" + result; var u = "/gnss/msg/status/export?" + result;
@ -121,6 +145,173 @@
return false; 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> </script>

View File

@ -0,0 +1,237 @@
<!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">
</head>
<body>
<div class="layuimini-container">
<div class="layuimini-main">
<div class="layui-card layui-panel">
<div class="layui-card-header">
<h3>请选择要升级的固件</h3>
</div>
<div class="layui-card-body">
<table class="layui-hide" id="firmware-table" lay-filter="firmware-table"></table>
</div>
</div>
<br><br>
<div class="layui-card layui-panel">
<div class="layui-card-header" name="app-title">
<h3>终端固件信息</h3>
</div>
<div class="layui-card-body">
<fieldset class="table-search-fieldset">
<legend>搜索信息</legend>
<div style="margin: 10px 10px 10px 10px">
<form class="layui-form layui-form-pane" action="">
<div class="layui-form-item">
<div class="layui-inline" th:if="${tenant_id==0}">
<label class="layui-form-label">所属部门</label>
<div class="layui-input-inline">
<select name="tenantname" id="tenantname" lay-search="">
<option value="">全部</option>
<option value="非SAAS服务商">非SAAS服务商</option>
<option th:each="item : ${tenant_list}" th:text="${item.name}" th:value="${item.name}"></option>
</select>
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">设备号</label>
<div class="layui-input-inline">
<input type="text" name="deviceid" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">项目号</label>
<div class="layui-input-inline">
<input type="text" name="project_id" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">桩号</label>
<div class="layui-input-inline">
<input type="text" name="sector" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">固件版本</label>
<div class="layui-input-inline">
<input type="text" name="appver" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">使用状态</label>
<div class="layui-input-inline">
<select name="opmode" id="opmode" lay-verify="required" lay-search="">
<option value="-1">全部</option>
<option value="0">正常</option>
<option value="1">维护</option>
<option value="2">停用</option>
</select>
</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>
</div>
</div>
</form>
</div>
</fieldset>
<table class="layui-hide" id="app-table" lay-filter="app-table"></table>
</div>
</div>
<br><br>
<script type="text/html" id="app-toolbar">
<div class="layui-btn-container">
<button class="layui-btn layui-btn-sm" lay-event="upgrade" id="btn_upgrade">升级</button>
</div>
</script>
</div>
</div>
<script src="../lib/layui-v2.6.3/layui.js" charset="utf-8"></script>
<script th:inline="javascript">
layui.use(['form', 'table'], function () {
var $ = layui.$,
form = layui.form,
table = layui.table;
var cfg_cols = [
{type: 'checkbox', fixed: 'left'},
{field: 'deviceid', title: '设备号', width: 100, sort: true},
{field: 'remark', title: '备注', width: 100},
{field: 'project_id', title: '项目号', width: 120, sort: true},
{field: 'sector', title: '桩号', width: 120, sort: true},
{field: 'name', title: '监测点号', width: 80},
{field: 'devicetype', title: '类型', width: 80,templet: "<div>{{d.devicetype==0?'监测站':'基准站'}}</div>"},
{field: 'parentid', title: '基站编号', width: 80, sort: true},
{field: 'tenantname', title: '所属组织', width: 120},
{field: 'opmode', title: '使用状态', width: 80,templet: '#modeTrans'},
{field: 'model', title: '型号', width: 80,templet: "<div>{{d.model==0?'G505':'G510'}}</div>"},
{field: 'appver', title: '固件版本', width: 80}
];
/**
* 初始化表单,要加上,不然刷新部分组件可能会不加载
*/
form.render();
table.render({
elem: '#app-table',
url: '/sys/ver_mgr/gnss_device_list',
toolbar: '#app-toolbar',
defaultToolbar: ['filter','exports'],
cols: [
cfg_cols
],
limits: [10, 20, 50],
limit: 10,
page: true,
skin: 'line'
});
table.render({
elem: '#firmware-table'
,url:'/sys/ver_mgr/firmware_list'
,cols: [
[
{type: 'radio', fixed: 'left'}
,{field:'name', title:'固件', sort: true}
,{field:'length', title:'长度(字节)'}
,{field:'create_time', title:'时间', sort: true, templet: "<div>{{layui.util.toDateString(d.create_time, 'yyyy-MM-dd HH:mm:ss')}}</div>"}
]
]
,page: true
});
// 监听搜索操作
form.on('submit(data-search-btn)', function (data) {
var result = JSON.stringify(data.field);
//执行搜索重载
table.reload('app-table', {
page: {
curr: 1
}
, where: {
searchParams: result
}
}, 'data');
table.render();
return false;
});
/**
* toolbar事件监听
*/
//头工具栏事件
table.on('toolbar(app-table)', function(obj){
let $ = layui.$;
switch(obj.event){
case 'upgrade':
var optionStatus = table.checkStatus('firmware-table')//获取选中行状态
var fdata = optionStatus.data;
var checkStatus = table.checkStatus('app-table')//获取选中行状态
var adata = checkStatus.data;
if(fdata.length>0 && adata.length>0){
var firmware = fdata[0]["name"];
var device_list = "";
for(var i=0;i<adata.length; i++){
device_list = device_list+adata[i]["deviceid"]+";";
}
$.ajax({
type:"POST",
url:"/sys/ver_mgr/upgrade",
data: {
'firmware': firmware,
'id_list': device_list
},
success: function (result) {
if(result.code == 0) layer.msg('升级进行中...');
else layer.alert(result.msg);
},
error: function () {
console.log("ajax error");
}
});
}
break;
};
});
});
function onDeviceCfgUpdated(){
layui.table.reload('currentTableId');
}
</script>
<script type="text/html" id="modeTrans">
{{# if(d.opmode == 0){ }}
<span class="layui-badge layui-bg-green">正常</span>
{{# } else if(d.opmode == 1){ }}
<span class="layui-badge layui-bg-blue">维护</span>
{{# } else { }}
<span class="layui-badge layui-bg-gray">停用</span>
{{# } }}
</script>
<script type="text/html" id="synTrans">
{{# if(d.syn == 0){ }}
<span class="layui-badge layui-bg-orange"></span>
{{# } else { }}
<span class="layui-badge layui-bg-green"></span>
{{# } }}
</script>
</body>
</html>

View File

@ -22,7 +22,7 @@
<div class="layui-inline"> <div class="layui-inline">
<label class="layui-form-label required">设备编号</label> <label class="layui-form-label required">设备编号</label>
<div class="layui-input-block"> <div class="layui-input-block">
<input type="text" name="deviceid" id="deviceid" th:field="*{deviceid}" lay-verify="required" lay-reqtext="不能为空" placeholder="请输入设备编号" value="" class="layui-input"> <input type="text" name="deviceid" id="deviceid" th:field="*{deviceid}" lay-verify="required" lay-reqtext="不能为空" placeholder="请输入设备编号" value="" class="layui-input" onblur="checkDeviceId()">
</div> </div>
</div> </div>
<div class="layui-inline"> <div class="layui-inline">
@ -57,14 +57,21 @@
</div> </div>
</div> </div>
<div class="layui-inline" pane=""> <div class="layui-inline" pane="">
<label class="layui-form-label">星座</label> <label class="layui-form-label">内置电池</label>
<div class="layui-input-block"> <div class="layui-input-inline">
<input type="checkbox" name="like1[write]" lay-skin="primary" title="北斗" checked="" disabled=""> <select name="has_battery" id="has_battery" th:field="*{has_battery}" lay-verify="required" lay-search="">
<input type="checkbox" name="like1[read]" lay-skin="primary" title="GPS" disabled=""> <option value="0"></option>
<input type="checkbox" name="like1[game]" lay-skin="primary" title="GLONASS" disabled=""> <option value="1"></option>
<input type="checkbox" name="like1[game]" lay-skin="primary" title="Galileo" disabled=""> </select>
</div> </div>
</div> </div>
<div class="layui-inline" pane="">
<label class="layui-form-label">电压系数</label>
<div class="layui-input-inline">
<input type="text" name="voltage_factor" id="voltage_factor" th:field="*{voltage_factor}" class="layui-input">
</div>
<div class="layui-form-mid layui-word-aux">*0.1</div>
</div>
</div> </div>
<div class="layui-form-item"> <div class="layui-form-item">
<div class="layui-inline"> <div class="layui-inline">
@ -82,6 +89,12 @@
<input type="number" name="parentid" id="parentid" th:field="*{parentid}" placeholder="请输入关联基准站编号" value="" class="layui-input"> <input type="number" name="parentid" id="parentid" th:field="*{parentid}" placeholder="请输入关联基准站编号" value="" class="layui-input">
</div> </div>
</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>
<div class="layui-form-item" id="ecef_div"> <div class="layui-form-item" id="ecef_div">
<div class="layui-inline"> <div class="layui-inline">
@ -288,6 +301,7 @@
}); });
function setEcefEditor(){ function setEcefEditor(){
var $ = layui.$; var $ = layui.$;
console.log($('#devicetype').val(), $('#model').val()); console.log($('#devicetype').val(), $('#model').val());
@ -299,6 +313,19 @@
} }
} }
function checkDeviceId(){
var $ = layui.$;
var value = $("#deviceid").val();
console.log(value);
if(value.startsWith("6") || value.startsWith("7")){
$("#has_battery").val('1');
$("#has_battery").attr('disabled',true);
}
else{
$("#has_battery").attr('disabled',false);
}
layui.form.render();
}
</script> </script>
</body> </body>

View File

@ -91,8 +91,12 @@
contentType: "application/json;charset=UTF-8", contentType: "application/json;charset=UTF-8",
data: JSON.stringify(data.field), data: JSON.stringify(data.field),
success: function (result) { success: function (result) {
if(result.code == 0) {
parent.onBaseParaUpdated(); parent.onBaseParaUpdated();
parent.layer.close(iframeIndex); parent.layer.close(iframeIndex);
}
else layer.alert(result.msg);
}, },
error: function () { error: function () {
console.log("ajax error"); console.log("ajax error");

View File

@ -56,6 +56,12 @@
<version>5.5.8</version> <version>5.5.8</version>
</dependency> </dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.78.Final</version>
</dependency>
</dependencies> </dependencies>

View File

@ -0,0 +1,109 @@
package com.imdroid.common.util;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESedeKeySpec;
import javax.crypto.spec.IvParameterSpec;
import java.nio.charset.StandardCharsets;
import java.security.Key;
public class Des3Utils {
private static final String KEY_ALGORITHM = "DESede";
private static final String CIPHER_ALGORITHM_CBC = "DESede/CBC/PKCS5Padding";
private static final String CIPHER_ALGORITHM_ECB = "DESede/ECB/PKCS5Padding";
static String mode = "DESede/CFB/NoPadding";
public static byte[] encrypt(Key deskey, IvParameterSpec desiv, String mode, byte[] data) {
byte[] res = null;
Cipher cipher;
try {
cipher = Cipher.getInstance(mode);
cipher.init(Cipher.ENCRYPT_MODE, deskey, desiv);
res = cipher.doFinal(data);
} catch (Exception e) {
e.printStackTrace();
}
return res;
}
public static String encrypt(String data, byte[] key, byte[] iv) {
String res = "";
ByteBuf buf = Unpooled.buffer(256);
byte[] databyte = data.getBytes(StandardCharsets.UTF_8);
buf.writeBytes(databyte);
int a = (8 - databyte.length % 8);
for (int c = 0; c < a; c++) {
buf.writeByte(0x00);
}
databyte = new byte[buf.readableBytes()];
buf.readBytes(databyte);
DESedeKeySpec spec;
SecretKeyFactory keyfactory;
Key deskey;
try {
keyfactory = SecretKeyFactory.getInstance("DESede");
spec = new DESedeKeySpec(key);
deskey = keyfactory.generateSecret(spec);
IvParameterSpec desiv = new IvParameterSpec(iv);
byte[] encryptbyte = encrypt(deskey, desiv, mode, databyte);
res = Base64.encodeBase64String(encryptbyte);
} catch (Exception e) {
e.printStackTrace();
}
return res;
}
public static String encryptCBC(byte[] key, byte[] iv, String data) throws Exception {
DESedeKeySpec spec = new DESedeKeySpec(key);
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(KEY_ALGORITHM);
Key desKey = keyFactory.generateSecret(spec);
Cipher cipher = Cipher.getInstance(mode);
IvParameterSpec ips = new IvParameterSpec(iv);
cipher.init(Cipher.ENCRYPT_MODE, desKey, ips);
byte[] encryptedData = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
return Base64.encodeBase64String(encryptedData);
}
public static String decryptCBC(byte[] key, byte[] iv, String data) throws Exception {
DESedeKeySpec spec = new DESedeKeySpec(key);
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(KEY_ALGORITHM);
Key desKey = keyFactory.generateSecret(spec);
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM_CBC);
IvParameterSpec ips = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, desKey, ips);
byte[] decryptedData = cipher.doFinal(Base64.decodeBase64(data));
return new String(decryptedData, StandardCharsets.UTF_8);
}
public static String encryptECB(byte[] key, String data) throws Exception {
DESedeKeySpec spec = new DESedeKeySpec(key);
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(KEY_ALGORITHM);
Key desKey = keyFactory.generateSecret(spec);
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM_ECB);
cipher.init(Cipher.ENCRYPT_MODE, desKey);
byte[] encryptedData = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
return Base64.encodeBase64String(encryptedData);
}
public static String decryptECB(byte[] key, String data) throws Exception {
DESedeKeySpec spec = new DESedeKeySpec(key);
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(KEY_ALGORITHM);
Key desKey = keyFactory.generateSecret(spec);
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM_ECB);
cipher.init(Cipher.DECRYPT_MODE, desKey);
byte[] decryptedData = cipher.doFinal(Base64.decodeBase64(data));
return new String(decryptedData, StandardCharsets.UTF_8);
}
}

View File

@ -3,8 +3,12 @@ package com.imdroid.ntripproxy.service;
public class Ntrip2Channels { public class Ntrip2Channels {
final private String localHost="127.0.0.1"; final private String localHost="127.0.0.1";
final private int localPort=9903; final private int localPort=9903;
final private String remoteHost="47.107.50.52"; // 将远程主机改为本地端口改为12000
final private int remotePort=9903; final private String remoteHost="127.0.0.1";
//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;
public static final Ntrip2Channels INSTANCE = new Ntrip2Channels(); public static final Ntrip2Channels INSTANCE = new Ntrip2Channels();

View File

@ -0,0 +1,185 @@
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());
}
}
}
}

104
sec-vermgr/pom.xml Normal file
View File

@ -0,0 +1,104 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.imdroid</groupId>
<artifactId>security-monitor</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>sec-vermgr</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.78.Final</version>
</dependency>
<dependency>
<groupId>com.imdroid</groupId>
<artifactId>sec-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.imdroid</groupId>
<artifactId>sec-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 矩阵工具 -->
<dependency>
<groupId>org.ejml</groupId>
<artifactId>ejml-all</artifactId>
<version>0.41</version>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>central</id>
<name>ali-mirror</name>
<url>https://maven.aliyun.com/repository/central</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
</project>

View File

@ -0,0 +1,25 @@
package com.imdroid.vermgr;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.ComponentScan;
/**
* @author Layton
* @date 2023/1/31 20:33
*/
@SpringBootApplication(scanBasePackages = {"com.imdroid"})
@MapperScan({"com.imdroid.secapi","com.imdroid.beidou.entity"})
@ComponentScan({"com.imdroid.*"})
@EntityScan({"com.imdroid.*"})
@EnableFeignClients(basePackages = "com.imdroid.*")
public class VerMgrApp {
public static void main(String[] args) {
SpringApplication.run(VerMgrApp.class, args);
}
}

View File

@ -0,0 +1,39 @@
package com.imdroid.vermgr.entity;
import lombok.Data;
import java.sql.Timestamp;
//member name must be same as the field name in mysql
@Data
public class DeviceApp {
//0:未连接1连接2升级中3传输完成4升级失败
public static final short STATE_DISCONNECTED = 0;
public static final short STATE_CONNECTED = 1;
public static final short STATE_UPGRADING = 2;
public static final short STATE_TX_COMPLETED = 3;
public static final short STATE_TX_FAILED = 4;
public static final String[] state_description = {
"未连接","连接","升级中","发送完成","发送失败"
};
public static final String[] result_description = {
"成功","其他错误","传输中断","超时","数据错误","超时","保存失败","文件错误"
};
long device_sn = 0;
String version;
String firmware;
short upgrade_state = STATE_DISCONNECTED;
short upgrade_result; //失败原因
Timestamp upgrade_time;
String strState;
String strResult;
public void translateCode() {
if(upgrade_state>STATE_TX_FAILED) upgrade_state=STATE_DISCONNECTED;
strState = state_description[upgrade_state];
if(upgrade_result>7) upgrade_result=7;
strResult = result_description[upgrade_result];
}
}

View File

@ -0,0 +1,290 @@
package com.imdroid.vermgr.service;
import com.imdroid.secapi.client.BeidouClient;
import com.imdroid.secapi.client.HttpResp;
import com.imdroid.vermgr.entity.DeviceApp;
import com.imdroid.vermgr.utils.CRC16;
import io.netty.channel.Channel;
import io.netty.util.AttributeKey;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
/******************************************************
* 批量升级终端版本
* 单例要求线程安全
* 状态机Idle---收到UpgradeDeviceList--->Upgrading
* ^ |
* | |
* +------Timerout or Complete <-----+
*
* 流程
* 1收到待升级的设备列表UpgradeDeviceList如果当前处于升级完毕状态则更新待升级列表回应答开始升级/升级未结束
* 2TCP通道激活 ---> 获取版本信息 ---> 关联TCP通道和待升级的设备
* 3启动升级会话 ---> 实时更新会话
* 数据结构deviceId,cur_ver,upgrade_firmware,TCP_channel,upgrade_session
* ****************************************************/
@Slf4j
@RestController
public class UpgradeManager {
// 版本管理协议
public static final byte GET_APP_INFO = (byte) 0xA0;
public static final byte APP_INFO_IND = (byte) 0xA1;
public static final byte UPGRADE_IND = (byte) 0xA2;
public static final byte NOT_UPGRADE_IND = (byte) 0xA3;
public static final byte UPGRADE_ACK = (byte) 0xA4;
@Autowired
static public UpgradeManager INSTANCE;
@Autowired
BeidouClient beidouClient;
Map<Long, DeviceApp> upgradeDeviceList = new ConcurrentHashMap<>();
Map<Long, UpgradeSession> upgradeSessionMap = new ConcurrentHashMap<>();
boolean isIdleState = true;
TimerTask operationTimerTask = null;
TimerTask txProgressTimerTask = null;
Timer timer; //30s定时器
@PostMapping("/upgrade_cmd")
HttpResp upgrade(@RequestParam(name = "deviceList") String deviceList, @RequestParam(name = "verFile") String verFile){
HttpResp resp = new HttpResp();
if(onRxUpgradeDeviceList(deviceList.split(";"),verFile)){
resp.setCode(HttpResp.HTTP_RSP_OK);
}
else{
resp.setCode(HttpResp.HTTP_RSP_FAILED);
resp.setResponseMessage("正在升级中");
}
return resp;
}
boolean onRxUpgradeDeviceList(String[] deviceList, String firmware){
if(isIdleState){
isIdleState = false;
//清空状态信息和升级会话
upgradeDeviceList.clear();
upgradeSessionMap.clear();
//创建新的状态信息
for(String deviceId:deviceList){
Long deviceSn = Long.valueOf(deviceId);
if(deviceSn!=null) {
DeviceApp deviceApp = new DeviceApp();
deviceApp.setDevice_sn(deviceSn);
deviceApp.setFirmware(firmware);
upgradeDeviceList.put(deviceSn, deviceApp);
}
}
//启动20s定时器如果这期间没有收到任何数据则超时失败
startTimer();
startTxProgressTimerTask();
return true;
}
else{
return false;
}
}
public void onChannelActive(Channel channel){
// get app info
Long deviceId = (Long) channel.attr(AttributeKey.valueOf("deviceId")).get();
if(deviceId == null){
getVerInfo(channel);
}
}
void onChannelInactive(Channel channel){
// 更新状态为未连接
long sn = (long) channel.attr(AttributeKey.valueOf("sn")).get();
if(sn > 0){
DeviceApp deviceApp = upgradeDeviceList.get(sn);
if (deviceApp != null) {
deviceApp.setUpgrade_state(DeviceApp.STATE_DISCONNECTED);
}
upgradeSessionMap.remove(sn);
}
}
void onRxData(Channel channel, byte[] rx_data){
if(isIdleState){
// 临时空闲状态下如果连续3次收到C则认为有终端主动请求升级
if(rx_data[0]==UpgradeSession.CC && rx_data.length<=4){
//onUpgradeRequest(channel);
}
}
else {
if (rx_data[0] == APP_INFO_IND && rx_data.length > 2) {
onVerInfoReceived(channel, rx_data);
restartTimer();
}
else {
Long sn = (Long) channel.attr(AttributeKey.valueOf("deviceId")).get();
if(sn==null) return;
UpgradeSession session = upgradeSessionMap.get(sn);
if (session == null) return;
boolean session_done = false;
if (rx_data[0] == UPGRADE_ACK) {
log.info("upgrade ack");
//outputHex(rx_data);
if (rx_data[5] != 0) {
//终端拒绝升级原因是产品序列号不对
session.reject();
session_done = true;
}
} else {
session_done = session.procYmodem(channel, rx_data);
}
if (session_done) {
if (isAllUpgraded()) {
//全部发完等待新版本启动回应答
stopTimer();
onUpgradeComplete();
}
}
}
}
}
void onUpgradeComplete(){
isIdleState = true;
stopTxProgressTimerTask();
refreshTxProgress();
beidouClient.onUpgradeComplete();
}
/***************************************************
* 定时器处理
****************************************************/
void startTimer(){
if(operationTimerTask == null) {
operationTimerTask = new TimerTask() {
@Override
public void run() {
onTimerout();
}
};
}
timer.schedule(operationTimerTask,15000);
}
void restartTimer(){
stopTimer();
startTimer();
}
void stopTimer(){
if(operationTimerTask != null){
operationTimerTask.cancel();
operationTimerTask = null;
}
}
void onTimerout(){
// 超时处理
for(UpgradeSession session:upgradeSessionMap.values()){
if(session.getState() == DeviceApp.STATE_UPGRADING) {
session.onTimeout();
}
}
//
onUpgradeComplete();
}
void startTxProgressTimerTask(){
if(txProgressTimerTask==null){
txProgressTimerTask = new TimerTask() {
@Override
public void run() {
refreshTxProgress();
}
};
}
timer.schedule(txProgressTimerTask,1000,1000);
}
void stopTxProgressTimerTask(){
if(txProgressTimerTask != null){
txProgressTimerTask.cancel();
txProgressTimerTask = null;
log.info("stopTxProgressTimerTask");
}
}
/***************************************************
* 获取终端信息
****************************************************/
public void getVerInfo(Channel channel){
// 向所有连接终端发读取版本请求
byte get_app_info[] = {GET_APP_INFO,0};
byte tailer_buff[] = {0,0};
int crc = CRC16.calculate(get_app_info);
tailer_buff[0] = (byte) (crc>>8);
tailer_buff[1] = (byte) crc;
channel.write(get_app_info);
channel.writeAndFlush(tailer_buff);
}
void onVerInfoReceived(Channel channel, byte[] rx_data){
log.info(channel.remoteAddress()+": received app info");
// 记录获取到的版本信息
if(rx_data.length<9) return;
int crc = CRC16.calculate(rx_data, 7);
int crc0 = (((short)rx_data[7]&0xFF)<<8) | ((short)rx_data[8]&0xFF);
if(crc != crc0) {
log.info(channel.remoteAddress()+": crc error");
return;
}
int i=1;
long sn = 0;
for(i=1;i<=4;i++){
sn = (sn<<8) | ((short)rx_data[i]&0xFF);
}
String cur_ver = ((short)rx_data[i++]&0xFF) + "." + ((short)rx_data[i++]&0xFF);
// 更新APP记录并检查此终端是否需要升级
DeviceApp deviceApp = upgradeDeviceList.get(sn);
if (deviceApp == null) return;
deviceApp.setVersion(cur_ver);
//记录这个channel是有应答的
channel.attr(AttributeKey.valueOf("deviceId")).set(sn);
// 如果原来是未连接状态则改为连接如果是其他状态则保持不变
if(deviceApp.getUpgrade_state() == DeviceApp.STATE_DISCONNECTED) {
deviceApp.setUpgrade_state(DeviceApp.STATE_CONNECTED);
}
//启动升级
UpgradeSession session = new UpgradeSession(deviceApp);
upgradeSessionMap.put(sn, session);
session.start(channel);
}
/***************************************************
* 升级
****************************************************/
synchronized public boolean isAllUpgraded(){
for(DeviceApp deviceApp:upgradeDeviceList.values()){
if(deviceApp.getUpgrade_state() != DeviceApp.STATE_TX_COMPLETED &&
deviceApp.getUpgrade_state() != DeviceApp.STATE_TX_FAILED) {
return false;
}
}
return true;
}
void refreshTxProgress(){
for(UpgradeSession session:upgradeSessionMap.values()){
if(session.getState() == DeviceApp.STATE_UPGRADING) {
beidouClient.onUpgradeProgress(Long.toString(session.getSN()), session.getTxPercentage() + "%");
}
}
for(DeviceApp deviceApp:upgradeDeviceList.values()){
if(deviceApp.getUpgrade_state()!=DeviceApp.STATE_UPGRADING){
beidouClient.onUpgradeProgress(Long.toString(deviceApp.getDevice_sn()),deviceApp.getStrState());
}
}
}
}

View File

@ -0,0 +1,394 @@
package com.imdroid.vermgr.service;
import com.imdroid.vermgr.entity.DeviceApp;
import com.imdroid.vermgr.utils.CRC16;
import io.netty.channel.Channel;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.Arrays;
@Slf4j
public class UpgradeSession {
public static final byte COM_OK = 0;
public static final byte COM_ERROR = 1;
public static final byte COM_ABORT = 2;
public static final byte COM_TIMEOUT = 3;
public static final byte COM_DATA = 4;
public static final byte COM_LIMIT = 5;
public static final byte COM_SAVE_ERR = 6;
public static final byte COM_FILE_ERR = 7;
public static final byte SOH = 0x01; /* start of 128-byte data packet */
public static final byte STX = 0x02; /* start of 1024-byte data packet */
public static final byte EOT = 0x04; /* end of transmission */
public static final byte ACK = 0x06; /* acknowledge */
public static final byte NAK = 0x15; /* negative acknowledge */
public static final byte CA = 0x18; /* two of these in succession aborts transfer */
public static final byte CC = 0x43; /* 'C' == 0x43, request 16-bit CRC */
public static final short PACKET_HEADER_SIZE = 3;
public static final short PACKET_TRAILER_SIZE = 2;
public static final short PACKET_SIZE = 128;
public static final short PACKET_1K_SIZE = 1024;
static private String version_path="firmware";
private DeviceApp deviceApp;
File firmware;
FileInputStream reader;
short tx_seq = 0;
int tx_bytes = 0;
byte nak_num = 0;
byte[] header_buff;
byte[] tailer_buff;
byte[] soh_buff;
byte[] stx_buff;
boolean session_done = false;
boolean file_done = false;
boolean is_file_accepted = false;
byte result = COM_OK;
public UpgradeSession(DeviceApp app) {
deviceApp = app;
firmware = null;
reader = null;
header_buff = new byte[PACKET_HEADER_SIZE];
tailer_buff = new byte[PACKET_TRAILER_SIZE];
soh_buff = new byte[PACKET_SIZE];
stx_buff = new byte[PACKET_1K_SIZE];
}
public byte getResult(){
return result;
}
public DeviceApp getDeviceApp(){return deviceApp;}
public long getSN(){return deviceApp.getDevice_sn();}
public int getState() {return deviceApp.getUpgrade_state();}
public int getTxPercentage() {
if(firmware == null) return 0;
return (int) (tx_bytes*100/firmware.length());
}
public void reset(){
session_done = true;
file_done = true;
is_file_accepted = false;
tx_seq = 0;
nak_num = 0;
tx_bytes = 0;
try {
if(reader!=null) reader.close();
}
catch (IOException e){
log.error(firmware+" reader.close()",e);
}
}
public boolean start(Channel channel) {
session_done = false;
file_done = false;
is_file_accepted = false;
tx_seq = 0;
nak_num = 0;
tx_bytes = 0;
result = COM_FILE_ERR;
String file_name = deviceApp.getFirmware();
if(file_name != null){
firmware = new File(version_path+"/" + file_name);
if (firmware.exists()) {
try {
if(reader!=null) reader.close();
reader = new FileInputStream(firmware);
result = COM_OK;
}
catch (FileNotFoundException e) {
log.error(firmware+" reader open",e);
reset();
} catch (IOException e) {
log.error(firmware+" reader open",e);
reset();
}
}
}
// 发升级指示
if(result != COM_OK){
deviceApp.setUpgrade_state(DeviceApp.STATE_TX_FAILED);
deviceApp.setUpgrade_result(result);
return false;
}
else {
long sn = deviceApp.getDevice_sn();
byte[] ind_msg = new byte[5];
ind_msg[0] = UpgradeManager.UPGRADE_IND;
ind_msg[1] = (byte) (sn >> 24);
ind_msg[2] = (byte) (sn >> 16);
ind_msg[3] = (byte) (sn >> 8);
ind_msg[4] = (byte) sn;
byte tailer_buff[] = {0, 0};
int crc = CRC16.calculate(ind_msg);
tailer_buff[0] = (byte) (crc >> 8);
tailer_buff[1] = (byte) crc;
channel.write(ind_msg);
channel.writeAndFlush(tailer_buff);
log.info(deviceApp.getDevice_sn()+": send UPGRADE_IND");
deviceApp.setUpgrade_state(DeviceApp.STATE_UPGRADING);
return true;
}
}
public boolean restart(Channel channel) {
session_done = false;
file_done = false;
is_file_accepted = false;
tx_seq = 0;
nak_num = 0;
result = COM_FILE_ERR;
try {
if(reader!=null) reader.close();
reader = new FileInputStream(firmware);
result = COM_OK;
}
catch (FileNotFoundException e) {
log.error(firmware+" reader open failed: ",e);
reset();
} catch (IOException e) {
log.error(firmware+" reader open failed: ",e);
reset();
}
return (result == COM_OK);
}
public void reject(){
result = COM_ERROR;
deviceApp.setUpgrade_state(DeviceApp.STATE_TX_FAILED);
deviceApp.setUpgrade_result(result);
deviceApp.setUpgrade_time(new Timestamp(System.currentTimeMillis()));
}
public void onTimeout(){
result = COM_TIMEOUT;
deviceApp.setUpgrade_state(DeviceApp.STATE_TX_FAILED);
deviceApp.setUpgrade_result(result);
deviceApp.setUpgrade_time(new Timestamp(System.currentTimeMillis()));
}
public boolean procYmodem(Channel channel, byte[] rx_data){
if(session_done) return true;
try {
switch (rx_data[0]) {
case CC:
log.info("C");
if(tx_seq!=0){
//这种情况说明上次接收端意外中断了需要重新开始
restart(channel);
}
if (!is_file_accepted) {
sendFileInfo(channel);
} else {
tx_seq++;
sendPacket(channel);
//sendSmallPacket(channel);
}
break;
case ACK:
if(rx_data.length == 4)
log.info("ACK "+rx_data[1]+" "+(rx_data[2]<<8|((short)rx_data[3]&0xFF)));
else log.info("ACK");
nak_num = 0;
if (!is_file_accepted) {
is_file_accepted = true;
}
else if(!file_done){
tx_seq++;
sendPacket(channel);
//sendSmallPacket(channel);
}
if(file_done) {
is_file_accepted=false;
tx_seq=0;
}
break;
case NAK:
if(rx_data.length == 4)
log.info("NAK "+rx_data[1]+" "+(rx_data[2]<<8|((short)rx_data[3]&0xFF)));
else log.info("NAK");
nak_num++;
if(nak_num>=3){
log.info("too much NAK, cancel transmit");
result = COM_ERROR;
reset();
}
else resendPacket(channel);
break;
case CA:
log.info("CA");
result = rx_data[1];
reset();
break;
default:
log.info("unknown ind received: "+rx_data[0]+" "+rx_data[1]);
break;
}
}
catch (IOException e){
reset();
log.error(firmware+" reader error: ",e);
result = COM_FILE_ERR;
}
if(result == COM_ERROR || result==COM_FILE_ERR){
header_buff[0] = CA;
channel.writeAndFlush(header_buff);
}
if(session_done){
if(result == COM_OK) {
deviceApp.setUpgrade_state(DeviceApp.STATE_TX_COMPLETED);
}
else{
deviceApp.setUpgrade_state(DeviceApp.STATE_TX_FAILED);
}
deviceApp.setUpgrade_result(result);
deviceApp.setUpgrade_time(new Timestamp(System.currentTimeMillis()));
}
return session_done;
}
void sendFileInfo(Channel channel){
header_buff[0] = SOH;
header_buff[1] = (byte) tx_seq;
header_buff[2] = (byte) ~tx_seq;
Arrays.fill(soh_buff, (byte) 0);
if(file_done) {
reset();//end of session
}
else{
int pos = 0;
byte[] file_name = firmware.getName().getBytes();
for (int i = 0; i < file_name.length; i++) soh_buff[pos++] = file_name[i];
soh_buff[pos++] = 0;
String strFileSize = "" + firmware.length();
byte[] file_size = strFileSize.getBytes();
for (int i = 0; i < file_size.length; i++) soh_buff[pos++] = file_size[i];
soh_buff[pos++] = 0;
}
int crc = CRC16.calculate(soh_buff);
tailer_buff[0] = (byte) (crc>>8);
tailer_buff[1] = (byte) crc;
channel.write(header_buff);
channel.write(soh_buff);
channel.writeAndFlush(tailer_buff);
log.info(deviceApp.getDevice_sn()+": send file info");
}
void sendPacket(Channel channel) throws IOException {
if(tx_seq==0) return;
if(file_done){
header_buff[0] = EOT;
channel.writeAndFlush(header_buff);
log.info(deviceApp.getDevice_sn()+": send end of tx, seq="+tx_seq);
return;
}
header_buff[0] = STX;
header_buff[1] = (byte) tx_seq;
header_buff[2] = (byte) ~tx_seq;
int read_bytes = reader.read(stx_buff);
if(read_bytes != -1) tx_bytes+=read_bytes;
if(read_bytes == -1 || read_bytes==0){
// send EOT
header_buff[0] = EOT;
channel.writeAndFlush(header_buff);
file_done = true;
log.info(deviceApp.getDevice_sn()+": send end of tx, seq="+tx_seq);
return;
}
else if(read_bytes>=PACKET_1K_SIZE){
}
else if(read_bytes<PACKET_1K_SIZE && read_bytes>PACKET_SIZE){
for(int i=read_bytes; i<PACKET_1K_SIZE; i++) stx_buff[i]=0x1A;
}
else{
header_buff[0] = SOH;
Arrays.fill(soh_buff, (byte) 0x1A);
for(int i=0; i<read_bytes; i++) soh_buff[i] = stx_buff[i];
}
int crc = CRC16.calculate((header_buff[0] == STX)?stx_buff:soh_buff);
tailer_buff[0] = (byte) (crc>>8);
tailer_buff[1] = (byte) crc;
channel.write(header_buff);
channel.write((header_buff[0] == STX)?stx_buff:soh_buff);
channel.writeAndFlush(tailer_buff);
log.info(deviceApp.getDevice_sn()+": send packet "+tx_seq);
}
void sendSmallPacket(Channel channel) throws IOException {
if(tx_seq==0) return;
if(file_done){
header_buff[0] = EOT;
channel.writeAndFlush(header_buff);
log.info(deviceApp.getDevice_sn()+": send end of tx, seq="+tx_seq);
return;
}
header_buff[0] = SOH;
header_buff[1] = (byte) tx_seq;
header_buff[2] = (byte) ~tx_seq;
int read_bytes = reader.read(soh_buff);
if(read_bytes != -1) tx_bytes+=read_bytes;
if(read_bytes == -1 || read_bytes==0){
// send EOT
header_buff[0] = EOT;
channel.writeAndFlush(header_buff);
file_done = true;
log.info(deviceApp.getDevice_sn()+": send end of tx, seq="+tx_seq);
return;
}
else if(read_bytes>=PACKET_SIZE){
}
else{
for(int i=read_bytes; i<PACKET_SIZE; i++) soh_buff[i]=0x1A;
}
int crc = CRC16.calculate(soh_buff);
tailer_buff[0] = (byte) (crc>>8);
tailer_buff[1] = (byte) crc;
channel.write(header_buff);
channel.write(soh_buff);
channel.writeAndFlush(tailer_buff);
log.info(deviceApp.getDevice_sn()+": send packet "+tx_seq);
}
void resendPacket(Channel channel){
if(file_done){
header_buff[0] = EOT;
channel.writeAndFlush(header_buff);
}
else {
channel.write(header_buff);
channel.write((header_buff[0] == STX) ? stx_buff : soh_buff);
channel.writeAndFlush(tailer_buff);
}
log.info(deviceApp.getDevice_sn()+": resend packet "+header_buff[1]);
}
}

View File

@ -0,0 +1,52 @@
package com.imdroid.vermgr.service;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class VersionHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
super.channelActive(ctx);
Channel channel = ctx.channel();
UpgradeManager.INSTANCE.onChannelActive(channel);
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if(evt instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) evt;
if (event.state() == IdleState.READER_IDLE) {
Channel channel = ctx.channel();
log.info(channel.remoteAddress() + " idle too long to be closed");
channel.close();
}
}
}
@Override
public void channelRead(ChannelHandlerContext ctx,
Object msg) throws Exception{
Channel channel = ctx.channel();
UpgradeManager.INSTANCE.onRxData(channel, (byte[]) msg);
}
@Override
public void channelInactive(ChannelHandlerContext ctx)
throws Exception {
Channel channel = ctx.channel();
UpgradeManager.INSTANCE.onChannelInactive(channel);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}

View File

@ -0,0 +1,71 @@
package com.imdroid.vermgr.service;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.bytes.ByteArrayDecoder;
import io.netty.handler.codec.bytes.ByteArrayEncoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.timeout.IdleStateHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
@Service
@Slf4j
public class VersionServer {
@Value("${version_server_port}")
private int data_port;
@PostConstruct
public void init() {
// 创建socket服务线程阻塞
new Thread(new Runnable() {
@Override
public void run() {
log.info("dtu config server starting...");
// Configure the server.
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)
//.option(ChannelOption.SO_REUSEADDR, true)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
//p.addLast(new LoggingHandler(LogLevel.INFO));
p.addLast(new ByteArrayDecoder());
p.addLast(new ByteArrayEncoder());
p.addLast(new IdleStateHandler(300, 300, 300)); //设置心跳超时时间
p.addLast(new VersionHandler());
}
});
// Start the server.
ChannelFuture f = b.bind(data_port).sync();
// Wait until the server socket is closed.
f.channel().closeFuture().sync();
} catch (InterruptedException e) {
log.error("error occurred during version server running: "+e);
} finally {
// Shut down all event loops to terminate all threads.
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}).start();
}
}

View File

@ -0,0 +1,87 @@
package com.imdroid.vermgr.utils;
public class CRC16 {
static final char[] TABLE1021 = { /* CRC1021余式表 */
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, 0x8108,
0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, 0x1231,
0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, 0x9339,
0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, 0x2462,
0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, 0xa56a,
0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, 0x3653,
0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, 0xb75b,
0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, 0x48c4,
0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, 0xc9cc,
0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, 0x5af5,
0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, 0xdbfd,
0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, 0x6ca6,
0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, 0xedae,
0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, 0x7e97,
0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, 0xff9f,
0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, 0x9188,
0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, 0x1080,
0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, 0x83b9,
0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, 0x02b1,
0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, 0xb5ea,
0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, 0x34e2,
0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 0xa7db,
0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, 0x26d3,
0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, 0xd94c,
0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, 0x5844,
0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, 0xcb7d,
0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, 0x4a75,
0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, 0xfd2e,
0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, 0x7c26,
0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, 0xef1f,
0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, 0x6e17,
0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0 };
public static int calculate(byte[] buf, int length) {
int counter;
int crc = 0;
if(length > buf.length) return 0;
for (counter = 0; counter < length; counter++) {
crc = (crc << 8) ^ TABLE1021[((crc >> 8) ^ buf[counter]) & 0x00FF];
}
return crc & 0xFFFF;
}
public static int calculate(byte[] buf) {
return calculate(buf, buf.length);
}
}

View File

@ -0,0 +1,10 @@
server.port=9914
server.servlet.context-path=/
spring.application.name=vermgr
spring.application.build=20250519
version_server_port = 9916
#version manage
version.path = firmware