diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp index dfddf6dd..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); @@ -2277,14 +2277,19 @@ 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); 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); @@ -2292,19 +2297,26 @@ 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 *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); + // On-demand channels + EventList *PP = nullptr; - 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); @@ -2318,7 +2330,12 @@ bool PRS1Import::ParseF3EventsV3() 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); @@ -2330,14 +2347,31 @@ bool PRS1Import::ParseF3EventsV3() 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); + if (e->m_value > 0) { + VS->AddEvent(t, 0); // VSnore flag on overview + } + break; case PRS1RespiratoryRateEvent::TYPE: RR->AddEvent(t, e->m_value); break; @@ -2353,15 +2387,18 @@ bool PRS1Import::ParseF3EventsV3() 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; 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; @@ -2390,120 +2427,162 @@ bool PRS1Import::ParseF3EventsV3() } } - 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; } - - int t = 0; - int pos = 0; - int datasize = this->m_data.size(); + 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 }; + static const int ncodes = sizeof(minimum_sizes) / sizeof(int); - 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) { + if (chunk_size < 1) { + // This does occasionally happen. + qDebug() << this->sessionid << "Empty event data"; return false; } - return true; + + // 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 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 + // 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 0x03: // Pressure Pulse + duration = data[pos++]; // TODO: is this a duration? + this->AddEvent(new PRS1PressurePulseEvent(t, duration)); + break; + 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 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 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 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 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 0x0a: // Hypopnea + // TODO: Why does this hypopnea have a different event code? + // fall through + 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 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)); + break; + } + pos = startpos + size; + } while (ok && pos < chunk_size); + + this->duration = t; + + return ok; } @@ -2586,9 +2665,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; @@ -3549,52 +3631,299 @@ bool PRS1DataChunk::ParseSummaryF0V4(void) } -// TODO: This is probably only F3V6, as it uses mainblock, only present in fileVersion 3. -bool PRS1DataChunk::ParseSummaryF3(void) +// TODO: Add support for F3V3 (1061T, 1160P). This is just a stub. +bool PRS1DataChunk::ParseSummaryF3V3(void) { - CPAPMode mode = MODE_UNKNOWN; - EventDataType epap, ipap; - - 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"; - } - + this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_CPAP_MODE, (int) MODE_UNKNOWN)); + this->duration = 0; return true; } +// 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); // 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) + 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 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; // "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; + } + this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_CPAP_MODE, (int) cpapmode)); + break; + case 1: // ??? + // 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: // ??? + 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 + // TODO: add a setting for this + 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; +} + + bool PRS1DataChunk::ParseSummaryF5V012(void) { const unsigned char * data = (unsigned char *)this->m_data.constData(); @@ -3856,7 +4185,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 @@ -4303,7 +4632,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 +4786,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 +4809,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) { @@ -4721,58 +5053,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 @@ -4787,7 +5067,11 @@ bool PRS1DataChunk::ParseSummary() return this->ParseSummaryF0V23(); } case 3: - return this->ParseSummaryF3(); + if (this->familyVersion == 6) { + return this->ParseSummaryF3V6(); + } else if (this->familyVersion == 3) { + return this->ParseSummaryF3V3(); + } break; case 5: if (this->familyVersion == 1) { @@ -4879,14 +5163,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(); @@ -5131,8 +5417,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 daeeb0dd..a8e49189 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.h +++ b/oscar/SleepLib/loader_plugins/prs1_loader.h @@ -149,8 +149,11 @@ 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 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); //! \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); @@ -206,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); }; @@ -271,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();