commit
3ab7d1f126
21 changed files with 2621 additions and 0 deletions
@ -0,0 +1,3 @@ |
|||
# 默认忽略的文件 |
|||
/shelf/ |
|||
/workspace.xml |
@ -0,0 +1,6 @@ |
|||
<component name="InspectionProjectProfileManager"> |
|||
<settings> |
|||
<option name="USE_PROJECT_PROFILE" value="false" /> |
|||
<version value="1.0" /> |
|||
</settings> |
|||
</component> |
@ -0,0 +1,7 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<project version="4"> |
|||
<component name="Black"> |
|||
<option name="sdkName" value="Python 3.12 (modbusTools)" /> |
|||
</component> |
|||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12" project-jdk-type="Python SDK" /> |
|||
</project> |
@ -0,0 +1,10 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<module type="PYTHON_MODULE" version="4"> |
|||
<component name="NewModuleRootManager"> |
|||
<content url="file://$MODULE_DIR$"> |
|||
<excludeFolder url="file://$MODULE_DIR$/.venv" /> |
|||
</content> |
|||
<orderEntry type="jdk" jdkName="Python 3.12" jdkType="Python SDK" /> |
|||
<orderEntry type="sourceFolder" forTests="false" /> |
|||
</component> |
|||
</module> |
@ -0,0 +1,8 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<project version="4"> |
|||
<component name="ProjectModuleManager"> |
|||
<modules> |
|||
<module fileurl="file://$PROJECT_DIR$/.idea/modbusTools.iml" filepath="$PROJECT_DIR$/.idea/modbusTools.iml" /> |
|||
</modules> |
|||
</component> |
|||
</project> |
Binary file not shown.
Binary file not shown.
@ -0,0 +1,24 @@ |
|||
# Logs |
|||
logs |
|||
*.log |
|||
npm-debug.log* |
|||
yarn-debug.log* |
|||
yarn-error.log* |
|||
pnpm-debug.log* |
|||
lerna-debug.log* |
|||
|
|||
node_modules |
|||
dist |
|||
dist-ssr |
|||
*.local |
|||
|
|||
# Editor directories and files |
|||
.vscode/* |
|||
!.vscode/extensions.json |
|||
.idea |
|||
.DS_Store |
|||
*.suo |
|||
*.ntvs* |
|||
*.njsproj |
|||
*.sln |
|||
*.sw? |
@ -0,0 +1,3 @@ |
|||
{ |
|||
"recommendations": ["Vue.volar"] |
|||
} |
@ -0,0 +1,5 @@ |
|||
# Vue 3 + Vite |
|||
|
|||
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more. |
|||
|
|||
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support). |
@ -0,0 +1,14 @@ |
|||
<!doctype html> |
|||
<html lang="en"> |
|||
<head> |
|||
<meta charset="UTF-8" /> |
|||
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
|||
<title>modbus扫描工具 |
|||
</title> |
|||
</head> |
|||
<body> |
|||
<div id="app"></div> |
|||
<script type="module" src="/src/main.js"></script> |
|||
</body> |
|||
</html> |
File diff suppressed because it is too large
@ -0,0 +1,19 @@ |
|||
{ |
|||
"name": "frontend", |
|||
"private": true, |
|||
"version": "0.0.0", |
|||
"type": "module", |
|||
"scripts": { |
|||
"dev": "vite", |
|||
"build": "vite build", |
|||
"preview": "vite preview" |
|||
}, |
|||
"dependencies": { |
|||
"@vueuse/core": "^13.5.0", |
|||
"vue": "^3.5.17" |
|||
}, |
|||
"devDependencies": { |
|||
"@vitejs/plugin-vue": "^6.0.0", |
|||
"vite": "^7.0.0" |
|||
} |
|||
} |
After Width: | Height: | Size: 1.5 KiB |
@ -0,0 +1,172 @@ |
|||
<template> |
|||
<h1>ModbusScan扫描工具</h1> |
|||
<!--参数配置界面--> |
|||
<div id="app" class="panel"> |
|||
<div> |
|||
端口<select name="PORT_ID" id="serial_port" v-model="serial_config.port"> |
|||
<option v-for="PORT in COM_ID_LIST"> |
|||
{{PORT.message}} |
|||
</option> |
|||
</select> |
|||
</div> |
|||
|
|||
<div> |
|||
波特率<select name="baudrate" id="serial_baudrate" v-model="serial_config.baudrate"> |
|||
<option v-for="BAUDRATE in BAUDRATE_LIST"> |
|||
{{BAUDRATE.message}} |
|||
</option> |
|||
</select> |
|||
</div> |
|||
|
|||
<div> |
|||
字节大小<select name="BYTESIZE" id="serial_bytesize" v-model="serial_config.bytesize"> |
|||
<option v-for="BYTESIZE in BYTESIZE_LIST"> |
|||
{{BYTESIZE.message}} |
|||
</option> |
|||
</select> |
|||
</div> |
|||
|
|||
<div> |
|||
停止位<select name="STOPBITS" id="serial_stopbits" v-model="serial_config.stopbits"> |
|||
<option v-for="STOPBITS in STOPBITS_LIST"> |
|||
{{STOPBITS.message}} |
|||
</option> |
|||
</select> |
|||
</div> |
|||
|
|||
<div> |
|||
校验位<select name="PARITY" id="serial_PARITY" v-model="serial_config.parity"> |
|||
<option v-for="PARITY in PARITY_LIST"> |
|||
{{PARITY.message}} |
|||
</option> |
|||
</select> |
|||
</div> |
|||
|
|||
<div>当前地址<input v-model="serial_config.current_addr"></div> |
|||
|
|||
<div>起始地址<input v-model="serial_config.startAddr"></div> |
|||
|
|||
<div>结束地址<input v-model="serial_config.endAddr"></div> |
|||
|
|||
<div>超时时间<input v-model="serial_config.timeout"></div> |
|||
|
|||
<div>重试次数<input v-model="serial_config.retries"></div> |
|||
|
|||
<div>发送间隔<input v-model="serial_config.sendInterval"></div> |
|||
|
|||
<div>寄存器地址<input v-model="serial_config.registerAddress"></div> |
|||
|
|||
<div>功能码<input v-model="serial_config.functionCode"></div> |
|||
|
|||
</div> |
|||
<!-- 扫描日志界面 --> |
|||
<div class="panel"> |
|||
</div> |
|||
<button @click="serial_config.type='sole',soleTest()">单点通讯</button> |
|||
<button @click="serial_config.type='list',soleTest()">批量通讯</button> |
|||
<div><ul> |
|||
<li v-for="(item,index) in response" :key="index">{{ item.sending_code }}<br>{{ item.receving_code }}<br>{{ item.register_value }}</li> |
|||
</ul> |
|||
|
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import {ref,reactive,onMounted, onBeforeUnmount} from "vue"; |
|||
import { useWebSocket } from '@vueuse/core'; |
|||
const serial_config = reactive({ |
|||
type:'sole', |
|||
port: 'COM3', |
|||
baudrate: '9600', |
|||
bytesize: '8', |
|||
stopbits: '1', |
|||
parity: '无校验', |
|||
functionCode: '03', |
|||
registerAddress: "0", |
|||
startAddr: 1, |
|||
endAddr: 100, |
|||
timeout: "0.2", |
|||
retries: "1", |
|||
sendInterval: "0.5", |
|||
current_addr:1 |
|||
}); |
|||
|
|||
const COM_ID_LIST=ref([{message:"COM1"},{message:"COM2"},{message:"COM3"},{message:"COM4"},{message:"COM5"},{message:"COM6"}]); |
|||
const BAUDRATE_LIST=ref([{message:"1200"},{message:"2400"},{message:"4800"},{message:"9600"},{message:"19200"},{message:"38400"},{message:"57600"},{message:"115200"}]); |
|||
const BYTESIZE_LIST=ref([{message:"5"},{message:"6"},{message:"7"},{message:"8"}]); |
|||
const STOPBITS_LIST=ref([{message:"1"},{message:"1.5"},{message:"2"}]); |
|||
const PARITY_LIST=ref([{message:"奇校验"},{message:"偶校验"},{message:"无校验"}]); |
|||
const websocketlog=ref([]); |
|||
const ws=new WebSocket('ws://localhost:8765'); |
|||
const messages = ref(''); |
|||
const response=ref([]); |
|||
|
|||
// 监听WebSocket打开事件 |
|||
ws.addEventListener('open', () => { |
|||
console.log('WebSocket已连接'); |
|||
}); |
|||
|
|||
// 监听接收到消息的事件 |
|||
ws.onmessage=(event)=>{ |
|||
const data=JSON.parse(event.data); |
|||
messages.value=data.status; |
|||
if(messages.value=='200'){ |
|||
response.value.push(data) |
|||
} |
|||
} |
|||
|
|||
// 发送消息至服务器的方法 |
|||
function sendMessage() { |
|||
ws.send(JSON.stringify(serial_config)) |
|||
}; |
|||
function soleTest(){ |
|||
ws.send(JSON.stringify(serial_config)) |
|||
} |
|||
// 定时执行函数 |
|||
// |
|||
// const startTimer=(interval=serial_config.interval*1000)=>{ |
|||
// timer=setInterval(soleTest,interval); |
|||
// serial_config.current_addr=serial_config.current_addr+1; |
|||
// if(serial_config.current_addr==serial_config.endAddr){ |
|||
// clearInterval(timer); |
|||
// timer=null; |
|||
// }}; |
|||
|
|||
const startTimer = () => { |
|||
if (timer.value) return; // 防止重复启动 |
|||
|
|||
isRunning.value = true; |
|||
isCompleted.value = false; |
|||
|
|||
timer.value = setInterval(soleTest(),() => { |
|||
|
|||
serial_config.current_addr=serial_config.startAddr; |
|||
serial_config.current_addr += 1; |
|||
|
|||
// 达到目标值停止 |
|||
if (serial_config.current_addr >= serial_config.endAddr) { |
|||
stopTimer(); |
|||
isCompleted.value = true; |
|||
} |
|||
}, 500); // 0.5秒间隔 |
|||
}; |
|||
const stopTimer=()=>{ |
|||
clearInterval(timer.value); |
|||
timer.value = null; |
|||
isRunning.value = false; |
|||
} |
|||
|
|||
// 监听WebSocket关闭事件 |
|||
ws.addEventListener('close', () => { |
|||
console.log('WebSocket连接已关闭'); |
|||
}); |
|||
|
|||
// 监听错误事件 |
|||
ws.addEventListener('error', (error) => { |
|||
console.error('WebSocket发生错误:', error); |
|||
}); |
|||
</script> |
|||
|
|||
<style> |
|||
|
|||
</style> |
@ -0,0 +1,4 @@ |
|||
import { createApp } from 'vue'; |
|||
import App from './App.vue'; |
|||
|
|||
createApp(App).mount('#app'); |
@ -0,0 +1,7 @@ |
|||
import { defineConfig } from 'vite' |
|||
import vue from '@vitejs/plugin-vue' |
|||
|
|||
// https://vite.dev/config/
|
|||
export default defineConfig({ |
|||
plugins: [vue()], |
|||
}) |
@ -0,0 +1,733 @@ |
|||
<!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> |
@ -0,0 +1,203 @@ |
|||
import serial |
|||
import time |
|||
|
|||
def calculate_crc(data): |
|||
"""计算Modbus RTU CRC16校验码""" |
|||
crc = 0xFFFF |
|||
for byte in data: |
|||
crc ^= byte |
|||
for _ in range(8): |
|||
if crc & 0x0001: |
|||
crc >>= 1 |
|||
crc ^= 0xA001 |
|||
else: |
|||
crc >>= 1 |
|||
return crc.to_bytes(2, byteorder='little') |
|||
|
|||
|
|||
def create_modbus_frame(slave_address, function_code=0x03, start_address=0x0000, num_registers=1): |
|||
"""创建Modbus RTU请求帧""" |
|||
frame = bytes([ |
|||
slave_address, # 设备地址 |
|||
function_code, # 功能码 |
|||
(start_address >> 8) & 0xFF, # 起始地址高字节 |
|||
start_address & 0xFF, # 起始地址低字节 |
|||
(num_registers >> 8) & 0xFF, # 寄存器数量高字节 |
|||
num_registers & 0xFF # 寄存器数量低字节 |
|||
]) |
|||
crc = calculate_crc(frame) |
|||
return frame + crc |
|||
|
|||
|
|||
def bytes_to_hex(data): |
|||
"""将字节数据转换为十六进制字符串""" |
|||
return ' '.join([f"{b:02X}" for b in data]) if data else "" |
|||
|
|||
|
|||
def validate_response(response, slave_address, function_code): |
|||
"""验证Modbus响应有效性""" |
|||
if len(response) < 5: |
|||
return False |
|||
|
|||
# 检查地址和功能码 |
|||
if response[0] != slave_address or response[1] != function_code: |
|||
# 检查是否是错误响应 |
|||
if response[0] == slave_address and response[1] == function_code + 0x80: |
|||
error_code = response[2] |
|||
print(f" Modbus错误响应 (异常码: {error_code:02X})") |
|||
return False |
|||
|
|||
# 检查数据长度 |
|||
data_length = response[2] |
|||
if len(response) != 5 + data_length: |
|||
return False |
|||
|
|||
# 验证CRC校验 |
|||
received_crc = response[-2:] |
|||
calculated_crc = calculate_crc(response[:-2]) |
|||
return received_crc == calculated_crc |
|||
|
|||
|
|||
def get_stopbits(stopbits_str): |
|||
"""将字符串表示的停止位转换为serial库常量""" |
|||
if stopbits_str == '1.5': |
|||
return serial.STOPBITS_ONE_POINT_FIVE |
|||
elif stopbits_str == '2': |
|||
return serial.STOPBITS_TWO |
|||
else: |
|||
return serial.STOPBITS_ONE |
|||
|
|||
|
|||
def get_function_code(func_str): |
|||
"""将字符串表示的功能码转换为整数""" |
|||
try: |
|||
# 尝试解析十六进制 |
|||
if func_str.lower().startswith('0x'): |
|||
return int(func_str, 16) |
|||
# 尝试解析十进制 |
|||
return int(func_str) |
|||
except ValueError: |
|||
# 解析失败,返回默认值 |
|||
return 0x03 |
|||
|
|||
|
|||
def scan(port_temp,baudrate_temp,timeout_temp,retries_temp,send_interval_temp,register_address_temp,func_input_temp,bytesize_input_temp,parity_input_temp,stopbits_input_temp): |
|||
port = port_temp.strip() |
|||
baudrate = baudrate_temp.strip() |
|||
timeout = float(timeout_temp.strip()) |
|||
retries = int(retries_temp.strip()) |
|||
send_interval = float(send_interval_temp.strip()) |
|||
register_address = int(register_address_temp.strip()) |
|||
|
|||
func_input = func_input_temp.strip() |
|||
function_code = get_function_code(func_input) if func_input else 0x03 |
|||
|
|||
# 数据位配置 |
|||
bytesize_input = bytesize_input_temp.strip() |
|||
if bytesize_input == '5': |
|||
bytesize = serial.FIVEBITS |
|||
elif bytesize_input == '6': |
|||
bytesize = serial.SIXBITS |
|||
elif bytesize_input == '7': |
|||
bytesize = serial.SEVENBITS |
|||
else: |
|||
bytesize = serial.EIGHTBITS # 默认 |
|||
|
|||
# 校验位配置 |
|||
parity_input = parity_input_temp.strip().upper() |
|||
if parity_input == 'E': |
|||
parity = serial.PARITY_EVEN |
|||
elif parity_input == 'O': |
|||
parity = serial.PARITY_ODD |
|||
elif parity_input == 'M': |
|||
parity = serial.PARITY_MARK |
|||
elif parity_input == 'S': |
|||
parity = serial.PARITY_SPACE |
|||
else: |
|||
parity = serial.PARITY_NONE # 默认 |
|||
|
|||
# 停止位配置 |
|||
stopbits_input = stopbits_input_temp.strip() |
|||
if stopbits_input == '1.5': |
|||
stopbits = serial.STOPBITS_ONE_POINT_FIVE |
|||
elif stopbits_input == '2': |
|||
stopbits = serial.STOPBITS_TWO |
|||
else: |
|||
stopbits = serial.STOPBITS_ONE # 默认 |
|||
|
|||
# 停止位描述转换 |
|||
stopbits_desc = { |
|||
serial.STOPBITS_ONE: "1位", |
|||
serial.STOPBITS_ONE_POINT_FIVE: "1.5位", |
|||
serial.STOPBITS_TWO: "2位" |
|||
} |
|||
|
|||
|
|||
|
|||
|
|||
|
|||
def scan_modbus_devices(port='COM3', baudrate=9600, addr=1, |
|||
timeout=0.2, retries=1, send_interval=0.05, register_address=0, |
|||
bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, |
|||
function_code=0x03): |
|||
try: |
|||
# 配置串口参数 |
|||
ser = serial.Serial( |
|||
port=port, |
|||
baudrate=baudrate, |
|||
bytesize=bytesize, |
|||
parity=parity, |
|||
stopbits=stopbits, |
|||
timeout=timeout |
|||
) |
|||
# 使用用户设置的寄存器地址和功能码创建请求帧 |
|||
frame = create_modbus_frame(addr, function_code=function_code, start_address=register_address) |
|||
|
|||
# 打印发送的发码 |
|||
# print(f"地址 {addr:3d}: 发送 -> {bytes_to_hex(frame)}") |
|||
sendding_frame=f"地址 {addr:3d}: 发送 -> {bytes_to_hex(frame)}" |
|||
for attempt in range(retries): |
|||
try: |
|||
ser.write(frame) |
|||
time.sleep(send_interval) # 发送间隔时间 |
|||
response = ser.read_all() |
|||
recvied_frame='' |
|||
register_value='' |
|||
if response: |
|||
# 打印接收的收码 |
|||
# print(f"地址 {addr:3d}: 接收 <- {bytes_to_hex(response)}") |
|||
recvied_frame=f"地址 {addr:3d}: 接收 <- {bytes_to_hex(response)}" |
|||
if response and validate_response(response, addr, function_code): |
|||
# 解析寄存器值 |
|||
data_length = response[2] |
|||
if data_length >= 2: # 至少2字节数据 |
|||
# 提取第一个寄存器的值 |
|||
reg_value = (response[3] << 8) | response[4] |
|||
# print(f"地址 {addr:3d} - 设备响应有效 | 寄存器 {register_address} 值: {reg_value}") |
|||
register_value=f"地址 {addr:3d} - 设备响应有效 | 寄存器 {register_address} 值: {reg_value}" |
|||
else: |
|||
# print(f"地址 {addr:3d} - 设备响应有效 | 无效数据长度") |
|||
register_value =f"地址 {addr:3d} - 设备响应有效 | 无效数据长度" |
|||
break |
|||
elif attempt == retries - 1: |
|||
# print(f"地址 {addr:3d} - 无响应") |
|||
recvied_frame =f"地址 {addr:3d} - 无响应" |
|||
except Exception as e: |
|||
print(f"\n地址 {addr} 通信错误: {str(e)}") |
|||
|
|||
ser.close() |
|||
status={ |
|||
'status':200, |
|||
'sending_code':f"发码:{sendding_frame}", |
|||
'receving_code':f"回码:{recvied_frame}", |
|||
'register_value':f"寄存器值: {register_value}" |
|||
} |
|||
except serial.SerialException as e: |
|||
print(f"\n串口错误: {str(e)}") |
|||
status={ |
|||
'status':403, |
|||
'error':f"\n串口错误: {str(e)}" |
|||
} |
|||
return status |
|||
print(status) |
|||
return status |
@ -0,0 +1,119 @@ |
|||
import asyncio |
|||
|
|||
import serial |
|||
import websocket |
|||
import websockets |
|||
import json |
|||
import sys |
|||
import time |
|||
from eventlet.green.http.client import responses |
|||
|
|||
import modscan |
|||
|
|||
|
|||
async def handle_connection(websocket): |
|||
"""处理 WebSocket 连接""" |
|||
client_ip = websocket.remote_address[0] |
|||
print(f"客户端连接成功: {client_ip}") |
|||
async for message in websocket: |
|||
try: |
|||
|
|||
data=json.loads(message) |
|||
port = data.get('port').strip() |
|||
baudrate = data.get('baudrate').strip() |
|||
|
|||
register_address = int(data.get('registerAddress').strip()) |
|||
timeout = float(data.get('timeout').strip()) |
|||
retries = int(data.get('retries').strip()) |
|||
send_interval = float(data.get('sendInterval').strip()) |
|||
func_input = data.get('functionCode').strip() |
|||
function_code = modscan.get_function_code(func_input) if func_input else 0x03 |
|||
|
|||
# 数据位配置 |
|||
bytesize_input = data.get('bytesize').strip() |
|||
if bytesize_input == '5': |
|||
bytesize = serial.FIVEBITS |
|||
elif bytesize_input == '6': |
|||
bytesize = serial.SIXBITS |
|||
elif bytesize_input == '7': |
|||
bytesize = serial.SEVENBITS |
|||
else: |
|||
bytesize = serial.EIGHTBITS # 默认 |
|||
|
|||
# 校验位配置 |
|||
parity_input = data.get('parity').strip().upper() |
|||
if parity_input == 'E': |
|||
parity = serial.PARITY_EVEN |
|||
elif parity_input == 'O': |
|||
parity = serial.PARITY_ODD |
|||
elif parity_input == 'M': |
|||
parity = serial.PARITY_MARK |
|||
elif parity_input == 'S': |
|||
parity = serial.PARITY_SPACE |
|||
else: |
|||
parity = serial.PARITY_NONE # 默认 |
|||
|
|||
# 停止位配置 |
|||
stopbits_input = data.get('stopbits').strip() |
|||
if stopbits_input == '1.5': |
|||
stopbits = serial.STOPBITS_ONE_POINT_FIVE |
|||
elif stopbits_input == '2': |
|||
stopbits = serial.STOPBITS_TWO |
|||
else: |
|||
stopbits = serial.STOPBITS_ONE # 默认 |
|||
if (data.get('type')=='sole'): |
|||
addr = int(data.get('current_addr')) |
|||
status = modscan.scan_modbus_devices(port, baudrate, addr, timeout, retries, send_interval, register_address, bytesize, |
|||
parity, stopbits, function_code) |
|||
print(data) |
|||
await websocket.send(json.dumps(status)) |
|||
print(status) |
|||
elif(data.get('type')=='list'): |
|||
startaddr=int(data.get('startAddr')) |
|||
endaddr=int(data.get('endAddr')) |
|||
for addr in range(startaddr,endaddr+1): |
|||
status = modscan.scan_modbus_devices(port, baudrate, addr, timeout, retries, send_interval, |
|||
register_address, bytesize, |
|||
parity, stopbits, function_code) |
|||
print(data) |
|||
await websocket.send(json.dumps(status)) |
|||
print(status) |
|||
time.sleep(send_interval) |
|||
|
|||
|
|||
|
|||
|
|||
except json.JSONDecodeError: |
|||
await websocket.send(json.dumps({ |
|||
"status": "error", |
|||
"message": "无效的JSON格式" |
|||
})) |
|||
|
|||
async def main(): |
|||
"""主异步函数""" |
|||
print("启动 WebSocket 服务器...") |
|||
|
|||
# 正确创建服务器实例 |
|||
server = await websockets.serve( |
|||
handle_connection, |
|||
"localhost", |
|||
8765 |
|||
) |
|||
print(f"WebSocket 服务器已启动: ws://localhost:8765") |
|||
print(f"监听地址: {server.sockets[0].getsockname()}") |
|||
|
|||
# 永久运行 |
|||
await server.wait_closed() |
|||
|
|||
|
|||
if __name__ == "__main__": |
|||
# 设置 Windows 事件循环策略 |
|||
if sys.platform == 'win32': |
|||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) |
|||
|
|||
# 运行主程序 |
|||
try: |
|||
asyncio.run(main()) |
|||
|
|||
except KeyboardInterrupt: |
|||
print("\n服务器已停止") |
Loading…
Reference in new issue