From 1f569276955a9406be5b4c04385a9a47d3659f90 Mon Sep 17 00:00:00 2001 From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com> Date: Tue, 23 Jul 2019 12:52:41 -0400 Subject: [PATCH 01/11] Recognize additional PRS1 900X settings. --- oscar/SleepLib/loader_plugins/prs1_loader.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp index dfddf6dd..38c8920e 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp +++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp @@ -4303,7 +4303,7 @@ bool PRS1DataChunk::ParseSummaryF5V3(void) CHECK_VALUE(data[pos+5], 0); CHECK_VALUE(data[pos+6], 2); CHECK_VALUE(data[pos+7], 1); - CHECK_VALUE(data[pos+8], 0); + CHECK_VALUES(data[pos+8], 0, 1); // 1 = patient disconnect alarm of 15 sec, not sure where time is encoded break; case 3: // Mask On tt += data[pos] | (data[pos+1] << 8); @@ -4457,10 +4457,11 @@ bool PRS1DataChunk::ParseSettingsF5V3(const unsigned char* data, int size) this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS_MIN, min_ps, GAIN)); this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS_MAX, max_ps, GAIN)); break; - case 0x14: // new to ASV, ??? - CHECK_VALUE(data[pos], 1); - CHECK_VALUE(data[pos+1], 0); - CHECK_VALUE(data[pos+2], 0); + case 0x14: // ASV backup rate + CHECK_VALUE(cpapmode, MODE_ASV_VARIABLE_EPAP); + CHECK_VALUES(data[pos], 1, 2); // 1 = auto, 2 = fixed BPM + //CHECK_VALUE(data[pos+1], 0); // 0 for auto, BPM for mode 2 + //CHECK_VALUE(data[pos+2], 0); // 0 for auto, timed inspiration for mode 2 (gain 0.1) break; /* case 0x2a: // EZ-Start @@ -4479,7 +4480,9 @@ bool PRS1DataChunk::ParseSettingsF5V3(const unsigned char* data, int size) this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_RAMP_PRESSURE, data[pos], GAIN)); break; case 0x2e: - CHECK_VALUE(data[pos], 0); + // [0x00, N] for Bi-Flex level N + // [0x20, 0x03] for no flex, rise time setting = 3, no rise lock + CHECK_VALUES(data[pos], 0, 0x20); //CHECK_VALUES(data[pos+1], 2, 3); // Bi-Flex level /* if (data[pos] != 0) { From 83b80cb252783cb840b3cffa0f2e0986d909f69e Mon Sep 17 00:00:00 2001 From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com> Date: Tue, 23 Jul 2019 20:40:24 -0400 Subject: [PATCH 02/11] Restrict the current PRS1 F3 summary parser to F3V6, which is all it could (badly) handle anyway. --- oscar/SleepLib/loader_plugins/prs1_loader.cpp | 116 +++++++++--------- oscar/SleepLib/loader_plugins/prs1_loader.h | 4 +- 2 files changed, 62 insertions(+), 58 deletions(-) diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp index 38c8920e..d95967e4 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp +++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp @@ -3549,12 +3549,66 @@ bool PRS1DataChunk::ParseSummaryF0V4(void) } -// TODO: This is probably only F3V6, as it uses mainblock, only present in fileVersion 3. -bool PRS1DataChunk::ParseSummaryF3(void) +bool PRS1DataChunk::ParseSummaryF3V6(void) { CPAPMode mode = MODE_UNKNOWN; EventDataType epap, ipap; + // TODO: The below mainblock creation is wrong. It should be removed when the summary + // parsing is fixed. + /* Example data block + 000000c6@0000: 00 [10] 01 [00 01 02 01 01 00 02 01 00 04 01 40 07 + 000000c6@0010: 01 60 1e 03 02 0c 14 2c 01 14 2d 01 40 2e 01 02 + 000000c6@0020: 2f 01 00 35 02 28 68 36 01 00 38 01 00 39 01 00 + 000000c6@0030: 3b 01 01 3c 01 80] 02 [00 01 00 01 01 00 02 01 00] + 000000c6@0040: 04 [00 00 28 68] 0c [78 00 2c 6c] 05 [e4 69] 07 [40 40] + 000000c6@0050: 08 [61 60] 0a [00 00 00 00 03 00 00 00 02 00 02 00 + 000000c6@0060: 05 00 2b 11 00 10 2b 5c 07 12 00 00] 03 [00 00 01 + 000000c6@0070: 1a 00 38 04] */ + const unsigned char * data = (unsigned char *)this->m_data.constData(); + if (this->fileVersion == 3) { + // Parse summary structures into bytearray map according to size given in header block + int size = this->m_data.size(); + + int pos = 0; + int bsize; + short val, len; + do { + val = data[pos++]; + auto it = this->hblock.find(val); + if (it == this->hblock.end()) { + qDebug() << "Block parse error in ParseSummary" << this->sessionid; + break; + } + bsize = it.value(); + + if (val != 1) { + if (this->hbdata.contains(val)) { + // We know this is entirely wrong. It will be removed after F3V6 is updated. + //qWarning() << this->sessionid << "duplicate hbdata val" << val; + } + // store the data block for later reference + this->hbdata[val] = QByteArray((const char *)(&data[pos]), bsize); + } else { + if (!this->mainblock.isEmpty()) { + qWarning() << this->sessionid << "duplicate mainblock"; + } + // Parse the nested data structure which contains settings + int p2 = 0; + do { + val = data[pos + p2++]; + len = data[pos + p2++]; + if (this->mainblock.contains(val)) { + qWarning() << this->sessionid << "duplicate mainblock val" << val; + } + this->mainblock[val] = QByteArray((const char *)(&data[pos+p2]), len); + p2 += len; + } while ((p2 < bsize) && ((pos+p2) < size)); + } + pos += bsize; + } while (pos < size); + } + QMap::iterator it; if ((it=this->mainblock.find(0x0a)) != this->mainblock.end()) { @@ -3856,7 +3910,7 @@ void PRS1DataChunk::ParseHumidifierSettingV3(unsigned char byte1, unsigned char } -// The below is based on a combination of the mainblock parsing for fileVersion == 3 +// The below is based on a combination of the old mainblock parsing for fileVersion == 3 // in ParseSummary() and the switch statements of ParseSummaryF0V6. // // Both compliance and summary files (at least for 200X and 400X machines) seem to have @@ -4724,58 +4778,6 @@ bool PRS1DataChunk::ParseSummary() return false; } - // TODO: The below mainblock creation is probably wrong. It should move to to its own function when it gets fixed. - /* Example data block - 000000c6@0000: 00 [10] 01 [00 01 02 01 01 00 02 01 00 04 01 40 07 - 000000c6@0010: 01 60 1e 03 02 0c 14 2c 01 14 2d 01 40 2e 01 02 - 000000c6@0020: 2f 01 00 35 02 28 68 36 01 00 38 01 00 39 01 00 - 000000c6@0030: 3b 01 01 3c 01 80] 02 [00 01 00 01 01 00 02 01 00] - 000000c6@0040: 04 [00 00 28 68] 0c [78 00 2c 6c] 05 [e4 69] 07 [40 40] - 000000c6@0050: 08 [61 60] 0a [00 00 00 00 03 00 00 00 02 00 02 00 - 000000c6@0060: 05 00 2b 11 00 10 2b 5c 07 12 00 00] 03 [00 00 01 - 000000c6@0070: 1a 00 38 04] */ - if (this->fileVersion == 3) { - // Parse summary structures into bytearray map according to size given in header block - int size = this->m_data.size(); - - int pos = 0; - int bsize; - short val, len; - do { - val = data[pos++]; - auto it = this->hblock.find(val); - if (it == this->hblock.end()) { - qDebug() << "Block parse error in ParseSummary" << this->sessionid; - break; - } - bsize = it.value(); - - if (val != 1) { - if (this->hbdata.contains(val)) { - // We know this is entirely wrong. It will be removed after F3V6 is updated. - //qWarning() << this->sessionid << "duplicate hbdata val" << val; - } - // store the data block for later reference - this->hbdata[val] = QByteArray((const char *)(&data[pos]), bsize); - } else { - if (!this->mainblock.isEmpty()) { - qWarning() << this->sessionid << "duplicate mainblock"; - } - // Parse the nested data structure which contains settings - int p2 = 0; - do { - val = data[pos + p2++]; - len = data[pos + p2++]; - if (this->mainblock.contains(val)) { - qWarning() << this->sessionid << "duplicate mainblock val" << val; - } - this->mainblock[val] = QByteArray((const char *)(&data[pos+p2]), len); - p2 += len; - } while ((p2 < bsize) && ((pos+p2) < size)); - } - pos += bsize; - } while (pos < size); - } // Family 0 = XPAP // Family 3 = BIPAP AVAPS // Family 5 = BIPAP AutoSV @@ -4790,7 +4792,9 @@ bool PRS1DataChunk::ParseSummary() return this->ParseSummaryF0V23(); } case 3: - return this->ParseSummaryF3(); + if (this->familyVersion == 6) { + return this->ParseSummaryF3V6(); + } break; case 5: if (this->familyVersion == 1) { diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.h b/oscar/SleepLib/loader_plugins/prs1_loader.h index daeeb0dd..6847d7da 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.h +++ b/oscar/SleepLib/loader_plugins/prs1_loader.h @@ -149,8 +149,8 @@ public: //! \brief Parse a single data chunk from a .001 file containing summary data for a family 0 CPAP/APAP family version 6 machine bool ParseSummaryF0V6(void); - //! \brief Parse a single data chunk from a .001 file containing summary data for a family 3 ventilator (family version 6?) machine - bool ParseSummaryF3(void); + //! \brief Parse a single data chunk from a .001 file containing summary data for a family 3 ventilator (family version 6) machine + bool ParseSummaryF3V6(void); //! \brief Parse a single data chunk from a .001 file containing summary data for a family 5 ASV family version 0-2 machine bool ParseSummaryF5V012(void); From 872fe74008a2aa0126c493085fb925fa805978a4 Mon Sep 17 00:00:00 2001 From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com> Date: Tue, 23 Jul 2019 20:54:39 -0400 Subject: [PATCH 03/11] Add stub F3V3 summary parser so that events and waveforms will still get loaded. --- oscar/SleepLib/loader_plugins/prs1_loader.cpp | 11 +++++++++++ oscar/SleepLib/loader_plugins/prs1_loader.h | 3 +++ 2 files changed, 14 insertions(+) diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp index d95967e4..f4987ce5 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp +++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp @@ -3549,6 +3549,15 @@ bool PRS1DataChunk::ParseSummaryF0V4(void) } +// TODO: Add support for F3V3 (1061T, 1160P). This is just a stub. +bool PRS1DataChunk::ParseSummaryF3V3(void) +{ + this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_CPAP_MODE, (int) MODE_UNKNOWN)); + this->duration = 0; + return true; +} + + bool PRS1DataChunk::ParseSummaryF3V6(void) { CPAPMode mode = MODE_UNKNOWN; @@ -4794,6 +4803,8 @@ bool PRS1DataChunk::ParseSummary() case 3: if (this->familyVersion == 6) { return this->ParseSummaryF3V6(); + } else if (this->familyVersion == 3) { + return this->ParseSummaryF3V3(); } break; case 5: diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.h b/oscar/SleepLib/loader_plugins/prs1_loader.h index 6847d7da..04381788 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.h +++ b/oscar/SleepLib/loader_plugins/prs1_loader.h @@ -149,6 +149,9 @@ public: //! \brief Parse a single data chunk from a .001 file containing summary data for a family 0 CPAP/APAP family version 6 machine bool ParseSummaryF0V6(void); + //! \brief Parse a single data chunk from a .001 file containing summary data for a family 3 ventilator (family version 3) machine + bool ParseSummaryF3V3(void); + //! \brief Parse a single data chunk from a .001 file containing summary data for a family 3 ventilator (family version 6) machine bool ParseSummaryF3V6(void); From 9e54b98cf6fb927ab937f20a401ee15e823f548f Mon Sep 17 00:00:00 2001 From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com> Date: Wed, 24 Jul 2019 16:50:51 -0400 Subject: [PATCH 04/11] First pass at actual F3V6 (1030X, 1130X) summary and settings support based on sample data. Events are still broken. --- oscar/SleepLib/loader_plugins/prs1_loader.cpp | 286 ++++++++++++++++++ oscar/SleepLib/loader_plugins/prs1_loader.h | 3 + 2 files changed, 289 insertions(+) diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp index f4987ce5..1f69d904 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp +++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp @@ -3558,6 +3558,291 @@ bool PRS1DataChunk::ParseSummaryF3V3(void) } +// Originally based on ParseSummaryF5V3, with changes observed in ventilator sample data +// +// TODO: surely there will be a way to merge ParseSummary (FV3) loops and abstract the machine-specific +// encodings into another function or class, but that's probably worth pursuing only after +// the details have been figured out. +bool PRS1DataChunk::ParseSummaryF3V6(void) +{ + if (this->family != 3 || this->familyVersion != 6) { + qWarning() << "ParseSummaryF3V6 called with family" << this->family << "familyVersion" << this->familyVersion; + return false; + } + const unsigned char * data = (unsigned char *)this->m_data.constData(); + int chunk_size = this->m_data.size(); + static const int minimum_sizes[] = { 1, 0x2e, 9, 7, 4, 2, 1, 2, 2, 1, 0x18, 2, 4 }; // F5V3 = { 1, 0x38, 4, 2, 4, 0x1e, 2, 4, 9 }; + static const int ncodes = sizeof(minimum_sizes) / sizeof(int); + // NOTE: The sizes contained in hblock can vary, even within a single machine, as can the length of hblock itself! + + // TODO: hardcoding this is ugly, think of a better approach + if (chunk_size < minimum_sizes[0] + minimum_sizes[1] + minimum_sizes[2]) { + qWarning() << this->sessionid << "summary data too short:" << chunk_size; + return false; + } + // We've once seen a short summary with no mask-on/off: just equipment-on, settings, 9, equipment-off + if (chunk_size < 75) UNEXPECTED_VALUE(chunk_size, ">= 75"); + + bool ok = true; + int pos = 0; + int code, size; + int tt = 0; + do { + code = data[pos++]; + if (!this->hblock.contains(code)) { + qWarning() << this->sessionid << "missing hblock entry for" << code; + ok = false; + break; + } + size = this->hblock[code]; + if (code < ncodes) { + // make sure the handlers below don't go past the end of the buffer + if (size < minimum_sizes[code]) { + qWarning() << this->sessionid << "slice" << code << "too small" << size << "<" << minimum_sizes[code]; + ok = false; + break; + } + } // else if it's past ncodes, we'll log its information below (rather than handle it) + if (pos + size > chunk_size) { + qWarning() << this->sessionid << "slice" << code << "@" << pos << "longer than remaining chunk"; + ok = false; + break; + } + + switch (code) { + case 0: // Equipment On + CHECK_VALUE(pos, 1); // Always first? + //CHECK_VALUE(data[pos], 0x10); // usually 0x10 for 1030X, sometimes 0x40 or 0x80 are set in addition or instead + CHECK_VALUE(size, 1); + break; + case 1: // Settings + ok = this->ParseSettingsF3V6(data + pos, size); + break; + case 2: // seems equivalent to F5V3 #9, comes right after settings, 9 bytes, identical values + CHECK_VALUE(data[pos], 0); + CHECK_VALUE(data[pos+1], 1); + CHECK_VALUE(data[pos+2], 0); + CHECK_VALUE(data[pos+3], 1); + CHECK_VALUE(data[pos+4], 1); + CHECK_VALUE(data[pos+5], 0); + CHECK_VALUE(data[pos+6], 2); + CHECK_VALUE(data[pos+7], 1); + CHECK_VALUES(data[pos+8], 0, 1); // 1 = patient disconnect alarm of 15 sec on F5V3, not sure where time is encoded + break; + case 4: // Mask On + tt += data[pos] | (data[pos+1] << 8); + this->AddEvent(new PRS1ParsedSliceEvent(tt, MaskOn)); + this->ParseHumidifierSettingV3(data[pos+2], data[pos+3]); + break; + case 5: // Mask Off + tt += data[pos] | (data[pos+1] << 8); + this->AddEvent(new PRS1ParsedSliceEvent(tt, MaskOff)); + break; + case 7: // Ventilator EPAP stats, presumably per mask-on slice + //CHECK_VALUE(data[pos], 0x69); // Average EPAP + //CHECK_VALUE(data[pos+1], 0x80); // Average 90% EPAP + break; + case 8: // Ventilator IPAP stats, presumably per mask-on slice + //CHECK_VALUE(data[pos], 0x86); // Average IPAP + //CHECK_VALUE(data[pos+1], 0xA8); // Average 90% IPAP + break; + case 0xa: // Patient statistics, presumably per mask-on slice + //CHECK_VALUE(data[pos], 0x00); // 16-bit OA count + CHECK_VALUE(data[pos+1], 0x00); + //CHECK_VALUE(data[pos+2], 0x00); // 16-bit CA count + CHECK_VALUE(data[pos+3], 0x00); + //CHECK_VALUE(data[pos+4], 0x00); // 16-bit minutes in LL + CHECK_VALUE(data[pos+5], 0x00); + //CHECK_VALUE(data[pos+6], 0x0A); // 16-bit VS count + CHECK_VALUE(data[pos+7], 0x00); + //CHECK_VALUE(data[pos+8], 0x01); // 16-bit H count (partial) + CHECK_VALUE(data[pos+9], 0x00); + //CHECK_VALUE(data[pos+0xa], 0x00); // 16-bit H count (partial) + CHECK_VALUE(data[pos+0xb], 0x00); + //CHECK_VALUE(data[pos+0xc], 0x00); // 16-bit RE count + CHECK_VALUE(data[pos+0xd], 0x00); + //CHECK_VALUE(data[pos+0xe], 0x3e); // average total leak + //CHECK_VALUE(data[pos+0xf], 0x03); // 16-bit H count (partial) + CHECK_VALUE(data[pos+0x10], 0x00); + //CHECK_VALUE(data[pos+0x11], 0x11); // average breath rate + //CHECK_VALUE(data[pos+0x12], 0x41); // average TV / 10 + //CHECK_VALUE(data[pos+0x13], 0x60); // average % PTB + //CHECK_VALUE(data[pos+0x14], 0x0b); // average minute vent + //CHECK_VALUE(data[pos+0x15], 0x1d); // average leak? (similar position to F5V3, similar delta to total leak) + //CHECK_VALUE(data[pos+0x16], 0x00); // 16-bit minutes in PB + CHECK_VALUE(data[pos+0x17], 0x00); + break; + case 3: // Equipment Off + tt += data[pos] | (data[pos+1] << 8); + this->AddEvent(new PRS1ParsedSliceEvent(tt, EquipmentOff)); + //CHECK_VALUES(data[pos+2], 1, 4); // bitmask, have seen 1, 4, 6, 0x41 + //CHECK_VALUE(data[pos+3], 0x17); // 0x16, etc. + CHECK_VALUES(data[pos+4], 0, 1); + //CHECK_VALUE(data[pos+5], 0x15); // 0x16, etc. + //CHECK_VALUES(data[pos+6], 0, 1); // or 2 + break; + case 0xc: // Humidier setting change + tt += data[pos] | (data[pos+1] << 8); // This adds to the total duration (otherwise it won't match report) + this->ParseHumidifierSettingV3(data[pos+2], data[pos+3]); + break; + default: + UNEXPECTED_VALUE(code, "known slice code"); + break; + } + pos += size; + } while (ok && pos < chunk_size); + + this->duration = tt; + + return ok; +} + + +// Based initially on ParseSettingsF5V3. Many of the codes look the same, like always starting with 0, 0x35 looking like +// a humidifier setting, etc., but the contents are sometimes a bit different, such as mode values and pressure settings. +// +// new settings to find: ... +bool PRS1DataChunk::ParseSettingsF3V6(const unsigned char* data, int size) +{ + static const QMap expected_lengths = { {0x1e,3}, {0x35,2} }; + bool ok = true; + + CPAPMode cpapmode = MODE_UNKNOWN; + + // F5V3 and F3V6 use a gain of 0.125 rather than 0.1 to allow for a maximum value of 30 cmH2O + static const float GAIN = 0.125; // TODO: parameterize this somewhere better + + int fixed_epap = 0; + int fixed_ipap = 0; + int min_ipap = 0; + int max_ipap = 0; + + // Parse the nested data structure which contains settings + int pos = 0; + do { + int code = data[pos++]; + int len = data[pos++]; + + int expected_len = 1; + if (expected_lengths.contains(code)) { + expected_len = expected_lengths[code]; + } + //CHECK_VALUE(len, expected_len); + if (len < expected_len) { + qWarning() << this->sessionid << "setting" << code << "too small" << len << "<" << expected_len; + ok = false; + break; + } + if (pos + len > size) { + qWarning() << this->sessionid << "setting" << code << "@" << pos << "longer than remaining slice"; + ok = false; + break; + } + + switch (code) { + case 0: // Device Mode + CHECK_VALUE(pos, 2); // always first? + // TODO: We may need additional enums for these modes, the below are just a rough guess mapping for now. + switch (data[pos]) { + case 1: cpapmode = MODE_BILEVEL_FIXED; break; // TODO This is marked "S - Bi-Flex" on reports. + case 2: cpapmode = MODE_ASV; break; // TODO: This is marked as "S/T" on reports, is that spontaneous/timed? Pressure also seems variable! + case 4: cpapmode = MODE_AVAPS; break; // "PC - AVAPS" on reports + default: + UNEXPECTED_VALUE(data[pos], "known device mode"); + break; + } + this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_CPAP_MODE, (int) cpapmode)); + break; + case 1: // ??? + if (cpapmode == MODE_AVAPS) { + CHECK_VALUE(data[pos], 2); // 2 when in AVAPS + } else { + CHECK_VALUES(data[pos], 0, 1); // 1 when in S - Bi-Flex, 0 when in S/T + } + break; + case 2: // ??? + CHECK_VALUE(data[pos], 0); + break; + case 4: // EPAP Pressure + // pressures seem variable on practice, maybe due to ramp or leaks? + fixed_epap = data[pos]; + this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_EPAP, fixed_epap, GAIN)); + break; + case 7: // IPAP Pressure + // pressures seem variable on practice, maybe due to ramp or leaks? + fixed_ipap = data[pos]; + this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP, fixed_ipap, GAIN)); + break; + case 8: // Min IPAP + min_ipap = data[pos]; + this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP_MIN, min_ipap, GAIN)); + break; + case 9: // Max IPAP + max_ipap = data[pos]; + this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP_MAX, max_ipap, GAIN)); + break; + case 0x19: // Tidal Volume (AVAPS) + CHECK_VALUE(data[pos], 47); // gain 10.0 + break; + case 0x1e: // Backup rate (S/T and AVAPS) + CHECK_VALUES(cpapmode, MODE_ASV, MODE_AVAPS); + // TODO: Does mode breath rate off mean this is essentially bilevel? The pressure graphs are confusing. + CHECK_VALUES(data[pos], 0, 2); // 0 = Breath Rate off (S), 2 = fixed BPM (1 = auto on F5V3 setting 0x14) + //CHECK_VALUE(data[pos+1], 10); // BPM for mode 2 + //CHECK_VALUE(data[pos+2], 10); // timed inspiration for mode 2 (gain 0.1) + break; + case 0x2c: // Ramp Time + if (data[pos] != 0) { // 0 == ramp off, and ramp pressure setting doesn't appear + this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_RAMP_TIME, data[pos])); + } + break; + case 0x2d: // Ramp Pressure (with ASV/ventilator pressure encoding) + this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_RAMP_PRESSURE, data[pos], GAIN)); + break; + case 0x2e: // Bi-Flex level or Rise Time + // On F5V3 the first byte could specify Bi-Flex or Rise Time, and second byte contained the value. + // On F3V6 there's only one byte, which seems to correspond to Rise Time on the reports in modes 2 and 4, + // and to Bi-Flex Setting (level) on mode 1. + break; + case 0x2f: // Rise Time lock? (was flex lock on F0V6, 0x80 for locked) + CHECK_VALUE(data[pos], 0); + break; + case 0x35: // Humidifier setting + this->ParseHumidifierSettingV3(data[pos], data[pos+1], true); + break; + case 0x36: // Mask Resistance Lock + CHECK_VALUE(data[pos], 0); // 0x80 = locked on F5V3, not yet observed on F3V6 + break; + case 0x38: // Mask Resistance + if (data[pos] != 0) { // 0 == mask resistance off + this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_SYSTEMONE_RESIST_SETTING, data[pos])); + } + break; + case 0x39: + CHECK_VALUE(data[pos], 0); + break; + case 0x3b: // Tubing Type + if (data[pos] != 0) { + CHECK_VALUES(data[pos], 2, 1); // 15HT = 2, 15 = 1, 22 = 0, though report only says "15" for 15HT + } + break; + case 0x3c: // View Optional Screens + CHECK_VALUES(data[pos], 0, 0x80); + break; + default: + qDebug() << "Unknown setting:" << hex << code << "in" << this->sessionid << "at" << pos; + this->AddEvent(new PRS1UnknownDataEvent(QByteArray((const char*) data, size), pos, len)); + break; + } + + pos += len; + } while (ok && pos + 2 <= size); + + return ok; +} + + +#if 0 bool PRS1DataChunk::ParseSummaryF3V6(void) { CPAPMode mode = MODE_UNKNOWN; @@ -3656,6 +3941,7 @@ bool PRS1DataChunk::ParseSummaryF3V6(void) return true; } +#endif bool PRS1DataChunk::ParseSummaryF5V012(void) diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.h b/oscar/SleepLib/loader_plugins/prs1_loader.h index 04381788..f43cf774 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.h +++ b/oscar/SleepLib/loader_plugins/prs1_loader.h @@ -209,6 +209,9 @@ protected: //! \brief Parse a settings slice from a .000 and .001 file bool ParseSettingsF5V3(const unsigned char* data, int size); + + //! \brief Parse a settings slice from a .000 and .001 file + bool ParseSettingsF3V6(const unsigned char* data, int size); }; From d33e7585bfa5af77a2357701d4ee88a1ce60b199 Mon Sep 17 00:00:00 2001 From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com> Date: Wed, 24 Jul 2019 16:51:50 -0400 Subject: [PATCH 05/11] Remove broken original F3V6 summary parser. --- oscar/SleepLib/loader_plugins/prs1_loader.cpp | 102 ------------------ 1 file changed, 102 deletions(-) diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp index 1f69d904..d23ae962 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp +++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp @@ -3842,108 +3842,6 @@ bool PRS1DataChunk::ParseSettingsF3V6(const unsigned char* data, int size) } -#if 0 -bool PRS1DataChunk::ParseSummaryF3V6(void) -{ - CPAPMode mode = MODE_UNKNOWN; - EventDataType epap, ipap; - - // TODO: The below mainblock creation is wrong. It should be removed when the summary - // parsing is fixed. - /* Example data block - 000000c6@0000: 00 [10] 01 [00 01 02 01 01 00 02 01 00 04 01 40 07 - 000000c6@0010: 01 60 1e 03 02 0c 14 2c 01 14 2d 01 40 2e 01 02 - 000000c6@0020: 2f 01 00 35 02 28 68 36 01 00 38 01 00 39 01 00 - 000000c6@0030: 3b 01 01 3c 01 80] 02 [00 01 00 01 01 00 02 01 00] - 000000c6@0040: 04 [00 00 28 68] 0c [78 00 2c 6c] 05 [e4 69] 07 [40 40] - 000000c6@0050: 08 [61 60] 0a [00 00 00 00 03 00 00 00 02 00 02 00 - 000000c6@0060: 05 00 2b 11 00 10 2b 5c 07 12 00 00] 03 [00 00 01 - 000000c6@0070: 1a 00 38 04] */ - const unsigned char * data = (unsigned char *)this->m_data.constData(); - if (this->fileVersion == 3) { - // Parse summary structures into bytearray map according to size given in header block - int size = this->m_data.size(); - - int pos = 0; - int bsize; - short val, len; - do { - val = data[pos++]; - auto it = this->hblock.find(val); - if (it == this->hblock.end()) { - qDebug() << "Block parse error in ParseSummary" << this->sessionid; - break; - } - bsize = it.value(); - - if (val != 1) { - if (this->hbdata.contains(val)) { - // We know this is entirely wrong. It will be removed after F3V6 is updated. - //qWarning() << this->sessionid << "duplicate hbdata val" << val; - } - // store the data block for later reference - this->hbdata[val] = QByteArray((const char *)(&data[pos]), bsize); - } else { - if (!this->mainblock.isEmpty()) { - qWarning() << this->sessionid << "duplicate mainblock"; - } - // Parse the nested data structure which contains settings - int p2 = 0; - do { - val = data[pos + p2++]; - len = data[pos + p2++]; - if (this->mainblock.contains(val)) { - qWarning() << this->sessionid << "duplicate mainblock val" << val; - } - this->mainblock[val] = QByteArray((const char *)(&data[pos+p2]), len); - p2 += len; - } while ((p2 < bsize) && ((pos+p2) < size)); - } - pos += bsize; - } while (pos < size); - } - - QMap::iterator it; - - if ((it=this->mainblock.find(0x0a)) != this->mainblock.end()) { - mode = MODE_CPAP; - this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PRESSURE, it.value()[0])); - } else if ((it=this->mainblock.find(0x0d)) != this->mainblock.end()) { - mode = MODE_APAP; - this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PRESSURE_MIN, it.value()[0])); - this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PRESSURE_MAX, it.value()[1])); - } else if ((it=this->mainblock.find(0x0e)) != this->mainblock.end()) { - mode = MODE_BILEVEL_FIXED; - ipap = it.value()[0]; - epap = it.value()[1]; - this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP, ipap)); - this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_EPAP, epap)); - this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS, ipap - epap)); - } else if ((it=this->mainblock.find(0x0f)) != this->mainblock.end()) { - mode = MODE_BILEVEL_AUTO_VARIABLE_PS; - this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_EPAP_MIN, it.value()[0])); - this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP_MAX, it.value()[1])); - this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS_MIN, it.value()[2])); - this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS_MAX, it.value()[3])); - } else if ((it=this->mainblock.find(0x10)) != this->mainblock.end()) { - mode = MODE_APAP; // Disgusting APAP "IQ" trial - this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PRESSURE_MIN, it.value()[0])); - this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PRESSURE_MAX, it.value()[1])); - } - - this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_CPAP_MODE, (int) mode)); - - if ((it=this->hbdata.find(5)) != this->hbdata.end()) { - this->duration = (it.value()[1] << 8 ) + it.value()[0]; - } else { - qWarning() << this->sessionid << "missing summary duration"; - } - - return true; -} -#endif - - bool PRS1DataChunk::ParseSummaryF5V012(void) { const unsigned char * data = (unsigned char *)this->m_data.constData(); From a2bcbf1b00e159386792df6b0975a170a1d2150d Mon Sep 17 00:00:00 2001 From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com> Date: Wed, 24 Jul 2019 22:42:00 -0400 Subject: [PATCH 06/11] Fix pressure gain for F3V6 events and waveforms. Also change fileVersion == 3 tests to appropriate familyVersion for ASV and ventilators, respectively. --- oscar/SleepLib/loader_plugins/prs1_loader.cpp | 23 ++++++++++++------- oscar/SleepLib/loader_plugins/prs1_loader.h | 6 ++--- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp index d23ae962..bf3b6cef 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp +++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp @@ -2277,8 +2277,12 @@ bool PRS1DataChunk::ParseEventsF5V012(void) } -bool PRS1Import::ParseF3EventsV3() + +bool PRS1Import::ParseEventsF3V6() { + // F3V6 uses a gain of 0.125 rather than 0.1 to allow for a maximum value of 30 cmH2O + static const float GAIN = 0.125F; // TODO: parameterize this somewhere better + // Required channels EventList *OA = session->AddEventList(CPAP_Obstructive, EVL_Event); EventList *HY = session->AddEventList(CPAP_Hypopnea, EVL_Event); @@ -2292,8 +2296,8 @@ bool PRS1Import::ParseF3EventsV3() EventList *PB = session->AddEventList(CPAP_PB, EVL_Event); EventList *PTB = session->AddEventList(CPAP_PTB, EVL_Event); EventList *TB = session->AddEventList(PRS1_TimedBreath, EVL_Event); - EventList *IPAP = session->AddEventList(CPAP_IPAP, EVL_Event, 0.1F); - EventList *EPAP = session->AddEventList(CPAP_EPAP, EVL_Event, 0.1F); + EventList *IPAP = session->AddEventList(CPAP_IPAP, EVL_Event, GAIN); + EventList *EPAP = session->AddEventList(CPAP_EPAP, EVL_Event, GAIN); EventList *RE = session->AddEventList(CPAP_RERA, EVL_Event); EventList *ZZ = session->AddEventList(CPAP_NRI, EVL_Event); EventList *TMV = session->AddEventList(CPAP_Test1, EVL_Event); @@ -5081,14 +5085,16 @@ bool PRS1Import::ParseEvents() res = ParseF0Events(); break; case 3: - if (event->fileVersion == 3) { - res = ParseF3EventsV3(); + // NOTE: The original comment in the header for ParseF3EventsV3 said there was a 1060P with fileVersion 3. + // We've never seen that, so we're reverting to checking familyVersion. + if (event->familyVersion == 6) { + res = ParseEventsF3V6(); } else { res = ParseF3Events(); } break; case 5: - if (event->fileVersion==3) { + if (event->familyVersion == 3) { res = ParseEventsF5V3(); } else { res = ParseF5Events(); @@ -5333,8 +5339,9 @@ bool PRS1Import::ParseWaveforms() if (num > 1) { float pressure_gain = 0.1F; // standard pressure gain - if (waveform->family == 5 && waveform->familyVersion == 3) { - // F5V3 uses a gain of 0.125 rather than 0.1 to allow for a maximum value of 30 cmH2O + if ((waveform->family == 5 && waveform->familyVersion == 3) || + (waveform->family == 3 && waveform->familyVersion == 6)){ + // F5V3 and F3V6 use a gain of 0.125 rather than 0.1 to allow for a maximum value of 30 cmH2O pressure_gain = 0.125F; // TODO: this should be parameterized somewhere better, once we have a clear idea of which machines use this } diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.h b/oscar/SleepLib/loader_plugins/prs1_loader.h index f43cf774..a8e49189 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.h +++ b/oscar/SleepLib/loader_plugins/prs1_loader.h @@ -277,11 +277,11 @@ public: bool ParseF0Events(); //! \brief Parse a single data chunk from a .002 file containing event data for a AVAPS 1060P machine bool ParseF3Events(); - //! \brief Parse a single data chunk from a .002 file containing event data for a AVAPS 1060P machine file version 3 - bool ParseF3EventsV3(); + //! \brief Parse a single data chunk from a .002 file containing event data for a family 3 ventilator machine (family version 6) + bool ParseEventsF3V6(); //! \brief Parse a single data chunk from a .002 file containing event data for a family 5 ASV machine (which has a different format) bool ParseF5Events(); - //! \brief Parse a single data chunk from a .002 file containing event data for a family 5 ASV file version 3 machine (which has a different format again) + //! \brief Parse a single data chunk from a .002 file containing event data for a family 5 ASV family version 3 machine (which has a different format again) bool ParseEventsF5V3(); From eedd41efdf33449a198a4d5adf0401733677db54 Mon Sep 17 00:00:00 2001 From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com> Date: Thu, 25 Jul 2019 21:44:36 -0400 Subject: [PATCH 07/11] First pass at parsing F3V6 events, largely based on F5V3 and revised based on a sample session. --- oscar/SleepLib/loader_plugins/prs1_loader.cpp | 223 +++++++++++++++++- 1 file changed, 213 insertions(+), 10 deletions(-) diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp index bf3b6cef..b9b20f54 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp +++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp @@ -2289,6 +2289,7 @@ bool PRS1Import::ParseEventsF3V6() EventList *CA = session->AddEventList(CPAP_ClearAirway, EVL_Event); EventList *LL = session->AddEventList(CPAP_LargeLeak, EVL_Event); + EventList *TOTLEAK = session->AddEventList(CPAP_LeakTotal, EVL_Event); EventList *LEAK = session->AddEventList(CPAP_Leak, EVL_Event); EventList *RR = session->AddEventList(CPAP_RespRate, EVL_Event); EventList *TV = session->AddEventList(CPAP_TidalVolume, EVL_Event, 10.0F); @@ -2300,15 +2301,20 @@ bool PRS1Import::ParseEventsF3V6() EventList *EPAP = session->AddEventList(CPAP_EPAP, EVL_Event, GAIN); EventList *RE = session->AddEventList(CPAP_RERA, EVL_Event); EventList *ZZ = session->AddEventList(CPAP_NRI, EVL_Event); + EventList *SNORE = session->AddEventList(CPAP_Snore, EVL_Event); EventList *TMV = session->AddEventList(CPAP_Test1, EVL_Event); EventList *FLOW = session->AddEventList(CPAP_Test2, EVL_Event); - qint64 t; - // missing session->updateFirst(t)? + qint64 duration; + qint64 t = qint64(event->timestamp) * 1000L; + session->updateFirst(t); bool ok; ok = event->ParseEvents(MODE_UNKNOWN); + if (!ok) { + return false; + } for (int i=0; i < event->m_parsedData.count(); i++) { PRS1ParsedEvent* e = event->m_parsedData.at(i); @@ -2322,7 +2328,12 @@ bool PRS1Import::ParseEventsF3V6() EPAP->AddEvent(t, e->m_value); break; case PRS1TimedBreathEvent::TYPE: - TB->AddEvent(t, e->m_duration); + // The duration appears to correspond to the length of the timed breath in seconds when multiplied by 0.1 (100ms)! + // TODO: consider changing parsers to use milliseconds for time, since it turns out there's at least one way + // they can express durations less than 1 second. + // TODO: consider allowing OSCAR to record millisecond durations so that the display will say "2.1" instead of "21" or "2". + duration = e->m_duration * 100L; // for now do this here rather than in parser, since parser events don't use milliseconds + TB->AddEvent(t - duration, e->m_duration * 0.1F); // TODO: a gain of 0.1 should render this unnecessary, but gain doesn't seem to work currently break; case PRS1ObstructiveApneaEvent::TYPE: OA->AddEvent(t, e->m_duration); @@ -2334,14 +2345,28 @@ bool PRS1Import::ParseEventsF3V6() HY->AddEvent(t, e->m_duration); break; case PRS1PeriodicBreathingEvent::TYPE: - PB->AddEvent(t, e->m_duration); + // TODO: The graphs silently treat the timestamp of a span as an end time rather than start (see gFlagsLine::paint). + // Decide whether to preserve that behavior or change it universally and update either this code or comment. + duration = e->m_duration * 1000L; + PB->AddEvent(t + duration, e->m_duration); break; case PRS1LargeLeakEvent::TYPE: - LL->AddEvent(t, e->m_duration); + // TODO: see PB comment above. + duration = e->m_duration * 1000L; + LL->AddEvent(t + duration, e->m_duration); + break; + case PRS1TotalLeakEvent::TYPE: + TOTLEAK->AddEvent(t, e->m_value); break; case PRS1LeakEvent::TYPE: LEAK->AddEvent(t, e->m_value); break; + case PRS1SnoreEvent::TYPE: // snore count that shows up in flags but not waveform + // TODO: The numeric snore graph is the right way to present this information, + // but it needs to be shifted left 2 minutes, since it's not a starting value + // but a past statistic. + SNORE->AddEvent(t, e->m_value); + break; case PRS1RespiratoryRateEvent::TYPE: RR->AddEvent(t, e->m_value); break; @@ -2394,19 +2419,193 @@ bool PRS1Import::ParseEventsF3V6() } } - if (!ok) { - return false; - } + //t = qint64(event->timestamp + event->duration) * 1000L; + session->updateLast(t); + session->m_cnt.clear(); + session->m_cph.clear(); + + session->m_valuesummary[CPAP_Pressure].clear(); + session->m_valuesummary.erase(session->m_valuesummary.find(CPAP_Pressure)); return true; } // 1030X, 11030X series +// based on ParseEventsF5V3, updated for F3V6 bool PRS1DataChunk::ParseEventsF3V6(void) { - // AVAPS machine... it's delta packed, unlike the older ones?? (double check that! :/) + if (this->family != 3 || this->familyVersion != 6) { + qWarning() << "ParseEventsF3V6 called with family" << this->family << "familyVersion" << this->familyVersion; + return false; + } + const unsigned char * data = (unsigned char *)this->m_data.constData(); + int chunk_size = this->m_data.size(); + static const int minimum_sizes[] = { 2, 3, 0xe, 3, 3, 3, 4, 5, 3, 5, 3, 3, 2, 2, 2, 2 }; + // F5V3: { 2, [3,] 3, 0xd, 3, 3, 3, 4, 3, 2, 5, 5, 3, 3, 3, 3 }; + static const int ncodes = sizeof(minimum_sizes) / sizeof(int); + if (chunk_size < 1) { + // This does occasionally happen. + qDebug() << this->sessionid << "Empty event data"; + return false; + } + + // F3V6 uses a gain of 0.125 rather than 0.1 to allow for a maximum value of 30 cmH2O + static const float GAIN = 0.125; // TODO: this should be parameterized somewhere more logical + bool ok = true; + int pos = 0, startpos; + int code, size; + int t = 0; + int /*elapsed,*/ duration; + do { + code = data[pos++]; + if (!this->hblock.contains(code)) { + qWarning() << this->sessionid << "missing hblock entry for event" << code; + ok = false; + break; + } + size = this->hblock[code]; + if (code < ncodes) { + // make sure the handlers below don't go past the end of the buffer + if (size < minimum_sizes[code]) { + qWarning() << this->sessionid << "event" << code << "too small" << size << "<" << minimum_sizes[code]; + ok = false; + break; + } + } // else if it's past ncodes, we'll log its information below (rather than handle it) + if (pos + size > chunk_size) { + qWarning() << this->sessionid << "event" << code << "@" << pos << "longer than remaining chunk"; + ok = false; + break; + } + startpos = pos; + t += data[pos] | (data[pos+1] << 8); + pos += 2; + + switch (code) { + /* + case 1: // Pressure adjustment + // TODO: Have OSCAR treat EPAP adjustment events differently than (average?) stats below. + //this->AddEvent(new PRS1EPAPEvent(t, data[pos++], GAIN)); + this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos-1, size+1)); + break; + */ + case 1: // Timed Breath + // TB events have a duration in 0.1s, based on the review of pressure waveforms. + // TODO: Ideally the starting time here would be adjusted here, but PRS1ParsedEvents + // currently assume integer seconds rather than ms, so that's done at import. + duration = data[pos++]; + // TODO: make sure F3 import logic matches F5 in adjusting TB start time + this->AddEvent(new PRS1TimedBreathEvent(t, duration)); + break; + case 2: // Statistics + // These appear every 2 minutes, so presumably summarize the preceding period. + pos++; // TODO: 0 = ??? + this->AddEvent(new PRS1EPAPEvent(t, data[pos++], GAIN)); // 01=EPAP (average?) + this->AddEvent(new PRS1IPAPEvent(t, data[pos++], GAIN)); // 02=IPAP (average?) + this->AddEvent(new PRS1TotalLeakEvent(t, data[pos++])); // 03=Total leak (average?) + this->AddEvent(new PRS1RespiratoryRateEvent(t, data[pos++])); // 04=Breaths Per Minute (average?) + this->AddEvent(new PRS1PatientTriggeredBreathsEvent(t, data[pos++])); // 05=Patient Triggered Breaths (average?) + this->AddEvent(new PRS1MinuteVentilationEvent(t, data[pos++])); // 06=Minute Ventilation (average?) + this->AddEvent(new PRS1TidalVolumeEvent(t, data[pos++])); // 07=Tidal Volume (average?) + this->AddEvent(new PRS1Test2Event(t, data[pos++])); // 08=Flow??? + this->AddEvent(new PRS1Test1Event(t, data[pos++])); // 09=TMV??? + this->AddEvent(new PRS1SnoreEvent(t, data[pos++])); // 0A=Snore count // TODO: not a VS on official waveform, but appears in flags and contributes to overall VS index + this->AddEvent(new PRS1LeakEvent(t, data[pos++])); // 0B=Leak (average?) + break; + /* + case 0x04: // Pressure Pulse + duration = data[pos++]; // TODO: is this a duration? + this->AddEvent(new PRS1PressurePulseEvent(t, duration)); + break; + case 0x05: // Obstructive Apnea + // OA events are instantaneous flags with no duration: reviewing waveforms + // shows that the time elapsed between the flag and reporting often includes + // non-apnea breathing. + elapsed = data[pos++]; + this->AddEvent(new PRS1ObstructiveApneaEvent(t - elapsed, 0)); + break; + case 0x06: // Clear Airway Apnea + // CA events are instantaneous flags with no duration: reviewing waveforms + // shows that the time elapsed between the flag and reporting often includes + // non-apnea breathing. + elapsed = data[pos++]; + this->AddEvent(new PRS1ClearAirwayEvent(t - elapsed, 0)); + break; + case 0x07: // Hypopnea + // TODO: How is this hypopnea different from events 0xd and 0xe? + // TODO: What is the first byte? + pos++; // unknown first byte? + elapsed = data[pos++]; // based on sample waveform, the hypopnea is over after this + this->AddEvent(new PRS1HypopneaEvent(t - elapsed, 0)); + break; + case 0x08: // Flow Limitation + // TODO: We should revisit whether this is elapsed or duration once (if) + // we start calculating flow limitations ourselves. Flow limitations aren't + // as obvious as OA/CA when looking at a waveform. + elapsed = data[pos++]; + this->AddEvent(new PRS1FlowLimitationEvent(t - elapsed, 0)); + break; + case 0x09: // Vibratory Snore + // VS events are instantaneous flags with no duration, drawn on the official waveform. + // The current thinking is that these are the snores that cause a change in auto-titrating + // pressure. The snoring statistic above seems to be a total count. It's unclear whether + // the trigger for pressure change is severity or count or something else. + // no data bytes + this->AddEvent(new PRS1VibratorySnoreEvent(t, 0)); + break; + case 0x0a: // Periodic Breathing + // PB events are reported some time after they conclude, and they do have a reported duration. + duration = 2 * (data[pos] | (data[pos+1] << 8)); + pos += 2; + elapsed = data[pos++]; + this->AddEvent(new PRS1PeriodicBreathingEvent(t - elapsed - duration, duration)); + break; + case 0x0b: // Large Leak + // LL events are reported some time after they conclude, and they do have a reported duration. + duration = 2 * (data[pos] | (data[pos+1] << 8)); + pos += 2; + elapsed = data[pos++]; + this->AddEvent(new PRS1LargeLeakEvent(t - elapsed - duration, duration)); + break; + case 0x0d: // Hypopnea + // TODO: Why does this hypopnea have a different event code? + // fall through + case 0x0e: // Hypopnea + */ + case 0x0b: // Hypopnea + // TODO: We should revisit whether this is elapsed or duration once (if) + // we start calculating hypopneas ourselves. Their official definition + // is 40% reduction in flow lasting at least 10s. + duration = data[pos++]; + this->AddEvent(new PRS1HypopneaEvent(t - duration, 0)); + break; + /* + case 0x0f: + // TODO: some other pressure adjustment? + // Appears near the beginning and end of a session when Opti-Start is on, at least once in middle + //CHECK_VALUES(data[pos], 0x20, 0x28); + this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos-1, size+1)); + break; + */ + default: + qWarning() << "Unknown event:" << code << "in" << this->sessionid << "at" << startpos-1; + this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos-1, size+1)); + break; + } + pos = startpos + size; + } while (ok && pos < chunk_size); + + this->duration = t; + + return ok; +} + + +#if 0 +bool PRS1DataChunk::ParseEventsF3V6(void) +{ if (this->family != 3 || this->familyVersion != 6) { qWarning() << "ParseEventsF3V6 called with family" << this->family << "familyVersion" << this->familyVersion; return false; @@ -2509,6 +2708,7 @@ bool PRS1DataChunk::ParseEventsF3V6(void) } return true; } +#endif bool PRS1Import::ParseF3Events() @@ -2590,9 +2790,12 @@ bool PRS1Import::ParseF3Events() } -// 1160P series +// 1061T, 1160P series bool PRS1DataChunk::ParseEventsF3V3(void) { + // NOTE: Older ventilators (BiPAP S/T and AVAPS) machines don't use timestamped events like everything else. + // Instead, they use a fixed interval format like waveforms do (see PRS1_HTYPE_INTERVAL). + if (this->family != 3 || this->familyVersion != 3) { qWarning() << "ParseEventsF3V3 called with family" << this->family << "familyVersion" << this->familyVersion; return false; From 56684de3bcc8a882f8181c0f3336ef553daf2277 Mon Sep 17 00:00:00 2001 From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com> Date: Fri, 26 Jul 2019 22:13:26 -0400 Subject: [PATCH 08/11] Add support for more F3V6 events based on more sample sessions. --- oscar/SleepLib/loader_plugins/prs1_loader.cpp | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp index b9b20f54..26b5ade5 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp +++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp @@ -2305,6 +2305,8 @@ bool PRS1Import::ParseEventsF3V6() EventList *TMV = session->AddEventList(CPAP_Test1, EVL_Event); EventList *FLOW = session->AddEventList(CPAP_Test2, EVL_Event); + // On-demand channels + EventList *PP = nullptr; qint64 duration; qint64 t = qint64(event->timestamp) * 1000L; @@ -2391,6 +2393,12 @@ bool PRS1Import::ParseEventsF3V6() case PRS1Test2Event::TYPE: FLOW->AddEvent(t, e->m_value); break; + case PRS1PressurePulseEvent::TYPE: + if (!PP) { + if (!(PP = session->AddEventList(CPAP_PressurePulse, EVL_Event))) { return false; } + } + PP->AddEvent(t, e->m_value); + break; case PRS1UnknownDataEvent::TYPE: { PRS1UnknownDataEvent* unk = (PRS1UnknownDataEvent*) e; @@ -2457,7 +2465,7 @@ bool PRS1DataChunk::ParseEventsF3V6(void) int pos = 0, startpos; int code, size; int t = 0; - int /*elapsed,*/ duration; + int elapsed, duration; do { code = data[pos++]; if (!this->hblock.contains(code)) { @@ -2514,32 +2522,32 @@ bool PRS1DataChunk::ParseEventsF3V6(void) this->AddEvent(new PRS1SnoreEvent(t, data[pos++])); // 0A=Snore count // TODO: not a VS on official waveform, but appears in flags and contributes to overall VS index this->AddEvent(new PRS1LeakEvent(t, data[pos++])); // 0B=Leak (average?) break; - /* - case 0x04: // Pressure Pulse + case 0x03: // Pressure Pulse duration = data[pos++]; // TODO: is this a duration? this->AddEvent(new PRS1PressurePulseEvent(t, duration)); break; - case 0x05: // Obstructive Apnea + case 0x04: // Obstructive Apnea // OA events are instantaneous flags with no duration: reviewing waveforms // shows that the time elapsed between the flag and reporting often includes // non-apnea breathing. elapsed = data[pos++]; this->AddEvent(new PRS1ObstructiveApneaEvent(t - elapsed, 0)); break; - case 0x06: // Clear Airway Apnea + case 0x05: // Clear Airway Apnea // CA events are instantaneous flags with no duration: reviewing waveforms // shows that the time elapsed between the flag and reporting often includes // non-apnea breathing. elapsed = data[pos++]; this->AddEvent(new PRS1ClearAirwayEvent(t - elapsed, 0)); break; - case 0x07: // Hypopnea + case 0x06: // Hypopnea // TODO: How is this hypopnea different from events 0xd and 0xe? // TODO: What is the first byte? pos++; // unknown first byte? elapsed = data[pos++]; // based on sample waveform, the hypopnea is over after this this->AddEvent(new PRS1HypopneaEvent(t - elapsed, 0)); break; + /* case 0x08: // Flow Limitation // TODO: We should revisit whether this is elapsed or duration once (if) // we start calculating flow limitations ourselves. Flow limitations aren't @@ -2556,24 +2564,28 @@ bool PRS1DataChunk::ParseEventsF3V6(void) this->AddEvent(new PRS1VibratorySnoreEvent(t, 0)); break; case 0x0a: // Periodic Breathing + */ + case 0x07: // Periodic Breathing // PB events are reported some time after they conclude, and they do have a reported duration. duration = 2 * (data[pos] | (data[pos+1] << 8)); pos += 2; elapsed = data[pos++]; this->AddEvent(new PRS1PeriodicBreathingEvent(t - elapsed - duration, duration)); break; - case 0x0b: // Large Leak + case 0x08: // RERA + elapsed = data[pos++]; // based on sample waveform, the RERA is over after this + this->AddEvent(new PRS1RERAEvent(t - elapsed, 0)); + break; + case 0x09: // Large Leak // LL events are reported some time after they conclude, and they do have a reported duration. duration = 2 * (data[pos] | (data[pos+1] << 8)); pos += 2; elapsed = data[pos++]; this->AddEvent(new PRS1LargeLeakEvent(t - elapsed - duration, duration)); break; - case 0x0d: // Hypopnea + case 0x0a: // Hypopnea // TODO: Why does this hypopnea have a different event code? // fall through - case 0x0e: // Hypopnea - */ case 0x0b: // Hypopnea // TODO: We should revisit whether this is elapsed or duration once (if) // we start calculating hypopneas ourselves. Their official definition From 5835e6de9cd21a1f1cccffdc92920eca6a154952 Mon Sep 17 00:00:00 2001 From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com> Date: Fri, 26 Jul 2019 22:29:23 -0400 Subject: [PATCH 09/11] Remove commented-out F3V6 event code. --- oscar/SleepLib/loader_plugins/prs1_loader.cpp | 149 +----------------- 1 file changed, 6 insertions(+), 143 deletions(-) diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp index 26b5ade5..af58d1ec 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp +++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp @@ -2449,8 +2449,7 @@ bool PRS1DataChunk::ParseEventsF3V6(void) } const unsigned char * data = (unsigned char *)this->m_data.constData(); int chunk_size = this->m_data.size(); - static const int minimum_sizes[] = { 2, 3, 0xe, 3, 3, 3, 4, 5, 3, 5, 3, 3, 2, 2, 2, 2 }; - // F5V3: { 2, [3,] 3, 0xd, 3, 3, 3, 4, 3, 2, 5, 5, 3, 3, 3, 3 }; + static const int minimum_sizes[] = { 2, 3, 0xe, 3, 3, 3, 4, 5, 3, 5, 3, 3, 2, 2, 2, 2 }; static const int ncodes = sizeof(minimum_sizes) / sizeof(int); if (chunk_size < 1) { @@ -2492,13 +2491,7 @@ bool PRS1DataChunk::ParseEventsF3V6(void) pos += 2; switch (code) { - /* - case 1: // Pressure adjustment - // TODO: Have OSCAR treat EPAP adjustment events differently than (average?) stats below. - //this->AddEvent(new PRS1EPAPEvent(t, data[pos++], GAIN)); - this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos-1, size+1)); - break; - */ + // case 0x00? case 1: // Timed Breath // TB events have a duration in 0.1s, based on the review of pressure waveforms. // TODO: Ideally the starting time here would be adjusted here, but PRS1ParsedEvents @@ -2547,24 +2540,6 @@ bool PRS1DataChunk::ParseEventsF3V6(void) elapsed = data[pos++]; // based on sample waveform, the hypopnea is over after this this->AddEvent(new PRS1HypopneaEvent(t - elapsed, 0)); break; - /* - case 0x08: // Flow Limitation - // TODO: We should revisit whether this is elapsed or duration once (if) - // we start calculating flow limitations ourselves. Flow limitations aren't - // as obvious as OA/CA when looking at a waveform. - elapsed = data[pos++]; - this->AddEvent(new PRS1FlowLimitationEvent(t - elapsed, 0)); - break; - case 0x09: // Vibratory Snore - // VS events are instantaneous flags with no duration, drawn on the official waveform. - // The current thinking is that these are the snores that cause a change in auto-titrating - // pressure. The snoring statistic above seems to be a total count. It's unclear whether - // the trigger for pressure change is severity or count or something else. - // no data bytes - this->AddEvent(new PRS1VibratorySnoreEvent(t, 0)); - break; - case 0x0a: // Periodic Breathing - */ case 0x07: // Periodic Breathing // PB events are reported some time after they conclude, and they do have a reported duration. duration = 2 * (data[pos] | (data[pos+1] << 8)); @@ -2593,14 +2568,10 @@ bool PRS1DataChunk::ParseEventsF3V6(void) duration = data[pos++]; this->AddEvent(new PRS1HypopneaEvent(t - duration, 0)); break; - /* - case 0x0f: - // TODO: some other pressure adjustment? - // Appears near the beginning and end of a session when Opti-Start is on, at least once in middle - //CHECK_VALUES(data[pos], 0x20, 0x28); - this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos-1, size+1)); - break; - */ + // case 0x0c? + // case 0x0d? + // case 0x0e? + // case 0x0f? default: qWarning() << "Unknown event:" << code << "in" << this->sessionid << "at" << startpos-1; this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos-1, size+1)); @@ -2615,114 +2586,6 @@ bool PRS1DataChunk::ParseEventsF3V6(void) } -#if 0 -bool PRS1DataChunk::ParseEventsF3V6(void) -{ - if (this->family != 3 || this->familyVersion != 6) { - qWarning() << "ParseEventsF3V6 called with family" << this->family << "familyVersion" << this->familyVersion; - return false; - } - - int t = 0; - int pos = 0; - int datasize = this->m_data.size(); - - unsigned char * data = (unsigned char *)this->m_data.data(); - unsigned char code; - unsigned short delta; - bool failed = false; - - unsigned char val, val2; - QString dump; - - do { - int startpos = pos; - code = data[pos++]; - delta = (data[pos+1] < 8) | data[pos]; - pos += 2; -#ifdef DEBUG_EVENTS - if (code == 0x00) { - this->AddEvent(new PRS1UnknownDataEvent(this->m_data, startpos)); - } -#endif - unsigned short epap; - - switch(code) { - case 0x01: // Who knows - val = data[pos++]; - this->AddEvent(new PRS1TimedBreathEvent(t, val)); - break; - case 0x02: - this->AddEvent(new PRS1LeakEvent(t, data[pos+3])); // TODO: F3V6, is this really unintentional leak rather than total leak? - this->AddEvent(new PRS1PatientTriggeredBreathsEvent(t, data[pos+5])); - this->AddEvent(new PRS1MinuteVentilationEvent(t, data[pos+6])); - this->AddEvent(new PRS1TidalVolumeEvent(t, data[pos+7])); - - - this->AddEvent(new PRS1EPAPEvent(t, epap=data[pos+0])); - this->AddEvent(new PRS1IPAPEvent(t, data[pos+1])); - this->AddEvent(new PRS1Test2Event(t, data[pos+4])); // Flow??? - this->AddEvent(new PRS1Test1Event(t, data[pos+8])); // TMV??? - this->AddEvent(new PRS1RespiratoryRateEvent(t, data[pos+9])); - pos += 12; - - break; - case 0x04: // ??? - val = data[pos++]; - this->AddEvent(new PRS1TimedBreathEvent(t, val)); - break; - case 0x05: // ??? - val = data[pos++]; - this->AddEvent(new PRS1ClearAirwayEvent(t, val)); - break; - case 0x06: // Obstructive Apnea - val = data[pos++]; - val2 = data[pos++]; - this->AddEvent(new PRS1ObstructiveApneaEvent(t + val2, val)); // ??? shouldn't this be t - val2? - break; - case 0x07: // PB - val = data[pos+1] << 8 | data[pos]; - pos += 2; - val2 = data[pos++]; - this->AddEvent(new PRS1PeriodicBreathingEvent(t - val2, val)); - break; - case 0x08: // RERA - val = data[pos++]; - this->AddEvent(new PRS1RERAEvent(t, val)); - break; - case 0x09: // ??? - val = data[pos+1] << 8 | data[pos]; - pos += 2; - val2 = data[pos++]; - this->AddEvent(new PRS1LargeLeakEvent(t - val, val2)); - break; - - case 0x0a: // ??? - val = data[pos++]; - this->AddEvent(new PRS1NonRespondingEvent(t, val)); - break; - case 0x0b: // Hypopnea - val = data[pos++]; - this->AddEvent(new PRS1HypopneaEvent(t, val)); - break; - - default: - this->AddEvent(new PRS1UnknownDataEvent(this->m_data, startpos)); - failed = true; - break; - }; - t += delta; - - } while ((pos < datasize) && !failed); - - if (failed) { - return false; - } - return true; -} -#endif - - bool PRS1Import::ParseF3Events() { // Required channels From 46a077cb439be4346048f39f6b2d5ed67474134d Mon Sep 17 00:00:00 2001 From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com> Date: Fri, 26 Jul 2019 22:53:15 -0400 Subject: [PATCH 10/11] Clean up remaining F3V6 import messages. --- oscar/SleepLib/loader_plugins/prs1_loader.cpp | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp index af58d1ec..bedd59f3 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp +++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp @@ -3736,7 +3736,7 @@ bool PRS1DataChunk::ParseSummaryF3V6(void) //CHECK_VALUE(data[pos+4], 0x00); // 16-bit minutes in LL CHECK_VALUE(data[pos+5], 0x00); //CHECK_VALUE(data[pos+6], 0x0A); // 16-bit VS count - CHECK_VALUE(data[pos+7], 0x00); + //CHECK_VALUE(data[pos+7], 0x00); // We've actually seen someone with more than 255 VS in a night! //CHECK_VALUE(data[pos+8], 0x01); // 16-bit H count (partial) CHECK_VALUE(data[pos+9], 0x00); //CHECK_VALUE(data[pos+0xa], 0x00); // 16-bit H count (partial) @@ -3824,11 +3824,11 @@ bool PRS1DataChunk::ParseSettingsF3V6(const unsigned char* data, int size) switch (code) { case 0: // Device Mode CHECK_VALUE(pos, 2); // always first? - // TODO: We may need additional enums for these modes, the below are just a rough guess mapping for now. + // TODO: We probably need additional enums for these modes, the below are just a rough guess mapping for now. switch (data[pos]) { - case 1: cpapmode = MODE_BILEVEL_FIXED; break; // TODO This is marked "S - Bi-Flex" on reports. - case 2: cpapmode = MODE_ASV; break; // TODO: This is marked as "S/T" on reports, is that spontaneous/timed? Pressure also seems variable! - case 4: cpapmode = MODE_AVAPS; break; // "PC - AVAPS" on reports + case 1: cpapmode = MODE_BILEVEL_FIXED; break; // "S" mode + case 2: cpapmode = MODE_ASV; break; // "S/T" mode; pressure seems variable? + case 4: cpapmode = MODE_AVAPS; break; // "PC" mode? Usually "PC - AVAPS", see setting 1 below default: UNEXPECTED_VALUE(data[pos], "known device mode"); break; @@ -3836,10 +3836,9 @@ bool PRS1DataChunk::ParseSettingsF3V6(const unsigned char* data, int size) this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_CPAP_MODE, (int) cpapmode)); break; case 1: // ??? - if (cpapmode == MODE_AVAPS) { - CHECK_VALUE(data[pos], 2); // 2 when in AVAPS - } else { - CHECK_VALUES(data[pos], 0, 1); // 1 when in S - Bi-Flex, 0 when in S/T + // How do these interact with the mode above? + if (data[pos] != 2) { // 2 = AVAPS: usually "PC - AVAPS", sometimes "S/T - AVAPS" + CHECK_VALUES(data[pos], 0, 1); // 0 = None, 1 = Bi-Flex } break; case 2: // ??? @@ -3864,7 +3863,8 @@ bool PRS1DataChunk::ParseSettingsF3V6(const unsigned char* data, int size) this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP_MAX, max_ipap, GAIN)); break; case 0x19: // Tidal Volume (AVAPS) - CHECK_VALUE(data[pos], 47); // gain 10.0 + //CHECK_VALUE(data[pos], 47); // gain 10.0 + // TODO: add a setting for this break; case 0x1e: // Backup rate (S/T and AVAPS) CHECK_VALUES(cpapmode, MODE_ASV, MODE_AVAPS); From 8ef4766efd96cc8c8dc3b1420df7a16d0f71a554 Mon Sep 17 00:00:00 2001 From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com> Date: Sat, 27 Jul 2019 15:04:20 -0400 Subject: [PATCH 11/11] Fix F3V6 imported channels to match reports. --- oscar/SleepLib/loader_plugins/prs1_loader.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp index bedd59f3..bf61cd3e 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp +++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp @@ -1371,7 +1371,7 @@ PRS1_VALUE_EVENT(PRS1SnoreEvent, EV_PRS1_SNORE); PRS1_VALUE_EVENT(PRS1VibratorySnoreEvent, EV_PRS1_VS); PRS1_VALUE_EVENT(PRS1PressurePulseEvent, EV_PRS1_PP); PRS1_VALUE_EVENT(PRS1RERAEvent, EV_PRS1_RERA); // TODO: should this really be a duration event? -PRS1_VALUE_EVENT(PRS1NonRespondingEvent, EV_PRS1_NRI); // TODO: is this a single event or an index/hour? +//PRS1_VALUE_EVENT(PRS1NonRespondingEvent, EV_PRS1_NRI); // TODO: is this a single event or an index/hour? PRS1_VALUE_EVENT(PRS1FlowRateEvent, EV_PRS1_FLOWRATE); // TODO: is this a single event or an index/hour? PRS1_VALUE_EVENT(PRS1Test1Event, EV_PRS1_TEST1); PRS1_VALUE_EVENT(PRS1Test2Event, EV_PRS1_TEST2); @@ -2300,8 +2300,8 @@ bool PRS1Import::ParseEventsF3V6() EventList *IPAP = session->AddEventList(CPAP_IPAP, EVL_Event, GAIN); EventList *EPAP = session->AddEventList(CPAP_EPAP, EVL_Event, GAIN); EventList *RE = session->AddEventList(CPAP_RERA, EVL_Event); - EventList *ZZ = session->AddEventList(CPAP_NRI, EVL_Event); EventList *SNORE = session->AddEventList(CPAP_Snore, EVL_Event); + EventList *VS = session->AddEventList(CPAP_VSnore, EVL_Event); EventList *TMV = session->AddEventList(CPAP_Test1, EVL_Event); EventList *FLOW = session->AddEventList(CPAP_Test2, EVL_Event); @@ -2368,6 +2368,9 @@ bool PRS1Import::ParseEventsF3V6() // but it needs to be shifted left 2 minutes, since it's not a starting value // but a past statistic. SNORE->AddEvent(t, e->m_value); + if (e->m_value > 0) { + VS->AddEvent(t, 0); // VSnore flag on overview + } break; case PRS1RespiratoryRateEvent::TYPE: RR->AddEvent(t, e->m_value); @@ -2384,9 +2387,6 @@ bool PRS1Import::ParseEventsF3V6() case PRS1RERAEvent::TYPE: RE->AddEvent(t, e->m_value); break; - case PRS1NonRespondingEvent::TYPE: - ZZ->AddEvent(t, e->m_value); - break; case PRS1Test1Event::TYPE: TMV->AddEvent(t, e->m_value); break;