OSCAR-code/oscar/SleepLib/loader_plugins/prs1_parser_asv.cpp
sawinglogz 184fe0d2ff Add 961TCA to the list of tested machines.
In doing so, add PP, CA, and VS support to F5V2 generally,
which so far is just the 960T. We still haven't encountered
LL on an F5V2.
2022-04-24 17:02:25 -04:00

1443 lines
71 KiB
C++

/* PRS1 Parsing for BiPAP autoSV (ASV) (Family 5)
*
* Copyright (c) 2019-2022 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"
//********************************************************************************************
// 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 devices 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 0xb in F5V2 and 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 0xc in F5V2 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 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,
PRS1PressurePulseEvent::TYPE,
PRS1TimedBreathEvent::TYPE,
PRS1ObstructiveApneaEvent::TYPE,
PRS1ClearAirwayEvent::TYPE,
PRS1HypopneaEvent::TYPE,
PRS1FlowLimitationEvent::TYPE,
PRS1VibratorySnoreEvent::TYPE,
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}, {8,3}, {9,4}, {0xa,3}, {0xb,2}, {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);
pos += 2;
}
switch (code) {
//case 0x00: // never seen on F5V2
//case 0x01: // never seen on F5V2
case 0x02: // Pressure adjustment
this->AddEvent(new PRS1EPAPSetEvent(t, data[pos++], GAIN));
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
// 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: // never seen on F5V2
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: // Vibratory Snore, note this is 0xa in F5V1 and 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 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: // never seen on F5V2
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;
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 device-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 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, 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 device 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_VALUES(data[pos+2], 0, 4); // Apnea Alarm, 0 = off, 4 = 40
CHECK_VALUE(data[pos+3], 1);
CHECK_VALUE(data[pos+4], 1);
if (data[pos+5] > 3) {
UNEXPECTED_VALUE(data[pos+5], "0-3"); // Low Minute Ventilation Alarm, 0 = off, 1-3 = 1-3
}
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 > 30) UNEXPECTED_VALUE(timed_inspiration, "12-30");
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;
}