From 15f7dacbabc8d506b326627cb7ba3fcfff07cb83 Mon Sep 17 00:00:00 2001 From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com> Date: Sat, 5 Oct 2019 20:52:34 -0400 Subject: [PATCH] Clean up ParseEventsF5V0 switch statement based on sample data. This fixes a lot of issues with event parsing. Now VS, LL, and EPAP adjustment events are now correctly parsed. And as a result the timestamps for events and overall session durations are now accurate. The handlers were essentially copied from F5V3 and F0V12 for consistency and cross-checking. --- oscar/SleepLib/loader_plugins/prs1_loader.cpp | 318 +++++------------- 1 file changed, 87 insertions(+), 231 deletions(-) diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp index ad6941aa..34ecd867 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp +++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp @@ -1620,7 +1620,7 @@ static QString DumpEvent(int t, int code, const unsigned char* data, int size) int s = t; int h = s / 3600; s -= h * 3600; int m = s / 60; s -= m * 60; - QString dump = QString("%1:%2:%3") + QString dump = QString("%1:%2:%3 ") .arg(h, 2, 10, QChar('0')) .arg(m, 2, 10, QChar('0')) .arg(s, 2, 10, QChar('0')); @@ -1630,7 +1630,7 @@ static QString DumpEvent(int t, int code, const unsigned char* data, int size) } return dump; } -#define DUMP_EVENT() qWarning() << this->sessionid << DumpEvent(t, code, data + pos, size - (pos - startpos)) << "@" << pos +#define DUMP_EVENT() qWarning() << this->sessionid << DumpEvent(t, code, data + pos, size - (pos - startpos)) + " @ " + hex(startpos-1) void PRS1DataChunk::AddEvent(PRS1ParsedEvent* const event) @@ -2128,7 +2128,7 @@ bool PRS1DataChunk::ParseEventsF5V0(void) } const unsigned char * data = (unsigned char *)this->m_data.constData(); int chunk_size = this->m_data.size(); - static const QMap event_sizes = { {0,3}, {1,2}, {3,4}, {8,3}, {9,3}, {0xa,2}, {0xb,5}, {0xc,5}, {0xd,0xc}, {0xf,5}, {0x10,5}, {0x11,2}, {0x12,6} }; + static const QMap event_sizes = { {1,2}, {3,4}, {0xa,2}, {0xb,5}, {0xc,5}, {0xd,0xc} }; if (chunk_size < 1) { // This does occasionally happen in F0V6. @@ -2140,8 +2140,7 @@ bool PRS1DataChunk::ParseEventsF5V0(void) int pos = 0, startpos; int code, size; int t = 0; - EventDataType data0, data1, data4, data5; - //int elapsed, duration, value; + int elapsed, duration; do { code = data[pos++]; @@ -2155,236 +2154,92 @@ bool PRS1DataChunk::ParseEventsF5V0(void) break; } startpos = pos; - if (code != 0 && code != 0x12) { // These two codes have no timestamp TODO: verify this applies to F5V012 - t += data[pos] /*| (data[pos+1] << 8)*/; // TODO: Is this really only 1 byte? + if (code != 0) { // Does this code really not have a timestamp? See below where we check. + t += data[pos] | (data[pos+1] << 8); pos += 2; } switch (code) { - case 0x00: // Unknown (ASV Pressure value) - // offset? - data0 = data[pos++]; - - if (!data[pos - 1]) { // WTH??? - data1 = data[pos++]; - } - - if (!data[pos - 1]) { - //data2 = data[pos++]; - pos++; - } - - break; - - case 0x01: // Unknown - this->AddEvent(new PRS1UnknownValueEvent(code, t, 0, 0.1F)); - break; - - case 0x02: // Pressure ??? - data0 = data[pos++]; - // if (!Code[2]) { - // if (!(Code[2]=session->AddEventList(cpapcode,EVL_Event,0.1))) return false; - // } - // Code[2]->AddEvent(t,data0); - break; - case 0x03: // BIPAP Pressure - qDebug() << "0x03 Observed in ASV data!!????"; - - data0 = data[pos++]; - data1 = data[pos++]; - // data0/=10.0; - // data1/=10.0; - // session->AddEvent(new Event(t,CPAP_EAP, 0, data, 1)); - // session->AddEvent(new Event(t,CPAP_IAP, 0, &data1, 1)); - break; - - case 0x04: // Timed Breath - data0 = data[pos++]; - - this->AddEvent(new PRS1TimedBreathEvent(t, data0)); - break; - - case 0x05: - //code=CPAP_Obstructive; - data0 = data[pos++]; - this->AddEvent(new PRS1ObstructiveApneaEvent(t - data0, data0)); - break; - - case 0x06: - //code=CPAP_ClearAirway; - data0 = data[pos++]; - this->AddEvent(new PRS1ClearAirwayEvent(t - data0, data0)); - break; - - case 0x07: - //code=CPAP_Hypopnea; - data0 = data[pos++]; - this->AddEvent(new PRS1HypopneaEvent(t - data0, data0)); - break; - - case 0x08: // ??? - // This was breaking parsing for F5V1 prior to using fixed lengths. - // That means it's probably very wrong. - data0 = data[pos++]; - //qDebug() << "Code 8 found at " << hex << pos - 1 << " " << tt; - - /* - if (this->familyVersion>=2) { - this->AddEvent(new PRS1HypopneaEvent(t - data0, data0)); - } else { - */ - this->AddEvent(new PRS1UnknownValueEvent(code, t - data0, data0)); - //???? - //data1=data[pos++]; // ??? - //pos++; - break; - - case 0x09: // ASV Codes - //if (this->familyVersion<2) { - //code=CPAP_FlowLimit; - data0 = data[pos++]; - - this->AddEvent(new PRS1FlowLimitationEvent(t - data0, data0)); - /* - } else { - data0 = data[pos++]; - data1 = data[pos++]; - */ - break; - - case 0x0a: - data0 = data[pos++]; - /* - if (this->familyVersion>=2) { - this->AddEvent(new PRS1FlowLimitationEvent(t - data0, data0)); - } else { - */ - this->AddEvent(new PRS1UnknownValueEvent(code, t - data0, data0)); - break; - - - case 0x0b: // Cheyne Stokes - data0 = ((unsigned char *)data)[pos + 1] << 8 | ((unsigned char *)data)[pos]; - //data0*=2; - pos += 2; - data1 = ((unsigned char *)data)[pos]; //|data[pos+1] << 8 - pos += 1; - //tt-=delta; - this->AddEvent(new PRS1PeriodicBreathingEvent(t - data1, data0)); - break; - - case 0x0c: - /* - if (this->familyVersion>=2) { - data0 = (data[pos + 1] << 8 | data[pos]); - data0 *= 2; - pos += 2; - data1 = data[pos++]; - this->AddEvent(new PRS1PeriodicBreathingEvent(t - data1, data0)); - - } else { - */ - data0 = data[pos++]; - qDebug() << "Code 12 found at " << hex << pos - 1 << " " << t - data0; - - this->AddEvent(new PRS1UnknownValueEvent(code, t - data0, data0)); - pos += 2; - break; - - case 0x0d: // All the other ASV graph stuff. - /* - if (this->familyVersion>=2) { - data0 = (data[pos + 1] << 8 | data[pos]); - data0 *= 2; - pos += 2; - data1 = data[pos++]; - //tt = t - qint64(data1) * 1000L; - } else { - */ - this->AddEvent(new PRS1IPAPAverageEvent(t, data[pos++])); // 00=IAP - data4 = data[pos++]; - this->AddEvent(new PRS1IPAPLowEvent(t, data4)); // 01=IAP Low - data5 = data[pos++]; - this->AddEvent(new PRS1IPAPHighEvent(t, data5)); // 02=IAP High - - this->AddEvent(new PRS1TotalLeakEvent(t, data[pos++])); // 03=LEAK - - this->AddEvent(new PRS1RespiratoryRateEvent(t, data[pos++])); // 04=Breaths Per Minute - this->AddEvent(new PRS1PatientTriggeredBreathsEvent(t, data[pos++])); // 05=Patient Triggered Breaths - this->AddEvent(new PRS1MinuteVentilationEvent(t, data[pos++])); // 06=Minute Ventilation - //tmp=data[pos++] * 10.0; - this->AddEvent(new PRS1TidalVolumeEvent(t, data[pos++])); // 07=Tidal Volume - this->AddEvent(new PRS1SnoreEvent(t, data[pos++])); // 08=Snore - this->AddEvent(new PRS1EPAPAverageEvent(t, data1 = data[pos++])); // 09=EPAP - /* - if (this->familyVersion >= 1) { - data0 = data[pos++]; - } - */ - break; - - case 0x0e: // Unknown - /* - // Family 5.2 has this code - if (this->familyVersion>=2) { - this->AddEvent(new PRS1IPAPAverageEvent(t, data1=data[pos+0])); // 0 - this->AddEvent(new PRS1IPAPLowEvent(t, data[pos+1])); // 1 - this->AddEvent(new PRS1IPAPHighEvent(t, data[pos+2])); // 2 - this->AddEvent(new PRS1LeakEvent(t, data[pos+3])); // 3 // F5V2, is this really unintentional leak rather than total leak? - this->AddEvent(new PRS1TidalVolumeEvent(t, data[pos+7])); // 7 - this->AddEvent(new PRS1RespiratoryRateEvent(t, data[pos+4])); // 4 - this->AddEvent(new PRS1PatientTriggeredBreathsEvent(t, data[pos+5])); // 5 - this->AddEvent(new PRS1MinuteVentilationEvent(t, data[pos+6])); //6 - this->AddEvent(new PRS1SnoreEvent(t, data[pos+8])); //?? - this->AddEvent(new PRS1EPAPAverageEvent(t, data[pos+9])); // 9 - pos+=11; - } else { - */ - qDebug() << "0x0E Observed in ASV data!!????"; - data0 = data[pos++]; // << 8) | data[pos]; - break; - case 0x0f: - qDebug() << "0x0f Observed in ASV data!!????"; - - data0 = data[pos + 1] << 8 | data[pos]; - pos += 2; - data1 = data[pos]; //|data[pos+1] << 8 - pos += 1; - //tt -= qint64(data1) * 1000L; - //session->AddEvent(new Event(tt,cpapcode, 0, data, 2)); - break; - - case 0x10: // Unknown - data0 = data[pos + 1] << 8 | data[pos]; - pos += 2; - data1 = data[pos++]; - - this->AddEvent(new PRS1LargeLeakEvent(t - data1, data0)); - -// qDebug() << "0x10 Observed in ASV data!!????"; -// data0 = data[pos++]; // << 8) | data[pos]; -// data1 = data[pos++]; -// data2 = data[pos++]; - //session->AddEvent(new Event(t,cpapcode, 0, data, 3)); - break; - case 0x11: // Not Leak Rate - qDebug() << "0x11 Observed in ASV data!!????"; - //if (!Code[24]) { - // Code[24]=new EventList(cpapcode,EVL_Event); - //} - //Code[24]->AddEvent(t,data[pos++]); - break; - - - case 0x12: // Summary - qDebug() << "0x12 Observed in ASV data!!????"; - data0 = data[pos++]; - data1 = data[pos++]; - //data2 = data[pos + 1] << 8 | data[pos]; - pos += 2; - //session->AddEvent(new Event(t,cpapcode, 0, data,3)); - break; - + case 0x00: // Unknown, only seen twice + //DUMP_EVENT(); + // So far we've only seen 0 for the first 2 bytes. Look for nonzero to see if it's actually a timestamp. If so, fix above to read it. + CHECK_VALUE(data[pos], 0); + CHECK_VALUE(data[pos+1], 0); + CHECK_VALUES(data[pos+2], 0x81, 0x83); // Only two values seen so far + break; + //case 0x01: // never seen on F5V0 + case 0x02: // Pressure adjustment + this->AddEvent(new PRS1EPAPSetEvent(t, data[pos++])); + break; + //case 0x03: // never seen on F5V0 + case 0x04: // 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]; + this->AddEvent(new PRS1TimedBreathEvent(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? + // NOTE: No additional (unknown) first byte as in F5V3. + elapsed = data[pos]; // based on sample waveform, the hypopnea is over after this + this->AddEvent(new PRS1HypopneaEvent(t - elapsed, 0)); + break; + //case 0x08: // never seen on F5V0 + case 0x09: // Flow Limitation, note this is 0x8 in F5V3 + // 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 0x0a: // Vibratory Snore, note this is 0x9 in F5V3 + // 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 0x0b: // Periodic Breathing, note this is 0xa in F5V3 + // PB events are reported some time after they conclude, and they do have a reported duration. + duration = 2 * (data[pos] | (data[pos+1] << 8)); // confirmed to double in F5V0 + elapsed = data[pos+2]; + this->AddEvent(new PRS1PeriodicBreathingEvent(t - elapsed - duration, duration)); + break; + case 0x0c: // Large Leak, note this is 0xb in F5V3 + // LL events are reported some time after they conclude, and they do have a reported duration. + duration = 2 * (data[pos] | (data[pos+1] << 8)); // confirmed to double in F5V0 + elapsed = data[pos+2]; + this->AddEvent(new PRS1LargeLeakEvent(t - elapsed - duration, duration)); + break; + case 0x0d: // Statistics + // These appear every 2 minutes, so presumably summarize the preceding period. + this->AddEvent(new PRS1IPAPAverageEvent(t, data[pos+0])); // 00=IPAP + this->AddEvent(new PRS1IPAPLowEvent(t, data[pos+1])); // 01=IAP Low + this->AddEvent(new PRS1IPAPHighEvent(t, data[pos+2])); // 02=IAP High + this->AddEvent(new PRS1TotalLeakEvent(t, data[pos+3])); // 03=Total leak (average?) + this->AddEvent(new PRS1RespiratoryRateEvent(t, data[pos+4])); // 04=Breaths Per Minute (average?) + this->AddEvent(new PRS1PatientTriggeredBreathsEvent(t, data[pos+5])); // 05=Patient Triggered Breaths (average?) + this->AddEvent(new PRS1MinuteVentilationEvent(t, data[pos+6])); // 06=Minute Ventilation (average?) + this->AddEvent(new PRS1TidalVolumeEvent(t, data[pos+7])); // 07=Tidal Volume (average?) + this->AddEvent(new PRS1SnoreEvent(t, data[pos+8])); // 08=Snore count // TODO: not a VS on official waveform, but appears in flags and contributes to overall VS index + this->AddEvent(new PRS1EPAPAverageEvent(t, data[pos+9])); // 09=EPAP average + break; default: DUMP_EVENT(); UNEXPECTED_VALUE(code, "known event code"); @@ -2415,6 +2270,7 @@ bool PRS1DataChunk::ParseEventsF5V1(void) const unsigned char * data = (unsigned char *)this->m_data.constData(); int chunk_size = this->m_data.size(); static const QMap event_sizes = { {0,4}, {1,2}, {3,4}, {8,4}, {9,3}, {0xa,2}, {0xb,5}, {0xc,5}, {0xd,0xd}, {0xf,5}, {0x10,5}, {0x11,2}, {0x12,6} }; + //static const QMap F5V0_sizes= { {0,3}, {1,2}, {3,4}, {8,3}, {9,3}, {0xa,2}, {0xb,5}, {0xc,5}, {0xd,0xc} }; if (chunk_size < 1) { // This does occasionally happen in F0V6.