uniapp安卓端读取485设备
·
<template>
<view class="container">
<!-- 状态卡片 -->
<view class="status-card">
<view class="status-header">
<text class="status-title">485 通信状态</text>
<view :class="['status-badge', isConnected ? 'connected' : 'disconnected']">
<text>{{ isConnected ? '已连接' : '未连接' }}</text>
</view>
</view>
<view class="status-info">
<view class="info-row">
<text class="info-label">平台:</text>
<text class="info-value">{{ platform }}</text>
</view>
<view class="info-row">
<text class="info-label">模式:</text>
<text :class="['info-value', useSimulation ? 'simulation-mode' : 'real-mode']">
{{ useSimulation ? '模拟模式' : '真实模式' }}
</text>
</view>
<view class="info-row" v-if="platform === 'Android'">
<text class="info-label">串口:</text>
<text class="info-value">{{ serialConfig.path }}</text>
</view>
<view class="info-row">
<text class="info-label">波特率:</text>
<text class="info-value">{{ serialConfig.baudRate }}</text>
</view>
</view>
</view>
<!-- 控制按钮 -->
<view class="control-section">
<view class="button-group">
<button class="btn btn-primary" :disabled="isConnected" @click="connect">
连接串口
</button>
<button class="btn btn-danger" :disabled="!isConnected" @click="disconnect">
断开连接
</button>
</view>
<view class="button-group">
<button class="btn btn-success" :disabled="!isConnected || isPolling" @click="startPolling">
启动轮询
</button>
<button class="btn btn-warning" :disabled="!isPolling" @click="stopPolling">
停止轮询
</button>
</view>
<view class="button-group">
<button class="btn btn-info" :disabled="!isConnected" @click="testWriteRegister0">
测试写入(寄存器0={{ testWriteValue }})
</button>
<button class="btn btn-purple" :disabled="!isConnected" @click="testWriteRegister1">
测试写入(寄存器1={{ testWriteValue }})
</button>
</view>
</view>
<!-- 设备信息 -->
<view class="device-section">
<text class="section-title">设备配置</text>
<view class="device-card">
<view class="device-header">
<text class="device-name">从站1 (slave1)</text>
<text class="device-slave">地址: 1</text>
</view>
<view class="device-info">
<text>轮询间隔: {{ pollInterval }}ms</text>
<text>寄存器: 0-9 (共10个)</text>
</view>
</view>
</view>
<!-- 数据展示 -->
<view class="data-section">
<view class="section-header">
<text class="section-title">接收数据</text>
<button class="btn-small" @click="clearData">清空</button>
</view>
<scroll-view scroll-y class="data-list" :style="{ height: dataListHeight + 'px' }">
<view v-if="receivedData.length === 0" class="empty-text">
暂无数据...
</view>
<view v-for="(item, index) in receivedData" :key="index" class="data-item">
<view class="data-header">
<text class="data-time">{{ item.time }}</text>
<text class="data-slave" v-if="item.slaveId">从站{{ item.slaveId }}</text>
</view>
<view class="data-frame">{{ item.frame }}</view>
<view class="data-registers" v-if="item.registers && item.registers.length > 0">
<text class="registers-label">寄存器值:</text>
<view class="registers-grid">
<view v-for="(reg, idx) in item.registers" :key="idx" class="register-item">
<text class="register-addr">[{{ reg.address }}]</text>
<text class="register-value">{{ reg.value }}</text>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
<!-- 日志 -->
<view class="log-section">
<view class="section-header">
<text class="section-title">运行日志</text>
<button class="btn-small" @click="clearLog">清空</button>
</view>
<scroll-view scroll-y class="log-list" :style="{ height: logListHeight + 'px' }">
<view v-if="logs.length === 0" class="empty-text">
暂无日志...
</view>
<view v-for="(log, index) in logs" :key="index" :class="['log-item', log.type]">
<text class="log-time">{{ log.time }}</text>
<text class="log-message">{{ log.message }}</text>
</view>
</scroll-view>
</view>
</view>
</template>
<script>
// #ifdef APP-PLUS
import {
SerialPortHelper
} from "@/uni_modules/android-serialport";
// #endif
export default {
data() {
return {
// 连接状态
isConnected: false,
isPolling: false,
platform: '',
useSimulation: true,
// 串口配置
serialConfig: {
path: '/dev/ttyS2',
baudRate: 9600
},
// 轮询配置
pollInterval: 1000,
pollingTimer: null,
// 串口助手实例
serialHelper: null,
// 数据缓冲区(用于粘包处理)
receiveBuffer: [],
lastFrameTime: null,
FRAME_TIMEOUT: 100,
// 接收数据
receivedData: [],
dataListHeight: 200,
// 日志
logs: [],
logListHeight: 150,
// 测试写入计数
testWriteValue: 0
}
},
onLoad() {
// 获取平台信息
// #ifdef APP-PLUS
this.platform = 'Android';
// #endif
// #ifdef H5
this.platform = 'Web';
// #endif
console.log('%c[485通信] ========== 页面加载完成 ==========', 'color: #1890ff; font-weight: bold;');
console.log('[485通信] 当前平台: ' + this.platform);
this.addLog('info', '页面加载完成,当前平台: ' + this.platform);
},
onUnload() {
this.stopAllTimers();
this.disconnect();
},
methods: {
// 连接串口
async connect() {
try {
console.log('[485通信] 正在连接串口...');
this.addLog('info', '正在连接串口...');
// 创建串口助手实例
this.serialHelper = new SerialPortHelper();
console.log('[485通信] SerialPortHelper 实例创建成功');
// 获取可用串口列表
var devices = this.serialHelper.getAllDevices();
console.log('[485通信] 可用串口列表:', devices);
this.addLog('info', '可用串口: ' + JSON.stringify(devices));
// 设置串口参数
console.log('[485通信] 设置串口参数:', this.serialConfig);
this.serialHelper.setPath(this.serialConfig.path);
this.serialHelper.setBaudrate(this.serialConfig.baudRate);
this.serialHelper.dataBits(8);
this.serialHelper.parity(0);
this.serialHelper.stopBits(1);
// 打开串口
console.log('[485通信] 正在打开串口...');
var state = this.serialHelper.open();
console.log('[485通信] 打开串口状态:', state);
this.addLog('info', '打开串口状态: ' + state);
if (state) {
this.isConnected = true;
this.useSimulation = false;
console.log('%c[485通信] 串口连接成功!', 'color: #52c41a; font-weight: bold;');
this.addLog('success', '串口连接成功');
// 设置数据接收回调
console.log('[485通信] 开始监听串口数据...');
this.serialHelper.onStartAutoReadData((res) => {
const dataArray = Array.from(res);
this.handleReceivedData(dataArray);
});
} else {
console.error('[485通信] 串口打开失败!');
this.addLog('error', '串口打开失败');
}
} catch (error) {
this.addLog('error', '连接失败: ' + error.message);
}
},
// 断开连接
disconnect() {
try {
console.log('[485通信] 正在断开连接...');
this.stopAllTimers();
if (this.serialHelper) {
this.serialHelper.close();
this.serialHelper = null;
console.log('[485通信] 串口已关闭');
}
this.isConnected = false;
this.isPolling = false;
console.log('[485通信] 已断开连接');
this.addLog('info', '已断开连接');
} catch (error) {
console.error('[485通信] 断开失败:', error.message);
this.addLog('error', '断开失败: ' + error.message);
}
},
// 启动轮询
startPolling() {
if (!this.isConnected) {
console.warn('[485通信] 请先连接串口');
this.addLog('warning', '请先连接串口');
return;
}
console.log('%c[485通信] 启动轮询,间隔: ' + this.pollInterval + 'ms', 'color: #52c41a; font-weight: bold;');
this.addLog('success', '启动轮询,间隔: ' + this.pollInterval + 'ms');
// 立即发送一次读取请求
this.sendReadRequest();
// 设置定时轮询
this.pollingTimer = setInterval(() => {
if (this.isConnected) {
this.sendReadRequest();
}
}, this.pollInterval);
this.isPolling = true;
},
// 停止轮询
stopPolling() {
if (this.pollingTimer) {
clearInterval(this.pollingTimer);
this.pollingTimer = null;
}
this.isPolling = false;
console.log('[485通信] 轮询已停止');
this.addLog('info', '轮询已停止');
},
// 发送读取请求(读取10个寄存器)
sendReadRequest() {
if (!this.serialHelper) return;
// Modbus RTU 读取请求帧
// 01 03 00 00 00 0A C5 CD
// 从站1,读取保持寄存器,起始地址0,读取10个寄存器
const frame = "01030000000AC5CD";
this.serialHelper.sendDataString(frame);
this.addLog('info', '发送读取请求: ' + this.formatHexString(frame));
},
// 测试写入寄存器0
testWriteRegister0() {
if (!this.isConnected) {
console.warn('[485通信] 请先连接串口');
this.addLog('warning', '请先连接串口');
return;
}
// 值+1
this.testWriteValue++;
const value = this.testWriteValue;
// 构建Modbus RTU帧
// 01 06 00 00 [value High] [value Low] [CRC]
const frame = this.buildWriteFrame(1, 0, value);
console.log('[写入] 寄存器0 = ' + value + ': ' + this.formatHexString(frame));
this.serialHelper.sendDataString(frame);
this.addLog('info', '写入寄存器0 = ' + value);
},
// 测试写入寄存器1
testWriteRegister1() {
if (!this.isConnected) {
console.warn('[485通信] 请先连接串口');
this.addLog('warning', '请先连接串口');
return;
}
// 值+1
this.testWriteValue++;
const value = this.testWriteValue;
// 构建Modbus RTU帧
// 01 06 00 01 [value High] [value Low] [CRC]
const frame = this.buildWriteFrame(1, 1, value);
console.log('[写入] 寄存器1 = ' + value + ': ' + this.formatHexString(frame));
this.serialHelper.sendDataString(frame);
this.addLog('info', '写入寄存器1 = ' + value);
},
// 构建Modbus RTU写入帧
buildWriteFrame(slaveId, address, value) {
// 字节数组
const bytes = [
slaveId, // 从站地址
0x06, // 功能码(写入单个寄存器)
(address >> 8) & 0xFF, // 寄存器地址高位
address & 0xFF, // 寄存器地址低位
(value >> 8) & 0xFF, // 值高位
value & 0xFF // 值低位
];
// 计算CRC
const crc = this.calculateCRC(bytes);
bytes.push(crc & 0xFF); // CRC低位
bytes.push((crc >> 8) & 0xFF); // CRC高位
// 转换为十六进制字符串
return bytes.map(b => b.toString(16).padStart(2, '0').toUpperCase()).join('');
},
// 停止所有定时器
stopAllTimers() {
if (this.pollingTimer) {
clearInterval(this.pollingTimer);
this.pollingTimer = null;
}
},
// 处理接收到的数据
handleReceivedData(dataArray) {
// 将数据添加到缓冲区
this.receiveBuffer = this.receiveBuffer.concat(dataArray);
// 检查是否应该开始新帧
const now = Date.now();
if (this.lastFrameTime && (now - this.lastFrameTime) > this.FRAME_TIMEOUT) {
// 超时,清空旧缓冲区
this.receiveBuffer = dataArray;
}
this.lastFrameTime = now;
// 尝试解析帧
this.parseFrame();
},
// 解析Modbus RTU帧
parseFrame() {
if (this.receiveBuffer.length < 5) {
return;
}
const slaveId = this.receiveBuffer[0];
const functionCode = this.receiveBuffer[1];
// 功能码 0x03 = 读取保持寄存器响应
if (functionCode === 0x03) {
const dataLength = this.receiveBuffer[2];
const expectedTotal = 3 + dataLength + 2;
if (this.receiveBuffer.length >= expectedTotal) {
// 检查CRC
const receivedCRC = this.receiveBuffer.slice(expectedTotal - 2, expectedTotal);
const calculatedCRC = this.calculateCRC(this.receiveBuffer.slice(0, expectedTotal - 2));
if (receivedCRC[0] === (calculatedCRC & 0xFF) &&
receivedCRC[1] === ((calculatedCRC >> 8) & 0xFF)) {
// CRC校验通过
const completeFrame = this.receiveBuffer.slice(0, expectedTotal);
this.receiveBuffer = this.receiveBuffer.slice(expectedTotal);
// 解析寄存器值
const registers = [];
for (let i = 0; i < dataLength; i += 2) {
const address = Math.floor(i / 2);
const value = (completeFrame[3 + i] << 8) | completeFrame[3 + i + 1];
registers.push({ address, value });
}
// 显示完整帧
const frameHex = this.arrayToHex(completeFrame);
// 添加到数据列表
this.receivedData.unshift({
time: this.formatTime(new Date()),
slaveId: slaveId,
frame: frameHex,
registers: registers
});
// 只保留最近20条
if (this.receivedData.length > 20) {
this.receiveBuffer.pop();
}
// 控制台输出(简洁版)
console.log('[帧] ' + frameHex);
console.log('[值] ' + registers.map(r => `[${r.address}]=${r.value}`).join(', '));
this.addLog('success', '帧解析成功: ' + frameHex);
this.addLog('success', '寄存器值: ' + registers.map(r => `[${r.address}]=${r.value}`).join(', '));
} else {
console.error('[485通信] CRC校验失败,丢弃数据');
this.addLog('error', 'CRC校验失败');
this.receiveBuffer = [];
}
}
}
// 功能码 0x06 = 写入单个寄存器响应
else if (functionCode === 0x06) {
const expectedTotal = 8;
if (this.receiveBuffer.length >= expectedTotal) {
const completeFrame = this.receiveBuffer.slice(0, expectedTotal);
this.receiveBuffer = this.receiveBuffer.slice(expectedTotal);
const address = (completeFrame[2] << 8) | completeFrame[3];
const value = (completeFrame[4] << 8) | completeFrame[5];
const frameHex = this.arrayToHex(completeFrame);
console.log('[帧] ' + frameHex);
console.log('[值] 寄存器' + address + ' = ' + value);
this.addLog('success', '写入响应: ' + frameHex);
this.addLog('success', '寄存器' + address + ' = ' + value);
}
}
},
// 计算CRC16校验
calculateCRC(data) {
let crc = 0xFFFF;
for (let i = 0; i < data.length; i++) {
crc ^= data[i];
for (let j = 0; j < 8; j++) {
if ((crc & 0x0001) !== 0) {
crc = (crc >> 1) ^ 0xA001;
} else {
crc >>= 1;
}
}
}
return crc;
},
// 数组转十六进制字符串
arrayToHex(array) {
return array.map(b => b.toString(16).toUpperCase().padStart(2, '0')).join(' ');
},
// 格式化十六进制字符串(添加空格)
formatHexString(str) {
return str.match(/.{1,2}/g).join(' ').toUpperCase();
},
// 格式化时间
formatTime(date) {
const pad = (n) => n.toString().padStart(2, '0');
return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds())}`;
},
// 添加日志
addLog(type, message) {
const timestamp = new Date();
this.logs.unshift({
time: this.formatTime(timestamp),
type,
message
});
// 只保留最近100条
if (this.logs.length > 100) {
this.logs.pop();
}
},
// 清空数据
clearData() {
this.receivedData = [];
this.receiveBuffer = [];
this.addLog('info', '数据已清空');
},
// 清空日志
clearLog() {
this.logs = [];
}
}
}
</script>
<style>
/* 容器 */
.container {
padding: 20rpx;
background-color: #f5f5f5;
min-height: 100vh;
}
/* 状态卡片 */
.status-card {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
}
.status-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.status-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.status-badge {
padding: 10rpx 20rpx;
border-radius: 20rpx;
font-size: 24rpx;
color: #fff;
}
.status-badge.connected {
background-color: #52c41a;
}
.status-badge.disconnected {
background-color: #ff4d4f;
}
.status-info {
padding: 20rpx;
background-color: #fafafa;
border-radius: 12rpx;
}
.info-row {
display: flex;
margin-bottom: 10rpx;
}
.info-row:last-child {
margin-bottom: 0;
}
.info-label {
color: #666;
font-size: 28rpx;
width: 160rpx;
}
.info-value {
color: #333;
font-size: 28rpx;
}
.simulation-mode {
color: #faad14;
}
.real-mode {
color: #52c41a;
}
/* 控制区域 */
.control-section {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
}
.button-group {
display: flex;
justify-content: space-between;
margin-bottom: 20rpx;
}
.button-group:last-child {
margin-bottom: 0;
}
/* 按钮样式 */
.btn {
flex: 1;
margin: 0 10rpx;
border-radius: 8rpx;
font-size: 28rpx;
}
.btn:first-child {
margin-left: 0;
}
.btn:last-child {
margin-right: 0;
}
.btn-primary {
background-color: #1890ff;
color: #fff;
}
.btn-danger {
background-color: #ff4d4f;
color: #fff;
}
.btn-success {
background-color: #52c41a;
color: #fff;
}
.btn-warning {
background-color: #faad14;
color: #fff;
}
.btn-info {
background-color: #13c2c2;
color: #fff;
}
.btn-purple {
background-color: #722ed1;
color: #fff;
}
.btn:disabled {
background-color: #d9d9d9;
color: #999;
}
.btn-small {
font-size: 24rpx;
padding: 10rpx 20rpx;
background-color: #f0f0f0;
border-radius: 6rpx;
}
/* 设备区域 */
.device-section {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
}
.section-title {
font-size: 30rpx;
font-weight: bold;
color: #333;
display: block;
margin-bottom: 20rpx;
}
.device-card {
background-color: #fafafa;
border-radius: 12rpx;
padding: 20rpx;
}
.device-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10rpx;
}
.device-name {
font-size: 28rpx;
font-weight: bold;
color: #333;
}
.device-slave {
font-size: 24rpx;
color: #666;
background-color: #e6f7ff;
padding: 6rpx 12rpx;
border-radius: 6rpx;
}
.device-info {
display: flex;
justify-content: space-between;
font-size: 24rpx;
color: #999;
}
/* 数据区域 */
.data-section,
.log-section {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.empty-text {
text-align: center;
color: #999;
font-size: 26rpx;
padding: 40rpx;
}
.data-item {
padding: 16rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.data-item:last-child {
border-bottom: none;
}
.data-header {
display: flex;
justify-content: space-between;
margin-bottom: 8rpx;
}
.data-time {
font-size: 22rpx;
color: #999;
}
.data-slave {
font-size: 22rpx;
color: #1890ff;
background-color: #e6f7ff;
padding: 4rpx 12rpx;
border-radius: 4rpx;
}
.data-frame {
font-size: 26rpx;
color: #333;
font-family: monospace;
word-break: break-all;
margin-bottom: 8rpx;
}
.registers-label {
font-size: 24rpx;
color: #666;
margin-bottom: 8rpx;
}
.registers-grid {
display: flex;
flex-wrap: wrap;
gap: 10rpx;
}
.register-item {
display: flex;
align-items: center;
background-color: #f5f5f5;
padding: 6rpx 12rpx;
border-radius: 6rpx;
}
.register-addr {
font-size: 22rpx;
color: #999;
margin-right: 8rpx;
}
.register-value {
font-size: 26rpx;
color: #52c41a;
font-weight: bold;
font-family: monospace;
}
.log-item {
padding: 16rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.log-item:last-child {
border-bottom: none;
}
.log-time {
font-size: 22rpx;
color: #999;
margin-bottom: 6rpx;
display: block;
}
.log-message {
font-size: 26rpx;
color: #333;
word-break: break-all;
}
.log-item.info .log-message {
color: #1890ff;
}
.log-item.success .log-message {
color: #52c41a;
}
.log-item.warning .log-message {
color: #faad14;
}
.log-item.error .log-message {
color: #ff4d4f;
}
</style>
更多推荐

所有评论(0)