提交 a233af51 编写于 作者: mahuifa's avatar mahuifa

Merge branch 'Dev'

......@@ -36,10 +36,21 @@ void PortTest::on_comStatus(QString name, bool flag)
{
if(flag) // 串口插入时自动添加串口名
{
ui->comboBox->addItem(name);
ui->comboBox_2->addItem(name);
ui->comboBox_3->addItem(name);
ui->comboBox_4->addItem(name);
if(ui->comboBox->findText(name) < 0) // 判断新增的串口名称在下拉框中是否存在,如果不存在则添加
{
ui->comboBox->addItem(name);
}
if(ui->comboBox_2->findText(name) < 0)
{
ui->comboBox_2->addItem(name);
}
if(ui->comboBox_3->findText(name) < 0)
{
ui->comboBox_3->addItem(name);
}
if(ui->comboBox_4->findText(name) < 0)
{
ui->comboBox_4->addItem(name);
}
else // 串口拔出时自动移除串口名
{
......
......@@ -6,13 +6,14 @@
## 1、说明
| 类名 | 功能 | 支持系统 |
| ---------------- | ---------------------------------- | -------- |
| QMWidget | 基于QWidget实现的自定义窗口模块 | windows |
| DeviceManagement | 串口、鼠标、键盘热插拔监测功能模块 | windows |
| QLog | Qt日志系统 | |
| QMPlayer | Qt实现的视频播放器界面Demo | windows |
| TestCrashHandler | windows下程序崩溃定位Demo | windows |
| 类名 | 功能 | 支持系统 |
| ---------------- | ---------------------------------- | -------------- |
| QMWidget | 基于QWidget实现的自定义窗口模块 | windows |
| DeviceManagement | 串口、鼠标、键盘热插拔监测功能模块 | windows |
| QLog | Qt日志系统 | |
| QMPlayer | Qt实现的视频播放器界面Demo | windows |
| TestCrashHandler | windows下程序崩溃定位Demo | windows |
| NtpClient | NTP时间同步客户端 | Windows、Linux |
......@@ -95,4 +96,22 @@
>
> 7. 支持release模块下生成dump文件。
![Dump](FunctionalModule.assets/Dump.gif)
\ No newline at end of file
![Dump](FunctionalModule.assets/Dump.gif)
### 1.6 NtpClient
> **NTP时间同步客户端程序Demo**
>
> 1. 使用UDP进行通信;
> 2. 毫秒级时间精度;
> 3. 使用多个阿里云NTP时间同步服务器、腾讯云NTP时间同步服务器;
> 4. 支持windows、linux下修改系统时间。
>
> **注意:**由于设置系统时间的功能比较重要,所以不管是Windows还是Linux都需要最高权限才可以。
>
> 1. Windows下需要【以管理员身份运行】打开QtCreator或者编译后给NtpClient.exe设置权限【属性->兼容性->以管理员身份运行此程序】,否则无法修改系统时间;
> 2. Linux下编译后使用【sudo ./NtpClient】 运行程序。
![NtpClient](FunctionalModule.assets/NtpClient.gif)
\ No newline at end of file
......@@ -14,7 +14,7 @@ SUBDIRS += QMWidget # qt自定义窗口
SUBDIRS += DeviceManagement # 串口、鼠标、键盘热插拔检测模块
SUBDIRS += QLog # 自定义日志系统
SUBDIRS += QMPlayer # 视频播放器界面
SUBDIRS += NtpClient # NTP时间同步客户端(需要管理员权限/超级用户权限打开)
win32 {
SUBDIRS += TestCrashHandler # windows下Qt程序崩溃问题定位Demo(只在msvc编译下有效
......
#---------------------------------------------------------------------------------------
# @功能: NTP时间同步客户端程序Demo
# 1、使用UDP进行通信;
# 2、毫秒级时间精度;
# 3、使用多个阿里云NTP时间同步服务器、腾讯云NTP时间同步服务器;
# 4、支持windowslinux下修改系统时间
# @编译器: Desktop Qt 5.12.5 MSVC2017 64bit(也支持其它编译器)
# @Qt IDE D:/Qt/Qt5.12.5/Tools/QtCreator/share/qtcreator
#
# @开发者 mhf
# @邮箱 1603291350@qq.com
# @时间 2022-08-09 15:52:56
# @备注 注意:由于设置系统时间的功能比较重要,所以不管是Windows还是Linux都需要最高权限才可以。
# Windows下需要【以管理员身份运行】打开QtCreator或者编译后给NtpClient.exe设置权限【属性->兼容性->以管理员身份运行此程序】,否则无法修改系统时间;
# Linux下编译后使用【sudo ./NtpClient 运行程序。
#---------------------------------------------------------------------------------------
QT += core gui network
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++11
# The following define makes your compiler emit warnings if you use
# any Qt feature that has been marked deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS
# You can also make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
SOURCES += \
main.cpp \
ntpclient.cpp \
widget.cpp
HEADERS += \
ntpclient.h \
widget.h
FORMS += \
widget.ui
# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target
contains(QT_ARCH, i386){ # 使用32位编译器
DESTDIR = $$PWD/../bin # 程序输出路径
}else{
DESTDIR = $$PWD/../bin64 # 使用64位编译器
}
# msvc 编译器使用utf-8编码(好像只在msvc2017以后才有效
msvc {
QMAKE_CFLAGS += /utf-8
QMAKE_CXXFLAGS += /utf-8
#QMAKE_LFLAGS += /MANIFESTUAC:\"level=\'requireAdministrator\' uiAccess=\'false\'\" # 设置程序使用管理员权限运行(只适用于MSVC
}
#include "widget.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w;
w.show();
return a.exec();
}
#include "ntpclient.h"
#include <QDateTime>
#include <QUdpSocket>
#include <QDebug>
#include <QtEndian>
#include <QElapsedTimer>
#include <QMetaEnum>
#ifdef Q_OS_WIN
#include <Windows.h>
#endif
#ifdef Q_OS_LINUX
#include <sys/time.h>
#endif
NtpClient::NtpClient(QObject *parent) : QObject(parent)
{
m_socket = new QUdpSocket(this);
connect(m_socket, &QUdpSocket::connected, this, &NtpClient::on_connected);
connect(m_socket, &QUdpSocket::readyRead, this, &NtpClient::on_readData);
}
/**
* @brief 连接Ntp服务器,端口号默认123
* @param url Ntp服务器IP地址或网址
*/
void NtpClient::connectServer(QString url)
{
close();
m_socket->connectToHost(url, 123);
}
void NtpClient::close()
{
m_socket->abort();
}
void NtpClient::on_connected()
{
qDebug() << "连接成功!";
QMetaEnum m = QMetaEnum::fromType<QAbstractSocket::SocketState>(); // 获取QUdpSocket连接状态字符串
emit updateData(QString("连接成功:%1 %2").arg(m_socket->peerName()).arg(m.key(m_socket->state())));
}
void NtpClient::getTime()
{
sendData();
}
QByteArray toNtpPacket() {
QByteArray result(40, 0);
quint8 li = 0; // LI闰秒标识器,占用2个bit,0 即可;
quint8 vn = 3; // VN 版本号,占用3个bits,表示NTP的版本号,现在为3;
quint8 mode = 3; // Mode 模式,占用3个bits,表示模式。 3 表示 client, 2 表示 server
quint8 stratum = 0; // 系统时钟的层数,取值范围为1~16,它定义了时钟的准确度。层数为1的时钟准确度最高,准确度从1到16依次递减,层数为16的时钟处于未同步状态,不能作为参考时钟。
quint8 poll = 4; // 轮询时间,即两个连续NTP报文之间的时间间隔(4-14)
qint8 precision = -6; // 系统时钟的精度,精确到秒的平方级(-6 到 -20)
result[0] = char((li << 6) | (vn <<3) | (mode));
result[1] = char(stratum & 0xff);
result[2] = char(poll & 0xff);
result[3] = char(precision & 0xff);
qint64 currentLocalTimestamp = QDateTime::currentMSecsSinceEpoch();
result.append((const char *)&currentLocalTimestamp, sizeof(qint64));
return result;
}
/**
* @brief 发送NTP请求帧
*/
void NtpClient::sendData()
{
QByteArray arr = toNtpPacket();
qint64 len = m_socket->write(arr);
if(len != arr.count())
{
qWarning() << "发送NTP请求帧失败:" << arr.toHex(' ');
}
}
/**
* @brief 将QByteArray类型时间戳数据转换为整形并且进行大小端转换
* @param bt
* @return
*/
quint32 byteToUInt32(QByteArray bt) {
if(bt.count() != 4) return 0;
quint32 value;
memcpy(&value, bt.data(), 4);
return qToBigEndian(value); // 大端转小端
}
/**
* @brief 将Ntp时间戳转换成QDateTime可用的时间戳
* @param bt
* @return
*/
qint64 byte64ToMillionSecond(QByteArray bt) {
qint64 second = byteToUInt32(bt.left(4));
qint64 millionSecond = byteToUInt32(bt.mid(4, 4));
return (second * 1000L) + ((millionSecond * 1000L) >> 32);
}
/**
* @brief 接收返回的NTP数据帧并解析
*/
void NtpClient::on_readData()
{
QElapsedTimer timer; // 统计数据解析消耗的时间
timer.start();
QByteArray buf = m_socket->readAll();
qint64 currentLocalTimestamp = QDateTime::currentDateTime().toMSecsSinceEpoch(); // 客户端接收到响应报文时的时间戳 T4
if(buf.count() < 48) // Ntp协议帧长度为48字节
{
return;
}
QDateTime epoch(QDate(1900, 1, 1), QTime(0, 0, 0)); // ntp时间计时从1900年开始
QDateTime unixStart(QDate(1970, 1, 1), QTime(0, 0, 0)); // UNIX操作系统考虑到计算机产生的年代和应用的时限综合取了1970年1月1日作为UNIX TIME的纪元时间(开始时间)
qint64 unixDiff = epoch.msecsTo(unixStart);
// 解析ntp协议中的时间
qint64 referenceTimestamp = byte64ToMillionSecond(buf.mid(16, 8)) - unixDiff; // 参考时间戳
qint64 originTimestamp; // 原始时间戳 T1
memcpy(&originTimestamp, buf.mid(24, 8), 8);
qint64 receiveTimestamp = byte64ToMillionSecond(buf.mid(32, 8)) - unixDiff; // 接收时间戳 T2
qint64 translateTimestamp = byte64ToMillionSecond(buf.mid(40, 8)) - unixDiff; // 传送时间戳 T3
QDateTime dateTime;
#if 0
qDebug() << "-----------NTP协议中包含的所有时间-----------";
dateTime.setMSecsSinceEpoch(referenceTimestamp);
qDebug() << "参考时间戳: " << dateTime.toString("yyyy-MM-dd HH:mm:ss zzz");
dateTime.setMSecsSinceEpoch(originTimestamp);
qDebug() << "原始时间戳T1:" << dateTime.toString("yyyy-MM-dd HH:mm:ss zzz");
dateTime.setMSecsSinceEpoch(receiveTimestamp);
qDebug() << "接收时间戳T2:" << dateTime.toString("yyyy-MM-dd HH:mm:ss zzz");
dateTime.setMSecsSinceEpoch(translateTimestamp);
qDebug() << "传送时间戳T3:" << dateTime.toString("yyyy-MM-dd HH:mm:ss zzz");
dateTime.setMSecsSinceEpoch(currentLocalTimestamp);
qDebug() << "本地时间戳T4:" << dateTime.toString("yyyy-MM-dd HH:mm:ss zzz");
qDebug() << "------------------------------------------";
#endif
QString strTime;
#if 1 // 计算方式1:时间差offset=((T2-T1)+(T3-T4))/2 实际时间=程序处理时间(timer.elapsed()) + 接收数据时间T4 + 客户端与服务端的时间差(offset)
qint64 currentLocalTimestamp1 = timer.elapsed() + currentLocalTimestamp + qint64((receiveTimestamp - originTimestamp + translateTimestamp - currentLocalTimestamp) / 2);
dateTime.setMSecsSinceEpoch(currentLocalTimestamp1);
strTime = dateTime.toString("yyyy-MM-dd HH:mm:ss zzz");
emit updateData(strTime);
#else // 计算方式2:往返时延Delay=(T4-T1)-(T3-T2) 实际时间=程序处理时间(timer.elapsed()) + 服务器数据发出时间(T3)+ 通信时延(Delay)
qint64 currentLocalTimestamp2 = timer.elapsed() + translateTimestamp + (((currentLocalTimestamp - originTimestamp) - (translateTimestamp - receiveTimestamp)) / 2);
dateTime.setMSecsSinceEpoch(currentLocalTimestamp2);
strTime = dateTime.toString("yyyy-MM-dd HH:mm:ss zzz");
#endif
qDebug() << strTime;
setDateTime(dateTime);
}
void NtpClient::setDateTime(QDateTime& dateTime)
{
QDate date = dateTime.date();
QTime time = dateTime.time();
#ifdef Q_OS_WIN
SYSTEMTIME system_time = {0};
memset(&system_time, 0, sizeof(SYSTEMTIME));
system_time.wYear = date.year();
system_time.wMonth = date.month();
system_time.wDay = date.day();
system_time.wHour = time.hour();
system_time.wMinute = time.minute();
system_time.wSecond = time.second();
system_time.wMilliseconds = time.msec();
if (SetLocalTime(&system_time)) // 仅限于管理员。
{
emit updateData("设置时间成功!");
}
else
{
emit updateData("设置时间失败!");
}
#endif
#ifdef Q_OS_LINUX
struct tm tptr;
struct timeval tv;
tptr.tm_year = date.year() - 1900; // 这里必须-1900,否则设置不成功
tptr.tm_mon = date.month();
tptr.tm_mday = date.day();
tptr.tm_hour = time.hour();
tptr.tm_min = time.minute();
tptr.tm_sec = time.second();
tv.tv_sec = mktime(&tptr); // 将tptr赋值给tv_sec
tv.tv_usec = time.msec() * 1000; // 设置微秒值
if (0 == settimeofday(&tv, NULL)) // 仅限于超级用户, 使用sudo ./NtpClient
{
emit updateData("设置时间成功!");
}
else
{
emit updateData("设置时间失败!");
}
#endif
}
#ifndef NTPCLIENT_H
#define NTPCLIENT_H
#include <QObject>
class QUdpSocket;
#if 0 // NTP协议帧(未使用)
typedef struct
{
char LI_VN_Mode;
char Stratum;
char Poll;
char Precision;
int RootDelay;
int RootDispersion;
int ReferenceIdentifier;
quint64 ReferenceTimeStamp; // 系统时钟最后一次被设定或更新的时间
quint64 OriginateTimeStamp; // NTP请求报文离开发送端时发送端的本地时间
quint64 ReceiveTimeStamp; // NTP请求报文到达Server端时接收端的本地时间。
quint64 TransmitTimeStamp; // 发送时间戳,客户端发送时填写,server接收到后会将TransmitTimeStamp值写入OriginateTimeStamp,然后NTP应答报文离开Server时在OriginateTimeStamp的本地时间。
}NtpPacket;
#endif
class NtpClient : public QObject
{
Q_OBJECT
public:
explicit NtpClient(QObject *parent = nullptr);
void connectServer(QString url); // 连接Ntp服务
void close();
void getTime();
signals:
void updateData(const QString& time); // 添加显示到界面上文本框中的信息
private slots:
void on_connected();
void on_readData();
void sendData();
void setDateTime(QDateTime& dateTime);
private:
QUdpSocket* m_socket = nullptr;
};
#endif // NTPCLIENT_H
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>
#include <QThread>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
this->setWindowTitle("NTP时间同步客户端");
connect(&m_ntpClient, &NtpClient::updateData, ui->textEdit, &QTextEdit::append);
}
Widget::~Widget()
{
delete ui;
}
void Widget::on_but_getTime_clicked()
{
m_ntpClient.getTime();
// QThread::msleep(100);
// m_ntpClient.getTime();
}
void Widget::on_but_connect_clicked()
{
m_ntpClient.connectServer(ui->com_address->currentText());
}
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include "ntpclient.h"
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private slots:
void on_but_getTime_clicked();
void on_but_connect_clicked();
private:
Ui::Widget *ui;
NtpClient m_ntpClient;
};
#endif // WIDGET_H
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Widget</class>
<widget class="QWidget" name="Widget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>410</width>
<height>311</height>
</rect>
</property>
<property name="windowTitle">
<string>Widget</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QComboBox" name="com_address">
<item>
<property name="text">
<string>ntp.tencent.com</string>
</property>
</item>
<item>
<property name="text">
<string>ntp1.tencent.com</string>
</property>
</item>
<item>
<property name="text">
<string>ntp2.tencent.com</string>
</property>
</item>
<item>
<property name="text">
<string>ntp3.tencent.com</string>
</property>
</item>
<item>
<property name="text">
<string>ntp4.tencent.com</string>
</property>
</item>
<item>
<property name="text">
<string>ntp5.tencent.com</string>
</property>
</item>
<item>
<property name="text">
<string>ntp.aliyun.com</string>
</property>
</item>
<item>
<property name="text">
<string>ntp1.aliyun.com</string>
</property>
</item>
<item>
<property name="text">
<string>ntp2.aliyun.com</string>
</property>
</item>
<item>
<property name="text">
<string>ntp3.aliyun.com</string>
</property>
</item>
<item>
<property name="text">
<string>ntp4.aliyun.com</string>
</property>
</item>
<item>
<property name="text">
<string>ntp5.aliyun.com</string>
</property>
</item>
<item>
<property name="text">
<string>ntp6.aliyun.com</string>
</property>
</item>
<item>
<property name="text">
<string>ntp7.aliyun.com</string>
</property>
</item>
</widget>
</item>
<item row="0" column="1">
<widget class="QPushButton" name="but_connect">
<property name="text">
<string>连接Ntp服务</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QPushButton" name="but_getTime">
<property name="text">
<string>获取Ntp时间</string>
</property>
</widget>
</item>
<item row="1" column="0" colspan="3">
<widget class="QTextEdit" name="textEdit"/>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册