2021-06-01 00:24:09 +00:00
/* PRS1 Parsing for CPAP and BIPAP (Family 0)
*
2021-11-02 20:34:12 +00:00
* Copyright ( c ) 2019 - 2022 The OSCAR Team
2021-06-01 00:24:09 +00:00
* 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"
2021-06-01 01:23:03 +00:00
//********************************************************************************************
// MARK: 50 Series
2021-06-01 00:41:44 +00:00
bool PRS1DataChunk : : ParseComplianceF0V23 ( void )
{
if ( this - > family ! = 0 | | ( this - > familyVersion ! = 2 & & this - > familyVersion ! = 3 ) ) {
qWarning ( ) < < " ParseComplianceF0V23 called with family " < < this - > family < < " familyVersion " < < this - > familyVersion ;
return false ;
}
2022-02-27 16:50:10 +00:00
// All sample devices with FamilyVersion 3 in the properties.txt file have familyVersion 2 in their .001/.002/.005 files!
2021-06-01 00:41:44 +00:00
// We should flag an actual familyVersion 3 file if we ever encounter one!
CHECK_VALUE ( this - > familyVersion , 2 ) ;
const unsigned char * data = ( unsigned char * ) this - > m_data . constData ( ) ;
int chunk_size = this - > m_data . size ( ) ;
static const int minimum_sizes [ ] = { 0xd , 5 , 2 , 2 } ;
static const int ncodes = sizeof ( minimum_sizes ) / sizeof ( int ) ;
// NOTE: These are fixed sizes, but are called minimum to more closely match the F0V6 parser.
bool ok = true ;
int pos = 0 ;
int code , size , delta ;
int tt = 0 ;
while ( ok & & pos < chunk_size ) {
code = data [ pos + + ] ;
// There is no hblock prior to F0V6.
size = 0 ;
if ( code < ncodes ) {
// make sure the handlers below don't go past the end of the buffer
size = minimum_sizes [ code ] ;
} // else if it's past ncodes, we'll log its information below (rather than handle it)
if ( pos + size > chunk_size ) {
qWarning ( ) < < this - > sessionid < < " slice " < < code < < " @ " < < pos < < " longer than remaining chunk " ;
ok = false ;
break ;
}
switch ( code ) {
case 0 : // Equipment On
CHECK_VALUE ( pos , 1 ) ; // Always first
CHECK_VALUES ( data [ pos ] , 1 , 0 ) ; // usually 1, occasionally 0, no visible difference in report
// F0V23 doesn't have a separate settings record like F0V6 does, the settings just follow the EquipmentOn data.
ok = ParseSettingsF0V23 ( data , 0x0e ) ;
// Compliance doesn't have pressure set events following settings like summary does.
break ;
case 2 : // Mask On
delta = data [ pos ] | ( data [ pos + 1 ] < < 8 ) ;
if ( tt = = 0 ) {
CHECK_VALUE ( delta , 0 ) ; // we've never seen the initial MaskOn have any delta
} else {
if ( delta % 60 ) UNEXPECTED_VALUE ( delta , " even minutes " ) ; // mask-off events seem to be whole minutes?
}
tt + = delta ;
this - > AddEvent ( new PRS1ParsedSliceEvent ( tt , MaskOn ) ) ;
// no per-slice humidifer settings as in F0V6
break ;
case 3 : // Mask Off
tt + = data [ pos ] | ( data [ pos + 1 ] < < 8 ) ;
this - > AddEvent ( new PRS1ParsedSliceEvent ( tt , MaskOff ) ) ;
// Compliance doesn't record any stats after mask-off like summary does.
break ;
case 1 : // Equipment Off
tt + = data [ pos ] | ( data [ pos + 1 ] < < 8 ) ;
this - > AddEvent ( new PRS1ParsedSliceEvent ( tt , EquipmentOff ) ) ;
// also seems to be a trailing 01 00 81 after the slices?
CHECK_VALUES ( data [ pos + 2 ] , 1 , 0 ) ; // usually 1, occasionally 0, no visible difference in report
//CHECK_VALUE(data[pos+3], 0); // sometimes 1, 2, or 5, no visible difference in report, maybe ramp?
ParseHumidifierSetting50Series ( data [ pos + 4 ] ) ;
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 : : ParseSummaryF0V23 ( )
{
if ( this - > family ! = 0 | | ( this - > familyVersion ! = 2 & & this - > familyVersion ! = 3 ) ) {
qWarning ( ) < < " ParseSummaryF0V23 called with family " < < this - > family < < " familyVersion " < < this - > familyVersion ;
return false ;
}
2022-02-27 16:50:10 +00:00
// All sample devices with FamilyVersion 3 in the properties.txt file have familyVersion 2 in their .001/.002/.005 files!
2021-06-01 00:41:44 +00:00
// We should flag an actual familyVersion 3 file if we ever encounter one!
CHECK_VALUE ( this - > familyVersion , 2 ) ;
const unsigned char * data = ( unsigned char * ) this - > m_data . constData ( ) ;
int chunk_size = this - > m_data . size ( ) ;
static const int minimum_sizes [ ] = { 0xf , 5 , 2 , 0x21 , 0 , 4 } ;
static const int ncodes = sizeof ( minimum_sizes ) / sizeof ( int ) ;
// NOTE: These are fixed sizes, but are called minimum to more closely match the F0V6 parser.
bool ok = true ;
int pos = 0 ;
int code , size , delta ;
int tt = 0 ;
while ( ok & & pos < chunk_size ) {
code = data [ pos + + ] ;
// There is no hblock prior to F0V6.
size = 0 ;
if ( code < ncodes ) {
// make sure the handlers below don't go past the end of the buffer
size = minimum_sizes [ code ] ;
} // else if it's past ncodes, we'll log its information below (rather than handle it)
if ( pos + size > chunk_size ) {
qWarning ( ) < < this - > sessionid < < " slice " < < code < < " @ " < < pos < < " longer than remaining chunk " ;
ok = false ;
break ;
}
switch ( code ) {
case 0 : // Equipment On
CHECK_VALUE ( pos , 1 ) ; // Always first
CHECK_VALUES ( data [ pos ] & 0xF0 , 0x60 , 0x70 ) ; // TODO: what are these?
switch ( data [ pos ] & 0x0F ) {
case 0 : // TODO: What is this? It seems to be related to errors.
case 1 : // This is the most frequent value.
case 3 : // TODO: What is this?
case 4 : // This seems to be related to an automatic transition from CPAP to AutoCPAP.
break ;
default :
UNEXPECTED_VALUE ( data [ pos ] & 0x0F , " [0,1,3,4] " ) ;
}
// F0V23 doesn't have a separate settings record like F0V6 does, the settings just follow the EquipmentOn data.
ok = ParseSettingsF0V23 ( data , 0x0e ) ;
// TODO: register these as pressure set events
//CHECK_VALUES(data[0x0e], ramp_pressure, min_pressure); // initial CPAP/EPAP, can be minimum pressure or ramp, or whatever auto decides to use
//if (cpapmode == PRS1_MODE_BILEVEL) { // initial IPAP for bilevel modes
// CHECK_VALUE(data[0x0f], max_pressure);
//} else if (cpapmode == PRS1_MODE_AUTOBILEVEL) {
// CHECK_VALUE(data[0x0f], min_pressure + 20);
//}
break ;
case 2 : // Mask On
delta = data [ pos ] | ( data [ pos + 1 ] < < 8 ) ;
if ( tt = = 0 ) {
if ( delta ) {
CHECK_VALUES ( delta , 1 , 59 ) ; // we've seen the 550P start its first mask-on at these time deltas
}
} else {
if ( delta % 60 ) {
if ( this - > familyVersion = = 2 & & ( ( delta + 1 ) % 60 ) = = 0 ) {
// For some reason F0V2 frequently is frequently 1 second less than whole minute intervals.
} else {
UNEXPECTED_VALUE ( delta , " even minutes " ) ; // mask-off events seem to be whole minutes?
}
}
}
tt + = delta ;
this - > AddEvent ( new PRS1ParsedSliceEvent ( tt , MaskOn ) ) ;
// no per-slice humidifer settings as in F0V6
break ;
case 3 : // Mask Off
tt + = data [ pos ] | ( data [ pos + 1 ] < < 8 ) ;
this - > AddEvent ( new PRS1ParsedSliceEvent ( tt , MaskOff ) ) ;
// F0V23 doesn't have a separate stats record like F0V6 does, the stats just follow the MaskOff data.
// These are 0x22 bytes in a summary vs. 3 bytes in compliance data
// TODO: What are these values?
break ;
case 1 : // Equipment Off
tt + = data [ pos ] | ( data [ pos + 1 ] < < 8 ) ;
this - > AddEvent ( new PRS1ParsedSliceEvent ( tt , EquipmentOff ) ) ;
switch ( data [ pos + 2 ] ) {
case 0 : // TODO: What is this? It seems to be related to errors.
case 1 : // This is the usual value.
case 3 : // TODO: What is this? This has been seen after 90 sec large leak before turning off.
case 4 : // TODO: What is this? We've seen it once.
case 5 : // This seems to be related to an automatic transition from CPAP to AutoCPAP.
break ;
default :
UNEXPECTED_VALUE ( data [ pos + 2 ] , " [0,1,3,4,5] " ) ;
}
//CHECK_VALUES(data[pos+3], 0, 1); // TODO: may be related to ramp? 1-5 seems to have a ramp start or two
ParseHumidifierSetting50Series ( data [ pos + 4 ] ) ;
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 ;
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 : : ParseSettingsF0V23 ( const unsigned char * data , int /*size*/ )
{
PRS1Mode cpapmode = PRS1_MODE_UNKNOWN ;
switch ( data [ 0x02 ] ) { // PRS1 mode // 0 = CPAP, 2 = APAP
case 0x00 :
cpapmode = PRS1_MODE_CPAP ;
break ;
case 0x01 :
cpapmode = PRS1_MODE_BILEVEL ;
break ;
case 0x02 :
cpapmode = PRS1_MODE_AUTOCPAP ;
break ;
case 0x03 :
cpapmode = PRS1_MODE_AUTOBILEVEL ;
break ;
default :
UNEXPECTED_VALUE ( data [ 0x02 ] , " known device mode " ) ;
break ;
}
this - > AddEvent ( new PRS1ParsedSettingEvent ( PRS1_SETTING_CPAP_MODE , ( int ) cpapmode ) ) ;
int min_pressure = data [ 0x03 ] ;
int max_pressure = data [ 0x04 ] ;
int ps = data [ 0x05 ] ; // max pressure support (for variable), seems to be zero otherwise
if ( cpapmode = = PRS1_MODE_CPAP ) {
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_PRESSURE , min_pressure ) ) ;
//CHECK_VALUE(max_pressure, 0); // occasionally nonzero, usually seems to be when the next session is AutoCPAP with this max
CHECK_VALUE ( ps , 0 ) ;
} else if ( cpapmode = = PRS1_MODE_AUTOCPAP ) {
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_PRESSURE_MIN , min_pressure ) ) ;
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_PRESSURE_MAX , max_pressure ) ) ;
CHECK_VALUE ( ps , 0 ) ;
} else if ( cpapmode = = PRS1_MODE_BILEVEL ) {
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_EPAP , min_pressure ) ) ;
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_IPAP , max_pressure ) ) ;
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_PS , max_pressure - min_pressure ) ) ;
CHECK_VALUE ( ps , 0 ) ; // this seems to be unused on fixed bilevel
} else if ( cpapmode = = PRS1_MODE_AUTOBILEVEL ) {
int min_ps = 20 ; // 2.0 cmH2O
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_EPAP_MIN , min_pressure ) ) ;
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_EPAP_MAX , max_pressure - min_ps ) ) ; // TODO: not yet confirmed
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_IPAP_MIN , min_pressure + min_ps ) ) ;
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_IPAP_MAX , max_pressure ) ) ;
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_PS_MIN , min_ps ) ) ;
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_PS_MAX , ps ) ) ;
}
int ramp_time = data [ 0x06 ] ;
int ramp_pressure = data [ 0x07 ] ;
if ( ramp_time > 0 ) {
this - > AddEvent ( new PRS1ParsedSettingEvent ( PRS1_SETTING_RAMP_TIME , ramp_time ) ) ;
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_RAMP_PRESSURE , ramp_pressure ) ) ;
}
quint8 flex = data [ 0x08 ] ;
this - > ParseFlexSettingF0V2345 ( flex , cpapmode ) ;
int humid = data [ 0x09 ] ;
this - > ParseHumidifierSetting50Series ( humid , true ) ;
// Tubing lock has no setting byte
// Menu Options
bool mask_resist_on = ( ( data [ 0x0a ] & 0x40 ) ! = 0 ) ; // System One Resistance Status bit
int mask_resist_setting = data [ 0x0a ] & 7 ; // System One Resistance setting value
CHECK_VALUE ( mask_resist_on , mask_resist_setting > 0 ) ; // Confirm that we can ignore the status bit.
this - > AddEvent ( new PRS1ParsedSettingEvent ( PRS1_SETTING_MASK_RESIST_LOCK , ( data [ 0x0a ] & 0x80 ) ! = 0 ) ) ; // System One Resistance Lock Setting, only seen on bricks
this - > AddEvent ( new PRS1ParsedSettingEvent ( PRS1_SETTING_HOSE_DIAMETER , ( data [ 0x0a ] & 0x08 ) ? 15 : 22 ) ) ; // TODO: unconfirmed
this - > AddEvent ( new PRS1ParsedSettingEvent ( PRS1_SETTING_MASK_RESIST_SETTING , mask_resist_setting ) ) ;
CHECK_VALUE ( data [ 0x0a ] & ( 0x20 | 0x10 ) , 0 ) ;
CHECK_VALUE ( data [ 0x0b ] , 1 ) ;
this - > AddEvent ( new PRS1ParsedSettingEvent ( PRS1_SETTING_AUTO_ON , ( data [ 0x0c ] & 0x40 ) ! = 0 ) ) ;
this - > AddEvent ( new PRS1ParsedSettingEvent ( PRS1_SETTING_AUTO_OFF , ( data [ 0x0c ] & 0x10 ) ! = 0 ) ) ;
this - > AddEvent ( new PRS1ParsedSettingEvent ( PRS1_SETTING_MASK_ALERT , ( data [ 0x0c ] & 0x04 ) ! = 0 ) ) ;
this - > AddEvent ( new PRS1ParsedSettingEvent ( PRS1_SETTING_SHOW_AHI , ( data [ 0x0c ] & 0x02 ) ! = 0 ) ) ;
CHECK_VALUE ( data [ 0x0c ] & ( 0xA0 | 0x09 ) , 0 ) ;
CHECK_VALUE ( data [ 0x0d ] , 0 ) ;
return true ;
}
2021-06-01 00:39:44 +00:00
// Flex F0V2 confirmed
// 0x00 = None
// 0x81 = C-Flex 1, lock off (AutoCPAP mode)
// 0x82 = Bi-Flex 2 (Bi-Level mode)
// 0x89 = A-Flex 1 (AutoCPAP mode)
// 0x8A = A-Flex 2, lock off (AutoCPAP mode)
// 0x8B = C-Flex+ 3, lock off (CPAP mode)
// 0x93 = Rise Time 3 (AutoBiLevel mode)
// Flex F0V4 confirmed
// 0x00 = None
// 0x81 = Bi-Flex 1 (AutoBiLevel mode)
// 0x81 = C-Flex 1 (AutoCPAP mode)
// 0x82 = C-Flex 2 (CPAP mode)
// 0x82 = C-Flex 2 (CPAP-Check mode)
// 0x82 = C-Flex 2 (Auto-Trial mode)
// 0x83 = Bi-Flex 3 (Bi-Level mode)
// 0x89 = A-Flex 1 (AutoCPAP mode)
// 0x8A = C-Flex+ 2 (CPAP mode)
// 0x8A = C-Flex+ 2, lock off (CPAP-Check mode)
// 0x8A = A-Flex 2, lock off (Auto-Trial mode)
// 0xCB = C-Flex+ 3 (CPAP-Check mode), C-Flex+ Lock on
//
// 0x8A = A-Flex 1 (AutoCPAP mode)
// 0x8B = C-Flex+ 3 (CPAP mode)
// 0x8B = A-Flex 3 (AutoCPAP mode)
// Flex F0V5 confirmed
// 0xE1 = Flex (AutoCPAP mode)
// 0xA1 = Flex (AutoCPAP mode)
// 0xA2 = Flex (AutoCPAP mode)
// 8 = enabled
// 4 = lock
// 2 = Flex (only seen on Dorma series)
// 1 = rise time
// 8 = C-Flex+ / A-Flex (depending on mode)
// 3 = level
void PRS1DataChunk : : ParseFlexSettingF0V2345 ( quint8 flex , int cpapmode )
{
FlexMode flexmode = FLEX_None ;
bool enabled = ( flex & 0x80 ) ! = 0 ;
bool lock = ( flex & 0x40 ) ! = 0 ;
bool plain_flex = ( flex & 0x20 ) ! = 0 ; // "Flex", seen on Dorma series
bool risetime = ( flex & 0x10 ) ! = 0 ;
bool plusmode = ( flex & 0x08 ) ! = 0 ;
int flexlevel = flex & 0x03 ;
if ( flex & 0x04 ) UNEXPECTED_VALUE ( flex , " known bits " ) ;
if ( this - > familyVersion = = 2 ) {
//CHECK_VALUE(lock, false); // We've seen this set on F0V2, but it doesn't appear on the reports.
}
if ( enabled ) {
if ( flexlevel < 1 ) UNEXPECTED_VALUE ( flexlevel , " != 0 " ) ;
if ( risetime ) {
flexmode = FLEX_RiseTime ;
CHECK_VALUES ( cpapmode , PRS1_MODE_BILEVEL , PRS1_MODE_AUTOBILEVEL ) ;
CHECK_VALUE ( plusmode , 0 ) ;
} else if ( plusmode ) {
switch ( cpapmode ) {
case PRS1_MODE_CPAP :
case PRS1_MODE_CPAPCHECK :
flexmode = FLEX_CFlexPlus ;
break ;
case PRS1_MODE_AUTOCPAP :
case PRS1_MODE_AUTOTRIAL :
flexmode = FLEX_AFlex ;
break ;
default :
HEX ( flex ) ;
UNEXPECTED_VALUE ( cpapmode , " expected C-Flex+/A-Flex mode " ) ;
break ;
}
} else if ( plain_flex ) {
CHECK_VALUE ( this - > familyVersion , 5 ) ; // so far only seen with F0V5
switch ( cpapmode ) {
case PRS1_MODE_AUTOCPAP :
flexmode = FLEX_Flex ; // unknown whether this is equivalent to C-Flex, C-Flex+, or A-Flex
break ;
default :
UNEXPECTED_VALUE ( cpapmode , " expected mode " ) ;
flexmode = FLEX_Flex ; // probably the same for CPAP mode as well, but we haven't tested that yet
break ;
}
} else {
switch ( cpapmode ) {
case PRS1_MODE_CPAP :
case PRS1_MODE_CPAPCHECK :
case PRS1_MODE_AUTOCPAP :
case PRS1_MODE_AUTOTRIAL :
flexmode = FLEX_CFlex ;
break ;
case PRS1_MODE_BILEVEL :
case PRS1_MODE_AUTOBILEVEL :
flexmode = FLEX_BiFlex ;
break ;
default :
HEX ( flex ) ;
UNEXPECTED_VALUE ( cpapmode , " expected mode " ) ;
break ;
}
}
}
this - > AddEvent ( new PRS1ParsedSettingEvent ( PRS1_SETTING_FLEX_MODE , ( int ) flexmode ) ) ;
if ( flexmode ! = FLEX_None ) {
this - > AddEvent ( new PRS1ParsedSettingEvent ( PRS1_SETTING_FLEX_LEVEL , flexlevel ) ) ;
}
this - > AddEvent ( new PRS1ParsedSettingEvent ( PRS1_SETTING_FLEX_LOCK , lock ) ) ;
}
2021-06-01 00:41:44 +00:00
const QVector < PRS1ParsedEventType > ParsedEventsF0V23 = {
PRS1PressureSetEvent : : TYPE ,
PRS1IPAPSetEvent : : TYPE ,
PRS1EPAPSetEvent : : TYPE ,
PRS1PressurePulseEvent : : TYPE ,
PRS1RERAEvent : : TYPE ,
PRS1ObstructiveApneaEvent : : TYPE ,
PRS1ClearAirwayEvent : : TYPE ,
PRS1HypopneaEvent : : TYPE ,
PRS1FlowLimitationEvent : : TYPE ,
PRS1VibratorySnoreEvent : : TYPE ,
PRS1VariableBreathingEvent : : TYPE ,
PRS1PeriodicBreathingEvent : : TYPE ,
PRS1LargeLeakEvent : : TYPE ,
PRS1TotalLeakEvent : : TYPE ,
PRS1SnoreEvent : : TYPE ,
PRS1SnoresAtPressureEvent : : TYPE ,
} ;
// 750P is F0V2; 550P is F0V2/F0V3 (properties.txt sometimes says F0V3, data files always say F0V2); 450P is F0V3
bool PRS1DataChunk : : ParseEventsF0V23 ( )
{
if ( this - > family ! = 0 | | this - > familyVersion < 2 | | this - > familyVersion > 3 ) {
qWarning ( ) < < " ParseEventsF0V23 called with family " < < this - > family < < " familyVersion " < < this - > familyVersion ;
return false ;
}
2022-02-27 16:50:10 +00:00
// All sample devices with FamilyVersion 3 in the properties.txt file have familyVersion 2 in their .001/.002/.005 files!
2021-06-01 00:41:44 +00:00
// We should flag an actual familyVersion 3 file if we ever encounter one!
CHECK_VALUE ( this - > familyVersion , 2 ) ;
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 } , { 0xb , 4 } , { 0xd , 2 } , { 0xe , 5 } , { 0xf , 5 } , { 0x10 , 5 } , { 0x11 , 4 } , { 0x12 , 4 } } ;
if ( chunk_size < 1 ) {
// This does occasionally happen in F0V6.
qDebug ( ) < < this - > sessionid < < " Empty event data " ;
return false ;
}
bool ok = true ;
int pos = 0 , startpos ;
int code , size ;
int t = 0 ;
int elapsed , duration , value ;
do {
code = data [ pos + + ] ;
size = 3 ; // default size = 2 bytes time delta + 1 byte data
if ( event_sizes . contains ( code ) ) {
size = event_sizes [ code ] ;
}
if ( pos + size > chunk_size ) {
qWarning ( ) < < this - > sessionid < < " event " < < code < < " @ " < < pos < < " longer than remaining chunk " ;
ok = false ;
break ;
}
startpos = pos ;
if ( code ! = 0x12 & & code ! = 0x01 ) { // This one event has no timestamp in F0V6
elapsed = data [ pos ] | ( data [ pos + 1 ] < < 8 ) ;
if ( elapsed > 0x7FFF ) UNEXPECTED_VALUE ( elapsed , " <32768s " ) ; // check whether this is generally unsigned, since 0x01 isn't
t + = elapsed ;
pos + = 2 ;
}
switch ( code ) {
case 0x00 : // Humidifier setting change (logged in summary in 60 series)
ParseHumidifierSetting50Series ( data [ pos ] ) ;
if ( this - > familyVersion = = 3 ) DUMP_EVENT ( ) ;
break ;
case 0x01 : // Time elapsed?
// Only seen twice, on a 550P and 650P.
// It looks almost like a time-elapsed event 4 found in F0V4 summaries, but
// 0xFFCC looks like it represents a time adjustment of -52 seconds,
// since the subsequent 0x11 statistics event has a time offset of 172 seconds,
// and counting this as -52 seconds results in a total session time that
// matches the summary and waveform data. Very weird.
//
// Similarly 0xFFDC looks like it represents a time adjustment of -36 seconds.
CHECK_VALUES ( data [ pos ] , 0xDC , 0xCC ) ;
CHECK_VALUE ( data [ pos + 1 ] , 0xFF ) ;
elapsed = data [ pos ] | ( data [ pos + 1 ] < < 8 ) ;
if ( elapsed & 0x8000 ) {
elapsed = ( ~ 0xFFFF | elapsed ) ; // sign extend 16-bit number to native int
}
t + = elapsed ;
break ;
case 0x02 : // Pressure adjustment
// See notes in ParseEventsF0V6.
this - > AddEvent ( new PRS1PressureSetEvent ( t , data [ pos ] ) ) ;
break ;
case 0x03 : // Pressure adjustment (bi-level)
// See notes in ParseEventsF0V6.
this - > AddEvent ( new PRS1IPAPSetEvent ( t , data [ pos + 1 ] ) ) ;
this - > AddEvent ( new PRS1EPAPSetEvent ( t , data [ pos ] ) ) ; // EPAP needs to be added second to calculate PS
break ;
case 0x04 : // Pressure Pulse
duration = data [ pos ] ; // TODO: is this a duration?
this - > AddEvent ( new PRS1PressurePulseEvent ( t , duration ) ) ;
break ;
case 0x05 : // RERA
elapsed = data [ pos + + ] ;
this - > AddEvent ( new PRS1RERAEvent ( t - elapsed , 0 ) ) ;
break ;
case 0x06 : // Obstructive Apnea
// OA events are instantaneous flags with no duration: reviewing waveforms
// shows that the time elapsed between the flag and reporting often includes
// non-apnea breathing.
elapsed = data [ pos ] ;
this - > AddEvent ( new PRS1ObstructiveApneaEvent ( t - elapsed , 0 ) ) ;
break ;
case 0x07 : // Clear Airway Apnea
// CA events are instantaneous flags with no duration: reviewing waveforms
// shows that the time elapsed between the flag and reporting often includes
// non-apnea breathing.
elapsed = data [ pos ] ;
this - > AddEvent ( new PRS1ClearAirwayEvent ( t - elapsed , 0 ) ) ;
break ;
//case 0x08: // never seen
//case 0x09: // never seen
case 0x0a : // Hypopnea
// TODO: How is this hypopnea different from events 0xb, [0x14 and 0x15 on F0V6]?
elapsed = data [ pos + + ] ;
this - > AddEvent ( new PRS1HypopneaEvent ( t - elapsed , 0 ) ) ;
break ;
case 0x0b : // Hypopnea
// TODO: How is this hypopnea different from events 0xa, [0x14 and 0x15 on F0V6]?
// TODO: What is the first byte?
//data[pos+0]; // unknown first byte?
elapsed = data [ pos + 1 ] ; // based on sample waveform, the hypopnea is over after this
this - > AddEvent ( new PRS1HypopneaEvent ( t - elapsed , 0 ) ) ;
break ;
case 0x0c : // Flow Limitation
// TODO: We should revisit whether this is elapsed or duration once (if)
// we start calculating flow limitations ourselves. Flow limitations aren't
// as obvious as OA/CA when looking at a waveform.
elapsed = data [ pos ] ;
this - > AddEvent ( new PRS1FlowLimitationEvent ( t - elapsed , 0 ) ) ;
break ;
case 0x0d : // Vibratory Snore
// VS events are instantaneous flags with no duration, drawn on the official waveform.
// The current thinking is that these are the snores that cause a change in auto-titrating
// pressure. The snoring statistics below seem to be a total count. It's unclear whether
// the trigger for pressure change is severity or count or something else.
// no data bytes
this - > AddEvent ( new PRS1VibratorySnoreEvent ( t , 0 ) ) ;
break ;
case 0x0e : // Variable Breathing?
// TODO: does duration double like F0V4?
duration = ( data [ pos ] | ( data [ pos + 1 ] < < 8 ) ) ;
elapsed = data [ pos + 2 ] ; // this is always 60 seconds unless it's at the end, so it seems like elapsed
CHECK_VALUES ( elapsed , 60 , 0 ) ;
this - > AddEvent ( new PRS1VariableBreathingEvent ( t - elapsed - duration , duration ) ) ;
break ;
case 0x0f : // Periodic Breathing
// PB events are reported some time after they conclude, and they do have a reported duration.
// NOTE: F0V2 does NOT double this like F0V6 does
if ( this - > familyVersion = = 3 ) // double-check whether there's doubling on F0V3
DUMP_EVENT ( ) ;
duration = ( data [ pos ] | ( data [ pos + 1 ] < < 8 ) ) ;
elapsed = data [ pos + 2 ] ;
this - > AddEvent ( new PRS1PeriodicBreathingEvent ( t - elapsed - duration , duration ) ) ;
break ;
case 0x10 : // Large Leak
// LL events are reported some time after they conclude, and they do have a reported duration.
// NOTE: F0V2 does NOT double this like F0V4 and F0V6 does
if ( this - > familyVersion = = 3 ) // double-check whether there's doubling on F0V3
DUMP_EVENT ( ) ;
duration = ( data [ pos ] | ( data [ pos + 1 ] < < 8 ) ) ;
elapsed = data [ pos + 2 ] ;
this - > AddEvent ( new PRS1LargeLeakEvent ( t - elapsed - duration , duration ) ) ;
break ;
case 0x11 : // Statistics
this - > AddEvent ( new PRS1TotalLeakEvent ( t , data [ pos ] ) ) ;
this - > AddEvent ( new PRS1SnoreEvent ( t , data [ pos + 1 ] ) ) ;
this - > AddEvent ( new PRS1IntervalBoundaryEvent ( t ) ) ;
break ;
case 0x12 : // Snore count per pressure
// Some sessions (with lots of ramps) have multiple of these, each with a
// different pressure. The total snore count across all of them matches the
// total found in the stats event.
if ( data [ pos ] ! = 0 ) {
CHECK_VALUES ( data [ pos ] , 1 , 2 ) ; // 0 = CPAP pressure, 1 = bi-level EPAP, 2 = bi-level IPAP
}
//CHECK_VALUE(data[pos+1], 0x78); // pressure
//CHECK_VALUE(data[pos+2], 1); // 16-bit snore count
//CHECK_VALUE(data[pos+3], 0);
value = ( data [ pos + 2 ] | ( data [ pos + 3 ] < < 8 ) ) ;
this - > AddEvent ( new PRS1SnoresAtPressureEvent ( t , data [ pos ] , data [ pos + 1 ] , value ) ) ;
break ;
default :
DUMP_EVENT ( ) ;
UNEXPECTED_VALUE ( code , " known event code " ) ;
this - > AddEvent ( new PRS1UnknownDataEvent ( m_data , startpos ) ) ;
ok = false ; // unlike F0V6, we don't know the size of unknown events, so we can't recover
break ;
}
pos = startpos + size ;
} while ( ok & & pos < chunk_size ) ;
if ( ok & & pos ! = chunk_size ) {
qWarning ( ) < < this - > sessionid < < ( this - > size ( ) - pos ) < < " trailing event bytes " ;
}
this - > duration = t ;
return ok ;
}
2021-06-01 01:23:03 +00:00
//********************************************************************************************
// MARK: -
// MARK: 60 Series
2021-06-01 00:39:44 +00:00
bool PRS1DataChunk : : ParseComplianceF0V4 ( void )
{
if ( this - > family ! = 0 | | ( this - > familyVersion ! = 4 ) ) {
qWarning ( ) < < " ParseComplianceF0V4 called with family " < < this - > family < < " familyVersion " < < this - > familyVersion ;
return false ;
}
const unsigned char * data = ( unsigned char * ) this - > m_data . constData ( ) ;
int chunk_size = this - > m_data . size ( ) ;
static const int minimum_sizes [ ] = { 0x18 , 7 , 4 , 2 , 0 , 0 , 0 , 4 , 0 } ;
static const int ncodes = sizeof ( minimum_sizes ) / sizeof ( int ) ;
// NOTE: These are fixed sizes, but are called minimum to more closely match the F0V6 parser.
bool ok = true ;
int pos = 0 ;
int code , size ;
int tt = 0 ;
while ( ok & & pos < chunk_size ) {
code = data [ pos + + ] ;
// There is no hblock prior to F0V6.
size = 0 ;
if ( code < ncodes ) {
// make sure the handlers below don't go past the end of the buffer
size = minimum_sizes [ code ] ;
} // else if it's past ncodes, we'll log its information below (rather than handle it)
if ( pos + size > chunk_size ) {
qWarning ( ) < < this - > sessionid < < " slice " < < code < < " @ " < < pos < < " longer than remaining chunk " ;
ok = false ;
break ;
}
switch ( code ) {
case 0 : // Equipment On
CHECK_VALUE ( pos , 1 ) ; // Always first
CHECK_VALUES ( data [ pos ] , 1 , 3 ) ;
// F0V4 doesn't have a separate settings record like F0V6 does, the settings just follow the EquipmentOn data.
ok = ParseSettingsF0V45 ( data , 0x11 ) ;
CHECK_VALUE ( data [ pos + 0x11 ] , 0 ) ;
CHECK_VALUE ( data [ pos + 0x12 ] , 0 ) ;
CHECK_VALUE ( data [ pos + 0x13 ] , 0 ) ;
CHECK_VALUE ( data [ pos + 0x14 ] , 0 ) ;
CHECK_VALUE ( data [ pos + 0x15 ] , 0 ) ;
CHECK_VALUE ( data [ pos + 0x16 ] , 0 ) ;
CHECK_VALUE ( data [ pos + 0x17 ] , 0 ) ;
break ;
case 2 : // Mask On
tt + = data [ pos ] | ( data [ pos + 1 ] < < 8 ) ;
this - > AddEvent ( new PRS1ParsedSliceEvent ( tt , MaskOn ) ) ;
this - > ParseHumidifierSetting60Series ( data [ pos + 2 ] , data [ pos + 3 ] ) ;
break ;
case 3 : // Mask Off
tt + = data [ pos ] | ( data [ pos + 1 ] < < 8 ) ;
this - > AddEvent ( new PRS1ParsedSliceEvent ( tt , MaskOff ) ) ;
// Compliance doesn't have any MaskOff stats like summary does
break ;
case 1 : // Equipment Off
tt + = data [ pos ] | ( data [ pos + 1 ] < < 8 ) ;
this - > AddEvent ( new PRS1ParsedSliceEvent ( tt , EquipmentOff ) ) ;
2021-11-29 15:07:11 +00:00
//CHECK_VALUES(data[pos+2], 1, 3); // or 0
2021-06-01 00:39:44 +00:00
//CHECK_VALUE(data[pos+2] & ~(0x40|8|4|2|1), 0); // ???, seen various bit combinations
//CHECK_VALUE(data[pos+3], 0x19); // 0x17, 0x16
2021-11-29 15:07:11 +00:00
//CHECK_VALUES(data[pos+4], 0, 1); // or 2
2021-06-01 00:39:44 +00:00
//CHECK_VALUES(data[pos+4], 0, 1); // or 2
//CHECK_VALUE(data[pos+5], 0x35); // 0x36, 0x36
if ( data [ pos + 6 ] ! = 1 ) {
CHECK_VALUE ( data [ pos + 6 ] & ~ ( 4 | 2 | 1 ) , 0 ) ; // On F0V23 0 seems to be related to errors, 3 seen after 90 sec large leak before turning off?
}
// pos+4 == 2, pos+6 == 10 on the session that had a time-elapsed event, maybe it shut itself off
// when approaching 24h of continuous use?
break ;
/*
case 4 : // Time Elapsed
// For example: mask-on 5:18:49 in a session of 23:41:20 total leaves mask-off time of 18:22:31.
// That's represented by a mask-off event 19129 seconds after the mask-on, then a time-elapsed
// event after 65535 seconds, then an equipment off event after another 616 seconds.
tt + = data [ pos ] | ( data [ pos + 1 ] < < 8 ) ;
// TODO: see if this event exists in earlier versions
break ;
case 5 : // Clock adjustment?
CHECK_VALUE ( pos , 1 ) ; // Always first
CHECK_VALUE ( chunk_size , 5 ) ; // and the only record in the session.
// This looks like it's minor adjustments to the clock, but 560PBT-3917 sessions 1-2 are weird:
// session 1 starts at 2015-12-23T00:01:20 and contains this event with timestamp 2015-12-23T00:05:14.
// session 2 starts at 2015-12-23T00:01:29, which suggests the event didn't change the clock.
//
// It looks like this happens when there are discontinuities in timestamps, for example 560P-4727:
// session 58 ends at 2015-05-26T09:53:17.
// session 59 starts at 2015-05-26T09:53:15 with an event 5 timestamp of 2015-05-26T09:53:18.
//
// So the session/chunk timestamp has gone backwards. Whenever this happens, it seems to be in
// a session with an event-5 event having a timestamp that hasn't gone backwards. So maybe
// this timestamp is the old clock before adjustment? This would explain the 560PBT-3917 sessions above.
//
// This doesn't seem particularly associated with discontinuities in the waveform data: there are
// often clock adjustments without corresponding discontinuities in the waveform, and vice versa.
// It's possible internal clock inaccuracy causes both independently.
//
2022-02-27 16:50:10 +00:00
// TODO: why do some devices have lots of these and others none? Maybe cellular modems make daily tweaks?
2021-06-01 00:39:44 +00:00
if ( false ) {
long value = data [ pos ] | data [ pos + 1 ] < < 8 | data [ pos + 2 ] < < 16 | data [ pos + 3 ] < < 24 ;
qDebug ( ) < < this - > sessionid < < " clock changing from " < < ts ( value * 1000L )
< < " to " < < ts ( this - > timestamp * 1000L )
< < " delta: " < < ( this - > timestamp - value ) ;
}
break ;
*/
case 6 : // Cleared?
// Appears in the very first session when that session number is > 1.
// Presumably previous sessions were cleared out.
// TODO: add an internal event for this.
CHECK_VALUE ( pos , 1 ) ; // Always first
CHECK_VALUE ( chunk_size , 1 ) ; // and the only record in the session.
if ( this - > sessionid = = 1 ) UNEXPECTED_VALUE ( this - > sessionid , " >1 " ) ;
break ;
case 7 : // Humidifier setting change (logged in events in 50 series)
tt + = data [ pos ] | ( data [ pos + 1 ] < < 8 ) ; // This adds to the total duration (otherwise it won't match report)
this - > ParseHumidifierSetting60Series ( data [ pos + 2 ] , data [ pos + 3 ] ) ;
break ;
/*
case 8 : // CPAP-Check related, follows Mask On in CPAP-Check mode
tt + = data [ pos ] | ( data [ pos + 1 ] < < 8 ) ; // This adds to the total duration (otherwise it won't match report)
//CHECK_VALUES(data[pos+2], 0, 79); // probably 16-bit value, sometimes matches OA + H + FL + VS + RE?
CHECK_VALUE ( data [ pos + 3 ] , 0 ) ;
//CHECK_VALUES(data[pos+4], 0, 10); // probably 16-bit value
CHECK_VALUE ( data [ pos + 5 ] , 0 ) ;
//CHECK_VALUES(data[pos+6], 0, 79); // probably 16-bit value, usually the same as +2, but not always?
CHECK_VALUE ( data [ pos + 7 ] , 0 ) ;
//CHECK_VALUES(data[pos+8], 0, 10); // probably 16-bit value
CHECK_VALUE ( data [ pos + 9 ] , 0 ) ;
//CHECK_VALUES(data[pos+0xa], 0, 4); // or 0? 44 when changed pressure mid-session?
break ;
*/
default :
UNEXPECTED_VALUE ( code , " known slice code " ) ;
ok = false ; // unlike F0V6, we don't know the size of unknown slices, so we can't recover
break ;
}
pos + = size ;
}
if ( ok & & pos ! = chunk_size ) {
qWarning ( ) < < this - > sessionid < < ( this - > size ( ) - pos ) < < " trailing bytes " ;
}
this - > duration = tt ;
return ok ;
}
2021-06-01 00:34:50 +00:00
bool PRS1DataChunk : : ParseSummaryF0V4 ( void )
{
if ( this - > family ! = 0 | | ( this - > familyVersion ! = 4 ) ) {
qWarning ( ) < < " ParseSummaryF0V4 called with family " < < this - > family < < " familyVersion " < < this - > familyVersion ;
return false ;
}
const unsigned char * data = ( unsigned char * ) this - > m_data . constData ( ) ;
int chunk_size = this - > m_data . size ( ) ;
static const int minimum_sizes [ ] = { 0x18 , 7 , 7 , 0x24 , 2 , 4 , 0 , 4 , 0xb } ;
static const int ncodes = sizeof ( minimum_sizes ) / sizeof ( int ) ;
// NOTE: These are fixed sizes, but are called minimum to more closely match the F0V6 parser.
bool ok = true ;
int pos = 0 ;
int code , size ;
int tt = 0 ;
while ( ok & & pos < chunk_size ) {
code = data [ pos + + ] ;
// There is no hblock prior to F0V6.
size = 0 ;
if ( code < ncodes ) {
// make sure the handlers below don't go past the end of the buffer
size = minimum_sizes [ code ] ;
} // else if it's past ncodes, we'll log its information below (rather than handle it)
if ( pos + size > chunk_size ) {
qWarning ( ) < < this - > sessionid < < " slice " < < code < < " @ " < < pos < < " longer than remaining chunk " ;
ok = false ;
break ;
}
switch ( code ) {
case 0 : // Equipment On
CHECK_VALUE ( pos , 1 ) ; // Always first
CHECK_VALUES ( data [ pos ] & 0xF0 , 0x80 , 0xC0 ) ; // 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.
}
// F0V4 doesn't have a separate settings record like F0V6 does, the settings just follow the EquipmentOn data.
ok = ParseSettingsF0V45 ( data , 0x11 ) ;
CHECK_VALUE ( data [ pos + 0x11 ] , 0 ) ;
CHECK_VALUE ( data [ pos + 0x12 ] , 0 ) ;
CHECK_VALUE ( data [ pos + 0x13 ] , 0 ) ;
CHECK_VALUE ( data [ pos + 0x14 ] , 0 ) ;
CHECK_VALUE ( data [ pos + 0x15 ] , 0 ) ;
CHECK_VALUE ( data [ pos + 0x16 ] , 0 ) ;
CHECK_VALUE ( data [ pos + 0x17 ] , 0 ) ;
break ;
case 2 : // Mask On
tt + = data [ pos ] | ( data [ pos + 1 ] < < 8 ) ;
this - > AddEvent ( new PRS1ParsedSliceEvent ( tt , MaskOn ) ) ;
//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 ) ) ;
// F0V4 doesn't have a separate stats record like F0V6 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 ) ;
2022-02-20 19:37:05 +00:00
//CHECK_VALUE(data[pos+0xf], 0); // 16-bit CA count
//CHECK_VALUE(data[pos+0x10], 0);
2021-06-01 00:34:50 +00:00
//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 ) ) ;
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 4 : // Time Elapsed
// For example: mask-on 5:18:49 in a session of 23:41:20 total leaves mask-off time of 18:22:31.
// That's represented by a mask-off event 19129 seconds after the mask-on, then a time-elapsed
// event after 65535 seconds, then an equipment off event after another 616 seconds.
tt + = data [ pos ] | ( data [ pos + 1 ] < < 8 ) ;
// TODO: see if this event exists in earlier versions
break ;
case 5 : // Clock adjustment?
CHECK_VALUE ( pos , 1 ) ; // Always first
CHECK_VALUE ( chunk_size , 5 ) ; // and the only record in the session.
// This looks like it's minor adjustments to the clock, but 560PBT-3917 sessions 1-2 are weird:
// session 1 starts at 2015-12-23T00:01:20 and contains this event with timestamp 2015-12-23T00:05:14.
// session 2 starts at 2015-12-23T00:01:29, which suggests the event didn't change the clock.
//
// It looks like this happens when there are discontinuities in timestamps, for example 560P-4727:
// session 58 ends at 2015-05-26T09:53:17.
// session 59 starts at 2015-05-26T09:53:15 with an event 5 timestamp of 2015-05-26T09:53:18.
//
// So the session/chunk timestamp has gone backwards. Whenever this happens, it seems to be in
// a session with an event-5 event having a timestamp that hasn't gone backwards. So maybe
// this timestamp is the old clock before adjustment? This would explain the 560PBT-3917 sessions above.
//
// This doesn't seem particularly associated with discontinuities in the waveform data: there are
// often clock adjustments without corresponding discontinuities in the waveform, and vice versa.
// It's possible internal clock inaccuracy causes both independently.
//
2022-02-27 16:50:10 +00:00
// TODO: why do some devices have lots of these and others none? Maybe cellular modems make daily tweaks?
2021-06-01 00:34:50 +00:00
if ( false ) {
long value = data [ pos ] | data [ pos + 1 ] < < 8 | data [ pos + 2 ] < < 16 | data [ pos + 3 ] < < 24 ;
qDebug ( ) < < this - > sessionid < < " clock changing from " < < ts ( value * 1000L )
< < " to " < < ts ( this - > timestamp * 1000L )
< < " delta: " < < ( this - > timestamp - value ) ;
}
break ;
case 6 : // Cleared?
// Appears in the very first session when that session number is > 1.
// Presumably previous sessions were cleared out.
// TODO: add an internal event for this.
CHECK_VALUE ( pos , 1 ) ; // Always first
CHECK_VALUE ( chunk_size , 1 ) ; // and the only record in the session.
if ( this - > sessionid = = 1 ) UNEXPECTED_VALUE ( this - > sessionid , " >1 " ) ;
break ;
case 7 : // Humidifier setting change (logged in events in 50 series)
tt + = data [ pos ] | ( data [ pos + 1 ] < < 8 ) ; // This adds to the total duration (otherwise it won't match report)
this - > ParseHumidifierSetting60Series ( data [ pos + 2 ] , data [ pos + 3 ] ) ;
break ;
case 8 : // CPAP-Check related, follows Mask On in CPAP-Check mode
tt + = data [ pos ] | ( data [ pos + 1 ] < < 8 ) ; // This adds to the total duration (otherwise it won't match report)
//CHECK_VALUES(data[pos+2], 0, 79); // probably 16-bit value, sometimes matches OA + H + FL + VS + RE?
CHECK_VALUE ( data [ pos + 3 ] , 0 ) ;
//CHECK_VALUES(data[pos+4], 0, 10); // probably 16-bit value
CHECK_VALUE ( data [ pos + 5 ] , 0 ) ;
//CHECK_VALUES(data[pos+6], 0, 79); // probably 16-bit value, usually the same as +2, but not always?
CHECK_VALUE ( data [ pos + 7 ] , 0 ) ;
//CHECK_VALUES(data[pos+8], 0, 10); // probably 16-bit value
CHECK_VALUE ( data [ pos + 9 ] , 0 ) ;
//CHECK_VALUES(data[pos+0xa], 0, 4); // or 0? 44 when changed pressure mid-session?
break ;
default :
UNEXPECTED_VALUE ( code , " known slice code " ) ;
ok = false ; // unlike F0V6, we don't know the size of unknown slices, so we can't recover
break ;
}
pos + = size ;
}
if ( ok & & pos ! = chunk_size ) {
qWarning ( ) < < this - > sessionid < < ( this - > size ( ) - pos ) < < " trailing bytes " ;
}
this - > duration = tt ;
return ok ;
}
bool PRS1DataChunk : : ParseSettingsF0V45 ( const unsigned char * data , int size )
{
if ( size < 0xd ) {
qWarning ( ) < < " invalid size passed to ParseSettingsF0V45 " ;
return false ;
}
PRS1Mode cpapmode = PRS1_MODE_UNKNOWN ;
switch ( data [ 0x02 ] ) { // PRS1 mode
case 0x00 :
cpapmode = PRS1_MODE_CPAP ;
break ;
case 0x20 :
cpapmode = PRS1_MODE_BILEVEL ;
break ;
case 0x40 :
cpapmode = PRS1_MODE_AUTOCPAP ;
break ;
case 0x60 :
cpapmode = PRS1_MODE_AUTOBILEVEL ;
break ;
case 0x80 :
cpapmode = PRS1_MODE_AUTOTRIAL ; // Auto-Trial TODO: where is duration?
break ;
case 0xA0 :
cpapmode = PRS1_MODE_CPAPCHECK ;
break ;
default :
UNEXPECTED_VALUE ( data [ 0x02 ] , " known device mode " ) ;
break ;
}
this - > AddEvent ( new PRS1ParsedSettingEvent ( PRS1_SETTING_CPAP_MODE , ( int ) cpapmode ) ) ;
int min_pressure = data [ 0x03 ] ;
int max_pressure = data [ 0x04 ] ;
int min_ps = data [ 0x05 ] ; // pressure support
int max_ps = data [ 0x06 ] ; // pressure support
if ( cpapmode = = PRS1_MODE_CPAP ) {
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_PRESSURE , min_pressure ) ) ;
CHECK_VALUE ( max_pressure , 0 ) ;
CHECK_VALUE ( min_ps , 0 ) ;
CHECK_VALUE ( max_ps , 0 ) ;
} else if ( cpapmode = = PRS1_MODE_AUTOCPAP | | cpapmode = = PRS1_MODE_AUTOTRIAL ) {
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_PRESSURE_MIN , min_pressure ) ) ;
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_PRESSURE_MAX , max_pressure ) ) ;
CHECK_VALUE ( min_ps , 0 ) ;
CHECK_VALUE ( max_ps , 0 ) ;
} else if ( cpapmode = = PRS1_MODE_CPAPCHECK ) {
// Sometimes the CPAP pressure is stored in max_ps instead of min_ps, not sure why.
if ( min_ps = = 0 ) {
if ( max_ps = = 0 ) UNEXPECTED_VALUE ( max_ps , " nonzero " ) ;
min_ps = max_ps ;
} else {
CHECK_VALUE ( max_ps , 0 ) ;
}
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_PRESSURE , min_ps ) ) ;
// TODO: Once OSCAR can handle more modes, we can include these settings; right now including
// these settings makes it think this is AutoCPAP.
//this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PRESSURE_MIN, min_pressure));
//this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PRESSURE_MAX, max_pressure));
} else if ( cpapmode = = PRS1_MODE_BILEVEL ) {
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_EPAP , min_pressure ) ) ;
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_IPAP , max_pressure ) ) ;
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_PS , max_pressure - min_pressure ) ) ;
CHECK_VALUE ( min_ps , 0 ) ; // this seems to be unused on fixed bilevel
CHECK_VALUE ( max_ps , 0 ) ; // this seems to be unused on fixed bilevel
} else if ( cpapmode = = PRS1_MODE_AUTOBILEVEL ) {
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_EPAP_MIN , min_pressure ) ) ;
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_EPAP_MAX , max_pressure - min_ps ) ) ;
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_IPAP_MIN , min_pressure + min_ps ) ) ;
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_IPAP_MAX , max_pressure ) ) ;
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_PS_MIN , min_ps ) ) ;
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_PS_MAX , max_ps ) ) ;
}
CHECK_VALUES ( data [ 0x07 ] , 0 , 0x20 ) ; // 0x20 seems to be Opti-Start
int ramp_time = data [ 0x08 ] ;
int ramp_pressure = data [ 0x09 ] ;
if ( ramp_time > 0 ) {
this - > AddEvent ( new PRS1ParsedSettingEvent ( PRS1_SETTING_RAMP_TIME , ramp_time ) ) ;
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_RAMP_PRESSURE , ramp_pressure ) ) ;
}
quint8 flex = data [ 0x0a ] ;
if ( this - > familyVersion = = 5 ) { if ( flex ! = 0xE1 ) CHECK_VALUES ( flex , 0xA1 , 0xA2 ) ; }
this - > ParseFlexSettingF0V2345 ( flex , cpapmode ) ;
if ( this - > familyVersion = = 5 ) {
CHECK_VALUES ( data [ 0x0c ] , 0x60 , 0x70 ) ;
}
this - > ParseHumidifierSetting60Series ( data [ 0x0b ] , data [ 0x0c ] , true ) ;
if ( size < = 0xd ) {
return true ;
}
int resist_level = ( data [ 0x0d ] > > 3 ) & 7 ; // 0x18 resist=3, 0x11 resist=2, 0x28 resist=5
this - > AddEvent ( new PRS1ParsedSettingEvent ( PRS1_SETTING_MASK_RESIST_LOCK , ( data [ 0x0d ] & 0x40 ) ! = 0 ) ) ;
this - > AddEvent ( new PRS1ParsedSettingEvent ( PRS1_SETTING_MASK_RESIST_SETTING , resist_level ) ) ;
this - > AddEvent ( new PRS1ParsedSettingEvent ( PRS1_SETTING_HOSE_DIAMETER , ( data [ 0x0d ] & 0x01 ) ? 15 : 22 ) ) ;
this - > AddEvent ( new PRS1ParsedSettingEvent ( PRS1_SETTING_TUBING_LOCK , ( data [ 0x0d ] & 0x02 ) ! = 0 ) ) ;
CHECK_VALUE ( data [ 0x0d ] & ( 0x80 | 0x04 ) , 0 ) ;
CHECK_VALUE ( data [ 0x0e ] , 1 ) ;
this - > AddEvent ( new PRS1ParsedSettingEvent ( PRS1_SETTING_AUTO_ON , ( data [ 0x0f ] & 0x40 ) ! = 0 ) ) ;
this - > AddEvent ( new PRS1ParsedSettingEvent ( PRS1_SETTING_AUTO_OFF , ( data [ 0x0f ] & 0x10 ) ! = 0 ) ) ;
this - > AddEvent ( new PRS1ParsedSettingEvent ( PRS1_SETTING_MASK_ALERT , ( data [ 0x0f ] & 0x04 ) ! = 0 ) ) ;
this - > AddEvent ( new PRS1ParsedSettingEvent ( PRS1_SETTING_SHOW_AHI , ( data [ 0x0f ] & 0x02 ) ! = 0 ) ) ;
CHECK_VALUE ( data [ 0x0f ] & ( 0xA0 | 0x08 ) , 0 ) ;
//CHECK_VALUE(data[0x0f] & 0x01, 0); // TODO: What is bit 1? It's sometimes set.
// TODO: Where is altitude compensation set? We've seen it on 261CA.
CHECK_VALUE ( data [ 0x10 ] , 0 ) ;
2021-08-16 14:43:42 +00:00
int autotrial_duration = data [ 0x11 ] ;
2021-06-01 00:34:50 +00:00
if ( cpapmode = = PRS1_MODE_AUTOTRIAL ) {
2021-08-16 14:43:42 +00:00
CHECK_VALUES ( autotrial_duration , 7 , 30 ) ;
this - > AddEvent ( new PRS1ParsedSettingEvent ( PRS1_SETTING_AUTO_TRIAL , autotrial_duration ) ) ;
2021-06-01 00:34:50 +00:00
} else {
2021-08-16 14:43:42 +00:00
CHECK_VALUE ( autotrial_duration , 0 ) ;
2021-06-01 00:34:50 +00:00
}
return true ;
}
2021-06-01 00:39:44 +00:00
const QVector < PRS1ParsedEventType > ParsedEventsF0V4 = {
PRS1PressureSetEvent : : TYPE ,
PRS1IPAPSetEvent : : TYPE ,
PRS1EPAPSetEvent : : TYPE ,
PRS1AutoPressureSetEvent : : TYPE ,
PRS1PressurePulseEvent : : TYPE ,
PRS1RERAEvent : : TYPE ,
PRS1ObstructiveApneaEvent : : TYPE ,
PRS1ClearAirwayEvent : : TYPE ,
PRS1HypopneaEvent : : TYPE ,
PRS1FlowLimitationEvent : : TYPE ,
PRS1VibratorySnoreEvent : : TYPE ,
PRS1VariableBreathingEvent : : TYPE ,
PRS1PeriodicBreathingEvent : : TYPE ,
PRS1LargeLeakEvent : : TYPE ,
PRS1TotalLeakEvent : : TYPE ,
PRS1SnoreEvent : : TYPE ,
PRS1PressureAverageEvent : : TYPE ,
PRS1FlexPressureAverageEvent : : TYPE ,
PRS1SnoresAtPressureEvent : : TYPE ,
} ;
// 460P, 560P[BT], 660P, 760P are F0V4
bool PRS1DataChunk : : ParseEventsF0V4 ( )
{
if ( this - > family ! = 0 | | this - > familyVersion ! = 4 ) {
qWarning ( ) < < " ParseEventsF0V4 called with family " < < this - > family < < " familyVersion " < < this - > familyVersion ;
return false ;
}
const unsigned char * data = ( unsigned char * ) this - > m_data . constData ( ) ;
int chunk_size = this - > m_data . size ( ) ;
static const QMap < int , int > event_sizes = { { 0 , 4 } , { 2 , 4 } , { 3 , 3 } , { 0xb , 4 } , { 0xd , 2 } , { 0xe , 5 } , { 0xf , 5 } , { 0x10 , 5 } , { 0x11 , 5 } , { 0x12 , 4 } } ;
if ( chunk_size < 1 ) {
// This does occasionally happen in F0V6.
qDebug ( ) < < this - > sessionid < < " Empty event data " ;
return false ;
}
bool ok = true ;
int pos = 0 , startpos ;
int code , size ;
int t = 0 ;
int elapsed , duration , value ;
bool is_bilevel = false ;
do {
code = data [ pos + + ] ;
size = 3 ; // default size = 2 bytes time delta + 1 byte data
if ( event_sizes . contains ( code ) ) {
size = event_sizes [ code ] ;
}
if ( pos + size > chunk_size ) {
qWarning ( ) < < this - > sessionid < < " event " < < code < < " @ " < < pos < < " longer than remaining chunk " ;
ok = false ;
break ;
}
startpos = pos ;
if ( code ! = 0x12 ) { // This one event has no timestamp in F0V6
t + = data [ pos ] | ( data [ pos + 1 ] < < 8 ) ;
pos + = 2 ;
}
switch ( code ) {
//case 0x00: // never seen
// NOTE: the original code thought 0x00 had 2 data bytes, unlike the 1 in F0V23.
// We don't have any sample data with this event, so it's left out here.
case 0x01 : // Pressure adjustment: note this was 0x02 in F0V23 and is 0x01 in F0V6
// See notes in ParseEventsF0V6.
this - > AddEvent ( new PRS1PressureSetEvent ( t , data [ pos ] ) ) ;
break ;
case 0x02 : // Pressure adjustment (bi-level): note that this was 0x03 in F0V23 and is 0x02 in F0V6
// See notes above on interpolation.
this - > AddEvent ( new PRS1IPAPSetEvent ( t , data [ pos + 1 ] ) ) ;
this - > AddEvent ( new PRS1EPAPSetEvent ( t , data [ pos ] ) ) ; // EPAP needs to be added second to calculate PS
is_bilevel = true ;
break ;
case 0x03 : // Adjust Opti-Start pressure
// On F0V4 this occasionally shows up in the middle of a session.
// In that cases, the new pressure corresponds to the next night's Opti-Start
// pressure. It does not appear to have any effect on the current night's pressure,
// though presumaby it could if there's a long gap between sessions.
// See F0V6 event 3 for comparison.
// TODO: Does this occur in bi-level mode?
this - > AddEvent ( new PRS1AutoPressureSetEvent ( t , data [ pos ] ) ) ;
break ;
case 0x04 : // Pressure Pulse
duration = data [ pos ] ; // TODO: is this a duration?
this - > AddEvent ( new PRS1PressurePulseEvent ( t , duration ) ) ;
break ;
case 0x05 : // RERA
elapsed = data [ pos ] ;
this - > AddEvent ( new PRS1RERAEvent ( t - elapsed , 0 ) ) ;
break ;
case 0x06 : // Obstructive Apnea
// OA events are instantaneous flags with no duration: reviewing waveforms
// shows that the time elapsed between the flag and reporting often includes
// non-apnea breathing.
elapsed = data [ pos ] ;
this - > AddEvent ( new PRS1ObstructiveApneaEvent ( t - elapsed , 0 ) ) ;
break ;
case 0x07 : // Clear Airway Apnea
// CA events are instantaneous flags with no duration: reviewing waveforms
// shows that the time elapsed between the flag and reporting often includes
// non-apnea breathing.
elapsed = data [ pos ] ;
this - > AddEvent ( new PRS1ClearAirwayEvent ( t - elapsed , 0 ) ) ;
break ;
//case 0x08: // never seen
//case 0x09: // never seen
case 0x0a : // Hypopnea
// TODO: How is this hypopnea different from events 0xb, [0x14 and 0x15 on F0V6]?
elapsed = data [ pos + + ] ;
this - > AddEvent ( new PRS1HypopneaEvent ( t - elapsed , 0 ) ) ;
break ;
case 0x0b : // Hypopnea
// TODO: How is this hypopnea different from events 0xa, 0x14 and 0x15?
// TODO: What is the first byte?
//data[pos+0]; // unknown first byte?
elapsed = data [ pos + 1 ] ; // based on sample waveform, the hypopnea is over after this
this - > AddEvent ( new PRS1HypopneaEvent ( t - elapsed , 0 ) ) ;
break ;
case 0x0c : // Flow Limitation
// TODO: We should revisit whether this is elapsed or duration once (if)
// we start calculating flow limitations ourselves. Flow limitations aren't
// as obvious as OA/CA when looking at a waveform.
elapsed = data [ pos ] ;
this - > AddEvent ( new PRS1FlowLimitationEvent ( t - elapsed , 0 ) ) ;
break ;
case 0x0d : // Vibratory Snore
// VS events are instantaneous flags with no duration, drawn on the official waveform.
// The current thinking is that these are the snores that cause a change in auto-titrating
// pressure. The snoring statistics below seem to be a total count. It's unclear whether
// the trigger for pressure change is severity or count or something else.
// no data bytes
this - > AddEvent ( new PRS1VibratorySnoreEvent ( t , 0 ) ) ;
break ;
case 0x0e : // Variable Breathing?
// TODO: does duration double like it does for PB/LL?
duration = 2 * ( data [ pos ] | ( data [ pos + 1 ] < < 8 ) ) ;
elapsed = data [ pos + 2 ] ; // this is always 60 seconds unless it's at the end, so it seems like elapsed
CHECK_VALUES ( elapsed , 60 , 0 ) ;
this - > AddEvent ( new PRS1VariableBreathingEvent ( t - elapsed - duration , duration ) ) ;
break ;
case 0x0f : // Periodic Breathing
// PB events are reported some time after they conclude, and they do have a reported duration.
// NOTE: This (and F0V6) doubles the duration, unlike F0V23.
duration = 2 * ( data [ pos ] | ( data [ pos + 1 ] < < 8 ) ) ;
elapsed = data [ pos + 2 ] ;
this - > AddEvent ( new PRS1PeriodicBreathingEvent ( t - elapsed - duration , duration ) ) ;
break ;
case 0x10 : // Large Leak
// LL events are reported some time after they conclude, and they do have a reported duration.
// NOTE: This (and F0V6) doubles the duration, unlike F0V23.
duration = 2 * ( data [ pos ] | ( data [ pos + 1 ] < < 8 ) ) ;
elapsed = data [ pos + 2 ] ;
this - > AddEvent ( new PRS1LargeLeakEvent ( t - elapsed - duration , duration ) ) ;
break ;
case 0x11 : // Statistics
this - > AddEvent ( new PRS1TotalLeakEvent ( t , data [ pos ] ) ) ;
this - > AddEvent ( new PRS1SnoreEvent ( t , data [ pos + 1 ] ) ) ;
value = data [ pos + 2 ] ;
if ( is_bilevel ) {
// For bi-level modes, this appears to be the time-weighted average of EPAP and IPAP actually provided.
this - > AddEvent ( new PRS1PressureAverageEvent ( t , value ) ) ;
} else {
// For single-pressure modes, this appears to be the average effective "EPAP" provided by Flex.
//
// Sample data shows this value around 10.3 cmH2O for a prescribed pressure of 12.0 (C-Flex+ 3).
// That's too low for an average pressure over time, but could easily be an average commanded EPAP.
// When flex mode is off, this is exactly the current CPAP set point.
this - > AddEvent ( new PRS1FlexPressureAverageEvent ( t , value ) ) ;
}
this - > AddEvent ( new PRS1IntervalBoundaryEvent ( t ) ) ;
break ;
case 0x12 : // Snore count per pressure
// Some sessions (with lots of ramps) have multiple of these, each with a
// different pressure. The total snore count across all of them matches the
// total found in the stats event.
if ( data [ pos ] ! = 0 ) {
CHECK_VALUES ( data [ pos ] , 1 , 2 ) ; // 0 = CPAP pressure, 1 = bi-level EPAP, 2 = bi-level IPAP
}
//CHECK_VALUE(data[pos+1], 0x78); // pressure
//CHECK_VALUE(data[pos+2], 1); // 16-bit snore count
//CHECK_VALUE(data[pos+3], 0);
value = ( data [ pos + 2 ] | ( data [ pos + 3 ] < < 8 ) ) ;
this - > AddEvent ( new PRS1SnoresAtPressureEvent ( t , data [ pos ] , data [ pos + 1 ] , value ) ) ;
break ;
default :
DUMP_EVENT ( ) ;
UNEXPECTED_VALUE ( code , " known event code " ) ;
this - > AddEvent ( new PRS1UnknownDataEvent ( m_data , startpos ) ) ;
ok = false ; // unlike F0V6, we don't know the size of unknown events, so we can't recover
break ;
}
pos = startpos + size ;
} while ( ok & & pos < chunk_size ) ;
if ( ok & & pos ! = chunk_size ) {
qWarning ( ) < < this - > sessionid < < ( this - > size ( ) - pos ) < < " trailing event bytes " ;
}
this - > duration = t ;
return ok ;
}
2021-06-01 00:34:50 +00:00
// Based on ParseComplianceF0V4, but this has shorter settings and stats following equipment off.
bool PRS1DataChunk : : ParseComplianceF0V5 ( void )
{
if ( this - > family ! = 0 | | ( this - > familyVersion ! = 5 ) ) {
qWarning ( ) < < " ParseComplianceF0V5 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 [ ] = { 0xf , 7 , 4 , 0xf , 0 , 4 , 0 , 4 } ;
static const int ncodes = sizeof ( minimum_sizes ) / sizeof ( int ) ;
// NOTE: These are fixed sizes, but are called minimum to more closely match the F0V6 parser.
bool ok = true ;
int pos = 0 ;
int code , size ;
int tt = 0 ;
while ( ok & & pos < chunk_size ) {
code = data [ pos + + ] ;
// There is no hblock prior to F0V6.
size = 0 ;
if ( code < ncodes ) {
// make sure the handlers below don't go past the end of the buffer
size = minimum_sizes [ code ] ;
} // else if it's past ncodes, we'll log its information below (rather than handle it)
if ( pos + size > chunk_size ) {
qWarning ( ) < < this - > sessionid < < " slice " < < code < < " @ " < < pos < < " longer than remaining chunk " ;
ok = false ;
break ;
}
switch ( code ) {
case 0 : // Equipment On
CHECK_VALUE ( pos , 1 ) ; // Always first
//CHECK_VALUES(data[pos], 0x73, 0x31); // 0x71
// F0V5 doesn't have a separate settings record like F0V6 does, the settings just follow the EquipmentOn data.
ok = ParseSettingsF0V45 ( data , 0x0d ) ;
CHECK_VALUE ( data [ pos + 0xd ] , 0 ) ;
CHECK_VALUE ( data [ pos + 0xe ] , 0 ) ;
CHECK_VALUES ( data [ pos + 0xf ] , 0 , 2 ) ;
break ;
case 2 : // Mask On
tt + = data [ pos ] | ( data [ pos + 1 ] < < 8 ) ;
this - > AddEvent ( new PRS1ParsedSliceEvent ( tt , MaskOn ) ) ;
CHECK_VALUES ( data [ pos + 3 ] , 0x60 , 0x70 ) ;
this - > ParseHumidifierSetting60Series ( data [ pos + 2 ] , data [ pos + 3 ] ) ;
break ;
case 3 : // Mask Off
tt + = data [ pos ] | ( data [ pos + 1 ] < < 8 ) ;
this - > AddEvent ( new PRS1ParsedSliceEvent ( tt , MaskOff ) ) ;
// F0V5 compliance has MaskOff stats unlike all other compliance.
// This is presumably because the 501V is an Auto-CPAP, so it needs to record titration data.
//CHECK_VALUES(data[pos+2], 40, 50); // min pressure
//CHECK_VALUES(data[pos+3], 40, 150); // max pressure
//CHECK_VALUES(data[pos+4], 40, 150); // Average Device Pressure <= 90% of Time (report is time-weighted per slice, for all sessions)
//CHECK_VALUES(data[pos+5], 40, 108); // Auto CPAP Mean Pressure (report is time-weighted per slice, for all sessions)
// Peak Average Pressure is the maximum "mean pressure" reported in any session.
//CHECK_VALUES(data[pos+6], 0, 5); // Apnea or Hypopnea count (probably 16-bit), contributes to AHI
CHECK_VALUE ( data [ pos + 7 ] , 0 ) ;
//CHECK_VALUES(data[pos+8], 0, 6); // Apnea or Hypopnea count (probably 16-bit), contributes to AHI
CHECK_VALUE ( data [ pos + 9 ] , 0 ) ;
//CHECK_VALUES(data[pos+10], 0, 2); // Average Large Leak minutes (probably 16-bit, report show sum of all slices)
CHECK_VALUE ( data [ pos + 11 ] , 0 ) ;
//CHECK_VALUES(data[pos+12], 179, 50); // Average 90% Leak (report is time-weighted per slice)
//CHECK_VALUES(data[pos+13], 178, 32); // Average Total Leak (report is time-weighted per slice)
//CHECK_VALUES(data[pos+14], 180, 36); // Max leak (report shows max for all slices)
break ;
case 1 : // Equipment Off
tt + = data [ pos ] | ( data [ pos + 1 ] < < 8 ) ;
this - > AddEvent ( new PRS1ParsedSliceEvent ( tt , EquipmentOff ) ) ;
CHECK_VALUE ( data [ pos + 2 ] & ~ ( 0x40 | 0x02 | 0x01 ) , 0 ) ;
//CHECK_VALUES(data[pos+3], 0x16, 0x13); // 22, 19
if ( data [ pos + 4 ] > 3 ) UNEXPECTED_VALUE ( data [ pos + 4 ] , " 0-3 " ) ;
//CHECK_VALUES(data[pos+5], 0x2F, 0x26); // 47, 38
if ( data [ pos + 6 ] > 7 ) UNEXPECTED_VALUE ( data [ pos + 6 ] , " 0-7 " ) ;
break ;
//case 4: // Time Elapsed? See ParseComplianceF0V4 if we encounter this.
case 5 : // Clock adjustment?
CHECK_VALUE ( pos , 1 ) ; // Always first
CHECK_VALUE ( chunk_size , 5 ) ; // and the only record in the session.
// This looks like it's minor adjustments to the clock, see ParseComplianceF0V4 for details.
break ;
//case 6: // Cleared? See ParseComplianceF0V4 if we encounter this.
case 7 : // Humidifier setting change (logged in events in 50 series)
tt + = data [ pos ] | ( data [ pos + 1 ] < < 8 ) ; // This adds to the total duration (otherwise it won't match report)
this - > ParseHumidifierSetting60Series ( data [ pos + 2 ] , data [ pos + 3 ] ) ;
break ;
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 ;
}
2021-06-01 01:23:03 +00:00
//********************************************************************************************
// MARK: -
// MARK: DreamStation
2021-06-01 00:34:50 +00:00
// The below is based on fixing the fileVersion == 3 parsing in ParseSummary() based
// on our understanding of slices from F0V23. The switch values come from sample files.
bool PRS1DataChunk : : ParseComplianceF0V6 ( void )
{
if ( this - > family ! = 0 | | this - > familyVersion ! = 6 ) {
qWarning ( ) < < " ParseComplianceF0V6 called with family " < < this - > family < < " familyVersion " < < this - > familyVersion ;
return false ;
}
// TODO: hardcoding this is ugly, think of a better approach
if ( this - > m_data . size ( ) < 82 ) {
qWarning ( ) < < this - > sessionid < < " compliance data too short: " < < this - > m_data . size ( ) ;
return false ;
}
const unsigned char * data = ( unsigned char * ) this - > m_data . constData ( ) ;
int chunk_size = this - > m_data . size ( ) ;
static const int expected_sizes [ ] = { 1 , 0x34 , 9 , 4 , 2 , 2 , 4 , 8 } ;
static const int ncodes = sizeof ( expected_sizes ) / sizeof ( int ) ;
for ( int i = 0 ; i < ncodes ; i + + ) {
if ( this - > hblock . contains ( i ) ) {
CHECK_VALUE ( this - > hblock [ i ] , expected_sizes [ i ] ) ;
} else {
UNEXPECTED_VALUE ( this - > hblock . contains ( i ) , true ) ;
}
}
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 ( size < expected_sizes [ code ] ) {
UNEXPECTED_VALUE ( size , expected_sizes [ code ] ) ;
qWarning ( ) < < this - > sessionid < < " slice " < < code < < " too small " < < size < < " < " < < expected_sizes [ code ] ;
if ( code ! = 1 ) { // Settings are variable-length, so shorter settings slices aren't fatal.
ok = false ;
break ;
}
}
if ( pos + size > chunk_size ) {
qWarning ( ) < < this - > sessionid < < " slice " < < code < < " @ " < < pos < < " longer than remaining chunk " ;
ok = false ;
break ;
}
switch ( code ) {
case 0 :
// always first? Maybe equipmenton? Maybe 0 was always equipmenton, even in F0V23?
CHECK_VALUE ( pos , 1 ) ;
//CHECK_VALUES(data[pos], 1, 3); // sometimes 7?
break ;
case 1 : // Settings
// This is where ParseSummaryF0V6 started (after "3 bytes that don't follow the pattern")
// Both compliance and summary files seem to have the same length for this slice, so maybe the
// settings are the same?
ok = this - > ParseSettingsF0V6 ( data + pos , size ) ;
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 7 :
// Always follows mask off?
//CHECK_VALUES(data[pos], 0x01, 0x00); // sometimes 32, 4
CHECK_VALUE ( data [ pos + 1 ] , 0x00 ) ;
//CHECK_VALUES(data[pos+2], 0x00, 0x01); // sometimes 11, 3, 15
CHECK_VALUE ( data [ pos + 3 ] , 0x00 ) ;
//CHECK_VALUE(data[pos+4], 0x05, 0x0A); // 00
CHECK_VALUE ( data [ pos + 5 ] , 0x00 ) ;
//CHECK_VALUE(data[pos+6], 0x64, 0x69); // 6E, 6D, 6E, 6E, 80
//CHECK_VALUE(data[pos+7], 0x3d, 0x5c); // 6A, 6A, 6B, 6C, 80
break ;
case 2 : // Equipment Off
tt + = data [ pos ] | ( data [ pos + 1 ] < < 8 ) ;
this - > AddEvent ( new PRS1ParsedSliceEvent ( tt , EquipmentOff ) ) ;
//CHECK_VALUE(data[pos+2], 0x08); // 0x01
//CHECK_VALUE(data[pos+3], 0x14); // 0x12
//CHECK_VALUE(data[pos+4], 0x01); // 0x00
//CHECK_VALUE(data[pos+5], 0x22); // 0x28
//CHECK_VALUE(data[pos+6], 0x02); // sometimes 1, 0
CHECK_VALUE ( data [ pos + 7 ] , 0x00 ) ; // 0x00
CHECK_VALUE ( data [ pos + 8 ] , 0x00 ) ; // 0x00
break ;
case 6 : // 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 ;
}
2021-06-01 00:24:09 +00:00
bool PRS1DataChunk : : ParseSummaryF0V6 ( void )
{
if ( this - > family ! = 0 | | this - > familyVersion ! = 6 ) {
qWarning ( ) < < " ParseSummaryF0V6 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 ( ) ;
2021-11-04 20:40:44 +00:00
static const int minimum_sizes [ ] = { 1 , 0x29 , 9 , 4 , 2 , 4 , 1 , 4 , 0x1b , 2 , 4 , 0x0b , 1 , 2 , 6 } ;
2021-06-01 00:24:09 +00:00
static const int ncodes = sizeof ( minimum_sizes ) / sizeof ( int ) ;
2022-02-27 16:50:10 +00:00
// NOTE: The sizes contained in hblock can vary, even within a single device, as can the length of hblock itself!
2021-06-01 00:24:09 +00:00
// 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 ;
}
2022-02-17 01:50:47 +00:00
if ( chunk_size < 55 ) UNEXPECTED_VALUE ( chunk_size , " >= 55 " ) ;
2021-06-01 00:24:09 +00:00
bool ok = true ;
int pos = 0 ;
int code , size ;
int tt = 0 ;
while ( ok & & pos < chunk_size ) {
code = data [ pos + + ] ;
if ( ! this - > hblock . contains ( code ) ) {
qWarning ( ) < < this - > sessionid < < " missing hblock entry for " < < code ;
ok = false ;
break ;
}
size = this - > hblock [ code ] ;
if ( code < ncodes ) {
// make sure the handlers below don't go past the end of the buffer
if ( size < minimum_sizes [ code ] ) {
UNEXPECTED_VALUE ( size , minimum_sizes [ code ] ) ;
qWarning ( ) < < this - > sessionid < < " slice " < < code < < " too small " < < size < < " < " < < minimum_sizes [ code ] ;
if ( code ! = 1 ) { // Settings are variable-length, so shorter settings slices aren't fatal.
ok = false ;
break ;
}
}
} // else if it's past ncodes, we'll log its information below (rather than handle it)
if ( pos + size > chunk_size ) {
qWarning ( ) < < this - > sessionid < < " slice " < < code < < " @ " < < pos < < " longer than remaining chunk " ;
ok = false ;
break ;
}
switch ( code ) {
case 0 : // Equipment On
CHECK_VALUE ( pos , 1 ) ; // Always first?
//CHECK_VALUES(data[pos], 1, 7); // or 3?
if ( size = = 4 ) { // 400G has 3 more bytes?
//CHECK_VALUE(data[pos+1], 0); // or 2, 14, 4, etc.
//CHECK_VALUES(data[pos+2], 8, 65); // or 1
//CHECK_VALUES(data[pos+3], 0, 20); // or 21, 22, etc.
}
break ;
case 1 : // Settings
ok = this - > ParseSettingsF0V6 ( data + pos , size ) ;
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 8 : // vs. 7 in compliance, always follows mask off (except when there's a 5, see below), also longer
2022-02-27 16:50:10 +00:00
// Maybe statistics of some kind, given the pressure stats that seem to appear before it on AutoCPAP devices?
2021-06-01 00:24:09 +00:00
//CHECK_VALUES(data[pos], 0x02, 0x01); // probably 16-bit value
CHECK_VALUE ( data [ pos + 1 ] , 0x00 ) ;
//CHECK_VALUES(data[pos+2], 0x0d, 0x0a); // probably 16-bit value, maybe OA count?
CHECK_VALUE ( data [ pos + 3 ] , 0x00 ) ;
//CHECK_VALUES(data[pos+4], 0x09, 0x0b); // probably 16-bit value
CHECK_VALUE ( data [ pos + 5 ] , 0x00 ) ;
//CHECK_VALUES(data[pos+6], 0x1e, 0x35); // probably 16-bit value
CHECK_VALUE ( data [ pos + 7 ] , 0x00 ) ;
//CHECK_VALUES(data[pos+8], 0x8c, 0x4c); // 16-bit value, not sure what
//CHECK_VALUE(data[pos+9], 0x00);
//CHECK_VALUES(data[pos+0xa], 0xbb, 0x00); // 16-bit minutes in large leak
//CHECK_VALUE(data[pos+0xb], 0x00);
//CHECK_VALUES(data[pos+0xc], 0x15, 0x02); // 16-bit minutes in PB
//CHECK_VALUE(data[pos+0xd], 0x00);
//CHECK_VALUES(data[pos+0xe], 0x01, 0x00); // 16-bit VS count
//CHECK_VALUE(data[pos+0xf], 0x00);
//CHECK_VALUES(data[pos+0x10], 0x21, 5); // probably 16-bit value, maybe H count?
CHECK_VALUE ( data [ pos + 0x11 ] , 0x00 ) ;
//CHECK_VALUES(data[pos+0x12], 0x13, 0); // 16-bit value, not sure what
//CHECK_VALUE(data[pos+0x13], 0x00);
//CHECK_VALUES(data[pos+0x14], 0x05, 0); // probably 16-bit value, maybe RE count?
CHECK_VALUE ( data [ pos + 0x15 ] , 0x00 ) ;
//CHECK_VALUE(data[pos+0x16], 0x00, 4); // probably a 16-bit value, PB or FL count?
CHECK_VALUE ( data [ pos + 0x17 ] , 0x00 ) ;
//CHECK_VALUES(data[pos+0x18], 0x69, 0x23);
//CHECK_VALUES(data[pos+0x19], 0x44, 0x18);
//CHECK_VALUES(data[pos+0x1a], 0x80, 0x49);
if ( size > = 0x1f ) { // 500X is only 0x1b long!
//CHECK_VALUES(data[pos+0x1b], 0x00, 6);
CHECK_VALUE ( data [ pos + 0x1c ] , 0x00 ) ;
//CHECK_VALUES(data[pos+0x1d], 0x0c, 0x0d);
//CHECK_VALUES(data[pos+0x1e], 0x31, 0x3b);
// TODO: 400G and 500G has 8 more bytes?
// TODO: 400G sometimes has another 4 on top of that?
}
break ;
case 2 : // Equipment Off
tt + = data [ pos ] | ( data [ pos + 1 ] < < 8 ) ;
this - > AddEvent ( new PRS1ParsedSliceEvent ( tt , EquipmentOff ) ) ;
//CHECK_VALUE(data[pos+2], 0x08); // 0x01
//CHECK_VALUE(data[pos+3], 0x14); // 0x12
//CHECK_VALUE(data[pos+4], 0x01); // 0x00
//CHECK_VALUE(data[pos+5], 0x22); // 0x28
//CHECK_VALUE(data[pos+6], 0x02); // sometimes 1, 0
CHECK_VALUE ( data [ pos + 7 ] , 0x00 ) ; // 0x00
CHECK_VALUE ( data [ pos + 8 ] , 0x00 ) ; // 0x00
if ( size = = 0x0c ) { // 400G has 3 more bytes, seem to match Equipment On bytes
//CHECK_VALUE(data[pos+1], 0);
//CHECK_VALUES(data[pos+2], 8, 65);
//CHECK_VALUE(data[pos+3], 0);
}
break ;
case 0x09 : // Time Elapsed (event 4 in F0V4)
tt + = data [ pos ] | ( data [ pos + 1 ] < < 8 ) ;
break ;
case 0x0a : // Humidifier 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 ;
case 0x0d : // ???
// seen on one 500G multiple times
//CHECK_VALUE(data[pos], 0); // 16-bit value
//CHECK_VALUE(data[pos+1], 0);
break ;
case 0x0e :
// only seen once on 400G, many times on 500G
//CHECK_VALUES(data[pos], 0, 6); // 16-bit value
//CHECK_VALUE(data[pos+1], 0);
//CHECK_VALUES(data[pos+2], 7, 9);
//CHECK_VALUES(data[pos+3], 7, 15);
//CHECK_VALUES(data[pos+4], 7, 12);
//CHECK_VALUES(data[pos+5], 0, 3);
break ;
case 0x05 :
// AutoCPAP-related? First appeared on 500X, follows 4, before 8, look like pressure values
//CHECK_VALUE(data[pos], 0x4b); // maybe min pressure? (matches ramp pressure, see ramp on pressure graph)
//CHECK_VALUE(data[pos+1], 0x5a); // maybe max pressure? (close to max on pressure graph, time at pressure graph)
//CHECK_VALUE(data[pos+2], 0x5a); // seems to match Average 90% Pressure
//CHECK_VALUE(data[pos+3], 0x58); // seems to match Average CPAP
break ;
case 0x07 :
// AutoBiLevel-related? First appeared on 700X, follows 4, before 8, looks like pressure values
//CHECK_VALUE(data[pos], 0x50); // maybe min IPAP or max titrated EPAP? (matches time at pressure graph, auto bi-level summary)
//CHECK_VALUE(data[pos+1], 0x64); // maybe max IPAP or max titrated IPAP? (matches time at pressure graph, auto bi-level summary)
//CHECK_VALUE(data[pos+2], 0x4b); // seems to match 90% EPAP
//CHECK_VALUE(data[pos+3], 0x64); // seems to match 90% IPAP
break ;
case 0x0b :
// CPAP-Check related, follows Mask On in CPAP-Check mode
tt + = data [ pos ] | ( data [ pos + 1 ] < < 8 ) ; // This adds to the total duration (otherwise it won't match report)
//CHECK_VALUE(data[pos+2], 0); // probably 16-bit value
CHECK_VALUE ( data [ pos + 3 ] , 0 ) ;
//CHECK_VALUE(data[pos+4], 0); // probably 16-bit value
CHECK_VALUE ( data [ pos + 5 ] , 0 ) ;
//CHECK_VALUE(data[pos+6], 0); // probably 16-bit value
CHECK_VALUE ( data [ pos + 7 ] , 0 ) ;
//CHECK_VALUE(data[pos+8], 0); // probably 16-bit value
CHECK_VALUE ( data [ pos + 9 ] , 0 ) ;
//CHECK_VALUES(data[pos+0xa], 20, 60); // or 0? 44 when changed pressure mid-session?
break ;
case 0x06 :
// Maybe starting pressure? follows 4, before 8, looks like a pressure value, seen with CPAP-Check and EZ-Start
// Maybe ending pressure: matches ending CPAP-Check pressure if it changes mid-session.
// TODO: The daily details will show when it changed, so maybe there's an event that indicates a pressure change.
//CHECK_VALUES(data[pos], 90, 60); // maybe CPAP-Check pressure, also matches EZ-Start Pressure
break ;
case 0x0c :
// EZ-Start pressure for Auto-CPAP, seen on 500X110 following 4, before 8
// Appears to reflect the current session's EZ-Start pressure, though reported afterwards
//CHECK_VALUE(data[pos], 70, 80);
break ;
default :
UNEXPECTED_VALUE ( code , " known slice code " ) ;
break ;
}
pos + = size ;
}
this - > duration = tt ;
return ok ;
}
// The below is based on a combination of the old mainblock parsing for fileVersion == 3
// in ParseSummary() and the switch statements of ParseSummaryF0V6.
//
2022-02-27 16:50:10 +00:00
// Both compliance and summary files (at least for 200X and 400X devices) seem to have
2021-06-01 00:24:09 +00:00
// the same length for this slice, so maybe the settings are the same? At least 0x0a
// looks like a pressure in compliance files.
bool PRS1DataChunk : : ParseSettingsF0V6 ( const unsigned char * data , int size )
{
static const QMap < int , int > expected_lengths = { { 0x0c , 3 } , { 0x0d , 2 } , { 0x0e , 2 } , { 0x0f , 4 } , { 0x10 , 3 } , { 0x35 , 2 } } ;
bool ok = true ;
PRS1Mode cpapmode = PRS1_MODE_UNKNOWN ;
FlexMode flexmode = FLEX_Unknown ;
int pressure = 0 ;
int imin_ps = 0 ;
int imax_ps = 0 ;
int min_pressure = 0 ;
int max_pressure = 0 ;
2021-11-05 13:30:44 +00:00
bool ramp_type_set = false ;
2021-06-01 00:24:09 +00:00
// Parse the nested data structure which contains settings
int pos = 0 ;
do {
int code = data [ pos + + ] ;
int len = data [ pos + + ] ;
int expected_len = 1 ;
if ( expected_lengths . contains ( code ) ) {
expected_len = expected_lengths [ code ] ;
}
//CHECK_VALUE(len, expected_len);
if ( len < expected_len ) {
qWarning ( ) < < this - > sessionid < < " setting " < < code < < " too small " < < len < < " < " < < expected_len ;
ok = false ;
break ;
}
if ( pos + len > size ) {
qWarning ( ) < < this - > sessionid < < " setting " < < code < < " @ " < < pos < < " longer than remaining slice " ;
ok = false ;
break ;
}
switch ( code ) {
case 0 : // Device Mode
CHECK_VALUE ( pos , 2 ) ; // always first?
CHECK_VALUE ( len , 1 ) ;
switch ( data [ pos ] ) {
case 0 : cpapmode = PRS1_MODE_CPAP ; break ;
case 1 : cpapmode = PRS1_MODE_BILEVEL ; break ;
case 2 : cpapmode = PRS1_MODE_AUTOCPAP ; break ;
case 3 : cpapmode = PRS1_MODE_AUTOBILEVEL ; break ;
case 4 : cpapmode = PRS1_MODE_CPAPCHECK ; break ;
default :
UNEXPECTED_VALUE ( data [ pos ] , " known device mode " ) ;
break ;
}
this - > AddEvent ( new PRS1ParsedSettingEvent ( PRS1_SETTING_CPAP_MODE , ( int ) cpapmode ) ) ;
break ;
case 1 : // ???
CHECK_VALUES ( len , 1 , 2 ) ;
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?
}
if ( len = = 2 ) { // 400G, 500G has extra byte
switch ( data [ pos + 1 ] ) {
case 0x00 : // 0x00 seen with EZ-Start disabled, no auto-trial, with CPAP-Check on 400X110
case 0x10 : // 0x10 seen with EZ-Start enabled, Opti-Start off on 500X110
case 0x20 : // 0x20 seen with Opti-Start enabled
case 0x30 : // 0x30 seen with both Opti-Start and EZ-Start enabled on 500X110
case 0x40 : // 0x40 seen with Auto-Trial
case 0x80 : // 0x80 seen with EZ-Start and CPAP-Check+ on 500X150
break ;
default :
UNEXPECTED_VALUE ( data [ pos + 1 ] , " [0,0x10,0x20,0x30,0x40,0x80] " )
}
}
break ;
case 0x0a : // CPAP pressure setting
CHECK_VALUE ( len , 1 ) ;
CHECK_VALUE ( cpapmode , PRS1_MODE_CPAP ) ;
pressure = data [ pos ] ;
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_PRESSURE , pressure ) ) ;
break ;
case 0x0c : // CPAP-Check pressure setting
CHECK_VALUE ( len , 3 ) ;
CHECK_VALUE ( cpapmode , PRS1_MODE_CPAPCHECK ) ;
min_pressure = data [ pos ] ; // Min Setting on pressure graph
max_pressure = data [ pos + 1 ] ; // Max Setting on pressure graph
pressure = data [ pos + 2 ] ; // CPAP on pressure graph and CPAP-Check Pressure on settings detail
// This seems to be the initial pressure. If the pressure changes mid-session, the pressure
// graph will show either the changed pressure or the majority pressure, not sure which.
// The time of change is most likely in the events file. See slice 6 for ending pressure.
//CHECK_VALUE(pressure, 0x5a);
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_PRESSURE , pressure ) ) ;
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_PRESSURE_MIN , min_pressure ) ) ;
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_PRESSURE_MAX , max_pressure ) ) ;
break ;
case 0x0d : // AutoCPAP pressure setting
CHECK_VALUE ( len , 2 ) ;
CHECK_VALUE ( cpapmode , PRS1_MODE_AUTOCPAP ) ;
min_pressure = data [ pos ] ;
max_pressure = data [ pos + 1 ] ;
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_PRESSURE_MIN , min_pressure ) ) ;
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_PRESSURE_MAX , max_pressure ) ) ;
break ;
case 0x0e : // Bi-Level pressure setting
CHECK_VALUE ( len , 2 ) ;
CHECK_VALUE ( cpapmode , PRS1_MODE_BILEVEL ) ;
min_pressure = data [ pos ] ;
max_pressure = data [ pos + 1 ] ;
imin_ps = max_pressure - min_pressure ;
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_EPAP , min_pressure ) ) ;
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_IPAP , max_pressure ) ) ;
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_PS , imin_ps ) ) ;
break ;
case 0x0f : // Auto Bi-Level pressure setting
CHECK_VALUE ( len , 4 ) ;
CHECK_VALUE ( cpapmode , PRS1_MODE_AUTOBILEVEL ) ;
min_pressure = data [ pos ] ;
max_pressure = data [ pos + 1 ] ;
imin_ps = data [ pos + 2 ] ;
imax_ps = data [ pos + 3 ] ;
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_EPAP_MIN , min_pressure ) ) ;
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_IPAP_MAX , max_pressure ) ) ;
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_PS_MIN , imin_ps ) ) ;
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_PS_MAX , imax_ps ) ) ;
break ;
case 0x10 : // Auto-Trial mode
// This is not encoded as a separate mode as in F0V4, but instead as an auto-trial
// duration on top of the CPAP or CPAP-Check mode. Reports show Auto-CPAP results,
// but curiously report the use of C-Flex+, even though Auto-CPAP uses A-Flex.
CHECK_VALUE ( len , 3 ) ;
CHECK_VALUES ( cpapmode , PRS1_MODE_CPAP , PRS1_MODE_CPAPCHECK ) ;
2021-12-10 18:43:03 +00:00
if ( data [ pos ] < 5 | | data [ pos ] > 30 ) { // We've seen 5, 9, 14, 25, and 30
UNEXPECTED_VALUE ( data [ pos ] , " 5-30 " ) ; // Auto-Trial Duration
2021-06-01 00:24:09 +00:00
}
this - > AddEvent ( new PRS1ParsedSettingEvent ( PRS1_SETTING_AUTO_TRIAL , data [ pos ] ) ) ;
// If we want C-Flex+ to be reported as A-Flex, we can set cpapmode = PRS1_MODE_AUTOTRIAL here.
// (Note that the setting event has already been added above, which is why ImportSummary needs
// to adjust it when it sees this setting.)
min_pressure = data [ pos + 1 ] ;
max_pressure = data [ pos + 2 ] ;
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_PRESSURE_MIN , min_pressure ) ) ;
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_PRESSURE_MAX , max_pressure ) ) ;
break ;
case 0x2a : // EZ-Start
CHECK_VALUE ( len , 1 ) ;
CHECK_VALUES ( data [ pos ] , 0x00 , 0x80 ) ; // both seem to mean enabled
// 0x80 is CPAP Mode - EZ-Start in pressure detail chart, 0x00 is just CPAP mode with no EZ-Start pressure
// TODO: How to represent which one is active in practice? Should this always be "true" since
// either value means that the setting is enabled?
this - > AddEvent ( new PRS1ParsedSettingEvent ( PRS1_SETTING_EZ_START , data [ pos ] ! = 0 ) ) ;
break ;
case 0x42 : // EZ-Start enabled for Auto-CPAP?
// Seen on 500X110 before 0x2b when EZ-Start is enabled on Auto-CPAP
CHECK_VALUE ( len , 1 ) ;
CHECK_VALUES ( data [ pos ] , 0x00 , 0x80 ) ; // both seem to mean enabled, 0x00 appears when Opti-Start is used instead
// TODO: How to represent which one is active in practice? Should this always be "true" since
// either value means that the setting is enabled?
this - > AddEvent ( new PRS1ParsedSettingEvent ( PRS1_SETTING_EZ_START , data [ pos ] ! = 0 ) ) ;
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 ) ) ;
2021-11-05 13:30:44 +00:00
ramp_type_set = true ;
2021-06-01 00:24:09 +00:00
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 " ) ;
}
2021-11-05 13:30:44 +00:00
if ( ! ramp_type_set ) {
// If there's a ramp time that's neither linear nor SmartRamp, then it's Ramp+.
this - > AddEvent ( new PRS1ParsedSettingEvent ( PRS1_SETTING_RAMP_TYPE , 2 ) ) ;
ramp_type_set = true ;
}
2021-06-01 00:24:09 +00:00
this - > AddEvent ( new PRS1ParsedSettingEvent ( PRS1_SETTING_RAMP_TIME , data [ pos ] ) ) ;
break ;
case 0x2d : // Ramp Pressure
CHECK_VALUE ( len , 1 ) ;
2021-11-05 13:30:44 +00:00
// 0 = Off for Ramp+ (since time is always set)
// Turning it on during therapy creates a new session.
2021-06-01 00:24:09 +00:00
this - > AddEvent ( new PRS1PressureSettingEvent ( PRS1_SETTING_RAMP_PRESSURE , data [ pos ] ) ) ;
break ;
case 0x2e : // Flex mode
CHECK_VALUE ( len , 1 ) ;
switch ( data [ pos ] ) {
case 0 :
flexmode = FLEX_None ;
break ;
case 0x80 :
switch ( cpapmode ) {
case PRS1_MODE_CPAP :
case PRS1_MODE_CPAPCHECK :
case PRS1_MODE_AUTOCPAP :
//case PRS1_MODE_AUTOTRIAL:
flexmode = FLEX_CFlex ;
break ;
case PRS1_MODE_BILEVEL :
case PRS1_MODE_AUTOBILEVEL :
flexmode = FLEX_BiFlex ;
break ;
default :
HEX ( flexmode ) ;
UNEXPECTED_VALUE ( cpapmode , " untested mode " ) ;
break ;
}
break ;
2022-02-27 16:50:10 +00:00
case 0x90 : // C-Flex+ or A-Flex, depending on device mode
2021-06-01 00:24:09 +00:00
switch ( cpapmode ) {
case PRS1_MODE_CPAP :
case PRS1_MODE_CPAPCHECK :
flexmode = FLEX_CFlexPlus ;
break ;
case PRS1_MODE_AUTOCPAP :
flexmode = FLEX_AFlex ;
break ;
default :
UNEXPECTED_VALUE ( cpapmode , " cpap or apap " ) ;
break ;
}
break ;
case 0xA0 : // Rise Time
flexmode = FLEX_RiseTime ;
switch ( cpapmode ) {
case PRS1_MODE_BILEVEL :
case PRS1_MODE_AUTOBILEVEL :
break ;
default :
HEX ( flexmode ) ;
UNEXPECTED_VALUE ( cpapmode , " autobilevel " ) ;
break ;
}
break ;
case 0xB0 : // P-Flex
flexmode = FLEX_PFlex ;
switch ( cpapmode ) {
case PRS1_MODE_AUTOCPAP :
break ;
default :
HEX ( flexmode ) ;
UNEXPECTED_VALUE ( cpapmode , " apap " ) ;
break ;
}
break ;
default :
UNEXPECTED_VALUE ( data [ pos ] , " known flex mode " ) ;
break ;
}
this - > AddEvent ( new PRS1ParsedSettingEvent ( PRS1_SETTING_FLEX_MODE , flexmode ) ) ;
break ;
case 0x2f : // Flex lock
CHECK_VALUE ( len , 1 ) ;
CHECK_VALUES ( data [ pos ] , 0 , 0x80 ) ;
2021-11-05 13:30:44 +00:00
// DS2 doesn't have a specific flex lock. See patient controls access below.
2021-06-01 00:24:09 +00:00
this - > AddEvent ( new PRS1ParsedSettingEvent ( PRS1_SETTING_FLEX_LOCK , data [ pos ] ! = 0 ) ) ;
break ;
case 0x30 : // Flex level
CHECK_VALUE ( len , 1 ) ;
this - > AddEvent ( new PRS1ParsedSettingEvent ( PRS1_SETTING_FLEX_LEVEL , data [ pos ] ) ) ;
if ( flexmode = = FLEX_PFlex ) {
CHECK_VALUE ( data [ pos ] , 4 ) ; // No number appears on reports.
}
if ( flexmode = = FLEX_RiseTime ) {
if ( data [ pos ] < 1 | | data [ pos ] > 3 ) UNEXPECTED_VALUE ( data [ pos ] , " 1-3 " ) ;
}
break ;
case 0x35 : // Humidifier setting
CHECK_VALUE ( len , 2 ) ;
this - > ParseHumidifierSettingV3 ( data [ pos ] , data [ pos + 1 ] , true ) ;
break ;
case 0x36 : // Mask Resistance Lock
CHECK_VALUE ( len , 1 ) ;
CHECK_VALUES ( data [ pos ] , 0 , 0x80 ) ;
2021-11-05 13:30:44 +00:00
// DS2 doesn't have a mask resistance lock, as the resistance setting is only in the provider menu.
2021-06-01 00:24:09 +00:00
this - > AddEvent ( new PRS1ParsedSettingEvent ( PRS1_SETTING_MASK_RESIST_LOCK , data [ pos ] ! = 0 ) ) ;
break ;
case 0x38 : // Mask Resistance
CHECK_VALUE ( len , 1 ) ;
if ( data [ pos ] ! = 0 ) { // 0 == mask resistance off
if ( data [ pos ] < 1 | | data [ pos ] > 5 ) UNEXPECTED_VALUE ( data [ pos ] , " 1-5 " ) ;
}
this - > AddEvent ( new PRS1ParsedSettingEvent ( PRS1_SETTING_MASK_RESIST_SETTING , data [ pos ] ) ) ;
break ;
case 0x39 : // Tubing Type Lock
CHECK_VALUE ( len , 1 ) ;
CHECK_VALUES ( data [ pos ] , 0 , 0x80 ) ;
2021-11-05 13:30:44 +00:00
// DS2 doesn't have a tubing type lock, it is always available (unless a heated tube is auto-detected).
2021-06-01 00:24:09 +00:00
this - > AddEvent ( new PRS1ParsedSettingEvent ( PRS1_SETTING_TUBING_LOCK , data [ pos ] ! = 0 ) ) ;
break ;
case 0x3b : // Tubing Type
CHECK_VALUE ( len , 1 ) ;
if ( data [ pos ] ! = 0 ) {
CHECK_VALUES ( data [ pos ] , 2 , 1 ) ; // 15HT = 2, 15 = 1, 22 = 0
}
this - > ParseTubingTypeV3 ( data [ pos ] ) ;
break ;
case 0x40 : // new to 400G, also seen on 500X110, alternate tubing type? appears after 0x39 and before 0x3c
CHECK_VALUE ( len , 1 ) ;
if ( data [ pos ] > 3 ) UNEXPECTED_VALUE ( data [ pos ] , " 0-3 " ) ; // 0 = 22mm, 1 = 15mm, 2 = 15HT, 3 = 12mm
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 0x3e : // Auto On
CHECK_VALUE ( len , 1 ) ;
CHECK_VALUES ( data [ pos ] , 0 , 0x80 ) ;
this - > AddEvent ( new PRS1ParsedSettingEvent ( PRS1_SETTING_AUTO_ON , data [ pos ] ! = 0 ) ) ;
break ;
case 0x3f : // Auto Off
CHECK_VALUE ( len , 1 ) ;
CHECK_VALUES ( data [ pos ] , 0 , 0x80 ) ;
this - > AddEvent ( new PRS1ParsedSettingEvent ( PRS1_SETTING_AUTO_OFF , data [ pos ] ! = 0 ) ) ;
break ;
case 0x43 : // new to 502G, sessions 3-8, Auto-Trial is off, Opti-Start is missing
CHECK_VALUE ( len , 1 ) ;
CHECK_VALUE ( data [ pos ] , 0x3C ) ;
break ;
case 0x44 : // new to 502G, sessions 3-8, Auto-Trial is off, Opti-Start is missing
CHECK_VALUE ( len , 1 ) ;
CHECK_VALUE ( data [ pos ] , 0xFF ) ;
break ;
case 0x45 : // Target Time, specific to DreamStation Go
CHECK_VALUE ( len , 1 ) ;
// Included in the data, but not shown on reports when humidifier is in Fixed mode.
// According to the FAQ, this setting is only available in Adaptive mode.
if ( data [ pos ] < 40 | | data [ pos ] > 100 ) { // 4.0 through 10.0 hours in 0.5-hour increments
CHECK_VALUES ( data [ pos ] , 0 , 1 ) ; // Off and Auto
}
this - > AddEvent ( new PRS1ScaledSettingEvent ( PRS1_SETTING_HUMID_TARGET_TIME , data [ pos ] , 0.1 ) ) ;
break ;
2021-11-04 20:40:44 +00:00
case 0x46 : // Tubing Type (alternate, seen instead of 0x3b on 700X110 v1.2 firmware and on DS2)
2021-10-28 18:14:06 +00:00
CHECK_VALUE ( len , 1 ) ;
2022-01-14 16:34:37 +00:00
if ( data [ pos ] > 4 ) UNEXPECTED_VALUE ( data [ pos ] , " 0-4 " ) ; // 0 = 22mm, 1 = 15mm, 2 = 15HT, 3 = 12mm, 4 = HT12
2021-11-04 20:40:44 +00:00
// TODO: Confirm that 4 is 12HT and update ParseTubingTypeV3.
this - > ParseTubingTypeV3 ( data [ pos ] ) ;
break ;
case 0x4a : // Patient controls access, specific to DreamStation 2.
CHECK_VALUE ( len , 1 ) ;
CHECK_VALUES ( data [ pos ] , 0 , 0x80 ) ;
2021-11-05 13:30:44 +00:00
// Turning off patient controls access essentially locks only flex and ramp time.
// Humidification, heated tube temperature, and ramp level are still adjustable during therapy.
// (DS2 doesn't have a separate flex lock setting.)
if ( data [ pos ] = = 0 ) {
this - > AddEvent ( new PRS1ParsedSettingEvent ( PRS1_SETTING_FLEX_LOCK , true ) ) ;
}
2021-11-04 20:40:44 +00:00
break ;
case 0x4b : // Patient data access, specific to DreamStation 2.
CHECK_VALUE ( len , 1 ) ;
CHECK_VALUES ( data [ pos ] , 0 , 0x80 ) ;
2021-11-05 13:30:44 +00:00
// Turning off patient data access hides both AHI and on-device reports.
this - > AddEvent ( new PRS1ParsedSettingEvent ( PRS1_SETTING_SHOW_AHI , data [ pos ] ! = 0 ) ) ;
2021-10-28 18:14:06 +00:00
break ;
2021-06-01 00:24:09 +00:00
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 > ParsedEventsF0V6 = {
PRS1PressureSetEvent : : TYPE ,
PRS1IPAPSetEvent : : TYPE ,
PRS1EPAPSetEvent : : TYPE ,
PRS1AutoPressureSetEvent : : TYPE ,
PRS1PressurePulseEvent : : TYPE ,
PRS1RERAEvent : : TYPE ,
PRS1ObstructiveApneaEvent : : TYPE ,
PRS1ClearAirwayEvent : : TYPE ,
PRS1HypopneaEvent : : TYPE ,
PRS1FlowLimitationEvent : : TYPE ,
PRS1VibratorySnoreEvent : : TYPE ,
PRS1VariableBreathingEvent : : TYPE ,
PRS1PeriodicBreathingEvent : : TYPE ,
PRS1LargeLeakEvent : : TYPE ,
PRS1TotalLeakEvent : : TYPE ,
PRS1SnoreEvent : : TYPE ,
PRS1PressureAverageEvent : : TYPE ,
PRS1FlexPressureAverageEvent : : TYPE ,
PRS1SnoresAtPressureEvent : : TYPE ,
} ;
2022-02-27 16:50:10 +00:00
// DreamStation family 0 CPAP/APAP devices (400X-700X, 400G-502G)
2021-06-01 00:24:09 +00:00
// Originally derived from F5V3 parsing + (incomplete) F0V234 parsing + sample data
bool PRS1DataChunk : : ParseEventsF0V6 ( )
{
if ( this - > family ! = 0 | | this - > familyVersion ! = 6 ) {
qWarning ( ) < < " ParseEventsF0V6 called with family " < < this - > family < < " familyVersion " < < this - > familyVersion ;
return false ;
}
const unsigned char * data = ( unsigned char * ) this - > m_data . constData ( ) ;
int chunk_size = this - > m_data . size ( ) ;
static const int minimum_sizes [ ] = { 2 , 3 , 4 , 3 , 3 , 3 , 3 , 3 , 3 , 2 , 3 , 4 , 3 , 2 , 5 , 5 , 5 , 5 , 4 , 3 , 3 , 3 } ;
static const int ncodes = sizeof ( minimum_sizes ) / sizeof ( int ) ;
if ( chunk_size < 1 ) {
// This does occasionally happen.
qDebug ( ) < < this - > sessionid < < " Empty event data " ;
return false ;
}
bool ok = true ;
int pos = 0 , startpos ;
int code , size ;
int t = 0 ;
int elapsed , duration , value ;
bool is_bilevel = false ;
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 ;
if ( code ! = 0x12 ) { // This one event has no timestamp
t + = data [ pos ] | ( data [ pos + 1 ] < < 8 ) ;
pos + = 2 ;
}
switch ( code ) {
//case 0x00: // never seen
case 0x01 : // Pressure adjustment
// Matches pressure setting, both initial and when ramp button pressed.
// Based on waveform reports, it looks like the pressure graph is drawn by
// interpolating between these pressure adjustments, by 0.5 cmH2O spaced evenly between
// adjustments. E.g. 6 at 28:11 and 7.3 at 29:05 results in the following dots:
// 6 at 28:11, 6.5 around 28:30, 7.0 around 28:50, 7(.3) at 29:05. That holds until
// subsequent "adjustment" of 7.3 at 30:09 followed by 8.0 at 30:19.
this - > AddEvent ( new PRS1PressureSetEvent ( t , data [ pos ] ) ) ;
break ;
case 0x02 : // Pressure adjustment (bi-level)
// See notes above on interpolation.
this - > AddEvent ( new PRS1IPAPSetEvent ( t , data [ pos + 1 ] ) ) ;
this - > AddEvent ( new PRS1EPAPSetEvent ( t , data [ pos ] ) ) ; // EPAP needs to be added second to calculate PS
is_bilevel = true ;
break ;
case 0x03 : // Auto-CPAP starting pressure
// Most of the time this occurs, it's at the start and end of a session with
// the same pressure at both. Occasionally an additional event shows up in the
// middle of a session, and then the pressure at the end matches that.
// In these cases, the new pressure corresponds to the next night's starting
// pressure for auto-CPAP. It does not appear to have any effect on the current
// night's pressure, unless there's a substantial gap between sessions, in
// which case the next session may use the new starting pressure.
//CHECK_VALUE(data[pos], 40);
// TODO: What does this mean in bi-level mode?
// See F0V4 event 3 for comparison. TODO: See if there's an Opti-Start label on F0V6 reports.
this - > AddEvent ( new PRS1AutoPressureSetEvent ( t , data [ pos ] ) ) ;
break ;
case 0x04 : // Pressure Pulse
duration = data [ pos ] ; // TODO: is this a duration?
this - > AddEvent ( new PRS1PressurePulseEvent ( t , duration ) ) ;
break ;
case 0x05 : // RERA
elapsed = data [ pos ] ; // based on sample waveform, the RERA is over after this
this - > AddEvent ( new PRS1RERAEvent ( t - elapsed , 0 ) ) ;
break ;
case 0x06 : // Obstructive Apnea
// OA events are instantaneous flags with no duration: reviewing waveforms
// shows that the time elapsed between the flag and reporting often includes
// non-apnea breathing.
elapsed = data [ pos ] ;
this - > AddEvent ( new PRS1ObstructiveApneaEvent ( t - elapsed , 0 ) ) ;
break ;
case 0x07 : // Clear Airway Apnea
// CA events are instantaneous flags with no duration: reviewing waveforms
// shows that the time elapsed between the flag and reporting often includes
// non-apnea breathing.
elapsed = data [ pos ] ;
this - > AddEvent ( new PRS1ClearAirwayEvent ( t - elapsed , 0 ) ) ;
break ;
//case 0x08: // never seen
//case 0x09: // never seen
//case 0x0a: // Hypopnea, see 0x15
case 0x0b : // Hypopnea
// TODO: How is this hypopnea different from events 0xa, 0x14 and 0x15?
// TODO: What is the first byte?
//data[pos+0]; // unknown first byte?
elapsed = data [ pos + 1 ] ; // based on sample waveform, the hypopnea is over after this
this - > AddEvent ( new PRS1HypopneaEvent ( t - elapsed , 0 ) ) ;
break ;
case 0x0c : // Flow Limitation
// TODO: We should revisit whether this is elapsed or duration once (if)
// we start calculating flow limitations ourselves. Flow limitations aren't
// as obvious as OA/CA when looking at a waveform.
elapsed = data [ pos ] ;
this - > AddEvent ( new PRS1FlowLimitationEvent ( t - elapsed , 0 ) ) ;
break ;
case 0x0d : // Vibratory Snore
// VS events are instantaneous flags with no duration, drawn on the official waveform.
// The current thinking is that these are the snores that cause a change in auto-titrating
// pressure. The snoring statistics below seem to be a total count. It's unclear whether
// the trigger for pressure change is severity or count or something else.
// no data bytes
this - > AddEvent ( new PRS1VibratorySnoreEvent ( t , 0 ) ) ;
break ;
case 0x0e : // Variable Breathing?
duration = 2 * ( data [ pos ] | ( data [ pos + 1 ] < < 8 ) ) ;
elapsed = data [ pos + 2 ] ; // this is always 60 seconds unless it's at the end, so it seems like elapsed
CHECK_VALUES ( elapsed , 60 , 0 ) ;
this - > AddEvent ( new PRS1VariableBreathingEvent ( t - elapsed - duration , duration ) ) ;
break ;
case 0x0f : // Periodic Breathing
// PB events are reported some time after they conclude, and they do have a reported duration.
duration = 2 * ( data [ pos ] | ( data [ pos + 1 ] < < 8 ) ) ;
elapsed = data [ pos + 2 ] ;
this - > AddEvent ( new PRS1PeriodicBreathingEvent ( t - elapsed - duration , duration ) ) ;
break ;
case 0x10 : // Large Leak
// LL events are reported some time after they conclude, and they do have a reported duration.
duration = 2 * ( data [ pos ] | ( data [ pos + 1 ] < < 8 ) ) ;
elapsed = data [ pos + 2 ] ;
this - > AddEvent ( new PRS1LargeLeakEvent ( t - elapsed - duration , duration ) ) ;
break ;
case 0x11 : // Statistics
this - > AddEvent ( new PRS1TotalLeakEvent ( t , data [ pos ] ) ) ;
this - > AddEvent ( new PRS1SnoreEvent ( t , data [ pos + 1 ] ) ) ;
value = data [ pos + 2 ] ;
if ( is_bilevel ) {
// For bi-level modes, this appears to be the time-weighted average of EPAP and IPAP actually provided.
this - > AddEvent ( new PRS1PressureAverageEvent ( t , value ) ) ;
} else {
// For single-pressure modes, this appears to be the average effective "EPAP" provided by Flex.
//
// Sample data shows this value around 10.3 cmH2O for a prescribed pressure of 12.0 (C-Flex+ 3).
// That's too low for an average pressure over time, but could easily be an average commanded EPAP.
// When flex mode is off, this is exactly the current CPAP set point.
this - > AddEvent ( new PRS1FlexPressureAverageEvent ( t , value ) ) ;
}
this - > AddEvent ( new PRS1IntervalBoundaryEvent ( t ) ) ;
break ;
case 0x12 : // Snore count per pressure
// Some sessions (with lots of ramps) have multiple of these, each with a
// different pressure. The total snore count across all of them matches the
// total found in the stats event.
if ( data [ pos ] ! = 0 ) {
CHECK_VALUES ( data [ pos ] , 1 , 2 ) ; // 0 = CPAP pressure, 1 = bi-level EPAP, 2 = bi-level IPAP
}
//CHECK_VALUE(data[pos+1], 0x78); // pressure
//CHECK_VALUE(data[pos+2], 1); // 16-bit snore count
//CHECK_VALUE(data[pos+3], 0);
value = ( data [ pos + 2 ] | ( data [ pos + 3 ] < < 8 ) ) ;
this - > AddEvent ( new PRS1SnoresAtPressureEvent ( t , data [ pos ] , data [ pos + 1 ] , value ) ) ;
break ;
//case 0x13: // never seen
case 0x0a : // Hypopnea
// TODO: Why does this hypopnea have a different event code?
// fall through
case 0x14 : // Hypopnea, new to F0V6
// TODO: Why does this hypopnea have a different event code?
// fall through
case 0x15 : // Hypopnea, new to F0V6
// 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 ;
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 ;
}