Add option to read frames using a start delimiter (@AnnabellePundaky)

This commit is contained in:
Alex Spataru 2025-01-07 10:09:15 -05:00
parent cf2af5ad66
commit 9c0f2b6c13
7 changed files with 129 additions and 27 deletions

View File

@ -386,6 +386,7 @@ ColumnLayout {
currentIndex: editableValue currentIndex: editableValue
font: Cpp_Misc_CommonFonts.monoFont font: Cpp_Misc_CommonFonts.monoFont
onCurrentIndexChanged: { onCurrentIndexChanged: {
if (currentIndex !== editableValue) {
root.modelPointer.setData( root.modelPointer.setData(
view.index(row, column), view.index(row, column),
currentIndex, currentIndex,
@ -393,6 +394,7 @@ ColumnLayout {
} }
} }
} }
}
// //
// CheckBox value editor // CheckBox value editor

View File

@ -58,7 +58,7 @@ public:
[[nodiscard]] T read(qsizetype size); [[nodiscard]] T read(qsizetype size);
[[nodiscard]] T peek(qsizetype size) const; [[nodiscard]] T peek(qsizetype size) const;
[[nodiscard]] int findPatternKMP(const T &pattern); [[nodiscard]] int findPatternKMP(const T &pattern, const int pos = 0);
private: private:
[[nodiscard]] std::vector<int> computeKMPTable(const T &p) const; [[nodiscard]] std::vector<int> computeKMPTable(const T &p) const;
@ -253,41 +253,66 @@ T IO::CircularBuffer<T, StorageType>::peek(qsizetype size) const
} }
/** /**
* @brief Searches for a pattern in the buffer using the KMP algorithm. * @brief Searches for a pattern in the circular buffer using the KMP algorithm.
* *
* Implements the Knuth-Morris-Pratt algorithm to efficiently find a pattern in * This function uses the Knuth-Morris-Pratt (KMP) string matching algorithm
* the buffer. * to efficiently find the first occurrence of a given pattern within the
* circular buffer, starting from a specified position. The circular nature of
* the buffer is correctly handled to ensure accurate matching, even if the
* pattern spans the end of the buffer and the beginning.
* *
* @param pattern The QByteArray representing the pattern to search for. * @tparam T The type of elements stored in the pattern and the buffer.
* @return The index of the first occurrence of the pattern, or -1 if not found. * @tparam StorageType The type of storage used for the circular buffer.
* @param pattern The pattern to search for in the buffer.
* @param pos The starting position (relative to the logical start of the
* buffer) for the search. Defaults to the beginning if set to 0.
*
* @return The index (relative to the logical start of the buffer) of the first
* occurrence of the pattern, or -1 if the pattern is not found.
*
* @warning If the `pattern` is empty or its size exceeds the current size of
* the buffer, the function returns -1 immediately.
*
* @pre The buffer must be properly initialized, and the pattern should not
* exceed the capacity of the buffer.
*/ */
template<typename T, typename StorageType> template<typename T, typename StorageType>
int IO::CircularBuffer<T, StorageType>::findPatternKMP(const T &pattern) int IO::CircularBuffer<T, StorageType>::findPatternKMP(const T &pattern,
const int pos)
{ {
std::lock_guard<std::mutex> lock(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
// Validate search pattern
if (pattern.isEmpty() || m_size < pattern.size()) if (pattern.isEmpty() || m_size < pattern.size())
return -1; return -1;
// Start search at `pos`
std::vector<int> lps = computeKMPTable(pattern); std::vector<int> lps = computeKMPTable(pattern);
qsizetype bufferIdx = m_head; qsizetype bufferIdx = (m_head + pos) % m_capacity;
int i = pos, j = 0;
int i = 0, j = 0;
while (i < m_size) while (i < m_size)
{ {
// Compare current buffer character with the pattern
if (m_buffer[bufferIdx] == pattern[j]) if (m_buffer[bufferIdx] == pattern[j])
{ {
i++; i++;
j++; j++;
bufferIdx = (bufferIdx + 1) % m_capacity; bufferIdx = (bufferIdx + 1) % m_capacity;
// If the whole pattern is matched, return the logical start index
if (j == pattern.size()) if (j == pattern.size())
return i - j; {
int matchStart = i - j;
return matchStart;
}
} }
// Mismatch after some matches, fall back in pattern
else if (j != 0) else if (j != 0)
j = lps[j - 1]; j = lps[j - 1];
// Mismatch at the start, move forward
else else
{ {
i++; i++;
@ -295,9 +320,9 @@ int IO::CircularBuffer<T, StorageType>::findPatternKMP(const T &pattern)
} }
} }
// Pattern not found
return -1; return -1;
} }
/** /**
* @brief Computes the KMP table for a given p. * @brief Computes the KMP table for a given p.
* *

View File

@ -153,6 +153,7 @@ void IO::FrameReader::processData(const QByteArray &data)
// Add data to circular buffer // Add data to circular buffer
m_dataBuffer.append(data); m_dataBuffer.append(data);
Q_EMIT dataReceived(data);
// Read frames in no-delimiter mode directly // Read frames in no-delimiter mode directly
if (m_operationMode == SerialStudio::ProjectFile if (m_operationMode == SerialStudio::ProjectFile
@ -163,9 +164,6 @@ void IO::FrameReader::processData(const QByteArray &data)
else else
QMetaObject::invokeMethod(this, &FrameReader::readFrames, QMetaObject::invokeMethod(this, &FrameReader::readFrames,
Qt::QueuedConnection); Qt::QueuedConnection);
// Notify UI of received data
Q_EMIT dataReceived(data);
} }
/** /**
@ -264,6 +262,10 @@ void IO::FrameReader::readFrames()
if (m_frameDetectionMode == SerialStudio::EndDelimiterOnly) if (m_frameDetectionMode == SerialStudio::EndDelimiterOnly)
readEndDelimetedFrames(); readEndDelimetedFrames();
// Read using only a start delimeter
else if (m_frameDetectionMode == SerialStudio::StartDelimiterOnly)
readStartDelimitedFrames();
// Read using both a start & end delimiter // Read using both a start & end delimiter
else if (m_frameDetectionMode == SerialStudio::StartAndEndDelimiter) else if (m_frameDetectionMode == SerialStudio::StartAndEndDelimiter)
readStartEndDelimetedFrames(); readStartEndDelimetedFrames();
@ -360,6 +362,59 @@ void IO::FrameReader::readEndDelimetedFrames()
} }
} }
/**
* @brief Reads frames delimited by a start sequence from the buffer.
*
* Extracts frames from the circular buffer that are bounded by specified
* start delimiters. Emits `frameReady` for each valid frame.
*/
void IO::FrameReader::readStartDelimitedFrames()
{
// Cap the number of frames that we can read in a single call
int framesRead = 0;
constexpr int maxFrames = 100;
// Consume the buffer until
while (framesRead < maxFrames)
{
// Initialize variables
int startIndex = -1;
int nextStartIndex = -1;
// Find the first start sequence in the buffer (project mode)
startIndex = m_dataBuffer.findPatternKMP(m_startSequence);
if (startIndex == -1)
break;
// Find the next start sequence after the current one
nextStartIndex = m_dataBuffer.findPatternKMP(
m_startSequence, startIndex + m_startSequence.size());
if (nextStartIndex == -1 || nextStartIndex == startIndex
|| nextStartIndex < startIndex)
break;
// Extract the frame from the buffer
qsizetype frameStart = startIndex + m_startSequence.size();
qsizetype frameLength = nextStartIndex - frameStart;
QByteArray frame = m_dataBuffer.peek(frameStart + frameLength)
.mid(frameStart, frameLength);
// Parse frame if not empty
if (!frame.isEmpty())
{
Q_EMIT frameReady(frame);
(void)m_dataBuffer.read(frameStart + frameLength);
}
// Avoid infinite loops when getting a frame length of 0
else
(void)m_dataBuffer.read(frameStart);
// Increment number of frames read
++framesRead;
}
}
/** /**
* @brief Reads frames delimited by both start and end sequences from the * @brief Reads frames delimited by both start and end sequences from the
* buffer. * buffer.

View File

@ -78,6 +78,7 @@ private slots:
private: private:
void readEndDelimetedFrames(); void readEndDelimetedFrames();
void readStartDelimitedFrames();
void readStartEndDelimetedFrames(); void readStartEndDelimetedFrames();
ValidationStatus integrityChecks(const QByteArray &frame, ValidationStatus integrityChecks(const QByteArray &frame,
const QByteArray &delimeter, const QByteArray &delimeter,

View File

@ -2090,7 +2090,8 @@ void JSON::ProjectModel::buildProjectModel()
frameDetection->setEditable(true); frameDetection->setEditable(true);
frameDetection->setData(ComboBox, WidgetType); frameDetection->setData(ComboBox, WidgetType);
frameDetection->setData(m_frameDetectionMethods, ComboBoxData); frameDetection->setData(m_frameDetectionMethods, ComboBoxData);
frameDetection->setData(m_frameDetection, EditableValue); frameDetection->setData(
m_frameDetectionMethodsValues.indexOf(m_frameDetection), EditableValue);
frameDetection->setData(tr("Frame Detection"), ParameterName); frameDetection->setData(tr("Frame Detection"), ParameterName);
frameDetection->setData(kProjectView_FrameDetection, ParameterType); frameDetection->setData(kProjectView_FrameDetection, ParameterType);
frameDetection->setData(tr("Strategy used for identifying frame data"), frameDetection->setData(tr("Strategy used for identifying frame data"),
@ -2098,7 +2099,8 @@ void JSON::ProjectModel::buildProjectModel()
m_projectModel->appendRow(frameDetection); m_projectModel->appendRow(frameDetection);
// Add frame start sequence // Add frame start sequence
if (m_frameDetection == SerialStudio::StartAndEndDelimiter) if (m_frameDetection == SerialStudio::StartAndEndDelimiter
|| m_frameDetection == SerialStudio::StartDelimiterOnly)
{ {
auto frameStart = new QStandardItem(); auto frameStart = new QStandardItem();
frameStart->setEditable(true); frameStart->setEditable(true);
@ -2701,9 +2703,15 @@ void JSON::ProjectModel::generateComboBoxModels()
// Initialize frame detection methods // Initialize frame detection methods
m_frameDetectionMethods.clear(); m_frameDetectionMethods.clear();
m_frameDetectionMethodsValues.clear();
m_frameDetectionMethods.append(tr("End Delimiter Only")); m_frameDetectionMethods.append(tr("End Delimiter Only"));
m_frameDetectionMethods.append(tr("Start Delimiter Only"));
m_frameDetectionMethods.append(tr("Start + End Delimiter")); m_frameDetectionMethods.append(tr("Start + End Delimiter"));
m_frameDetectionMethods.append(tr("No Delimiters")); m_frameDetectionMethods.append(tr("No Delimiters"));
m_frameDetectionMethodsValues.append(SerialStudio::EndDelimiterOnly);
m_frameDetectionMethodsValues.append(SerialStudio::StartDelimiterOnly);
m_frameDetectionMethodsValues.append(SerialStudio::StartAndEndDelimiter);
m_frameDetectionMethodsValues.append(SerialStudio::NoDelimiters);
// Initialize group-level widgets // Initialize group-level widgets
m_groupWidgets.clear(); m_groupWidgets.clear();
@ -2997,8 +3005,7 @@ void JSON::ProjectModel::onProjectItemChanged(QStandardItem *item)
m_frameDecoder = static_cast<SerialStudio::DecoderMethod>(value.toInt()); m_frameDecoder = static_cast<SerialStudio::DecoderMethod>(value.toInt());
break; break;
case kProjectView_FrameDetection: case kProjectView_FrameDetection:
m_frameDetection m_frameDetection = m_frameDetectionMethodsValues.at(value.toInt());
= static_cast<SerialStudio::FrameDetection>(value.toInt());
Q_EMIT frameDetectionChanged(); Q_EMIT frameDetectionChanged();
buildProjectModel(); buildProjectModel();
break; break;

View File

@ -340,6 +340,8 @@ private:
QStringList m_fftSamples; QStringList m_fftSamples;
QStringList m_decoderOptions; QStringList m_decoderOptions;
QStringList m_frameDetectionMethods; QStringList m_frameDetectionMethods;
QList<SerialStudio::FrameDetection> m_frameDetectionMethodsValues;
QMap<QString, QString> m_eolSequences; QMap<QString, QString> m_eolSequences;
QMap<QString, QString> m_groupWidgets; QMap<QString, QString> m_groupWidgets;
QMap<QString, QString> m_datasetWidgets; QMap<QString, QString> m_datasetWidgets;

View File

@ -142,6 +142,9 @@ public:
PlainText, /**< Standard decoding, interprets data as plain text. */ PlainText, /**< Standard decoding, interprets data as plain text. */
Hexadecimal, /**< Decodes data assuming a hexadecimal-encoded format. */ Hexadecimal, /**< Decodes data assuming a hexadecimal-encoded format. */
Base64 /**< Decodes data assuming a Base64-encoded format. */ Base64 /**< Decodes data assuming a Base64-encoded format. */
/* IMPORTANT: When adding other modes, please don't modify the order of the
* enums to ensure backward compatiblity with previous project
* files!! */
}; };
Q_ENUM(DecoderMethod) Q_ENUM(DecoderMethod)
@ -153,15 +156,19 @@ public:
* of a data frame in a continuous stream. Each value represents a specific * of a data frame in a continuous stream. Each value represents a specific
* detection method. * detection method.
*/ */
// clang-format off
enum FrameDetection enum FrameDetection
{ {
EndDelimiterOnly, /**< Detects frames based only on an end delimiter. */ EndDelimiterOnly = 0x00, /**< Detects frames based only on an end delimiter. */
StartAndEndDelimiter, /**< Detects frames based on both start and end StartAndEndDelimiter = 0x01, /**< Detects frames based on both start and end delimiters. */
delimiters. */ NoDelimiters = 0x02, /**< Disables frame detection and processes incoming data directly */
NoDelimiters /**< Disables frame detection and processes incoming StartDelimiterOnly = 0x03 /**< Detects frames with only a header */
data directly */ /* IMPORTANT: When adding other modes, please don't modify the order of the
* enums to ensure backward compatiblity with previous project
* files!! */
}; };
Q_ENUM(FrameDetection) Q_ENUM(FrameDetection)
// clang-format on
/** /**
* @enum OperationMode * @enum OperationMode
@ -176,6 +183,9 @@ public:
ProjectFile, /**< Builds the dashboard using a predefined project file. */ ProjectFile, /**< Builds the dashboard using a predefined project file. */
DeviceSendsJSON, /**< Builds the dashboard from device-sent JSON. */ DeviceSendsJSON, /**< Builds the dashboard from device-sent JSON. */
QuickPlot, /**< Quick and simple data plotting mode. */ QuickPlot, /**< Quick and simple data plotting mode. */
/* IMPORTANT: When adding other modes, please don't modify the order of the
* enums to ensure backward compatiblity with previous project
* files!! */
}; };
Q_ENUM(OperationMode) Q_ENUM(OperationMode)