diff --git a/FunctionalModule/DeviceManagement/porttest.cpp b/FunctionalModule/DeviceManagement/porttest.cpp index ff4f79a40513c6d0b285a46e68f4add31c7a7c4d..5603270a3210c81216bb054a55a5847137d42cab 100644 --- a/FunctionalModule/DeviceManagement/porttest.cpp +++ b/FunctionalModule/DeviceManagement/porttest.cpp @@ -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 // 串口拔出时自动移除串口名 { diff --git a/FunctionalModule/FunctionalModule.assets/NtpClient.gif b/FunctionalModule/FunctionalModule.assets/NtpClient.gif new file mode 100644 index 0000000000000000000000000000000000000000..c6c470079da8c7677c6ec1fc4785f83f59d76fdf Binary files /dev/null and b/FunctionalModule/FunctionalModule.assets/NtpClient.gif differ diff --git a/FunctionalModule/FunctionalModule.md b/FunctionalModule/FunctionalModule.md index 40ac15c285ca25bfd25a86caee6d2bc663e5a5f3..8fe9d4ff1d084043f05cd611f21f3d71f2eaa81a 100644 --- a/FunctionalModule/FunctionalModule.md +++ b/FunctionalModule/FunctionalModule.md @@ -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 diff --git a/FunctionalModule/FunctionalModule.pro b/FunctionalModule/FunctionalModule.pro index 6f0fea103341410edaf9129b36080eea92ef5fbb..fb313c1ce8112edc10d6729a9ab6401faf0f6d76 100644 --- a/FunctionalModule/FunctionalModule.pro +++ b/FunctionalModule/FunctionalModule.pro @@ -14,7 +14,7 @@ SUBDIRS += QMWidget # qt自定义窗口 SUBDIRS += DeviceManagement # 串口、鼠标、键盘热插拔检测模块 SUBDIRS += QLog # 自定义日志系统 SUBDIRS += QMPlayer # 视频播放器界面 - +SUBDIRS += NtpClient # NTP时间同步客户端(需要管理员权限/超级用户权限打开) win32 { SUBDIRS += TestCrashHandler # windows下Qt程序崩溃问题定位Demo(只在msvc编译下有效) diff --git a/FunctionalModule/NtpClient/NtpClient.pro b/FunctionalModule/NtpClient/NtpClient.pro new file mode 100644 index 0000000000000000000000000000000000000000..10c6bb998361d65ecb9b6d440c847fa816975a4f --- /dev/null +++ b/FunctionalModule/NtpClient/NtpClient.pro @@ -0,0 +1,62 @@ +#--------------------------------------------------------------------------------------- +# @功能: NTP时间同步客户端程序Demo +# 1、使用UDP进行通信; +# 2、毫秒级时间精度; +# 3、使用多个阿里云NTP时间同步服务器、腾讯云NTP时间同步服务器; +# 4、支持windows、linux下修改系统时间。 +# @编译器: 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) +} diff --git a/FunctionalModule/NtpClient/main.cpp b/FunctionalModule/NtpClient/main.cpp new file mode 100644 index 0000000000000000000000000000000000000000..b0a4ec26478f6b9aba3e1747ec464ea0c26dd5b9 --- /dev/null +++ b/FunctionalModule/NtpClient/main.cpp @@ -0,0 +1,11 @@ +#include "widget.h" + +#include + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + Widget w; + w.show(); + return a.exec(); +} diff --git a/FunctionalModule/NtpClient/ntpclient.cpp b/FunctionalModule/NtpClient/ntpclient.cpp new file mode 100644 index 0000000000000000000000000000000000000000..296e84eaefa8b6e6ca22113678010834e523d004 --- /dev/null +++ b/FunctionalModule/NtpClient/ntpclient.cpp @@ -0,0 +1,220 @@ +#include "ntpclient.h" +#include +#include +#include +#include +#include +#include + +#ifdef Q_OS_WIN +#include +#endif +#ifdef Q_OS_LINUX +#include +#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(); // 获取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 *)¤tLocalTimestamp, 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 +} + diff --git a/FunctionalModule/NtpClient/ntpclient.h b/FunctionalModule/NtpClient/ntpclient.h new file mode 100644 index 0000000000000000000000000000000000000000..9fd942c98b885aa4dd45a663ccccb98964e95fc3 --- /dev/null +++ b/FunctionalModule/NtpClient/ntpclient.h @@ -0,0 +1,47 @@ +#ifndef NTPCLIENT_H +#define NTPCLIENT_H + +#include +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 diff --git a/FunctionalModule/NtpClient/widget.cpp b/FunctionalModule/NtpClient/widget.cpp new file mode 100644 index 0000000000000000000000000000000000000000..29f270cb9831ba5d67c172e89b4015fb0705682f --- /dev/null +++ b/FunctionalModule/NtpClient/widget.cpp @@ -0,0 +1,34 @@ +#include "widget.h" +#include "ui_widget.h" +#include +#include + +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()); +} diff --git a/FunctionalModule/NtpClient/widget.h b/FunctionalModule/NtpClient/widget.h new file mode 100644 index 0000000000000000000000000000000000000000..e503fe5f7331d8b2e502309c64d4b5f3de1ed245 --- /dev/null +++ b/FunctionalModule/NtpClient/widget.h @@ -0,0 +1,29 @@ +#ifndef WIDGET_H +#define WIDGET_H + +#include +#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 diff --git a/FunctionalModule/NtpClient/widget.ui b/FunctionalModule/NtpClient/widget.ui new file mode 100644 index 0000000000000000000000000000000000000000..45bbfe996170b2195a72cc7008efe3ec4139a6c2 --- /dev/null +++ b/FunctionalModule/NtpClient/widget.ui @@ -0,0 +1,112 @@ + + + Widget + + + + 0 + 0 + 410 + 311 + + + + Widget + + + + + + + ntp.tencent.com + + + + + ntp1.tencent.com + + + + + ntp2.tencent.com + + + + + ntp3.tencent.com + + + + + ntp4.tencent.com + + + + + ntp5.tencent.com + + + + + ntp.aliyun.com + + + + + ntp1.aliyun.com + + + + + ntp2.aliyun.com + + + + + ntp3.aliyun.com + + + + + ntp4.aliyun.com + + + + + ntp5.aliyun.com + + + + + ntp6.aliyun.com + + + + + ntp7.aliyun.com + + + + + + + + 连接Ntp服务 + + + + + + + 获取Ntp时间 + + + + + + + + + + +