Browse Source

程序

master
xing 2 months ago
commit
3ab7d1f126
  1. 3
      .idea/.gitignore
  2. 6
      .idea/inspectionProfiles/profiles_settings.xml
  3. 7
      .idea/misc.xml
  4. 10
      .idea/modbusTools.iml
  5. 8
      .idea/modules.xml
  6. BIN
      __pycache__/modscan.cpython-312.pyc
  7. BIN
      __pycache__/server.cpython-312.pyc
  8. 24
      frontend/.gitignore
  9. 3
      frontend/.vscode/extensions.json
  10. 5
      frontend/README.md
  11. 14
      frontend/index.html
  12. 1283
      frontend/package-lock.json
  13. 19
      frontend/package.json
  14. 1
      frontend/public/vite.svg
  15. 172
      frontend/src/App.vue
  16. 4
      frontend/src/main.js
  17. 0
      frontend/src/style.css
  18. 7
      frontend/vite.config.js
  19. 733
      index.html
  20. 203
      modscan.py
  21. 119
      server.py

3
.idea/.gitignore

@ -0,0 +1,3 @@
# 默认忽略的文件
/shelf/
/workspace.xml

6
.idea/inspectionProfiles/profiles_settings.xml

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
.idea/misc.xml

@ -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>

10
.idea/modbusTools.iml

@ -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>

8
.idea/modules.xml

@ -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>

BIN
__pycache__/modscan.cpython-312.pyc

Binary file not shown.

BIN
__pycache__/server.cpython-312.pyc

Binary file not shown.

24
frontend/.gitignore

@ -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?

3
frontend/.vscode/extensions.json

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

5
frontend/README.md

@ -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).

14
frontend/index.html

@ -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>

1283
frontend/package-lock.json

File diff suppressed because it is too large

19
frontend/package.json

@ -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"
}
}

1
frontend/public/vite.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

172
frontend/src/App.vue

@ -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>

4
frontend/src/main.js

@ -0,0 +1,4 @@
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');

0
frontend/src/style.css

7
frontend/vite.config.js

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
})

733
index.html

@ -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>

203
modscan.py

@ -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

119
server.py

@ -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…
Cancel
Save