mirror of
https://gitlab.com/pholy/OSCAR-code.git
synced 2025-04-06 19:20:45 +00:00
Now that the post-process calcLeaks properly handles discontinuous data, don't make the loader pretend that the machine generated CPAP_Leak data when it didn't. The resulting data is nearly identical, except for around edge cases where the "correct" result is isn't clear. For example, when a pressure changes within a 2-minute reporting interval, the post-process calcLeaks will use that pressure when calculating the unintended leak for that interval. The previous PRS1 loader calculations were inconsistent, but would often apply the pressure in place at the beginning of the 2-minute interval instead. Either interpretation could be reasonable, but consistency is preferred. These minor differences aren't worth pursuing further, since the calculated unintended leak looks dubious regardless. This affects all CPAP/APAP/BiPAP models in the 4xx-7xx range. In contrast, most ventilators and ASV record unintended leak data (only the oldest ones don't), and so aren't affected by these changes.
1553 lines
73 KiB
C++
1553 lines
73 KiB
C++
/* PRS1 Parsing for BiPAP autoSV (ASV) (Family 5)
|
|
*
|
|
* Copyright (c) 2019-2021 The OSCAR Team
|
|
* Portions copyright (c) 2011-2018 Mark Watkins <mark@jedimark.net>
|
|
*
|
|
* 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"
|
|
|
|
static QString hex(int i)
|
|
{
|
|
return QString("0x") + QString::number(i, 16).toUpper();
|
|
}
|
|
|
|
//********************************************************************************************
|
|
// MARK: -
|
|
// MARK: 50 and 60 Series
|
|
|
|
// borrowed largely from ParseSummaryF0V4
|
|
bool PRS1DataChunk::ParseSummaryF5V012(void)
|
|
{
|
|
if (this->family != 5 || (this->familyVersion > 2)) {
|
|
qWarning() << "ParseSummaryF5V012 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;
|
|
switch (this->familyVersion) {
|
|
case 0: minimum_sizes = { 0x12, 4, 3, 0x1f, 0, 4, 0, 2, 2 }; break;
|
|
case 1: minimum_sizes = { 0x13, 7, 5, 0x20, 0, 4, 0, 2, 2, 4 }; break;
|
|
case 2: minimum_sizes = { 0x13, 7, 5, 0x22, 0, 4, 0, 2, 2, 4 }; break;
|
|
}
|
|
// NOTE: These are fixed sizes, but are called minimum to more closely match the F0V6 parser.
|
|
|
|
bool ok = true;
|
|
int pos = 0;
|
|
int code, size;
|
|
int tt = 0;
|
|
while (ok && pos < chunk_size) {
|
|
code = data[pos++];
|
|
// There is no hblock prior to F0V6.
|
|
size = 0;
|
|
if (code < minimum_sizes.length()) {
|
|
// make sure the handlers below don't go past the end of the buffer
|
|
size = minimum_sizes[code];
|
|
} else {
|
|
// We can't defer warning until later, because F5V0 doesn't have slice 9.
|
|
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;
|
|
}
|
|
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] & 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.
|
|
}
|
|
*/
|
|
// F5V012 doesn't have a separate settings record like F5V3 does, the settings just follow the EquipmentOn data.
|
|
ok = this->ParseSettingsF5V012(data, size);
|
|
/*
|
|
CHECK_VALUE(data[pos+0x11], 0);
|
|
CHECK_VALUE(data[pos+0x12], 0);
|
|
CHECK_VALUE(data[pos+0x13], 0);
|
|
CHECK_VALUE(data[pos+0x14], 0);
|
|
CHECK_VALUE(data[pos+0x15], 0);
|
|
CHECK_VALUE(data[pos+0x16], 0);
|
|
CHECK_VALUE(data[pos+0x17], 0);
|
|
*/
|
|
break;
|
|
case 2: // Mask On
|
|
tt += data[pos] | (data[pos+1] << 8);
|
|
this->AddEvent(new PRS1ParsedSliceEvent(tt, MaskOn));
|
|
/*
|
|
//CHECK_VALUES(data[pos+2], 120, 110); // probably initial pressure
|
|
//CHECK_VALUE(data[pos+3], 0); // initial IPAP on bilevel?
|
|
//CHECK_VALUES(data[pos+4], 0, 130); // minimum pressure in auto-cpap
|
|
this->ParseHumidifierSetting60Series(data[pos+5], data[pos+6]);
|
|
*/
|
|
break;
|
|
case 3: // Mask Off
|
|
tt += data[pos] | (data[pos+1] << 8);
|
|
this->AddEvent(new PRS1ParsedSliceEvent(tt, MaskOff));
|
|
// F5V012 doesn't have a separate stats record like F5V3 does, the stats just follow the MaskOff data.
|
|
/*
|
|
//CHECK_VALUES(data[pos+2], 130); // probably ending pressure
|
|
//CHECK_VALUE(data[pos+3], 0); // ending IPAP for bilevel? average?
|
|
//CHECK_VALUES(data[pos+4], 0, 130); // 130 pressure in auto-cpap: min pressure? 90% IPAP in bilevel?
|
|
//CHECK_VALUES(data[pos+5], 0, 130); // 130 pressure in auto-cpap, 90% EPAP in bilevel?
|
|
//CHECK_VALUE(data[pos+6], 0); // 145 maybe max pressure in Auto-CPAP?
|
|
//CHECK_VALUE(data[pos+7], 0); // Average 90% Pressure (Auto-CPAP)
|
|
//CHECK_VALUE(data[pos+8], 0); // Average CPAP (Auto-CPAP)
|
|
//CHECK_VALUES(data[pos+9], 0, 4); // or 1; PB count? LL count? minutes of something?
|
|
CHECK_VALUE(data[pos+0xa], 0);
|
|
//CHECK_VALUE(data[pos+0xb], 0); // OA count, probably 16-bit
|
|
CHECK_VALUE(data[pos+0xc], 0);
|
|
//CHECK_VALUE(data[pos+0xd], 0);
|
|
CHECK_VALUE(data[pos+0xe], 0);
|
|
//CHECK_VALUE(data[pos+0xf], 0); // CA count, probably 16-bit
|
|
CHECK_VALUE(data[pos+0x10], 0);
|
|
//CHECK_VALUE(data[pos+0x11], 40); // 16-bit something: 0x88, 0x26, etc. ???
|
|
//CHECK_VALUE(data[pos+0x12], 0);
|
|
//CHECK_VALUE(data[pos+0x13], 0); // 16-bit minutes in LL
|
|
//CHECK_VALUE(data[pos+0x14], 0);
|
|
//CHECK_VALUE(data[pos+0x15], 0); // minutes in PB, probably 16-bit
|
|
CHECK_VALUE(data[pos+0x16], 0);
|
|
//CHECK_VALUE(data[pos+0x17], 0); // 16-bit VS count
|
|
//CHECK_VALUE(data[pos+0x18], 0);
|
|
//CHECK_VALUE(data[pos+0x19], 0); // H count, probably 16-bit
|
|
CHECK_VALUE(data[pos+0x1a], 0);
|
|
//CHECK_VALUE(data[pos+0x1b], 0); // 0 when no PB or LL?
|
|
CHECK_VALUE(data[pos+0x1c], 0);
|
|
//CHECK_VALUE(data[pos+0x1d], 9); // RE count, probably 16-bit
|
|
CHECK_VALUE(data[pos+0x1e], 0);
|
|
//CHECK_VALUE(data[pos+0x1f], 0); // FL count, probably 16-bit
|
|
CHECK_VALUE(data[pos+0x20], 0);
|
|
//CHECK_VALUE(data[pos+0x21], 0x32); // 0x55, 0x19 // ???
|
|
//CHECK_VALUE(data[pos+0x22], 0x23); // 0x3f, 0x14 // Average total leak
|
|
//CHECK_VALUE(data[pos+0x23], 0x40); // 0x7d, 0x3d // ???
|
|
*/
|
|
break;
|
|
case 1: // Equipment Off
|
|
tt += data[pos] | (data[pos+1] << 8);
|
|
this->AddEvent(new PRS1ParsedSliceEvent(tt, EquipmentOff));
|
|
if (this->familyVersion == 0) {
|
|
//CHECK_VALUE(data[pos+2], 1); // Usually 1, also seen 0, 6, and 7.
|
|
ParseHumidifierSetting50Series(data[pos+3]);
|
|
}
|
|
/* Possibly F5V12?
|
|
CHECK_VALUE(data[pos+2] & ~(0x40|8|4|2|1), 0); // ???, seen various bit combinations
|
|
//CHECK_VALUE(data[pos+3], 0x19); // 0x17, 0x16
|
|
//CHECK_VALUES(data[pos+4], 0, 1); // or 2
|
|
//CHECK_VALUE(data[pos+5], 0x35); // 0x36, 0x36
|
|
if (data[pos+6] != 1) { // This is the usual value.
|
|
CHECK_VALUE(data[pos+6] & ~(8|4|2|1), 0); // On F0V23 0 seems to be related to errors, 3 seen after 90 sec large leak before turning off?
|
|
}
|
|
// pos+4 == 2, pos+6 == 10 on the session that had a time-elapsed event, maybe it shut itself off
|
|
// when approaching 24h of continuous use?
|
|
*/
|
|
break;
|
|
case 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: // Time Elapsed?
|
|
tt += data[pos] | (data[pos+1] << 8); // This adds to the total duration (otherwise it won't match report)
|
|
break;
|
|
case 8: // Time Elapsed? How is this different from 7?
|
|
tt += data[pos] | (data[pos+1] << 8); // This also adds to the total duration (otherwise it won't match report)
|
|
break;
|
|
case 9: // Humidifier setting change, F5V1 and F5V2 only
|
|
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;
|
|
}
|
|
|
|
|
|
bool PRS1DataChunk::ParseSettingsF5V012(const unsigned char* data, int /*size*/)
|
|
{
|
|
PRS1Mode cpapmode = PRS1_MODE_UNKNOWN;
|
|
|
|
float GAIN = PRS1PressureSettingEvent::GAIN;
|
|
if (this->familyVersion == 2) GAIN = 0.125f; // TODO: parameterize this somewhere better
|
|
|
|
int imax_pressure = data[0x2];
|
|
int imin_epap = data[0x3];
|
|
int imax_epap = data[0x4];
|
|
int imin_ps = data[0x5];
|
|
int imax_ps = data[0x6];
|
|
|
|
// Only one mode available, so apparently there's no byte in the settings that encodes it?
|
|
cpapmode = PRS1_MODE_ASV;
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_CPAP_MODE, (int) cpapmode));
|
|
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_EPAP_MIN, imin_epap));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_EPAP_MAX, imax_epap));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP_MIN, imin_epap + imin_ps));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP_MAX, imax_pressure));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS_MIN, imin_ps));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS_MAX, imax_ps));
|
|
|
|
//CHECK_VALUE(data[0x07], 1, 2); // 1 = backup breath rate "Auto"; 2 = fixed BPM, see below
|
|
//CHECK_VALUE(data[0x08], 0); // backup "Breath Rate" in mode 2
|
|
//CHECK_VALUE(data[0x09], 0); // backup "Timed Inspiration" (gain 0.1) in mode 2
|
|
int pos = 0x7;
|
|
int backup_mode = data[pos];
|
|
int breath_rate;
|
|
int timed_inspiration;
|
|
switch (backup_mode) {
|
|
case 0:
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_BACKUP_BREATH_MODE, PRS1Backup_Off));
|
|
break;
|
|
case 1:
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_BACKUP_BREATH_MODE, PRS1Backup_Auto));
|
|
CHECK_VALUE(data[pos+1], 0);
|
|
CHECK_VALUE(data[pos+2], 0);
|
|
break;
|
|
case 2:
|
|
breath_rate = data[pos+1];
|
|
timed_inspiration = data[pos+2];
|
|
if (breath_rate < 4 || breath_rate > 29) UNEXPECTED_VALUE(breath_rate, "4-29");
|
|
if (timed_inspiration < 5 || timed_inspiration > 30) UNEXPECTED_VALUE(timed_inspiration, "5-30");
|
|
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:
|
|
UNEXPECTED_VALUE(backup_mode, "0-2");
|
|
break;
|
|
}
|
|
|
|
int ramp_time = data[0x0a];
|
|
int ramp_pressure = data[0x0b];
|
|
if (ramp_time > 0) {
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_RAMP_TIME, ramp_time));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_RAMP_PRESSURE, ramp_pressure, GAIN));
|
|
}
|
|
|
|
quint8 flex = data[0x0c];
|
|
this->ParseFlexSettingF5V012(flex, cpapmode);
|
|
|
|
if (this->familyVersion == 0) { // TODO: either split this into two functions or use size to differentiate like FV3 parsers do
|
|
this->ParseHumidifierSetting50Series(data[0x0d], true);
|
|
pos = 0xe;
|
|
} else {
|
|
// 60-Series machines have a 2-byte humidfier setting.
|
|
this->ParseHumidifierSetting60Series(data[0x0d], data[0x0e], true);
|
|
pos = 0xf;
|
|
}
|
|
|
|
// TODO: may differ between F5V0 and F5V12
|
|
// 0x01, 0x41 = auto-on, view AHI, tubing type = 15
|
|
// 0x41, 0x41 = auto-on, view AHI, tubing type = 15, resist lock
|
|
// 0x42, 0x01 = (no auto-on), view AHI, tubing type = 22, resist lock, tubing lock
|
|
// 0x00, 0x41 = auto-on, view AHI, tubing type = 22, no tubing lock
|
|
// 0x0B, 0x41 = mask resist 1, tube lock, tubing type = 15, auto-on, view AHI
|
|
// 0x09, 0x01 = mask resist 1, tubing 15, view AHI
|
|
// 0x19, 0x41 = mask resist 3, tubing 15, auto-on, view AHI
|
|
// 0x29, 0x41 = mask resist 5, tubing 15, auto-on, view AHI
|
|
// 1 = view AHI
|
|
// 4 = auto-on
|
|
// 1 = tubing type: 0=22, 1=15
|
|
// 2 = tubing lock
|
|
// 38 = mask resist level
|
|
// 4 = resist lock
|
|
int resist_level = (data[pos] >> 3) & 7; // 0x09 resist=1, 0x11 resist=2, 0x19=resist 3, 0x29=resist 5
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_MASK_RESIST_LOCK, (data[pos] & 0x40) != 0));
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_MASK_RESIST_SETTING, resist_level));
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HOSE_DIAMETER, (data[pos] & 0x01) ? 15 : 22));
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_TUBING_LOCK, (data[pos] & 0x02) != 0));
|
|
CHECK_VALUE(data[pos] & (0x80|0x04), 0);
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_AUTO_ON, (data[pos+1] & 0x40) != 0));
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_SHOW_AHI, (data[pos+1] & 1) != 0));
|
|
CHECK_VALUE(data[pos+1] & ~(0x40|1), 0);
|
|
|
|
int apnea_alarm = data[pos+2];
|
|
int low_mv_alarm = data[pos+3];
|
|
int disconnect_alarm = data[pos+4];
|
|
if (apnea_alarm) {
|
|
CHECK_VALUES(apnea_alarm, 1, 3); // 1 = apnea alarm 10, 3 = apnea alarm 30
|
|
}
|
|
if (low_mv_alarm) {
|
|
if (low_mv_alarm < 20 || low_mv_alarm > 99) {
|
|
UNEXPECTED_VALUE(low_mv_alarm, "20-99"); // we've seen 20, 80 and 99, all of which correspond to the number on the report
|
|
}
|
|
}
|
|
if (disconnect_alarm) {
|
|
CHECK_VALUES(disconnect_alarm, 1, 2); // 1 = disconnect alarm 15, 2 = disconnect alarm 60
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
// Flex F5V0 confirmed
|
|
// 0x81 = Bi-Flex 1 (ASV mode)
|
|
// 0x82 = Bi-Flex 2 (ASV mode)
|
|
// 0x83 = Bi-Flex 3 (ASV mode)
|
|
|
|
// Flex F5V1 confirmed
|
|
// 0x81 = Bi-Flex 1 (ASV mode)
|
|
// 0x82 = Bi-Flex 2 (ASV mode)
|
|
// 0x83 = Bi-Flex 3 (ASV mode)
|
|
// 0xC9 = Rise Time 1, Rise Time Lock (ASV mode)
|
|
// 0x8A = Rise Time 2 (ASV mode) (Shows "ASV - None" in mode summary, but then rise time in details)
|
|
// 0x8B = Rise Time 3 (ASV mode) (breath rate auto)
|
|
// 0x08 = Rise Time 2 (ASV mode) (falls back to level=2? bits encode level=0)
|
|
|
|
// Flex F5V2 confirmed
|
|
// 0x02 = Bi-Flex 2 (ASV mode) (breath rate auto, but min/max PS=0)
|
|
// this could be different from F5V01, or PS=0 could disable flex?
|
|
|
|
// 8 = ? (once was 0 when rise time was on and backup breathing was off, rise time level was also 0 in that case)
|
|
// (was also 0 on F5V2)
|
|
// 4 = Rise Time Lock
|
|
// 8 = Rise Time (vs. Bi-Flex)
|
|
// 3 = level
|
|
|
|
void PRS1DataChunk::ParseFlexSettingF5V012(quint8 flex, int cpapmode)
|
|
{
|
|
FlexMode flexmode = FLEX_Unknown;
|
|
bool valid = (flex & 0x80) != 0;
|
|
bool lock = (flex & 0x40) != 0;
|
|
bool risetime = (flex & 0x08) != 0;
|
|
int flexlevel = flex & 0x03;
|
|
|
|
if (flex & (0x20 | 0x10 | 0x04)) UNEXPECTED_VALUE(flex, "known bits");
|
|
CHECK_VALUE(cpapmode, PRS1_MODE_ASV);
|
|
if (this->familyVersion == 0) {
|
|
CHECK_VALUE(valid, true);
|
|
CHECK_VALUE(lock, false);
|
|
CHECK_VALUE(risetime, false);
|
|
} else if (this->familyVersion == 1) {
|
|
if (valid == false) {
|
|
CHECK_VALUE(flex, 0x08);
|
|
flexlevel = 2; // These get reported as Rise Time 2
|
|
valid = true;
|
|
}
|
|
} else {
|
|
CHECK_VALUE(flex, 0x02); // only seen one example, unsure if it matches F5V01; seems to encode Bi-Flex 2
|
|
valid = true; // add the flex mode and setting to the parsed settings
|
|
}
|
|
if (flexlevel == 0 || flexlevel >3) UNEXPECTED_VALUE(flexlevel, "1-3");
|
|
|
|
CHECK_VALUE(valid, true);
|
|
if (risetime) {
|
|
flexmode = FLEX_RiseTime;
|
|
} else {
|
|
flexmode = FLEX_BiFlex;
|
|
}
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_MODE, (int) flexmode));
|
|
|
|
if (flexmode == FLEX_BiFlex) {
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_LEVEL, flexlevel));
|
|
CHECK_VALUE(lock, 0); // Flag any sample data that will let us confirm flex lock
|
|
//this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_LOCK, lock != 0));
|
|
} else {
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_RISE_TIME, flexlevel));
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_RISE_TIME_LOCK, lock != 0));
|
|
}
|
|
}
|
|
|
|
|
|
const QVector<PRS1ParsedEventType> ParsedEventsF5V0 = {
|
|
PRS1EPAPSetEvent::TYPE,
|
|
// No PP, unlike F5V1
|
|
PRS1TimedBreathEvent::TYPE,
|
|
PRS1ObstructiveApneaEvent::TYPE,
|
|
PRS1ClearAirwayEvent::TYPE,
|
|
PRS1HypopneaEvent::TYPE,
|
|
PRS1FlowLimitationEvent::TYPE,
|
|
PRS1VibratorySnoreEvent::TYPE,
|
|
PRS1PeriodicBreathingEvent::TYPE,
|
|
PRS1LargeLeakEvent::TYPE,
|
|
PRS1IPAPAverageEvent::TYPE,
|
|
PRS1IPAPLowEvent::TYPE,
|
|
PRS1IPAPHighEvent::TYPE,
|
|
PRS1TotalLeakEvent::TYPE,
|
|
PRS1RespiratoryRateEvent::TYPE,
|
|
PRS1PatientTriggeredBreathsEvent::TYPE,
|
|
PRS1MinuteVentilationEvent::TYPE,
|
|
PRS1TidalVolumeEvent::TYPE,
|
|
PRS1SnoreEvent::TYPE,
|
|
PRS1EPAPAverageEvent::TYPE,
|
|
// No LEAK, unlike F5V1
|
|
};
|
|
|
|
// 950P is F5V0
|
|
bool PRS1DataChunk::ParseEventsF5V0(void)
|
|
{
|
|
if (this->family != 5 || this->familyVersion != 0) {
|
|
qWarning() << "ParseEventsF5V0 called with family" << this->family << "familyVersion" << this->familyVersion;
|
|
return false;
|
|
}
|
|
const unsigned char * data = (unsigned char *)this->m_data.constData();
|
|
int chunk_size = this->m_data.size();
|
|
static const QMap<int,int> event_sizes = { {1,2}, {3,4}, {8,4}, {0xa,2}, {0xb,5}, {0xc,5}, {0xd,0xc} };
|
|
|
|
if (chunk_size < 1) {
|
|
// This does occasionally happen in F0V6.
|
|
qDebug() << this->sessionid << "Empty event data";
|
|
return false;
|
|
}
|
|
|
|
bool ok = true;
|
|
int pos = 0, startpos;
|
|
int code, size;
|
|
int t = 0;
|
|
int elapsed, duration;
|
|
do {
|
|
code = data[pos++];
|
|
|
|
size = 3; // default size = 2 bytes time delta + 1 byte data
|
|
if (event_sizes.contains(code)) {
|
|
size = event_sizes[code];
|
|
}
|
|
if (pos + size > chunk_size) {
|
|
qWarning() << this->sessionid << "event" << code << "@" << pos << "longer than remaining chunk";
|
|
ok = false;
|
|
break;
|
|
}
|
|
startpos = pos;
|
|
t += data[pos] | (data[pos+1] << 8);
|
|
pos += 2;
|
|
|
|
switch (code) {
|
|
case 0x00: // Humidifier setting change (logged in summary in 60 series)
|
|
this->ParseHumidifierSetting50Series(data[pos]);
|
|
break;
|
|
//case 0x01: // never seen on F5V0
|
|
case 0x02: // Pressure adjustment
|
|
this->AddEvent(new PRS1EPAPSetEvent(t, data[pos++]));
|
|
break;
|
|
//case 0x03: // never seen on F5V0; probably pressure pulse, see F5V1
|
|
case 0x04: // 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 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
|
|
// NOTE: No additional (unknown) first byte as in F5V3 0x07, but see below.
|
|
// This seems closer to F5V3 0x0d or 0x0e.
|
|
elapsed = data[pos]; // based on sample waveform, the hypopnea is over after this
|
|
this->AddEvent(new PRS1HypopneaEvent(t - elapsed, 0));
|
|
break;
|
|
case 0x08: // Hypopnea, note this is 0x7 in F5V3
|
|
// TODO: How is this hypopnea different from event 0x7?
|
|
// 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 0x09: // Flow Limitation, note this is 0x8 in F5V3
|
|
// 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 0x0a: // Vibratory Snore, note this is 0x9 in F5V3
|
|
// 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 0x0b: // Periodic Breathing, note this is 0xa in F5V3
|
|
// PB events are reported some time after they conclude, and they do have a reported duration.
|
|
duration = 2 * (data[pos] | (data[pos+1] << 8)); // confirmed to double in F5V0
|
|
elapsed = data[pos+2];
|
|
this->AddEvent(new PRS1PeriodicBreathingEvent(t - elapsed - duration, duration));
|
|
break;
|
|
case 0x0c: // Large Leak, note this is 0xb in F5V3
|
|
// LL events are reported some time after they conclude, and they do have a reported duration.
|
|
duration = 2 * (data[pos] | (data[pos+1] << 8)); // confirmed to double in F5V0
|
|
elapsed = data[pos+2];
|
|
this->AddEvent(new PRS1LargeLeakEvent(t - elapsed - duration, duration));
|
|
break;
|
|
case 0x0d: // Statistics
|
|
// These appear every 2 minutes, so presumably summarize the preceding period.
|
|
this->AddEvent(new PRS1IPAPAverageEvent(t, data[pos+0])); // 00=IPAP
|
|
this->AddEvent(new PRS1IPAPLowEvent(t, data[pos+1])); // 01=IAP Low
|
|
this->AddEvent(new PRS1IPAPHighEvent(t, data[pos+2])); // 02=IAP High
|
|
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 PRS1SnoreEvent(t, data[pos+8])); // 08=Snore count // TODO: not a VS on official waveform, but appears in flags and contributes to overall VS index
|
|
this->AddEvent(new PRS1EPAPAverageEvent(t, data[pos+9])); // 09=EPAP average
|
|
this->AddEvent(new PRS1IntervalBoundaryEvent(t));
|
|
break;
|
|
default:
|
|
DUMP_EVENT();
|
|
UNEXPECTED_VALUE(code, "known event code");
|
|
this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos));
|
|
ok = false; // unlike F0V6, we don't know the size of unknown slices, so we can't recover
|
|
break;
|
|
}
|
|
pos = startpos + size;
|
|
} while (ok && pos < chunk_size);
|
|
|
|
if (ok && pos != chunk_size) {
|
|
qWarning() << this->sessionid << (this->size() - pos) << "trailing event bytes";
|
|
}
|
|
|
|
this->duration = t;
|
|
|
|
return ok;
|
|
}
|
|
|
|
|
|
const QVector<PRS1ParsedEventType> ParsedEventsF5V1 = {
|
|
PRS1EPAPSetEvent::TYPE,
|
|
PRS1PressurePulseEvent::TYPE,
|
|
PRS1TimedBreathEvent::TYPE,
|
|
PRS1ObstructiveApneaEvent::TYPE,
|
|
PRS1ClearAirwayEvent::TYPE,
|
|
PRS1HypopneaEvent::TYPE,
|
|
PRS1FlowLimitationEvent::TYPE,
|
|
PRS1VibratorySnoreEvent::TYPE,
|
|
PRS1PeriodicBreathingEvent::TYPE,
|
|
PRS1LargeLeakEvent::TYPE,
|
|
PRS1IPAPAverageEvent::TYPE,
|
|
PRS1IPAPLowEvent::TYPE,
|
|
PRS1IPAPHighEvent::TYPE,
|
|
PRS1TotalLeakEvent::TYPE,
|
|
PRS1RespiratoryRateEvent::TYPE,
|
|
PRS1PatientTriggeredBreathsEvent::TYPE,
|
|
PRS1MinuteVentilationEvent::TYPE,
|
|
PRS1TidalVolumeEvent::TYPE,
|
|
PRS1SnoreEvent::TYPE,
|
|
PRS1EPAPAverageEvent::TYPE,
|
|
PRS1LeakEvent::TYPE,
|
|
};
|
|
|
|
// 960P and 961P are F5V1
|
|
bool PRS1DataChunk::ParseEventsF5V1(void)
|
|
{
|
|
if (this->family != 5 || this->familyVersion != 1) {
|
|
qWarning() << "ParseEventsF5V1 called with family" << this->family << "familyVersion" << this->familyVersion;
|
|
return false;
|
|
}
|
|
const unsigned char * data = (unsigned char *)this->m_data.constData();
|
|
int chunk_size = this->m_data.size();
|
|
static const QMap<int,int> event_sizes = { {1,2}, {8,4}, {9,3}, {0xa,2}, {0xb,5}, {0xc,5}, {0xd,0xd} };
|
|
|
|
if (chunk_size < 1) {
|
|
// This does occasionally happen in F0V6.
|
|
qDebug() << this->sessionid << "Empty event data";
|
|
return false;
|
|
}
|
|
|
|
bool ok = true;
|
|
int pos = 0, startpos;
|
|
int code, size;
|
|
int t = 0;
|
|
int elapsed, duration;
|
|
do {
|
|
code = data[pos++];
|
|
|
|
size = 3; // default size = 2 bytes time delta + 1 byte data
|
|
if (event_sizes.contains(code)) {
|
|
size = event_sizes[code];
|
|
}
|
|
if (pos + size > chunk_size) {
|
|
qWarning() << this->sessionid << "event" << code << "@" << pos << "longer than remaining chunk";
|
|
ok = false;
|
|
break;
|
|
}
|
|
startpos = pos;
|
|
if (code != 0) { // Does this code really not have a timestamp? Never seen on F5V1, checked in F5V0.
|
|
t += data[pos] | (data[pos+1] << 8);
|
|
pos += 2;
|
|
}
|
|
|
|
switch (code) {
|
|
//case 0x00: // never seen on F5V1
|
|
//case 0x01: // never seen on F5V1
|
|
case 0x02: // Pressure adjustment
|
|
this->AddEvent(new PRS1EPAPSetEvent(t, data[pos++]));
|
|
break;
|
|
case 0x03: // Pressure Pulse
|
|
duration = data[pos]; // TODO: is this a duration?
|
|
this->AddEvent(new PRS1PressurePulseEvent(t, duration));
|
|
break;
|
|
case 0x04: // 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 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 event 0x8?
|
|
// NOTE: No additional (unknown) first byte as in F5V3 0x7, but see below.
|
|
// This seems closer to F5V3 0x0d or 0x0e.
|
|
elapsed = data[pos]; // based on sample waveform, the hypopnea is over after this
|
|
this->AddEvent(new PRS1HypopneaEvent(t - elapsed, 0));
|
|
break;
|
|
case 0x08: // Hypopnea, note this is 0x7 in F5V3
|
|
// TODO: How is this hypopnea different from event 0x7?
|
|
// 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 0x09: // Flow Limitation, note this is 0x8 in F5V3
|
|
// 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 0x0a: // Vibratory Snore, note this is 0x9 in F5V3
|
|
// 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 0x0b: // Periodic Breathing, note this is 0xa in F5V3
|
|
// PB events are reported some time after they conclude, and they do have a reported duration.
|
|
duration = 2 * (data[pos] | (data[pos+1] << 8)); // confirmed to double in F5V0
|
|
elapsed = data[pos+2];
|
|
this->AddEvent(new PRS1PeriodicBreathingEvent(t - elapsed - duration, duration));
|
|
break;
|
|
case 0x0c: // Large Leak, note this is 0xb in F5V3
|
|
// LL events are reported some time after they conclude, and they do have a reported duration.
|
|
duration = 2 * (data[pos] | (data[pos+1] << 8)); // confirmed to double in F5V0
|
|
elapsed = data[pos+2];
|
|
this->AddEvent(new PRS1LargeLeakEvent(t - elapsed - duration, duration));
|
|
break;
|
|
case 0x0d: // Statistics
|
|
// These appear every 2 minutes, so presumably summarize the preceding period.
|
|
this->AddEvent(new PRS1IPAPAverageEvent(t, data[pos+0])); // 00=IPAP
|
|
this->AddEvent(new PRS1IPAPLowEvent(t, data[pos+1])); // 01=IAP Low
|
|
this->AddEvent(new PRS1IPAPHighEvent(t, data[pos+2])); // 02=IAP High
|
|
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 PRS1SnoreEvent(t, data[pos+8])); // 08=Snore count // TODO: not a VS on official waveform, but appears in flags and contributes to overall VS index
|
|
this->AddEvent(new PRS1EPAPAverageEvent(t, data[pos+9])); // 09=EPAP average
|
|
this->AddEvent(new PRS1LeakEvent(t, data[pos+0xa])); // 0A=Leak (average?) new to F5V1 (originally found in F5V3)
|
|
this->AddEvent(new PRS1IntervalBoundaryEvent(t));
|
|
break;
|
|
default:
|
|
DUMP_EVENT();
|
|
UNEXPECTED_VALUE(code, "known event code");
|
|
this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos));
|
|
ok = false; // unlike F0V6, we don't know the size of unknown slices, so we can't recover
|
|
break;
|
|
}
|
|
pos = startpos + size;
|
|
} while (ok && pos < chunk_size);
|
|
|
|
if (ok && pos != chunk_size) {
|
|
qWarning() << this->sessionid << (this->size() - pos) << "trailing event bytes";
|
|
}
|
|
|
|
this->duration = t;
|
|
|
|
return ok;
|
|
}
|
|
|
|
|
|
const QVector<PRS1ParsedEventType> ParsedEventsF5V2 = {
|
|
PRS1EPAPSetEvent::TYPE,
|
|
PRS1TimedBreathEvent::TYPE,
|
|
PRS1ObstructiveApneaEvent::TYPE,
|
|
//PRS1ClearAirwayEvent::TYPE, // not yet seen
|
|
PRS1HypopneaEvent::TYPE,
|
|
PRS1FlowLimitationEvent::TYPE,
|
|
//PRS1VibratorySnoreEvent::TYPE, // not yet seen
|
|
PRS1PeriodicBreathingEvent::TYPE,
|
|
//PRS1LargeLeakEvent::TYPE, // not yet seen
|
|
PRS1IPAPAverageEvent::TYPE,
|
|
PRS1IPAPLowEvent::TYPE,
|
|
PRS1IPAPHighEvent::TYPE,
|
|
PRS1TotalLeakEvent::TYPE,
|
|
PRS1RespiratoryRateEvent::TYPE,
|
|
PRS1PatientTriggeredBreathsEvent::TYPE,
|
|
PRS1MinuteVentilationEvent::TYPE,
|
|
PRS1TidalVolumeEvent::TYPE,
|
|
PRS1SnoreEvent::TYPE,
|
|
PRS1EPAPAverageEvent::TYPE,
|
|
PRS1LeakEvent::TYPE,
|
|
};
|
|
|
|
// 960T is F5V2
|
|
bool PRS1DataChunk::ParseEventsF5V2(void)
|
|
{
|
|
if (this->family != 5 || this->familyVersion != 2) {
|
|
qWarning() << "ParseEventsF5V2 called with family" << this->family << "familyVersion" << this->familyVersion;
|
|
return false;
|
|
}
|
|
const unsigned char * data = (unsigned char *)this->m_data.constData();
|
|
int chunk_size = this->m_data.size();
|
|
static const QMap<int,int> event_sizes = { {0,4}, {1,2}, {3,4}, {8,3}, {9,4}, {0xa,3}, {0xb,5}, {0xc,5}, {0xd,5}, {0xe,0xd}, {0xf,5}, {0x10,5}, {0x11,2}, {0x12,6} };
|
|
|
|
if (chunk_size < 1) {
|
|
// This does occasionally happen in F0V6.
|
|
qDebug() << this->sessionid << "Empty event data";
|
|
return false;
|
|
}
|
|
|
|
// F5V3 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, value*/;
|
|
do {
|
|
code = data[pos++];
|
|
|
|
size = 3; // default size = 2 bytes time delta + 1 byte data
|
|
if (event_sizes.contains(code)) {
|
|
size = event_sizes[code];
|
|
}
|
|
if (pos + size > chunk_size) {
|
|
qWarning() << this->sessionid << "event" << code << "@" << pos << "longer than remaining chunk";
|
|
ok = false;
|
|
break;
|
|
}
|
|
startpos = pos;
|
|
if (code != 0 && code != 0x12) { // These two codes have no timestamp TODO: verify this applies to F5V012
|
|
t += data[pos] /*| (data[pos+1] << 8)*/; // TODO: Is this really only 1 byte?
|
|
if (data[pos+1] != 0) qWarning() << this->sessionid << "nonzero time? byte" << hex(startpos);
|
|
CHECK_VALUE(data[pos+1], 0);
|
|
pos += 2;
|
|
}
|
|
|
|
switch (code) {
|
|
/*
|
|
case 0x00: // Unknown (ASV Pressure value)
|
|
DUMP_EVENT();
|
|
// offset?
|
|
data0 = data[pos++];
|
|
|
|
if (!data[pos - 1]) { // WTH???
|
|
data1 = data[pos++];
|
|
}
|
|
|
|
if (!data[pos - 1]) {
|
|
//data2 = data[pos++];
|
|
pos++;
|
|
}
|
|
|
|
break;
|
|
|
|
case 0x01: // Unknown
|
|
DUMP_EVENT();
|
|
this->AddEvent(new PRS1UnknownValueEvent(code, t, 0, 0.1F));
|
|
break;
|
|
*/
|
|
case 0x02: // Pressure adjustment
|
|
this->AddEvent(new PRS1EPAPSetEvent(t, data[pos++], GAIN));
|
|
break;
|
|
/*
|
|
case 0x03: // BIPAP Pressure
|
|
DUMP_EVENT();
|
|
qDebug() << "0x03 Observed in ASV data!!????";
|
|
|
|
data0 = data[pos++];
|
|
data1 = data[pos++];
|
|
// data0/=10.0;
|
|
// data1/=10.0;
|
|
// session->AddEvent(new Event(t,CPAP_EAP, 0, data, 1));
|
|
// session->AddEvent(new Event(t,CPAP_IAP, 0, &data1, 1));
|
|
break;
|
|
*/
|
|
case 0x04: // 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 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:
|
|
DUMP_EVENT();
|
|
//code=CPAP_ClearAirway;
|
|
data0 = data[pos++];
|
|
this->AddEvent(new PRS1ClearAirwayEvent(t - data0, data0));
|
|
break;
|
|
*/
|
|
|
|
case 0x07: // Hypopnea
|
|
// NOTE: No additional (unknown) first byte as in F5V3 0x07, but see below.
|
|
// This seems closer to F5V3 0x0d or 0x0e.
|
|
// What's different about this an 0x08? This was seen in a PB at least once?
|
|
elapsed = data[pos]; // based on sample waveform, the hypopnea is over after this
|
|
this->AddEvent(new PRS1HypopneaEvent(t - elapsed, 0));
|
|
break;
|
|
case 0x08: // Hypopnea, note this is 0x7 in F5V1
|
|
// TODO: How is this hypopnea different from event 0x9 and 0x7?
|
|
// NOTE: No additional (unknown) first byte as in F5V3 0x7, but see below.
|
|
// This seems closer to F5V3 0x0d or 0x0e.
|
|
elapsed = data[pos]; // based on sample waveform, the hypopnea is over after this
|
|
this->AddEvent(new PRS1HypopneaEvent(t - elapsed, 0));
|
|
break;
|
|
/*
|
|
case 0x09: // ASV Codes
|
|
DUMP_EVENT();
|
|
/ *
|
|
if (this->familyVersion<2) {
|
|
//code=CPAP_FlowLimit;
|
|
data0 = data[pos++];
|
|
|
|
this->AddEvent(new PRS1FlowLimitationEvent(t - data0, data0));
|
|
} else {
|
|
* /
|
|
data0 = data[pos++];
|
|
data1 = data[pos++];
|
|
break;
|
|
*/
|
|
case 0x0a: // Flow Limitation, note this is 0x9 in F5V1 and 0x8 in F5V3
|
|
// 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 0x0b: // Cheyne Stokes
|
|
DUMP_EVENT();
|
|
data0 = ((unsigned char *)data)[pos + 1] << 8 | ((unsigned char *)data)[pos];
|
|
//data0*=2;
|
|
pos += 2;
|
|
data1 = ((unsigned char *)data)[pos]; //|data[pos+1] << 8
|
|
pos += 1;
|
|
//tt-=delta;
|
|
this->AddEvent(new PRS1PeriodicBreathingEvent(t - data1, data0));
|
|
break;
|
|
*/
|
|
case 0x0c: // Periodic Breathing, note this is 0xb in F5V1 and 0xa in F5V3
|
|
// PB events are reported some time after they conclude, and they do have a reported duration.
|
|
duration = 2 * (data[pos] | (data[pos+1] << 8)); // confirmed to double in F5V0
|
|
elapsed = data[pos+2];
|
|
this->AddEvent(new PRS1PeriodicBreathingEvent(t - elapsed - duration, duration));
|
|
break;
|
|
/*
|
|
case 0x0d:
|
|
DUMP_EVENT();
|
|
|
|
data0 = (data[pos + 1] << 8 | data[pos]);
|
|
data0 *= 2;
|
|
pos += 2;
|
|
data1 = data[pos++];
|
|
//tt = t - qint64(data1) * 1000L;
|
|
break;
|
|
*/
|
|
case 0x0e: // Statistics, note this was 0x0d in F5V0 and F5V1
|
|
// These appear every 2 minutes, so presumably summarize the preceding period.
|
|
this->AddEvent(new PRS1IPAPAverageEvent(t, data[pos+0], GAIN)); // 00=IPAP
|
|
this->AddEvent(new PRS1IPAPLowEvent(t, data[pos+1], GAIN)); // 01=IAP Low
|
|
this->AddEvent(new PRS1IPAPHighEvent(t, data[pos+2], GAIN)); // 02=IAP High
|
|
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 PRS1SnoreEvent(t, data[pos+8])); // 08=Snore count // TODO: not a VS on official waveform, but appears in flags and contributes to overall VS index
|
|
this->AddEvent(new PRS1EPAPAverageEvent(t, data[pos+9], GAIN)); // 09=EPAP average
|
|
this->AddEvent(new PRS1LeakEvent(t, data[pos+0xa])); // 0A=Leak (average?) new to F5V1 (originally found in F5V3)
|
|
this->AddEvent(new PRS1IntervalBoundaryEvent(t));
|
|
break;
|
|
/*
|
|
case 0x0f:
|
|
DUMP_EVENT();
|
|
qDebug() << "0x0f Observed in ASV data!!????";
|
|
|
|
data0 = data[pos + 1] << 8 | data[pos];
|
|
pos += 2;
|
|
data1 = data[pos]; //|data[pos+1] << 8
|
|
pos += 1;
|
|
//tt -= qint64(data1) * 1000L;
|
|
//session->AddEvent(new Event(tt,cpapcode, 0, data, 2));
|
|
break;
|
|
|
|
case 0x10: // Unknown
|
|
DUMP_EVENT();
|
|
data0 = data[pos + 1] << 8 | data[pos];
|
|
pos += 2;
|
|
data1 = data[pos++];
|
|
|
|
this->AddEvent(new PRS1LargeLeakEvent(t - data1, data0));
|
|
|
|
// qDebug() << "0x10 Observed in ASV data!!????";
|
|
// data0 = data[pos++]; // << 8) | data[pos];
|
|
// data1 = data[pos++];
|
|
// data2 = data[pos++];
|
|
//session->AddEvent(new Event(t,cpapcode, 0, data, 3));
|
|
break;
|
|
case 0x11: // Not Leak Rate
|
|
DUMP_EVENT();
|
|
qDebug() << "0x11 Observed in ASV data!!????";
|
|
//if (!Code[24]) {
|
|
// Code[24]=new EventList(cpapcode,EVL_Event);
|
|
//}
|
|
//Code[24]->AddEvent(t,data[pos++]);
|
|
break;
|
|
|
|
|
|
case 0x12: // Summary
|
|
DUMP_EVENT();
|
|
qDebug() << "0x12 Observed in ASV data!!????";
|
|
data0 = data[pos++];
|
|
data1 = data[pos++];
|
|
//data2 = data[pos + 1] << 8 | data[pos];
|
|
pos += 2;
|
|
//session->AddEvent(new Event(t,cpapcode, 0, data,3));
|
|
break;
|
|
*/
|
|
default:
|
|
DUMP_EVENT();
|
|
UNEXPECTED_VALUE(code, "known event code");
|
|
this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos));
|
|
ok = false; // unlike F0V6, we don't know the size of unknown slices, so we can't recover
|
|
break;
|
|
}
|
|
pos = startpos + size;
|
|
} while (ok && pos < chunk_size);
|
|
|
|
if (ok && pos != chunk_size) {
|
|
qWarning() << this->sessionid << (this->size() - pos) << "trailing event bytes";
|
|
}
|
|
|
|
this->duration = t;
|
|
|
|
return ok;
|
|
}
|
|
|
|
|
|
//********************************************************************************************
|
|
// MARK: -
|
|
// MARK: DreamStation
|
|
|
|
// Originally based on ParseSummaryF0V6, with changes observed in ASV sample data
|
|
// based on size, slices 0-5 look similar, and it looks like F0V6 slides 8-B are equivalent to 6-9
|
|
//
|
|
// TODO: surely there will be a way to merge these loops and abstract the machine-specific
|
|
// encodings into another function or class, but that's probably worth pursuing only after
|
|
// the details have been figured out.
|
|
bool PRS1DataChunk::ParseSummaryF5V3(void)
|
|
{
|
|
if (this->family != 5 || this->familyVersion != 3) {
|
|
qWarning() << "ParseSummaryF5V3 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, 0x35, 9, 4, 2, 4, 0x1e, 2, 4, 9 };
|
|
static const int ncodes = sizeof(minimum_sizes) / sizeof(int);
|
|
// NOTE: The sizes contained in hblock can vary, even within a single machine, as can the length of hblock itself!
|
|
|
|
// TODO: hardcoding this is ugly, think of a better approach
|
|
if (chunk_size < minimum_sizes[0] + minimum_sizes[1] + minimum_sizes[2]) {
|
|
qWarning() << this->sessionid << "summary data too short:" << chunk_size;
|
|
return false;
|
|
}
|
|
// We've once seen a short summary with no mask-on/off: just equipment-on, settings, 9, equipment-off
|
|
// (And we've seen something similar in F3V6.)
|
|
if (chunk_size < 75) UNEXPECTED_VALUE(chunk_size, ">= 75");
|
|
|
|
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;
|
|
}
|
|
|
|
int alarm;
|
|
switch (code) {
|
|
case 0: // Equipment On
|
|
CHECK_VALUE(pos, 1); // Always first?
|
|
//CHECK_VALUES(data[pos], 1, 7); // or 3, or 0? 3 when machine turned on via auto-on, 1 when turned on via button
|
|
CHECK_VALUE(size, 1);
|
|
break;
|
|
case 1: // Settings
|
|
ok = this->ParseSettingsF5V3(data + pos, size);
|
|
break;
|
|
case 9: // new to F5V3 vs. F0V6, comes right after settings, before mask on?
|
|
CHECK_VALUE(data[pos], 0);
|
|
CHECK_VALUE(data[pos+1], 1);
|
|
CHECK_VALUE(data[pos+2], 0);
|
|
CHECK_VALUE(data[pos+3], 1);
|
|
CHECK_VALUE(data[pos+4], 1);
|
|
CHECK_VALUE(data[pos+5], 0);
|
|
CHECK_VALUE(data[pos+6], 2);
|
|
CHECK_VALUE(data[pos+7], 1);
|
|
|
|
alarm = 0;
|
|
switch (data[pos+8]) {
|
|
case 1: alarm = 15; break; // 15 sec
|
|
case 2: alarm = 60; break; // 60 sec
|
|
case 0: break;
|
|
default:
|
|
UNEXPECTED_VALUE(data[pos+8], "0-2");
|
|
break;
|
|
}
|
|
if (alarm) {
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_DISCONNECT_ALARM, alarm));
|
|
}
|
|
|
|
CHECK_VALUE(size, 9);
|
|
break;
|
|
case 3: // 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 4: // Mask Off
|
|
tt += data[pos] | (data[pos+1] << 8);
|
|
this->AddEvent(new PRS1ParsedSliceEvent(tt, MaskOff));
|
|
break;
|
|
case 5: // ASV pressure stats per mask-on slice
|
|
//CHECK_VALUE(data[pos], 0x28); // 90% EPAP
|
|
//CHECK_VALUE(data[pos+1], 0x23); // average EPAP
|
|
//CHECK_VALUE(data[pos+2], 0x24); // 90% PS
|
|
//CHECK_VALUE(data[pos+3], 0x17); // average PS
|
|
break;
|
|
case 6: // Patient statistics per mask-on slice
|
|
// These get averaged on a time-weighted basis in the final report.
|
|
// Where is H count?
|
|
//CHECK_VALUE(data[pos], 0x00); // probably 16-bit value
|
|
CHECK_VALUE(data[pos+1], 0x00);
|
|
//CHECK_VALUE(data[pos+2], 0x00); // 16-bit OA count
|
|
//CHECK_VALUE(data[pos+3], 0x00);
|
|
//CHECK_VALUE(data[pos+4], 0x00); // probably 16-bit value
|
|
CHECK_VALUE(data[pos+5], 0x00);
|
|
//CHECK_VALUE(data[pos+6], 0x00); // 16-bit CA count
|
|
//CHECK_VALUE(data[pos+7], 0x00);
|
|
//CHECK_VALUE(data[pos+8], 0x00); // 16-bit minutes in LL
|
|
//CHECK_VALUE(data[pos+9], 0x00);
|
|
//CHECK_VALUE(data[pos+0xa], 0x0f); // 16-bit minutes in PB
|
|
//CHECK_VALUE(data[pos+0xb], 0x00);
|
|
//CHECK_VALUE(data[pos+0xc], 0x14); // 16-bit VS count
|
|
//CHECK_VALUE(data[pos+0xd], 0x00);
|
|
//CHECK_VALUE(data[pos+0xe], 0x05); // 16-bit H count for type 0xd
|
|
//CHECK_VALUE(data[pos+0xf], 0x00);
|
|
//CHECK_VALUE(data[pos+0x10], 0x00); // 16-bit H count for type 7
|
|
//CHECK_VALUE(data[pos+0x11], 0x00);
|
|
//CHECK_VALUE(data[pos+0x12], 0x02); // 16-bit FL count
|
|
//CHECK_VALUE(data[pos+0x13], 0x00);
|
|
//CHECK_VALUE(data[pos+0x14], 0x28); // 0x69 (105)
|
|
//CHECK_VALUE(data[pos+0x15], 0x17); // average total leak
|
|
//CHECK_VALUE(data[pos+0x16], 0x5b); // 0x7d (125)
|
|
//CHECK_VALUE(data[pos+0x17], 0x09); // 16-bit H count for type 0xe
|
|
//CHECK_VALUE(data[pos+0x18], 0x00);
|
|
//CHECK_VALUE(data[pos+0x19], 0x10); // average breath rate
|
|
//CHECK_VALUE(data[pos+0x1a], 0x2d); // average TV / 10
|
|
//CHECK_VALUE(data[pos+0x1b], 0x63); // average % PTB
|
|
//CHECK_VALUE(data[pos+0x1c], 0x07); // average minute vent
|
|
//CHECK_VALUE(data[pos+0x1d], 0x06); // average leak
|
|
break;
|
|
case 2: // Equipment Off
|
|
tt += data[pos] | (data[pos+1] << 8);
|
|
this->AddEvent(new PRS1ParsedSliceEvent(tt, EquipmentOff));
|
|
//CHECK_VALUE(data[pos+2], 0x01); // 0x08
|
|
//CHECK_VALUE(data[pos+3], 0x17); // 0x16, 0x18
|
|
//CHECK_VALUE(data[pos+4], 0x00);
|
|
//CHECK_VALUE(data[pos+5], 0x29); // 0x2a, 0x28, 0x26, 0x36
|
|
//CHECK_VALUE(data[pos+6], 0x01); // 0x00
|
|
CHECK_VALUE(data[pos+7], 0x00);
|
|
CHECK_VALUE(data[pos+8], 0x00);
|
|
break;
|
|
case 8: // 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 ParseSettingsF0V6. 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: breath rate, tubing lock, alarms,
|
|
bool PRS1DataChunk::ParseSettingsF5V3(const unsigned char* data, int size)
|
|
{
|
|
static const QMap<int,int> expected_lengths = { {0x0a,5}, /*{0x0c,3}, {0x0d,2}, {0x0e,2}, {0x0f,4}, {0x10,3},*/ {0x14,3}, {0x2e,2}, {0x35,2} };
|
|
bool ok = true;
|
|
|
|
PRS1Mode cpapmode = PRS1_MODE_UNKNOWN;
|
|
|
|
// F5V3 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: parameterize this somewhere better
|
|
|
|
int max_pressure = 0;
|
|
int min_ps = 0;
|
|
int max_ps = 0;
|
|
int min_epap = 0;
|
|
int max_epap = 0;
|
|
int rise_time;
|
|
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_ASV; break;
|
|
default:
|
|
UNEXPECTED_VALUE(data[pos], "known device mode");
|
|
break;
|
|
}
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_CPAP_MODE, (int) cpapmode));
|
|
break;
|
|
case 1: // ???
|
|
CHECK_VALUE(len, 1);
|
|
CHECK_VALUES(data[pos], 0, 1); // 1 when when Opti-Start is on? 0 when off?
|
|
/*
|
|
if (data[pos] != 0 && data[pos] != 3) {
|
|
CHECK_VALUES(data[pos], 1, 2); // 1 when EZ-Start is enabled? 2 when Auto-Trial? 3 when Auto-Trial is off or Opti-Start isn't off?
|
|
}
|
|
*/
|
|
break;
|
|
case 0x0a: // ASV with variable EPAP pressure setting
|
|
CHECK_VALUE(len, 5);
|
|
CHECK_VALUE(cpapmode, PRS1_MODE_ASV);
|
|
max_pressure = data[pos];
|
|
min_epap = data[pos+1];
|
|
max_epap = data[pos+2];
|
|
min_ps = data[pos+3];
|
|
max_ps = data[pos+4];
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_EPAP_MIN, min_epap, GAIN));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_EPAP_MAX, max_epap, GAIN));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP_MIN, min_epap + min_ps, GAIN));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP_MAX, qMin(max_pressure, max_epap + max_ps), GAIN));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS_MIN, min_ps, GAIN));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS_MAX, max_ps, GAIN));
|
|
break;
|
|
case 0x14: // ASV backup rate
|
|
CHECK_VALUE(len, 3);
|
|
CHECK_VALUE(cpapmode, PRS1_MODE_ASV);
|
|
switch (data[pos]) {
|
|
//case 0: // Breath Rate Off in F3V6 setting 0x1e
|
|
case 1: // Breath Rate Auto
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_BACKUP_BREATH_MODE, PRS1Backup_Auto));
|
|
CHECK_VALUE(data[pos+1], 0); // 0 for auto
|
|
CHECK_VALUE(data[pos+2], 0); // 0 for auto
|
|
break;
|
|
case 2: // Breath Rate (fixed BPM)
|
|
breath_rate = data[pos+1];
|
|
timed_inspiration = data[pos+2];
|
|
if (breath_rate < 4 || breath_rate > 16) UNEXPECTED_VALUE(breath_rate, "4-16");
|
|
if (timed_inspiration < 12 || timed_inspiration > 25) UNEXPECTED_VALUE(timed_inspiration, "12-25");
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_BACKUP_BREATH_MODE, PRS1Backup_Fixed));
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_BACKUP_BREATH_RATE, breath_rate)); // BPM
|
|
this->AddEvent(new PRS1ScaledSettingEvent(PRS1_SETTING_BACKUP_TIMED_INSPIRATION, timed_inspiration, 0.1));
|
|
break;
|
|
default:
|
|
CHECK_VALUES(data[pos], 1, 2); // 1 = auto, 2 = fixed BPM (0 = off in F3V6 setting 0x1e)
|
|
break;
|
|
}
|
|
break;
|
|
/*
|
|
case 0x2a: // EZ-Start
|
|
CHECK_VALUE(data[pos], 0x80); // EZ-Start enabled
|
|
break;
|
|
*/
|
|
case 0x2b: // Ramp Type
|
|
CHECK_VALUE(len, 1);
|
|
CHECK_VALUES(data[pos], 0, 0x80); // 0 == "Linear", 0x80 = "SmartRamp"
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_RAMP_TYPE, data[pos] != 0));
|
|
break;
|
|
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 pressure encoding)
|
|
CHECK_VALUE(len, 1);
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_RAMP_PRESSURE, data[pos], GAIN));
|
|
break;
|
|
case 0x2e: // Flex mode and level (ASV variant)
|
|
CHECK_VALUE(len, 2);
|
|
switch (data[pos]) {
|
|
case 0: // Bi-Flex
|
|
// [0x00, N] for Bi-Flex level N
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_MODE, FLEX_BiFlex));
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_LEVEL, data[pos+1]));
|
|
break;
|
|
case 0x20: // Rise Time
|
|
// [0x20, 0x03] for no flex, rise time setting = 3, no rise lock
|
|
rise_time = data[pos+1];
|
|
if (rise_time < 1 || rise_time > 6) UNEXPECTED_VALUE(rise_time, "1-6");
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_RISE_TIME, rise_time));
|
|
break;
|
|
default:
|
|
CHECK_VALUES(data[pos], 0, 0x20);
|
|
break;
|
|
}
|
|
break;
|
|
case 0x2f: // Flex lock? (was on F0V6, 0x80 for locked)
|
|
CHECK_VALUE(len, 1);
|
|
CHECK_VALUE(data[pos], 0);
|
|
//this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_LOCK, data[pos] != 0));
|
|
break;
|
|
//case 0x30: ASV puts the flex level in the 0x2e setting for some reason
|
|
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); // 0x80 = locked
|
|
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:
|
|
CHECK_VALUE(len, 1);
|
|
CHECK_VALUE(data[pos], 0); // 0x80 maybe auto-trial in F0V6?
|
|
break;
|
|
case 0x3b: // Tubing Type
|
|
CHECK_VALUE(len, 1);
|
|
if (data[pos] > 2) UNEXPECTED_VALUE(data[pos], "0-2"); // 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;
|
|
case 0x3d: // Auto On (ASV variant)
|
|
CHECK_VALUE(len, 1);
|
|
CHECK_VALUES(data[pos], 0, 0x80);
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_AUTO_ON, data[pos] != 0));
|
|
break;
|
|
default:
|
|
UNEXPECTED_VALUE(code, "known setting");
|
|
qDebug() << "Unknown setting:" << hex << code << "in" << this->sessionid << "at" << pos;
|
|
this->AddEvent(new PRS1UnknownDataEvent(QByteArray((const char*) data, size), pos, len));
|
|
break;
|
|
}
|
|
|
|
pos += len;
|
|
} while (ok && pos + 2 <= size);
|
|
|
|
return ok;
|
|
}
|
|
|
|
|
|
const QVector<PRS1ParsedEventType> ParsedEventsF5V3 = {
|
|
PRS1EPAPSetEvent::TYPE,
|
|
PRS1TimedBreathEvent::TYPE,
|
|
PRS1IPAPAverageEvent::TYPE,
|
|
PRS1IPAPLowEvent::TYPE,
|
|
PRS1IPAPHighEvent::TYPE,
|
|
PRS1TotalLeakEvent::TYPE,
|
|
PRS1RespiratoryRateEvent::TYPE,
|
|
PRS1PatientTriggeredBreathsEvent::TYPE,
|
|
PRS1MinuteVentilationEvent::TYPE,
|
|
PRS1TidalVolumeEvent::TYPE,
|
|
PRS1SnoreEvent::TYPE,
|
|
PRS1EPAPAverageEvent::TYPE,
|
|
PRS1LeakEvent::TYPE,
|
|
PRS1PressurePulseEvent::TYPE,
|
|
PRS1ObstructiveApneaEvent::TYPE,
|
|
PRS1ClearAirwayEvent::TYPE,
|
|
PRS1HypopneaEvent::TYPE,
|
|
PRS1FlowLimitationEvent::TYPE,
|
|
PRS1VibratorySnoreEvent::TYPE,
|
|
PRS1PeriodicBreathingEvent::TYPE,
|
|
PRS1LargeLeakEvent::TYPE,
|
|
};
|
|
|
|
// Outer loop based on ParseSummaryF5V3 along with hint as to event codes from old ParseEventsF5V3,
|
|
// except this actually does something with the data.
|
|
bool PRS1DataChunk::ParseEventsF5V3(void)
|
|
{
|
|
if (this->family != 5 || this->familyVersion != 3) {
|
|
qWarning() << "ParseEventsF5V3 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, 3, 0xd, 3, 3, 3, 4, 3, 2, 5, 5, 3, 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;
|
|
}
|
|
|
|
// F5V3 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 1: // Pressure adjustment
|
|
this->AddEvent(new PRS1EPAPSetEvent(t, data[pos++], GAIN));
|
|
this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos-1, size+1)); // TODO: what is this?
|
|
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 PRS1IPAPAverageEvent(t, data[pos+0], GAIN)); // 00=IPAP
|
|
this->AddEvent(new PRS1IPAPLowEvent(t, data[pos+1], GAIN)); // 01=IAP Low
|
|
this->AddEvent(new PRS1IPAPHighEvent(t, data[pos+2], GAIN)); // 02=IAP High
|
|
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 PRS1SnoreEvent(t, data[pos+8])); // 08=Snore count // TODO: not a VS on official waveform, but appears in flags and contributes to overall VS index
|
|
this->AddEvent(new PRS1EPAPAverageEvent(t, data[pos+9], GAIN)); // 09=EPAP average
|
|
this->AddEvent(new PRS1LeakEvent(t, data[pos+0xa])); // 0A=Leak (average?)
|
|
this->AddEvent(new PRS1IntervalBoundaryEvent(t));
|
|
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?
|
|
//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 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));
|
|
elapsed = data[pos+2];
|
|
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));
|
|
elapsed = data[pos+2];
|
|
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:
|
|
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;
|
|
}
|