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,10 +386,12 @@ ColumnLayout {
currentIndex: editableValue
font: Cpp_Misc_CommonFonts.monoFont
onCurrentIndexChanged: {
root.modelPointer.setData(
view.index(row, column),
currentIndex,
ProjectModel.EditableValue)
if (currentIndex !== editableValue) {
root.modelPointer.setData(
view.index(row, column),
currentIndex,
ProjectModel.EditableValue)
}
}
}
}

View File

@ -58,7 +58,7 @@ public:
[[nodiscard]] T read(qsizetype size);
[[nodiscard]] T peek(qsizetype size) const;
[[nodiscard]] int findPatternKMP(const T &pattern);
[[nodiscard]] int findPatternKMP(const T &pattern, const int pos = 0);
private:
[[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
* the buffer.
* This function uses the Knuth-Morris-Pratt (KMP) string matching algorithm
* 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.
* @return The index of the first occurrence of the pattern, or -1 if not found.
* @tparam T The type of elements stored in the pattern and the buffer.
* @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>
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);
// Validate search pattern
if (pattern.isEmpty() || m_size < pattern.size())
return -1;
// Start search at `pos`
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)
{
// Compare current buffer character with the pattern
if (m_buffer[bufferIdx] == pattern[j])
{
i++;
j++;
bufferIdx = (bufferIdx + 1) % m_capacity;
// If the whole pattern is matched, return the logical start index
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)
j = lps[j - 1];
// Mismatch at the start, move forward
else
{
i++;
@ -295,9 +320,9 @@ int IO::CircularBuffer<T, StorageType>::findPatternKMP(const T &pattern)
}
}
// Pattern not found
return -1;
}
/**
* @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
m_dataBuffer.append(data);
Q_EMIT dataReceived(data);
// Read frames in no-delimiter mode directly
if (m_operationMode == SerialStudio::ProjectFile
@ -163,9 +164,6 @@ void IO::FrameReader::processData(const QByteArray &data)
else
QMetaObject::invokeMethod(this, &FrameReader::readFrames,
Qt::QueuedConnection);
// Notify UI of received data
Q_EMIT dataReceived(data);
}
/**
@ -264,6 +262,10 @@ void IO::FrameReader::readFrames()
if (m_frameDetectionMode == SerialStudio::EndDelimiterOnly)
readEndDelimetedFrames();
// Read using only a start delimeter
else if (m_frameDetectionMode == SerialStudio::StartDelimiterOnly)
readStartDelimitedFrames();
// Read using both a start & end delimiter
else if (m_frameDetectionMode == SerialStudio::StartAndEndDelimiter)
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
* buffer.

View File

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

View File

@ -2090,7 +2090,8 @@ void JSON::ProjectModel::buildProjectModel()
frameDetection->setEditable(true);
frameDetection->setData(ComboBox, WidgetType);
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(kProjectView_FrameDetection, ParameterType);
frameDetection->setData(tr("Strategy used for identifying frame data"),
@ -2098,7 +2099,8 @@ void JSON::ProjectModel::buildProjectModel()
m_projectModel->appendRow(frameDetection);
// Add frame start sequence
if (m_frameDetection == SerialStudio::StartAndEndDelimiter)
if (m_frameDetection == SerialStudio::StartAndEndDelimiter
|| m_frameDetection == SerialStudio::StartDelimiterOnly)
{
auto frameStart = new QStandardItem();
frameStart->setEditable(true);
@ -2701,9 +2703,15 @@ void JSON::ProjectModel::generateComboBoxModels()
// Initialize frame detection methods
m_frameDetectionMethods.clear();
m_frameDetectionMethodsValues.clear();
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("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
m_groupWidgets.clear();
@ -2997,8 +3005,7 @@ void JSON::ProjectModel::onProjectItemChanged(QStandardItem *item)
m_frameDecoder = static_cast<SerialStudio::DecoderMethod>(value.toInt());
break;
case kProjectView_FrameDetection:
m_frameDetection
= static_cast<SerialStudio::FrameDetection>(value.toInt());
m_frameDetection = m_frameDetectionMethodsValues.at(value.toInt());
Q_EMIT frameDetectionChanged();
buildProjectModel();
break;

View File

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

View File

@ -142,6 +142,9 @@ public:
PlainText, /**< Standard decoding, interprets data as plain text. */
Hexadecimal, /**< Decodes data assuming a hexadecimal-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)
@ -153,15 +156,19 @@ public:
* of a data frame in a continuous stream. Each value represents a specific
* detection method.
*/
// clang-format off
enum FrameDetection
{
EndDelimiterOnly, /**< Detects frames based only on an end delimiter. */
StartAndEndDelimiter, /**< Detects frames based on both start and end
delimiters. */
NoDelimiters /**< Disables frame detection and processes incoming
data directly */
EndDelimiterOnly = 0x00, /**< Detects frames based only on an end delimiter. */
StartAndEndDelimiter = 0x01, /**< Detects frames based on both start and end delimiters. */
NoDelimiters = 0x02, /**< Disables frame detection and processes incoming data directly */
StartDelimiterOnly = 0x03 /**< Detects frames with only a header */
/* 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)
// clang-format on
/**
* @enum OperationMode
@ -176,6 +183,9 @@ public:
ProjectFile, /**< Builds the dashboard using a predefined project file. */
DeviceSendsJSON, /**< Builds the dashboard from device-sent JSON. */
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)