From e0d4872f6b1040aa82eeed7f5f651cd0c094e2a3 Mon Sep 17 00:00:00 2001
From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com>
Date: Sun, 4 Aug 2019 19:36:40 -0500
Subject: [PATCH 01/20] Create placeholder event parser for PRS1 F0V6, separate
from other F0 machines.
---
oscar/SleepLib/loader_plugins/prs1_loader.cpp | 170 +++++++++++++++++-
oscar/SleepLib/loader_plugins/prs1_loader.h | 5 +-
2 files changed, 172 insertions(+), 3 deletions(-)
diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp
index bf61cd3e..1f8933e4 100644
--- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp
+++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp
@@ -2984,7 +2984,7 @@ bool PRS1Import::ParseF0Events()
}
-bool PRS1DataChunk::ParseEventsF0(CPAPMode mode)
+bool PRS1DataChunk::ParseEventsF0V234(CPAPMode mode)
{
unsigned char code=0;
@@ -3212,6 +3212,168 @@ bool PRS1DataChunk::ParseEventsF0(CPAPMode mode)
}
+// DreamStation family 0 CPAP/APAP machines (400X-700X)
+// Originally derived from F5V3 parsing + (incomplete/broken) F0V234 parsing + sample data
+bool PRS1DataChunk::ParseEventsF0V6(CPAPMode /*mode*/)
+{
+ if (this->family != 0 || this->familyVersion != 6) {
+ qWarning() << "ParseEventsF0V6 called with family" << this->family << "familyVersion" << this->familyVersion;
+ return false;
+ }
+ const unsigned char * data = (unsigned char *)this->m_data.constData();
+ int chunk_size = this->m_data.size();
+ static const int minimum_sizes[] = { 2, 3, 4, 3, 3, 3, 3, 3, 3, 2, 3, 4, 3, 2, 5, 5, 5, 5, 4, 3, 3, 3 };
+ static const int ncodes = sizeof(minimum_sizes) / sizeof(int);
+
+ if (chunk_size < 1) {
+ // This does occasionally happen.
+ qDebug() << this->sessionid << "Empty event data";
+ return false;
+ }
+
+ bool ok = true;
+ int pos = 0, startpos;
+ int code, size;
+ int t = 0;
+ //int elapsed, duration;
+ do {
+ code = data[pos++];
+ if (!this->hblock.contains(code)) {
+ qWarning() << this->sessionid << "missing hblock entry for event" << code;
+ ok = false;
+ break;
+ }
+ size = this->hblock[code];
+ if (code < ncodes) {
+ // make sure the handlers below don't go past the end of the buffer
+ if (size < minimum_sizes[code]) {
+ qWarning() << this->sessionid << "event" << code << "too small" << size << "<" << minimum_sizes[code];
+ ok = false;
+ break;
+ }
+ } // else if it's past ncodes, we'll log its information below (rather than handle it)
+ if (pos + size > chunk_size) {
+ qWarning() << this->sessionid << "event" << code << "@" << pos << "longer than remaining chunk";
+ ok = false;
+ break;
+ }
+ startpos = pos;
+ t += data[pos] | (data[pos+1] << 8);
+ pos += 2;
+
+ switch (code) {
+ /*
+ case 1: // Pressure adjustment
+ // TODO: Have OSCAR treat EPAP adjustment events differently than (average?) stats below.
+ //this->AddEvent(new PRS1EPAPEvent(t, data[pos++], GAIN));
+ this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos-1, size+1));
+ break;
+ case 2: // Timed Breath
+ // TB events have a duration in 0.1s, based on the review of pressure waveforms.
+ // TODO: Ideally the starting time here would be adjusted here, but PRS1ParsedEvents
+ // currently assume integer seconds rather than ms, so that's done at import.
+ duration = data[pos++];
+ this->AddEvent(new PRS1TimedBreathEvent(t, duration));
+ break;
+ case 3: // Statistics
+ // These appear every 2 minutes, so presumably summarize the preceding period.
+ this->AddEvent(new PRS1IPAPEvent(t, data[pos++], GAIN)); // 00=IPAP (average?)
+ this->AddEvent(new PRS1IPAPLowEvent(t, data[pos++], GAIN)); // 01=IAP Low
+ this->AddEvent(new PRS1IPAPHighEvent(t, data[pos++], GAIN)); // 02=IAP High
+ 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 PRS1SnoreEvent(t, data[pos++])); // 08=Snore count // TODO: not a VS on official waveform, but appears in flags and contributes to overall VS index
+ this->AddEvent(new PRS1EPAPEvent(t, data[pos++], GAIN)); // 09=EPAP (average? see event 1 above)
+ this->AddEvent(new PRS1LeakEvent(t, data[pos++])); // 0A=Leak (average?)
+ break;
+ case 0x04: // Pressure Pulse
+ duration = data[pos++]; // TODO: is this a duration?
+ this->AddEvent(new PRS1PressurePulseEvent(t, duration));
+ break;
+ case 0x05: // Obstructive Apnea
+ // OA events are instantaneous flags with no duration: reviewing waveforms
+ // shows that the time elapsed between the flag and reporting often includes
+ // non-apnea breathing.
+ elapsed = data[pos++];
+ this->AddEvent(new PRS1ObstructiveApneaEvent(t - elapsed, 0));
+ break;
+ case 0x06: // Clear Airway Apnea
+ // CA events are instantaneous flags with no duration: reviewing waveforms
+ // shows that the time elapsed between the flag and reporting often includes
+ // non-apnea breathing.
+ elapsed = data[pos++];
+ this->AddEvent(new PRS1ClearAirwayEvent(t - elapsed, 0));
+ break;
+ case 0x07: // Hypopnea
+ // TODO: How is this hypopnea different from events 0xd and 0xe?
+ // TODO: What is the first byte?
+ pos++; // unknown first byte?
+ elapsed = data[pos++]; // based on sample waveform, the hypopnea is over after this
+ this->AddEvent(new PRS1HypopneaEvent(t - elapsed, 0));
+ break;
+ case 0x08: // Flow Limitation
+ // TODO: We should revisit whether this is elapsed or duration once (if)
+ // we start calculating flow limitations ourselves. Flow limitations aren't
+ // as obvious as OA/CA when looking at a waveform.
+ elapsed = data[pos++];
+ this->AddEvent(new PRS1FlowLimitationEvent(t - elapsed, 0));
+ break;
+ case 0x09: // Vibratory Snore
+ // VS events are instantaneous flags with no duration, drawn on the official waveform.
+ // The current thinking is that these are the snores that cause a change in auto-titrating
+ // pressure. The snoring statistic above seems to be a total count. It's unclear whether
+ // the trigger for pressure change is severity or count or something else.
+ // no data bytes
+ this->AddEvent(new PRS1VibratorySnoreEvent(t, 0));
+ break;
+ case 0x0a: // Periodic Breathing
+ // PB events are reported some time after they conclude, and they do have a reported duration.
+ duration = 2 * (data[pos] | (data[pos+1] << 8));
+ pos += 2;
+ elapsed = data[pos++];
+ this->AddEvent(new PRS1PeriodicBreathingEvent(t - elapsed - duration, duration));
+ break;
+ case 0x0b: // Large Leak
+ // LL events are reported some time after they conclude, and they do have a reported duration.
+ duration = 2 * (data[pos] | (data[pos+1] << 8));
+ pos += 2;
+ elapsed = data[pos++];
+ this->AddEvent(new PRS1LargeLeakEvent(t - elapsed - duration, duration));
+ break;
+ case 0x0d: // Hypopnea
+ // TODO: Why does this hypopnea have a different event code?
+ // fall through
+ case 0x0e: // Hypopnea
+ // TODO: We should revisit whether this is elapsed or duration once (if)
+ // we start calculating hypopneas ourselves. Their official definition
+ // is 40% reduction in flow lasting at least 10s.
+ duration = data[pos++];
+ this->AddEvent(new PRS1HypopneaEvent(t - duration, 0));
+ break;
+ case 0x0f:
+ // TODO: some other pressure adjustment?
+ // Appears near the beginning and end of a session when Opti-Start is on, at least once in middle
+ //CHECK_VALUES(data[pos], 0x20, 0x28);
+ this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos-1, size+1));
+ break;
+ */
+ default:
+ qWarning() << "Unknown event:" << code << "in" << this->sessionid << "at" << startpos-1;
+ this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos-1, size+1));
+ break;
+ }
+ pos = startpos + size;
+ } while (ok && pos < chunk_size);
+
+ this->duration = t;
+
+ return ok;
+}
+
+
bool PRS1Import::ImportCompliance()
{
bool ok;
@@ -5130,7 +5292,11 @@ bool PRS1DataChunk::ParseEvents(CPAPMode mode)
bool ok = false;
switch (this->family) {
case 0:
- ok = this->ParseEventsF0(mode);
+ if (this->familyVersion == 6) {
+ ok = this->ParseEventsF0V6(mode);
+ } else if (this->familyVersion >= 2 && this->familyVersion <= 4) {
+ ok = this->ParseEventsF0V234(mode);
+ }
break;
case 3:
if (this->familyVersion == 6) {
diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.h b/oscar/SleepLib/loader_plugins/prs1_loader.h
index a8e49189..a5d9a8eb 100644
--- a/oscar/SleepLib/loader_plugins/prs1_loader.h
+++ b/oscar/SleepLib/loader_plugins/prs1_loader.h
@@ -174,8 +174,11 @@ public:
bool ParseEvents(CPAPMode mode);
//! \brief Parse a single data chunk from a .002 file containing event data for a family 0 CPAP/APAP machine
- bool ParseEventsF0(CPAPMode mode);
+ bool ParseEventsF0V234(CPAPMode mode);
+ //! \brief Parse a single data chunk from a .002 file containing event data for a DreamStation family 0 CPAP/APAP machine
+ bool ParseEventsF0V6(CPAPMode mode);
+
//! \brief Parse a single data chunk from a .002 file containing event data for a family 3 ventilator family version 3 machine
bool ParseEventsF3V3(void);
From 4e863ba48486c3071dd537bc9d8b86ac4456f202 Mon Sep 17 00:00:00 2001
From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com>
Date: Sun, 4 Aug 2019 21:09:42 -0500
Subject: [PATCH 02/20] Add first confirmed F0V6 events from sample data.
---
oscar/SleepLib/loader_plugins/prs1_loader.cpp | 29 ++++++++++++++-----
1 file changed, 22 insertions(+), 7 deletions(-)
diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp
index 1f8933e4..8373876b 100644
--- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp
+++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp
@@ -3235,7 +3235,7 @@ bool PRS1DataChunk::ParseEventsF0V6(CPAPMode /*mode*/)
int pos = 0, startpos;
int code, size;
int t = 0;
- //int elapsed, duration;
+ int elapsed/*, duration*/;
do {
code = data[pos++];
if (!this->hblock.contains(code)) {
@@ -3275,6 +3275,11 @@ bool PRS1DataChunk::ParseEventsF0V6(CPAPMode /*mode*/)
duration = data[pos++];
this->AddEvent(new PRS1TimedBreathEvent(t, duration));
break;
+ */
+ case 0x11: // Statistics
+ this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos-1, size+1));
+ break;
+ /*
case 3: // Statistics
// These appear every 2 minutes, so presumably summarize the preceding period.
this->AddEvent(new PRS1IPAPEvent(t, data[pos++], GAIN)); // 00=IPAP (average?)
@@ -3289,31 +3294,37 @@ bool PRS1DataChunk::ParseEventsF0V6(CPAPMode /*mode*/)
this->AddEvent(new PRS1EPAPEvent(t, data[pos++], GAIN)); // 09=EPAP (average? see event 1 above)
this->AddEvent(new PRS1LeakEvent(t, data[pos++])); // 0A=Leak (average?)
break;
+ */
case 0x04: // Pressure Pulse
duration = data[pos++]; // TODO: is this a duration?
this->AddEvent(new PRS1PressurePulseEvent(t, duration));
break;
- case 0x05: // Obstructive Apnea
+ case 0x05: // RERA
+ elapsed = data[pos++]; // based on sample waveform, the RERA is over after this
+ 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 0x06: // Clear Airway Apnea
+ 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 0x07: // Hypopnea
+ case 0x0b: // Hypopnea
// TODO: How is this hypopnea different from events 0xd and 0xe?
// TODO: What is the first byte?
pos++; // unknown first byte?
elapsed = data[pos++]; // based on sample waveform, the hypopnea is over after this
this->AddEvent(new PRS1HypopneaEvent(t - elapsed, 0));
break;
+ /*
case 0x08: // Flow Limitation
// TODO: We should revisit whether this is elapsed or duration once (if)
// we start calculating flow limitations ourselves. Flow limitations aren't
@@ -3329,30 +3340,34 @@ bool PRS1DataChunk::ParseEventsF0V6(CPAPMode /*mode*/)
// no data bytes
this->AddEvent(new PRS1VibratorySnoreEvent(t, 0));
break;
- case 0x0a: // Periodic Breathing
+ */
+ case 0x0f: // Periodic Breathing
// PB events are reported some time after they conclude, and they do have a reported duration.
duration = 2 * (data[pos] | (data[pos+1] << 8));
pos += 2;
elapsed = data[pos++];
this->AddEvent(new PRS1PeriodicBreathingEvent(t - elapsed - duration, duration));
break;
- case 0x0b: // Large Leak
+ case 0x10: // Large Leak
// LL events are reported some time after they conclude, and they do have a reported duration.
duration = 2 * (data[pos] | (data[pos+1] << 8));
pos += 2;
elapsed = data[pos++];
this->AddEvent(new PRS1LargeLeakEvent(t - elapsed - duration, duration));
break;
+ /*
case 0x0d: // Hypopnea
// TODO: Why does this hypopnea have a different event code?
// fall through
- case 0x0e: // Hypopnea
+ */
+ case 0x14: // Hypopnea
// TODO: We should revisit whether this is elapsed or duration once (if)
// we start calculating hypopneas ourselves. Their official definition
// is 40% reduction in flow lasting at least 10s.
duration = data[pos++];
this->AddEvent(new PRS1HypopneaEvent(t - duration, 0));
break;
+ /*
case 0x0f:
// TODO: some other pressure adjustment?
// Appears near the beginning and end of a session when Opti-Start is on, at least once in middle
From 84f1389d51f6e113846b55dc513603c7b4034edb Mon Sep 17 00:00:00 2001
From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com>
Date: Mon, 5 Aug 2019 15:22:22 -0500
Subject: [PATCH 03/20] Support more F0V6 events based on all sessions from a
single 400X machine.
---
oscar/SleepLib/loader_plugins/prs1_loader.cpp | 38 ++++++++++++++-----
1 file changed, 28 insertions(+), 10 deletions(-)
diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp
index 8373876b..7432525d 100644
--- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp
+++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp
@@ -3258,16 +3258,18 @@ bool PRS1DataChunk::ParseEventsF0V6(CPAPMode /*mode*/)
break;
}
startpos = pos;
- t += data[pos] | (data[pos+1] << 8);
- pos += 2;
+ if (code != 0x12) { // TODO: Some events have no timestamp?
+ t += data[pos] | (data[pos+1] << 8);
+ pos += 2;
+ }
switch (code) {
- /*
case 1: // Pressure adjustment
+ // Matches pressure setting, both initial and when ramp button pressed.
// TODO: Have OSCAR treat EPAP adjustment events differently than (average?) stats below.
- //this->AddEvent(new PRS1EPAPEvent(t, data[pos++], GAIN));
- this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos-1, size+1));
+ //this->AddEvent(new PRS1EPAPEvent(t, data[pos++]));
break;
+ /*
case 2: // 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
@@ -3277,7 +3279,11 @@ bool PRS1DataChunk::ParseEventsF0V6(CPAPMode /*mode*/)
break;
*/
case 0x11: // Statistics
- this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos-1, size+1));
+ this->AddEvent(new PRS1TotalLeakEvent(t, data[pos++]));
+ this->AddEvent(new PRS1SnoreEvent(t, data[pos++]));
+ // pressure? usually lower, but on a brief session was exactly set pressure
+ // also lower on session where pressure was at ramp most of the time
+ //this->AddEvent(new PRS1EPAPEvent(t, data[pos++]));
break;
/*
case 3: // Statistics
@@ -3341,6 +3347,14 @@ bool PRS1DataChunk::ParseEventsF0V6(CPAPMode /*mode*/)
this->AddEvent(new PRS1VibratorySnoreEvent(t, 0));
break;
*/
+ case 0x0e: // ???
+ // 5 bytes like PB and LL, but what is it?
+ 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++]; // this is always 60 seconds unless it's at the end, so it seems like elapsed
+ CHECK_VALUES(elapsed, 60, 0);
+ //this->AddEvent(new PRS1PeriodicBreathingEvent(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.
duration = 2 * (data[pos] | (data[pos+1] << 8));
@@ -3355,12 +3369,10 @@ bool PRS1DataChunk::ParseEventsF0V6(CPAPMode /*mode*/)
elapsed = data[pos++];
this->AddEvent(new PRS1LargeLeakEvent(t - elapsed - duration, duration));
break;
- /*
- case 0x0d: // Hypopnea
+ case 0x14: // Hypopnea
// TODO: Why does this hypopnea have a different event code?
// fall through
- */
- case 0x14: // Hypopnea
+ case 0x15: // 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.
@@ -3375,6 +3387,12 @@ bool PRS1DataChunk::ParseEventsF0V6(CPAPMode /*mode*/)
this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos-1, size+1));
break;
*/
+ case 0x12: // Summary
+ CHECK_VALUE(data[pos], 0);
+ CHECK_VALUE(data[pos+1], 0x78); // pressure?
+ //CHECK_VALUE(data[pos+2], 1); // Total snore count
+ CHECK_VALUE(data[pos+3], 0);
+ break;
default:
qWarning() << "Unknown event:" << code << "in" << this->sessionid << "at" << startpos-1;
this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos-1, size+1));
From 64309f366ad7bfc2f5eb2d9877fe0deea6ce8cac Mon Sep 17 00:00:00 2001
From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com>
Date: Mon, 5 Aug 2019 20:46:05 -0500
Subject: [PATCH 04/20] Add PRS1 model 562P to list of tested machines.
---
oscar/SleepLib/loader_plugins/prs1_loader.cpp | 1 +
1 file changed, 1 insertion(+)
diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp
index 7432525d..93dd045c 100644
--- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp
+++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp
@@ -226,6 +226,7 @@ static const PRS1TestedModel s_PRS1TestedModels[] = {
{ "560P", 0, 4, "REMstar Auto (System One 60 Series)" },
{ "560PBT", 0, 4, "REMstar Auto (System One 60 Series)" },
{ "561P", 0, 4, "REMstar Auto (System One 60 Series)" },
+ { "562P", 0, 4, "REMstar Auto (System One 60 Series)" },
{ "660P", 0, 4, "BiPAP Pro (System One 60 Series)" },
{ "760P", 0, 4, "BiPAP Auto (System One 60 Series)" },
From 7dd891df310636dec2ea4fccaba6a83262b1f847 Mon Sep 17 00:00:00 2001
From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com>
Date: Mon, 5 Aug 2019 21:37:19 -0500
Subject: [PATCH 05/20] Add more F0V6 events based on remaining 400X sample
data.
---
oscar/SleepLib/loader_plugins/prs1_loader.cpp | 29 ++++++++++++-------
1 file changed, 19 insertions(+), 10 deletions(-)
diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp
index 93dd045c..1d00a16a 100644
--- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp
+++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp
@@ -3267,8 +3267,13 @@ bool PRS1DataChunk::ParseEventsF0V6(CPAPMode /*mode*/)
switch (code) {
case 1: // Pressure adjustment
// Matches pressure setting, both initial and when ramp button pressed.
- // TODO: Have OSCAR treat EPAP adjustment events differently than (average?) stats below.
- //this->AddEvent(new PRS1EPAPEvent(t, data[pos++]));
+ // TODO: Have OSCAR treat CPAP adjustment events differently than (average?) stats below.
+ // TODO: Based on waveform reports, it looks like the pressure graph is drawn by
+ // interpolating between these pressure adjustments, by 0.5 cmH2O spaced evenly between
+ // adjustments. E.g. 6 at 28:11 and 7.3 at 29:05 results in the following dots:
+ // 6 at 28:11, 6.5 around 28:30, 7.0 around 28:50, 7(.3) at 29:05. That holds until
+ // subsequent "adjustment" of 7.3 at 30:09 followed by 8.0 at 30:19.
+ this->AddEvent(new PRS1CPAPEvent(t, data[pos++]));
break;
/*
case 2: // Timed Breath
@@ -3331,15 +3336,14 @@ bool PRS1DataChunk::ParseEventsF0V6(CPAPMode /*mode*/)
elapsed = data[pos++]; // based on sample waveform, the hypopnea is over after this
this->AddEvent(new PRS1HypopneaEvent(t - elapsed, 0));
break;
- /*
- case 0x08: // Flow Limitation
+ 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 0x09: // 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 statistic above seems to be a total count. It's unclear whether
@@ -3347,7 +3351,6 @@ bool PRS1DataChunk::ParseEventsF0V6(CPAPMode /*mode*/)
// no data bytes
this->AddEvent(new PRS1VibratorySnoreEvent(t, 0));
break;
- */
case 0x0e: // ???
// 5 bytes like PB and LL, but what is it?
duration = 2 * (data[pos] | (data[pos+1] << 8)); // this looks like a 16-bit value, so may be duration like PB?
@@ -3370,6 +3373,9 @@ bool PRS1DataChunk::ParseEventsF0V6(CPAPMode /*mode*/)
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 0x14: // Hypopnea
// TODO: Why does this hypopnea have a different event code?
// fall through
@@ -3388,11 +3394,14 @@ bool PRS1DataChunk::ParseEventsF0V6(CPAPMode /*mode*/)
this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos-1, size+1));
break;
*/
- case 0x12: // Summary
+ 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.
CHECK_VALUE(data[pos], 0);
- CHECK_VALUE(data[pos+1], 0x78); // pressure?
- //CHECK_VALUE(data[pos+2], 1); // Total snore count
- CHECK_VALUE(data[pos+3], 0);
+ //CHECK_VALUE(data[pos+1], 0x78); // pressure
+ //CHECK_VALUE(data[pos+2], 1); // 16-bit snore count
+ //CHECK_VALUE(data[pos+3], 0);
break;
default:
qWarning() << "Unknown event:" << code << "in" << this->sessionid << "at" << startpos-1;
From d99e397cb39ef0a6cb36e84019dc23390936f7ae Mon Sep 17 00:00:00 2001
From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com>
Date: Tue, 6 Aug 2019 15:47:36 -0500
Subject: [PATCH 06/20] Update PRS1 parser for 500X, 600X, and 700X events.
---
oscar/SleepLib/loader_plugins/prs1_loader.cpp | 27 +++++++++++--------
1 file changed, 16 insertions(+), 11 deletions(-)
diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp
index 1d00a16a..735163d1 100644
--- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp
+++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp
@@ -3275,20 +3275,23 @@ bool PRS1DataChunk::ParseEventsF0V6(CPAPMode /*mode*/)
// subsequent "adjustment" of 7.3 at 30:09 followed by 8.0 at 30:19.
this->AddEvent(new PRS1CPAPEvent(t, data[pos++]));
break;
- /*
- case 2: // Timed Breath
- // TB events have a duration in 0.1s, based on the review of pressure waveforms.
- // TODO: Ideally the starting time here would be adjusted here, but PRS1ParsedEvents
- // currently assume integer seconds rather than ms, so that's done at import.
- duration = data[pos++];
- this->AddEvent(new PRS1TimedBreathEvent(t, duration));
+ case 2: // Pressure adjustment (bi-level)
+ // TODO: Have OSCAR treat pressure adjustment events differently than (average?) stats below.
+ // See notes above on interpolation.
+ this->AddEvent(new PRS1EPAPEvent(t, data[pos++]));
+ this->AddEvent(new PRS1IPAPEvent(t, data[pos++]));
+ break;
+ case 3: // Pressure adjustment? (auto-CPAP)
+ // This seems to correspond to the minimum auto-CPAP pressure setting, and
+ // seems to stay fixed throughout the session.
+ //this->AddEvent(new PRS1CPAPEvent(t, data[pos]));
break;
- */
case 0x11: // Statistics
this->AddEvent(new PRS1TotalLeakEvent(t, data[pos++]));
this->AddEvent(new PRS1SnoreEvent(t, data[pos++]));
- // pressure? usually lower, but on a brief session was exactly set pressure
- // also lower on session where pressure was at ramp most of the time
+ // 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
+ // bi-level it's presumably an average of the actual pressures.
//this->AddEvent(new PRS1EPAPEvent(t, data[pos++]));
break;
/*
@@ -3398,7 +3401,9 @@ bool PRS1DataChunk::ParseEventsF0V6(CPAPMode /*mode*/)
// 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.
- CHECK_VALUE(data[pos], 0);
+ 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);
From fc3ec0d4857be78ba29f014d0860b67cee7fd5cd Mon Sep 17 00:00:00 2001
From: Seeker4
Date: Fri, 9 Aug 2019 15:32:37 -0700
Subject: [PATCH 07/20] View/Reset Graphs now additionally enables all graphs
and all event flags
---
oscar/Graphs/gGraphView.cpp | 2 +-
oscar/daily.cpp | 25 +++++++++++++++++++++++--
oscar/docs/release_notes.html | 2 +-
3 files changed, 25 insertions(+), 4 deletions(-)
diff --git a/oscar/Graphs/gGraphView.cpp b/oscar/Graphs/gGraphView.cpp
index 9ca8074e..0e057c9e 100644
--- a/oscar/Graphs/gGraphView.cpp
+++ b/oscar/Graphs/gGraphView.cpp
@@ -3239,7 +3239,6 @@ void gGraphView::keyPressEvent(QKeyEvent *event)
//qDebug() << "Keypress??";
}
-
void gGraphView::setDay(Day *day)
{
@@ -3251,6 +3250,7 @@ void gGraphView::setDay(Day *day)
ResetBounds(false);
}
+
bool gGraphView::isEmpty()
{
bool res = true;
diff --git a/oscar/daily.cpp b/oscar/daily.cpp
index ec368fe2..fe58d54f 100644
--- a/oscar/daily.cpp
+++ b/oscar/daily.cpp
@@ -852,6 +852,28 @@ void Daily::ResetGraphLayout()
void Daily::ResetGraphOrder()
{
GraphView->resetGraphOrder(true);
+
+ // Enable all graphs (make them not hidden)
+ for (int i=0;igraphCombo->count();i++) {
+ // If disabled, emulate a click to enable the graph
+ if (!ui->graphCombo->itemData(i,Qt::UserRole).toBool()) {
+ qDebug() << "resetting graph" << i;
+ Daily::on_graphCombo_activated(i);
+ }
+ }
+
+ // Mark all events as active
+ for (int i=0;ieventsCombo->count();i++) {
+ // If disabled, emulate a click to enable the event
+ ChannelID code = ui->eventsCombo->itemData(i, Qt::UserRole).toUInt();
+ schema::Channel * chan = &schema::channel[code];
+ if (!chan->enabled()) {
+ qDebug() << "resetting event" << i;
+ Daily::on_eventsCombo_activated(i);
+ }
+ }
+
+ // Reset graph heights (and repaint)
ResetGraphLayout();
}
@@ -2416,6 +2438,7 @@ void Daily::on_graphCombo_activated(int index)
GraphView->updateScale();
GraphView->redraw();
}
+
void Daily::updateCube()
{
//brick..
@@ -2495,7 +2518,6 @@ void Daily::updateGraphCombo()
}
ui->graphCombo->setCurrentIndex(0);
-
updateCube();
}
@@ -2504,7 +2526,6 @@ void Daily::on_eventsCombo_activated(int index)
if (index<0)
return;
-
ChannelID code = ui->eventsCombo->itemData(index, Qt::UserRole).toUInt();
schema::Channel * chan = &schema::channel[code];
diff --git a/oscar/docs/release_notes.html b/oscar/docs/release_notes.html
index 6b01d55d..bb52b713 100644
--- a/oscar/docs/release_notes.html
+++ b/oscar/docs/release_notes.html
@@ -11,7 +11,7 @@ Which was written and copyright 2011-2018 © Mark Watkins
- Portions of OSCAR are © 2019 by The OSCAR Team
- [new]
-- [fix]
+- [fix] View/Reset Graphs now enables all graphs and all event flags
- [fix] Calendar date now formatted per national settings
From 83a01fa203a0425c2b9c6825059766758011ff32 Mon Sep 17 00:00:00 2001
From: Seeker4
Date: Fri, 9 Aug 2019 22:54:00 -0700
Subject: [PATCH 08/20] Date bar on bottom of Daily graph now in local time
when no line cursor displayed, and formatting updated
---
oscar/Graphs/gGraphView.cpp | 29 ++++++++++++++++++++++++++---
1 file changed, 26 insertions(+), 3 deletions(-)
diff --git a/oscar/Graphs/gGraphView.cpp b/oscar/Graphs/gGraphView.cpp
index 9ca8074e..cfc3b65c 100644
--- a/oscar/Graphs/gGraphView.cpp
+++ b/oscar/Graphs/gGraphView.cpp
@@ -1554,6 +1554,30 @@ void gGraphView::paintGL()
QString gGraphView::getRangeString()
{
+ QDateTime st = QDateTime::fromMSecsSinceEpoch(m_minx);
+ QDateTime et = QDateTime::fromMSecsSinceEpoch(m_maxx);
+
+ QDate std = st.date();
+ QDate etd = et.date();
+ // Format if Begin and End are on different days
+ if (std != etd) {
+ QString txt = st.toString(" d MMM [ HH:mm:ss") + " - " + et.toString("HH:mm:ss ] d MMM yyyy");
+ return txt;
+ }
+
+ qint64 diff = m_maxx - m_minx;
+ QString fmt;
+
+ if (diff > 60000) {
+ fmt = "HH:mm:ss";
+ } else {
+ fmt = "HH:mm:ss:zzz";
+ }
+ QString txt = st.toString(QObject::tr("d MMM yyyy [ %1 - %2 ]").arg(fmt).arg(et.toString(fmt))) ;
+
+ return txt;
+
+/***** WTF is this code trying to do? Replaced by above 8/9/2019
// a note about time zone usage here
// even though this string will be displayed to the user
// the graph is drawn using UTC times, so no conversion
@@ -1566,7 +1590,7 @@ QString gGraphView::getRangeString()
qint64 diff = m_maxx - m_minx;
- if (diff > 86400000) {
+ if (diff > 86400000) { // 86400000 is one day, in milliseconds
int days = ceil(double(m_maxx-m_minx) / 86400000.0);
qint64 minx = floor(double(m_minx)/86400000.0);
@@ -1584,12 +1608,11 @@ QString gGraphView::getRangeString()
} else {
fmt = "HH:mm:ss:zzz";
}
- QDateTime st = QDateTime::fromMSecsSinceEpoch(m_minx, Qt::UTC);
- QDateTime et = QDateTime::fromMSecsSinceEpoch(m_maxx, Qt::UTC);
QString txt = st.toString(QObject::tr("d MMM [ %1 - %2 ]").arg(fmt).arg(et.toString(fmt))) ;
return txt;
+*/
}
void gGraphView::leaveEvent(QEvent * event)
From cd30fd73ab2b0666b6804248d9388ac8e1bb8bbe Mon Sep 17 00:00:00 2001
From: Seeker4
Date: Fri, 9 Aug 2019 23:00:17 -0700
Subject: [PATCH 09/20] Release notes for Daily page date bar change
---
oscar/docs/release_notes.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/oscar/docs/release_notes.html b/oscar/docs/release_notes.html
index bb52b713..eccd8655 100644
--- a/oscar/docs/release_notes.html
+++ b/oscar/docs/release_notes.html
@@ -10,7 +10,7 @@ Which was written and copyright 2011-2018 © Mark Watkins
Changes and fixes in OSCAR AFTER v1.1.0-testing-3
- Portions of OSCAR are © 2019 by The OSCAR Team
-- [new]
+- [fix] Date bar on bottom of Daily graph now in local time when no line cursor displayed, and formatting improved
- [fix] View/Reset Graphs now enables all graphs and all event flags
- [fix] Calendar date now formatted per national settings
From edfbb692e9d19f7dbac6c5d59c6c483abaa9bf56 Mon Sep 17 00:00:00 2001
From: Seeker4
Date: Fri, 9 Aug 2019 23:14:32 -0700
Subject: [PATCH 10/20] Tweak format of date bar on Daily page slightly.
---
oscar/Graphs/gGraphView.cpp | 2 ++
1 file changed, 2 insertions(+)
diff --git a/oscar/Graphs/gGraphView.cpp b/oscar/Graphs/gGraphView.cpp
index 846d251a..68f06828 100644
--- a/oscar/Graphs/gGraphView.cpp
+++ b/oscar/Graphs/gGraphView.cpp
@@ -1559,12 +1559,14 @@ QString gGraphView::getRangeString()
QDate std = st.date();
QDate etd = et.date();
+
// Format if Begin and End are on different days
if (std != etd) {
QString txt = st.toString(" d MMM [ HH:mm:ss") + " - " + et.toString("HH:mm:ss ] d MMM yyyy");
return txt;
}
+ // Range is within one (local) day
qint64 diff = m_maxx - m_minx;
QString fmt;
From 946293b67d0cd993eff0e9ab1abc3c0beb10a335 Mon Sep 17 00:00:00 2001
From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com>
Date: Mon, 12 Aug 2019 16:58:27 -0400
Subject: [PATCH 11/20] Split PRS1Import::ParseF0Events into ParseEventsF0V6,
no changes yet.
---
oscar/SleepLib/loader_plugins/prs1_loader.cpp | 176 +++++++++++++++++-
oscar/SleepLib/loader_plugins/prs1_loader.h | 2 +
2 files changed, 176 insertions(+), 2 deletions(-)
diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp
index 735163d1..5d75cc69 100644
--- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp
+++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp
@@ -2817,7 +2817,6 @@ void SmoothEventList(Session * session, EventList * ev, ChannelID code)
// 750P is F0V2; 550P is F0V2/F0V3; 450P is F0V3; 460P, 560P[BT], 660P, 760P are F0V4
-// 200X, 400X, 400G, 500X, 502G, 600X, 700X are F0V6
bool PRS1Import::ParseF0Events()
{
// Required channels
@@ -3213,6 +3212,175 @@ bool PRS1DataChunk::ParseEventsF0V234(CPAPMode mode)
}
+// 200X, 400X, 400G, 500X, 502G, 600X, 700X are F0V6
+// Originally a copy of PRS1Import::ParseF0Events, modified based on F5V3/F3V6 importers and comparison to reports
+bool PRS1Import::ParseEventsF0V6()
+{
+ // 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 *TOTLEAK = session->AddEventList(CPAP_LeakTotal, EVL_Event);
+ EventList *LEAK = session->AddEventList(CPAP_Leak, EVL_Event);
+ EventList *PB = session->AddEventList(CPAP_PB, EVL_Event);
+ EventList *FL = session->AddEventList(CPAP_FlowLimit, EVL_Event);
+ EventList *SNORE = session->AddEventList(CPAP_Snore, EVL_Event);
+ EventList *VS = session->AddEventList(CPAP_VSnore, EVL_Event);
+ EventList *VS2 = session->AddEventList(CPAP_VSnore2, EVL_Event);
+ EventList *PP = session->AddEventList(CPAP_PressurePulse, EVL_Event);
+ EventList *RE = session->AddEventList(CPAP_RERA, EVL_Event);
+
+
+ // On-demand channels
+ ChannelID Codes[] = {
+ PRS1_00, PRS1_01, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ PRS1_0B, 0, 0, PRS1_0E
+ };
+
+ int ncodes = sizeof(Codes) / sizeof(ChannelID);
+ EventList *Code[0x20] = {0};
+
+ Code[0x0e] = session->AddEventList(PRS1_0E, EVL_Event);
+ EventList * LL = session->AddEventList(CPAP_LargeLeak, EVL_Event);
+
+ EventList *PRESSURE = nullptr;
+ EventList *EPAP = nullptr;
+ EventList *IPAP = nullptr;
+ EventList *PS = nullptr;
+
+
+ // Unintentional leak calculation, see zMaskProfile:calcLeak in calcs.cpp for explanation
+ EventDataType currentPressure=0, leak;
+
+ bool calcLeaks = p_profile->cpap->calculateUnintentionalLeaks();
+ EventDataType lpm4 = p_profile->cpap->custom4cmH2OLeaks();
+ EventDataType lpm20 = p_profile->cpap->custom20cmH2OLeaks();
+
+ EventDataType lpm = lpm20 - lpm4;
+ EventDataType ppm = lpm / 16.0;
+
+ CPAPMode mode = (CPAPMode) session->settings[CPAP_Mode].toInt();
+
+ qint64 t = qint64(event->timestamp) * 1000L;
+ session->updateFirst(t);
+
+ bool ok;
+ ok = event->ParseEvents(mode);
+
+ for (int i=0; i < event->m_parsedData.count(); i++) {
+ PRS1ParsedEvent* e = event->m_parsedData.at(i);
+ t = qint64(event->timestamp + e->m_start) * 1000L;
+
+ switch (e->m_type) {
+ case PRS1CPAPEvent::TYPE:
+ if (!PRESSURE) {
+ if (!(PRESSURE = session->AddEventList(CPAP_Pressure, EVL_Event, e->m_gain))) { return false; }
+ }
+ PRESSURE->AddEvent(t, e->m_value);
+ currentPressure = e->m_value;
+ break;
+ case PRS1IPAPEvent::TYPE:
+ if(!IPAP) {
+ if (!(IPAP = session->AddEventList(CPAP_IPAP, EVL_Event, e->m_gain))) { return false; }
+ }
+ IPAP->AddEvent(t, e->m_value);
+ currentPressure = e->m_value;
+ break;
+ case PRS1EPAPEvent::TYPE:
+ if (!EPAP) {
+ if (!(EPAP = session->AddEventList(CPAP_EPAP, EVL_Event, e->m_gain))) { return false; }
+ }
+ if(!PS) {
+ if (!(PS = session->AddEventList(CPAP_PS, EVL_Event, e->m_gain))) { return false; }
+ }
+ EPAP->AddEvent(t, e->m_value);
+ PS->AddEvent(t, currentPressure - e->m_value); // Pressure Support
+ break;
+ case PRS1PressureReliefEvent::TYPE:
+ if (!EPAP) {
+ if (!(EPAP = session->AddEventList(CPAP_EPAP, EVL_Event, e->m_gain))) { return false; }
+ }
+ EPAP->AddEvent(t, e->m_value);
+ break;
+ case PRS1ObstructiveApneaEvent::TYPE:
+ OA->AddEvent(t, e->m_duration);
+ break;
+ case PRS1ClearAirwayEvent::TYPE:
+ CA->AddEvent(t, e->m_duration);
+ break;
+ case PRS1HypopneaEvent::TYPE:
+ HY->AddEvent(t, e->m_duration);
+ break;
+ case PRS1FlowLimitationEvent::TYPE:
+ FL->AddEvent(t, e->m_duration);
+ break;
+ case PRS1PeriodicBreathingEvent::TYPE:
+ PB->AddEvent(t, e->m_duration);
+ break;
+ case PRS1LargeLeakEvent::TYPE:
+ LL->AddEvent(t, e->m_duration);
+ break;
+ case PRS1TotalLeakEvent::TYPE:
+ TOTLEAK->AddEvent(t, e->m_value);
+ leak = e->m_value;
+ if (calcLeaks) { // Much Quicker doing this here than the recalc method.
+ leak -= (((currentPressure/10.0f) - 4.0) * ppm + lpm4);
+ if (leak < 0) leak = 0;
+ LEAK->AddEvent(t, leak);
+ }
+ break;
+ case PRS1SnoreEvent::TYPE:
+ SNORE->AddEvent(t, e->m_value);
+ if (e->m_value > 0) {
+ VS2->AddEvent(t, e->m_value);
+ }
+ break;
+ case PRS1VibratorySnoreEvent::TYPE: // F0: Is this really distinct from SNORE and VS2?
+ VS->AddEvent(t, 0);
+ break;
+ case PRS1RERAEvent::TYPE:
+ RE->AddEvent(t, e->m_value);
+ break;
+ case PRS1PressurePulseEvent::TYPE:
+ PP->AddEvent(t, e->m_value);
+ break;
+ case PRS1UnknownValueEvent::TYPE:
+ {
+ int code = ((PRS1UnknownValueEvent*) e)->m_code;
+ Q_ASSERT(code < ncodes);
+ if (!Code[code]) {
+ ChannelID cpapcode = Codes[(int)code];
+ Q_ASSERT(cpapcode); // any unknown codes returned by chunk parser should be given a channel above
+ if (!(Code[code] = session->AddEventList(cpapcode, EVL_Event, e->m_gain))) { return false; }
+ }
+ Code[code]->AddEvent(t, e->m_value);
+ break;
+ }
+ default:
+ qWarning() << "Unknown PRS1 event type" << (int) e->m_type;
+ break;
+ }
+ }
+
+ if (!ok) {
+ return false;
+ }
+
+ t = qint64(event->timestamp + event->duration) * 1000L;
+ session->updateLast(t);
+ session->m_cnt.clear();
+ session->m_cph.clear();
+
+ session->m_lastchan.clear();
+ session->m_firstchan.clear();
+ session->m_valuesummary[CPAP_Pressure].clear();
+ session->m_valuesummary.erase(session->m_valuesummary.find(CPAP_Pressure));
+
+ return true;
+}
+
+
// DreamStation family 0 CPAP/APAP machines (400X-700X)
// Originally derived from F5V3 parsing + (incomplete/broken) F0V234 parsing + sample data
bool PRS1DataChunk::ParseEventsF0V6(CPAPMode /*mode*/)
@@ -5374,7 +5542,11 @@ bool PRS1Import::ParseEvents()
if (!event) return false;
switch (event->family) {
case 0:
- res = ParseF0Events();
+ if (event->familyVersion == 6) {
+ res = this->ParseEventsF0V6();
+ } else {
+ res = this->ParseF0Events();
+ }
break;
case 3:
// NOTE: The original comment in the header for ParseF3EventsV3 said there was a 1060P with fileVersion 3.
diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.h b/oscar/SleepLib/loader_plugins/prs1_loader.h
index a5d9a8eb..8c0e21f3 100644
--- a/oscar/SleepLib/loader_plugins/prs1_loader.h
+++ b/oscar/SleepLib/loader_plugins/prs1_loader.h
@@ -278,6 +278,8 @@ public:
//! \brief Parse a single data chunk from a .002 file containing event data for a standard system one machine
bool ParseF0Events();
+ //! \brief Parse a single data chunk from a .002 file containing event data for a standard system one machine (family version 6)
+ bool ParseEventsF0V6();
//! \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 family 3 ventilator machine (family version 6)
From 3de32dd21bd734404761a11f3b1d0355c5991d97 Mon Sep 17 00:00:00 2001
From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com>
Date: Mon, 12 Aug 2019 17:20:25 -0400
Subject: [PATCH 12/20] Fix F0V6 PB starting time, and probably LL and PS as
well.
---
oscar/SleepLib/loader_plugins/prs1_loader.cpp | 44 ++++++++++++-------
1 file changed, 27 insertions(+), 17 deletions(-)
diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp
index 5d75cc69..3bf27afe 100644
--- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp
+++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp
@@ -3221,6 +3221,7 @@ bool PRS1Import::ParseEventsF0V6()
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 *PB = session->AddEventList(CPAP_PB, EVL_Event);
@@ -3234,15 +3235,14 @@ bool PRS1Import::ParseEventsF0V6()
// On-demand channels
ChannelID Codes[] = {
- PRS1_00, PRS1_01, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- PRS1_0B, 0, 0, PRS1_0E
+ PRS1_00, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, PRS1_0E
};
int ncodes = sizeof(Codes) / sizeof(ChannelID);
EventList *Code[0x20] = {0};
Code[0x0e] = session->AddEventList(PRS1_0E, EVL_Event);
- EventList * LL = session->AddEventList(CPAP_LargeLeak, EVL_Event);
EventList *PRESSURE = nullptr;
EventList *EPAP = nullptr;
@@ -3262,6 +3262,7 @@ bool PRS1Import::ParseEventsF0V6()
CPAPMode mode = (CPAPMode) session->settings[CPAP_Mode].toInt();
+ qint64 duration;
qint64 t = qint64(event->timestamp) * 1000L;
session->updateFirst(t);
@@ -3297,12 +3298,6 @@ bool PRS1Import::ParseEventsF0V6()
EPAP->AddEvent(t, e->m_value);
PS->AddEvent(t, currentPressure - e->m_value); // Pressure Support
break;
- case PRS1PressureReliefEvent::TYPE:
- if (!EPAP) {
- if (!(EPAP = session->AddEventList(CPAP_EPAP, EVL_Event, e->m_gain))) { return false; }
- }
- EPAP->AddEvent(t, e->m_value);
- break;
case PRS1ObstructiveApneaEvent::TYPE:
OA->AddEvent(t, e->m_duration);
break;
@@ -3316,27 +3311,42 @@ bool PRS1Import::ParseEventsF0V6()
FL->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);
leak = e->m_value;
+ // F0V6 doesn't appear to report non-total leak
if (calcLeaks) { // Much Quicker doing this here than the recalc method.
leak -= (((currentPressure/10.0f) - 4.0) * ppm + lpm4);
if (leak < 0) leak = 0;
LEAK->AddEvent(t, leak);
}
break;
- case PRS1SnoreEvent::TYPE:
+ 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) {
- VS2->AddEvent(t, e->m_value);
+ // TODO: currently these get drawn on our waveforms, but they probably shouldn't,
+ // since they don't have a precise timestamp. They should continue to be drawn
+ // on the flags overview.
+ VS2->AddEvent(t, 0);
}
break;
- case PRS1VibratorySnoreEvent::TYPE: // F0: Is this really distinct from SNORE and VS2?
+ case PRS1VibratorySnoreEvent::TYPE: // real VS marker on waveform
+ // TODO: These don't need to be drawn separately on the flag overview, since
+ // they're presumably included in the overall snore count statistic. They should
+ // continue to be drawn on the waveform, due to their precise timestamp.
VS->AddEvent(t, 0);
break;
case PRS1RERAEvent::TYPE:
@@ -3404,7 +3414,7 @@ bool PRS1DataChunk::ParseEventsF0V6(CPAPMode /*mode*/)
int pos = 0, startpos;
int code, size;
int t = 0;
- int elapsed/*, duration*/;
+ int elapsed, duration;
do {
code = data[pos++];
if (!this->hblock.contains(code)) {
@@ -3446,8 +3456,8 @@ bool PRS1DataChunk::ParseEventsF0V6(CPAPMode /*mode*/)
case 2: // Pressure adjustment (bi-level)
// TODO: Have OSCAR treat pressure adjustment events differently than (average?) stats below.
// See notes above on interpolation.
- this->AddEvent(new PRS1EPAPEvent(t, data[pos++]));
- this->AddEvent(new PRS1IPAPEvent(t, data[pos++]));
+ this->AddEvent(new PRS1IPAPEvent(t, data[pos+1]));
+ this->AddEvent(new PRS1EPAPEvent(t, data[pos]));
break;
case 3: // Pressure adjustment? (auto-CPAP)
// This seems to correspond to the minimum auto-CPAP pressure setting, and
From 3492323216ef8da57b72db68afb3abd5a216a010 Mon Sep 17 00:00:00 2001
From: Seeker4
Date: Mon, 12 Aug 2019 15:59:47 -0700
Subject: [PATCH 13/20] =?UTF-8?q?Change=20Romanian=20name=20to=20Rom=C3=A2?=
=?UTF-8?q?ne=C8=99te,=20clarify=20which=20translation=20file=20qDebug=20s?=
=?UTF-8?q?tatements=20refer=20to.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
oscar/translation.cpp | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/oscar/translation.cpp b/oscar/translation.cpp
index a377abe4..d8cb45c3 100644
--- a/oscar/translation.cpp
+++ b/oscar/translation.cpp
@@ -50,7 +50,7 @@ void initTranslations()
langNames["en_UK"] = "English (UK)";
langNames["nl"] = "Nederlands";
langNames["pt_BR"] = "Portugues (BR)";
- langNames["ro"] = "Romanian";
+ langNames["ro"] = "Românește";
langNames[DefaultLanguage]="English (US)";
@@ -176,7 +176,7 @@ void initTranslations()
QString qtLang = language.left(2);
if ( qtLang.compare("zh") == 0 )
qtLang.append("_CN");
- qDebug() << "Loading" << langname << "translation" << "qt_" + qtLang + ".qm" << "from" << qtLangPath.toLocal8Bit().data();
+ qDebug() << "Loading" << langname << "QT translation" << "qt_" + qtLang + ".qm" << "from" << qtLangPath.toLocal8Bit().data();
QTranslator * qtranslator = new QTranslator();
if (!langfile.isEmpty() && !qtranslator->load("qt_" + qtLang + ".qm", qtLangPath)) {
@@ -186,7 +186,7 @@ void initTranslations()
qApp->installTranslator(qtranslator);
// Install OSCAR translation files
- qDebug() << "Loading" << langname << "translation" << langfile.toLocal8Bit().data() << "from" << langpath.toLocal8Bit().data();
+ qDebug() << "Loading" << langname << "OSCAR translation" << langfile.toLocal8Bit().data() << "from" << langpath.toLocal8Bit().data();
QTranslator * translator = new QTranslator();
if (!langfile.isEmpty() && !translator->load(langfile, langpath)) {
From e315baf6dfe1dcf2007cf3a86ba962ba5c6324da Mon Sep 17 00:00:00 2001
From: Seeker4
Date: Mon, 12 Aug 2019 16:02:53 -0700
Subject: [PATCH 14/20] Test builds use settings key of oscar-test, branch
builds oscar-branch, and release builds just oscar. Default data directory
named similarly.
---
oscar/SleepLib/common.cpp | 30 +++++++++++++++++++++---------
1 file changed, 21 insertions(+), 9 deletions(-)
diff --git a/oscar/SleepLib/common.cpp b/oscar/SleepLib/common.cpp
index 78947c21..9c0aebca 100644
--- a/oscar/SleepLib/common.cpp
+++ b/oscar/SleepLib/common.cpp
@@ -90,23 +90,35 @@ const QString getDeveloperDomain()
const QString getAppName()
{
QString name = STR_AppName;
- if ((GIT_BRANCH != "master") ||
- (!((ReleaseStatus.compare("r", Qt::CaseInsensitive)==0) ||
- (ReleaseStatus.compare("rc", Qt::CaseInsensitive)==0) ||
- (ReleaseStatus.compare("beta", Qt::CaseInsensitive)==0)))) {
+
+ // Append branch if there is a branch specified
+ if (GIT_BRANCH != "master") {
name += "-"+GIT_BRANCH;
+// qDebug() << "getAppName, not master, name is" << name << "branch is" << GIT_BRANCH;
}
+
+ // Append "-test" if not release
+ else if (!((ReleaseStatus.compare("r", Qt::CaseInsensitive)==0) ||
+ (ReleaseStatus.compare("rc", Qt::CaseInsensitive)==0) )) {
+ name += "-test";
+// qDebug() << "getAppName, not release, name is" << name << "release type is" << ReleaseStatus;
+ }
+
return name;
}
const QString getModifiedAppData()
{
QString appdata = STR_AppData;
- if ((GIT_BRANCH != "master") ||
- (!((ReleaseStatus.compare("r", Qt::CaseInsensitive)==0) ||
- (ReleaseStatus.compare("rc", Qt::CaseInsensitive)==0) ||
- (ReleaseStatus.compare("beta", Qt::CaseInsensitive)==0)))) {
+
+ // Append branch if there is a branch specified
+ if (GIT_BRANCH != "master")
appdata += "-"+GIT_BRANCH;
+
+ // Append "-test" if not release
+ else if (!((ReleaseStatus.compare("r", Qt::CaseInsensitive)==0) ||
+ (ReleaseStatus.compare("rc", Qt::CaseInsensitive)==0) )) {
+ appdata += "-test";
}
return appdata;
}
@@ -230,7 +242,7 @@ QStringList makeBuildInfo (QString relinfo, QString forcedEngine){
branch = QObject::tr("Branch:") + " " + GIT_BRANCH + ", ";
}
buildInfo << branch + (QObject::tr("Revision")) + " " + GIT_REVISION;
- if (GIT_BRANCH != "master")
+ if (getAppName() != STR_AppName) // Report any non-standard app key
buildInfo << (QObject::tr("App key:") + " " + getAppName());
buildInfo << QString("");
buildInfo << (QObject::tr("Operating system:") + " " + QSysInfo::prettyProductName());
From 8a98cf1400214b87c211e0983314b171da27bb05 Mon Sep 17 00:00:00 2001
From: Seeker4
Date: Mon, 12 Aug 2019 16:04:33 -0700
Subject: [PATCH 15/20] Windows installers now support Oscar, Oscar (test),
Oscar 32-bit, and Oscar 32-bit (test). First two are 64-bit.
---
Building/Windows/BuildInstall.iss | 44 +++++++++++++++++++++----------
1 file changed, 30 insertions(+), 14 deletions(-)
diff --git a/Building/Windows/BuildInstall.iss b/Building/Windows/BuildInstall.iss
index faa3b7fa..1bb99ff3 100644
--- a/Building/Windows/BuildInstall.iss
+++ b/Building/Windows/BuildInstall.iss
@@ -7,29 +7,45 @@
#define MyAppVersion MyMajorVersion+"."+MyMinorVersion+"."+MyRevision+"-"+MyReleaseStatus
#if MyReleaseStatus == "r"
-#define MyAppVersion MyAppVersion+MyBuildNumber
+ #define MyAppVersion MyAppVersion+MyBuildNumber
#else
-#define MyAppVersion MyAppVersion+"-"+MyBuildNumber
+ #define MyAppVersion MyAppVersion+"-"+MyBuildNumber
#endif
-#define MyAppName "OSCAR"
#define MyAppPublisher "The OSCAR Team"
#define MyAppExeName "OSCAR.exe"
+#define MyAppName "OSCAR"
[Setup]
SetupLogging=yes
; NOTE: The value of AppId uniquely identifies this application.
; Do not use the same AppId value in installers for other applications.
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
-; Now using separate AppID for Win32 and Win64 -- GTS 4/6/2019
+; Now using separate AppID for Win32 and Win64 and for test builds -- GTS 4/6/2019
#if MyPlatform == "Win64"
ArchitecturesAllowed=x64
ArchitecturesInstallIn64BitMode=x64
- AppId={{FC6F08E6-69BF-4469-ADE3-78199288D305}
+ #if MyReleaseStatus == "r" || MyReleaseStatus == "rc"
+ AppId={{FC6F08E6-69BF-4469-ADE3-78199288D305}
+ #define MyGroupName "OSCAR"
+ #define MyDirName "OSCAR"
+ #else
+ AppId={{C5E23210-4BC5-499D-A0E4-81192748D322}
+ #define MyGroupName "OSCAR (test)"
+ #define MyDirName "OSCAR-test"
+ #endif
; DefaultDirName={%PROGRAMFILES|{pf}}\OSCAR
; above doesn't work. Always returns x86 directory
#else // 32-bit
- AppId={{4F3EB81B-1866-4124-B388-5FB5DA34FFDD}
+ #if MyReleaseStatus == "r" || MyReleaseStatus == "rc"
+ AppId={{4F3EB81B-1866-4124-B388-5FB5DA34FFDD}
+ #define MyGroupName "OSCAR 32-bit"
+ #define MyDirName "OSCAR"
+ #else
+ AppId={{B0382AB3-ECB4-4F9D-ABB1-F6EF73D4E3DB}
+ #define MyGroupName "OSCAR 32-bit (test)"
+ #define MyDirName "OSCAR-test"
+ #endif
; DefaultDirName={%PROGRAMFILES(X86)|{pf}}\OSCAR
#endif
AppName={#MyAppName}
@@ -38,20 +54,20 @@ AppVerName={#MyAppName} {#MyAppVersion}-{#MyPlatform}-{#MyGitRevision}{#MySuffix
AppPublisher={#MyAppPublisher}
AppCopyright=Copyright 2019 {#MyAppPublisher}
; **** AppCopyright=Copyright {GetDateTimeString('yyyy', #0, #0)} {%MyAppPublisher}
-DefaultDirName={pf}\OSCAR
-DefaultGroupName={#MyAppName}
+DefaultDirName={pf}\{#MyDirName}
+DefaultGroupName={#MyGroupName}
OutputDir=.\Installer
#if MyReleaseStatus == "r"
-OutputBaseFilename={#MyAppName}-{#MyAppVersion}-{#MyPlatform}{#MySuffix}
+ OutputBaseFilename={#MyAppName}-{#MyAppVersion}-{#MyPlatform}{#MySuffix}
#else
-OutputBaseFilename={#MyAppName}-{#MyAppVersion}-{#MyPlatform}-{#MyGitRevision}{#MySuffix}
+ OutputBaseFilename={#MyAppName}-{#MyAppVersion}-{#MyPlatform}-{#MyGitRevision}{#MySuffix}
#endif
SetupIconFile=setup.ico
Compression=lzma
SolidCompression=yes
VersionInfoCompany={#MyAppPublisher}
VersionInfoProductName={#MyAppName}
-UninstallDisplayName={#MyAppName}
+UninstallDisplayName={#MyGroupName}
UninstallDisplayIcon={app}\{#MyAppExeName}
[Languages]
@@ -89,9 +105,9 @@ Source: ".\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs cre
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[Icons]
-Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
-Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"
-Name: "{commondesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
+Name: "{group}\{#MyGroupName}"; Filename: "{app}\{#MyAppExeName}"
+Name: "{group}\{cm:UninstallProgram,{#MyGroupName}}"; Filename: "{uninstallexe}"
+Name: "{commondesktop}\{#MyGroupName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
; Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: quicklaunchicon
[Messages]
From 4b67af970f35969ed7aa9c446e4d8d2f7e3c3e21 Mon Sep 17 00:00:00 2001
From: Seeker4
Date: Mon, 12 Aug 2019 16:26:35 -0700
Subject: [PATCH 16/20] Update release notes for recent changes.
---
oscar/docs/release_notes.html | 2 ++
1 file changed, 2 insertions(+)
diff --git a/oscar/docs/release_notes.html b/oscar/docs/release_notes.html
index eccd8655..e60df6b2 100644
--- a/oscar/docs/release_notes.html
+++ b/oscar/docs/release_notes.html
@@ -10,6 +10,8 @@ Which was written and copyright 2011-2018 © Mark Watkins
Changes and fixes in OSCAR AFTER v1.1.0-testing-3
- Portions of OSCAR are © 2019 by The OSCAR Team
+- [new] Windows installers support Oscar, Oscar 32-bit, Oscar (test) and Oscar 32-bit (test)
+- [fix] Release builds use a Settings key of OSCAR, Test builds use OSCAR-test, and Branch builds use OSCAR-branch. Default data directories are similarly named.
- [fix] Date bar on bottom of Daily graph now in local time when no line cursor displayed, and formatting improved
- [fix] View/Reset Graphs now enables all graphs and all event flags
- [fix] Calendar date now formatted per national settings
From 73dfdac81b21f783b077b7a70b6a71b9827cb5a2 Mon Sep 17 00:00:00 2001
From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com>
Date: Mon, 12 Aug 2019 19:23:47 -0400
Subject: [PATCH 17/20] Add debug logging for PRS1 F0V6 event 3.
---
oscar/SleepLib/loader_plugins/prs1_loader.cpp | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp
index 3bf27afe..cccbe05f 100644
--- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp
+++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp
@@ -3446,7 +3446,7 @@ bool PRS1DataChunk::ParseEventsF0V6(CPAPMode /*mode*/)
case 1: // Pressure adjustment
// Matches pressure setting, both initial and when ramp button pressed.
// TODO: Have OSCAR treat CPAP adjustment events differently than (average?) stats below.
- // TODO: Based on waveform reports, it looks like the pressure graph is drawn by
+ // Based on waveform reports, it looks like the pressure graph is drawn by
// interpolating between these pressure adjustments, by 0.5 cmH2O spaced evenly between
// adjustments. E.g. 6 at 28:11 and 7.3 at 29:05 results in the following dots:
// 6 at 28:11, 6.5 around 28:30, 7.0 around 28:50, 7(.3) at 29:05. That holds until
@@ -3457,12 +3457,13 @@ bool PRS1DataChunk::ParseEventsF0V6(CPAPMode /*mode*/)
// TODO: Have OSCAR treat pressure adjustment events differently than (average?) stats below.
// See notes above on interpolation.
this->AddEvent(new PRS1IPAPEvent(t, data[pos+1]));
- this->AddEvent(new PRS1EPAPEvent(t, data[pos]));
+ this->AddEvent(new PRS1EPAPEvent(t, data[pos])); // EPAP needs to be added second to calculate PS
break;
case 3: // Pressure adjustment? (auto-CPAP)
// This seems to correspond to the minimum auto-CPAP pressure setting, and
// seems to stay fixed throughout the session.
//this->AddEvent(new PRS1CPAPEvent(t, data[pos]));
+ CHECK_VALUE(data[pos++], 4);
break;
case 0x11: // Statistics
this->AddEvent(new PRS1TotalLeakEvent(t, data[pos++]));
@@ -3470,6 +3471,7 @@ bool PRS1DataChunk::ParseEventsF0V6(CPAPMode /*mode*/)
// 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
// bi-level it's presumably an average of the actual pressures.
+ // TODO: What to do with this average pressure? Actual pressure adjustments are handled above.
//this->AddEvent(new PRS1EPAPEvent(t, data[pos++]));
break;
/*
From 3eee72390e32894d00ac9ba49a109f0f22c53a56 Mon Sep 17 00:00:00 2001
From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com>
Date: Tue, 13 Aug 2019 17:29:05 -0400
Subject: [PATCH 18/20] Clean up PRS1DataChunk::ParseEventsF0V6, no change in
functionality.
---
oscar/SleepLib/loader_plugins/prs1_loader.cpp | 83 +++++++------------
1 file changed, 32 insertions(+), 51 deletions(-)
diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp
index cccbe05f..89011f18 100644
--- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp
+++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp
@@ -3392,7 +3392,7 @@ bool PRS1Import::ParseEventsF0V6()
// DreamStation family 0 CPAP/APAP machines (400X-700X)
-// Originally derived from F5V3 parsing + (incomplete/broken) F0V234 parsing + sample data
+// Originally derived from F5V3 parsing + (incomplete) F0V234 parsing + sample data
bool PRS1DataChunk::ParseEventsF0V6(CPAPMode /*mode*/)
{
if (this->family != 0 || this->familyVersion != 6) {
@@ -3437,13 +3437,14 @@ bool PRS1DataChunk::ParseEventsF0V6(CPAPMode /*mode*/)
break;
}
startpos = pos;
- if (code != 0x12) { // TODO: Some events have no timestamp?
+ if (code != 0x12) { // This one event has no timestamp
t += data[pos] | (data[pos+1] << 8);
pos += 2;
}
switch (code) {
- case 1: // Pressure adjustment
+ //case 0x00: // never seen
+ case 0x01: // Pressure adjustment
// Matches pressure setting, both initial and when ramp button pressed.
// TODO: Have OSCAR treat CPAP adjustment events differently than (average?) stats below.
// Based on waveform reports, it looks like the pressure graph is drawn by
@@ -3453,43 +3454,18 @@ bool PRS1DataChunk::ParseEventsF0V6(CPAPMode /*mode*/)
// subsequent "adjustment" of 7.3 at 30:09 followed by 8.0 at 30:19.
this->AddEvent(new PRS1CPAPEvent(t, data[pos++]));
break;
- case 2: // Pressure adjustment (bi-level)
+ case 0x02: // Pressure adjustment (bi-level)
// TODO: Have OSCAR treat pressure adjustment events differently than (average?) stats below.
// See notes above on interpolation.
this->AddEvent(new PRS1IPAPEvent(t, data[pos+1]));
this->AddEvent(new PRS1EPAPEvent(t, data[pos])); // EPAP needs to be added second to calculate PS
break;
- case 3: // Pressure adjustment? (auto-CPAP)
+ case 0x03: // Pressure adjustment? (auto-CPAP)
// This seems to correspond to the minimum auto-CPAP pressure setting, and
// seems to stay fixed throughout the session.
//this->AddEvent(new PRS1CPAPEvent(t, data[pos]));
CHECK_VALUE(data[pos++], 4);
break;
- case 0x11: // Statistics
- this->AddEvent(new PRS1TotalLeakEvent(t, data[pos++]));
- this->AddEvent(new PRS1SnoreEvent(t, data[pos++]));
- // 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
- // bi-level it's presumably an average of the actual pressures.
- // TODO: What to do with this average pressure? Actual pressure adjustments are handled above.
- //this->AddEvent(new PRS1EPAPEvent(t, data[pos++]));
- break;
- /*
- case 3: // Statistics
- // These appear every 2 minutes, so presumably summarize the preceding period.
- this->AddEvent(new PRS1IPAPEvent(t, data[pos++], GAIN)); // 00=IPAP (average?)
- this->AddEvent(new PRS1IPAPLowEvent(t, data[pos++], GAIN)); // 01=IAP Low
- this->AddEvent(new PRS1IPAPHighEvent(t, data[pos++], GAIN)); // 02=IAP High
- 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 PRS1SnoreEvent(t, data[pos++])); // 08=Snore count // TODO: not a VS on official waveform, but appears in flags and contributes to overall VS index
- this->AddEvent(new PRS1EPAPEvent(t, data[pos++], GAIN)); // 09=EPAP (average? see event 1 above)
- this->AddEvent(new PRS1LeakEvent(t, data[pos++])); // 0A=Leak (average?)
- break;
- */
case 0x04: // Pressure Pulse
duration = data[pos++]; // TODO: is this a duration?
this->AddEvent(new PRS1PressurePulseEvent(t, duration));
@@ -3512,8 +3488,11 @@ bool PRS1DataChunk::ParseEventsF0V6(CPAPMode /*mode*/)
elapsed = data[pos++];
this->AddEvent(new PRS1ClearAirwayEvent(t - elapsed, 0));
break;
+ //case 0x08: // never seen
+ //case 0x09: // never seen
+ //case 0x0a: // Hypopnea, see 0x15
case 0x0b: // Hypopnea
- // TODO: How is this hypopnea different from events 0xd and 0xe?
+ // TODO: How is this hypopnea different from events 0xa, 0x14 and 0x15?
// TODO: What is the first byte?
pos++; // unknown first byte?
elapsed = data[pos++]; // based on sample waveform, the hypopnea is over after this
@@ -3529,7 +3508,7 @@ bool PRS1DataChunk::ParseEventsF0V6(CPAPMode /*mode*/)
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 statistic above seems to be a total count. It's unclear whether
+ // 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));
@@ -3556,6 +3535,27 @@ bool PRS1DataChunk::ParseEventsF0V6(CPAPMode /*mode*/)
elapsed = data[pos++];
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++]));
+ // 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
+ // bi-level it's presumably an average of the actual pressures.
+ // TODO: What to do with this average pressure? Actual pressure adjustments are handled above.
+ //this->AddEvent(new PRS1EPAPEvent(t, data[pos++]));
+ 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);
+ break;
+ //case 0x13: // never seen
case 0x0a: // Hypopnea
// TODO: Why does this hypopnea have a different event code?
// fall through
@@ -3569,25 +3569,6 @@ bool PRS1DataChunk::ParseEventsF0V6(CPAPMode /*mode*/)
duration = data[pos++];
this->AddEvent(new PRS1HypopneaEvent(t - duration, 0));
break;
- /*
- case 0x0f:
- // TODO: some other pressure adjustment?
- // Appears near the beginning and end of a session when Opti-Start is on, at least once in middle
- //CHECK_VALUES(data[pos], 0x20, 0x28);
- this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos-1, size+1));
- break;
- */
- case 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);
- break;
default:
qWarning() << "Unknown event:" << code << "in" << this->sessionid << "at" << startpos-1;
this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos-1, size+1));
From cc80a3ef093ee8f37700a4186f2adc3df932c5ce Mon Sep 17 00:00:00 2001
From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com>
Date: Tue, 13 Aug 2019 21:09:55 -0400
Subject: [PATCH 19/20] Remove F0V6 logic from
PRS1DataChunk::ParseEventsF0V234, no functional change.
---
oscar/SleepLib/loader_plugins/prs1_loader.cpp | 34 +++++++++++--------
1 file changed, 19 insertions(+), 15 deletions(-)
diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp
index 89011f18..53c9b9b1 100644
--- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp
+++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp
@@ -502,7 +502,9 @@ void parseModel(MachineInfo & info, const QString & modelnum)
}
}
if (series == nullptr) {
- qWarning() << "unknown series for" << name << modelnum;
+ if (modelnum != "100X100") { // Bogus model number seen in empty C0/Clear0 directories.
+ qWarning() << "unknown series for" << name << modelnum;
+ }
series = "unknown";
}
info.series = QObject::tr(series);
@@ -2986,6 +2988,10 @@ bool PRS1Import::ParseF0Events()
bool PRS1DataChunk::ParseEventsF0V234(CPAPMode mode)
{
+ if (this->family != 0 || this->familyVersion < 2 || this->familyVersion > 4) {
+ qWarning() << "ParseEventsF0V234 called with family" << this->family << "familyVersion" << this->familyVersion;
+ return false;
+ }
unsigned char code=0;
EventDataType data0, data1, data2;
@@ -3000,7 +3006,7 @@ bool PRS1DataChunk::ParseEventsF0V234(CPAPMode mode)
int size = this->m_data.size();
- bool FV3 = (this->fileVersion == 3);
+ CHECK_VALUE(this->fileVersion, 2);
unsigned char * buffer = (unsigned char *)this->m_data.data();
for (pos = 0; pos < size;) {
@@ -3035,13 +3041,13 @@ bool PRS1DataChunk::ParseEventsF0V234(CPAPMode mode)
case 0x00: // Unknown 00
this->AddEvent(new PRS1UnknownValueEvent(code, t, buffer[pos++]));
- if (((this->family == 0) && (this->familyVersion >= 4)) || (this->fileVersion == 3)){
+ if (((this->family == 0) && (this->familyVersion == 4))){
pos++;
}
break;
case 0x01: // Unknown
- if ((this->family == 0) && (this->familyVersion >= 4)) {
+ if ((this->family == 0) && (this->familyVersion == 4)) {
this->AddEvent(new PRS1CPAPEvent(t, buffer[pos++]));
} else {
this->AddEvent(new PRS1UnknownValueEvent(code, t, 0));
@@ -3049,7 +3055,7 @@ bool PRS1DataChunk::ParseEventsF0V234(CPAPMode mode)
break;
case 0x02: // Pressure
- if ((this->family == 0) && (this->familyVersion >= 4)) { // BiPAP Pressure
+ if ((this->family == 0) && (this->familyVersion == 4)) { // BiPAP Pressure
data0 = buffer[pos++];
data1 = buffer[pos++];
this->AddEvent(new PRS1IPAPEvent(t, data1));
@@ -3060,9 +3066,7 @@ bool PRS1DataChunk::ParseEventsF0V234(CPAPMode mode)
break;
case 0x03: // BIPAP Pressure
- if (FV3) {
- this->AddEvent(new PRS1CPAPEvent(t, buffer[pos++]));
- } else {
+ {
data0 = buffer[pos++];
data1 = buffer[pos++];
this->AddEvent(new PRS1IPAPEvent(t, data1));
@@ -3105,7 +3109,7 @@ bool PRS1DataChunk::ParseEventsF0V234(CPAPMode mode)
data1 = buffer[pos+1];
pos += 2;
- if (this->familyVersion >= 4) {
+ if (this->familyVersion == 4) {
// might not doublerize on older machines?
// data0 *= 2;
}
@@ -3122,7 +3126,7 @@ bool PRS1DataChunk::ParseEventsF0V234(CPAPMode mode)
case 0x0e: // Unknown
data0 = buffer[pos + 1] << 8 | buffer[pos];
- if (this->familyVersion >= 4) {
+ if (this->familyVersion == 4) {
// might not doublerize on older machines?
data0 *= 2;
}
@@ -3134,7 +3138,7 @@ bool PRS1DataChunk::ParseEventsF0V234(CPAPMode mode)
case 0x0f: // Cheyne Stokes Respiration
data0 = (buffer[pos + 1] << 8 | buffer[pos]);
- if (this->familyVersion >= 4) {
+ if (this->familyVersion == 4) {
// might not doublerize on older machines
data0 *= 2;
}
@@ -3151,7 +3155,7 @@ bool PRS1DataChunk::ParseEventsF0V234(CPAPMode mode)
case 0x10: // Large Leak
data0 = buffer[pos + 1] << 8 | buffer[pos];
- if (this->familyVersion >= 4) {
+ if (this->familyVersion == 4) {
// might not doublerize on older machines
data0 *= 2;
}
@@ -3166,7 +3170,7 @@ bool PRS1DataChunk::ParseEventsF0V234(CPAPMode mode)
this->AddEvent(new PRS1TotalLeakEvent(t, data0));
this->AddEvent(new PRS1SnoreEvent(t, data1));
- if ((this->family == 0) && (this->familyVersion >= 4)) {
+ if ((this->family == 0) && (this->familyVersion == 4)) {
// EPAP / Flex Pressure
data0 = buffer[pos++];
@@ -3187,7 +3191,7 @@ bool PRS1DataChunk::ParseEventsF0V234(CPAPMode mode)
// Could end here, but I've seen data sets valid data after!!!
break;
-
+ /*
case 0x14: // DreamStation Hypopnea
data0 = buffer[pos++];
this->AddEvent(new PRS1HypopneaEvent(t - data0, data0));
@@ -3197,7 +3201,7 @@ bool PRS1DataChunk::ParseEventsF0V234(CPAPMode mode)
data0 = buffer[pos++];
this->AddEvent(new PRS1HypopneaEvent(t - data0, data0));
break;
-
+ */
default:
// ERROR!!!
qWarning() << "Some new fandangled PRS1 code detected in" << this->sessionid << hex
From 675f6d436118ab159a6606df63e7440dab237d1c Mon Sep 17 00:00:00 2001
From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com>
Date: Tue, 13 Aug 2019 23:02:28 -0400
Subject: [PATCH 20/20] Resolve remaining F0V6 event 3 debug messages in sample
data.
---
oscar/SleepLib/loader_plugins/prs1_loader.cpp | 14 +++++++++-----
1 file changed, 9 insertions(+), 5 deletions(-)
diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp
index 53c9b9b1..8e9ce235 100644
--- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp
+++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp
@@ -3464,11 +3464,15 @@ bool PRS1DataChunk::ParseEventsF0V6(CPAPMode /*mode*/)
this->AddEvent(new PRS1IPAPEvent(t, data[pos+1]));
this->AddEvent(new PRS1EPAPEvent(t, data[pos])); // EPAP needs to be added second to calculate PS
break;
- case 0x03: // Pressure adjustment? (auto-CPAP)
- // This seems to correspond to the minimum auto-CPAP pressure setting, and
- // seems to stay fixed throughout the session.
- //this->AddEvent(new PRS1CPAPEvent(t, data[pos]));
- CHECK_VALUE(data[pos++], 4);
+ case 0x03: // Auto-CPAP starting pressure
+ // Most of the time this occurs, it's at the start and end of a session with
+ // the same pressure at both. Occasionally an additional event shows up in the
+ // middle of a session, and then the pressure at the end matches that.
+ // In these cases, the new pressure corresponds to the next night's starting
+ // pressure for auto-CPAP. It does not appear to have any effect on the current
+ // night's pressure, unless there's a substantial gap between sessions, in
+ // which case the next session may use the new starting pressure.
+ //CHECK_VALUE(data[pos], 40);
break;
case 0x04: // Pressure Pulse
duration = data[pos++]; // TODO: is this a duration?