feat:【iot】单元测试:优化单测质量(格式)

This commit is contained in:
YunaiV
2026-01-27 20:03:51 +08:00
parent d2c000d64d
commit 432e1ed230
19 changed files with 50 additions and 909 deletions

View File

@@ -49,6 +49,7 @@ public class IotDirectDeviceCoapProtocolIntegrationTest {
private static final int SERVER_PORT = 5683;
// ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT";
private static final String DEVICE_NAME = "small";
private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3";

View File

@@ -58,6 +58,7 @@ public class IotGatewayDeviceCoapProtocolIntegrationTest {
private static final int SERVER_PORT = 5683;
// ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) =====================
private static final String GATEWAY_PRODUCT_KEY = "m6XcS1ZJ3TW8eC0v";
private static final String GATEWAY_DEVICE_NAME = "sub-ddd";
private static final String GATEWAY_DEVICE_SECRET = "b3d62c70f8a4495487ed1d35d61ac2b3";
@@ -68,6 +69,7 @@ public class IotGatewayDeviceCoapProtocolIntegrationTest {
private static final String GATEWAY_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoibTZYY1MxWkozVFc4ZUMwdiIsImV4cCI6MTc2OTg2NjY3OCwiZGV2aWNlTmFtZSI6InN1Yi1kZGQifQ.nCLSAfHEjXLtTDRXARjOoFqpuo5WfArjFWweUAzrjKU";
// ===================== 子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
private static final String SUB_DEVICE_PRODUCT_KEY = "jAufEMTF1W6wnPhn";
private static final String SUB_DEVICE_NAME = "chazuo-it";
private static final String SUB_DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af";

View File

@@ -51,6 +51,7 @@ public class IotGatewaySubDeviceCoapProtocolIntegrationTest {
private static final int SERVER_PORT = 5683;
// ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
private static final String PRODUCT_KEY = "jAufEMTF1W6wnPhn";
private static final String DEVICE_NAME = "chazuo-it";
private static final String DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af";

View File

@@ -43,6 +43,7 @@ public class IotDirectDeviceHttpProtocolIntegrationTest {
private static final int SERVER_PORT = 8092;
// ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT";
private static final String DEVICE_NAME = "small";
private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3";

View File

@@ -52,6 +52,7 @@ public class IotGatewayDeviceHttpProtocolIntegrationTest {
private static final int SERVER_PORT = 8092;
// ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) =====================
private static final String GATEWAY_PRODUCT_KEY = "m6XcS1ZJ3TW8eC0v";
private static final String GATEWAY_DEVICE_NAME = "sub-ddd";
private static final String GATEWAY_DEVICE_SECRET = "b3d62c70f8a4495487ed1d35d61ac2b3";
@@ -62,6 +63,7 @@ public class IotGatewayDeviceHttpProtocolIntegrationTest {
private static final String GATEWAY_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoibTZYY1MxWkozVFc4ZUMwdiIsImV4cCI6MTc2OTg2NjY3OCwiZGV2aWNlTmFtZSI6InN1Yi1kZGQifQ.nCLSAfHEjXLtTDRXARjOoFqpuo5WfArjFWweUAzrjKU";
// ===================== 子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
private static final String SUB_DEVICE_PRODUCT_KEY = "jAufEMTF1W6wnPhn";
private static final String SUB_DEVICE_NAME = "chazuo-it";
private static final String SUB_DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af";

View File

@@ -45,6 +45,7 @@ public class IotGatewaySubDeviceHttpProtocolIntegrationTest {
private static final int SERVER_PORT = 8092;
// ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
private static final String PRODUCT_KEY = "jAufEMTF1W6wnPhn";
private static final String DEVICE_NAME = "chazuo-it";
private static final String DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af";

View File

@@ -55,17 +55,18 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
private static final int SERVER_PORT = 1883;
private static final int TIMEOUT_SECONDS = 10;
private static Vertx vertx;
// ===================== 编解码器MQTT 使用 Alink 协议) =====================
private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec();
// ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询) =====================
private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT";
private static final String DEVICE_NAME = "small";
private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3";
// ===================== 全局共享 Vertx 实例 =====================
private static Vertx vertx;
@BeforeAll
public static void setUp() {
vertx = Vertx.vertx();

View File

@@ -63,22 +63,24 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
private static final int SERVER_PORT = 1883;
private static final int TIMEOUT_SECONDS = 10;
private static Vertx vertx;
// ===================== 编解码器MQTT 使用 Alink 协议) =====================
private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec();
// ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) =====================
private static final String GATEWAY_PRODUCT_KEY = "m6XcS1ZJ3TW8eC0v";
private static final String GATEWAY_DEVICE_NAME = "sub-ddd";
private static final String GATEWAY_DEVICE_SECRET = "b3d62c70f8a4495487ed1d35d61ac2b3";
// ===================== 子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
private static final String SUB_DEVICE_PRODUCT_KEY = "jAufEMTF1W6wnPhn";
private static final String SUB_DEVICE_NAME = "chazuo-it";
private static final String SUB_DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af";
// ===================== 全局共享 Vertx 实例 =====================
private static Vertx vertx;
@BeforeAll
public static void setUp() {
vertx = Vertx.vertx();

View File

@@ -57,17 +57,18 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
private static final int SERVER_PORT = 1883;
private static final int TIMEOUT_SECONDS = 10;
private static Vertx vertx;
// ===================== 编解码器MQTT 使用 Alink 协议) =====================
private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec();
// ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
private static final String PRODUCT_KEY = "jAufEMTF1W6wnPhn";
private static final String DEVICE_NAME = "chazuo-it";
private static final String DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af";
// ===================== 全局共享 Vertx 实例 =====================
private static Vertx vertx;
@BeforeAll
public static void setUp() {
vertx = Vertx.vertx();

View File

@@ -57,10 +57,12 @@ public class IotDirectDeviceTcpProtocolIntegrationTest {
private static final int TIMEOUT_MS = 5000;
// ===================== 编解码器选择(修改此处切换 JSON / Binary =====================
// private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec();
private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec();
// ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询) =====================
private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT";
private static final String DEVICE_NAME = "small";
private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3";

View File

@@ -65,15 +65,18 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest {
private static final int TIMEOUT_MS = 5000;
// ===================== 编解码器选择(修改此处切换 JSON / Binary =====================
private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec();
// private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec();
// ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) =====================
private static final String GATEWAY_PRODUCT_KEY = "m6XcS1ZJ3TW8eC0v";
private static final String GATEWAY_DEVICE_NAME = "sub-ddd";
private static final String GATEWAY_DEVICE_SECRET = "b3d62c70f8a4495487ed1d35d61ac2b3";
// ===================== 子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
private static final String SUB_DEVICE_PRODUCT_KEY = "jAufEMTF1W6wnPhn";
private static final String SUB_DEVICE_NAME = "chazuo-it";
private static final String SUB_DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af";

View File

@@ -58,10 +58,12 @@ public class IotGatewaySubDeviceTcpProtocolIntegrationTest {
private static final int TIMEOUT_MS = 5000;
// ===================== 编解码器选择(修改此处切换 JSON / Binary =====================
private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec();
// private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec();
// ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
private static final String PRODUCT_KEY = "jAufEMTF1W6wnPhn";
private static final String DEVICE_NAME = "chazuo-it";
private static final String DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af";

View File

@@ -58,10 +58,12 @@ public class IotDirectDeviceUdpProtocolIntegrationTest {
private static final int TIMEOUT_MS = 5000;
// ===================== 编解码器选择(修改此处切换 JSON / Binary =====================
private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec();
// private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec();
// ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT";
private static final String DEVICE_NAME = "small";
private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3";

View File

@@ -64,10 +64,12 @@ public class IotGatewayDeviceUdpProtocolIntegrationTest {
private static final int TIMEOUT_MS = 5000;
// ===================== 编解码器选择(修改此处切换 JSON / Binary =====================
private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec();
// private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec();
// ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) =====================
private static final String GATEWAY_PRODUCT_KEY = "m6XcS1ZJ3TW8eC0v";
private static final String GATEWAY_DEVICE_NAME = "sub-ddd";
private static final String GATEWAY_DEVICE_SECRET = "b3d62c70f8a4495487ed1d35d61ac2b3";

View File

@@ -59,10 +59,12 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest {
private static final int TIMEOUT_MS = 5000;
// ===================== 编解码器选择(修改此处切换 JSON / Binary =====================
private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec();
// private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec();
// ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
private static final String PRODUCT_KEY = "jAufEMTF1W6wnPhn";
private static final String DEVICE_NAME = "chazuo-it";
private static final String DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af";

View File

@@ -55,17 +55,18 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest {
private static final String WS_PATH = "/ws";
private static final int TIMEOUT_SECONDS = 5;
// 编解码器
private static Vertx vertx;
// ===================== 编解码器选择 =====================
private static final IotDeviceMessageCodec CODEC = new IotWebSocketJsonDeviceMessageCodec();
// ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询) =====================
private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT";
private static final String DEVICE_NAME = "small";
private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3";
// Vert.x 实例
private static Vertx vertx;
@BeforeAll
public static void setUp() {
vertx = Vertx.vertx();

View File

@@ -63,22 +63,24 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest {
private static final String WS_PATH = "/ws";
private static final int TIMEOUT_SECONDS = 5;
// 编解码器
private static Vertx vertx;
// ===================== 编解码器选择 =====================
private static final IotDeviceMessageCodec CODEC = new IotWebSocketJsonDeviceMessageCodec();
// ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) =====================
private static final String GATEWAY_PRODUCT_KEY = "m6XcS1ZJ3TW8eC0v";
private static final String GATEWAY_DEVICE_NAME = "sub-ddd";
private static final String GATEWAY_DEVICE_SECRET = "b3d62c70f8a4495487ed1d35d61ac2b3";
// ===================== 子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
private static final String SUB_DEVICE_PRODUCT_KEY = "jAufEMTF1W6wnPhn";
private static final String SUB_DEVICE_NAME = "chazuo-it";
private static final String SUB_DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af";
// Vert.x 实例
private static Vertx vertx;
@BeforeAll
public static void setUp() {
vertx = Vertx.vertx();

View File

@@ -56,17 +56,18 @@ public class IotGatewaySubDeviceWebSocketProtocolIntegrationTest {
private static final String WS_PATH = "/ws";
private static final int TIMEOUT_SECONDS = 5;
// 编解码器
private static Vertx vertx;
// ===================== 编解码器选择 =====================
private static final IotDeviceMessageCodec CODEC = new IotWebSocketJsonDeviceMessageCodec();
// ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
private static final String PRODUCT_KEY = "jAufEMTF1W6wnPhn";
private static final String DEVICE_NAME = "chazuo-it";
private static final String DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af";
// Vert.x 实例
private static Vertx vertx;
@BeforeAll
public static void setUp() {
vertx = Vertx.vertx();

View File

@@ -1,888 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title>MQTT WebSocket 测试客户端</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 10px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
font-size: 28px;
margin-bottom: 10px;
}
.header p {
opacity: 0.9;
}
.info-box {
background: #f8f9fa;
border-left: 4px solid #667eea;
padding: 15px;
margin: 20px;
border-radius: 5px;
}
.info-box h3 {
color: #667eea;
margin-bottom: 10px;
font-size: 16px;
}
.info-box ul {
margin-left: 20px;
color: #666;
font-size: 14px;
line-height: 1.6;
}
.info-box code {
background: #e9ecef;
padding: 2px 6px;
border-radius: 3px;
font-family: 'Courier New', monospace;
color: #d63384;
}
.content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
padding: 30px;
}
.panel {
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
}
.panel h2 {
color: #667eea;
margin-bottom: 20px;
font-size: 20px;
border-bottom: 2px solid #667eea;
padding-bottom: 10px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
color: #333;
font-weight: 500;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: #667eea;
}
.form-group textarea {
resize: vertical;
min-height: 80px;
font-family: monospace;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.3s;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover {
background: #5568d3;
}
.btn-success {
background: #28a745;
color: white;
}
.btn-success:hover {
background: #218838;
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-danger:hover {
background: #c82333;
}
.btn-warning {
background: #ffc107;
color: #333;
}
.btn-warning:hover {
background: #e0a800;
}
.btn-group {
display: flex;
gap: 10px;
margin-top: 15px;
}
.status {
padding: 10px;
border-radius: 4px;
margin-bottom: 20px;
font-weight: 500;
}
.status.disconnected {
background: #f8d7da;
color: #721c24;
}
.status.connected {
background: #d4edda;
color: #155724;
}
.status.connecting {
background: #fff3cd;
color: #856404;
}
.log-area {
background: #1e1e1e;
color: #d4d4d4;
padding: 15px;
border-radius: 4px;
height: 400px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 12px;
line-height: 1.6;
}
.log-entry {
margin-bottom: 5px;
}
.log-entry.info {
color: #4ec9b0;
}
.log-entry.success {
color: #6a9955;
}
.log-entry.error {
color: #f48771;
}
.log-entry.warning {
color: #dcdcaa;
}
.stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-top: 15px;
}
.stat-item {
background: white;
padding: 15px;
border-radius: 4px;
text-align: center;
}
.stat-item .value {
font-size: 24px;
font-weight: bold;
color: #667eea;
}
.stat-item .label {
font-size: 12px;
color: #666;
margin-top: 5px;
}
@media (max-width: 768px) {
.content {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🚀 MQTT WebSocket 测试客户端</h1>
<p>RuoYi-Vue-Pro IoT 模块 - MQTT over WebSocket 在线测试工具</p>
</div>
<!-- 协议格式说明 -->
<div class="info-box">
<h3>📌 标准协议格式说明</h3>
<ul>
<li><strong>Topic 格式:</strong><code>/sys/{productKey}/{deviceName}/thing/property/post</code></li>
<li><strong>Client ID 格式:</strong><code>{productKey}.{deviceName}</code> 例如:<code>zOXKLvHjUqTo7ipD.ceshi001</code>
</li>
<li><strong>Username 格式:</strong><code>{deviceName}&{productKey}</code> 例如:<code>ceshi001&zOXKLvHjUqTo7ipD</code>
</li>
<li><strong>消息格式Alink 协议):</strong>
<pre style="background: #e9ecef; padding: 10px; border-radius: 5px; margin-top: 5px; overflow-x: auto;">
{
"id": "消息 ID唯一标识",
"version": "1.0",
"method": "thing.property.post",
"params": {
"temperature": 25.5,
"humidity": 60
}
}</pre>
</li>
<li><strong>常用 Topic下行 - 服务端推送):</strong>
<ul style="margin-top: 5px;">
<li>属性设置:<code>/sys/{pk}/{dn}/thing/property/set</code></li>
<li>服务调用:<code>/sys/{pk}/{dn}/thing/service/invoke</code></li>
<li>配置推送:<code>/sys/{pk}/{dn}/thing/config/push</code></li>
<li>OTA 升级:<code>/sys/{pk}/{dn}/thing/ota/upgrade</code></li>
</ul>
</li>
<li><strong>常用 Topic上行 - 设备上报):</strong>
<ul style="margin-top: 5px;">
<li>状态更新:<code>/sys/{pk}/{dn}/thing/state/update</code></li>
<li>属性上报:<code>/sys/{pk}/{dn}/thing/property/post</code></li>
<li>事件上报:<code>/sys/{pk}/{dn}/thing/event/post</code></li>
<li>OTA 进度:<code>/sys/{pk}/{dn}/thing/ota/progress</code></li>
</ul>
</li>
</ul>
</div>
<div class="content">
<!-- 连接配置面板 -->
<div class="panel">
<h2>📡 连接配置</h2>
<div class="status disconnected" id="statusBar">
⚫ 未连接
</div>
<div class="form-group">
<label>服务器地址</label>
<input id="serverUrl" placeholder="ws://host:port/path" type="text" value="ws://localhost:8083/mqtt">
<small style="color: #666; font-size: 12px;">WebSocket 地址,支持 ws:// 和 wss://</small>
</div>
<div class="form-group">
<label>Client ID</label>
<input id="clientId" placeholder="设备客户端 ID" type="text" value="fqTn4Afs982Nak4N.jiali001">
<small style="color: #666; font-size: 12px;">格式:{productKey}.{deviceName}</small>
</div>
<div class="form-group">
<label>Username</label>
<input id="username" placeholder="用户名" type="text" value="jiali001&fqTn4Afs982Nak4N">
<small style="color: #666; font-size: 12px;">格式:{deviceName}&{productKey}</small>
</div>
<div class="form-group">
<label>Password</label>
<input id="password" placeholder="设备密钥"
type="password" value="ae10188f93febbb6b37bd57f463b2a795ae2800fab8933aef75d3c6422873f28">
<small style="color: #666; font-size: 12px;">设备的认证密钥Device Secret</small>
</div>
<div class="btn-group">
<button class="btn btn-success" id="connectBtn" onclick="connect()">🔌 连接</button>
<button class="btn btn-danger" disabled id="disconnectBtn" onclick="disconnect()">🔌 断开</button>
<button class="btn btn-warning" onclick="clearLogs()">🗑️ 清空日志</button>
</div>
<!-- 统计信息 -->
<div class="stats">
<div class="stat-item">
<div class="value" id="sentCount">0</div>
<div class="label">发送消息数</div>
</div>
<div class="stat-item">
<div class="value" id="receivedCount">0</div>
<div class="label">接收消息数</div>
</div>
<div class="stat-item">
<div class="value" id="errorCount">0</div>
<div class="label">错误次数</div>
</div>
</div>
</div>
<!-- 消息发布面板 -->
<div class="panel">
<h2>📤 消息发布</h2>
<div class="form-group">
<label>快捷主题选择(上行消息 - 设备 → 服务端)</label>
<select id="quickPublishTopicSelect" onchange="selectQuickPublishTopic()"
style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 5px;">
<option value="">-- 选择上行消息类型 --</option>
<option value="thing.state.update">设备状态更新 (thing.state.update)</option>
<option value="thing.property.post">属性上报 (thing.property.post)</option>
<option value="thing.event.post">事件上报 (thing.event.post)</option>
<option value="thing.ota.progress">OTA 升级进度 (thing.ota.progress)</option>
</select>
</div>
<div class="form-group">
<label>主题 (Topic)</label>
<input id="pubTopic" placeholder="消息主题,格式:/sys/{productKey}/{deviceName}/thing/property/post" type="text"
value="/sys/fqTn4Afs982Nak4N/jiali001/thing/property/post">
<small style="color: #666; font-size: 12px;">标准格式:/sys/{productKey}/{deviceName}/thing/property/post</small>
</div>
<div class="form-group">
<label>QoS 级别</label>
<select id="pubQos">
<option value="0">0 - 最多一次</option>
<option selected value="1">1 - 至少一次</option>
<option value="2">2 - 刚好一次</option>
</select>
</div>
<div class="form-group">
<label>消息内容 (JSON - Alink 协议格式)</label>
<textarea id="pubMessage" placeholder='Alink 协议格式消息'>{
"id": "123456789",
"version": "1.0",
"method": "thing.property.post",
"params": {
"temperature": 25.5,
"humidity": 60
}
}</textarea>
<small style="color: #666; font-size: 12px;">
Alink 协议格式id消息 ID、version协议版本、method方法、params参数
</small>
</div>
<div class="btn-group">
<button class="btn btn-primary" onclick="publish()">📤 发布消息</button>
<button class="btn btn-success" onclick="publishSampleData()">📊 发送样例数据</button>
</div>
<h2 style="margin-top: 30px;">📥 主题订阅</h2>
<div class="form-group">
<label>快捷主题选择(下行消息 - 服务端 → 设备)</label>
<select id="quickTopicSelect" onchange="selectQuickTopic()"
style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 5px;">
<option value="">-- 选择下行消息类型 --</option>
<optgroup label="📥 下行消息">
<option value="thing.property.set">属性设置 (thing.property.set)</option>
<option value="thing.service.invoke">服务调用 (thing.service.invoke)</option>
<option value="thing.config.push">配置推送 (thing.config.push)</option>
<option value="thing.ota.upgrade">OTA 固件推送 (thing.ota.upgrade)</option>
</optgroup>
<optgroup label="🔄 回复主题(上行消息的回复)">
<option value="thing.property.post_reply">属性上报回复 (thing.property.post_reply)</option>
<option value="thing.event.post_reply">事件上报回复 (thing.event.post_reply)</option>
</optgroup>
<optgroup label="🔧 通配符订阅">
<option value="wildcard_all">订阅所有主题 (/sys/+/+/#)</option>
<option value="wildcard_thing">订阅所有 thing 主题 (/sys/+/+/thing/#)</option>
<option value="wildcard_reply">订阅所有回复主题 (/sys/+/+/#_reply)</option>
</optgroup>
</select>
</div>
<div class="form-group">
<label>订阅主题</label>
<input id="subTopic" placeholder="订阅主题,格式:/sys/{productKey}/{deviceName}/thing/property/set" type="text"
value="/sys/fqTn4Afs982Nak4N/jiali001/thing/property/set">
<small style="color: #666; font-size: 12px;">标准格式:/sys/{productKey}/{deviceName}/thing/method 或使用通配符
/sys/+/+/#</small>
</div>
<div class="form-group">
<label>QoS 级别</label>
<select id="subQos">
<option value="0">0 - 最多一次</option>
<option selected value="1">1 - 至少一次</option>
<option value="2">2 - 刚好一次</option>
</select>
</div>
<div class="btn-group">
<button class="btn btn-primary" onclick="subscribe()">📥 订阅</button>
<button class="btn btn-danger" onclick="unsubscribe()">❌ 取消订阅</button>
</div>
</div>
<!-- 日志面板 -->
<div class="panel" style="grid-column: 1 / -1;">
<h2>📝 日志输出</h2>
<div class="log-area" id="logArea"></div>
</div>
</div>
</div>
<!-- 使用 MQTT.js 库 -->
<script src="https://unpkg.com/mqtt/dist/mqtt.min.js"></script>
<script>
let client = null;
let sentCount = 0;
let receivedCount = 0;
let errorCount = 0;
// 添加日志
function addLog(message, type = 'info') {
const logArea = document.getElementById('logArea');
const timestamp = new Date().toLocaleTimeString();
const logEntry = document.createElement('div');
logEntry.className = `log-entry ${type}`;
logEntry.textContent = `[${timestamp}] ${message}`;
logArea.appendChild(logEntry);
logArea.scrollTop = logArea.scrollHeight;
}
// 更新状态栏
function updateStatus(status, text) {
const statusBar = document.getElementById('statusBar');
statusBar.className = `status ${status}`;
const icons = {
'disconnected': '⚫',
'connecting': '🟡',
'connected': '🟢'
};
statusBar.textContent = `${icons[status]} ${text}`;
}
// 更新统计信息
function updateStats() {
document.getElementById('sentCount').textContent = sentCount;
document.getElementById('receivedCount').textContent = receivedCount;
document.getElementById('errorCount').textContent = errorCount;
}
// 连接到服务器
function connect() {
const serverUrl = document.getElementById('serverUrl').value;
const clientId = document.getElementById('clientId').value;
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
if (!serverUrl || !clientId) {
addLog('❌ 请填写服务器地址和 Client ID', 'error');
errorCount++;
updateStats();
return;
}
updateStatus('connecting', '正在连接...');
addLog(`🔄 正在连接到 ${serverUrl}...`, 'info');
const options = {
clientId: clientId,
username: username,
password: password,
clean: true,
reconnectPeriod: 5000,
connectTimeout: 30000,
};
client = mqtt.connect(serverUrl, options);
// 连接成功
client.on('connect', () => {
updateStatus('connected', '已连接');
addLog('✅ 连接成功!', 'success');
document.getElementById('connectBtn').disabled = true;
document.getElementById('disconnectBtn').disabled = false;
});
// 接收消息
client.on('message', (topic, message) => {
receivedCount++;
updateStats();
addLog(`📥 收到消息 [${topic}]: ${message.toString()}`, 'success');
});
// 连接错误
client.on('error', (error) => {
errorCount++;
updateStats();
addLog(`❌ 连接错误: ${error.message}`, 'error');
});
// 断开连接
client.on('close', () => {
updateStatus('disconnected', '未连接');
addLog('🔌 连接已断开', 'warning');
document.getElementById('connectBtn').disabled = false;
document.getElementById('disconnectBtn').disabled = true;
});
// 离线
client.on('offline', () => {
updateStatus('disconnected', '离线');
addLog('⚠️ 客户端离线', 'warning');
});
// 重连
client.on('reconnect', () => {
updateStatus('connecting', '正在重连...');
addLog('🔄 正在重连...', 'info');
});
}
// 断开连接
function disconnect() {
if (client) {
client.end();
addLog('👋 主动断开连接', 'info');
}
}
// 发布消息
function publish() {
if (!client || !client.connected) {
addLog('❌ 请先连接到服务器', 'error');
errorCount++;
updateStats();
return;
}
const topic = document.getElementById('pubTopic').value;
const qos = parseInt(document.getElementById('pubQos').value);
const message = document.getElementById('pubMessage').value;
if (!topic || !message) {
addLog('❌ 请填写主题和消息内容', 'error');
errorCount++;
updateStats();
return;
}
// 验证 JSON 格式
try {
JSON.parse(message);
} catch (e) {
addLog('⚠️ 消息不是有效的 JSON 格式,将作为纯文本发送', 'warning');
}
client.publish(topic, message, {qos: qos}, (error) => {
if (error) {
errorCount++;
updateStats();
addLog(`❌ 发布失败: ${error.message}`, 'error');
} else {
sentCount++;
updateStats();
addLog(`📤 消息已发布 [${topic}] (QoS ${qos})`, 'success');
}
});
}
// 发送样例数据
function publishSampleData() {
// 使用 Alink 协议格式的样例数据
const sampleData = {
id: Date.now().toString(),
version: "1.0",
method: "thing.property.post",
params: {
temperature: parseFloat((20 + Math.random() * 10).toFixed(2)),
humidity: parseFloat((50 + Math.random() * 20).toFixed(2)),
pressure: parseFloat((1000 + Math.random() * 50).toFixed(2))
}
};
document.getElementById('pubMessage').value = JSON.stringify(sampleData, null, 2);
addLog('样例数据已生成Alink 协议格式)', 'info');
publish();
}
// 获取 productKey 和 deviceName
function getDeviceInfo() {
const clientId = document.getElementById('clientId').value;
const parts = clientId.split('.');
if (parts.length !== 2) {
addLog('❌ Client ID 格式不正确(应为 {productKey}.{deviceName}),无法生成主题', 'error');
return null;
}
return {
productKey: parts[0],
deviceName: parts[1]
};
}
// 快捷主题选择(消息发布 - 上行消息)
function selectQuickPublishTopic() {
const select = document.getElementById('quickPublishTopicSelect');
const selectedValue = select.value;
console.log('[selectQuickPublishTopic] 选择的值:', selectedValue);
if (!selectedValue) {
return;
}
const deviceInfo = getDeviceInfo();
if (!deviceInfo) {
return;
}
console.log('[selectQuickPublishTopic] 设备信息:', deviceInfo);
// 构建标准主题,将枚举中的点号替换为斜杠
// 例如thing.property.post -> /sys/{pk}/{dn}/thing/property/post
const topic = `/sys/${deviceInfo.productKey}/${deviceInfo.deviceName}/${selectedValue.replace(/\./g, '/')}`;
console.log('[selectQuickPublishTopic] 生成的主题:', topic);
const pubTopicInput = document.getElementById('pubTopic');
pubTopicInput.value = topic;
console.log('[selectQuickPublishTopic] 输入框的值已设置为:', pubTopicInput.value);
addLog(`📋 已选择发布主题: ${topic}`, 'info');
// 需要 reply 的消息类型(不在 REPLY_DISABLED 列表中)
const needsReply = [
'thing.property.post',
'thing.event.post'
];
// 如果需要 reply自动订阅 reply 主题
if (needsReply.includes(selectedValue)) {
const replyTopic = `${topic}_reply`;
if (client && client.connected) {
// 自动订阅 reply 主题
client.subscribe(replyTopic, {qos: 1}, (err) => {
if (!err) {
addLog(`✅ 已自动订阅回复主题: ${replyTopic}`, 'success');
} else {
addLog(`❌ 自动订阅回复主题失败: ${err.message}`, 'error');
}
});
} else {
addLog(`💡 提示: 该消息需要订阅回复主题 ${replyTopic}`, 'warning');
}
}
// 重置下拉框到默认选项
select.selectedIndex = 0;
console.log('[selectQuickPublishTopic] 下拉框已重置');
}
// 快捷主题选择(主题订阅 - 下行消息)
function selectQuickTopic() {
const select = document.getElementById('quickTopicSelect');
const selectedValue = select.value;
console.log('[selectQuickTopic] 选择的值:', selectedValue);
if (!selectedValue) {
return;
}
const subTopicInput = document.getElementById('subTopic');
// 处理通配符订阅
if (selectedValue === 'wildcard_all') {
subTopicInput.value = '/sys/+/+/#';
addLog('📋 已选择订阅主题: /sys/+/+/#(订阅所有主题)', 'info');
console.log('[selectQuickTopic] 输入框的值已设置为:', subTopicInput.value);
select.selectedIndex = 0;
console.log('[selectQuickTopic] 下拉框已重置');
return;
} else if (selectedValue === 'wildcard_thing') {
subTopicInput.value = '/sys/+/+/thing/#';
addLog('📋 已选择订阅主题: /sys/+/+/thing/#(订阅所有 thing 主题)', 'info');
console.log('[selectQuickTopic] 输入框的值已设置为:', subTopicInput.value);
select.selectedIndex = 0;
console.log('[selectQuickTopic] 下拉框已重置');
return;
} else if (selectedValue === 'wildcard_reply') {
const deviceInfo = getDeviceInfo();
if (!deviceInfo) {
select.selectedIndex = 0;
return;
}
subTopicInput.value = `/sys/${deviceInfo.productKey}/${deviceInfo.deviceName}/#_reply`;
addLog(`📋 已选择订阅主题: /sys/${deviceInfo.productKey}/${deviceInfo.deviceName}/#_reply订阅所有回复主题`, 'info');
console.log('[selectQuickTopic] 输入框的值已设置为:', subTopicInput.value);
select.selectedIndex = 0;
console.log('[selectQuickTopic] 下拉框已重置');
return;
}
const deviceInfo = getDeviceInfo();
if (!deviceInfo) {
select.selectedIndex = 0;
return;
}
console.log('[selectQuickTopic] 设备信息:', deviceInfo);
// 构建标准主题,将枚举中的点号替换为斜杠
// 例如thing.property.set -> /sys/{pk}/{dn}/thing/property/set
const topic = `/sys/${deviceInfo.productKey}/${deviceInfo.deviceName}/${selectedValue.replace(/\./g, '/')}`;
console.log('[selectQuickTopic] 生成的主题:', topic);
subTopicInput.value = topic;
addLog(`📋 已选择订阅主题: ${topic}`, 'info');
console.log('[selectQuickTopic] 输入框的值已设置为:', subTopicInput.value);
// 重置下拉框到默认选项
select.selectedIndex = 0;
console.log('[selectQuickTopic] 下拉框已重置');
}
// 订阅主题
function subscribe() {
if (!client || !client.connected) {
addLog('❌ 请先连接到服务器', 'error');
errorCount++;
updateStats();
return;
}
const topic = document.getElementById('subTopic').value;
const qos = parseInt(document.getElementById('subQos').value);
if (!topic) {
addLog('❌ 请填写订阅主题', 'error');
errorCount++;
updateStats();
return;
}
client.subscribe(topic, {qos: qos}, (error) => {
if (error) {
errorCount++;
updateStats();
addLog(`❌ 订阅失败: ${error.message}`, 'error');
} else {
addLog(`📥 已订阅主题 [${topic}] (QoS ${qos})`, 'success');
}
});
}
// 取消订阅
function unsubscribe() {
if (!client || !client.connected) {
addLog('❌ 请先连接到服务器', 'error');
errorCount++;
updateStats();
return;
}
const topic = document.getElementById('subTopic').value;
if (!topic) {
addLog('❌ 请填写要取消的订阅主题', 'error');
errorCount++;
updateStats();
return;
}
client.unsubscribe(topic, (error) => {
if (error) {
errorCount++;
updateStats();
addLog(`❌ 取消订阅失败: ${error.message}`, 'error');
} else {
addLog(`❌ 已取消订阅 [${topic}]`, 'info');
}
});
}
// 清空日志
function clearLogs() {
document.getElementById('logArea').innerHTML = '';
sentCount = 0;
receivedCount = 0;
errorCount = 0;
updateStats();
addLog('🗑️ 日志已清空', 'info');
}
// 页面加载完成
window.onload = function () {
addLog('👋 欢迎使用 MQTT WebSocket 测试客户端!', 'success');
addLog('📚 请配置连接参数后点击"连接"按钮', 'info');
};
// 页面关闭前断开连接
window.onbeforeunload = function () {
if (client && client.connected) {
client.end();
}
};
</script>
</body>
</html>