QT 篇 五、手把手教学用QT编写TCP上位机并显示温湿度

QT应用篇

一、QT上位机串口编程
二、QML用Image组件实现Progress Bar 的效果
三、QML自定义显示SpinBox的加减按键图片及显示值效果
四、window编译LibModbus库并用QT编写一个Modbus主机
五、手把手教学用QT编写TCP上位机并显示温湿度


文章目录

  • QT应用篇
  • 软件篇
    • 1.新建QT工程
    • 2.UI设计
      • 1.添加标签
      • 2.添加按钮 输入框和显示温湿度标签
    • 3.功能设计
        • 结果展示


目标:
1.上位机显示温湿度
2.qt编写tcp服务器端并成功通讯

软件:
TCP客户端
我这里我用的时候通讯猫:下载地址:https://wws.lanzoui.com/iE96Rpk2cxc

软件篇

1.新建QT工程

1.首先,新建工程
在这里插入图片描述
2.然后自定义项目名称和路径(路径和名称不能有中文)
在这里插入图片描述

3.构建系统
一般是默认直接下一步在这里插入图片描述
然后自己定义类名称
2.这里最好勾一下,这样就可以快速的先完成界面设计
如果不✓上的话,就要自己手动写代码来完成UI设计
在这里插入图片描述
这里直接下一步
在这里插入图片描述
编译套件一般来说window大部分选择MinGw64
在这里插入图片描述
下一步
在这里插入图片描述
如你所看到的,这里就算完成QT工程创建了

试着先编译一下快捷键 CTRL+R或者点击左下角绿色三角形
在这里插入图片描述

编译成功
可以看到弹出一个空白界面
在这里插入图片描述

双击.ui文件,打开界面设计
在这里插入图片描述
图片中的红色框框里面代表许多控件
在这里插入图片描述
我就不一一介绍了

找到label控件,拖到界面中
在这里插入图片描述
随便输入点内容
然后点击编译
在这里插入图片描述

到这里创建工程部分就算结束了

2.UI设计

1.添加标签

这四个都是标签,所以不用修改名称
在这里插入图片描述

2.添加按钮 输入框和显示温湿度标签

添加完成后在右下角的控件属性那里可以看到控件的属性
位置,大小,字体等等
顺便修改字体大小和控件名称,方便后续自己用的时候好找一点

在这里插入图片描述
左上角的标签的名称我也修改了,后续用来显示日期和时间
在这里插入图片描述
按钮的属性的check 选中,把按钮的信号槽函数添加到文件里面,这里用的是官方的自动生成功能
在这里插入图片描述
可以看到cpp文件和h文件已经生成槽函数了
后续可以在里面添加功能来实现按钮的点击作用

在这里插入图片描述

在这里插入图片描述

到这里UI部分暂时设计完成

3.功能设计

双击打开TCPtest.pro文件 在第一行 后面加上 network

在这里插入图片描述
然后开是编写TCP服务器部分
由于是TCP通讯,所以要添加TCP部分的控件
打开tcptest.h文件添加内容

#include <QTcpServer>   //服务器头文件
#include <QTcpSocket>   //客户端头文件
#include <QList>    //链表头文件用来存放客户端容器
#include <QDebug>   //打印
#include <QTimer>   //定时器
#include <QDateTime> //日期时间
#include <QTime>    //时间
#include <QDate>    //日期
#include <QMessageBox>  //弹窗#define  LOG qDebug()<<QTime::currentTime() //

public:添加

    //定义服务器指针QTcpServer *tcp_server;//定义客户端指针链表容器QList<QTcpSocket *> clientList;QTcpSocket *socket;

private slots:添加函数

	void timer_500ms();void newConnectionSlot();void readyReadSlot();  //自定义处理readyRead信号的槽函数void startServer();void stopServer();void dateProcess(QByteArray qstr);

private:添加一个定时器

QTimer *t_timer500;

添加完成后如图
在这里插入图片描述

打开tcptest.cpp
ui->setupUi(this);后面添加

    //给服务器指针实例化对象tcp_server = new QTcpServer(this);  //服务器创建完成t_timer500 = new QTimer(this); //实例化定时器t_timer500->start(500);  //开始运行,500ms回调一次connect(t_timer500,SIGNAL(timeout()),this,SLOT(timer_500ms())); //手动连接定时器的信号和槽函数

然后在构建槽函数内需要实现的的功能
1.timer_500ms()这里要是实现左上角的日期标签刷新显示时间

void TCPtest::timer_500ms()//500ms运行一次的函数
{QDateTime datetime;datetime = QDateTime::currentDateTime(); //把日期时间赋值刷新ui->label->setText(datetime.toString("yyyy/MM/dd hh:mm:ss")); //label为左上角显示日期的标签
}

2.startServer()这里要开始创建TCP服务器,获取监听的端口,并建立监听

void TCPtest::startServer()
{//获取UI界面的端口号quint16 port = ui->led_port->text().toUInt();ui->led_port->setEnabled(false);//返回值:成功返回真,失败返回假if(!tcp_server->listen(QHostAddress::Any,port)){LOG<<"Error!:"<<tcp_server->errorString();QMessageBox::critical(this, "失败", "服务器启动失败");}//执行到这表明服务器启动成功,并对客户端连接进行监听,如果有客户端向服务器发来连接请求,那么该服务器就会自动发射一个newConnection信号//我们可以将信号连接到对应的槽函数中处理相关逻辑connect(tcp_server, &QTcpServer::newConnection, this, &TCPtest::newConnectionSlot);
}

3.newConnectionSlot()这里要把客户端套接字存放起来,再手动连接readyRead信号和槽函数

void TCPtest::newConnectionSlot()
{//获取连接客户端套接字socket = tcp_server->nextPendingConnection();//套接字放到客户端容器clientList.push_back(socket);//客户端有数据向服务器发送过来,套接字自动发送一个readyread信号//将信号连接到自定义的槽函数中处理connect(socket, &QTcpSocket::readyRead, this, &TCPtest::readyReadSlot);QHostAddress clientAddress = socket->peerAddress();//获取客户端IPQString clientIP = clientAddress.toString();LOG << "A Client IP:" << clientIP<<"requests a connection";
}

4.readyReadSlot() 删除无效客户端,然后从所有连接的客户端查找是哪一个客户端发的数据

void TCPtest::readyReadSlot()
{//删除客户端链表套接字for(int i=0; i<clientList.count(); i++){if(clientList[i]->state() == 0){clientList.removeAt(i);     //将下标为i的客户端移除LOG<<"removeAt clientList "<<i;}}//遍历所有客户端,查看是哪个客户端发来数据for(int i=0; i<clientList.count(); i++){if(clientList[i]->bytesAvailable() != 0){QByteArray msg = clientList[i]->readAll();dateProcess(msg);}}
}//自定义处理readyRead信号的槽函数

5.dateProcess(QByteArray qstr)处理客户端收到的数据

void TCPtest::dateProcess(QByteArray qstr)//处理数据
{LOG<<"msg"<<qstr;}

这里我们还没有定义数据,就先打印接收到的数据

6.stopServer()关闭服务器,删除无效套接字

void TCPtest::stopServer()
{ui->led_port->setEnabled(true); //使能端口输入if(tcp_server->isListening()){socket->disconnectFromHost();//删除客户端链表中的无效客户端套接字for(int i=0; i<clientList.count(); i++){if(clientList[i]->state() == 0){clientList.removeAt(i);     //将下标为i的客户端移除LOG<<"remove client  :"<<i;}}tcp_server->disconnect();tcp_server->close();}else{LOG<<"close server";tcp_server->disconnect();tcp_server->close();}
}

7.on_btn_open_toggled(bool checked)最后处理按键槽函数,把创建服务器和关闭服务器放到里面

void TCPtest::on_btn_open_toggled(bool checked)
{if(checked == 1){startServer();ui->btn_open->setText("打开");}else{stopServer();ui->btn_open->setText("关闭");}}

最后在TCPtest::~TCPtest()里面也加上关闭服务器的函数,避免意外关闭窗口的时候服务器没关闭

TCPtest::~TCPtest()
{delete ui;stopServer();
}

最后如下图
在这里插入图片描述

然后点击编译
成功后如图
在这里插入图片描述
点击按钮打开服务器
然后打开下载好的TCP客户端

在这里插入图片描述
然后点击启用

结果展示

如图在这里插入图片描述
到这里说明TCP通讯已经成功了

那接下来该给发送的数据定义一个格式了

一般来说最简单的通讯协议主要有帧头,命令类型,数据,数据长度,校验,帧尾等等

在这里可以稍微简化一些

只要帧头和命令类型和数据就行了
例如:EE A0 00 64 帧头定义:0xEE 命令类型:0xA0开始 数据:100

则在客户端里面输入EE A0 00 64
在这里插入图片描述
那现在回过头来编写dateProcess(QByteArray qstr)

void TCPtest::dateProcess(QByteArray qstr)//处理数据
{unsigned char cmd;uint16_t data;QByteArray read_buf;if (qstr.isEmpty()){return ;}read_buf += qstr;LOG<<"接收长度"<<read_buf.length()<<"msg"<<read_buf;while (read_buf.length() >= 4){if (read_buf.at(0) == (char)0xEE){cmd = read_buf.at(1);if (cmd >= 0xA0){QByteArray frame = read_buf.left(4);    //取前4个buffmemcpy(&data, frame.right(2).data(), 2); //取出数据read_buf.remove(0, 8);LOG<<"接收内容"<<frame;LOG<<"data"<<data;}else{read_buf.remove(0, 1);}}else{read_buf.remove(0, 4);//帧头不对,除掉一包数据}}}

编译后用客户端发两条命令

EEA01000
EEA11600

如图
在这里插入图片描述
这里已经把数据分开解析了,接下来在创建一个函数,用来区分命令是A0的时候干啥,A1的时候干啥

在cpp文件中

void TCPtest::analysis(unsigned char cmd,int data)
{switch (cmd){case 0xA0:{LOG<<"温度 "<<data;ui->lbl_temp->setNum(data);}case 0xA1:{LOG<<"湿度 "<<data;ui->lbl_hum->setNum(data);}}}

在这里插入图片描述

并在h文件中添加

void analysis(unsigned char cmd,int data);

在这里插入图片描述
同时在dateProcess中调用这个函数
在这里插入图片描述
编译后运行在通过TCP客户端发送数据包

注意:必须选择16进制发送
在这里插入图片描述

当然,也可以试一下多包数据一起发
在这里插入图片描述
代码如下
tcptest.cpp

#include "tcptest.h"
#include "ui_tcptest.h"TCPtest::TCPtest(QWidget *parent): QMainWindow(parent), ui(new Ui::TCPtest)
{ui->setupUi(this);//给服务器指针实例化对象tcp_server = new QTcpServer(this);  //服务器创建完成t_timer500 = new QTimer(this); //实例化定时器t_timer500->start(500);  //开始运行,500ms回调一次connect(t_timer500,SIGNAL(timeout()),this,SLOT(timer_500ms())); //手动连接定时器的信号和槽函数
}TCPtest::~TCPtest()
{delete ui;stopServer();
}void TCPtest::timer_500ms()//500ms运行一次的函数
{QDateTime datetime;datetime = QDateTime::currentDateTime(); //把日期时间赋值刷新ui->label->setText(datetime.toString("yyyy/MM/dd hh:mm:ss")); //label为左上角显示日期的标签
}void TCPtest::startServer()
{//获取UI界面的端口号quint16 port = ui->led_port->text().toUInt();ui->led_port->setEnabled(false);//返回值:成功返回真,失败返回假if(!tcp_server->listen(QHostAddress::Any,port)){LOG<<"Error!:"<<tcp_server->errorString();QMessageBox::critical(this, "失败", "服务器启动失败");}//执行到这表明服务器启动成功,并对客户端连接进行监听,如果有客户端向服务器发来连接请求,那么该服务器就会自动发射一个newConnection信号//我们可以将信号连接到对应的槽函数中处理相关逻辑connect(tcp_server, &QTcpServer::newConnection, this, &TCPtest::newConnectionSlot);
}void TCPtest::newConnectionSlot()
{//获取连接客户端套接字socket = tcp_server->nextPendingConnection();//套接字放到客户端容器clientList.push_back(socket);//客户端有数据向服务器发送过来,套接字自动发送一个readyread信号//将信号连接到自定义的槽函数中处理connect(socket, &QTcpSocket::readyRead, this, &TCPtest::readyReadSlot);QHostAddress clientAddress = socket->peerAddress();//获取客户端IPQString clientIP = clientAddress.toString();LOG << "A Client IP:" << clientIP<<"requests a connection";
}void TCPtest::readyReadSlot()
{//删除客户端链表套接字for(int i=0; i<clientList.count(); i++){if(clientList[i]->state() == 0){clientList.removeAt(i);     //将下标为i的客户端移除LOG<<"removeAt clientList "<<i;}}//遍历所有客户端,查看是哪个客户端发来数据for(int i=0; i<clientList.count(); i++){if(clientList[i]->bytesAvailable() != 0){QByteArray msg = clientList[i]->readAll();dateProcess(msg);}}
}//自定义处理readyRead信号的槽函数void TCPtest::dateProcess(QByteArray qstr)//处理数据
{unsigned char cmd;uint16_t data;QByteArray read_buf;if (qstr.isEmpty()){return ;}read_buf += qstr;LOG<<"接收长度"<<read_buf.length()<<"msg"<<read_buf;while (read_buf.length() >= 4){if (read_buf.at(0) == (char)0xEE){cmd = read_buf.at(1);if (cmd >= 0xA0){QByteArray frame = read_buf.left(4);    //取前4个buffmemcpy(&data, frame.right(2).data(), 2); //取出数据read_buf.remove(0, 8);analysis(cmd,data);LOG<<"接收内容"<<frame;LOG<<"data"<<data;}else{read_buf.remove(0, 1);}}else{read_buf.remove(0, 4);//帧头不对,除掉一包数据}}
}//串口解析函数
void TCPtest::analysis(unsigned char cmd,int data)
{switch (cmd){case 0xA0:{LOG<<"温度 "<<data;ui->lbl_temp->setNum(data);break;}case 0xA1:{LOG<<"湿度 "<<data;ui->lbl_hum->setNum(data);break;}}}void TCPtest::stopServer()
{ui->led_port->setEnabled(true);if(tcp_server->isListening()){socket->disconnectFromHost();//删除客户端链表中的无效客户端套接字for(int i=0; i<clientList.count(); i++){if(clientList[i]->state() == 0){clientList.removeAt(i);     //将下标为i的客户端移除LOG<<"remove client  :"<<i;}}socket->close();tcp_server->disconnect();tcp_server->close();}else{LOG<<"close server";socket->close();tcp_server->disconnect();tcp_server->close();}
}void TCPtest::on_btn_open_toggled(bool checked)
{if(checked == 1){ui->btn_open->setText("关闭");startServer();}else{stopServer();ui->btn_open->setText("打开");}}

tcptest.h

#ifndef TCPTEST_H
#define TCPTEST_H#include <QMainWindow>
#include <QTcpServer>   //服务器头文件
#include <QTcpSocket>   //客户端头文件
#include <QList>    //链表头文件用来存放客户端容器
#include <QDebug>   //打印
#include <QTimer>   //定时器
#include <QDateTime> //日期时间
#include <QTime>    //时间
#include <QDate>    //日期
#include <QMessageBox>  //弹窗#define  LOG qDebug()<<QTime::currentTime() //用于打印,带时间参数QT_BEGIN_NAMESPACE
namespace Ui { class TCPtest; }
QT_END_NAMESPACEclass TCPtest : public QMainWindow
{Q_OBJECTpublic:TCPtest(QWidget *parent = nullptr);~TCPtest();//定义服务器指针QTcpServer *tcp_server;//定义客户端指针链表容器QList<QTcpSocket *> clientList;QTcpSocket *socket;private slots:void on_btn_open_toggled(bool checked);void timer_500ms();void newConnectionSlot();void readyReadSlot();  //自定义处理readyRead信号的槽函数void startServer();void stopServer();void dateProcess(QByteArray qstr);void analysis(unsigned char cmd,int data);private:Ui::TCPtest *ui;QTimer *t_timer500;};
#endif // TCPTEST_H

当然后续如何想要真正的温湿度数据的话需要用过MCU采集传感器信息,在通过一个wifi模块连接上位机把数据发到上位机上即可

如果觉得对你有用,请点赞评论谢谢

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/22260.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

PW1558A规格探秘:为何它是电源系统不可或缺的6A双向保护芯片?

描述 PW1558A 是一款先进的 28V 6A 额定双向负载开关&#xff0c; 提供过载、 短路、 输入电压浪涌、 过大冲击电流和过热保护&#xff0c; 为系统供电。 内置的 24mΩ超低 RDS(ON)电源开关有助于减少正常操作期间的功率损耗。 该设备具有两个输入/输出端口 VBUS1 和 VBUS2&…

实验11 OSPF协议配置

实验11 OSPF协议配置 一、OSPF单区域配置&#xff08;一&#xff09;原理描述&#xff08;二&#xff09;实验目的&#xff08;三&#xff09;实验内容&#xff08;四&#xff09;实验配置&#xff08;五&#xff09;实验步骤 二、OSPF多区域配置&#xff08;一&#xff09;原理…

5252DE 5G 外场通信测试仪

5252DE 5G 外场通信测试仪 集先进算法和高性能硬件于一体的便携式测试仪表 产品综述 5252DE 5G 外场通信测试仪是集合高性能频谱处理模块、多制式解析算法软件于一体的手持式测试仪表&#xff0c;具有很好的便携性、兼容性与可拓展性。 5252DE 具有工作频段宽、性能指标高…

SOLIDWORKS修改零件时出现错误怎么办?

我们在使用SOLIDWOKRS进行零件建模过程中往往避免不了修改&#xff0c;但在修改后又常常会出现零件报错的情况&#xff0c;设计树中会出现一堆的错误和警告&#xff0c;我们如何快速处理这些问题呢&#xff1f; 我们都知道SOLIDWOKRS零件通常包含两大类的对象&#xff0c;分别…

骑行无界,勇者无限!2024COSP上海国际户外展带您畅享生活的速度与激情!

随着夏日来临&#xff0c;越来越多的人选择借由“绿色骑行”去触碰一座城市的脉搏&#xff0c;“城市骑行”正在成为时尚潮流活动和生活休闲方式。对于一部分城市打工人来说&#xff0c;自行车是一种通勤的工具&#xff0c;骑行成为健身的新选择。 在小红书里输入“骑行”&…

Python语言在地球科学交叉领域中的技术应用

Python是功能强大、免费、开源&#xff0c;实现面向对象的编程语言&#xff0c;Python能够运行在Linux、Windows、Macintosh、AIX操作系统上及不同平台&#xff08;x86和arm&#xff09;&#xff0c;Python简洁的语法和对动态输入的支持&#xff0c;再加上解释性语言的本质&…

以30大龄转行AI真的难吗?绝对不是的!

前言 在这个快速变化的时代&#xff0c;"30岁门槛"似乎成了许多人心中的一道坎&#xff0c;尤其是在考虑职业转型时。当提到转向人工智能&#xff08;AI&#xff09;这样技术驱动的领域&#xff0c;一些人可能会担忧年龄成为阻碍。然而&#xff0c;事实证明&#xf…

深度学习Week15——利用TensorFlow实现猫狗识别2

文章目录 深度学习Week15——利用TensorFlow实现猫狗识别2—数据增强 一、前言 二、我的环境 三、前期工作 1、配置环境 2、导入数据 四、数据预处理 1、加载数据 2、可视化数据 3、检查数据 4、配置数据集 五、构建VGG-16模型 1、设置动态学习率 2、早停与保存最佳模型参数 五…

华为OD机试 - 图像物体的边界 - 深度优先搜索(Java 2024 D卷 200分)

华为OD机试 2024C卷题库疯狂收录中&#xff0c;刷题点这里 专栏导读 本专栏收录于《华为OD机试&#xff08;JAVA&#xff09;真题&#xff08;D卷C卷A卷B卷&#xff09;》。 刷的越多&#xff0c;抽中的概率越大&#xff0c;每一题都有详细的答题思路、详细的代码注释、样例测…

栈的最小值

题目链接 栈的最小值 题目描述 注意点 执行push、pop和min操作的时间复杂度必须为O(1) 解答思路 使用两个栈&#xff0c;一个栈deque存储栈中对应的元素值&#xff0c;另一个栈minDeque存储当前栈中所有元素的最小值&#xff0c;当执行push(int x)操作&#xff0c;deque直…

2041:【例5.9】新矩阵

#include <iostream> using namespace std; int main(){const int N 21;//几行几列 int g[N][N] {};int n 0;cin >> n;for (int i 1; i < n; i){for (int j 1; j < n; j){// 输入到几行几列 cin >> g[i][j];if (i j || i j n 1){//如果是这种…

【计算机毕设】基于SpringBoot的民宿在线预定平台设计与实现 - 源码免费(私信领取)

免费领取源码 &#xff5c; 项目完整可运行 &#xff5c; v&#xff1a;chengn7890 诚招源码校园代理&#xff01; 1. 研究目的 本研究旨在设计并实现一个基于SpringBoot的民宿在线预定平台。通过信息化手段提高民宿预定效率&#xff0c;方便用户查询房源、预定房间、在线支付和…

嵌入式Linux系统编程 — 1.2 文件管理与错误处理

目录 1 Linux 系统如何管理文件 1.1 什么是静态文件 1.2 扇区&#xff08;Sector&#xff09;和块&#xff08;Block&#xff09;概念&#xff1f; 1.3 inode 1.4 进程控制块&#xff08;PCB&#xff09; 2 返回错误处理与 errno 2.1 errno变量介绍 2.3 perror函数介绍…

SwiftUI 利用 Swizz 黑魔法为系统创建的默认对象插入新协议方法(三)

功能需求 在 SwiftUI 的开发中,我们往往需要借助底层 UIKit 的“上帝之手”来进一步实现额外的定制功能。比如,在可拖放(Dragable)SwiftUI 的实现中,会缺失拖放取消的回调方法让我们这些秃头码农们“欲哭无泪” 如上图所示,我们在拖放取消时将界面中的一切改变都恢复如初…

年中企业业绩管理新篇章:用友BIP收入云助力高效管理!

随着市场竞争的加剧&#xff0c;年中时刻对于企业而言&#xff0c;不仅是评估上半年业绩的节点&#xff0c;更是调整策略、确保全年目标达成的关键时期。高效的业绩管理不仅需要明确的目标设定和计划制定&#xff0c;更需要借助先进的信息技术工具来提升管理效率和决策质量。在…

Java项目:98 springboot在线教育系统

作者主页&#xff1a;舒克日记 简介&#xff1a;Java领域优质创作者、Java项目、学习资料、技术互助 文中获取源码 项目介绍 本系统共有管理员、用户等角色 本在线教育系统管理员功能有个人中心&#xff0c;用户管理&#xff0c;讲师管理&#xff0c;普通管理员管理&#xff…

Qt开发技术:Q3D图表开发笔记(四):Q3DSurface三维曲面图颜色样式详解、Demo以及代码详解

若该文为原创文章&#xff0c;转载请注明原文出处 本文章博客地址&#xff1a;https://hpzwl.blog.csdn.net/article/details/139424086 各位读者&#xff0c;知识无穷而人力有穷&#xff0c;要么改需求&#xff0c;要么找专业人士&#xff0c;要么自己研究 红胖子网络科技博…

Linux云计算架构师涨薪班课程内容包含哪些?

第一阶段&#xff1a;Linux云计算运维初级工程师 目标 云计算工程师&#xff0c;Linux运维工程师都必须掌握Linux的基本功&#xff0c;这是一切的根本&#xff0c;必须全部掌握&#xff0c;非常重要&#xff0c;有了这些基础&#xff0c;学习上层业务和云计算等都非常快&#x…

LangChain框架介绍

LangChain 的核心组件 模型 I/O 封装 LLMs&#xff1a;大语言模型Chat Models&#xff1a;一般基于 LLMs&#xff0c;但按对话结构重新封装PromptTemple&#xff1a;提示词模板OutputParser&#xff1a;解析输出 数据连接封装 Document Loaders&#xff1a;各种格式文件的加载…

指纹考勤系统

目录 1.课题研究目的和内容 1.1 课题研究目的 1.2 课题研究内容 2.系统总体方案设计及功能模块介绍 2.1总体方案设计 2.2 ATK-301模块介绍 2.3 TFTLCD显示功能模块介绍 2.4 蜂鸣器报警功能模块介绍 2.5 时钟模块介绍 3.系统硬件设计与实现 3.1 系统硬件电…