Skip to content

Commit 2999466

Browse files
committed
feat: 增强自动断帧功能,支持按接收间隔、帧头、帧尾和固定长度断帧
1 parent 84ac96e commit 2999466

7 files changed

Lines changed: 242 additions & 21 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Fluent 串口助手是一个基于 C++17、Qt 6 Widgets 和 FluentQtWidgets 的
2727
- 连接参数:支持常用和扩展波特率、数据位、校验位、停止位、流控、RTS、DTR。
2828
- 终端显示:支持文本、HEX、混合显示模式。
2929
- 编码支持:接收和文本发送可分别选择 UTF-8、GBK、ASCII、Latin1。
30-
- 自动断帧:按接收间隔自动插入帧分隔
30+
- 自动断帧:支持按接收间隔、帧头、帧尾和固定长度断帧
3131
- 记录显示:RX/TX 统一会话记录,支持暂停显示、自动滚动、清空和计数重置。
3232
- 终端检索:支持按关键字搜索,并按全部、接收、发送过滤当前显示。
3333
- 状态统计:显示 RX/TX 总量、实时速率和连接时长。

docs/feature-roadmap.md

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

6565
### 3. 帧解析增强
6666

67-
当前已有按接收间隔自动断帧。后续需要支持更多协议调试场景。
67+
状态:已实现。
68+
69+
当前支持按接收间隔、固定帧头、固定帧尾和固定长度断帧。断帧只影响终端显示,接收保存仍写入原始字节流。
6870

6971
需求:
7072

src/app/view/workbench/workbench_page_internal.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ constexpr int ReconnectIntervalMs = 2000;
5555
constexpr int CompactControlHeight = 32;
5656
constexpr int DefaultFileChunkSize = 256;
5757
constexpr int DefaultFileChunkIntervalMs = 10;
58+
constexpr int MaxFrameBufferBytes = 1024 * 1024;
5859

5960
inline QColor terminalTimestampColor()
6061
{
@@ -326,6 +327,14 @@ inline void selectEncodingOption(ComboBox *combo, const QString &key)
326327
combo->setCurrentIndex(index >= 0 ? index : combo->findData(AppTextEncoding::defaultKey()));
327328
}
328329

330+
inline void addFrameModeOptions(ComboBox *combo)
331+
{
332+
combo->addItem(QStringLiteral("超时"), QIcon(), QStringLiteral("timeout"));
333+
combo->addItem(QStringLiteral("帧头"), QIcon(), QStringLiteral("header"));
334+
combo->addItem(QStringLiteral("帧尾"), QIcon(), QStringLiteral("tail"));
335+
combo->addItem(QStringLiteral("固定长度"), QIcon(), QStringLiteral("length"));
336+
}
337+
329338
inline void addChecksumAlgorithmOptions(ComboBox *combo)
330339
{
331340
for (const AppChecksum::AlgorithmOption &option : AppChecksum::options()) {

src/app/view/workbench/workbench_page_side_sections.cpp

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -121,27 +121,35 @@ QWidget *WorkbenchPage::createReceiveSettingsSection()
121121
receiveOptionsGrid->addWidget(m_pauseCheck, 1, 1);
122122
root->addWidget(receiveOptions);
123123

124-
auto *frameRowWidget = new QWidget(section);
125-
auto *frameRow = new QHBoxLayout(frameRowWidget);
126-
frameRow->setContentsMargins(0, 0, 0, 0);
127-
frameRow->setSpacing(6);
128-
m_autoFrameBreakCheck = new CheckBox(QStringLiteral("自动断帧"), frameRowWidget);
124+
m_frameModeCombo = new ComboBox(section);
125+
addFrameModeOptions(m_frameModeCombo);
126+
makeCompactControl(m_frameModeCombo);
127+
m_autoFrameBreakCheck = new CheckBox(QStringLiteral("启用"), section);
129128
m_autoFrameBreakCheck->setMinimumWidth(0);
130-
m_autoFrameBreakCheck->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
131-
m_frameBreakIntervalSpin = new SpinBox(frameRowWidget);
129+
m_autoFrameBreakCheck->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Fixed);
130+
addFormRow(root, QStringLiteral("断帧"), m_frameModeCombo, m_autoFrameBreakCheck);
131+
132+
m_framePatternEdit = new LineEdit(section);
133+
m_framePatternEdit->setPlaceholderText(QStringLiteral("HEX 边界,如 AA 55 或 0D 0A"));
134+
makeCompactControl(m_framePatternEdit);
135+
addFormRow(root, QStringLiteral("边界"), m_framePatternEdit);
136+
137+
m_frameFixedLengthSpin = new SpinBox(section);
138+
m_frameFixedLengthSpin->setRange(1, 65536);
139+
m_frameFixedLengthSpin->setValue(8);
140+
m_frameFixedLengthSpin->setSuffix(QStringLiteral(" B"));
141+
addFormRow(root, QStringLiteral("长度"), m_frameFixedLengthSpin);
142+
143+
m_frameBreakIntervalSpin = new SpinBox(section);
132144
m_frameBreakIntervalSpin->setRange(1, 60000);
133145
m_frameBreakIntervalSpin->setValue(20);
134146
m_frameBreakIntervalSpin->setSymbolVisible(false);
135147
m_frameBreakIntervalSpin->setFixedHeight(CompactControlHeight);
136-
setFixedControlWidth(m_frameBreakIntervalSpin, 92);
137-
auto *frameBreakUnitLabel = new CaptionLabel(QStringLiteral("ms"), frameRowWidget);
148+
auto *frameBreakUnitLabel = new CaptionLabel(QStringLiteral("ms"), section);
138149
frameBreakUnitLabel->setFixedHeight(CompactControlHeight);
139150
frameBreakUnitLabel->setFixedWidth(22);
140151
frameBreakUnitLabel->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
141-
frameRow->addWidget(m_autoFrameBreakCheck);
142-
frameRow->addWidget(m_frameBreakIntervalSpin);
143-
frameRow->addWidget(frameBreakUnitLabel);
144-
root->addWidget(frameRowWidget);
152+
addFormRow(root, QStringLiteral("超时"), m_frameBreakIntervalSpin, frameBreakUnitLabel);
145153

146154
auto *actionRow = new QHBoxLayout;
147155
actionRow->setSpacing(8);
@@ -177,9 +185,26 @@ QWidget *WorkbenchPage::createReceiveSettingsSection()
177185
renderTerminal();
178186
});
179187
connect(m_receiveEncodingCombo, &ComboBox::currentIndexChanged, this, [this](int) {
188+
flushRxFrameBuffer();
180189
QSettings settings;
181190
settings.setValue(QStringLiteral("receive/encoding"), receiveEncodingKey());
182191
});
192+
connect(m_frameModeCombo, &ComboBox::currentIndexChanged, this, [this](int) {
193+
flushRxFrameBuffer();
194+
QSettings settings;
195+
settings.setValue(QStringLiteral("receive/frameMode"), frameBreakModeKey());
196+
updateFrameControlState();
197+
});
198+
connect(m_framePatternEdit, &LineEdit::textChanged, this, [this](const QString &text) {
199+
flushRxFrameBuffer();
200+
QSettings settings;
201+
settings.setValue(QStringLiteral("receive/framePattern"), text);
202+
});
203+
connect(m_frameFixedLengthSpin, &SpinBox::valueChanged, this, [this](int value) {
204+
flushRxFrameBuffer();
205+
QSettings settings;
206+
settings.setValue(QStringLiteral("receive/frameFixedLength"), value);
207+
});
183208
connect(m_saveReceiveCheck, &CheckBox::toggled, this, &WorkbenchPage::updateReceiveCapture);
184209
connect(m_autoScrollCheck, &CheckBox::toggled, this, [this](bool checked) {
185210
QSettings settings;
@@ -199,17 +224,20 @@ QWidget *WorkbenchPage::createReceiveSettingsSection()
199224
}
200225
});
201226
connect(m_autoFrameBreakCheck, &CheckBox::toggled, this, [this](bool checked) {
227+
flushRxFrameBuffer();
202228
QSettings settings;
203229
settings.setValue(QStringLiteral("receive/autoFrameBreak"), checked);
204230
if (!checked) {
205231
m_lastRxTimestamp = QDateTime();
206232
}
233+
updateFrameControlState();
207234
});
208235
connect(m_frameBreakIntervalSpin, &SpinBox::valueChanged, this, [](int value) {
209236
QSettings settings;
210237
settings.setValue(QStringLiteral("receive/frameBreakMs"), value);
211238
});
212239
connect(m_clearButton, &PushButton::clicked, this, [this]() {
240+
m_rxFrameBuffer.clear();
213241
m_terminalStartRecord = m_records.size();
214242
m_pendingRecordIndexes.clear();
215243
m_terminalView->clear();

src/app/view/workbench/workbench_page_state.cpp

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ void WorkbenchPage::setupSerialSignals()
99
m_reconnectTimer.stop();
1010
m_manualDisconnect = false;
1111
m_lastRxTimestamp = QDateTime();
12+
m_rxFrameBuffer.clear();
1213
m_connectionStartedAt = QDateTime::currentDateTime();
1314
m_lastStatsRxCount = m_rxCount;
1415
m_lastStatsTxCount = m_txCount;
@@ -21,6 +22,7 @@ void WorkbenchPage::setupSerialSignals()
2122
showSuccess(QStringLiteral("连接成功"), QStringLiteral("%1 已打开").arg(portName));
2223
});
2324
connect(&m_serial, &SerialController::closed, this, [this]() {
25+
flushRxFrameBuffer();
2426
if (m_fileSendFile.isOpen()) {
2527
m_fileSendTimer.stop();
2628
m_fileSendFile.close();
@@ -39,7 +41,7 @@ void WorkbenchPage::setupSerialSignals()
3941
showInfo(QStringLiteral("连接已关闭"), QStringLiteral("串口已断开"));
4042
});
4143
connect(&m_serial, &SerialController::dataReceived, this,
42-
[this](const QByteArray &data) { appendRecord(RecordDirection::Rx, data); });
44+
[this](const QByteArray &data) { handleReceivedData(data); });
4345
connect(&m_serial, &SerialController::errorOccurred, this, [this](const QString &message) {
4446
if (!message.isEmpty()) {
4547
if (m_reconnectTimer.isActive()) {
@@ -101,6 +103,9 @@ void WorkbenchPage::restoreSettings()
101103
.toString();
102104
const QString lineEnding = settings.value(QStringLiteral("send/lineEnding"), QStringLiteral("none")).toString();
103105
const int loopInterval = settings.value(QStringLiteral("send/loopIntervalMs"), 1000).toInt();
106+
const QString frameMode = settings.value(QStringLiteral("receive/frameMode"), QStringLiteral("timeout")).toString();
107+
const QString framePattern = settings.value(QStringLiteral("receive/framePattern")).toString();
108+
const int frameFixedLength = settings.value(QStringLiteral("receive/frameFixedLength"), 8).toInt();
104109
const int frameBreakMs = settings.value(QStringLiteral("receive/frameBreakMs"), 20).toInt();
105110
const QString sendPayload = settings.value(QStringLiteral("send/currentPayload")).toString();
106111
const QString packetName = settings.value(QStringLiteral("send/currentPacketName")).toString();
@@ -155,8 +160,14 @@ void WorkbenchPage::restoreSettings()
155160
m_saveReceiveCheck->setChecked(settings.value(QStringLiteral("receive/saveToFile"), false).toBool());
156161
m_autoScrollCheck->setChecked(settings.value(QStringLiteral("receive/autoScroll"), true).toBool());
157162
m_timestampCheck->setChecked(settings.value(QStringLiteral("receive/timestamp"), false).toBool());
163+
const int frameModeIndex = m_frameModeCombo->findData(frameMode);
164+
m_frameModeCombo->setCurrentIndex(frameModeIndex >= 0 ? frameModeIndex
165+
: m_frameModeCombo->findData(QStringLiteral("timeout")));
166+
m_framePatternEdit->setText(framePattern);
167+
m_frameFixedLengthSpin->setValue(qBound(1, frameFixedLength, 65536));
158168
m_autoFrameBreakCheck->setChecked(settings.value(QStringLiteral("receive/autoFrameBreak"), false).toBool());
159169
m_frameBreakIntervalSpin->setValue(qBound(1, frameBreakMs, 60000));
170+
updateFrameControlState();
160171
m_showTxCheck->setChecked(settings.value(QStringLiteral("send/showTx"), true).toBool());
161172
const QColor txColor(
162173
settings.value(QStringLiteral("send/txColor"), defaultTxColor().name(QColor::HexRgb)).toString());
@@ -203,6 +214,9 @@ void WorkbenchPage::saveSettings() const
203214
settings.setValue(QStringLiteral("receive/autoScroll"), m_autoScrollCheck->isChecked());
204215
settings.setValue(QStringLiteral("receive/timestamp"), m_timestampCheck->isChecked());
205216
settings.setValue(QStringLiteral("receive/autoFrameBreak"), m_autoFrameBreakCheck->isChecked());
217+
settings.setValue(QStringLiteral("receive/frameMode"), frameBreakModeKey());
218+
settings.setValue(QStringLiteral("receive/framePattern"), m_framePatternEdit->text());
219+
settings.setValue(QStringLiteral("receive/frameFixedLength"), m_frameFixedLengthSpin->value());
206220
settings.setValue(QStringLiteral("receive/frameBreakMs"), m_frameBreakIntervalSpin->value());
207221
settings.setValue(QStringLiteral("receive/encoding"), receiveEncodingKey());
208222
settings.setValue(QStringLiteral("terminal/displayMode"), currentDisplayMode());

src/app/view/workbench/workbench_page_terminal.cpp

Lines changed: 161 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,38 @@ QString WorkbenchPage::sendEncodingKey() const
8585
: AppTextEncoding::defaultKey());
8686
}
8787

88+
QString WorkbenchPage::frameBreakModeKey() const
89+
{
90+
const QString mode = m_frameModeCombo ? m_frameModeCombo->currentData().toString() : QStringLiteral("timeout");
91+
if (mode == QStringLiteral("header") || mode == QStringLiteral("tail") || mode == QStringLiteral("length")) {
92+
return mode;
93+
}
94+
return QStringLiteral("timeout");
95+
}
96+
97+
QByteArray WorkbenchPage::frameBoundaryPattern(bool *ok) const
98+
{
99+
if (ok) {
100+
*ok = false;
101+
}
102+
if (!m_framePatternEdit) {
103+
return {};
104+
}
105+
const QString patternText = m_framePatternEdit->text().trimmed();
106+
if (patternText.isEmpty()) {
107+
return {};
108+
}
109+
110+
const HexParseResult result = parseHexPayload(patternText);
111+
if (!result.ok || result.bytes.isEmpty()) {
112+
return {};
113+
}
114+
if (ok) {
115+
*ok = true;
116+
}
117+
return result.bytes;
118+
}
119+
88120
QString WorkbenchPage::currentDisplayMode() const
89121
{
90122
return m_displayModeSegment ? m_displayModeSegment->currentItem() : QStringLiteral("text");
@@ -195,15 +227,140 @@ bool WorkbenchPage::recordMatchesTerminalFilter(const SessionRecord &record) con
195227
record.displayText.contains(searchText, Qt::CaseInsensitive);
196228
}
197229

198-
void WorkbenchPage::appendRecord(RecordDirection direction, const QByteArray &data)
230+
void WorkbenchPage::handleReceivedData(const QByteArray &data)
231+
{
232+
if (data.isEmpty()) {
233+
return;
234+
}
235+
236+
if (!m_autoFrameBreakCheck || !m_autoFrameBreakCheck->isChecked() ||
237+
frameBreakModeKey() == QStringLiteral("timeout")) {
238+
appendRecord(RecordDirection::Rx, data);
239+
return;
240+
}
241+
242+
recordReceivedBytes(data);
243+
processBufferedFrameData(data);
244+
}
245+
246+
void WorkbenchPage::recordReceivedBytes(const QByteArray &data)
247+
{
248+
if (data.isEmpty()) {
249+
return;
250+
}
251+
252+
m_rxCount += data.size();
253+
if (m_saveReceiveCheck && m_saveReceiveCheck->isChecked()) {
254+
if (!m_receiveCaptureFile.isOpen()) {
255+
updateReceiveCapture(true);
256+
}
257+
if (m_receiveCaptureFile.isOpen()) {
258+
m_receiveCaptureFile.write(data);
259+
m_receiveCaptureFile.flush();
260+
}
261+
}
262+
updateCounters();
263+
}
264+
265+
void WorkbenchPage::processBufferedFrameData(const QByteArray &data)
266+
{
267+
const QString mode = frameBreakModeKey();
268+
if (mode == QStringLiteral("length")) {
269+
const int frameLength = m_frameFixedLengthSpin ? qBound(1, m_frameFixedLengthSpin->value(), 65536) : 1;
270+
m_rxFrameBuffer.append(data);
271+
while (m_rxFrameBuffer.size() >= frameLength) {
272+
appendRecord(RecordDirection::Rx, m_rxFrameBuffer.left(frameLength), false);
273+
m_rxFrameBuffer.remove(0, frameLength);
274+
}
275+
return;
276+
}
277+
278+
bool patternOk = false;
279+
const QByteArray pattern = frameBoundaryPattern(&patternOk);
280+
if (!patternOk || pattern.isEmpty()) {
281+
appendRecord(RecordDirection::Rx, data, false);
282+
return;
283+
}
284+
285+
m_rxFrameBuffer.append(data);
286+
if (mode == QStringLiteral("tail")) {
287+
while (true) {
288+
const int index = m_rxFrameBuffer.indexOf(pattern);
289+
if (index < 0) {
290+
break;
291+
}
292+
const int frameEnd = index + pattern.size();
293+
appendRecord(RecordDirection::Rx, m_rxFrameBuffer.left(frameEnd), false);
294+
m_rxFrameBuffer.remove(0, frameEnd);
295+
}
296+
} else if (mode == QStringLiteral("header")) {
297+
while (true) {
298+
const int firstHeader = m_rxFrameBuffer.indexOf(pattern);
299+
if (firstHeader < 0) {
300+
const int keepBytes = qMin(pattern.size() - 1, m_rxFrameBuffer.size());
301+
const int emitBytes = m_rxFrameBuffer.size() - keepBytes;
302+
if (emitBytes > 0) {
303+
appendRecord(RecordDirection::Rx, m_rxFrameBuffer.left(emitBytes), false);
304+
m_rxFrameBuffer.remove(0, emitBytes);
305+
}
306+
break;
307+
}
308+
if (firstHeader > 0) {
309+
appendRecord(RecordDirection::Rx, m_rxFrameBuffer.left(firstHeader), false);
310+
m_rxFrameBuffer.remove(0, firstHeader);
311+
}
312+
313+
const int nextHeader = m_rxFrameBuffer.indexOf(pattern, pattern.size());
314+
if (nextHeader < 0) {
315+
break;
316+
}
317+
appendRecord(RecordDirection::Rx, m_rxFrameBuffer.left(nextHeader), false);
318+
m_rxFrameBuffer.remove(0, nextHeader);
319+
}
320+
}
321+
322+
if (m_rxFrameBuffer.size() > MaxFrameBufferBytes) {
323+
flushRxFrameBuffer();
324+
}
325+
}
326+
327+
void WorkbenchPage::flushRxFrameBuffer()
328+
{
329+
if (m_rxFrameBuffer.isEmpty()) {
330+
return;
331+
}
332+
const QByteArray data = m_rxFrameBuffer;
333+
m_rxFrameBuffer.clear();
334+
appendRecord(RecordDirection::Rx, data, false);
335+
}
336+
337+
void WorkbenchPage::updateFrameControlState()
338+
{
339+
const bool enabled = m_autoFrameBreakCheck && m_autoFrameBreakCheck->isChecked();
340+
const QString mode = frameBreakModeKey();
341+
if (m_frameModeCombo) {
342+
m_frameModeCombo->setEnabled(true);
343+
}
344+
if (m_framePatternEdit) {
345+
m_framePatternEdit->setEnabled(enabled && (mode == QStringLiteral("header") || mode == QStringLiteral("tail")));
346+
}
347+
if (m_frameFixedLengthSpin) {
348+
m_frameFixedLengthSpin->setEnabled(enabled && mode == QStringLiteral("length"));
349+
}
350+
if (m_frameBreakIntervalSpin) {
351+
m_frameBreakIntervalSpin->setEnabled(enabled && mode == QStringLiteral("timeout"));
352+
}
353+
}
354+
355+
void WorkbenchPage::appendRecord(RecordDirection direction, const QByteArray &data, bool updateStats)
199356
{
200357
if (data.isEmpty()) {
201358
return;
202359
}
203360

204361
const QDateTime now = QDateTime::currentDateTime();
205362
if (direction == RecordDirection::Rx && m_autoFrameBreakCheck && m_autoFrameBreakCheck->isChecked() &&
206-
m_lastRxTimestamp.isValid()) {
363+
frameBreakModeKey() == QStringLiteral("timeout") && m_lastRxTimestamp.isValid()) {
207364
const int thresholdMs = m_frameBreakIntervalSpin ? m_frameBreakIntervalSpin->value() : 20;
208365
if (m_lastRxTimestamp.msecsTo(now) >= thresholdMs) {
209366
SessionRecord separator;
@@ -225,7 +382,7 @@ void WorkbenchPage::appendRecord(RecordDirection direction, const QByteArray &da
225382
m_records.append(record);
226383
m_pendingRecordIndexes.append(m_records.size() - 1);
227384

228-
if (direction == RecordDirection::Rx) {
385+
if (direction == RecordDirection::Rx && updateStats) {
229386
m_lastRxTimestamp = now;
230387
m_rxCount += data.size();
231388
if (m_saveReceiveCheck && m_saveReceiveCheck->isChecked()) {
@@ -237,7 +394,7 @@ void WorkbenchPage::appendRecord(RecordDirection direction, const QByteArray &da
237394
m_receiveCaptureFile.flush();
238395
}
239396
}
240-
} else {
397+
} else if (direction == RecordDirection::Tx && updateStats) {
241398
m_txCount += data.size();
242399
}
243400

0 commit comments

Comments
 (0)