14 Commits

Author SHA1 Message Date
Self Not Found 695f963991 More general build steps for Linux
Tested on Ubuntu 18.04.6 and 23.10.1
2023-11-03 23:32:55 +08:00
Self Not Found 38c7916715 Merge pull request #54 from setime/workaround-ubuntu-22_04
Workaround to get the GUI working on Ubuntu 22.04
2023-10-29 02:27:54 +08:00
setime 687c27672f Workaround to get the GUI working on Ubuntu 22.04 2023-10-28 19:12:59 +02:00
wh201906 e2fb18970e V0.2.8 2023-06-27 21:54:21 +08:00
wh201906 184a9ed5f2 Fix saving/loading trace(sniff) files
Fix file extension for RRG repo
Fix overwriting existing file
Add support for RRG v4.16717(path in prefs)
2023-06-27 21:09:08 +08:00
wh201906 0738aff2bf Check 125k/134k radiobutton when getting lf config 2023-06-27 17:46:25 +08:00
wh201906 d466e5536b Show more information when running card info 2023-06-27 17:32:41 +08:00
wh201906 b9da3e9209 Add support for RRG v4.16717
Change the order and the default option of config files
2023-06-27 17:16:50 +08:00
wh201906 ee7aeda91e Fix a bug in finding client
In commit e6be456cfa, I uses QFileInfo::exists() to check if the client
exists. However, it doesn't work for client with extensions in
environment variable %PATHEXT% when specifying the path without
extension
2023-06-06 20:04:52 +08:00
wh201906 99bb58adf9 Fix the user input for "Port"
Port can be something not listed in the combobox, like
tcp:192.168.xxx.xxx:xxxxx(25 characters)
bt:aa:bb:cc:dd:ee:ff(20 characters)
2023-06-04 19:10:03 +08:00
wh201906 785fde6e2d Add tutorial for ProxSpace 2023-06-04 18:41:30 +08:00
wh201906 a9b03f081a Remember different client paths(max num:32)
Helpful for me
2023-06-04 02:25:46 +08:00
wh201906 0634e530b6 Use QString::contains() 2023-06-04 00:57:09 +08:00
wh201906 e6be456cfa Add <client dir> for preload script path 2023-06-04 00:53:15 +08:00
24 changed files with 1378 additions and 749 deletions
+6
View File
@@ -2,6 +2,12 @@
[中文](doc/CHANGELOG/CHANGELOG_zh_CN.md) [中文](doc/CHANGELOG/CHANGELOG_zh_CN.md)
### V0.2.8
+ Add support for Iceman/RRG repo v4.16717
+ Fix some bugs
+ Make it easier for testing this GUI across multiple clients
+ Add support for Bluetooth and TCP connection
### V0.2.7 ### V0.2.7
+ Fix writing to Block 0 failure when using with RRG repo v4.15864 + Fix writing to Block 0 failure when using with RRG repo v4.15864
+ Disable disconnection detection on Linux/macOS by default + Disable disconnection detection on Linux/macOS by default
+10 -5
View File
@@ -53,21 +53,23 @@ You can also download them in SourceForge
[![Download Proxmark3GUI](https://a.fsdn.com/con/app/sf-download-button)](https://sourceforge.net/projects/proxmark3gui/files/latest/download) [![Download Proxmark3GUI](https://a.fsdn.com/con/app/sf-download-button)](https://sourceforge.net/projects/proxmark3gui/files/latest/download)
## Build on Linux ## Build on Linux
``` ```bash
cd ~ cd ~
# sudo add-apt-repository universe
sudo apt-get update sudo apt-get update
sudo apt-get install git build-essential # sudo apt-get install git build-essential
sudo apt-get install qt5-default libqt5serialport5-dev sudo apt-get install qtbase5-dev qt5-qmake libqt5serialport5-dev
git clone https://github.com/wh201906/Proxmark3GUI.git --depth=1 git clone https://github.com/wh201906/Proxmark3GUI.git --depth=1
cd Proxmark3GUI cd Proxmark3GUI
mkdir build && cd build mkdir build && cd build
export QT_SELECT=qt5
qmake ../src qmake ../src
make -j4 && make clean make -j4 && make clean
./Proxmark3GUI ./Proxmark3GUI
``` ```
## Build on macOS ## Build on macOS
``` ```zsh
cd ~ cd ~
brew update brew update
brew install qt@5 brew install qt@5
@@ -85,10 +87,13 @@ open Proxmark3GUI.app
![macOS_settings](doc/README/macOS_settings.png) ![macOS_settings](doc/README/macOS_settings.png)
*** ***
## Tutorial ## Tutorial
[1.Quickstart](doc/tutorial/Quickstart/quickstart.md) [1.Quickstart](doc/tutorial/Quickstart/quickstart.md)
[2.Edit Mifare Classic data](doc/tutorial/Edit_Mifare_Classic_data/Edit_Mifare_Classic_data.md)(Proxmark3 hardware is not necessary) [2.Work with ProxSpace](doc/tutorial/Work_With_ProxSpace/work_with_proxspace.md)
[3.Edit Mifare Classic data](doc/tutorial/Edit_Mifare_Classic_data/Edit_Mifare_Classic_data.md)(Proxmark3 hardware is not necessary)
*** ***
## Change Log ## Change Log
+1
View File
@@ -3,5 +3,6 @@
<file>config_official.json</file> <file>config_official.json</file>
<file>config_rrgv4.13441.json</file> <file>config_rrgv4.13441.json</file>
<file>config_rrgv4.15864.json</file> <file>config_rrgv4.15864.json</file>
<file>config_rrgv4.16717.json</file>
</qresource> </qresource>
</RCC> </RCC>
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"//": "Based on Proxmark3 official repo v3.1.0, commit 6116334", "//": "Based on Proxmark3 official repo v3.1.0, commit 6116334485",
"//": "You can change this file if the command format of client changes", "//": "You can change this file if the command format of client changes",
"mifare classic": { "mifare classic": {
"nested": { "nested": {
+3 -2
View File
@@ -1,5 +1,5 @@
{ {
"//": "Based on Proxmark3 rrg repo v4.13441, commit 35ddebc", "//": "Based on Proxmark3 rrg repo v4.13441, commit 35ddebc03c",
"//": "You can change this file if the command format of client changes", "//": "You can change this file if the command format of client changes",
"mifare classic": { "mifare classic": {
"nested": { "nested": {
@@ -48,7 +48,8 @@
"key B index": 4 "key B index": 4
}, },
"info": { "info": {
"cmd": "hf 14a info" "cmd": "hf 14a info -nsv",
"basic cmd": "hf 14a info"
}, },
"sniff": { "sniff": {
"cmd": "hf sniff" "cmd": "hf sniff"
+3 -2
View File
@@ -1,5 +1,5 @@
{ {
"//": "Based on Proxmark3 rrg repo v4.15864, commit 1f75adc", "//": "Based on Proxmark3 rrg repo v4.15864, commit 1f75adcf6d",
"//": "You can change this file if the command format of client changes", "//": "You can change this file if the command format of client changes",
"mifare classic": { "mifare classic": {
"nested": { "nested": {
@@ -48,7 +48,8 @@
"key B index": 4 "key B index": 4
}, },
"info": { "info": {
"cmd": "hf 14a info" "cmd": "hf 14a info -nsv",
"basic cmd": "hf 14a info"
}, },
"sniff": { "sniff": {
"cmd": "hf sniff" "cmd": "hf sniff"
+244
View File
@@ -0,0 +1,244 @@
{
"//": "Based on Proxmark3 rrg repo v4.16717, commit adfebd6510",
"//": "You can change this file if the command format of client changes",
"mifare classic": {
"nested": {
"cmd": "hf mf nested --<card type> --blk <block> -<key type> -k <key>",
"static cmd": "hf mf staticnested --<card type> --blk <block> -<key type> -k <key>",
"card type": {
"mini": "mini",
"1k": "1k",
"2k": "2k",
"4k": "4k"
},
"key type": {
"A": "a",
"B": "b"
},
"//": "[+] -----+-----+--------------+---+--------------+----",
"//": "[+] Sec | Blk | key A |res| key B |res",
"//": "[+] -----+-----+--------------+---+--------------+----",
"//": "[+] 000 | 003 | FFFFFFFFFFFF | 1 | FFFFFFFFFFFF | 1",
"//": "......",
"//": "[+] -----+-----+--------------+---+--------------+----",
"//": "[+] ( 0:Failed / 1:Success )",
"key pattern": "\\s*\\d{3}\\s*\\|\\s*\\d{3}\\s*\\|\\s*.+?\\s*\\|\\s*.+?\\s*\\|\\s*.+?\\s*\\|\\s*.+?\\s*$",
"key A index": 2,
"key B index": 4
},
"check": {
"cmd": "hf mf chk --<card type>",
"card type": {
"mini": "mini",
"1k": "1k",
"2k": "2k",
"4k": "4k"
},
"//": "[+] -----+-----+--------------+---+--------------+----",
"//": "[+] Sec | Blk | key A |res| key B |res",
"//": "[+] -----+-----+--------------+---+--------------+----",
"//": "[+] 000 | 003 | FFFFFFFFFFFF | 1 | FFFFFFFFFFFF | 1",
"//": "......",
"//": "[+] 004 | 019 | ------------ | 0 | ------------ | 0",
"//": "......",
"//": "[+] -----+-----+--------------+---+--------------+----",
"//": "[+] ( 0:Failed / 1:Success )",
"key pattern": "\\s*\\d{3}\\s*\\|\\s*\\d{3}\\s*\\|\\s*.+?\\s*\\|\\s*.+?\\s*\\|\\s*.+?\\s*\\|\\s*.+?\\s*$",
"key A index": 2,
"key B index": 4
},
"info": {
"cmd": "hf 14a info -nsv",
"basic cmd": "hf 14a info"
},
"sniff": {
"cmd": "hf sniff"
},
"sniff 14a": {
"cmd": "hf 14a sniff"
},
"list": {
"cmd": "trace list -t mf"
},
"dump": {
"cmd": "hf mf dump --<card type>",
"card type": {
"mini": "mini",
"1k": "1k",
"2k": "2k",
"4k": "4k"
}
},
"restore": {
"cmd": "hf mf restore --<card type> --force",
"card type": {
"mini": "mini",
"1k": "1k",
"2k": "2k",
"4k": "4k"
}
},
"emulator wipe": {
"cmd": "hf mf eclr"
},
"Magic Card wipe": {
"cmd": "hf mf cwipe"
},
"emulator read block": {
"cmd": "hf mf egetblk --blk <block>",
"data pattern": "([0-9a-fA-F]{2} ){15}[0-9a-fA-F]{2}"
},
"Magic Card read block": {
"cmd": "hf mf cgetblk --blk <block>",
"data pattern": "([0-9a-fA-F]{2} ){15}[0-9a-fA-F]{2}"
},
"normal read block": {
"cmd": "hf mf rdbl --blk <block> -<key type> -k <key>",
"key type": {
"A": "a",
"B": "b"
},
"data pattern": "([0-9a-fA-F]{2} ){15}[0-9a-fA-F]{2}"
},
"darkside": {
"cmd": "hf mf darkside"
},
"save sniff": {
"cmd": "trace save -f <filename>",
"path cmd":"prefs show",
"path pattern":"trace save path\\.+\\s*(.+)$"
},
"load sniff": {
"cmd": "trace load -f <filename>",
"show cmd": "trace list --buffer -t mf"
},
"hardnested": {
"cmd": "hf mf hardnested --blk <known key block> -<known key type> -k <known key> --tblk <target key block> --t<target key type>",
"known key type": {
"A": "a",
"B": "b"
},
"target key type": {
"A": "a",
"B": "b"
}
},
"normal read sector": {
"cmd": "hf mf rdsc --sec <sector> -<key type> -k <key>",
"key type": {
"A": "a",
"B": "b"
},
"data pattern": "([0-9a-fA-F]{2} ){15}[0-9a-fA-F]{2}"
},
"Magic Card read sector": {
"cmd": "hf mf cgetsc --sec <sector>",
"data pattern": "([0-9a-fA-F]{2} ){15}[0-9a-fA-F]{2}"
},
"//": "When writing a block, if the result is not empty and doesn't contain the failed flag, the function will return true",
"normal write block": {
"cmd": "hf mf wrbl --blk <block> -<key type> -k <key> -d <data> --force",
"key type": {
"A": "a",
"B": "b"
},
"failed flag": [
"fail",
"error"
]
},
"Magic Card write block": {
"cmd": "hf mf csetblk --blk <block> -d <data>",
"failed flag": [
"fail",
"error"
]
},
"emulator write block": {
"cmd": "hf mf esetblk --blk <block> -d <data>"
},
"Magic Card lock": {
"cmd": "hf 14a raw ",
"sequence": [
"-ak -b 7 40",
"-ak 43",
"-ak E0 00 39 F7",
"-ak E1 00 E1 EE",
"-ak 85 00 00 00 00 00 00 00 00 00 00 00 00 00 00 08 18 47",
"-a 52"
]
},
"Magic Card set parameter": {
"cmd": "hf mf csetuid --uid <uid> --atqa <atqa> --sak <sak>"
}
},
"lf": {
"read": {
"cmd": "lf read -v",
"show cmd": "data plot"
},
"sniff": {
"cmd": "lf sniff -v",
"show cmd": "data plot"
},
"search": {
"cmd": "lf search -u"
},
"tune": {
"cmd": "lf tune --divisor <divisor>"
},
"get config": {
"cmd": "hw status",
"field start": "LF Sampling config",
"field end": "\\[#\\] \\S",
"divisor": {
"flag": "divisor",
"pattern": "\\d+"
},
"bits per sample": {
"flag": "bits per sample",
"pattern": "\\d+"
},
"decimation": {
"flag": "decimation",
"pattern": "\\d+"
},
"averaging": {
"flag": "averaging",
"pattern": "\\d+",
"replace": {
"yes": "1",
"no": "0",
"Yes": "1",
"No": "0"
}
},
"trigger threshold": {
"flag": "trigger threshold",
"pattern": "\\d+"
},
"samples to skip": {
"flag": "samples to skip",
"pattern": "\\d+"
},
"//": "execute 'cmd' then find parameters between 'field stard' and 'field end'",
"//": "for each line, if the line doesn't have any flag, skip",
"//": "otherwise, delete characters before 'flag' and 'flag' itself, then use 'pattern' to get the parameter",
"//": "If 'replace' dict exists, replace all keys with respective values before getting parameters"
},
"set config": {
"cmd": "lf config --divisor <divisor> --bps <bits per sample> --dec <decimation> --avg <averaging> --trig <trigger threshold> --skip <samples to skip>",
"divisor cmd": "hw setlfdivisor -d <divisor>"
}
},
"t55xx": {
"clone em410x": {
"read": "lf em 410x reader",
"successful read flag": "EM 410x ID",
"pattern": "EM 410x ID\\s*\\K[0-9a-fA-F]{10}",
"clone cmd": "lf em 410x clone --id <id> <type>",
"t5555 flag": "--q5",
"t55x7 flag": ""
}
}
}
+6
View File
@@ -2,6 +2,12 @@
[English](../../CHANGELOG.md) [English](../../CHANGELOG.md)
### V0.2.8
+ 支持冰人版客户端 v4.16717
+ 修复若干Bug
+ 便于在不同版本客户端之间切换
+ 支持蓝牙及TCP连接
### V0.2.7 ### V0.2.7
+ 修复使用冰人版v4.15864时无法写入块0的问题 + 修复使用冰人版v4.15864时无法写入块0的问题
+ 默认关闭Linux/macOS系统下对PM3硬件断连的检测 + 默认关闭Linux/macOS系统下对PM3硬件断连的检测
+8 -5
View File
@@ -53,21 +53,23 @@ SourceForge平台上也可下载
[![Download Proxmark3GUI](https://a.fsdn.com/con/app/sf-download-button)](https://sourceforge.net/projects/proxmark3gui/files/latest/download) [![Download Proxmark3GUI](https://a.fsdn.com/con/app/sf-download-button)](https://sourceforge.net/projects/proxmark3gui/files/latest/download)
## 在Linux系统下编译 ## 在Linux系统下编译
``` ```bash
cd ~ cd ~
# sudo add-apt-repository universe
sudo apt-get update sudo apt-get update
sudo apt-get install git build-essential # sudo apt-get install git build-essential
sudo apt-get install qt5-default libqt5serialport5-dev sudo apt-get install qtbase5-dev qt5-qmake libqt5serialport5-dev
git clone https://github.com/wh201906/Proxmark3GUI.git --depth=1 git clone https://github.com/wh201906/Proxmark3GUI.git --depth=1
cd Proxmark3GUI cd Proxmark3GUI
mkdir build && cd build mkdir build && cd build
export QT_SELECT=qt5
qmake ../src qmake ../src
make -j4 && make clean make -j4 && make clean
./Proxmark3GUI ./Proxmark3GUI
``` ```
## 在macOS系统下编译 ## 在macOS系统下编译
``` ```zsh
cd ~ cd ~
brew update brew update
brew install qt@5 brew install qt@5
@@ -87,7 +89,8 @@ open Proxmark3GUI.app
*** ***
## 教程 ## 教程
[1.快速上手](../tutorial/Quickstart/quickstart_zh_CN.md) [1.快速上手](../tutorial/Quickstart/quickstart_zh_CN.md)
[2.编辑Mifare(IC)卡数据](../tutorial/Edit_Mifare_Classic_data/Edit_Mifare_Classic_data_zh_CN.md)(无需PM3硬件) [2.使用ProxSpace编译的客户端](../tutorial/Work_With_ProxSpace/work_with_proxspace_zh_CN.md)
[3.编辑Mifare(IC)卡数据](../tutorial/Edit_Mifare_Classic_data/Edit_Mifare_Classic_data_zh_CN.md)(无需PM3硬件)
*** ***
## 更新日志 ## 更新日志
@@ -0,0 +1,81 @@
# Work with ProxSpace
[中文教程](work_with_proxspace_zh_CN.md)
This GUI supports clients built from ProxSpace, making it easier to use the latest client with this GUI.
## 1. Set up ProxSpace
Please refer to this tutorial to set up ProxSpace and compile Proxmark3 client:
https://github.com/Gator96100/ProxSpace/blob/HEAD/README.md
After that, you will get a folder structure like this:
```
.
├── autobuild.bat
├── msys2
│   ├── autorebase.bat
│   └── ......
├── pm3
│   └── proxmark3
│   ├── client
│   ├── ......
│   ├── pm3
│   ├── README.md
│   └── ......
├── README.md
├── runme64.bat
└── setup
└── ......
```
The proxmark3 repository is located in `./pm3/proxmark3`
## 2. Download GUI
You can download prebuilt binaries there.
[Prebuilt binaries](../../../README.md#download-binaries-for-windows)
Please download a version without the client, like `V0.2.7-win64.7z`.
After downloading, extract it to a path without non-ASCII characters and spaces. Plus, the target folder should not be the same as the proxmark3 repository.
For example, if you extract it to ./pm3/GUI/, you will get a folder structure like this:
```
.
├── autobuild.bat
├── msys2
│   ├── autorebase.bat
│   └── ......
├── pm3
│   ├── GUI
│   │   └── V0.2.7-win64
│   │   ├── plugins
│   │   ├── Proxmark3GUI.exe
│   │   └── ......
│   └── proxmark3
│   ├── client
│   ├── ......
│   ├── pm3
│   ├── README.md
│   └── ......
├── README.md
├── runme64.bat
└── setup
└── ......
```
## 3. Run the GUI in ProxSpace
Double-click `./runme64.bat` to run the ProxSpace. Now the working directory is `./pm3`.
In the terminal of ProxSpace, run `cd GUI` to go to the `./pm3/GUI`, then run `./V0.2.7-win64/Proxmark3GUI.exe` to open the GUI.
***
**Note:** You can run the GUI in your preferred folder, but please avoid running it in the same directory as the GUI itself, otherwise it will cause errors when loading the client.
Using the directory structure above as an example, you can first run `cd /pm3` to set the working directory to `/pm3`, then run `./V0.2.7-win64/Proxmark3GUI.exe` to open the GUI.
However, you should not first run `cd /pm3/GUI/V0.2.7-win64` and then run `./Proxmark3GUI.exe`.
## 4. Connect to the device in GUI
## Specify the Client Path
Using the directory structure above as an example, assuming the current working directory is `./pm3/GUI`, and the client path is `./pm3/proxmark3/client/proxmark3.exe`, you need to enter `../proxmark3/client/proxmark3.exe` in the `Client Path`.
## Clear the Preload Script Path
The GUI is launched in ProxSpace so it includes the necessary environment variables for the client, there is no need to use any preload script.
## Select the Config File
Depending on the client version, you need to select the corresponding config file. For example, select `config_official.json` for the official client, and select `config_rrgv4.15864.json` for the rrg version v4.15864. If your client version is not listed, try selecting the config file with a similar version number.
## Connect to the Device
Select the port number of the PM3 hardware, then click `Connect`.
@@ -0,0 +1,77 @@
# 使用ProxSpace编译的客户端
[English](work_with_proxspace.md)
此GUI可以和ProxSpace编译出来的客户端配合运行,满足一部分用户使用最新版客户端的需求
## 1. 搭建ProxSpace环境
请参考此教程搭建ProxSpace环境并编译proxmark3客户端:
https://github.com/Gator96100/ProxSpace/blob/HEAD/README.md
搭建完成后,你将会获得如下的目录结构:
```
.
├── autobuild.bat
├── msys2
│   ├── autorebase.bat
│   └── ......
├── pm3
│   └── proxmark3
│   ├── client
│   ├── ......
│   ├── pm3
│   ├── README.md
│   └── ......
├── README.md
├── runme64.bat
└── setup
└── ......
```
其中proxmark3仓库位于 `./pm3/proxmark3`
## 2. 下载GUI
你可以在此处下载编译好的客户端
[预编译客户端](../../README/README_zh_CN.md#关于预编译windows客户端)
下载时请选择不含客户端的版本,例如`V0.2.7-win64.7z`
下载后请将其解压到不含中文字符和空格的路径中,同时要确保GUI文件和proxmark3仓库不位于同一文件夹下
以解压到`./pm3/GUI/`为例,你将会获得如下的目录结构
```
.
├── autobuild.bat
├── msys2
│   ├── autorebase.bat
│   └── ......
├── pm3
│   ├── GUI
│   │   └── V0.2.7-win64
│   │   ├── plugins
│   │   ├── Proxmark3GUI.exe
│   │   └── ......
│   └── proxmark3
│   ├── client
│   ├── ......
│   ├── pm3
│   ├── README.md
│   └── ......
├── README.md
├── runme64.bat
└── setup
└── ......
```
## 3. 从ProxSpace运行GUI
双击`./runme64.bat`运行ProxSpace环境,此时工作目录位于`./pm3`
在新弹出的终端中运行`cd GUI`来到`./pm3/GUI/`目录下,然后运行`./V0.2.7-win64/Proxmark3GUI.exe`,此时的GUI将会包含客户端运行所需要的所有环境变量
***
**请注意**,你可以在你喜欢的工作目录下运行GUI,但请不要在GUI文件所在目录下运行,否则加载客户端时会出错
以上面的目录结构为例,你可以先运行`cd /pm3`将工作目录设为`/pm3`,然后运行`./V0.2.7-win64/Proxmark3GUI.exe`来打开GUI
但是你不能先运行`cd /pm3/GUI/V0.2.7-win64`,然后运行`./Proxmark3GUI.exe`,否则之后无法正确加载客户端
## 4. 从GUI连接设备
### 填写客户端路径
以上面的目录结构为例,假设此时的工作目录为`./pm3/GUI`,而客户端路径为`./pm3/proxmark3/client/proxmark3.exe`,则需要在客户端路径里面填写`../proxmark3/client/proxmark3.exe`
### 清空预加载脚本路径
因为GUI是从ProxSpace中启动的,本身已包含客户端所需的环境变量,因此不需要使用任何预加载脚本
### 选择(客户端)配置文件
根据客户端版本的不同,你需要选择对应的配置文件。例如official版本对应`config_official.json`rrg版本号v4.15864对应`config_rrgv4.15864.json`。如果选项中没有你所使用的客户端版本,可以尝试选择版本号相近的对应配置文件
### 连接设备
在“端口”当中选择需要连接的端口号,单击“连接”即可
+412 -405
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+287 -279
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -59,9 +59,9 @@ qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target !isEmpty(target.path): INSTALLS += target
VERSION = 0.2.7 VERSION = 0.2.8
QMAKE_TARGET_PRODUCT = "Proxmark3GUI" QMAKE_TARGET_PRODUCT = "Proxmark3GUI"
QMAKE_TARGET_DESCRIPTION = "Proxmark3GUI" QMAKE_TARGET_DESCRIPTION = "A cross-platform GUI for Proxmark3 client"
QMAKE_TARGET_COMPANY = "wh201906" QMAKE_TARGET_COMPANY = "wh201906"
RESOURCES += \ RESOURCES += \
+23 -10
View File
@@ -35,25 +35,25 @@ void PM3Process::connectPM3(const QString& path, const QStringList args)
waitForReadyRead(10000); waitForReadyRead(10000);
setRequiringOutput(false); setRequiringOutput(false);
result = *requiredOutput; result = *requiredOutput;
if(result.indexOf("[=]") != -1) // Workaround for wayland system, e.g. Ubuntu 22.04
// The issue is that the warning is read and nothing else, hence the process is killed.
if(result.contains("Warning: Ignoring XDG_SESSION_TYPE=wayland on Gnome. Use QT_QPA_PLATFORM=wayland to run on Wayland anyway."))
{
setRequiringOutput(true);
readWaitForConnection(&result);
}
if(result.contains("[=]"))
{ {
clientType = Util::CLIENTTYPE_ICEMAN; clientType = Util::CLIENTTYPE_ICEMAN;
setRequiringOutput(true); setRequiringOutput(true);
write("hw version\n"); write("hw version\n");
for(int i = 0; i < 50; i++) readWaitForConnection(&result);
{
waitForReadyRead(200);
result += *requiredOutput;
if(result.indexOf("os: ") != -1)
break;
}
setRequiringOutput(false);
} }
else else
{ {
clientType = Util::CLIENTTYPE_OFFICIAL; clientType = Util::CLIENTTYPE_OFFICIAL;
} }
if(result.indexOf("os: ") != -1) // make sure the PM3 is connected if(result.contains("os: ")) // make sure the PM3 is connected
{ {
emit changeClientType(clientType); emit changeClientType(clientType);
result = result.mid(result.indexOf("os: ")); result = result.mid(result.indexOf("os: "));
@@ -63,6 +63,7 @@ void PM3Process::connectPM3(const QString& path, const QStringList args)
} }
else else
{ {
qDebug() << "unexpected output:" << (result.isEmpty() ? "(empty)" : result);
emit HWConnectFailed(); emit HWConnectFailed();
kill(); kill();
} }
@@ -71,6 +72,18 @@ void PM3Process::connectPM3(const QString& path, const QStringList args)
setRequiringOutput(false); setRequiringOutput(false);
} }
void PM3Process::readWaitForConnection(QString *result)
{
for(int i = 0; i < 50; i++)
{
waitForReadyRead(200);
(*result) += *requiredOutput;
if(result->contains("os: "))
break;
}
setRequiringOutput(false);
}
void PM3Process::reconnectPM3() void PM3Process::reconnectPM3()
{ {
connectPM3(currPath, currArgs); connectPM3(currPath, currArgs);
+1
View File
@@ -38,6 +38,7 @@ private:
bool isRequiringOutput; bool isRequiringOutput;
QString* requiredOutput; // It only works in this class now QString* requiredOutput; // It only works in this class now
void setRequiringOutput(bool st);// It only works in this class now void setRequiringOutput(bool st);// It only works in this class now
void readWaitForConnection(QString *result);
QTimer* serialListener; QTimer* serialListener;
QSerialPortInfo* portInfo; QSerialPortInfo* portInfo;
QString currPath; QString currPath;
+1
View File
@@ -151,6 +151,7 @@ void LF::syncWithUI()
ui->LF_LFConf_averagingBox->setChecked(currLFConfig.averaging); ui->LF_LFConf_averagingBox->setChecked(currLFConfig.averaging);
ui->LF_LFConf_thresholdBox->setValue(currLFConfig.triggerThreshold); ui->LF_LFConf_thresholdBox->setValue(currLFConfig.triggerThreshold);
ui->LF_LFConf_skipsBox->setValue(currLFConfig.samplesToSkip); ui->LF_LFConf_skipsBox->setValue(currLFConfig.samplesToSkip);
emit LFfreqConfChanged(currLFConfig.divisor, false);
} }
void LF::setConfigMap(const QVariantMap& configMap) void LF::setConfigMap(const QVariantMap& configMap)
+1 -1
View File
@@ -53,7 +53,7 @@ private:
void syncWithUI(); void syncWithUI();
bool getLFConfig_helper(const QVariantMap& map, QString& str, int* result); bool getLFConfig_helper(const QVariantMap& map, QString& str, int* result);
signals: signals:
void LFfreqConfChanged(int divisor, bool isCustomized);
}; };
#endif // LF_H #endif // LF_H
+21 -1
View File
@@ -102,7 +102,12 @@ QMap<QString, QString> Mifare::info(bool isRequiringOutput)
QVariantMap config = configMap["info"].toMap(); QVariantMap config = configMap["info"].toMap();
if(isRequiringOutput) if(isRequiringOutput)
{ {
QString result = util->execCMDWithOutput(config["cmd"].toString(), 500); QString cmd = config["basic cmd"].toString();
// for official client
if(cmd.isEmpty())
cmd = config["cmd"].toString();
QString result = util->execCMDWithOutput(cmd, 500);
QStringList lineList = result.split("\n"); QStringList lineList = result.split("\n");
for(auto line = lineList.begin(); line != lineList.end(); line++) for(auto line = lineList.begin(); line != lineList.end(); line++)
@@ -1370,3 +1375,18 @@ quint16 Mifare::getTrailerBlockId(quint8 sectorId, qint8 cardTypeId)
// other cardTypeId: use current cardtype(include default -1) // other cardTypeId: use current cardtype(include default -1)
return (cardType.blks[sectorId] + cardType.blk[sectorId] - 1); return (cardType.blks[sectorId] + cardType.blk[sectorId] - 1);
} }
QString Mifare::getTraceSavePath()
{
QVariantMap config = configMap["save sniff"].toMap();
QString pathCmd = config["path cmd"].toString();
QString patternText = config["path pattern"].toString();
QRegularExpression pattern = QRegularExpression(patternText, QRegularExpression::MultilineOption);
if(pathCmd.isEmpty() || patternText.isEmpty())
return QString();
QString result = util->execCMDWithOutput(pathCmd, 500);
QRegularExpressionMatch reMatch = pattern.match(result);
if(!reMatch.hasMatch())
return QString();
return reMatch.captured(1).trimmed();
}
+1
View File
@@ -115,6 +115,7 @@ public:
QString data_getUID(); QString data_getUID();
quint16 getTrailerBlockId(quint8 sectorId, qint8 cardTypeId = -1); // -1: use current cardtype quint16 getTrailerBlockId(quint8 sectorId, qint8 cardTypeId = -1); // -1: use current cardtype
void setConfigMap(const QVariantMap& configMap); void setConfigMap(const QVariantMap& configMap);
QString getTraceSavePath();
public slots: public slots:
signals: signals:
+165 -31
View File
@@ -43,6 +43,7 @@ MainWindow::MainWindow(QWidget *parent):
mifare = new Mifare(ui, util, this); mifare = new Mifare(ui, util, this);
lf = new LF(ui, util, this); lf = new LF(ui, util, this);
t55xxTab = new T55xxTab(util); t55xxTab = new T55xxTab(util);
connect(lf, &LF::LFfreqConfChanged, this, &MainWindow::onLFfreqConfChanged);
connect(t55xxTab, &T55xxTab::setParentGUIState, this, &MainWindow::setState); connect(t55xxTab, &T55xxTab::setParentGUIState, this, &MainWindow::setState);
ui->funcTab->insertTab(2, t55xxTab, tr("T55xx")); ui->funcTab->insertTab(2, t55xxTab, tr("T55xx"));
@@ -158,8 +159,45 @@ void MainWindow::on_PM3_connectButton_clicked()
{ {
qDebug() << "Main:" << QThread::currentThread(); qDebug() << "Main:" << QThread::currentThread();
QString port = ui->PM3_portBox->currentData().toString(); const QComboBox* portBox = ui->PM3_portBox;
QString port;
if(portBox->currentText() == portBox->itemText(portBox->currentIndex()))
// in the list
port = portBox->currentData().toString();
else
// not in the list
port = portBox->currentText();
qDebug() << "port:" << port;
QString startArgs = ui->Set_Client_startArgsEdit->text(); QString startArgs = ui->Set_Client_startArgsEdit->text();
QString clientPath = ui->PM3_pathBox->currentText();
QFileInfo clientFile(clientPath);
bool clientExist = false;
QStringList extList = {""};
#ifdef Q_OS_WIN
if(clientFile.suffix().isEmpty())
{
QString pathExt = QProcessEnvironment::systemEnvironment().value("pathext");
extList += pathExt.split(";", Qt::SkipEmptyParts);
if(extList.size() == 1)
extList += ".exe";
}
#endif
for(const QString& ext : extList)
{
QFileInfo executable(clientFile.filePath() + ext);
if(executable.isFile())
{
clientExist = true;
break;
}
}
if(!clientExist)
{
QMessageBox::information(this, tr("Info"), tr("The client path is invalid"), QMessageBox::Ok);
return;
}
// on RRG repo, if no port is specified, the client will search the available port // on RRG repo, if no port is specified, the client will search the available port
if(port == "" && startArgs.contains("<port>")) // has <port>, no port if(port == "" && startArgs.contains("<port>")) // has <port>, no port
@@ -172,25 +210,29 @@ void MainWindow::on_PM3_connectButton_clicked()
port = ""; // a symbol port = ""; // a symbol
QStringList args = startArgs.replace("<port>", port).split(' '); QStringList args = startArgs.replace("<port>", port).split(' ');
saveClientPath(ui->PM3_pathEdit->text()); addClientPath(clientPath);
QProcess envSetProcess; QProcess envSetProcess;
QFileInfo envScriptPath(ui->Set_Client_envScriptEdit->text()); QString envScriptPath = ui->Set_Client_envScriptEdit->text();
if(envScriptPath.exists()) if(envScriptPath.contains("<client dir>"))
envScriptPath.replace("<client dir>", clientFile.absoluteDir().absolutePath());
QFileInfo envScript(envScriptPath);
if(envScript.exists())
{ {
qDebug() << envScriptPath.absoluteFilePath(); qDebug() << envScript.absoluteFilePath();
// use the shell session to keep the environment then read it // use the shell session to keep the environment then read it
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
// cmd /c "<path>">>nul && set // cmd /c "<path>">>nul && set
envSetProcess.start("cmd", {}, QProcess::Unbuffered | QProcess::ReadWrite | QProcess::Text); envSetProcess.start("cmd", {}, QProcess::Unbuffered | QProcess::ReadWrite | QProcess::Text);
envSetProcess.write(QString("\"" + envScriptPath.absoluteFilePath() + "\">>nul\n").toLatin1()); envSetProcess.write(QString("\"" + envScript.absoluteFilePath() + "\">>nul\n").toLatin1());
envSetProcess.waitForReadyRead(10000); envSetProcess.waitForReadyRead(10000);
envSetProcess.readAll(); envSetProcess.readAll();
envSetProcess.write("set\n"); envSetProcess.write("set\n");
#else #else
// need implementation(or test if space works) // need implementation(or test if space works)
// sh -c '. "<path>">>/dev/null && env' // sh -c '. "<path>">>/dev/null && env'
envSetProcess.start("sh -c \' . \"" + envScriptPath.absoluteFilePath() + "\">>/dev/null && env"); envSetProcess.start("sh -c \' . \"" + envScript.absoluteFilePath() + "\">>/dev/null && env");
#endif #endif
envSetProcess.waitForReadyRead(10000); envSetProcess.waitForReadyRead(10000);
QString envSetResult = QString(envSetProcess.readAll()); QString envSetResult = QString(envSetProcess.readAll());
@@ -219,7 +261,7 @@ void MainWindow::on_PM3_connectButton_clicked()
emit setWorkingDir(clientWorkingDir->absolutePath()); emit setWorkingDir(clientWorkingDir->absolutePath());
loadConfig(); loadConfig();
emit connectPM3(ui->PM3_pathEdit->text(), args); emit connectPM3(clientPath, args);
if(port != "" && !keepClientActive) if(port != "" && !keepClientActive)
emit setSerialListener(port, true); emit setSerialListener(port, true);
else if(!keepClientActive) else if(!keepClientActive)
@@ -232,7 +274,7 @@ void MainWindow::onPM3ErrorOccurred(QProcess::ProcessError error)
{ {
qDebug() << "PM3 Error:" << error << pm3->errorString(); qDebug() << "PM3 Error:" << error << pm3->errorString();
if(error == QProcess::FailedToStart) if(error == QProcess::FailedToStart)
QMessageBox::information(this, tr("Info"), tr("Failed to start the client")); QMessageBox::information(this, tr("Info"), tr("Failed to start the client") + "\n" + pm3->errorString());
} }
void MainWindow::onPM3HWConnectFailed() void MainWindow::onPM3HWConnectFailed()
@@ -914,18 +956,31 @@ void MainWindow::on_MF_Sniff_loadButton_clicked() // use a tmp file to support c
{ {
QString title = ""; QString title = "";
QString filename = ""; QString filename = "";
QString defaultExtension;
QDir clientTracePath;
if(Util::getClientType() == Util::CLIENTTYPE_OFFICIAL)
defaultExtension = ".trc";
else if(Util::getClientType() == Util::CLIENTTYPE_ICEMAN)
defaultExtension = ".trace";
QString userTraceSavePath = mifare->getTraceSavePath();
if(userTraceSavePath.isEmpty())
clientTracePath = *clientWorkingDir;
else
clientTracePath = QDir(userTraceSavePath); // For v4.16717 and later
title = tr("Plz select the trace file:"); title = tr("Plz select the trace file:");
filename = QFileDialog::getOpenFileName(this, title, clientWorkingDir->absolutePath(), tr("Trace Files(*.trc)") + ";;" + tr("All Files(*.*)")); filename = QFileDialog::getOpenFileName(this, title, clientTracePath.absolutePath(), tr("Trace Files") + "(*" + defaultExtension + ")" + ";;" + tr("All Files(*.*)"));
qDebug() << filename; qDebug() << filename;
if(filename != "") if(filename != "")
{ {
QString tmpFile = "tmp" + QString::number(QDateTime::currentDateTime().toTime_t()) + ".trc"; QString tmpFile = "tmp" + QString::number(QDateTime::currentDateTimeUtc().toTime_t()) + defaultExtension;
if(QFile::copy(filename, clientWorkingDir->absolutePath() + "/" + tmpFile)) if(QFile::copy(filename, clientTracePath.absolutePath() + "/" + tmpFile))
{ {
mifare->loadSniff(tmpFile); mifare->loadSniff(tmpFile);
util->delay(3000); util->delay(3000);
QFile::remove(clientWorkingDir->absolutePath() + "/" + tmpFile); QFile::remove(clientTracePath.absolutePath() + "/" + tmpFile);
} }
else else
{ {
@@ -938,25 +993,41 @@ void MainWindow::on_MF_Sniff_saveButton_clicked()
{ {
QString title = ""; QString title = "";
QString filename = ""; QString filename = "";
QString defaultExtension;
QDir clientTracePath;
if(Util::getClientType() == Util::CLIENTTYPE_OFFICIAL)
defaultExtension = ".trc";
else if(Util::getClientType() == Util::CLIENTTYPE_ICEMAN)
defaultExtension = ".trace";
QString userTraceSavePath = mifare->getTraceSavePath();
if(userTraceSavePath.isEmpty())
clientTracePath = *clientWorkingDir;
else
clientTracePath = QDir(userTraceSavePath); // For v4.16717 and later
title = tr("Plz select the location to save trace file:"); title = tr("Plz select the location to save trace file:");
filename = QFileDialog::getSaveFileName(this, title, clientWorkingDir->absolutePath(), tr("Trace Files(*.trc)")); filename = QFileDialog::getSaveFileName(this, title, clientTracePath.absolutePath(), tr("Trace Files") + "(*" + defaultExtension + ")");
qDebug() << filename; qDebug() << filename;
if(filename != "") if(filename != "")
{ {
QString tmpFile = "tmp" + QString::number(QDateTime::currentDateTime().toTime_t()) + ".trc"; QString tmpFile = "tmp" + QString::number(QDateTime::currentDateTimeUtc().toTime_t()) + defaultExtension;
mifare->saveSniff(tmpFile); mifare->saveSniff(tmpFile);
for(int i = 0; i < 100; i++) for(int i = 0; i < 100; i++)
{ {
util->delay(100); util->delay(100);
if(QFile::exists(clientWorkingDir->absolutePath() + "/" + tmpFile)) if(QFile::exists(clientTracePath.absolutePath() + "/" + tmpFile))
break; break;
} }
if(!QFile::copy(clientWorkingDir->absolutePath() + "/" + tmpFile, filename)) // filename is not empty -> the user has chosen to overwrite the existing file
if(QFile::exists(filename))
QFile::remove(filename);
if(!QFile::copy(clientTracePath.absolutePath() + "/" + tmpFile, filename))
{ {
QMessageBox::information(this, tr("Info"), tr("Failed to save to") + "\n" + filename); QMessageBox::information(this, tr("Info"), tr("Failed to save to") + "\n" + filename);
} }
QFile::remove(clientWorkingDir->absolutePath() + "/" + tmpFile); QFile::remove(clientTracePath.absolutePath() + "/" + tmpFile);
} }
} }
@@ -1090,9 +1161,7 @@ void MainWindow::uiInit()
} }
settings->endGroup(); settings->endGroup();
settings->beginGroup("Client_Path"); loadClientPathList();
ui->PM3_pathEdit->setText(settings->value("path", "proxmark3").toString());
settings->endGroup();
ui->Set_Client_GUIWorkingDirLabel->setText(QDir::currentPath()); ui->Set_Client_GUIWorkingDirLabel->setText(QDir::currentPath());
@@ -1118,13 +1187,17 @@ void MainWindow::uiInit()
settings->endGroup(); settings->endGroup();
ui->Set_Client_keepClientActiveBox->setChecked(keepClientActive); ui->Set_Client_keepClientActiveBox->setChecked(keepClientActive);
QDirIterator configFiles(":/config/"); QDir configFiles(":/config/");
configFiles.setSorting(QDir::Name);
const QFileInfoList configFileList = configFiles.entryInfoList();
ui->Set_Client_configFileBox->blockSignals(true); ui->Set_Client_configFileBox->blockSignals(true);
while(configFiles.hasNext()) for(const auto& file : configFileList)
{ {
configFiles.next(); ui->Set_Client_configFileBox->addItem(file.fileName(), file.filePath());
ui->Set_Client_configFileBox->addItem(configFiles.fileName(), configFiles.filePath());
} }
// Use the last one as the default one
ui->Set_Client_configFileBox->setCurrentIndex(ui->Set_Client_configFileBox->count() - 1);
ui->Set_Client_configFileBox->addItem(tr("External file"), "(ext)"); ui->Set_Client_configFileBox->addItem(tr("External file"), "(ext)");
int configId = -1; int configId = -1;
@@ -1331,10 +1404,67 @@ void MainWindow::on_GroupBox_clicked(bool checked)
settings->endGroup(); settings->endGroup();
} }
void MainWindow::saveClientPath(const QString& path) void MainWindow::addClientPath(const QString& path)
{
m_clientPathList.removeAll(path);
m_clientPathList.prepend(path);
while(m_clientPathList.size() > 32) // the maximum count of path items
m_clientPathList.removeLast();
// sync to the storage
saveClientPathList();
// sync to the UI
loadClientPathList();
}
void MainWindow::loadClientPathList()
{
m_clientPathList.clear();
settings->beginGroup("Client_Path");
int len = settings->beginReadArray("pathList");
settings->endArray();
if(settings->contains("path") && len == 0)
{
qDebug() << "Using old client path storage";
m_clientPathList += settings->value("path", "proxmark3").toString();
}
else
{
int arrayLen = settings->beginReadArray("pathList");
for(int i = 0; i < arrayLen; i++)
{
settings->setArrayIndex(i);
QString path = settings->value("path").toString();
if(!path.isEmpty())
m_clientPathList += path;
}
settings->endArray();
}
settings->endGroup();
ui->PM3_pathBox->clear();
for(const QString& clientPath : qAsConst(m_clientPathList))
ui->PM3_pathBox->addItem(clientPath);
}
void MainWindow::saveClientPathList()
{ {
settings->beginGroup("Client_Path"); settings->beginGroup("Client_Path");
settings->setValue("path", path); if(settings->contains("path"))
{
qDebug() << "Upgrading client path storage";
QString oldPath = settings->value("path").toString();
if(!oldPath.isEmpty() && !m_clientPathList.contains(oldPath))
m_clientPathList.append(oldPath);
settings->remove("path");
}
settings->beginWriteArray("pathList");
for(int i = 0; i < m_clientPathList.size(); i++)
{
settings->setArrayIndex(i);
settings->setValue("path", m_clientPathList[i]);
}
settings->endArray();
settings->endGroup(); settings->endGroup();
} }
// *********************************************** // ***********************************************
@@ -1411,16 +1541,20 @@ void MainWindow::on_LF_LFConf_freqSlider_valueChanged(int value)
onLFfreqConfChanged(value, true); onLFfreqConfChanged(value, true);
} }
void MainWindow::onLFfreqConfChanged(int value, bool isCustomized) void MainWindow::onLFfreqConfChanged(int divisor, bool isCustomized)
{ {
ui->LF_LFConf_freqDivisorBox->blockSignals(true); ui->LF_LFConf_freqDivisorBox->blockSignals(true);
ui->LF_LFConf_freqSlider->blockSignals(true); ui->LF_LFConf_freqSlider->blockSignals(true);
if(isCustomized) if(isCustomized)
ui->LF_LFConf_freqOtherButton->setChecked(true); ui->LF_LFConf_freqOtherButton->setChecked(true);
ui->LF_LFConf_freqLabel->setText(tr("Actural Freq: ") + QString("%1kHz").arg(LF::divisor2Freq(value), 0, 'f', 3)); else if(divisor == 95)
ui->LF_LFConf_freqDivisorBox->setValue(value); ui->LF_LFConf_freq125kButton->setChecked(true);
ui->LF_LFConf_freqSlider->setValue(value); else if(divisor == 88)
ui->LF_LFConf_freq134kButton->setChecked(true);
ui->LF_LFConf_freqLabel->setText(tr("Actural Freq: ") + QString("%1kHz").arg(LF::divisor2Freq(divisor), 0, 'f', 3));
ui->LF_LFConf_freqDivisorBox->setValue(divisor);
ui->LF_LFConf_freqSlider->setValue(divisor);
ui->LF_LFConf_freqDivisorBox->blockSignals(false); ui->LF_LFConf_freqDivisorBox->blockSignals(false);
ui->LF_LFConf_freqSlider->blockSignals(false); ui->LF_LFConf_freqSlider->blockSignals(false);
+7 -2
View File
@@ -220,6 +220,8 @@ private slots:
void on_Set_UI_CMDFont_setButton_clicked(); void on_Set_UI_CMDFont_setButton_clicked();
void onLFfreqConfChanged(int divisor, bool isCustomized);
private: private:
Ui::MainWindow* ui; Ui::MainWindow* ui;
QButtonGroup* MFCardTypeBtnGroup; QButtonGroup* MFCardTypeBtnGroup;
@@ -260,11 +262,14 @@ private:
MF_trailerDecoderDialog* decDialog; MF_trailerDecoderDialog* decDialog;
QStringList m_clientPathList;
void signalInit(); void signalInit();
void MF_widgetReset(); void MF_widgetReset();
void setTableItem(QTableWidget *widget, int row, int column, const QString& text); void setTableItem(QTableWidget *widget, int row, int column, const QString& text);
void saveClientPath(const QString& path); void addClientPath(const QString& path);
void onLFfreqConfChanged(int value, bool isCustomized); void loadClientPathList();
void saveClientPathList();
void dockInit(); void dockInit();
void loadConfig(); void loadConfig();
protected: protected:
+16 -2
View File
@@ -58,7 +58,17 @@
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QLineEdit" name="PM3_pathEdit"/> <widget class="QComboBox" name="PM3_pathBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="editable">
<bool>true</bool>
</property>
</widget>
</item> </item>
<item> <item>
<widget class="QLabel" name="label_18"> <widget class="QLabel" name="label_18">
@@ -84,6 +94,9 @@
<property name="editable"> <property name="editable">
<bool>true</bool> <bool>true</bool>
</property> </property>
<property name="minimumContentsLength">
<number>15</number>
</property>
</widget> </widget>
</item> </item>
<item> <item>
@@ -2714,7 +2727,8 @@ or the communication between a tag and a reader.</string>
<item> <item>
<widget class="QLabel" name="label_13"> <widget class="QLabel" name="label_13">
<property name="text"> <property name="text">
<string>If the client requires some enviroment variables, you can make a script file(*.bat on Windows or *.sh on Linux) to configure them, then put the path of the script there.</string> <string>If the client requires some enviroment variables, you can make a script file(*.bat on Windows or *.sh on Linux) to configure them, then put the path of the script there.
The &quot;&lt;client dir&gt;&quot; will be replaced by the directory of the &quot;Client Path&quot;</string>
</property> </property>
<property name="wordWrap"> <property name="wordWrap">
<bool>true</bool> <bool>true</bool>