feat:测试平台2.1更新(测试指令-语音播报)
All checks were successful
iot-test-platform CI/CD / build-and-deploy (push) Successful in 17s
All checks were successful
iot-test-platform CI/CD / build-and-deploy (push) Successful in 17s
This commit is contained in:
@@ -42,5 +42,9 @@ public class Consts {
|
|||||||
public static final int CMD_PARAM_SETTINGS = 0X8103;
|
public static final int CMD_PARAM_SETTINGS = 0X8103;
|
||||||
/** 查询终端参数 **/
|
/** 查询终端参数 **/
|
||||||
public static final int CMD_PARAM_QUERY = 0x8104;
|
public static final int CMD_PARAM_QUERY = 0x8104;
|
||||||
|
/** 文本信息下发 **/
|
||||||
|
public static final int CMD_TEXT_INFO_DOWN = 0x8300;
|
||||||
|
/** 位置信息查询 **/
|
||||||
|
public static final int CMD_LOCATION_INQUIRY = 0x8201;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,14 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
|||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.iot.transport.jt808.entity.Session;
|
||||||
|
import com.iot.transport.jt808.entity.request.LocationInquiryPack;
|
||||||
|
import com.iot.transport.jt808.entity.request.TextInfoDownPack;
|
||||||
|
import com.iot.transport.jt808.server.SessionManager;
|
||||||
|
import com.iot.transport.jt808.service.codec.DataEncoder;
|
||||||
|
import com.iot.transport.jt808.util.CommandBuilder;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@@ -23,6 +31,112 @@ public class DeviceController {
|
|||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private ApiLogService apiLogService;
|
private ApiLogService apiLogService;
|
||||||
|
|
||||||
|
private final SessionManager sessionManager = SessionManager.getInstance();
|
||||||
|
private final DataEncoder dataEncoder = new DataEncoder();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下发指令 (通用)
|
||||||
|
* e.g. {"imei": "...", "cmd": "reboot", "params": []}
|
||||||
|
* or {"imei": "...", "cmd": "ip", "params": ["1.1.1.1", "80"]}
|
||||||
|
*/
|
||||||
|
@PostMapping("/command/send")
|
||||||
|
public CommonResult<String> sendCommand(@RequestBody Map<String, Object> payload) {
|
||||||
|
String imei = (String) payload.get("imei");
|
||||||
|
String cmdType = (String) payload.get("cmd");
|
||||||
|
List<String> params = (List<String>) payload.get("params");
|
||||||
|
|
||||||
|
String commandStr = "";
|
||||||
|
|
||||||
|
if (cmdType == null) return CommonResult.failed("cmd is required");
|
||||||
|
|
||||||
|
switch (cmdType.toLowerCase()) {
|
||||||
|
case "reboot":
|
||||||
|
commandStr = CommandBuilder.reboot();
|
||||||
|
break;
|
||||||
|
case "ip":
|
||||||
|
if (params != null && params.size() >= 2) {
|
||||||
|
commandStr = CommandBuilder.setIp(params.get(0), Integer.parseInt(params.get(1)));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "workmode":
|
||||||
|
if (params != null && params.size() >= 2) {
|
||||||
|
commandStr = CommandBuilder.setWorkMode(Integer.parseInt(params.get(0)), Integer.parseInt(params.get(1)));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "realtime":
|
||||||
|
if (params != null && params.size() >= 2) {
|
||||||
|
commandStr = CommandBuilder.setRealtimeMode(Integer.parseInt(params.get(0)), Integer.parseInt(params.get(1)));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "custom": // 自定义指令内容
|
||||||
|
commandStr = (String) payload.get("raw");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return CommonResult.failed("Unknown command type: " + cmdType);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commandStr == null || commandStr.isEmpty()) {
|
||||||
|
return CommonResult.failed("Invalid command parameters");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用 DeviceService 发送 8300 指令 (这里直接复用现有的 sendTextCommand 逻辑)
|
||||||
|
// 也可以直接在这里调用
|
||||||
|
return sendTextCommand(imei, commandStr, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下发文本信息指令 (0x8300)
|
||||||
|
*/
|
||||||
|
@PostMapping("/command/text")
|
||||||
|
public CommonResult<String> sendTextCommand(@RequestParam String phone,
|
||||||
|
@RequestParam String content,
|
||||||
|
@RequestParam(defaultValue = "1") int flag) { // 默认为1(紧急)
|
||||||
|
try {
|
||||||
|
Session session = findSessionByPhone(phone);
|
||||||
|
if (session == null) {
|
||||||
|
return CommonResult.failed("设备未连接: " + phone);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextInfoDownPack pack = new TextInfoDownPack(flag, content);
|
||||||
|
byte[] bytes = dataEncoder.encode4TextInfoDown(pack, session);
|
||||||
|
|
||||||
|
session.getChannel().writeAndFlush(io.netty.buffer.Unpooled.copiedBuffer(bytes)).sync();
|
||||||
|
log.info("Send text command to {}: {}", phone, content);
|
||||||
|
return CommonResult.success("指令下发成功: " + content);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Send text command failed", e);
|
||||||
|
return CommonResult.failed("指令下发失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下发位置查询指令 (0x8201)
|
||||||
|
*/
|
||||||
|
@PostMapping("/command/location")
|
||||||
|
public CommonResult<String> sendLocationInquiry(@RequestParam String phone) {
|
||||||
|
try {
|
||||||
|
Session session = findSessionByPhone(phone);
|
||||||
|
if (session == null) {
|
||||||
|
return CommonResult.failed("设备未连接: " + phone);
|
||||||
|
}
|
||||||
|
|
||||||
|
LocationInquiryPack pack = new LocationInquiryPack();
|
||||||
|
byte[] bytes = dataEncoder.encode4LocationInquiry(pack, session);
|
||||||
|
|
||||||
|
session.getChannel().writeAndFlush(io.netty.buffer.Unpooled.copiedBuffer(bytes)).sync();
|
||||||
|
log.info("Send location inquiry to {}", phone);
|
||||||
|
return CommonResult.success("指令下发成功");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Send location inquiry failed", e);
|
||||||
|
return CommonResult.failed("指令下发失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to find session by phone
|
||||||
|
private Session findSessionByPhone(String phone) {
|
||||||
|
return sessionManager.findByTerminalPhone(phone);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Standard Location Report (Typed)
|
* Standard Location Report (Typed)
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.iot.transport.jt808.entity.request;
|
||||||
|
|
||||||
|
import com.iot.transport.jt808.entity.DataPack;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 位置信息查询 (0x8201)
|
||||||
|
* 消息体为空
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@ToString(callSuper = true)
|
||||||
|
public class LocationInquiryPack extends DataPack {
|
||||||
|
public LocationInquiryPack() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.iot.transport.jt808.entity.request;
|
||||||
|
|
||||||
|
import com.iot.transport.jt808.entity.DataPack;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文本信息下发消息 (0x8300)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@ToString(callSuper = true)
|
||||||
|
public class TextInfoDownPack extends DataPack {
|
||||||
|
/**
|
||||||
|
* 标志位
|
||||||
|
* 0: 紧急
|
||||||
|
* 2: 终端显示器显示
|
||||||
|
* 3: 终端TTS播报
|
||||||
|
* 4: 广告屏显示
|
||||||
|
* 5: 0-中心导航信息,1-CAN故障码信息
|
||||||
|
*
|
||||||
|
* 这里协议文档说 "8300指令下发时需将标志位的紧急位置1",且 "5.2.1文本数据下发 01:属性(01:指令下发;08/09:TTS文本播报下发)"
|
||||||
|
* 似乎这里的 "属性" 就是标志位。
|
||||||
|
* 示例中 01 对应二进制 0000 0001,即 bit0=1 (紧急)。
|
||||||
|
* 示例 08/09 可能对应 bit3=1 (TTS) + bit0=0/1。
|
||||||
|
*/
|
||||||
|
private int flag;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文本信息
|
||||||
|
* 协议文档示例:23323031342a5345542a4d3a3223 -> #2014*SET*M:2#
|
||||||
|
*/
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
public TextInfoDownPack() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public TextInfoDownPack(int flag, String content) {
|
||||||
|
this.flag = flag;
|
||||||
|
this.content = content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ public class SessionManager {
|
|||||||
return sessionIdMap.entrySet();
|
return sessionIdMap.entrySet();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Session> toList() {
|
public List<Session> getValues() {
|
||||||
return this.sessionIdMap.entrySet().stream().map(e -> e.getValue()).collect(Collectors.toList());
|
return this.sessionIdMap.entrySet().stream().map(e -> e.getValue()).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import com.iot.transport.jt808.entity.response.ServerBodyPack;
|
|||||||
import com.iot.transport.jt808.entity.response.RegisterBodyPack;
|
import com.iot.transport.jt808.entity.response.RegisterBodyPack;
|
||||||
import com.iot.transport.jt808.util.BitUtil;
|
import com.iot.transport.jt808.util.BitUtil;
|
||||||
import com.iot.transport.jt808.util.JT808Util;
|
import com.iot.transport.jt808.util.JT808Util;
|
||||||
|
import com.iot.transport.jt808.entity.request.LocationInquiryPack;
|
||||||
|
import com.iot.transport.jt808.entity.request.TextInfoDownPack;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 数据包编码器
|
* 数据包编码器
|
||||||
@@ -25,6 +27,38 @@ public class DataEncoder {
|
|||||||
this.jt808Util = new JT808Util();
|
this.jt808Util = new JT808Util();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public byte[] encode4TextInfoDown(TextInfoDownPack req, Session session) throws Exception {
|
||||||
|
byte[] contentBytes = req.getContent().getBytes(Consts.DEFAULT_CHARSET);
|
||||||
|
byte[] msgBody = this.bitUtil.concatAll(Arrays.asList(//
|
||||||
|
new byte[] { (byte) req.getFlag() }, // 标志
|
||||||
|
contentBytes // 文本信息
|
||||||
|
));
|
||||||
|
|
||||||
|
// 消息头
|
||||||
|
int msgBodyProps = this.jt808Util.generateMsgBodyProps(msgBody.length, 0b000, false, 0);
|
||||||
|
byte[] msgHeader = this.jt808Util.generateMsgHeader(session.getTerminalPhone(),
|
||||||
|
Consts.CMD_TEXT_INFO_DOWN, msgBody, msgBodyProps, session.currentFlowId());
|
||||||
|
byte[] headerAndBody = this.bitUtil.concatAll(msgHeader, msgBody);
|
||||||
|
|
||||||
|
// 校验码
|
||||||
|
int checkSum = this.bitUtil.getCheckSum4JT808(headerAndBody, 0, headerAndBody.length - 1);
|
||||||
|
return this.doEncode(headerAndBody, checkSum);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] encode4LocationInquiry(LocationInquiryPack req, Session session) throws Exception {
|
||||||
|
byte[] msgBody = new byte[0]; // 空消息体
|
||||||
|
|
||||||
|
// 消息头
|
||||||
|
int msgBodyProps = this.jt808Util.generateMsgBodyProps(msgBody.length, 0b000, false, 0);
|
||||||
|
byte[] msgHeader = this.jt808Util.generateMsgHeader(session.getTerminalPhone(),
|
||||||
|
Consts.CMD_LOCATION_INQUIRY, msgBody, msgBodyProps, session.currentFlowId());
|
||||||
|
byte[] headerAndBody = this.bitUtil.concatAll(msgHeader, msgBody);
|
||||||
|
|
||||||
|
// 校验码
|
||||||
|
int checkSum = this.bitUtil.getCheckSum4JT808(headerAndBody, 0, headerAndBody.length - 1);
|
||||||
|
return this.doEncode(headerAndBody, checkSum);
|
||||||
|
}
|
||||||
|
|
||||||
public byte[] encode4TerminalRegisterResp(RegisterPack req, RegisterBodyPack respMsgBody,
|
public byte[] encode4TerminalRegisterResp(RegisterPack req, RegisterBodyPack respMsgBody,
|
||||||
int flowId) throws Exception {
|
int flowId) throws Exception {
|
||||||
// 消息体字节数组
|
// 消息体字节数组
|
||||||
@@ -100,3 +134,4 @@ public class DataEncoder {
|
|||||||
return jt808Util.doEscape4Send(noEscapedBytes, 1, noEscapedBytes.length - 2);
|
return jt808Util.doEscape4Send(noEscapedBytes, 1, noEscapedBytes.length - 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
115
src/main/java/com/iot/transport/jt808/util/CommandBuilder.java
Normal file
115
src/main/java/com/iot/transport/jt808/util/CommandBuilder.java
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
package com.iot.transport.jt808.util;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 指令构建工具类
|
||||||
|
* 用于构建 #2014*SET*...# 格式的指令
|
||||||
|
*/
|
||||||
|
public class CommandBuilder {
|
||||||
|
|
||||||
|
private static final String PREFIX = "#2014*SET*";
|
||||||
|
private static final String SUFFIX = "#";
|
||||||
|
private static final String SEPARATOR = "*";
|
||||||
|
|
||||||
|
// 指令标识符
|
||||||
|
public static final String CMD_IP = "T";
|
||||||
|
public static final String CMD_WORK_MODE = "M"; // M0, M2...
|
||||||
|
public static final String CMD_APN = "A";
|
||||||
|
public static final String CMD_REBOOT = "R";
|
||||||
|
public static final String CMD_SOS_NUM = "D"; // D1, D2...
|
||||||
|
public static final String CMD_DEL_SOS = "DO";
|
||||||
|
public static final String CMD_ANGLE_COMP = "GD";
|
||||||
|
public static final String CMD_FACTORY_RESET = "V";
|
||||||
|
public static final String CMD_CLEAR_CACHE = "QC";
|
||||||
|
public static final String CMD_MODIFY_ID = "S";
|
||||||
|
public static final String CMD_REG_AUTH = "NS";
|
||||||
|
public static final String CMD_GPS_MODE = "GM";
|
||||||
|
public static final String CMD_WHITE_LIST = "WC";
|
||||||
|
public static final String CMD_BATCH_UPLOAD = "BUL";
|
||||||
|
public static final String CMD_DISABLE_POWER_KEY = "DP";
|
||||||
|
public static final String CMD_DEL_SMS = "DSMS";
|
||||||
|
public static final String CMD_WIFI_PRIORITY = "WD";
|
||||||
|
public static final String CMD_VOLUME = "SP";
|
||||||
|
public static final String CMD_STEP_COUNT = "STP";
|
||||||
|
public static final String CMD_BT_BROADCAST = "BA";
|
||||||
|
public static final String CMD_BT_SCAN = "BN";
|
||||||
|
public static final String CMD_BT_SCAN_TIME = "BST";
|
||||||
|
public static final String CMD_BT_FILTER_UUID = "BUID";
|
||||||
|
public static final String CMD_BT_PROTOCOL = "BSP";
|
||||||
|
public static final String CMD_BT_INTERACTIVE = "BS";
|
||||||
|
public static final String CMD_SUB_IP = "IP2";
|
||||||
|
|
||||||
|
// 查询指令
|
||||||
|
public static final String CHK_PREFIX = "#2014*CHK*";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建设置指令
|
||||||
|
* @param cmdKey 指令标识符 (e.g. "T", "M2")
|
||||||
|
* @param params 参数,可以是多个 (e.g. "58.61.154.247", "7018")
|
||||||
|
* @return 完整指令字符串 (e.g. "#2014*SET*T:58.61.154.247,7018#")
|
||||||
|
*/
|
||||||
|
public static String buildSetCmd(String cmdKey, String... params) {
|
||||||
|
StringBuilder sb = new StringBuilder(PREFIX);
|
||||||
|
sb.append(cmdKey).append(":");
|
||||||
|
if (params != null && params.length > 0) {
|
||||||
|
sb.append(String.join(",", params));
|
||||||
|
}
|
||||||
|
sb.append(SUFFIX);
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建复合指令 (多个指令合并)
|
||||||
|
* e.g. #2014*SET*A:CMNET# #2014*SET*A:APN,USER,PASS#
|
||||||
|
* 文档示例:#2014*SET*D1:电话号码*D2:电话号码*D3:电话号码#
|
||||||
|
*/
|
||||||
|
public static String buildCompositeCmd(String... subCmds) {
|
||||||
|
// subCmds 格式应为 "D1:123", "D2:456"
|
||||||
|
StringBuilder sb = new StringBuilder(PREFIX);
|
||||||
|
if (subCmds != null && subCmds.length > 0) {
|
||||||
|
sb.append(String.join(SEPARATOR, subCmds));
|
||||||
|
}
|
||||||
|
sb.append(SUFFIX);
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建查询指令
|
||||||
|
* @param params 参数,可为空
|
||||||
|
* @return 完整查询指令
|
||||||
|
*/
|
||||||
|
public static String buildCheckCmd(String... params) {
|
||||||
|
StringBuilder sb = new StringBuilder(CHK_PREFIX);
|
||||||
|
if (params != null && params.length > 0) {
|
||||||
|
sb.append(String.join(SEPARATOR, params));
|
||||||
|
} else {
|
||||||
|
// Remove last * if no params
|
||||||
|
sb.setLength(sb.length() - 1);
|
||||||
|
}
|
||||||
|
sb.append(SUFFIX);
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 便捷方法示例
|
||||||
|
|
||||||
|
public static String setIp(String ip, int port) {
|
||||||
|
return buildSetCmd(CMD_IP, ip, String.valueOf(port));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String setWorkMode(int mode, int interval) {
|
||||||
|
// #2014*SET*M0:600#
|
||||||
|
return buildSetCmd(CMD_WORK_MODE + mode, String.valueOf(interval));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String setRealtimeMode(int activeInterval, int staticInterval) {
|
||||||
|
// #2014*SET*M2:30,300#
|
||||||
|
return buildSetCmd(CMD_WORK_MODE + "2", String.valueOf(activeInterval), String.valueOf(staticInterval));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String reboot() {
|
||||||
|
return buildSetCmd(CMD_REBOOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String factoryReset() {
|
||||||
|
return buildSetCmd(CMD_FACTORY_RESET);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -143,6 +143,9 @@
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<button class="nav-link py-2 small" @click="mode='custom'" :class="{active: mode==='custom'}">自定义</button>
|
<button class="nav-link py-2 small" @click="mode='custom'" :class="{active: mode==='custom'}">自定义</button>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<button class="nav-link py-2 small" @click="mode='command'" :class="{active: mode==='command'}">指令下发</button>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div v-if="mode === 'badge'">
|
<div v-if="mode === 'badge'">
|
||||||
@@ -195,6 +198,24 @@
|
|||||||
</div>
|
</div>
|
||||||
<button @click="sendCustomJson" class="btn btn-secondary btn-sm w-100">发送自定义数据</button>
|
<button @click="sendCustomJson" class="btn btn-secondary btn-sm w-100">发送自定义数据</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="mode === 'command'">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label small text-muted">接口类型</label>
|
||||||
|
<select v-model="commandForm.apiType" class="form-select form-select-sm" @change="updateJsonTemplate">
|
||||||
|
<option value="location">位置查询 (8201)</option>
|
||||||
|
<option value="text">文本下发 (8300)</option>
|
||||||
|
<option value="general">通用指令 (API)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label small text-muted">请求参数 (JSON)</label>
|
||||||
|
<textarea v-model="commandForm.jsonBody" class="form-control font-monospace form-control-sm" rows="8"></textarea>
|
||||||
|
</div>
|
||||||
|
<button @click="sendCommand" class="btn btn-primary btn-sm w-100">
|
||||||
|
<i class="fas fa-terminal me-2"></i>发送指令
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -380,6 +401,11 @@
|
|||||||
},
|
},
|
||||||
customJson: '{\n "deviceType": "sensor",\n "temp": 24.5\n}',
|
customJson: '{\n "deviceType": "sensor",\n "temp": 24.5\n}',
|
||||||
|
|
||||||
|
commandForm: {
|
||||||
|
apiType: 'location',
|
||||||
|
jsonBody: '{\n "phone": "09207455611"\n}'
|
||||||
|
},
|
||||||
|
|
||||||
// 实时数据存储
|
// 实时数据存储
|
||||||
badges: {}, // Map: id -> badge data
|
badges: {}, // Map: id -> badge data
|
||||||
counters: {}, // Map: id -> counter data
|
counters: {}, // Map: id -> counter data
|
||||||
@@ -493,6 +519,75 @@
|
|||||||
alert('JSON 格式错误');
|
alert('JSON 格式错误');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateJsonTemplate() {
|
||||||
|
const type = this.commandForm.apiType;
|
||||||
|
if (type === 'location') {
|
||||||
|
this.commandForm.jsonBody = '{\n "phone": "09207455611"\n}';
|
||||||
|
} else if (type === 'text') {
|
||||||
|
this.commandForm.jsonBody = '{\n "phone": "09207455611",\n "content": "#2014*SET*R:#",\n "flag": 1\n}';
|
||||||
|
} else if (type === 'general') {
|
||||||
|
this.commandForm.jsonBody = '{\n "imei": "09207455611",\n "cmd": "workmode",\n "params": ["2", "300"]\n}';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async sendCommand() {
|
||||||
|
let payload;
|
||||||
|
try {
|
||||||
|
payload = JSON.parse(this.commandForm.jsonBody);
|
||||||
|
} catch (e) {
|
||||||
|
alert('JSON 格式错误');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (this.commandForm.apiType === 'location') {
|
||||||
|
// 调用 /api/v1/device/command/location
|
||||||
|
const formData = new FormData();
|
||||||
|
if (!payload.phone) { alert('缺少 phone 字段'); return; }
|
||||||
|
formData.append('phone', payload.phone);
|
||||||
|
|
||||||
|
const res = await fetch('/api/v1/device/command/location', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
const result = await res.json();
|
||||||
|
if (result.code === 200) alert('指令已发送');
|
||||||
|
else alert('失败: ' + result.message);
|
||||||
|
|
||||||
|
} else if (this.commandForm.apiType === 'text') {
|
||||||
|
// 调用 /api/v1/device/command/text
|
||||||
|
const formData = new FormData();
|
||||||
|
if (!payload.phone || !payload.content) { alert('缺少 phone 或 content 字段'); return; }
|
||||||
|
formData.append('phone', payload.phone);
|
||||||
|
formData.append('content', payload.content);
|
||||||
|
formData.append('flag', payload.flag || 1);
|
||||||
|
|
||||||
|
const res = await fetch('/api/v1/device/command/text', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
const result = await res.json();
|
||||||
|
if (result.code === 200) alert('指令已发送');
|
||||||
|
else alert('失败: ' + result.message);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// 通用指令接口 /api/v1/device/command/send
|
||||||
|
if (!payload.imei || !payload.cmd) { alert('缺少 imei 或 cmd 字段'); return; }
|
||||||
|
|
||||||
|
const res = await fetch('/api/v1/device/command/send', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
const result = await res.json();
|
||||||
|
if (result.code === 200) alert('指令已发送');
|
||||||
|
else alert('失败: ' + result.message);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert('发送异常: ' + e.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// 辅助函数
|
// 辅助函数
|
||||||
formatTime(date) {
|
formatTime(date) {
|
||||||
|
|||||||
@@ -143,6 +143,9 @@
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<button class="nav-link py-2 small" @click="mode='custom'" :class="{active: mode==='custom'}">自定义</button>
|
<button class="nav-link py-2 small" @click="mode='custom'" :class="{active: mode==='custom'}">自定义</button>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<button class="nav-link py-2 small" @click="mode='command'" :class="{active: mode==='command'}">指令下发</button>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div v-if="mode === 'badge'">
|
<div v-if="mode === 'badge'">
|
||||||
@@ -195,6 +198,24 @@
|
|||||||
</div>
|
</div>
|
||||||
<button @click="sendCustomJson" class="btn btn-secondary btn-sm w-100">发送自定义数据</button>
|
<button @click="sendCustomJson" class="btn btn-secondary btn-sm w-100">发送自定义数据</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="mode === 'command'">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label small text-muted">接口类型</label>
|
||||||
|
<select v-model="commandForm.apiType" class="form-select form-select-sm" @change="updateJsonTemplate">
|
||||||
|
<option value="location">位置查询 (8201)</option>
|
||||||
|
<option value="text">文本下发 (8300)</option>
|
||||||
|
<option value="general">通用指令 (API)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label small text-muted">请求参数 (JSON)</label>
|
||||||
|
<textarea v-model="commandForm.jsonBody" class="form-control font-monospace form-control-sm" rows="8"></textarea>
|
||||||
|
</div>
|
||||||
|
<button @click="sendCommand" class="btn btn-primary btn-sm w-100">
|
||||||
|
<i class="fas fa-terminal me-2"></i>发送指令
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -380,6 +401,11 @@
|
|||||||
},
|
},
|
||||||
customJson: '{\n "deviceType": "sensor",\n "temp": 24.5\n}',
|
customJson: '{\n "deviceType": "sensor",\n "temp": 24.5\n}',
|
||||||
|
|
||||||
|
commandForm: {
|
||||||
|
apiType: 'location',
|
||||||
|
jsonBody: '{\n "phone": "09207455611"\n}'
|
||||||
|
},
|
||||||
|
|
||||||
// 实时数据存储
|
// 实时数据存储
|
||||||
badges: {}, // Map: id -> badge data
|
badges: {}, // Map: id -> badge data
|
||||||
counters: {}, // Map: id -> counter data
|
counters: {}, // Map: id -> counter data
|
||||||
@@ -493,6 +519,75 @@
|
|||||||
alert('JSON 格式错误');
|
alert('JSON 格式错误');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateJsonTemplate() {
|
||||||
|
const type = this.commandForm.apiType;
|
||||||
|
if (type === 'location') {
|
||||||
|
this.commandForm.jsonBody = '{\n "phone": "09207455611"\n}';
|
||||||
|
} else if (type === 'text') {
|
||||||
|
this.commandForm.jsonBody = '{\n "phone": "09207455611",\n "content": "#2014*SET*R:#",\n "flag": 1\n}';
|
||||||
|
} else if (type === 'general') {
|
||||||
|
this.commandForm.jsonBody = '{\n "imei": "09207455611",\n "cmd": "workmode",\n "params": ["2", "300"]\n}';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async sendCommand() {
|
||||||
|
let payload;
|
||||||
|
try {
|
||||||
|
payload = JSON.parse(this.commandForm.jsonBody);
|
||||||
|
} catch (e) {
|
||||||
|
alert('JSON 格式错误');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (this.commandForm.apiType === 'location') {
|
||||||
|
// 调用 /api/v1/device/command/location
|
||||||
|
const formData = new FormData();
|
||||||
|
if (!payload.phone) { alert('缺少 phone 字段'); return; }
|
||||||
|
formData.append('phone', payload.phone);
|
||||||
|
|
||||||
|
const res = await fetch('/api/v1/device/command/location', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
const result = await res.json();
|
||||||
|
if (result.code === 200) alert('指令已发送');
|
||||||
|
else alert('失败: ' + result.message);
|
||||||
|
|
||||||
|
} else if (this.commandForm.apiType === 'text') {
|
||||||
|
// 调用 /api/v1/device/command/text
|
||||||
|
const formData = new FormData();
|
||||||
|
if (!payload.phone || !payload.content) { alert('缺少 phone 或 content 字段'); return; }
|
||||||
|
formData.append('phone', payload.phone);
|
||||||
|
formData.append('content', payload.content);
|
||||||
|
formData.append('flag', payload.flag || 1);
|
||||||
|
|
||||||
|
const res = await fetch('/api/v1/device/command/text', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
const result = await res.json();
|
||||||
|
if (result.code === 200) alert('指令已发送');
|
||||||
|
else alert('失败: ' + result.message);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// 通用指令接口 /api/v1/device/command/send
|
||||||
|
if (!payload.imei || !payload.cmd) { alert('缺少 imei 或 cmd 字段'); return; }
|
||||||
|
|
||||||
|
const res = await fetch('/api/v1/device/command/send', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
const result = await res.json();
|
||||||
|
if (result.code === 200) alert('指令已发送');
|
||||||
|
else alert('失败: ' + result.message);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert('发送异常: ' + e.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// 辅助函数
|
// 辅助函数
|
||||||
formatTime(date) {
|
formatTime(date) {
|
||||||
|
|||||||
Reference in New Issue
Block a user