You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
733 lines
21 KiB
733 lines
21 KiB
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Modbus RS485扫描工具</title>
|
|
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
}
|
|
|
|
body {
|
|
background: linear-gradient(135deg, #1a2a6c, #2c3e50);
|
|
color: #333;
|
|
min-height: 100vh;
|
|
padding: 20px;
|
|
}
|
|
|
|
#app {
|
|
max-width: 1800px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
header {
|
|
text-align: center;
|
|
padding: 20px 0;
|
|
color: white;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
header h1 {
|
|
font-size: 2.5rem;
|
|
margin-bottom: 10px;
|
|
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
|
}
|
|
|
|
header p {
|
|
font-size: 1.1rem;
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
opacity: 0.9;
|
|
}
|
|
|
|
.app-container {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr 1fr;
|
|
gap: 20px;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.panel {
|
|
background: rgba(255, 255, 255, 0.93);
|
|
border-radius: 15px;
|
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
|
padding: 25px;
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.panel-title {
|
|
font-size: 1.4rem;
|
|
color: #2c3e50;
|
|
margin-bottom: 20px;
|
|
padding-bottom: 12px;
|
|
border-bottom: 2px solid #3498db;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.panel-title i {
|
|
margin-right: 10px;
|
|
color: #3498db;
|
|
}
|
|
|
|
.config-group {
|
|
margin-bottom: 25px;
|
|
}
|
|
|
|
.config-group h3 {
|
|
font-size: 1.1rem;
|
|
color: #2c3e50;
|
|
margin-bottom: 15px;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.config-group h3 i {
|
|
margin-right: 8px;
|
|
color: #3498db;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.form-row {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 15px;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.form-item {
|
|
flex: 1;
|
|
min-width: 200px;
|
|
}
|
|
|
|
label {
|
|
display: block;
|
|
margin-bottom: 8px;
|
|
font-weight: 600;
|
|
color: #34495e;
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
select, input {
|
|
width: 100%;
|
|
padding: 12px 15px;
|
|
border: 2px solid #ddd;
|
|
border-radius: 8px;
|
|
font-size: 1rem;
|
|
background: #f8f9fa;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
select:focus, input:focus {
|
|
border-color: #3498db;
|
|
outline: none;
|
|
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2);
|
|
background: white;
|
|
}
|
|
|
|
.log-container {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
background: #1e1f2a;
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
font-family: 'Courier New', monospace;
|
|
color: #e0e0e0;
|
|
height: 500px;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.log-entry {
|
|
margin-bottom: 12px;
|
|
line-height: 1.5;
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.log-send {
|
|
color: #64b5f6;
|
|
}
|
|
|
|
.log-receive {
|
|
color: #81c784;
|
|
}
|
|
|
|
.log-valid {
|
|
color: #4caf50;
|
|
}
|
|
|
|
.log-error {
|
|
color: #e57373;
|
|
}
|
|
|
|
.device-list {
|
|
list-style-type: none;
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.device-item {
|
|
background: #f8f9fa;
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
margin-bottom: 15px;
|
|
border-left: 4px solid #3498db;
|
|
transition: all 0.3s;
|
|
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
|
|
}
|
|
|
|
.device-item:hover {
|
|
transform: translateY(-3px);
|
|
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.device-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.device-address {
|
|
font-weight: bold;
|
|
font-size: 1.2rem;
|
|
color: #2c3e50;
|
|
}
|
|
|
|
.device-status {
|
|
padding: 5px 12px;
|
|
border-radius: 20px;
|
|
font-size: 0.85rem;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.status-active {
|
|
background: #e8f5e9;
|
|
color: #2e7d32;
|
|
}
|
|
|
|
.status-inactive {
|
|
background: #ffebee;
|
|
color: #c62828;
|
|
}
|
|
|
|
.device-details {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 10px;
|
|
}
|
|
|
|
.device-detail {
|
|
background: white;
|
|
padding: 10px;
|
|
border-radius: 6px;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.control-bar {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 20px;
|
|
margin-top: 30px;
|
|
}
|
|
|
|
.btn {
|
|
padding: 15px 35px;
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-size: 1.1rem;
|
|
font-weight: bold;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
transition: all 0.3s;
|
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.btn-primary {
|
|
background: linear-gradient(to right, #3498db, #2980b9);
|
|
color: white;
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
background: linear-gradient(to right, #2980b9, #2573a7);
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 6px 12px rgba(0,0,0,0.15);
|
|
}
|
|
|
|
.btn-stop {
|
|
background: linear-gradient(to right, #e74c3c, #c0392b);
|
|
color: white;
|
|
}
|
|
|
|
.btn-stop:hover {
|
|
background: linear-gradient(to right, #c0392b, #a93226);
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 6px 12px rgba(0,0,0,0.15);
|
|
}
|
|
|
|
.stats-container {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 15px;
|
|
margin-bottom: 25px;
|
|
}
|
|
|
|
.stat-card {
|
|
background: #f8f9fa;
|
|
border-radius: 10px;
|
|
padding: 20px;
|
|
text-align: center;
|
|
box-shadow: 0 3px 6px rgba(0,0,0,0.05);
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 2.2rem;
|
|
font-weight: bold;
|
|
color: #3498db;
|
|
margin: 10px 0;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 0.9rem;
|
|
color: #7f8c8d;
|
|
}
|
|
|
|
.progress-container {
|
|
margin-top: 20px;
|
|
background: #e0e0e0;
|
|
border-radius: 10px;
|
|
height: 15px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.progress-bar {
|
|
height: 100%;
|
|
background: linear-gradient(to right, #3498db, #2ecc71);
|
|
border-radius: 10px;
|
|
transition: width 0.5s;
|
|
}
|
|
|
|
/* 响应式设计 */
|
|
@media (max-width: 1200px) {
|
|
.app-container {
|
|
grid-template-columns: 1fr 1fr;
|
|
}
|
|
.panel:last-child {
|
|
grid-column: span 2;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.app-container {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
.panel:last-child {
|
|
grid-column: span 1;
|
|
}
|
|
.form-row {
|
|
flex-direction: column;
|
|
}
|
|
.stats-container {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="app">
|
|
<header>
|
|
<h1><i class="fas fa-microchip"></i> Modbus RS485设备扫描工具</h1>
|
|
<p>配置RS485参数,扫描Modbus设备并查看通信状态</p>
|
|
</header>
|
|
|
|
<div class="app-container">
|
|
<!-- 参数配置面板 -->
|
|
<div class="panel">
|
|
<h2 class="panel-title"><i class="fas fa-cog"></i> 通信参数配置</h2>
|
|
|
|
<div class="config-group">
|
|
<h3><i class="fas fa-portrait"></i> 串口设置</h3>
|
|
<div class="form-row">
|
|
<div class="form-item">
|
|
<label for="port">串口号</label>
|
|
<select id="port" v-model="config.port">
|
|
<option>COM1</option>
|
|
<option>COM2</option>
|
|
<option>COM3</option>
|
|
<option>COM4</option>
|
|
<option>/dev/ttyUSB0</option>
|
|
<option>/dev/ttyUSB1</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-item">
|
|
<label for="baudrate">波特率</label>
|
|
<select id="baudrate" v-model="config.baudrate">
|
|
<option>1200</option>
|
|
<option>2400</option>
|
|
<option>4800</option>
|
|
<option>9600</option>
|
|
<option>19200</option>
|
|
<option>38400</option>
|
|
<option>57600</option>
|
|
<option>115200</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-item">
|
|
<label for="bytesize">数据位</label>
|
|
<select id="bytesize" v-model="config.bytesize">
|
|
<option>8</option>
|
|
<option>7</option>
|
|
<option>6</option>
|
|
<option>5</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-item">
|
|
<label for="stopbits">停止位</label>
|
|
<select id="stopbits" v-model="config.stopbits">
|
|
<option>1</option>
|
|
<option>1.5</option>
|
|
<option>2</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-item">
|
|
<label for="parity">校验位</label>
|
|
<select id="parity" v-model="config.parity">
|
|
<option>无校验</option>
|
|
<option>偶校验</option>
|
|
<option>奇校验</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="config-group">
|
|
<h3><i class="fas fa-broadcast-tower"></i> Modbus协议设置</h3>
|
|
<div class="form-row">
|
|
<div class="form-item">
|
|
<label for="functionCode">功能码</label>
|
|
<select id="functionCode" v-model="config.functionCode">
|
|
<option value="03">03 - 读保持寄存器</option>
|
|
<option value="04">04 - 读输入寄存器</option>
|
|
<option value="01">01 - 读线圈状态</option>
|
|
<option value="02">02 - 读离散输入</option>
|
|
<option value="05">05 - 写单个线圈</option>
|
|
<option value="06">06 - 写单个寄存器</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-item">
|
|
<label for="registerAddress">寄存器地址</label>
|
|
<input type="number" id="registerAddress" v-model.number="config.registerAddress" min="0" max="65535">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="config-group">
|
|
<h3><i class="fas fa-search"></i> 扫描设置</h3>
|
|
<div class="form-row">
|
|
<div class="form-item">
|
|
<label for="startAddr">起始地址</label>
|
|
<input type="number" id="startAddr" v-model.number="config.startAddr" min="1" max="247">
|
|
</div>
|
|
<div class="form-item">
|
|
<label for="endAddr">结束地址</label>
|
|
<input type="number" id="endAddr" v-model.number="config.endAddr" min="1" max="247">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-item">
|
|
<label for="timeout">超时时间(秒)</label>
|
|
<input type="number" id="timeout" v-model.number="config.timeout" min="0.1" max="5" step="0.1">
|
|
</div>
|
|
<div class="form-item">
|
|
<label for="retries">重试次数</label>
|
|
<input type="number" id="retries" v-model.number="config.retries" min="1" max="5">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-item">
|
|
<label for="sendInterval">发送间隔(秒)</label>
|
|
<input type="number" id="sendInterval" v-model.number="config.sendInterval" min="0.01" max="1" step="0.01">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 实时日志面板 -->
|
|
<div class="panel">
|
|
<h2 class="panel-title"><i class="fas fa-terminal"></i> 通信日志</h2>
|
|
|
|
<div class="stats-container">
|
|
<div class="stat-card">
|
|
<div class="stat-label">扫描进度</div>
|
|
<div class="stat-value">{{ progress }}%</div>
|
|
<div class="progress-container">
|
|
<div class="progress-bar" :style="{width: progress + '%'}"></div>
|
|
</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">活动设备</div>
|
|
<div class="stat-value">{{ activeDevices.length }}</div>
|
|
<div class="stat-label">检测到</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">当前状态</div>
|
|
<div class="stat-value" :style="{color: isScanning ? '#2ecc71' : '#e74c3c'}">
|
|
{{ isScanning ? '扫描中' : '就绪' }}
|
|
</div>
|
|
<div class="stat-label">{{ currentAddress }}/{{ config.endAddr }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="log-container">
|
|
<div v-for="(log, index) in logs" :key="index" class="log-entry" :class="logClass(log)">
|
|
<i :class="logIcon(log)"></i> {{ log.message }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 结果展示面板 -->
|
|
<div class="panel">
|
|
<h2 class="panel-title"><i class="fas fa-list"></i> 扫描结果</h2>
|
|
|
|
<div v-if="activeDevices.length > 0">
|
|
<h3>活动设备列表 ({{ activeDevices.length }})</h3>
|
|
<ul class="device-list">
|
|
<li v-for="device in activeDevices" :key="device.address" class="device-item">
|
|
<div class="device-header">
|
|
<div class="device-address">设备地址 {{ device.address }}</div>
|
|
<div class="device-status status-active">响应有效</div>
|
|
</div>
|
|
<div class="device-details">
|
|
<div class="device-detail">
|
|
<strong>寄存器地址:</strong> {{ config.registerAddress }} (0x{{ config.registerAddress.toString(16).toUpperCase().padStart(4, '0') }})
|
|
</div>
|
|
<div class="device-detail">
|
|
<strong>寄存器值:</strong> {{ device.registerValue }}
|
|
</div>
|
|
<div class="device-detail">
|
|
<strong>响应时间:</strong> {{ device.responseTime }} ms
|
|
</div>
|
|
<div class="device-detail">
|
|
<strong>尝试次数:</strong> {{ device.attempts }}
|
|
</div>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div v-else style="text-align: center; padding: 40px 0; color: #7f8c8d;">
|
|
<i class="fas fa-inbox" style="font-size: 3rem; margin-bottom: 20px;"></i>
|
|
<h3>未检测到活动设备</h3>
|
|
<p>请检查设备连接和参数设置</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="control-bar">
|
|
<button class="btn btn-primary" @click="startScan" :disabled="isScanning">
|
|
<i class="fas fa-play"></i> 开始扫描
|
|
</button>
|
|
<button class="btn btn-stop" @click="stopScan" :disabled="!isScanning">
|
|
<i class="fas fa-stop"></i> 停止扫描
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const { createApp, ref, computed, reactive } = Vue
|
|
|
|
createApp({
|
|
setup() {
|
|
// 默认配置
|
|
const config = reactive({
|
|
port: 'COM3',
|
|
baudrate: '9600',
|
|
bytesize: '8',
|
|
stopbits: '1',
|
|
parity: '无校验',
|
|
functionCode: '03',
|
|
registerAddress: 0,
|
|
startAddr: 1,
|
|
endAddr: 10,
|
|
timeout: 0.2,
|
|
retries: 1,
|
|
sendInterval: 0.5
|
|
})
|
|
|
|
// 状态变量
|
|
const isScanning = ref(false)
|
|
const logs = ref([])
|
|
const activeDevices = ref([])
|
|
const currentAddress = ref(0)
|
|
const progress = ref(0)
|
|
let scanInterval
|
|
|
|
// 计算扫描进度
|
|
const scanProgress = computed(() => {
|
|
const total = config.endAddr - config.startAddr + 1
|
|
const completed = currentAddress.value - config.startAddr
|
|
return total > 0 ? Math.round((completed / total) * 100) : 0
|
|
})
|
|
|
|
// 添加日志
|
|
const addLog = (message, type = 'info') => {
|
|
logs.value.push({
|
|
message,
|
|
type,
|
|
timestamp: new Date().toLocaleTimeString()
|
|
})
|
|
// 保持日志在100条以内
|
|
if (logs.value.length > 100) {
|
|
logs.value.shift()
|
|
}
|
|
}
|
|
|
|
// 日志样式
|
|
const logClass = (log) => {
|
|
return {
|
|
'log-send': log.type === 'send',
|
|
'log-receive': log.type === 'receive',
|
|
'log-valid': log.type === 'valid',
|
|
'log-error': log.type === 'error'
|
|
}
|
|
}
|
|
|
|
// 日志图标
|
|
const logIcon = (log) => {
|
|
return {
|
|
'fas fa-paper-plane': log.type === 'send',
|
|
'fas fa-inbox': log.type === 'receive',
|
|
'fas fa-check-circle': log.type === 'valid',
|
|
'fas fa-exclamation-circle': log.type === 'error',
|
|
'fas fa-info-circle': log.type === 'info'
|
|
}
|
|
}
|
|
|
|
// 开始扫描
|
|
const startScan = () => {
|
|
// 重置状态
|
|
logs.value = []
|
|
activeDevices.value = []
|
|
isScanning.value = true
|
|
currentAddress.value = config.startAddr
|
|
progress.value = 0
|
|
|
|
addLog('开始扫描 Modbus 设备...', 'info')
|
|
addLog(`串口: ${config.port}, 波特率: ${config.baudrate}, 数据位: ${config.bytesize}, 停止位: ${config.stopbits}, 校验: ${config.parity}`, 'info')
|
|
addLog(`功能码: ${config.functionCode}, 寄存器地址: ${config.registerAddress} (0x${config.registerAddress.toString(16).toUpperCase().padStart(4, '0')})`, 'info')
|
|
addLog(`地址范围: ${config.startAddr} - ${config.endAddr}`, 'info')
|
|
|
|
// 模拟扫描过程
|
|
simulateScan()
|
|
}
|
|
|
|
// 停止扫描
|
|
const stopScan = () => {
|
|
isScanning.value = false
|
|
clearInterval(scanInterval)
|
|
addLog('扫描已停止', 'info')
|
|
}
|
|
|
|
// 模拟扫描过程
|
|
const simulateScan = () => {
|
|
scanInterval = setInterval(() => {
|
|
if (currentAddress.value > config.endAddr) {
|
|
clearInterval(scanInterval)
|
|
isScanning.value = false
|
|
addLog(`扫描完成! 发现 ${activeDevices.value.length} 个活动设备`, 'info')
|
|
return
|
|
}
|
|
|
|
// 更新进度
|
|
progress.value = scanProgress.value
|
|
|
|
// 发送请求
|
|
const frame = `地址 ${currentAddress.value}: 发送 -> ${generateFrame(currentAddress.value)}`
|
|
addLog(frame, 'send')
|
|
|
|
// 模拟响应
|
|
setTimeout(() => {
|
|
// 随机决定是否有响应
|
|
const hasResponse = Math.random() > 0.7
|
|
|
|
if (hasResponse) {
|
|
// 模拟接收数据
|
|
const response = generateResponse(currentAddress.value)
|
|
addLog(`地址 ${currentAddress.value}: 接收 <- ${response}`, 'receive')
|
|
|
|
// 模拟有效响应
|
|
const isValid = Math.random() > 0.2
|
|
|
|
if (isValid) {
|
|
const registerValue = Math.floor(Math.random() * 65536)
|
|
addLog(`地址 ${currentAddress.value} - 设备响应有效 | 寄存器 ${config.registerAddress} 值: ${registerValue}`, 'valid')
|
|
|
|
// 添加到活动设备列表
|
|
activeDevices.value.push({
|
|
address: currentAddress.value,
|
|
registerValue: registerValue,
|
|
responseTime: Math.floor(Math.random() * 100) + 10,
|
|
attempts: 1
|
|
})
|
|
} else {
|
|
addLog(`地址 ${currentAddress.value} - 设备响应无效`, 'error')
|
|
}
|
|
} else {
|
|
addLog(`地址 ${currentAddress.value} - 无响应`, 'error')
|
|
}
|
|
|
|
// 移动到下一个地址
|
|
currentAddress.value++
|
|
}, config.sendInterval * 1000)
|
|
}, config.sendInterval * 1000 + 100)
|
|
}
|
|
|
|
// 生成模拟帧
|
|
const generateFrame = (address) => {
|
|
const funcCode = config.functionCode.padStart(2, '0')
|
|
const regAddr = config.registerAddress.toString(16).toUpperCase().padStart(4, '0')
|
|
return `${address.toString(16).toUpperCase().padStart(2, '0')} ${funcCode} ${regAddr.substring(0, 2)} ${regAddr.substring(2, 4)} 00 01 XX XX`
|
|
}
|
|
|
|
// 生成模拟响应
|
|
const generateResponse = (address) => {
|
|
const funcCode = config.functionCode.padStart(2, '0')
|
|
const regValue = Math.floor(Math.random() * 65536).toString(16).toUpperCase().padStart(4, '0')
|
|
return `${address.toString(16).toUpperCase().padStart(2, '0')} ${funcCode} 02 ${regValue.substring(0, 2)} ${regValue.substring(2, 4)} XX XX`
|
|
}
|
|
|
|
return {
|
|
config,
|
|
isScanning,
|
|
logs,
|
|
activeDevices,
|
|
currentAddress,
|
|
progress,
|
|
startScan,
|
|
stopScan,
|
|
logClass,
|
|
logIcon
|
|
}
|
|
}
|
|
}).mount('#app')
|
|
</script>
|
|
</body>
|
|
</html>
|