Merge branch 'dev'

master
wh201906 2 years ago
commit 9aff432b1b
No known key found for this signature in database

@ -2,15 +2,26 @@
[中文](doc/CHANGELOG/CHANGELOG_zh_CN.md) [中文](doc/CHANGELOG/CHANGELOG_zh_CN.md)
### V0.2.6
+ Add support for Iceman/RRG repo v4.15864 [#37](https://github.com/wh201906/Proxmark3GUI/issues/37)
+ Optimize mifare classic block writing logic
+ Fix the default lf config
+ Add feedback for the GUI failing to start the client
+ Add feedback for the client failing to connect to PM3 hardware
+ Detect PM3 hardware when searching serial ports
+ Remove extra empty lines in raw command output
+ Use embedded config files
+ Remove the wait time between performing nested attack then switching to staticnested attack
### V0.2.5 ### V0.2.5
+ Fix bug [#28](https://github.com/wh201906/Proxmark3GUI/issues/28) + Fix bug [#28](https://github.com/wh201906/Proxmark3GUI/issues/28)
### V0.2.4 ### V0.2.4
+ Clone EM410x card to T55xx card + Clone EM410x card to T55xx card
### V0.2.3 ### V0.2.3
+ Fix bug [#27](https://github.com/wh201906/Proxmark3GUI/issues/27) + Fix bug [#27](https://github.com/wh201906/Proxmark3GUI/issues/27)
+ Try to support Non-ASCII path + Try to support Non-ASCII path
### V0.2.2 ### V0.2.2
+ Load command format from external json file + Load command format from external json file

@ -60,7 +60,6 @@ Great thanks to him.
mkdir build && cd build mkdir build && cd build
qmake ../src qmake ../src
make -j4 && make clean make -j4 && make clean
cp -r ../config ./
./Proxmark3GUI ./Proxmark3GUI
## Build on macOS ## Build on macOS

@ -0,0 +1,7 @@
<RCC>
<qresource prefix="/config">
<file>config_official.json</file>
<file>config_rrgv4.13441.json</file>
<file>config_rrgv4.15864.json</file>
</qresource>
</RCC>

@ -10,6 +10,13 @@
"2k": "2", "2k": "2",
"4k": "4" "4k": "4"
}, },
"//": "|---|----------------|---|----------------|---| ",
"//": "|sec|key A |res|key B |res| ",
"//": "|---|----------------|---|----------------|---| ",
"//": "|000| ffffffffffff | 1 | ffffffffffff | 1 | ",
"//": "......",
"//": "|---|----------------|---|----------------|---| ",
"//": "",
"key pattern": "\\|\\s*\\d{3}\\s*\\|\\s*.+?\\s*\\|\\s*.+?\\s*\\|\\s*.+?\\s*\\|\\s*.+?\\s*\\|", "key pattern": "\\|\\s*\\d{3}\\s*\\|\\s*.+?\\s*\\|\\s*.+?\\s*\\|\\s*.+?\\s*\\|\\s*.+?\\s*\\|",
"key A index": 2, "key A index": 2,
"key B index": 4 "key B index": 4
@ -22,6 +29,15 @@
"2k": "2", "2k": "2",
"4k": "4" "4k": "4"
}, },
"//": "|---|----------------|----------------| ",
"//": "|sec|key A |key B | ",
"//": "|---|----------------|----------------| ",
"//": "|000| ffffffffffff | ffffffffffff | ",
"//": "......",
"//": "|004| ? | ? | ",
"//": "......",
"//": "|---|----------------|----------------| ",
"//": " ",
"key pattern": "\\|\\s*\\d{3}\\s*\\|\\s*.+?\\s*\\|\\s*.+?\\s*\\|", "key pattern": "\\|\\s*\\d{3}\\s*\\|\\s*.+?\\s*\\|\\s*.+?\\s*\\|",
"key A index": 2, "key A index": 2,
"key B index": 3 "key B index": 3
@ -39,10 +55,22 @@
"cmd": "hf list mf" "cmd": "hf list mf"
}, },
"dump": { "dump": {
"cmd": "hf mf dump" "cmd": "hf mf dump <card type>",
"card type": {
"mini": "0",
"1k": "1",
"2k": "2",
"4k": "4"
}
}, },
"restore": { "restore": {
"cmd": "hf mf restore" "cmd": "hf mf restore <card type>",
"card type": {
"mini": "0",
"1k": "1",
"2k": "2",
"4k": "4"
}
}, },
"emulator wipe": { "emulator wipe": {
"cmd": "hf mf eclr" "cmd": "hf mf eclr"
@ -192,14 +220,14 @@
"divisor cmd": "hw setlfdivisor <divisor>" "divisor cmd": "hw setlfdivisor <divisor>"
} }
}, },
"t55xx":{ "t55xx": {
"clone em410x":{ "clone em410x": {
"read":"lf search", "read": "lf search",
"successful read flag":"Valid EM410x ID", "successful read flag": "Valid EM410x ID",
"pattern":"EM TAG ID\\s*:\\s\\K[0-9a-fA-F]{10}", "pattern": "EM TAG ID\\s*:\\s\\K[0-9a-fA-F]{10}",
"clone cmd":"lf em 410xwrite <id> <type>", "clone cmd": "lf em 410xwrite <id> <type>",
"t5555 flag":"0", "t5555 flag": "0",
"t55x7 flag":"1" "t55x7 flag": "1"
} }
} }
} }

@ -15,6 +15,13 @@
"A": "a", "A": "a",
"B": "b" "B": "b"
}, },
"//": "[+] |-----|----------------|---|----------------|---|",
"//": "[+] | Sec | key A |res| key B |res|",
"//": "[+] |-----|----------------|---|----------------|---|",
"//": "[+] | 000 | ffffffffffff | 1 | ffffffffffff | 1 |",
"//": "......",
"//": "[+] |-----|----------------|---|----------------|---|",
"//": "[+] ( 0:Failed / 1:Success )",
"key pattern": "\\|\\s*\\d{3}\\s*\\|\\s*.+?\\s*\\|\\s*.+?\\s*\\|\\s*.+?\\s*\\|\\s*.+?\\s*\\|", "key pattern": "\\|\\s*\\d{3}\\s*\\|\\s*.+?\\s*\\|\\s*.+?\\s*\\|\\s*.+?\\s*\\|\\s*.+?\\s*\\|",
"key A index": 2, "key A index": 2,
"key B index": 4 "key B index": 4
@ -27,6 +34,15 @@
"2k": "2k", "2k": "2k",
"4k": "4k" "4k": "4k"
}, },
"//": "[+] |-----|----------------|---|----------------|---|",
"//": "[+] | Sec | key A |res| key B |res|",
"//": "[+] |-----|----------------|---|----------------|---|",
"//": "[+] | 000 | ffffffffffff | 1 | ffffffffffff | 1 |",
"//": "......",
"//": "[+] | 004 | ------------ | 0 | ------------ | 0 |",
"//": "......",
"//": "[+] |-----|----------------|---|----------------|---|",
"//": "[+] ( 0:Failed / 1:Success )",
"key pattern": "\\|\\s*\\d{3}\\s*\\|\\s*.+?\\s*\\|\\s*.+?\\s*\\|\\s*.+?\\s*\\|\\s*.+?\\s*\\|", "key pattern": "\\|\\s*\\d{3}\\s*\\|\\s*.+?\\s*\\|\\s*.+?\\s*\\|\\s*.+?\\s*\\|\\s*.+?\\s*\\|",
"key A index": 2, "key A index": 2,
"key B index": 4 "key B index": 4
@ -44,10 +60,22 @@
"cmd": "trace list -t mf" "cmd": "trace list -t mf"
}, },
"dump": { "dump": {
"cmd": "hf mf dump" "cmd": "hf mf dump --<card type>",
"card type": {
"mini": "mini",
"1k": "1k",
"2k": "2k",
"4k": "4k"
}
}, },
"restore": { "restore": {
"cmd": "hf mf restore" "cmd": "hf mf restore --<card type>",
"card type": {
"mini": "mini",
"1k": "1k",
"2k": "2k",
"4k": "4k"
}
}, },
"emulator wipe": { "emulator wipe": {
"cmd": "hf mf eclr" "cmd": "hf mf eclr"
@ -200,14 +228,14 @@
"divisor cmd": "hw setlfdivisor -d <divisor>" "divisor cmd": "hw setlfdivisor -d <divisor>"
} }
}, },
"t55xx":{ "t55xx": {
"clone em410x":{ "clone em410x": {
"read":"lf em 410x reader", "read": "lf em 410x reader",
"successful read flag":"EM 410x ID", "successful read flag": "EM 410x ID",
"pattern":"EM 410x ID\\s*\\K[0-9a-fA-F]{10}", "pattern": "EM 410x ID\\s*\\K[0-9a-fA-F]{10}",
"clone cmd":"lf em 410x clone --id <id> <type>", "clone cmd": "lf em 410x clone --id <id> <type>",
"t5555 flag":"--q5", "t5555 flag": "--q5",
"t55x7 flag":"" "t55x7 flag": ""
} }
} }
} }

@ -0,0 +1,241 @@
{
"//": "Based on Proxmark3 rrg repo v4.15864, commit 1f75adc",
"//": "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"
},
"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>",
"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>"
},
"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>",
"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": ""
}
}
}

@ -1,17 +1,19 @@
import os, sys, shutil import os, sys, shutil
from win32api import GetFileVersionInfo from win32api import GetFileVersionInfo
from json import load from json import load
from re import fullmatch, IGNORECASE from re import fullmatch, sub, IGNORECASE
compressDirList = [] compressDirList = []
def getPEVersion(fname): def getPEVersion(fname):
try: try:
fileInfo = GetFileVersionInfo(fname, '\\') fileInfo = GetFileVersionInfo(fname, "\\")
version = "V%d.%d.%d" % (fileInfo['FileVersionMS'] / 65536, version = "V%d.%d.%d" % (
fileInfo['FileVersionMS'] % 65536, fileInfo["FileVersionMS"] / 65536,
fileInfo['FileVersionLS'] / 65536) fileInfo["FileVersionMS"] % 65536,
fileInfo["FileVersionLS"] / 65536,
)
except Exception: except Exception:
print("Cannot get version number of", fname) print("Cannot get version number of", fname)
return version return version
@ -19,7 +21,7 @@ def getPEVersion(fname):
os.chdir(sys.path[0]) os.chdir(sys.path[0])
print("Current Directory:", os.getcwd()) print("Current Directory:", os.getcwd())
targetName = os.path.abspath(os.getcwd()).split('\\')[-2] targetName = os.path.abspath(os.getcwd()).split("\\")[-2]
print("Target Name", targetName) print("Target Name", targetName)
src32Dir = "" src32Dir = ""
@ -63,11 +65,6 @@ elif not os.path.exists(dst32Dir):
print(dst32Dir, "doesn't exist, creating...") print(dst32Dir, "doesn't exist, creating...")
shutil.copytree("./32", dst32Dir) shutil.copytree("./32", dst32Dir)
shutil.copyfile(src32Path, dst32Path) shutil.copyfile(src32Path, dst32Path)
configPath = dst32Dir + "/config"
if os.path.exists(configPath):
print(configPath, "exists, replacing...")
shutil.rmtree(configPath)
shutil.copytree("../config", configPath)
compressDirList.append(dst32Dir) compressDirList.append(dst32Dir)
if os.path.exists(dst64Dir) and os.path.exists(dst64Path): if os.path.exists(dst64Dir) and os.path.exists(dst64Path):
@ -77,19 +74,23 @@ elif not os.path.exists(dst64Dir):
print(dst64Dir, "doesn't exist, creating...") print(dst64Dir, "doesn't exist, creating...")
shutil.copytree("./64", dst64Dir) shutil.copytree("./64", dst64Dir)
shutil.copyfile(src64Path, dst64Path) shutil.copyfile(src64Path, dst64Path)
configPath = dst64Dir + "/config"
if os.path.exists(configPath):
print(configPath, "exists, replacing...")
shutil.rmtree(configPath)
shutil.copytree("../config", configPath)
compressDirList.append(dst64Dir) compressDirList.append(dst64Dir)
# TODO: GUI+client # TODO: GUI+client
clientList = [ clientList = [
"official-v3.1.0", "rrg_other-v4.13441", "rrg_other-v4.14434", "official-v3.1.0",
"rrg_other-v4.14831" "rrg_other-v4.13441",
"rrg_other-v4.14434",
"rrg_other-v4.14831",
"rrg_other-v4.15864",
] ]
configList = []
for config in os.listdir("../config"):
configPath = os.path.join("../config", config)
if os.path.isfile(configPath) and config.endswith(".json"):
configList.append(config)
def generateClient(clientName): def generateClient(clientName):
global compressDirList global compressDirList
@ -105,12 +106,34 @@ def generateClient(clientName):
shutil.copytree(clientSrcDir, clientDstDir) shutil.copytree(clientSrcDir, clientDstDir)
shutil.copytree(dst64Dir, clientDstGUIDir) shutil.copytree(dst64Dir, clientDstGUIDir)
if "official" in clientName: if "official" in clientName:
shutil.copyfile("./client/GUIsettings_Official.ini", shutil.copyfile(
clientDstGUIDir + "/GUIsettings.ini") "./client/GUIsettings_Official.ini", clientDstGUIDir + "/GUIsettings.ini"
)
elif "rrg" in clientName: elif "rrg" in clientName:
shutil.copyfile("./client/GUIsettings_RRG.ini", shutil.copyfile(
clientDstGUIDir + "/GUIsettings.ini") "./client/GUIsettings_RRG.ini", clientDstGUIDir + "/GUIsettings.ini"
)
# Use exactly matched configFile if possible
version = clientName[clientName.find("v") :]
for config in configList:
if version in config:
print("Find matched config file", config)
with open(
clientDstGUIDir + "/GUIsettings.ini", "r", encoding="utf-8"
) as f:
data = f.read()
data = sub(
"configFile=:/config/.+\\.json",
"configFile=:/config/" + config,
data,
)
with open(
clientDstGUIDir + "/GUIsettings.ini", "w", encoding="utf-8"
) as f:
f.write(data)
compressDirList.append(clientDstDir) compressDirList.append(clientDstDir)
return clientDstDir
for cl in clientList: for cl in clientList:

@ -2,6 +2,17 @@
[English](../../CHANGELOG.md) [English](../../CHANGELOG.md)
### V0.2.6
+ 支持冰人版客户端 v4.15864 [#37](https://github.com/wh201906/Proxmark3GUI/issues/37)
+ 优化Mifare Classic卡写卡逻辑
+ 修复lf config默认配置
+ 添加客户端无法启动的提示
+ 添加PM3硬件连接失败的提示
+ 为PM3对应串口添加提示并自动选中
+ 修复原始指令框中有多余空行的问题
+ 内嵌不同客户端的配置文件
+ 去除从nested attack切换到staticnested attack的等待时间
### V0.2.5 ### V0.2.5
+ 修复 [#28](https://github.com/wh201906/Proxmark3GUI/issues/28) + 修复 [#28](https://github.com/wh201906/Proxmark3GUI/issues/28)

@ -59,7 +59,6 @@ release页面中有含客户端的GUI。这个GUI也可以搭配你自己的客
mkdir build && cd build mkdir build && cd build
qmake ../src qmake ../src
make -j4 && make clean make -j4 && make clean
cp -r ../config ./
./Proxmark3GUI ./Proxmark3GUI
## 在macOS系统下编译 ## 在macOS系统下编译

Binary file not shown.

File diff suppressed because it is too large Load Diff

@ -1,4 +1,3 @@
[Languages] [Languages]
en_US=English en_US=English
zh_CN=简体中文 zh_CN=简体中文
ext=Load from external file

File diff suppressed because it is too large Load Diff

@ -51,18 +51,19 @@ FORMS += \
ui/mf_attack_hardnesteddialog.ui ui/mf_attack_hardnesteddialog.ui
TRANSLATIONS += \ TRANSLATIONS += \
i18n/zh_CN.ts \ ../i18n/zh_CN.ts \
i18n/en_US.ts ../i18n/en_US.ts
# Default rules for deployment. # Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin 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.5 VERSION = 0.2.6
QMAKE_TARGET_PRODUCT = "Proxmark3GUI" QMAKE_TARGET_PRODUCT = "Proxmark3GUI"
QMAKE_TARGET_DESCRIPTION = "Proxmark3GUI" QMAKE_TARGET_DESCRIPTION = "Proxmark3GUI"
QMAKE_TARGET_COMPANY = "wh201906" QMAKE_TARGET_COMPANY = "wh201906"
RESOURCES += \ RESOURCES += \
i18n/language.qrc ../i18n/language.qrc \
../config/config.qrc

@ -13,6 +13,8 @@ PM3Process::PM3Process(QThread* thread, QObject* parent): QProcess(parent)
connect(serialListener, &QTimer::timeout, this, &PM3Process::onTimeout); connect(serialListener, &QTimer::timeout, this, &PM3Process::onTimeout);
connect(this, &PM3Process::readyRead, this, &PM3Process::onReadyRead); connect(this, &PM3Process::readyRead, this, &PM3Process::onReadyRead);
portInfo = nullptr; portInfo = nullptr;
qRegisterMetaType<QProcess::ProcessError>("QProcess::ProcessError");
} }
void PM3Process::connectPM3(const QString& path, const QStringList args) void PM3Process::connectPM3(const QString& path, const QStringList args)
@ -26,7 +28,8 @@ void PM3Process::connectPM3(const QString& path, const QStringList args)
currArgs = args; currArgs = args;
// using "-f" option to make the client output flushed after every print. // using "-f" option to make the client output flushed after every print.
start(path, args, QProcess::Unbuffered | QProcess::ReadWrite | QProcess::Text); // single '\r' might appear. Don't use QProcess::Text there or '\r' is ignored.
start(path, args, QProcess::Unbuffered | QProcess::ReadWrite);
if(waitForStarted(10000)) if(waitForStarted(10000))
{ {
waitForReadyRead(10000); waitForReadyRead(10000);
@ -59,8 +62,13 @@ void PM3Process::connectPM3(const QString& path, const QStringList args)
emit PM3StatedChanged(true, result); emit PM3StatedChanged(true, result);
} }
else else
{
emit HWConnectFailed();
kill(); kill();
}
} }
setRequiringOutput(false);
} }
void PM3Process::reconnectPM3() void PM3Process::reconnectPM3()

@ -48,6 +48,7 @@ signals:
void PM3StatedChanged(bool st, const QString& info = ""); void PM3StatedChanged(bool st, const QString& info = "");
void newOutput(const QString& output); void newOutput(const QString& output);
void changeClientType(Util::ClientType); void changeClientType(Util::ClientType);
void HWConnectFailed();
}; };
#endif // PM3PROCESS_H #endif // PM3PROCESS_H

@ -119,17 +119,29 @@ bool Util::chooseLanguage(QSettings* guiSettings, QMainWindow* window)
QStringList langList = langSettings->allKeys(); QStringList langList = langSettings->allKeys();
for(int i = 0; i < langList.size(); i++) for(int i = 0; i < langList.size(); i++)
langMap.insert(langSettings->value(langList[i]).toString(), langList[i]); langMap.insert(langSettings->value(langList[i]).toString(), langList[i]);
langMap.insert(tr("Load from external file"), "(ext)");
langSettings->endGroup(); langSettings->endGroup();
delete langSettings; delete langSettings;
bool isOk = false; bool isOk = false;
QString selectedText = QInputDialog::getItem(window, "", "Choose a language:", langMap.keys(), 0, false, &isOk); QString selectedText = QInputDialog::getItem(window, "", tr("Choose a language:"), langMap.keys(), 0, false, &isOk);
if(isOk) if(!isOk)
return false;
if(langMap[selectedText] == "(ext)")
{ {
guiSettings->beginGroup("lang"); QString extPath = QFileDialog::getOpenFileName(nullptr, tr("Select the translation file:"));
guiSettings->setValue("language", langMap[selectedText]); if(extPath.isEmpty())
return false;
guiSettings->beginGroup("language");
guiSettings->setValue("extPath", extPath);
guiSettings->endGroup(); guiSettings->endGroup();
guiSettings->sync();
} }
guiSettings->beginGroup("language");
guiSettings->setValue("name", langMap[selectedText]);
guiSettings->endGroup();
guiSettings->sync();
return isOk; return isOk;
} }

@ -13,6 +13,7 @@
#include <QSettings> #include <QSettings>
#include <QMainWindow> #include <QMainWindow>
#include <QInputDialog> #include <QInputDialog>
#include <QFileDialog>
#include <QDockWidget> #include <QDockWidget>
#include "ui_mainwindow.h" #include "ui_mainwindow.h"

Binary file not shown.

@ -27,33 +27,34 @@ int main(int argc, char *argv[])
QSettings* settings = new QSettings("GUIsettings.ini", QSettings::IniFormat); QSettings* settings = new QSettings("GUIsettings.ini", QSettings::IniFormat);
settings->setIniCodec("UTF-8"); settings->setIniCodec("UTF-8");
settings->beginGroup("lang"); settings->beginGroup("language");
QString currLang = settings->value("language", "").toString(); QString languageFile = settings->value("extPath").toString();
QString languageName = settings->value("name").toString();
settings->endGroup(); settings->endGroup();
if(currLang == "") if(languageName == "")
{ {
if(Util::chooseLanguage(settings, &w)) if(Util::chooseLanguage(settings, &w))
{ {
settings->beginGroup("lang"); settings->beginGroup("language");
currLang = settings->value("language", "").toString(); languageName = settings->value("name").toString();
settings->endGroup(); settings->endGroup();
} }
else else
currLang = "en_US"; languageName = "en_US";
}
if(languageName == "(ext)")
{
settings->beginGroup("language");
languageFile = settings->value("extPath").toString();
settings->endGroup();
} }
if(currLang == "ext")
currLang = QFileDialog::getOpenFileName(nullptr, "Select the translation file:");
else else
currLang = ":/i18n/" + currLang + ".qm"; languageFile = ":/i18n/" + languageName + ".qm";
QTranslator* translator = new QTranslator(&w); QTranslator* translator = new QTranslator(&w);
if(translator->load(currLang)) if(translator->load(languageFile))
{
a.installTranslator(translator); a.installTranslator(translator);
}
else else
{ QMessageBox::information(&w, "Error", "Can't load " + languageFile + " as translation file.");
QMessageBox::information(&w, "Error", "Can't load " + currLang + " as translation file.");
}
delete settings; delete settings;
w.initUI(); w.initUI();
w.show(); w.show();

@ -81,8 +81,10 @@ void LF::getLFConfig()
QVariantMap config = configMap["get config"].toMap(); QVariantMap config = configMap["get config"].toMap();
QString cmd = config["cmd"].toString(); QString cmd = config["cmd"].toString();
result = util->execCMDWithOutput(cmd, 400); result = util->execCMDWithOutput(cmd, 400);
start = result.indexOf(config["field start"].toString()); reMatch = QRegularExpression(config["field start"].toString(), QRegularExpression::MultilineOption).match(result);
end = result.indexOf(config["field end"].toString()); start = reMatch.hasMatch() ? reMatch.capturedEnd() : 0;
reMatch = QRegularExpression(config["field end"].toString(), QRegularExpression::MultilineOption).match(result, start);
end = reMatch.hasMatch() ? reMatch.capturedStart() : result.length();
result = result.mid(start, end - start); result = result.mid(start, end - start);
#if (QT_VERSION <= QT_VERSION_CHECK(5,14,0)) #if (QT_VERSION <= QT_VERSION_CHECK(5,14,0))
resultList = result.split("\n", QString::SkipEmptyParts); resultList = result.split("\n", QString::SkipEmptyParts);

@ -130,7 +130,7 @@ void Mifare::chk()
QString cmd = config["cmd"].toString(); QString cmd = config["cmd"].toString();
int keyAindex = config["key A index"].toInt(); int keyAindex = config["key A index"].toInt();
int keyBindex = config["key B index"].toInt(); int keyBindex = config["key B index"].toInt();
QRegularExpression keyPattern = QRegularExpression(config["key pattern"].toString()); QRegularExpression keyPattern = QRegularExpression(config["key pattern"].toString(), QRegularExpression::MultilineOption);
cmd.replace("<card type>", config["card type"].toMap()[cardType.typeText].toString()); cmd.replace("<card type>", config["card type"].toMap()[cardType.typeText].toString());
result = util->execCMDWithOutput( result = util->execCMDWithOutput(
@ -169,7 +169,7 @@ void Mifare::nested(bool isStaticNested)
cmd = config["cmd"].toString(); cmd = config["cmd"].toString();
int keyAindex = config["key A index"].toInt(); int keyAindex = config["key A index"].toInt();
int keyBindex = config["key B index"].toInt(); int keyBindex = config["key B index"].toInt();
QRegularExpression keyPattern = QRegularExpression(config["key pattern"].toString()); QRegularExpression keyPattern = QRegularExpression(config["key pattern"].toString(), QRegularExpression::MultilineOption);
QRegularExpressionMatch reMatch; QRegularExpressionMatch reMatch;
QString result; QString result;
int offset = 0; int offset = 0;
@ -212,7 +212,7 @@ void Mifare::nested(bool isStaticNested)
} }
result = util->execCMDWithOutput( result = util->execCMDWithOutput(
cmd, cmd,
Util::ReturnTrigger(15000, {"Can't found", "Can't authenticate", keyPattern_res->pattern()}), Util::ReturnTrigger(15000, {"Quit", "Can't found", "Can't authenticate", keyPattern_res->pattern()}),
true); true);
if(result.contains("static") && !isStaticNested) if(result.contains("static") && !isStaticNested)
@ -691,10 +691,14 @@ void Mifare::writeSelected(TargetType targetType)
{ {
result = _writeblk(item, KEY_B, keyBList->at(data_b2s(item)), dataList->at(item), TARGET_MIFARE); result = _writeblk(item, KEY_B, keyBList->at(data_b2s(item)), dataList->at(item), TARGET_MIFARE);
} }
if(!result) if(!result && keyAList->at(data_b2s(item)) != "FFFFFFFFFFFF")
{ {
result = _writeblk(item, KEY_A, "FFFFFFFFFFFF", dataList->at(item), TARGET_MIFARE); result = _writeblk(item, KEY_A, "FFFFFFFFFFFF", dataList->at(item), TARGET_MIFARE);
} }
if(!result && keyBList->at(data_b2s(item)) != "FFFFFFFFFFFF") // for access bits like "80 f7 87", the block can only be written with keyB
{
result = _writeblk(item, KEY_B, "FFFFFFFFFFFF", dataList->at(item), TARGET_MIFARE);
}
} }
else // key doesn't matter when writing to Chinese Magic Card and Emulator Memory else // key doesn't matter when writing to Chinese Magic Card and Emulator Memory
{ {
@ -743,14 +747,20 @@ void Mifare::writeSelected(TargetType targetType)
void Mifare::dump() void Mifare::dump()
{ {
QVariantMap config = configMap["dump"].toMap(); QVariantMap config = configMap["dump"].toMap();
util->execCMD(config["cmd"].toString()); QString cmd = config["cmd"].toString();
if(cmd.contains("<card type>"))
cmd.replace("<card type>", config["card type"].toMap()[cardType.typeText].toString());
util->execCMD(cmd);
Util::gotoRawTab(); Util::gotoRawTab();
} }
void Mifare::restore() void Mifare::restore()
{ {
QVariantMap config = configMap["restore"].toMap(); QVariantMap config = configMap["restore"].toMap();
util->execCMD(config["cmd"].toString()); QString cmd = config["cmd"].toString();
if(cmd.contains("<card type>"))
cmd.replace("<card type>", config["card type"].toMap()[cardType.typeText].toString());
util->execCMD(cmd);
Util::gotoRawTab(); Util::gotoRawTab();
} }

@ -2,6 +2,7 @@
#include "ui_mainwindow.h" #include "ui_mainwindow.h"
#include <QJsonDocument> #include <QJsonDocument>
#include <QDirIterator>
MainWindow::MainWindow(QWidget *parent): MainWindow::MainWindow(QWidget *parent):
QMainWindow(parent) QMainWindow(parent)
@ -30,7 +31,9 @@ MainWindow::MainWindow(QWidget *parent):
settings->setIniCodec("UTF-8"); settings->setIniCodec("UTF-8");
pm3Thread = new QThread(this); pm3Thread = new QThread(this);
connect(QApplication::instance(), &QApplication::aboutToQuit, pm3Thread, &QThread::quit);
pm3 = new PM3Process(pm3Thread); pm3 = new PM3Process(pm3Thread);
connect(pm3Thread, &QThread::finished, pm3, &PM3Process::deleteLater);
pm3Thread->start(); pm3Thread->start();
pm3state = false; pm3state = false;
clientWorkingDir = new QDir; clientWorkingDir = new QDir;
@ -77,7 +80,11 @@ MainWindow::~MainWindow()
void MainWindow::loadConfig() void MainWindow::loadConfig()
{ {
QFile configList(ui->Set_Client_configPathEdit->text()); QString filename = ui->Set_Client_configFileBox->currentData().toString();
if(filename == "(ext)")
filename = ui->Set_Client_configPathEdit->text();
qDebug() << "config file:" << filename;
QFile configList(filename);
if(!configList.open(QFile::ReadOnly | QFile::Text)) if(!configList.open(QFile::ReadOnly | QFile::Text))
{ {
QMessageBox::information(this, tr("Info"), tr("Failed to load config file")); QMessageBox::information(this, tr("Info"), tr("Failed to load config file"));
@ -104,18 +111,46 @@ void MainWindow::initUI() // will be called by main.app
void MainWindow::on_portSearchTimer_timeout() void MainWindow::on_portSearchTimer_timeout()
{ {
QStringList newPortList; QStringList newPortList; // for actural port name
QStringList newPortNameList; // for display name
const QString hint = " *";
foreach(const QSerialPortInfo &info, QSerialPortInfo::availablePorts()) foreach(const QSerialPortInfo &info, QSerialPortInfo::availablePorts())
{ {
// qDebug() << info.isBusy() << info.isNull() << info.portName() << info.description(); // qDebug() << info.isNull() << info.portName() << info.description() << info.serialNumber() << info.manufacturer();
if(!info.isNull()) if(!info.isNull())
newPortList << info.portName(); {
QString idString = (info.description() + info.serialNumber() + info.manufacturer()).toLower();
QString portName = info.portName();
newPortList << portName;
if(info.hasVendorIdentifier() && info.hasProductIdentifier())
{
quint16 vid = info.vendorIdentifier();
quint16 pid = info.productIdentifier();
if(vid == 0x9AC4 && pid == 0x4B8F)
portName += hint;
else if(vid == 0x2D2D && pid == 0x504D)
portName += hint;
}
else if(idString.contains("proxmark") || idString.contains("iceman"))
portName += hint;
newPortNameList << portName;
}
} }
if(newPortList != portList) // update PM3_portBox when available ports changed if(newPortList != portList) // update PM3_portBox when available ports changed
{ {
portList = newPortList; portList = newPortList;
ui->PM3_portBox->clear(); ui->PM3_portBox->clear();
ui->PM3_portBox->addItems(portList); int selectId = -1;
for(int i = 0; i < portList.size(); i++)
{
ui->PM3_portBox->addItem(newPortNameList[i], newPortList[i]);
if(selectId == -1 && newPortNameList[i].endsWith(hint))
selectId = i;
}
if(selectId != -1)
ui->PM3_portBox->setCurrentIndex(selectId);
} }
} }
@ -123,7 +158,7 @@ void MainWindow::on_PM3_connectButton_clicked()
{ {
qDebug() << "Main:" << QThread::currentThread(); qDebug() << "Main:" << QThread::currentThread();
QString port = ui->PM3_portBox->currentText(); QString port = ui->PM3_portBox->currentData().toString();
QString startArgs = ui->Set_Client_startArgsEdit->text(); QString startArgs = ui->Set_Client_startArgsEdit->text();
// 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
@ -193,6 +228,18 @@ void MainWindow::on_PM3_connectButton_clicked()
envSetProcess.kill(); envSetProcess.kill();
} }
void MainWindow::onPM3ErrorOccurred(QProcess::ProcessError error)
{
qDebug() << "PM3 Error:" << error << pm3->errorString();
if(error == QProcess::FailedToStart)
QMessageBox::information(this, tr("Info"), tr("Failed to start the client"));
}
void MainWindow::onPM3HWConnectFailed()
{
QMessageBox::information(this, tr("Info"), tr("Failed to connect to the hardware"));
}
void MainWindow::onPM3StateChanged(bool st, const QString& info) void MainWindow::onPM3StateChanged(bool st, const QString& info)
{ {
pm3state = st; pm3state = st;
@ -220,7 +267,8 @@ void MainWindow::on_PM3_disconnectButton_clicked()
void MainWindow::refreshOutput(const QString& output) void MainWindow::refreshOutput(const QString& output)
{ {
// qDebug() << "MainWindow::refresh:" << output; // qDebug() << "MainWindow::refresh:" << output;
ui->Raw_outputEdit->appendPlainText(output); ui->Raw_outputEdit->moveCursor(QTextCursor::End);
ui->Raw_outputEdit->insertPlainText(output);
ui->Raw_outputEdit->moveCursor(QTextCursor::End); ui->Raw_outputEdit->moveCursor(QTextCursor::End);
} }
@ -1054,6 +1102,8 @@ void MainWindow::uiInit()
ui->PM3_pathEdit->setText(settings->value("path", "proxmark3").toString()); ui->PM3_pathEdit->setText(settings->value("path", "proxmark3").toString());
settings->endGroup(); settings->endGroup();
ui->Set_Client_GUIWorkingDirLabel->setText(QDir::currentPath());
settings->beginGroup("Client_Args"); settings->beginGroup("Client_Args");
ui->Set_Client_startArgsEdit->setText(settings->value("args", "<port> -f").toString()); ui->Set_Client_startArgsEdit->setText(settings->value("args", "<port> -f").toString());
settings->endGroup(); settings->endGroup();
@ -1068,11 +1118,27 @@ void MainWindow::uiInit()
ui->Set_Client_keepClientActiveBox->setChecked(keepClientActive); ui->Set_Client_keepClientActiveBox->setChecked(keepClientActive);
settings->endGroup(); settings->endGroup();
QDirIterator configFiles(":/config/");
ui->Set_Client_configFileBox->blockSignals(true);
while(configFiles.hasNext())
{
configFiles.next();
ui->Set_Client_configFileBox->addItem(configFiles.fileName(), configFiles.filePath());
}
ui->Set_Client_configFileBox->addItem(tr("External file"), "(ext)");
int configId = -1;
settings->beginGroup("Client_Env"); settings->beginGroup("Client_Env");
ui->Set_Client_envScriptEdit->setText(settings->value("scriptPath").toString()); ui->Set_Client_envScriptEdit->setText(settings->value("scriptPath").toString());
ui->Set_Client_workingDirEdit->setText(settings->value("workingDir", "../data").toString()); ui->Set_Client_workingDirEdit->setText(settings->value("workingDir", "../data").toString());
ui->Set_Client_configPathEdit->setText(settings->value("configPath", "config.json").toString()); configId = ui->Set_Client_configFileBox->findData(settings->value("configFile"));
ui->Set_Client_configPathEdit->setText(settings->value("extConfigFilePath", "config.json").toString());
settings->endGroup(); settings->endGroup();
if(configId != -1)
ui->Set_Client_configFileBox->setCurrentIndex(configId);
ui->Set_Client_configFileBox->blockSignals(false);
on_Set_Client_configFileBox_currentIndexChanged(ui->Set_Client_configFileBox->currentIndex());
ui->MF_RW_keyTypeBox->addItem("A", Mifare::KEY_A); ui->MF_RW_keyTypeBox->addItem("A", Mifare::KEY_A);
ui->MF_RW_keyTypeBox->addItem("B", Mifare::KEY_B); ui->MF_RW_keyTypeBox->addItem("B", Mifare::KEY_B);
@ -1091,6 +1157,8 @@ void MainWindow::signalInit()
connect(this, &MainWindow::reconnectPM3, pm3, &PM3Process::reconnectPM3); connect(this, &MainWindow::reconnectPM3, pm3, &PM3Process::reconnectPM3);
connect(pm3, &PM3Process::PM3StatedChanged, this, &MainWindow::onPM3StateChanged); connect(pm3, &PM3Process::PM3StatedChanged, this, &MainWindow::onPM3StateChanged);
connect(pm3, &PM3Process::PM3StatedChanged, util, &Util::setRunningState); connect(pm3, &PM3Process::PM3StatedChanged, util, &Util::setRunningState);
connect(pm3, &PM3Process::errorOccurred, this, &MainWindow::onPM3ErrorOccurred);
connect(pm3, &PM3Process::HWConnectFailed, this, &MainWindow::onPM3HWConnectFailed);
connect(this, &MainWindow::killPM3, pm3, &PM3Process::killPM3); connect(this, &MainWindow::killPM3, pm3, &PM3Process::killPM3);
connect(this, &MainWindow::setProcEnv, pm3, &PM3Process::setProcEnv); connect(this, &MainWindow::setProcEnv, pm3, &PM3Process::setProcEnv);
connect(this, &MainWindow::setWorkingDir, pm3, &PM3Process::setWorkingDir); connect(this, &MainWindow::setWorkingDir, pm3, &PM3Process::setWorkingDir);
@ -1274,7 +1342,7 @@ void MainWindow::on_Set_Client_workingDirEdit_editingFinished()
void MainWindow::on_Set_Client_configPathEdit_editingFinished() void MainWindow::on_Set_Client_configPathEdit_editingFinished()
{ {
settings->beginGroup("Client_Env"); settings->beginGroup("Client_Env");
settings->setValue("configPath", ui->Set_Client_configPathEdit->text()); settings->setValue("extConfigFilePath", ui->Set_Client_configPathEdit->text());
settings->endGroup(); settings->endGroup();
} }
@ -1412,3 +1480,11 @@ void MainWindow::on_LF_LFConf_resetButton_clicked()
setState(true); setState(true);
} }
void MainWindow::on_Set_Client_configFileBox_currentIndexChanged(int index)
{
ui->Set_Client_configPathEdit->setVisible(ui->Set_Client_configFileBox->itemData(index).toString() == "(ext)");
settings->beginGroup("Client_Env");
settings->setValue("configFile", ui->Set_Client_configFileBox->currentData());
settings->endGroup();
}

@ -51,7 +51,7 @@ public:
~MainWindow(); ~MainWindow();
void initUI(); void initUI();
bool eventFilter(QObject *watched, QEvent *event); bool eventFilter(QObject *watched, QEvent *event) override;
public slots: public slots:
void refreshOutput(const QString& output); void refreshOutput(const QString& output);
void refreshCMD(const QString& cmd); void refreshCMD(const QString& cmd);
@ -60,6 +60,8 @@ public slots:
void MF_onMFCardTypeChanged(int id, bool st); void MF_onMFCardTypeChanged(int id, bool st);
void on_Raw_keyPressed(QObject *obj_addr, QEvent &event); void on_Raw_keyPressed(QObject *obj_addr, QEvent &event);
void on_MF_keyWidget_resized(QObject *obj_addr, QEvent &event); void on_MF_keyWidget_resized(QObject *obj_addr, QEvent &event);
void onPM3ErrorOccurred(QProcess::ProcessError error);
void onPM3HWConnectFailed();
private slots: private slots:
void on_PM3_connectButton_clicked(); void on_PM3_connectButton_clicked();
@ -207,6 +209,9 @@ private slots:
void on_Set_Client_configPathEdit_editingFinished(); void on_Set_Client_configPathEdit_editingFinished();
void setState(bool st); void setState(bool st);
void on_Set_Client_configFileBox_currentIndexChanged(int index);
private: private:
Ui::MainWindow* ui; Ui::MainWindow* ui;
QButtonGroup* MFCardTypeBtnGroup; QButtonGroup* MFCardTypeBtnGroup;

@ -1454,8 +1454,8 @@ When setting the freq, the &quot;hw setlfdivisor&quot; will also be called.</str
</item> </item>
<item row="2" column="1"> <item row="2" column="1">
<widget class="QCheckBox" name="LF_LFConf_averagingBox"> <widget class="QCheckBox" name="LF_LFConf_averagingBox">
<property name="text"> <property name="checked">
<string/> <bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
@ -2650,6 +2650,23 @@ or the communication between a tag and a reader.</string>
<string>Client</string> <string>Client</string>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_10"> <layout class="QVBoxLayout" name="verticalLayout_10">
<item>
<widget class="QLabel" name="label_65">
<property name="text">
<string>GUI working directory:</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="Set_Client_GUIWorkingDirLabel"/>
</item>
<item>
<widget class="Line" name="line_9">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item> <item>
<widget class="QLabel" name="label_11"> <widget class="QLabel" name="label_11">
<property name="text"> <property name="text">
@ -2746,10 +2763,34 @@ or the communication between a tag and a reader.</string>
<item> <item>
<widget class="QLabel" name="label_63"> <widget class="QLabel" name="label_63">
<property name="text"> <property name="text">
<string>Config file path(Reconnect to apply):</string> <string>Config file(Reconnect to apply):</string>
</property> </property>
</widget> </widget>
</item> </item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_15">
<item>
<widget class="QComboBox" name="Set_Client_configFileBox">
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToContents</enum>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_13">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item> <item>
<widget class="QLineEdit" name="Set_Client_configPathEdit"> <widget class="QLineEdit" name="Set_Client_configPathEdit">
<property name="text"> <property name="text">

Loading…
Cancel
Save