2导
This commit is contained in:
10
XYParser/XYEegParser2.h
Normal file
10
XYParser/XYEegParser2.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "XYEegParserCommon.h"
|
||||
|
||||
using XYEegFrame2 = xyparser::XYEegFrame<2>;
|
||||
|
||||
class XYEegParser2 final : public xyparser::XYEegTcpParserCommon<2> {
|
||||
public:
|
||||
using Frame = XYEegFrame2;
|
||||
};
|
||||
@@ -11,6 +11,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "XYParser64Demo", "XYParserW
|
||||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "XYParser8Demo", "XYParserWorkflowDemo\XYParser8Demo.vcxproj", "{1B7FA4A1-8BC2-4D49-9B5A-BD4C6B8F2107}"
|
||||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "XYParser2Demo", "XYParserWorkflowDemo\XYParser2Demo.vcxproj", "{D2E0C130-89D7-4E3A-A2C8-4F86A9A71E23}"
|
||||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "XYAlgorithmUdpServer", "XYParserWorkflowDemo\XYAlgorithmUdpServer.vcxproj", "{6D6DCD3D-995A-4E79-9338-C1D36A3D2A61}"
|
||||
EndProject
|
||||
Global
|
||||
@@ -53,6 +55,14 @@ Global
|
||||
{1B7FA4A1-8BC2-4D49-9B5A-BD4C6B8F2107}.Release|x64.Build.0 = Release|x64
|
||||
{1B7FA4A1-8BC2-4D49-9B5A-BD4C6B8F2107}.Release|x86.ActiveCfg = Release|Win32
|
||||
{1B7FA4A1-8BC2-4D49-9B5A-BD4C6B8F2107}.Release|x86.Build.0 = Release|Win32
|
||||
{D2E0C130-89D7-4E3A-A2C8-4F86A9A71E23}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{D2E0C130-89D7-4E3A-A2C8-4F86A9A71E23}.Debug|x64.Build.0 = Debug|x64
|
||||
{D2E0C130-89D7-4E3A-A2C8-4F86A9A71E23}.Debug|x86.ActiveCfg = Debug|Win32
|
||||
{D2E0C130-89D7-4E3A-A2C8-4F86A9A71E23}.Debug|x86.Build.0 = Debug|Win32
|
||||
{D2E0C130-89D7-4E3A-A2C8-4F86A9A71E23}.Release|x64.ActiveCfg = Release|x64
|
||||
{D2E0C130-89D7-4E3A-A2C8-4F86A9A71E23}.Release|x64.Build.0 = Release|x64
|
||||
{D2E0C130-89D7-4E3A-A2C8-4F86A9A71E23}.Release|x86.ActiveCfg = Release|Win32
|
||||
{D2E0C130-89D7-4E3A-A2C8-4F86A9A71E23}.Release|x86.Build.0 = Release|Win32
|
||||
{6D6DCD3D-995A-4E79-9338-C1D36A3D2A61}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{6D6DCD3D-995A-4E79-9338-C1D36A3D2A61}.Debug|x64.Build.0 = Debug|x64
|
||||
{6D6DCD3D-995A-4E79-9338-C1D36A3D2A61}.Debug|x86.ActiveCfg = Debug|Win32
|
||||
|
||||
@@ -146,6 +146,7 @@
|
||||
<ClInclude Include="XYImpedanceProcessor.h" />
|
||||
<ClInclude Include="XYParserApi.h" />
|
||||
<ClInclude Include="XYWelchProcessor.h" />
|
||||
<ClInclude Include="XYEegParser2.h" />
|
||||
<ClInclude Include="XYEegParser8.h" />
|
||||
<ClInclude Include="XYEegParser64.h" />
|
||||
<ClInclude Include="XYEegParserCommon.h" />
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "XYImpedanceProcessor.h"
|
||||
#include "XYWelchProcessor.h"
|
||||
|
||||
#include "XYEegParser2.h"
|
||||
#include "XYEegParser8.h"
|
||||
#include "XYEegParser64.h"
|
||||
|
||||
@@ -20,6 +21,7 @@ constexpr std::uint8_t kCommandFrameTail = 0x55;
|
||||
constexpr std::size_t k8ChImpedanceCommandSize = 7;
|
||||
constexpr std::size_t kTriggerPayloadSize = 3;
|
||||
constexpr std::size_t kTriggerCommandSize = 1 + kTriggerPayloadSize + 1 + 2;
|
||||
constexpr int k2ChLeadCount = 2;
|
||||
constexpr int k8ChLeadCount = 8;
|
||||
constexpr int k64ChLeadCount = 64;
|
||||
constexpr std::uint8_t kAlgorithmChannelCount = 64;
|
||||
@@ -77,6 +79,9 @@ std::array<std::uint8_t, kTriggerCommandSize> BuildTriggerCommand(std::uint8_t t
|
||||
kCommandFrameTail};
|
||||
}
|
||||
|
||||
constexpr std::array<XYParserLeadChannelNumber, k2ChLeadCount> k2ChLeadMap = {
|
||||
LeadChannel_FP1, LeadChannel_FP2};
|
||||
|
||||
constexpr std::array<XYParserLeadChannelNumber, k8ChLeadCount> k8ChLeadMap = {
|
||||
LeadChannel_PO5, LeadChannel_POZ, LeadChannel_PO6, LeadChannel_PO7,
|
||||
LeadChannel_O1, LeadChannel_OZ, LeadChannel_O2, LeadChannel_PO8};
|
||||
@@ -101,6 +106,7 @@ constexpr std::array<XYParserLeadChannelNumber, k64ChLeadCount> k64ChLeadMap = {
|
||||
|
||||
struct ParserContext {
|
||||
std::uint8_t channel_count = 0;
|
||||
XYEegParser2 parser2;
|
||||
XYEegParser8 parser8;
|
||||
XYEegParser64 parser64;
|
||||
XYImpedanceProcessor impedance_processor;
|
||||
@@ -127,6 +133,29 @@ constexpr std::array<WelchBandInfo, XYPARSER_WELCH_BAND_COUNT> kWelchBandInfos =
|
||||
{"gamma", 30.0, 50.0},
|
||||
}};
|
||||
|
||||
void FillSummary(const XYEegFrame2& frame, XYParserFrameSummary& summary)
|
||||
{
|
||||
summary.frame_index = frame.index;
|
||||
summary.channel_count = frame.channel_count;
|
||||
summary.battery = frame.battery;
|
||||
summary.sample_count = static_cast<std::uint8_t>(frame.samples.size());
|
||||
summary.impedance_enabled = frame.impedance_enabled;
|
||||
summary.current_gain = frame.current_gain;
|
||||
summary.current_sample_rate_hz = frame.current_sample_rate_hz;
|
||||
summary.cap_type = frame.cap_type;
|
||||
summary.gnd_detached = frame.gnd_detached;
|
||||
for (std::size_t sample_index = 0; sample_index < XYPARSER_SAMPLES_PER_FRAME; ++sample_index) {
|
||||
summary.sample_trigger_types[sample_index] = frame.samples[sample_index].trigger_type;
|
||||
summary.sample_trigger_indices[sample_index] = frame.samples[sample_index].trigger_index;
|
||||
for (std::size_t channel_index = 0; channel_index < XYPARSER_MAX_CHANNELS; ++channel_index) {
|
||||
summary.channel_values_uv[sample_index][channel_index] =
|
||||
channel_index < frame.channel_count
|
||||
? frame.samples[sample_index].channel_values_uv[channel_index]
|
||||
: 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FillSummary(const XYEegFrame8& frame, XYParserFrameSummary& summary)
|
||||
{
|
||||
summary.frame_index = frame.index;
|
||||
@@ -173,6 +202,32 @@ void FillSummary(const XYEegFrame64& frame, XYParserFrameSummary& summary)
|
||||
}
|
||||
}
|
||||
|
||||
void Convert2ChSummaryTo64ChSummary(const XYParserFrameSummary& input_summary,
|
||||
XYParserFrameSummary& output_summary)
|
||||
{
|
||||
std::memset(&output_summary, 0, sizeof(output_summary));
|
||||
output_summary.frame_index = input_summary.frame_index;
|
||||
output_summary.channel_count = 64;
|
||||
output_summary.battery = input_summary.battery;
|
||||
output_summary.sample_count = input_summary.sample_count;
|
||||
output_summary.impedance_enabled = input_summary.impedance_enabled;
|
||||
output_summary.current_gain = input_summary.current_gain;
|
||||
output_summary.current_sample_rate_hz = input_summary.current_sample_rate_hz;
|
||||
output_summary.cap_type = input_summary.cap_type;
|
||||
output_summary.gnd_detached = input_summary.gnd_detached;
|
||||
|
||||
for (std::size_t sample_index = 0; sample_index < XYPARSER_SAMPLES_PER_FRAME; ++sample_index) {
|
||||
output_summary.sample_trigger_types[sample_index] = input_summary.sample_trigger_types[sample_index];
|
||||
output_summary.sample_trigger_indices[sample_index] = input_summary.sample_trigger_indices[sample_index];
|
||||
|
||||
for (std::size_t two_channel_index = 0; two_channel_index < k2ChLeadMap.size(); ++two_channel_index) {
|
||||
const XYParserLeadChannelNumber lead = k2ChLeadMap[two_channel_index];
|
||||
output_summary.channel_values_uv[sample_index][static_cast<std::size_t>(lead)] =
|
||||
input_summary.channel_values_uv[sample_index][two_channel_index];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Convert8ChSummaryTo64ChSummary(const XYParserFrameSummary& input_summary,
|
||||
XYParserFrameSummary& output_summary)
|
||||
{
|
||||
@@ -306,6 +361,8 @@ bool TryMapGain(std::uint8_t gain, XYEegParser64::GainSwitch& mapped_gain)
|
||||
int GetLeadMapCount(std::uint8_t channel_count)
|
||||
{
|
||||
switch (channel_count) {
|
||||
case 2:
|
||||
return k2ChLeadCount;
|
||||
case 8:
|
||||
return k8ChLeadCount;
|
||||
case 64:
|
||||
@@ -321,7 +378,7 @@ extern "C" {
|
||||
|
||||
XYParserHandle XYParser_CreateParser(std::uint8_t channel_count)
|
||||
{
|
||||
if (channel_count != 8 && channel_count != 64) {
|
||||
if (channel_count != 2 && channel_count != 8 && channel_count != 64) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -356,7 +413,9 @@ void XYParser_SetAdcParams(XYParserHandle handle, double vref, double gain)
|
||||
context->adc_gain = gain;
|
||||
}
|
||||
|
||||
if (context->channel_count == 8) {
|
||||
if (context->channel_count == 2) {
|
||||
context->parser2.SetAdcParams(vref, gain);
|
||||
} else if (context->channel_count == 8) {
|
||||
context->parser8.SetAdcParams(vref, gain);
|
||||
} else {
|
||||
context->parser64.SetAdcParams(vref, gain);
|
||||
@@ -371,7 +430,9 @@ void XYParser_SetBypassChecksum(XYParserHandle handle, int bypass)
|
||||
}
|
||||
|
||||
const bool enabled = bypass != 0;
|
||||
if (context->channel_count == 8) {
|
||||
if (context->channel_count == 2) {
|
||||
context->parser2.SetBypassChecksum(enabled);
|
||||
} else if (context->channel_count == 8) {
|
||||
context->parser8.SetBypassChecksum(enabled);
|
||||
} else {
|
||||
context->parser64.SetBypassChecksum(enabled);
|
||||
@@ -400,7 +461,9 @@ void XYParser_SetImpedanceDetection(XYParserHandle handle, int enabled)
|
||||
const double impedance_gain = impedance_enabled ? 24.0 : 6.0;
|
||||
context->adc_gain = impedance_gain;
|
||||
|
||||
if (context->channel_count == 8) {
|
||||
if (context->channel_count == 2) {
|
||||
context->parser2.SetAdcParams(context->adc_vref, impedance_gain);
|
||||
} else if (context->channel_count == 8) {
|
||||
context->parser8.SetAdcParams(context->adc_vref, impedance_gain);
|
||||
} else {
|
||||
context->parser64.SetAdcParams(context->adc_vref, impedance_gain);
|
||||
@@ -424,6 +487,11 @@ std::size_t XYParser_Get64ImpedanceCommandSize(void)
|
||||
return XYEegParser64::kImpedanceCommandSize;
|
||||
}
|
||||
|
||||
std::size_t XYParser_Get2ChImpedanceCommandSize(void)
|
||||
{
|
||||
return XYEegParser64::kImpedanceCommandSize;
|
||||
}
|
||||
|
||||
std::size_t XYParser_GetTriggerCommandSize(void)
|
||||
{
|
||||
return kTriggerCommandSize;
|
||||
@@ -447,6 +515,11 @@ std::size_t XYParser_Get64GainSampleRateCommandSize(void)
|
||||
return XYEegParser64::kGainSampleRateCommandSize;
|
||||
}
|
||||
|
||||
std::size_t XYParser_Get2ChGainSampleRateCommandSize(void)
|
||||
{
|
||||
return XYEegParser64::kGainSampleRateCommandSize;
|
||||
}
|
||||
|
||||
int XYParser_Serialize64ImpedanceCommand(int open,
|
||||
std::uint8_t* out_buffer,
|
||||
std::size_t buffer_size)
|
||||
@@ -459,6 +532,13 @@ int XYParser_Serialize64ImpedanceCommand(int open,
|
||||
return static_cast<int>(command.size());
|
||||
}
|
||||
|
||||
int XYParser_Serialize2ChImpedanceCommand(int open,
|
||||
std::uint8_t* out_buffer,
|
||||
std::size_t buffer_size)
|
||||
{
|
||||
return XYParser_Serialize64ImpedanceCommand(open, out_buffer, buffer_size);
|
||||
}
|
||||
|
||||
int XYParser_Serialize64GainSampleRateCommand(std::uint8_t gain,
|
||||
std::uint16_t sample_rate,
|
||||
std::uint8_t* out_buffer,
|
||||
@@ -477,6 +557,14 @@ int XYParser_Serialize64GainSampleRateCommand(std::uint8_t gain,
|
||||
return static_cast<int>(command.size());
|
||||
}
|
||||
|
||||
int XYParser_Serialize2ChGainSampleRateCommand(std::uint8_t gain,
|
||||
std::uint16_t sample_rate,
|
||||
std::uint8_t* out_buffer,
|
||||
std::size_t buffer_size)
|
||||
{
|
||||
return XYParser_Serialize64GainSampleRateCommand(gain, sample_rate, out_buffer, buffer_size);
|
||||
}
|
||||
|
||||
int XYParser_Feed(XYParserHandle handle,
|
||||
const std::uint8_t* data,
|
||||
std::size_t size,
|
||||
@@ -492,6 +580,21 @@ int XYParser_Feed(XYParserHandle handle,
|
||||
max_summaries = 0;
|
||||
}
|
||||
|
||||
if (context->channel_count == 2) {
|
||||
const std::vector<XYEegFrame2> frames = context->parser2.Feed(data, size);
|
||||
context->last_error = context->parser2.LastError();
|
||||
const int write_count = std::min<int>(static_cast<int>(frames.size()), max_summaries);
|
||||
for (std::size_t i = 0; i < frames.size(); ++i) {
|
||||
XYParserFrameSummary summary{};
|
||||
FillSummary(frames[i], summary);
|
||||
context->impedance_processor.PushFrame(summary);
|
||||
if (static_cast<int>(i) < write_count) {
|
||||
out_summaries[static_cast<int>(i)] = summary;
|
||||
}
|
||||
}
|
||||
return write_count;
|
||||
}
|
||||
|
||||
if (context->channel_count == 8) {
|
||||
const std::vector<XYEegFrame8> frames = context->parser8.Feed(data, size);
|
||||
context->last_error = context->parser8.LastError();
|
||||
@@ -540,6 +643,25 @@ int XYParser_Convert8ChFramesTo64Ch(const XYParserFrameSummary* input_summaries,
|
||||
return convert_count;
|
||||
}
|
||||
|
||||
int XYParser_Convert2ChFramesTo64Ch(const XYParserFrameSummary* input_summaries,
|
||||
int input_count,
|
||||
XYParserFrameSummary* output_summaries,
|
||||
int max_summaries)
|
||||
{
|
||||
if (input_summaries == nullptr || output_summaries == nullptr || input_count <= 0 || max_summaries <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const int convert_count = input_count < max_summaries ? input_count : max_summaries;
|
||||
for (int i = 0; i < convert_count; ++i) {
|
||||
if (input_summaries[i].channel_count != 2) {
|
||||
return i;
|
||||
}
|
||||
Convert2ChSummaryTo64ChSummary(input_summaries[i], output_summaries[i]);
|
||||
}
|
||||
return convert_count;
|
||||
}
|
||||
|
||||
std::size_t XYParser_GetAlgorithmDataValueCount()
|
||||
{
|
||||
return XYPARSER_FRAME_ALGORITHM_VALUE_COUNT;
|
||||
@@ -566,8 +688,7 @@ int XYParser_FeedAlgorithmData(XYParserHandle handle,
|
||||
int max_summaries)
|
||||
{
|
||||
ParserContext* context = static_cast<ParserContext*>(handle);
|
||||
if (context == nullptr || context->channel_count != kAlgorithmChannelCount ||
|
||||
data == nullptr || size == 0) {
|
||||
if (context == nullptr || data == nullptr || size == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -618,7 +739,7 @@ int XYParser_FeedAlgorithmData(XYParserHandle handle,
|
||||
void XYParser_ResetAlgorithmDataCache(XYParserHandle handle)
|
||||
{
|
||||
ParserContext* context = static_cast<ParserContext*>(handle);
|
||||
if (context == nullptr || context->channel_count != kAlgorithmChannelCount) {
|
||||
if (context == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -631,8 +752,7 @@ int XYParser_FlushAlgorithmData(XYParserHandle handle,
|
||||
XYParserFrameSummary* out_summary)
|
||||
{
|
||||
ParserContext* context = static_cast<ParserContext*>(handle);
|
||||
if (context == nullptr || context->channel_count != kAlgorithmChannelCount ||
|
||||
out_summary == nullptr || context->algorithm_sample_cache.empty()) {
|
||||
if (context == nullptr || out_summary == nullptr || context->algorithm_sample_cache.empty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -658,6 +778,11 @@ int XYParser_GetLeadMap(std::uint8_t channel_count,
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (channel_count == 2) {
|
||||
std::copy(k2ChLeadMap.begin(), k2ChLeadMap.end(), out_leads);
|
||||
return required_count;
|
||||
}
|
||||
|
||||
if (channel_count == 8) {
|
||||
std::copy(k8ChLeadMap.begin(), k8ChLeadMap.end(), out_leads);
|
||||
return required_count;
|
||||
|
||||
@@ -143,7 +143,7 @@ struct XYParserWelchSummary {
|
||||
};
|
||||
|
||||
// 创建解析器实例。
|
||||
// @param channel_count 解析器支持的导联数。
|
||||
// @param channel_count 解析器支持的导联数,当前支持 2/8/64。
|
||||
// @return 创建成功时返回解析器句柄,失败时返回 nullptr。
|
||||
XYPARSER_API XYParserHandle XYParser_CreateParser(std::uint8_t channel_count);
|
||||
|
||||
@@ -248,6 +248,10 @@ XYPARSER_API int XYParser_SerializeTriggerCommand(std::uint8_t trigger_type,
|
||||
// @return 64 导阻抗命令的字节长度。
|
||||
XYPARSER_API std::size_t XYParser_Get64ImpedanceCommandSize(void);
|
||||
|
||||
// 获取 2 导阻抗命令的字节长度。
|
||||
// @return 2 导阻抗命令的字节长度。
|
||||
XYPARSER_API std::size_t XYParser_Get2ChImpedanceCommandSize(void);
|
||||
|
||||
// 生成 64 导阻抗开关命令。
|
||||
// @param open 非 0 表示打开阻抗检测,0 表示关闭阻抗检测。
|
||||
// @param out_buffer 输出的命令缓冲区。
|
||||
@@ -257,10 +261,24 @@ XYPARSER_API int XYParser_Serialize64ImpedanceCommand(int open,
|
||||
std::uint8_t* out_buffer,
|
||||
std::size_t buffer_size);
|
||||
|
||||
// 生成 2 导阻抗开关命令。
|
||||
// 协议格式与 64 导控制命令一致,但单独保留 2 导接口用于区分 workflow。
|
||||
// @param open 非 0 表示打开阻抗检测,0 表示关闭阻抗检测。
|
||||
// @param out_buffer 输出的命令缓冲区。
|
||||
// @param buffer_size 输出缓冲区大小,单位为字节。
|
||||
// @return 生成成功时返回写入的字节数,失败时返回 0。
|
||||
XYPARSER_API int XYParser_Serialize2ChImpedanceCommand(int open,
|
||||
std::uint8_t* out_buffer,
|
||||
std::size_t buffer_size);
|
||||
|
||||
// 获取 64 导增益和采样率命令的字节长度。
|
||||
// @return 64 导增益和采样率命令的字节长度。
|
||||
XYPARSER_API std::size_t XYParser_Get64GainSampleRateCommandSize(void);
|
||||
|
||||
// 获取 2 导增益和采样率命令的字节长度。
|
||||
// @return 2 导增益和采样率命令的字节长度。
|
||||
XYPARSER_API std::size_t XYParser_Get2ChGainSampleRateCommandSize(void);
|
||||
|
||||
// 生成 64 导增益和采样率设置命令。
|
||||
// @param gain 要设置的增益值。
|
||||
// @param sample_rate 要设置的采样率。
|
||||
@@ -272,6 +290,18 @@ XYPARSER_API int XYParser_Serialize64GainSampleRateCommand(std::uint8_t gain,
|
||||
std::uint8_t* out_buffer,
|
||||
std::size_t buffer_size);
|
||||
|
||||
// 生成 2 导增益和采样率设置命令。
|
||||
// 协议格式与 64 导控制命令一致,但单独保留 2 导接口用于区分 workflow。
|
||||
// @param gain 要设置的增益值。
|
||||
// @param sample_rate 要设置的采样率。
|
||||
// @param out_buffer 输出的命令缓冲区。
|
||||
// @param buffer_size 输出缓冲区大小,单位为字节。
|
||||
// @return 生成成功时返回写入的字节数,失败时返回 0。
|
||||
XYPARSER_API int XYParser_Serialize2ChGainSampleRateCommand(std::uint8_t gain,
|
||||
std::uint16_t sample_rate,
|
||||
std::uint8_t* out_buffer,
|
||||
std::size_t buffer_size);
|
||||
|
||||
// Frame conversion and algorithm data output.
|
||||
// 将 8 导帧批量转换为 64 导帧,未映射的导联补 0。
|
||||
// @param input_summaries 输入的 8 导帧数组。
|
||||
@@ -284,6 +314,18 @@ XYPARSER_API int XYParser_Convert8ChFramesTo64Ch(const XYParserFrameSummary* inp
|
||||
XYParserFrameSummary* output_summaries,
|
||||
int max_summaries);
|
||||
|
||||
// 将 2 导帧批量转换为 64 导帧,未映射的导联补 0。
|
||||
// 2 导映射固定为 FP1、FP2。
|
||||
// @param input_summaries 输入的 2 导帧数组。
|
||||
// @param input_count 输入数组中的帧数。
|
||||
// @param output_summaries 输出的 64 导帧数组。
|
||||
// @param max_summaries 输出数组可写入的最大帧数,用于避免越界写入。
|
||||
// @return 实际成功转换的帧数;如果参数无效则返回 0,如果中途遇到非 2 导帧则返回已完成转换的数量。
|
||||
XYPARSER_API int XYParser_Convert2ChFramesTo64Ch(const XYParserFrameSummary* input_summaries,
|
||||
int input_count,
|
||||
XYParserFrameSummary* output_summaries,
|
||||
int max_summaries);
|
||||
|
||||
// 获取算法数据数组所需的元素数量。
|
||||
// @return 算法数据数组所需的元素数量。
|
||||
XYPARSER_API std::size_t XYParser_GetAlgorithmDataValueCount();
|
||||
|
||||
@@ -396,6 +396,12 @@ TEST(XYParserApiTests, CreateParserRejectsUnsupportedChannelCount)
|
||||
EXPECT_EQ(XYParser_CreateParser(7), nullptr);
|
||||
}
|
||||
|
||||
TEST(XYParserApiTests, CreateParserAccepts2ChannelCount)
|
||||
{
|
||||
ParserGuard parser(XYParser_CreateParser(2));
|
||||
EXPECT_NE(parser.get(), nullptr);
|
||||
}
|
||||
|
||||
/// 测试:对空解析器句柄调用 GetLastError 应返回正确错误信息
|
||||
TEST(XYParserApiTests, GetLastErrorReturnsMessageForNullParser)
|
||||
{
|
||||
@@ -484,11 +490,23 @@ TEST(XYParserApiTests, GetAlgorithmDataValueCountMatchesFrameLayout)
|
||||
|
||||
TEST(XYParserApiTests, GetLeadMapCountReturnsExpectedCounts)
|
||||
{
|
||||
EXPECT_EQ(XYParser_GetLeadMapCount(2), 2);
|
||||
EXPECT_EQ(XYParser_GetLeadMapCount(8), 8);
|
||||
EXPECT_EQ(XYParser_GetLeadMapCount(64), 64);
|
||||
EXPECT_EQ(XYParser_GetLeadMapCount(7), 0);
|
||||
}
|
||||
|
||||
TEST(XYParserApiTests, GetLeadMapReturnsExpected2ChannelSubset)
|
||||
{
|
||||
std::array<XYParserLeadChannelNumber, 2> leads{};
|
||||
const int count = XYParser_GetLeadMap(2, leads.data(), static_cast<int>(leads.size()));
|
||||
|
||||
ASSERT_EQ(count, 2);
|
||||
const std::array<XYParserLeadChannelNumber, 2> expected = {
|
||||
LeadChannel_FP1, LeadChannel_FP2};
|
||||
EXPECT_EQ(leads, expected);
|
||||
}
|
||||
|
||||
TEST(XYParserApiTests, GetLeadMapReturnsExpected8ChannelSubset)
|
||||
{
|
||||
std::array<XYParserLeadChannelNumber, 8> leads{};
|
||||
@@ -578,6 +596,34 @@ TEST(XYParserApiTests, Convert8ChFramesTo64ChMapsKnownLeadsAndPadsOthersWithZero
|
||||
EXPECT_DOUBLE_EQ(output[1].channel_values_uv[1][LeadChannel_PO5], 21.0);
|
||||
}
|
||||
|
||||
TEST(XYParserApiTests, Convert2ChFramesTo64ChMapsFp1Fp2AndPadsOthersWithZero)
|
||||
{
|
||||
XYParserFrameSummary input{};
|
||||
input.frame_index = 11U;
|
||||
input.channel_count = 2U;
|
||||
input.sample_count = 5U;
|
||||
input.impedance_enabled = 1U;
|
||||
input.current_gain = 24U;
|
||||
input.current_sample_rate_hz = 250U;
|
||||
input.cap_type = 4U;
|
||||
input.gnd_detached = 1U;
|
||||
input.sample_trigger_types[0] = 0xBB;
|
||||
input.sample_trigger_indices[0] = 1U;
|
||||
input.channel_values_uv[0][0] = 101.0;
|
||||
input.channel_values_uv[0][1] = 202.0;
|
||||
|
||||
XYParserFrameSummary output{};
|
||||
ASSERT_EQ(XYParser_Convert2ChFramesTo64Ch(&input, 1, &output, 1), 1);
|
||||
EXPECT_EQ(output.channel_count, 64U);
|
||||
EXPECT_EQ(output.channel_values_uv[0][LeadChannel_FP1], 101.0);
|
||||
EXPECT_EQ(output.channel_values_uv[0][LeadChannel_FP2], 202.0);
|
||||
EXPECT_DOUBLE_EQ(output.channel_values_uv[0][LeadChannel_PO6], 0.0);
|
||||
EXPECT_EQ(output.sample_trigger_types[0], 0xBB);
|
||||
EXPECT_EQ(output.sample_trigger_indices[0], 1U);
|
||||
EXPECT_EQ(output.current_gain, 24U);
|
||||
EXPECT_EQ(output.current_sample_rate_hz, 250U);
|
||||
}
|
||||
|
||||
TEST(XYParserApiTests, Convert8ChFramesTo64ChRejectsInvalidArgumentsAndStopsAtNon8ChannelInput)
|
||||
{
|
||||
std::array<XYParserFrameSummary, 2> input{};
|
||||
@@ -700,6 +746,40 @@ TEST(XYParserApiTests, FeedAlgorithmDataCachesSamplesBuildsFramesAndFlushesTailS
|
||||
EXPECT_EQ(XYParser_FlushAlgorithmData(parser.get(), &tail_summary), 0);
|
||||
}
|
||||
|
||||
TEST(XYParserApiTests, FeedAlgorithmDataAccepts2ChannelParserAndBuildsAlgorithmFrames)
|
||||
{
|
||||
ParserGuard parser(XYParser_CreateParser(2));
|
||||
ASSERT_NE(parser.get(), nullptr);
|
||||
|
||||
constexpr int kTotalSamples = 7;
|
||||
constexpr int kColumnCount = kAlgorithmReturnColumnCount;
|
||||
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 + static_cast<std::size_t>(LeadChannel_FP1)] = 100.0 + sample_index;
|
||||
input_data[row_offset + static_cast<std::size_t>(LeadChannel_FP2)] = 200.0 + sample_index;
|
||||
}
|
||||
|
||||
XYParserFrameSummary frame_summary{};
|
||||
ASSERT_EQ(
|
||||
XYParser_FeedAlgorithmData(parser.get(),
|
||||
reinterpret_cast<const std::uint8_t*>(input_data.data()),
|
||||
sizeof(input_data),
|
||||
&frame_summary,
|
||||
1),
|
||||
1);
|
||||
EXPECT_EQ(frame_summary.channel_count, 64U);
|
||||
EXPECT_EQ(frame_summary.sample_count, 5U);
|
||||
EXPECT_DOUBLE_EQ(frame_summary.channel_values_uv[0][LeadChannel_FP1], 100.0);
|
||||
EXPECT_DOUBLE_EQ(frame_summary.channel_values_uv[4][LeadChannel_FP2], 204.0);
|
||||
|
||||
XYParserFrameSummary tail_summary{};
|
||||
ASSERT_EQ(XYParser_FlushAlgorithmData(parser.get(), &tail_summary), 1);
|
||||
EXPECT_EQ(tail_summary.sample_count, 2U);
|
||||
EXPECT_DOUBLE_EQ(tail_summary.channel_values_uv[0][LeadChannel_FP1], 105.0);
|
||||
EXPECT_DOUBLE_EQ(tail_summary.channel_values_uv[1][LeadChannel_FP2], 206.0);
|
||||
}
|
||||
|
||||
TEST(XYParserApiTests, GetLeadNameReturnsExpectedNames)
|
||||
{
|
||||
EXPECT_EQ(std::string(XYParser_GetLeadName(LeadChannel_FP1)), "FP1");
|
||||
@@ -1865,6 +1945,21 @@ TEST(XYParserApiTests, Serialize64ImpedanceOpenCommandMatchesWirelessEegProtocol
|
||||
EXPECT_TRUE(std::equal(expected.begin(), expected.end(), buffer.begin()));
|
||||
}
|
||||
|
||||
TEST(XYParserApiTests, Serialize2ImpedanceOpenCommandMatchesWirelessEegProtocol)
|
||||
{
|
||||
std::array<std::uint8_t, 32> buffer{};
|
||||
const int size = XYParser_Serialize2ChImpedanceCommand(1, buffer.data(), buffer.size());
|
||||
|
||||
ASSERT_EQ(size, static_cast<int>(XYParser_Get2ChImpedanceCommandSize()));
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -1881,6 +1976,21 @@ TEST(XYParserApiTests, Serialize64GainAndSampleRateCommandMatchesWirelessEegProt
|
||||
EXPECT_TRUE(std::equal(expected.begin(), expected.end(), buffer.begin()));
|
||||
}
|
||||
|
||||
TEST(XYParserApiTests, Serialize2GainAndSampleRateCommandMatchesWirelessEegProtocol)
|
||||
{
|
||||
std::array<std::uint8_t, 32> buffer{};
|
||||
const int size = XYParser_Serialize2ChGainSampleRateCommand(24, 1000, buffer.data(), buffer.size());
|
||||
|
||||
ASSERT_EQ(size, static_cast<int>(XYParser_Get2ChGainSampleRateCommandSize()));
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -1961,6 +2071,28 @@ TEST(XYParserApiTests, FeedParses64ChannelFrame)
|
||||
EXPECT_EQ(summaries[0].channel_count, 64U);
|
||||
}
|
||||
|
||||
TEST(XYParserApiTests, FeedParses2ChannelFrame)
|
||||
{
|
||||
ParserGuard parser(XYParser_CreateParser(2));
|
||||
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(2);
|
||||
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, 2U);
|
||||
}
|
||||
|
||||
TEST(XYParserApiTests, FeedParsesReservedMetadataInto64ChannelSummary)
|
||||
{
|
||||
ParserGuard parser(XYParser_CreateParser(64));
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# XYParser64Demo / XYParser8Demo
|
||||
# XYParser64Demo / XYParser2Demo / XYParser8Demo
|
||||
|
||||
业务流程 demo,可直接连你的设备模拟器验证:
|
||||
|
||||
- `XYParser64Demo`:TCP 接收 64 导数据,可额外打开一个串口发送 `TRAIN_0 / TRAIN_1`
|
||||
- `XYParser2Demo`:TCP 接收 2 导数据,固定映射 `FP1/FP2`,送算法前扩展为 64 导
|
||||
- `XYParser8Demo`:串口接收 8 导数据
|
||||
- `XYAlgorithmUdpServer`:算法 ZMQ 服务端,接收 demo 发来的算法输入并回包
|
||||
- Welch/PSD 固定走 ZMQ:
|
||||
@@ -44,7 +45,7 @@
|
||||
联调顺序:
|
||||
|
||||
- 先启动 `XYAlgorithmUdpServer`
|
||||
- 再启动 `XYParser64Demo` 或 `XYParser8Demo`
|
||||
- 再启动 `XYParser64Demo`、`XYParser2Demo` 或 `XYParser8Demo`
|
||||
- 如果 demo 端 `rxPackets` 开始增长,说明 ZMQ 链路已打通
|
||||
|
||||
## 64 导示例
|
||||
@@ -72,6 +73,40 @@
|
||||
- `--algorithm-port 8100`
|
||||
- `--train-duration-ms 3000`
|
||||
|
||||
## 2 导示例
|
||||
|
||||
```powershell
|
||||
.\x64\Debug\XYParser2Demo.exe
|
||||
```
|
||||
|
||||
默认:
|
||||
|
||||
- 2 导数据走 TCP
|
||||
- 默认 TCP 主机为 `127.0.0.1`
|
||||
- 默认 TCP 端口为 `5086`
|
||||
- trigger 串口可选,不传则不打标
|
||||
- 启动后先按 2 导控制协议发送阻抗开启、`250Hz / 24增益`
|
||||
- 关闭阻抗后恢复 `250Hz / 6增益`
|
||||
- 算法前固定调用 `XYParser_Convert2ChFramesTo64Ch`
|
||||
- 2 个输入通道固定映射到 `FP1`、`FP2`
|
||||
|
||||
2 导到 64 导导联映射图:
|
||||
|
||||
```text
|
||||
2ch[0] -> FP1
|
||||
2ch[1] -> FP2
|
||||
others -> 0
|
||||
```
|
||||
|
||||
常用覆盖参数:
|
||||
|
||||
- `--tcp-host 127.0.0.1`
|
||||
- `--tcp-port 5086`
|
||||
- `--trigger-com COM44`
|
||||
- `--algorithm-host 127.0.0.1`
|
||||
- `--algorithm-port 8100`
|
||||
- `--train-duration-ms 3000`
|
||||
|
||||
## 8 导示例
|
||||
|
||||
```powershell
|
||||
|
||||
178
XYParser/XYParserWorkflowDemo/XYParser2Demo.vcxproj
Normal file
178
XYParser/XYParserWorkflowDemo/XYParser2Demo.vcxproj
Normal file
@@ -0,0 +1,178 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|Win32">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>Win32</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|Win32">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Win32</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>17.0</VCProjectVersion>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<ProjectGuid>{D2E0C130-89D7-4E3A-A2C8-4F86A9A71E23}</ProjectGuid>
|
||||
<RootNamespace>XYParser2Demo</RootNamespace>
|
||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings" />
|
||||
<ImportGroup Label="Shared" />
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup>
|
||||
<ZmqRoot>E:\Gitea\Swallow\XYParadigmDll\ZMQ</ZmqRoot>
|
||||
<ZmqIncludeDir>$(ZmqRoot)\include</ZmqIncludeDir>
|
||||
<ZmqLibDir>$(ZmqRoot)\msvclib</ZmqLibDir>
|
||||
<ZmqDllPath>$(ZmqLibDir)\libzmq-v142-mt-4_3_4.dll</ZmqDllPath>
|
||||
<SodiumDllPath>E:\Gitea\Swallow\SwallowBCI\release\decoder_mainSSVEP\_internal\libsodium.dll</SodiumDllPath>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\</OutDir>
|
||||
<IntDir>$(SolutionDir)$(Platform)\$(Configuration)\XYParserWorkflow2Demo\</IntDir>
|
||||
<TargetName>XYParser2Demo</TargetName>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>$(ZmqIncludeDir);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<AdditionalLibraryDirectories>$(ZmqLibDir);%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
<AdditionalDependencies>libzmq-v142-mt-4_3_4.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
<PostBuildEvent>
|
||||
<Command>if exist "$(ZmqDllPath)" (copy /Y "$(ZmqDllPath)" "$(OutDir)" >nul 2>nul || echo Skip copying libzmq dll)
|
||||
if exist "$(SodiumDllPath)" (copy /Y "$(SodiumDllPath)" "$(OutDir)" >nul 2>nul || echo Skip copying libsodium dll)
|
||||
exit /b 0</Command>
|
||||
</PostBuildEvent>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>WIN32;_DEBUG;_CONSOLE;XY_WORKFLOW_DEMO_2;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
<AdditionalIncludeDirectories>$(SolutionDir);$(ProjectDir);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<AdditionalDependencies>Ws2_32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>WIN32;NDEBUG;_CONSOLE;XY_WORKFLOW_DEMO_2;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
<AdditionalIncludeDirectories>$(SolutionDir);$(ProjectDir);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<AdditionalDependencies>Ws2_32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>_DEBUG;_CONSOLE;XY_WORKFLOW_DEMO_2;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
<AdditionalIncludeDirectories>$(SolutionDir);$(ProjectDir);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<AdditionalDependencies>Ws2_32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>NDEBUG;_CONSOLE;XY_WORKFLOW_DEMO_2;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
<AdditionalIncludeDirectories>$(SolutionDir);$(ProjectDir);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<AdditionalDependencies>Ws2_32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="main.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\XYParser.vcxproj">
|
||||
<Project>{CB1FF804-BB1F-41C8-92FA-7B15F6B86347}</Project>
|
||||
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
|
||||
<LinkLibraryDependencies>true</LinkLibraryDependencies>
|
||||
<UseLibraryDependencyInputs>false</UseLibraryDependencyInputs>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets" />
|
||||
</Project>
|
||||
@@ -26,7 +26,8 @@ using Clock = std::chrono::steady_clock;
|
||||
|
||||
enum class DemoMode {
|
||||
Mode64Tcp,
|
||||
Mode8Serial
|
||||
Mode8Serial,
|
||||
Mode2Tcp
|
||||
};
|
||||
|
||||
#if defined(XY_WORKFLOW_DEMO_64)
|
||||
@@ -35,14 +36,23 @@ constexpr wchar_t kProgramName[] = L"XYParser64Demo";
|
||||
constexpr char kDefaultTcpHost[] = "127.0.0.1";
|
||||
constexpr wchar_t kDefaultDataComPort[] = L"";
|
||||
constexpr wchar_t kDefaultTriggerComPort[] = L"COM44";
|
||||
constexpr double kDefaultVref = 4.5;
|
||||
#elif defined(XY_WORKFLOW_DEMO_2)
|
||||
constexpr DemoMode kDemoMode = DemoMode::Mode2Tcp;
|
||||
constexpr wchar_t kProgramName[] = L"XYParser2Demo";
|
||||
constexpr char kDefaultTcpHost[] = "127.0.0.1";
|
||||
constexpr wchar_t kDefaultDataComPort[] = L"";
|
||||
constexpr wchar_t kDefaultTriggerComPort[] = L"";
|
||||
constexpr double kDefaultVref = 2.42;
|
||||
#elif defined(XY_WORKFLOW_DEMO_8)
|
||||
constexpr DemoMode kDemoMode = DemoMode::Mode8Serial;
|
||||
constexpr wchar_t kProgramName[] = L"XYParser8Demo";
|
||||
constexpr char kDefaultTcpHost[] = "127.0.0.1";
|
||||
constexpr wchar_t kDefaultDataComPort[] = L"COM44";
|
||||
constexpr wchar_t kDefaultTriggerComPort[] = L"";
|
||||
constexpr double kDefaultVref = 4.5;
|
||||
#else
|
||||
#error Define either XY_WORKFLOW_DEMO_64 or XY_WORKFLOW_DEMO_8 for this target.
|
||||
#error Define either XY_WORKFLOW_DEMO_64, XY_WORKFLOW_DEMO_2 or XY_WORKFLOW_DEMO_8 for this target.
|
||||
#endif
|
||||
|
||||
struct ZmqRoundTripStats {
|
||||
@@ -58,7 +68,7 @@ struct DemoOptions {
|
||||
DemoMode mode = kDemoMode;
|
||||
std::string tcp_host = kDefaultTcpHost;
|
||||
int tcp_port = 5086;
|
||||
std::string algorithm_host = "192.168.254.102";
|
||||
std::string algorithm_host = "127.0.0.1";
|
||||
int algorithm_port = 8100;
|
||||
int algorithm_timeout_ms = 1000;
|
||||
std::wstring data_com_port = kDefaultDataComPort;
|
||||
@@ -66,7 +76,7 @@ struct DemoOptions {
|
||||
int serial_baud_rate = 460800;
|
||||
int sample_rate = 250;
|
||||
int gain = 6;
|
||||
double vref = 4.5;
|
||||
double vref = kDefaultVref;
|
||||
int bypass_checksum = 1;
|
||||
int train_duration_ms = 3000;
|
||||
};
|
||||
@@ -623,7 +633,7 @@ void PrintUsage()
|
||||
{
|
||||
std::wcout
|
||||
<< kProgramName << L" usage:\n";
|
||||
#if defined(XY_WORKFLOW_DEMO_64)
|
||||
#if defined(XY_WORKFLOW_DEMO_64) || defined(XY_WORKFLOW_DEMO_2)
|
||||
std::wcout
|
||||
<< L" " << kProgramName << L" [--tcp-host 127.0.0.1] [--tcp-port 5086]\n"
|
||||
<< L" [--trigger-com COM44]\n"
|
||||
@@ -641,6 +651,9 @@ void PrintUsage()
|
||||
#if defined(XY_WORKFLOW_DEMO_64)
|
||||
<< L" - Default TCP host is 127.0.0.1 and default trigger serial is COM44.\n"
|
||||
<< L" - Receive 64ch stream from TCP, optional trigger serial for TRAIN_0/TRAIN_1.\n"
|
||||
#elif defined(XY_WORKFLOW_DEMO_2)
|
||||
<< L" - Default TCP host is 127.0.0.1 and trigger serial is optional.\n"
|
||||
<< L" - Receive 2ch stream from TCP, map FP1/FP2 to 64ch before algorithm and Welch.\n"
|
||||
#else
|
||||
<< L" - Default 8ch data serial is COM44.\n"
|
||||
<< L" - Receive 8ch stream from serial, then run 8ch -> 64ch -> algorithm data -> Welch.\n"
|
||||
@@ -705,7 +718,7 @@ bool ParseArguments(int argc, wchar_t* argv[], DemoOptions& options)
|
||||
return false;
|
||||
}
|
||||
|
||||
#if defined(XY_WORKFLOW_DEMO_64)
|
||||
#if defined(XY_WORKFLOW_DEMO_64) || defined(XY_WORKFLOW_DEMO_2)
|
||||
return options.tcp_port > 0;
|
||||
#else
|
||||
return !options.data_com_port.empty();
|
||||
@@ -832,21 +845,54 @@ void PrintWelchSummary(const XYParserWelchSummary& summary)
|
||||
const char* gamma_name = XYParser_GetWelchBandName(4);
|
||||
const char* lead_name = XYParser_GetLeadName(lead);
|
||||
|
||||
std::cout << "[Welch] sampleRate=" << summary.sample_rate
|
||||
<< " window=" << summary.window_sample_count
|
||||
<< " freqCount=" << summary.frequency_count
|
||||
<< " lead=" << (lead_name != nullptr ? lead_name : "lead?");
|
||||
std::ostringstream stream;
|
||||
stream << "[Welch] sampleRate=" << summary.sample_rate
|
||||
<< " window=" << summary.window_sample_count
|
||||
<< " freqCount=" << summary.frequency_count
|
||||
<< " lead=" << (lead_name != nullptr ? lead_name : "lead?");
|
||||
if (peak_index >= 0) {
|
||||
std::cout << " peakHz=" << summary.frequencies[static_cast<std::size_t>(peak_index)]
|
||||
<< " peakPsd=" << summary.psd_values[lead][static_cast<std::size_t>(peak_index)];
|
||||
stream << " peakHz=" << summary.frequencies[static_cast<std::size_t>(peak_index)]
|
||||
<< " peakPsd=" << summary.psd_values[lead][static_cast<std::size_t>(peak_index)];
|
||||
}
|
||||
std::cout << ' ' << (alpha_name != nullptr ? alpha_name : "alpha")
|
||||
<< '=' << summary.band_values[2][lead]
|
||||
<< ' ' << (gamma_name != nullptr ? gamma_name : "gamma")
|
||||
<< '=' << summary.band_values[4][lead]
|
||||
<< std::endl;
|
||||
stream << ' ' << (alpha_name != nullptr ? alpha_name : "alpha")
|
||||
<< '=' << summary.band_values[2][lead]
|
||||
<< ' ' << (gamma_name != nullptr ? gamma_name : "gamma")
|
||||
<< '=' << summary.band_values[4][lead];
|
||||
|
||||
static std::string last_welch_line;
|
||||
const std::string current_line = stream.str();
|
||||
if (current_line == last_welch_line) {
|
||||
return;
|
||||
}
|
||||
|
||||
last_welch_line = current_line;
|
||||
std::cout << current_line << std::endl;
|
||||
}
|
||||
|
||||
#if defined(XY_WORKFLOW_DEMO_2)
|
||||
void Print2ChAlgorithmPreview(const char* tag,
|
||||
const double* values,
|
||||
std::size_t sample_count,
|
||||
std::size_t stride)
|
||||
{
|
||||
if (values == nullptr || stride < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::ostringstream stream;
|
||||
stream << tag;
|
||||
const std::size_t preview_count = (sample_count < 5) ? sample_count : 5;
|
||||
for (std::size_t sample_index = 0; sample_index < preview_count; ++sample_index) {
|
||||
const std::size_t sample_offset = sample_index * stride;
|
||||
stream << " s" << sample_index
|
||||
<< "(FP1=" << values[sample_offset + static_cast<std::size_t>(LeadChannel_FP1)]
|
||||
<< ",FP2=" << values[sample_offset + static_cast<std::size_t>(LeadChannel_FP2)]
|
||||
<< ')';
|
||||
}
|
||||
std::cout << stream.str() << std::endl;
|
||||
}
|
||||
#endif
|
||||
|
||||
void DrainImpedance(XYParserHandle parser)
|
||||
{
|
||||
std::array<XYParserImpedanceSummary, 4> summaries{};
|
||||
@@ -933,11 +979,26 @@ bool RunAlgorithmZmqRoundTrip(XYParserHandle parser,
|
||||
std::cout << "[AlgorithmZmqRx] payloadBytes=" << received
|
||||
<< " doubles=" << (received / static_cast<int>(sizeof(double)))
|
||||
<< std::endl;
|
||||
XYParser_FeedAlgorithmData(parser,
|
||||
response_payload.data(),
|
||||
response_payload.size(),
|
||||
nullptr,
|
||||
0);
|
||||
#if defined(XY_WORKFLOW_DEMO_2)
|
||||
static int rx_preview_count = 0;
|
||||
const std::size_t rx_sample_count =
|
||||
response_payload.size() / (static_cast<std::size_t>(XYPARSER_MAX_CHANNELS) * sizeof(double));
|
||||
if (rx_preview_count < 3 && rx_sample_count > 0) {
|
||||
Print2ChAlgorithmPreview("[AlgorithmRxPreview]",
|
||||
reinterpret_cast<const double*>(response_payload.data()),
|
||||
rx_sample_count,
|
||||
XYPARSER_MAX_CHANNELS);
|
||||
++rx_preview_count;
|
||||
}
|
||||
#endif
|
||||
const int algorithm_frame_count = XYParser_FeedAlgorithmData(parser,
|
||||
response_payload.data(),
|
||||
response_payload.size(),
|
||||
nullptr,
|
||||
0);
|
||||
#if defined(XY_WORKFLOW_DEMO_2)
|
||||
std::cout << "[AlgorithmFeed] frameCount=" << algorithm_frame_count << std::endl;
|
||||
#endif
|
||||
}
|
||||
|
||||
if (received_any_response || stats.sent_packets <= 3 || (stats.sent_packets % 100) == 0) {
|
||||
@@ -960,6 +1021,20 @@ bool Send64GainAndSampleRate(TcpClient& client, int gain, int sample_rate)
|
||||
return client.Send(command.data(), static_cast<std::size_t>(size));
|
||||
}
|
||||
|
||||
bool Send2GainAndSampleRate(TcpClient& client, int gain, int sample_rate)
|
||||
{
|
||||
std::array<std::uint8_t, 32> command{};
|
||||
const int size = XYParser_Serialize2ChGainSampleRateCommand(static_cast<std::uint8_t>(gain),
|
||||
static_cast<std::uint16_t>(sample_rate),
|
||||
command.data(),
|
||||
command.size());
|
||||
if (size <= 0) {
|
||||
return false;
|
||||
}
|
||||
PrintBytes("Send2GainSampleRate", command.data(), static_cast<std::size_t>(size));
|
||||
return client.Send(command.data(), static_cast<std::size_t>(size));
|
||||
}
|
||||
|
||||
bool Send64ImpedanceSwitch(TcpClient& client, bool open)
|
||||
{
|
||||
std::array<std::uint8_t, 32> command{};
|
||||
@@ -973,6 +1048,19 @@ bool Send64ImpedanceSwitch(TcpClient& client, bool open)
|
||||
return client.Send(command.data(), static_cast<std::size_t>(size));
|
||||
}
|
||||
|
||||
bool Send2ImpedanceSwitch(TcpClient& client, bool open)
|
||||
{
|
||||
std::array<std::uint8_t, 32> command{};
|
||||
const int size = XYParser_Serialize2ChImpedanceCommand(open ? 1 : 0, command.data(), command.size());
|
||||
if (size <= 0) {
|
||||
return false;
|
||||
}
|
||||
PrintBytes(open ? "Send2ImpedanceOpen" : "Send2ImpedanceClose",
|
||||
command.data(),
|
||||
static_cast<std::size_t>(size));
|
||||
return client.Send(command.data(), static_cast<std::size_t>(size));
|
||||
}
|
||||
|
||||
bool Send8ImpedanceSwitch(SerialPort& port, bool open)
|
||||
{
|
||||
std::array<std::uint8_t, 8> command{};
|
||||
@@ -1012,6 +1100,36 @@ bool Process8AlgorithmPath(XYParserHandle parser,
|
||||
return Process64AlgorithmPath(parser, converted, zmq_client, zmq_stats);
|
||||
}
|
||||
|
||||
bool Process2AlgorithmPath(XYParserHandle parser,
|
||||
const XYParserFrameSummary& summary,
|
||||
ZmqDuplexClient& zmq_client,
|
||||
ZmqRoundTripStats& zmq_stats)
|
||||
{
|
||||
XYParserFrameSummary converted{};
|
||||
if (XYParser_Convert2ChFramesTo64Ch(&summary, 1, &converted, 1) != 1) {
|
||||
std::cerr << "Convert2ChFramesTo64Ch failed" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::array<double, XYPARSER_FRAME_ALGORITHM_VALUE_COUNT> algorithm_data{};
|
||||
if (XYParser_ConvertSampleFramesToAlgorithmData(&converted, algorithm_data.data()) == 0) {
|
||||
std::cerr << "ConvertSampleFramesToAlgorithmData failed: " << DescribeParserError(parser) << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
#if defined(XY_WORKFLOW_DEMO_2)
|
||||
static int tx_preview_count = 0;
|
||||
if (tx_preview_count < 3) {
|
||||
Print2ChAlgorithmPreview("[AlgorithmTxPreview]",
|
||||
algorithm_data.data(),
|
||||
XYPARSER_SAMPLES_PER_FRAME,
|
||||
XYPARSER_FRAME_DATA_COLUMN_COUNT);
|
||||
++tx_preview_count;
|
||||
}
|
||||
#endif
|
||||
return RunAlgorithmZmqRoundTrip(parser, zmq_client, algorithm_data.data(), zmq_stats);
|
||||
}
|
||||
|
||||
bool SendTrigger(SerialPort& port, XYParserTriggerType trigger_type)
|
||||
{
|
||||
std::array<std::uint8_t, 8> command{};
|
||||
@@ -1031,7 +1149,7 @@ void PrintFrameProgress(const XYParserFrameSummary* summaries, int count)
|
||||
return;
|
||||
}
|
||||
const XYParserFrameSummary& last = summaries[static_cast<std::size_t>(count - 1)];
|
||||
#if defined(XY_WORKFLOW_DEMO_64)
|
||||
#if defined(XY_WORKFLOW_DEMO_64) || defined(XY_WORKFLOW_DEMO_2)
|
||||
struct DeviceStateSnapshot {
|
||||
std::uint8_t impedance_enabled = 0;
|
||||
std::uint8_t current_gain = 0;
|
||||
@@ -1347,6 +1465,205 @@ int Run64Workflow(const DemoOptions& options)
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Run2Workflow(const DemoOptions& options)
|
||||
{
|
||||
constexpr auto kImpedanceDuration = std::chrono::seconds(10);
|
||||
constexpr int kImpedanceSampleRate = 250;
|
||||
constexpr int kImpedanceGain = 24;
|
||||
constexpr int kNormalSampleRate = 250;
|
||||
constexpr int kNormalGain = 6;
|
||||
WinsockRuntime winsock;
|
||||
if (!winsock.ok()) {
|
||||
std::cerr << "WSAStartup failed" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
TcpClient data_client;
|
||||
if (!data_client.Connect(options.tcp_host, options.tcp_port)) {
|
||||
std::cerr << "Connect 2ch TCP failed: " << options.tcp_host << ':' << options.tcp_port << std::endl;
|
||||
return 1;
|
||||
}
|
||||
bool tcp_connected = true;
|
||||
const auto close_data_tcp = [&]() {
|
||||
if (!tcp_connected) {
|
||||
return;
|
||||
}
|
||||
std::cout << "Close 2ch TCP connection" << std::endl;
|
||||
data_client.Close();
|
||||
tcp_connected = false;
|
||||
};
|
||||
|
||||
SerialPort trigger_port;
|
||||
const bool has_trigger_port = !options.trigger_com_port.empty();
|
||||
bool trigger_serial_open = false;
|
||||
const auto close_trigger_serial = [&]() {
|
||||
if (!trigger_serial_open) {
|
||||
return;
|
||||
}
|
||||
std::cout << "Close trigger serial" << std::endl;
|
||||
trigger_port.Close();
|
||||
trigger_serial_open = false;
|
||||
};
|
||||
if (has_trigger_port && !trigger_port.Open(options.trigger_com_port, options.serial_baud_rate)) {
|
||||
std::cerr << "Open trigger serial failed: " << Narrow(options.trigger_com_port) << std::endl;
|
||||
close_data_tcp();
|
||||
return 1;
|
||||
}
|
||||
trigger_serial_open = has_trigger_port;
|
||||
|
||||
ParserHandleGuard parser(XYParser_CreateParser(2));
|
||||
if (parser.get() == nullptr) {
|
||||
std::cerr << "Create 2ch parser failed" << std::endl;
|
||||
close_data_tcp();
|
||||
return 1;
|
||||
}
|
||||
|
||||
XYParser_SetAdcParams(parser.get(), options.vref, static_cast<double>(kNormalGain));
|
||||
XYParser_SetSampleRate(parser.get(), kNormalSampleRate);
|
||||
XYParser_SetBypassChecksum(parser.get(), options.bypass_checksum);
|
||||
XYParser_SetWelchDetection(parser.get(), 0);
|
||||
XYParser_SetImpedanceDetection(parser.get(), 0);
|
||||
|
||||
if (!Send2ImpedanceSwitch(data_client, true)) {
|
||||
std::cerr << "Send 2ch impedance open command failed" << std::endl;
|
||||
close_data_tcp();
|
||||
return 1;
|
||||
}
|
||||
if (!Send2GainAndSampleRate(data_client, kImpedanceGain, kImpedanceSampleRate)) {
|
||||
std::cerr << "Send 2ch impedance gain/sample-rate command failed" << std::endl;
|
||||
close_data_tcp();
|
||||
return 1;
|
||||
}
|
||||
XYParser_SetSampleRate(parser.get(), kImpedanceSampleRate);
|
||||
XYParser_SetImpedanceDetection(parser.get(), 1);
|
||||
|
||||
ZmqDuplexClient algorithm_zmq;
|
||||
bool algorithm_zmq_open = false;
|
||||
const auto close_algorithm_zmq = [&]() {
|
||||
if (!algorithm_zmq_open) {
|
||||
return;
|
||||
}
|
||||
std::cout << "Close algorithm ZMQ" << std::endl;
|
||||
algorithm_zmq.Close();
|
||||
algorithm_zmq_open = false;
|
||||
};
|
||||
const auto close_all = [&]() {
|
||||
close_algorithm_zmq();
|
||||
close_trigger_serial();
|
||||
close_data_tcp();
|
||||
};
|
||||
if (!algorithm_zmq.Open(options.algorithm_host,
|
||||
options.algorithm_port,
|
||||
options.algorithm_timeout_ms)) {
|
||||
std::cerr << "Open algorithm ZMQ client failed"
|
||||
<< " remote=" << BuildZmqTcpEndpoint(options.algorithm_host, options.algorithm_port)
|
||||
<< " zmqError=" << algorithm_zmq.last_error()
|
||||
<< std::endl;
|
||||
close_all();
|
||||
return 1;
|
||||
}
|
||||
algorithm_zmq_open = true;
|
||||
std::cout << "Algorithm ZMQ enabled: identity=" << algorithm_zmq.identity()
|
||||
<< " algorithmHost=" << options.algorithm_host
|
||||
<< " algorithmPort=" << options.algorithm_port
|
||||
<< std::endl;
|
||||
std::cout << "2ch impedance enabled for "
|
||||
<< std::chrono::duration_cast<std::chrono::seconds>(kImpedanceDuration).count()
|
||||
<< " seconds before 2ch->64ch algorithm path" << std::endl;
|
||||
std::cout << "Welch waiting: impedance phase" << std::endl;
|
||||
if (has_trigger_port) {
|
||||
std::cout << "2ch cyclic trigger armed: alternating TRAIN_0/TRAIN_1 every "
|
||||
<< options.train_duration_ms << " ms" << std::endl;
|
||||
}
|
||||
|
||||
XYParserTriggerType next_trigger_type = XYPARSER_TRIGGER_TRAIN_0;
|
||||
bool impedance_phase = true;
|
||||
bool logged_waiting_algorithm_rx = false;
|
||||
const Clock::time_point impedance_end_time = Clock::now() + kImpedanceDuration;
|
||||
Clock::time_point next_trigger_time = Clock::time_point::max();
|
||||
|
||||
std::array<std::uint8_t, 8192> read_buffer{};
|
||||
std::array<XYParserFrameSummary, 32> frame_summaries{};
|
||||
ZmqRoundTripStats zmq_stats{};
|
||||
|
||||
while (true) {
|
||||
if (!impedance_phase && has_trigger_port && Clock::now() >= next_trigger_time) {
|
||||
if (!SendTrigger(trigger_port, next_trigger_type)) {
|
||||
std::cerr << "Send "
|
||||
<< (next_trigger_type == XYPARSER_TRIGGER_TRAIN_0 ? "TRAIN_0" : "TRAIN_1")
|
||||
<< " failed" << std::endl;
|
||||
close_all();
|
||||
return 1;
|
||||
}
|
||||
next_trigger_type = (next_trigger_type == XYPARSER_TRIGGER_TRAIN_0)
|
||||
? XYPARSER_TRIGGER_TRAIN_1
|
||||
: XYPARSER_TRIGGER_TRAIN_0;
|
||||
next_trigger_time = Clock::now() + std::chrono::milliseconds(options.train_duration_ms);
|
||||
}
|
||||
|
||||
bool disconnected = false;
|
||||
const int received = data_client.Receive(read_buffer.data(), static_cast<int>(read_buffer.size()), disconnected);
|
||||
if (disconnected) {
|
||||
std::cout << "2ch TCP disconnected" << std::endl;
|
||||
break;
|
||||
}
|
||||
if (received <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const int frame_count = XYParser_Feed(parser.get(),
|
||||
read_buffer.data(),
|
||||
static_cast<std::size_t>(received),
|
||||
frame_summaries.data(),
|
||||
static_cast<int>(frame_summaries.size()));
|
||||
if (frame_count > 0) {
|
||||
PrintFrameProgress(frame_summaries.data(), frame_count);
|
||||
}
|
||||
if (impedance_phase) {
|
||||
DrainImpedance(parser.get());
|
||||
if (Clock::now() >= impedance_end_time) {
|
||||
if (!Send2GainAndSampleRate(data_client, kNormalGain, kNormalSampleRate)) {
|
||||
std::cerr << "Send 2ch normal gain/sample-rate command failed" << std::endl;
|
||||
close_all();
|
||||
return 1;
|
||||
}
|
||||
if (!Send2ImpedanceSwitch(data_client, false)) {
|
||||
std::cerr << "Send 2ch impedance close command failed" << std::endl;
|
||||
close_all();
|
||||
return 1;
|
||||
}
|
||||
XYParser_SetSampleRate(parser.get(), kNormalSampleRate);
|
||||
XYParser_SetImpedanceDetection(parser.get(), 0);
|
||||
XYParser_SetWelchDetection(parser.get(), 1);
|
||||
DrainImpedance(parser.get());
|
||||
std::cout << "2ch impedance disabled after "
|
||||
<< std::chrono::duration_cast<std::chrono::seconds>(kImpedanceDuration).count()
|
||||
<< " seconds, Welch/PSD and 2ch->64ch algorithm path resumed" << std::endl;
|
||||
std::cout << "Welch waiting: no algorithm rx yet" << std::endl;
|
||||
logged_waiting_algorithm_rx = true;
|
||||
impedance_phase = false;
|
||||
next_trigger_time = Clock::now();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for (int i = 0; i < frame_count; ++i) {
|
||||
PrintTriggerEvents(frame_summaries[static_cast<std::size_t>(i)]);
|
||||
Process2AlgorithmPath(parser.get(),
|
||||
frame_summaries[static_cast<std::size_t>(i)],
|
||||
algorithm_zmq,
|
||||
zmq_stats);
|
||||
}
|
||||
if (logged_waiting_algorithm_rx && zmq_stats.received_packets > 0) {
|
||||
std::cout << "Welch input ready: algorithm rx started" << std::endl;
|
||||
logged_waiting_algorithm_rx = false;
|
||||
}
|
||||
DrainWelch(parser.get());
|
||||
}
|
||||
|
||||
close_all();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Run8Workflow(const DemoOptions& options)
|
||||
{
|
||||
constexpr auto kImpedanceDuration = std::chrono::seconds(60);
|
||||
@@ -1486,6 +1803,9 @@ int wmain(int argc, wchar_t* argv[])
|
||||
#if defined(XY_WORKFLOW_DEMO_64)
|
||||
constexpr int kImpedanceEnabled = 0;
|
||||
constexpr int kParserGainDuringRun = 6;
|
||||
#elif defined(XY_WORKFLOW_DEMO_2)
|
||||
constexpr int kImpedanceEnabled = 0;
|
||||
constexpr int kParserGainDuringRun = 6;
|
||||
#else
|
||||
constexpr int kImpedanceEnabled = 1;
|
||||
constexpr int kParserGainDuringRun = 24;
|
||||
@@ -1513,6 +1833,8 @@ int wmain(int argc, wchar_t* argv[])
|
||||
|
||||
#if defined(XY_WORKFLOW_DEMO_64)
|
||||
return Run64Workflow(options);
|
||||
#elif defined(XY_WORKFLOW_DEMO_2)
|
||||
return Run2Workflow(options);
|
||||
#else
|
||||
return Run8Workflow(options);
|
||||
#endif
|
||||
|
||||
@@ -258,5 +258,174 @@ XYParser_Feed(8导原始数据)
|
||||
代码依据:
|
||||
|
||||
- `XYParser_Convert8ChFramesTo64Ch`
|
||||
- `Convert8ChSummaryTo64ChSummary`
|
||||
- 8导映射表 `k8ChLeadMap`
|
||||
|
||||
### 3.9 2导初始化连接阶段
|
||||
|
||||
```mermaid
|
||||
%%{init: {'theme': 'default', 'sequence': {'diagramMarginX': 80, 'diagramMarginY': 30, 'actorMargin': 80, 'width': 220, 'height': 80, 'messageMargin': 35}}}%%
|
||||
sequenceDiagram
|
||||
participant Dev as 2导EEG采集设备
|
||||
participant Host as 上位机
|
||||
participant Lib as XYParser库
|
||||
|
||||
Dev-->>Host: 设备连接成功
|
||||
Host->>Lib: parser2 = XYParser_CreateParser(2)
|
||||
Host->>Lib: XYParser_SetAdcParams(parser2, 2.42, 6.0)
|
||||
Note over Host,Lib: 2导默认 vref = 2.42,与 8/64 导不同
|
||||
Host->>Lib: XYParser_SetSampleRate(parser2, 250)
|
||||
Host->>Lib: XYParser_SetBypassChecksum(parser2, 1)
|
||||
Host->>Lib: gain_cmd_size = XYParser_Get2ChGainSampleRateCommandSize()
|
||||
Lib-->>Host: gain_cmd_size
|
||||
Host->>Lib: gain_cmd_bytes = XYParser_Serialize2ChGainSampleRateCommand(6, 250, gain_cmd_buf, gain_cmd_size)
|
||||
Lib-->>Host: gain_cmd_bytes
|
||||
Host->>Dev: 下发采样率250、增益6命令
|
||||
```
|
||||
|
||||
### 3.10 2导阻抗阶段
|
||||
|
||||
```mermaid
|
||||
%%{init: {'theme': 'default', 'sequence': {'diagramMarginX': 80, 'diagramMarginY': 30, 'actorMargin': 80, 'width': 220, 'height': 80, 'messageMargin': 35}}}%%
|
||||
sequenceDiagram
|
||||
participant Dev as 2导EEG采集设备
|
||||
participant Host as 上位机
|
||||
participant Lib as XYParser库
|
||||
|
||||
Host->>Lib: XYParser_SetImpedanceDetection(parser2, 1)
|
||||
Host->>Lib: XYParser_SetAdcParams(parser2, 2.42, 24.0)
|
||||
Note over Host,Lib: 2导阻抗阶段仍使用 vref = 2.42
|
||||
Host->>Lib: impedance_gain_cmd_size = XYParser_Get2ChGainSampleRateCommandSize()
|
||||
Lib-->>Host: impedance_gain_cmd_size
|
||||
Host->>Lib: impedance_gain_cmd_bytes = XYParser_Serialize2ChGainSampleRateCommand(24, 250, impedance_gain_cmd_buf, impedance_gain_cmd_size)
|
||||
Lib-->>Host: impedance_gain_cmd_bytes
|
||||
Host->>Dev: 下发采样率250、增益24命令
|
||||
Host->>Lib: impedance_cmd_size = XYParser_Get2ChImpedanceCommandSize()
|
||||
Lib-->>Host: impedance_cmd_size
|
||||
Host->>Lib: open_impedance_bytes = XYParser_Serialize2ChImpedanceCommand(1, impedance_cmd_buf, impedance_cmd_size)
|
||||
Lib-->>Host: open_impedance_bytes
|
||||
Host->>Dev: 下发阻抗开启命令
|
||||
|
||||
loop 持续获取阻抗
|
||||
Dev-->>Host: 原始EEG字节流
|
||||
Host->>Lib: frame_count = XYParser_Feed(parser2, raw_data, raw_size, frame2_summaries, max_frames)
|
||||
Lib-->>Host: frame_count + frame2_summaries
|
||||
Host->>Lib: impedance_count = XYParser_ReadImpedance(parser2, impedance_summaries, max_impedance)
|
||||
Lib-->>Host: impedance_count + impedance_summaries
|
||||
end
|
||||
|
||||
Host->>Lib: close_impedance_bytes = XYParser_Serialize2ChImpedanceCommand(0, impedance_cmd_buf, impedance_cmd_size)
|
||||
Lib-->>Host: close_impedance_bytes
|
||||
Host->>Dev: 下发阻抗关闭命令
|
||||
Host->>Lib: restore_gain_cmd_size = XYParser_Get2ChGainSampleRateCommandSize()
|
||||
Lib-->>Host: restore_gain_cmd_size
|
||||
Host->>Lib: restore_gain_cmd_bytes = XYParser_Serialize2ChGainSampleRateCommand(6, 250, restore_gain_cmd_buf, restore_gain_cmd_size)
|
||||
Lib-->>Host: restore_gain_cmd_bytes
|
||||
Host->>Dev: 下发采样率250、增益6命令
|
||||
Host->>Lib: XYParser_SetAdcParams(parser2, 2.42, 6.0)
|
||||
Host->>Lib: XYParser_SetImpedanceDetection(parser2, 0)
|
||||
```
|
||||
|
||||
### 3.11 2导转64导导联映射关系
|
||||
|
||||
2导 workflow 在送入算法前,会先调用 `XYParser_Convert2ChFramesTo64Ch` 将 2 导帧扩展为 64 导帧。
|
||||
|
||||
- 2 个输入通道按固定导联位置写入 64 导 summary
|
||||
- 未覆盖到的其余 62 个 64 导导联全部补 `0`
|
||||
- `trigger type` 和 `trigger index` 原样透传
|
||||
|
||||
映射图如下:
|
||||
|
||||
```text
|
||||
2ch[0] -> FP1
|
||||
2ch[1] -> FP2
|
||||
others -> 0
|
||||
```
|
||||
|
||||
也可以理解为下面这张对应表:
|
||||
|
||||
| 2导索引 | 2导写入到的64导导联 |
|
||||
| --- | --- |
|
||||
| 0 | FP1 |
|
||||
| 1 | FP2 |
|
||||
|
||||
转换过程示意:
|
||||
|
||||
```text
|
||||
XYParser_Feed(2导原始数据)
|
||||
-> frame2_summary
|
||||
-> XYParser_Convert2ChFramesTo64Ch
|
||||
-> frame64_summary
|
||||
-> XYParser_ConvertSampleFramesToAlgorithmData
|
||||
-> algorithm_input_data
|
||||
```
|
||||
|
||||
代码依据:
|
||||
|
||||
- `XYParser_Convert2ChFramesTo64Ch`
|
||||
- `Convert2ChSummaryTo64ChSummary`
|
||||
- `k2ChLeadMap = { FP1, FP2 }`
|
||||
|
||||
### 3.12 2导 workflow 时序图
|
||||
|
||||
2导 demo 的完整链路与 64 导 workflow 保持一致,只是前端输入是 2 导,送算法前会先补成 64 导。
|
||||
|
||||
```mermaid
|
||||
%%{init: {'theme': 'default', 'sequence': {'diagramMarginX': 80, 'diagramMarginY': 30, 'actorMargin': 80, 'width': 220, 'height': 80, 'messageMargin': 35}}}%%
|
||||
sequenceDiagram
|
||||
participant Dev as 2导EEG采集设备
|
||||
participant Host as 上位机
|
||||
participant Lib as XYParser库
|
||||
participant Algo as 算法
|
||||
|
||||
Host->>Lib: XYParser_SetWelchDetection(parser2, 1)
|
||||
|
||||
loop 持续采集
|
||||
Dev-->>Host: 原始EEG字节流
|
||||
Host->>Lib: frame_count = XYParser_Feed(parser2, raw_data, raw_size, frame2_summaries, max_frames)
|
||||
Lib-->>Host: frame_count + frame2_summaries
|
||||
Host->>Lib: converted = XYParser_Convert2ChFramesTo64Ch(frame2_summary, 1, frame64_summary, 1)
|
||||
Lib-->>Host: converted + frame64_summary
|
||||
Host->>Lib: value_count = XYParser_GetAlgorithmDataValueCount()
|
||||
Lib-->>Host: value_count
|
||||
Host->>Lib: ok = XYParser_ConvertSampleFramesToAlgorithmData(frame64_summary, algorithm_input_data)
|
||||
Lib-->>Host: ok + algorithm_input_data
|
||||
Host->>Algo: 输入算法数据 algorithm_input_data
|
||||
Algo-->>Host: 算法输出数据 algorithm_output_bytes
|
||||
Host->>Lib: alg_frame_count = XYParser_FeedAlgorithmData(parser2, algorithm_output_bytes, algorithm_output_size, algorithm_frames, max_algorithm_frames)
|
||||
Lib-->>Host: alg_frame_count + algorithm_frames
|
||||
Host->>Lib: welch_count = XYParser_ReadWelch(parser2, welch_summaries, max_welch)
|
||||
Lib-->>Host: welch_count + welch_summaries
|
||||
end
|
||||
|
||||
opt 结束时处理尾数据
|
||||
Host->>Lib: flushed = XYParser_FlushAlgorithmData(parser2, tail_frame_summary)
|
||||
Lib-->>Host: flushed + tail_frame_summary
|
||||
Host->>Lib: welch_count = XYParser_ReadWelch(parser2, welch_summaries, max_welch)
|
||||
Lib-->>Host: welch_count + welch_summaries
|
||||
end
|
||||
|
||||
Host->>Lib: XYParser_DestroyParser(parser2)
|
||||
```
|
||||
|
||||
时序步骤如下:
|
||||
|
||||
1. `XYParser2Demo` 通过 TCP 接收 2 导原始数据。
|
||||
2. `XYParser_Feed(handle=2)` 解析出 `frame2_summary`。
|
||||
3. `XYParser_Convert2ChFramesTo64Ch` 将 `FP1/FP2` 写入 64 导 summary,其余导联补 `0`。
|
||||
4. `XYParser_ConvertSampleFramesToAlgorithmData` 将 64 导 summary 打平成算法输入。
|
||||
5. ZMQ 将 64 通道 payload 发给算法服务端。
|
||||
6. 算法服务端回 64 通道结果。
|
||||
7. `XYParser_FeedAlgorithmData` 将算法回包喂回 parser。
|
||||
8. Welch/PSD 输出 `peakHz`、`peakPsd`、各 band 能量等结果。
|
||||
|
||||
如果打开阻抗流程,则 2 导还会额外穿插以下控制阶段:
|
||||
|
||||
- 发送 2 导阻抗开启命令
|
||||
- 发送 `250Hz / 24增益`
|
||||
- 打开 parser 阻抗开关
|
||||
- 读取并打印阻抗
|
||||
- 阻抗结束后发送 `250Hz / 6增益`
|
||||
- 发送 2 导阻抗关闭命令
|
||||
- 关闭 parser 阻抗开关
|
||||
- 恢复 2->64->算法->Welch 主链路
|
||||
- `Convert2ChSummaryTo64ChSummary`
|
||||
- 2导映射表 `k2ChLeadMap`
|
||||
|
||||
Reference in New Issue
Block a user