Compare commits
91 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
a2764a9ade | ||
943fb6fb4b | |||
|
ce673bf330 | ||
|
28f2f60532 | ||
|
c846ae81a0 | ||
|
fd72d00925 | ||
|
8055f4e70f | ||
5ae5c5adc6 | |||
5b602e607c | |||
c3430caf2f | |||
0aa2721f67 | |||
|
6ee75bfae1 | ||
|
404889a19c | ||
|
881048a286 | ||
|
a6541c6a52 | ||
|
59d0a475f4 | ||
|
6f1aad0bac | ||
1c92079320 | |||
2a69cdc84b | |||
14dd4c8ee6 | |||
e9ec8ece00 | |||
4f9919a931 | |||
0e1e76a3a5 | |||
8fa26363b7 | |||
36c0a0226a | |||
|
e518f0841e | ||
|
2661f0729a | ||
9c8194dec2 | |||
f5f6569051 | |||
f968a0a242 | |||
e55983b41f | |||
ba63b6b491 | |||
4788f1c04d | |||
e3d537951c | |||
f0cbe30768 | |||
800369d7a3 | |||
10aea27972 | |||
a57dc9a11b | |||
fd97a1f727 | |||
9fb565d310 | |||
dd25ec9aa4 | |||
|
34294c5c85 | ||
9ec600a0c3 | |||
57a438b4d4 | |||
b8830003e8 | |||
059a3f6de6 | |||
8abee988e7 | |||
e184bf32e7 | |||
8f50360ea9 | |||
a6fbdc24e9 | |||
edfc2a4e82 | |||
20fe4de46c | |||
e1e36b3bad | |||
149ae2a212 | |||
7c3dbcedfe | |||
4d2affb862 | |||
2d78ddbd04 | |||
1ce4afdc25 | |||
|
49499357b5 | ||
|
1731ea445f | ||
|
699a9150c0 | ||
|
9ed27e2c37 | ||
f74081a06a | |||
|
2c06e4d1e0 | ||
|
36a5877a0c | ||
239cb2d10f | |||
|
c7c24e9388 | ||
|
5e24a831ae | ||
|
1277ef7495 | ||
|
bdc7711bbb | ||
|
a2a79ac084 | ||
|
219c68ea45 | ||
|
928326efb2 | ||
|
9d495d487d | ||
|
2610c40189 | ||
|
c6554704ac | ||
|
5009528f3a | ||
|
5c2440c32c | ||
|
59281e3a75 | ||
|
e588d4abdf | ||
|
ec34bd50f6 | ||
|
a92e6948c3 | ||
|
809bca3e91 | ||
|
bd97f3a380 | ||
|
d9a9815a89 | ||
|
6e11d190c0 | ||
|
174e22ea79 | ||
|
4724af4086 | ||
|
4febb5fb7f | ||
|
48ebf2be92 | ||
09cb96f97b |
44
.gitea/workflows/build_ubuntu.yaml
Normal file
44
.gitea/workflows/build_ubuntu.yaml
Normal file
@ -0,0 +1,44 @@
|
||||
name: Build Applications
|
||||
run-name: ${{ github.actor }} is building SmartLockerTools...
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
tags-ignore:
|
||||
- 'v*'
|
||||
paths:
|
||||
- '**.cpp'
|
||||
- '**.h'
|
||||
- '**.conf'
|
||||
jobs:
|
||||
Build:
|
||||
runs-on: [ubuntu-latest, ubuntu-24.04]
|
||||
container:
|
||||
image: registry.cn-shenzhen.aliyuncs.com/amass_toolset/ubuntu_dev:24.04
|
||||
credentials:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
steps:
|
||||
- name: Set up SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh/
|
||||
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
ssh-keyscan -t ed25519 -p 22022 frp-by1.wwvvww.cn >> ~/.ssh/known_hosts
|
||||
- name: Clone repository
|
||||
run: |
|
||||
echo "git clone --depth 1 --branch=${GITHUB_REF##*/} ssh://git@frp-by1.wwvvww.cn:22022/${{ gitea.repository }}.git"
|
||||
git clone --depth 1 --branch=${GITHUB_REF##*/} ssh://git@frp-by1.wwvvww.cn:22022/${{ gitea.repository }}.git .
|
||||
git checkout ${GITHUB_SHA}
|
||||
- run: resources/build.sh build
|
||||
- run: resources/build.sh deploy
|
||||
- name: Notify
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
echo "仓库名: ${{ github.repository }}" >> notify.tpl
|
||||
echo "构建状态: ${{ job.status }}">> notify.tpl
|
||||
echo "构建地址: https://amass.fun/gitea/${{ github.repository }}/actions/runs/${{ github.run_number }}">> notify.tpl
|
||||
echo "仓库地址: https://amass.fun/gitea/${{ github.repository }}">> notify.tpl
|
||||
echo "提交ID: $(git rev-parse --short HEAD)">> notify.tpl
|
||||
echo -n "提交消息: ${{ github.event.head_commit.message }}">> notify.tpl
|
||||
cat notify.tpl | envsubst | jq -sR . | xargs -0 -I {} curl -H "Content-Type: application/json" -X POST -d '{"type":"text","msg":{} }' https://amass.fun/notify
|
39
.gitea/workflows/build_windows.yaml
Normal file
39
.gitea/workflows/build_windows.yaml
Normal file
@ -0,0 +1,39 @@
|
||||
name: Windows CI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
tags-ignore:
|
||||
- 'v*'
|
||||
paths:
|
||||
- '**.cpp'
|
||||
- '**.h'
|
||||
- '**.conf'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: [windows11]
|
||||
steps:
|
||||
- name: Clone repository
|
||||
run: |
|
||||
$ref = "${{ github.ref }}"
|
||||
if ($ref -like 'refs/heads/*') {
|
||||
$branch = $ref -replace '^refs/heads/', ''
|
||||
} elseif ($ref -like 'refs/tags/*') {
|
||||
$branch = $ref -replace '^refs/tags/', ''
|
||||
} else {
|
||||
$branch = $ref
|
||||
}
|
||||
git clone --depth 1 --branch=$branch https://${{ secrets.GIT_USERNAME }}:${{ secrets.GIT_PASSWORD }}@amass.fun/gitea/${{ gitea.repository }}.git .
|
||||
git checkout ${{ github.sha }}
|
||||
- name: Build and deploy
|
||||
run: |
|
||||
resources/build.ps1 build
|
||||
resources/build.ps1 deploy
|
||||
resources/build.ps1 changelog
|
||||
- name: Upload Gitea Release
|
||||
uses: akkuman/gitea-release-action@v1
|
||||
with:
|
||||
body_path: build/CHANGELOG.txt
|
||||
files: |-
|
||||
build/掌静脉工具v*.zip
|
42
.gitea/workflows/release_ubuntu.yaml
Normal file
42
.gitea/workflows/release_ubuntu.yaml
Normal file
@ -0,0 +1,42 @@
|
||||
name: Deploy Release
|
||||
run-name: ${{ github.actor }} is building SmartLockerTools...
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
jobs:
|
||||
Build:
|
||||
runs-on: [ubuntu-latest, ubuntu-24.04]
|
||||
container:
|
||||
image: registry.cn-shenzhen.aliyuncs.com/amass_toolset/ubuntu_dev:24.04
|
||||
credentials:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
steps:
|
||||
- name: Set up SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh/
|
||||
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
ssh-keyscan -t ed25519 -p 22022 frp-by1.wwvvww.cn >> ~/.ssh/known_hosts
|
||||
- name: Clone repository
|
||||
run: |
|
||||
echo "git clone --depth 1 --branch=${GITHUB_REF##*/} ssh://git@frp-by1.wwvvww.cn:22022/${{ gitea.repository }}.git"
|
||||
git clone --depth 1 --branch=${GITHUB_REF##*/} ssh://git@frp-by1.wwvvww.cn:22022/${{ gitea.repository }}.git .
|
||||
git checkout ${GITHUB_SHA}
|
||||
- run: resources/build.sh build
|
||||
- run: resources/build.sh deploy
|
||||
- name: Generate Changelog
|
||||
run: |
|
||||
current_tag=$(git describe --tags --abbrev=0)
|
||||
previous_tag=$(git describe --tags --abbrev=0 $(git rev-list --tags --skip=1 --max-count=1))
|
||||
echo "Commits from ${previous_tag} to ${current_tag}:"
|
||||
git log ${previous_tag}..${current_tag} --reverse --pretty=format:"%B" | nl -w2 -s". "
|
||||
git log ${previous_tag}..${current_tag} --reverse --pretty=format:"%B" | nl -w2 -s". " > ${{ github.workspace }}-CHANGELOG.txt
|
||||
- name: Upload Gitea Release
|
||||
uses: akkuman/gitea-release-action@v1
|
||||
with:
|
||||
body_path: ${{ github.workspace }}-CHANGELOG.txt
|
||||
files: |-
|
||||
build/SmartLockerTools-0.3-Linux.sh
|
||||
|
33
.gitea/workflows/release_windows.yaml
Normal file
33
.gitea/workflows/release_windows.yaml
Normal file
@ -0,0 +1,33 @@
|
||||
name: Release tag
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: [windows11]
|
||||
steps:
|
||||
- name: Clone repository
|
||||
run: |
|
||||
$ref = "${{ github.ref }}"
|
||||
if ($ref -like 'refs/heads/*') {
|
||||
$branch = $ref -replace '^refs/heads/', ''
|
||||
} elseif ($ref -like 'refs/tags/*') {
|
||||
$branch = $ref -replace '^refs/tags/', ''
|
||||
} else {
|
||||
$branch = $ref
|
||||
}
|
||||
git clone --depth 1 --branch=$branch https://${{ secrets.GIT_USERNAME }}:${{ secrets.GIT_PASSWORD }}@amass.fun/gitea/${{ gitea.repository }}.git .
|
||||
git checkout ${{ github.sha }}
|
||||
- name: Build and deploy
|
||||
run: |
|
||||
resources/build.ps1 build
|
||||
resources/build.ps1 deploy
|
||||
resources/build.ps1 changelog
|
||||
- name: Upload Gitea Release
|
||||
uses: akkuman/gitea-release-action@v1
|
||||
with:
|
||||
body_path: build/CHANGELOG.txt
|
||||
files: |-
|
||||
build/掌静脉工具v*.zip
|
508
Analyser/Application.cpp
Normal file
508
Analyser/Application.cpp
Normal file
@ -0,0 +1,508 @@
|
||||
#include "Application.h"
|
||||
#include "AsyncEvent.h"
|
||||
#include "BoostLog.h"
|
||||
#include "CategoryLogSinkBackend.h"
|
||||
#include "CdcUpdater.h"
|
||||
#include "Configuration.h"
|
||||
#include "Database.h"
|
||||
#include "DateTime.h"
|
||||
#include "DeviceDiscovery.h"
|
||||
#include "ImageDecoder.h"
|
||||
#include "StringUtility.h"
|
||||
#include "VideoFrameProvider.h"
|
||||
#include "VideoPlayer.h"
|
||||
#include <QFile>
|
||||
#include <QFont>
|
||||
#include <QGuiApplication>
|
||||
#include <QImage>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QSerialPortInfo>
|
||||
#include <QTimer>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <mbedtls/md5.h>
|
||||
|
||||
constexpr uint32_t ImageSliceSize = (4000 - 32);
|
||||
|
||||
Application::Application(int &argc, char **argv) : m_app(std::make_shared<QGuiApplication>(argc, argv)) {
|
||||
m_app->setApplicationName(APPLICATION_NAME);
|
||||
m_app->setApplicationVersion(QString("v%1_%2 build: %3 %4").arg(APP_VERSION, GIT_COMMIT_ID, __DATE__, __TIME__));
|
||||
QFont font;
|
||||
font.setPointSize(12);
|
||||
m_app->setFont(font);
|
||||
|
||||
m_verifyTimer = new QTimer(this);
|
||||
m_verifyTimer->setSingleShot(true);
|
||||
connect(m_verifyTimer, &QTimer::timeout, this, &Application::onVerifyTimeout);
|
||||
|
||||
m_database = std::make_shared<Database>();
|
||||
QTimer::singleShot(0, this, [this]() {
|
||||
if (!m_database->open("database.db")) {
|
||||
LOG(error) << "open database failed.";
|
||||
}
|
||||
});
|
||||
|
||||
m_videoFrameProvider = new VideoFrameProvider();
|
||||
if (!std::filesystem::exists(JpgPath)) {
|
||||
std::filesystem::create_directory(JpgPath);
|
||||
}
|
||||
if (!std::filesystem::exists(YuvPath)) {
|
||||
std::filesystem::create_directory(YuvPath);
|
||||
}
|
||||
}
|
||||
|
||||
void Application::onNewEnrollResult(uint16_t userid) {
|
||||
m_palmId = userid;
|
||||
QTimer::singleShot(0, this, [this, userid]() {
|
||||
emit newStatusTip(Info, "录入成功", QString("用户: %1, ID: %2").arg(m_palmUsername).arg(userid));
|
||||
});
|
||||
}
|
||||
|
||||
void Application::onNewVerifyResult(uint16_t userid, const QString &username, uint16_t elapsed) {
|
||||
m_palmUsername = username;
|
||||
m_palmId = userid;
|
||||
QTimer::singleShot(0, this, [this, userid, username, elapsed]() {
|
||||
emit newStatusTip(Info, QString("%1,识别耗时: %2ms")
|
||||
.arg(userid == ModuleCommunication::InvalidUserId ? "未录入用户" : username)
|
||||
.arg(elapsed));
|
||||
});
|
||||
}
|
||||
|
||||
void Application::initializeLogger() {
|
||||
auto backend = boost::make_shared<CategoryLogSinkBackend>("GUI", [this](const std::string &log) {
|
||||
Amass::executeAtObjectThread(this, [this, log]() { emit newLog(QString::fromStdString(log)); });
|
||||
});
|
||||
using SynchronousCategorySink = boost::log::sinks::synchronous_sink<CategoryLogSinkBackend>;
|
||||
auto sink = boost::make_shared<SynchronousCategorySink>(backend);
|
||||
boost::log::core::get()->add_sink(sink);
|
||||
}
|
||||
|
||||
int Application::exec() {
|
||||
QQmlApplicationEngine engine;
|
||||
engine.addImageProvider("videoframe", m_videoFrameProvider);
|
||||
const QUrl url(QStringLiteral("qrc:/qt/qml/Analyser/qml/Main.qml"));
|
||||
QObject::connect(
|
||||
&engine, &QQmlApplicationEngine::objectCreationFailed, m_app.get(), []() { QCoreApplication::exit(-1); },
|
||||
Qt::QueuedConnection);
|
||||
engine.load(url);
|
||||
|
||||
return m_app->exec();
|
||||
}
|
||||
|
||||
Application *Application::create(QQmlEngine *qmlEngine, QJSEngine *jsEngine) {
|
||||
Application *ret = nullptr;
|
||||
auto app = Amass::Singleton<Application>::instance();
|
||||
if (app) {
|
||||
ret = app.get();
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
QJSEngine::setObjectOwnership(ret, QJSEngine::CppOwnership);
|
||||
#endif
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
QStringList Application::availableSerialPorts() const {
|
||||
QStringList ret;
|
||||
auto ports = QSerialPortInfo::availablePorts();
|
||||
auto iterator = std::find_if(ports.cbegin(), ports.cend(), [](const QSerialPortInfo &info) {
|
||||
return (info.productIdentifier() == ModuleCommunication::ProductIdentifier) &&
|
||||
(info.vendorIdentifier() == ModuleCommunication::VendorIdentifier);
|
||||
});
|
||||
if (iterator != ports.cend()) {
|
||||
ret << iterator->portName();
|
||||
ports.erase(iterator);
|
||||
}
|
||||
for (auto &port : ports) {
|
||||
if (port.description() == "蓝牙链接上的标准串行") continue;
|
||||
ret << port.portName();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
QVariantList Application::availableUsbVideoCameras() const {
|
||||
QVariantList ret;
|
||||
DeviceDiscovery d;
|
||||
auto devices = d.devices();
|
||||
for (auto &device : devices) {
|
||||
QVariantMap item;
|
||||
item.insert("name", QString::fromStdString(device.friendlyName));
|
||||
item.insert("path", QString::fromStdString(device.alternativeName));
|
||||
ret << item;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool Application::open(const QString &portName, int baudRate) {
|
||||
m_communication = std::make_shared<ModuleCommunication>();
|
||||
connect(m_communication.get(), &ModuleCommunication::commandStarted, this, &Application::onCommandStarted);
|
||||
connect(m_communication.get(), &ModuleCommunication::commandFinished, this, &Application::onCommandFinished);
|
||||
connect(m_communication.get(), &ModuleCommunication::newEnrollResult, this, &Application::onNewEnrollResult);
|
||||
connect(m_communication.get(), &ModuleCommunication::newVerifyResult, this, &Application::onNewVerifyResult);
|
||||
connect(m_communication.get(), &ModuleCommunication::newPalmFeature, this, &Application::onNewPalmFeature);
|
||||
connect(m_communication.get(), &ModuleCommunication::newImageInfo, this, &Application::onNewImageInfo);
|
||||
connect(m_communication.get(), &ModuleCommunication::newImageSliceData, this, &Application::onNewImageSliceData);
|
||||
connect(m_communication.get(), &ModuleCommunication::errorOccurred, this, &Application::onErrorOccurred);
|
||||
emit connectedChanged();
|
||||
|
||||
auto status = m_communication->open(portName, baudRate);
|
||||
emit newStatusTip(status ? Tip : Error, status ? "串口打开成功" : "串口打开失败");
|
||||
if (status) {
|
||||
QTimer::singleShot(0, this, [this]() { m_communication->requestVersion(); });
|
||||
QTimer::singleShot(0, this, [this]() { m_communication->requestDebugSettings(); });
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
bool Application::openUVC(const QString &deviceName) {
|
||||
m_videoPlayer = std::make_shared<VideoPlayer>();
|
||||
m_videoPlayer->setFrameCallback([this](const QImage &image) {
|
||||
Amass::executeAtObjectThread(this, [this, image]() {
|
||||
m_videoFrameProvider->setImage(image);
|
||||
emit newVideoFrame();
|
||||
});
|
||||
});
|
||||
m_videoPlayer->setErrorCallback([this](int error, const std::string &message) {
|
||||
LOG_CAT(info, GUI) << "UVC错误: " << message;
|
||||
Amass::executeAtObjectThread(this, [this, error]() {
|
||||
if (error == -EIO) {
|
||||
closeUVC();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
bool status = m_videoPlayer->open(deviceName.toStdString());
|
||||
if (!status) {
|
||||
m_videoPlayer.reset();
|
||||
}
|
||||
emit uvcOpenedChanged();
|
||||
emit newStatusTip(status ? Tip : Error, status ? "UVC打开成功" : "UVC打开失败");
|
||||
return status;
|
||||
}
|
||||
|
||||
void Application::resetModule() {
|
||||
if (connected()) {
|
||||
m_communication->reset();
|
||||
}
|
||||
m_imageUploadling = false;
|
||||
m_persistenceModeStarted = false;
|
||||
if (m_verifyTimer->isActive()) {
|
||||
m_verifyTimer->stop();
|
||||
}
|
||||
emit currentMessageIdChanged();
|
||||
}
|
||||
|
||||
void Application::close() {
|
||||
resetModule();
|
||||
m_communication.reset();
|
||||
emit connectedChanged();
|
||||
}
|
||||
|
||||
void Application::closeUVC() {
|
||||
m_videoFrameProvider->reset();
|
||||
emit newVideoFrame();
|
||||
|
||||
m_videoPlayer.reset();
|
||||
emit uvcOpenedChanged();
|
||||
}
|
||||
|
||||
void Application::verify(bool captureImage, uint8_t timeout) {
|
||||
if (m_communication->currentMessageId() != ModuleCommunication::Idle) {
|
||||
m_communication->reset();
|
||||
}
|
||||
if (captureImage) {
|
||||
m_communication->verifyExtended(captureImage, timeout);
|
||||
} else {
|
||||
m_communication->verify(timeout);
|
||||
}
|
||||
m_verifyExtendedMode = captureImage;
|
||||
}
|
||||
|
||||
void Application::enroll(const QString &username, bool strictMode, uint8_t excludeMode, bool persistence,
|
||||
uint8_t timeout) {
|
||||
if (m_communication->currentMessageId() != ModuleCommunication::Idle) {
|
||||
m_communication->reset();
|
||||
}
|
||||
m_communication->enroll(username.toStdString(), strictMode, excludeMode, persistence, timeout);
|
||||
m_palmUsername = username;
|
||||
}
|
||||
|
||||
void Application::deleteUser(uint16_t userid) {
|
||||
if (m_communication->currentMessageId() != ModuleCommunication::Idle) {
|
||||
m_communication->reset();
|
||||
}
|
||||
m_communication->deleteUser(userid);
|
||||
}
|
||||
|
||||
void Application::deleteAll() {
|
||||
if (m_communication->currentMessageId() != ModuleCommunication::Idle) {
|
||||
m_communication->reset();
|
||||
}
|
||||
m_communication->deleteAll();
|
||||
}
|
||||
|
||||
void Application::enrollExtended(const QString &username, bool strictMode, uint8_t excludeMode, bool persistence,
|
||||
uint8_t timeout) {
|
||||
if (m_communication->currentMessageId() != ModuleCommunication::Idle) {
|
||||
m_communication->reset();
|
||||
}
|
||||
m_communication->enrollExtended(username.toStdString(), strictMode, excludeMode, persistence, timeout);
|
||||
m_palmUsername = username;
|
||||
}
|
||||
|
||||
void Application::uploadImage(const QString &path, const QString &username, int operation) {
|
||||
m_uploadImageSendedSize = 0;
|
||||
ModuleCommunication::UploadImageInformation request;
|
||||
if (path.endsWith(".jpg")) {
|
||||
auto image = ImageDecoder::extractJpegYComponent(Amass::StringUtility::UTF8ToGBK(path.toStdString()));
|
||||
if (!image) {
|
||||
LOG(error) << "decode failed.";
|
||||
return;
|
||||
}
|
||||
m_uploadBuffer = image->data;
|
||||
LOG(info) << path.toStdString() << ", width: " << image->width << ", height: " << image->height;
|
||||
request.width = image->width;
|
||||
request.height = image->height;
|
||||
} else if (path.endsWith(".yuv")) {
|
||||
std::ifstream ifs(Amass::StringUtility::UTF8ToGBK(path.toStdString()), std::ofstream::binary);
|
||||
m_uploadBuffer = std::vector<uint8_t>((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>());
|
||||
if (m_uploadBuffer.empty()) {
|
||||
LOG(error) << "file is empty.";
|
||||
}
|
||||
request.width = 600;
|
||||
request.height = 800;
|
||||
} else {
|
||||
LOG(error) << "not supported format.";
|
||||
return;
|
||||
}
|
||||
|
||||
mbedtls_md5_context context;
|
||||
mbedtls_md5_init(&context);
|
||||
mbedtls_md5_starts(&context);
|
||||
mbedtls_md5_update(&context, m_uploadBuffer.data(), m_uploadBuffer.size());
|
||||
mbedtls_md5_finish(&context, request.md5);
|
||||
mbedtls_md5_free(&context);
|
||||
|
||||
request.operation = operation;
|
||||
request.size = m_uploadBuffer.size();
|
||||
strncpy(request.username, username.toStdString().c_str(), sizeof(request.username));
|
||||
m_communication->uploadImageInfo(request);
|
||||
LOG(info) << "upload image, md5: "
|
||||
<< ModuleCommunication::protocolDataFormatString(request.md5, sizeof(request.md5));
|
||||
m_imageUploadling = true;
|
||||
m_uploadPath = path;
|
||||
m_uploadUsername = username;
|
||||
m_currentUploadOperation = operation;
|
||||
m_startUploadTime = std::chrono::system_clock::now();
|
||||
}
|
||||
|
||||
ModuleCommunication *Application::module() const {
|
||||
return m_communication.get();
|
||||
}
|
||||
|
||||
bool Application::connected() const {
|
||||
return static_cast<bool>(m_communication);
|
||||
}
|
||||
|
||||
bool Application::uvcOpened() const {
|
||||
return static_cast<bool>(m_videoPlayer);
|
||||
}
|
||||
|
||||
ModuleCommunication::MessageId Application::persistenceCommand() const {
|
||||
return m_persistenceCommand;
|
||||
}
|
||||
|
||||
void Application::setPersistenceCommand(ModuleCommunication::MessageId command) {
|
||||
if (m_persistenceCommand != command) {
|
||||
m_persistenceCommand = command;
|
||||
emit persistenceCommandChanged();
|
||||
}
|
||||
}
|
||||
|
||||
ModuleCommunication::MessageId Application::currentMessageId() const {
|
||||
ModuleCommunication::MessageId ret = ModuleCommunication::Idle;
|
||||
if (connected()) {
|
||||
if (m_persistenceModeStarted) {
|
||||
ret = m_persistenceCommand;
|
||||
} else {
|
||||
ret = m_communication->currentMessageId();
|
||||
}
|
||||
}
|
||||
// LOG(info) << "current message id: " << static_cast<int>(ret)
|
||||
// << ", persistence mode started: " << m_persistenceModeStarted;
|
||||
return ret;
|
||||
}
|
||||
|
||||
int Application::persistenceVerifyInterval() const {
|
||||
return m_persistenceVerifyInterval;
|
||||
}
|
||||
|
||||
void Application::setPersistenceVerifyInterval(int interval) {
|
||||
if (m_persistenceVerifyInterval != interval) {
|
||||
m_persistenceVerifyInterval = interval;
|
||||
emit persistenceVerifyIntervalChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void Application::onNewPalmFeature(const PalmFeature &feature) {
|
||||
auto palms = m_database->palmFeatures();
|
||||
if (std::find(palms.cbegin(), palms.cend(), feature) != palms.cend()) {
|
||||
LOG(warning) << "本地数据库已有相同特征数据。";
|
||||
return;
|
||||
}
|
||||
if (!m_database->addPalmFeature(feature)) {
|
||||
LOG(error) << "add palm feature failed.";
|
||||
}
|
||||
}
|
||||
|
||||
void Application::onErrorOccurred(ModuleCommunication::NoteId note, const QString &error,
|
||||
const QString &detailMessage) {
|
||||
TipType type = Tip;
|
||||
if (note == ModuleCommunication::NoteId::DeviceError) {
|
||||
type = Error;
|
||||
QTimer::singleShot(0, this, [this]() { close(); });
|
||||
} else if (note == ModuleCommunication::NoteId::InteractWarning) {
|
||||
type = Warnging;
|
||||
} else {
|
||||
type = Warnging;
|
||||
}
|
||||
emit newStatusTip(type, error, detailMessage);
|
||||
}
|
||||
|
||||
void Application::onNewImageInfo(ModuleCommunication::MessageId messageId, uint32_t size, const uint8_t *md5) {
|
||||
using namespace std::chrono;
|
||||
m_palmImageId = messageId;
|
||||
m_palmImageSize = size;
|
||||
m_communication->requestEnrolledImage(0, ImageSliceSize);
|
||||
m_palmYImageBuffer.clear();
|
||||
m_startUploadTime = system_clock::now();
|
||||
if (messageId == ModuleCommunication::Note) {
|
||||
emit newStatusTip(Error, "活体未通过照片");
|
||||
}
|
||||
}
|
||||
|
||||
void Application::onNewImageSliceData(const std::vector<uint8_t> &data) {
|
||||
using namespace std::chrono;
|
||||
// LOG(info) << "onNewImageSliceData:" << data.size() << ", already received: " << m_palmYImageBuffer.size()
|
||||
// << ", total size: " << m_palmImageSize;
|
||||
m_palmYImageBuffer.append(reinterpret_cast<const char *>(data.data()), data.size());
|
||||
if (m_palmYImageBuffer.size() < m_palmImageSize) {
|
||||
m_communication->requestEnrolledImage(m_palmYImageBuffer.size(), ImageSliceSize);
|
||||
} else {
|
||||
auto username = m_palmUsername.toStdString();
|
||||
auto way = (m_palmImageId == ModuleCommunication::VerifyExtended) ? "verify" : "enroll";
|
||||
if (m_palmImageId == ModuleCommunication::Note) {
|
||||
way = "no_alive";
|
||||
}
|
||||
LOG(info) << "request image finished, username: " << username << ", userid: " << m_palmId
|
||||
<< ", elapsed: " << duration_cast<milliseconds>(system_clock::now() - m_startUploadTime);
|
||||
std::ostringstream oss;
|
||||
oss << YuvPath << "/" << username << "_" << m_palmId << "_" << way << "_"
|
||||
<< DateTime::toString(std::chrono::system_clock::now(), "%Y%m%d%H%M%S") << ".yuv";
|
||||
std::ofstream ofs(Amass::StringUtility::UTF8ToGBK(oss.str()), std::ofstream::binary);
|
||||
ofs.write(m_palmYImageBuffer.data(), m_palmYImageBuffer.size());
|
||||
|
||||
QImage image(reinterpret_cast<const uint8_t *>(m_palmYImageBuffer.data()), 600, 800, QImage::Format_Grayscale8);
|
||||
oss.str("");
|
||||
oss << JpgPath << "/" << username << "_" << m_palmId << "_" << way << "_"
|
||||
<< DateTime::toString(std::chrono::system_clock::now(), "%Y%m%d%H%M%S") << ".jpg";
|
||||
image.save(QString::fromStdString(oss.str()), "jpg", 80);
|
||||
}
|
||||
}
|
||||
|
||||
void Application::onCommandStarted(ModuleCommunication::MessageId messageId) {
|
||||
using namespace std::chrono;
|
||||
emit currentMessageIdChanged();
|
||||
}
|
||||
|
||||
void Application::onCommandFinished(ModuleCommunication::MessageId messageId,
|
||||
ModuleCommunication::MessageStatus status) {
|
||||
// LOG(info) << m_persistenceMode << " " << m_persistenceModeStarted << " " << m_persistenceVerifyInterval;
|
||||
using namespace std::chrono;
|
||||
if (messageId == ModuleCommunication::UploadImageInfo) {
|
||||
m_communication->uploadImageData(m_uploadImageSendedSize, m_uploadBuffer.data() + m_uploadImageSendedSize,
|
||||
ImageSliceSize);
|
||||
} else if (messageId == ModuleCommunication::UploadImageData) {
|
||||
m_uploadImageSendedSize += ImageSliceSize;
|
||||
if (m_imageUploadling && (m_uploadImageSendedSize < m_uploadBuffer.size())) {
|
||||
auto remainSize = m_uploadBuffer.size() - m_uploadImageSendedSize;
|
||||
m_communication->uploadImageData(m_uploadImageSendedSize,
|
||||
(const uint8_t *)m_uploadBuffer.data() + m_uploadImageSendedSize,
|
||||
remainSize < ImageSliceSize ? remainSize : ImageSliceSize);
|
||||
}
|
||||
if (status != ModuleCommunication::Needmore) {
|
||||
LOG(info) << "upload image finished, status: " << static_cast<int>(status)
|
||||
<< ", elapsed: " << duration_cast<milliseconds>(system_clock::now() - m_startUploadTime);
|
||||
if (m_imageUploadling && (m_persistenceCommand == ModuleCommunication::UploadImageInfo)) {
|
||||
QTimer::singleShot(0, this,
|
||||
[this]() { uploadImage(m_uploadPath, m_uploadUsername, m_currentUploadOperation); });
|
||||
}
|
||||
m_imageUploadling = false;
|
||||
}
|
||||
}
|
||||
|
||||
// LOG(info) << "messageId: " << (int)messageId << ", m_persistenceCommand: " << (int)m_persistenceCommand;
|
||||
if (messageId == m_persistenceCommand) {
|
||||
m_persistenceModeStarted = true;
|
||||
}
|
||||
if (messageId == ModuleCommunication::Reset) {
|
||||
m_persistenceModeStarted = false;
|
||||
if (m_verifyTimer->isActive()) {
|
||||
m_verifyTimer->stop();
|
||||
}
|
||||
}
|
||||
|
||||
if (((m_persistenceCommand == ModuleCommunication::Verify) ||
|
||||
(m_persistenceCommand == ModuleCommunication::VerifyExtended)) &&
|
||||
m_persistenceModeStarted &&
|
||||
((messageId == ModuleCommunication::Verify) || (messageId == ModuleCommunication::VerifyExtended)) &&
|
||||
((status == ModuleCommunication::Success) || (status == ModuleCommunication::Failed4UnknownUser) ||
|
||||
(status == ModuleCommunication::Failed4Timeout) || (status == ModuleCommunication::Failed4UnknownReason) ||
|
||||
(status == ModuleCommunication::Failed4LivenessCheck))) {
|
||||
m_verifyTimer->start(m_persistenceVerifyInterval * 1000);
|
||||
}
|
||||
emit currentMessageIdChanged();
|
||||
}
|
||||
|
||||
void Application::onVerifyTimeout() {
|
||||
if (m_verifyExtendedMode) {
|
||||
m_communication->verifyExtended(m_verifyExtendedMode, 120);
|
||||
} else {
|
||||
m_communication->verify(120);
|
||||
}
|
||||
}
|
||||
|
||||
bool Application::startOta(const QString &path) {
|
||||
if (!QFile::exists(path)) {
|
||||
emit otaMessage("文件不存在");
|
||||
return false;
|
||||
}
|
||||
auto device = CdcUpdater::searchDevice();
|
||||
if (device) {
|
||||
LOG(info) << "device already in ota mode.";
|
||||
} else {
|
||||
if (m_communication) {
|
||||
resetModule();
|
||||
m_communication->startOta();
|
||||
} else {
|
||||
emit otaMessage("请先打开设备");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
LOG(info) << "start ota, ota path: " << path.toStdString();
|
||||
m_updater = std::make_shared<CdcUpdater>();
|
||||
connect(m_updater.get(), &CdcUpdater::deviceDiscovered, this, &Application::onCdcDeviceDiscovered);
|
||||
connect(m_updater.get(), &CdcUpdater::updateFinished, this, &Application::updateFinished);
|
||||
connect(m_updater.get(), &CdcUpdater::progressChanged, this, &Application::otaProgressChanged);
|
||||
connect(m_updater.get(), &CdcUpdater::message, this, &Application::otaMessage);
|
||||
|
||||
m_updater->start(path, device ? *device : QSerialPortInfo());
|
||||
return true;
|
||||
}
|
||||
|
||||
void Application::onCdcDeviceDiscovered(const QSerialPortInfo &info) {
|
||||
auto status = m_updater->open(info);
|
||||
LOG(info) << "open cdc port: " << info.portName().toStdString() << ", status: " << status;
|
||||
if (!status) {
|
||||
QTimer::singleShot(0, this, [this]() { m_updater->startSearchDevice(); });
|
||||
}
|
||||
}
|
128
Analyser/Application.h
Normal file
128
Analyser/Application.h
Normal file
@ -0,0 +1,128 @@
|
||||
#ifndef __APPLICATION_H__
|
||||
#define __APPLICATION_H__
|
||||
|
||||
#include "DataStructure.h"
|
||||
#include "ModuleCommunication.h"
|
||||
#include "Singleton.h"
|
||||
#include <QObject>
|
||||
#include <QQmlEngine>
|
||||
#include <memory>
|
||||
|
||||
class QGuiApplication;
|
||||
class Database;
|
||||
class VideoPlayer;
|
||||
class VideoFrameProvider;
|
||||
class QTimer;
|
||||
class CdcUpdater;
|
||||
|
||||
class Application : public QObject {
|
||||
Q_OBJECT
|
||||
QML_NAMED_ELEMENT(App)
|
||||
QML_SINGLETON
|
||||
Q_PROPERTY(ModuleCommunication *module READ module NOTIFY connectedChanged)
|
||||
Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged)
|
||||
Q_PROPERTY(bool uvcOpened READ uvcOpened NOTIFY uvcOpenedChanged)
|
||||
Q_PROPERTY(ModuleCommunication::MessageId persistenceCommand READ persistenceCommand WRITE setPersistenceCommand NOTIFY persistenceCommandChanged)
|
||||
Q_PROPERTY(int persistenceVerifyInterval READ persistenceVerifyInterval WRITE setPersistenceVerifyInterval NOTIFY
|
||||
persistenceVerifyIntervalChanged)
|
||||
Q_PROPERTY(ModuleCommunication::MessageId currentMessageId READ currentMessageId NOTIFY currentMessageIdChanged)
|
||||
friend class Amass::Singleton<Application>;
|
||||
static constexpr auto JpgPath = "jpg";
|
||||
static constexpr auto YuvPath = "yuv";
|
||||
|
||||
public:
|
||||
enum TipType {
|
||||
Tip,
|
||||
Error,
|
||||
Info,
|
||||
Warnging,
|
||||
};
|
||||
Q_ENUM(TipType)
|
||||
|
||||
void initializeLogger();
|
||||
int exec();
|
||||
static Application *create(QQmlEngine *qmlEngine, QJSEngine *jsEngine);
|
||||
Q_INVOKABLE QStringList availableSerialPorts() const;
|
||||
Q_INVOKABLE QVariantList availableUsbVideoCameras() const;
|
||||
Q_INVOKABLE bool open(const QString &portName, int baudRate);
|
||||
Q_INVOKABLE bool openUVC(const QString &deviceName);
|
||||
Q_INVOKABLE void close();
|
||||
Q_INVOKABLE void closeUVC();
|
||||
Q_INVOKABLE bool startOta(const QString &path);
|
||||
Q_INVOKABLE void verify(bool captureImage, uint8_t timeout);
|
||||
Q_INVOKABLE void enroll(const QString &username, bool strictMode, uint8_t excludeMode, bool persistence,
|
||||
uint8_t timeout);
|
||||
Q_INVOKABLE void enrollExtended(const QString &username, bool strictMode, uint8_t excludeMode, bool persistence,
|
||||
uint8_t timeout);
|
||||
Q_INVOKABLE void deleteUser(uint16_t userid);
|
||||
Q_INVOKABLE void deleteAll();
|
||||
Q_INVOKABLE void uploadImage(const QString &path, const QString &username, int operation);
|
||||
ModuleCommunication *module() const;
|
||||
Q_INVOKABLE void resetModule();
|
||||
bool connected() const;
|
||||
bool uvcOpened() const;
|
||||
ModuleCommunication::MessageId persistenceCommand() const;
|
||||
void setPersistenceCommand(ModuleCommunication::MessageId command);
|
||||
ModuleCommunication::MessageId currentMessageId() const;
|
||||
|
||||
int persistenceVerifyInterval() const;
|
||||
void setPersistenceVerifyInterval(int interval);
|
||||
|
||||
signals:
|
||||
void connectedChanged();
|
||||
void persistenceCommandChanged();
|
||||
void persistenceVerifyIntervalChanged();
|
||||
void currentMessageIdChanged();
|
||||
void uvcOpenedChanged();
|
||||
void newVideoFrame();
|
||||
void newLog(const QString &log);
|
||||
void newStatusTip(TipType type, const QString &tip, const QString &detailMessage = "");
|
||||
void updateFinished();
|
||||
void otaMessage(const QString &text);
|
||||
void otaProgressChanged(int32_t progress);
|
||||
|
||||
protected:
|
||||
Application(int &argc, char **argv);
|
||||
void onNewEnrollResult(uint16_t userid);
|
||||
void onNewVerifyResult(uint16_t userid, const QString &username, uint16_t elapsed);
|
||||
void onNewPalmFeature(const PalmFeature &feature);
|
||||
void onErrorOccurred(ModuleCommunication::NoteId note, const QString &error, const QString &detailMessage);
|
||||
void onNewImageInfo(ModuleCommunication::MessageId messageId, uint32_t size, const uint8_t *md5);
|
||||
void onNewImageSliceData(const std::vector<uint8_t> &data);
|
||||
void onCommandStarted(ModuleCommunication::MessageId messageId);
|
||||
void onCommandFinished(ModuleCommunication::MessageId messageId, ModuleCommunication::MessageStatus status);
|
||||
void onVerifyTimeout();
|
||||
void onCdcDeviceDiscovered(const QSerialPortInfo &info);
|
||||
void onUpdateFinished();
|
||||
|
||||
private:
|
||||
std::shared_ptr<QGuiApplication> m_app;
|
||||
std::shared_ptr<ModuleCommunication> m_communication;
|
||||
std::shared_ptr<CdcUpdater> m_updater;
|
||||
std::shared_ptr<Database> m_database;
|
||||
|
||||
ModuleCommunication::MessageId m_persistenceCommand = ModuleCommunication::Idle; // 模组持续识别
|
||||
bool m_verifyExtendedMode = false;
|
||||
bool m_persistenceModeStarted = false;
|
||||
int m_persistenceVerifyInterval = 1;
|
||||
QTimer *m_verifyTimer = nullptr;
|
||||
|
||||
ModuleCommunication::MessageId m_palmImageId; // 通过哪个指令获取的图片
|
||||
uint32_t m_palmImageSize = 0;
|
||||
QString m_palmUsername;
|
||||
uint16_t m_palmId = ModuleCommunication::InvalidUserId;
|
||||
QByteArray m_palmYImageBuffer;
|
||||
std::chrono::system_clock::time_point m_startUploadTime;
|
||||
|
||||
uint32_t m_uploadImageSendedSize;
|
||||
std::vector<uint8_t> m_uploadBuffer;
|
||||
int m_currentUploadOperation = 0;
|
||||
QString m_uploadPath;
|
||||
QString m_uploadUsername;
|
||||
bool m_imageUploadling = false;
|
||||
|
||||
std::shared_ptr<VideoPlayer> m_videoPlayer;
|
||||
VideoFrameProvider *m_videoFrameProvider;
|
||||
};
|
||||
|
||||
#endif // __APPLICATION_H__
|
@ -1,21 +1,108 @@
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
project(Analyser VERSION 0.4 LANGUAGES C CXX)
|
||||
set(APPLICATION_NAME "掌静脉测试工具")
|
||||
|
||||
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets SerialPort)
|
||||
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets SerialPort)
|
||||
find_package(Boost REQUIRED COMPONENTS json)
|
||||
|
||||
add_executable(Analyser Analyser.rc
|
||||
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Quick QuickTemplates2 SerialPort JpegPrivate BundledLibjpeg)
|
||||
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Quick QuickTemplates2 SerialPort JpegPrivate BundledLibjpeg)
|
||||
|
||||
qt_standard_project_setup(REQUIRES 6.5)
|
||||
|
||||
configure_file(Configuration.h.in Configuration.h)
|
||||
|
||||
qt_add_executable(Analyser Analyser.rc
|
||||
main.cpp
|
||||
Application.h Application.cpp
|
||||
CategoryLogSinkBackend.h CategoryLogSinkBackend.cpp
|
||||
Widget.h Widget.cpp
|
||||
ImageDecoder.h ImageDecoder.cpp
|
||||
ModuleCommunication.h ModuleCommunication.cpp
|
||||
PalmFeatureTableModel.h PalmFeatureTableModel.cpp
|
||||
VideoFrameProvider.h VideoFrameProvider.cpp
|
||||
VideoPlayer.h VideoPlayer.cpp
|
||||
)
|
||||
|
||||
qt_add_qml_module(Analyser
|
||||
URI Analyser
|
||||
VERSION 1.0
|
||||
QML_FILES
|
||||
qml/Main.qml
|
||||
qml/ConnectionItem.qml
|
||||
qml/OperationItem.qml
|
||||
qml/OtaPage.qml
|
||||
qml/EnrollVerifyOperations.qml
|
||||
qml/ExtendedOperations.qml
|
||||
RESOURCES
|
||||
resources/successfull.svg
|
||||
resources/warning.svg
|
||||
resources/palm-middle.png
|
||||
QML_FILES qml/LogView.qml
|
||||
)
|
||||
|
||||
target_compile_definitions(Analyser
|
||||
PRIVATE _CRT_SECURE_NO_WARNINGS
|
||||
)
|
||||
|
||||
if(UNIX)
|
||||
set_target_properties(Analyser PROPERTIES
|
||||
SUFFIX .AppImage
|
||||
)
|
||||
set(EXECUTABLE_NAME Analyser.AppImage)
|
||||
else()
|
||||
set(EXECUTABLE_NAME Analyser.exe)
|
||||
endif()
|
||||
if(CMAKE_BUILD_TYPE STREQUAL "Release")
|
||||
set_property(TARGET Analyser PROPERTY
|
||||
WIN32_EXECUTABLE true
|
||||
)
|
||||
endif()
|
||||
|
||||
target_include_directories(Analyser
|
||||
PRIVATE ${FFmpeg_INCLUDE_DIR}
|
||||
PRIVATE ${CMAKE_CURRENT_BINARY_DIR}
|
||||
)
|
||||
|
||||
target_link_directories(Analyser
|
||||
PRIVATE ${FFmpeg_LIB_DIR}
|
||||
)
|
||||
|
||||
target_link_libraries(Analyser
|
||||
PRIVATE QtComponets
|
||||
PRIVATE Fluent
|
||||
PRIVATE Fluentplugin
|
||||
PRIVATE Encrypt
|
||||
PRIVATE Database
|
||||
PRIVATE Ws2_32
|
||||
PRIVATE Qt${QT_VERSION_MAJOR}::Widgets
|
||||
PRIVATE Peripheral
|
||||
PRIVATE avcodec
|
||||
PRIVATE swscale
|
||||
PRIVATE avutil
|
||||
PRIVATE avdevice
|
||||
PRIVATE avformat
|
||||
$<$<PLATFORM_ID:Windows>:Ws2_32>
|
||||
PRIVATE Boost::json
|
||||
PRIVATE Qt${QT_VERSION_MAJOR}::Quick
|
||||
PRIVATE Qt${QT_VERSION_MAJOR}::QuickTemplates2
|
||||
PRIVATE Qt${QT_VERSION_MAJOR}::SerialPort
|
||||
PRIVATE Qt${QT_VERSION_MAJOR}::JpegPrivate
|
||||
PRIVATE Qt${QT_VERSION_MAJOR}::BundledLibjpeg
|
||||
)
|
||||
|
||||
install(TARGETS Analyser
|
||||
BUNDLE DESTINATION .
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
)
|
||||
|
||||
configure_file(
|
||||
${CMAKE_SOURCE_DIR}/resources/run.sh.in
|
||||
${CMAKE_CURRENT_BINARY_DIR}/scripts/${EXECUTABLE_NAME}
|
||||
@ONLY
|
||||
)
|
||||
install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/scripts/${EXECUTABLE_NAME} DESTINATION .)
|
||||
|
||||
qt_generate_deploy_qml_app_script(
|
||||
TARGET Analyser
|
||||
OUTPUT_SCRIPT deploy_script
|
||||
NO_UNSUPPORTED_PLATFORM_ERROR
|
||||
NO_TRANSLATIONS
|
||||
DEPLOY_USER_QML_MODULES_ON_UNSUPPORTED_PLATFORM
|
||||
)
|
||||
install(SCRIPT ${deploy_script})
|
||||
|
||||
|
@ -1,15 +1,13 @@
|
||||
#include "CategoryLogSinkBackend.h"
|
||||
#include "AsyncEvent.h"
|
||||
#include <QTextBrowser>
|
||||
#include <iostream>
|
||||
#include "BoostLog.h"
|
||||
|
||||
CategoryLogSinkBackend::CategoryLogSinkBackend(const std::string &category, QTextBrowser *target)
|
||||
: m_category(category), m_target(target) {
|
||||
CategoryLogSinkBackend::CategoryLogSinkBackend(const std::string &category, Append &&append)
|
||||
: m_category(category), m_append(std::move(append)) {
|
||||
}
|
||||
|
||||
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)); });
|
||||
if ((category == m_category) && m_append) {
|
||||
m_append(output);
|
||||
}
|
||||
}
|
||||
|
@ -2,19 +2,19 @@
|
||||
#define __CATEGORYLOGSINKBACKEND_H__
|
||||
|
||||
#include <boost/log/sinks.hpp>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
class QTextBrowser;
|
||||
|
||||
class CategoryLogSinkBackend
|
||||
: public boost::log::sinks::basic_formatted_sink_backend<char, boost::log::sinks::synchronized_feeding> {
|
||||
public:
|
||||
CategoryLogSinkBackend(const std::string &category, QTextBrowser *target);
|
||||
using Append = std::function<void(const std::string &)>;
|
||||
CategoryLogSinkBackend(const std::string &category, Append &&append);
|
||||
void consume(const boost::log::record_view &record, string_type const &output);
|
||||
|
||||
private:
|
||||
std::string m_category;
|
||||
QTextBrowser *m_target = nullptr;
|
||||
Append m_append;
|
||||
};
|
||||
|
||||
#endif // __CATEGORYLOGSINKBACKEND_H__
|
||||
|
3
Analyser/Configuration.h.in
Normal file
3
Analyser/Configuration.h.in
Normal file
@ -0,0 +1,3 @@
|
||||
#define APPLICATION_NAME "@APPLICATION_NAME@"
|
||||
#define GIT_COMMIT_ID "@GIT_COMMIT_ID@"
|
||||
#define APP_VERSION "@PROJECT_VERSION@"
|
60
Analyser/ImageDecoder.cpp
Normal file
60
Analyser/ImageDecoder.cpp
Normal file
@ -0,0 +1,60 @@
|
||||
#include "ImageDecoder.h"
|
||||
#include "BoostLog.h"
|
||||
#include <jpeglib.h>
|
||||
#include <stdio.h>
|
||||
|
||||
std::optional<ImageDecoder::Image> ImageDecoder::extractJpegYComponent(const std::string &filename) {
|
||||
struct jpeg_decompress_struct cinfo;
|
||||
struct jpeg_error_mgr jerr;
|
||||
cinfo.err = jpeg_std_error(&jerr);
|
||||
jpeg_create_decompress(&cinfo);
|
||||
|
||||
FILE *infile = fopen(filename.c_str(), "rb");
|
||||
if (infile == nullptr) {
|
||||
LOG(error) << "cannot open " << filename;
|
||||
return std::nullopt;
|
||||
}
|
||||
ImageDecoder::Image ret;
|
||||
jpeg_stdio_src(&cinfo, infile);
|
||||
jpeg_read_header(&cinfo, TRUE); // 1
|
||||
LOG(info) << "jpeg color space: " << cinfo.jpeg_color_space;
|
||||
if (cinfo.jpeg_color_space == JCS_YCbCr) {
|
||||
cinfo.out_color_space = JCS_YCbCr; // We want YCbCr color space
|
||||
jpeg_start_decompress(&cinfo);
|
||||
|
||||
int row_stride = cinfo.output_width * cinfo.output_components;
|
||||
JSAMPARRAY buffer = (*cinfo.mem->alloc_sarray)((j_common_ptr)&cinfo, JPOOL_IMAGE, row_stride, 1);
|
||||
|
||||
ret.width = cinfo.output_width;
|
||||
ret.height = cinfo.output_height;
|
||||
ret.data.resize(ret.width * ret.height);
|
||||
while (cinfo.output_scanline < cinfo.output_height) {
|
||||
jpeg_read_scanlines(&cinfo, buffer, 1);
|
||||
for (unsigned int i = 0; i < cinfo.output_width; ++i) {
|
||||
ret.data[(cinfo.output_scanline - 1) * cinfo.output_width + i] =
|
||||
buffer[0][i * 3]; // Y component is the first byte in YCbCr
|
||||
}
|
||||
}
|
||||
|
||||
jpeg_finish_decompress(&cinfo);
|
||||
} else if (cinfo.jpeg_color_space == JCS_GRAYSCALE) {
|
||||
cinfo.out_color_space = JCS_GRAYSCALE; // We want grayscale color space
|
||||
jpeg_start_decompress(&cinfo);
|
||||
int row_stride = cinfo.output_width * cinfo.output_components;
|
||||
JSAMPARRAY buffer = (*cinfo.mem->alloc_sarray)((j_common_ptr)&cinfo, JPOOL_IMAGE, row_stride, 1);
|
||||
|
||||
ret.width = cinfo.output_width;
|
||||
ret.height = cinfo.output_height;
|
||||
ret.data.resize(ret.width * ret.height);
|
||||
while (cinfo.output_scanline < cinfo.output_height) {
|
||||
(void)jpeg_read_scanlines(&cinfo, buffer, 1);
|
||||
memcpy(ret.data.data() + (cinfo.output_scanline - 1) * row_stride, buffer[0], row_stride);
|
||||
}
|
||||
} else {
|
||||
LOG(warning) << "jpeg color space(" << cinfo.jpeg_color_space << ") not supported.";
|
||||
}
|
||||
|
||||
jpeg_destroy_decompress(&cinfo);
|
||||
fclose(infile);
|
||||
return std::make_optional(ret);
|
||||
}
|
19
Analyser/ImageDecoder.h
Normal file
19
Analyser/ImageDecoder.h
Normal file
@ -0,0 +1,19 @@
|
||||
#ifndef __IMAGEDECODER_H__
|
||||
#define __IMAGEDECODER_H__
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class ImageDecoder {
|
||||
public:
|
||||
struct Image {
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
std::vector<uint8_t> data;
|
||||
};
|
||||
static std::optional<Image> extractJpegYComponent(const std::string &filename);
|
||||
};
|
||||
|
||||
#endif // __IMAGEDECODER_H__
|
@ -1,9 +1,18 @@
|
||||
#include "ModuleCommunication.h"
|
||||
#include "BoostLog.h"
|
||||
#include <QSerialPort>
|
||||
#include <WinSock2.h>
|
||||
#include "StringUtility.h"
|
||||
#include <boost/describe.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/parse.hpp>
|
||||
#include <boost/json/serialize.hpp>
|
||||
#include <boost/mp11.hpp>
|
||||
#include <mbedtls/md5.h>
|
||||
#include <sstream>
|
||||
#ifdef WIN32
|
||||
#include <WinSock2.h>
|
||||
#else
|
||||
#include <arpa/inet.h>
|
||||
#endif
|
||||
|
||||
static inline uint8_t xor_checksum_byte(const uint8_t *data, uint32_t len) {
|
||||
uint8_t sum = 0;
|
||||
@ -16,52 +25,94 @@ static inline uint8_t xor_checksum_byte(const uint8_t *data, uint32_t len) {
|
||||
ModuleCommunication::ModuleCommunication(QObject *parent) : QObject{parent} {
|
||||
}
|
||||
|
||||
bool ModuleCommunication::open(const QString &portName) {
|
||||
bool ModuleCommunication::open(const QString &portName, int baudRate) {
|
||||
if (portName.isEmpty()) return false;
|
||||
bool ret = true;
|
||||
m_serialPort = std::make_shared<QSerialPort>(portName);
|
||||
m_serialPort->setBaudRate(QSerialPort::Baud115200);
|
||||
m_serialPort->setBaudRate(baudRate);
|
||||
connect(m_serialPort.get(), &QSerialPort::readyRead, this, &ModuleCommunication::onReadyRead);
|
||||
connect(m_serialPort.get(), &QSerialPort::errorOccurred, this, &ModuleCommunication::onErrorOccurred);
|
||||
|
||||
ret = m_serialPort->open(QSerialPort::ReadWrite);
|
||||
LOG_CAT(info, GUI) << "打开串口(" << portName.toStdString() << ")" << (ret ? "成功" : "失败") << "。";
|
||||
LOG_CAT(info, GUI) << "打开串口(" << portName.toStdString() << ")" << (ret ? "成功" : "失败")
|
||||
<< ", 波特率: " << baudRate;
|
||||
if (!ret) {
|
||||
LOG(error) << m_serialPort->errorString().toStdString();
|
||||
}
|
||||
LOG_CAT(info, GUI) << Separator;
|
||||
return ret;
|
||||
}
|
||||
|
||||
void ModuleCommunication::verify(uint8_t timeout) {
|
||||
VerifyInfo data = {0};
|
||||
VerifyRequest data = {0};
|
||||
data.timeout = timeout;
|
||||
|
||||
auto [frameData, frameSize] = generateFrame(Verify, reinterpret_cast<const uint8_t *>(&data), sizeof(data));
|
||||
m_serialPort->write(reinterpret_cast<const char *>(frameData), frameSize);
|
||||
setCurrentMessageIdStatus(Verify);
|
||||
|
||||
LOG_CAT(info, GUI) << "发送识别指令: " << protocolDataFormatString(frameData, frameSize);
|
||||
LOG_CAT(info, GUI) << Separator;
|
||||
}
|
||||
|
||||
void ModuleCommunication::verifyExtended(bool captureImage, uint8_t timeout) {
|
||||
VerifyRequest data = {0};
|
||||
data.timeout = timeout;
|
||||
data.save_image = captureImage ? 0x01 : 0x00;
|
||||
auto [frameData, frameSize] = generateFrame(VerifyExtended, reinterpret_cast<const uint8_t *>(&data), sizeof(data));
|
||||
m_serialPort->write(reinterpret_cast<const char *>(frameData), frameSize);
|
||||
setCurrentMessageIdStatus(VerifyExtended);
|
||||
|
||||
LOG_CAT(info, GUI) << "发送扩展识别指令: " << protocolDataFormatString(frameData, frameSize);
|
||||
LOG_CAT(info, GUI) << Separator;
|
||||
}
|
||||
|
||||
void ModuleCommunication::reset() {
|
||||
auto [frameData, frameSize] = generateFrame(Reset);
|
||||
m_serialPort->write(reinterpret_cast<const char *>(frameData), frameSize);
|
||||
setCurrentMessageIdStatus(Reset);
|
||||
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};
|
||||
void ModuleCommunication::enroll(const std::string &username, bool strictMode, uint8_t excludeMode, bool persistence,
|
||||
uint8_t timeout) {
|
||||
EnrollRequest data = {0};
|
||||
data.strictMode = strictMode ? 1 : 0;
|
||||
data.excludeMode = excludeMode;
|
||||
data.timeout = timeout;
|
||||
data.skipSave = persistence ? 0 : 1;
|
||||
strncpy(reinterpret_cast<char *>(data.username), username.c_str(), sizeof(data.username));
|
||||
auto [frameData, frameSize] = generateFrame(EnrollSingle, reinterpret_cast<const uint8_t *>(&data), sizeof(data));
|
||||
m_serialPort->write(reinterpret_cast<const char *>(frameData), frameSize);
|
||||
setCurrentMessageIdStatus(EnrollSingle);
|
||||
|
||||
LOG_CAT(info, GUI) << "发送注册指令: " << protocolDataFormatString(frameData, frameSize);
|
||||
LOG_CAT(info, GUI) << "用户名: " << username << ", 超时时间: " << static_cast<int>(timeout) << "s";
|
||||
LOG_CAT(info, GUI) << Separator;
|
||||
}
|
||||
|
||||
void ModuleCommunication::enrollExtended(const std::string &username, bool strictMode, uint8_t excludeMode,
|
||||
bool persistence, uint8_t timeout) {
|
||||
EnrollRequest data = {};
|
||||
data.strictMode = strictMode ? 1 : 0;
|
||||
data.excludeMode = excludeMode;
|
||||
data.timeout = timeout;
|
||||
data.skipSave = persistence ? 0 : 1;
|
||||
strncpy(reinterpret_cast<char *>(data.username), username.c_str(), sizeof(data.username));
|
||||
auto [frameData, frameSize] = generateFrame(EnrollExtended, reinterpret_cast<const uint8_t *>(&data), sizeof(data));
|
||||
m_serialPort->write(reinterpret_cast<const char *>(frameData), frameSize);
|
||||
setCurrentMessageIdStatus(EnrollExtended);
|
||||
LOG_CAT(info, GUI) << "发送获取注册照片指令: " << protocolDataFormatString(frameData, frameSize);
|
||||
LOG_CAT(info, GUI) << "用户名: " << username << ", 超时时间: " << static_cast<int>(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<const uint8_t *>(&n), sizeof(n));
|
||||
m_serialPort->write(reinterpret_cast<const char *>(frameData), frameSize);
|
||||
setCurrentMessageIdStatus(DeleteUser);
|
||||
LOG_CAT(info, GUI) << "发送删除用户指令: " << protocolDataFormatString(frameData, frameSize);
|
||||
LOG_CAT(info, GUI) << "删除用户ID: " << userid;
|
||||
LOG_CAT(info, GUI) << Separator;
|
||||
@ -70,44 +121,102 @@ void ModuleCommunication::deleteUser(uint16_t userid) {
|
||||
void ModuleCommunication::deleteAll() {
|
||||
auto [frameData, frameSize] = generateFrame(DeleteAll);
|
||||
m_serialPort->write(reinterpret_cast<const char *>(frameData), frameSize);
|
||||
setCurrentMessageIdStatus(DeleteAll);
|
||||
LOG_CAT(info, GUI) << "发送删除所有指令: " << protocolDataFormatString(frameData, frameSize);
|
||||
LOG_CAT(info, GUI) << Separator;
|
||||
}
|
||||
|
||||
void ModuleCommunication::requestPalmFeature(uint16_t userid) {
|
||||
uint16_t n = htons(userid);
|
||||
auto [frameData, frameSize] = generateFrame(RequestPalmFeature, reinterpret_cast<const uint8_t *>(&n), sizeof(n));
|
||||
void ModuleCommunication::requestEnrolledImage(uint32_t offset, uint32_t size) {
|
||||
ImageSliceRequest request;
|
||||
request.offset = htonl(offset);
|
||||
request.size = htonl(size);
|
||||
auto [data, frameSize] = generateFrame(GetImage, reinterpret_cast<const uint8_t *>(&request), sizeof(request));
|
||||
m_serialPort->write(reinterpret_cast<const char *>(data), frameSize);
|
||||
// 打印太耗时
|
||||
// LOG_CAT(info, GUI) << "发送获取图片指令: " << protocolDataFormatString(data, frameSize);
|
||||
// LOG_CAT(info, GUI) << Separator;
|
||||
}
|
||||
|
||||
void ModuleCommunication::uploadImageInfo(const UploadImageInformation &info) {
|
||||
UploadImageInformation request;
|
||||
request.operation = info.operation;
|
||||
request.width = htons(info.width);
|
||||
request.height = htons(info.height);
|
||||
request.size = htonl(info.size);
|
||||
memcpy(request.username, info.username, sizeof(request.username));
|
||||
memcpy(request.md5, info.md5, sizeof(request.md5));
|
||||
auto [data, frameSize] =
|
||||
generateFrame(UploadImageInfo, reinterpret_cast<const uint8_t *>(&request), sizeof(request));
|
||||
m_serialPort->write(reinterpret_cast<const char *>(data), frameSize);
|
||||
}
|
||||
|
||||
void ModuleCommunication::uploadImageData(uint32_t offset, const uint8_t *data, uint32_t size) {
|
||||
uint32_t dataSize = sizeof(UploadImageDataSlice) + size;
|
||||
auto buffer = new uint8_t[dataSize];
|
||||
auto request = reinterpret_cast<UploadImageDataSlice *>(buffer);
|
||||
request->size = htonl(size);
|
||||
request->offset = htonl(offset);
|
||||
memcpy(request->data, data, size);
|
||||
auto [frameData, frameSize] = generateFrame(UploadImageData, buffer, dataSize);
|
||||
m_serialPort->write(reinterpret_cast<const char *>(frameData), frameSize);
|
||||
LOG_CAT(info, GUI) << "发送获取掌静脉特征值指令: " << protocolDataFormatString(frameData, frameSize);
|
||||
LOG_CAT(info, GUI) << "获取特征值用户ID: " << userid;
|
||||
delete[] buffer;
|
||||
}
|
||||
|
||||
void ModuleCommunication::requestCurrentStatus() {
|
||||
auto [frameData, frameSize] = generateFrame(GetCurrentStatus);
|
||||
m_serialPort->write(reinterpret_cast<const char *>(frameData), frameSize);
|
||||
LOG_CAT(info, GUI) << "发送状态指令: " << protocolDataFormatString(frameData, frameSize);
|
||||
LOG_CAT(info, GUI) << Separator;
|
||||
}
|
||||
|
||||
void ModuleCommunication::enrollPalmFeature(uint16_t userid, const PalmFeature &feature) {
|
||||
auto buffer = new uint8_t[sizeof(PalmFeatureHeader) + feature.feature.size()];
|
||||
auto header = reinterpret_cast<PalmFeatureHeader *>(buffer);
|
||||
header->userid = htons(userid);
|
||||
header->featureTotalSize = htons(feature.feature.size());
|
||||
strncpy(reinterpret_cast<char *>(header->username), feature.username.c_str(), sizeof(header->username));
|
||||
|
||||
mbedtls_md5_context context;
|
||||
mbedtls_md5_init(&context);
|
||||
mbedtls_md5_starts(&context);
|
||||
uint8_t md5[16];
|
||||
mbedtls_md5_update(&context, feature.feature.data(), feature.feature.size());
|
||||
mbedtls_md5_finish(&context, md5);
|
||||
mbedtls_md5_free(&context);
|
||||
memcpy(header->featureDataMd5, md5, sizeof(header->featureDataMd5));
|
||||
|
||||
memcpy(buffer + sizeof(PalmFeatureHeader), feature.feature.data(), feature.feature.size());
|
||||
void ModuleCommunication::setDebugEnabled(bool enabled) {
|
||||
DebugRequest request;
|
||||
boost::json::object object;
|
||||
object["command"] = "set_configurations";
|
||||
object["cdc_log_enabled"] = enabled;
|
||||
auto text = boost::json::serialize(object);
|
||||
std::strncpy(request.message, text.c_str(), sizeof(request.message));
|
||||
request.length = htons(text.size());
|
||||
|
||||
auto [frameData, frameSize] =
|
||||
generateFrame(RegisterPalmFeature, buffer, sizeof(PalmFeatureHeader) + feature.feature.size());
|
||||
generateFrame(EnableDebug, reinterpret_cast<const uint8_t *>(&request), sizeof(request));
|
||||
m_serialPort->write(reinterpret_cast<const char *>(frameData), frameSize);
|
||||
LOG_CAT(info, GUI) << "发送注册掌静脉特征指令: " << protocolDataFormatString(frameData, frameSize);
|
||||
LOG_CAT(info, GUI) << Separator;
|
||||
}
|
||||
|
||||
if (buffer != nullptr) delete[] buffer;
|
||||
void ModuleCommunication::requestDebugSettings() {
|
||||
DebugRequest request;
|
||||
boost::json::object object;
|
||||
object["command"] = "get_configurations";
|
||||
auto text = boost::json::serialize(object);
|
||||
std::strncpy(request.message, text.c_str(), sizeof(request.message));
|
||||
request.length = htons(text.size());
|
||||
|
||||
auto [frameData, frameSize] =
|
||||
generateFrame(EnableDebug, reinterpret_cast<const uint8_t *>(&request), sizeof(request));
|
||||
m_serialPort->write(reinterpret_cast<const char *>(frameData), frameSize);
|
||||
}
|
||||
|
||||
void ModuleCommunication::startOta() {
|
||||
auto [frameData, frameSize] = generateFrame(StartOta);
|
||||
m_serialPort->write(reinterpret_cast<const char *>(frameData), frameSize);
|
||||
}
|
||||
|
||||
void ModuleCommunication::requestUniqueId() {
|
||||
auto [frameData, frameSize] = generateFrame(GetUniqueID);
|
||||
m_serialPort->write(reinterpret_cast<const char *>(frameData), frameSize);
|
||||
LOG_CAT(info, GUI) << "发送获取ID指令: " << protocolDataFormatString(frameData, frameSize);
|
||||
LOG_CAT(info, GUI) << Separator;
|
||||
}
|
||||
|
||||
void ModuleCommunication::requestVersion() {
|
||||
auto [frameData, frameSize] = generateFrame(GetVersion);
|
||||
m_serialPort->write(reinterpret_cast<const char *>(frameData), frameSize);
|
||||
LOG_CAT(info, GUI) << "发送获取版本指令: " << protocolDataFormatString(frameData, frameSize);
|
||||
LOG_CAT(info, GUI) << Separator;
|
||||
}
|
||||
|
||||
ModuleCommunication::MessageId ModuleCommunication::currentMessageId() const {
|
||||
return m_currentMessageId;
|
||||
}
|
||||
|
||||
void ModuleCommunication::processPackage(const uint8_t *data, uint16_t size) {
|
||||
@ -125,32 +234,97 @@ void ModuleCommunication::processPackage(const uint8_t *data, uint16_t size) {
|
||||
LOG_CAT(info, GUI) << Separator;
|
||||
break;
|
||||
}
|
||||
case Verify: {
|
||||
case Verify:
|
||||
case VerifyExtended: {
|
||||
LOG_CAT(info, GUI) << "模组: " << protocolDataFormatString(data, size);
|
||||
if (result == Success) {
|
||||
auto info = reinterpret_cast<const VerifyDataReply *>(data + 7);
|
||||
LOG_CAT(info, GUI) << "用户ID: " << ntohs(info->userid)
|
||||
<< ", 用户名: " << std::string_view(reinterpret_cast<const char *>(info->username));
|
||||
auto info = reinterpret_cast<const VerifyReply *>(data + 7);
|
||||
uint16_t userid = ntohs(info->userid);
|
||||
uint16_t elapsed = ntohs(info->elapsed);
|
||||
LOG_CAT(info, GUI) << "用户ID: " << userid
|
||||
<< ", 用户名: " << std::string_view(reinterpret_cast<const char *>(info->username))
|
||||
<< ", 耗时: " << elapsed << "ms";
|
||||
emit newVerifyResult(userid, reinterpret_cast<const char *>(info->username), elapsed);
|
||||
} else if (result == Failed4Timeout) {
|
||||
LOG_CAT(info, GUI) << "识别超时。";
|
||||
} else if (result == Rejected) {
|
||||
LOG_CAT(info, GUI) << "模组拒绝该命令。";
|
||||
} else if (result == Failed4UnknownUser) {
|
||||
auto info = reinterpret_cast<const VerifyReply *>(data + 7);
|
||||
uint16_t elapsed = ntohs(info->elapsed);
|
||||
emit newVerifyResult(InvalidUserId, "", elapsed);
|
||||
LOG_CAT(info, GUI) << "未录入用户, 耗时: " << elapsed << "ms";
|
||||
} else if (result == Failed4UnknownReason) {
|
||||
LOG_CAT(info, GUI) << "未录入用户。";
|
||||
auto info = reinterpret_cast<const VerifyReply *>(data + 7);
|
||||
uint16_t elapsed = ntohs(info->elapsed);
|
||||
emit newVerifyResult(InvalidUserId, "", elapsed);
|
||||
LOG_CAT(info, GUI) << "未知错误, 耗时: " << elapsed << "ms";
|
||||
} else {
|
||||
LOG_CAT(info, GUI) << "未知错误(" << static_cast<int>(result) << ")。";
|
||||
}
|
||||
if (replyId == VerifyExtended) {
|
||||
auto info = reinterpret_cast<const VerifyExtendReply *>(data + 7);
|
||||
uint16_t width = ntohs(info->image_width);
|
||||
uint16_t height = ntohs(info->image_height);
|
||||
if ((width > 0) && (height > 0)) {
|
||||
emit newImageInfo(static_cast<MessageId>(replyId), width * height, info->md5);
|
||||
}
|
||||
}
|
||||
LOG_CAT(info, GUI) << Separator;
|
||||
break;
|
||||
}
|
||||
case EnrollSingle: {
|
||||
case EnrollSingle:
|
||||
case EnrollExtended: {
|
||||
LOG_CAT(info, GUI) << "模组: " << protocolDataFormatString(data, size);
|
||||
if (result == Success) {
|
||||
auto info = reinterpret_cast<const EnrollDataReply *>(data + 7);
|
||||
LOG_CAT(info, GUI) << "注册成功,用户ID: " << ntohs(info->userid);
|
||||
uint16_t userId = InvalidUserId;
|
||||
if (replyId == EnrollExtended) {
|
||||
auto info = reinterpret_cast<const EnrollExtendedReply *>(data + 7);
|
||||
uint16_t width = ntohs(info->imageWidth);
|
||||
uint16_t height = ntohs(info->imageHeight);
|
||||
userId = ntohs(info->userid);
|
||||
uint16_t plamX = ntohs(info->palmVeinInformation.x1);
|
||||
uint16_t plamY = ntohs(info->palmVeinInformation.y1);
|
||||
LOG_CAT(info, GUI) << "注册成功,用户ID: " << userId << ", 图片大小: " << width << "x" << height
|
||||
<< ", 已有ID: " << ntohs(info->enrolledId)
|
||||
<< ", 姓名: " << (const char *)info->enrolledUsername << ". palm vein: ("
|
||||
<< plamX << "," << plamY << " " << (ntohs(info->palmVeinInformation.x2) - plamX)
|
||||
<< "x" << (ntohs(info->palmVeinInformation.y2) - plamY)
|
||||
<< "), detection probability: "
|
||||
<< ntohs(info->palmVeinInformation.detectionProbability) / 1000.f
|
||||
<< ", quality: " << ntohs(info->palmVeinInformation.quality) / 1000.f;
|
||||
|
||||
emit newImageInfo(static_cast<MessageId>(replyId), width * height, info->md5);
|
||||
} else {
|
||||
auto info = reinterpret_cast<const EnrollReply *>(data + 7);
|
||||
userId = ntohs(info->userid);
|
||||
LOG_CAT(info, GUI) << "注册成功,用户ID: " << userId;
|
||||
}
|
||||
emit newEnrollResult(userId);
|
||||
} else if (result == Failed4Timeout) {
|
||||
LOG_CAT(info, GUI) << "识别超时。";
|
||||
LOG_CAT(info, GUI) << "录入超时。";
|
||||
emit errorOccurred(NoteId::InteractWarning, "录入", "录入超时");
|
||||
} else if (result == Failed4PalmEnrolled) {
|
||||
emit errorOccurred(NoteId::InteractWarning, "手掌已被录入");
|
||||
LOG_CAT(info, GUI) << "手掌已被录入。";
|
||||
} else if (result == Failed4MaxUser) {
|
||||
emit errorOccurred(NoteId::InteractWarning, "录入", "注册已达上限");
|
||||
} else {
|
||||
LOG_CAT(info, GUI) << "未知错误(" << static_cast<int>(result) << ")。";
|
||||
}
|
||||
LOG_CAT(info, GUI) << Separator;
|
||||
break;
|
||||
}
|
||||
case GetImage: {
|
||||
if (result == Success) {
|
||||
auto info = reinterpret_cast<const ImageSliceReply *>(data + 7);
|
||||
uint32_t sliceSize = ntohl(info->size);
|
||||
emit newImageSliceData(std::vector<uint8_t>(info->data, info->data + sliceSize));
|
||||
} else {
|
||||
LOG(info) << "GetImage failed, status: " << static_cast<int>(result);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case DeleteUser: {
|
||||
if (result == Success) {
|
||||
LOG_CAT(info, GUI) << "模组: " << protocolDataFormatString(data, size);
|
||||
@ -171,49 +345,148 @@ void ModuleCommunication::processPackage(const uint8_t *data, uint16_t size) {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case RequestPalmFeature: {
|
||||
LOG_CAT(info, GUI) << "模组: " << protocolDataFormatString(data, size);
|
||||
case UploadImageInfo: {
|
||||
break;
|
||||
}
|
||||
case UploadImageData: {
|
||||
auto info = reinterpret_cast<const UploadImageReply *>(data + 7);
|
||||
if (result == Success) {
|
||||
auto info = reinterpret_cast<const PalmFeatureHeader *>(data + 7);
|
||||
LOG_CAT(info, GUI) << "用户ID: " << ntohs(info->userid)
|
||||
<< ", 用户名: " << std::string_view(reinterpret_cast<const char *>(info->username))
|
||||
<< ", 特征值长度: " << ntohs(info->featureTotalSize);
|
||||
PalmFeature feature;
|
||||
feature.username = std::string_view(reinterpret_cast<const char *>(info->username));
|
||||
const uint8_t *start = data + 7 + sizeof(PalmFeatureHeader);
|
||||
feature.feature = std::vector<uint8_t>(start, start + ntohs(info->featureTotalSize));
|
||||
emit newPalmFeature(feature);
|
||||
if (info->operation == 0) {
|
||||
uint16_t userid = ntohs(info->userid);
|
||||
LOG_CAT(info, GUI) << "图片下发注册成功,用户ID: " << userid;
|
||||
emit newEnrollResult(userid);
|
||||
} else if (info->operation == 1) {
|
||||
auto result = reinterpret_cast<const UploadImageVerifyReply *>(info);
|
||||
LOG_CAT(info, GUI) << "图片下发识别成功,用户ID: " << ntohs(result->userid) << ", 用户名: "
|
||||
<< std::string_view(reinterpret_cast<const char *>(result->result.username))
|
||||
<< ", 耗时: " << ntohs(result->result.elapsed) << "ms";
|
||||
} else {
|
||||
LOG(warning) << "unknown upload image operation: " << info->operation;
|
||||
}
|
||||
LOG_CAT(info, GUI) << Separator;
|
||||
} else if (result == Needmore) {
|
||||
} else {
|
||||
LOG(info) << "upload image failed, operation: " << static_cast<uint16_t>(info->operation)
|
||||
<< ", status: " << static_cast<uint16_t>(result);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case GetCurrentStatus: {
|
||||
LOG_CAT(info, GUI) << "模组: " << protocolDataFormatString(data, size);
|
||||
LOG_CAT(info, GUI) << "模组当前状态: " << static_cast<int>(data[7]);
|
||||
LOG_CAT(info, GUI) << Separator;
|
||||
break;
|
||||
}
|
||||
case RegisterPalmFeature: {
|
||||
LOG_CAT(info, GUI) << "模组: " << protocolDataFormatString(data, size);
|
||||
if (result == Success) {
|
||||
LOG_CAT(info, GUI) << "掌静脉特征值注册成功。";
|
||||
case GetUniqueID: {
|
||||
auto id = reinterpret_cast<const ModuleId *>(data + 7);
|
||||
int idCount = 0;
|
||||
std::ostringstream oss;
|
||||
oss << "[";
|
||||
for (int byteIndex = 0; byteIndex < sizeof(id->userids); byteIndex++) {
|
||||
for (uint8_t bitPosition = 0; bitPosition < 8; bitPosition++) {
|
||||
if (id->userids[byteIndex] & (1 << bitPosition)) {
|
||||
uint16_t uid = byteIndex * 8 + bitPosition;
|
||||
if (uid >= 1) {
|
||||
oss << uid << ", ";
|
||||
idCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
oss << "]";
|
||||
LOG_CAT(info, GUI) << "模组ID: " << std::string_view(id->id, sizeof(id->id)) << ", user ids[" << idCount
|
||||
<< "]: " << oss.str();
|
||||
LOG_CAT(info, GUI) << Separator;
|
||||
break;
|
||||
}
|
||||
case GetVersion: {
|
||||
LOG_CAT(info, GUI) << "模组: " << protocolDataFormatString(data, size);
|
||||
auto version = reinterpret_cast<const ModuleVersion *>(data + 7);
|
||||
int length = std::strlen(version->version);
|
||||
if (length > sizeof(version->version)) {
|
||||
length = sizeof(version->version);
|
||||
}
|
||||
int ota = ntohl(version->otaVersion);
|
||||
LOG_CAT(info, GUI) << "模组烧录版本: " << std::string_view(version->version, length)
|
||||
<< ", OTA版本: " << ntohl(version->otaVersion);
|
||||
LOG_CAT(info, GUI) << Separator;
|
||||
m_verison = QString::fromLocal8Bit(version->version, length);
|
||||
m_otaVerison = ota;
|
||||
emit verisonChanged();
|
||||
break;
|
||||
}
|
||||
case EnableDebug: {
|
||||
auto replyData = reinterpret_cast<const DebugReply *>(data + 7);
|
||||
LOG(info) << "module debug: " << replyData->message;
|
||||
std::error_code error;
|
||||
auto replyValue = boost::json::parse(replyData->message, error);
|
||||
if (error) {
|
||||
LOG(error) << error.message();
|
||||
} else {
|
||||
auto &reply = replyValue.as_object();
|
||||
if (reply.contains("command")) {
|
||||
auto &command = reply.at("command");
|
||||
if ((command == "get_configurations") || (command == "set_configurations")) {
|
||||
if (reply.contains("cdc_log_enabled")) {
|
||||
auto &cdcEnabled = reply.at("cdc_log_enabled").as_bool();
|
||||
if (m_cdcDebugEnabled != cdcEnabled) {
|
||||
m_cdcDebugEnabled = cdcEnabled;
|
||||
emit cdcDebugEnabledChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
LOG(warning) << "unknown reply command: 0x" << (static_cast<int>(replyId) & 0xff)
|
||||
<< ", data: " << protocolDataFormatString(data, size);
|
||||
break;
|
||||
}
|
||||
m_currentMessageId = Idle;
|
||||
emit currentMessageIdChanged();
|
||||
emit commandFinished(static_cast<MessageId>(replyId), static_cast<MessageStatus>(result));
|
||||
break;
|
||||
}
|
||||
case Note: {
|
||||
uint8_t noteId = data[5];
|
||||
auto noteId = static_cast<NoteId>(data[5]);
|
||||
switch (noteId) {
|
||||
case Ready: {
|
||||
case NoteId::Ready: {
|
||||
LOG_CAT(info, GUI) << "模组: " << protocolDataFormatString(data, size);
|
||||
LOG_CAT(info, GUI) << "模组上电初始化成功。";
|
||||
LOG_CAT(info, GUI) << Separator;
|
||||
break;
|
||||
}
|
||||
case 0x01: { // 模组返回的数据为当前帧的手掌状态
|
||||
auto info = reinterpret_cast<const VerifyNoteInfo *>(data + 7);
|
||||
LOG(info) << info->state;
|
||||
case NoteId::PalmState: { // 模组返回的数据为当前帧的手掌状态
|
||||
auto state = reinterpret_cast<const PalmStateNote *>(data + 6);
|
||||
PalmState palmState = static_cast<PalmState>(ntohs(state->state));
|
||||
if (palmState == NeedMoveToCenter) {
|
||||
emit errorOccurred(NoteId::InteractWarning, "录入提示", "请将手掌置于画面中心");
|
||||
} else if (palmState == TooFar) {
|
||||
emit errorOccurred(NoteId::InteractWarning, "录入提示", "请将手掌靠近一点");
|
||||
} else if (palmState == ManyPalm) {
|
||||
emit errorOccurred(NoteId::InteractWarning, "录入提示", "检测到多个手掌");
|
||||
} else if (palmState == NoAlive) {
|
||||
emit errorOccurred(NoteId::InteractWarning, "录入提示", "活体检测未通过");
|
||||
}
|
||||
LOG(info) << "palm state: " << palmState;
|
||||
break;
|
||||
}
|
||||
case NoteId::UnknownError: {
|
||||
LOG_CAT(info, GUI) << "模组: " << protocolDataFormatString(data, size);
|
||||
LOG_CAT(info, GUI) << "未知错误。";
|
||||
LOG_CAT(info, GUI) << Separator;
|
||||
break;
|
||||
}
|
||||
case NoteId::DebugInfo: {
|
||||
auto message = reinterpret_cast<const char *>(data + 6);
|
||||
LOG_CAT(info, GUI) << "模组日志: " << message;
|
||||
break;
|
||||
}
|
||||
case NoteId::NoAliveImage: {
|
||||
LOG(info) << "no alive image";
|
||||
emit newImageInfo(Note, 600 * 800, nullptr);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@ -232,6 +505,7 @@ void ModuleCommunication::processPackage(const uint8_t *data, uint16_t size) {
|
||||
}
|
||||
|
||||
void ModuleCommunication::onReadyRead() {
|
||||
// LOG(info) << "ModuleCommunication::onReadyRead";
|
||||
m_receivedBuffer.append(m_serialPort->readAll());
|
||||
while (m_receivedBuffer.size() >= 2) {
|
||||
int beginIndex = -1;
|
||||
@ -249,6 +523,7 @@ void ModuleCommunication::onReadyRead() {
|
||||
m_receivedBuffer.remove(0, beginIndex);
|
||||
beginIndex = 0;
|
||||
}
|
||||
|
||||
if (m_receivedBuffer.size() < 5) break;
|
||||
uint16_t packageSize = *reinterpret_cast<uint16_t *>(m_receivedBuffer.data() + 3);
|
||||
packageSize = ntohs(packageSize);
|
||||
@ -268,9 +543,15 @@ void ModuleCommunication::onReadyRead() {
|
||||
}
|
||||
}
|
||||
|
||||
void ModuleCommunication::onErrorOccurred(QSerialPort::SerialPortError error) {
|
||||
if (error == QSerialPort::NoError) return;
|
||||
LOG_CAT(info, GUI) << m_serialPort->portName().toStdString() << ": " << m_serialPort->errorString().toStdString();
|
||||
emit errorOccurred(NoteId::DeviceError, m_serialPort->errorString());
|
||||
}
|
||||
|
||||
std::pair<uint8_t *, uint32_t> ModuleCommunication::generateFrame(MessageId command, const uint8_t *data,
|
||||
uint16_t size) {
|
||||
static uint8_t sendBuffer[1024] = {0};
|
||||
static uint8_t sendBuffer[4096] = {0};
|
||||
memset(sendBuffer, 0, sizeof(sendBuffer));
|
||||
sendBuffer[0] = 0xef;
|
||||
sendBuffer[1] = 0xaa;
|
||||
@ -284,6 +565,14 @@ std::pair<uint8_t *, uint32_t> ModuleCommunication::generateFrame(MessageId comm
|
||||
return std::make_pair(sendBuffer, size + 6);
|
||||
}
|
||||
|
||||
void ModuleCommunication::setCurrentMessageIdStatus(MessageId messageId) {
|
||||
if (m_currentMessageId != messageId) {
|
||||
m_currentMessageId = messageId;
|
||||
emit commandStarted(messageId);
|
||||
emit currentMessageIdChanged();
|
||||
}
|
||||
}
|
||||
|
||||
std::string ModuleCommunication::protocolDataFormatString(const uint8_t *data, int size) {
|
||||
std::ostringstream oss;
|
||||
for (int i = 0; i < size; i++) {
|
||||
@ -291,3 +580,15 @@ std::string ModuleCommunication::protocolDataFormatString(const uint8_t *data, i
|
||||
}
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
BOOST_DESCRIBE_ENUM(ModuleCommunication::PalmState, NoPalm, TooFar, NeedMoveToCenter, ManyPalm, NoAlive)
|
||||
namespace std {
|
||||
std::ostream &operator<<(std::ostream &stream, const ModuleCommunication::PalmState &element) {
|
||||
char const *r = "(unnamed)";
|
||||
boost::mp11::mp_for_each<boost::describe::describe_enumerators<ModuleCommunication::PalmState>>([&](auto D) {
|
||||
if (element == D.value) r = D.name;
|
||||
});
|
||||
stream << r << "(" << static_cast<int>(element) << ")";
|
||||
return stream;
|
||||
}
|
||||
} // namespace std
|
||||
|
@ -1,33 +1,69 @@
|
||||
#ifndef MODULECOMMUNICATION_H
|
||||
#define MODULECOMMUNICATION_H
|
||||
|
||||
#include "Database.h"
|
||||
#include "DataStructure.h"
|
||||
#include <QObject>
|
||||
#include <QQmlEngine>
|
||||
#include <QSerialPort>
|
||||
#include <memory>
|
||||
|
||||
class QSerialPort;
|
||||
|
||||
class ModuleCommunication : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("Only created in C++...")
|
||||
static constexpr uint32_t UsernameSize = 32;
|
||||
static constexpr uint32_t VersionSize = 32;
|
||||
static constexpr const char *Separator = "----------";
|
||||
Q_PROPERTY(QString verison MEMBER m_verison NOTIFY verisonChanged)
|
||||
Q_PROPERTY(int otaVerison MEMBER m_otaVerison NOTIFY verisonChanged)
|
||||
Q_PROPERTY(bool cdcDebugEnabled MEMBER m_cdcDebugEnabled NOTIFY cdcDebugEnabledChanged)
|
||||
|
||||
public:
|
||||
constexpr static uint16_t VendorIdentifier = 0x3346;
|
||||
constexpr static uint16_t ProductIdentifier = 0x0001;
|
||||
constexpr static uint16_t MaxUserCount = 4000;
|
||||
constexpr static uint16_t InvalidUserId = std::numeric_limits<uint16_t>::max();
|
||||
enum MessageId : uint8_t {
|
||||
Reply = 0,
|
||||
Note = 0x01,
|
||||
Reset = 0x10,
|
||||
GetCurrentStatus = 0x11,
|
||||
Verify = 0x12,
|
||||
VerifyExtended = 0x16,
|
||||
EnrollSingle = 0x1D,
|
||||
EnrollExtended = 0x1E,
|
||||
GetImage = 0x1F, // 获取图片数据,通过VerifyExtended或EnrollExtended保存的
|
||||
DeleteUser = 0x20,
|
||||
DeleteAll = 0x21,
|
||||
RegisterPalmFeature = 0xF9,
|
||||
RequestPalmFeature = 0xFA,
|
||||
GetVersion = 0x30,
|
||||
StartOta = 0x40, // 模组进入boot模式进行ota升级
|
||||
EnableDebug = 0x82,
|
||||
GetUniqueID = 0xAC,
|
||||
UploadImageInfo = 0xF6,
|
||||
UploadImageData = 0xF7,
|
||||
Idle = 0xFF,
|
||||
};
|
||||
Q_ENUM(MessageId)
|
||||
|
||||
enum class NoteId : uint8_t {
|
||||
Ready = 0x00,
|
||||
PalmState = 0x01,
|
||||
UnknownError = 0x02,
|
||||
DebugInfo = 0x55,
|
||||
NoAliveImage = 0x56,
|
||||
|
||||
DeviceError = 0xF0, // 系统设备出现的错误,例如 QSerialPort
|
||||
InteractWarning,
|
||||
};
|
||||
|
||||
enum NoteId : uint8_t {
|
||||
Ready = 0x00,
|
||||
enum PalmState {
|
||||
NoPalm = 1,
|
||||
TooFar = 6,
|
||||
NeedMoveToCenter = 15,
|
||||
ManyPalm = 101, // 太多掌静脉
|
||||
NoAlive = 122,
|
||||
};
|
||||
|
||||
enum MessageStatus : uint8_t {
|
||||
Success = 0,
|
||||
Rejected = 1,
|
||||
@ -38,83 +74,193 @@ public:
|
||||
Failed4NoMemory = 7,
|
||||
Failed4UnknownUser = 8,
|
||||
Failed4MaxUser = 9,
|
||||
Failed4FaceEnrolled = 10,
|
||||
Failed4PalmEnrolled = 10,
|
||||
Failed4LivenessCheck = 12,
|
||||
Failed4Timeout = 13,
|
||||
Failed4Authorization = 14,
|
||||
Failed4ReadFile = 19,
|
||||
Failed4WriteFile = 20,
|
||||
Failed4NoEncrypt = 21,
|
||||
PalmNotDetected = 23,
|
||||
Needmore = 25,
|
||||
};
|
||||
|
||||
#pragma pack(1)
|
||||
struct VerifyInfo {
|
||||
uint8_t powerDownRightAway; // power down right away after verifying
|
||||
uint8_t timeout; // timeout, unit second, default 10s
|
||||
struct VerifyRequest {
|
||||
uint8_t save_image;
|
||||
uint8_t timeout; // timeout, unit second, default 10s
|
||||
uint16_t interval = 0;
|
||||
uint8_t reserved[4];
|
||||
};
|
||||
struct VerifyNoteInfo {
|
||||
int16_t state; // corresponding to PALM_STATE_*
|
||||
|
||||
struct PalmStateNote {
|
||||
uint16_t 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
|
||||
uint16_t left; // in pixel
|
||||
uint16_t top;
|
||||
uint16_t right;
|
||||
uint16_t bottom;
|
||||
};
|
||||
|
||||
struct EnrollData {
|
||||
uint8_t admin = 0;
|
||||
struct EnrollRequest {
|
||||
uint8_t username[32];
|
||||
uint8_t palmDirection;
|
||||
uint8_t strictMode = 0;
|
||||
uint8_t excludeMode = 0; // 掌静脉库里面是否已经存在 0:不进行验证 1:只进行比对 2:如果存在,则现在不予录入
|
||||
uint8_t skipSave = 0;
|
||||
uint8_t timeout;
|
||||
uint8_t reserved[4];
|
||||
};
|
||||
|
||||
struct EnrollDataReply {
|
||||
struct EnrollReply {
|
||||
uint16_t userid;
|
||||
uint8_t face_direction; // depleted, user ignore this field
|
||||
uint8_t operation;
|
||||
};
|
||||
|
||||
struct VerifyDataReply {
|
||||
struct PalmVeinInformation {
|
||||
uint16_t x1;
|
||||
uint16_t y1;
|
||||
uint16_t x2;
|
||||
uint16_t y2;
|
||||
uint16_t detectionProbability;
|
||||
uint16_t quality;
|
||||
};
|
||||
|
||||
struct EnrollExtendedReply {
|
||||
uint16_t userid;
|
||||
uint8_t username[UsernameSize]; // 32Bytes uint8_t admin;
|
||||
uint8_t unlockStatus;
|
||||
uint16_t imageWidth;
|
||||
uint16_t imageHeight;
|
||||
uint8_t imageFormat; // 0: 只有Y分量,灰度图
|
||||
uint8_t md5[16];
|
||||
uint8_t enrolledUsername[32];
|
||||
uint16_t enrolledId; // 掌静脉库里已经存在的id
|
||||
PalmVeinInformation palmVeinInformation;
|
||||
uint8_t reserved[4];
|
||||
};
|
||||
|
||||
struct PalmFeatureHeader {
|
||||
uint16_t userid; // 用户ID
|
||||
uint8_t username[32]; // 用户姓名
|
||||
uint8_t admin; // 是否管理员,YES:1 NO:0
|
||||
uint8_t featureDataMd5[16]; // 整体特征数据的MD5值
|
||||
uint16_t featureTotalSize; // 特征数据总长度
|
||||
struct ImageSliceRequest {
|
||||
uint32_t offset;
|
||||
uint32_t size;
|
||||
};
|
||||
|
||||
struct ImageSliceReply {
|
||||
uint32_t size;
|
||||
uint8_t data[0];
|
||||
};
|
||||
|
||||
struct VerifyReply {
|
||||
uint16_t userid;
|
||||
uint8_t username[UsernameSize]; // 32Bytes
|
||||
uint16_t elapsed; // 此时识别耗时时间
|
||||
};
|
||||
|
||||
struct VerifyExtendReply : public VerifyReply {
|
||||
uint16_t image_width;
|
||||
uint16_t image_height;
|
||||
uint8_t image_format; // 0: 只有Y分量,灰度图
|
||||
uint8_t md5[16];
|
||||
PalmVeinInformation palmVeinInformation;
|
||||
};
|
||||
|
||||
struct UploadImageInformation {
|
||||
uint8_t operation; // 0:图片录入掌静脉
|
||||
uint8_t format; // 0: 灰度图(纯Y分量)
|
||||
uint16_t width;
|
||||
uint16_t height;
|
||||
uint32_t size;
|
||||
uint8_t md5[16]; // 图片内容 md5 值
|
||||
char username[32];
|
||||
};
|
||||
|
||||
struct UploadImageDataSlice {
|
||||
uint32_t offset;
|
||||
uint32_t size;
|
||||
uint8_t reserved[4]; // 预留
|
||||
uint8_t data[0];
|
||||
};
|
||||
|
||||
struct UploadImageReply : public EnrollReply {};
|
||||
struct UploadImageVerifyReply : public UploadImageReply {
|
||||
VerifyReply result;
|
||||
};
|
||||
|
||||
struct ModuleVersion {
|
||||
char version[VersionSize];
|
||||
uint32_t otaVersion;
|
||||
};
|
||||
|
||||
struct ModuleId {
|
||||
char id[32];
|
||||
uint8_t userids[512]; // bit位
|
||||
};
|
||||
|
||||
struct DebugRequest {
|
||||
uint16_t length;
|
||||
char message[2000];
|
||||
};
|
||||
using DebugReply = DebugRequest;
|
||||
|
||||
#pragma pack()
|
||||
explicit ModuleCommunication(QObject *parent = nullptr);
|
||||
bool open(const QString &portName);
|
||||
bool open(const QString &portName, int baudRate);
|
||||
void verify(uint8_t timeout);
|
||||
void verifyExtended(bool captureImage, uint8_t timeout);
|
||||
void reset();
|
||||
|
||||
void enroll(const std::string &username, uint8_t timeout);
|
||||
void deleteUser(uint16_t userid);
|
||||
void deleteAll();
|
||||
void enroll(const std::string &username, bool strictMode, uint8_t excludeMode, bool persistence, uint8_t timeout);
|
||||
void enrollExtended(const std::string &username, bool strictMode, uint8_t excludeMode, bool persistence,
|
||||
uint8_t timeout);
|
||||
Q_INVOKABLE void deleteUser(uint16_t userid);
|
||||
Q_INVOKABLE void deleteAll();
|
||||
Q_INVOKABLE void requestUniqueId();
|
||||
Q_INVOKABLE void requestVersion();
|
||||
void requestEnrolledImage(uint32_t offset, uint32_t size);
|
||||
|
||||
void requestPalmFeature(uint16_t userid);
|
||||
void enrollPalmFeature(uint16_t userid, const PalmFeature &feature);
|
||||
void uploadImageInfo(const UploadImageInformation &info);
|
||||
void uploadImageData(uint32_t offset, const uint8_t *data, uint32_t size);
|
||||
Q_INVOKABLE void requestCurrentStatus();
|
||||
Q_INVOKABLE void setDebugEnabled(bool enabled);
|
||||
void requestDebugSettings();
|
||||
void startOta();
|
||||
|
||||
MessageId currentMessageId() const;
|
||||
static std::string protocolDataFormatString(const uint8_t *data, int size);
|
||||
signals:
|
||||
/**
|
||||
* @brief newVerifyResult
|
||||
* @param userid
|
||||
* @param username
|
||||
* @param elapsed ms毫秒
|
||||
*/
|
||||
void newVerifyResult(uint16_t userid, const QString &username, uint16_t elapsed);
|
||||
void newEnrollResult(uint16_t userid);
|
||||
void newPalmFeature(const PalmFeature &feature);
|
||||
void newImageInfo(MessageId id, uint32_t size, const uint8_t *md5);
|
||||
void newImageSliceData(const std::vector<uint8_t> &data);
|
||||
void errorOccurred(NoteId note, const QString &error, const QString &detailMessage = "");
|
||||
void commandStarted(ModuleCommunication::MessageId messageId);
|
||||
void commandFinished(MessageId messageId, MessageStatus status);
|
||||
void currentMessageIdChanged();
|
||||
void verisonChanged();
|
||||
void cdcDebugEnabledChanged();
|
||||
|
||||
protected:
|
||||
void processPackage(const uint8_t *data, uint16_t size);
|
||||
void onReadyRead();
|
||||
void onErrorOccurred(QSerialPort::SerialPortError error);
|
||||
std::pair<uint8_t *, uint32_t> generateFrame(MessageId command, const uint8_t *data = nullptr, uint16_t size = 0);
|
||||
std::string protocolDataFormatString(const uint8_t *data, int size);
|
||||
void setCurrentMessageIdStatus(ModuleCommunication::MessageId messageId);
|
||||
|
||||
private:
|
||||
std::shared_ptr<QSerialPort> m_serialPort;
|
||||
QByteArray m_receivedBuffer;
|
||||
MessageId m_currentMessageId = ModuleCommunication::Idle;
|
||||
QString m_verison;
|
||||
int m_otaVerison;
|
||||
bool m_cdcDebugEnabled = false;
|
||||
};
|
||||
|
||||
namespace std {
|
||||
std::ostream &operator<<(std::ostream &stream, const ModuleCommunication::PalmState &element);
|
||||
} // namespace std
|
||||
|
||||
#endif // MODULECOMMUNICATION_H
|
||||
|
17
Analyser/PalmFeatureTableModel.cpp
Normal file
17
Analyser/PalmFeatureTableModel.cpp
Normal file
@ -0,0 +1,17 @@
|
||||
#include "PalmFeatureTableModel.h"
|
||||
|
||||
PalmFeatureTableModel::PalmFeatureTableModel(QObject *parent) {
|
||||
}
|
||||
|
||||
int PalmFeatureTableModel::rowCount(const QModelIndex &parent) const {
|
||||
return static_cast<int>(m_features.size());
|
||||
}
|
||||
|
||||
int PalmFeatureTableModel::columnCount(const QModelIndex &parent) const {
|
||||
return 2;
|
||||
}
|
||||
|
||||
QVariant PalmFeatureTableModel::data(const QModelIndex &index, int role) const {
|
||||
QVariant ret;
|
||||
return ret;
|
||||
}
|
19
Analyser/PalmFeatureTableModel.h
Normal file
19
Analyser/PalmFeatureTableModel.h
Normal file
@ -0,0 +1,19 @@
|
||||
#ifndef __PALMFEATURETABLEMODEL_H__
|
||||
#define __PALMFEATURETABLEMODEL_H__
|
||||
|
||||
#include "DataStructure.h"
|
||||
#include <QAbstractTableModel>
|
||||
|
||||
class PalmFeatureTableModel : public QAbstractTableModel {
|
||||
Q_OBJECT
|
||||
public:
|
||||
PalmFeatureTableModel(QObject *parent = nullptr);
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const final;
|
||||
int columnCount(const QModelIndex &parent = QModelIndex()) const final;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const final;
|
||||
|
||||
private:
|
||||
PalmFeatures m_features;
|
||||
};
|
||||
|
||||
#endif // __PALMFEATURETABLEMODEL_H__
|
26
Analyser/VideoFrameProvider.cpp
Normal file
26
Analyser/VideoFrameProvider.cpp
Normal file
@ -0,0 +1,26 @@
|
||||
#include "VideoFrameProvider.h"
|
||||
|
||||
VideoFrameProvider::VideoFrameProvider()
|
||||
: QQuickImageProvider(QQuickImageProvider::Image), m_image(600, 800, QImage::Format_RGB32) {
|
||||
m_image.fill(Qt::black);
|
||||
}
|
||||
|
||||
QImage VideoFrameProvider::requestImage(const QString &id, QSize *size, const QSize &requestedSize) {
|
||||
Q_UNUSED(id);
|
||||
|
||||
if (size) *size = m_image.size();
|
||||
|
||||
if (requestedSize.width() > 0 && requestedSize.height() > 0)
|
||||
return m_image.scaled(requestedSize.width(), requestedSize.height(), Qt::KeepAspectRatio);
|
||||
|
||||
return m_image;
|
||||
}
|
||||
|
||||
void VideoFrameProvider::setImage(const QImage &image) {
|
||||
m_image = image;
|
||||
}
|
||||
|
||||
void VideoFrameProvider::reset() {
|
||||
m_image = QImage(600, 800, QImage::Format_RGB32);
|
||||
m_image.fill(Qt::black);
|
||||
}
|
16
Analyser/VideoFrameProvider.h
Normal file
16
Analyser/VideoFrameProvider.h
Normal file
@ -0,0 +1,16 @@
|
||||
#ifndef __VIDEOFRAMEPROVIDER_H__
|
||||
#define __VIDEOFRAMEPROVIDER_H__
|
||||
|
||||
#include <QQuickImageProvider>
|
||||
|
||||
class VideoFrameProvider : public QQuickImageProvider {
|
||||
public:
|
||||
VideoFrameProvider();
|
||||
QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize) final;
|
||||
void setImage(const QImage &image);
|
||||
void reset();
|
||||
|
||||
private:
|
||||
QImage m_image;
|
||||
};
|
||||
#endif // __VIDEOFRAMEPROVIDER_H__
|
108
Analyser/VideoPlayer.cpp
Normal file
108
Analyser/VideoPlayer.cpp
Normal file
@ -0,0 +1,108 @@
|
||||
#include "VideoPlayer.h"
|
||||
#include "BoostLog.h"
|
||||
#include <QImage>
|
||||
extern "C" {
|
||||
#include <libavdevice/avdevice.h>
|
||||
#include <libavformat/avformat.h>
|
||||
}
|
||||
|
||||
VideoPlayer::VideoPlayer() {
|
||||
static bool init = false;
|
||||
if (!init) {
|
||||
avdevice_register_all();
|
||||
init = true;
|
||||
}
|
||||
}
|
||||
|
||||
void VideoPlayer::setFrameCallback(FrameCallback &&callback) {
|
||||
m_callback = std::move(callback);
|
||||
}
|
||||
|
||||
void VideoPlayer::setErrorCallback(ErrorCallback &&callback) {
|
||||
m_errorCallback = std::move(callback);
|
||||
}
|
||||
|
||||
VideoPlayer::~VideoPlayer() {
|
||||
close();
|
||||
}
|
||||
|
||||
bool VideoPlayer::open(const std::string &deviceName) {
|
||||
bool ret = true;
|
||||
m_formatContext = avformat_alloc_context();
|
||||
#ifdef WIN32
|
||||
constexpr auto format = "dshow";
|
||||
std::ostringstream oss;
|
||||
oss << "video=@device_pnp_" << deviceName;
|
||||
auto device = oss.str();
|
||||
#else
|
||||
constexpr auto format = "v4l2";
|
||||
auto &device = deviceName;
|
||||
#endif
|
||||
auto inputFormat = av_find_input_format(format);
|
||||
|
||||
AVDictionary *dictionary = nullptr;
|
||||
// clang-format off
|
||||
// ffplay -f dshow -i video="@device_pnp_\\?\usb#vid_3346&pid_0001&mi_00#6&6ff22b9&0&0000#{65e8773d-8f56-11d0-a3b9-00a0c9223196}\global"
|
||||
// ffmpeg -f dshow -list_options true -i video="UVC Camera"
|
||||
// clang-format on
|
||||
av_dict_set(&dictionary, "video_size", "800*600", 0);
|
||||
|
||||
int status = avformat_open_input(&m_formatContext, device.c_str(), inputFormat, &dictionary);
|
||||
if (status != 0) {
|
||||
char message[256] = {0};
|
||||
av_make_error_string(message, sizeof(message), status);
|
||||
LOG(error) << "open device[" << device << "] failed: " << status << "," << message;
|
||||
ret = false;
|
||||
} else {
|
||||
av_dump_format(m_formatContext, 0, device.c_str(), 0);
|
||||
// for (unsigned int i = 0; i < m_formatContext->nb_streams; i++) {
|
||||
// AVStream *stream = m_formatContext->streams[i];
|
||||
// if (stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
|
||||
// std::cout << "当前分辨率: " << stream->codecpar->width << "x" << stream->codecpar->height << std::endl;
|
||||
// }
|
||||
// }
|
||||
m_exit = false;
|
||||
m_thread = std::thread(&VideoPlayer::run, this);
|
||||
}
|
||||
av_dict_free(&dictionary);
|
||||
return ret;
|
||||
}
|
||||
|
||||
void VideoPlayer::close() {
|
||||
m_exit = true;
|
||||
if (m_thread.joinable()) {
|
||||
m_thread.join();
|
||||
}
|
||||
avformat_close_input(&m_formatContext);
|
||||
avformat_free_context(m_formatContext);
|
||||
m_formatContext = nullptr;
|
||||
}
|
||||
|
||||
void VideoPlayer::run() {
|
||||
auto packet = av_packet_alloc();
|
||||
while (!m_exit) {
|
||||
auto status = av_read_frame(m_formatContext, packet);
|
||||
if (status == 0) {
|
||||
QImage image;
|
||||
image.loadFromData(packet->data, packet->size);
|
||||
QTransform transform;
|
||||
transform.rotate(90);
|
||||
image = image.transformed(transform);
|
||||
if (!image.isNull() && m_callback) m_callback(image);
|
||||
} else {
|
||||
char message[256] = {0};
|
||||
av_make_error_string(message, sizeof(message), status);
|
||||
if (m_errorCallback) m_errorCallback(status, message);
|
||||
LOG(error) << "av_read_frame() failed: " << status << "," << message;
|
||||
if (status == -EIO) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
av_packet_unref(packet);
|
||||
}
|
||||
|
||||
av_packet_free(&packet);
|
||||
QImage image(800, 600, QImage::Format_RGB888);
|
||||
image.fill(Qt::black);
|
||||
if (m_callback) m_callback(image);
|
||||
}
|
34
Analyser/VideoPlayer.h
Normal file
34
Analyser/VideoPlayer.h
Normal file
@ -0,0 +1,34 @@
|
||||
#ifndef __VIDEOPLAYER_H__
|
||||
#define __VIDEOPLAYER_H__
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
class QImage;
|
||||
struct AVFormatContext;
|
||||
|
||||
class VideoPlayer {
|
||||
public:
|
||||
using FrameCallback = std::function<void(const QImage &image)>;
|
||||
using ErrorCallback = std::function<void(int error, const std::string &message)>;
|
||||
VideoPlayer();
|
||||
void setFrameCallback(FrameCallback &&callback);
|
||||
void setErrorCallback(ErrorCallback &&callback);
|
||||
~VideoPlayer();
|
||||
bool open(const std::string &deviceName);
|
||||
void close();
|
||||
|
||||
protected:
|
||||
void run();
|
||||
|
||||
private:
|
||||
AVFormatContext *m_formatContext = nullptr;
|
||||
bool m_exit = true;
|
||||
std::thread m_thread;
|
||||
|
||||
FrameCallback m_callback;
|
||||
ErrorCallback m_errorCallback;
|
||||
};
|
||||
|
||||
#endif // __VIDEOPLAYER_H__
|
@ -1,280 +0,0 @@
|
||||
#include "Widget.h"
|
||||
#include "BoostLog.h"
|
||||
#include "CategoryLogSinkBackend.h"
|
||||
#include "ModuleCommunication.h"
|
||||
#include <QComboBox>
|
||||
#include <QFormLayout>
|
||||
#include <QGridLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QSerialPortInfo>
|
||||
#include <QTabWidget>
|
||||
#include <QTextBrowser>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
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 connectLayout = new QHBoxLayout();
|
||||
connectLayout->addWidget(serialGroupBox);
|
||||
connectLayout->addWidget(initializeUvcGroupBox());
|
||||
|
||||
m_logBrowser = new QTextBrowser();
|
||||
m_logBrowser->setReadOnly(true);
|
||||
auto logLayout = new QVBoxLayout();
|
||||
m_logBrowser->setLayout(logLayout);
|
||||
auto btn = new QPushButton("清空");
|
||||
connect(btn, &QPushButton::clicked, this, &Widget::onClearLogButtonClicked);
|
||||
logLayout->addWidget(btn, 0, Qt::AlignBottom | Qt::AlignRight);
|
||||
|
||||
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);
|
||||
|
||||
m_database = std::make_shared<Database>();
|
||||
|
||||
QTimer::singleShot(0, this, [this]() {
|
||||
onSerialRefreshButtonClicked();
|
||||
m_commandGroupBox->setEnabled(false);
|
||||
if (!m_database->open("database.db")) {
|
||||
LOG(error) << "open database failed.";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Widget::initializeLogger() {
|
||||
auto backend = boost::make_shared<CategoryLogSinkBackend>("GUI", m_logBrowser);
|
||||
using SynchronousCategorySink = boost::log::sinks::synchronous_sink<CategoryLogSinkBackend>;
|
||||
auto sink = boost::make_shared<SynchronousCategorySink>(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);
|
||||
|
||||
auto resetButton = new QPushButton("复位");
|
||||
connect(resetButton, &QPushButton::clicked, this, &Widget::onResetButtonClicked);
|
||||
layout->addWidget(resetButton, 2, 0);
|
||||
|
||||
ret->setLayout(layout);
|
||||
return ret;
|
||||
}
|
||||
|
||||
void Widget::onClearLogButtonClicked() {
|
||||
m_logBrowser->clear();
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
m_palmFeatureEdit = new QLineEdit("");
|
||||
layout->addRow("用户ID:", m_palmFeatureEdit);
|
||||
|
||||
auto button1 = new QPushButton("特征值上报");
|
||||
layout->addRow("", button1);
|
||||
connect(button1, &QPushButton::clicked, this, &Widget::onRequestPalmFeatureButtonClicked);
|
||||
|
||||
auto button = new QPushButton("特征值下发");
|
||||
connect(button, &QPushButton::clicked, this, &Widget::onRegisterPalmFeatureButtonClicked);
|
||||
layout->addRow("", button);
|
||||
|
||||
ret->setLayout(layout);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
QGroupBox *Widget::initializeUvcGroupBox() {
|
||||
auto ret = new QGroupBox("UVC设置");
|
||||
auto uvcLayout = new QGridLayout();
|
||||
auto label = new QLabel("设备名");
|
||||
uvcLayout->addWidget(label, 0, 0);
|
||||
|
||||
auto comboBox = new QComboBox();
|
||||
uvcLayout->addWidget(comboBox, 0, 1);
|
||||
|
||||
auto uvcRefreshButton = new QPushButton("刷新");
|
||||
auto uvcConnectButton = new QPushButton("连接");
|
||||
uvcLayout->addWidget(uvcRefreshButton, 1, 0);
|
||||
uvcLayout->addWidget(uvcConnectButton, 1, 1);
|
||||
ret->setLayout(uvcLayout);
|
||||
return ret;
|
||||
}
|
||||
|
||||
void Widget::onNewPalmFeature(const PalmFeature &feature) {
|
||||
if (!m_database->addPalmFeature(feature)) {
|
||||
LOG(error) << "add palm feature failed.";
|
||||
}
|
||||
|
||||
auto palms = m_database->palmFeatures();
|
||||
for (auto &p : palms) {
|
||||
LOG(info) << p.id << " " << p.username << " " << p.feature.size();
|
||||
}
|
||||
}
|
||||
|
||||
void Widget::onSerialConnectButtonClicked() {
|
||||
auto button = dynamic_cast<QPushButton *>(sender());
|
||||
if (button == nullptr) return;
|
||||
auto text = button->text();
|
||||
if (text == "连接") {
|
||||
auto portName = m_serialComboBox->currentText();
|
||||
m_communication = std::make_shared<ModuleCommunication>();
|
||||
connect(m_communication.get(), &ModuleCommunication::newPalmFeature, this, &Widget::onNewPalmFeature);
|
||||
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::onUvcRefreshButtonClicked() {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
void Widget::onRequestPalmFeatureButtonClicked() {
|
||||
if (!m_communication) return;
|
||||
auto id = m_palmFeatureEdit->text().toInt();
|
||||
m_communication->requestPalmFeature(id);
|
||||
}
|
||||
|
||||
void Widget::onRegisterPalmFeatureButtonClicked() {
|
||||
if (!m_communication) return;
|
||||
auto features = m_database->palmFeatures();
|
||||
if (features.empty()) {
|
||||
LOG(error) << "feature is empty.";
|
||||
return;
|
||||
}
|
||||
m_communication->enrollPalmFeature(264, features.at(0));
|
||||
}
|
||||
|
||||
void Widget::onResetButtonClicked() {
|
||||
if (!m_communication) return;
|
||||
m_communication->reset();
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
#ifndef WIDGET_H
|
||||
#define WIDGET_H
|
||||
|
||||
#include "Database.h"
|
||||
#include <QWidget>
|
||||
|
||||
class QPushButton;
|
||||
class QTextBrowser;
|
||||
class QComboBox;
|
||||
class QLineEdit;
|
||||
class QGroupBox;
|
||||
class ModuleCommunication;
|
||||
|
||||
class Widget : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit Widget(QWidget *parent = nullptr);
|
||||
void initializeLogger();
|
||||
|
||||
protected:
|
||||
QGroupBox *initializeCommandGroupBox();
|
||||
void onClearLogButtonClicked();
|
||||
void onSerialConnectButtonClicked();
|
||||
void onSerialRefreshButtonClicked();
|
||||
|
||||
void onUvcRefreshButtonClicked();
|
||||
|
||||
void onEnrollButtonClicked();
|
||||
void onVerifyButtonClicked();
|
||||
void onDeleteAllButtonClicked();
|
||||
void onDeleteButtonClicked();
|
||||
void onRequestPalmFeatureButtonClicked();
|
||||
void onRegisterPalmFeatureButtonClicked();
|
||||
void onResetButtonClicked();
|
||||
|
||||
QGroupBox *initializeEnrollGroupBox();
|
||||
QGroupBox *initializeVerifyGroupBox();
|
||||
QGroupBox *initializeDeleteGroupBox();
|
||||
QGroupBox *initializePalmFeatureGroupBox();
|
||||
|
||||
QGroupBox *initializeUvcGroupBox();
|
||||
|
||||
void onNewPalmFeature(const PalmFeature &feature);
|
||||
|
||||
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;
|
||||
|
||||
QLineEdit *m_palmFeatureEdit = nullptr;
|
||||
|
||||
std::shared_ptr<ModuleCommunication> m_communication;
|
||||
std::shared_ptr<Database> m_database;
|
||||
};
|
||||
|
||||
#endif // WIDGET_H
|
@ -1,21 +1,16 @@
|
||||
#include "Application.h"
|
||||
#include "BoostLog.h"
|
||||
#include "Widget.h"
|
||||
#include <QApplication>
|
||||
#include <QFont>
|
||||
#include "Configuration.h"
|
||||
#include <QQuickStyle>
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
using namespace Amass;
|
||||
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();
|
||||
LOG(info) << "Compiled on: " << __DATE__ << " " << __TIME__ << std::endl;
|
||||
LOG(info) << "Git commit ID: " << GIT_COMMIT_ID << std::endl;
|
||||
LOG(info) << "Program version: " << APP_VERSION << std::endl;
|
||||
QQuickStyle::setStyle("Basic"); // Basic Material
|
||||
auto app = Singleton<Application>::instance<Construct>(argc, argv);
|
||||
app->initializeLogger();
|
||||
return app->exec();
|
||||
}
|
||||
|
80
Analyser/qml/ConnectionItem.qml
Normal file
80
Analyser/qml/ConnectionItem.qml
Normal file
@ -0,0 +1,80 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Analyser
|
||||
|
||||
RowLayout {
|
||||
GroupBox {
|
||||
title: "串口设置"
|
||||
Layout.fillWidth: true
|
||||
GridLayout {
|
||||
columns: 2
|
||||
Text {
|
||||
text: qsTr("端口")
|
||||
}
|
||||
ComboBox {
|
||||
id: serialPort
|
||||
enabled: !App.connected
|
||||
implicitWidth: 116
|
||||
}
|
||||
|
||||
Text {
|
||||
text: qsTr("波特率")
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
id: baudrate
|
||||
enabled: !App.connected
|
||||
implicitWidth: 116
|
||||
model: ["2000000", "115200"]
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "刷新"
|
||||
enabled: !App.connected
|
||||
onClicked: {
|
||||
serialPort.model = App.availableSerialPorts()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: App.connected ? "断开" : "连接"
|
||||
onClicked: App.connected ? App.close() : App.open(
|
||||
serialPort.currentText,
|
||||
parseInt(baudrate.currentText))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GroupBox {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
title: "UVC设置"
|
||||
GridLayout {
|
||||
columns: 2
|
||||
Text {
|
||||
text: qsTr("设备名")
|
||||
}
|
||||
ComboBox {
|
||||
id: uvcs
|
||||
textRole: "name"
|
||||
enabled: !App.uvcOpened
|
||||
implicitWidth: 150
|
||||
}
|
||||
Button {
|
||||
enabled: !App.uvcOpened
|
||||
text: "刷新"
|
||||
onClicked: uvcs.model = App.availableUsbVideoCameras()
|
||||
}
|
||||
Button {
|
||||
text: App.uvcOpened ? "关闭" : "连接"
|
||||
onClicked: App.uvcOpened ? App.closeUVC() : App.openUVC(uvcs.currentValue.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
serialPort.model = App.availableSerialPorts()
|
||||
uvcs.model = App.availableUsbVideoCameras()
|
||||
}
|
||||
}
|
135
Analyser/qml/EnrollVerifyOperations.qml
Normal file
135
Analyser/qml/EnrollVerifyOperations.qml
Normal file
@ -0,0 +1,135 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Analyser
|
||||
|
||||
RowLayout {
|
||||
id: control
|
||||
GroupBox {
|
||||
title: "注册用户"
|
||||
GridLayout {
|
||||
columns: 2
|
||||
Label {
|
||||
text: qsTr("用户姓名")
|
||||
}
|
||||
TextField {
|
||||
id: enrollName
|
||||
implicitWidth: 100
|
||||
}
|
||||
Label {
|
||||
text: qsTr("严格模式")
|
||||
}
|
||||
Switch {
|
||||
id: strictMode
|
||||
}
|
||||
Label {
|
||||
text: qsTr("互斥模式")
|
||||
}
|
||||
ComboBox {
|
||||
id: excludeMode
|
||||
model: ["无", "仅比对", "互斥"]
|
||||
}
|
||||
Label {
|
||||
text: qsTr("超时时间")
|
||||
}
|
||||
TextField {
|
||||
id: enrollTimeout
|
||||
implicitWidth: 100
|
||||
text: "30"
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr("持久化")
|
||||
}
|
||||
Switch {
|
||||
id: persistence
|
||||
checked: true
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr("保存图片")
|
||||
}
|
||||
Switch {
|
||||
id: extendedMode
|
||||
}
|
||||
|
||||
Button {
|
||||
property bool enrolling: App.module ? (App.module.currentMessageId
|
||||
=== ModuleCommunication.EnrollSingle)
|
||||
|| (App.module.currentMessageId === ModuleCommunication.EnrollExtended) : false
|
||||
text: enrolling ? "取消" : "注册"
|
||||
onClicked: {
|
||||
if (enrolling) {
|
||||
App.module.reset()
|
||||
} else if (extendedMode.checked) {
|
||||
App.enrollExtended(enrollName.text, strictMode.checked,
|
||||
excludeMode.currentIndex,
|
||||
persistence.checked,
|
||||
parseInt(enrollTimeout.text))
|
||||
} else {
|
||||
App.enroll(enrollName.text, strictMode.checked,
|
||||
excludeMode.currentIndex,
|
||||
persistence.checked,
|
||||
parseInt(enrollTimeout.text))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GroupBox {
|
||||
id: verifyBox
|
||||
title: "识别用户"
|
||||
property bool verifying: (App.currentMessageId == ModuleCommunication.Verify) || (App.currentMessageId == ModuleCommunication.VerifyExtended)
|
||||
GridLayout {
|
||||
columns: 2
|
||||
Label {
|
||||
text: qsTr("超时时间(s)")
|
||||
}
|
||||
TextField {
|
||||
id: verifyTimeout
|
||||
implicitWidth: 80
|
||||
text: "10"
|
||||
}
|
||||
Label {
|
||||
text: qsTr("持续识别")
|
||||
}
|
||||
Switch {
|
||||
checked: (App.persistenceCommand == ModuleCommunication.Verify) || (App.persistenceCommand == ModuleCommunication.VerifyExtended)
|
||||
onToggled: {
|
||||
if (checked) {
|
||||
App.persistenceCommand = extendedVerifyMode.checked ? ModuleCommunication.VerifyExtended : ModuleCommunication.Verify
|
||||
} else {
|
||||
App.persistenceCommand = ModuleCommunication.Idle
|
||||
}
|
||||
}
|
||||
}
|
||||
Label {
|
||||
text: qsTr("保存图片")
|
||||
}
|
||||
Switch {
|
||||
id: extendedVerifyMode
|
||||
}
|
||||
Label {
|
||||
text: qsTr("识别间隔(s)")
|
||||
}
|
||||
TextField {
|
||||
id: verifyIntetval
|
||||
implicitWidth: 80
|
||||
text: App.persistenceVerifyInterval
|
||||
}
|
||||
Item {}
|
||||
Button {
|
||||
text: verifyBox.verifying ? "停止" : "识别"
|
||||
onClicked: {
|
||||
if (verifyBox.verifying) {
|
||||
App.resetModule()
|
||||
} else {
|
||||
App.persistenceVerifyInterval = parseInt(verifyIntetval.text)
|
||||
App.verify(extendedVerifyMode.checked, parseInt(verifyTimeout.text))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
153
Analyser/qml/ExtendedOperations.qml
Normal file
153
Analyser/qml/ExtendedOperations.qml
Normal file
@ -0,0 +1,153 @@
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Dialogs
|
||||
import QtQuick.Layouts
|
||||
import Analyser
|
||||
|
||||
Item {
|
||||
id: control
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
enabled: App.connected
|
||||
Column {
|
||||
GroupBox {
|
||||
title: "删除用户"
|
||||
GridLayout {
|
||||
columns: 2
|
||||
Label {
|
||||
text: qsTr("用户ID")
|
||||
}
|
||||
TextField {
|
||||
id: deleteUserId
|
||||
implicitWidth: 100
|
||||
}
|
||||
Button {
|
||||
text: "删除"
|
||||
onClicked: App.deleteUser(parseInt(deleteUserId.text))
|
||||
}
|
||||
Button {
|
||||
text: "删除所有"
|
||||
onClicked: App.deleteAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
GroupBox {
|
||||
title: "其它"
|
||||
enabled: App.connected
|
||||
GridLayout {
|
||||
columns: 2
|
||||
Button {
|
||||
text: "复位"
|
||||
onClicked: App.module.reset()
|
||||
}
|
||||
Button {
|
||||
text: "状态查询"
|
||||
onClicked: App.module.requestCurrentStatus()
|
||||
}
|
||||
Button {
|
||||
text: "ID查询"
|
||||
onClicked: App.module.requestUniqueId()
|
||||
}
|
||||
Button {
|
||||
id: otaButton
|
||||
text: "OTA升级"
|
||||
onClicked: loader.active = true
|
||||
}
|
||||
Row {
|
||||
Label {
|
||||
text: qsTr("日志")
|
||||
}
|
||||
Switch {
|
||||
id: debugMode
|
||||
checked: App.module ? App.module.cdcDebugEnabled : false
|
||||
onToggled: App.module.setDebugEnabled(debugMode.checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GroupBox {
|
||||
id: imageBox
|
||||
title: "图片注册"
|
||||
visible: true
|
||||
property bool imaging: (App.currentMessageId == ModuleCommunication.UploadImageInfo) || (App.currentMessageId == ModuleCommunication.UploadImageData)
|
||||
GridLayout {
|
||||
columns: 2
|
||||
TextField {
|
||||
id: imagePath
|
||||
Layout.columnSpan: 2
|
||||
implicitWidth: 200
|
||||
placeholderText: "请选择图片"
|
||||
onPressed: {
|
||||
fileDialog.open()
|
||||
}
|
||||
}
|
||||
Label {
|
||||
text: qsTr("用户姓名")
|
||||
}
|
||||
TextField {
|
||||
id: imageEnrollName
|
||||
implicitWidth: 100
|
||||
text: "测试下发"
|
||||
}
|
||||
Label {
|
||||
text: qsTr("操作")
|
||||
}
|
||||
ComboBox {
|
||||
id: imageMode
|
||||
implicitWidth: 100
|
||||
model: ["注册", "识别"]
|
||||
}
|
||||
Label {
|
||||
text: qsTr("循环")
|
||||
}
|
||||
Switch {
|
||||
checked: App.persistenceCommand == ModuleCommunication.UploadImageInfo
|
||||
onToggled: App.persistenceCommand = checked ? ModuleCommunication.UploadImageInfo : ModuleCommunication.Idle
|
||||
}
|
||||
Button {
|
||||
text: imageBox.imaging ? "取消" : "执行"
|
||||
onClicked: {
|
||||
if (imageBox.imaging) {
|
||||
App.resetModule()
|
||||
} else {
|
||||
App.uploadImage(imagePath.text, imageEnrollName.text, imageMode.currentIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileDialog {
|
||||
id: fileDialog
|
||||
nameFilters: ["图片 (*.jpg *.yuv)"]
|
||||
currentFolder: StandardPaths.standardLocations(StandardPaths.DesktopLocation)[0]
|
||||
onAccepted: {
|
||||
var fileUrl = fileDialog.selectedFile.toString()
|
||||
var localFilePath = fileUrl.startsWith("file:///") ? fileUrl.substring(8) : fileUrl
|
||||
imagePath.text = localFilePath
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: otaMouseArea
|
||||
width: otaButton.width
|
||||
height: otaButton.height
|
||||
enabled: !App.connected
|
||||
onClicked: {
|
||||
loader.active = true
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (!visible)
|
||||
return
|
||||
Qt.callLater(() => {
|
||||
otaMouseArea.x = otaButton.mapToItem(control, 0, 0).x
|
||||
otaMouseArea.y = otaButton.mapToItem(control, 0, 0).y
|
||||
})
|
||||
}
|
||||
}
|
34
Analyser/qml/LogView.qml
Normal file
34
Analyser/qml/LogView.qml
Normal file
@ -0,0 +1,34 @@
|
||||
import QtQuick
|
||||
|
||||
ListView {
|
||||
id: root
|
||||
model: ListModel {
|
||||
id: logModel
|
||||
ListElement {
|
||||
message: ""
|
||||
}
|
||||
}
|
||||
delegate: TextEdit {
|
||||
width: ListView.view.width
|
||||
readOnly: true
|
||||
text: message
|
||||
selectByMouse: true
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
|
||||
function append(message){
|
||||
let item = {
|
||||
message: message
|
||||
};
|
||||
logModel.append(item)
|
||||
if(root.atYEnd){
|
||||
Qt.callLater(()=>{
|
||||
root.positionViewAtEnd() })
|
||||
}
|
||||
}
|
||||
|
||||
function clear(){
|
||||
logModel.clear()
|
||||
}
|
||||
}
|
98
Analyser/qml/Main.qml
Normal file
98
Analyser/qml/Main.qml
Normal file
@ -0,0 +1,98 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Fluent as Fluent
|
||||
import Analyser
|
||||
|
||||
Window {
|
||||
id: window
|
||||
width: 1120
|
||||
height: 650
|
||||
visible: true
|
||||
title: qsTr(Qt.application.name + " " + Qt.application.version)
|
||||
|
||||
OperationItem {
|
||||
id: operationItem
|
||||
width: 530
|
||||
anchors.top: parent.top
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.left: operationItem.right
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
TabBar {
|
||||
id: bar
|
||||
width: parent.width
|
||||
TabButton {
|
||||
implicitWidth: 100
|
||||
text: qsTr("视频流")
|
||||
}
|
||||
TabButton {
|
||||
text: qsTr("日志")
|
||||
}
|
||||
}
|
||||
|
||||
StackLayout {
|
||||
width: parent.width
|
||||
currentIndex: bar.currentIndex
|
||||
Item {
|
||||
Image {
|
||||
id: image
|
||||
property real aspectRatio: 600 / 800
|
||||
anchors.centerIn: parent
|
||||
width: Math.min(parent.width, parent.height * aspectRatio)
|
||||
height: width / aspectRatio
|
||||
cache: false
|
||||
fillMode: Image.PreserveAspectFit
|
||||
source: "image://videoframe/"
|
||||
}
|
||||
Image {
|
||||
anchors.fill: image
|
||||
source: "qrc:/qt/qml/Analyser/resources/palm-middle.png"
|
||||
}
|
||||
}
|
||||
Item {
|
||||
LogView {
|
||||
id: logBrowser
|
||||
anchors.fill: parent
|
||||
}
|
||||
Button {
|
||||
text: "清空"
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
onClicked: logBrowser.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Fluent.InfoBar{
|
||||
id:info_bar
|
||||
root: window
|
||||
layoutY: 10
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: App
|
||||
function onNewLog(text) {
|
||||
logBrowser.append(text)
|
||||
}
|
||||
function onNewStatusTip(level, tip, detailMessage) {
|
||||
if (level === App.Tip) {
|
||||
info_bar.showSuccess(tip,2000,detailMessage)
|
||||
} else if (level === App.Warnging) {
|
||||
info_bar.showWarning(tip,2000,detailMessage)
|
||||
} else if (level === App.Error) {
|
||||
info_bar.showError(tip,2000,detailMessage)
|
||||
} else if (level === 2) {
|
||||
info_bar.showInfo(tip,2000,detailMessage)
|
||||
}
|
||||
}
|
||||
function onNewVideoFrame() {
|
||||
image.source = ""
|
||||
image.source = "image://videoframe/"
|
||||
}
|
||||
}
|
||||
}
|
55
Analyser/qml/OperationItem.qml
Normal file
55
Analyser/qml/OperationItem.qml
Normal file
@ -0,0 +1,55 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Analyser
|
||||
|
||||
Item {
|
||||
id: root
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
ConnectionItem {}
|
||||
Column {
|
||||
Text{
|
||||
width: 50
|
||||
text: "烧录版本: "+ (App.module!==null? App.module.verison:"")
|
||||
}
|
||||
Text{
|
||||
width: 50
|
||||
text: "OTA版本: "+(App.module!==null?App.module.otaVerison:"")
|
||||
}
|
||||
}
|
||||
TabBar {
|
||||
id: operationBar
|
||||
Layout.fillWidth: true
|
||||
TabButton {
|
||||
text: "注册/识别"
|
||||
}
|
||||
TabButton {
|
||||
text: "删除/其它"
|
||||
}
|
||||
}
|
||||
StackLayout {
|
||||
currentIndex: operationBar.currentIndex
|
||||
EnrollVerifyOperations {
|
||||
enabled: App.connected
|
||||
}
|
||||
ExtendedOperations {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: loader
|
||||
source: "OtaPage.qml"
|
||||
active: false
|
||||
onLoaded: {
|
||||
if (loader.item && loader.item.open) {
|
||||
loader.item.open()
|
||||
loader.item.onClose = () => {
|
||||
loader.active = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
113
Analyser/qml/OtaPage.qml
Normal file
113
Analyser/qml/OtaPage.qml
Normal file
@ -0,0 +1,113 @@
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Dialogs
|
||||
import Analyser
|
||||
|
||||
Popup {
|
||||
id: root
|
||||
parent: Overlay.overlay
|
||||
anchors.centerIn: Overlay.overlay
|
||||
width: 500
|
||||
height: 200
|
||||
modal: true
|
||||
focus: true
|
||||
closePolicy: Popup.CloseOnEscape
|
||||
property var onClose
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
spacing: 10
|
||||
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
Button {
|
||||
text: "关闭"
|
||||
onClicked: root.close()
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: 10
|
||||
TextField {
|
||||
id: otaFile
|
||||
Layout.fillWidth: true
|
||||
placeholderText: "请选择升级文件或将文件拖入工具中"
|
||||
}
|
||||
Button {
|
||||
text: "选择"
|
||||
onClicked: fileDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: 10
|
||||
ProgressBar {
|
||||
id: progressBar
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: 100
|
||||
value: 0.0
|
||||
}
|
||||
Text {
|
||||
id: progressText
|
||||
text: "0%"
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Text {
|
||||
id: otaMessage
|
||||
text: "请选择升级文件,点击开始按钮升级模组"
|
||||
wrapMode: Text.Wrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Button {
|
||||
text: "开始"
|
||||
Layout.alignment: Qt.AlignRight
|
||||
onClicked: {
|
||||
otaMessage.color = "black"
|
||||
enabled = !App.startOta(otaFile.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClosed: {
|
||||
if (onClose)
|
||||
onClose()
|
||||
}
|
||||
|
||||
FileDialog {
|
||||
id: fileDialog
|
||||
nameFilters: ["OTA文件 (*.Pkg)"]
|
||||
currentFolder: StandardPaths.standardLocations(
|
||||
StandardPaths.DesktopLocation)[0]
|
||||
onAccepted: {
|
||||
var fileUrl = fileDialog.selectedFile.toString()
|
||||
var localFilePath = fileUrl.startsWith(
|
||||
"file:///") ? fileUrl.substring(8) : fileUrl
|
||||
otaFile.text = localFilePath
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: App
|
||||
function onUpdateFinished() {
|
||||
otaMessage.text = "OTA升级完成"
|
||||
otaMessage.color = "green"
|
||||
}
|
||||
function onOtaMessage(message) {
|
||||
otaMessage.text = message
|
||||
}
|
||||
function onOtaProgressChanged(progress) {
|
||||
progressBar.value = progress
|
||||
progressText.text = `${progress}%`
|
||||
}
|
||||
}
|
||||
}
|
BIN
Analyser/resources/palm-middle.png
Normal file
BIN
Analyser/resources/palm-middle.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
9
Analyser/resources/successfull.svg
Normal file
9
Analyser/resources/successfull.svg
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="12px" height="12px" viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 53.2 (72643) - https://sketchapp.com -->
|
||||
<title>Combined Shape</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="下载页" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M6.1,11.6 C3.0072054,11.6 0.5,9.0927946 0.5,6 C0.5,2.9072054 3.0072054,0.4 6.1,0.4 C9.1927946,0.4 11.7,2.9072054 11.7,6 C11.7,9.0927946 9.1927946,11.6 6.1,11.6 Z M5.74088496,8.2481404 L9.2254819,4.76354346 C9.44990579,4.53911957 9.44990579,4.17579395 9.2254819,3.95194404 C9.00105802,3.72752015 8.63773239,3.72752015 8.41388248,3.95194404 L5.33508525,7.03074127 L4.28528656,5.98094259 C4.06143665,5.75709268 3.69811103,5.75709268 3.47368714,5.98094259 C3.24983723,6.20536647 3.24983723,6.5686921 3.47368714,6.79311598 L4.92928554,8.2481404 C5.15313545,8.47256429 5.51646107,8.47256429 5.74088496,8.2481404 Z" id="Combined-Shape" fill="#37BD4B"></path>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
17
Analyser/resources/warning.svg
Normal file
17
Analyser/resources/warning.svg
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="18px" height="17px" viewBox="0 0 18 17" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 53.2 (72643) - https://sketchapp.com -->
|
||||
<title>💚 Icon</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
<path d="M10.7557911,2.21895043 L17.3867068,14.3756291 C17.9156322,15.3453257 17.5583166,16.560199 16.5886199,17.0891245 C16.2948427,17.2493666 15.9655536,17.3333333 15.6309156,17.3333333 L2.36908438,17.3333333 C1.26451488,17.3333333 0.369084379,16.4379028 0.369084379,15.3333333 C0.369084379,14.9986954 0.453051125,14.6694063 0.613293233,14.3756291 L7.24420885,2.21895043 C7.77313431,1.24925376 8.98800759,0.891938091 9.95770426,1.42086355 C10.2948373,1.6047543 10.5719004,1.8818174 10.7557911,2.21895043 Z M9.07936508,12.75 C8.61912779,12.75 8.24603175,13.123096 8.24603175,13.5833333 C8.24603175,14.0435706 8.61912779,14.4166667 9.07936508,14.4166667 C9.53960237,14.4166667 9.91269841,14.0435706 9.91269841,13.5833333 C9.91269841,13.123096 9.53960237,12.75 9.07936508,12.75 Z M9.07936508,6.5 C8.61912779,6.5 8.24603175,6.87309604 8.24603175,7.33333333 L8.24603175,10.6666667 C8.24603175,11.126904 8.61912779,11.5 9.07936508,11.5 C9.53960237,11.5 9.91269841,11.126904 9.91269841,10.6666667 L9.91269841,7.33333333 C9.91269841,6.87309604 9.53960237,6.5 9.07936508,6.5 Z" id="path-1"></path>
|
||||
</defs>
|
||||
<g id="下载页" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="💚-Icon" transform="translate(0.000000, -1.000000)">
|
||||
<mask id="mask-2" fill="white">
|
||||
<use xlink:href="#path-1"></use>
|
||||
</mask>
|
||||
<use id="Mask" fill="#F5A623" xlink:href="#path-1"></use>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.8 KiB |
@ -1,25 +1,66 @@
|
||||
cmake_minimum_required(VERSION 3.5)
|
||||
cmake_minimum_required(VERSION 3.28)
|
||||
|
||||
project(SmartLockerTools VERSION 0.1 LANGUAGES C CXX)
|
||||
project(SmartLockerTools VERSION 0.4 LANGUAGES C CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
set(Projects_ROOT E:/Projects)
|
||||
set(Libraries_ROOT ${Projects_ROOT}/Libraries)
|
||||
if(WIN32)
|
||||
set(Libraries_ROOT E:/Projects/Libraries CACHE STRING "Libraries directory.")
|
||||
set(BOOST_ROOT ${Libraries_ROOT}/boost_1_86_0_msvc2022_64bit)
|
||||
set(Boost_INCLUDE_DIR ${BOOST_ROOT}/include/boost-1_86)
|
||||
add_compile_definitions(
|
||||
BOOST_USE_WINAPI_VERSION=BOOST_WINAPI_VERSION_WIN10
|
||||
)
|
||||
if(CMAKE_BUILD_TYPE STREQUAL "Release")
|
||||
set(MBEDTLS_ROOT ${Libraries_ROOT}/mbedtls-3.6.2_msvc2022_64bit_release)
|
||||
else()
|
||||
set(MBEDTLS_ROOT ${Libraries_ROOT}/mbedtls-3.6.2_msvc2022_64bit_debug)
|
||||
endif()
|
||||
else()
|
||||
execute_process(
|
||||
COMMAND sh -c "echo $HOME"
|
||||
OUTPUT_VARIABLE USER_HOME
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
set(Libraries_ROOT /opt/Libraries)
|
||||
set(BOOST_ROOT ${Libraries_ROOT}/boost_1_86_0)
|
||||
set(Boost_INCLUDE_DIR ${BOOST_ROOT}/include)
|
||||
set(MBEDTLS_ROOT ${Libraries_ROOT}/mbedtls-3.6.2)
|
||||
endif()
|
||||
|
||||
set(BOOST_ROOT ${Libraries_ROOT}/boost_1_85_0_msvc2022_64bit)
|
||||
set(Boost_INCLUDE_DIR ${BOOST_ROOT}/include/boost-1_85)
|
||||
option(Boost_USE_STATIC_LIBS OFF)
|
||||
add_compile_definitions(
|
||||
BOOST_USE_WINAPI_VERSION=BOOST_WINAPI_VERSION_WIN10
|
||||
|
||||
set(OpenSSL_ROOT D:/Qt/Tools/OpenSSLv3/Win_x64)
|
||||
set(OPENSSL_INCLUDE_DIR ${OpenSSL_ROOT}/include)
|
||||
set(OpenSSL_LIBRARY_DIRS ${OpenSSL_ROOT}/lib)
|
||||
set(OpenSSL_LIBRARIES libssl libcrypto)
|
||||
|
||||
set(FFmpeg_ROOT ${Libraries_ROOT}/ffmpeg-7.0.2-full_build-shared)
|
||||
set(FFmpeg_INCLUDE_DIR ${FFmpeg_ROOT}/include)
|
||||
set(FFmpeg_LIB_DIR ${FFmpeg_ROOT}/lib)
|
||||
|
||||
set(MBEDTLS_INCLUDE_DIR ${MBEDTLS_ROOT}/include)
|
||||
set(MBEDTLS_LIBRARY_DIRS ${MBEDTLS_ROOT}/lib)
|
||||
|
||||
execute_process(
|
||||
COMMAND D:/msys64/usr/bin/git rev-parse --short HEAD
|
||||
OUTPUT_VARIABLE GIT_COMMIT_ID
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
|
||||
add_subdirectory(${Projects_ROOT}/Kylin/Universal Universal)
|
||||
add_subdirectory(${Projects_ROOT}/Kylin/Encrypt Encrypt)
|
||||
add_subdirectory(${Projects_ROOT}/Kylin/QtComponets QtComponets)
|
||||
include(CPack)
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(Kylin
|
||||
GIT_REPOSITORY https://amass.fun/gitea/amass/Kylin.git
|
||||
)
|
||||
set(KYLIN_WITH_FLUENT ON)
|
||||
FetchContent_MakeAvailable(Kylin)
|
||||
|
||||
add_subdirectory(Peripheral)
|
||||
add_subdirectory(Database)
|
||||
add_subdirectory(Analyser)
|
||||
add_subdirectory(OtaUpdate)
|
||||
add_subdirectory(UnitTest)
|
||||
|
||||
install(DIRECTORY ${CMAKE_BINARY_DIR}/lib DESTINATION .)
|
||||
|
@ -1,5 +1,6 @@
|
||||
add_library(Database
|
||||
Database.h Database.cpp
|
||||
DataStructure.h DataStructure.cpp
|
||||
# shell.c
|
||||
sqlite3.h sqlite3.c
|
||||
sqlite3ext.h
|
||||
|
5
Database/DataStructure.cpp
Normal file
5
Database/DataStructure.cpp
Normal file
@ -0,0 +1,5 @@
|
||||
#include "DataStructure.h"
|
||||
|
||||
bool operator==(const PalmFeature &lhs, const PalmFeature &rhs) {
|
||||
return lhs.feature == rhs.feature;
|
||||
}
|
24
Database/DataStructure.h
Normal file
24
Database/DataStructure.h
Normal file
@ -0,0 +1,24 @@
|
||||
#ifndef __DATASTRUCTURE_H__
|
||||
#define __DATASTRUCTURE_H__
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
|
||||
class PalmFeature {
|
||||
public:
|
||||
int64_t id; // 对应本地数据库的id
|
||||
std::string username;
|
||||
std::vector<uint8_t> feature;
|
||||
};
|
||||
|
||||
using PalmFeatures = std::vector<PalmFeature>;
|
||||
|
||||
bool operator==(const PalmFeature &lhs, const PalmFeature &rhs);
|
||||
|
||||
namespace std {
|
||||
template <>
|
||||
struct hash<PalmFeature> {};
|
||||
} // namespace std
|
||||
|
||||
#endif // __DATASTRUCTURE_H__
|
@ -23,8 +23,8 @@ bool Database::addPalmFeature(const PalmFeature &palm) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<PalmFeature> Database::palmFeatures() const {
|
||||
std::vector<PalmFeature> ret;
|
||||
PalmFeatures Database::palmFeatures() const {
|
||||
PalmFeatures ret;
|
||||
sqlite3_stmt *statement = nullptr;
|
||||
constexpr const char *sql = "SELECT * FROM palm_feature";
|
||||
if (sqlite3_prepare_v2(m_sqlite, sql, -1, &statement, NULL) != SQLITE_OK) {
|
||||
|
@ -1,23 +1,16 @@
|
||||
#ifndef __DATABASE_H__
|
||||
#define __DATABASE_H__
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "DataStructure.h"
|
||||
|
||||
struct sqlite3;
|
||||
|
||||
class PalmFeature {
|
||||
public:
|
||||
int64_t id; // 对应本地数据库的id
|
||||
std::string username;
|
||||
std::vector<uint8_t> feature;
|
||||
};
|
||||
|
||||
class Database{
|
||||
public:
|
||||
bool open(const std::string &path);
|
||||
bool addPalmFeature(const PalmFeature &palm);
|
||||
std::vector<PalmFeature> palmFeatures() const;
|
||||
PalmFeatures palmFeatures() const;
|
||||
|
||||
private:
|
||||
void initializeTables();
|
||||
|
@ -1,12 +1,15 @@
|
||||
set(APPLICATION_NAME "掌静脉升级工具")
|
||||
|
||||
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)
|
||||
|
||||
configure_file(Configuration.h.in Configuration.h)
|
||||
|
||||
set(PROJECT_SOURCES OtaUpdate.rc
|
||||
main.cpp
|
||||
CdcUpdater.h CdcUpdater.cpp
|
||||
Widget.cpp
|
||||
Widget.h
|
||||
)
|
||||
@ -16,15 +19,14 @@ qt_add_executable(SmartLockerUpdater
|
||||
${PROJECT_SOURCES}
|
||||
)
|
||||
|
||||
target_include_directories(SmartLockerUpdater
|
||||
PRIVATE ${CMAKE_CURRENT_BINARY_DIR}
|
||||
)
|
||||
|
||||
target_link_libraries(SmartLockerUpdater
|
||||
PRIVATE Peripheral
|
||||
PRIVATE Encrypt
|
||||
PRIVATE mfplat
|
||||
PRIVATE mfuuid
|
||||
PRIVATE Mfreadwrite
|
||||
PRIVATE Qt${QT_VERSION_MAJOR}::Widgets
|
||||
PRIVATE Qt${QT_VERSION_MAJOR}::SerialPort
|
||||
PRIVATE Mf
|
||||
)
|
||||
|
||||
set_target_properties(SmartLockerUpdater PROPERTIES
|
||||
|
3
OtaUpdate/Configuration.h.in
Normal file
3
OtaUpdate/Configuration.h.in
Normal file
@ -0,0 +1,3 @@
|
||||
#define APPLICATION_NAME "@APPLICATION_NAME@"
|
||||
#define GIT_COMMIT_ID "@GIT_COMMIT_ID@"
|
||||
#define APP_VERSION "@PROJECT_VERSION@"
|
@ -2,6 +2,7 @@
|
||||
#include "BoostLog.h"
|
||||
#include "CdcUpdater.h"
|
||||
#include "DeviceDiscovery.h"
|
||||
#include "StringUtility.h"
|
||||
#include <QDropEvent>
|
||||
#include <QFileDialog>
|
||||
#include <QHBoxLayout>
|
||||
@ -12,6 +13,7 @@
|
||||
#include <QProgressBar>
|
||||
#include <QPushButton>
|
||||
#include <QSerialPortInfo>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
#include <filesystem>
|
||||
|
||||
@ -52,38 +54,52 @@ Widget::Widget(QWidget *parent) : QWidget(parent) {
|
||||
setAcceptDrops(true);
|
||||
}
|
||||
|
||||
Widget::~Widget() {}
|
||||
Widget::~Widget() {
|
||||
}
|
||||
|
||||
void Widget::start() {
|
||||
m_progressBar->setValue(0);
|
||||
auto filePath = m_fileEditor->text();
|
||||
if (!std::filesystem::exists(filePath.toStdString())) {
|
||||
#ifdef Q_OS_WIN
|
||||
auto path = Amass::StringUtility::UTF8ToGBK(filePath.toStdString());
|
||||
#else
|
||||
auto path = filePath.toStdString();
|
||||
#endif
|
||||
if (!std::filesystem::exists(path)) {
|
||||
QMessageBox::warning(this, "升级", "升级文件不存在!");
|
||||
return;
|
||||
}
|
||||
auto discovery = std::make_shared<DeviceDiscovery>();
|
||||
auto device = CdcUpdater::searchDevice();
|
||||
if (device) {
|
||||
LOG(info) << "device already in ota mode.";
|
||||
} else {
|
||||
auto discovery = std::make_shared<DeviceDiscovery>();
|
||||
|
||||
std::error_code error;
|
||||
setMessage("尝试发现设备......");
|
||||
auto device = discovery->find("UVC Camera", error);
|
||||
if (!device) {
|
||||
QMessageBox::warning(this, "升级", "未检测到模组,请尝试重新插入模组!");
|
||||
return;
|
||||
std::error_code error;
|
||||
setMessage("尝试发现设备......");
|
||||
auto device = discovery->find(DeviceDiscovery::DeviceName, error);
|
||||
if (!device) {
|
||||
QMessageBox::warning(this, "升级", "未检测到模组,请尝试重新插入模组!");
|
||||
return;
|
||||
}
|
||||
setMessage("发现设备成功,进入BOOT模式......");
|
||||
discovery->enterOtaMode(device, error);
|
||||
}
|
||||
setMessage("发现设备成功,进入BOOT模式......");
|
||||
discovery->enterOtaMode(device, error);
|
||||
m_updater = std::make_shared<CdcUpdater>();
|
||||
connect(m_updater.get(), &CdcUpdater::deviceDiscovered, this, &Widget::onCdcDeviceDiscovered);
|
||||
connect(m_updater.get(), &CdcUpdater::updateFinished, this, &Widget::onUpdateFinished);
|
||||
connect(m_updater.get(), &CdcUpdater::progressChanged, this, &Widget::setProgress);
|
||||
connect(m_updater.get(), &CdcUpdater::message, this, &Widget::setMessage);
|
||||
m_updater->start(filePath);
|
||||
m_updater->start(filePath, device ? *device : QSerialPortInfo());
|
||||
setControlsEnabled(false);
|
||||
}
|
||||
|
||||
void Widget::onCdcDeviceDiscovered(const QSerialPortInfo &info) {
|
||||
auto status = m_updater->open(info);
|
||||
LOG(info) << "open cdc port: " << info.portName().toStdString() << ", status: " << status;
|
||||
if (!status) {
|
||||
QTimer::singleShot(0, this, [this]() { m_updater->startSearchDevice(); });
|
||||
}
|
||||
}
|
||||
|
||||
void Widget::onSelectButtonClicked() {
|
||||
|
@ -1,4 +1,5 @@
|
||||
#include "BoostLog.h"
|
||||
#include "Configuration.h"
|
||||
#include "Widget.h"
|
||||
#include <QApplication>
|
||||
#include <QFont>
|
||||
@ -7,11 +8,14 @@ int main(int argc, char *argv[]) {
|
||||
boost::log::initialize("logs/app");
|
||||
|
||||
QApplication a(argc, argv);
|
||||
a.setApplicationName(APPLICATION_NAME);
|
||||
a.setApplicationVersion(QString("v%1_%2 build: %3 %4").arg(APP_VERSION, GIT_COMMIT_ID, __DATE__, __TIME__));
|
||||
|
||||
QFont font;
|
||||
font.setPointSize(16);
|
||||
a.setFont(font);
|
||||
Widget w;
|
||||
w.setWindowTitle("L015模组升级工具");
|
||||
w.setWindowTitle(QString("%1 %2").arg(a.applicationName()).arg(a.applicationVersion()));
|
||||
w.setMinimumWidth(520);
|
||||
w.setMinimumHeight(100);
|
||||
w.show();
|
||||
|
@ -1,5 +1,11 @@
|
||||
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS SerialPort)
|
||||
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS SerialPort)
|
||||
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
|
||||
add_library(Peripheral
|
||||
DeviceDiscovery.h DeviceDiscovery.cpp
|
||||
CdcUpdater.h CdcUpdater.cpp
|
||||
)
|
||||
|
||||
target_include_directories(Peripheral
|
||||
@ -8,4 +14,7 @@ target_include_directories(Peripheral
|
||||
|
||||
target_link_libraries(Peripheral
|
||||
PUBLIC Universal
|
||||
PRIVATE Encrypt
|
||||
$<$<PLATFORM_ID:Windows>:Mfreadwrite Mf mfplat mfuuid strmiids comsuppw>
|
||||
PRIVATE Qt${QT_VERSION_MAJOR}::SerialPort
|
||||
)
|
||||
|
@ -1,8 +1,8 @@
|
||||
#include "CdcUpdater.h"
|
||||
#include "BoostLog.h"
|
||||
#include "StringUtility.h"
|
||||
#include <QDebug>
|
||||
#include <QSerialPort>
|
||||
#include <QSerialPortInfo>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <mbedtls/md5.h>
|
||||
@ -40,13 +40,25 @@ CdcUpdater::CdcUpdater(QObject *parent) : QObject(parent) {
|
||||
CdcUpdater::~CdcUpdater() {
|
||||
}
|
||||
|
||||
void CdcUpdater::start(const QString &path) {
|
||||
void CdcUpdater::start(const QString &path, const QSerialPortInfo &info) {
|
||||
if (info.isNull()) {
|
||||
startSearchDevice();
|
||||
} else {
|
||||
open(info);
|
||||
}
|
||||
#ifdef Q_OS_WINDOWS
|
||||
m_path = Amass::StringUtility::UTF8ToGBK(path.toStdString());
|
||||
#else
|
||||
m_path = path.toStdString();
|
||||
#endif
|
||||
LOG(info) << "ota file: " << m_path;
|
||||
emit progressChanged(++m_progress);
|
||||
}
|
||||
|
||||
void CdcUpdater::startSearchDevice() {
|
||||
if (m_timerId < 0) {
|
||||
m_timerId = startTimer(1000);
|
||||
}
|
||||
m_path = path.toStdString();
|
||||
LOG(info) << "ota file: " << m_path;
|
||||
emit progressChanged(++m_progress);
|
||||
}
|
||||
|
||||
bool CdcUpdater::open(const QSerialPortInfo &info) {
|
||||
@ -73,24 +85,41 @@ bool CdcUpdater::write(Command command, const uint8_t *data, uint32_t size) {
|
||||
if (data != nullptr) {
|
||||
memcpy(&packet[3], data, size);
|
||||
}
|
||||
std::ostringstream oss;
|
||||
for (int i = 0; i < packet.size(); i++) {
|
||||
oss << "0x" << std::hex << (static_cast<int>(packet[i]) & 0xff) << " ";
|
||||
if (command != TransferBin) {
|
||||
std::ostringstream oss;
|
||||
for (int i = 0; i < packet.size(); i++) {
|
||||
oss << "0x" << std::hex << (static_cast<int>(packet[i]) & 0xff) << " ";
|
||||
}
|
||||
LOG(info) << "write " << oss.str();
|
||||
}
|
||||
LOG(info) << "write " << oss.str();
|
||||
return m_serialPort->write(reinterpret_cast<const char *>(packet.data()), packet.size());
|
||||
}
|
||||
|
||||
void CdcUpdater::timerEvent(QTimerEvent *event) {
|
||||
std::optional<QSerialPortInfo> CdcUpdater::searchDevice() {
|
||||
std::optional<QSerialPortInfo> ret = std::nullopt;
|
||||
const auto serialPortInfos = QSerialPortInfo::availablePorts();
|
||||
for (const QSerialPortInfo &portInfo : serialPortInfos) {
|
||||
if ((portInfo.vendorIdentifier() == 0) && (portInfo.productIdentifier() == 0)) continue;
|
||||
LOG(info) << "portName:" << portInfo.portName().toStdString() << ", vendorIdentifier: 0x" << std::hex
|
||||
<< portInfo.vendorIdentifier() << ", productIdentifier: 0x" << std::hex
|
||||
<< portInfo.productIdentifier();
|
||||
if (portInfo.vendorIdentifier() == 0xffff) {
|
||||
LOG(info) << "founded device: " << portInfo.portName().toStdString();
|
||||
emit deviceDiscovered(portInfo);
|
||||
killTimer(m_timerId);
|
||||
m_timerId = -1;
|
||||
ret = portInfo;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
void CdcUpdater::timerEvent(QTimerEvent *event) {
|
||||
auto device = searchDevice();
|
||||
if (device) {
|
||||
LOG(info) << "founded device: " << device->portName().toStdString();
|
||||
emit deviceDiscovered(*device);
|
||||
killTimer(m_timerId);
|
||||
m_timerId = -1;
|
||||
}
|
||||
LOG(info) << "----------";
|
||||
}
|
||||
|
||||
void CdcUpdater::transferBin() {
|
||||
@ -99,7 +128,8 @@ void CdcUpdater::transferBin() {
|
||||
auto readSize = m_ifs->gcount();
|
||||
|
||||
if (readSize > 0) {
|
||||
m_progress += (99 - m_progressBeforeTranster) * static_cast<float>(m_packetIndex) / m_totalPackageSize;
|
||||
m_progress = m_progressBeforeTranster +
|
||||
(99 - m_progressBeforeTranster) * static_cast<float>(m_packetIndex) / m_totalPackageSize;
|
||||
if (m_progress > 99) m_progress = 99;
|
||||
emit progressChanged(m_progress);
|
||||
|
||||
@ -120,18 +150,18 @@ void CdcUpdater::transferBin() {
|
||||
|
||||
void CdcUpdater::onReadyRead() {
|
||||
auto data = m_serialPort->readAll();
|
||||
|
||||
std::ostringstream oss;
|
||||
for (int i = 0; i < data.size(); i++) {
|
||||
oss << "0x" << std::hex << (static_cast<int>(data[i]) & 0xff) << " ";
|
||||
}
|
||||
LOG(info) << "onReadyRead, size: " << data.size() << ", " << oss.str();
|
||||
|
||||
uint8_t command = static_cast<uint8_t>(data[2]);
|
||||
if (command != TransferBinReply) {
|
||||
std::ostringstream oss;
|
||||
for (int i = 0; i < data.size(); i++) {
|
||||
oss << "0x" << std::hex << (static_cast<int>(data[i]) & 0xff) << " ";
|
||||
}
|
||||
LOG(info) << "onReadyRead, size: " << data.size() << ", " << oss.str();
|
||||
}
|
||||
if (command == EnterUpgradeReply) {
|
||||
write(GetVersion);
|
||||
emit progressChanged(++m_progress);
|
||||
message("获取模组OTA版本......");
|
||||
emit message("获取模组OTA版本......");
|
||||
} else if (command == GetVersionReply) {
|
||||
LOG(info) << "device ota version: 0x" << std::hex << (static_cast<int>(data[4]) & 0xff);
|
||||
int fileSize = std::filesystem::file_size(m_path);
|
||||
@ -143,7 +173,7 @@ void CdcUpdater::onReadyRead() {
|
||||
content[2] = 0x14;
|
||||
write(StartUpgrade, content.data(), content.size());
|
||||
emit progressChanged(++m_progress);
|
||||
message("模组进入升级状态......");
|
||||
emit message("模组进入升级状态......");
|
||||
} else if (command == StartUpgradeReply) {
|
||||
std::ifstream ifs(m_path, std::ifstream::binary);
|
||||
char buffer[4096] = {0};
|
@ -2,9 +2,10 @@
|
||||
#define __CDCUPDATER_H__
|
||||
|
||||
#include <QObject>
|
||||
#include <QSerialPortInfo>
|
||||
#include <optional>
|
||||
|
||||
class QSerialPort;
|
||||
class QSerialPortInfo;
|
||||
|
||||
class CdcUpdater : public QObject {
|
||||
Q_OBJECT
|
||||
@ -35,8 +36,11 @@ public:
|
||||
};
|
||||
CdcUpdater(QObject *parent = nullptr);
|
||||
~CdcUpdater();
|
||||
void start(const QString &path);
|
||||
void start(const QString &path, const QSerialPortInfo &info = QSerialPortInfo());
|
||||
void startSearchDevice();
|
||||
bool open(const QSerialPortInfo &info);
|
||||
static std::optional<QSerialPortInfo> searchDevice();
|
||||
|
||||
signals:
|
||||
void deviceDiscovered(const QSerialPortInfo &info);
|
||||
void updateFinished();
|
@ -1,10 +1,21 @@
|
||||
#include "DeviceDiscovery.h"
|
||||
#include "BoostLog.h"
|
||||
#include "StringUtility.h"
|
||||
#include <boost/scope/scope_exit.hpp>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#ifdef WIN32
|
||||
#include <comutil.h>
|
||||
#include <dshow.h>
|
||||
#include <mfapi.h>
|
||||
#include <mfcaptureengine.h>
|
||||
#include <strmif.h>
|
||||
#else
|
||||
#include <fcntl.h>
|
||||
#include <linux/videodev2.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/mman.h>
|
||||
#endif
|
||||
|
||||
template <class T>
|
||||
void SafeRelease(T **ppT) {
|
||||
@ -17,6 +28,39 @@ void SafeRelease(T **ppT) {
|
||||
DeviceDiscovery::DeviceDiscovery() {
|
||||
}
|
||||
|
||||
#ifdef WIN32
|
||||
|
||||
void DeleteMediaType(AM_MEDIA_TYPE *pmt) {
|
||||
if (pmt != nullptr) {
|
||||
if (pmt->cbFormat != 0) {
|
||||
CoTaskMemFree((PVOID)pmt->pbFormat);
|
||||
pmt->cbFormat = 0;
|
||||
pmt->pbFormat = nullptr;
|
||||
}
|
||||
if (pmt->pUnk != nullptr) {
|
||||
// Unecessary because pUnk should not be used, but safest.
|
||||
pmt->pUnk->Release();
|
||||
pmt->pUnk = nullptr;
|
||||
}
|
||||
CoTaskMemFree((PVOID)pmt);
|
||||
}
|
||||
}
|
||||
|
||||
static std::string deviceName(IMFActivate *device) {
|
||||
std::string ret;
|
||||
WCHAR *friendlyName = nullptr;
|
||||
uint32_t nameLength = 0;
|
||||
auto result = device->GetAllocatedString(MF_DEVSOURCE_ATTRIBUTE_FRIENDLY_NAME, &friendlyName, &nameLength);
|
||||
|
||||
if (SUCCEEDED(result)) {
|
||||
ret = _com_util::ConvertBSTRToString(friendlyName);
|
||||
}
|
||||
if (friendlyName != nullptr) {
|
||||
CoTaskMemFree(friendlyName);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::shared_ptr<DeviceDiscovery::Device> DeviceDiscovery::find(const std::string &deviceName, std::error_code &error) {
|
||||
std::shared_ptr<Device> ret;
|
||||
|
||||
@ -45,7 +89,7 @@ std::shared_ptr<DeviceDiscovery::Device> DeviceDiscovery::find(const std::string
|
||||
|
||||
int index = -1;
|
||||
for (int i = 0; i < count; i++) {
|
||||
auto name = this->deviceName(devices[i]);
|
||||
auto name = ::deviceName(devices[i]);
|
||||
LOG(info) << "device[" << i << "]: " << name;
|
||||
if (name == deviceName) {
|
||||
index = i;
|
||||
@ -54,7 +98,7 @@ std::shared_ptr<DeviceDiscovery::Device> DeviceDiscovery::find(const std::string
|
||||
}
|
||||
if (index >= 0) {
|
||||
IMFMediaSource *source = nullptr;
|
||||
result = devices[0]->ActivateObject(IID_PPV_ARGS(&source));
|
||||
result = devices[index]->ActivateObject(IID_PPV_ARGS(&source));
|
||||
if (FAILED(result)) {
|
||||
} else {
|
||||
ret = std::make_shared<Device>(source);
|
||||
@ -63,28 +107,13 @@ std::shared_ptr<DeviceDiscovery::Device> DeviceDiscovery::find(const std::string
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::string DeviceDiscovery::deviceName(IMFActivate *device) {
|
||||
std::string ret;
|
||||
WCHAR *friendlyName = nullptr;
|
||||
uint32_t nameLength = 0;
|
||||
auto result = device->GetAllocatedString(MF_DEVSOURCE_ATTRIBUTE_FRIENDLY_NAME, &friendlyName, &nameLength);
|
||||
|
||||
if (SUCCEEDED(result)) {
|
||||
ret = Amass::StringUtility::wstringToString(std::wstring(friendlyName, nameLength));
|
||||
}
|
||||
if (friendlyName != nullptr) {
|
||||
CoTaskMemFree(friendlyName);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
void DeviceDiscovery::enterOtaMode(const std::shared_ptr<Device> &device, std::error_code &error) {
|
||||
auto resolutions = deviceResolutions(device);
|
||||
// for (auto &[w, h] : resolutions) {
|
||||
// LOG(info) << w << " " << h;
|
||||
// }
|
||||
LOG(info) << "device resolutions:";
|
||||
for (auto &[w, h] : resolutions) {
|
||||
LOG(info) << "\t" << w << "*" << h;
|
||||
}
|
||||
|
||||
// LOG(info) << "resolutions: " << resolutions.size();
|
||||
int32_t otaSpecificHeight = -1;
|
||||
for (auto &[width, height] : resolutions) {
|
||||
if (width == OtaSpecificWidth) {
|
||||
@ -95,6 +124,8 @@ void DeviceDiscovery::enterOtaMode(const std::shared_ptr<Device> &device, std::e
|
||||
if (otaSpecificHeight <= 0) {
|
||||
LOG(error) << "cannot find ota specific resolution.";
|
||||
return;
|
||||
} else {
|
||||
LOG(info) << "found ota specific resolution: " << OtaSpecificWidth << "x" << otaSpecificHeight;
|
||||
}
|
||||
|
||||
IMFMediaType *type = nullptr;
|
||||
@ -116,6 +147,110 @@ void DeviceDiscovery::enterOtaMode(const std::shared_ptr<Device> &device, std::e
|
||||
&llTimeStamp, &pSample);
|
||||
}
|
||||
|
||||
std::vector<DeviceDiscovery::Device> DeviceDiscovery::devices() {
|
||||
std::vector<Device> devices;
|
||||
ICreateDevEnum *pDevEnum = nullptr;
|
||||
IEnumMoniker *pEnum = nullptr;
|
||||
|
||||
HRESULT hr =
|
||||
CoCreateInstance(CLSID_SystemDeviceEnum, nullptr, CLSCTX_INPROC_SERVER, IID_ICreateDevEnum, (void **)&pDevEnum);
|
||||
|
||||
if (FAILED(hr)) {
|
||||
return devices;
|
||||
}
|
||||
|
||||
hr = pDevEnum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory, &pEnum, 0);
|
||||
if (hr != S_OK) {
|
||||
pDevEnum->Release();
|
||||
return devices;
|
||||
}
|
||||
|
||||
IMoniker *pMoniker = nullptr;
|
||||
while (pEnum->Next(1, &pMoniker, nullptr) == S_OK) {
|
||||
Device info;
|
||||
GetDeviceNames(pMoniker, info);
|
||||
|
||||
IBaseFilter *pFilter;
|
||||
hr = pMoniker->BindToObject(nullptr, nullptr, IID_IBaseFilter, (void **)&pFilter);
|
||||
if (SUCCEEDED(hr)) {
|
||||
IEnumPins *pEnumPins;
|
||||
hr = pFilter->EnumPins(&pEnumPins);
|
||||
if (SUCCEEDED(hr)) {
|
||||
IPin *pPin;
|
||||
while (pEnumPins->Next(1, &pPin, nullptr) == S_OK) {
|
||||
PIN_DIRECTION pinDir;
|
||||
pPin->QueryDirection(&pinDir);
|
||||
if (pinDir == PINDIR_OUTPUT) {
|
||||
GetSupportedResolutions(pPin, info.resolutions);
|
||||
}
|
||||
pPin->Release();
|
||||
}
|
||||
pEnumPins->Release();
|
||||
}
|
||||
pFilter->Release();
|
||||
}
|
||||
devices.push_back(info);
|
||||
pMoniker->Release();
|
||||
}
|
||||
pEnum->Release();
|
||||
pDevEnum->Release();
|
||||
return devices;
|
||||
}
|
||||
|
||||
bool DeviceDiscovery::SetResolution(const std::string &deviceName, int width, int height) {
|
||||
ICreateDevEnum *pDevEnum = nullptr;
|
||||
IEnumMoniker *pEnum = nullptr;
|
||||
|
||||
HRESULT hr =
|
||||
CoCreateInstance(CLSID_SystemDeviceEnum, nullptr, CLSCTX_INPROC_SERVER, IID_ICreateDevEnum, (void **)&pDevEnum);
|
||||
|
||||
if (FAILED(hr)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
hr = pDevEnum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory, &pEnum, 0);
|
||||
if (hr != S_OK) {
|
||||
pDevEnum->Release();
|
||||
return false;
|
||||
}
|
||||
|
||||
IMoniker *pMoniker = nullptr;
|
||||
while (pEnum->Next(1, &pMoniker, nullptr) == S_OK) {
|
||||
IPropertyBag *pPropBag;
|
||||
hr = pMoniker->BindToStorage(nullptr, nullptr, IID_IPropertyBag, (void **)&pPropBag);
|
||||
if (SUCCEEDED(hr)) {
|
||||
VARIANT varName;
|
||||
VariantInit(&varName);
|
||||
hr = pPropBag->Read(L"FriendlyName", &varName, nullptr);
|
||||
if (SUCCEEDED(hr)) {
|
||||
std::string currentName = _com_util::ConvertBSTRToString(varName.bstrVal);
|
||||
if (currentName == deviceName) {
|
||||
IBaseFilter *pFilter;
|
||||
hr = pMoniker->BindToObject(nullptr, nullptr, IID_IBaseFilter, (void **)&pFilter);
|
||||
if (SUCCEEDED(hr)) {
|
||||
if (SetPinResolution(pFilter, width, height)) {
|
||||
pFilter->Release();
|
||||
VariantClear(&varName);
|
||||
pPropBag->Release();
|
||||
pMoniker->Release();
|
||||
pEnum->Release();
|
||||
pDevEnum->Release();
|
||||
return true;
|
||||
}
|
||||
pFilter->Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
VariantClear(&varName);
|
||||
pPropBag->Release();
|
||||
}
|
||||
pMoniker->Release();
|
||||
}
|
||||
pEnum->Release();
|
||||
pDevEnum->Release();
|
||||
return false;
|
||||
}
|
||||
|
||||
DeviceDiscovery::Resolutions DeviceDiscovery::deviceResolutions(const std::shared_ptr<Device> &source) {
|
||||
DeviceDiscovery::Resolutions ret;
|
||||
HRESULT result = S_OK;
|
||||
@ -138,6 +273,89 @@ DeviceDiscovery::Resolutions DeviceDiscovery::deviceResolutions(const std::share
|
||||
return ret;
|
||||
}
|
||||
|
||||
void DeviceDiscovery::GetDeviceNames(IMoniker *pMoniker, Device &info) {
|
||||
IPropertyBag *pPropBag;
|
||||
HRESULT hr = pMoniker->BindToStorage(nullptr, nullptr, IID_IPropertyBag, (void **)&pPropBag);
|
||||
if (SUCCEEDED(hr)) {
|
||||
VARIANT varName;
|
||||
VariantInit(&varName);
|
||||
hr = pPropBag->Read(L"FriendlyName", &varName, nullptr);
|
||||
if (SUCCEEDED(hr)) {
|
||||
|
||||
info.friendlyName = _com_util::ConvertBSTRToString(varName.bstrVal);
|
||||
}
|
||||
VariantClear(&varName);
|
||||
|
||||
hr = pPropBag->Read(L"DevicePath", &varName, nullptr);
|
||||
if (SUCCEEDED(hr)) {
|
||||
info.alternativeName = _com_util::ConvertBSTRToString(varName.bstrVal);
|
||||
}
|
||||
VariantClear(&varName);
|
||||
|
||||
pPropBag->Release();
|
||||
}
|
||||
}
|
||||
|
||||
void DeviceDiscovery::GetSupportedResolutions(IPin *pPin, std::vector<std::pair<int, int>> &resolutions) {
|
||||
IEnumMediaTypes *pEnumMediaTypes = nullptr;
|
||||
HRESULT hr = pPin->EnumMediaTypes(&pEnumMediaTypes);
|
||||
if (FAILED(hr)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AM_MEDIA_TYPE *pMediaType = nullptr;
|
||||
while (pEnumMediaTypes->Next(1, &pMediaType, nullptr) == S_OK) {
|
||||
if (pMediaType->formattype == FORMAT_VideoInfo) {
|
||||
VIDEOINFOHEADER *pVih = reinterpret_cast<VIDEOINFOHEADER *>(pMediaType->pbFormat);
|
||||
int width = pVih->bmiHeader.biWidth;
|
||||
int height = pVih->bmiHeader.biHeight;
|
||||
resolutions.emplace_back(width, height);
|
||||
}
|
||||
DeleteMediaType(pMediaType);
|
||||
}
|
||||
pEnumMediaTypes->Release();
|
||||
}
|
||||
|
||||
bool DeviceDiscovery::SetPinResolution(IBaseFilter *pFilter, int width, int height) {
|
||||
IEnumPins *pEnumPins;
|
||||
HRESULT hr = pFilter->EnumPins(&pEnumPins);
|
||||
if (FAILED(hr)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
IPin *pPin;
|
||||
bool success = false;
|
||||
while (pEnumPins->Next(1, &pPin, nullptr) == S_OK) {
|
||||
PIN_DIRECTION pinDir;
|
||||
pPin->QueryDirection(&pinDir);
|
||||
if (pinDir == PINDIR_OUTPUT) {
|
||||
IEnumMediaTypes *pEnumMediaTypes = nullptr;
|
||||
hr = pPin->EnumMediaTypes(&pEnumMediaTypes);
|
||||
if (SUCCEEDED(hr)) {
|
||||
AM_MEDIA_TYPE *pMediaType = nullptr;
|
||||
while (pEnumMediaTypes->Next(1, &pMediaType, nullptr) == S_OK) {
|
||||
if (pMediaType->formattype == FORMAT_VideoInfo) {
|
||||
VIDEOINFOHEADER *pVih = reinterpret_cast<VIDEOINFOHEADER *>(pMediaType->pbFormat);
|
||||
if (pVih->bmiHeader.biWidth == width && pVih->bmiHeader.biHeight == height) {
|
||||
hr = pPin->QueryAccept(pMediaType);
|
||||
if (hr == S_OK) {
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
DeleteMediaType(pMediaType);
|
||||
if (success) break;
|
||||
}
|
||||
pEnumMediaTypes->Release();
|
||||
}
|
||||
}
|
||||
pPin->Release();
|
||||
if (success) break;
|
||||
}
|
||||
pEnumPins->Release();
|
||||
return success;
|
||||
}
|
||||
|
||||
DeviceDiscovery::Device::Device(IMFMediaSource *source) : source(source) {
|
||||
source->AddRef();
|
||||
auto result = MFCreateSourceReaderFromMediaSource(source, nullptr, &reader);
|
||||
@ -145,12 +363,206 @@ DeviceDiscovery::Device::Device(IMFMediaSource *source) : source(source) {
|
||||
LOG(error) << "MFCreateSourceReaderFromMediaSource() failed, result: " << result;
|
||||
}
|
||||
}
|
||||
#else
|
||||
std::vector<DeviceDiscovery::Device> DeviceDiscovery::devices() {
|
||||
std::vector<Device> ret;
|
||||
for (const auto &entry : std::filesystem::directory_iterator("/dev")) {
|
||||
if (entry.is_character_file() && entry.path().string().find("video") != std::string::npos) {
|
||||
int fd = open(entry.path().c_str(), O_RDWR);
|
||||
if (fd < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
struct v4l2_capability cap;
|
||||
if (ioctl(fd, VIDIOC_QUERYCAP, &cap) == 0) {
|
||||
if ((strstr(reinterpret_cast<const char *>(cap.card), DeviceName) != nullptr) &&
|
||||
(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
|
||||
Device device;
|
||||
device.friendlyName = entry.path().string();
|
||||
device.alternativeName = entry.path().string();
|
||||
ret.push_back(device);
|
||||
}
|
||||
}
|
||||
close(fd);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static std::string find_video_device_by_name(const std::string &targetName) {
|
||||
std::string ret;
|
||||
const std::string base_path = "/sys/class/video4linux";
|
||||
for (const auto &entry : std::filesystem::directory_iterator(base_path)) {
|
||||
if (!std::filesystem::is_directory(entry.path())) continue;
|
||||
|
||||
std::string interface;
|
||||
std::ifstream ifs(entry.path().string() + "/device/interface");
|
||||
if (ifs.is_open()) {
|
||||
std::getline(ifs, interface);
|
||||
}
|
||||
if (interface != targetName) continue;
|
||||
|
||||
ifs = std::ifstream(entry.path().string() + "/index");
|
||||
std::string index;
|
||||
if (ifs.is_open()) {
|
||||
std::getline(ifs, index);
|
||||
}
|
||||
if (index != "0") continue;
|
||||
|
||||
ret = "/dev/" + entry.path().filename().string();
|
||||
break;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::shared_ptr<DeviceDiscovery::Device> DeviceDiscovery::find(const std::string &deviceName, std::error_code &error) {
|
||||
auto ret = std::make_shared<Device>();
|
||||
ret->friendlyName = find_video_device_by_name(deviceName);
|
||||
return ret;
|
||||
}
|
||||
|
||||
void DeviceDiscovery::enterOtaMode(const std::shared_ptr<Device> &device, std::error_code &error) {
|
||||
auto resolutions = deviceResolutions(device);
|
||||
LOG(info) << "device resolutions:";
|
||||
for (auto &[w, h] : resolutions) {
|
||||
LOG(info) << "\t" << w << "*" << h;
|
||||
}
|
||||
int32_t otaSpecificHeight = -1;
|
||||
for (auto &[width, height] : resolutions) {
|
||||
if (width == OtaSpecificWidth) {
|
||||
otaSpecificHeight = height;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (otaSpecificHeight <= 0) {
|
||||
LOG(error) << "cannot find ota specific resolution.";
|
||||
return;
|
||||
} else {
|
||||
LOG(info) << "found ota specific resolution: " << OtaSpecificWidth << "x" << otaSpecificHeight;
|
||||
}
|
||||
int fd = open(device->friendlyName.c_str(), O_RDWR);
|
||||
if (fd <= 0) {
|
||||
LOG(error) << "Failed to open device " << device->friendlyName;
|
||||
} else {
|
||||
struct v4l2_format format;
|
||||
memset(&format, 0, sizeof(format));
|
||||
format.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
format.fmt.pix.width = OtaSpecificWidth;
|
||||
format.fmt.pix.height = otaSpecificHeight;
|
||||
format.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;
|
||||
format.fmt.pix.field = V4L2_FIELD_INTERLACED;
|
||||
|
||||
if (ioctl(fd, VIDIOC_S_FMT, &format) == -1) {
|
||||
perror("Setting Pixel Format");
|
||||
return;
|
||||
}
|
||||
#define CLEAR(x) memset(&(x), 0, sizeof(x))
|
||||
struct v4l2_requestbuffers req;
|
||||
CLEAR(req);
|
||||
req.count = 1;
|
||||
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
req.memory = V4L2_MEMORY_MMAP;
|
||||
|
||||
if (ioctl(fd, VIDIOC_REQBUFS, &req) == -1) {
|
||||
perror("Requesting Buffer");
|
||||
return;
|
||||
}
|
||||
|
||||
struct v4l2_buffer buf;
|
||||
CLEAR(buf);
|
||||
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
buf.memory = V4L2_MEMORY_MMAP;
|
||||
buf.index = 0;
|
||||
|
||||
if (ioctl(fd, VIDIOC_QUERYBUF, &buf) == -1) {
|
||||
perror("Querying Buffer");
|
||||
return;
|
||||
}
|
||||
|
||||
struct buffer {
|
||||
void *start;
|
||||
size_t length;
|
||||
};
|
||||
|
||||
struct buffer buffer;
|
||||
buffer.length = buf.length;
|
||||
buffer.start = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset);
|
||||
|
||||
if (buffer.start == MAP_FAILED) {
|
||||
perror("Mapping Buffer");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ioctl(fd, VIDIOC_QBUF, &buf) == -1) {
|
||||
perror("Queue Buffer");
|
||||
return;
|
||||
}
|
||||
|
||||
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
if (ioctl(fd, VIDIOC_STREAMON, &type) == -1) {
|
||||
perror("Start Capture");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ioctl(fd, VIDIOC_STREAMOFF, &type) == -1) {
|
||||
perror("Stop Capture");
|
||||
return;
|
||||
}
|
||||
|
||||
if (munmap(buffer.start, buffer.length) == -1) {
|
||||
perror("Unmapping Buffer");
|
||||
return;
|
||||
}
|
||||
|
||||
close(fd);
|
||||
}
|
||||
}
|
||||
|
||||
DeviceDiscovery::Resolutions DeviceDiscovery::deviceResolutions(const std::shared_ptr<Device> &source) {
|
||||
Resolutions ret;
|
||||
int fd = open(source->friendlyName.c_str(), O_RDWR);
|
||||
if (fd <= 0) {
|
||||
LOG(error) << "Failed to open device " << source->friendlyName;
|
||||
} else {
|
||||
struct v4l2_fmtdesc fmt;
|
||||
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
fmt.index = 0;
|
||||
|
||||
while (ioctl(fd, VIDIOC_ENUM_FMT, &fmt) == 0) {
|
||||
std::cout << "Pixel Format: " << fmt.description << std::endl;
|
||||
struct v4l2_frmsizeenum frmsize;
|
||||
frmsize.pixel_format = fmt.pixelformat;
|
||||
frmsize.index = 0;
|
||||
|
||||
while (ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &frmsize) == 0) {
|
||||
if (frmsize.type == V4L2_FRMSIZE_TYPE_DISCRETE) {
|
||||
ret.emplace_back(frmsize.discrete.width, frmsize.discrete.height);
|
||||
} else if (frmsize.type == V4L2_FRMSIZE_TYPE_STEPWISE) {
|
||||
for (int width = frmsize.stepwise.min_width; width <= frmsize.stepwise.max_width;
|
||||
width += frmsize.stepwise.step_width) {
|
||||
for (int height = frmsize.stepwise.min_height; height <= frmsize.stepwise.max_height;
|
||||
height += frmsize.stepwise.step_height) {
|
||||
ret.emplace_back(width, height);
|
||||
}
|
||||
}
|
||||
}
|
||||
frmsize.index++;
|
||||
}
|
||||
fmt.index++;
|
||||
}
|
||||
close(fd);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
#endif
|
||||
|
||||
DeviceDiscovery::Device::~Device() {
|
||||
#ifdef Q_OS_WIN
|
||||
if (source != nullptr) {
|
||||
source->Release();
|
||||
}
|
||||
if (reader != nullptr) {
|
||||
reader->Release();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
@ -2,31 +2,49 @@
|
||||
#define __DEVICEDISCOVERY_H__
|
||||
|
||||
#include <memory>
|
||||
#include <mfidl.h>
|
||||
#include <mfreadwrite.h>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
#include <vector>
|
||||
#ifdef WIN32
|
||||
#include <mfidl.h>
|
||||
#include <mfreadwrite.h>
|
||||
|
||||
class IPin;
|
||||
class IBaseFilter;
|
||||
#endif
|
||||
|
||||
class DeviceDiscovery {
|
||||
constexpr static int32_t OtaSpecificWidth = 96;
|
||||
|
||||
public:
|
||||
struct Device {
|
||||
Device(IMFMediaSource *source);
|
||||
~Device();
|
||||
IMFMediaSource *source = nullptr;
|
||||
IMFSourceReader *reader = nullptr;
|
||||
};
|
||||
|
||||
constexpr static auto DeviceName = "UVC Camera";
|
||||
using Resolution = std::pair<int32_t, int32_t>;
|
||||
using Resolutions = std::vector<Resolution>;
|
||||
struct Device {
|
||||
Device() = default;
|
||||
std::string friendlyName;
|
||||
std::string alternativeName;
|
||||
Resolutions resolutions;
|
||||
~Device();
|
||||
#ifdef WIN32
|
||||
Device(IMFMediaSource *source);
|
||||
IMFMediaSource *source = nullptr;
|
||||
IMFSourceReader *reader = nullptr;
|
||||
#endif
|
||||
};
|
||||
|
||||
DeviceDiscovery();
|
||||
std::shared_ptr<Device> find(const std::string &deviceName, std::error_code &error);
|
||||
void enterOtaMode(const std::shared_ptr<Device> &device, std::error_code &error);
|
||||
std::vector<Device> devices();
|
||||
bool SetResolution(const std::string &deviceName, int width, int height);
|
||||
|
||||
protected:
|
||||
std::string deviceName(IMFActivate *device);
|
||||
Resolutions deviceResolutions(const std::shared_ptr<Device> &source);
|
||||
#ifdef WIN32
|
||||
void GetDeviceNames(IMoniker *pMoniker, Device &info);
|
||||
void GetSupportedResolutions(IPin *pPin, std::vector<std::pair<int, int>> &resolutions);
|
||||
bool SetPinResolution(IBaseFilter *pFilter, int width, int height);
|
||||
#endif
|
||||
};
|
||||
#endif // __DEVICEDISCOVERY_H__
|
||||
|
90
Readme.md
90
Readme.md
@ -7,9 +7,91 @@
|
||||
|
||||
|
||||
```
|
||||
ln -s /opt/Xuantie-900-gcc-elf-newlib-x86_64-V2.6.1 ~/Projects/cv181x_alios/host-tools/Xuantie-900-gcc-elf-newlib-x86_64-V2.6.1
|
||||
palmDetectionProcess
|
||||
facePalmDetectionProcess # 处理每帧,在MPP的回调函数里面
|
||||
uart_msg_proc() -> cmd_exec()执行串口协议命令
|
||||
|
||||
./rebuild-app.sh y L015 V200 R002
|
||||
sock_cmd_exec() 内部线程逻辑之间通信
|
||||
|
||||
线程函数frame_proc_task()
|
||||
algo_cb()->onFacePalmDetectionPassed()->app_server_alive()->sock_cmd_exec:SOCK_CMD__ALIVE_PALM
|
||||
|
||||
ST__PlamRegister()->__PalmRegister() -> PalmFeatureExtract() -> spv2_get_feature()
|
||||
|
||||
frame_proc_task()
|
||||
frame_sync_task() // L015 V200 __DUAL_SNS_FAKE__ 双sensor活体
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 门锁开发环境搭建
|
||||
|
||||
smart_doorbell 文件夹、cv181xc_qfn。
|
||||
CONSOLE_UART_IDX
|
||||
业务串口:2
|
||||
|
||||
安装如下 python 环境:
|
||||
|
||||
```shell
|
||||
sudo apt-get install lz4 unzip python3-pip
|
||||
pip3 install yoctools # 安装在 ~/.local 目录下
|
||||
|
||||
# Ubuntu下product实际为product64
|
||||
cd ~/.local/bin
|
||||
ln -s product64 product
|
||||
|
||||
# 以下两条命令打印版本,验证是否安装成功,如无版本信息输出,最好重启一下机器。可能安装yoctools后,可能yoc、product还找不到
|
||||
yoc -V
|
||||
product version
|
||||
|
||||
# 将 python 软链接定向为 python3
|
||||
cd /usr/bin
|
||||
ln -s python3 python
|
||||
```
|
||||
|
||||
原门锁构建环境应该是采用 CenterOS 搭建的,所以很多脚本使用的是 `sh`,在 Ubuntu 环境下,需要注意构建输出,一旦出现疑似脚本错误的问题,可以将脚本开头的 `#/bin/sh` 改为 `#/bin/bash` 再重试。例如脚本文件 `Xuantie-900-gcc-elf-newlib-x86_64-V2.6.1/bin/riscv64-unknown-elf-g++`。
|
||||
|
||||
修改编译器位置,我习惯于将编译器独立于项目工程之外:
|
||||
|
||||
```makefile title="solutions/smart_doorbell/Makefile"
|
||||
HOST_TOOLS := /opt/Xuantie-900-gcc-elf-newlib-x86_64-V2.6.1/bin
|
||||
```
|
||||
|
||||
上述步骤执行完毕之后,即可编译打包:
|
||||
|
||||
```shell
|
||||
./boot-rebuild.sh # 编译boot
|
||||
./rebuild-app.sh y L015 V200 R002 # 编译烧录固件
|
||||
|
||||
# 编译OTA固件,11为OTA版本号,这个版本号只做固件文件名显示。
|
||||
# 实际的版本设置在 cv181x_alios/solutions/smart_doorbell/package.yaml.L015_V200R002
|
||||
600X800
|
||||
|
||||
|
||||
|
||||
# cv181x_alios/rebuild-app.sh 、cv181x_alios/rebuild-app-ota.sh 这两个文件需要改成 #!/bin/bash, 否则编译不过
|
||||
|
||||
docker run -it --rm --user 1000:1000 -v /opt:/opt -v $(pwd)/..:$(pwd)/.. -w $(pwd) registry.cn-shenzhen.aliyuncs.com/amass_toolset/yoctools:22.04 ./rebuild-app.sh y L015 V200 R002
|
||||
|
||||
docker run -it --rm --user 1000:1000 -v /opt:/opt -v $(pwd)/..:$(pwd)/.. -w $(pwd) registry.cn-shenzhen.aliyuncs.com/amass_toolset/yoctools:22.04 ./rebuild-app-ota.sh y L015 V200 R002 09
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Flash设置
|
||||
|
||||
```
|
||||
__FLASH_16MB__
|
||||
cv181x_alios/boards/cv181xc_qfn/configs/config.yaml
|
||||
cv181x_alios/boards/cv181xc_qfn/configs/partition_alios_spinor.xml
|
||||
cv181x_alios/solutions/smart_doorbell/face_lock_app/flash-part.h
|
||||
```
|
||||
|
||||
修改`cv181x_alios/pack-ota.sh`,以支持将boot打进ota升级文件中。(有升级变砖风险)
|
||||
|
||||
```
|
||||
# $ota_pack_tool $OTA_PKG imtb yoc.bin algo.bin.1 boot $@
|
||||
```
|
||||
|
||||
./rebuild-app-ota.sh y L015 V200 R002 11
|
||||
```
|
19
UnitTest/CMakeLists.txt
Normal file
19
UnitTest/CMakeLists.txt
Normal file
@ -0,0 +1,19 @@
|
||||
find_package(Boost REQUIRED COMPONENTS unit_test_framework)
|
||||
|
||||
# --detect_memory_leak=0 --run_test=MarkdownParserTest,ProcessUtilityTest
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
add_executable(UnitTest main.cpp
|
||||
LinuxDeviceEnums.cpp
|
||||
)
|
||||
|
||||
target_compile_definitions(UnitTest
|
||||
PUBLIC LOG_FILTER_LEVEL=2
|
||||
)
|
||||
|
||||
target_link_libraries(UnitTest
|
||||
PRIVATE DataStructure
|
||||
PRIVATE Peripheral
|
||||
PRIVATE Universal
|
||||
)
|
15
UnitTest/LinuxDeviceEnums.cpp
Normal file
15
UnitTest/LinuxDeviceEnums.cpp
Normal file
@ -0,0 +1,15 @@
|
||||
#include "BoostLog.h"
|
||||
#include "DeviceDiscovery.h"
|
||||
#include <boost/test/unit_test.hpp>
|
||||
|
||||
BOOST_AUTO_TEST_CASE(EnumDevice) {
|
||||
std::error_code error;
|
||||
DeviceDiscovery discovery;
|
||||
auto device = discovery.find("UVC Camera", error);
|
||||
auto devices = discovery.devices();
|
||||
for (int i = 0; i < devices.size(); i++) {
|
||||
LOG(info) << "device[" << i << "] " << devices.at(i).friendlyName;
|
||||
}
|
||||
|
||||
discovery.enterOtaMode(device, error);
|
||||
}
|
2
UnitTest/main.cpp
Normal file
2
UnitTest/main.cpp
Normal file
@ -0,0 +1,2 @@
|
||||
#define BOOST_TEST_MODULE KylinLibraryTest
|
||||
#include <boost/test/included/unit_test.hpp>
|
BIN
resources/7za.exe
Normal file
BIN
resources/7za.exe
Normal file
Binary file not shown.
26
resources/L015掌静脉模组集成拓扑图.md
Normal file
26
resources/L015掌静脉模组集成拓扑图.md
Normal file
@ -0,0 +1,26 @@
|
||||
掌静脉模组支持掌静脉注册,识别功能,同时也提供通过图片注册掌静脉功能。
|
||||
|
||||
常见有两种应用场景:单模组使用,多设备集群分发注册识别。
|
||||
|
||||
## 单模组使用
|
||||
|
||||

|
||||
|
||||
在此应用场景下,【门禁面板机】需要完成和【掌静脉模组】通信协议的对接。
|
||||
|
||||
当注册用户时,门禁面板机向掌静脉模组发送注册命令使模块进入【注册模式】,并提示【用户】将【手掌】置于掌静脉模组上方进行注册,注册成功后,掌静脉模组将会上报一个【唯一标识ID】至门禁面板机,面板机需要将该ID与用户进行绑定。
|
||||
|
||||
注册成功后,门禁面板机可发送指令使模组切换至【识别模式】,当用户将手置于掌静脉模组上方,模组会和之前注册的掌静脉底库进行比对,比对结束之后,将会向门禁面板机上报ID以标识此次识别结果,此时面板机可依靠此ID和之前记录的注册ID进行比对,如果ID相同则表示匹配上该用户。
|
||||
|
||||
## 多模组集群使用
|
||||
|
||||

|
||||
|
||||
在此应用场景下,【门禁面板机】需要完成和【掌静脉模组】通行协议的对接,且需要服务器进行掌静脉注册图片管理及分发。
|
||||
|
||||
考虑到常见的门禁通行场景,掌静脉模组支持注册时获取掌静脉注册图片,然后再使用该图片进行掌静脉注册。
|
||||
|
||||
通过这种方式,用户在注册时,可以通掌静脉注册机获取掌静脉注册图片,然后在将掌静脉注册图片发送至服务器,服务器接收到图片之后,再将图片下发至各个门禁面板机以图片方式进行注册。这样就可以达到一端注册,多端使用的效果。
|
||||
|
||||
|
||||
|
403
resources/L015掌静脉识别模组串口通信协议 - 简化版.md
Normal file
403
resources/L015掌静脉识别模组串口通信协议 - 简化版.md
Normal file
@ -0,0 +1,403 @@
|
||||
## 1 串口配置
|
||||
|
||||
目前模组支持两种通信接口:UART,USB(CDC)。其中
|
||||
|
||||
UART:
|
||||
|
||||
- 波特率:115200
|
||||
- 数据位:8
|
||||
- 停止位:1
|
||||
- 奇偶检验:无
|
||||
- 流控制:无
|
||||
|
||||
USB(CDC):
|
||||
|
||||
- 波特率:2000000
|
||||
- 数据位:8
|
||||
- 停止位:1
|
||||
- 奇偶检验:无
|
||||
- 流控制:无
|
||||
|
||||
## 2 消息格式
|
||||
|
||||
主控和模块通讯的基本格式如下表所示,字节序为 **大端字节序(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 。<br/>0表示此消息无参数 |
|
||||
| ParityCheck | 1 byte | 协议的奇偶检验码。<br/>去除SyncWord,对 MsgID、DataSize、Data 的内容字节做XOR运算 |
|
||||
|
||||
## 3 消息列表
|
||||
|
||||
| MsgID | Code | 说明 |
|
||||
| ----------------- | ---- | ------------------------------------------------------------ |
|
||||
| MID_REPLY | 0x00 | 模组对主控发送出的命令的应答,对于主控下发的每条命令,模组最终都会使用 MID_REPLY 进行结果应答上报 |
|
||||
| MID_NOTE | 0x01 | 摸组主动上报给主控的信息,根据 NID 判断消息类型和对应的 Data 结构(详细内容见下文) |
|
||||
| MID_RESET | 0x10 | 主控下发,用于打断模组当前执行的任务 |
|
||||
| MID_VERIFY | 0x12 | 掌静脉识别比对 |
|
||||
| MID_ENROLL_SINGLE | 0x1D | 掌静脉录入 |
|
||||
| MID_DELUSER | 0x20 | 删除一个已录入的掌静脉 |
|
||||
| MID_DELALL | 0x21 | 删除所有已录入的掌静脉 |
|
||||
| MID_MODULE_ID | 0xAC | 获取模组ID,唯一标识 |
|
||||
|
||||
### 3.1 设备初始化完成
|
||||
|
||||
模组上电初始化完成后,会通过串口向主控发送一条 NID 为 NID_READY 的 MID_NOTE 消息: 0xEF 0xAA 0x01 0x00 0x01 0x00 0x00。(消息详细解释见 `模组状态上报(MID_NOTE)`)。
|
||||
|
||||
> 该消息仅支持使用串口连接,使用CDC不会上报此消息
|
||||
|
||||
主控在接收到 MID_NOTE 信息后,可以和模组进行指令交互。
|
||||
|
||||
### 3.2 模组复位(MID_RESET)
|
||||
|
||||
模组同一时刻,只能执行一个任务,当模组在执行耗时长的任务时(例如掌静脉识别 MID_VERIFY),主控可以向模组下发 MID_RESET 打断取消当前任务,进而再去执行其它任务。
|
||||
|
||||
该指令无需携带参数。
|
||||
|
||||
指令执行结束后,通过消息 MID_REPLY 返回结果:
|
||||
|
||||
- 0x00(MR_SUCCESS):复位成功
|
||||
- 0x05(MR_FAILED4_UNKNOWNREASON):未知错误
|
||||
|
||||
### 3.3 掌静脉识别(MID_VERIFY)
|
||||
|
||||
主控下发该指令给模组,模组开始检测摄像头图像中的掌静脉,并和底库中的掌静脉进行比对。指令下发携带的参数 msg_verify_data 定义如下:
|
||||
|
||||
```c++
|
||||
struct msg_verify_data {
|
||||
uint8_t reserved1;
|
||||
uint8_t timeout;
|
||||
uint8_t reserved2[6];
|
||||
};
|
||||
```
|
||||
|
||||
参数说明:
|
||||
|
||||
- reserved1:保留字段,暂未使用。需设置为 0x00。
|
||||
- timeout:识别超时时间,默认为10s,用户可以自行设置(最大不超过255s)。**主控等待模组录入应答的超时时间应大于此参数设置值。**
|
||||
- reserved2:保留字段,暂未使用。需设置为 0x00。
|
||||
|
||||
主控下发消息格式如下:
|
||||
|
||||
<br/>
|
||||
|
||||
<table align="center">
|
||||
<tr align="center">
|
||||
<th>SyncWord</th>
|
||||
<th>MsgID</th>
|
||||
<th>DataSize</th>
|
||||
<th colspan="3">Data</th>
|
||||
<th>ParityCheck</th>
|
||||
</tr>
|
||||
<tr align="center">
|
||||
<td>2 bytes</td>
|
||||
<td>1 byte</td>
|
||||
<td>2 bytes</td>
|
||||
<td colspan="3">8 bytes</td>
|
||||
<td>1 byte</td>
|
||||
</tr>
|
||||
<tr align="center">
|
||||
<td>0xEF 0xAA</td>
|
||||
<td>MID_VERIFY<br/>(0x12)</td>
|
||||
<td>0x08</td>
|
||||
<td>reserved1<br/>(1 byte)</td>
|
||||
<td>timeout<br/>(1 byte)</td>
|
||||
<td>reserved2<br/>(6 bytes)</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
|
||||
识别结果 Result 确认码的返回值见 `命令结果上报(MID_REPLY)`。
|
||||
|
||||
模组掌静脉识别成功后,通过消息 MID_REPLY 返回的数据 ResultData 定义如下:
|
||||
|
||||
```c++
|
||||
struct msg_reply_verify_data {
|
||||
uint16_t user_id;
|
||||
uint8_t username[32];
|
||||
uint8_t reserved[2];
|
||||
};
|
||||
```
|
||||
|
||||
参数说明:
|
||||
|
||||
- user_id:识别成功的用户 ID
|
||||
- username:识别成功的用户名字
|
||||
- reserved:保留字段,暂未使用。需设置为 0x00。
|
||||
|
||||
模组识别成功上报消息格式如下:
|
||||
|
||||
<table align="center">
|
||||
<tr align="center">
|
||||
<th rowspan="3">SyncWord</th>
|
||||
<th rowspan="3">MsgID</th>
|
||||
<th rowspan="3">DataSize</th>
|
||||
<th colspan="5">Data</th>
|
||||
<th rowspan="2">ParityCheck</th>
|
||||
</tr>
|
||||
<td>RID</td>
|
||||
<td>Result</td>
|
||||
<td colspan="3">ResultData</td>
|
||||
<tr>
|
||||
</tr>
|
||||
<tr align="center">
|
||||
<td>2 bytes</td>
|
||||
<td>1 byte</td>
|
||||
<td>2 bytes</td>
|
||||
<td colspan="5">N bytes</td>
|
||||
<td>1 byte</td>
|
||||
</tr>
|
||||
<tr>
|
||||
</tr>
|
||||
<tr align="center">
|
||||
<td>0xEF 0xAA</td>
|
||||
<td>MID_REPLY<br/>(0x00)</td>
|
||||
<td>0x26</td>
|
||||
<td>MID_VERIFY<br/>(0x12)</td>
|
||||
<td>Result<br/>(1 byte)</td>
|
||||
<td>user_id<br/>(2 bytes)</td>
|
||||
<td>username<br/>(32 bytes)</td>
|
||||
<td>reserved<br/>(2 bytes)</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### 3.4 掌静脉录入(MID_ENROLL_SINGLE)
|
||||
|
||||
掌静脉录入指令下发携带的参数 `msg_enroll_data` 定义如下:
|
||||
|
||||
```c++
|
||||
struct msg_enroll_data {
|
||||
uint8_t username[32];
|
||||
uint8_t reserved1[3];
|
||||
uint8_t timeout;
|
||||
uint8_t reserved2[4];
|
||||
};
|
||||
```
|
||||
|
||||
参数说明:
|
||||
|
||||
- username:录入用户的用户名。
|
||||
- reserved1:保留字段,暂未使用。需设置为 0x00。
|
||||
- timeout:录入过程的超时时间(s),默认为10s,用户可以自行设置(最大不超过255s)。**主控等待模组录入应答的超时时间应大于此参数设置值。**
|
||||
- reserved2:保留字段,暂未使用。需设置为 0x00。
|
||||
|
||||
录入结果 Result 确认码的返回值见 `命令结果上报(MID_REPLY)`。
|
||||
|
||||
模组掌静脉录入成功后,通过消息 MID_REPLY 返回的数据 ResultData 定义如下:
|
||||
|
||||
```c++
|
||||
struct msg_reply_enroll_data {
|
||||
uint16_t user_id;
|
||||
uint8_t reserved;
|
||||
};
|
||||
```
|
||||
|
||||
参数说明:
|
||||
|
||||
- user_id:用户 ID
|
||||
- reserved:保留,暂未使用。
|
||||
|
||||
### 3.5 删除单个掌静脉(MID_DELUSER)
|
||||
|
||||
通过传入用户 ID, 删除指定 ID 的单个用户。
|
||||
|
||||
删除单个用户指令携带参数 msg_deluser_data 定义如下:
|
||||
|
||||
```c++
|
||||
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不存在
|
||||
|
||||
### 3.6 删除所有掌静脉(MID_DELALL)
|
||||
|
||||
删除所有已录入的用户。
|
||||
|
||||
该指令无需携带参数。
|
||||
|
||||
指令执行结束后,通过消息 MID_REPLY 返回结果(主控等待应答超时时间建议设置为 5s):
|
||||
|
||||
- 0x00(MR_SUCCESS):删除成功
|
||||
- 0x05(MR_FAILED4_UNKNOWNREASON):未知错误
|
||||
|
||||
### 3.7 获取模组ID
|
||||
|
||||
该命令用于获取模组ID,类型为长度为32的字符串。
|
||||
|
||||
该指令无需携带参数。
|
||||
|
||||
模组通过消息 MID_REPLY 返回的数据 msg_reply_module_id 定义如下:
|
||||
|
||||
```c++
|
||||
struct msg_reply_module_id {
|
||||
char id[32];
|
||||
uint8_t reserved[512];
|
||||
};
|
||||
```
|
||||
|
||||
参数说明:
|
||||
|
||||
- id:长度不超过32的字符串ID。
|
||||
- reserved:保留,暂未使用。
|
||||
|
||||
### 3.9 命令结果上报(MID_REPLY)
|
||||
|
||||
模组向主控发送的 MID_REPLY 消息的完整协议如下所示:
|
||||
|
||||
<table align="center">
|
||||
<tr align="center">
|
||||
<th>SyncWord</th>
|
||||
<th>MsgID</th>
|
||||
<th>DataSize</th>
|
||||
<th colspan="3">Data</th>
|
||||
<th>ParityCheck</th>
|
||||
</tr>
|
||||
<tr align="center">
|
||||
<td>2 bytes</td>
|
||||
<td>1 byte</td>
|
||||
<td>2 bytes</td>
|
||||
<td colspan="3">N bytes</td>
|
||||
<td>1 byte</td>
|
||||
</tr>
|
||||
<tr align="center">
|
||||
<td>0xEF 0xAA</td>
|
||||
<td>MID_REPLY(0x00)</td>
|
||||
<td> </td>
|
||||
<td>RID(1 byte)</td>
|
||||
<td>Result(1 byte)</td>
|
||||
<td>ResultData( N-2 bytes)</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
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 | 未采用加密通讯 |
|
||||
|
||||
|
||||
|
||||
### 3.10 模组状态上报(MID_NOTE)
|
||||
|
||||
MID_NOTE 消息主要作用是主动向主控上报一些信息,报文格式如下:
|
||||
|
||||
<table align="center">
|
||||
<tr align="center">
|
||||
<th>SyncWord</th>
|
||||
<th>MsgID</th>
|
||||
<th>DataSize</th>
|
||||
<th colspan="2">Data</th>
|
||||
<th>ParityCheck</th>
|
||||
</tr>
|
||||
<tr align="center">
|
||||
<td>2 bytes</td>
|
||||
<td>1 byte</td>
|
||||
<td>2 bytes</td>
|
||||
<td colspan="2">N bytes</td>
|
||||
<td>1 byte</td>
|
||||
</tr>
|
||||
<tr align="center">
|
||||
<td>0xEF 0xAA</td>
|
||||
<td>MID_NOTE(0x01)</td>
|
||||
<td> </td>
|
||||
<td>NID(1 byte)</td>
|
||||
<td>NoteData( N-1 bytes)</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
目前NID主要内容如下:
|
||||
|
||||
| NID(*表示该消息携带 NoteData 数据) | Code | 说明 |
|
||||
| ------------------------------------ | ---- | -------------------- |
|
||||
| NID_READY | 0x00 | 模组已上电初始化成功 |
|
||||
|
||||
## 4 示例报文
|
||||
|
||||
- 掌静脉录入
|
||||
|
||||
录入 username为 test 的用户,超时时间 10s,主控发送:
|
||||
|
||||
0xef 0xaa 0x1d 0x00 0x28 0x74 0x65 0x73 0x74 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x0a 0x00 0x00 0x00 0x00 0x29
|
||||
|
||||
录入成功,ID为1,模组上报:
|
||||
|
||||
0xef 0xaa 0x00 0x00 0x05 0x1d 0x00 0x00 0x01 0x00 0x19
|
||||
|
||||
- 掌静脉识别
|
||||
|
||||
开始识别比对,超时时间20s,主控发送:
|
||||
|
||||
0xef 0xaa 0x12 0x00 0x02 0x00 0x14 0x04
|
||||
|
||||
识别比对成功,user_id为1,username为test ,模组上报:
|
||||
|
||||
0xef 0xaa 0x00 0x00 0x26 0x12 0x00 0x00 0x01 0x74 0x65 0x73 0x74 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x7a 0x59
|
||||
|
||||
- 删除所有录入掌静脉
|
||||
|
||||
主控发送:
|
||||
|
||||
0xef 0xaa 0x21 0x00 0x00 0x21
|
||||
|
||||
删除成功,模组上报:
|
||||
|
||||
0xef 0xaa 0x00 0x00 0x02 0x21 0x00 0x23
|
||||
|
||||
## 5 文档修改记录
|
||||
|
||||
2024/10/14:V0.1版本,描述模组识别、录入、删除几个基本协议指令。
|
||||
|
||||
2024/11/18:V0.2版本,`MID_VERIFY` 消息新增字段预留空间,用于功能扩展。
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -1,11 +1,23 @@
|
||||
## 1 串口配置
|
||||
|
||||
目前模组支持两种通信接口:UART,USB(CDC)。其中
|
||||
|
||||
UART:
|
||||
|
||||
- 波特率:115200
|
||||
- 数据位:8
|
||||
- 停止位:1
|
||||
- 奇偶检验:无
|
||||
- 流控制:无
|
||||
|
||||
USB(CDC):
|
||||
|
||||
- 波特率:2000000
|
||||
- 数据位:8
|
||||
- 停止位:1
|
||||
- 奇偶检验:无
|
||||
- 流控制:无
|
||||
|
||||
## 2 消息格式
|
||||
|
||||
主控和模块通讯的基本格式如下表所示,字节序为 **大端字节序(Big Endian)**:
|
||||
@ -35,6 +47,7 @@
|
||||
| MID_ENROLL_SINGLE | 0x1D | 掌静脉录入(单帧) |
|
||||
| MID_DELUSER | 0x20 | 删除一个已录入的掌静脉 |
|
||||
| MID_DELALL | 0x21 | 删除所有已录入的掌静脉 |
|
||||
| MID_MODULE_ID | 0xAC | 获取模组ID,唯一标识 |
|
||||
| MID_ENROLL_PALM_FEATUTE | 0xF9 | 主控下发掌静脉特征值给模组进行录入 |
|
||||
| MID_GET_PALM_FEATUTE | 0xFA | 主控请求获取指定用户掌静脉特征值 |
|
||||
|
||||
@ -230,6 +243,20 @@ struct msg_deluser_data {
|
||||
- 0x00(MR_SUCCESS):删除成功
|
||||
- 0x05(MR_FAILED4_UNKNOWNREASON):未知错误
|
||||
|
||||
### 3.7 获取模组ID
|
||||
|
||||
该命令用于获取模组ID,类型为长度为32的字符串。
|
||||
|
||||
该指令无需携带参数。
|
||||
|
||||
模组通过消息 MID_REPLY 返回的数据 msg_reply_module_id 定义如下:
|
||||
|
||||
```c++
|
||||
struct msg_reply_module_id {
|
||||
char id[32];
|
||||
};
|
||||
```
|
||||
|
||||
### 3.7 掌静脉特征值录入(MID_ENROLL_PALM_FEATUTE)
|
||||
|
||||
该命令用于直接将掌静脉特征值下发给模组进行录入。
|
||||
|
133
resources/build.ps1
Normal file
133
resources/build.ps1
Normal file
@ -0,0 +1,133 @@
|
||||
param($type)
|
||||
|
||||
# [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::GetEncoding("GBK")
|
||||
|
||||
$MsvcScript = 'D:\Program Files\Microsoft Visual Studio\2022\\Community\Common7\Tools\Launch-VsDevShell.ps1'
|
||||
if (!(Test-Path $MsvcScript)) { $MsvcScript = 'D:\Program Files\Microsoft Visual Studio\2022\\Professional\Common7\Tools\Launch-VsDevShell.ps1' }
|
||||
. $MsvcScript -SkipAutomaticLocation -Arch amd64
|
||||
|
||||
$qtHome = "D:\Qt\6.8.0\msvc2022_64"
|
||||
$openSSLRoot = "D:\Qt\Tools\OpenSSLv3\Win_x64"
|
||||
|
||||
$librariesPath = "E:\Projects\Libraries"
|
||||
if (!(Test-Path $librariesPath)) { $librariesPath = "D:\Projects\Libraries" }
|
||||
$boostRoot = "$librariesPath\boost_1_86_0_msvc2022_64bit"
|
||||
$ffmpegRoot = "$librariesPath\ffmpeg-7.0.2-full_build-shared"
|
||||
|
||||
$projectPath = Get-Location
|
||||
$buildPath = Join-Path -Path $projectPath -ChildPath "build"
|
||||
|
||||
$fileContent = (Get-Content -Path "CMakeLists.txt") -join " "
|
||||
if ($fileContent -match 'project\([^\)]+VERSION\s+([0-9]+\.[0-9]+)') {
|
||||
$version = $Matches[1]
|
||||
} else {
|
||||
Write-Output "未找到版本号"
|
||||
}
|
||||
|
||||
$deployPath = Join-Path -Path $buildPath -ChildPath "掌静脉工具v$version"
|
||||
$changelogPath = Join-Path -Path $buildPath -ChildPath "CHANGELOG.txt"
|
||||
|
||||
function Build() {
|
||||
if (!(Test-Path $buildPath\CMakeCache.txt)) {
|
||||
cmake.exe -G Ninja -S . -B build `
|
||||
-DCMAKE_BUILD_TYPE=Release `
|
||||
-DCMAKE_PREFIX_PATH=$qtHome `
|
||||
-DQT_DIR="$qtHome\lib\cmake\Qt6" `
|
||||
-DQt6_DIR="$qtHome\lib\cmake\Qt6" `
|
||||
-DQt6CoreTools_DIR="$qtHome\lib\cmake\Qt6CoreTools" `
|
||||
-DQt6QmlTools_DIR="$qtHome\lib\cmake\Qt6QmlTools" `
|
||||
-DLibraries_ROOT="$librariesPath"
|
||||
}
|
||||
|
||||
cmake.exe --build $buildPath --target all
|
||||
}
|
||||
|
||||
function Deploy() {
|
||||
if (Test-Path $deployPath) {
|
||||
Remove-Item $deployPath -Recurse -Force
|
||||
}
|
||||
New-Item $deployPath -ItemType Directory
|
||||
Copy-Item $buildPath\Analyser\Analyser.exe $deployPath\Analyser.exe
|
||||
Copy-Item $buildPath\OtaUpdate\SmartLockerUpdater.exe $deployPath\SmartLockerUpdater.exe
|
||||
|
||||
$executables = @(
|
||||
@("Analyser.exe", "掌静脉测试工具.exe"),
|
||||
@("SmartLockerUpdater.exe", "掌静脉模组升级工具.exe")
|
||||
)
|
||||
foreach ($executablePair in $executables) {
|
||||
$oldName = $executablePair[0]
|
||||
$newName = $executablePair[1]
|
||||
& $qtHome\bin\windeployqt.exe $deployPath\$oldName --qmldir=$qtHome\qml
|
||||
Rename-Item -Path $deployPath\$oldName -NewName $deployPath\$newName
|
||||
}
|
||||
Remove-Item -Path $deployPath\Qt6Quick3D*
|
||||
Remove-Item -Path $deployPath\translations -Recurse -Force # 暂时不需要翻译文件
|
||||
Remove-Item -Path $deployPath\qmltooling -Recurse -Force
|
||||
|
||||
$modules = "QmlCore"
|
||||
foreach ($module in $modules) {
|
||||
Copy-Item -Path $qtHome\bin\Qt6$module.dll -Destination $deployPath
|
||||
}
|
||||
|
||||
if (-Not (Test-Path -Path $deployPath\qml\QtCore)) {
|
||||
New-Item $deployPath\qml\QtCore -ItemType Directory
|
||||
$plugins = "qtqmlcoreplugin.dll", "qmldir", "plugins.qmltypes"
|
||||
foreach ($plugin in $plugins) {
|
||||
Copy-Item -Path $QtHome\qml\QtCore\$plugin -Destination $deployPath\qml\QtCore
|
||||
}
|
||||
}
|
||||
|
||||
Copy-Item $openSSLRoot\bin\libssl-3-x64.dll $deployPath
|
||||
Copy-Item $openSSLRoot\bin\libcrypto-3-x64.dll $deployPath
|
||||
|
||||
$boosts = "thread", "filesystem", "log", "json"
|
||||
foreach ($boost in $boosts) {
|
||||
Copy-Item -Path $boostRoot\lib\boost_$boost-vc143-mt-x64-1_86.dll -Destination $deployPath
|
||||
}
|
||||
|
||||
$ffmpegs = "avcodec-61", "avdevice-61", "avformat-61", "avutil-59", "postproc-58", "swresample-5", "swscale-8" # avfilter-10
|
||||
foreach ($ffmpeg in $ffmpegs) {
|
||||
Copy-Item -Path $ffmpegRoot\bin\$ffmpeg.dll -Destination $deployPath
|
||||
}
|
||||
|
||||
$zipFilePath = Join-Path -Path $buildPath -ChildPath "掌静脉工具v$version.7z"
|
||||
# Compress-Archive -Path $deployPath -DestinationPath $zipFilePath -Force
|
||||
& resources\7za.exe a -t7z -mx=9 $zipFilePath $deployPath
|
||||
}
|
||||
|
||||
function Clean() {
|
||||
if (Test-Path $buildPath) {
|
||||
Remove-Item $buildPath -Recurse -Force
|
||||
}
|
||||
}
|
||||
|
||||
function Changelog() {
|
||||
$commit_message = git log -1 --pretty=format:"%B"
|
||||
Write-Output "Latest commit message:"
|
||||
Write-Output $commit_message
|
||||
$commit_message | Out-File -FilePath $changelogPath -Encoding utf8
|
||||
Write-Output "Commit message has been written to $changelogPath"
|
||||
}
|
||||
|
||||
|
||||
switch ($type) {
|
||||
"build" {
|
||||
Build
|
||||
}
|
||||
"deploy" {
|
||||
Deploy
|
||||
}
|
||||
"clean" {
|
||||
Clean
|
||||
}
|
||||
"installer" {
|
||||
Installer
|
||||
}
|
||||
"changelog" {
|
||||
Changelog
|
||||
}
|
||||
"update" {
|
||||
UpdateServer
|
||||
}
|
||||
}
|
134
resources/build.sh
Executable file
134
resources/build.sh
Executable file
@ -0,0 +1,134 @@
|
||||
#!/bin/bash
|
||||
|
||||
base_path=$(pwd)
|
||||
if [[ $base_path =~ ^/mnt/ ]]; then
|
||||
build_path=/tmp/build
|
||||
else
|
||||
build_path=${base_path}/build
|
||||
fi
|
||||
echo "build path: $build_path"
|
||||
qt_prefix_path="/opt/Qt/6.8.0/gcc_64"
|
||||
|
||||
debug_deploy=false
|
||||
|
||||
if [ -d ${qt_prefix_path} ]; then # 先找Qt6
|
||||
cmake_qt_parameters="-DCMAKE_PREFIX_PATH=${qt_prefix_path} \
|
||||
-DQT_QMAKE_EXECUTABLE=${qt_prefix_path}/bin/qmake \
|
||||
-DQT_DIR=${qt_prefix_path}/lib/cmake/Qt6 \
|
||||
-DQt6_DIR=${qt_prefix_path}/lib/cmake/Qt6 \
|
||||
-DQt6Core_DIR=${qt_prefix_path}/lib/cmake/Qt6Core \
|
||||
-DQt6Gui_DIR=${qt_prefix_path}/lib/cmake/Qt6Gui \
|
||||
-DQt6Qml_DIR=${qt_prefix_path}/lib/cmake/Qt6Qml \
|
||||
-DQt6Widgets_DIR=${qt_prefix_path}/lib/cmake/Qt6Widgets \
|
||||
-DQt6Quick_DIR=${qt_prefix_path}/lib/cmake/Qt6Quick \
|
||||
-DQt6Svg_DIR=${qt_prefix_path}/lib/cmake/Qt6Svg "
|
||||
elif [ -d "/opt/Qt/5.15.2/gcc_64" ]; then
|
||||
qt_prefix_path="/opt/Qt/5.15.2/gcc_64"
|
||||
cmake_qt_parameters="-DCMAKE_PREFIX_PATH=${qt_prefix_path} \
|
||||
-DQT_QMAKE_EXECUTABLE=${qt_prefix_path}/bin/qmake \
|
||||
-DQT_DIR=${qt_prefix_path}/lib/cmake/Qt5 \
|
||||
-DQt5_DIR=${qt_prefix_path}/lib/cmake/Qt5 \
|
||||
-DQt5Core_DIR=${qt_prefix_path}/lib/cmake/Qt5Core \
|
||||
-DQt5Gui_DIR=${qt_prefix_path}/lib/cmake/Qt5Gui \
|
||||
-DQt5Qml_DIR=${qt_prefix_path}/lib/cmake/Qt5Qml \
|
||||
-DQt5Widgets_DIR=${qt_prefix_path}/lib/cmake/Qt5Widgets \
|
||||
-DQt5Quick_DIR=${qt_prefix_path}/lib/cmake/Qt5Quick \
|
||||
-DQt5Svg_DIR=${qt_prefix_path}/lib/cmake/Qt5Svg "
|
||||
else
|
||||
cmake_qt_parameters=""
|
||||
echo "please install qt6.8.0 or qt5.15.2 ..."
|
||||
fi
|
||||
|
||||
function cmake_scan() {
|
||||
if [ ! -d ${build_path} ]; then
|
||||
mkdir ${build_path}
|
||||
fi
|
||||
|
||||
if [ $debug_deploy = true ]; then
|
||||
build_mode=-DCMAKE_BUILD_TYPE=Debug
|
||||
echo "build application in debug mode."
|
||||
else
|
||||
build_mode=-DCMAKE_BUILD_TYPE=Release
|
||||
echo "build application in release mode."
|
||||
fi
|
||||
|
||||
cmake \
|
||||
-G Ninja \
|
||||
-S ${base_path} \
|
||||
-B ${build_path} \
|
||||
${cmake_qt_parameters} \
|
||||
$build_mode
|
||||
}
|
||||
|
||||
function build() {
|
||||
if [ ! -f "${build_path}/CMakeCache.txt" ]; then
|
||||
cmake_scan
|
||||
fi
|
||||
if [ $? -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
cmake \
|
||||
--build ${build_path} \
|
||||
--target all
|
||||
if [ $? -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
function clean(){
|
||||
rm -fr ${build_path}
|
||||
}
|
||||
|
||||
function deploy() {
|
||||
if [ -d ${build_path}/lib ]; then
|
||||
rm -fr ${build_path}/lib
|
||||
fi
|
||||
mkdir ${build_path}/lib
|
||||
ldd ${build_path}/Analyser/Analyser.AppImage | grep "=> /" | awk '{print $3}' | xargs -I '{}' cp -v '{}' ${build_path}/lib
|
||||
ldd ${build_path}/Analyser/Analyser.AppImage | grep "ld-linux" | awk '{print $1}' | xargs -I '{}' cp -v '{}' ${build_path}/lib
|
||||
# ldd ${build_path}/OtaUpdate/SmartLockerUpdater | grep "=> /" | awk '{print $3}' | xargs -I '{}' cp -v '{}' ${build_path}/lib
|
||||
# ldd ${build_path}/OtaUpdate/SmartLockerUpdater | grep "ld-linux" | awk '{print $1}' | xargs -I '{}' cp -v '{}' ${build_path}/lib
|
||||
|
||||
rm ${build_path}/lib/ld-linux-x86-64.so.2
|
||||
rm ${build_path}/lib/libc.so.6
|
||||
rm ${build_path}/lib/libm.so.6
|
||||
rm ${build_path}/lib/libstdc++.so.6
|
||||
cd build
|
||||
cpack
|
||||
cd ..
|
||||
}
|
||||
|
||||
function change_log() {
|
||||
commit_message=$(git log -1 --pretty=format:"%B")
|
||||
echo "Latest commit message:"
|
||||
echo "$commit_message"
|
||||
echo "$commit_message" >${build_path}/CHANGELOG.txt
|
||||
echo "Commit message has been written to ${build_path}/CHANGELOG.txt"
|
||||
}
|
||||
|
||||
function main() {
|
||||
local cmd=$1
|
||||
shift 1
|
||||
case $cmd in
|
||||
build)
|
||||
build
|
||||
;;
|
||||
scan)
|
||||
cmake_scan
|
||||
;;
|
||||
clean)
|
||||
clean
|
||||
;;
|
||||
deploy)
|
||||
deploy
|
||||
;;
|
||||
changelog)
|
||||
change_log
|
||||
;;
|
||||
*)
|
||||
build
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main $@
|
288
resources/local_use.svg
Normal file
288
resources/local_use.svg
Normal file
@ -0,0 +1,288 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- 由 Microsoft Visio, SVG Export 生成 绘图1.svg 页-1 -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:ev="http://www.w3.org/2001/xml-events"
|
||||
xmlns:v="http://schemas.microsoft.com/visio/2003/SVGExtensions/" width="1.39583in" height="3.17224in"
|
||||
viewBox="0 0 100.5 228.401" xml:space="preserve" color-interpolation-filters="sRGB" class="st20">
|
||||
<v:documentProperties v:langID="2052" v:metric="true" v:viewMarkup="false"/>
|
||||
|
||||
<style type="text/css">
|
||||
<![CDATA[
|
||||
.st1 {fill:#ff00ff;fill-opacity:0;stroke:#c7c8c8;stroke-opacity:0;stroke-width:0.72}
|
||||
.st2 {fill:url(#grad0-7);stroke:#ffffff;stroke-width:0.72}
|
||||
.st3 {fill:url(#grad11-11)}
|
||||
.st4 {stroke:#000000;stroke-width:0.72}
|
||||
.st5 {stroke:#c8c8c8;stroke-width:1.5}
|
||||
.st6 {fill:#ff00ff;fill-opacity:0}
|
||||
.st7 {stroke:#c7c8c8;stroke-opacity:0;stroke-width:0.72}
|
||||
.st8 {fill:url(#grad7-25);stroke:none;stroke-width:0.72}
|
||||
.st9 {fill:url(#grad0-29);stroke:none;stroke-width:0.72}
|
||||
.st10 {stroke:#ffffff;stroke-width:0.72}
|
||||
.st11 {fill:url(#grad3-40);stroke:#c7c8c8;stroke-width:0.12}
|
||||
.st12 {fill:url(#grad7-44);stroke:#c7c8c8;stroke-width:0.24}
|
||||
.st13 {fill:url(#grad10-48);stroke:#000000;stroke-width:0.72}
|
||||
.st14 {fill:#ffffff;stroke:#000000;stroke-width:0.24}
|
||||
.st15 {fill:url(#grad0-58);stroke:#000000;stroke-width:0.72}
|
||||
.st16 {fill:none;stroke:none;stroke-width:0.25}
|
||||
.st17 {fill:#4672c4;font-family:黑体;font-size:1.16666em}
|
||||
.st18 {marker-end:url(#mrkr13-73);marker-start:url(#mrkr13-71);stroke:#4672c4;stroke-linecap:round;stroke-linejoin:round;stroke-width:1}
|
||||
.st19 {fill:#4672c4;fill-opacity:1;stroke:#4672c4;stroke-opacity:1;stroke-width:0.28409090909091}
|
||||
.st20 {fill:none;fill-rule:evenodd;font-size:12px;overflow:visible;stroke-linecap:square;stroke-miterlimit:3}
|
||||
]]>
|
||||
</style>
|
||||
|
||||
<defs id="Patterns_And_Gradients">
|
||||
<linearGradient id="grad0-7" x1="0" y1="0" x2="1" y2="0" gradientTransform="rotate(270 0.5 0.5)">
|
||||
<stop offset="0" stop-color="#668ace" stop-opacity="1"/>
|
||||
<stop offset="1" stop-color="#626262" stop-opacity="1"/>
|
||||
</linearGradient>
|
||||
<pattern id="grad11-11" x="0" y="0" width="1" height="1" patternContentUnits="objectBoundingBox">
|
||||
<path d="M 0 1 L 0 0 L 1 0 z" style="fill:url(#grad0-12)"/>
|
||||
<path d="M 0 1 L 1 1 L 1 0 z" style="fill:url(#grad0-13)"/>
|
||||
</pattern>
|
||||
<linearGradient id="grad0-12" x1="0" y1="0" x2="1" y2="0" gradientTransform="rotate(180 0.5 0.5)">
|
||||
<stop offset="0" stop-color="#ffffff" stop-opacity="1"/>
|
||||
<stop offset="1" stop-color="#c0c0c0" stop-opacity="1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="grad0-13" x1="0" y1="0" x2="1" y2="0" gradientTransform="rotate(90 0.5 0.5)">
|
||||
<stop offset="0" stop-color="#ffffff" stop-opacity="1"/>
|
||||
<stop offset="1" stop-color="#c0c0c0" stop-opacity="1"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="grad7-25" cx="0" cy="0" r="1.4">
|
||||
<stop offset="0" stop-color="#ffffff" stop-opacity="1"/>
|
||||
<stop offset="1" stop-color="#c0c0c0" stop-opacity="1"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="grad0-29" x1="0" y1="0" x2="1" y2="0" gradientTransform="rotate(180 0.5 0.5)">
|
||||
<stop offset="0.01" stop-color="#3e3e3e" stop-opacity="1"/>
|
||||
<stop offset="0.5" stop-color="#365fa9" stop-opacity="1"/>
|
||||
<stop offset="1" stop-color="#3e3e3e" stop-opacity="1"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="grad3-40" cx="0.5" cy="0.5" r="0.73">
|
||||
<stop offset="0" stop-color="#ffffff" stop-opacity="1"/>
|
||||
<stop offset="1" stop-color="#008000" stop-opacity="1"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="grad7-44" cx="0" cy="0" r="1.4">
|
||||
<stop offset="0" stop-color="#ffffff" stop-opacity="1"/>
|
||||
<stop offset="1" stop-color="#000000" stop-opacity="1"/>
|
||||
</radialGradient>
|
||||
<pattern id="grad10-48" x="0" y="0" width="1" height="1" patternContentUnits="objectBoundingBox">
|
||||
<path d="M 0.5 0.5 L 0 0 L 0 1 z" style="fill:url(#grad0-49)"/>
|
||||
<path d="M 0.5 0.5 L 1 0 L 1 1 z" style="fill:url(#grad0-50)"/>
|
||||
<path d="M 0.5 0.5 L 0 0 L 1 0 z" style="fill:url(#grad0-51)"/>
|
||||
<path d="M 0.5 0.5 L 0 1 L 1 1 z" style="fill:url(#grad0-52)"/>
|
||||
</pattern>
|
||||
<linearGradient id="grad0-49" x1="0" y1="0" x2="1" y2="0" gradientTransform="rotate(180 0.5 0.5)">
|
||||
<stop offset="0" stop-color="#ffffff" stop-opacity="1"/>
|
||||
<stop offset="1" stop-color="#c6d1e3" stop-opacity="1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="grad0-50" x1="0" y1="0" x2="1" y2="0" gradientTransform="rotate(360 0.5 0.5)">
|
||||
<stop offset="0" stop-color="#ffffff" stop-opacity="1"/>
|
||||
<stop offset="1" stop-color="#c6d1e3" stop-opacity="1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="grad0-51" x1="0" y1="0" x2="1" y2="0" gradientTransform="rotate(270 0.5 0.5)">
|
||||
<stop offset="0" stop-color="#ffffff" stop-opacity="1"/>
|
||||
<stop offset="1" stop-color="#c6d1e3" stop-opacity="1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="grad0-52" x1="0" y1="0" x2="1" y2="0" gradientTransform="rotate(90 0.5 0.5)">
|
||||
<stop offset="0" stop-color="#ffffff" stop-opacity="1"/>
|
||||
<stop offset="1" stop-color="#c6d1e3" stop-opacity="1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="grad0-58" x1="0" y1="0" x2="1" y2="0" gradientTransform="rotate(90 0.5 0.5)">
|
||||
<stop offset="0.01" stop-color="#000000" stop-opacity="1"/>
|
||||
<stop offset="0.5" stop-color="#ffffff" stop-opacity="1"/>
|
||||
<stop offset="1" stop-color="#000000" stop-opacity="1"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<defs id="Markers">
|
||||
<g id="lend13">
|
||||
<path d="M 3 1 L 0 0 L 3 -1 L 3 1 " style="stroke:none"/>
|
||||
</g>
|
||||
<marker id="mrkr13-71" class="st19" v:arrowType="13" v:arrowSize="2" v:setback="10.2" refX="10.2" orient="auto"
|
||||
markerUnits="strokeWidth" overflow="visible">
|
||||
<use xlink:href="#lend13" transform="scale(3.52) "/>
|
||||
</marker>
|
||||
<marker id="mrkr13-73" class="st19" v:arrowType="13" v:arrowSize="2" v:setback="10.56" refX="-10.56" orient="auto"
|
||||
markerUnits="strokeWidth" overflow="visible">
|
||||
<use xlink:href="#lend13" transform="scale(-3.52,-3.52) "/>
|
||||
</marker>
|
||||
</defs>
|
||||
<g v:mID="0" v:index="1" v:groupContext="foregroundPage">
|
||||
<title>页-1</title>
|
||||
<v:pageProperties v:drawingScale="0.0393701" v:pageScale="0.0393701" v:drawingUnits="24" v:shadowOffsetX="8.50394"
|
||||
v:shadowOffsetY="-8.50394"/>
|
||||
<v:layer v:name="连接线" v:index="0"/>
|
||||
<g id="group66-1" transform="translate(33.9508,-22.4051)" v:mID="66" v:groupContext="group">
|
||||
<v:custProps>
|
||||
<v:cp v:nameU="ShapeClass" v:lbl="ShapeClass" v:type="0" v:invis="true" v:ask="false" v:langID="2052"
|
||||
v:val="VT4(连接性)"/>
|
||||
<v:cp v:nameU="ShapeType" v:lbl="ShapeType" v:type="0" v:invis="true" v:ask="false" v:langID="2052" v:val="VT4(概念)"/>
|
||||
<v:cp v:nameU="SubShapeType" v:lbl="SubShapeType" v:type="0" v:invis="true" v:ask="false" v:langID="2052"
|
||||
v:val="VT4(数据)"/>
|
||||
</v:custProps>
|
||||
<v:userDefs>
|
||||
<v:ud v:nameU="visLegendShape" v:val="VT0(2):26"/>
|
||||
<v:ud v:nameU="HasText" v:val="VT0(0):5"/>
|
||||
<v:ud v:nameU="SolSH" v:val="VT15({BF0433D9-CD73-4EB5-8390-8653BE590246}):41"/>
|
||||
<v:ud v:nameU="ShapeClass" v:val="VT0(4):26"/>
|
||||
<v:ud v:nameU="ShapeType" v:val="VT0(7):26"/>
|
||||
<v:ud v:nameU="SubShapeType" v:val="VT0(15):26"/>
|
||||
<v:ud v:nameU="visVersion" v:prompt="" v:val="VT0(15):26"/>
|
||||
</v:userDefs>
|
||||
<title>数据</title>
|
||||
<g id="shape67-2" v:mID="67" v:groupContext="shape" transform="translate(0,0.000575512)">
|
||||
<title>工作表.67</title>
|
||||
<path d="M32.54 205.26 A16.2701 9.45359 0.18 0 1 32.15 207.35 A16.2701 9.45359 -179.82 0 0 32.54 205.26 ZM32.54 205.26
|
||||
A16.2701 9.45359 -179.82 0 0 0 205.26 L0 218.95 A16.2702 9.45363 -179.82 0 0 32.54 218.95 L32.6 218.95
|
||||
L32.6 205.26 L32.54 205.26 Z" class="st1"/>
|
||||
</g>
|
||||
<g id="shape68-4" v:mID="68" v:groupContext="shape" transform="translate(-1.65867E-13,-13.6911)">
|
||||
<title>工作表.68</title>
|
||||
<path d="M0 218.9 A16.2701 9.45359 -179.82 1 1 32.54 219 A16.2701 9.45359 -179.82 1 1 0 218.9 Z" class="st2"/>
|
||||
</g>
|
||||
<g id="shape69-8" v:mID="69" v:groupContext="shape">
|
||||
<title>工作表.69</title>
|
||||
<path d="M0 205.26 L0 218.9 L0 218.95 A16.2702 9.45363 -179.82 0 0 32.54 218.95 L32.6 218.95 L32.6 205.26 L32.54
|
||||
205.26 A16.2701 9.45359 0.18 0 1 0 205.26 L0 205.26 Z" class="st3"/>
|
||||
<path d="M0 205.26 L0 218.9 L0 218.95 A16.2702 9.45363 -179.82 0 0 32.54 218.95 L32.6 218.95 L32.6 205.26 L32.54
|
||||
205.26 A16.2701 9.45359 0.18 0 1 0 205.26" class="st4"/>
|
||||
</g>
|
||||
<g id="shape70-15" v:mID="70" v:groupContext="shape" transform="translate(0,0.000264735)">
|
||||
<title>工作表.70</title>
|
||||
<path d="M32.54 205.26 A16.2701 9.45359 -179.82 1 0 0 205.26 L0 218.95 A16.2702 9.45363 -179.82 0 0 32.54 218.95
|
||||
L32.6 218.95 L32.6 205.26 L32.54 205.26" class="st5"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="group71-18" transform="translate(16.2343,-106.027)" v:mID="71" v:groupContext="group">
|
||||
<v:custProps>
|
||||
<v:cp v:nameU="ShapeClass" v:lbl="ShapeClass" v:type="0" v:invis="true" v:ask="false" v:langID="2052"
|
||||
v:val="VT4(设备)"/>
|
||||
<v:cp v:nameU="ShapeType" v:lbl="ShapeType" v:type="0" v:invis="true" v:ask="false" v:langID="2052"
|
||||
v:val="VT4(计算机)"/>
|
||||
<v:cp v:nameU="SubShapeType" v:lbl="SubShapeType" v:type="0" v:invis="true" v:ask="false" v:langID="2052"
|
||||
v:val="VT4(平板)"/>
|
||||
<v:cp v:nameU="Manufacturer" v:lbl="制造商" v:type="0" v:sortKey="Equipment" v:invis="false" v:ask="false"
|
||||
v:langID="2052"/>
|
||||
<v:cp v:nameU="ProductNumber" v:lbl="产品编号" v:type="0" v:sortKey="Equipment" v:invis="false" v:ask="false"
|
||||
v:langID="2052"/>
|
||||
<v:cp v:nameU="PartNumber" v:lbl="部件号" v:type="0" v:sortKey="Equipment" v:invis="false" v:ask="false"
|
||||
v:langID="2052"/>
|
||||
<v:cp v:nameU="ProductDescription" v:lbl="产品说明" v:type="0" v:sortKey="Equipment" v:invis="false" v:ask="false"
|
||||
v:langID="2052"/>
|
||||
<v:cp v:nameU="AssetNumber" v:lbl="资产号" v:type="0" v:sortKey="Asset" v:invis="false" v:ask="false" v:langID="2052"/>
|
||||
<v:cp v:nameU="SerialNumber" v:lbl="序列号" v:type="0" v:sortKey="Asset" v:invis="false" v:ask="false" v:langID="2052"/>
|
||||
<v:cp v:nameU="Location" v:lbl="位置" v:type="0" v:sortKey="Asset" v:invis="false" v:ask="false" v:langID="2052"/>
|
||||
<v:cp v:nameU="Building" v:lbl="建筑物" v:type="0" v:sortKey="Asset" v:invis="false" v:ask="false" v:langID="2052"/>
|
||||
<v:cp v:nameU="Room" v:lbl="房间" v:type="0" v:sortKey="Asset" v:invis="false" v:ask="false" v:langID="2052"/>
|
||||
<v:cp v:nameU="NetworkName" v:lbl="网络名称" v:type="0" v:sortKey="Network" v:invis="false" v:ask="false"
|
||||
v:langID="2052"/>
|
||||
<v:cp v:nameU="IPAddress" v:lbl="IP 地址" v:type="0" v:sortKey="Network" v:invis="false" v:ask="false"
|
||||
v:langID="2052"/>
|
||||
<v:cp v:nameU="SubnetMask" v:lbl="子网掩码" v:type="0" v:sortKey="Network" v:invis="false" v:ask="false"
|
||||
v:langID="2052"/>
|
||||
<v:cp v:nameU="AdminInterface" v:lbl="管理接口" v:type="0" v:sortKey="Network" v:invis="false" v:ask="false"
|
||||
v:langID="2052"/>
|
||||
<v:cp v:nameU="NumberofPorts" v:lbl="端口数目" v:type="0" v:sortKey="Network" v:invis="false" v:ask="false"
|
||||
v:langID="2052"/>
|
||||
<v:cp v:nameU="MACAddress" v:lbl="MAC 地址" v:type="0" v:sortKey="Network" v:invis="false" v:ask="false"
|
||||
v:langID="2052"/>
|
||||
<v:cp v:nameU="CommunityString" v:lbl="团体字符串" v:type="0" v:sortKey="Network" v:invis="false" v:ask="false"
|
||||
v:langID="2052"/>
|
||||
<v:cp v:nameU="NetworkDescription" v:lbl="网络说明" v:type="0" v:sortKey="Network" v:invis="false" v:ask="false"
|
||||
v:langID="2052"/>
|
||||
<v:cp v:nameU="HardDriveSize" v:lbl="硬盘容量" v:type="0" v:sortKey="Workstation" v:invis="false" v:ask="false"
|
||||
v:langID="2052"/>
|
||||
<v:cp v:nameU="CPU" v:lbl="CPU" v:type="0" v:sortKey="Workstation" v:invis="false" v:ask="false" v:langID="2052"/>
|
||||
<v:cp v:nameU="Memory" v:lbl="内存" v:type="0" v:sortKey="Workstation" v:invis="false" v:ask="false" v:langID="2052"/>
|
||||
<v:cp v:nameU="OperatingSystem" v:lbl="操作系统" v:type="0" v:sortKey="Workstation" v:invis="false" v:ask="false"
|
||||
v:langID="2052"/>
|
||||
</v:custProps>
|
||||
<v:userDefs>
|
||||
<v:ud v:nameU="HasText" v:val="VT0(0):5"/>
|
||||
<v:ud v:nameU="ShapeClass" v:val="VT0(5):26"/>
|
||||
<v:ud v:nameU="ShapeType" v:val="VT0(6):26"/>
|
||||
<v:ud v:nameU="SubShapeType" v:val="VT0(69):26"/>
|
||||
<v:ud v:nameU="SolSH" v:val="VT15({BF0433D9-CD73-4EB5-8390-8653BE590246}):41"/>
|
||||
<v:ud v:nameU="visLegendShape" v:val="VT0(2):26"/>
|
||||
<v:ud v:nameU="visVersion" v:prompt="" v:val="VT0(15):26"/>
|
||||
</v:userDefs>
|
||||
<title>平板电脑</title>
|
||||
<g id="shape72-19" v:mID="72" v:groupContext="shape" transform="translate(0,8.6288E-06)">
|
||||
<title>工作表.72</title>
|
||||
<path d="M22.75 134.89 A158.111 145.912 89.84 0 1 41.19 144.31 A158.315 146.1 -89.8 0 0 22.75 134.89 ZM2.33 154.56
|
||||
A2.43186 1.43688 -80 1 0 1.93 158.87 A3.97821 3.67126 90 0 1 1.53 159.34 L22.75 163.3 L22.75 205.59
|
||||
A105.542 97.3984 -90.11 0 0 59.76 228.4 A11.7512 10.8448 -89.38 0 0 66.73 223.18 L66.84 153.37 A5.68818
|
||||
5.2493 89.94 0 1 59.76 157.7 A6.18103 5.7045 -89 0 0 66.84 153.37 A126.613 116.845 -90.37 0 0 29.5 130.79
|
||||
A6.74865 6.22798 -89.71 0 0 22.75 134.89 L22.75 159.32 L2.27 154.55 L2.33 154.56 Z" class="st6"/>
|
||||
<path d="M22.75 134.89 A158.111 145.912 89.84 0 1 41.19 144.31 A158.315 146.1 -89.8 0 0 22.75 134.89M2.33 154.56
|
||||
A2.43186 1.43688 -80 1 0 1.93 158.87 A3.97821 3.67126 90 0 1 1.53 159.34 L22.75 163.3 L22.75 205.59
|
||||
A105.542 97.3984 -90.11 0 0 59.76 228.4 A11.7512 10.8448 -89.38 0 0 66.73 223.18 L66.84 153.37 A5.68818
|
||||
5.2493 89.94 0 1 59.76 157.7 A6.18103 5.7045 -89 0 0 66.84 153.37 A126.613 116.845 -90.37 0 0 29.5 130.79
|
||||
A6.74865 6.22798 -89.71 0 0 22.75 134.89 L22.75 159.32 L2.27 154.55" class="st7"/>
|
||||
</g>
|
||||
<g id="shape73-22" v:mID="73" v:groupContext="shape" transform="translate(23.9471,0)">
|
||||
<title>工作表.73</title>
|
||||
<path d="M0 205.59 A105.542 97.3984 -90.11 0 0 37.01 228.4 L37.01 157.7 A158.315 146.1 -89.8 0 0 0 134.89 L0 205.59
|
||||
Z" class="st8"/>
|
||||
</g>
|
||||
<g id="shape74-26" v:mID="74" v:groupContext="shape" transform="translate(60.9562,0)">
|
||||
<title>工作表.74</title>
|
||||
<path d="M-0 228.4 A11.7512 10.8448 -89.38 0 0 6.97 223.18 L7.08 153.37 A5.68818 5.2493 89.94 0 1 0 157.7 L0 228.4
|
||||
Z" class="st9"/>
|
||||
</g>
|
||||
<g id="shape75-30" v:mID="75" v:groupContext="shape" transform="translate(23.9471,-70.4662)">
|
||||
<title>工作表.75</title>
|
||||
<path d="M-0 205.36 A158.111 145.912 89.84 0 1 37.01 228.17 A6.18103 5.7045 -89 0 0 44.08 223.83 A126.613 116.845
|
||||
-90.37 0 0 6.75 201.25 A6.74865 6.22798 -89.71 0 0 0 205.36 Z" class="st2"/>
|
||||
</g>
|
||||
<g id="shape76-33" v:mID="76" v:groupContext="shape" transform="translate(27.648,-10.263)">
|
||||
<title>工作表.76</title>
|
||||
<path d="M30.59 228.4 L0 209.93 L0 151.2" class="st10"/>
|
||||
</g>
|
||||
<g id="shape77-37" v:mID="77" v:groupContext="shape" transform="translate(-62.8892,22.9027) rotate(-30)">
|
||||
<title>工作表.77</title>
|
||||
<ellipse cx="1.27835" cy="226.724" rx="1.27835" ry="1.67692" class="st11"/>
|
||||
</g>
|
||||
<g id="shape78-41" v:mID="78" v:groupContext="shape" transform="translate(40.4972,-66.0307) rotate(10)">
|
||||
<title>工作表.78</title>
|
||||
<ellipse cx="1.43688" cy="225.969" rx="1.43688" ry="2.43186" class="st12"/>
|
||||
</g>
|
||||
<g id="shape79-45" v:mID="79" v:groupContext="shape" transform="translate(27.648,-10.263)">
|
||||
<title>工作表.79</title>
|
||||
<path d="M1.2 209.24 L30.59 227.26 L30.59 228.4 L30.59 169.67 L0 151.2 L1.2 152 L1.2 209.24 Z" class="st13"/>
|
||||
</g>
|
||||
<g id="shape80-53" v:mID="80" v:groupContext="shape" transform="translate(36.0639,-63.1276)">
|
||||
<title>工作表.80</title>
|
||||
<path d="M0 228.4 L3.08 227.75 L0.23 225.57 L0 228.4 Z" class="st14"/>
|
||||
</g>
|
||||
<g id="shape81-55" v:mID="81" v:groupContext="shape" transform="translate(2.72921,-62.8833)">
|
||||
<title>工作表.81</title>
|
||||
<path d="M0 222.22 L33.1 228.4 L33.6 225.08 L0.74 217.43 A3.67126 3.97821 0 0 1 0 222.22 Z" class="st15"/>
|
||||
</g>
|
||||
<g id="shape82-59" v:mID="82" v:groupContext="shape" transform="translate(0,1.38061E-05)">
|
||||
<title>工作表.82</title>
|
||||
<path d="M23.95 205.59 A108.227 98.8731 -90.39 0 0 60.96 228.4 A8.47998 7.74722 -90.69 0 0 67.93 223.63 L68.03 153.37
|
||||
A137.519 125.635 -90.57 0 0 30.7 130.79 A6.89496 6.29907 -89.53 0 0 23.95 134.89 L23.96 157.82 L3.63
|
||||
153.09 A3.13064 3.42684 -180 0 0 0.02 156.81 A3.85578 4.22058 -180 0 0 2.7 160.59 L23.95 164.28 L23.95
|
||||
205.59" class="st5"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="shape83-62" v:mID="83" v:groupContext="shape" transform="translate(0.25,-3.31851)">
|
||||
<title>工作表.83</title>
|
||||
<desc>掌静脉模组</desc>
|
||||
<v:textBlock v:margins="rect(2,2,2,2)" v:tabSpace="42.5197"/>
|
||||
<v:textRect cx="50" cy="222.401" width="100.01" height="12"/>
|
||||
<rect x="0" y="216.401" width="100" height="12" class="st16"/>
|
||||
<text x="15" y="227.06" class="st17" v:langID="2052"><v:paragraph v:horizAlign="1"/><v:tabList/>掌静脉模组</text> </g>
|
||||
<g id="shape84-65" v:mID="84" v:groupContext="shape" v:layerMember="0" transform="translate(43.1634,-55.0034)">
|
||||
<title>动态连接线</title>
|
||||
<path d="M7.09 218.2 L7.09 217.84 L7.09 174.68" class="st18"/>
|
||||
</g>
|
||||
<g id="shape85-74" v:mID="85" v:groupContext="shape" transform="translate(0.25,-213.082)">
|
||||
<title>工作表.85</title>
|
||||
<desc>门禁面板机</desc>
|
||||
<v:textBlock v:margins="rect(2,2,2,2)" v:tabSpace="42.5197"/>
|
||||
<v:textRect cx="50" cy="222.401" width="100.01" height="12"/>
|
||||
<rect x="0" y="216.401" width="100" height="12" class="st16"/>
|
||||
<text x="15" y="227.06" class="st17" v:langID="2052"><v:paragraph v:horizAlign="1"/><v:tabList/>门禁面板机</text> </g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 18 KiB |
1558
resources/muti_use.svg
Normal file
1558
resources/muti_use.svg
Normal file
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 98 KiB |
5
resources/run.sh.in
Normal file
5
resources/run.sh.in
Normal file
@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
export LD_LIBRARY_PATH="$(dirname "\$0")/lib:$LD_LIBRARY_PATH"
|
||||
|
||||
"$(dirname "\$0")/bin/@EXECUTABLE_NAME@" "$@"
|
Loading…
x
Reference in New Issue
Block a user