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

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐