2026-06-06 15:04:04 +08:00
|
|
|
|
// XYParser API 单元测试文件
|
|
|
|
|
|
// 测试 XYParser 库的核心功能,包括解析器创建、错误处理、帧解析等
|
|
|
|
|
|
|
2026-06-06 14:13:35 +08:00
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
|
|
#include "../XYParserApi.h"
|
|
|
|
|
|
|
2026-06-08 17:33:16 +08:00
|
|
|
|
#include <algorithm>
|
2026-06-06 14:13:35 +08:00
|
|
|
|
#include <array>
|
|
|
|
|
|
#include <cstddef>
|
|
|
|
|
|
#include <cstdint>
|
2026-06-08 17:33:16 +08:00
|
|
|
|
#include <cmath>
|
2026-06-06 14:13:35 +08:00
|
|
|
|
#include <string>
|
|
|
|
|
|
#include <vector>
|
|
|
|
|
|
|
2026-06-06 15:04:04 +08:00
|
|
|
|
// 匿名命名空间,包含测试辅助代码
|
2026-06-06 14:13:35 +08:00
|
|
|
|
namespace {
|
|
|
|
|
|
|
2026-06-06 15:04:04 +08:00
|
|
|
|
/// ParserGuard 类:RAII 封装,确保解析器资源自动释放
|
|
|
|
|
|
/// 当对象生命周期结束时自动调用 XYParser_DestroyParser 释放资源
|
2026-06-06 14:13:35 +08:00
|
|
|
|
class ParserGuard {
|
|
|
|
|
|
public:
|
2026-06-06 15:04:04 +08:00
|
|
|
|
/// 构造函数,接管解析器句柄的所有权
|
|
|
|
|
|
/// @param handle XYParser 解析器句柄
|
2026-06-06 14:13:35 +08:00
|
|
|
|
explicit ParserGuard(XYParserHandle handle) : handle_(handle) {}
|
|
|
|
|
|
|
2026-06-06 15:04:04 +08:00
|
|
|
|
/// 析构函数,自动释放解析器资源
|
2026-06-06 14:13:35 +08:00
|
|
|
|
~ParserGuard()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (handle_ != nullptr) {
|
|
|
|
|
|
XYParser_DestroyParser(handle_);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-06 15:04:04 +08:00
|
|
|
|
/// 获取解析器句柄
|
|
|
|
|
|
/// @return XYParser 解析器句柄
|
2026-06-06 14:13:35 +08:00
|
|
|
|
XYParserHandle get() const
|
|
|
|
|
|
{
|
|
|
|
|
|
return handle_;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private:
|
2026-06-06 15:04:04 +08:00
|
|
|
|
XYParserHandle handle_; ///< 解析器句柄
|
2026-06-06 14:13:35 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-06 15:04:04 +08:00
|
|
|
|
/// 构建最小帧数据的辅助函数
|
|
|
|
|
|
/// 生成符合 XYParser 协议格式的测试帧数据
|
|
|
|
|
|
/// @param channel_count 通道数量
|
2026-06-06 15:26:46 +08:00
|
|
|
|
/// @param frame_index 帧索引,小端写入标签区前 4 字节
|
2026-06-06 15:04:04 +08:00
|
|
|
|
/// @return 包含完整帧数据的字节向量
|
2026-06-09 16:12:42 +08:00
|
|
|
|
std::vector<std::uint8_t> BuildMinimalFrame(std::uint8_t channel_count,
|
|
|
|
|
|
std::uint32_t frame_index,
|
|
|
|
|
|
std::array<std::uint8_t, 6> reserved = {})
|
2026-06-06 14:13:35 +08:00
|
|
|
|
{
|
2026-06-06 15:04:04 +08:00
|
|
|
|
constexpr std::size_t kSamplesPerFrame = 5; ///< 每帧采样数
|
|
|
|
|
|
constexpr std::uint8_t kHeader = 0xAA; ///< 帧头标记
|
|
|
|
|
|
constexpr std::uint8_t kTail = 0x55; ///< 帧尾标记
|
|
|
|
|
|
constexpr std::size_t kTagLen = 25; ///< 标签长度
|
2026-06-06 14:13:35 +08:00
|
|
|
|
|
2026-06-06 15:04:04 +08:00
|
|
|
|
// 计算帧结构大小
|
2026-06-06 14:13:35 +08:00
|
|
|
|
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;
|
|
|
|
|
|
|
2026-06-06 15:04:04 +08:00
|
|
|
|
// 写入帧头
|
2026-06-06 14:13:35 +08:00
|
|
|
|
frame[offset++] = kHeader;
|
|
|
|
|
|
|
2026-06-06 15:26:46 +08:00
|
|
|
|
// 写入标签数据中的帧索引(小端序)
|
|
|
|
|
|
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);
|
2026-06-06 14:13:35 +08:00
|
|
|
|
|
2026-06-06 15:04:04 +08:00
|
|
|
|
// 写入负载长度(大端序)
|
2026-06-06 14:13:35 +08:00
|
|
|
|
frame[offset++] = static_cast<std::uint8_t>((payload_length >> 8) & 0xFF);
|
|
|
|
|
|
frame[offset++] = static_cast<std::uint8_t>(payload_length & 0xFF);
|
|
|
|
|
|
|
2026-06-06 15:04:04 +08:00
|
|
|
|
// 写入电池电量和通道数
|
2026-06-06 14:13:35 +08:00
|
|
|
|
frame[offset++] = 95;
|
|
|
|
|
|
frame[offset++] = channel_count;
|
|
|
|
|
|
|
2026-06-09 16:12:42 +08:00
|
|
|
|
// 写入姿态、生理量和保留字段
|
|
|
|
|
|
offset += 2 + 2 + 2 + 2 + 2;
|
|
|
|
|
|
for (std::uint8_t value : reserved) {
|
|
|
|
|
|
frame[offset++] = value;
|
|
|
|
|
|
}
|
2026-06-06 14:13:35 +08:00
|
|
|
|
|
2026-06-06 15:04:04 +08:00
|
|
|
|
// 写入采样数据
|
2026-06-06 14:13:35 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-06-06 15:04:04 +08:00
|
|
|
|
// 每个采样后的额外字节
|
2026-06-06 14:13:35 +08:00
|
|
|
|
frame[offset++] = 0x00;
|
|
|
|
|
|
frame[offset++] = 0x00;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-06 15:04:04 +08:00
|
|
|
|
// 写入帧尾
|
2026-06-06 14:13:35 +08:00
|
|
|
|
frame[offset++] = 0x00;
|
|
|
|
|
|
frame[offset++] = kTail;
|
|
|
|
|
|
frame[offset++] = kTail;
|
|
|
|
|
|
|
|
|
|
|
|
return frame;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-06 15:26:46 +08:00
|
|
|
|
/// 为现有调用点保留默认 frame_index=1 的便捷重载
|
|
|
|
|
|
std::vector<std::uint8_t> BuildMinimalFrame(std::uint8_t channel_count)
|
|
|
|
|
|
{
|
|
|
|
|
|
return BuildMinimalFrame(channel_count, 1U);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 17:33:16 +08:00
|
|
|
|
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,
|
2026-06-09 16:12:42 +08:00
|
|
|
|
const std::array<std::array<int, XYPARSER_MAX_CHANNELS>, XYPARSER_SAMPLES_PER_FRAME>& raw_samples,
|
|
|
|
|
|
std::array<std::uint8_t, 6> reserved = {})
|
2026-06-08 17:33:16 +08:00
|
|
|
|
{
|
|
|
|
|
|
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;
|
2026-06-09 16:12:42 +08:00
|
|
|
|
offset += 2 + 2 + 2 + 2 + 2;
|
|
|
|
|
|
for (std::uint8_t value : reserved) {
|
|
|
|
|
|
frame[offset++] = value;
|
|
|
|
|
|
}
|
2026-06-08 17:33:16 +08:00
|
|
|
|
|
|
|
|
|
|
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));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 23:28:50 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-09 16:12:42 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 23:28:50 +08:00
|
|
|
|
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)];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-06 14:13:35 +08:00
|
|
|
|
} // namespace
|
|
|
|
|
|
|
2026-06-06 15:04:04 +08:00
|
|
|
|
/// 测试:创建解析器时拒绝不支持的通道数
|
2026-06-06 14:13:35 +08:00
|
|
|
|
TEST(XYParserApiTests, CreateParserRejectsUnsupportedChannelCount)
|
|
|
|
|
|
{
|
2026-06-06 15:04:04 +08:00
|
|
|
|
// 7 通道是不支持的配置,应返回 nullptr
|
2026-06-06 14:13:35 +08:00
|
|
|
|
EXPECT_EQ(XYParser_CreateParser(7), nullptr);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-06 15:04:04 +08:00
|
|
|
|
/// 测试:对空解析器句柄调用 GetLastError 应返回正确错误信息
|
2026-06-06 14:13:35 +08:00
|
|
|
|
TEST(XYParserApiTests, GetLastErrorReturnsMessageForNullParser)
|
|
|
|
|
|
{
|
2026-06-06 15:04:04 +08:00
|
|
|
|
// 传入 nullptr 应返回 "invalid parser handle"
|
2026-06-06 14:13:35 +08:00
|
|
|
|
EXPECT_EQ(std::string(XYParser_GetLastError(nullptr)), std::string("invalid parser handle"));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 17:33:16 +08:00
|
|
|
|
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));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 22:58:58 +08:00
|
|
|
|
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()));
|
2026-06-09 16:12:42 +08:00
|
|
|
|
const std::array<std::uint8_t, 7> expected = {0xAA, 0x00, 0x00, 0xBB, 0xBB, 0x55, 0x55};
|
2026-06-08 22:58:58 +08:00
|
|
|
|
EXPECT_TRUE(std::equal(expected.begin(), expected.end(), command.begin()));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 23:28:50 +08:00
|
|
|
|
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()));
|
2026-06-09 16:12:42 +08:00
|
|
|
|
const std::array<std::uint8_t, 7> expected = {0xAA, 0x00, 0x00, 0xBC, 0xBC, 0x55, 0x55};
|
2026-06-08 23:28:50 +08:00
|
|
|
|
EXPECT_TRUE(std::equal(expected.begin(), expected.end(), command.begin()));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 22:58:58 +08:00
|
|
|
|
TEST(XYParserApiTests, SerializeTriggerCommandRejectsUnsupportedTriggerType)
|
|
|
|
|
|
{
|
|
|
|
|
|
std::array<std::uint8_t, 8> command{};
|
|
|
|
|
|
EXPECT_EQ(XYParser_SerializeTriggerCommand(0xAA, command.data(), command.size()), 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
TEST(XYParserApiTests, SerializeTriggerCommandRejectsSmallBuffer)
|
|
|
|
|
|
{
|
2026-06-09 16:12:42 +08:00
|
|
|
|
std::array<std::uint8_t, 6> command{};
|
2026-06-08 22:58:58 +08:00
|
|
|
|
EXPECT_EQ(XYParser_SerializeTriggerCommand(XYPARSER_TRIGGER_TRAIN_1, command.data(), command.size()), 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 17:33:16 +08:00
|
|
|
|
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;
|
2026-06-09 16:12:42 +08:00
|
|
|
|
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;
|
2026-06-08 17:33:16 +08:00
|
|
|
|
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);
|
2026-06-09 16:12:42 +08:00
|
|
|
|
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);
|
2026-06-08 17:33:16 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 23:28:50 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 17:33:16 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 23:28:50 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 17:33:16 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 23:28:50 +08:00
|
|
|
|
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]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-09 16:12:42 +08:00
|
|
|
|
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]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 23:28:50 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 17:33:16 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-06 15:04:04 +08:00
|
|
|
|
/// 测试:Feed 函数能正确解析完整的 8 通道帧
|
2026-06-06 14:13:35 +08:00
|
|
|
|
TEST(XYParserApiTests, FeedParsesAComplete8ChannelFrame)
|
|
|
|
|
|
{
|
2026-06-06 15:04:04 +08:00
|
|
|
|
// 创建 8 通道解析器
|
2026-06-06 14:13:35 +08:00
|
|
|
|
ParserGuard parser(XYParser_CreateParser(8));
|
|
|
|
|
|
ASSERT_NE(parser.get(), nullptr);
|
|
|
|
|
|
|
2026-06-06 15:04:04 +08:00
|
|
|
|
// 设置 ADC 参数和校验和绕过标志
|
2026-06-06 14:13:35 +08:00
|
|
|
|
XYParser_SetAdcParams(parser.get(), 4.5, 6.0);
|
|
|
|
|
|
XYParser_SetBypassChecksum(parser.get(), 1);
|
|
|
|
|
|
|
2026-06-06 15:04:04 +08:00
|
|
|
|
// 构建测试帧并解析
|
2026-06-06 14:13:35 +08:00
|
|
|
|
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()));
|
|
|
|
|
|
|
2026-06-06 15:04:04 +08:00
|
|
|
|
// 验证解析结果
|
|
|
|
|
|
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
|
2026-06-08 17:33:16 +08:00
|
|
|
|
EXPECT_EQ(summaries[0].sample_trigger_types[0], 0U); // 触发类型应为 0
|
|
|
|
|
|
EXPECT_EQ(summaries[0].sample_trigger_indices[0], 0U); // 触发索引应为 0
|
2026-06-06 14:13:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-09 16:12:42 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-06 15:04:04 +08:00
|
|
|
|
/// 测试:Feed 函数能缓冲部分数据直到完整帧可用
|
2026-06-06 14:13:35 +08:00
|
|
|
|
TEST(XYParserApiTests, FeedBuffersPartialDataUntilAFullFrameIsAvailable)
|
|
|
|
|
|
{
|
2026-06-06 15:04:04 +08:00
|
|
|
|
// 创建 8 通道解析器
|
2026-06-06 14:13:35 +08:00
|
|
|
|
ParserGuard parser(XYParser_CreateParser(8));
|
|
|
|
|
|
ASSERT_NE(parser.get(), nullptr);
|
|
|
|
|
|
|
2026-06-06 15:04:04 +08:00
|
|
|
|
// 设置校验和绕过标志
|
2026-06-06 14:13:35 +08:00
|
|
|
|
XYParser_SetBypassChecksum(parser.get(), 1);
|
|
|
|
|
|
|
2026-06-06 15:04:04 +08:00
|
|
|
|
// 构建测试帧并分成两部分
|
2026-06-06 14:13:35 +08:00
|
|
|
|
const std::vector<std::uint8_t> bytes = BuildMinimalFrame(8);
|
|
|
|
|
|
const std::size_t split_index = bytes.size() / 2;
|
|
|
|
|
|
std::array<XYParserFrameSummary, 1> summaries{};
|
|
|
|
|
|
|
2026-06-06 15:04:04 +08:00
|
|
|
|
// 第一次 Feed:传入前半部分数据
|
2026-06-06 14:13:35 +08:00
|
|
|
|
const int first_result = XYParser_Feed(
|
|
|
|
|
|
parser.get(),
|
|
|
|
|
|
bytes.data(),
|
|
|
|
|
|
split_index,
|
|
|
|
|
|
summaries.data(),
|
|
|
|
|
|
static_cast<int>(summaries.size()));
|
2026-06-06 15:04:04 +08:00
|
|
|
|
EXPECT_EQ(first_result, 0); // 数据不完整,不应解析出帧
|
2026-06-06 14:13:35 +08:00
|
|
|
|
|
2026-06-06 15:04:04 +08:00
|
|
|
|
// 第二次 Feed:传入后半部分数据
|
2026-06-06 14:13:35 +08:00
|
|
|
|
const int second_result = XYParser_Feed(
|
|
|
|
|
|
parser.get(),
|
|
|
|
|
|
bytes.data() + split_index,
|
|
|
|
|
|
bytes.size() - split_index,
|
|
|
|
|
|
summaries.data(),
|
|
|
|
|
|
static_cast<int>(summaries.size()));
|
2026-06-06 15:04:04 +08:00
|
|
|
|
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));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 17:33:16 +08:00
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-06 15:04:04 +08:00
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-09 16:12:42 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-06 15:04:04 +08:00
|
|
|
|
/// 测试:连续解析多个帧
|
|
|
|
|
|
TEST(XYParserApiTests, FeedParsesMultipleFrames)
|
|
|
|
|
|
{
|
|
|
|
|
|
ParserGuard parser(XYParser_CreateParser(8));
|
|
|
|
|
|
ASSERT_NE(parser.get(), nullptr);
|
|
|
|
|
|
|
|
|
|
|
|
XYParser_SetBypassChecksum(parser.get(), 1);
|
|
|
|
|
|
|
|
|
|
|
|
// 构建两个连续的帧
|
2026-06-06 15:26:46 +08:00
|
|
|
|
const std::vector<std::uint8_t> frame1 = BuildMinimalFrame(8, 1U);
|
|
|
|
|
|
const std::vector<std::uint8_t> frame2 = BuildMinimalFrame(8, 2U);
|
2026-06-06 15:04:04 +08:00
|
|
|
|
|
|
|
|
|
|
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) {
|
2026-06-06 15:26:46 +08:00
|
|
|
|
const std::vector<std::uint8_t> frame = BuildMinimalFrame(8, static_cast<std::uint32_t>(i + 1));
|
2026-06-06 15:04:04 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
// 析构时自动销毁所有解析器
|
|
|
|
|
|
}
|
2026-06-06 15:37:39 +08:00
|
|
|
|
|
2026-06-06 15:04:04 +08:00
|
|
|
|
/// 测试:GetLastError 在正常操作后
|
|
|
|
|
|
TEST(XYParserApiTests, GetLastErrorAfterSuccessfulCreate)
|
|
|
|
|
|
{
|
|
|
|
|
|
ParserGuard parser(XYParser_CreateParser(8));
|
|
|
|
|
|
ASSERT_NE(parser.get(), nullptr);
|
|
|
|
|
|
|
2026-06-06 15:48:46 +08:00
|
|
|
|
// 正常操作后,错误信息应为空(空字符串),不能是 nullptr
|
2026-06-06 15:04:04 +08:00
|
|
|
|
const char* error = XYParser_GetLastError(parser.get());
|
2026-06-06 15:48:46 +08:00
|
|
|
|
EXPECT_TRUE(std::string(error).empty());
|
2026-06-06 15:04:04 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-06 15:37:39 +08:00
|
|
|
|
|