From cb451b3070d876066325091a73ff7721afa0dbcd Mon Sep 17 00:00:00 2001 From: luocai <168062547@qq.com> Date: Tue, 21 May 2024 21:09:55 +0800 Subject: [PATCH] =?UTF-8?q?1.=E6=B7=BB=E5=8A=A0=E4=B8=8A=E4=BD=8D=E6=9C=BA?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Analyser/Analyser.rc | 61 ++++++ Analyser/CMakeLists.txt | 20 ++ Analyser/CategoryLogSinkBackend.cpp | 15 ++ Analyser/CategoryLogSinkBackend.h | 20 ++ Analyser/ModuleCommunication.cpp | 204 ++++++++++++++++++ Analyser/ModuleCommunication.h | 100 +++++++++ Analyser/Widget.cpp | 221 ++++++++++++++++++++ Analyser/Widget.h | 54 +++++ Analyser/main.cpp | 21 ++ CMakeLists.txt | 2 + OtaUpdate/CMakeLists.txt | 2 +- OtaUpdate/CdcUpdater.h | 2 - OtaUpdate/OtaUpdate.rc | 61 ++++++ Readme.md | 310 ++++++++++++++++++++++++++++ resources/logo.ico | Bin 0 -> 1150 bytes resources/logo.png | Bin 0 -> 3353 bytes 16 files changed, 1090 insertions(+), 3 deletions(-) create mode 100644 Analyser/Analyser.rc create mode 100644 Analyser/CMakeLists.txt create mode 100644 Analyser/CategoryLogSinkBackend.cpp create mode 100644 Analyser/CategoryLogSinkBackend.h create mode 100644 Analyser/ModuleCommunication.cpp create mode 100644 Analyser/ModuleCommunication.h create mode 100644 Analyser/Widget.cpp create mode 100644 Analyser/Widget.h create mode 100644 Analyser/main.cpp create mode 100644 OtaUpdate/OtaUpdate.rc create mode 100644 resources/logo.ico create mode 100644 resources/logo.png diff --git a/Analyser/Analyser.rc b/Analyser/Analyser.rc new file mode 100644 index 0000000..88ed4a7 --- /dev/null +++ b/Analyser/Analyser.rc @@ -0,0 +1,61 @@ +// Microsoft Visual C++ generated resource script. +// + + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// 中文(简体,中国) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_CHS) +LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED +#pragma code_page(936) + +#ifdef APSTUDIO_INVOKED + ///////////////////////////////////////////////////////////////////////////// + // + // TEXTINCLUDE + // + + 2 TEXTINCLUDE BEGIN "#include " + "winres.h" + "\r\n" + "\0" END + + 3 TEXTINCLUDE BEGIN "\r\n" + "\0" END + +#endif // APSTUDIO_INVOKED + + ///////////////////////////////////////////////////////////////////////////// + // + // Icon + // + + // Icon with lowest ID value placed first to ensure application icon + // remains consistent on all systems. + IDI_ICON1 ICON "..\\resources\\logo.ico" + +#endif // 中文(简体,中国) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + diff --git a/Analyser/CMakeLists.txt b/Analyser/CMakeLists.txt new file mode 100644 index 0000000..5d61efa --- /dev/null +++ b/Analyser/CMakeLists.txt @@ -0,0 +1,20 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets SerialPort) +find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets SerialPort) + +add_executable(Analyser Analyser.rc + main.cpp + CategoryLogSinkBackend.h CategoryLogSinkBackend.cpp + Widget.h Widget.cpp + ModuleCommunication.h ModuleCommunication.cpp +) + +target_link_libraries(Analyser + PRIVATE Universal + PRIVATE QtComponets + PRIVATE Ws2_32 + PRIVATE Qt${QT_VERSION_MAJOR}::Widgets + PRIVATE Qt${QT_VERSION_MAJOR}::SerialPort +) diff --git a/Analyser/CategoryLogSinkBackend.cpp b/Analyser/CategoryLogSinkBackend.cpp new file mode 100644 index 0000000..33a208d --- /dev/null +++ b/Analyser/CategoryLogSinkBackend.cpp @@ -0,0 +1,15 @@ +#include "CategoryLogSinkBackend.h" +#include "AsyncEvent.h" +#include +#include + +CategoryLogSinkBackend::CategoryLogSinkBackend(const std::string &category, QTextBrowser *target) + : m_category(category), m_target(target) { +} + +void CategoryLogSinkBackend::consume(const boost::log::record_view &record, string_type const &output) { + auto &&category = record[AmassKeywords::category]; + if (category == m_category) { + Amass::executeAtObjectThread(m_target, [this, output]() { m_target->append(QString::fromStdString(output)); }); + } +} diff --git a/Analyser/CategoryLogSinkBackend.h b/Analyser/CategoryLogSinkBackend.h new file mode 100644 index 0000000..a11f4d1 --- /dev/null +++ b/Analyser/CategoryLogSinkBackend.h @@ -0,0 +1,20 @@ +#ifndef __CATEGORYLOGSINKBACKEND_H__ +#define __CATEGORYLOGSINKBACKEND_H__ + +#include +#include + +class QTextBrowser; + +class CategoryLogSinkBackend + : public boost::log::sinks::basic_formatted_sink_backend { +public: + CategoryLogSinkBackend(const std::string &category, QTextBrowser *target); + void consume(const boost::log::record_view &record, string_type const &output); + +private: + std::string m_category; + QTextBrowser *m_target = nullptr; +}; + +#endif // __CATEGORYLOGSINKBACKEND_H__ diff --git a/Analyser/ModuleCommunication.cpp b/Analyser/ModuleCommunication.cpp new file mode 100644 index 0000000..1f67b84 --- /dev/null +++ b/Analyser/ModuleCommunication.cpp @@ -0,0 +1,204 @@ +#include "ModuleCommunication.h" +#include "BoostLog.h" +#include +#include +#include + +static inline uint8_t xor_checksum_byte(const uint8_t *data, uint32_t len) { + uint8_t sum = 0; + for (uint32_t i = 0; i < len; ++i) { + sum ^= data[i]; + } + return sum; +} + +ModuleCommunication::ModuleCommunication(QObject *parent) : QObject{parent} { +} + +bool ModuleCommunication::open(const QString &portName) { + bool ret = true; + m_serialPort = std::make_shared(portName); + m_serialPort->setBaudRate(QSerialPort::Baud115200); + connect(m_serialPort.get(), &QSerialPort::readyRead, this, &ModuleCommunication::onReadyRead); + + ret = m_serialPort->open(QSerialPort::ReadWrite); + LOG_CAT(info, GUI) << "打开串口(" << portName.toStdString() << ")" << (ret ? "成功" : "失败") << "。"; + LOG_CAT(info, GUI) << Separator; + return ret; +} + +void ModuleCommunication::verify(uint8_t timeout) { + VerifyInfo data = {0}; + data.timeout = timeout; + + auto [frameData, frameSize] = generateFrame(Verify, reinterpret_cast(&data), sizeof(data)); + m_serialPort->write(reinterpret_cast(frameData), frameSize); + + LOG_CAT(info, GUI) << "发送识别指令: " << protocolDataFormatString(frameData, frameSize); + LOG_CAT(info, GUI) << Separator; +} + +void ModuleCommunication::enroll(const std::string &username, uint8_t timeout) { + EnrollData data = {0}; + data.timeout = timeout; + strncpy(reinterpret_cast(data.username), username.c_str(), sizeof(data.username)); + auto [frameData, frameSize] = generateFrame(EnrollSingle, reinterpret_cast(&data), sizeof(data)); + m_serialPort->write(reinterpret_cast(frameData), frameSize); + + LOG_CAT(info, GUI) << "发送注册指令: " << protocolDataFormatString(frameData, frameSize); + LOG_CAT(info, GUI) << "用户名: " << username << ", 超时时间: " << static_cast(timeout) << "s"; + LOG_CAT(info, GUI) << Separator; +} + +void ModuleCommunication::deleteUser(uint16_t userid) { + uint16_t n = htons(userid); + auto [frameData, frameSize] = generateFrame(DeleteUser, reinterpret_cast(&n), sizeof(n)); + m_serialPort->write(reinterpret_cast(frameData), frameSize); + LOG_CAT(info, GUI) << "发送删除用户指令: " << protocolDataFormatString(frameData, frameSize); + LOG_CAT(info, GUI) << "删除用户ID: " << userid; + LOG_CAT(info, GUI) << Separator; +} + +void ModuleCommunication::deleteAll() { + auto [frameData, frameSize] = generateFrame(DeleteAll); + m_serialPort->write(reinterpret_cast(frameData), frameSize); + LOG_CAT(info, GUI) << "发送删除所有指令: " << protocolDataFormatString(frameData, frameSize); + LOG_CAT(info, GUI) << Separator; +} + +void ModuleCommunication::onReadyRead() { + auto data = m_serialPort->readAll(); + if (data.size() < 6) { + LOG(warning) << "invalid data: " + << protocolDataFormatString(reinterpret_cast(data.data()), data.size()); + return; + } + uint8_t messageId = data[2]; + switch (messageId) { + case Reply: { + uint8_t replyId = data[5]; + auto result = data[6]; + switch (replyId) { + case Verify: { + if (result == Success) { + LOG_CAT(info, GUI) << "模组: " + << protocolDataFormatString(reinterpret_cast(data.data()), + data.size()); + auto info = reinterpret_cast(data.data() + 7); + LOG_CAT(info, GUI) << "用户ID: " << ntohs(info->userid) + << ", 用户名: " << std::string_view(reinterpret_cast(info->username)); + LOG_CAT(info, GUI) << Separator; + } else if (result == Failed4Timeout) { + LOG_CAT(info, GUI) << "模组: " + << protocolDataFormatString(reinterpret_cast(data.data()), + data.size()); + LOG_CAT(info, GUI) << "识别超时。"; + LOG_CAT(info, GUI) << Separator; + } else if (result == Failed4UnknownReason) { + LOG_CAT(info, GUI) << "模组: " + << protocolDataFormatString(reinterpret_cast(data.data()), + data.size()); + LOG_CAT(info, GUI) << "未录入用户。"; + LOG_CAT(info, GUI) << Separator; + } else { + } + + break; + } + case EnrollSingle: { + if (result == Success) { + auto info = reinterpret_cast(data.data() + 7); + LOG_CAT(info, GUI) << "模组: " + << protocolDataFormatString(reinterpret_cast(data.data()), + data.size()); + LOG_CAT(info, GUI) << "注册成功,用户ID: " << ntohs(info->userid); + LOG_CAT(info, GUI) << Separator; + } + break; + } + case DeleteUser: { + if (result == Success) { + LOG_CAT(info, GUI) << "模组: " + << protocolDataFormatString(reinterpret_cast(data.data()), + data.size()); + LOG_CAT(info, GUI) << "删除用户成功。"; + LOG_CAT(info, GUI) << Separator; + } else { + LOG_CAT(info, GUI) << "模组: " + << protocolDataFormatString(reinterpret_cast(data.data()), + data.size()); + LOG_CAT(info, GUI) << "删除用户失败。"; + LOG_CAT(info, GUI) << Separator; + } + break; + } + case DeleteAll: { + if (result == Success) { + LOG_CAT(info, GUI) << "模组: " + << protocolDataFormatString(reinterpret_cast(data.data()), + data.size()); + LOG_CAT(info, GUI) << "删除所有用户成功。"; + LOG_CAT(info, GUI) << Separator; + } + break; + } + default: + LOG(warning) << "unknown reply command: 0x" << (static_cast(replyId) & 0xff) << ", data: " + << protocolDataFormatString(reinterpret_cast(data.data()), data.size()); + break; + } + break; + } + case Note: { + uint8_t noteId = data[5]; + switch (noteId) { + case Ready: { + LOG_CAT(info, GUI) << "模组: " + << protocolDataFormatString(reinterpret_cast(data.data()), data.size()); + LOG_CAT(info, GUI) << "模组上电初始化成功。"; + LOG_CAT(info, GUI) << Separator; + break; + } + case 0x01: { // 模组返回的数据为当前帧的手掌状态 + auto info = reinterpret_cast(data.data() + 7); + LOG(info) << info->state; + break; + } + default: + LOG(warning) << "unknown note command: 0x" << (static_cast(noteId) & 0xff) << ", data: " + << protocolDataFormatString(reinterpret_cast(data.data()), data.size()); + break; + } + + break; + } + default: + LOG(warning) << "unknown command: 0x" << (static_cast(data[2]) & 0xff) << ", data: " + << protocolDataFormatString(reinterpret_cast(data.data()), data.size()); + break; + } +} + +std::pair ModuleCommunication::generateFrame(MessageId command, const uint8_t *data, + uint16_t size) { + static uint8_t sendBuffer[1024] = {0}; + memset(sendBuffer, 0, sizeof(sendBuffer)); + sendBuffer[0] = 0xef; + sendBuffer[1] = 0xaa; + sendBuffer[2] = command; + sendBuffer[3] = size >> 8; + sendBuffer[4] = size & 0xff; + if (size > 0) { + memcpy(sendBuffer + 5, data, size); + } + sendBuffer[5 + size] = xor_checksum_byte(sendBuffer + 2, 3 + size); + return std::make_pair(sendBuffer, size + 6); +} + +std::string ModuleCommunication::protocolDataFormatString(const uint8_t *data, int size) { + std::ostringstream oss; + for (int i = 0; i < size; i++) { + oss << "0x" << std::hex << (static_cast(data[i]) & 0xff) << " "; + } + return oss.str(); +} diff --git a/Analyser/ModuleCommunication.h b/Analyser/ModuleCommunication.h new file mode 100644 index 0000000..d7a433a --- /dev/null +++ b/Analyser/ModuleCommunication.h @@ -0,0 +1,100 @@ +#ifndef MODULECOMMUNICATION_H +#define MODULECOMMUNICATION_H + +#include +#include + +class QSerialPort; + +class ModuleCommunication : public QObject { + Q_OBJECT + static constexpr uint32_t UsernameSize = 32; + static constexpr const char *Separator = "----------"; + +public: + enum MessageId : uint8_t { + Reply = 0, + Note = 0x01, + Verify = 0x12, + EnrollSingle = 0x1D, + DeleteUser = 0x20, + DeleteAll = 0x21, + }; + + enum NoteId : uint8_t { + Ready = 0x00, + }; + enum MessageStatus : uint8_t { + Success = 0, + Rejected = 1, + Aborted = 2, + Failed4Camera = 4, + Failed4UnknownReason = 5, + Failed4InvalidParam = 6, + Failed4NoMemory = 7, + Failed4UnknownUser = 8, + Failed4MaxUser = 9, + Failed4FaceEnrolled = 10, + Failed4LivenessCheck = 12, + Failed4Timeout = 13, + Failed4Authorization = 14, + Failed4ReadFile = 19, + Failed4WriteFile = 20, + Failed4NoEncrypt = 21, + }; + +#pragma pack(1) + struct VerifyInfo { + uint8_t powerDownRightAway; // power down right away after verifying + uint8_t timeout; // timeout, unit second, default 10s + }; + struct VerifyNoteInfo { + int16_t state; // corresponding to PALM_STATE_* + // position + int16_t left; // in pixel + int16_t top; + int16_t right; + int16_t bottom; + // pose + int16_t yaw; // up and down in vertical orientation + int16_t pitch; // right or left turned in horizontal orientation + int16_t roll; // slope + }; + + struct EnrollData { + uint8_t admin = 0; + uint8_t username[32]; + uint8_t palmDirection; + uint8_t timeout; + }; + + struct EnrollDataReply { + uint16_t userid; + uint8_t face_direction; // depleted, user ignore this field + }; + + struct VerifyDataReply { + uint16_t userid; + uint8_t username[UsernameSize]; // 32Bytes uint8_t admin; + uint8_t unlockStatus; + }; + +#pragma pack() + explicit ModuleCommunication(QObject *parent = nullptr); + bool open(const QString &portName); + void verify(uint8_t timeout); + + void enroll(const std::string &username, uint8_t timeout); + void deleteUser(uint16_t userid); + void deleteAll(); + +protected: + void onReadyRead(); + std::pair generateFrame(MessageId command, const uint8_t *data = nullptr, uint16_t size = 0); + std::string protocolDataFormatString(const uint8_t *data, int size); + +private: + std::shared_ptr m_serialPort; +}; + +#endif // MODULECOMMUNICATION_H diff --git a/Analyser/Widget.cpp b/Analyser/Widget.cpp new file mode 100644 index 0000000..1b77c4e --- /dev/null +++ b/Analyser/Widget.cpp @@ -0,0 +1,221 @@ +#include "Widget.h" +#include "BoostLog.h" +#include "CategoryLogSinkBackend.h" +#include "ModuleCommunication.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +Widget::Widget(QWidget *parent) : QWidget{parent} { + auto serialGroupBox = new QGroupBox("串口设置"); + auto serialLayout = new QGridLayout(); + + auto label = new QLabel("端口"); + serialLayout->addWidget(label, 0, 0); + + m_serialComboBox = new QComboBox(); + serialLayout->addWidget(m_serialComboBox, 0, 1); + + label = new QLabel("波特率"); + serialLayout->addWidget(label, 1, 0); + + label = new QLabel("115200"); + serialLayout->addWidget(label, 1, 1); + + auto refreshButton = new QPushButton("刷新"); + connect(refreshButton, &QPushButton::clicked, this, &Widget::onSerialRefreshButtonClicked); + m_serialConnectButton = new QPushButton("连接"); + connect(m_serialConnectButton, &QPushButton::clicked, this, &Widget::onSerialConnectButtonClicked); + serialLayout->addWidget(refreshButton, 2, 0); + serialLayout->addWidget(m_serialConnectButton, 2, 1); + serialGroupBox->setLayout(serialLayout); + + auto uvcGroupBox = new QGroupBox("UVC设置"); + auto uvcLayout = new QGridLayout(); + label = new QLabel("设备名"); + uvcLayout->addWidget(label, 0, 0); + + label = new QLabel("未发现设备"); + uvcLayout->addWidget(label, 0, 1); + + auto uvcRefreshButton = new QPushButton("刷新"); + auto uvcConnectButton = new QPushButton("连接"); + uvcLayout->addWidget(uvcRefreshButton, 1, 0); + uvcLayout->addWidget(uvcConnectButton, 1, 1); + uvcGroupBox->setLayout(uvcLayout); + + auto connectLayout = new QHBoxLayout(); + connectLayout->addWidget(serialGroupBox); + connectLayout->addWidget(uvcGroupBox); + + m_logBrowser = new QTextBrowser(); + m_logBrowser->setReadOnly(true); + + auto tabWidget = new QTabWidget(); + tabWidget->addTab(m_logBrowser, "日志"); + tabWidget->addTab(new QWidget(), "视频流"); + tabWidget->addTab(new QWidget(), "已注册列表"); + + m_commandGroupBox = initializeCommandGroupBox(); + + auto operatorLayout = new QVBoxLayout(); + operatorLayout->addLayout(connectLayout); + operatorLayout->addWidget(m_commandGroupBox); + operatorLayout->addStretch(2); + + auto layout = new QHBoxLayout(this); + layout->addLayout(operatorLayout, 1); + layout->addWidget(tabWidget, 3); + + QTimer::singleShot(0, this, [this]() { + onSerialRefreshButtonClicked(); + m_commandGroupBox->setEnabled(false); + }); +} + +void Widget::initializeLogger() { + auto backend = boost::make_shared("GUI", m_logBrowser); + using SynchronousCategorySink = boost::log::sinks::synchronous_sink; + auto sink = boost::make_shared(backend); + // sink->set_formatter(&zlogFormatter); + boost::log::core::get()->add_sink(sink); +} + +QGroupBox *Widget::initializeCommandGroupBox() { + auto ret = new QGroupBox("命令"); + auto layout = new QGridLayout(); + + layout->addWidget(initializeEnrollGroupBox(), 0, 0); + layout->addWidget(initializeVerifyGroupBox(), 0, 1); + layout->addWidget(initializeDeleteGroupBox(), 1, 0); + layout->addWidget(initializePalmFeatureGroupBox(), 1, 1); + + ret->setLayout(layout); + return ret; +} + +QGroupBox *Widget::initializeEnrollGroupBox() { + auto ret = new QGroupBox("注册用户"); + auto layout = new QFormLayout(); + + m_enrollNameEdit = new QLineEdit(); + layout->addRow("用户姓名:", m_enrollNameEdit); + + m_enrollTimeoutEdit = new QLineEdit("10"); + layout->addRow("超时时间:", m_enrollTimeoutEdit); + + m_enrollButton = new QPushButton("注册"); + connect(m_enrollButton, &QPushButton::clicked, this, &Widget::onEnrollButtonClicked); + layout->addRow("", m_enrollButton); + + ret->setLayout(layout); + return ret; +} +QGroupBox *Widget::initializeVerifyGroupBox() { + auto ret = new QGroupBox("识别用户"); + auto layout = new QFormLayout(); + + m_verifyTimeoutEdit = new QLineEdit("10"); + layout->addRow("超时时间:", m_verifyTimeoutEdit); + + m_verifyButton = new QPushButton("识别"); + connect(m_verifyButton, &QPushButton::clicked, this, &Widget::onVerifyButtonClicked); + layout->addRow("", m_verifyButton); + ret->setLayout(layout); + return ret; +} +QGroupBox *Widget::initializeDeleteGroupBox() { + auto ret = new QGroupBox("删除用户"); + auto layout = new QFormLayout(); + + m_deleteIdEdit = new QLineEdit(""); + layout->addRow("用户ID:", m_deleteIdEdit); + + m_deleteButton = new QPushButton("删除"); + connect(m_deleteButton, &QPushButton::clicked, this, &Widget::onDeleteButtonClicked); + layout->addRow("", m_deleteButton); + + m_deleteAllButton = new QPushButton("删除所有"); + connect(m_deleteAllButton, &QPushButton::clicked, this, &Widget::onDeleteAllButtonClicked); + layout->addRow("", m_deleteAllButton); + + ret->setLayout(layout); + return ret; +} +QGroupBox *Widget::initializePalmFeatureGroupBox() { + auto ret = new QGroupBox("特征值下发/上报"); + + auto layout = new QFormLayout(); + + auto edit = new QLineEdit(""); + layout->addRow("用户ID:", edit); + + auto button = new QPushButton("特征值下发"); + layout->addRow("", button); + + auto button1 = new QPushButton("特征值上报"); + layout->addRow("", button1); + + ret->setLayout(layout); + + return ret; +} + +void Widget::onSerialConnectButtonClicked() { + auto button = dynamic_cast(sender()); + if (button == nullptr) return; + auto text = button->text(); + if (text == "连接") { + auto portName = m_serialComboBox->currentText(); + m_communication = std::make_shared(); + bool status = m_communication->open(portName); + if (status) { + m_commandGroupBox->setEnabled(true); + button->setText("关闭"); + } + } else if (text == "关闭") { + m_communication.reset(); + m_commandGroupBox->setEnabled(false); + button->setText("连接"); + } +} + +void Widget::onSerialRefreshButtonClicked() { + m_serialComboBox->clear(); + auto ports = QSerialPortInfo::availablePorts(); + for (auto &port : ports) { + m_serialComboBox->addItem(port.portName()); + } +} + +void Widget::onEnrollButtonClicked() { + auto name = m_enrollNameEdit->text(); + auto timeout = m_enrollTimeoutEdit->text().toInt(); + m_communication->enroll(name.toStdString(), timeout); +} + +void Widget::onVerifyButtonClicked() { + if (!m_communication) return; + auto timeout = m_verifyTimeoutEdit->text().toInt(); + m_communication->verify(timeout); +} + +void Widget::onDeleteAllButtonClicked() { + if (!m_communication) return; + m_communication->deleteAll(); +} + +void Widget::onDeleteButtonClicked() { + if (!m_communication) return; + auto id = m_deleteIdEdit->text().toInt(); + m_communication->deleteUser(id); +} diff --git a/Analyser/Widget.h b/Analyser/Widget.h new file mode 100644 index 0000000..a4de317 --- /dev/null +++ b/Analyser/Widget.h @@ -0,0 +1,54 @@ +#ifndef WIDGET_H +#define WIDGET_H + +#include "qgroupbox.h" +#include + +class QPushButton; +class QTextBrowser; +class QComboBox; +class QLineEdit; +class ModuleCommunication; + +class Widget : public QWidget { + Q_OBJECT +public: + explicit Widget(QWidget *parent = nullptr); + void initializeLogger(); + +protected: + QGroupBox *initializeCommandGroupBox(); + void onSerialConnectButtonClicked(); + void onSerialRefreshButtonClicked(); + + void onEnrollButtonClicked(); + void onVerifyButtonClicked(); + void onDeleteAllButtonClicked(); + void onDeleteButtonClicked(); + + QGroupBox *initializeEnrollGroupBox(); + QGroupBox *initializeVerifyGroupBox(); + QGroupBox *initializeDeleteGroupBox(); + QGroupBox *initializePalmFeatureGroupBox(); + +private: + QComboBox *m_serialComboBox = nullptr; + QPushButton *m_serialConnectButton = nullptr; + QTextBrowser *m_logBrowser = nullptr; + QGroupBox *m_commandGroupBox = nullptr; + + QLineEdit *m_enrollNameEdit = nullptr; + QLineEdit *m_enrollTimeoutEdit = nullptr; + QPushButton *m_enrollButton = nullptr; + + QLineEdit *m_verifyTimeoutEdit = nullptr; + QPushButton *m_verifyButton = nullptr; + + QLineEdit *m_deleteIdEdit = nullptr; + QPushButton *m_deleteButton = nullptr; + QPushButton *m_deleteAllButton = nullptr; + + std::shared_ptr m_communication; +}; + +#endif // WIDGET_H diff --git a/Analyser/main.cpp b/Analyser/main.cpp new file mode 100644 index 0000000..4fac0ff --- /dev/null +++ b/Analyser/main.cpp @@ -0,0 +1,21 @@ +#include "BoostLog.h" +#include "Widget.h" +#include +#include + +int main(int argc, char *argv[]) { + boost::log::initialize("logs/app"); + + QApplication a(argc, argv); + QFont font; + font.setPointSize(16); + a.setFont(font); + Widget w; + w.initializeLogger(); + w.setWindowTitle("L015上位机工具"); + w.setMinimumWidth(1120); + w.setMinimumHeight(640); + w.show(); + + return a.exec(); +} diff --git a/CMakeLists.txt b/CMakeLists.txt index dfff420..f6a4fce 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,5 +17,7 @@ add_compile_definitions( add_subdirectory(${Projects_ROOT}/Kylin/Universal Universal) add_subdirectory(${Projects_ROOT}/Kylin/Encrypt Encrypt) +add_subdirectory(${Projects_ROOT}/Kylin/QtComponets QtComponets) +add_subdirectory(Analyser) add_subdirectory(OtaUpdate) diff --git a/OtaUpdate/CMakeLists.txt b/OtaUpdate/CMakeLists.txt index 31f1d4a..b2887dc 100644 --- a/OtaUpdate/CMakeLists.txt +++ b/OtaUpdate/CMakeLists.txt @@ -4,7 +4,7 @@ set(CMAKE_AUTORCC ON) find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets SerialPort) find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets SerialPort) -set(PROJECT_SOURCES +set(PROJECT_SOURCES OtaUpdate.rc main.cpp CdcUpdater.h CdcUpdater.cpp DeviceDiscovery.h DeviceDiscovery.cpp diff --git a/OtaUpdate/CdcUpdater.h b/OtaUpdate/CdcUpdater.h index 070fbd7..1e146e2 100644 --- a/OtaUpdate/CdcUpdater.h +++ b/OtaUpdate/CdcUpdater.h @@ -47,8 +47,6 @@ protected: bool write(Command command, const uint8_t *data = nullptr, uint32_t size = 0); void timerEvent(QTimerEvent *event) final; void transferBin(); - -protected slots: void onReadyRead(); private: diff --git a/OtaUpdate/OtaUpdate.rc b/OtaUpdate/OtaUpdate.rc new file mode 100644 index 0000000..88ed4a7 --- /dev/null +++ b/OtaUpdate/OtaUpdate.rc @@ -0,0 +1,61 @@ +// Microsoft Visual C++ generated resource script. +// + + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// 中文(简体,中国) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_CHS) +LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED +#pragma code_page(936) + +#ifdef APSTUDIO_INVOKED + ///////////////////////////////////////////////////////////////////////////// + // + // TEXTINCLUDE + // + + 2 TEXTINCLUDE BEGIN "#include " + "winres.h" + "\r\n" + "\0" END + + 3 TEXTINCLUDE BEGIN "\r\n" + "\0" END + +#endif // APSTUDIO_INVOKED + + ///////////////////////////////////////////////////////////////////////////// + // + // Icon + // + + // Icon with lowest ID value placed first to ensure application icon + // remains consistent on all systems. + IDI_ICON1 ICON "..\\resources\\logo.ico" + +#endif // 中文(简体,中国) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + diff --git a/Readme.md b/Readme.md index 078123a..18d03b7 100644 --- a/Readme.md +++ b/Readme.md @@ -14,3 +14,313 @@ + + +## 模组串口通讯协议 + +### 串口配置 + +- 波特率:115200 +- 数据位:8 +- 停止位:1 +- 奇偶检验:无 +- 流控制:无 + +### 消息格式 + +主控和模块通讯的基本格式如下表所示,字节序为 **大端字节序(Big Endian)**: + +| SyncWord | MsgID | DataSize | Data | ParityCheck | +| -------- | ------ | -------- | ------- | ----------- | +| 2 bytes | 1 byte | 2 bytes | N bytes | 1 byte | + +下表是对上述各个字段的详细说明: + +| 字段 | 长度 | 说明 | +| ----------- | ------- | ------------------------------------------------------------ | +| SyncWord | 2 bytes | 固定的消息开头同步字:0xEF 0xAA | +| MsgID | 1 byte | 消息ID(例如 MID_VERIFY) | +| DataSize | 2 bytes | Data数据的长度,0 ≤ size ≤ 65535 | +| Data | N bytes | 消息(MsgID)对应的数据内容,长度 N 为 DataSize 。
0表示此消息无参数 | +| ParityCheck | 1 byte | 协议的奇偶检验码。
去除SyncWord,对 MsgID、DataSize、Data 的内容字节做XOR运算 | + +### 消息列表 + +| MsgID | Code | 说明 | +| ----------------------- | ---- | ------------------------------------------------------------ | +| MID_REPLY | 0x00 | 模组对主控发送出的命令的应答,对于主控下发的每条命令,模组最终都会使用 MID_REPLY 进行结果应答上报 | +| MID_NOTE | 0x01 | 摸组主动上报给主控的信息,根据 NID 判断消息类型和对应的 Data 结构(详细内容见下文) | +| MID_VERIFY | 0x12 | 掌静脉识别比对 | +| MID_ENROLL_SINGLE | 0x1D | 掌静脉录入(单帧) | +| MID_DELUSER | 0x20 | 删除一个已注册的掌静脉 | +| MID_DELALL | 0x21 | 删除所有已注册的掌静脉 | +| MID_ENROLL_PALM_FEATUTE | 0xF9 | 主控下发掌静脉特征值给模组进行注册 | +| MID_GET_PALM_FEATUTE | 0xFA | 主控请求获取指定用户掌静脉特征值 | + +#### 设备初始化完成 + +模组上电初始化完成后,会通过串口向主控发送 NID_READY 报文: 0xEF 0xAA 0x01 0x00 0x01 0x00 0x00。(详细解释见 `模组状态上报(MID_NOTE)`)。 + +主机在接收到握手信号后,可以和模组进行指令交互。 + +#### 掌静脉识别(MID_VERIFY) + +主控下发该指令给模组,模组开始识别掌静脉进行比对,指令下发携带的参数 msg_verify_data 定义如下: + +```c++ +struct msg_verify_data { + uint8_t pd_rightaway; + uint8_t timeout; +}; +``` + +参数说明: + +- pd_rightaway:表示是否在识别完成后立刻关机(预留参数),默认设 00。 +- timeout:识别超时时间,默认为10s,用户可以自行设置(最大不超过255s)。**主控等待模组录入应答的超时时间应大于此参数设置值。** + +识别结果 Result 确认码的返回值见 `命令结果上报(MID_REPLY)`。 + +模组掌静脉识别成功后,通过消息 MID_REPLY 返回的数据 ResultData 定义如下: + +```c++ +struct VerifyDataReply { + uint16_t user_id; + uint8_t username[32]; + uint8_t admin; + uint8_t unlockStatus; +}; +``` + +参数说明: + +- user_id:识别成功的用户 ID +- username:识别成功的用户名字 +- admin:识别成功的用户是否为管理员用户 +- unlockStatus:保留参数,未使用 + +#### 掌静脉注册(MID_ENROLL_SINGLE) + +掌静脉注册指令下发携带的参数 `msg_enroll_data` 定义如下: + +```c++ +struct msg_enroll_data { + uint8_t admin; + uint8_t username[32]; + uint8_t palm_direction; + uint8_t timeout; +}; +``` + +参数说明: + +- admin:设置该录入的掌静脉人员为管理员。 +- username:录入用户的用户名。 +- palm_direction:保留,暂未使用。可设置为 0x00。 +- timeout:录入过程的超时时间(s),默认为10s,用户可以自行设置(最大不超过255s)。**主控等待模组录入应答的超时时间应大于此参数设置值。** + +录入结果 Result 确认码的返回值见 `命令结果上报(MID_REPLY)`。 + +模组掌静脉录入成功后,通过消息 MID_REPLY 返回的数据 ResultData 定义如下: + +```c++ +struct msg_reply_enroll_data { + uint16_t user_id; + uint8_t face_direction; // depleted, user ignore this field +}; +``` + +参数说明: + +- user_id:用户 ID 每次录入都会增加 1,即使删除某个 ID 后,再次录入 ID 会继续增加,直至 65530,ID 变为 0。 +- face_direction:保留,暂未使用。 + +#### 删除单个掌静脉(MID_DELUSER) + +通过传入用户 ID, 删除指定 ID 的单个用户。 + +删除单个用户指令携带参数 msg_deluser_data 定义如下: + +``` +struct msg_deluser_data { + uint16_t user_id; +}; +``` + +参数说明: + +- user_id:需要删除的用户 ID。 + +指令执行结束后,通过消息 MID_REPLY 返回结果(主控等待应答超时时间建议设置为 5s): + +- 0x00(MR_SUCCESS):删除成功 +- 0x05(MR_FAILED4_UNKNOWNREASON):未知错误 +- 0x08(MR_FAILED4_UNKNOWNUSER):删除的用户ID不存在 + +#### 删除所有掌静脉(MID_DELALL) + +删除所有已注册的用户。 + +该指令无需携带参数。 + +指令执行结束后,通过消息 MID_REPLY 返回结果(主控等待应答超时时间建议设置为 5s): + +- 0x00(MR_SUCCESS):删除成功 +- 0x05(MR_FAILED4_UNKNOWNREASON):未知错误 + +#### 掌静脉特征值注册(MID_ENROLL_PALM_FEATUTE) + +该命令用于直接将掌静脉特征值下发给模组进行注册。 + +下发掌静脉特征值注册携带的参数 msg_palm_feature_enroll_data 如下: + +```c++ +struct msg_palm_feature_enroll_data { + uint16_t user_id; + uint8_t username[32]; + uint8_t admin; + uint8_t feature_data_md5[16]; + uint16_t feature_total_size; + uint8_t feature_data[feature_total_size]; +}; +``` + +参数说明: + +- user_id:用户ID,下发掌静脉特征值时,由用户设置。 +- username:录入用户的用户名。 +- admin:设置该录入的掌静脉人员为管理员。 +- feature_data_md5:特征值 feature_data 的 md5 校验值 +- feature_total_size:特征值 feature_data 的长度 +- feature_data:掌静脉特征值数据 + +指令执行结束后,通过消息 MID_REPLY 返回结果(主控等待应答超时时间建议设置为 5s): + +- 0x00(MR_SUCCESS):注册成功 +- 0x05(MR_FAILED4_UNKNOWNREASON):未知错误 + +#### 获取掌静脉特征值(MID_GET_PALM_FEATUTE) + +该命令用于获取对应已注册用户的掌静脉特征值,携带参数 msg_get_palm_feature_data 如下: + +```c++ +struct msg_get_palm_feature_data { + uint16_t user_id; +} +``` + +录入结果 Result 确认码的返回值见 `命令结果上报(MID_REPLY)`。 + +获取掌静脉特征值任务成功后,通过消息 MID_REPLY 返回的数据 ResultData 定义如下: + +```c++ +struct msg_palm_feature_enroll_data { + uint16_t user_id; + uint8_t username[32]; + uint8_t admin; + uint8_t feature_data_md5[16]; + uint16_t feature_total_size; + uint8_t feature_data[feature_total_size]; +}; +``` + +参数定义见 `掌静脉特征值注册(MID_ENROLL_PALM_FEATUTE)` 携带参数的参数定义说明。 + +#### 命令结果上报(MID_REPLY) + +模组向主控发送的 MID_NOTE 消息的完整协议如下所示: + + + + + + + + + + + + + + + + + + + + + + + + + +
SyncWordMsgIDDataSizeDataParityCheck
2 bytes1 byte2 bytesN bytes1 byte
0xEF 0xAAMID_REPLY(0x00) RID(1 byte)Result(1 byte)ResultData( N-2 bytes)
+ +RID表示模组当前正在处理的任务,例如当 RID 为 MID_ENROLL_SINGLE 是,表示该消息是模组处理完单帧录入任务后回复的消息。 + +消息对应的 ResultData 会在主控下发命令(例如 MID_ENROLL_SINGLE )中进行介绍。 + +Result 表示该命令的最终执行结果,详细如下表所示。 + +| Result | Code | 说明 | +| ------------------------ | ---- | ------------------- | +| MR_SUCCESS | 0 | 指令执行成功 | +| MR_REJECTED | 1 | 模组拒绝该命令 | +| MR_ABORTED | 2 | 录入/解锁算法已终止 | +| MR_FAILED4_CAMERA | 4 | 相机打开失败 | +| MR_FAILED4_UNKNOWNREASON | 5 | 未知错误 | +| MR_FAILED4_INVALIDPARAM | 6 | 无效的参数 | +| MR_FAILED4_NOMEMORY | 7 | 内存不足 | +| MR_FAILED4_UNKNOWNUSER | 8 | 未录入的用户 | +| MR_FAILED4_MAXUSER | 9 | 录入超过最大数量 | +| MR_FAILED4_PALMENROLLED | 10 | 掌静脉已录入 | +| MR_FAILED4_LIVENESSCHECK | 12 | 活体检测失败 | +| MR_FAILED4_TIMEOUT | 13 | 录入或解锁超时 | +| MR_FAILED4_AUTHORIZATION | 14 | 加密芯片授权失败 | +| MR_FAILED4_READ_FILE | 19 | 读文件失败 | +| MR_FAILED4_WRITE_FILE | 20 | 写文件失败 | +| MR_FAILED4_NO_ENCRYPT | 21 | 未采用加密通讯 | + + + +#### 模组状态上报(MID_NOTE) + +MID_NOTE 消息主要作用是主动向主控上报一些信息,报文格式如下: + + + + + + + + + + + + + + + + + + + + + + + + +
SyncWordMsgIDDataSizeDataParityCheck
2 bytes1 byte2 bytesN bytes1 byte
0xEF 0xAAMID_NOTE(0x01) NID(1 byte)NoteData( N-1 bytes)
+ +目前NID主要内容如下: + +| NID(*表示该消息携带 NoteData 数据) | Code | 说明 | +| ------------------------------------ | ---- | -------------------- | +| NID_READY | 0x00 | 模组已上电初始化成功 | + +### 示例报文 + + + + + diff --git a/resources/logo.ico b/resources/logo.ico new file mode 100644 index 0000000000000000000000000000000000000000..ce91dac18d2f676574ed53d1ec76e4f70157053e GIT binary patch literal 1150 zcmb7AJxjzu5PgTDou&}sDzDQ*#9mOavG!m13pSk1`4?;jy*e=xKZ-RPE(rF5Tw!5h zD`;hP7>%=8=CZlnQ*XoU?7VsJ?PP%leVzweE$mGLi*#C{P*97aQSNl0bw8&=SMI$R zM?1H1P;m77;f$MUqgfn#N|#+mY6RTLv!<$T<_MDby9()DL>r@go?}E&m2$rcDc%?` z`Dq*;>N-5)zLljYNMD=r<~(?0pEM4xEDZxv9G6<{@y+nRGv6)6tKpMRtY<;xr~k>W zpP5=_?$uKj%RZHd{oZDe`Q!4ncpBdGhc52`pBY6L7*IZ;x?2Ft6zlWA<`l3s Tp!gzO1)U#0fR|YyUjlvrDSG;= literal 0 HcmV?d00001 diff --git a/resources/logo.png b/resources/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f77639fbb7d364ce04d2656a9d83fd7169c6e7e6 GIT binary patch literal 3353 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 jjb=o%_;&#Z07U--W{Z literal 0 HcmV?d00001