From 7b0e732ae5504b3e4b63ba7b6147b1f8a34f9f5e Mon Sep 17 00:00:00 2001 From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com> Date: Mon, 31 May 2021 20:39:44 -0400 Subject: [PATCH] Move remaining PRS1 F0V4 parsing into separate F0 parser file. No change in functionality. Use git blame dd9a087 to follow the history before this refactoring. --- oscar/SleepLib/loader_plugins/prs1_loader.cpp | 472 ------------------ .../loader_plugins/prs1_parser_xpap.cpp | 472 ++++++++++++++++++ 2 files changed, 472 insertions(+), 472 deletions(-) diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp index af7206b1..bd786e5e 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp +++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp @@ -3338,215 +3338,6 @@ bool PRS1DataChunk::ParseEventsF0V23() } -const QVector ParsedEventsF0V4 = { - PRS1PressureSetEvent::TYPE, - PRS1IPAPSetEvent::TYPE, - PRS1EPAPSetEvent::TYPE, - PRS1AutoPressureSetEvent::TYPE, - PRS1PressurePulseEvent::TYPE, - PRS1RERAEvent::TYPE, - PRS1ObstructiveApneaEvent::TYPE, - PRS1ClearAirwayEvent::TYPE, - PRS1HypopneaEvent::TYPE, - PRS1FlowLimitationEvent::TYPE, - PRS1VibratorySnoreEvent::TYPE, - PRS1VariableBreathingEvent::TYPE, - PRS1PeriodicBreathingEvent::TYPE, - PRS1LargeLeakEvent::TYPE, - PRS1TotalLeakEvent::TYPE, - PRS1SnoreEvent::TYPE, - PRS1PressureAverageEvent::TYPE, - PRS1FlexPressureAverageEvent::TYPE, - PRS1SnoresAtPressureEvent::TYPE, -}; - -// 460P, 560P[BT], 660P, 760P are F0V4 -bool PRS1DataChunk::ParseEventsF0V4() -{ - if (this->family != 0 || this->familyVersion != 4) { - qWarning() << "ParseEventsF0V4 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 QMap 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) { - // This does occasionally happen in F0V6. - qDebug() << this->sessionid << "Empty event data"; - return false; - } - - bool ok = true; - int pos = 0, startpos; - int code, size; - int t = 0; - int elapsed, duration, value; - bool is_bilevel = false; - do { - code = data[pos++]; - - size = 3; // default size = 2 bytes time delta + 1 byte data - if (event_sizes.contains(code)) { - size = event_sizes[code]; - } - if (pos + size > chunk_size) { - qWarning() << this->sessionid << "event" << code << "@" << pos << "longer than remaining chunk"; - ok = false; - break; - } - startpos = pos; - if (code != 0x12) { // This one event has no timestamp in F0V6 - t += data[pos] | (data[pos+1] << 8); - pos += 2; - } - - switch (code) { - //case 0x00: // never seen - // NOTE: the original code thought 0x00 had 2 data bytes, unlike the 1 in F0V23. - // 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; - case 0x02: // Pressure adjustment (bi-level): note that this was 0x03 in F0V23 and is 0x02 in F0V6 - // See notes above on interpolation. - this->AddEvent(new PRS1IPAPSetEvent(t, data[pos+1])); - this->AddEvent(new PRS1EPAPSetEvent(t, data[pos])); // EPAP needs to be added second to calculate PS - is_bilevel = true; - break; - case 0x03: // Adjust Opti-Start pressure - // On F0V4 this occasionally shows up in the middle of a session. - // In that cases, the new pressure corresponds to the next night's Opti-Start - // pressure. It does not appear to have any effect on the current night's pressure, - // though presumaby it could if there's a long gap between sessions. - // See F0V6 event 3 for comparison. - // TODO: Does this occur in bi-level mode? - this->AddEvent(new PRS1AutoPressureSetEvent(t, data[pos])); - break; - case 0x04: // Pressure Pulse - duration = data[pos]; // TODO: is this a duration? - this->AddEvent(new PRS1PressurePulseEvent(t, duration)); - break; - case 0x05: // RERA - elapsed = data[pos]; - this->AddEvent(new PRS1RERAEvent(t - elapsed, 0)); - break; - case 0x06: // 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 0x07: // 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 0x08: // never seen - //case 0x09: // never seen - case 0x0a: // Hypopnea - // TODO: How is this hypopnea different from events 0xb, [0x14 and 0x15 on F0V6]? - 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; - case 0x0c: // 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 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)); - break; - case 0x0e: // Variable Breathing? - // TODO: does duration double like it does for PB/LL? - duration = 2 * (data[pos] | (data[pos+1] << 8)); - elapsed = data[pos+2]; // this is always 60 seconds unless it's at the end, so it seems like elapsed - CHECK_VALUES(elapsed, 60, 0); - this->AddEvent(new PRS1VariableBreathingEvent(t - elapsed - duration, duration)); - break; - case 0x0f: // Periodic Breathing - // PB events are reported some time after they conclude, and they do have a reported duration. - // NOTE: This (and F0V6) doubles the duration, unlike F0V23. - duration = 2 * (data[pos] | (data[pos+1] << 8)); - elapsed = data[pos+2]; - this->AddEvent(new PRS1PeriodicBreathingEvent(t - elapsed - duration, duration)); - break; - case 0x10: // Large Leak - // LL events are reported some time after they conclude, and they do have a reported duration. - // NOTE: This (and F0V6) doubles the duration, unlike F0V23. - duration = 2 * (data[pos] | (data[pos+1] << 8)); - elapsed = data[pos+2]; - this->AddEvent(new PRS1LargeLeakEvent(t - elapsed - duration, duration)); - break; - case 0x11: // Statistics - this->AddEvent(new PRS1TotalLeakEvent(t, data[pos])); - this->AddEvent(new PRS1SnoreEvent(t, data[pos+1])); - - value = data[pos+2]; - if (is_bilevel) { - // For bi-level modes, this appears to be the time-weighted average of EPAP and IPAP actually provided. - this->AddEvent(new PRS1PressureAverageEvent(t, value)); - } else { - // For single-pressure modes, this appears to be the average effective "EPAP" provided by Flex. - // - // Sample data shows this value around 10.3 cmH2O for a prescribed pressure of 12.0 (C-Flex+ 3). - // That's too low for an average pressure over time, but could easily be an average commanded EPAP. - // When flex mode is off, this is exactly the current CPAP set point. - this->AddEvent(new PRS1FlexPressureAverageEvent(t, value)); - } - this->AddEvent(new PRS1IntervalBoundaryEvent(t)); - break; - case 0x12: // Snore count per pressure - // Some sessions (with lots of ramps) have multiple of these, each with a - // different pressure. The total snore count across all of them matches the - // total found in the stats event. - 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; - default: - DUMP_EVENT(); - UNEXPECTED_VALUE(code, "known event code"); - this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos)); - ok = false; // unlike F0V6, we don't know the size of unknown events, so we can't recover - break; - } - pos = startpos + size; - } while (ok && pos < chunk_size); - - if (ok && pos != chunk_size) { - qWarning() << this->sessionid << (this->size() - pos) << "trailing event bytes"; - } - - this->duration = t; - - return ok; -} - - // TODO: This really should be in some kind of class hierarchy, once we figure out // the right one. const QVector & GetSupportedEvents(const PRS1DataChunk* chunk) @@ -4223,156 +4014,6 @@ void PRS1DataChunk::ParseHumidifierSetting60Series(unsigned char humid1, unsigne } -bool PRS1DataChunk::ParseComplianceF0V4(void) -{ - if (this->family != 0 || (this->familyVersion != 4)) { - qWarning() << "ParseComplianceF0V4 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[] = { 0x18, 7, 4, 2, 0, 0, 0, 4, 0 }; - static const int ncodes = sizeof(minimum_sizes) / sizeof(int); - // NOTE: These are fixed sizes, but are called minimum to more closely match the F0V6 parser. - - bool ok = true; - int pos = 0; - int code, size; - int tt = 0; - while (ok && pos < chunk_size) { - code = data[pos++]; - // There is no hblock prior to F0V6. - size = 0; - if (code < ncodes) { - // make sure the handlers below don't go past the end of the buffer - size = minimum_sizes[code]; - } // 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_VALUES(data[pos], 1, 3); - // F0V4 doesn't have a separate settings record like F0V6 does, the settings just follow the EquipmentOn data. - ok = ParseSettingsF0V45(data, 0x11); - CHECK_VALUE(data[pos+0x11], 0); - CHECK_VALUE(data[pos+0x12], 0); - CHECK_VALUE(data[pos+0x13], 0); - CHECK_VALUE(data[pos+0x14], 0); - CHECK_VALUE(data[pos+0x15], 0); - CHECK_VALUE(data[pos+0x16], 0); - CHECK_VALUE(data[pos+0x17], 0); - break; - case 2: // Mask On - tt += data[pos] | (data[pos+1] << 8); - this->AddEvent(new PRS1ParsedSliceEvent(tt, MaskOn)); - this->ParseHumidifierSetting60Series(data[pos+2], data[pos+3]); - break; - case 3: // Mask Off - tt += data[pos] | (data[pos+1] << 8); - this->AddEvent(new PRS1ParsedSliceEvent(tt, MaskOff)); - // Compliance doesn't have any MaskOff stats like summary does - break; - case 1: // Equipment Off - tt += data[pos] | (data[pos+1] << 8); - this->AddEvent(new PRS1ParsedSliceEvent(tt, EquipmentOff)); - // TODO: check values - CHECK_VALUES(data[pos+2], 1, 3); - //CHECK_VALUE(data[pos+2] & ~(0x40|8|4|2|1), 0); // ???, seen various bit combinations - //CHECK_VALUE(data[pos+3], 0x19); // 0x17, 0x16 - CHECK_VALUES(data[pos+4], 0, 1); - //CHECK_VALUES(data[pos+4], 0, 1); // or 2 - //CHECK_VALUE(data[pos+5], 0x35); // 0x36, 0x36 - if (data[pos+6] != 1) { - CHECK_VALUE(data[pos+6] & ~(4|2|1), 0); // On F0V23 0 seems to be related to errors, 3 seen after 90 sec large leak before turning off? - } - // pos+4 == 2, pos+6 == 10 on the session that had a time-elapsed event, maybe it shut itself off - // when approaching 24h of continuous use? - break; - /* - case 4: // Time Elapsed - // For example: mask-on 5:18:49 in a session of 23:41:20 total leaves mask-off time of 18:22:31. - // That's represented by a mask-off event 19129 seconds after the mask-on, then a time-elapsed - // event after 65535 seconds, then an equipment off event after another 616 seconds. - tt += data[pos] | (data[pos+1] << 8); - // TODO: see if this event exists in earlier versions - break; - case 5: // Clock adjustment? - CHECK_VALUE(pos, 1); // Always first - CHECK_VALUE(chunk_size, 5); // and the only record in the session. - // This looks like it's minor adjustments to the clock, but 560PBT-3917 sessions 1-2 are weird: - // session 1 starts at 2015-12-23T00:01:20 and contains this event with timestamp 2015-12-23T00:05:14. - // session 2 starts at 2015-12-23T00:01:29, which suggests the event didn't change the clock. - // - // It looks like this happens when there are discontinuities in timestamps, for example 560P-4727: - // session 58 ends at 2015-05-26T09:53:17. - // session 59 starts at 2015-05-26T09:53:15 with an event 5 timestamp of 2015-05-26T09:53:18. - // - // So the session/chunk timestamp has gone backwards. Whenever this happens, it seems to be in - // a session with an event-5 event having a timestamp that hasn't gone backwards. So maybe - // this timestamp is the old clock before adjustment? This would explain the 560PBT-3917 sessions above. - // - // This doesn't seem particularly associated with discontinuities in the waveform data: there are - // often clock adjustments without corresponding discontinuities in the waveform, and vice versa. - // It's possible internal clock inaccuracy causes both independently. - // - // TODO: why do some machines have lots of these and others none? Maybe cellular modems make daily tweaks? - if (false) { - long value = data[pos] | data[pos+1]<<8 | data[pos+2]<<16 | data[pos+3]<<24; - qDebug() << this->sessionid << "clock changing from" << ts(value * 1000L) - << "to" << ts(this->timestamp * 1000L) - << "delta:" << (this->timestamp - value); - } - break; - */ - case 6: // Cleared? - // Appears in the very first session when that session number is > 1. - // Presumably previous sessions were cleared out. - // TODO: add an internal event for this. - CHECK_VALUE(pos, 1); // Always first - CHECK_VALUE(chunk_size, 1); // and the only record in the session. - if (this->sessionid == 1) UNEXPECTED_VALUE(this->sessionid, ">1"); - break; - case 7: // Humidifier setting change (logged in events in 50 series) - tt += data[pos] | (data[pos+1] << 8); // This adds to the total duration (otherwise it won't match report) - this->ParseHumidifierSetting60Series(data[pos+2], data[pos+3]); - break; - /* - case 8: // CPAP-Check related, follows Mask On in CPAP-Check mode - tt += data[pos] | (data[pos+1] << 8); // This adds to the total duration (otherwise it won't match report) - //CHECK_VALUES(data[pos+2], 0, 79); // probably 16-bit value, sometimes matches OA + H + FL + VS + RE? - CHECK_VALUE(data[pos+3], 0); - //CHECK_VALUES(data[pos+4], 0, 10); // probably 16-bit value - CHECK_VALUE(data[pos+5], 0); - //CHECK_VALUES(data[pos+6], 0, 79); // probably 16-bit value, usually the same as +2, but not always? - CHECK_VALUE(data[pos+7], 0); - //CHECK_VALUES(data[pos+8], 0, 10); // probably 16-bit value - CHECK_VALUE(data[pos+9], 0); - //CHECK_VALUES(data[pos+0xa], 0, 4); // or 0? 44 when changed pressure mid-session? - break; - */ - default: - UNEXPECTED_VALUE(code, "known slice code"); - ok = false; // unlike F0V6, we don't know the size of unknown slices, so we can't recover - break; - } - pos += size; - } - - if (ok && pos != chunk_size) { - qWarning() << this->sessionid << (this->size() - pos) << "trailing bytes"; - } - - this->duration = tt; - - return ok; -} - - // XX XX = F3V3 Humidifier bytes // 43 15 = heated tube temp 5, humidity 2 // 43 14 = heated tube temp 4, humidity 2 @@ -5543,119 +5184,6 @@ bool PRS1DataChunk::ParseSummaryF5V012(void) } -// Flex F0V2 confirmed -// 0x00 = None -// 0x81 = C-Flex 1, lock off (AutoCPAP mode) -// 0x82 = Bi-Flex 2 (Bi-Level mode) -// 0x89 = A-Flex 1 (AutoCPAP mode) -// 0x8A = A-Flex 2, lock off (AutoCPAP mode) -// 0x8B = C-Flex+ 3, lock off (CPAP mode) -// 0x93 = Rise Time 3 (AutoBiLevel mode) - -// Flex F0V4 confirmed -// 0x00 = None -// 0x81 = Bi-Flex 1 (AutoBiLevel mode) -// 0x81 = C-Flex 1 (AutoCPAP mode) -// 0x82 = C-Flex 2 (CPAP mode) -// 0x82 = C-Flex 2 (CPAP-Check mode) -// 0x82 = C-Flex 2 (Auto-Trial mode) -// 0x83 = Bi-Flex 3 (Bi-Level mode) -// 0x89 = A-Flex 1 (AutoCPAP mode) -// 0x8A = C-Flex+ 2 (CPAP mode) -// 0x8A = C-Flex+ 2, lock off (CPAP-Check mode) -// 0x8A = A-Flex 2, lock off (Auto-Trial mode) -// 0xCB = C-Flex+ 3 (CPAP-Check mode), C-Flex+ Lock on -// -// 0x8A = A-Flex 1 (AutoCPAP mode) -// 0x8B = C-Flex+ 3 (CPAP mode) -// 0x8B = A-Flex 3 (AutoCPAP mode) - -// Flex F0V5 confirmed -// 0xE1 = Flex (AutoCPAP mode) -// 0xA1 = Flex (AutoCPAP mode) -// 0xA2 = Flex (AutoCPAP mode) - -// 8 = enabled -// 4 = lock -// 2 = Flex (only seen on Dorma series) -// 1 = rise time -// 8 = C-Flex+ / A-Flex (depending on mode) -// 3 = level - -void PRS1DataChunk::ParseFlexSettingF0V2345(quint8 flex, int cpapmode) -{ - FlexMode flexmode = FLEX_None; - bool enabled = (flex & 0x80) != 0; - bool lock = (flex & 0x40) != 0; - bool plain_flex = (flex & 0x20) != 0; // "Flex", seen on Dorma series - bool risetime = (flex & 0x10) != 0; - bool plusmode = (flex & 0x08) != 0; - int flexlevel = flex & 0x03; - if (flex & 0x04) UNEXPECTED_VALUE(flex, "known bits"); - if (this->familyVersion == 2) { - //CHECK_VALUE(lock, false); // We've seen this set on F0V2, but it doesn't appear on the reports. - } - - if (enabled) { - if (flexlevel < 1) UNEXPECTED_VALUE(flexlevel, "!= 0"); - if (risetime) { - flexmode = FLEX_RiseTime; - CHECK_VALUES(cpapmode, PRS1_MODE_BILEVEL, PRS1_MODE_AUTOBILEVEL); - CHECK_VALUE(plusmode, 0); - } else if (plusmode) { - switch (cpapmode) { - case PRS1_MODE_CPAP: - case PRS1_MODE_CPAPCHECK: - flexmode = FLEX_CFlexPlus; - break; - case PRS1_MODE_AUTOCPAP: - case PRS1_MODE_AUTOTRIAL: - flexmode = FLEX_AFlex; - break; - default: - HEX(flex); - UNEXPECTED_VALUE(cpapmode, "expected C-Flex+/A-Flex mode"); - break; - } - } else if (plain_flex) { - CHECK_VALUE(this->familyVersion, 5); // so far only seen with F0V5 - switch (cpapmode) { - case PRS1_MODE_AUTOCPAP: - flexmode = FLEX_Flex; // unknown whether this is equivalent to C-Flex, C-Flex+, or A-Flex - break; - default: - UNEXPECTED_VALUE(cpapmode, "expected mode"); - flexmode = FLEX_Flex; // probably the same for CPAP mode as well, but we haven't tested that yet - break; - } - } else { - switch (cpapmode) { - case PRS1_MODE_CPAP: - case PRS1_MODE_CPAPCHECK: - case PRS1_MODE_AUTOCPAP: - case PRS1_MODE_AUTOTRIAL: - flexmode = FLEX_CFlex; - break; - case PRS1_MODE_BILEVEL: - case PRS1_MODE_AUTOBILEVEL: - flexmode = FLEX_BiFlex; - break; - default: - HEX(flex); - UNEXPECTED_VALUE(cpapmode, "expected mode"); - break; - } - } - } - - this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_MODE, (int) flexmode)); - if (flexmode != FLEX_None) { - this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_LEVEL, flexlevel)); - } - this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_LOCK, lock)); -} - - // Flex F5V0 confirmed // 0x81 = Bi-Flex 1 (ASV mode) // 0x82 = Bi-Flex 2 (ASV mode) diff --git a/oscar/SleepLib/loader_plugins/prs1_parser_xpap.cpp b/oscar/SleepLib/loader_plugins/prs1_parser_xpap.cpp index 7cabb20c..c273310b 100644 --- a/oscar/SleepLib/loader_plugins/prs1_parser_xpap.cpp +++ b/oscar/SleepLib/loader_plugins/prs1_parser_xpap.cpp @@ -10,6 +10,269 @@ #include "prs1_parser.h" #include "prs1_loader.h" +// Flex F0V2 confirmed +// 0x00 = None +// 0x81 = C-Flex 1, lock off (AutoCPAP mode) +// 0x82 = Bi-Flex 2 (Bi-Level mode) +// 0x89 = A-Flex 1 (AutoCPAP mode) +// 0x8A = A-Flex 2, lock off (AutoCPAP mode) +// 0x8B = C-Flex+ 3, lock off (CPAP mode) +// 0x93 = Rise Time 3 (AutoBiLevel mode) + +// Flex F0V4 confirmed +// 0x00 = None +// 0x81 = Bi-Flex 1 (AutoBiLevel mode) +// 0x81 = C-Flex 1 (AutoCPAP mode) +// 0x82 = C-Flex 2 (CPAP mode) +// 0x82 = C-Flex 2 (CPAP-Check mode) +// 0x82 = C-Flex 2 (Auto-Trial mode) +// 0x83 = Bi-Flex 3 (Bi-Level mode) +// 0x89 = A-Flex 1 (AutoCPAP mode) +// 0x8A = C-Flex+ 2 (CPAP mode) +// 0x8A = C-Flex+ 2, lock off (CPAP-Check mode) +// 0x8A = A-Flex 2, lock off (Auto-Trial mode) +// 0xCB = C-Flex+ 3 (CPAP-Check mode), C-Flex+ Lock on +// +// 0x8A = A-Flex 1 (AutoCPAP mode) +// 0x8B = C-Flex+ 3 (CPAP mode) +// 0x8B = A-Flex 3 (AutoCPAP mode) + +// Flex F0V5 confirmed +// 0xE1 = Flex (AutoCPAP mode) +// 0xA1 = Flex (AutoCPAP mode) +// 0xA2 = Flex (AutoCPAP mode) + +// 8 = enabled +// 4 = lock +// 2 = Flex (only seen on Dorma series) +// 1 = rise time +// 8 = C-Flex+ / A-Flex (depending on mode) +// 3 = level + +void PRS1DataChunk::ParseFlexSettingF0V2345(quint8 flex, int cpapmode) +{ + FlexMode flexmode = FLEX_None; + bool enabled = (flex & 0x80) != 0; + bool lock = (flex & 0x40) != 0; + bool plain_flex = (flex & 0x20) != 0; // "Flex", seen on Dorma series + bool risetime = (flex & 0x10) != 0; + bool plusmode = (flex & 0x08) != 0; + int flexlevel = flex & 0x03; + if (flex & 0x04) UNEXPECTED_VALUE(flex, "known bits"); + if (this->familyVersion == 2) { + //CHECK_VALUE(lock, false); // We've seen this set on F0V2, but it doesn't appear on the reports. + } + + if (enabled) { + if (flexlevel < 1) UNEXPECTED_VALUE(flexlevel, "!= 0"); + if (risetime) { + flexmode = FLEX_RiseTime; + CHECK_VALUES(cpapmode, PRS1_MODE_BILEVEL, PRS1_MODE_AUTOBILEVEL); + CHECK_VALUE(plusmode, 0); + } else if (plusmode) { + switch (cpapmode) { + case PRS1_MODE_CPAP: + case PRS1_MODE_CPAPCHECK: + flexmode = FLEX_CFlexPlus; + break; + case PRS1_MODE_AUTOCPAP: + case PRS1_MODE_AUTOTRIAL: + flexmode = FLEX_AFlex; + break; + default: + HEX(flex); + UNEXPECTED_VALUE(cpapmode, "expected C-Flex+/A-Flex mode"); + break; + } + } else if (plain_flex) { + CHECK_VALUE(this->familyVersion, 5); // so far only seen with F0V5 + switch (cpapmode) { + case PRS1_MODE_AUTOCPAP: + flexmode = FLEX_Flex; // unknown whether this is equivalent to C-Flex, C-Flex+, or A-Flex + break; + default: + UNEXPECTED_VALUE(cpapmode, "expected mode"); + flexmode = FLEX_Flex; // probably the same for CPAP mode as well, but we haven't tested that yet + break; + } + } else { + switch (cpapmode) { + case PRS1_MODE_CPAP: + case PRS1_MODE_CPAPCHECK: + case PRS1_MODE_AUTOCPAP: + case PRS1_MODE_AUTOTRIAL: + flexmode = FLEX_CFlex; + break; + case PRS1_MODE_BILEVEL: + case PRS1_MODE_AUTOBILEVEL: + flexmode = FLEX_BiFlex; + break; + default: + HEX(flex); + UNEXPECTED_VALUE(cpapmode, "expected mode"); + break; + } + } + } + + this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_MODE, (int) flexmode)); + if (flexmode != FLEX_None) { + this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_LEVEL, flexlevel)); + } + this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_LOCK, lock)); +} + + +bool PRS1DataChunk::ParseComplianceF0V4(void) +{ + if (this->family != 0 || (this->familyVersion != 4)) { + qWarning() << "ParseComplianceF0V4 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[] = { 0x18, 7, 4, 2, 0, 0, 0, 4, 0 }; + static const int ncodes = sizeof(minimum_sizes) / sizeof(int); + // NOTE: These are fixed sizes, but are called minimum to more closely match the F0V6 parser. + + bool ok = true; + int pos = 0; + int code, size; + int tt = 0; + while (ok && pos < chunk_size) { + code = data[pos++]; + // There is no hblock prior to F0V6. + size = 0; + if (code < ncodes) { + // make sure the handlers below don't go past the end of the buffer + size = minimum_sizes[code]; + } // 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_VALUES(data[pos], 1, 3); + // F0V4 doesn't have a separate settings record like F0V6 does, the settings just follow the EquipmentOn data. + ok = ParseSettingsF0V45(data, 0x11); + CHECK_VALUE(data[pos+0x11], 0); + CHECK_VALUE(data[pos+0x12], 0); + CHECK_VALUE(data[pos+0x13], 0); + CHECK_VALUE(data[pos+0x14], 0); + CHECK_VALUE(data[pos+0x15], 0); + CHECK_VALUE(data[pos+0x16], 0); + CHECK_VALUE(data[pos+0x17], 0); + break; + case 2: // Mask On + tt += data[pos] | (data[pos+1] << 8); + this->AddEvent(new PRS1ParsedSliceEvent(tt, MaskOn)); + this->ParseHumidifierSetting60Series(data[pos+2], data[pos+3]); + break; + case 3: // Mask Off + tt += data[pos] | (data[pos+1] << 8); + this->AddEvent(new PRS1ParsedSliceEvent(tt, MaskOff)); + // Compliance doesn't have any MaskOff stats like summary does + break; + case 1: // Equipment Off + tt += data[pos] | (data[pos+1] << 8); + this->AddEvent(new PRS1ParsedSliceEvent(tt, EquipmentOff)); + // TODO: check values + CHECK_VALUES(data[pos+2], 1, 3); + //CHECK_VALUE(data[pos+2] & ~(0x40|8|4|2|1), 0); // ???, seen various bit combinations + //CHECK_VALUE(data[pos+3], 0x19); // 0x17, 0x16 + CHECK_VALUES(data[pos+4], 0, 1); + //CHECK_VALUES(data[pos+4], 0, 1); // or 2 + //CHECK_VALUE(data[pos+5], 0x35); // 0x36, 0x36 + if (data[pos+6] != 1) { + CHECK_VALUE(data[pos+6] & ~(4|2|1), 0); // On F0V23 0 seems to be related to errors, 3 seen after 90 sec large leak before turning off? + } + // pos+4 == 2, pos+6 == 10 on the session that had a time-elapsed event, maybe it shut itself off + // when approaching 24h of continuous use? + break; + /* + case 4: // Time Elapsed + // For example: mask-on 5:18:49 in a session of 23:41:20 total leaves mask-off time of 18:22:31. + // That's represented by a mask-off event 19129 seconds after the mask-on, then a time-elapsed + // event after 65535 seconds, then an equipment off event after another 616 seconds. + tt += data[pos] | (data[pos+1] << 8); + // TODO: see if this event exists in earlier versions + break; + case 5: // Clock adjustment? + CHECK_VALUE(pos, 1); // Always first + CHECK_VALUE(chunk_size, 5); // and the only record in the session. + // This looks like it's minor adjustments to the clock, but 560PBT-3917 sessions 1-2 are weird: + // session 1 starts at 2015-12-23T00:01:20 and contains this event with timestamp 2015-12-23T00:05:14. + // session 2 starts at 2015-12-23T00:01:29, which suggests the event didn't change the clock. + // + // It looks like this happens when there are discontinuities in timestamps, for example 560P-4727: + // session 58 ends at 2015-05-26T09:53:17. + // session 59 starts at 2015-05-26T09:53:15 with an event 5 timestamp of 2015-05-26T09:53:18. + // + // So the session/chunk timestamp has gone backwards. Whenever this happens, it seems to be in + // a session with an event-5 event having a timestamp that hasn't gone backwards. So maybe + // this timestamp is the old clock before adjustment? This would explain the 560PBT-3917 sessions above. + // + // This doesn't seem particularly associated with discontinuities in the waveform data: there are + // often clock adjustments without corresponding discontinuities in the waveform, and vice versa. + // It's possible internal clock inaccuracy causes both independently. + // + // TODO: why do some machines have lots of these and others none? Maybe cellular modems make daily tweaks? + if (false) { + long value = data[pos] | data[pos+1]<<8 | data[pos+2]<<16 | data[pos+3]<<24; + qDebug() << this->sessionid << "clock changing from" << ts(value * 1000L) + << "to" << ts(this->timestamp * 1000L) + << "delta:" << (this->timestamp - value); + } + break; + */ + case 6: // Cleared? + // Appears in the very first session when that session number is > 1. + // Presumably previous sessions were cleared out. + // TODO: add an internal event for this. + CHECK_VALUE(pos, 1); // Always first + CHECK_VALUE(chunk_size, 1); // and the only record in the session. + if (this->sessionid == 1) UNEXPECTED_VALUE(this->sessionid, ">1"); + break; + case 7: // Humidifier setting change (logged in events in 50 series) + tt += data[pos] | (data[pos+1] << 8); // This adds to the total duration (otherwise it won't match report) + this->ParseHumidifierSetting60Series(data[pos+2], data[pos+3]); + break; + /* + case 8: // CPAP-Check related, follows Mask On in CPAP-Check mode + tt += data[pos] | (data[pos+1] << 8); // This adds to the total duration (otherwise it won't match report) + //CHECK_VALUES(data[pos+2], 0, 79); // probably 16-bit value, sometimes matches OA + H + FL + VS + RE? + CHECK_VALUE(data[pos+3], 0); + //CHECK_VALUES(data[pos+4], 0, 10); // probably 16-bit value + CHECK_VALUE(data[pos+5], 0); + //CHECK_VALUES(data[pos+6], 0, 79); // probably 16-bit value, usually the same as +2, but not always? + CHECK_VALUE(data[pos+7], 0); + //CHECK_VALUES(data[pos+8], 0, 10); // probably 16-bit value + CHECK_VALUE(data[pos+9], 0); + //CHECK_VALUES(data[pos+0xa], 0, 4); // or 0? 44 when changed pressure mid-session? + break; + */ + default: + UNEXPECTED_VALUE(code, "known slice code"); + ok = false; // unlike F0V6, we don't know the size of unknown slices, so we can't recover + break; + } + pos += size; + } + + if (ok && pos != chunk_size) { + qWarning() << this->sessionid << (this->size() - pos) << "trailing bytes"; + } + + this->duration = tt; + + return ok; +} + + bool PRS1DataChunk::ParseSummaryF0V4(void) { if (this->family != 0 || (this->familyVersion != 4)) { @@ -320,6 +583,215 @@ bool PRS1DataChunk::ParseSettingsF0V45(const unsigned char* data, int size) } +const QVector ParsedEventsF0V4 = { + PRS1PressureSetEvent::TYPE, + PRS1IPAPSetEvent::TYPE, + PRS1EPAPSetEvent::TYPE, + PRS1AutoPressureSetEvent::TYPE, + PRS1PressurePulseEvent::TYPE, + PRS1RERAEvent::TYPE, + PRS1ObstructiveApneaEvent::TYPE, + PRS1ClearAirwayEvent::TYPE, + PRS1HypopneaEvent::TYPE, + PRS1FlowLimitationEvent::TYPE, + PRS1VibratorySnoreEvent::TYPE, + PRS1VariableBreathingEvent::TYPE, + PRS1PeriodicBreathingEvent::TYPE, + PRS1LargeLeakEvent::TYPE, + PRS1TotalLeakEvent::TYPE, + PRS1SnoreEvent::TYPE, + PRS1PressureAverageEvent::TYPE, + PRS1FlexPressureAverageEvent::TYPE, + PRS1SnoresAtPressureEvent::TYPE, +}; + +// 460P, 560P[BT], 660P, 760P are F0V4 +bool PRS1DataChunk::ParseEventsF0V4() +{ + if (this->family != 0 || this->familyVersion != 4) { + qWarning() << "ParseEventsF0V4 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 QMap 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) { + // This does occasionally happen in F0V6. + qDebug() << this->sessionid << "Empty event data"; + return false; + } + + bool ok = true; + int pos = 0, startpos; + int code, size; + int t = 0; + int elapsed, duration, value; + bool is_bilevel = false; + do { + code = data[pos++]; + + size = 3; // default size = 2 bytes time delta + 1 byte data + if (event_sizes.contains(code)) { + size = event_sizes[code]; + } + if (pos + size > chunk_size) { + qWarning() << this->sessionid << "event" << code << "@" << pos << "longer than remaining chunk"; + ok = false; + break; + } + startpos = pos; + if (code != 0x12) { // This one event has no timestamp in F0V6 + t += data[pos] | (data[pos+1] << 8); + pos += 2; + } + + switch (code) { + //case 0x00: // never seen + // NOTE: the original code thought 0x00 had 2 data bytes, unlike the 1 in F0V23. + // 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; + case 0x02: // Pressure adjustment (bi-level): note that this was 0x03 in F0V23 and is 0x02 in F0V6 + // See notes above on interpolation. + this->AddEvent(new PRS1IPAPSetEvent(t, data[pos+1])); + this->AddEvent(new PRS1EPAPSetEvent(t, data[pos])); // EPAP needs to be added second to calculate PS + is_bilevel = true; + break; + case 0x03: // Adjust Opti-Start pressure + // On F0V4 this occasionally shows up in the middle of a session. + // In that cases, the new pressure corresponds to the next night's Opti-Start + // pressure. It does not appear to have any effect on the current night's pressure, + // though presumaby it could if there's a long gap between sessions. + // See F0V6 event 3 for comparison. + // TODO: Does this occur in bi-level mode? + this->AddEvent(new PRS1AutoPressureSetEvent(t, data[pos])); + break; + case 0x04: // Pressure Pulse + duration = data[pos]; // TODO: is this a duration? + this->AddEvent(new PRS1PressurePulseEvent(t, duration)); + break; + case 0x05: // RERA + elapsed = data[pos]; + this->AddEvent(new PRS1RERAEvent(t - elapsed, 0)); + break; + case 0x06: // 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 0x07: // 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 0x08: // never seen + //case 0x09: // never seen + case 0x0a: // Hypopnea + // TODO: How is this hypopnea different from events 0xb, [0x14 and 0x15 on F0V6]? + 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; + case 0x0c: // 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 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)); + break; + case 0x0e: // Variable Breathing? + // TODO: does duration double like it does for PB/LL? + duration = 2 * (data[pos] | (data[pos+1] << 8)); + elapsed = data[pos+2]; // this is always 60 seconds unless it's at the end, so it seems like elapsed + CHECK_VALUES(elapsed, 60, 0); + this->AddEvent(new PRS1VariableBreathingEvent(t - elapsed - duration, duration)); + break; + case 0x0f: // Periodic Breathing + // PB events are reported some time after they conclude, and they do have a reported duration. + // NOTE: This (and F0V6) doubles the duration, unlike F0V23. + duration = 2 * (data[pos] | (data[pos+1] << 8)); + elapsed = data[pos+2]; + this->AddEvent(new PRS1PeriodicBreathingEvent(t - elapsed - duration, duration)); + break; + case 0x10: // Large Leak + // LL events are reported some time after they conclude, and they do have a reported duration. + // NOTE: This (and F0V6) doubles the duration, unlike F0V23. + duration = 2 * (data[pos] | (data[pos+1] << 8)); + elapsed = data[pos+2]; + this->AddEvent(new PRS1LargeLeakEvent(t - elapsed - duration, duration)); + break; + case 0x11: // Statistics + this->AddEvent(new PRS1TotalLeakEvent(t, data[pos])); + this->AddEvent(new PRS1SnoreEvent(t, data[pos+1])); + + value = data[pos+2]; + if (is_bilevel) { + // For bi-level modes, this appears to be the time-weighted average of EPAP and IPAP actually provided. + this->AddEvent(new PRS1PressureAverageEvent(t, value)); + } else { + // For single-pressure modes, this appears to be the average effective "EPAP" provided by Flex. + // + // Sample data shows this value around 10.3 cmH2O for a prescribed pressure of 12.0 (C-Flex+ 3). + // That's too low for an average pressure over time, but could easily be an average commanded EPAP. + // When flex mode is off, this is exactly the current CPAP set point. + this->AddEvent(new PRS1FlexPressureAverageEvent(t, value)); + } + this->AddEvent(new PRS1IntervalBoundaryEvent(t)); + break; + case 0x12: // Snore count per pressure + // Some sessions (with lots of ramps) have multiple of these, each with a + // different pressure. The total snore count across all of them matches the + // total found in the stats event. + 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; + default: + DUMP_EVENT(); + UNEXPECTED_VALUE(code, "known event code"); + this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos)); + ok = false; // unlike F0V6, we don't know the size of unknown events, so we can't recover + break; + } + pos = startpos + size; + } while (ok && pos < chunk_size); + + if (ok && pos != chunk_size) { + qWarning() << this->sessionid << (this->size() - pos) << "trailing event bytes"; + } + + this->duration = t; + + return ok; +} + + // Based on ParseComplianceF0V4, but this has shorter settings and stats following equipment off. bool PRS1DataChunk::ParseComplianceF0V5(void) {