// XYParser API 单元测试文件 // 测试 XYParser 库的核心功能,包括解析器创建、错误处理、帧解析等 #include #include "../XYParserApi.h" #include #include #include #include #include #include #include // 匿名命名空间,包含测试辅助代码 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 BuildMinimalFrame(std::uint8_t channel_count, std::uint32_t frame_index, std::array 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(channel_count) * 3 + 2; const std::uint16_t payload_length = static_cast(sample_bytes * kSamplesPerFrame); const std::size_t frame_size = 1 + kTagLen + payload_length + 2; std::vector frame(frame_size, 0); std::size_t offset = 0; // 写入帧头 frame[offset++] = kHeader; // 写入标签数据中的帧索引(小端序) frame[offset++] = static_cast(frame_index & 0xFF); frame[offset++] = static_cast((frame_index >> 8) & 0xFF); frame[offset++] = static_cast((frame_index >> 16) & 0xFF); frame[offset++] = static_cast((frame_index >> 24) & 0xFF); // 写入负载长度(大端序) frame[offset++] = static_cast((payload_length >> 8) & 0xFF); frame[offset++] = static_cast(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(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 BuildMinimalFrame(std::uint8_t channel_count) { return BuildMinimalFrame(channel_count, 1U); } void WriteSigned24(std::vector& 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((1 << 24) + raw_value); } else { encoded = static_cast(raw_value); } frame[offset++] = static_cast((encoded >> 16) & 0xFF); frame[offset++] = static_cast((encoded >> 8) & 0xFF); frame[offset++] = static_cast(encoded & 0xFF); } std::vector BuildFrameWithRawSamples( std::uint8_t channel_count, std::uint32_t frame_index, const std::array, XYPARSER_SAMPLES_PER_FRAME>& raw_samples, std::array 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(channel_count) * 3 + 2; const std::uint16_t payload_length = static_cast(sample_bytes * XYPARSER_SAMPLES_PER_FRAME); const std::size_t frame_size = 1 + kTagLen + payload_length + 2; std::vector frame(frame_size, 0); std::size_t offset = 0; frame[offset++] = kHeader; frame[offset++] = static_cast(frame_index & 0xFF); frame[offset++] = static_cast((frame_index >> 8) & 0xFF); frame[offset++] = static_cast((frame_index >> 16) & 0xFF); frame[offset++] = static_cast((frame_index >> 24) & 0xFF); frame[offset++] = static_cast((payload_length >> 8) & 0xFF); frame[offset++] = static_cast(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(sample_index) / static_cast(sample_rate); return static_cast(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 BuildAlgorithmDataForSingleChannel(int sample_rate, double primary_frequency_hz, double primary_amplitude, double secondary_frequency_hz = 0.0, double secondary_amplitude = 0.0) { std::vector algorithm_data( static_cast(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(sample) * XYPARSER_FRAME_DATA_COLUMN_COUNT; algorithm_data[sample_offset] = static_cast(BuildCombinedSineRawValue(sample, sample_rate, primary_frequency_hz, primary_amplitude, secondary_frequency_hz, secondary_amplitude)); } return algorithm_data; } std::vector BuildAlgorithmDataForTwoChannels(int sample_rate, double channel0_frequency_hz, double channel0_amplitude, double channel1_frequency_hz, double channel1_amplitude) { std::vector algorithm_data( static_cast(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(sample) * XYPARSER_FRAME_DATA_COLUMN_COUNT; algorithm_data[sample_offset] = static_cast( BuildSineRawValue(sample, sample_rate, channel0_frequency_hz, channel0_amplitude)); algorithm_data[sample_offset + 1] = static_cast( BuildSineRawValue(sample, sample_rate, channel1_frequency_hz, channel1_amplitude)); } return algorithm_data; } std::vector> 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(XYPARSER_SAMPLES_PER_FRAME); std::vector> frames; frames.reserve(static_cast(frame_count)); for (int frame_index = 0; frame_index < frame_count; ++frame_index) { std::array, XYPARSER_SAMPLES_PER_FRAME> raw_samples{}; for (int sample_offset = 0; sample_offset < static_cast(XYPARSER_SAMPLES_PER_FRAME); ++sample_offset) { const int sample_index = frame_index * static_cast(XYPARSER_SAMPLES_PER_FRAME) + sample_offset; raw_samples[static_cast(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(frame_index + 1), raw_samples)); } return frames; } std::vector> 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(XYPARSER_SAMPLES_PER_FRAME); std::vector> frames; frames.reserve(static_cast(frame_count)); for (int frame_index = 0; frame_index < frame_count; ++frame_index) { std::array, XYPARSER_SAMPLES_PER_FRAME> raw_samples{}; for (int sample_offset = 0; sample_offset < static_cast(XYPARSER_SAMPLES_PER_FRAME); ++sample_offset) { const int sample_index = frame_index * static_cast(XYPARSER_SAMPLES_PER_FRAME) + sample_offset; raw_samples[static_cast(sample_offset)][0] = BuildSineRawValue(sample_index, sample_rate, channel0_frequency_hz, channel0_amplitude); raw_samples[static_cast(sample_offset)][1] = BuildSineRawValue(sample_index, sample_rate, channel1_frequency_hz, channel1_amplitude); } frames.push_back(BuildFrameWithRawSamples( channel_count, static_cast(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(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(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(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 summaries{}; for (const auto& frame : frames) { if (XYParser_Feed(parser.get(), frame.data(), frame.size(), summaries.data(), static_cast(summaries.size())) != 1) { ADD_FAILURE() << "failed to feed frame for impedance measurement"; return 0; } } std::array impedance{}; if (XYParser_ReadImpedance(parser.get(), impedance.data(), static_cast(impedance.size())) != 1) { ADD_FAILURE() << "failed to read impedance summary"; return 0; } return impedance[0].impedance_values[static_cast(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 command{}; const int command_size = XYParser_Serialize8ChImpedanceCommand(1, command.data(), command.size()); ASSERT_EQ(command_size, static_cast(kCommandSize)); const std::array expected = { 0xAA, 0x00, 0x01, 0xA1, 0xA1, 0x55, 0x55}; EXPECT_EQ(command, expected); } TEST(XYParserApiTests, Serialize8ChImpedanceCloseCommandMatchesWirelessEegPacket) { constexpr std::size_t kCommandSize = 7; std::array command{}; const int command_size = XYParser_Serialize8ChImpedanceCommand(0, command.data(), command.size()); ASSERT_EQ(command_size, static_cast(kCommandSize)); const std::array expected = { 0xAA, 0x00, 0x01, 0xA0, 0xA0, 0x55, 0x55}; EXPECT_EQ(command, expected); } TEST(XYParserApiTests, Serialize8ChImpedanceCommandRejectsSmallBuffer) { std::array command{}; EXPECT_EQ(XYParser_Serialize8ChImpedanceCommand(1, command.data(), command.size()), 0); } TEST(XYParserApiTests, Get8ChImpedanceCommandSizeMatchesSerializedLength) { EXPECT_EQ(XYParser_Get8ChImpedanceCommandSize(), static_cast(7)); } TEST(XYParserApiTests, SerializeTriggerCommandMatchesWirelessEegPacket) { std::array command{}; const int command_size = XYParser_SerializeTriggerCommand( XYPARSER_TRIGGER_TRAIN_0, command.data(), command.size()); ASSERT_EQ(command_size, static_cast(XYParser_GetTriggerCommandSize())); const std::array expected = {0xAA, 0x00, 0x00, 0xBB, 0xBB, 0x55, 0x55}; EXPECT_TRUE(std::equal(expected.begin(), expected.end(), command.begin())); } TEST(XYParserApiTests, SerializeTrain1TriggerCommandMatchesWirelessEegPacket) { std::array command{}; const int command_size = XYParser_SerializeTriggerCommand( XYPARSER_TRIGGER_TRAIN_1, command.data(), command.size()); ASSERT_EQ(command_size, static_cast(XYParser_GetTriggerCommandSize())); const std::array expected = {0xAA, 0x00, 0x00, 0xBC, 0xBC, 0x55, 0x55}; EXPECT_TRUE(std::equal(expected.begin(), expected.end(), command.begin())); } TEST(XYParserApiTests, SerializeTriggerCommandRejectsUnsupportedTriggerType) { std::array command{}; EXPECT_EQ(XYParser_SerializeTriggerCommand(0xAA, command.data(), command.size()), 0); } TEST(XYParserApiTests, SerializeTriggerCommandRejectsSmallBuffer) { std::array command{}; EXPECT_EQ(XYParser_SerializeTriggerCommand(XYPARSER_TRIGGER_TRAIN_1, command.data(), command.size()), 0); } TEST(XYParserApiTests, GetAlgorithmDataValueCountMatchesFrameLayout) { EXPECT_EQ(XYParser_GetAlgorithmDataValueCount(), static_cast(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 leads{}; const int count = XYParser_GetLeadMap(8, leads.data(), static_cast(leads.size())); ASSERT_EQ(count, 8); const std::array 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 leads{}; const int count = XYParser_GetLeadMap(64, leads.data(), static_cast(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 small_buffer{}; EXPECT_EQ(XYParser_GetLeadMap(8, small_buffer.data(), static_cast(small_buffer.size())), 0); EXPECT_EQ(XYParser_GetLeadMap(7, nullptr, 0), 0); } TEST(XYParserApiTests, Convert8ChFramesTo64ChMapsKnownLeadsAndPadsOthersWithZero) { std::array 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 output{}; ASSERT_EQ(XYParser_Convert8ChFramesTo64Ch(input.data(), static_cast(input.size()), output.data(), static_cast(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 input{}; input[0].channel_count = 8U; input[1].channel_count = 64U; std::array output{}; EXPECT_EQ(XYParser_Convert8ChFramesTo64Ch(nullptr, 1, output.data(), static_cast(output.size())), 0); EXPECT_EQ(XYParser_Convert8ChFramesTo64Ch(input.data(), 1, nullptr, static_cast(output.size())), 0); EXPECT_EQ(XYParser_Convert8ChFramesTo64Ch(input.data(), 0, output.data(), static_cast(output.size())), 0); EXPECT_EQ(XYParser_Convert8ChFramesTo64Ch(input.data(), static_cast(input.size()), output.data(), static_cast(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 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 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(kTotalSamples) * kColumnCount> input_data{}; for (int sample_index = 0; sample_index < kTotalSamples; ++sample_index) { const std::size_t row_offset = static_cast(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(input_data.data()); const std::size_t first_chunk_size = 3 * static_cast(kColumnCount) * sizeof(double) + sizeof(double); const std::size_t second_chunk_size = sizeof(input_data) - first_chunk_size; std::array summaries{}; EXPECT_EQ(XYParser_FeedAlgorithmData( parser.get(), input_bytes, first_chunk_size, summaries.data(), static_cast(summaries.size())), 0); ASSERT_EQ(XYParser_FeedAlgorithmData( parser.get(), input_bytes + first_chunk_size, second_chunk_size, summaries.data(), static_cast(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(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 frame1 = BuildMinimalFrame(8, 1U); const std::vector frame2 = BuildMinimalFrame(8, 2U); const std::vector frame3 = BuildMinimalFrame(8, 3U); std::array summaries{}; ASSERT_EQ(XYParser_Feed(parser.get(), frame1.data(), frame1.size(), summaries.data(), static_cast(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(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(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, XYPARSER_SAMPLES_PER_FRAME> raw_samples1{}; std::array, 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(XYPARSER_SAMPLES_PER_FRAME)) { raw_samples1[static_cast(sample)][0] = raw_value; } else { raw_samples2[static_cast(sample - XYPARSER_SAMPLES_PER_FRAME)][0] = raw_value; } } const std::vector frame1 = BuildFrameWithRawSamples(8, 1U, raw_samples1); const std::vector frame2 = BuildFrameWithRawSamples(8, 2U, raw_samples2); std::array summaries{}; std::array impedance{}; EXPECT_EQ(XYParser_Feed(parser.get(), frame1.data(), frame1.size(), summaries.data(), static_cast(summaries.size())), 1); EXPECT_EQ(XYParser_ReadImpedance(parser.get(), impedance.data(), static_cast(impedance.size())), 0); EXPECT_EQ(XYParser_Feed(parser.get(), frame2.data(), frame2.size(), summaries.data(), static_cast(summaries.size())), 1); ASSERT_EQ(XYParser_ReadImpedance(parser.get(), impedance.data(), static_cast(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, XYPARSER_SAMPLES_PER_FRAME> raw_samples1{}; std::array, 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(XYPARSER_SAMPLES_PER_FRAME)) { raw_samples1[static_cast(sample)][0] = raw_value; } else { raw_samples2[static_cast(sample - XYPARSER_SAMPLES_PER_FRAME)][0] = raw_value; } } const std::vector frame1 = BuildFrameWithRawSamples(8, 1U, raw_samples1); const std::vector frame2 = BuildFrameWithRawSamples(8, 2U, raw_samples2); std::array summaries{}; std::array impedance{}; ASSERT_EQ(XYParser_Feed(parser.get(), frame1.data(), frame1.size(), summaries.data(), static_cast(summaries.size())), 1); ASSERT_EQ(XYParser_Feed(parser.get(), frame2.data(), frame2.size(), summaries.data(), static_cast(summaries.size())), 1); ASSERT_EQ(XYParser_ReadImpedance(parser.get(), impedance.data(), static_cast(impedance.size())), 1); EXPECT_GT(impedance[0].impedance_values[LeadChannel_PO5], 0); EXPECT_EQ(XYParser_ReadImpedance(parser.get(), impedance.data(), static_cast(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, XYPARSER_SAMPLES_PER_FRAME> raw_samples1{}; std::array, 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(XYPARSER_SAMPLES_PER_FRAME)) { raw_samples1[static_cast(sample)][0] = raw_value; } else { raw_samples2[static_cast(sample - XYPARSER_SAMPLES_PER_FRAME)][0] = raw_value; } } const std::vector frame1 = BuildFrameWithRawSamples(64, 1U, raw_samples1); const std::vector frame2 = BuildFrameWithRawSamples(64, 2U, raw_samples2); std::array summaries{}; std::array impedance{}; EXPECT_EQ(XYParser_Feed(parser.get(), frame1.data(), frame1.size(), summaries.data(), static_cast(summaries.size())), 1); EXPECT_EQ(XYParser_Feed(parser.get(), frame2.data(), frame2.size(), summaries.data(), static_cast(summaries.size())), 1); ASSERT_EQ(XYParser_ReadImpedance(parser.get(), impedance.data(), static_cast(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 summaries{}; std::array impedance{}; for (const auto& frame : frames) { ASSERT_EQ(XYParser_Feed(parser.get(), frame.data(), frame.size(), summaries.data(), static_cast(summaries.size())), 1); } ASSERT_EQ(XYParser_ReadImpedance(parser.get(), impedance.data(), static_cast(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 summaries{}; std::array impedance{}; ASSERT_EQ(XYParser_Feed(parser.get(), full_frames[0].data(), full_frames[0].size(), summaries.data(), static_cast(summaries.size())), 1); EXPECT_EQ(XYParser_ReadImpedance(parser.get(), impedance.data(), static_cast(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(summaries.size())), 1); EXPECT_EQ(XYParser_ReadImpedance(parser.get(), impedance.data(), static_cast(impedance.size())), 0); ASSERT_EQ(XYParser_Feed(parser.get(), full_frames[1].data(), full_frames[1].size(), summaries.data(), static_cast(summaries.size())), 1); ASSERT_EQ(XYParser_ReadImpedance(parser.get(), impedance.data(), static_cast(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 summaries{}; std::array impedance{}; ASSERT_EQ(XYParser_Feed(parser.get(), frames_10hz_window[0].data(), frames_10hz_window[0].size(), summaries.data(), static_cast(summaries.size())), 1); EXPECT_EQ(XYParser_ReadImpedance(parser.get(), impedance.data(), static_cast(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(summaries.size())), 1); } ASSERT_EQ(XYParser_ReadImpedance(parser.get(), impedance.data(), static_cast(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(mixed_signal), static_cast(signal_10hz), std::max(1.0, static_cast(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(mixed_signal), static_cast(signal_10hz), std::max(1.0, static_cast(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 algorithm_data(static_cast(10 * XYPARSER_FRAME_DATA_COLUMN_COUNT), 0.0); for (int sample = 0; sample < 10; ++sample) { const std::size_t sample_offset = static_cast(sample) * XYPARSER_FRAME_DATA_COLUMN_COUNT; algorithm_data[sample_offset] = static_cast(BuildSineRawValue(sample, 10, 2.0, 1000000.0)); } std::array welch{}; EXPECT_EQ(XYParser_FeedAlgorithmData( parser.get(), reinterpret_cast(algorithm_data.data()), algorithm_data.size() * sizeof(double), nullptr, 0), 0); ASSERT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast(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 algorithm_data = BuildAlgorithmDataForSingleChannel( kSampleRate, 50.0, 1000000.0, 10.0, 100000.0); std::array welch{}; EXPECT_EQ(XYParser_FeedAlgorithmData( parser.get(), reinterpret_cast(algorithm_data.data()), algorithm_data.size() * sizeof(double), nullptr, 0), 0); ASSERT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast(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(peak_index)], 50.0); EXPECT_GT(welch[0].psd_values[LeadChannel_FP1][static_cast(frequency_50hz_index)], welch[0].psd_values[LeadChannel_FP1][static_cast(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 algorithm_data = BuildAlgorithmDataForSingleChannel( kSampleRate, 49.0, 1000000.0, 10.0, 100000.0); std::array welch{}; EXPECT_EQ(XYParser_FeedAlgorithmData( parser.get(), reinterpret_cast(algorithm_data.data()), algorithm_data.size() * sizeof(double), nullptr, 0), 0); ASSERT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast(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(peak_index)], 49.0); EXPECT_GT(welch[0].psd_values[LeadChannel_FP1][static_cast(frequency_49hz_index)], welch[0].psd_values[LeadChannel_FP1][static_cast(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 algorithm_data = BuildAlgorithmDataForSingleChannel( kSampleRate, 51.0, 1000000.0, 100.0, 500000.0); std::array welch{}; EXPECT_EQ(XYParser_FeedAlgorithmData( parser.get(), reinterpret_cast(algorithm_data.data()), algorithm_data.size() * sizeof(double), nullptr, 0), 0); ASSERT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast(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 algorithm_data = BuildAlgorithmDataForSingleChannel( kSampleRate, line_noise_hz, 1000000.0, 10.0, 100000.0); std::array welch{}; EXPECT_EQ(XYParser_FeedAlgorithmData( parser.get(), reinterpret_cast(algorithm_data.data()), algorithm_data.size() * sizeof(double), nullptr, 0), 0); ASSERT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast(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(peak_index)], line_noise_hz) << "line noise hz=" << line_noise_hz; EXPECT_GT(welch[0].psd_values[LeadChannel_FP1][static_cast(line_noise_index)], welch[0].psd_values[LeadChannel_FP1][static_cast(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 algorithm_data = BuildAlgorithmDataForSingleChannel( kSampleRate, 51.0, 1000000.0, 10.0, 100000.0); std::array welch{}; EXPECT_EQ(XYParser_FeedAlgorithmData( parser.get(), reinterpret_cast(algorithm_data.data()), algorithm_data.size() * sizeof(double), nullptr, 0), 0); ASSERT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast(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(frequency_50hz_index)], welch[0].psd_values[LeadChannel_FP1][static_cast(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 algorithm_data = BuildAlgorithmDataForSingleChannel( kSampleRate, 100.0, 1000000.0, 10.0, 100000.0); std::array welch{}; EXPECT_EQ(XYParser_FeedAlgorithmData( parser.get(), reinterpret_cast(algorithm_data.data()), algorithm_data.size() * sizeof(double), nullptr, 0), 0); ASSERT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast(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(frequency_10hz_index)], 0.0); EXPECT_LT(welch[0].psd_values[LeadChannel_FP1][static_cast(frequency_50hz_index)], welch[0].psd_values[LeadChannel_FP1][static_cast(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 full_data = BuildAlgorithmDataForSingleChannel(10, 2.0, 1000000.0); const std::size_t half_row_count = static_cast(XYPARSER_SAMPLES_PER_FRAME) * XYPARSER_FRAME_DATA_COLUMN_COUNT; const std::vector first_half(full_data.begin(), full_data.begin() + static_cast(half_row_count)); const std::vector second_half(full_data.begin() + static_cast(half_row_count), full_data.end()); std::array welch{}; EXPECT_EQ(XYParser_FeedAlgorithmData( parser.get(), reinterpret_cast(first_half.data()), first_half.size() * sizeof(double), nullptr, 0), 0); EXPECT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast(welch.size())), 0); XYParser_SetWelchDetection(parser.get(), 0); XYParser_SetWelchDetection(parser.get(), 1); EXPECT_EQ(XYParser_FeedAlgorithmData( parser.get(), reinterpret_cast(first_half.data()), first_half.size() * sizeof(double), nullptr, 0), 0); EXPECT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast(welch.size())), 0); EXPECT_EQ(XYParser_FeedAlgorithmData( parser.get(), reinterpret_cast(second_half.data()), second_half.size() * sizeof(double), nullptr, 0), 0); ASSERT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast(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 algorithm_data = BuildAlgorithmDataForTwoChannels( kSampleRate, 10.0, 1000000.0, 20.0, 1000000.0); std::array welch{}; EXPECT_EQ(XYParser_FeedAlgorithmData( parser.get(), reinterpret_cast(algorithm_data.data()), algorithm_data.size() * sizeof(double), nullptr, 0), 0); ASSERT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast(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(fp1_peak_index)], 10.0); EXPECT_DOUBLE_EQ(welch[0].frequencies[static_cast(fp2_peak_index)], 20.0); EXPECT_GT(welch[0].psd_values[LeadChannel_FP1][static_cast(frequency_10hz_index)], welch[0].psd_values[LeadChannel_FP1][static_cast(frequency_20hz_index)]); EXPECT_GT(welch[0].psd_values[LeadChannel_FP2][static_cast(frequency_20hz_index)], welch[0].psd_values[LeadChannel_FP2][static_cast(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 low_amplitude_data = BuildAlgorithmDataForSingleChannel( kSampleRate, 10.0, 100000.0); const std::vector high_amplitude_data = BuildAlgorithmDataForSingleChannel( kSampleRate, 10.0, 1000000.0); std::array low_welch{}; std::array high_welch{}; EXPECT_EQ(XYParser_FeedAlgorithmData( low_amplitude_parser.get(), reinterpret_cast(low_amplitude_data.data()), low_amplitude_data.size() * sizeof(double), nullptr, 0), 0); EXPECT_EQ(XYParser_FeedAlgorithmData( high_amplitude_parser.get(), reinterpret_cast(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(low_welch.size())), 1); ASSERT_EQ(XYParser_ReadWelch(high_amplitude_parser.get(), high_welch.data(), static_cast(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(high_frequency_10hz_index)], low_welch[0].psd_values[LeadChannel_FP1][static_cast(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 algorithm_data = BuildAlgorithmDataForSingleChannel(kSampleRate, kFrequencyHz, kPeakAmplitudeUv); std::array welch{}; EXPECT_EQ(XYParser_FeedAlgorithmData( parser.get(), reinterpret_cast(algorithm_data.data()), algorithm_data.size() * sizeof(double), nullptr, 0), 0); ASSERT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast(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(peak_index)], kFrequencyHz); EXPECT_DOUBLE_EQ(welch[0].frequencies[static_cast(frequency_10hz_index)], kFrequencyHz); const double peak_psd = welch[0].psd_values[LeadChannel_FP1][static_cast(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 algorithm_data = BuildAlgorithmDataForSingleChannel(10, 2.0, 1000000.0); std::array welch{}; EXPECT_EQ(XYParser_FeedAlgorithmData( parser.get(), reinterpret_cast(algorithm_data.data()), algorithm_data.size() * sizeof(double), nullptr, 0), 0); ASSERT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast(welch.size())), 1); EXPECT_EQ(welch[0].ok, 1); EXPECT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast(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 leads8{}; std::array leads64{}; ASSERT_EQ(XYParser_GetLeadMap(8, leads8.data(), static_cast(leads8.size())), 8); ASSERT_EQ(XYParser_GetLeadMap(64, leads64.data(), static_cast(leads64.size())), 64); std::array, XYPARSER_SAMPLES_PER_FRAME> raw_samples8{}; std::array, 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((sample_index + 1) * 1000 + static_cast(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 frame8 = BuildFrameWithRawSamples(8, 1U, raw_samples8); const std::vector frame64 = BuildFrameWithRawSamples(64, 1U, raw_samples64); std::array summaries8{}; std::array summaries64{}; std::array converted64{}; ASSERT_EQ(XYParser_Feed(parser8.get(), frame8.data(), frame8.size(), summaries8.data(), static_cast(summaries8.size())), 1); ASSERT_EQ(XYParser_Feed(parser64.get(), frame64.data(), frame64.size(), summaries64.data(), static_cast(summaries64.size())), 1); ASSERT_EQ(XYParser_Convert8ChFramesTo64Ch(summaries8.data(), 1, converted64.data(), 1), 1); std::vector converted_algorithm_data(XYPARSER_FRAME_ALGORITHM_VALUE_COUNT, 0.0); std::vector 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, XYPARSER_SAMPLES_PER_FRAME> raw_samples1{}; std::array, 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(XYPARSER_SAMPLES_PER_FRAME)) { raw_samples1[static_cast(sample)][0] = raw_value; } else { raw_samples2[static_cast(sample - XYPARSER_SAMPLES_PER_FRAME)][0] = raw_value; } } const std::vector frame1 = BuildFrameWithRawSamples(64, 1U, raw_samples1); const std::vector frame2 = BuildFrameWithRawSamples(64, 2U, raw_samples2); std::array summaries{}; std::array welch{}; EXPECT_EQ(XYParser_Feed(parser.get(), frame1.data(), frame1.size(), summaries.data(), static_cast(summaries.size())), 1); EXPECT_EQ(XYParser_Feed(parser.get(), frame2.data(), frame2.size(), summaries.data(), static_cast(summaries.size())), 1); EXPECT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast(welch.size())), 0); } TEST(XYParserApiTests, WelchDisabledDoesNotProduceResultsFromAlgorithmData) { ParserGuard parser(XYParser_CreateParser(64)); ASSERT_NE(parser.get(), nullptr); XYParser_SetSampleRate(parser.get(), 10); std::vector algorithm_data(static_cast(10 * XYPARSER_FRAME_DATA_COLUMN_COUNT), 0.0); for (int sample = 0; sample < 10; ++sample) { const std::size_t sample_offset = static_cast(sample) * XYPARSER_FRAME_DATA_COLUMN_COUNT; algorithm_data[sample_offset] = static_cast(BuildSineRawValue(sample, 10, 2.0, 1000000.0)); } std::array welch{}; EXPECT_EQ(XYParser_FeedAlgorithmData( parser.get(), reinterpret_cast(algorithm_data.data()), algorithm_data.size() * sizeof(double), nullptr, 0), 0); EXPECT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast(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 bytes = BuildMinimalFrame(8); std::array summaries{}; const int frame_count = XYParser_Feed( parser.get(), bytes.data(), bytes.size(), summaries.data(), static_cast(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 reserved = {0x01, 24, 2, 9, 1, 0x5A}; const std::vector bytes = BuildMinimalFrame(8, 1U, reserved); std::array summaries{}; ASSERT_EQ(XYParser_Feed( parser.get(), bytes.data(), bytes.size(), summaries.data(), static_cast(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 bytes = BuildMinimalFrame(8); const std::size_t split_index = bytes.size() / 2; std::array summaries{}; // 第一次 Feed:传入前半部分数据 const int first_result = XYParser_Feed( parser.get(), bytes.data(), split_index, summaries.data(), static_cast(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(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 buffer{}; const int size = XYParser_Serialize64ImpedanceCommand(1, buffer.data(), buffer.size()); ASSERT_EQ(size, static_cast(XYParser_Get64ImpedanceCommandSize())); const std::vector 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 buffer{}; const int size = XYParser_Serialize64GainSampleRateCommand(24, 1000, buffer.data(), buffer.size()); ASSERT_EQ(size, static_cast(XYParser_Get64GainSampleRateCommandSize())); const std::vector 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 buffer{}; EXPECT_EQ(XYParser_Serialize64GainSampleRateCommand(24, 256, buffer.data(), buffer.size()), 0); } /// 测试:64 导阻抗命令在输出缓冲区不足时返回失败 TEST(XYParserApiTests, Serialize64ImpedanceCommandRejectsSmallBuffer) { std::array 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 summaries{}; const int result = XYParser_Feed( parser.get(), nullptr, 0, summaries.data(), static_cast(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 header_only = {0xAA, 0x01, 0x00, 0x00, 0x00}; std::array summaries{}; const int result = XYParser_Feed( parser.get(), header_only.data(), header_only.size(), summaries.data(), static_cast(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 bytes = BuildMinimalFrame(64); std::array summaries{}; const int frame_count = XYParser_Feed( parser.get(), bytes.data(), bytes.size(), summaries.data(), static_cast(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 reserved = {0x00, 6, 1, 4, 0, 0x00}; const std::vector bytes = BuildMinimalFrame(64, 1U, reserved); std::array summaries{}; ASSERT_EQ(XYParser_Feed( parser.get(), bytes.data(), bytes.size(), summaries.data(), static_cast(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 frame1 = BuildMinimalFrame(8, 1U); const std::vector frame2 = BuildMinimalFrame(8, 2U); std::vector combined(frame1); combined.insert(combined.end(), frame2.begin(), frame2.end()); std::array summaries{}; const int frame_count = XYParser_Feed( parser.get(), combined.data(), combined.size(), summaries.data(), static_cast(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 summaries{}; int total_frames = 0; for (int i = 0; i < 3; ++i) { const std::vector frame = BuildMinimalFrame(8, static_cast(i + 1)); const int count = XYParser_Feed( parser.get(), frame.data(), frame.size(), summaries.data() + total_frames, static_cast(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(i + 1)); } } /// 测试:电池电量字段解析 TEST(XYParserApiTests, FeedParsesBatteryValue) { ParserGuard parser(XYParser_CreateParser(8)); ASSERT_NE(parser.get(), nullptr); XYParser_SetBypassChecksum(parser.get(), 1); const std::vector bytes = BuildMinimalFrame(8); std::array summaries{}; XYParser_Feed( parser.get(), bytes.data(), bytes.size(), summaries.data(), static_cast(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 bytes = BuildMinimalFrame(8); std::array 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(summaries.size())), 0); EXPECT_EQ(XYParser_Feed(parser.get(), bytes.data() + part1_size, part2_size, summaries.data(), static_cast(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(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()); }