mirror of
https://gitlab.com/pholy/OSCAR-code.git
synced 2025-04-04 02:00:43 +00:00
1210 lines
58 KiB
C++
1210 lines
58 KiB
C++
/* PRS1 Parsing for S/T and AVAPS ventilators (Family 3)
|
|
*
|
|
* Copyright (c) 2019-2024 The OSCAR Team
|
|
* Portions copyright (c) 2011-2018 Mark Watkins
|
|
*
|
|
* This file is subject to the terms and conditions of the GNU General Public
|
|
* License. See the file COPYING in the main directory of the source code
|
|
* for more details. */
|
|
|
|
#include "prs1_parser.h"
|
|
#include "prs1_loader.h"
|
|
|
|
// The qt5.15 obsolescence of hex requires this change.
|
|
// this solution to QT's obsolescence is only used in debug statements
|
|
#if QT_VERSION >= QT_VERSION_CHECK(5,15,0)
|
|
#define QTHEX Qt::hex
|
|
#define QTDEC Qt::dec
|
|
#else
|
|
#define QTHEX hex
|
|
#define QTDEC dec
|
|
#endif
|
|
|
|
static QString hex(int i)
|
|
{
|
|
return QString("0x") + QString::number(i, 16).toUpper();
|
|
}
|
|
|
|
//********************************************************************************************
|
|
// MARK: -
|
|
// MARK: 50 and 60 Series
|
|
|
|
// borrowed largely from ParseSummaryF5V012
|
|
bool PRS1DataChunk::ParseSummaryF3V03(void)
|
|
{
|
|
if (this->family != 3 || (this->familyVersion > 3)) {
|
|
qWarning() << "ParseSummaryF3V03 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();
|
|
QVector<int> minimum_sizes;
|
|
if (this->familyVersion == 0) {
|
|
minimum_sizes = { 0x19, 3, 3, 9 };
|
|
} else {
|
|
minimum_sizes = { 0x1b, 3, 5, 9 };
|
|
}
|
|
// 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 F3V6.
|
|
size = 0;
|
|
if (code < minimum_sizes.length()) {
|
|
// 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;
|
|
}
|
|
|
|
// NOTE: F3V3 doesn't use 16-bit time deltas in its summary events, it uses absolute timestamps!
|
|
// It's possible that these are 24-bit, but haven't yet seen a timestamp that large.
|
|
|
|
const unsigned char * ondata = data;
|
|
switch (code) {
|
|
case 0: // Equipment On
|
|
CHECK_VALUE(pos, 1); // Always first
|
|
if (this->familyVersion == 0) {
|
|
// F3V0 inserts an extra byte in front
|
|
CHECK_VALUE(data[pos], 1);
|
|
ondata = ondata + 1;
|
|
}
|
|
CHECK_VALUE(ondata[pos], 0);
|
|
/*
|
|
CHECK_VALUE(data[pos] & 0xF0, 0); // TODO: what are these?
|
|
if ((data[pos] & 0x0F) != 1) { // This is the most frequent value.
|
|
//CHECK_VALUES(data[pos] & 0x0F, 3, 5); // TODO: what are these? 0 seems to be related to errors.
|
|
}
|
|
*/
|
|
// F3V3 doesn't have a separate settings record like F3V6 does, the settings just follow the EquipmentOn data.
|
|
ok = this->ParseSettingsF3V03(ondata, size);
|
|
break;
|
|
case 2: // Mask On
|
|
tt = data[pos] | (data[pos+1] << 8);
|
|
this->AddEvent(new PRS1ParsedSliceEvent(tt, MaskOn));
|
|
CHECK_VALUE(data[pos+2], 0); // may be high byte of timestamp
|
|
if (size > 3) { // F3V3 records the humidifier setting at each mask-on, F3V0 only records the initial setting.
|
|
this->ParseHumidifierSettingF3V3(data[pos+3], data[pos+4]);
|
|
}
|
|
break;
|
|
case 3: // Mask Off
|
|
tt = data[pos] | (data[pos+1] << 8);
|
|
this->AddEvent(new PRS1ParsedSliceEvent(tt, MaskOff));
|
|
// F3V3 doesn't have a separate stats record like F3V6 does, the stats just follow the MaskOff data.
|
|
CHECK_VALUE(data[pos+0x2], 0); // may be high byte of timestamp
|
|
//CHECK_VALUES(data[pos+0x3], 0, 1); // OA count, probably 16-bit
|
|
CHECK_VALUE(data[pos+0x4], 0);
|
|
//CHECK_VALUE(data[pos+0x5], 0); // CA count, probably 16-bit
|
|
CHECK_VALUE(data[pos+0x6], 0);
|
|
//CHECK_VALUE(data[pos+0x7], 0); // H count, probably 16-bit
|
|
CHECK_VALUE(data[pos+0x8], 0);
|
|
break;
|
|
case 1: // Equipment Off
|
|
tt = data[pos] | (data[pos+1] << 8);
|
|
this->AddEvent(new PRS1ParsedSliceEvent(tt, EquipmentOff));
|
|
CHECK_VALUE(data[pos+2], 0); // may be high byte of timestamp
|
|
break;
|
|
/*
|
|
case 5: // Clock adjustment? See ParseSummaryF0V4.
|
|
CHECK_VALUE(pos, 1); // Always first
|
|
CHECK_VALUE(chunk_size, 5); // and the only record in the session.
|
|
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: // ???
|
|
tt += data[pos] | (data[pos+1] << 8); // This adds to the total duration (otherwise it won't match report)
|
|
break;
|
|
case 8: // ???
|
|
tt += data[pos] | (data[pos+1] << 8); // Since 7 and 8 seem to occur near each other, let's assume 8 also has a timestamp
|
|
CHECK_VALUE(pos, 1);
|
|
CHECK_VALUE(chunk_size, 3);
|
|
CHECK_VALUE(data[pos], 0); // and alert us if the timestamp is nonzero
|
|
CHECK_VALUE(data[pos+1], 0);
|
|
break;
|
|
case 9: // Humidifier setting change
|
|
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;
|
|
*/
|
|
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;
|
|
}
|
|
|
|
|
|
// Support for 1061, 1061T, 1160P
|
|
// logic largely borrowed from ParseSettingsF3V6, values based on sample data
|
|
bool PRS1DataChunk::ParseSettingsF3V03(const unsigned char* data, int /*size*/)
|
|
{
|
|
PRS1Mode cpapmode = PRS1_MODE_UNKNOWN;
|
|
FlexMode flexmode = FLEX_Unknown;
|
|
|
|
// data[0] is the event code
|
|
// data[1] is checked in the calling function
|
|
|
|
switch (data[2]) {
|
|
case 0: cpapmode = PRS1_MODE_CPAP; break; // "CPAP" mode
|
|
case 1: cpapmode = PRS1_MODE_S; break; // "S" mode
|
|
case 2: cpapmode = PRS1_MODE_ST; break; // "S/T" mode; pressure seems variable?
|
|
case 4: cpapmode = PRS1_MODE_PC; break; // "PC" mode? Usually "PC - AVAPS", see setting 1 below
|
|
default:
|
|
UNEXPECTED_VALUE(data[2], "known device mode");
|
|
break;
|
|
}
|
|
|
|
switch (data[3]) {
|
|
case 0: // 0 = None
|
|
switch (cpapmode) {
|
|
case PRS1_MODE_CPAP: flexmode = FLEX_None; break;
|
|
case PRS1_MODE_S: flexmode = FLEX_RiseTime; break; // reports say "None" but then list a rise time setting
|
|
case PRS1_MODE_ST: flexmode = FLEX_RiseTime; break; // reports say "None" but then list a rise time setting
|
|
default:
|
|
UNEXPECTED_VALUE(cpapmode, "CPAP, S, or S/T");
|
|
break;
|
|
}
|
|
break;
|
|
case 1: // 1 = Bi-Flex, only seen with "S - Bi-Flex"
|
|
flexmode = FLEX_BiFlex;
|
|
CHECK_VALUE(cpapmode, PRS1_MODE_S);
|
|
break;
|
|
case 2: // 2 = AVAPS: usually "PC - AVAPS", sometimes "S/T - AVAPS"
|
|
switch (cpapmode) {
|
|
case PRS1_MODE_ST: cpapmode = PRS1_MODE_ST_AVAPS; break;
|
|
case PRS1_MODE_PC: cpapmode = PRS1_MODE_PC_AVAPS; break;
|
|
default:
|
|
UNEXPECTED_VALUE(cpapmode, "S/T or PC");
|
|
break;
|
|
}
|
|
flexmode = FLEX_RiseTime; // reports say "AVAPS" but then list a rise time setting
|
|
break;
|
|
default:
|
|
UNEXPECTED_VALUE(data[3], "known flex mode");
|
|
break;
|
|
}
|
|
if (this->familyVersion == 0) {
|
|
// Confirm F3V0 setting encoding
|
|
switch (cpapmode) {
|
|
case PRS1_MODE_CPAP: break; // CPAP has been confirmed
|
|
case PRS1_MODE_S: break; // S bi-flex and rise time have been confirmed
|
|
case PRS1_MODE_ST:
|
|
CHECK_VALUE(flexmode, FLEX_RiseTime); // only rise time has been confirmed
|
|
break;
|
|
default:
|
|
UNEXPECTED_VALUE(cpapmode, "tested modes");
|
|
}
|
|
}
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_CPAP_MODE, (int) cpapmode));
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_MODE, (int) flexmode));
|
|
|
|
int epap = data[4] + (data[5] << 8); // 0x82 = EPAP 13 cmH2O; 0x78 = EPAP 12 cmH2O; 0x50 = EPAP 8 cmH2O
|
|
int min_ipap = data[6] + (data[7] << 8); // 0xA0 = IPAP 16 cmH2O; 0xBE = 19 cmH2O min IPAP (in AVAPS); 0x78 = IPAP 12 cmH2O
|
|
int max_ipap = data[8] + (data[9] << 8); // 0xAA = ???; 0x12C = 30 cmH2O max IPAP (in AVAPS); 0x78 = ???
|
|
switch (cpapmode) {
|
|
case PRS1_MODE_CPAP:
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PRESSURE, epap));
|
|
CHECK_VALUE(min_ipap, 0);
|
|
CHECK_VALUE(max_ipap, 0);
|
|
break;
|
|
case PRS1_MODE_S:
|
|
case PRS1_MODE_ST:
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_EPAP, epap));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP, min_ipap));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS, min_ipap - epap));
|
|
//CHECK_VALUES(max_ipap, 170, 300);
|
|
break;
|
|
case PRS1_MODE_ST_AVAPS:
|
|
case PRS1_MODE_PC_AVAPS:
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_EPAP, epap));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP_MIN, min_ipap));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP_MAX, max_ipap));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS_MIN, min_ipap - epap));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS_MAX, max_ipap - epap));
|
|
break;
|
|
default:
|
|
UNEXPECTED_VALUE(cpapmode, "expected mode");
|
|
break;
|
|
}
|
|
|
|
if (cpapmode == PRS1_MODE_CPAP) {
|
|
CHECK_VALUE(flexmode, FLEX_None);
|
|
CHECK_VALUE(data[0xa], 0);
|
|
CHECK_VALUE(data[0xb], 0);
|
|
CHECK_VALUE(data[0xc], 0);
|
|
CHECK_VALUE(data[0xd], 0);
|
|
}
|
|
if (flexmode == FLEX_RiseTime) {
|
|
int rise_time = data[0xa]; // 1 = Rise Time Setting 1, 2 = Rise Time Setting 2, 3 = Rise Time Setting 3
|
|
if (rise_time < 1 || rise_time > 6) UNEXPECTED_VALUE(rise_time, "1-6"); // TODO: what is 0?
|
|
CHECK_VALUES(data[0xb], 0, 1); // 1 = Rise Time Lock (in "None" and AVAPS flex mode)
|
|
CHECK_VALUE(data[0xc], 0);
|
|
CHECK_VALUES(data[0xd], 0, 1); // TODO: What is this? It's usually 0.
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_RISE_TIME, rise_time));
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_RISE_TIME_LOCK, data[0xb] == 1));
|
|
} else if (flexmode == FLEX_BiFlex) {
|
|
CHECK_VALUES(data[0xa], 2, 3); // TODO: May also be Bi-Flex level? But how is this different from [0xc] below?
|
|
CHECK_VALUES(data[0xb], 0, 1); // TODO: What is this? It doesn't always match [0xd].
|
|
CHECK_VALUES(data[0xc], 2, 3);
|
|
CHECK_VALUE(data[0x0a], data[0xc]);
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_LEVEL, data[0xc])); // 3 = Bi-Flex 3, 2 = Bi-Flex 2 (in bi-flex mode)
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_LOCK, data[0xd] == 1));
|
|
}
|
|
|
|
if (flexmode == FLEX_None) CHECK_VALUE(data[0xe], 0);
|
|
if (cpapmode == PRS1_MODE_ST_AVAPS || cpapmode == PRS1_MODE_PC_AVAPS) {
|
|
if (data[0xe] < 24 || data[0xe] > 65) UNEXPECTED_VALUE(data[0xe], "24-65");
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_TIDAL_VOLUME, data[0xe] * 10.0));
|
|
} else if (flexmode == FLEX_BiFlex || flexmode == FLEX_RiseTime) {
|
|
CHECK_VALUE(data[0xe], 0x14); // 0x14 = ???
|
|
}
|
|
|
|
|
|
int breath_rate = data[0xf];
|
|
int timed_inspiration = data[0x10];
|
|
bool backup = false;
|
|
switch (cpapmode) {
|
|
case PRS1_MODE_CPAP:
|
|
CHECK_VALUE(breath_rate, 0);
|
|
CHECK_VALUE(timed_inspiration, 0);
|
|
break;
|
|
case PRS1_MODE_S:
|
|
if (this->familyVersion == 0) {
|
|
CHECK_VALUE(breath_rate, 10);
|
|
CHECK_VALUE(timed_inspiration, 10);
|
|
} else {
|
|
CHECK_VALUE(breath_rate, 0);
|
|
CHECK_VALUE(timed_inspiration, 0);
|
|
}
|
|
break;
|
|
case PRS1_MODE_PC_AVAPS:
|
|
CHECK_VALUE(breath_rate, 0); // only ever seen 0 on reports so far
|
|
CHECK_VALUE(timed_inspiration, 30);
|
|
backup = true;
|
|
break;
|
|
case PRS1_MODE_ST_AVAPS:
|
|
if (breath_rate) { // can be 0 on reports
|
|
CHECK_VALUES(breath_rate, 9, 10);
|
|
}
|
|
if (timed_inspiration < 10 || timed_inspiration > 30) UNEXPECTED_VALUE(timed_inspiration, "10-30");
|
|
backup = true;
|
|
break;
|
|
case PRS1_MODE_ST:
|
|
if (breath_rate < 8 || breath_rate > 18) UNEXPECTED_VALUE(breath_rate, "8-18"); // can this be 0?
|
|
if (timed_inspiration < 10 || timed_inspiration > 20) UNEXPECTED_VALUE(timed_inspiration, "10-20"); // 16 = 1.6s
|
|
backup = true;
|
|
break;
|
|
default:
|
|
UNEXPECTED_VALUE(cpapmode, "CPAP, S, S/T, or PC");
|
|
break;
|
|
}
|
|
if (backup) {
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_BACKUP_BREATH_MODE, PRS1Backup_Fixed));
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_BACKUP_BREATH_RATE, breath_rate));
|
|
this->AddEvent(new PRS1ScaledSettingEvent(PRS1_SETTING_BACKUP_TIMED_INSPIRATION, timed_inspiration, 0.1));
|
|
}
|
|
|
|
CHECK_VALUE(data[0x11], 0);
|
|
|
|
//CHECK_VALUE(data[0x12], 0x1E, 0x0F); // 0x1E = ramp time 30 minutes, 0x0F = ramp time 15 minutes
|
|
//CHECK_VALUE(data[0x13], 0x3C, 0x5A, 0x28); // 0x3C = ramp pressure 6 cmH2O, 0x28 = ramp pressure 4 cmH2O, 0x5A = ramp pressure 9 cmH2O
|
|
CHECK_VALUE(data[0x14], 0); // the ramp pressure is probably a 16-bit value like the ones above are
|
|
int ramp_time = data[0x12];
|
|
int ramp_pressure = data[0x13];
|
|
if (ramp_time > 0) {
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_RAMP_TIME, ramp_time));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_RAMP_PRESSURE, ramp_pressure));
|
|
}
|
|
|
|
int pos;
|
|
if (this->familyVersion == 0) {
|
|
ParseHumidifierSetting50Series(data[0x15], true);
|
|
pos = 0x16;
|
|
} else {
|
|
this->ParseHumidifierSettingF3V3(data[0x15], data[0x16], true);
|
|
|
|
// Menu options?
|
|
CHECK_VALUES(data[0x17], 0x10, 0x90); // 0x10 = resist 1; 0x90 = resist 1, resist lock
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_MASK_RESIST_LOCK, (data[0x17] & 0x80) != 0));
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_MASK_RESIST_SETTING, 1)); // only value seen so far, CHECK_VALUES above will flag any others
|
|
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_TUBING_LOCK, (data[0x18] & 0x80) != 0));
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HOSE_DIAMETER, (data[0x18] & 0x7f)));
|
|
CHECK_VALUES(data[0x18] & 0x7f, 22, 15); // 0x16 = tubing 22; 0x0F = tubing 15, 0x96 = tubing 22 with lock
|
|
pos = 0x19;
|
|
}
|
|
|
|
// Alarms?
|
|
if (this->familyVersion == 0) {
|
|
if (data[pos] != 0) {
|
|
CHECK_VALUES(data[pos], 10, 30); // Apnea alarm on F3V0
|
|
}
|
|
CHECK_VALUES(data[pos+1], 0, 15); // Disconnect alarm on F3V0
|
|
CHECK_VALUES(data[pos+2], 0, 17); // Low MV alarm on F3V0
|
|
} else {
|
|
CHECK_VALUE(data[pos], 0);
|
|
CHECK_VALUE(data[pos+1], 0);
|
|
CHECK_VALUE(data[pos+2], 0);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
// XX XX = F3V3 Humidifier bytes
|
|
// 43 15 = heated tube temp 5, humidity 2
|
|
// 43 14 = heated tube temp 4, humidity 2
|
|
// 63 13 = heated tube temp 3, humidity 3
|
|
// 63 11 = heated tube temp 1, humidity 3
|
|
// 45 08 = system one 5
|
|
// 44 08 = system one 4
|
|
// 43 08 = system one 3
|
|
// 42 08 = system one 2
|
|
// 41 08 = system one 1
|
|
// 40 08 = system one 0 (off)
|
|
// 40 60 = system one 3, no data
|
|
// 40 20 = system one 3, no data
|
|
// 40 90 = heated tube, tube off, data=tube t=0,h=0
|
|
// 45 80 = classic 5
|
|
// 44 80 = classic 4
|
|
// 43 80 = classic 3
|
|
// 42 80 = classic 2
|
|
|
|
// 40 80 = classic 0 (off)
|
|
//
|
|
// 7 = humidity level without tube
|
|
// 8 = ? (never seen)
|
|
// 1 = ? (never seen)
|
|
// 6 = heated tube humidity level (when tube present, 0x40 all other times? including when tube is off?)
|
|
// 8 = ? (never seen)
|
|
// 7 = tube temp
|
|
// 8 = "System One" mode
|
|
// 1 = tube present
|
|
// 6 = no data, seems to show system one 3 in settings
|
|
// 8 = (classic mode; also seen when heated tube present but off, possibly ignored in that case)
|
|
//
|
|
// Note that, while containing similar fields as ParseHumidifierSetting60Series, the bit arrangement is different for F3V3!
|
|
|
|
void PRS1DataChunk::ParseHumidifierSettingF3V3(unsigned char humid1, unsigned char humid2, bool add_setting)
|
|
{
|
|
if (false) qWarning() << this->sessionid << "humid" << hex(humid1) << hex(humid2) << add_setting;
|
|
|
|
int humidlevel = humid1 & 7; // Ignored when heated tube is present: humidifier setting on tube disconnect is always reported as 3
|
|
if (humidlevel > 5) UNEXPECTED_VALUE(humidlevel, "<= 5");
|
|
CHECK_VALUE(humid1 & 0x40, 0x40); // seems always set, even without heated tube
|
|
CHECK_VALUE(humid1 & 0x98, 0); // never seen
|
|
int tubehumidlevel = (humid1 >> 5) & 7; // This mask is a best guess based on other masks.
|
|
if (tubehumidlevel > 5) UNEXPECTED_VALUE(tubehumidlevel, "<= 5");
|
|
CHECK_VALUE(tubehumidlevel & 4, 0); // never seen, but would clarify whether above mask is correct
|
|
|
|
int tubetemp = humid2 & 7;
|
|
if (tubetemp > 5) UNEXPECTED_VALUE(tubetemp, "<= 5");
|
|
|
|
if (humid2 & 0x60) {
|
|
CHECK_VALUES(humid2 & 0x60, 0x20, 0x60); // no humidifier data on chart
|
|
}
|
|
bool humidclassic = (humid2 & 0x80) != 0; // Set on classic mode reports; evidently ignored (sometimes set!) when tube is present
|
|
//bool no_tube? = (humid2 & 0x20) != 0; // Something tube related: whenever it is set, tube is never present (inverse is not true)
|
|
bool no_data = (humid2 & 0x60) != 0; // As described in chart, settings still show up
|
|
int tubepresent = (humid2 & 0x10) != 0;
|
|
bool humidsystemone = (humid2 & 0x08) != 0; // Set on "System One" humidification mode reports when tubepresent is false
|
|
|
|
if (humidsystemone + tubepresent + no_data == 0) CHECK_VALUE(humidclassic, true); // Always set when everything else is off in F0V4
|
|
if (humidsystemone + tubepresent /*+ no_data*/ > 1) UNEXPECTED_VALUE(humid2, "one bit set"); // Only one of these ever seems to be set at a time
|
|
//if (tubepresent && tubetemp == 0) CHECK_VALUE(tubehumidlevel, 0); // When the heated tube is off, tube humidity seems to be 0 in F0V4, but not F3V3
|
|
|
|
if (tubepresent) humidclassic = false; // Classic mode bit is evidently ignored when tube is present
|
|
|
|
//qWarning() << this->sessionid << (humidclassic ? "C" : ".") << (humid2 & 0x20 ? "?" : ".") << (tubepresent ? "T" : ".") << (no_data ? "X" : ".") << (humidsystemone ? "1" : ".");
|
|
/*
|
|
if (tubepresent) {
|
|
if (tubetemp) {
|
|
qWarning() << this->sessionid << "tube temp" << tubetemp << "tube humidity" << tubehumidlevel << (humidclassic ? "classic" : "systemone") << "humidity" << humidlevel;
|
|
} else {
|
|
qWarning() << this->sessionid << "heated tube off" << (humidclassic ? "classic" : "systemone") << "humidity" << humidlevel;
|
|
}
|
|
} else {
|
|
qWarning() << this->sessionid << (humidclassic ? "classic" : "systemone") << "humidity" << humidlevel;
|
|
}
|
|
*/
|
|
HumidMode humidmode = HUMID_Fixed;
|
|
if (tubepresent) {
|
|
humidmode = HUMID_HeatedTube;
|
|
} else {
|
|
if (humidsystemone + humidclassic > 1) UNEXPECTED_VALUE(humid2, "fixed or adaptive");
|
|
if (humidsystemone) humidmode = HUMID_Adaptive;
|
|
}
|
|
|
|
if (add_setting) {
|
|
bool humidifier_present = (no_data == 0);
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HUMID_STATUS, humidifier_present));
|
|
if (humidifier_present) {
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HUMID_MODE, humidmode));
|
|
if (humidmode == HUMID_HeatedTube) {
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HEATED_TUBE_TEMP, tubetemp));
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HUMID_LEVEL, tubehumidlevel));
|
|
} else {
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HUMID_LEVEL, humidlevel));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for previously unseen data that we expect to be normal:
|
|
if (humidclassic && humidlevel == 1) UNEXPECTED_VALUE(humidlevel, "!= 1");
|
|
if (tubepresent) {
|
|
if (tubetemp) CHECK_VALUES(tubehumidlevel, 2, 3);
|
|
if (tubetemp == 2) UNEXPECTED_VALUE(tubetemp, "!= 2");
|
|
}
|
|
}
|
|
|
|
|
|
const QVector<PRS1ParsedEventType> ParsedEventsF3V0 = {
|
|
PRS1IPAPAverageEvent::TYPE,
|
|
PRS1EPAPAverageEvent::TYPE,
|
|
PRS1TotalLeakEvent::TYPE,
|
|
PRS1TidalVolumeEvent::TYPE,
|
|
PRS1FlowRateEvent::TYPE,
|
|
PRS1PatientTriggeredBreathsEvent::TYPE,
|
|
PRS1RespiratoryRateEvent::TYPE,
|
|
PRS1MinuteVentilationEvent::TYPE,
|
|
// No LEAK, unlike F3V3
|
|
PRS1HypopneaCount::TYPE,
|
|
PRS1ClearAirwayCount::TYPE, // TODO
|
|
PRS1ObstructiveApneaCount::TYPE, // TODO
|
|
// No PP, FL, VS, RERA, PB, LL
|
|
// No TB
|
|
};
|
|
|
|
const QVector<PRS1ParsedEventType> ParsedEventsF3V3 = {
|
|
PRS1IPAPAverageEvent::TYPE,
|
|
PRS1EPAPAverageEvent::TYPE,
|
|
PRS1TotalLeakEvent::TYPE,
|
|
PRS1TidalVolumeEvent::TYPE,
|
|
PRS1FlowRateEvent::TYPE,
|
|
PRS1PatientTriggeredBreathsEvent::TYPE,
|
|
PRS1RespiratoryRateEvent::TYPE,
|
|
PRS1MinuteVentilationEvent::TYPE,
|
|
PRS1LeakEvent::TYPE,
|
|
PRS1HypopneaCount::TYPE,
|
|
PRS1ClearAirwayCount::TYPE,
|
|
PRS1ObstructiveApneaCount::TYPE,
|
|
// No PP, FL, VS, RERA, PB, LL
|
|
// No TB
|
|
};
|
|
|
|
// 1061, 1061T, 1160P series
|
|
bool PRS1DataChunk::ParseEventsF3V03(void)
|
|
{
|
|
// NOTE: Older ventilators (BiPAP S/T and AVAPS) devices don't use timestamped events like everything else.
|
|
// Instead, they use a fixed interval format like waveforms do (see PRS1_HTYPE_INTERVAL).
|
|
|
|
if (this->family != 3 || (this->familyVersion != 0 && this->familyVersion != 3)) {
|
|
qWarning() << "ParseEventsF3V03 called with family" << this->family << "familyVersion" << this->familyVersion;
|
|
return false;
|
|
}
|
|
if (this->fileVersion == 3) {
|
|
// NOTE: The original comment in the header for ParseF3EventsV3 said there was a 1060P with fileVersion 3.
|
|
// We've never seen that, so warn if it ever shows up.
|
|
qWarning() << "F3V3 event file with fileVersion 3?";
|
|
}
|
|
|
|
int t = 0;
|
|
static const int record_size = 0x10;
|
|
int size = this->m_data.size()/record_size;
|
|
CHECK_VALUE(this->m_data.size() % record_size, 0);
|
|
unsigned char * h = (unsigned char *)this->m_data.data();
|
|
|
|
static const qint64 block_duration = 120;
|
|
|
|
// Make sure the assumptions here agree with the header
|
|
CHECK_VALUE(this->htype, PRS1_HTYPE_INTERVAL);
|
|
CHECK_VALUE(this->interval_count, size);
|
|
CHECK_VALUE(this->interval_seconds, block_duration);
|
|
for (auto & channel : this->waveformInfo) {
|
|
CHECK_VALUE(channel.interleave, 1);
|
|
}
|
|
|
|
for (int x=0; x < size; x++) {
|
|
// Use the timestamp of the end of this interval, to be consistent with other parsers,
|
|
// but see note below regarding the duration of the final interval.
|
|
t += block_duration;
|
|
|
|
// TODO: The duration of the final interval isn't clearly defined in this format:
|
|
// there appears to be no way (apart from looking at the summary or waveform data)
|
|
// to determine the end time, which may truncate the last interval.
|
|
//
|
|
// TODO: What if there are multiple "final" intervals in a session due to multiple
|
|
// mask-on slices?
|
|
this->AddEvent(new PRS1IPAPAverageEvent(t, h[0] | (h[1] << 8)));
|
|
this->AddEvent(new PRS1EPAPAverageEvent(t, h[2] | (h[3] << 8)));
|
|
this->AddEvent(new PRS1TotalLeakEvent(t, h[4]));
|
|
this->AddEvent(new PRS1TidalVolumeEvent(t, h[5]));
|
|
this->AddEvent(new PRS1FlowRateEvent(t, h[6]));
|
|
this->AddEvent(new PRS1PatientTriggeredBreathsEvent(t, h[7]));
|
|
this->AddEvent(new PRS1RespiratoryRateEvent(t, h[8]));
|
|
if (this->familyVersion == 0) {
|
|
if (h[9] < 4 || h[9] > 65) UNEXPECTED_VALUE(h[9], "4-65");
|
|
} else {
|
|
if (h[9] < 4 || h[9] > 84) UNEXPECTED_VALUE(h[9], "5-84"); // not sure what this is.. encore doesn't graph it.
|
|
}
|
|
if (this->familyVersion == 0) {
|
|
// 1 shows as Apnea (AP) alarm
|
|
// 2 shows as a Patient Disconnect (PD) alarm
|
|
// 4 shows as a Low Minute Vent (LMV) alarm
|
|
// 8 shows as a Low Pressure (LP) alarm
|
|
// 10 shows as PD + LP in the same interval
|
|
if (h[10] & ~(0x01 | 0x02 | 0x04 | 0x08)) UNEXPECTED_VALUE(h[10], "known bits");
|
|
} else {
|
|
// This is probably the same as F3V0, but we don't yet have the sample data to confirm.
|
|
CHECK_VALUES(h[10], 0, 8); // 8 shows as a Low Pressure (LP) alarm
|
|
}
|
|
this->AddEvent(new PRS1MinuteVentilationEvent(t, h[11]));
|
|
if (this->familyVersion == 0) {
|
|
CHECK_VALUE(h[12], 0);
|
|
this->AddEvent(new PRS1HypopneaCount(t, h[13])); // count of hypopnea events
|
|
this->AddEvent(new PRS1ClearAirwayCount(t, h[14])); // count of clear airway events
|
|
this->AddEvent(new PRS1ObstructiveApneaCount(t, h[15])); // count of obstructive events
|
|
} else {
|
|
this->AddEvent(new PRS1HypopneaCount(t, h[12])); // count of hypopnea events
|
|
this->AddEvent(new PRS1ClearAirwayCount(t, h[13])); // count of clear airway events
|
|
this->AddEvent(new PRS1ObstructiveApneaCount(t, h[14])); // count of obstructive events
|
|
this->AddEvent(new PRS1LeakEvent(t, h[15]));
|
|
}
|
|
this->AddEvent(new PRS1IntervalBoundaryEvent(t));
|
|
|
|
h += record_size;
|
|
}
|
|
|
|
this->duration = t;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
//********************************************************************************************
|
|
// MARK: -
|
|
// MARK: DreamStation
|
|
|
|
// Originally based on ParseSummaryF5V3, with changes observed in ventilator sample data
|
|
//
|
|
// TODO: surely there will be a way to merge ParseSummary (FV3) loops and abstract the device-specific
|
|
// encodings into another function or class, but that's probably worth pursuing only after
|
|
// the details have been figured out.
|
|
bool PRS1DataChunk::ParseSummaryF3V6(void)
|
|
{
|
|
if (this->family != 3 || this->familyVersion != 6) {
|
|
qWarning() << "ParseSummaryF3V6 called with family" << this->family << "familyVersion" << this->familyVersion;
|
|
return false;
|
|
}
|
|
const unsigned char * data = (unsigned char *)this->m_data.constData();
|
|
int chunk_size = this->m_data.size();
|
|
static const int minimum_sizes[] = { 1, 0x25, 9, 7, 4, 2, 1, 2, 2, 1, 0x18, 2, 4 }; // F5V3 = { 1, 0x38, 4, 2, 4, 0x1e, 2, 4, 9 };
|
|
static const int ncodes = sizeof(minimum_sizes) / sizeof(int);
|
|
// NOTE: The sizes contained in hblock can vary, even within a single device, as can the length of hblock itself!
|
|
|
|
// TODO: hardcoding this is ugly, think of a better approach
|
|
if (chunk_size < minimum_sizes[0] + minimum_sizes[1] + minimum_sizes[2]) {
|
|
qWarning() << this->sessionid << "summary data too short:" << chunk_size;
|
|
return false;
|
|
}
|
|
// We've once seen a short summary with no mask-on/off: just equipment-on, settings, 2, equipment-off
|
|
// (And we've seen something similar in F5V3.)
|
|
if (chunk_size < 58) UNEXPECTED_VALUE(chunk_size, ">= 58");
|
|
|
|
bool ok = true;
|
|
int pos = 0;
|
|
int code, size;
|
|
int tt = 0;
|
|
while (ok && pos < chunk_size) {
|
|
code = data[pos++];
|
|
if (!this->hblock.contains(code)) {
|
|
qWarning() << this->sessionid << "missing hblock entry for" << code;
|
|
ok = false;
|
|
break;
|
|
}
|
|
size = this->hblock[code];
|
|
if (code < ncodes) {
|
|
// make sure the handlers below don't go past the end of the buffer
|
|
if (size < minimum_sizes[code]) {
|
|
UNEXPECTED_VALUE(size, minimum_sizes[code]);
|
|
qWarning() << this->sessionid << "slice" << code << "too small" << size << "<" << minimum_sizes[code];
|
|
if (code != 1) { // Settings are variable-length, so shorter settings slices aren't fatal.
|
|
ok = false;
|
|
break;
|
|
}
|
|
}
|
|
} // else if it's past ncodes, we'll log its information below (rather than handle it)
|
|
if (pos + size > chunk_size) {
|
|
qWarning() << this->sessionid << "slice" << code << "@" << pos << "longer than remaining chunk";
|
|
ok = false;
|
|
break;
|
|
}
|
|
|
|
switch (code) {
|
|
case 0: // Equipment On
|
|
CHECK_VALUE(pos, 1); // Always first?
|
|
//CHECK_VALUE(data[pos], 0x10); // usually 0x10 for 1030X, sometimes 0x40 or 0x80 are set in addition or instead
|
|
CHECK_VALUE(size, 1);
|
|
break;
|
|
case 1: // Settings
|
|
ok = this->ParseSettingsF3V6(data + pos, size);
|
|
break;
|
|
case 2: // seems equivalent to F5V3 #9, comes right after settings, usually 9 bytes, identical values
|
|
// TODO: This may be structurally similar to settings: a list of (code, length, value).
|
|
CHECK_VALUE(data[pos], 0);
|
|
CHECK_VALUE(data[pos+1], 1);
|
|
//CHECK_VALUE(data[pos+2], 0); // Apnea Alarm (0=off, 1=10, 2=20)
|
|
if (data[pos+2] != 0) {
|
|
CHECK_VALUES(data[pos+2], 1, 2);
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_APNEA_ALARM, data[pos+2] * 10));
|
|
}
|
|
CHECK_VALUE(data[pos+3], 1);
|
|
CHECK_VALUE(data[pos+4], 1);
|
|
CHECK_VALUES(data[pos+5], 0, 1); // 1 = Low Minute Ventilation Alarm set to 1
|
|
CHECK_VALUE(data[pos+6], 2);
|
|
CHECK_VALUE(data[pos+7], 1);
|
|
CHECK_VALUE(data[pos+8], 0); // 1 = patient disconnect alarm of 15 sec on F5V3, not sure where time is encoded
|
|
if (size > 9) {
|
|
CHECK_VALUE(data[pos+9], 3);
|
|
CHECK_VALUE(data[pos+10], 1);
|
|
CHECK_VALUE(data[pos+11], 0);
|
|
CHECK_VALUE(size, 12);
|
|
}
|
|
break;
|
|
case 4: // Mask On
|
|
tt += data[pos] | (data[pos+1] << 8);
|
|
this->AddEvent(new PRS1ParsedSliceEvent(tt, MaskOn));
|
|
this->ParseHumidifierSettingV3(data[pos+2], data[pos+3]);
|
|
break;
|
|
case 5: // Mask Off
|
|
tt += data[pos] | (data[pos+1] << 8);
|
|
this->AddEvent(new PRS1ParsedSliceEvent(tt, MaskOff));
|
|
break;
|
|
case 6: // Ventilator CPAP stats, presumably per mask-on slice
|
|
//CHECK_VALUE(data[pos], 0x3C); // Average CPAP
|
|
break;
|
|
case 7: // Ventilator EPAP stats, presumably per mask-on slice
|
|
//CHECK_VALUE(data[pos], 0x69); // Average EPAP
|
|
//CHECK_VALUE(data[pos+1], 0x80); // Average 90% EPAP
|
|
break;
|
|
case 8: // Ventilator IPAP stats, presumably per mask-on slice
|
|
//CHECK_VALUE(data[pos], 0x86); // Average IPAP
|
|
//CHECK_VALUE(data[pos+1], 0xA8); // Average 90% IPAP
|
|
break;
|
|
case 0xa: // Patient statistics, presumably per mask-on slice
|
|
//CHECK_VALUE(data[pos], 0x00); // 16-bit OA count
|
|
CHECK_VALUE(data[pos+1], 0x00);
|
|
//CHECK_VALUE(data[pos+2], 0x00); // 16-bit CA count
|
|
CHECK_VALUE(data[pos+3], 0x00);
|
|
//CHECK_VALUE(data[pos+4], 0x00); // 16-bit minutes in LL
|
|
CHECK_VALUE(data[pos+5], 0x00);
|
|
//CHECK_VALUE(data[pos+6], 0x0A); // 16-bit VS count
|
|
//CHECK_VALUE(data[pos+7], 0x00); // We've actually seen someone with more than 255 VS in a night!
|
|
//CHECK_VALUE(data[pos+8], 0x01); // 16-bit H count (partial)
|
|
CHECK_VALUE(data[pos+9], 0x00);
|
|
//CHECK_VALUE(data[pos+0xa], 0x00); // 16-bit H count (partial)
|
|
CHECK_VALUE(data[pos+0xb], 0x00);
|
|
//CHECK_VALUE(data[pos+0xc], 0x00); // 16-bit RE count
|
|
CHECK_VALUE(data[pos+0xd], 0x00);
|
|
//CHECK_VALUE(data[pos+0xe], 0x3e); // average total leak
|
|
//CHECK_VALUE(data[pos+0xf], 0x03); // 16-bit H count (partial)
|
|
CHECK_VALUE(data[pos+0x10], 0x00);
|
|
//CHECK_VALUE(data[pos+0x11], 0x11); // average breath rate
|
|
//CHECK_VALUE(data[pos+0x12], 0x41); // average TV / 10
|
|
//CHECK_VALUE(data[pos+0x13], 0x60); // average % PTB
|
|
//CHECK_VALUE(data[pos+0x14], 0x0b); // average minute vent
|
|
//CHECK_VALUE(data[pos+0x15], 0x1d); // average leak? (similar position to F5V3, similar delta to total leak)
|
|
//CHECK_VALUE(data[pos+0x16], 0x00); // 16-bit minutes in PB
|
|
CHECK_VALUE(data[pos+0x17], 0x00);
|
|
break;
|
|
case 3: // Equipment Off
|
|
tt += data[pos] | (data[pos+1] << 8);
|
|
this->AddEvent(new PRS1ParsedSliceEvent(tt, EquipmentOff));
|
|
//CHECK_VALUES(data[pos+2], 1, 4); // bitmask, have seen 1, 4, 6, 0x41
|
|
//CHECK_VALUE(data[pos+3], 0x17); // 0x16, etc.
|
|
//CHECK_VALUES(data[pos+4], 0, 1); // or 2
|
|
//CHECK_VALUE(data[pos+5], 0x15); // 0x16, etc.
|
|
//CHECK_VALUES(data[pos+6], 0, 1); // or 2
|
|
break;
|
|
case 0xc: // Humidier setting change
|
|
tt += data[pos] | (data[pos+1] << 8); // This adds to the total duration (otherwise it won't match report)
|
|
this->ParseHumidifierSettingV3(data[pos+2], data[pos+3]);
|
|
break;
|
|
default:
|
|
UNEXPECTED_VALUE(code, "known slice code");
|
|
break;
|
|
}
|
|
pos += size;
|
|
}
|
|
|
|
this->duration = tt;
|
|
|
|
return ok;
|
|
}
|
|
|
|
|
|
// Based initially on ParseSettingsF5V3. Many of the codes look the same, like always starting with 0, 0x35 looking like
|
|
// a humidifier setting, etc., but the contents are sometimes a bit different, such as mode values and pressure settings.
|
|
//
|
|
// new settings to find: ...
|
|
bool PRS1DataChunk::ParseSettingsF3V6(const unsigned char* data, int size)
|
|
{
|
|
static const QMap<int,int> expected_lengths = { {0x1e,3}, {0x35,2} };
|
|
bool ok = true;
|
|
|
|
PRS1Mode cpapmode = PRS1_MODE_UNKNOWN;
|
|
FlexMode flexmode = FLEX_Unknown;
|
|
|
|
// F5V3 and F3V6 use a gain of 0.125 rather than 0.1 to allow for a maximum value of 30 cmH2O
|
|
static const float GAIN = 0.125; // TODO: parameterize this somewhere better
|
|
|
|
int fixed_pressure = 0;
|
|
int fixed_epap = 0;
|
|
int fixed_ipap = 0;
|
|
int min_ipap = 0;
|
|
int max_ipap = 0;
|
|
int breath_rate;
|
|
int timed_inspiration;
|
|
|
|
// Parse the nested data structure which contains settings
|
|
int pos = 0;
|
|
do {
|
|
int code = data[pos++];
|
|
int len = data[pos++];
|
|
|
|
int expected_len = 1;
|
|
if (expected_lengths.contains(code)) {
|
|
expected_len = expected_lengths[code];
|
|
}
|
|
//CHECK_VALUE(len, expected_len);
|
|
if (len < expected_len) {
|
|
qWarning() << this->sessionid << "setting" << code << "too small" << len << "<" << expected_len;
|
|
ok = false;
|
|
break;
|
|
}
|
|
if (pos + len > size) {
|
|
qWarning() << this->sessionid << "setting" << code << "@" << pos << "longer than remaining slice";
|
|
ok = false;
|
|
break;
|
|
}
|
|
|
|
switch (code) {
|
|
case 0: // Device Mode
|
|
CHECK_VALUE(pos, 2); // always first?
|
|
CHECK_VALUE(len, 1);
|
|
switch (data[pos]) {
|
|
case 0: cpapmode = PRS1_MODE_CPAP; break; // "CPAP" mode
|
|
case 1: cpapmode = PRS1_MODE_S; break; // "S" mode
|
|
case 2: cpapmode = PRS1_MODE_ST; break; // "S/T" mode; pressure seems variable?
|
|
case 4: cpapmode = PRS1_MODE_PC; break; // "PC" mode? Usually "PC - AVAPS", see setting 1 below
|
|
default:
|
|
UNEXPECTED_VALUE(data[pos], "known device mode");
|
|
break;
|
|
}
|
|
break;
|
|
case 1: // Flex Mode
|
|
CHECK_VALUE(len, 1);
|
|
switch (data[pos]) {
|
|
case 0: // 0 = None
|
|
switch (cpapmode) {
|
|
case PRS1_MODE_CPAP: flexmode = FLEX_None; break;
|
|
case PRS1_MODE_S: flexmode = FLEX_RiseTime; break; // reports say "None" but then list a rise time setting
|
|
case PRS1_MODE_ST: flexmode = FLEX_RiseTime; break; // reports say "None" but then list a rise time setting
|
|
default:
|
|
UNEXPECTED_VALUE(cpapmode, "CPAP, S, or S/T");
|
|
break;
|
|
}
|
|
break;
|
|
case 1: // 1 = Bi-Flex, only seen with "S - Bi-Flex"
|
|
flexmode = FLEX_BiFlex;
|
|
CHECK_VALUE(cpapmode, PRS1_MODE_S);
|
|
break;
|
|
case 2: // 2 = AVAPS: usually "PC - AVAPS", sometimes "S/T - AVAPS"
|
|
switch (cpapmode) {
|
|
case PRS1_MODE_ST: cpapmode = PRS1_MODE_ST_AVAPS; break;
|
|
case PRS1_MODE_PC: cpapmode = PRS1_MODE_PC_AVAPS; break;
|
|
default:
|
|
UNEXPECTED_VALUE(cpapmode, "S/T or PC");
|
|
break;
|
|
}
|
|
flexmode = FLEX_RiseTime; // reports say "AVAPS" but then list a rise time setting
|
|
break;
|
|
default:
|
|
UNEXPECTED_VALUE(data[pos], "known flex mode");
|
|
break;
|
|
}
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_CPAP_MODE, (int) cpapmode));
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_MODE, (int) flexmode));
|
|
break;
|
|
case 2: // ??? Maybe AAM?
|
|
CHECK_VALUE(len, 1);
|
|
CHECK_VALUE(data[pos], 0);
|
|
break;
|
|
case 3: // CPAP Pressure
|
|
CHECK_VALUE(len, 1);
|
|
CHECK_VALUE(cpapmode, PRS1_MODE_CPAP);
|
|
fixed_pressure = data[pos];
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PRESSURE, fixed_pressure, GAIN));
|
|
break;
|
|
case 4: // EPAP Pressure
|
|
CHECK_VALUE(len, 1);
|
|
if (cpapmode == PRS1_MODE_CPAP) UNEXPECTED_VALUE(cpapmode, "!cpap");
|
|
// pressures seem variable on practice, maybe due to ramp or leaks?
|
|
fixed_epap = data[pos];
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_EPAP, fixed_epap, GAIN));
|
|
break;
|
|
case 7: // IPAP Pressure
|
|
CHECK_VALUE(len, 1);
|
|
CHECK_VALUES(cpapmode, PRS1_MODE_S, PRS1_MODE_ST);
|
|
// pressures seem variable on practice, maybe due to ramp or leaks?
|
|
fixed_ipap = data[pos];
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP, fixed_ipap, GAIN));
|
|
// TODO: We need to revisit whether PS should be shown as a setting.
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS, fixed_ipap - fixed_epap, GAIN));
|
|
if (fixed_epap == 0) UNEXPECTED_VALUE(fixed_epap, ">0");
|
|
break;
|
|
case 8: // Min IPAP
|
|
CHECK_VALUE(len, 1);
|
|
CHECK_VALUE(fixed_ipap, 0);
|
|
CHECK_VALUES(cpapmode, PRS1_MODE_ST_AVAPS, PRS1_MODE_PC_AVAPS);
|
|
min_ipap = data[pos];
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP_MIN, min_ipap, GAIN));
|
|
// TODO: We need to revisit whether PS should be shown as a setting.
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS_MIN, min_ipap - fixed_epap, GAIN));
|
|
if (fixed_epap == 0) UNEXPECTED_VALUE(fixed_epap, ">0");
|
|
break;
|
|
case 9: // Max IPAP
|
|
CHECK_VALUE(len, 1);
|
|
CHECK_VALUE(fixed_ipap, 0);
|
|
CHECK_VALUES(cpapmode, PRS1_MODE_ST_AVAPS, PRS1_MODE_PC_AVAPS);
|
|
if (min_ipap == 0) UNEXPECTED_VALUE(min_ipap, ">0");
|
|
max_ipap = data[pos];
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP_MAX, max_ipap, GAIN));
|
|
// TODO: We need to revisit whether PS should be shown as a setting.
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS_MAX, max_ipap - fixed_epap, GAIN));
|
|
if (fixed_epap == 0) UNEXPECTED_VALUE(fixed_epap, ">0");
|
|
break;
|
|
case 0x19: // Tidal Volume (AVAPS)
|
|
CHECK_VALUE(len, 1);
|
|
CHECK_VALUES(cpapmode, PRS1_MODE_ST_AVAPS, PRS1_MODE_PC_AVAPS);
|
|
//CHECK_VALUE(data[pos], 47); // gain 10.0
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_TIDAL_VOLUME, data[pos] * 10.0));
|
|
break;
|
|
case 0x1e: // (Backup) Breath Rate (S/T and PC)
|
|
CHECK_VALUE(len, 3);
|
|
if (cpapmode == PRS1_MODE_CPAP || cpapmode == PRS1_MODE_S) UNEXPECTED_VALUE(cpapmode, "S/T or PC");
|
|
switch (data[pos]) {
|
|
case 0: // Breath Rate Off
|
|
// TODO: Is this mode essentially bilevel? The pressure graphs are confusing.
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_BACKUP_BREATH_MODE, PRS1Backup_Off));
|
|
CHECK_VALUE(data[pos+1], 0);
|
|
CHECK_VALUE(data[pos+2], 0);
|
|
break;
|
|
//case 1: // Breath Rate Auto in F5V3 setting 0x14
|
|
case 2: // Breath Rate (fixed BPM)
|
|
breath_rate = data[pos+1];
|
|
timed_inspiration = data[pos+2];
|
|
if (breath_rate < 9 || breath_rate > 15) UNEXPECTED_VALUE(breath_rate, "9-15");
|
|
if (timed_inspiration < 8 || timed_inspiration > 20) UNEXPECTED_VALUE(timed_inspiration, "8-20");
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_BACKUP_BREATH_MODE, PRS1Backup_Fixed));
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_BACKUP_BREATH_RATE, breath_rate));
|
|
this->AddEvent(new PRS1ScaledSettingEvent(PRS1_SETTING_BACKUP_TIMED_INSPIRATION, timed_inspiration, 0.1));
|
|
break;
|
|
default:
|
|
CHECK_VALUES(data[pos], 0, 2); // 0 = Breath Rate off (S), 2 = fixed BPM (1 = auto on F5V3 setting 0x14, haven't seen it on F3V6 yet)
|
|
break;
|
|
}
|
|
break;
|
|
//0x2b: Ramp type sounds like it's linear for F3V6 unless AAM is enabled, so no setting may be needed.
|
|
case 0x2c: // Ramp Time
|
|
CHECK_VALUE(len, 1);
|
|
if (data[pos] != 0) { // 0 == ramp off, and ramp pressure setting doesn't appear
|
|
if (data[pos] < 5 || data[pos] > 45) UNEXPECTED_VALUE(data[pos], "5-45");
|
|
}
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_RAMP_TIME, data[pos]));
|
|
break;
|
|
case 0x2d: // Ramp Pressure (with ASV/ventilator pressure encoding), only present when ramp is on
|
|
CHECK_VALUE(len, 1);
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_RAMP_PRESSURE, data[pos], GAIN));
|
|
break;
|
|
case 0x2e: // Bi-Flex level or Rise Time
|
|
// On F5V3 the first byte could specify Bi-Flex or Rise Time, and second byte contained the value.
|
|
// On F3V6 there's only one byte, which seems to correspond to Rise Time on the reports with flex
|
|
// mode None or AVAPS and to Bi-Flex Setting (level) in Bi-Flex mode.
|
|
CHECK_VALUE(len, 1);
|
|
if (flexmode == FLEX_BiFlex) {
|
|
// Bi-Flex level
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_LEVEL, data[pos]));
|
|
} else if (flexmode == FLEX_RiseTime) {
|
|
// Rise time
|
|
if (data[pos] < 1 || data[pos] > 6) UNEXPECTED_VALUE(data[pos], "1-6"); // 1-6 have been seen
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_RISE_TIME, data[pos]));
|
|
} else {
|
|
UNEXPECTED_VALUE(flexmode, "BiFlex or RiseTime");
|
|
}
|
|
// Timed inspiration specified in the backup breath rate.
|
|
break;
|
|
case 0x2f: // Flex / Rise Time lock
|
|
CHECK_VALUE(len, 1);
|
|
if (flexmode == FLEX_BiFlex) {
|
|
CHECK_VALUE(cpapmode, PRS1_MODE_S);
|
|
CHECK_VALUES(data[pos], 0, 0x80); // Bi-Flex Lock
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_LOCK, data[pos] != 0));
|
|
} else if (flexmode == FLEX_RiseTime) {
|
|
CHECK_VALUES(data[pos], 0, 0x80); // Rise Time Lock
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_RISE_TIME_LOCK, data[pos] != 0));
|
|
} else {
|
|
UNEXPECTED_VALUE(flexmode, "BiFlex or RiseTime");
|
|
}
|
|
break;
|
|
case 0x35: // Humidifier setting
|
|
CHECK_VALUE(len, 2);
|
|
this->ParseHumidifierSettingV3(data[pos], data[pos+1], true);
|
|
break;
|
|
case 0x36: // Mask Resistance Lock
|
|
CHECK_VALUE(len, 1);
|
|
CHECK_VALUES(data[pos], 0, 0x80);
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_MASK_RESIST_LOCK, data[pos] != 0));
|
|
break;
|
|
case 0x38: // Mask Resistance
|
|
CHECK_VALUE(len, 1);
|
|
if (data[pos] != 0) { // 0 == mask resistance off
|
|
if (data[pos] < 1 || data[pos] > 5) UNEXPECTED_VALUE(data[pos], "1-5");
|
|
}
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_MASK_RESIST_SETTING, data[pos]));
|
|
break;
|
|
case 0x39: // Tubing Type Lock
|
|
CHECK_VALUE(len, 1);
|
|
CHECK_VALUES(data[pos], 0, 0x80);
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_TUBING_LOCK, data[pos] != 0));
|
|
break;
|
|
case 0x3b: // Tubing Type
|
|
CHECK_VALUE(len, 1);
|
|
if (data[pos] != 0) {
|
|
CHECK_VALUES(data[pos], 2, 1); // 15HT = 2, 15 = 1, 22 = 0, though report only says "15" for 15HT
|
|
}
|
|
this->ParseTubingTypeV3(data[pos]);
|
|
break;
|
|
case 0x3c: // View Optional Screens
|
|
CHECK_VALUE(len, 1);
|
|
CHECK_VALUES(data[pos], 0, 0x80);
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_SHOW_AHI, data[pos] != 0));
|
|
break;
|
|
default:
|
|
UNEXPECTED_VALUE(code, "known setting");
|
|
qDebug() << "Unknown setting:" << QTHEX << code << "in" << this->sessionid << "at" << pos;
|
|
this->AddEvent(new PRS1UnknownDataEvent(QByteArray((const char*) data, size), pos, len));
|
|
break;
|
|
}
|
|
|
|
pos += len;
|
|
} while (ok && pos + 2 <= size);
|
|
|
|
return ok;
|
|
}
|
|
|
|
|
|
const QVector<PRS1ParsedEventType> ParsedEventsF3V6 = {
|
|
PRS1TimedBreathEvent::TYPE,
|
|
PRS1IPAPAverageEvent::TYPE,
|
|
PRS1EPAPAverageEvent::TYPE,
|
|
PRS1TotalLeakEvent::TYPE,
|
|
PRS1RespiratoryRateEvent::TYPE,
|
|
PRS1PatientTriggeredBreathsEvent::TYPE,
|
|
PRS1MinuteVentilationEvent::TYPE,
|
|
PRS1TidalVolumeEvent::TYPE,
|
|
PRS1Test2Event::TYPE,
|
|
PRS1Test1Event::TYPE,
|
|
PRS1SnoreEvent::TYPE, // No individual VS, only snore count
|
|
PRS1LeakEvent::TYPE,
|
|
PRS1PressurePulseEvent::TYPE,
|
|
PRS1ObstructiveApneaEvent::TYPE,
|
|
PRS1ClearAirwayEvent::TYPE,
|
|
PRS1HypopneaEvent::TYPE,
|
|
PRS1PeriodicBreathingEvent::TYPE,
|
|
PRS1RERAEvent::TYPE,
|
|
PRS1LargeLeakEvent::TYPE,
|
|
PRS1ApneaAlarmEvent::TYPE,
|
|
// No FL?
|
|
};
|
|
|
|
// 1030X, 11030X series
|
|
// based on ParseEventsF5V3, updated for F3V6
|
|
bool PRS1DataChunk::ParseEventsF3V6(void)
|
|
{
|
|
if (this->family != 3 || this->familyVersion != 6) {
|
|
qWarning() << "ParseEventsF3V6 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, 0xe, 3, 3, 3, 4, 5, 3, 5, 3, 3, 2, 2, 2, 2 };
|
|
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;
|
|
}
|
|
|
|
// F3V6 uses a gain of 0.125 rather than 0.1 to allow for a maximum value of 30 cmH2O
|
|
static const float GAIN = 0.125; // TODO: this should be parameterized somewhere more logical
|
|
bool ok = true;
|
|
int pos = 0, startpos;
|
|
int code, size;
|
|
int t = 0;
|
|
int elapsed, duration;
|
|
do {
|
|
code = data[pos++];
|
|
if (!this->hblock.contains(code)) {
|
|
qWarning() << this->sessionid << "missing hblock entry for event" << code;
|
|
ok = false;
|
|
break;
|
|
}
|
|
size = this->hblock[code];
|
|
if (code < ncodes) {
|
|
// make sure the handlers below don't go past the end of the buffer
|
|
if (size < minimum_sizes[code]) {
|
|
qWarning() << this->sessionid << "event" << code << "too small" << size << "<" << minimum_sizes[code];
|
|
ok = false;
|
|
break;
|
|
}
|
|
} // else if it's past ncodes, we'll log its information below (rather than handle it)
|
|
if (pos + size > chunk_size) {
|
|
qWarning() << this->sessionid << "event" << code << "@" << pos << "longer than remaining chunk";
|
|
ok = false;
|
|
break;
|
|
}
|
|
startpos = pos;
|
|
t += data[pos] | (data[pos+1] << 8);
|
|
pos += 2;
|
|
|
|
switch (code) {
|
|
// case 0x00?
|
|
case 1: // Timed Breath
|
|
// TB events have a duration in 0.1s, based on the review of pressure waveforms.
|
|
// TODO: Ideally the starting time here would be adjusted here, but PRS1ParsedEvents
|
|
// currently assume integer seconds rather than ms, so that's done at import.
|
|
duration = data[pos];
|
|
// TODO: make sure F3 import logic matches F5 in adjusting TB start time
|
|
this->AddEvent(new PRS1TimedBreathEvent(t, duration));
|
|
break;
|
|
case 2: // Statistics
|
|
// These appear every 2 minutes, so presumably summarize the preceding period.
|
|
//data[pos+0]; // TODO: 0 = ???
|
|
this->AddEvent(new PRS1IPAPAverageEvent(t, data[pos+2], GAIN)); // 02=IPAP
|
|
this->AddEvent(new PRS1EPAPAverageEvent(t, data[pos+1], GAIN)); // 01=EPAP, needs to be added second to calculate PS
|
|
this->AddEvent(new PRS1TotalLeakEvent(t, data[pos+3])); // 03=Total leak (average?)
|
|
this->AddEvent(new PRS1RespiratoryRateEvent(t, data[pos+4])); // 04=Breaths Per Minute (average?)
|
|
this->AddEvent(new PRS1PatientTriggeredBreathsEvent(t, data[pos+5])); // 05=Patient Triggered Breaths (average?)
|
|
this->AddEvent(new PRS1MinuteVentilationEvent(t, data[pos+6])); // 06=Minute Ventilation (average?)
|
|
this->AddEvent(new PRS1TidalVolumeEvent(t, data[pos+7])); // 07=Tidal Volume (average?)
|
|
this->AddEvent(new PRS1Test2Event(t, data[pos+8])); // 08=Flow???
|
|
this->AddEvent(new PRS1Test1Event(t, data[pos+9])); // 09=TMV???
|
|
this->AddEvent(new PRS1SnoreEvent(t, data[pos+0xa])); // 0A=Snore count // TODO: not a VS on official waveform, but appears in flags and contributes to overall VS index
|
|
this->AddEvent(new PRS1LeakEvent(t, data[pos+0xb])); // 0B=Leak (average?)
|
|
this->AddEvent(new PRS1IntervalBoundaryEvent(t));
|
|
break;
|
|
case 0x03: // Pressure Pulse
|
|
duration = data[pos]; // TODO: is this a duration?
|
|
this->AddEvent(new PRS1PressurePulseEvent(t, duration));
|
|
break;
|
|
case 0x04: // Obstructive Apnea
|
|
// OA events are instantaneous flags with no duration: reviewing waveforms
|
|
// shows that the time elapsed between the flag and reporting often includes
|
|
// non-apnea breathing.
|
|
elapsed = data[pos];
|
|
this->AddEvent(new PRS1ObstructiveApneaEvent(t - elapsed, 0));
|
|
break;
|
|
case 0x05: // Clear Airway Apnea
|
|
// CA events are instantaneous flags with no duration: reviewing waveforms
|
|
// shows that the time elapsed between the flag and reporting often includes
|
|
// non-apnea breathing.
|
|
elapsed = data[pos];
|
|
this->AddEvent(new PRS1ClearAirwayEvent(t - elapsed, 0));
|
|
break;
|
|
case 0x06: // Hypopnea
|
|
// TODO: How is this hypopnea different from events 0xd and 0xe?
|
|
// TODO: What is the first byte?
|
|
//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 0x07: // Periodic Breathing
|
|
// PB events are reported some time after they conclude, and they do have a reported duration.
|
|
duration = 2 * (data[pos] | (data[pos+1] << 8));
|
|
elapsed = data[pos+2];
|
|
this->AddEvent(new PRS1PeriodicBreathingEvent(t - elapsed - duration, duration));
|
|
break;
|
|
case 0x08: // RERA
|
|
elapsed = data[pos]; // based on sample waveform, the RERA is over after this
|
|
this->AddEvent(new PRS1RERAEvent(t - elapsed, 0));
|
|
break;
|
|
case 0x09: // Large Leak
|
|
// LL events are reported some time after they conclude, and they do have a reported duration.
|
|
duration = 2 * (data[pos] | (data[pos+1] << 8));
|
|
elapsed = data[pos+2];
|
|
this->AddEvent(new PRS1LargeLeakEvent(t - elapsed - duration, duration));
|
|
break;
|
|
case 0x0a: // Hypopnea
|
|
// TODO: Why does this hypopnea have a different event code?
|
|
// fall through
|
|
case 0x0b: // Hypopnea
|
|
// TODO: We should revisit whether this is elapsed or duration once (if)
|
|
// we start calculating hypopneas ourselves. Their official definition
|
|
// is 40% reduction in flow lasting at least 10s.
|
|
duration = data[pos];
|
|
this->AddEvent(new PRS1HypopneaEvent(t - duration, 0));
|
|
break;
|
|
case 0x0c: // Apnea Alarm
|
|
// no additional data
|
|
this->AddEvent(new PRS1ApneaAlarmEvent(t, 0));
|
|
break;
|
|
case 0x0d: // Low MV Alarm
|
|
// no additional data
|
|
this->AddEvent(new PRS1LowMinuteVentilationAlarmEvent(t, 0));
|
|
break;
|
|
// case 0x0e?
|
|
// case 0x0f?
|
|
default:
|
|
DUMP_EVENT();
|
|
UNEXPECTED_VALUE(code, "known event code");
|
|
this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos-1, size+1));
|
|
break;
|
|
}
|
|
pos = startpos + size;
|
|
} while (ok && pos < chunk_size);
|
|
|
|
this->duration = t;
|
|
|
|
return ok;
|
|
}
|