Skip to content

Commit 84ac96e

Browse files
committed
feat: 添加校验工具支持,允许选择多种校验算法并自动追加校验结果
1 parent c1283e7 commit 84ac96e

12 files changed

Lines changed: 432 additions & 13 deletions

CHANGELOG.md

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,6 @@
22

33
本项目遵循面向用户的变更记录格式。版本号在正式发布前可能继续调整。
44

5-
## Unreleased
6-
7-
### 新增
8-
9-
- 接收显示和文本发送支持分别选择 UTF-8、GBK、ASCII、Latin1 编码,并持久化配置。
10-
- 发送设置增加校验工具,支持 CRC16-Modbus、CRC16-CCITT、CRC32、LRC、XOR、SUM8 手动计算和发送时自动追加。
11-
125
## [0.1.5] - 2026-07-08
136

147
### 新增

CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ add_subdirectory("${FLUENTQTWIDGETS_ROOT}" "${CMAKE_BINARY_DIR}/_deps/fluentqtwi
5959
add_executable(FluentSerialAssistant
6060
src/app/core/font_preferences.cpp
6161
src/app/core/font_preferences.h
62+
src/app/core/checksum_utils.cpp
63+
src/app/core/checksum_utils.h
6264
src/app/core/hex_utils.cpp
6365
src/app/core/hex_utils.h
6466
src/app/core/text_encoding.cpp
@@ -75,6 +77,7 @@ add_executable(FluentSerialAssistant
7577
src/app/view/settings_page.h
7678
src/app/view/workbench_page.cpp
7779
src/app/view/workbench_page.h
80+
src/app/view/workbench/workbench_page_checksum.cpp
7881
src/app/view/workbench/workbench_page_connection.cpp
7982
src/app/view/workbench/workbench_page_files.cpp
8083
src/app/view/workbench/workbench_page_internal.h

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Fluent 串口助手是一个基于 C++17、Qt 6 Widgets 和 FluentQtWidgets 的
3333
- 状态统计:显示 RX/TX 总量、实时速率和连接时长。
3434
- TX 着色:可选择发送记录显示颜色。
3535
- 数据发送:支持文本和 HEX 发送,支持 None、CR、LF、CRLF 行结束符。
36+
- 校验工具:支持 CRC16-Modbus、CRC16-CCITT、CRC32、LRC、XOR、SUM8 手动计算和发送时自动追加。
3637
- 发送历史:保留最近 20 条唯一发送记录。
3738
- 常用包:支持保存、载入、发送、删除和排序常用发送包。
3839
- 循环发送:支持毫秒级发送间隔。
@@ -188,7 +189,7 @@ dist/FluentSerialAssistant-release
188189
2. 配置波特率、数据位、校验位、停止位、流控。
189190
3. 点击连接。
190191
4. 在终端记录区查看 RX/TX。
191-
5. 在发送区输入文本或 HEX 数据,必要时选择收发编码和行结束符后发送
192+
5. 在发送区输入文本或 HEX 数据,必要时选择收发编码、行结束符和校验自动追加后发送
192193
6. 可在常用包中填写名称、内容、模式和换行后保存,之后选中条目即可填入发送区或直接发送。
193194
7. 可选择文件,配置块大小和间隔后分块发送。
194195
8. 可开启自动断帧、时间戳、自动滚动、循环发送等选项。

docs/feature-roadmap.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@
4444

4545
### 2. 校验工具
4646

47-
增加常用校验计算能力,减少用户借助外部工具的成本。
47+
状态:已实现。
48+
49+
已经增加常用校验计算能力,减少用户借助外部工具的成本。
4850

4951
需求:
5052

src/app/core/checksum_utils.cpp

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
#include "app/core/checksum_utils.h"
2+
3+
#include <QtCore/QtGlobal>
4+
5+
namespace {
6+
7+
const QList<AppChecksum::AlgorithmOption> &algorithmOptions()
8+
{
9+
static const QList<AppChecksum::AlgorithmOption> options = {
10+
{QStringLiteral("crc16-modbus"), QStringLiteral("CRC16-Modbus"), 2},
11+
{QStringLiteral("crc16-ccitt"), QStringLiteral("CRC16-CCITT"), 2},
12+
{QStringLiteral("crc32"), QStringLiteral("CRC32"), 4},
13+
{QStringLiteral("lrc"), QStringLiteral("LRC"), 1},
14+
{QStringLiteral("xor"), QStringLiteral("XOR"), 1},
15+
{QStringLiteral("sum8"), QStringLiteral("SUM8"), 1},
16+
};
17+
return options;
18+
}
19+
20+
const AppChecksum::AlgorithmOption &optionForKey(const QString &key)
21+
{
22+
const QString normalized = AppChecksum::normalizedAlgorithmKey(key);
23+
for (const AppChecksum::AlgorithmOption &option : algorithmOptions()) {
24+
if (option.key == normalized) {
25+
return option;
26+
}
27+
}
28+
return algorithmOptions().first();
29+
}
30+
31+
quint16 crc16Modbus(const QByteArray &data)
32+
{
33+
quint16 crc = 0xFFFF;
34+
for (unsigned char byte : data) {
35+
crc ^= byte;
36+
for (int i = 0; i < 8; ++i) {
37+
if ((crc & 0x0001) != 0) {
38+
crc = static_cast<quint16>((crc >> 1) ^ 0xA001);
39+
} else {
40+
crc = static_cast<quint16>(crc >> 1);
41+
}
42+
}
43+
}
44+
return crc;
45+
}
46+
47+
quint16 crc16CcittFalse(const QByteArray &data)
48+
{
49+
quint16 crc = 0xFFFF;
50+
for (unsigned char byte : data) {
51+
crc ^= static_cast<quint16>(byte << 8);
52+
for (int i = 0; i < 8; ++i) {
53+
if ((crc & 0x8000) != 0) {
54+
crc = static_cast<quint16>((crc << 1) ^ 0x1021);
55+
} else {
56+
crc = static_cast<quint16>(crc << 1);
57+
}
58+
}
59+
}
60+
return crc;
61+
}
62+
63+
quint32 crc32Ieee(const QByteArray &data)
64+
{
65+
quint32 crc = 0xFFFFFFFFU;
66+
for (unsigned char byte : data) {
67+
crc ^= byte;
68+
for (int i = 0; i < 8; ++i) {
69+
if ((crc & 0x00000001U) != 0) {
70+
crc = (crc >> 1) ^ 0xEDB88320U;
71+
} else {
72+
crc >>= 1;
73+
}
74+
}
75+
}
76+
return crc ^ 0xFFFFFFFFU;
77+
}
78+
79+
quint8 lrc(const QByteArray &data)
80+
{
81+
quint8 sum = 0;
82+
for (unsigned char byte : data) {
83+
sum = static_cast<quint8>(sum + byte);
84+
}
85+
return static_cast<quint8>(0U - sum);
86+
}
87+
88+
quint8 xor8(const QByteArray &data)
89+
{
90+
quint8 result = 0;
91+
for (unsigned char byte : data) {
92+
result ^= byte;
93+
}
94+
return result;
95+
}
96+
97+
quint8 sum8(const QByteArray &data)
98+
{
99+
quint8 result = 0;
100+
for (unsigned char byte : data) {
101+
result = static_cast<quint8>(result + byte);
102+
}
103+
return result;
104+
}
105+
106+
QByteArray valueBytes(quint32 value, int widthBytes, AppChecksum::ByteOrder byteOrder)
107+
{
108+
QByteArray bytes;
109+
bytes.reserve(widthBytes);
110+
if (byteOrder == AppChecksum::ByteOrder::LittleEndian) {
111+
for (int i = 0; i < widthBytes; ++i) {
112+
bytes.append(static_cast<char>((value >> (8 * i)) & 0xFF));
113+
}
114+
return bytes;
115+
}
116+
117+
for (int i = widthBytes - 1; i >= 0; --i) {
118+
bytes.append(static_cast<char>((value >> (8 * i)) & 0xFF));
119+
}
120+
return bytes;
121+
}
122+
123+
} // namespace
124+
125+
namespace AppChecksum {
126+
127+
QList<AlgorithmOption> options() { return algorithmOptions(); }
128+
129+
QString defaultAlgorithmKey() { return QStringLiteral("crc16-modbus"); }
130+
131+
QString normalizedAlgorithmKey(const QString &key)
132+
{
133+
const QString normalized = key.trimmed().toLower();
134+
if (normalized == QStringLiteral("modbus") || normalized == QStringLiteral("crc-modbus")) {
135+
return QStringLiteral("crc16-modbus");
136+
}
137+
if (normalized == QStringLiteral("ccitt") || normalized == QStringLiteral("crc16")) {
138+
return QStringLiteral("crc16-ccitt");
139+
}
140+
if (normalized == QStringLiteral("sum")) {
141+
return QStringLiteral("sum8");
142+
}
143+
144+
for (const AlgorithmOption &option : algorithmOptions()) {
145+
if (option.key == normalized) {
146+
return option.key;
147+
}
148+
}
149+
return defaultAlgorithmKey();
150+
}
151+
152+
QString labelForAlgorithm(const QString &key) { return optionForKey(key).label; }
153+
154+
int widthForAlgorithm(const QString &key) { return optionForKey(key).widthBytes; }
155+
156+
ByteOrder byteOrderFromKey(const QString &key)
157+
{
158+
return key.trimmed().toLower() == QStringLiteral("big") ? ByteOrder::BigEndian : ByteOrder::LittleEndian;
159+
}
160+
161+
QString byteOrderKey(ByteOrder byteOrder)
162+
{
163+
return byteOrder == ByteOrder::BigEndian ? QStringLiteral("big") : QStringLiteral("little");
164+
}
165+
166+
QString byteOrderLabel(ByteOrder byteOrder)
167+
{
168+
return byteOrder == ByteOrder::BigEndian ? QStringLiteral("高字节在前") : QStringLiteral("低字节在前");
169+
}
170+
171+
ChecksumResult calculate(const QByteArray &data, const QString &algorithmKey, ByteOrder byteOrder)
172+
{
173+
ChecksumResult result;
174+
const QString key = normalizedAlgorithmKey(algorithmKey);
175+
const int widthBytes = widthForAlgorithm(key);
176+
177+
if (key == QStringLiteral("crc16-modbus")) {
178+
result.value = crc16Modbus(data);
179+
} else if (key == QStringLiteral("crc16-ccitt")) {
180+
result.value = crc16CcittFalse(data);
181+
} else if (key == QStringLiteral("crc32")) {
182+
result.value = crc32Ieee(data);
183+
} else if (key == QStringLiteral("lrc")) {
184+
result.value = lrc(data);
185+
} else if (key == QStringLiteral("xor")) {
186+
result.value = xor8(data);
187+
} else if (key == QStringLiteral("sum8")) {
188+
result.value = sum8(data);
189+
} else {
190+
result.errorMessage = QStringLiteral("不支持的校验算法");
191+
return result;
192+
}
193+
194+
result.bytes = valueBytes(result.value, widthBytes, byteOrder);
195+
result.ok = true;
196+
return result;
197+
}
198+
199+
} // namespace AppChecksum

src/app/core/checksum_utils.h

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#pragma once
2+
3+
#include <QtCore/QByteArray>
4+
#include <QtCore/QList>
5+
#include <QtCore/QString>
6+
7+
namespace AppChecksum {
8+
9+
enum class ByteOrder
10+
{
11+
LittleEndian,
12+
BigEndian
13+
};
14+
15+
struct AlgorithmOption
16+
{
17+
QString key;
18+
QString label;
19+
int widthBytes = 1;
20+
};
21+
22+
struct ChecksumResult
23+
{
24+
bool ok = false;
25+
quint32 value = 0;
26+
QByteArray bytes;
27+
QString errorMessage;
28+
};
29+
30+
QList<AlgorithmOption> options();
31+
QString defaultAlgorithmKey();
32+
QString normalizedAlgorithmKey(const QString &key);
33+
QString labelForAlgorithm(const QString &key);
34+
int widthForAlgorithm(const QString &key);
35+
ByteOrder byteOrderFromKey(const QString &key);
36+
QString byteOrderKey(ByteOrder byteOrder);
37+
QString byteOrderLabel(ByteOrder byteOrder);
38+
39+
ChecksumResult calculate(const QByteArray &data, const QString &algorithmKey, ByteOrder byteOrder);
40+
41+
} // namespace AppChecksum
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
#include "app/view/workbench/workbench_page_internal.h"
2+
3+
using namespace FluentQt;
4+
using namespace WorkbenchPagePrivate;
5+
6+
QString WorkbenchPage::checksumAlgorithmKey() const
7+
{
8+
return AppChecksum::normalizedAlgorithmKey(m_checksumAlgorithmCombo
9+
? m_checksumAlgorithmCombo->currentData().toString()
10+
: AppChecksum::defaultAlgorithmKey());
11+
}
12+
13+
AppChecksum::ByteOrder WorkbenchPage::checksumByteOrder() const
14+
{
15+
return AppChecksum::byteOrderFromKey(m_checksumByteOrderCombo
16+
? m_checksumByteOrderCombo->currentData().toString()
17+
: AppChecksum::byteOrderKey(AppChecksum::ByteOrder::LittleEndian));
18+
}
19+
20+
QByteArray WorkbenchPage::payloadWithOptionalChecksum(const QByteArray &payload, bool *ok)
21+
{
22+
if (ok) {
23+
*ok = false;
24+
}
25+
26+
if (!m_checksumAppendCheck || !m_checksumAppendCheck->isChecked()) {
27+
if (ok) {
28+
*ok = true;
29+
}
30+
return payload;
31+
}
32+
33+
const AppChecksum::ChecksumResult result =
34+
AppChecksum::calculate(payload, checksumAlgorithmKey(), checksumByteOrder());
35+
if (!result.ok) {
36+
showWarning(QStringLiteral("校验计算失败"), result.errorMessage);
37+
return payload;
38+
}
39+
40+
QByteArray output = payload;
41+
output.append(result.bytes);
42+
if (ok) {
43+
*ok = true;
44+
}
45+
setChecksumResultText(QStringLiteral("%1 0x%2 · %3")
46+
.arg(AppChecksum::labelForAlgorithm(checksumAlgorithmKey()),
47+
QStringLiteral("%1")
48+
.arg(result.value, AppChecksum::widthForAlgorithm(checksumAlgorithmKey()) * 2,
49+
16, QLatin1Char('0'))
50+
.toUpper(),
51+
bytesToHex(result.bytes)));
52+
return output;
53+
}
54+
55+
void WorkbenchPage::calculateChecksumForCurrentPayload()
56+
{
57+
bool ok = false;
58+
const QByteArray payload = currentPayload(&ok);
59+
if (!ok) {
60+
return;
61+
}
62+
if (payload.isEmpty()) {
63+
showWarning(QStringLiteral("无法计算校验"), QStringLiteral("发送内容为空"));
64+
return;
65+
}
66+
67+
const AppChecksum::ChecksumResult result =
68+
AppChecksum::calculate(payload, checksumAlgorithmKey(), checksumByteOrder());
69+
if (!result.ok) {
70+
showWarning(QStringLiteral("校验计算失败"), result.errorMessage);
71+
return;
72+
}
73+
74+
setChecksumResultText(QStringLiteral("%1 0x%2 · %3")
75+
.arg(AppChecksum::labelForAlgorithm(checksumAlgorithmKey()),
76+
QStringLiteral("%1")
77+
.arg(result.value, AppChecksum::widthForAlgorithm(checksumAlgorithmKey()) * 2,
78+
16, QLatin1Char('0'))
79+
.toUpper(),
80+
bytesToHex(result.bytes)));
81+
}
82+
83+
void WorkbenchPage::setChecksumResultText(const QString &text)
84+
{
85+
if (m_checksumResultLabel) {
86+
m_checksumResultLabel->setText(text);
87+
}
88+
}

0 commit comments

Comments
 (0)