幽冥大陆(三十四)VUE +node智慧农业电子秤读取——东方仙盟炼气期

Vue 是 前端框架 ,无法直接操作串口硬件(浏览器安全限制),需通过 「前端(Vue)+ 后端(Node.js)」 架构实现:前端负责 UI 展示,后端通过 Node.js 的串口库(如 serialport)操作电子秤,前后端通过 WebSocket 实时通信。

以下是完整实现方案,包含 Vue 前端界面和 Node.js 后端服务,功能与原 C# 版本一致:

一、整体架构

plaintext

复制代码
电子秤硬件 → Node.js 后端(串口通信 + WebSocket 服务)→ Vue 前端(UI 展示 + 交互)

二、后端实现(Node.js + serialport + ws)

负责串口通信、数据解析、稳定判定,通过 WebSocket 向前端推送数据。

1. 项目初始化

bash

运行

复制代码
mkdir scale-backend && cd scale-backend
npm init -y
npm install serialport ws  # serialport:串口通信;ws:WebSocket 服务
2. 后端代码(server.js

javascript

运行

复制代码
const SerialPort = require('serialport');
const Readline = require('@serialport/parser-readline');
const WebSocket = require('ws');

// 配置参数
const CONFIG = {
  portName: 'COM3',       // 串口号(Windows:COMx;Linux:/dev/ttyUSB0;macOS:/dev/cu.usbserial-*)
  baudRate: 9600,         // 波特率
  stableCount: 6,         // 连续稳定次数
  epsilon: 0.001,         // 浮点数精度容忍度
  wsPort: 8081            // WebSocket 服务端口
};

// 全局状态
const state = {
  serialPort: null,
  parser: null,
  recentWeights: [],      // 最近6次重量
  stableWeight: -1,       // 稳定重量
  isRunning: true,
  clients: new Set()      // 连接的前端客户端
};

// 初始化串口
function initSerial() {
  try {
    state.serialPort = new SerialPort(CONFIG.portName, {
      baudRate: CONFIG.baudRate,
      parity: 'none',
      dataBits: 8,
      stopBits: 1,
      timeout: 500
    });

    // 解析串口数据(按行读取)
    state.parser = state.serialPort.pipe(new Readline({ delimiter: '\n' }));

    // 串口打开成功
    state.serialPort.on('open', () => {
      console.log(`串口 ${CONFIG.portName} 已打开,波特率:${CONFIG.baudRate}`);
      broadcast({ type: 'status', data: `串口 ${CONFIG.portName} 已打开,等待数据...` });
    });

    // 接收串口数据
    state.parser.on('data', (data) => {
      const weight = parseWeight(data.trim());
      if (weight !== null) {
        // 推送当前重量到前端
        broadcast({ type: 'currentWeight', data: weight });
        // 检查是否稳定
        checkStableWeight(weight);
      }
    });

    // 串口错误
    state.serialPort.on('error', (err) => {
      console.error('串口错误:', err.message);
      broadcast({ type: 'error', data: `串口错误:${err.message}` });
    });

    // 串口关闭
    state.serialPort.on('close', () => {
      console.log('串口已关闭');
      broadcast({ type: 'status', data: '串口已关闭' });
    });

  } catch (err) {
    console.error('串口初始化失败:', err.message);
    broadcast({ type: 'error', data: `串口打开失败:${err.message}` });
  }
}

// 解析重量数据(提取数字部分)
function parseWeight(data) {
  const match = data.match(/[-+]?\d+\.?\d*/);
  if (match) {
    const weight = parseFloat(match[0]);
    return isNaN(weight) ? null : weight;
  }
  return null;
}

// 检查是否连续6次稳定
function checkStableWeight(currentWeight) {
  // 保留最近6次数据
  state.recentWeights.push(currentWeight);
  if (state.recentWeights.length > CONFIG.stableCount) {
    state.recentWeights.shift();
  }

  // 判定稳定
  if (state.recentWeights.length === CONFIG.stableCount && allEqual()) {
    state.stableWeight = currentWeight;
    console.log(`数据稳定:${currentWeight} kg`);
    // 推送稳定结果到前端
    broadcast({ 
      type: 'stableWeight', 
      data: currentWeight 
    });
    broadcast({ 
      type: 'status', 
      data: `数据稳定:${currentWeight} kg,自动确认中...` 
    });
    // 500ms后关闭串口(可选)
    setTimeout(() => {
      if (state.serialPort && state.serialPort.isOpen) {
        state.serialPort.close();
      }
    }, 500);
  }
}

// 检查数组中所有元素是否相等(考虑精度)
function allEqual() {
  const first = state.recentWeights[0];
  return state.recentWeights.every(w => Math.abs(w - first) < CONFIG.epsilon);
}

// 初始化 WebSocket 服务
function initWebSocket() {
  const wss = new WebSocket.Server({ port: CONFIG.wsPort });
  console.log(`WebSocket 服务启动,端口:${CONFIG.wsPort}`);

  // 客户端连接
  wss.on('connection', (ws) => {
    console.log('新客户端连接');
    state.clients.add(ws);

    // 推送初始状态
    ws.send(JSON.stringify({
      type: 'status',
      data: state.serialPort?.isOpen ? `串口 ${CONFIG.portName} 已打开` : '等待串口初始化...'
    }));

    // 客户端断开连接
    ws.on('close', () => {
      console.log('客户端断开连接');
      state.clients.delete(ws);
    });

    // 接收前端指令(如取消采集)
    ws.on('message', (message) => {
      const msg = JSON.parse(message);
      if (msg.type === 'cancel') {
        console.log('收到取消指令');
        state.isRunning = false;
        if (state.serialPort && state.serialPort.isOpen) {
          state.serialPort.close();
        }
        broadcast({ type: 'status', data: '采集已取消' });
      }
    });
  });
}

// 广播消息到所有前端客户端
function broadcast(data) {
  const msg = JSON.stringify(data);
  state.clients.forEach(client => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(msg);
    }
  });
}

// 启动服务
initWebSocket();
initSerial();

// 进程退出时清理资源
process.on('SIGINT', () => {
  console.log('进程退出,清理资源');
  if (state.serialPort && state.serialPort.isOpen) {
    state.serialPort.close();
  }
  process.exit(0);
});

三、前端实现(Vue 3 + Vite)

负责 UI 展示(重量、状态)、用户交互(取消按钮),通过 WebSocket 与后端通信。

1. 项目初始化

bash

运行

复制代码
npm create vite@latest scale-frontend -- --template vue
cd scale-frontend
npm install
2. 前端代码(src/App.vue

vue

复制代码
<template>
  <div class="scale-dialog">
    <h2>{{ title }}</h2>
    
    <!-- 重量显示区域 -->
    <div class="weight-container">
      <span class="weight-label">当前重量:</span>
      <span class="weight-value">{{ currentWeight }} kg</span>
    </div>
    
    <!-- 状态提示区域 -->
    <div class="status-container">
      <span :class="statusClass">{{ status }}</span>
    </div>
    
    <!-- 取消按钮 -->
    <button class="cancel-btn" @click="handleCancel" :disabled="isDisabled">
      取消
    </button>
    
    <!-- 稳定结果弹窗 -->
    <div class="modal" v-if="showModal">
      <div class="modal-content">
        <h3>采集完成</h3>
        <p>最终稳定重量:{{ stableWeight }} kg</p>
        <button @click="closeModal">确定</button>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';

// 配置
const WS_URL = 'ws://localhost:8081'; // 后端 WebSocket 地址
const title = ref('生鲜电子秤采集');

// 状态
const currentWeight = ref('--');
const status = ref('等待连接后端...');
const statusClass = ref('status-info');
const showModal = ref(false);
const stableWeight = ref(0);
const isDisabled = ref(false);
let ws = null;

// 初始化 WebSocket 连接
function initWebSocket() {
  ws = new WebSocket(WS_URL);

  // 连接成功
  ws.onopen = () => {
    status.value = '正在连接电子秤...';
    statusClass.value = 'status-info';
  };

  // 接收后端消息
  ws.onmessage = (event) => {
    const res = JSON.parse(event.data);
    switch (res.type) {
      case 'currentWeight':
        currentWeight.value = res.data.toFixed(3); // 保留3位小数
        break;
      case 'status':
        status.value = res.data;
        statusClass.value = res.data.includes('错误') ? 'status-error' : 'status-info';
        break;
      case 'stableWeight':
        stableWeight.value = res.data.toFixed(3);
        showModal.value = true;
        isDisabled.value = true; // 禁用取消按钮
        break;
      case 'error':
        status.value = res.data;
        statusClass.value = 'status-error';
        break;
    }
  };

  // 连接错误
  ws.onerror = (err) => {
    status.value = `WebSocket 连接失败:${err.message}`;
    statusClass.value = 'status-error';
  };

  // 连接关闭
  ws.onclose = () => {
    status.value = '连接已关闭';
    statusClass.value = 'status-error';
    // 自动重连(可选)
    setTimeout(initWebSocket, 3000);
  };
}

// 取消采集
function handleCancel() {
  if (ws && ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({ type: 'cancel' }));
    status.value = '正在取消采集...';
    isDisabled.value = true;
  }
}

// 关闭弹窗
function closeModal() {
  showModal.value = false;
  // 可选:关闭弹窗后刷新页面或重新连接
  window.location.reload();
}

// 生命周期钩子
onMounted(() => {
  initWebSocket();
});

onUnmounted(() => {
  if (ws) {
    ws.close();
  }
});
</script>

<style scoped>
.scale-dialog {
  width: 300px;
  height: 200px;
  margin: 100px auto;
  padding: 20px;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  text-align: center;
}

h2 {
  margin: 0 0 20px 0;
  font-size: 18px;
  color: #333;
}

.weight-container {
  margin: 20px 0;
  font-size: 16px;
  color: #333;
}

.weight-value {
  font-weight: bold;
  color: #2c3e50;
  margin-left: 8px;
}

.status-container {
  margin: 15px 0;
  font-size: 14px;
}

.status-info {
  color: #3498db;
}

.status-error {
  color: #e74c3c;
}

.cancel-btn {
  margin-top: 10px;
  padding: 8px 24px;
  background-color: #e74c3c;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
}

.cancel-btn:disabled {
  background-color: #bdc3c7;
  cursor: not-allowed;
}

/* 弹窗样式 */
.modal {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
}

.modal-content {
  background-color: white;
  padding: 24px;
  border-radius: 8px;
  text-align: center;
  width: 250px;
}

.modal-content h3 {
  margin: 0 0 16px 0;
  color: #333;
}

.modal-content p {
  margin: 0 0 20px 0;
  font-size: 16px;
  color: #2c3e50;
}

.modal-content button {
  padding: 8px 24px;
  background-color: #3498db;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
</style>

四、运行步骤

1. 启动后端服务

bash

运行

复制代码
cd scale-backend
node server.js
  • 注意:修改 server.js 中的 portName 为实际电子秤串口号(如 COM3/dev/ttyUSB0)。
2. 启动前端项目

bash

运行

复制代码
cd scale-frontend
npm run dev
  • 前端默认运行在 http://localhost:5173,打开浏览器访问即可。

五、核心功能说明

  1. 串口通信 :后端通过 serialport 库操作电子秤,支持跨平台(Windows/macOS/Linux)。
  2. 实时通信:前后端通过 WebSocket 实现双向通信,前端实时显示重量和状态。
  3. 稳定判定:后端保留最近 6 次重量,误差 < 0.001 时判定为稳定,推送结果到前端。
  4. 用户交互:前端提供「取消」按钮,点击后通过 WebSocket 通知后端关闭串口。
  5. 错误处理:串口打开失败、数据读取错误等异常会实时推送到前端显示。
  6. 跨平台支持:后端和前端均支持 Windows/macOS/Linux,只需修改串口号格式。

六、注意事项

  1. 串口权限
    • Linux/macOS 系统需给串口添加读写权限:sudo chmod 666 /dev/ttyUSB0(替换为实际串口号)。
    • Windows 系统需确保电子秤已正确安装驱动,串口号无冲突。
  2. 数据协议适配 :若电子秤输出格式特殊(如带前缀、校验位),需修改后端 parseWeight 函数的正则表达式。
  3. WebSocket 跨域 :若前端和后端部署在不同域名 / 端口,需在后端添加跨域支持(如使用 cors 库)。
  4. 依赖安装
    • 后端若安装 serialport 失败(Windows),需先安装 Windows Build Tools:npm install --global --production windows-build-tools
    • macOS 需先安装 Xcode 命令行工具:xcode-select --install

七、扩展功能(可选)

  1. 前端添加串口号配置界面,支持用户手动输入串口号(无需修改后端代码)。
  2. 后端添加日志记录功能,保存采集的重量数据到文件(如 CSV)。
  3. 前端添加历史重量曲线展示(使用 echartschart.js)。
  4. 支持多个电子秤同时采集(后端扩展串口管理逻辑)。

阿雪技术观

让我们积极投身于技术共享的浪潮中,不仅仅是作为受益者,更要成为贡献者。无论是分享自己的代码、撰写技术博客,还是参与开源项目的维护和改进,每一个小小的举动都可能成为推动技术进步的巨大力量

Embrace open source and sharing, witness the miracle of technological progress, and enjoy the happy times of humanity! Let's actively join the wave of technology sharing. Not only as beneficiaries, but also as contributors. Whether sharing our own code, writing technical blogs, or participating in the maintenance and improvement of open source projects, every small action may become a huge force driving technological progress.

相关推荐
T***u33330 分钟前
JavaScript在Node.js中的流处理大
开发语言·javascript·node.js
CoderYanger1 小时前
优选算法-字符串:63.二进制求和
java·开发语言·算法·leetcode·职场和发展·1024程序员节
3***31211 小时前
java进阶1——JVM
java·开发语言·jvm
charlie1145141911 小时前
深入理解C/C++的编译链接技术6——A2:动态库设计基础之ABI设计接口
c语言·开发语言·c++·学习·动态库·函数
Cx330❀2 小时前
C++ STL set 完全指南:从基础用法到实战技巧
开发语言·数据结构·c++·算法·leetcode·面试
white-persist2 小时前
【攻防世界】reverse | Reversing-x64Elf-100 详细题解 WP
c语言·开发语言·网络·python·学习·安全·php
FeiHuo565152 小时前
微信个人号开发中如何高效实现API二次开发
java·开发语言·python·微信
zmzb01032 小时前
C++课后习题训练记录Day33
开发语言·c++
csbysj20202 小时前
Bootstrap 折叠
开发语言