Compare commits

...

91 Commits

Author SHA1 Message Date
amass
a2764a9ade check upload. 2024-11-21 21:39:49 +08:00
943fb6fb4b adapt for linux.
All checks were successful
Build Applications / Build (push) Successful in 3m3s
Windows CI / build (push) Successful in 3m4s
2024-11-21 13:12:45 +00:00
luocai
ce673bf330 add dshow api.
Some checks failed
Build Applications / Build (push) Failing after 1m17s
Windows CI / build (push) Successful in 3m4s
2024-11-21 19:34:13 +08:00
luocai
28f2f60532 use 7z for deploy. 2024-11-21 10:54:22 +08:00
luocai
c846ae81a0 show version for updater.
All checks were successful
Build Applications / Build (push) Successful in 6m12s
Windows CI / build (push) Successful in 7m58s
2024-11-21 09:31:41 +08:00
luocai
fd72d00925 添加简要的说明。
Some checks failed
Windows CI / build (push) Failing after 11s
Build Applications / Build (push) Successful in 3m6s
2024-11-19 14:34:01 +08:00
luocai
8055f4e70f VerifyRequest新增字段,完善协议。
All checks were successful
Build Applications / Build (push) Successful in 6m4s
Windows CI / build (push) Successful in 8m7s
2024-11-18 18:39:50 +08:00
5ae5c5adc6 构建乱码的问题。
All checks were successful
Build Applications / PullDocker (push) Successful in 4s
Build Applications / Build (push) Successful in 6m22s
Windows CI / build (push) Successful in 8m0s
2024-11-14 17:57:17 +08:00
5b602e607c 构建乱码的问题。
All checks were successful
Build Applications / PullDocker (push) Successful in 4s
Build Applications / Build (push) Successful in 2m31s
Windows CI / build (push) Successful in 5m10s
2024-11-14 17:25:53 +08:00
c3430caf2f 构建乱码的问题。
Some checks failed
Build Applications / PullDocker (push) Successful in 4s
Build Applications / Build (push) Successful in 5m16s
Windows CI / build (push) Failing after 36s
2024-11-14 16:35:34 +08:00
0aa2721f67 构建乱码的问题。
Some checks failed
Build Applications / PullDocker (push) Successful in 5s
Build Applications / Build (push) Has been cancelled
Windows CI / build (push) Has been cancelled
2024-11-14 16:30:56 +08:00
luocai
6ee75bfae1 修正构建文件编码。
Some checks failed
Build Applications / Build (push) Successful in 2m30s
Build Applications / PullDocker (push) Successful in 8m41s
Windows CI / build (push) Failing after 31s
2024-11-14 15:12:50 +08:00
luocai
404889a19c 修正构建文件编码。
Some checks failed
Windows CI / build (push) Failing after 32s
Build Applications / Build (push) Successful in 2m49s
Build Applications / PullDocker (push) Has been cancelled
2024-11-14 15:08:24 +08:00
luocai
881048a286 reset module before ota.
All checks were successful
Build Applications / PullDocker (push) Successful in 3s
Build Applications / Build (push) Successful in 5m14s
Windows CI / build (push) Successful in 8m3s
2024-11-08 18:26:33 +08:00
luocai
a6541c6a52 make gitea actions more reasonable.
All checks were successful
Build Applications / PullDocker (push) Successful in 3s
Build Applications / Build (push) Successful in 2m16s
Windows CI / build (push) Successful in 4m41s
Deploy Release / Build (push) Successful in 2m19s
Release tag / build (push) Successful in 4m40s
2024-11-06 10:01:18 +08:00
luocai
59d0a475f4 make gitea actions more reasonable. 2024-11-06 09:58:45 +08:00
luocai
6f1aad0bac make gitea actions more reasonable. 2024-11-06 09:53:05 +08:00
1c92079320 fix ci upload file.
Some checks failed
Build Applications / PullDocker (push) Successful in 3s
Build Applications / Build (push) Successful in 2m14s
Deploy Release / Build (push) Successful in 2m16s
Windows CI / build (push) Successful in 4m41s
Release tag / build (push) Failing after 11s
2024-11-06 00:36:33 +08:00
2a69cdc84b fix tags.
Some checks failed
Build Applications / PullDocker (push) Successful in 4s
Build Applications / Build (push) Successful in 2m16s
Deploy Release / Build (push) Has been cancelled
Windows CI / build (push) Has been cancelled
Release tag / build (push) Successful in 3m53s
2024-11-06 00:02:58 +08:00
14dd4c8ee6 fix checkou.
Some checks failed
Build Applications / PullDocker (push) Successful in 4s
Windows CI / build (push) Successful in 3m37s
Build Applications / Build (push) Successful in 2m18s
Deploy Release / Build (push) Successful in 2m21s
Release tag / build (push) Failing after 13m50s
2024-11-05 23:43:18 +08:00
e9ec8ece00 update linux.
Some checks failed
Build Applications / PullDocker (push) Successful in 4s
Windows CI / build (push) Failing after 4s
Build Applications / Build (push) Has been cancelled
Deploy Release / Build (push) Successful in 2m17s
Release tag / build (push) Failing after 5s
2024-11-05 21:07:15 +08:00
4f9919a931 update linux.
All checks were successful
Build Applications / PullDocker (push) Successful in 6s
Build Applications / Build (push) Successful in 1m49s
Windows CI / build (push) Successful in 3m36s
2024-11-05 20:54:28 +08:00
0e1e76a3a5 complete deploy.
Some checks failed
Build Applications / PullDocker (push) Successful in 4s
Build Applications / Build (push) Failing after 29s
Windows CI / build (push) Successful in 3m44s
2024-11-05 19:22:20 +08:00
8fa26363b7 add simple log view.
Some checks failed
Build Applications / PullDocker (push) Successful in 3s
Build Applications / Build (push) Failing after 26s
Windows CI / build (push) Failing after 52s
2024-11-05 00:23:54 +08:00
36c0a0226a add code.
Some checks failed
Windows CI / build (push) Waiting to run
Build Applications / PullDocker (push) Successful in 3s
Build Applications / Build (push) Failing after 32s
2024-11-04 22:50:44 +08:00
luocai
e518f0841e update action url.
All checks were successful
Build Applications / PullDocker (push) Successful in 5s
Build Applications / Build (push) Successful in 2m30s
Windows CI / build (push) Successful in 3m47s
2024-10-25 17:51:47 +08:00
luocai
2661f0729a optimize communicate stop.
All checks were successful
Build Applications / PullDocker (push) Successful in 7s
Build Applications / Build (push) Successful in 2m26s
Windows CI / build (push) Successful in 3m41s
2024-10-25 17:18:09 +08:00
9c8194dec2 fix ci.
Some checks failed
Build Applications / PullDocker (push) Successful in 4s
Build Applications / Build (push) Has been cancelled
Deploy Release / Build (push) Successful in 3m14s
Release tag / build (push) Failing after 18s
Windows CI / build (push) Failing after 2s
2024-10-16 01:38:35 +08:00
f5f6569051 fix ci.
All checks were successful
Windows CI / build (push) Successful in 3m35s
2024-10-16 01:32:28 +08:00
f968a0a242 fix ci.
Some checks are pending
Windows CI / build (push) Waiting to run
2024-10-16 01:28:29 +08:00
e55983b41f fix ci.
Some checks failed
Windows CI / build (push) Has been cancelled
2024-10-16 01:27:18 +08:00
ba63b6b491 fix ci.
Some checks failed
Windows CI / build (push) Failing after 2s
2024-10-16 01:26:12 +08:00
4788f1c04d fix ci.
Some checks failed
Windows CI / build (push) Failing after 2s
2024-10-16 01:24:36 +08:00
e3d537951c fix ci.
Some checks failed
Build Applications / PullDocker (push) Successful in 4s
Windows CI / build (push) Failing after 1s
Build Applications / Build (push) Successful in 2m26s
2024-10-16 01:19:25 +08:00
f0cbe30768 fix ci.
Some checks failed
Windows CI / build (push) Failing after 2s
Build Applications / PullDocker (push) Successful in 4s
Build Applications / Build (push) Successful in 2m25s
2024-10-16 01:10:13 +08:00
800369d7a3 fix ci.
Some checks failed
Windows CI / build (push) Failing after 3s
Build Applications / PullDocker (push) Successful in 3s
Build Applications / Build (push) Successful in 2m27s
2024-10-16 01:03:56 +08:00
10aea27972 fix ci.
Some checks failed
Build Applications / PullDocker (push) Successful in 4s
Windows CI / build (push) Failing after 4s
Build Applications / Build (push) Successful in 2m27s
2024-10-16 00:46:15 +08:00
a57dc9a11b fix ci.
Some checks failed
Build Applications / PullDocker (push) Successful in 4s
Windows CI / build (push) Failing after 4s
Build Applications / Build (push) Failing after 9s
2024-10-16 00:37:45 +08:00
fd97a1f727 fix ci.
Some checks failed
Build Applications / PullDocker (push) Successful in 4s
Windows CI / build (push) Failing after 4s
Build Applications / Build (push) Failing after 7s
2024-10-16 00:28:44 +08:00
9fb565d310 fix ci.
Some checks failed
Build Applications / PullDocker (push) Successful in 4s
Build Applications / Build (push) Failing after 48s
Windows CI / build (push) Failing after 4s
2024-10-16 00:16:50 +08:00
dd25ec9aa4 fix ci.
Some checks failed
Build Applications / PullDocker (push) Failing after 5s
Build Applications / Build (push) Failing after 3s
Windows CI / build (push) Failing after 1s
2024-10-16 00:10:50 +08:00
luocai
34294c5c85 支持获取掌静脉以注册id.
Some checks failed
Windows CI / build (push) Failing after 10s
Build Applications / PullDocker (push) Failing after 16s
Build Applications / Build (push) Failing after 3s
2024-10-12 17:06:50 +08:00
9ec600a0c3 add docker build step. 2024-10-09 22:31:27 +08:00
57a438b4d4 fix windows ci release.
All checks were successful
Build Applications / PullDocker (push) Successful in 9s
Build Applications / Build (push) Successful in 3m0s
Deploy Release / Build (push) Successful in 3m45s
Release tag / build (push) Successful in 4m18s
Windows CI / build (push) Successful in 3m17s
2024-10-04 07:49:05 +08:00
b8830003e8 add ubuntu build ci.
Some checks failed
Windows CI / build (push) Failing after 3m4s
Build Applications / PullDocker (push) Successful in 12s
Build Applications / Build (push) Successful in 2m29s
2024-10-04 00:50:22 +08:00
059a3f6de6 add linux release ci.
Some checks failed
Windows CI / build (push) Successful in 3m51s
Deploy Release / Build (push) Successful in 3m27s
Release tag / build (push) Failing after 3m4s
2024-10-03 16:32:55 +00:00
8abee988e7 add linux deploy.
Some checks failed
Windows CI / build (push) Failing after 3m5s
2024-10-03 20:47:45 +08:00
e184bf32e7 make workflow correct.
Some checks failed
Windows CI / build (push) Failing after 12s
2024-10-02 17:01:21 +08:00
8f50360ea9 添加提交文字.
All checks were successful
Windows CI / build (push) Successful in 5m29s
2024-10-02 16:48:17 +08:00
a6fbdc24e9 add ci.
Some checks failed
Windows CI / build (push) Failing after 20s
2024-10-02 16:38:36 +08:00
edfc2a4e82 adapt path.
Some checks failed
Windows CI / build (push) Failing after 2m41s
2024-10-02 15:41:57 +08:00
20fe4de46c add ci.
Some checks failed
Windows CI / build (push) Failing after 47s
2024-10-02 14:47:10 +08:00
e1e36b3bad add ci.
Some checks failed
Windows CI / build (push) Failing after 45s
2024-10-02 14:44:51 +08:00
149ae2a212 update for linux. 2024-10-02 03:13:15 +08:00
7c3dbcedfe add linux. 2024-10-02 03:01:37 +08:00
4d2affb862 update for linux. 2024-10-01 23:33:22 +08:00
2d78ddbd04 update for linux. 2024-10-01 23:01:52 +08:00
1ce4afdc25 add linux. 2024-10-01 00:50:05 +08:00
luocai
49499357b5 整理ui布局。 2024-09-25 18:44:04 +08:00
luocai
1731ea445f 同步代码/ 2024-09-25 17:09:00 +08:00
luocai
699a9150c0 add hint code. 2024-09-23 19:19:10 +08:00
luocai
9ed27e2c37 实现互斥。 2024-09-11 15:45:19 +08:00
f74081a06a adapt linux. 2024-09-05 22:33:07 +08:00
luocai
2c06e4d1e0 适配模组的strict模式。 2024-09-05 16:36:54 +08:00
luocai
36a5877a0c update code. 2024-09-04 19:23:34 +08:00
239cb2d10f add linux. 2024-09-03 23:30:45 +08:00
luocai
c7c24e9388 correct code. 2024-08-31 11:37:20 +08:00
luocai
5e24a831ae 更新版本。 2024-08-29 11:12:14 +08:00
luocai
1277ef7495 实现图片下发识别。 2024-08-16 19:05:50 +08:00
luocai
bdc7711bbb 实现获取版本号显示。 2024-08-16 11:34:21 +08:00
luocai
a2a79ac084 添加ota cdc串口打开失败后继续尝试。 2024-08-14 19:51:37 +08:00
luocai
219c68ea45 注册分辨率加入分辨率读取。 2024-08-12 10:20:37 +08:00
luocai
928326efb2 添加升级串口打开失败后继续尝试的逻辑。 2024-08-09 18:10:24 +08:00
luocai
9d495d487d 完善升级功能。 2024-08-08 17:05:32 +08:00
luocai
2610c40189 完善调试功能。 2024-08-07 11:45:13 +08:00
luocai
c6554704ac 实现通过cdc进行升级。 2024-08-05 17:42:27 +08:00
luocai
5009528f3a 实现识别图片上传。 2024-07-31 16:08:38 +08:00
luocai
5c2440c32c 适配新协议。 2024-07-23 20:03:19 +08:00
luocai
59281e3a75 1.实现掌静脉注册时保存图片的实现。 2024-07-15 17:47:19 +08:00
luocai
e588d4abdf 1.添加识别耗时统计显示。 2024-07-01 15:07:13 +08:00
luocai
ec34bd50f6 1.添加一些ota升级调试信息。 2024-06-21 18:56:48 +08:00
luocai
a92e6948c3 1.设置软件名。 2024-06-21 18:08:23 +08:00
luocai
809bca3e91 add status query command. 2024-06-21 15:11:18 +08:00
luocai
bd97f3a380 1.实现配置文件应用。 2024-06-21 11:33:11 +08:00
luocai
d9a9815a89 1.实现图片上报及下发注册的对接实现。 2024-06-21 11:02:28 +08:00
luocai
6e11d190c0 实现图片上传下载。 2024-06-20 21:28:30 +08:00
luocai
174e22ea79 add request image. 2024-06-19 16:02:58 +08:00
luocai
4724af4086 1.添加uvc视频流, 2024-06-13 15:41:40 +08:00
luocai
4febb5fb7f 1.加入note id处理。 2024-06-05 12:13:10 +08:00
luocai
48ebf2be92 1.添加构建环境搭建。 2024-05-31 00:45:46 +08:00
09cb96f97b 1.添加特征值model。 2024-05-24 10:23:05 +08:00
61 changed files with 5816 additions and 593 deletions

View 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

View 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

View 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

View 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
View 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
View 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__

View File

@ -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})

View File

@ -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);
}
}

View File

@ -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__

View 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
View 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
View 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__

View File

@ -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

View File

@ -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; // 是否管理员YES1 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

View 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;
}

View 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__

View 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);
}

View 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
View 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
View 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__

View File

@ -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();
}

View File

@ -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

View File

@ -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();
}

View 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()
}
}

View 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))
}
}
}
}
}
}

View 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
View 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
View 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/"
}
}
}

View 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
View 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}%`
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View 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

View 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

View File

@ -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 .)

View File

@ -1,5 +1,6 @@
add_library(Database
Database.h Database.cpp
DataStructure.h DataStructure.cpp
# shell.c
sqlite3.h sqlite3.c
sqlite3ext.h

View 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
View 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__

View File

@ -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) {

View File

@ -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();

View File

@ -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

View File

@ -0,0 +1,3 @@
#define APPLICATION_NAME "@APPLICATION_NAME@"
#define GIT_COMMIT_ID "@GIT_COMMIT_ID@"
#define APP_VERSION "@PROJECT_VERSION@"

View File

@ -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() {

View File

@ -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();

View File

@ -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
)

View File

@ -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};

View File

@ -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();

View File

@ -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
}

View File

@ -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__

View File

@ -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
View 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
)

View 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
View File

@ -0,0 +1,2 @@
#define BOOST_TEST_MODULE KylinLibraryTest
#include <boost/test/included/unit_test.hpp>

BIN
resources/7za.exe Normal file

Binary file not shown.

View File

@ -0,0 +1,26 @@
掌静脉模组支持掌静脉注册,识别功能,同时也提供通过图片注册掌静脉功能。
常见有两种应用场景:单模组使用,多设备集群分发注册识别。
## 单模组使用
![](./local_use.svg)
在此应用场景下,【门禁面板机】需要完成和【掌静脉模组】通信协议的对接。
当注册用户时门禁面板机向掌静脉模组发送注册命令使模块进入【注册模式】并提示【用户】将【手掌】置于掌静脉模组上方进行注册注册成功后掌静脉模组将会上报一个【唯一标识ID】至门禁面板机面板机需要将该ID与用户进行绑定。
注册成功后门禁面板机可发送指令使模组切换至【识别模式】当用户将手置于掌静脉模组上方模组会和之前注册的掌静脉底库进行比对比对结束之后将会向门禁面板机上报ID以标识此次识别结果此时面板机可依靠此ID和之前记录的注册ID进行比对如果ID相同则表示匹配上该用户。
## 多模组集群使用
![](muti_use.svg)
在此应用场景下,【门禁面板机】需要完成和【掌静脉模组】通行协议的对接,且需要服务器进行掌静脉注册图片管理及分发。
考虑到常见的门禁通行场景,掌静脉模组支持注册时获取掌静脉注册图片,然后再使用该图片进行掌静脉注册。
通过这种方式,用户在注册时,可以通掌静脉注册机获取掌静脉注册图片,然后在将掌静脉注册图片发送至服务器,服务器接收到图片之后,再将图片下发至各个门禁面板机以图片方式进行注册。这样就可以达到一端注册,多端使用的效果。

View File

@ -0,0 +1,403 @@
## 1 串口配置
目前模组支持两种通信接口UARTUSBCDC。其中
UART
- 波特率115200
- 数据位8
- 停止位1
- 奇偶检验:无
- 流控制:无
USBCDC
- 波特率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 返回结果:
- 0x00MR_SUCCESS复位成功
- 0x05MR_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
- 0x00MR_SUCCESS删除成功
- 0x05MR_FAILED4_UNKNOWNREASON未知错误
- 0x08MR_FAILED4_UNKNOWNUSER删除的用户ID不存在
### 3.6 删除所有掌静脉MID_DELALL
删除所有已录入的用户。
该指令无需携带参数。
指令执行结束后,通过消息 MID_REPLY 返回结果(主控等待应答超时时间建议设置为 5s
- 0x00MR_SUCCESS删除成功
- 0x05MR_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为1username为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/14V0.1版本,描述模组识别、录入、删除几个基本协议指令。
2024/11/18V0.2版本,`MID_VERIFY` 消息新增字段预留空间,用于功能扩展。

View File

@ -1,11 +1,23 @@
## 1 串口配置
目前模组支持两种通信接口UARTUSBCDC。其中
UART
- 波特率115200
- 数据位8
- 停止位1
- 奇偶检验:无
- 流控制:无
USBCDC
- 波特率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 {
- 0x00MR_SUCCESS删除成功
- 0x05MR_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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 98 KiB

5
resources/run.sh.in Normal file
View File

@ -0,0 +1,5 @@
#!/bin/bash
export LD_LIBRARY_PATH="$(dirname "\$0")/lib:$LD_LIBRARY_PATH"
"$(dirname "\$0")/bin/@EXECUTABLE_NAME@" "$@"