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
|
EndProject
|
||||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "XYParser8Demo", "XYParserWorkflowDemo\XYParser8Demo.vcxproj", "{1B7FA4A1-8BC2-4D49-9B5A-BD4C6B8F2107}"
|
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "XYParser8Demo", "XYParserWorkflowDemo\XYParser8Demo.vcxproj", "{1B7FA4A1-8BC2-4D49-9B5A-BD4C6B8F2107}"
|
||||||
EndProject
|
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}"
|
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "XYAlgorithmUdpServer", "XYParserWorkflowDemo\XYAlgorithmUdpServer.vcxproj", "{6D6DCD3D-995A-4E79-9338-C1D36A3D2A61}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
@@ -53,6 +55,14 @@ Global
|
|||||||
{1B7FA4A1-8BC2-4D49-9B5A-BD4C6B8F2107}.Release|x64.Build.0 = Release|x64
|
{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.ActiveCfg = Release|Win32
|
||||||
{1B7FA4A1-8BC2-4D49-9B5A-BD4C6B8F2107}.Release|x86.Build.0 = 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.ActiveCfg = Debug|x64
|
||||||
{6D6DCD3D-995A-4E79-9338-C1D36A3D2A61}.Debug|x64.Build.0 = Debug|x64
|
{6D6DCD3D-995A-4E79-9338-C1D36A3D2A61}.Debug|x64.Build.0 = Debug|x64
|
||||||
{6D6DCD3D-995A-4E79-9338-C1D36A3D2A61}.Debug|x86.ActiveCfg = Debug|Win32
|
{6D6DCD3D-995A-4E79-9338-C1D36A3D2A61}.Debug|x86.ActiveCfg = Debug|Win32
|
||||||
|
|||||||
@@ -146,6 +146,7 @@
|
|||||||
<ClInclude Include="XYImpedanceProcessor.h" />
|
<ClInclude Include="XYImpedanceProcessor.h" />
|
||||||
<ClInclude Include="XYParserApi.h" />
|
<ClInclude Include="XYParserApi.h" />
|
||||||
<ClInclude Include="XYWelchProcessor.h" />
|
<ClInclude Include="XYWelchProcessor.h" />
|
||||||
|
<ClInclude Include="XYEegParser2.h" />
|
||||||
<ClInclude Include="XYEegParser8.h" />
|
<ClInclude Include="XYEegParser8.h" />
|
||||||
<ClInclude Include="XYEegParser64.h" />
|
<ClInclude Include="XYEegParser64.h" />
|
||||||
<ClInclude Include="XYEegParserCommon.h" />
|
<ClInclude Include="XYEegParserCommon.h" />
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "XYImpedanceProcessor.h"
|
#include "XYImpedanceProcessor.h"
|
||||||
#include "XYWelchProcessor.h"
|
#include "XYWelchProcessor.h"
|
||||||
|
|
||||||
|
#include "XYEegParser2.h"
|
||||||
#include "XYEegParser8.h"
|
#include "XYEegParser8.h"
|
||||||
#include "XYEegParser64.h"
|
#include "XYEegParser64.h"
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ constexpr std::uint8_t kCommandFrameTail = 0x55;
|
|||||||
constexpr std::size_t k8ChImpedanceCommandSize = 7;
|
constexpr std::size_t k8ChImpedanceCommandSize = 7;
|
||||||
constexpr std::size_t kTriggerPayloadSize = 3;
|
constexpr std::size_t kTriggerPayloadSize = 3;
|
||||||
constexpr std::size_t kTriggerCommandSize = 1 + kTriggerPayloadSize + 1 + 2;
|
constexpr std::size_t kTriggerCommandSize = 1 + kTriggerPayloadSize + 1 + 2;
|
||||||
|
constexpr int k2ChLeadCount = 2;
|
||||||
constexpr int k8ChLeadCount = 8;
|
constexpr int k8ChLeadCount = 8;
|
||||||
constexpr int k64ChLeadCount = 64;
|
constexpr int k64ChLeadCount = 64;
|
||||||
constexpr std::uint8_t kAlgorithmChannelCount = 64;
|
constexpr std::uint8_t kAlgorithmChannelCount = 64;
|
||||||
@@ -77,6 +79,9 @@ std::array<std::uint8_t, kTriggerCommandSize> BuildTriggerCommand(std::uint8_t t
|
|||||||
kCommandFrameTail};
|
kCommandFrameTail};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constexpr std::array<XYParserLeadChannelNumber, k2ChLeadCount> k2ChLeadMap = {
|
||||||
|
LeadChannel_FP1, LeadChannel_FP2};
|
||||||
|
|
||||||
constexpr std::array<XYParserLeadChannelNumber, k8ChLeadCount> k8ChLeadMap = {
|
constexpr std::array<XYParserLeadChannelNumber, k8ChLeadCount> k8ChLeadMap = {
|
||||||
LeadChannel_PO5, LeadChannel_POZ, LeadChannel_PO6, LeadChannel_PO7,
|
LeadChannel_PO5, LeadChannel_POZ, LeadChannel_PO6, LeadChannel_PO7,
|
||||||
LeadChannel_O1, LeadChannel_OZ, LeadChannel_O2, LeadChannel_PO8};
|
LeadChannel_O1, LeadChannel_OZ, LeadChannel_O2, LeadChannel_PO8};
|
||||||
@@ -101,6 +106,7 @@ constexpr std::array<XYParserLeadChannelNumber, k64ChLeadCount> k64ChLeadMap = {
|
|||||||
|
|
||||||
struct ParserContext {
|
struct ParserContext {
|
||||||
std::uint8_t channel_count = 0;
|
std::uint8_t channel_count = 0;
|
||||||
|
XYEegParser2 parser2;
|
||||||
XYEegParser8 parser8;
|
XYEegParser8 parser8;
|
||||||
XYEegParser64 parser64;
|
XYEegParser64 parser64;
|
||||||
XYImpedanceProcessor impedance_processor;
|
XYImpedanceProcessor impedance_processor;
|
||||||
@@ -127,6 +133,29 @@ constexpr std::array<WelchBandInfo, XYPARSER_WELCH_BAND_COUNT> kWelchBandInfos =
|
|||||||
{"gamma", 30.0, 50.0},
|
{"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)
|
void FillSummary(const XYEegFrame8& frame, XYParserFrameSummary& summary)
|
||||||
{
|
{
|
||||||
summary.frame_index = frame.index;
|
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,
|
void Convert8ChSummaryTo64ChSummary(const XYParserFrameSummary& input_summary,
|
||||||
XYParserFrameSummary& output_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)
|
int GetLeadMapCount(std::uint8_t channel_count)
|
||||||
{
|
{
|
||||||
switch (channel_count) {
|
switch (channel_count) {
|
||||||
|
case 2:
|
||||||
|
return k2ChLeadCount;
|
||||||
case 8:
|
case 8:
|
||||||
return k8ChLeadCount;
|
return k8ChLeadCount;
|
||||||
case 64:
|
case 64:
|
||||||
@@ -321,7 +378,7 @@ extern "C" {
|
|||||||
|
|
||||||
XYParserHandle XYParser_CreateParser(std::uint8_t channel_count)
|
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;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,7 +413,9 @@ void XYParser_SetAdcParams(XYParserHandle handle, double vref, double gain)
|
|||||||
context->adc_gain = 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);
|
context->parser8.SetAdcParams(vref, gain);
|
||||||
} else {
|
} else {
|
||||||
context->parser64.SetAdcParams(vref, gain);
|
context->parser64.SetAdcParams(vref, gain);
|
||||||
@@ -371,7 +430,9 @@ void XYParser_SetBypassChecksum(XYParserHandle handle, int bypass)
|
|||||||
}
|
}
|
||||||
|
|
||||||
const bool enabled = bypass != 0;
|
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);
|
context->parser8.SetBypassChecksum(enabled);
|
||||||
} else {
|
} else {
|
||||||
context->parser64.SetBypassChecksum(enabled);
|
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;
|
const double impedance_gain = impedance_enabled ? 24.0 : 6.0;
|
||||||
context->adc_gain = impedance_gain;
|
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);
|
context->parser8.SetAdcParams(context->adc_vref, impedance_gain);
|
||||||
} else {
|
} else {
|
||||||
context->parser64.SetAdcParams(context->adc_vref, impedance_gain);
|
context->parser64.SetAdcParams(context->adc_vref, impedance_gain);
|
||||||
@@ -424,6 +487,11 @@ std::size_t XYParser_Get64ImpedanceCommandSize(void)
|
|||||||
return XYEegParser64::kImpedanceCommandSize;
|
return XYEegParser64::kImpedanceCommandSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::size_t XYParser_Get2ChImpedanceCommandSize(void)
|
||||||
|
{
|
||||||
|
return XYEegParser64::kImpedanceCommandSize;
|
||||||
|
}
|
||||||
|
|
||||||
std::size_t XYParser_GetTriggerCommandSize(void)
|
std::size_t XYParser_GetTriggerCommandSize(void)
|
||||||
{
|
{
|
||||||
return kTriggerCommandSize;
|
return kTriggerCommandSize;
|
||||||
@@ -447,6 +515,11 @@ std::size_t XYParser_Get64GainSampleRateCommandSize(void)
|
|||||||
return XYEegParser64::kGainSampleRateCommandSize;
|
return XYEegParser64::kGainSampleRateCommandSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::size_t XYParser_Get2ChGainSampleRateCommandSize(void)
|
||||||
|
{
|
||||||
|
return XYEegParser64::kGainSampleRateCommandSize;
|
||||||
|
}
|
||||||
|
|
||||||
int XYParser_Serialize64ImpedanceCommand(int open,
|
int XYParser_Serialize64ImpedanceCommand(int open,
|
||||||
std::uint8_t* out_buffer,
|
std::uint8_t* out_buffer,
|
||||||
std::size_t buffer_size)
|
std::size_t buffer_size)
|
||||||
@@ -459,6 +532,13 @@ int XYParser_Serialize64ImpedanceCommand(int open,
|
|||||||
return static_cast<int>(command.size());
|
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,
|
int XYParser_Serialize64GainSampleRateCommand(std::uint8_t gain,
|
||||||
std::uint16_t sample_rate,
|
std::uint16_t sample_rate,
|
||||||
std::uint8_t* out_buffer,
|
std::uint8_t* out_buffer,
|
||||||
@@ -477,6 +557,14 @@ int XYParser_Serialize64GainSampleRateCommand(std::uint8_t gain,
|
|||||||
return static_cast<int>(command.size());
|
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,
|
int XYParser_Feed(XYParserHandle handle,
|
||||||
const std::uint8_t* data,
|
const std::uint8_t* data,
|
||||||
std::size_t size,
|
std::size_t size,
|
||||||
@@ -492,6 +580,21 @@ int XYParser_Feed(XYParserHandle handle,
|
|||||||
max_summaries = 0;
|
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) {
|
if (context->channel_count == 8) {
|
||||||
const std::vector<XYEegFrame8> frames = context->parser8.Feed(data, size);
|
const std::vector<XYEegFrame8> frames = context->parser8.Feed(data, size);
|
||||||
context->last_error = context->parser8.LastError();
|
context->last_error = context->parser8.LastError();
|
||||||
@@ -540,6 +643,25 @@ int XYParser_Convert8ChFramesTo64Ch(const XYParserFrameSummary* input_summaries,
|
|||||||
return convert_count;
|
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()
|
std::size_t XYParser_GetAlgorithmDataValueCount()
|
||||||
{
|
{
|
||||||
return XYPARSER_FRAME_ALGORITHM_VALUE_COUNT;
|
return XYPARSER_FRAME_ALGORITHM_VALUE_COUNT;
|
||||||
@@ -566,8 +688,7 @@ int XYParser_FeedAlgorithmData(XYParserHandle handle,
|
|||||||
int max_summaries)
|
int max_summaries)
|
||||||
{
|
{
|
||||||
ParserContext* context = static_cast<ParserContext*>(handle);
|
ParserContext* context = static_cast<ParserContext*>(handle);
|
||||||
if (context == nullptr || context->channel_count != kAlgorithmChannelCount ||
|
if (context == nullptr || data == nullptr || size == 0) {
|
||||||
data == nullptr || size == 0) {
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -618,7 +739,7 @@ int XYParser_FeedAlgorithmData(XYParserHandle handle,
|
|||||||
void XYParser_ResetAlgorithmDataCache(XYParserHandle handle)
|
void XYParser_ResetAlgorithmDataCache(XYParserHandle handle)
|
||||||
{
|
{
|
||||||
ParserContext* context = static_cast<ParserContext*>(handle);
|
ParserContext* context = static_cast<ParserContext*>(handle);
|
||||||
if (context == nullptr || context->channel_count != kAlgorithmChannelCount) {
|
if (context == nullptr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -631,8 +752,7 @@ int XYParser_FlushAlgorithmData(XYParserHandle handle,
|
|||||||
XYParserFrameSummary* out_summary)
|
XYParserFrameSummary* out_summary)
|
||||||
{
|
{
|
||||||
ParserContext* context = static_cast<ParserContext*>(handle);
|
ParserContext* context = static_cast<ParserContext*>(handle);
|
||||||
if (context == nullptr || context->channel_count != kAlgorithmChannelCount ||
|
if (context == nullptr || out_summary == nullptr || context->algorithm_sample_cache.empty()) {
|
||||||
out_summary == nullptr || context->algorithm_sample_cache.empty()) {
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -658,6 +778,11 @@ int XYParser_GetLeadMap(std::uint8_t channel_count,
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (channel_count == 2) {
|
||||||
|
std::copy(k2ChLeadMap.begin(), k2ChLeadMap.end(), out_leads);
|
||||||
|
return required_count;
|
||||||
|
}
|
||||||
|
|
||||||
if (channel_count == 8) {
|
if (channel_count == 8) {
|
||||||
std::copy(k8ChLeadMap.begin(), k8ChLeadMap.end(), out_leads);
|
std::copy(k8ChLeadMap.begin(), k8ChLeadMap.end(), out_leads);
|
||||||
return required_count;
|
return required_count;
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ struct XYParserWelchSummary {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 创建解析器实例。
|
// 创建解析器实例。
|
||||||
// @param channel_count 解析器支持的导联数。
|
// @param channel_count 解析器支持的导联数,当前支持 2/8/64。
|
||||||
// @return 创建成功时返回解析器句柄,失败时返回 nullptr。
|
// @return 创建成功时返回解析器句柄,失败时返回 nullptr。
|
||||||
XYPARSER_API XYParserHandle XYParser_CreateParser(std::uint8_t channel_count);
|
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 导阻抗命令的字节长度。
|
// @return 64 导阻抗命令的字节长度。
|
||||||
XYPARSER_API std::size_t XYParser_Get64ImpedanceCommandSize(void);
|
XYPARSER_API std::size_t XYParser_Get64ImpedanceCommandSize(void);
|
||||||
|
|
||||||
|
// 获取 2 导阻抗命令的字节长度。
|
||||||
|
// @return 2 导阻抗命令的字节长度。
|
||||||
|
XYPARSER_API std::size_t XYParser_Get2ChImpedanceCommandSize(void);
|
||||||
|
|
||||||
// 生成 64 导阻抗开关命令。
|
// 生成 64 导阻抗开关命令。
|
||||||
// @param open 非 0 表示打开阻抗检测,0 表示关闭阻抗检测。
|
// @param open 非 0 表示打开阻抗检测,0 表示关闭阻抗检测。
|
||||||
// @param out_buffer 输出的命令缓冲区。
|
// @param out_buffer 输出的命令缓冲区。
|
||||||
@@ -257,10 +261,24 @@ XYPARSER_API int XYParser_Serialize64ImpedanceCommand(int open,
|
|||||||
std::uint8_t* out_buffer,
|
std::uint8_t* out_buffer,
|
||||||
std::size_t buffer_size);
|
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 导增益和采样率命令的字节长度。
|
// 获取 64 导增益和采样率命令的字节长度。
|
||||||
// @return 64 导增益和采样率命令的字节长度。
|
// @return 64 导增益和采样率命令的字节长度。
|
||||||
XYPARSER_API std::size_t XYParser_Get64GainSampleRateCommandSize(void);
|
XYPARSER_API std::size_t XYParser_Get64GainSampleRateCommandSize(void);
|
||||||
|
|
||||||
|
// 获取 2 导增益和采样率命令的字节长度。
|
||||||
|
// @return 2 导增益和采样率命令的字节长度。
|
||||||
|
XYPARSER_API std::size_t XYParser_Get2ChGainSampleRateCommandSize(void);
|
||||||
|
|
||||||
// 生成 64 导增益和采样率设置命令。
|
// 生成 64 导增益和采样率设置命令。
|
||||||
// @param gain 要设置的增益值。
|
// @param gain 要设置的增益值。
|
||||||
// @param sample_rate 要设置的采样率。
|
// @param sample_rate 要设置的采样率。
|
||||||
@@ -272,6 +290,18 @@ XYPARSER_API int XYParser_Serialize64GainSampleRateCommand(std::uint8_t gain,
|
|||||||
std::uint8_t* out_buffer,
|
std::uint8_t* out_buffer,
|
||||||
std::size_t buffer_size);
|
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.
|
// Frame conversion and algorithm data output.
|
||||||
// 将 8 导帧批量转换为 64 导帧,未映射的导联补 0。
|
// 将 8 导帧批量转换为 64 导帧,未映射的导联补 0。
|
||||||
// @param input_summaries 输入的 8 导帧数组。
|
// @param input_summaries 输入的 8 导帧数组。
|
||||||
@@ -284,6 +314,18 @@ XYPARSER_API int XYParser_Convert8ChFramesTo64Ch(const XYParserFrameSummary* inp
|
|||||||
XYParserFrameSummary* output_summaries,
|
XYParserFrameSummary* output_summaries,
|
||||||
int max_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 算法数据数组所需的元素数量。
|
// @return 算法数据数组所需的元素数量。
|
||||||
XYPARSER_API std::size_t XYParser_GetAlgorithmDataValueCount();
|
XYPARSER_API std::size_t XYParser_GetAlgorithmDataValueCount();
|
||||||
|
|||||||
@@ -396,6 +396,12 @@ TEST(XYParserApiTests, CreateParserRejectsUnsupportedChannelCount)
|
|||||||
EXPECT_EQ(XYParser_CreateParser(7), nullptr);
|
EXPECT_EQ(XYParser_CreateParser(7), nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(XYParserApiTests, CreateParserAccepts2ChannelCount)
|
||||||
|
{
|
||||||
|
ParserGuard parser(XYParser_CreateParser(2));
|
||||||
|
EXPECT_NE(parser.get(), nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
/// 测试:对空解析器句柄调用 GetLastError 应返回正确错误信息
|
/// 测试:对空解析器句柄调用 GetLastError 应返回正确错误信息
|
||||||
TEST(XYParserApiTests, GetLastErrorReturnsMessageForNullParser)
|
TEST(XYParserApiTests, GetLastErrorReturnsMessageForNullParser)
|
||||||
{
|
{
|
||||||
@@ -484,11 +490,23 @@ TEST(XYParserApiTests, GetAlgorithmDataValueCountMatchesFrameLayout)
|
|||||||
|
|
||||||
TEST(XYParserApiTests, GetLeadMapCountReturnsExpectedCounts)
|
TEST(XYParserApiTests, GetLeadMapCountReturnsExpectedCounts)
|
||||||
{
|
{
|
||||||
|
EXPECT_EQ(XYParser_GetLeadMapCount(2), 2);
|
||||||
EXPECT_EQ(XYParser_GetLeadMapCount(8), 8);
|
EXPECT_EQ(XYParser_GetLeadMapCount(8), 8);
|
||||||
EXPECT_EQ(XYParser_GetLeadMapCount(64), 64);
|
EXPECT_EQ(XYParser_GetLeadMapCount(64), 64);
|
||||||
EXPECT_EQ(XYParser_GetLeadMapCount(7), 0);
|
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)
|
TEST(XYParserApiTests, GetLeadMapReturnsExpected8ChannelSubset)
|
||||||
{
|
{
|
||||||
std::array<XYParserLeadChannelNumber, 8> leads{};
|
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);
|
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)
|
TEST(XYParserApiTests, Convert8ChFramesTo64ChRejectsInvalidArgumentsAndStopsAtNon8ChannelInput)
|
||||||
{
|
{
|
||||||
std::array<XYParserFrameSummary, 2> input{};
|
std::array<XYParserFrameSummary, 2> input{};
|
||||||
@@ -700,6 +746,40 @@ TEST(XYParserApiTests, FeedAlgorithmDataCachesSamplesBuildsFramesAndFlushesTailS
|
|||||||
EXPECT_EQ(XYParser_FlushAlgorithmData(parser.get(), &tail_summary), 0);
|
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)
|
TEST(XYParserApiTests, GetLeadNameReturnsExpectedNames)
|
||||||
{
|
{
|
||||||
EXPECT_EQ(std::string(XYParser_GetLeadName(LeadChannel_FP1)), "FP1");
|
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()));
|
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 协议保持一致
|
/// 测试:64 导增益和采样率命令序列化与 WirelessEEG 协议保持一致
|
||||||
TEST(XYParserApiTests, Serialize64GainAndSampleRateCommandMatchesWirelessEegProtocol)
|
TEST(XYParserApiTests, Serialize64GainAndSampleRateCommandMatchesWirelessEegProtocol)
|
||||||
{
|
{
|
||||||
@@ -1881,6 +1976,21 @@ TEST(XYParserApiTests, Serialize64GainAndSampleRateCommandMatchesWirelessEegProt
|
|||||||
EXPECT_TRUE(std::equal(expected.begin(), expected.end(), buffer.begin()));
|
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 导增益和采样率命令拒绝非法采样率
|
/// 测试:64 导增益和采样率命令拒绝非法采样率
|
||||||
TEST(XYParserApiTests, Serialize64GainAndSampleRateCommandRejectsUnsupportedSampleRate)
|
TEST(XYParserApiTests, Serialize64GainAndSampleRateCommandRejectsUnsupportedSampleRate)
|
||||||
{
|
{
|
||||||
@@ -1961,6 +2071,28 @@ TEST(XYParserApiTests, FeedParses64ChannelFrame)
|
|||||||
EXPECT_EQ(summaries[0].channel_count, 64U);
|
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)
|
TEST(XYParserApiTests, FeedParsesReservedMetadataInto64ChannelSummary)
|
||||||
{
|
{
|
||||||
ParserGuard parser(XYParser_CreateParser(64));
|
ParserGuard parser(XYParser_CreateParser(64));
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
# XYParser64Demo / XYParser8Demo
|
# XYParser64Demo / XYParser2Demo / XYParser8Demo
|
||||||
|
|
||||||
业务流程 demo,可直接连你的设备模拟器验证:
|
业务流程 demo,可直接连你的设备模拟器验证:
|
||||||
|
|
||||||
- `XYParser64Demo`:TCP 接收 64 导数据,可额外打开一个串口发送 `TRAIN_0 / TRAIN_1`
|
- `XYParser64Demo`:TCP 接收 64 导数据,可额外打开一个串口发送 `TRAIN_0 / TRAIN_1`
|
||||||
|
- `XYParser2Demo`:TCP 接收 2 导数据,固定映射 `FP1/FP2`,送算法前扩展为 64 导
|
||||||
- `XYParser8Demo`:串口接收 8 导数据
|
- `XYParser8Demo`:串口接收 8 导数据
|
||||||
- `XYAlgorithmUdpServer`:算法 ZMQ 服务端,接收 demo 发来的算法输入并回包
|
- `XYAlgorithmUdpServer`:算法 ZMQ 服务端,接收 demo 发来的算法输入并回包
|
||||||
- Welch/PSD 固定走 ZMQ:
|
- Welch/PSD 固定走 ZMQ:
|
||||||
@@ -44,7 +45,7 @@
|
|||||||
联调顺序:
|
联调顺序:
|
||||||
|
|
||||||
- 先启动 `XYAlgorithmUdpServer`
|
- 先启动 `XYAlgorithmUdpServer`
|
||||||
- 再启动 `XYParser64Demo` 或 `XYParser8Demo`
|
- 再启动 `XYParser64Demo`、`XYParser2Demo` 或 `XYParser8Demo`
|
||||||
- 如果 demo 端 `rxPackets` 开始增长,说明 ZMQ 链路已打通
|
- 如果 demo 端 `rxPackets` 开始增长,说明 ZMQ 链路已打通
|
||||||
|
|
||||||
## 64 导示例
|
## 64 导示例
|
||||||
@@ -72,6 +73,40 @@
|
|||||||
- `--algorithm-port 8100`
|
- `--algorithm-port 8100`
|
||||||
- `--train-duration-ms 3000`
|
- `--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 导示例
|
## 8 导示例
|
||||||
|
|
||||||
```powershell
|
```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 {
|
enum class DemoMode {
|
||||||
Mode64Tcp,
|
Mode64Tcp,
|
||||||
Mode8Serial
|
Mode8Serial,
|
||||||
|
Mode2Tcp
|
||||||
};
|
};
|
||||||
|
|
||||||
#if defined(XY_WORKFLOW_DEMO_64)
|
#if defined(XY_WORKFLOW_DEMO_64)
|
||||||
@@ -35,14 +36,23 @@ constexpr wchar_t kProgramName[] = L"XYParser64Demo";
|
|||||||
constexpr char kDefaultTcpHost[] = "127.0.0.1";
|
constexpr char kDefaultTcpHost[] = "127.0.0.1";
|
||||||
constexpr wchar_t kDefaultDataComPort[] = L"";
|
constexpr wchar_t kDefaultDataComPort[] = L"";
|
||||||
constexpr wchar_t kDefaultTriggerComPort[] = L"COM44";
|
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)
|
#elif defined(XY_WORKFLOW_DEMO_8)
|
||||||
constexpr DemoMode kDemoMode = DemoMode::Mode8Serial;
|
constexpr DemoMode kDemoMode = DemoMode::Mode8Serial;
|
||||||
constexpr wchar_t kProgramName[] = L"XYParser8Demo";
|
constexpr wchar_t kProgramName[] = L"XYParser8Demo";
|
||||||
constexpr char kDefaultTcpHost[] = "127.0.0.1";
|
constexpr char kDefaultTcpHost[] = "127.0.0.1";
|
||||||
constexpr wchar_t kDefaultDataComPort[] = L"COM44";
|
constexpr wchar_t kDefaultDataComPort[] = L"COM44";
|
||||||
constexpr wchar_t kDefaultTriggerComPort[] = L"";
|
constexpr wchar_t kDefaultTriggerComPort[] = L"";
|
||||||
|
constexpr double kDefaultVref = 4.5;
|
||||||
#else
|
#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
|
#endif
|
||||||
|
|
||||||
struct ZmqRoundTripStats {
|
struct ZmqRoundTripStats {
|
||||||
@@ -58,7 +68,7 @@ struct DemoOptions {
|
|||||||
DemoMode mode = kDemoMode;
|
DemoMode mode = kDemoMode;
|
||||||
std::string tcp_host = kDefaultTcpHost;
|
std::string tcp_host = kDefaultTcpHost;
|
||||||
int tcp_port = 5086;
|
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_port = 8100;
|
||||||
int algorithm_timeout_ms = 1000;
|
int algorithm_timeout_ms = 1000;
|
||||||
std::wstring data_com_port = kDefaultDataComPort;
|
std::wstring data_com_port = kDefaultDataComPort;
|
||||||
@@ -66,7 +76,7 @@ struct DemoOptions {
|
|||||||
int serial_baud_rate = 460800;
|
int serial_baud_rate = 460800;
|
||||||
int sample_rate = 250;
|
int sample_rate = 250;
|
||||||
int gain = 6;
|
int gain = 6;
|
||||||
double vref = 4.5;
|
double vref = kDefaultVref;
|
||||||
int bypass_checksum = 1;
|
int bypass_checksum = 1;
|
||||||
int train_duration_ms = 3000;
|
int train_duration_ms = 3000;
|
||||||
};
|
};
|
||||||
@@ -623,7 +633,7 @@ void PrintUsage()
|
|||||||
{
|
{
|
||||||
std::wcout
|
std::wcout
|
||||||
<< kProgramName << L" usage:\n";
|
<< kProgramName << L" usage:\n";
|
||||||
#if defined(XY_WORKFLOW_DEMO_64)
|
#if defined(XY_WORKFLOW_DEMO_64) || defined(XY_WORKFLOW_DEMO_2)
|
||||||
std::wcout
|
std::wcout
|
||||||
<< L" " << kProgramName << L" [--tcp-host 127.0.0.1] [--tcp-port 5086]\n"
|
<< L" " << kProgramName << L" [--tcp-host 127.0.0.1] [--tcp-port 5086]\n"
|
||||||
<< L" [--trigger-com COM44]\n"
|
<< L" [--trigger-com COM44]\n"
|
||||||
@@ -641,6 +651,9 @@ void PrintUsage()
|
|||||||
#if defined(XY_WORKFLOW_DEMO_64)
|
#if defined(XY_WORKFLOW_DEMO_64)
|
||||||
<< L" - Default TCP host is 127.0.0.1 and default trigger serial is COM44.\n"
|
<< 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"
|
<< 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
|
#else
|
||||||
<< L" - Default 8ch data serial is COM44.\n"
|
<< L" - Default 8ch data serial is COM44.\n"
|
||||||
<< L" - Receive 8ch stream from serial, then run 8ch -> 64ch -> algorithm data -> Welch.\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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if defined(XY_WORKFLOW_DEMO_64)
|
#if defined(XY_WORKFLOW_DEMO_64) || defined(XY_WORKFLOW_DEMO_2)
|
||||||
return options.tcp_port > 0;
|
return options.tcp_port > 0;
|
||||||
#else
|
#else
|
||||||
return !options.data_com_port.empty();
|
return !options.data_com_port.empty();
|
||||||
@@ -832,21 +845,54 @@ void PrintWelchSummary(const XYParserWelchSummary& summary)
|
|||||||
const char* gamma_name = XYParser_GetWelchBandName(4);
|
const char* gamma_name = XYParser_GetWelchBandName(4);
|
||||||
const char* lead_name = XYParser_GetLeadName(lead);
|
const char* lead_name = XYParser_GetLeadName(lead);
|
||||||
|
|
||||||
std::cout << "[Welch] sampleRate=" << summary.sample_rate
|
std::ostringstream stream;
|
||||||
|
stream << "[Welch] sampleRate=" << summary.sample_rate
|
||||||
<< " window=" << summary.window_sample_count
|
<< " window=" << summary.window_sample_count
|
||||||
<< " freqCount=" << summary.frequency_count
|
<< " freqCount=" << summary.frequency_count
|
||||||
<< " lead=" << (lead_name != nullptr ? lead_name : "lead?");
|
<< " lead=" << (lead_name != nullptr ? lead_name : "lead?");
|
||||||
if (peak_index >= 0) {
|
if (peak_index >= 0) {
|
||||||
std::cout << " peakHz=" << summary.frequencies[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)];
|
<< " peakPsd=" << summary.psd_values[lead][static_cast<std::size_t>(peak_index)];
|
||||||
}
|
}
|
||||||
std::cout << ' ' << (alpha_name != nullptr ? alpha_name : "alpha")
|
stream << ' ' << (alpha_name != nullptr ? alpha_name : "alpha")
|
||||||
<< '=' << summary.band_values[2][lead]
|
<< '=' << summary.band_values[2][lead]
|
||||||
<< ' ' << (gamma_name != nullptr ? gamma_name : "gamma")
|
<< ' ' << (gamma_name != nullptr ? gamma_name : "gamma")
|
||||||
<< '=' << summary.band_values[4][lead]
|
<< '=' << summary.band_values[4][lead];
|
||||||
<< std::endl;
|
|
||||||
|
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)
|
void DrainImpedance(XYParserHandle parser)
|
||||||
{
|
{
|
||||||
std::array<XYParserImpedanceSummary, 4> summaries{};
|
std::array<XYParserImpedanceSummary, 4> summaries{};
|
||||||
@@ -933,11 +979,26 @@ bool RunAlgorithmZmqRoundTrip(XYParserHandle parser,
|
|||||||
std::cout << "[AlgorithmZmqRx] payloadBytes=" << received
|
std::cout << "[AlgorithmZmqRx] payloadBytes=" << received
|
||||||
<< " doubles=" << (received / static_cast<int>(sizeof(double)))
|
<< " doubles=" << (received / static_cast<int>(sizeof(double)))
|
||||||
<< std::endl;
|
<< std::endl;
|
||||||
XYParser_FeedAlgorithmData(parser,
|
#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.data(),
|
||||||
response_payload.size(),
|
response_payload.size(),
|
||||||
nullptr,
|
nullptr,
|
||||||
0);
|
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) {
|
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));
|
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)
|
bool Send64ImpedanceSwitch(TcpClient& client, bool open)
|
||||||
{
|
{
|
||||||
std::array<std::uint8_t, 32> command{};
|
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));
|
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)
|
bool Send8ImpedanceSwitch(SerialPort& port, bool open)
|
||||||
{
|
{
|
||||||
std::array<std::uint8_t, 8> command{};
|
std::array<std::uint8_t, 8> command{};
|
||||||
@@ -1012,6 +1100,36 @@ bool Process8AlgorithmPath(XYParserHandle parser,
|
|||||||
return Process64AlgorithmPath(parser, converted, zmq_client, zmq_stats);
|
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)
|
bool SendTrigger(SerialPort& port, XYParserTriggerType trigger_type)
|
||||||
{
|
{
|
||||||
std::array<std::uint8_t, 8> command{};
|
std::array<std::uint8_t, 8> command{};
|
||||||
@@ -1031,7 +1149,7 @@ void PrintFrameProgress(const XYParserFrameSummary* summaries, int count)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const XYParserFrameSummary& last = summaries[static_cast<std::size_t>(count - 1)];
|
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 {
|
struct DeviceStateSnapshot {
|
||||||
std::uint8_t impedance_enabled = 0;
|
std::uint8_t impedance_enabled = 0;
|
||||||
std::uint8_t current_gain = 0;
|
std::uint8_t current_gain = 0;
|
||||||
@@ -1347,6 +1465,205 @@ int Run64Workflow(const DemoOptions& options)
|
|||||||
return 0;
|
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)
|
int Run8Workflow(const DemoOptions& options)
|
||||||
{
|
{
|
||||||
constexpr auto kImpedanceDuration = std::chrono::seconds(60);
|
constexpr auto kImpedanceDuration = std::chrono::seconds(60);
|
||||||
@@ -1486,6 +1803,9 @@ int wmain(int argc, wchar_t* argv[])
|
|||||||
#if defined(XY_WORKFLOW_DEMO_64)
|
#if defined(XY_WORKFLOW_DEMO_64)
|
||||||
constexpr int kImpedanceEnabled = 0;
|
constexpr int kImpedanceEnabled = 0;
|
||||||
constexpr int kParserGainDuringRun = 6;
|
constexpr int kParserGainDuringRun = 6;
|
||||||
|
#elif defined(XY_WORKFLOW_DEMO_2)
|
||||||
|
constexpr int kImpedanceEnabled = 0;
|
||||||
|
constexpr int kParserGainDuringRun = 6;
|
||||||
#else
|
#else
|
||||||
constexpr int kImpedanceEnabled = 1;
|
constexpr int kImpedanceEnabled = 1;
|
||||||
constexpr int kParserGainDuringRun = 24;
|
constexpr int kParserGainDuringRun = 24;
|
||||||
@@ -1513,6 +1833,8 @@ int wmain(int argc, wchar_t* argv[])
|
|||||||
|
|
||||||
#if defined(XY_WORKFLOW_DEMO_64)
|
#if defined(XY_WORKFLOW_DEMO_64)
|
||||||
return Run64Workflow(options);
|
return Run64Workflow(options);
|
||||||
|
#elif defined(XY_WORKFLOW_DEMO_2)
|
||||||
|
return Run2Workflow(options);
|
||||||
#else
|
#else
|
||||||
return Run8Workflow(options);
|
return Run8Workflow(options);
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -258,5 +258,174 @@ XYParser_Feed(8导原始数据)
|
|||||||
代码依据:
|
代码依据:
|
||||||
|
|
||||||
- `XYParser_Convert8ChFramesTo64Ch`
|
- `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