Update ParseEventsF0V4 with event handlers from F0V23 and F0V6 after verifying them against sample F0V4 data.

F0V4 is unsurprisingly a midpoint between the two. PB/LL durations are
doubled as in F0V6 and, in fact, the contents of all events seem
identical between F0V4 and F0V6, apart from the new hypopnea events 0x14
and 0x15 in F0V6. We haven't encountered event 0 in F0V4 or F0V6 data
yet, but it would be an interesting comparison to F0V23.

It's now clear that PRS1PressureAverageEvent can't just be the average
overall pressure, at least not for CPAP: it's too low. This needs
further study.

Import changes:
- As with F0V23, apneas/hypopneas now have a (correct) duration of 0.
- PB/LL will now be drawn at the correct time, instead of starting when
  they end. This was already fixed previously in F0V23.
- The EPAP graph is removed from CPAP data until we understand what
  the underlying data is.
This commit is contained in:
sawinglogz 2019-10-04 18:53:10 -04:00
parent 027f82e0bb
commit 6992a2f1a9

View File

@ -1030,7 +1030,6 @@ enum PRS1ParsedEventType
EV_PRS1_IPAPLOW, EV_PRS1_IPAPLOW,
EV_PRS1_IPAPHIGH, EV_PRS1_IPAPHIGH,
EV_PRS1_EPAP_AVG, EV_PRS1_EPAP_AVG,
EV_PRS1_FLEX,
EV_PRS1_RR, EV_PRS1_RR,
EV_PRS1_PTB, EV_PRS1_PTB,
EV_PRS1_MV, EV_PRS1_MV,
@ -1415,7 +1414,6 @@ PRS1_PRESSURE_EVENT(PRS1IPAPAverageEvent, EV_PRS1_IPAP_AVG);
PRS1_PRESSURE_EVENT(PRS1IPAPHighEvent, EV_PRS1_IPAPHIGH); PRS1_PRESSURE_EVENT(PRS1IPAPHighEvent, EV_PRS1_IPAPHIGH);
PRS1_PRESSURE_EVENT(PRS1IPAPLowEvent, EV_PRS1_IPAPLOW); PRS1_PRESSURE_EVENT(PRS1IPAPLowEvent, EV_PRS1_IPAPLOW);
PRS1_PRESSURE_EVENT(PRS1EPAPAverageEvent, EV_PRS1_EPAP_AVG); PRS1_PRESSURE_EVENT(PRS1EPAPAverageEvent, EV_PRS1_EPAP_AVG);
PRS1_PRESSURE_EVENT(PRS1PressureReliefEvent, EV_PRS1_FLEX); // value is pressure applied during pressure relief, similar to EPAP
PRS1_VALUE_EVENT(PRS1RespiratoryRateEvent, EV_PRS1_RR); PRS1_VALUE_EVENT(PRS1RespiratoryRateEvent, EV_PRS1_RR);
PRS1_VALUE_EVENT(PRS1PatientTriggeredBreathsEvent, EV_PRS1_PTB); PRS1_VALUE_EVENT(PRS1PatientTriggeredBreathsEvent, EV_PRS1_PTB);
@ -1481,7 +1479,6 @@ static QString parsedEventTypeName(PRS1ParsedEventType t)
ENUMSTRING(EV_PRS1_IPAPLOW); ENUMSTRING(EV_PRS1_IPAPLOW);
ENUMSTRING(EV_PRS1_IPAPHIGH); ENUMSTRING(EV_PRS1_IPAPHIGH);
ENUMSTRING(EV_PRS1_EPAP_AVG); ENUMSTRING(EV_PRS1_EPAP_AVG);
ENUMSTRING(EV_PRS1_FLEX);
ENUMSTRING(EV_PRS1_RR); ENUMSTRING(EV_PRS1_RR);
ENUMSTRING(EV_PRS1_PTB); ENUMSTRING(EV_PRS1_PTB);
ENUMSTRING(EV_PRS1_MV); ENUMSTRING(EV_PRS1_MV);
@ -2996,6 +2993,7 @@ bool PRS1Import::ParseF0Events()
switch (e->m_type) { switch (e->m_type) {
case PRS1SnoresAtPressureEvent::TYPE: case PRS1SnoresAtPressureEvent::TYPE:
case PRS1UnknownDurationEvent::TYPE: // TODO: we should import and graph this case PRS1UnknownDurationEvent::TYPE: // TODO: we should import and graph this
case PRS1AutoPressureSetEvent::TYPE:
break; // not imported or displayed break; // not imported or displayed
case PRS1PressureSetEvent::TYPE: case PRS1PressureSetEvent::TYPE:
if (!PRESSURE) { if (!PRESSURE) {
@ -3021,12 +3019,17 @@ bool PRS1Import::ParseF0Events()
EPAP->AddEvent(t, e->m_value); EPAP->AddEvent(t, e->m_value);
PS->AddEvent(t, currentPressure - e->m_value); // Pressure Support PS->AddEvent(t, currentPressure - e->m_value); // Pressure Support
break; break;
case PRS1PressureAverageEvent::TYPE:
// TODO, we need OSCAR channels for average stats
break;
/*
case PRS1PressureReliefEvent::TYPE: case PRS1PressureReliefEvent::TYPE:
if (!EPAP) { if (!EPAP) {
if (!(EPAP = session->AddEventList(CPAP_EPAP, EVL_Event, e->m_gain))) { return false; } if (!(EPAP = session->AddEventList(CPAP_EPAP, EVL_Event, e->m_gain))) { return false; }
} }
EPAP->AddEvent(t, e->m_value); EPAP->AddEvent(t, e->m_value);
break; break;
*/
case PRS1ObstructiveApneaEvent::TYPE: case PRS1ObstructiveApneaEvent::TYPE:
OA->AddEvent(t, e->m_duration); OA->AddEvent(t, e->m_duration);
break; break;
@ -3255,14 +3258,14 @@ bool PRS1DataChunk::ParseEventsF0V23(CPAPMode /*mode*/)
break; break;
case 0x10: // Large Leak case 0x10: // Large Leak
// LL events are reported some time after they conclude, and they do have a reported duration. // LL events are reported some time after they conclude, and they do have a reported duration.
// NOTE: F0V2 does NOT double this like F0V6 does // NOTE: F0V2 does NOT double this like F0V4 and F0V6 does
if (this->familyVersion == 3) // double-check whether there's doubling on F0V3 if (this->familyVersion == 3) // double-check whether there's doubling on F0V3
DUMP_EVENT(); DUMP_EVENT();
duration = (data[pos] | (data[pos+1] << 8)); duration = (data[pos] | (data[pos+1] << 8));
elapsed = data[pos+2]; elapsed = data[pos+2];
this->AddEvent(new PRS1LargeLeakEvent(t - elapsed - duration, duration)); this->AddEvent(new PRS1LargeLeakEvent(t - elapsed - duration, duration));
break; break;
case 0x11: // Leak Rate & Snore Graphs case 0x11: // Statistics
this->AddEvent(new PRS1TotalLeakEvent(t, data[pos])); this->AddEvent(new PRS1TotalLeakEvent(t, data[pos]));
this->AddEvent(new PRS1SnoreEvent(t, data[pos+1])); this->AddEvent(new PRS1SnoreEvent(t, data[pos+1]));
break; break;
@ -3299,7 +3302,7 @@ bool PRS1DataChunk::ParseEventsF0V23(CPAPMode /*mode*/)
} }
bool PRS1DataChunk::ParseEventsF0V4(CPAPMode mode) bool PRS1DataChunk::ParseEventsF0V4(CPAPMode /*mode*/)
{ {
if (this->family != 0 || this->familyVersion != 4) { if (this->family != 0 || this->familyVersion != 4) {
qWarning() << "ParseEventsF0V4 called with family" << this->family << "familyVersion" << this->familyVersion; qWarning() << "ParseEventsF0V4 called with family" << this->family << "familyVersion" << this->familyVersion;
@ -3307,7 +3310,7 @@ bool PRS1DataChunk::ParseEventsF0V4(CPAPMode mode)
} }
const unsigned char * data = (unsigned char *)this->m_data.constData(); const unsigned char * data = (unsigned char *)this->m_data.constData();
int chunk_size = this->m_data.size(); int chunk_size = this->m_data.size();
static const QMap<int,int> event_sizes = { {0,4}, {2,4}, {3,4}, {0xb,4}, {0xd,2}, {0xe,5}, {0xf,5}, {0x10,5}, {0x11,5}, {0x12,4} }; static const QMap<int,int> event_sizes = { {0,4}, {2,4}, {3,3}, {0xb,4}, {0xd,2}, {0xe,5}, {0xf,5}, {0x10,5}, {0x11,5}, {0x12,4} };
if (chunk_size < 1) { if (chunk_size < 1) {
// This does occasionally happen in F0V6. // This does occasionally happen in F0V6.
@ -3319,9 +3322,7 @@ bool PRS1DataChunk::ParseEventsF0V4(CPAPMode mode)
int pos = 0, startpos; int pos = 0, startpos;
int code, size; int code, size;
int t = 0; int t = 0;
EventDataType data0, data1, data2; int elapsed, duration, value;
Q_UNUSED(data2)
//int elapsed, duration, value;
do { do {
code = data[pos++]; code = data[pos++];
@ -3341,144 +3342,125 @@ bool PRS1DataChunk::ParseEventsF0V4(CPAPMode mode)
} }
switch (code) { switch (code) {
case 0x00: // Unknown 00 //case 0x00: // never seen
this->AddEvent(new PRS1UnknownValueEvent(code, t, data[pos++])); // NOTE: the original code thought 0x00 had 2 data bytes, unlike the 1 in F0V23.
pos++; // TODO // We don't have any sample data with this event, so it's left out here.
case 0x01: // Pressure adjustment: note this was 0x02 in F0V23 and is 0x01 in F0V6
// See notes in ParseEventsF0V6.
this->AddEvent(new PRS1PressureSetEvent(t, data[pos]));
break; break;
case 0x02: // Pressure adjustment (bi-level): note that this was 0x03 in F0V23 and is 0x02 in F0V6
case 0x01: // Unknown // See notes above on interpolation.
this->AddEvent(new PRS1PressureSetEvent(t, data[pos++])); this->AddEvent(new PRS1IPAPSetEvent(t, data[pos+1]));
this->AddEvent(new PRS1EPAPSetEvent(t, data[pos])); // EPAP needs to be added second to calculate PS
break; break;
case 0x03: // Adjust Opti-Start pressure
case 0x02: // Pressure // On F0V4 this occasionally shows up in the middle of a session.
//if ((this->family == 0) && (this->familyVersion == 4)) { // BiPAP Pressure // In that cases, the new pressure corresponds to the next night's Opti-Start
data0 = data[pos++]; // pressure. It does not appear to have any effect on the current night's pressure,
data1 = data[pos++]; // though presumaby it could if there's a long gap between sessions.
this->AddEvent(new PRS1IPAPSetEvent(t, data1)); // See F0V6 event 3 for comparison.
this->AddEvent(new PRS1EPAPSetEvent(t, data0)); // EPAP needs to be added second to calculate PS // TODO: Does this occur in bi-level mode?
this->AddEvent(new PRS1AutoPressureSetEvent(t, data[pos]));
break; break;
case 0x03: // BIPAP Pressure
{
data0 = data[pos++];
data1 = data[pos++];
this->AddEvent(new PRS1IPAPSetEvent(t, data1));
this->AddEvent(new PRS1EPAPSetEvent(t, data0)); // EPAP needs to be added second to calculate PS
}
break;
case 0x04: // Pressure Pulse case 0x04: // Pressure Pulse
data0 = data[pos++]; duration = data[pos]; // TODO: is this a duration?
this->AddEvent(new PRS1PressurePulseEvent(t, data0)); this->AddEvent(new PRS1PressurePulseEvent(t, duration));
break; break;
case 0x05: // RERA case 0x05: // RERA
data0 = data[pos++]; elapsed = data[pos];
this->AddEvent(new PRS1RERAEvent(t - data0, data0)); this->AddEvent(new PRS1RERAEvent(t - elapsed, 0));
break; break;
case 0x06: // Obstructive Apnea
case 0x06: // Obstructive Apoanea // OA events are instantaneous flags with no duration: reviewing waveforms
data0 = data[pos++]; // shows that the time elapsed between the flag and reporting often includes
this->AddEvent(new PRS1ObstructiveApneaEvent(t - data0, data0)); // non-apnea breathing.
elapsed = data[pos];
this->AddEvent(new PRS1ObstructiveApneaEvent(t - elapsed, 0));
break; break;
case 0x07: // Clear Airway Apnea
case 0x07: // Clear Airway // CA events are instantaneous flags with no duration: reviewing waveforms
data0 = data[pos++]; // shows that the time elapsed between the flag and reporting often includes
this->AddEvent(new PRS1ClearAirwayEvent(t - data0, data0)); // non-apnea breathing.
elapsed = data[pos];
this->AddEvent(new PRS1ClearAirwayEvent(t - elapsed, 0));
break; break;
//case 0x08: // never seen
//case 0x09: // never seen
case 0x0a: // Hypopnea case 0x0a: // Hypopnea
data0 = data[pos++]; // TODO: How is this hypopnea different from events 0xb, [0x14 and 0x15 on F0V6]?
this->AddEvent(new PRS1HypopneaEvent(t - data0, data0)); elapsed = data[pos++];
this->AddEvent(new PRS1HypopneaEvent(t - elapsed, 0));
break;
case 0x0b: // Hypopnea
// TODO: How is this hypopnea different from events 0xa, 0x14 and 0x15?
// TODO: What is the first byte?
//data[pos+0]; // unknown first byte?
elapsed = data[pos+1]; // based on sample waveform, the hypopnea is over after this
this->AddEvent(new PRS1HypopneaEvent(t - elapsed, 0));
break; break;
case 0x0c: // Flow Limitation case 0x0c: // Flow Limitation
data0 = data[pos++]; // TODO: We should revisit whether this is elapsed or duration once (if)
this->AddEvent(new PRS1FlowLimitationEvent(t - data0, data0)); // 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; break;
case 0x0b: // Breathing not Detected flag???? but it doesn't line up
data0 = data[pos];
data1 = data[pos+1];
pos += 2;
// might not doublerize on older machines?
//data0 *= 2;
this->AddEvent(new PRS1UnknownValueEvent(code, t, data0)); // FIXME
break;
case 0x0d: // Vibratory Snore case 0x0d: // 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 statistics below seem 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)); this->AddEvent(new PRS1VibratorySnoreEvent(t, 0));
break; break;
case 0x0e: // ???
case 0x0e: // Unknown // 5 bytes like PB and LL, but what is it?
data0 = data[pos + 1] << 8 | data[pos]; // TODO: does duration double like it does for PB/LL?
data0 *= 2; duration = 2 * (data[pos] | (data[pos+1] << 8)); // this looks like a 16-bit value, so may be duration like PB?
pos += 2; elapsed = data[pos+2]; // this is always 60 seconds unless it's at the end, so it seems like elapsed
data1 = data[pos++]; CHECK_VALUES(elapsed, 60, 0);
this->AddEvent(new PRS1UnknownValueEvent(code, t - data1, data0)); // TODO: start time should probably match PB below this->AddEvent(new PRS1UnknownDurationEvent(t - elapsed - duration, duration));
break; break;
case 0x0f: // Periodic Breathing
case 0x0f: // Cheyne Stokes Respiration // PB events are reported some time after they conclude, and they do have a reported duration.
data0 = (data[pos + 1] << 8 | data[pos]); // NOTE: This (and F0V6) doubles the duration, unlike F0V23.
data0 *= 2; duration = 2 * (data[pos] | (data[pos+1] << 8));
pos += 2; elapsed = data[pos+2];
data1 = data[pos++]; this->AddEvent(new PRS1PeriodicBreathingEvent(t - elapsed - duration, duration));
/*
if (this->familyVersion == 2 || this->familyVersion == 3) {
// TODO: this fixed some timing errors on parsing/import, but may have broken drawing, since OSCAR
// apparently does treat a span's timestamp as an endpoint (at least when drawing, see gFlagsLine::paint)!
this->AddEvent(new PRS1PeriodicBreathingEvent(t - data1 - data0, data0)); // PB event appears data1 seconds after conclusion
} else {
*/
this->AddEvent(new PRS1PeriodicBreathingEvent(t - data1, data0)); // TODO: this should probably be the same as F0V23, but it hasn't been tested
break; break;
case 0x10: // Large Leak case 0x10: // Large Leak
data0 = data[pos + 1] << 8 | data[pos]; // LL events are reported some time after they conclude, and they do have a reported duration.
data0 *= 2; // NOTE: This (and F0V6) doubles the duration, unlike F0V23.
pos += 2; duration = 2 * (data[pos] | (data[pos+1] << 8));
data1 = data[pos++]; elapsed = data[pos+2];
this->AddEvent(new PRS1LargeLeakEvent(t - data1, data0)); // TODO: start time should probably match PB above this->AddEvent(new PRS1LargeLeakEvent(t - elapsed - duration, duration));
break; break;
case 0x11: // Statistics
this->AddEvent(new PRS1TotalLeakEvent(t, data[pos]));
this->AddEvent(new PRS1SnoreEvent(t, data[pos+1]));
case 0x11: // Leak Rate & Snore Graphs // TODO: We need to track down what this average means. The original code for F0V4 called it "EPAP / Flex Pressure".
data0 = data[pos++]; // Other parsers treated it as an EPAP event. Most other parsers now treat it as an average pressure (as a guess).
data1 = data[pos++]; // But sample data shows this value around 10.3 cmH2O for a prescribed pressure of 12.0 (C-Flex+ 3). That's too low
this->AddEvent(new PRS1TotalLeakEvent(t, data0)); // for an average pressure over time, but could easily be an average commanded EPAP, per discussions.
this->AddEvent(new PRS1SnoreEvent(t, data1)); this->AddEvent(new PRS1PressureAverageEvent(t, data[pos+2]));
// TODO: The original code also handled the above differently for different modes. It looks like it ignored the
// EPAP / Flex Pressure // value for Auto-BiLevel.
data0 = data[pos++]; break;
case 0x12: // Snore count per pressure
// Perhaps this check is not necessary, as it will theoretically add extra resolution to pressure chart // Some sessions (with lots of ramps) have multiple of these, each with a
// for bipap models and above??? // different pressure. The total snore count across all of them matches the
if (mode <= MODE_BILEVEL_FIXED) { // total found in the stats event.
this->AddEvent(new PRS1PressureReliefEvent(t, data0)); if (data[pos] != 0) {
CHECK_VALUES(data[pos], 1, 2); // 0 = CPAP pressure, 1 = bi-level EPAP, 2 = bi-level IPAP
} }
//CHECK_VALUE(data[pos+1], 0x78); // pressure
//CHECK_VALUE(data[pos+2], 1); // 16-bit snore count
//CHECK_VALUE(data[pos+3], 0);
value = (data[pos+2] | (data[pos+3] << 8));
this->AddEvent(new PRS1SnoresAtPressureEvent(t, data[pos], data[pos+1], value));
break; break;
case 0x12: // Summary
data0 = data[pos++];
data1 = data[pos++];
data2 = data[pos + 1] << 8 | data[pos];
pos += 2;
// Could end here, but I've seen data sets valid data after!!!
break;
/*
case 0x14: // DreamStation Hypopnea
data0 = data[pos++];
this->AddEvent(new PRS1HypopneaEvent(t - data0, data0));
break;
case 0x15: // DreamStation Hypopnea
data0 = data[pos++];
this->AddEvent(new PRS1HypopneaEvent(t - data0, data0));
break;
*/
default: default:
DUMP_EVENT(); DUMP_EVENT();
UNEXPECTED_VALUE(code, "known event code"); UNEXPECTED_VALUE(code, "known event code");
@ -3762,6 +3744,7 @@ bool PRS1DataChunk::ParseEventsF0V6(CPAPMode /*mode*/)
// which case the next session may use the new starting pressure. // which case the next session may use the new starting pressure.
//CHECK_VALUE(data[pos], 40); //CHECK_VALUE(data[pos], 40);
// TODO: What does this mean in bi-level mode? // TODO: What does this mean in bi-level mode?
// See F0V4 event 3 for comparison. TODO: See if there's an Opti-Start label on F0V6 reports.
this->AddEvent(new PRS1AutoPressureSetEvent(t, data[pos])); this->AddEvent(new PRS1AutoPressureSetEvent(t, data[pos]));
break; break;
case 0x04: // Pressure Pulse case 0x04: // Pressure Pulse
@ -3836,6 +3819,9 @@ bool PRS1DataChunk::ParseEventsF0V6(CPAPMode /*mode*/)
// Average pressure: this reads lower than the current CPAP set point when // Average pressure: this reads lower than the current CPAP set point when
// a flex mode is on, and exactly the current CPAP set point when off. For // a flex mode is on, and exactly the current CPAP set point when off. For
// bi-level it's presumably an average of the actual pressures. // bi-level it's presumably an average of the actual pressures.
//
// TODO: See F0V4 comments, this may be average EPAP with pressure relief.
// We should look carefully at what that means for bilevel, both fixed and auto.
this->AddEvent(new PRS1PressureAverageEvent(t, data[pos+2])); this->AddEvent(new PRS1PressureAverageEvent(t, data[pos+2]));
break; break;
case 0x12: // Snore count per pressure case 0x12: // Snore count per pressure
@ -3855,10 +3841,10 @@ bool PRS1DataChunk::ParseEventsF0V6(CPAPMode /*mode*/)
case 0x0a: // Hypopnea case 0x0a: // Hypopnea
// TODO: Why does this hypopnea have a different event code? // TODO: Why does this hypopnea have a different event code?
// fall through // fall through
case 0x14: // Hypopnea case 0x14: // Hypopnea, new to F0V6
// TODO: Why does this hypopnea have a different event code? // TODO: Why does this hypopnea have a different event code?
// fall through // fall through
case 0x15: // Hypopnea case 0x15: // Hypopnea, new to F0V6
// TODO: We should revisit whether this is elapsed or duration once (if) // TODO: We should revisit whether this is elapsed or duration once (if)
// we start calculating hypopneas ourselves. Their official definition // we start calculating hypopneas ourselves. Their official definition
// is 40% reduction in flow lasting at least 10s. // is 40% reduction in flow lasting at least 10s.