Files
XYParser/XYParser/XYParserTests/Tests.cpp
2026-06-09 16:12:42 +08:00

2135 lines
89 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// XYParser API 单元测试文件
// 测试 XYParser 库的核心功能,包括解析器创建、错误处理、帧解析等
#include <gtest/gtest.h>
#include "../XYParserApi.h"
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <cmath>
#include <string>
#include <vector>
// 匿名命名空间,包含测试辅助代码
namespace {
/// ParserGuard 类RAII 封装,确保解析器资源自动释放
/// 当对象生命周期结束时自动调用 XYParser_DestroyParser 释放资源
class ParserGuard {
public:
/// 构造函数,接管解析器句柄的所有权
/// @param handle XYParser 解析器句柄
explicit ParserGuard(XYParserHandle handle) : handle_(handle) {}
/// 析构函数,自动释放解析器资源
~ParserGuard()
{
if (handle_ != nullptr) {
XYParser_DestroyParser(handle_);
}
}
/// 获取解析器句柄
/// @return XYParser 解析器句柄
XYParserHandle get() const
{
return handle_;
}
private:
XYParserHandle handle_; ///< 解析器句柄
};
/// 构建最小帧数据的辅助函数
/// 生成符合 XYParser 协议格式的测试帧数据
/// @param channel_count 通道数量
/// @param frame_index 帧索引,小端写入标签区前 4 字节
/// @return 包含完整帧数据的字节向量
std::vector<std::uint8_t> BuildMinimalFrame(std::uint8_t channel_count,
std::uint32_t frame_index,
std::array<std::uint8_t, 6> reserved = {})
{
constexpr std::size_t kSamplesPerFrame = 5; ///< 每帧采样数
constexpr std::uint8_t kHeader = 0xAA; ///< 帧头标记
constexpr std::uint8_t kTail = 0x55; ///< 帧尾标记
constexpr std::size_t kTagLen = 25; ///< 标签长度
// 计算帧结构大小
const std::size_t sample_bytes = static_cast<std::size_t>(channel_count) * 3 + 2;
const std::uint16_t payload_length = static_cast<std::uint16_t>(sample_bytes * kSamplesPerFrame);
const std::size_t frame_size = 1 + kTagLen + payload_length + 2;
std::vector<std::uint8_t> frame(frame_size, 0);
std::size_t offset = 0;
// 写入帧头
frame[offset++] = kHeader;
// 写入标签数据中的帧索引(小端序)
frame[offset++] = static_cast<std::uint8_t>(frame_index & 0xFF);
frame[offset++] = static_cast<std::uint8_t>((frame_index >> 8) & 0xFF);
frame[offset++] = static_cast<std::uint8_t>((frame_index >> 16) & 0xFF);
frame[offset++] = static_cast<std::uint8_t>((frame_index >> 24) & 0xFF);
// 写入负载长度(大端序)
frame[offset++] = static_cast<std::uint8_t>((payload_length >> 8) & 0xFF);
frame[offset++] = static_cast<std::uint8_t>(payload_length & 0xFF);
// 写入电池电量和通道数
frame[offset++] = 95;
frame[offset++] = channel_count;
// 写入姿态、生理量和保留字段
offset += 2 + 2 + 2 + 2 + 2;
for (std::uint8_t value : reserved) {
frame[offset++] = value;
}
// 写入采样数据
for (std::size_t sample = 0; sample < kSamplesPerFrame; ++sample) {
for (std::size_t channel = 0; channel < channel_count; ++channel) {
frame[offset++] = 0x00;
frame[offset++] = 0x00;
frame[offset++] = static_cast<std::uint8_t>(sample + channel + 1);
}
// 每个采样后的额外字节
frame[offset++] = 0x00;
frame[offset++] = 0x00;
}
// 写入帧尾
frame[offset++] = 0x00;
frame[offset++] = kTail;
frame[offset++] = kTail;
return frame;
}
/// 为现有调用点保留默认 frame_index=1 的便捷重载
std::vector<std::uint8_t> BuildMinimalFrame(std::uint8_t channel_count)
{
return BuildMinimalFrame(channel_count, 1U);
}
void WriteSigned24(std::vector<std::uint8_t>& frame, std::size_t& offset, int raw_value)
{
constexpr int kMinSigned24 = -8388608;
constexpr int kMaxSigned24 = 8388607;
raw_value = std::max(kMinSigned24, std::min(kMaxSigned24, raw_value));
std::uint32_t encoded = 0;
if (raw_value < 0) {
encoded = static_cast<std::uint32_t>((1 << 24) + raw_value);
} else {
encoded = static_cast<std::uint32_t>(raw_value);
}
frame[offset++] = static_cast<std::uint8_t>((encoded >> 16) & 0xFF);
frame[offset++] = static_cast<std::uint8_t>((encoded >> 8) & 0xFF);
frame[offset++] = static_cast<std::uint8_t>(encoded & 0xFF);
}
std::vector<std::uint8_t> BuildFrameWithRawSamples(
std::uint8_t channel_count,
std::uint32_t frame_index,
const std::array<std::array<int, XYPARSER_MAX_CHANNELS>, XYPARSER_SAMPLES_PER_FRAME>& raw_samples,
std::array<std::uint8_t, 6> reserved = {})
{
constexpr std::uint8_t kHeader = 0xAA;
constexpr std::uint8_t kTail = 0x55;
constexpr std::size_t kTagLen = 25;
const std::size_t sample_bytes = static_cast<std::size_t>(channel_count) * 3 + 2;
const std::uint16_t payload_length = static_cast<std::uint16_t>(sample_bytes * XYPARSER_SAMPLES_PER_FRAME);
const std::size_t frame_size = 1 + kTagLen + payload_length + 2;
std::vector<std::uint8_t> frame(frame_size, 0);
std::size_t offset = 0;
frame[offset++] = kHeader;
frame[offset++] = static_cast<std::uint8_t>(frame_index & 0xFF);
frame[offset++] = static_cast<std::uint8_t>((frame_index >> 8) & 0xFF);
frame[offset++] = static_cast<std::uint8_t>((frame_index >> 16) & 0xFF);
frame[offset++] = static_cast<std::uint8_t>((frame_index >> 24) & 0xFF);
frame[offset++] = static_cast<std::uint8_t>((payload_length >> 8) & 0xFF);
frame[offset++] = static_cast<std::uint8_t>(payload_length & 0xFF);
frame[offset++] = 95;
frame[offset++] = channel_count;
offset += 2 + 2 + 2 + 2 + 2;
for (std::uint8_t value : reserved) {
frame[offset++] = value;
}
for (std::size_t sample = 0; sample < XYPARSER_SAMPLES_PER_FRAME; ++sample) {
for (std::size_t channel = 0; channel < channel_count; ++channel) {
WriteSigned24(frame, offset, raw_samples[sample][channel]);
}
frame[offset++] = 0x00;
frame[offset++] = 0x00;
}
frame[offset++] = 0x00;
frame[offset++] = kTail;
frame[offset++] = kTail;
return frame;
}
int BuildSineRawValue(int sample_index, int sample_rate, double frequency_hz, double amplitude)
{
const double angle = 2.0 * 3.14159265358979323846 * frequency_hz *
static_cast<double>(sample_index) / static_cast<double>(sample_rate);
return static_cast<int>(std::lround(std::sin(angle) * amplitude));
}
int BuildCombinedSineRawValue(int sample_index,
int sample_rate,
double primary_frequency_hz,
double primary_amplitude,
double secondary_frequency_hz = 0.0,
double secondary_amplitude = 0.0)
{
return BuildSineRawValue(sample_index, sample_rate, primary_frequency_hz, primary_amplitude) +
BuildSineRawValue(sample_index, sample_rate, secondary_frequency_hz, secondary_amplitude);
}
std::vector<double> BuildAlgorithmDataForSingleChannel(int sample_rate,
double primary_frequency_hz,
double primary_amplitude,
double secondary_frequency_hz = 0.0,
double secondary_amplitude = 0.0)
{
std::vector<double> algorithm_data(
static_cast<std::size_t>(sample_rate * XYPARSER_FRAME_DATA_COLUMN_COUNT), 0.0);
for (int sample = 0; sample < sample_rate; ++sample) {
const std::size_t sample_offset = static_cast<std::size_t>(sample) * XYPARSER_FRAME_DATA_COLUMN_COUNT;
algorithm_data[sample_offset] = static_cast<double>(BuildCombinedSineRawValue(sample,
sample_rate,
primary_frequency_hz,
primary_amplitude,
secondary_frequency_hz,
secondary_amplitude));
}
return algorithm_data;
}
std::vector<double> BuildAlgorithmDataForTwoChannels(int sample_rate,
double channel0_frequency_hz,
double channel0_amplitude,
double channel1_frequency_hz,
double channel1_amplitude)
{
std::vector<double> algorithm_data(
static_cast<std::size_t>(sample_rate * XYPARSER_FRAME_DATA_COLUMN_COUNT), 0.0);
for (int sample = 0; sample < sample_rate; ++sample) {
const std::size_t sample_offset = static_cast<std::size_t>(sample) * XYPARSER_FRAME_DATA_COLUMN_COUNT;
algorithm_data[sample_offset] = static_cast<double>(
BuildSineRawValue(sample, sample_rate, channel0_frequency_hz, channel0_amplitude));
algorithm_data[sample_offset + 1] = static_cast<double>(
BuildSineRawValue(sample, sample_rate, channel1_frequency_hz, channel1_amplitude));
}
return algorithm_data;
}
std::vector<std::vector<std::uint8_t>> BuildFrameSequenceForSingleChannel(std::uint8_t channel_count,
int sample_rate,
double primary_frequency_hz,
double primary_amplitude,
double secondary_frequency_hz = 0.0,
double secondary_amplitude = 0.0)
{
const int frame_count = sample_rate / static_cast<int>(XYPARSER_SAMPLES_PER_FRAME);
std::vector<std::vector<std::uint8_t>> frames;
frames.reserve(static_cast<std::size_t>(frame_count));
for (int frame_index = 0; frame_index < frame_count; ++frame_index) {
std::array<std::array<int, XYPARSER_MAX_CHANNELS>, XYPARSER_SAMPLES_PER_FRAME> raw_samples{};
for (int sample_offset = 0; sample_offset < static_cast<int>(XYPARSER_SAMPLES_PER_FRAME); ++sample_offset) {
const int sample_index = frame_index * static_cast<int>(XYPARSER_SAMPLES_PER_FRAME) + sample_offset;
raw_samples[static_cast<std::size_t>(sample_offset)][0] =
BuildCombinedSineRawValue(sample_index,
sample_rate,
primary_frequency_hz,
primary_amplitude,
secondary_frequency_hz,
secondary_amplitude);
}
frames.push_back(BuildFrameWithRawSamples(
channel_count, static_cast<std::uint32_t>(frame_index + 1), raw_samples));
}
return frames;
}
std::vector<std::vector<std::uint8_t>> BuildFrameSequenceForTwoChannels(std::uint8_t channel_count,
int sample_rate,
double channel0_frequency_hz,
double channel0_amplitude,
double channel1_frequency_hz,
double channel1_amplitude)
{
const int frame_count = sample_rate / static_cast<int>(XYPARSER_SAMPLES_PER_FRAME);
std::vector<std::vector<std::uint8_t>> frames;
frames.reserve(static_cast<std::size_t>(frame_count));
for (int frame_index = 0; frame_index < frame_count; ++frame_index) {
std::array<std::array<int, XYPARSER_MAX_CHANNELS>, XYPARSER_SAMPLES_PER_FRAME> raw_samples{};
for (int sample_offset = 0; sample_offset < static_cast<int>(XYPARSER_SAMPLES_PER_FRAME); ++sample_offset) {
const int sample_index = frame_index * static_cast<int>(XYPARSER_SAMPLES_PER_FRAME) + sample_offset;
raw_samples[static_cast<std::size_t>(sample_offset)][0] =
BuildSineRawValue(sample_index, sample_rate, channel0_frequency_hz, channel0_amplitude);
raw_samples[static_cast<std::size_t>(sample_offset)][1] =
BuildSineRawValue(sample_index, sample_rate, channel1_frequency_hz, channel1_amplitude);
}
frames.push_back(BuildFrameWithRawSamples(
channel_count, static_cast<std::uint32_t>(frame_index + 1), raw_samples));
}
return frames;
}
int FindFrequencyIndex(const XYParserWelchSummary& summary, double target_frequency_hz)
{
for (std::uint32_t index = 0; index < summary.frequency_count; ++index) {
if (std::abs(summary.frequencies[index] - target_frequency_hz) < 1e-9) {
return static_cast<int>(index);
}
}
return -1;
}
int FindPeakPsdIndex(const XYParserWelchSummary& summary, XYParserLeadChannelNumber lead)
{
if (summary.frequency_count == 0) {
return -1;
}
const std::size_t lead_index = static_cast<std::size_t>(lead);
std::uint32_t peak_index = 0;
double peak_value = summary.psd_values[lead_index][0];
for (std::uint32_t index = 1; index < summary.frequency_count; ++index) {
if (summary.psd_values[lead_index][index] > peak_value) {
peak_value = summary.psd_values[lead_index][index];
peak_index = index;
}
}
return static_cast<int>(peak_index);
}
double ExpectedBinCenteredHanningPeakPsd(double peak_amplitude_uv)
{
// For a 1-second window where sample_rate == n_per_seg, the periodic Hann window used by
// the implementation yields a one-sided PSD peak of A^2 / 3 for a bin-centered sine wave.
return (peak_amplitude_uv * peak_amplitude_uv) / 3.0;
}
std::uint16_t MeasureLeadImpedanceForMixedSine(std::uint8_t channel_count,
XYParserLeadChannelNumber lead,
int sample_rate,
double primary_frequency_hz,
double primary_amplitude,
double secondary_frequency_hz,
double secondary_amplitude);
std::uint16_t MeasureLeadImpedanceForSine(std::uint8_t channel_count,
XYParserLeadChannelNumber lead,
int sample_rate,
double frequency_hz,
double amplitude)
{
return MeasureLeadImpedanceForMixedSine(
channel_count, lead, sample_rate, frequency_hz, amplitude, 0.0, 0.0);
}
std::uint16_t MeasureLeadImpedanceForMixedSine(std::uint8_t channel_count,
XYParserLeadChannelNumber lead,
int sample_rate,
double primary_frequency_hz,
double primary_amplitude,
double secondary_frequency_hz,
double secondary_amplitude)
{
ParserGuard parser(XYParser_CreateParser(channel_count));
if (parser.get() == nullptr) {
ADD_FAILURE() << "failed to create parser";
return 0;
}
XYParser_SetBypassChecksum(parser.get(), 1);
XYParser_SetSampleRate(parser.get(), sample_rate);
XYParser_SetImpedanceDetection(parser.get(), 1);
const auto frames = BuildFrameSequenceForSingleChannel(channel_count,
sample_rate,
primary_frequency_hz,
primary_amplitude,
secondary_frequency_hz,
secondary_amplitude);
std::array<XYParserFrameSummary, 1> summaries{};
for (const auto& frame : frames) {
if (XYParser_Feed(parser.get(),
frame.data(),
frame.size(),
summaries.data(),
static_cast<int>(summaries.size())) != 1) {
ADD_FAILURE() << "failed to feed frame for impedance measurement";
return 0;
}
}
std::array<XYParserImpedanceSummary, 1> impedance{};
if (XYParser_ReadImpedance(parser.get(), impedance.data(), static_cast<int>(impedance.size())) != 1) {
ADD_FAILURE() << "failed to read impedance summary";
return 0;
}
return impedance[0].impedance_values[static_cast<std::size_t>(lead)];
}
} // namespace
/// 测试:创建解析器时拒绝不支持的通道数
TEST(XYParserApiTests, CreateParserRejectsUnsupportedChannelCount)
{
// 7 通道是不支持的配置,应返回 nullptr
EXPECT_EQ(XYParser_CreateParser(7), nullptr);
}
/// 测试:对空解析器句柄调用 GetLastError 应返回正确错误信息
TEST(XYParserApiTests, GetLastErrorReturnsMessageForNullParser)
{
// 传入 nullptr 应返回 "invalid parser handle"
EXPECT_EQ(std::string(XYParser_GetLastError(nullptr)), std::string("invalid parser handle"));
}
TEST(XYParserApiTests, Serialize8ChImpedanceOpenCommandMatchesWirelessEegPacket)
{
constexpr std::size_t kCommandSize = 7;
std::array<std::uint8_t, kCommandSize> command{};
const int command_size = XYParser_Serialize8ChImpedanceCommand(1, command.data(), command.size());
ASSERT_EQ(command_size, static_cast<int>(kCommandSize));
const std::array<std::uint8_t, kCommandSize> expected = {
0xAA, 0x00, 0x01, 0xA1, 0xA1, 0x55, 0x55};
EXPECT_EQ(command, expected);
}
TEST(XYParserApiTests, Serialize8ChImpedanceCloseCommandMatchesWirelessEegPacket)
{
constexpr std::size_t kCommandSize = 7;
std::array<std::uint8_t, kCommandSize> command{};
const int command_size = XYParser_Serialize8ChImpedanceCommand(0, command.data(), command.size());
ASSERT_EQ(command_size, static_cast<int>(kCommandSize));
const std::array<std::uint8_t, kCommandSize> expected = {
0xAA, 0x00, 0x01, 0xA0, 0xA0, 0x55, 0x55};
EXPECT_EQ(command, expected);
}
TEST(XYParserApiTests, Serialize8ChImpedanceCommandRejectsSmallBuffer)
{
std::array<std::uint8_t, 6> command{};
EXPECT_EQ(XYParser_Serialize8ChImpedanceCommand(1, command.data(), command.size()), 0);
}
TEST(XYParserApiTests, Get8ChImpedanceCommandSizeMatchesSerializedLength)
{
EXPECT_EQ(XYParser_Get8ChImpedanceCommandSize(), static_cast<std::size_t>(7));
}
TEST(XYParserApiTests, SerializeTriggerCommandMatchesWirelessEegPacket)
{
std::array<std::uint8_t, 8> command{};
const int command_size = XYParser_SerializeTriggerCommand(
XYPARSER_TRIGGER_TRAIN_0,
command.data(),
command.size());
ASSERT_EQ(command_size, static_cast<int>(XYParser_GetTriggerCommandSize()));
const std::array<std::uint8_t, 7> expected = {0xAA, 0x00, 0x00, 0xBB, 0xBB, 0x55, 0x55};
EXPECT_TRUE(std::equal(expected.begin(), expected.end(), command.begin()));
}
TEST(XYParserApiTests, SerializeTrain1TriggerCommandMatchesWirelessEegPacket)
{
std::array<std::uint8_t, 8> command{};
const int command_size = XYParser_SerializeTriggerCommand(
XYPARSER_TRIGGER_TRAIN_1,
command.data(),
command.size());
ASSERT_EQ(command_size, static_cast<int>(XYParser_GetTriggerCommandSize()));
const std::array<std::uint8_t, 7> expected = {0xAA, 0x00, 0x00, 0xBC, 0xBC, 0x55, 0x55};
EXPECT_TRUE(std::equal(expected.begin(), expected.end(), command.begin()));
}
TEST(XYParserApiTests, SerializeTriggerCommandRejectsUnsupportedTriggerType)
{
std::array<std::uint8_t, 8> command{};
EXPECT_EQ(XYParser_SerializeTriggerCommand(0xAA, command.data(), command.size()), 0);
}
TEST(XYParserApiTests, SerializeTriggerCommandRejectsSmallBuffer)
{
std::array<std::uint8_t, 6> command{};
EXPECT_EQ(XYParser_SerializeTriggerCommand(XYPARSER_TRIGGER_TRAIN_1, command.data(), command.size()), 0);
}
TEST(XYParserApiTests, GetAlgorithmDataValueCountMatchesFrameLayout)
{
EXPECT_EQ(XYParser_GetAlgorithmDataValueCount(),
static_cast<std::size_t>(XYPARSER_FRAME_ALGORITHM_VALUE_COUNT));
}
TEST(XYParserApiTests, GetLeadMapCountReturnsExpectedCounts)
{
EXPECT_EQ(XYParser_GetLeadMapCount(8), 8);
EXPECT_EQ(XYParser_GetLeadMapCount(64), 64);
EXPECT_EQ(XYParser_GetLeadMapCount(7), 0);
}
TEST(XYParserApiTests, GetLeadMapReturnsExpected8ChannelSubset)
{
std::array<XYParserLeadChannelNumber, 8> leads{};
const int count = XYParser_GetLeadMap(8, leads.data(), static_cast<int>(leads.size()));
ASSERT_EQ(count, 8);
const std::array<XYParserLeadChannelNumber, 8> expected = {
LeadChannel_PO5, LeadChannel_POZ, LeadChannel_PO6, LeadChannel_PO7,
LeadChannel_O1, LeadChannel_OZ, LeadChannel_O2, LeadChannel_PO8};
EXPECT_EQ(leads, expected);
}
TEST(XYParserApiTests, GetLeadMapReturnsExpected64ChannelOrder)
{
std::array<XYParserLeadChannelNumber, 64> leads{};
const int count = XYParser_GetLeadMap(64, leads.data(), static_cast<int>(leads.size()));
ASSERT_EQ(count, 64);
EXPECT_EQ(leads.front(), LeadChannel_FP1);
EXPECT_EQ(leads[1], LeadChannel_FP2);
EXPECT_EQ(leads[13], LeadChannel_PO5);
EXPECT_EQ(leads[55], LeadChannel_PO7);
EXPECT_EQ(leads.back(), LeadChannel_F1);
}
TEST(XYParserApiTests, GetLeadMapRejectsUnsupportedChannelCountOrSmallBuffer)
{
std::array<XYParserLeadChannelNumber, 7> small_buffer{};
EXPECT_EQ(XYParser_GetLeadMap(8, small_buffer.data(), static_cast<int>(small_buffer.size())), 0);
EXPECT_EQ(XYParser_GetLeadMap(7, nullptr, 0), 0);
}
TEST(XYParserApiTests, Convert8ChFramesTo64ChMapsKnownLeadsAndPadsOthersWithZero)
{
std::array<XYParserFrameSummary, 2> input{};
input[0].frame_index = 7U;
input[0].channel_count = 8U;
input[0].battery = 88U;
input[0].sample_count = 5U;
input[0].impedance_enabled = 1U;
input[0].current_gain = 24U;
input[0].current_sample_rate_hz = 1000U;
input[0].cap_type = 7U;
input[0].gnd_detached = 1U;
input[0].sample_trigger_types[0] = 0xB2;
input[0].sample_trigger_indices[0] = 3U;
input[0].channel_values_uv[0][0] = 11.0;
input[0].channel_values_uv[0][1] = 12.0;
input[0].channel_values_uv[0][2] = 13.0;
input[0].channel_values_uv[0][3] = 14.0;
input[0].channel_values_uv[0][4] = 15.0;
input[0].channel_values_uv[0][5] = 16.0;
input[0].channel_values_uv[0][6] = 17.0;
input[0].channel_values_uv[0][7] = 18.0;
input[1].frame_index = 8U;
input[1].channel_count = 8U;
input[1].battery = 77U;
input[1].sample_count = 5U;
input[1].channel_values_uv[1][0] = 21.0;
std::array<XYParserFrameSummary, 2> output{};
ASSERT_EQ(XYParser_Convert8ChFramesTo64Ch(input.data(), static_cast<int>(input.size()), output.data(), static_cast<int>(output.size())), 2);
EXPECT_EQ(output[0].frame_index, 7U);
EXPECT_EQ(output[0].channel_count, 64U);
EXPECT_EQ(output[0].battery, 88U);
EXPECT_EQ(output[0].sample_count, 5U);
EXPECT_EQ(output[0].impedance_enabled, 1U);
EXPECT_EQ(output[0].current_gain, 24U);
EXPECT_EQ(output[0].current_sample_rate_hz, 1000U);
EXPECT_EQ(output[0].cap_type, 7U);
EXPECT_EQ(output[0].gnd_detached, 1U);
EXPECT_EQ(output[0].sample_trigger_types[0], 0xB2);
EXPECT_EQ(output[0].sample_trigger_indices[0], 3U);
EXPECT_DOUBLE_EQ(output[0].channel_values_uv[0][LeadChannel_PO5], 11.0);
EXPECT_DOUBLE_EQ(output[0].channel_values_uv[0][LeadChannel_POZ], 12.0);
EXPECT_DOUBLE_EQ(output[0].channel_values_uv[0][LeadChannel_PO6], 13.0);
EXPECT_DOUBLE_EQ(output[0].channel_values_uv[0][LeadChannel_PO7], 14.0);
EXPECT_DOUBLE_EQ(output[0].channel_values_uv[0][LeadChannel_O1], 15.0);
EXPECT_DOUBLE_EQ(output[0].channel_values_uv[0][LeadChannel_OZ], 16.0);
EXPECT_DOUBLE_EQ(output[0].channel_values_uv[0][LeadChannel_O2], 17.0);
EXPECT_DOUBLE_EQ(output[0].channel_values_uv[0][LeadChannel_PO8], 18.0);
EXPECT_DOUBLE_EQ(output[0].channel_values_uv[0][LeadChannel_FP1], 0.0);
EXPECT_DOUBLE_EQ(output[0].channel_values_uv[0][LeadChannel_CZ], 0.0);
EXPECT_EQ(output[1].frame_index, 8U);
EXPECT_EQ(output[1].channel_count, 64U);
EXPECT_DOUBLE_EQ(output[1].channel_values_uv[1][LeadChannel_PO5], 21.0);
}
TEST(XYParserApiTests, Convert8ChFramesTo64ChRejectsInvalidArgumentsAndStopsAtNon8ChannelInput)
{
std::array<XYParserFrameSummary, 2> input{};
input[0].channel_count = 8U;
input[1].channel_count = 64U;
std::array<XYParserFrameSummary, 2> output{};
EXPECT_EQ(XYParser_Convert8ChFramesTo64Ch(nullptr, 1, output.data(), static_cast<int>(output.size())), 0);
EXPECT_EQ(XYParser_Convert8ChFramesTo64Ch(input.data(), 1, nullptr, static_cast<int>(output.size())), 0);
EXPECT_EQ(XYParser_Convert8ChFramesTo64Ch(input.data(), 0, output.data(), static_cast<int>(output.size())), 0);
EXPECT_EQ(XYParser_Convert8ChFramesTo64Ch(input.data(), static_cast<int>(input.size()), output.data(), static_cast<int>(output.size())), 1);
EXPECT_EQ(output[0].channel_count, 64U);
}
TEST(XYParserApiTests, ConvertSampleFramesToAlgorithmDataAppendsTriggerColumnsPerSample)
{
constexpr std::size_t kFrameValueCount = XYPARSER_FRAME_ALGORITHM_VALUE_COUNT;
XYParserFrameSummary input{};
input.channel_count = 64U;
input.sample_count = 5U;
input.channel_values_uv[0][0] = 101.0;
input.channel_values_uv[0][63] = 164.0;
input.sample_trigger_types[0] = 0xA5;
input.sample_trigger_indices[0] = 9U;
input.channel_values_uv[1][10] = 210.0;
input.sample_trigger_types[1] = 0x7F;
input.sample_trigger_indices[1] = 5U;
input.channel_values_uv[4][3] = 403.0;
input.sample_trigger_types[4] = 0xB2;
input.sample_trigger_indices[4] = 11U;
std::array<double, kFrameValueCount> output{};
ASSERT_EQ(XYParser_ConvertSampleFramesToAlgorithmData(&input, output.data()), 1);
const std::size_t sample0 = 0;
const std::size_t sample1 = XYPARSER_FRAME_DATA_COLUMN_COUNT;
const std::size_t sample4 = 4 * XYPARSER_FRAME_DATA_COLUMN_COUNT;
EXPECT_DOUBLE_EQ(output[sample0 + 0], 101.0);
EXPECT_DOUBLE_EQ(output[sample0 + 63], 164.0);
EXPECT_DOUBLE_EQ(output[sample0 + XYPARSER_FRAME_DATA_TRIGGER_TYPE_INDEX], 165.0);
EXPECT_DOUBLE_EQ(output[sample0 + XYPARSER_FRAME_DATA_TRIGGER_INDEX_INDEX], 9.0);
EXPECT_DOUBLE_EQ(output[sample4 + 3], 403.0);
EXPECT_DOUBLE_EQ(output[sample4 + XYPARSER_FRAME_DATA_TRIGGER_TYPE_INDEX], 178.0);
EXPECT_DOUBLE_EQ(output[sample4 + XYPARSER_FRAME_DATA_TRIGGER_INDEX_INDEX], 11.0);
EXPECT_DOUBLE_EQ(output[sample1 + 10], 210.0);
EXPECT_DOUBLE_EQ(output[sample1 + XYPARSER_FRAME_DATA_TRIGGER_TYPE_INDEX], 127.0);
EXPECT_DOUBLE_EQ(output[sample1 + XYPARSER_FRAME_DATA_TRIGGER_INDEX_INDEX], 5.0);
}
TEST(XYParserApiTests, ConvertSampleFramesToAlgorithmDataRejectsInvalidArgumentsAndNon64ChannelInput)
{
constexpr std::size_t kFrameValueCount = XYPARSER_FRAME_ALGORITHM_VALUE_COUNT;
XYParserFrameSummary input{};
input.channel_count = 8U;
std::array<double, kFrameValueCount> output{};
EXPECT_EQ(XYParser_ConvertSampleFramesToAlgorithmData(nullptr, output.data()), 0);
EXPECT_EQ(XYParser_ConvertSampleFramesToAlgorithmData(&input, nullptr), 0);
EXPECT_EQ(XYParser_ConvertSampleFramesToAlgorithmData(&input, output.data()), 0);
EXPECT_DOUBLE_EQ(output[XYPARSER_FRAME_DATA_TRIGGER_TYPE_INDEX], 0.0);
}
TEST(XYParserApiTests, FeedAlgorithmDataCachesSamplesBuildsFramesAndFlushesTailSamples)
{
ParserGuard parser(XYParser_CreateParser(64));
ASSERT_NE(parser.get(), nullptr);
constexpr int kTotalSamples = 7;
constexpr int kColumnCount = XYPARSER_FRAME_DATA_COLUMN_COUNT;
std::array<double, static_cast<std::size_t>(kTotalSamples) * kColumnCount> input_data{};
for (int sample_index = 0; sample_index < kTotalSamples; ++sample_index) {
const std::size_t row_offset = static_cast<std::size_t>(sample_index) * kColumnCount;
input_data[row_offset + 0] = 1000.0 + sample_index;
input_data[row_offset + 10] = 2000.0 + sample_index;
input_data[row_offset + 63] = 3000.0 + sample_index;
input_data[row_offset + XYPARSER_FRAME_DATA_TRIGGER_TYPE_INDEX] = 10.0 + sample_index;
input_data[row_offset + XYPARSER_FRAME_DATA_TRIGGER_INDEX_INDEX] = 20.0 + sample_index;
}
const std::uint8_t* input_bytes = reinterpret_cast<const std::uint8_t*>(input_data.data());
const std::size_t first_chunk_size = 3 * static_cast<std::size_t>(kColumnCount) * sizeof(double) + sizeof(double);
const std::size_t second_chunk_size = sizeof(input_data) - first_chunk_size;
std::array<XYParserFrameSummary, 1> summaries{};
EXPECT_EQ(XYParser_FeedAlgorithmData(
parser.get(),
input_bytes,
first_chunk_size,
summaries.data(),
static_cast<int>(summaries.size())),
0);
ASSERT_EQ(XYParser_FeedAlgorithmData(
parser.get(),
input_bytes + first_chunk_size,
second_chunk_size,
summaries.data(),
static_cast<int>(summaries.size())),
1);
EXPECT_EQ(summaries[0].frame_index, 1U);
EXPECT_EQ(summaries[0].channel_count, 64U);
EXPECT_EQ(summaries[0].sample_count, 5U);
EXPECT_DOUBLE_EQ(summaries[0].channel_values_uv[0][0], 1000.0);
EXPECT_DOUBLE_EQ(summaries[0].channel_values_uv[2][10], 2002.0);
EXPECT_DOUBLE_EQ(summaries[0].channel_values_uv[4][63], 3004.0);
EXPECT_EQ(summaries[0].sample_trigger_types[0], 10U);
EXPECT_EQ(summaries[0].sample_trigger_indices[4], 24U);
XYParserFrameSummary tail_summary{};
ASSERT_EQ(XYParser_FlushAlgorithmData(parser.get(), &tail_summary), 1);
EXPECT_EQ(tail_summary.frame_index, 2U);
EXPECT_EQ(tail_summary.channel_count, 64U);
EXPECT_EQ(tail_summary.sample_count, 2U);
EXPECT_DOUBLE_EQ(tail_summary.channel_values_uv[0][0], 1005.0);
EXPECT_DOUBLE_EQ(tail_summary.channel_values_uv[1][63], 3006.0);
EXPECT_EQ(tail_summary.sample_trigger_types[0], 15U);
EXPECT_EQ(tail_summary.sample_trigger_indices[1], 26U);
EXPECT_DOUBLE_EQ(tail_summary.channel_values_uv[2][0], 0.0);
EXPECT_EQ(tail_summary.sample_trigger_types[2], 0U);
XYParser_ResetAlgorithmDataCache(parser.get());
EXPECT_EQ(XYParser_FlushAlgorithmData(parser.get(), &tail_summary), 0);
}
TEST(XYParserApiTests, GetLeadNameReturnsExpectedNames)
{
EXPECT_EQ(std::string(XYParser_GetLeadName(LeadChannel_FP1)), "FP1");
EXPECT_EQ(std::string(XYParser_GetLeadName(LeadChannel_PO5)), "PO5");
EXPECT_EQ(std::string(XYParser_GetLeadName(LeadChannel_OZ)), "OZ");
EXPECT_EQ(std::string(XYParser_GetLeadName(LeadChannel_GND)), "GND");
}
TEST(XYParserApiTests, GetLeadNameCovers8ChannelSubsetNames)
{
EXPECT_EQ(std::string(XYParser_GetLeadName(LeadChannel_POZ)), "POZ");
EXPECT_EQ(std::string(XYParser_GetLeadName(LeadChannel_PO6)), "PO6");
EXPECT_EQ(std::string(XYParser_GetLeadName(LeadChannel_PO7)), "PO7");
EXPECT_EQ(std::string(XYParser_GetLeadName(LeadChannel_PO8)), "PO8");
EXPECT_EQ(std::string(XYParser_GetLeadName(LeadChannel_O1)), "O1");
EXPECT_EQ(std::string(XYParser_GetLeadName(LeadChannel_O2)), "O2");
}
TEST(XYParserApiTests, GetLeadNameReturnsEmptyStringForInvalidLead)
{
EXPECT_EQ(std::string(XYParser_GetLeadName(static_cast<XYParserLeadChannelNumber>(255))), "");
}
TEST(XYParserApiTests, GetWelchBandNameReturnsExpectedNames)
{
EXPECT_EQ(std::string(XYParser_GetWelchBandName(0)), "delta");
EXPECT_EQ(std::string(XYParser_GetWelchBandName(1)), "theta");
EXPECT_EQ(std::string(XYParser_GetWelchBandName(2)), "alpha");
EXPECT_EQ(std::string(XYParser_GetWelchBandName(3)), "beta");
EXPECT_EQ(std::string(XYParser_GetWelchBandName(4)), "gamma");
}
TEST(XYParserApiTests, GetWelchBandNameReturnsEmptyStringForInvalidIndex)
{
EXPECT_EQ(std::string(XYParser_GetWelchBandName(-1)), "");
EXPECT_EQ(std::string(XYParser_GetWelchBandName(XYPARSER_WELCH_BAND_COUNT)), "");
}
TEST(XYParserApiTests, GetWelchBandRangeReturnsExpectedRanges)
{
double low_hz = 0.0;
double high_hz = 0.0;
ASSERT_EQ(XYParser_GetWelchBandRange(0, &low_hz, &high_hz), 1);
EXPECT_DOUBLE_EQ(low_hz, 0.5);
EXPECT_DOUBLE_EQ(high_hz, 4.0);
ASSERT_EQ(XYParser_GetWelchBandRange(4, &low_hz, &high_hz), 1);
EXPECT_DOUBLE_EQ(low_hz, 30.0);
EXPECT_DOUBLE_EQ(high_hz, 50.0);
}
TEST(XYParserApiTests, GetWelchBandRangeRejectsInvalidArguments)
{
double low_hz = 0.0;
double high_hz = 0.0;
EXPECT_EQ(XYParser_GetWelchBandRange(-1, &low_hz, &high_hz), 0);
EXPECT_EQ(XYParser_GetWelchBandRange(XYPARSER_WELCH_BAND_COUNT, &low_hz, &high_hz), 0);
EXPECT_EQ(XYParser_GetWelchBandRange(0, nullptr, &high_hz), 0);
EXPECT_EQ(XYParser_GetWelchBandRange(0, &low_hz, nullptr), 0);
}
TEST(XYParserApiTests, SetImpedanceDetectionSwitchesParserGainBetween24And6)
{
ParserGuard parser(XYParser_CreateParser(8));
ASSERT_NE(parser.get(), nullptr);
XYParser_SetAdcParams(parser.get(), 4.5, 6.0);
XYParser_SetBypassChecksum(parser.get(), 1);
const std::vector<std::uint8_t> frame1 = BuildMinimalFrame(8, 1U);
const std::vector<std::uint8_t> frame2 = BuildMinimalFrame(8, 2U);
const std::vector<std::uint8_t> frame3 = BuildMinimalFrame(8, 3U);
std::array<XYParserFrameSummary, 1> summaries{};
ASSERT_EQ(XYParser_Feed(parser.get(), frame1.data(), frame1.size(), summaries.data(), static_cast<int>(summaries.size())), 1);
const double gain6_value_before = summaries[0].channel_values_uv[0][0];
XYParser_SetImpedanceDetection(parser.get(), 1);
ASSERT_EQ(XYParser_Feed(parser.get(), frame2.data(), frame2.size(), summaries.data(), static_cast<int>(summaries.size())), 1);
const double gain24_value = summaries[0].channel_values_uv[0][0];
XYParser_SetImpedanceDetection(parser.get(), 0);
ASSERT_EQ(XYParser_Feed(parser.get(), frame3.data(), frame3.size(), summaries.data(), static_cast<int>(summaries.size())), 1);
const double gain6_value_after = summaries[0].channel_values_uv[0][0];
EXPECT_NEAR(gain24_value * 4.0, gain6_value_before, 1e-9);
EXPECT_NEAR(gain6_value_after, gain6_value_before, 1e-9);
}
TEST(XYParserApiTests, ImpedanceReturnsOneResultAfterOneSecondFor8Channels)
{
ParserGuard parser(XYParser_CreateParser(8));
ASSERT_NE(parser.get(), nullptr);
XYParser_SetBypassChecksum(parser.get(), 1);
XYParser_SetSampleRate(parser.get(), 10);
XYParser_SetImpedanceDetection(parser.get(), 1);
std::array<std::array<int, XYPARSER_MAX_CHANNELS>, XYPARSER_SAMPLES_PER_FRAME> raw_samples1{};
std::array<std::array<int, XYPARSER_MAX_CHANNELS>, XYPARSER_SAMPLES_PER_FRAME> raw_samples2{};
for (int sample = 0; sample < 10; ++sample) {
const int raw_value = BuildSineRawValue(sample, 10, 2.0, 1000000.0);
if (sample < static_cast<int>(XYPARSER_SAMPLES_PER_FRAME)) {
raw_samples1[static_cast<std::size_t>(sample)][0] = raw_value;
} else {
raw_samples2[static_cast<std::size_t>(sample - XYPARSER_SAMPLES_PER_FRAME)][0] = raw_value;
}
}
const std::vector<std::uint8_t> frame1 = BuildFrameWithRawSamples(8, 1U, raw_samples1);
const std::vector<std::uint8_t> frame2 = BuildFrameWithRawSamples(8, 2U, raw_samples2);
std::array<XYParserFrameSummary, 2> summaries{};
std::array<XYParserImpedanceSummary, 2> impedance{};
EXPECT_EQ(XYParser_Feed(parser.get(), frame1.data(), frame1.size(), summaries.data(), static_cast<int>(summaries.size())), 1);
EXPECT_EQ(XYParser_ReadImpedance(parser.get(), impedance.data(), static_cast<int>(impedance.size())), 0);
EXPECT_EQ(XYParser_Feed(parser.get(), frame2.data(), frame2.size(), summaries.data(), static_cast<int>(summaries.size())), 1);
ASSERT_EQ(XYParser_ReadImpedance(parser.get(), impedance.data(), static_cast<int>(impedance.size())), 1);
EXPECT_EQ(impedance[0].channel_count, 8);
EXPECT_EQ(impedance[0].sample_rate, 10U);
EXPECT_EQ(impedance[0].window_sample_count, 10U);
EXPECT_GT(impedance[0].impedance_values[LeadChannel_PO5], 0);
EXPECT_EQ(impedance[0].impedance_values[LeadChannel_FP1], 0);
}
TEST(XYParserApiTests, ReadImpedanceConsumesQueuedResults)
{
ParserGuard parser(XYParser_CreateParser(8));
ASSERT_NE(parser.get(), nullptr);
XYParser_SetBypassChecksum(parser.get(), 1);
XYParser_SetSampleRate(parser.get(), 10);
XYParser_SetImpedanceDetection(parser.get(), 1);
std::array<std::array<int, XYPARSER_MAX_CHANNELS>, XYPARSER_SAMPLES_PER_FRAME> raw_samples1{};
std::array<std::array<int, XYPARSER_MAX_CHANNELS>, XYPARSER_SAMPLES_PER_FRAME> raw_samples2{};
for (int sample = 0; sample < 10; ++sample) {
const int raw_value = BuildSineRawValue(sample, 10, 2.0, 1000000.0);
if (sample < static_cast<int>(XYPARSER_SAMPLES_PER_FRAME)) {
raw_samples1[static_cast<std::size_t>(sample)][0] = raw_value;
} else {
raw_samples2[static_cast<std::size_t>(sample - XYPARSER_SAMPLES_PER_FRAME)][0] = raw_value;
}
}
const std::vector<std::uint8_t> frame1 = BuildFrameWithRawSamples(8, 1U, raw_samples1);
const std::vector<std::uint8_t> frame2 = BuildFrameWithRawSamples(8, 2U, raw_samples2);
std::array<XYParserFrameSummary, 1> summaries{};
std::array<XYParserImpedanceSummary, 1> impedance{};
ASSERT_EQ(XYParser_Feed(parser.get(), frame1.data(), frame1.size(), summaries.data(), static_cast<int>(summaries.size())), 1);
ASSERT_EQ(XYParser_Feed(parser.get(), frame2.data(), frame2.size(), summaries.data(), static_cast<int>(summaries.size())), 1);
ASSERT_EQ(XYParser_ReadImpedance(parser.get(), impedance.data(), static_cast<int>(impedance.size())), 1);
EXPECT_GT(impedance[0].impedance_values[LeadChannel_PO5], 0);
EXPECT_EQ(XYParser_ReadImpedance(parser.get(), impedance.data(), static_cast<int>(impedance.size())), 0);
}
TEST(XYParserApiTests, ImpedanceUsesUnifiedLeadIndexesFor64Channels)
{
ParserGuard parser(XYParser_CreateParser(64));
ASSERT_NE(parser.get(), nullptr);
XYParser_SetBypassChecksum(parser.get(), 1);
XYParser_SetSampleRate(parser.get(), 10);
XYParser_SetImpedanceDetection(parser.get(), 1);
std::array<std::array<int, XYPARSER_MAX_CHANNELS>, XYPARSER_SAMPLES_PER_FRAME> raw_samples1{};
std::array<std::array<int, XYPARSER_MAX_CHANNELS>, XYPARSER_SAMPLES_PER_FRAME> raw_samples2{};
for (int sample = 0; sample < 10; ++sample) {
const int raw_value = BuildSineRawValue(sample, 10, 3.0, 1000000.0);
if (sample < static_cast<int>(XYPARSER_SAMPLES_PER_FRAME)) {
raw_samples1[static_cast<std::size_t>(sample)][0] = raw_value;
} else {
raw_samples2[static_cast<std::size_t>(sample - XYPARSER_SAMPLES_PER_FRAME)][0] = raw_value;
}
}
const std::vector<std::uint8_t> frame1 = BuildFrameWithRawSamples(64, 1U, raw_samples1);
const std::vector<std::uint8_t> frame2 = BuildFrameWithRawSamples(64, 2U, raw_samples2);
std::array<XYParserFrameSummary, 2> summaries{};
std::array<XYParserImpedanceSummary, 2> impedance{};
EXPECT_EQ(XYParser_Feed(parser.get(), frame1.data(), frame1.size(), summaries.data(), static_cast<int>(summaries.size())), 1);
EXPECT_EQ(XYParser_Feed(parser.get(), frame2.data(), frame2.size(), summaries.data(), static_cast<int>(summaries.size())), 1);
ASSERT_EQ(XYParser_ReadImpedance(parser.get(), impedance.data(), static_cast<int>(impedance.size())), 1);
EXPECT_EQ(impedance[0].channel_count, 64);
EXPECT_GT(impedance[0].impedance_values[LeadChannel_FP1], 0);
EXPECT_EQ(impedance[0].impedance_values[LeadChannel_FP2], 0);
}
TEST(XYParserApiTests, ImpedanceSeparatesDifferentChannelLeadsFor64Channels)
{
ParserGuard parser(XYParser_CreateParser(64));
ASSERT_NE(parser.get(), nullptr);
XYParser_SetBypassChecksum(parser.get(), 1);
XYParser_SetSampleRate(parser.get(), 10);
XYParser_SetImpedanceDetection(parser.get(), 1);
const auto frames = BuildFrameSequenceForTwoChannels(64, 10, 3.0, 1000000.0, 2.0, 250000.0);
ASSERT_EQ(frames.size(), 2U);
std::array<XYParserFrameSummary, 1> summaries{};
std::array<XYParserImpedanceSummary, 1> impedance{};
for (const auto& frame : frames) {
ASSERT_EQ(XYParser_Feed(parser.get(),
frame.data(),
frame.size(),
summaries.data(),
static_cast<int>(summaries.size())),
1);
}
ASSERT_EQ(XYParser_ReadImpedance(parser.get(), impedance.data(), static_cast<int>(impedance.size())), 1);
EXPECT_GT(impedance[0].impedance_values[LeadChannel_FP1], 0);
EXPECT_GT(impedance[0].impedance_values[LeadChannel_FP2], 0);
EXPECT_GT(impedance[0].impedance_values[LeadChannel_FP1], impedance[0].impedance_values[LeadChannel_FP2]);
EXPECT_EQ(impedance[0].impedance_values[LeadChannel_PO6], 0);
}
TEST(XYParserApiTests, ImpedanceDisableClearsHalfWindow)
{
ParserGuard parser(XYParser_CreateParser(64));
ASSERT_NE(parser.get(), nullptr);
XYParser_SetBypassChecksum(parser.get(), 1);
XYParser_SetSampleRate(parser.get(), 10);
XYParser_SetImpedanceDetection(parser.get(), 1);
const auto full_frames = BuildFrameSequenceForSingleChannel(64, 10, 3.0, 1000000.0);
ASSERT_EQ(full_frames.size(), 2U);
std::array<XYParserFrameSummary, 1> summaries{};
std::array<XYParserImpedanceSummary, 1> impedance{};
ASSERT_EQ(XYParser_Feed(parser.get(),
full_frames[0].data(),
full_frames[0].size(),
summaries.data(),
static_cast<int>(summaries.size())),
1);
EXPECT_EQ(XYParser_ReadImpedance(parser.get(), impedance.data(), static_cast<int>(impedance.size())), 0);
XYParser_SetImpedanceDetection(parser.get(), 0);
XYParser_SetImpedanceDetection(parser.get(), 1);
ASSERT_EQ(XYParser_Feed(parser.get(),
full_frames[0].data(),
full_frames[0].size(),
summaries.data(),
static_cast<int>(summaries.size())),
1);
EXPECT_EQ(XYParser_ReadImpedance(parser.get(), impedance.data(), static_cast<int>(impedance.size())), 0);
ASSERT_EQ(XYParser_Feed(parser.get(),
full_frames[1].data(),
full_frames[1].size(),
summaries.data(),
static_cast<int>(summaries.size())),
1);
ASSERT_EQ(XYParser_ReadImpedance(parser.get(), impedance.data(), static_cast<int>(impedance.size())), 1);
EXPECT_GT(impedance[0].impedance_values[LeadChannel_FP1], 0);
}
TEST(XYParserApiTests, ImpedanceSampleRateChangeClearsHalfWindow)
{
ParserGuard parser(XYParser_CreateParser(64));
ASSERT_NE(parser.get(), nullptr);
XYParser_SetBypassChecksum(parser.get(), 1);
XYParser_SetSampleRate(parser.get(), 10);
XYParser_SetImpedanceDetection(parser.get(), 1);
const auto frames_10hz_window = BuildFrameSequenceForSingleChannel(64, 10, 3.0, 1000000.0);
ASSERT_EQ(frames_10hz_window.size(), 2U);
std::array<XYParserFrameSummary, 1> summaries{};
std::array<XYParserImpedanceSummary, 1> impedance{};
ASSERT_EQ(XYParser_Feed(parser.get(),
frames_10hz_window[0].data(),
frames_10hz_window[0].size(),
summaries.data(),
static_cast<int>(summaries.size())),
1);
EXPECT_EQ(XYParser_ReadImpedance(parser.get(), impedance.data(), static_cast<int>(impedance.size())), 0);
XYParser_SetSampleRate(parser.get(), 20);
const auto frames_20hz_window = BuildFrameSequenceForSingleChannel(64, 20, 3.0, 1000000.0);
ASSERT_EQ(frames_20hz_window.size(), 4U);
for (const auto& frame : frames_20hz_window) {
ASSERT_EQ(XYParser_Feed(parser.get(),
frame.data(),
frame.size(),
summaries.data(),
static_cast<int>(summaries.size())),
1);
}
ASSERT_EQ(XYParser_ReadImpedance(parser.get(), impedance.data(), static_cast<int>(impedance.size())), 1);
EXPECT_EQ(impedance[0].sample_rate, 20U);
EXPECT_EQ(impedance[0].window_sample_count, 20U);
EXPECT_GT(impedance[0].impedance_values[LeadChannel_FP1], 0);
}
TEST(XYParserApiTests, ImpedanceSuppresses50HzLineNoiseFor64Channels)
{
constexpr int kSampleRate = 200;
constexpr double kAmplitude = 1000000.0;
const std::uint16_t signal_10hz = MeasureLeadImpedanceForSine(
64, LeadChannel_FP1, kSampleRate, 10.0, kAmplitude);
const std::uint16_t line_noise_50hz = MeasureLeadImpedanceForSine(
64, LeadChannel_FP1, kSampleRate, 50.0, kAmplitude);
EXPECT_GT(signal_10hz, 0);
EXPECT_LT(line_noise_50hz, signal_10hz);
}
TEST(XYParserApiTests, ImpedanceSuppressesFrequenciesAround50HzAnd100HzFor64Channels)
{
constexpr int kSampleRate = 200;
constexpr double kAmplitude = 1000000.0;
const std::uint16_t signal_10hz = MeasureLeadImpedanceForSine(
64, LeadChannel_FP1, kSampleRate, 10.0, kAmplitude);
const std::uint16_t signal_49hz = MeasureLeadImpedanceForSine(
64, LeadChannel_FP1, kSampleRate, 49.0, kAmplitude);
const std::uint16_t signal_50hz = MeasureLeadImpedanceForSine(
64, LeadChannel_FP1, kSampleRate, 50.0, kAmplitude);
const std::uint16_t signal_51hz = MeasureLeadImpedanceForSine(
64, LeadChannel_FP1, kSampleRate, 51.0, kAmplitude);
const std::uint16_t signal_100hz = MeasureLeadImpedanceForSine(
64, LeadChannel_FP1, kSampleRate, 100.0, kAmplitude);
ASSERT_GT(signal_10hz, 0);
EXPECT_LT(signal_49hz, signal_10hz);
EXPECT_LT(signal_50hz, signal_10hz);
EXPECT_LT(signal_51hz, signal_10hz);
EXPECT_LT(signal_100hz, signal_10hz);
}
TEST(XYParserApiTests, ImpedancePreserves10HzSignalWhenMixedWithStrong50HzLineNoise)
{
constexpr int kSampleRate = 200;
constexpr double kSignalAmplitude = 1000000.0;
constexpr double kLineNoiseAmplitude = 3000000.0;
const std::uint16_t signal_10hz = MeasureLeadImpedanceForSine(
64, LeadChannel_FP1, kSampleRate, 10.0, kSignalAmplitude);
const std::uint16_t line_noise_50hz = MeasureLeadImpedanceForSine(
64, LeadChannel_FP1, kSampleRate, 50.0, kLineNoiseAmplitude);
const std::uint16_t mixed_signal = MeasureLeadImpedanceForMixedSine(
64, LeadChannel_FP1, kSampleRate, 10.0, kSignalAmplitude, 50.0, kLineNoiseAmplitude);
ASSERT_GT(signal_10hz, 0);
EXPECT_LT(line_noise_50hz, signal_10hz);
EXPECT_GT(mixed_signal, line_noise_50hz);
EXPECT_NEAR(static_cast<double>(mixed_signal),
static_cast<double>(signal_10hz),
std::max(1.0, static_cast<double>(signal_10hz) * 0.2));
}
TEST(XYParserApiTests, ImpedancePreserves10HzSignalWhenMixedWithStrong49To51HzLineNoise)
{
constexpr int kSampleRate = 200;
constexpr double kSignalAmplitude = 1000000.0;
constexpr double kLineNoiseAmplitude = 3000000.0;
const std::uint16_t signal_10hz = MeasureLeadImpedanceForSine(
64, LeadChannel_FP1, kSampleRate, 10.0, kSignalAmplitude);
ASSERT_GT(signal_10hz, 0);
for (double line_noise_hz : {49.0, 50.0, 51.0}) {
const std::uint16_t pure_line_noise = MeasureLeadImpedanceForSine(
64, LeadChannel_FP1, kSampleRate, line_noise_hz, kLineNoiseAmplitude);
const std::uint16_t mixed_signal = MeasureLeadImpedanceForMixedSine(
64,
LeadChannel_FP1,
kSampleRate,
10.0,
kSignalAmplitude,
line_noise_hz,
kLineNoiseAmplitude);
EXPECT_LT(pure_line_noise, signal_10hz) << "line noise hz=" << line_noise_hz;
EXPECT_GT(mixed_signal, pure_line_noise) << "line noise hz=" << line_noise_hz;
EXPECT_NEAR(static_cast<double>(mixed_signal),
static_cast<double>(signal_10hz),
std::max(1.0, static_cast<double>(signal_10hz) * 0.2))
<< "line noise hz=" << line_noise_hz;
}
}
TEST(XYParserApiTests, WelchReturnsOneResultAfterOneSecondFromAlgorithmData)
{
ParserGuard parser(XYParser_CreateParser(64));
ASSERT_NE(parser.get(), nullptr);
XYParser_SetSampleRate(parser.get(), 10);
XYParser_SetWelchDetection(parser.get(), 1);
std::vector<double> algorithm_data(static_cast<std::size_t>(10 * XYPARSER_FRAME_DATA_COLUMN_COUNT), 0.0);
for (int sample = 0; sample < 10; ++sample) {
const std::size_t sample_offset = static_cast<std::size_t>(sample) * XYPARSER_FRAME_DATA_COLUMN_COUNT;
algorithm_data[sample_offset] = static_cast<double>(BuildSineRawValue(sample, 10, 2.0, 1000000.0));
}
std::array<XYParserWelchSummary, 2> welch{};
EXPECT_EQ(XYParser_FeedAlgorithmData(
parser.get(),
reinterpret_cast<const std::uint8_t*>(algorithm_data.data()),
algorithm_data.size() * sizeof(double),
nullptr,
0),
0);
ASSERT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast<int>(welch.size())), 1);
EXPECT_EQ(welch[0].ok, 1);
EXPECT_EQ(welch[0].channel_count, 64);
EXPECT_EQ(welch[0].sample_rate, 10U);
EXPECT_EQ(welch[0].window_sample_count, 10U);
EXPECT_GT(welch[0].frequency_count, 0U);
EXPECT_DOUBLE_EQ(welch[0].frequencies[0], 1.0);
EXPECT_GT(welch[0].psd_values[LeadChannel_FP1][0], 0.0);
EXPECT_EQ(welch[0].psd_values[LeadChannel_FP2][0], 0.0);
EXPECT_GT(welch[0].band_values[0][LeadChannel_FP1], 0.0);
}
TEST(XYParserApiTests, WelchDetects50HzPeakFromAlgorithmData)
{
ParserGuard parser(XYParser_CreateParser(64));
ASSERT_NE(parser.get(), nullptr);
constexpr int kSampleRate = 200;
XYParser_SetSampleRate(parser.get(), kSampleRate);
XYParser_SetWelchDetection(parser.get(), 1);
const std::vector<double> algorithm_data = BuildAlgorithmDataForSingleChannel(
kSampleRate, 50.0, 1000000.0, 10.0, 100000.0);
std::array<XYParserWelchSummary, 1> welch{};
EXPECT_EQ(XYParser_FeedAlgorithmData(
parser.get(),
reinterpret_cast<const std::uint8_t*>(algorithm_data.data()),
algorithm_data.size() * sizeof(double),
nullptr,
0),
0);
ASSERT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast<int>(welch.size())), 1);
ASSERT_EQ(welch[0].ok, 1);
const int frequency_10hz_index = FindFrequencyIndex(welch[0], 10.0);
const int frequency_50hz_index = FindFrequencyIndex(welch[0], 50.0);
const int peak_index = FindPeakPsdIndex(welch[0], LeadChannel_FP1);
ASSERT_GE(frequency_10hz_index, 0);
ASSERT_GE(frequency_50hz_index, 0);
ASSERT_GE(peak_index, 0);
EXPECT_DOUBLE_EQ(welch[0].frequencies[static_cast<std::size_t>(peak_index)], 50.0);
EXPECT_GT(welch[0].psd_values[LeadChannel_FP1][static_cast<std::size_t>(frequency_50hz_index)],
welch[0].psd_values[LeadChannel_FP1][static_cast<std::size_t>(frequency_10hz_index)]);
EXPECT_GT(welch[0].band_values[4][LeadChannel_FP1], welch[0].band_values[2][LeadChannel_FP1]);
}
TEST(XYParserApiTests, WelchDetects49HzPeakFromAlgorithmData)
{
ParserGuard parser(XYParser_CreateParser(64));
ASSERT_NE(parser.get(), nullptr);
constexpr int kSampleRate = 200;
XYParser_SetSampleRate(parser.get(), kSampleRate);
XYParser_SetWelchDetection(parser.get(), 1);
const std::vector<double> algorithm_data = BuildAlgorithmDataForSingleChannel(
kSampleRate, 49.0, 1000000.0, 10.0, 100000.0);
std::array<XYParserWelchSummary, 1> welch{};
EXPECT_EQ(XYParser_FeedAlgorithmData(
parser.get(),
reinterpret_cast<const std::uint8_t*>(algorithm_data.data()),
algorithm_data.size() * sizeof(double),
nullptr,
0),
0);
ASSERT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast<int>(welch.size())), 1);
ASSERT_EQ(welch[0].ok, 1);
const int frequency_10hz_index = FindFrequencyIndex(welch[0], 10.0);
const int frequency_49hz_index = FindFrequencyIndex(welch[0], 49.0);
const int peak_index = FindPeakPsdIndex(welch[0], LeadChannel_FP1);
ASSERT_GE(frequency_10hz_index, 0);
ASSERT_GE(frequency_49hz_index, 0);
ASSERT_GE(peak_index, 0);
EXPECT_DOUBLE_EQ(welch[0].frequencies[static_cast<std::size_t>(peak_index)], 49.0);
EXPECT_GT(welch[0].psd_values[LeadChannel_FP1][static_cast<std::size_t>(frequency_49hz_index)],
welch[0].psd_values[LeadChannel_FP1][static_cast<std::size_t>(frequency_10hz_index)]);
EXPECT_GT(welch[0].band_values[4][LeadChannel_FP1], welch[0].band_values[2][LeadChannel_FP1]);
}
TEST(XYParserApiTests, WelchReportedFrequenciesDoNotExceed50Hz)
{
ParserGuard parser(XYParser_CreateParser(64));
ASSERT_NE(parser.get(), nullptr);
constexpr int kSampleRate = 200;
XYParser_SetSampleRate(parser.get(), kSampleRate);
XYParser_SetWelchDetection(parser.get(), 1);
const std::vector<double> algorithm_data = BuildAlgorithmDataForSingleChannel(
kSampleRate, 51.0, 1000000.0, 100.0, 500000.0);
std::array<XYParserWelchSummary, 1> welch{};
EXPECT_EQ(XYParser_FeedAlgorithmData(
parser.get(),
reinterpret_cast<const std::uint8_t*>(algorithm_data.data()),
algorithm_data.size() * sizeof(double),
nullptr,
0),
0);
ASSERT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast<int>(welch.size())), 1);
ASSERT_EQ(welch[0].ok, 1);
ASSERT_GT(welch[0].frequency_count, 0U);
EXPECT_LE(welch[0].frequencies[welch[0].frequency_count - 1], 50.0);
EXPECT_EQ(FindFrequencyIndex(welch[0], 51.0), -1);
EXPECT_EQ(FindFrequencyIndex(welch[0], 100.0), -1);
}
TEST(XYParserApiTests, WelchDetects49And50HzPeakWhenMixedWith10Hz)
{
constexpr int kSampleRate = 200;
for (double line_noise_hz : {49.0, 50.0}) {
ParserGuard parser(XYParser_CreateParser(64));
ASSERT_NE(parser.get(), nullptr);
XYParser_SetSampleRate(parser.get(), kSampleRate);
XYParser_SetWelchDetection(parser.get(), 1);
const std::vector<double> algorithm_data = BuildAlgorithmDataForSingleChannel(
kSampleRate, line_noise_hz, 1000000.0, 10.0, 100000.0);
std::array<XYParserWelchSummary, 1> welch{};
EXPECT_EQ(XYParser_FeedAlgorithmData(
parser.get(),
reinterpret_cast<const std::uint8_t*>(algorithm_data.data()),
algorithm_data.size() * sizeof(double),
nullptr,
0),
0);
ASSERT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast<int>(welch.size())), 1);
ASSERT_EQ(welch[0].ok, 1);
const int frequency_10hz_index = FindFrequencyIndex(welch[0], 10.0);
const int line_noise_index = FindFrequencyIndex(welch[0], line_noise_hz);
const int peak_index = FindPeakPsdIndex(welch[0], LeadChannel_FP1);
ASSERT_GE(frequency_10hz_index, 0) << "line noise hz=" << line_noise_hz;
ASSERT_GE(line_noise_index, 0) << "line noise hz=" << line_noise_hz;
ASSERT_GE(peak_index, 0) << "line noise hz=" << line_noise_hz;
EXPECT_DOUBLE_EQ(welch[0].frequencies[static_cast<std::size_t>(peak_index)], line_noise_hz)
<< "line noise hz=" << line_noise_hz;
EXPECT_GT(welch[0].psd_values[LeadChannel_FP1][static_cast<std::size_t>(line_noise_index)],
welch[0].psd_values[LeadChannel_FP1][static_cast<std::size_t>(frequency_10hz_index)])
<< "line noise hz=" << line_noise_hz;
EXPECT_GT(welch[0].band_values[4][LeadChannel_FP1], welch[0].band_values[2][LeadChannel_FP1])
<< "line noise hz=" << line_noise_hz;
}
}
TEST(XYParserApiTests, WelchDoesNotReport51HzButGammaBandRisesWhenMixedWith10Hz)
{
ParserGuard parser(XYParser_CreateParser(64));
ASSERT_NE(parser.get(), nullptr);
constexpr int kSampleRate = 200;
XYParser_SetSampleRate(parser.get(), kSampleRate);
XYParser_SetWelchDetection(parser.get(), 1);
const std::vector<double> algorithm_data = BuildAlgorithmDataForSingleChannel(
kSampleRate, 51.0, 1000000.0, 10.0, 100000.0);
std::array<XYParserWelchSummary, 1> welch{};
EXPECT_EQ(XYParser_FeedAlgorithmData(
parser.get(),
reinterpret_cast<const std::uint8_t*>(algorithm_data.data()),
algorithm_data.size() * sizeof(double),
nullptr,
0),
0);
ASSERT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast<int>(welch.size())), 1);
ASSERT_EQ(welch[0].ok, 1);
const int frequency_10hz_index = FindFrequencyIndex(welch[0], 10.0);
const int frequency_50hz_index = FindFrequencyIndex(welch[0], 50.0);
ASSERT_GE(frequency_10hz_index, 0);
ASSERT_GE(frequency_50hz_index, 0);
EXPECT_EQ(FindFrequencyIndex(welch[0], 51.0), -1);
EXPECT_GT(welch[0].band_values[4][LeadChannel_FP1], welch[0].band_values[2][LeadChannel_FP1]);
EXPECT_GT(welch[0].psd_values[LeadChannel_FP1][static_cast<std::size_t>(frequency_50hz_index)],
welch[0].psd_values[LeadChannel_FP1][static_cast<std::size_t>(frequency_10hz_index)]);
}
TEST(XYParserApiTests, WelchDoesNotReport100HzOrPolluteLowFrequencyBands)
{
ParserGuard parser(XYParser_CreateParser(64));
ASSERT_NE(parser.get(), nullptr);
constexpr int kSampleRate = 200;
XYParser_SetSampleRate(parser.get(), kSampleRate);
XYParser_SetWelchDetection(parser.get(), 1);
const std::vector<double> algorithm_data = BuildAlgorithmDataForSingleChannel(
kSampleRate, 100.0, 1000000.0, 10.0, 100000.0);
std::array<XYParserWelchSummary, 1> welch{};
EXPECT_EQ(XYParser_FeedAlgorithmData(
parser.get(),
reinterpret_cast<const std::uint8_t*>(algorithm_data.data()),
algorithm_data.size() * sizeof(double),
nullptr,
0),
0);
ASSERT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast<int>(welch.size())), 1);
ASSERT_EQ(welch[0].ok, 1);
const int frequency_10hz_index = FindFrequencyIndex(welch[0], 10.0);
const int frequency_50hz_index = FindFrequencyIndex(welch[0], 50.0);
ASSERT_GE(frequency_10hz_index, 0);
ASSERT_GE(frequency_50hz_index, 0);
EXPECT_EQ(FindFrequencyIndex(welch[0], 100.0), -1);
EXPECT_LE(welch[0].frequencies[welch[0].frequency_count - 1], 50.0);
EXPECT_GT(welch[0].psd_values[LeadChannel_FP1][static_cast<std::size_t>(frequency_10hz_index)], 0.0);
EXPECT_LT(welch[0].psd_values[LeadChannel_FP1][static_cast<std::size_t>(frequency_50hz_index)],
welch[0].psd_values[LeadChannel_FP1][static_cast<std::size_t>(frequency_10hz_index)]);
EXPECT_GT(welch[0].band_values[2][LeadChannel_FP1], welch[0].band_values[0][LeadChannel_FP1]);
EXPECT_GT(welch[0].band_values[2][LeadChannel_FP1], welch[0].band_values[1][LeadChannel_FP1]);
}
TEST(XYParserApiTests, WelchResetClearsHalfWindowAfterDisableEnable)
{
ParserGuard parser(XYParser_CreateParser(64));
ASSERT_NE(parser.get(), nullptr);
XYParser_SetSampleRate(parser.get(), 10);
XYParser_SetWelchDetection(parser.get(), 1);
const std::vector<double> full_data = BuildAlgorithmDataForSingleChannel(10, 2.0, 1000000.0);
const std::size_t half_row_count =
static_cast<std::size_t>(XYPARSER_SAMPLES_PER_FRAME) * XYPARSER_FRAME_DATA_COLUMN_COUNT;
const std::vector<double> first_half(full_data.begin(), full_data.begin() + static_cast<std::ptrdiff_t>(half_row_count));
const std::vector<double> second_half(full_data.begin() + static_cast<std::ptrdiff_t>(half_row_count), full_data.end());
std::array<XYParserWelchSummary, 1> welch{};
EXPECT_EQ(XYParser_FeedAlgorithmData(
parser.get(),
reinterpret_cast<const std::uint8_t*>(first_half.data()),
first_half.size() * sizeof(double),
nullptr,
0),
0);
EXPECT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast<int>(welch.size())), 0);
XYParser_SetWelchDetection(parser.get(), 0);
XYParser_SetWelchDetection(parser.get(), 1);
EXPECT_EQ(XYParser_FeedAlgorithmData(
parser.get(),
reinterpret_cast<const std::uint8_t*>(first_half.data()),
first_half.size() * sizeof(double),
nullptr,
0),
0);
EXPECT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast<int>(welch.size())), 0);
EXPECT_EQ(XYParser_FeedAlgorithmData(
parser.get(),
reinterpret_cast<const std::uint8_t*>(second_half.data()),
second_half.size() * sizeof(double),
nullptr,
0),
0);
ASSERT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast<int>(welch.size())), 1);
EXPECT_EQ(welch[0].ok, 1);
}
TEST(XYParserApiTests, WelchSeparatesDifferentChannelPeaks)
{
ParserGuard parser(XYParser_CreateParser(64));
ASSERT_NE(parser.get(), nullptr);
constexpr int kSampleRate = 200;
XYParser_SetSampleRate(parser.get(), kSampleRate);
XYParser_SetWelchDetection(parser.get(), 1);
const std::vector<double> algorithm_data = BuildAlgorithmDataForTwoChannels(
kSampleRate, 10.0, 1000000.0, 20.0, 1000000.0);
std::array<XYParserWelchSummary, 1> welch{};
EXPECT_EQ(XYParser_FeedAlgorithmData(
parser.get(),
reinterpret_cast<const std::uint8_t*>(algorithm_data.data()),
algorithm_data.size() * sizeof(double),
nullptr,
0),
0);
ASSERT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast<int>(welch.size())), 1);
ASSERT_EQ(welch[0].ok, 1);
const int fp1_peak_index = FindPeakPsdIndex(welch[0], LeadChannel_FP1);
const int fp2_peak_index = FindPeakPsdIndex(welch[0], LeadChannel_FP2);
const int frequency_10hz_index = FindFrequencyIndex(welch[0], 10.0);
const int frequency_20hz_index = FindFrequencyIndex(welch[0], 20.0);
ASSERT_GE(fp1_peak_index, 0);
ASSERT_GE(fp2_peak_index, 0);
ASSERT_GE(frequency_10hz_index, 0);
ASSERT_GE(frequency_20hz_index, 0);
EXPECT_DOUBLE_EQ(welch[0].frequencies[static_cast<std::size_t>(fp1_peak_index)], 10.0);
EXPECT_DOUBLE_EQ(welch[0].frequencies[static_cast<std::size_t>(fp2_peak_index)], 20.0);
EXPECT_GT(welch[0].psd_values[LeadChannel_FP1][static_cast<std::size_t>(frequency_10hz_index)],
welch[0].psd_values[LeadChannel_FP1][static_cast<std::size_t>(frequency_20hz_index)]);
EXPECT_GT(welch[0].psd_values[LeadChannel_FP2][static_cast<std::size_t>(frequency_20hz_index)],
welch[0].psd_values[LeadChannel_FP2][static_cast<std::size_t>(frequency_10hz_index)]);
}
TEST(XYParserApiTests, WelchPsdIncreasesWithSignalAmplitude)
{
ParserGuard low_amplitude_parser(XYParser_CreateParser(64));
ParserGuard high_amplitude_parser(XYParser_CreateParser(64));
ASSERT_NE(low_amplitude_parser.get(), nullptr);
ASSERT_NE(high_amplitude_parser.get(), nullptr);
constexpr int kSampleRate = 200;
XYParser_SetSampleRate(low_amplitude_parser.get(), kSampleRate);
XYParser_SetWelchDetection(low_amplitude_parser.get(), 1);
XYParser_SetSampleRate(high_amplitude_parser.get(), kSampleRate);
XYParser_SetWelchDetection(high_amplitude_parser.get(), 1);
const std::vector<double> low_amplitude_data = BuildAlgorithmDataForSingleChannel(
kSampleRate, 10.0, 100000.0);
const std::vector<double> high_amplitude_data = BuildAlgorithmDataForSingleChannel(
kSampleRate, 10.0, 1000000.0);
std::array<XYParserWelchSummary, 1> low_welch{};
std::array<XYParserWelchSummary, 1> high_welch{};
EXPECT_EQ(XYParser_FeedAlgorithmData(
low_amplitude_parser.get(),
reinterpret_cast<const std::uint8_t*>(low_amplitude_data.data()),
low_amplitude_data.size() * sizeof(double),
nullptr,
0),
0);
EXPECT_EQ(XYParser_FeedAlgorithmData(
high_amplitude_parser.get(),
reinterpret_cast<const std::uint8_t*>(high_amplitude_data.data()),
high_amplitude_data.size() * sizeof(double),
nullptr,
0),
0);
ASSERT_EQ(XYParser_ReadWelch(low_amplitude_parser.get(), low_welch.data(), static_cast<int>(low_welch.size())), 1);
ASSERT_EQ(XYParser_ReadWelch(high_amplitude_parser.get(), high_welch.data(), static_cast<int>(high_welch.size())), 1);
const int low_frequency_10hz_index = FindFrequencyIndex(low_welch[0], 10.0);
const int high_frequency_10hz_index = FindFrequencyIndex(high_welch[0], 10.0);
ASSERT_GE(low_frequency_10hz_index, 0);
ASSERT_GE(high_frequency_10hz_index, 0);
EXPECT_GT(high_welch[0].psd_values[LeadChannel_FP1][static_cast<std::size_t>(high_frequency_10hz_index)],
low_welch[0].psd_values[LeadChannel_FP1][static_cast<std::size_t>(low_frequency_10hz_index)]);
EXPECT_GT(high_welch[0].band_values[2][LeadChannel_FP1],
low_welch[0].band_values[2][LeadChannel_FP1]);
}
TEST(XYParserApiTests, WelchAbsolutePsdMatches10HzSineAt100UvPeakToPeak)
{
ParserGuard parser(XYParser_CreateParser(64));
ASSERT_NE(parser.get(), nullptr);
constexpr int kSampleRate = 250;
constexpr double kFrequencyHz = 10.0;
constexpr double kPeakToPeakUv = 100.0;
constexpr double kPeakAmplitudeUv = kPeakToPeakUv / 2.0;
constexpr double kExpectedPeakPsd = 833.3333333333334;
XYParser_SetSampleRate(parser.get(), kSampleRate);
XYParser_SetWelchDetection(parser.get(), 1);
const std::vector<double> algorithm_data =
BuildAlgorithmDataForSingleChannel(kSampleRate, kFrequencyHz, kPeakAmplitudeUv);
std::array<XYParserWelchSummary, 1> welch{};
EXPECT_EQ(XYParser_FeedAlgorithmData(
parser.get(),
reinterpret_cast<const std::uint8_t*>(algorithm_data.data()),
algorithm_data.size() * sizeof(double),
nullptr,
0),
0);
ASSERT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast<int>(welch.size())), 1);
ASSERT_EQ(welch[0].ok, 1);
const int peak_index = FindPeakPsdIndex(welch[0], LeadChannel_FP1);
const int frequency_10hz_index = FindFrequencyIndex(welch[0], kFrequencyHz);
ASSERT_GE(peak_index, 0);
ASSERT_GE(frequency_10hz_index, 0);
EXPECT_DOUBLE_EQ(welch[0].frequencies[static_cast<std::size_t>(peak_index)], kFrequencyHz);
EXPECT_DOUBLE_EQ(welch[0].frequencies[static_cast<std::size_t>(frequency_10hz_index)], kFrequencyHz);
const double peak_psd = welch[0].psd_values[LeadChannel_FP1][static_cast<std::size_t>(frequency_10hz_index)];
EXPECT_NEAR(peak_psd,
ExpectedBinCenteredHanningPeakPsd(kPeakAmplitudeUv),
kExpectedPeakPsd * 0.1);
EXPECT_GT(welch[0].band_values[2][LeadChannel_FP1], welch[0].band_values[4][LeadChannel_FP1]);
}
TEST(XYParserApiTests, ReadWelchConsumesQueuedResults)
{
ParserGuard parser(XYParser_CreateParser(64));
ASSERT_NE(parser.get(), nullptr);
XYParser_SetSampleRate(parser.get(), 10);
XYParser_SetWelchDetection(parser.get(), 1);
const std::vector<double> algorithm_data = BuildAlgorithmDataForSingleChannel(10, 2.0, 1000000.0);
std::array<XYParserWelchSummary, 1> welch{};
EXPECT_EQ(XYParser_FeedAlgorithmData(
parser.get(),
reinterpret_cast<const std::uint8_t*>(algorithm_data.data()),
algorithm_data.size() * sizeof(double),
nullptr,
0),
0);
ASSERT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast<int>(welch.size())), 1);
EXPECT_EQ(welch[0].ok, 1);
EXPECT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast<int>(welch.size())), 0);
}
TEST(XYParserApiTests, Convert8ChTo64ChAlgorithmDataMatchesDirect64Ch)
{
ParserGuard parser8(XYParser_CreateParser(8));
ParserGuard parser64(XYParser_CreateParser(64));
ASSERT_NE(parser8.get(), nullptr);
ASSERT_NE(parser64.get(), nullptr);
XYParser_SetBypassChecksum(parser8.get(), 1);
XYParser_SetBypassChecksum(parser64.get(), 1);
std::array<XYParserLeadChannelNumber, 8> leads8{};
std::array<XYParserLeadChannelNumber, 64> leads64{};
ASSERT_EQ(XYParser_GetLeadMap(8, leads8.data(), static_cast<int>(leads8.size())), 8);
ASSERT_EQ(XYParser_GetLeadMap(64, leads64.data(), static_cast<int>(leads64.size())), 64);
std::array<std::array<int, XYPARSER_MAX_CHANNELS>, XYPARSER_SAMPLES_PER_FRAME> raw_samples8{};
std::array<std::array<int, XYPARSER_MAX_CHANNELS>, XYPARSER_SAMPLES_PER_FRAME> raw_samples64{};
for (std::size_t sample_index = 0; sample_index < XYPARSER_SAMPLES_PER_FRAME; ++sample_index) {
for (std::size_t channel_index = 0; channel_index < leads8.size(); ++channel_index) {
const int raw_value = static_cast<int>((sample_index + 1) * 1000 + static_cast<int>(channel_index) * 100);
raw_samples8[sample_index][channel_index] = raw_value;
for (std::size_t raw64_index = 0; raw64_index < leads64.size(); ++raw64_index) {
if (leads64[raw64_index] == leads8[channel_index]) {
raw_samples64[sample_index][raw64_index] = raw_value;
break;
}
}
}
}
const std::vector<std::uint8_t> frame8 = BuildFrameWithRawSamples(8, 1U, raw_samples8);
const std::vector<std::uint8_t> frame64 = BuildFrameWithRawSamples(64, 1U, raw_samples64);
std::array<XYParserFrameSummary, 1> summaries8{};
std::array<XYParserFrameSummary, 1> summaries64{};
std::array<XYParserFrameSummary, 1> converted64{};
ASSERT_EQ(XYParser_Feed(parser8.get(), frame8.data(), frame8.size(), summaries8.data(), static_cast<int>(summaries8.size())), 1);
ASSERT_EQ(XYParser_Feed(parser64.get(), frame64.data(), frame64.size(), summaries64.data(), static_cast<int>(summaries64.size())), 1);
ASSERT_EQ(XYParser_Convert8ChFramesTo64Ch(summaries8.data(), 1, converted64.data(), 1), 1);
std::vector<double> converted_algorithm_data(XYPARSER_FRAME_ALGORITHM_VALUE_COUNT, 0.0);
std::vector<double> direct_algorithm_data(XYPARSER_FRAME_ALGORITHM_VALUE_COUNT, 0.0);
ASSERT_EQ(XYParser_ConvertSampleFramesToAlgorithmData(&converted64[0], converted_algorithm_data.data()), 1);
ASSERT_EQ(XYParser_ConvertSampleFramesToAlgorithmData(&summaries64[0], direct_algorithm_data.data()), 1);
for (std::size_t i = 0; i < converted_algorithm_data.size(); ++i) {
EXPECT_DOUBLE_EQ(converted_algorithm_data[i], direct_algorithm_data[i]) << "index=" << i;
}
}
TEST(XYParserApiTests, WelchFrameFeedDoesNotProduceResults)
{
ParserGuard parser(XYParser_CreateParser(64));
ASSERT_NE(parser.get(), nullptr);
XYParser_SetBypassChecksum(parser.get(), 1);
XYParser_SetSampleRate(parser.get(), 10);
XYParser_SetWelchDetection(parser.get(), 1);
std::array<std::array<int, XYPARSER_MAX_CHANNELS>, XYPARSER_SAMPLES_PER_FRAME> raw_samples1{};
std::array<std::array<int, XYPARSER_MAX_CHANNELS>, XYPARSER_SAMPLES_PER_FRAME> raw_samples2{};
for (int sample = 0; sample < 10; ++sample) {
const int raw_value = BuildSineRawValue(sample, 10, 2.0, 1000000.0);
if (sample < static_cast<int>(XYPARSER_SAMPLES_PER_FRAME)) {
raw_samples1[static_cast<std::size_t>(sample)][0] = raw_value;
} else {
raw_samples2[static_cast<std::size_t>(sample - XYPARSER_SAMPLES_PER_FRAME)][0] = raw_value;
}
}
const std::vector<std::uint8_t> frame1 = BuildFrameWithRawSamples(64, 1U, raw_samples1);
const std::vector<std::uint8_t> frame2 = BuildFrameWithRawSamples(64, 2U, raw_samples2);
std::array<XYParserFrameSummary, 2> summaries{};
std::array<XYParserWelchSummary, 1> welch{};
EXPECT_EQ(XYParser_Feed(parser.get(), frame1.data(), frame1.size(), summaries.data(), static_cast<int>(summaries.size())), 1);
EXPECT_EQ(XYParser_Feed(parser.get(), frame2.data(), frame2.size(), summaries.data(), static_cast<int>(summaries.size())), 1);
EXPECT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast<int>(welch.size())), 0);
}
TEST(XYParserApiTests, WelchDisabledDoesNotProduceResultsFromAlgorithmData)
{
ParserGuard parser(XYParser_CreateParser(64));
ASSERT_NE(parser.get(), nullptr);
XYParser_SetSampleRate(parser.get(), 10);
std::vector<double> algorithm_data(static_cast<std::size_t>(10 * XYPARSER_FRAME_DATA_COLUMN_COUNT), 0.0);
for (int sample = 0; sample < 10; ++sample) {
const std::size_t sample_offset = static_cast<std::size_t>(sample) * XYPARSER_FRAME_DATA_COLUMN_COUNT;
algorithm_data[sample_offset] = static_cast<double>(BuildSineRawValue(sample, 10, 2.0, 1000000.0));
}
std::array<XYParserWelchSummary, 1> welch{};
EXPECT_EQ(XYParser_FeedAlgorithmData(
parser.get(),
reinterpret_cast<const std::uint8_t*>(algorithm_data.data()),
algorithm_data.size() * sizeof(double),
nullptr,
0),
0);
EXPECT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast<int>(welch.size())), 0);
}
/// 测试Feed 函数能正确解析完整的 8 通道帧
TEST(XYParserApiTests, FeedParsesAComplete8ChannelFrame)
{
// 创建 8 通道解析器
ParserGuard parser(XYParser_CreateParser(8));
ASSERT_NE(parser.get(), nullptr);
// 设置 ADC 参数和校验和绕过标志
XYParser_SetAdcParams(parser.get(), 4.5, 6.0);
XYParser_SetBypassChecksum(parser.get(), 1);
// 构建测试帧并解析
const std::vector<std::uint8_t> bytes = BuildMinimalFrame(8);
std::array<XYParserFrameSummary, 1> summaries{};
const int frame_count = XYParser_Feed(
parser.get(),
bytes.data(),
bytes.size(),
summaries.data(),
static_cast<int>(summaries.size()));
// 验证解析结果
ASSERT_EQ(frame_count, 1); // 应解析出 1 帧
EXPECT_EQ(summaries[0].frame_index, 1U); // 帧索引应为 1
EXPECT_EQ(summaries[0].channel_count, 8U); // 通道数应为 8
EXPECT_EQ(summaries[0].battery, 95U); // 电池电量应为 95
EXPECT_EQ(summaries[0].sample_count, 5U); // 采样数应为 5
EXPECT_GT(summaries[0].channel_values_uv[0][0], 0.0); // 通道值应大于 0
EXPECT_EQ(summaries[0].sample_trigger_types[0], 0U); // 触发类型应为 0
EXPECT_EQ(summaries[0].sample_trigger_indices[0], 0U); // 触发索引应为 0
}
TEST(XYParserApiTests, FeedParsesReservedMetadataInto8ChannelSummary)
{
ParserGuard parser(XYParser_CreateParser(8));
ASSERT_NE(parser.get(), nullptr);
XYParser_SetBypassChecksum(parser.get(), 1);
const std::array<std::uint8_t, 6> reserved = {0x01, 24, 2, 9, 1, 0x5A};
const std::vector<std::uint8_t> bytes = BuildMinimalFrame(8, 1U, reserved);
std::array<XYParserFrameSummary, 1> summaries{};
ASSERT_EQ(XYParser_Feed(
parser.get(),
bytes.data(),
bytes.size(),
summaries.data(),
static_cast<int>(summaries.size())),
1);
EXPECT_EQ(summaries[0].impedance_enabled, 1U);
EXPECT_EQ(summaries[0].current_gain, 24U);
EXPECT_EQ(summaries[0].current_sample_rate_hz, 1000U);
EXPECT_EQ(summaries[0].cap_type, 9U);
EXPECT_EQ(summaries[0].gnd_detached, 1U);
}
/// 测试Feed 函数能缓冲部分数据直到完整帧可用
TEST(XYParserApiTests, FeedBuffersPartialDataUntilAFullFrameIsAvailable)
{
// 创建 8 通道解析器
ParserGuard parser(XYParser_CreateParser(8));
ASSERT_NE(parser.get(), nullptr);
// 设置校验和绕过标志
XYParser_SetBypassChecksum(parser.get(), 1);
// 构建测试帧并分成两部分
const std::vector<std::uint8_t> bytes = BuildMinimalFrame(8);
const std::size_t split_index = bytes.size() / 2;
std::array<XYParserFrameSummary, 1> summaries{};
// 第一次 Feed传入前半部分数据
const int first_result = XYParser_Feed(
parser.get(),
bytes.data(),
split_index,
summaries.data(),
static_cast<int>(summaries.size()));
EXPECT_EQ(first_result, 0); // 数据不完整,不应解析出帧
// 第二次 Feed传入后半部分数据
const int second_result = XYParser_Feed(
parser.get(),
bytes.data() + split_index,
bytes.size() - split_index,
summaries.data(),
static_cast<int>(summaries.size()));
ASSERT_EQ(second_result, 1); // 数据完整,应解析出 1 帧
EXPECT_EQ(summaries[0].frame_index, 1U); // 帧索引应为 1
}
// ============================================================================
// 解析器创建测试
// ============================================================================
/// 测试:成功创建 8 通道解析器
TEST(XYParserApiTests, CreateParserSucceedsFor8Channels)
{
ParserGuard parser(XYParser_CreateParser(8));
EXPECT_NE(parser.get(), nullptr);
}
/// 测试:成功创建 64 通道解析器
TEST(XYParserApiTests, CreateParserSucceedsFor64Channels)
{
ParserGuard parser(XYParser_CreateParser(64));
EXPECT_NE(parser.get(), nullptr);
}
/// 测试:创建 0 通道解析器应失败
TEST(XYParserApiTests, CreateParserRejectsZeroChannels)
{
EXPECT_EQ(XYParser_CreateParser(0), nullptr);
}
/// 测试:创建不支持的通道数(如 100应失败
TEST(XYParserApiTests, CreateParserRejectsExcessiveChannels)
{
EXPECT_EQ(XYParser_CreateParser(100), nullptr);
}
// ============================================================================
// 参数设置函数测试
// ============================================================================
/// 测试:设置正常的 ADC 参数
TEST(XYParserApiTests, SetAdcParamsAcceptsValidValues)
{
ParserGuard parser(XYParser_CreateParser(8));
ASSERT_NE(parser.get(), nullptr);
// 不应崩溃
EXPECT_NO_THROW(XYParser_SetAdcParams(parser.get(), 4.5, 6.0));
}
/// 测试:设置边界值的 ADC 参数
TEST(XYParserApiTests, SetAdcParamsAcceptsBoundaryValues)
{
ParserGuard parser(XYParser_CreateParser(8));
ASSERT_NE(parser.get(), nullptr);
// 测试零值
EXPECT_NO_THROW(XYParser_SetAdcParams(parser.get(), 0.0, 0.0));
// 测试较大值
EXPECT_NO_THROW(XYParser_SetAdcParams(parser.get(), 100.0, 1000.0));
}
/// 测试:对空句柄调用 SetAdcParams
TEST(XYParserApiTests, SetAdcParamsOnNullHandle)
{
// 不应崩溃
EXPECT_NO_THROW(XYParser_SetAdcParams(nullptr, 4.5, 6.0));
}
/// 测试:关闭校验和绕过
TEST(XYParserApiTests, SetBypassChecksumOff)
{
ParserGuard parser(XYParser_CreateParser(8));
ASSERT_NE(parser.get(), nullptr);
EXPECT_NO_THROW(XYParser_SetBypassChecksum(parser.get(), 0));
}
/// 测试:对空句柄调用 SetBypassChecksum
TEST(XYParserApiTests, SetBypassChecksumOnNullHandle)
{
EXPECT_NO_THROW(XYParser_SetBypassChecksum(nullptr, 1));
}
// ============================================================================
// 64 导控制协议序列化测试
// ============================================================================
/// 测试64 导开启阻抗命令序列化与 WirelessEEG 协议保持一致
TEST(XYParserApiTests, Serialize64ImpedanceOpenCommandMatchesWirelessEegProtocol)
{
std::array<std::uint8_t, 32> buffer{};
const int size = XYParser_Serialize64ImpedanceCommand(1, buffer.data(), buffer.size());
ASSERT_EQ(size, static_cast<int>(XYParser_Get64ImpedanceCommandSize()));
const std::vector<std::uint8_t> expected = {
0xAA, 0x01, 0x00, 0x07,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x09, 0x55, 0x55
};
EXPECT_TRUE(std::equal(expected.begin(), expected.end(), buffer.begin()));
}
/// 测试64 导增益和采样率命令序列化与 WirelessEEG 协议保持一致
TEST(XYParserApiTests, Serialize64GainAndSampleRateCommandMatchesWirelessEegProtocol)
{
std::array<std::uint8_t, 32> buffer{};
const int size = XYParser_Serialize64GainSampleRateCommand(24, 1000, buffer.data(), buffer.size());
ASSERT_EQ(size, static_cast<int>(XYParser_Get64GainSampleRateCommandSize()));
const std::vector<std::uint8_t> expected = {
0xAA, 0x02, 0x00, 0x08,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x18, 0x02, 0x24, 0x55, 0x55
};
EXPECT_TRUE(std::equal(expected.begin(), expected.end(), buffer.begin()));
}
/// 测试64 导增益和采样率命令拒绝非法采样率
TEST(XYParserApiTests, Serialize64GainAndSampleRateCommandRejectsUnsupportedSampleRate)
{
std::array<std::uint8_t, 32> buffer{};
EXPECT_EQ(XYParser_Serialize64GainSampleRateCommand(24, 256, buffer.data(), buffer.size()), 0);
}
/// 测试64 导阻抗命令在输出缓冲区不足时返回失败
TEST(XYParserApiTests, Serialize64ImpedanceCommandRejectsSmallBuffer)
{
std::array<std::uint8_t, 4> buffer{};
EXPECT_EQ(XYParser_Serialize64ImpedanceCommand(1, buffer.data(), buffer.size()), 0);
}
// ============================================================================
// Feed 函数帧解析测试
// ============================================================================
/// 测试:传入空数据
TEST(XYParserApiTests, FeedParsesEmptyData)
{
ParserGuard parser(XYParser_CreateParser(8));
ASSERT_NE(parser.get(), nullptr);
std::array<XYParserFrameSummary, 10> summaries{};
const int result = XYParser_Feed(
parser.get(),
nullptr,
0,
summaries.data(),
static_cast<int>(summaries.size()));
EXPECT_EQ(result, 0);
}
/// 测试:只传入帧头数据
TEST(XYParserApiTests, FeedParsesOnlyHeader)
{
ParserGuard parser(XYParser_CreateParser(8));
ASSERT_NE(parser.get(), nullptr);
XYParser_SetBypassChecksum(parser.get(), 1);
// 只发送帧头
const std::vector<std::uint8_t> header_only = {0xAA, 0x01, 0x00, 0x00, 0x00};
std::array<XYParserFrameSummary, 10> summaries{};
const int result = XYParser_Feed(
parser.get(),
header_only.data(),
header_only.size(),
summaries.data(),
static_cast<int>(summaries.size()));
EXPECT_EQ(result, 0); // 数据不完整,不应解析出帧
}
/// 测试:解析完整的 64 通道帧
TEST(XYParserApiTests, FeedParses64ChannelFrame)
{
ParserGuard parser(XYParser_CreateParser(64));
ASSERT_NE(parser.get(), nullptr);
XYParser_SetAdcParams(parser.get(), 4.5, 6.0);
XYParser_SetBypassChecksum(parser.get(), 1);
const std::vector<std::uint8_t> bytes = BuildMinimalFrame(64);
std::array<XYParserFrameSummary, 1> summaries{};
const int frame_count = XYParser_Feed(
parser.get(),
bytes.data(),
bytes.size(),
summaries.data(),
static_cast<int>(summaries.size()));
ASSERT_EQ(frame_count, 1);
EXPECT_EQ(summaries[0].channel_count, 64U);
}
TEST(XYParserApiTests, FeedParsesReservedMetadataInto64ChannelSummary)
{
ParserGuard parser(XYParser_CreateParser(64));
ASSERT_NE(parser.get(), nullptr);
XYParser_SetBypassChecksum(parser.get(), 1);
const std::array<std::uint8_t, 6> reserved = {0x00, 6, 1, 4, 0, 0x00};
const std::vector<std::uint8_t> bytes = BuildMinimalFrame(64, 1U, reserved);
std::array<XYParserFrameSummary, 1> summaries{};
ASSERT_EQ(XYParser_Feed(
parser.get(),
bytes.data(),
bytes.size(),
summaries.data(),
static_cast<int>(summaries.size())),
1);
EXPECT_EQ(summaries[0].impedance_enabled, 0U);
EXPECT_EQ(summaries[0].current_gain, 6U);
EXPECT_EQ(summaries[0].current_sample_rate_hz, 500U);
EXPECT_EQ(summaries[0].cap_type, 4U);
EXPECT_EQ(summaries[0].gnd_detached, 0U);
}
/// 测试:连续解析多个帧
TEST(XYParserApiTests, FeedParsesMultipleFrames)
{
ParserGuard parser(XYParser_CreateParser(8));
ASSERT_NE(parser.get(), nullptr);
XYParser_SetBypassChecksum(parser.get(), 1);
// 构建两个连续的帧
const std::vector<std::uint8_t> frame1 = BuildMinimalFrame(8, 1U);
const std::vector<std::uint8_t> frame2 = BuildMinimalFrame(8, 2U);
std::vector<std::uint8_t> combined(frame1);
combined.insert(combined.end(), frame2.begin(), frame2.end());
std::array<XYParserFrameSummary, 10> summaries{};
const int frame_count = XYParser_Feed(
parser.get(),
combined.data(),
combined.size(),
summaries.data(),
static_cast<int>(summaries.size()));
EXPECT_EQ(frame_count, 2);
EXPECT_EQ(summaries[0].frame_index, 1U);
EXPECT_EQ(summaries[1].frame_index, 2U);
}
/// 测试:帧索引递增
TEST(XYParserApiTests, FeedIncrementsFrameIndex)
{
ParserGuard parser(XYParser_CreateParser(8));
ASSERT_NE(parser.get(), nullptr);
XYParser_SetBypassChecksum(parser.get(), 1);
// 连续发送多个帧
std::array<XYParserFrameSummary, 10> summaries{};
int total_frames = 0;
for (int i = 0; i < 3; ++i) {
const std::vector<std::uint8_t> frame = BuildMinimalFrame(8, static_cast<std::uint32_t>(i + 1));
const int count = XYParser_Feed(
parser.get(),
frame.data(),
frame.size(),
summaries.data() + total_frames,
static_cast<int>(summaries.size()) - total_frames);
total_frames += count;
}
EXPECT_EQ(total_frames, 3);
for (int i = 0; i < 3; ++i) {
EXPECT_EQ(summaries[i].frame_index, static_cast<std::uint32_t>(i + 1));
}
}
/// 测试:电池电量字段解析
TEST(XYParserApiTests, FeedParsesBatteryValue)
{
ParserGuard parser(XYParser_CreateParser(8));
ASSERT_NE(parser.get(), nullptr);
XYParser_SetBypassChecksum(parser.get(), 1);
const std::vector<std::uint8_t> bytes = BuildMinimalFrame(8);
std::array<XYParserFrameSummary, 1> summaries{};
XYParser_Feed(
parser.get(),
bytes.data(),
bytes.size(),
summaries.data(),
static_cast<int>(summaries.size()));
EXPECT_EQ(summaries[0].battery, 95U); // BuildMinimalFrame 中设置的电池电量
}
// ============================================================================
// 帧数据边界测试
// ============================================================================
/// 测试:跨多次 Feed 的不完整帧
TEST(XYParserApiTests, FeedHandlesIncompleteFrameAcrossMultipleFeeds)
{
ParserGuard parser(XYParser_CreateParser(8));
ASSERT_NE(parser.get(), nullptr);
XYParser_SetBypassChecksum(parser.get(), 1);
const std::vector<std::uint8_t> bytes = BuildMinimalFrame(8);
std::array<XYParserFrameSummary, 1> summaries{};
// 分成 3 次发送
const std::size_t part1_size = bytes.size() / 3;
const std::size_t part2_size = bytes.size() / 3;
EXPECT_EQ(XYParser_Feed(parser.get(), bytes.data(), part1_size,
summaries.data(), static_cast<int>(summaries.size())), 0);
EXPECT_EQ(XYParser_Feed(parser.get(), bytes.data() + part1_size, part2_size,
summaries.data(), static_cast<int>(summaries.size())), 0);
const int result = XYParser_Feed(
parser.get(),
bytes.data() + part1_size + part2_size,
bytes.size() - part1_size - part2_size,
summaries.data(),
static_cast<int>(summaries.size()));
ASSERT_EQ(result, 1);
EXPECT_EQ(summaries[0].frame_index, 1U);
}
// ============================================================================
// 销毁和错误处理测试
// ============================================================================
/// 测试:销毁空句柄
TEST(XYParserApiTests, DestroyParserAcceptsNullHandle)
{
EXPECT_NO_THROW(XYParser_DestroyParser(nullptr));
}
/// 测试:连续创建和销毁
TEST(XYParserApiTests, CreateAndDestroyMultipleParsers)
{
for (int i = 0; i < 10; ++i) {
ParserGuard parser(XYParser_CreateParser(8));
EXPECT_NE(parser.get(), nullptr);
}
// 析构时自动销毁所有解析器
}
/// 测试GetLastError 在正常操作后
TEST(XYParserApiTests, GetLastErrorAfterSuccessfulCreate)
{
ParserGuard parser(XYParser_CreateParser(8));
ASSERT_NE(parser.get(), nullptr);
// 正常操作后,错误信息应为空(空字符串),不能是 nullptr
const char* error = XYParser_GetLastError(parser.get());
EXPECT_TRUE(std::string(error).empty());
}