mirror of
https://gitlab.com/pholy/OSCAR-code.git
synced 2025-04-05 18:50:44 +00:00
This fixes the mask pressure graph as well as many of the events. There are still some issues with presentation: some of the events are being drawn at the wrong time, and certain events and statistics don't really behave the way they're displayed. Also several events have yet to be encountered in sample data.
6271 lines
235 KiB
C++
6271 lines
235 KiB
C++
/* SleepLib PRS1 Loader Implementation
|
|
*
|
|
* Copyright (c) 2019 The OSCAR Team
|
|
* 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 <QApplication>
|
|
#include <QString>
|
|
#include <QDateTime>
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QDataStream>
|
|
#include <QMessageBox>
|
|
#include <QDebug>
|
|
#include <cmath>
|
|
|
|
#include "SleepLib/schema.h"
|
|
#include "prs1_loader.h"
|
|
#include "SleepLib/session.h"
|
|
#include "SleepLib/calcs.h"
|
|
|
|
|
|
// Disable this to cut excess debug messages
|
|
|
|
#define DEBUG_SUMMARY
|
|
|
|
|
|
//const int PRS1_MAGIC_NUMBER = 2;
|
|
//const int PRS1_SUMMARY_FILE=1;
|
|
//const int PRS1_EVENT_FILE=2;
|
|
//const int PRS1_WAVEFORM_FILE=5;
|
|
|
|
const int PRS1_HTYPE_NORMAL=0;
|
|
const int PRS1_HTYPE_INTERVAL=1;
|
|
|
|
|
|
//********************************************************************************************
|
|
/// IMPORTANT!!!
|
|
//********************************************************************************************
|
|
// Please INCREMENT the prs1_data_version in prs1_loader.h when making changes to this loader
|
|
// that change loader behaviour or modify channels.
|
|
//********************************************************************************************
|
|
|
|
QHash<int, QString> ModelMap;
|
|
|
|
|
|
// CRC-16/KERMIT, polynomial: 0x11021, bit reverse algorithm
|
|
// Table generated by crcmod (crc-kermit)
|
|
|
|
typedef quint16 crc16_t;
|
|
static crc16_t CRC16(unsigned char * data, size_t data_len, crc16_t crc=0)
|
|
{
|
|
static const crc16_t table[256] = {
|
|
0x0000U, 0x1189U, 0x2312U, 0x329bU, 0x4624U, 0x57adU, 0x6536U, 0x74bfU,
|
|
0x8c48U, 0x9dc1U, 0xaf5aU, 0xbed3U, 0xca6cU, 0xdbe5U, 0xe97eU, 0xf8f7U,
|
|
0x1081U, 0x0108U, 0x3393U, 0x221aU, 0x56a5U, 0x472cU, 0x75b7U, 0x643eU,
|
|
0x9cc9U, 0x8d40U, 0xbfdbU, 0xae52U, 0xdaedU, 0xcb64U, 0xf9ffU, 0xe876U,
|
|
0x2102U, 0x308bU, 0x0210U, 0x1399U, 0x6726U, 0x76afU, 0x4434U, 0x55bdU,
|
|
0xad4aU, 0xbcc3U, 0x8e58U, 0x9fd1U, 0xeb6eU, 0xfae7U, 0xc87cU, 0xd9f5U,
|
|
0x3183U, 0x200aU, 0x1291U, 0x0318U, 0x77a7U, 0x662eU, 0x54b5U, 0x453cU,
|
|
0xbdcbU, 0xac42U, 0x9ed9U, 0x8f50U, 0xfbefU, 0xea66U, 0xd8fdU, 0xc974U,
|
|
0x4204U, 0x538dU, 0x6116U, 0x709fU, 0x0420U, 0x15a9U, 0x2732U, 0x36bbU,
|
|
0xce4cU, 0xdfc5U, 0xed5eU, 0xfcd7U, 0x8868U, 0x99e1U, 0xab7aU, 0xbaf3U,
|
|
0x5285U, 0x430cU, 0x7197U, 0x601eU, 0x14a1U, 0x0528U, 0x37b3U, 0x263aU,
|
|
0xdecdU, 0xcf44U, 0xfddfU, 0xec56U, 0x98e9U, 0x8960U, 0xbbfbU, 0xaa72U,
|
|
0x6306U, 0x728fU, 0x4014U, 0x519dU, 0x2522U, 0x34abU, 0x0630U, 0x17b9U,
|
|
0xef4eU, 0xfec7U, 0xcc5cU, 0xddd5U, 0xa96aU, 0xb8e3U, 0x8a78U, 0x9bf1U,
|
|
0x7387U, 0x620eU, 0x5095U, 0x411cU, 0x35a3U, 0x242aU, 0x16b1U, 0x0738U,
|
|
0xffcfU, 0xee46U, 0xdcddU, 0xcd54U, 0xb9ebU, 0xa862U, 0x9af9U, 0x8b70U,
|
|
0x8408U, 0x9581U, 0xa71aU, 0xb693U, 0xc22cU, 0xd3a5U, 0xe13eU, 0xf0b7U,
|
|
0x0840U, 0x19c9U, 0x2b52U, 0x3adbU, 0x4e64U, 0x5fedU, 0x6d76U, 0x7cffU,
|
|
0x9489U, 0x8500U, 0xb79bU, 0xa612U, 0xd2adU, 0xc324U, 0xf1bfU, 0xe036U,
|
|
0x18c1U, 0x0948U, 0x3bd3U, 0x2a5aU, 0x5ee5U, 0x4f6cU, 0x7df7U, 0x6c7eU,
|
|
0xa50aU, 0xb483U, 0x8618U, 0x9791U, 0xe32eU, 0xf2a7U, 0xc03cU, 0xd1b5U,
|
|
0x2942U, 0x38cbU, 0x0a50U, 0x1bd9U, 0x6f66U, 0x7eefU, 0x4c74U, 0x5dfdU,
|
|
0xb58bU, 0xa402U, 0x9699U, 0x8710U, 0xf3afU, 0xe226U, 0xd0bdU, 0xc134U,
|
|
0x39c3U, 0x284aU, 0x1ad1U, 0x0b58U, 0x7fe7U, 0x6e6eU, 0x5cf5U, 0x4d7cU,
|
|
0xc60cU, 0xd785U, 0xe51eU, 0xf497U, 0x8028U, 0x91a1U, 0xa33aU, 0xb2b3U,
|
|
0x4a44U, 0x5bcdU, 0x6956U, 0x78dfU, 0x0c60U, 0x1de9U, 0x2f72U, 0x3efbU,
|
|
0xd68dU, 0xc704U, 0xf59fU, 0xe416U, 0x90a9U, 0x8120U, 0xb3bbU, 0xa232U,
|
|
0x5ac5U, 0x4b4cU, 0x79d7U, 0x685eU, 0x1ce1U, 0x0d68U, 0x3ff3U, 0x2e7aU,
|
|
0xe70eU, 0xf687U, 0xc41cU, 0xd595U, 0xa12aU, 0xb0a3U, 0x8238U, 0x93b1U,
|
|
0x6b46U, 0x7acfU, 0x4854U, 0x59ddU, 0x2d62U, 0x3cebU, 0x0e70U, 0x1ff9U,
|
|
0xf78fU, 0xe606U, 0xd49dU, 0xc514U, 0xb1abU, 0xa022U, 0x92b9U, 0x8330U,
|
|
0x7bc7U, 0x6a4eU, 0x58d5U, 0x495cU, 0x3de3U, 0x2c6aU, 0x1ef1U, 0x0f78U,
|
|
};
|
|
|
|
for (size_t i=0; i < data_len; i++) {
|
|
crc = table[(*data ^ (unsigned char)crc) & 0xFF] ^ (crc >> 8);
|
|
data++;
|
|
}
|
|
return crc;
|
|
}
|
|
|
|
|
|
// CRC-32/MPEG-2, polynomial: 0x104C11DB7
|
|
// Table generated by crcmod (crc-32-mpeg)
|
|
|
|
typedef quint32 crc32_t;
|
|
static crc32_t CRC32(const unsigned char *data, size_t data_len, crc32_t crc=0xffffffffU)
|
|
{
|
|
static const crc32_t table[256] = {
|
|
0x00000000U, 0x04c11db7U, 0x09823b6eU, 0x0d4326d9U,
|
|
0x130476dcU, 0x17c56b6bU, 0x1a864db2U, 0x1e475005U,
|
|
0x2608edb8U, 0x22c9f00fU, 0x2f8ad6d6U, 0x2b4bcb61U,
|
|
0x350c9b64U, 0x31cd86d3U, 0x3c8ea00aU, 0x384fbdbdU,
|
|
0x4c11db70U, 0x48d0c6c7U, 0x4593e01eU, 0x4152fda9U,
|
|
0x5f15adacU, 0x5bd4b01bU, 0x569796c2U, 0x52568b75U,
|
|
0x6a1936c8U, 0x6ed82b7fU, 0x639b0da6U, 0x675a1011U,
|
|
0x791d4014U, 0x7ddc5da3U, 0x709f7b7aU, 0x745e66cdU,
|
|
0x9823b6e0U, 0x9ce2ab57U, 0x91a18d8eU, 0x95609039U,
|
|
0x8b27c03cU, 0x8fe6dd8bU, 0x82a5fb52U, 0x8664e6e5U,
|
|
0xbe2b5b58U, 0xbaea46efU, 0xb7a96036U, 0xb3687d81U,
|
|
0xad2f2d84U, 0xa9ee3033U, 0xa4ad16eaU, 0xa06c0b5dU,
|
|
0xd4326d90U, 0xd0f37027U, 0xddb056feU, 0xd9714b49U,
|
|
0xc7361b4cU, 0xc3f706fbU, 0xceb42022U, 0xca753d95U,
|
|
0xf23a8028U, 0xf6fb9d9fU, 0xfbb8bb46U, 0xff79a6f1U,
|
|
0xe13ef6f4U, 0xe5ffeb43U, 0xe8bccd9aU, 0xec7dd02dU,
|
|
0x34867077U, 0x30476dc0U, 0x3d044b19U, 0x39c556aeU,
|
|
0x278206abU, 0x23431b1cU, 0x2e003dc5U, 0x2ac12072U,
|
|
0x128e9dcfU, 0x164f8078U, 0x1b0ca6a1U, 0x1fcdbb16U,
|
|
0x018aeb13U, 0x054bf6a4U, 0x0808d07dU, 0x0cc9cdcaU,
|
|
0x7897ab07U, 0x7c56b6b0U, 0x71159069U, 0x75d48ddeU,
|
|
0x6b93dddbU, 0x6f52c06cU, 0x6211e6b5U, 0x66d0fb02U,
|
|
0x5e9f46bfU, 0x5a5e5b08U, 0x571d7dd1U, 0x53dc6066U,
|
|
0x4d9b3063U, 0x495a2dd4U, 0x44190b0dU, 0x40d816baU,
|
|
0xaca5c697U, 0xa864db20U, 0xa527fdf9U, 0xa1e6e04eU,
|
|
0xbfa1b04bU, 0xbb60adfcU, 0xb6238b25U, 0xb2e29692U,
|
|
0x8aad2b2fU, 0x8e6c3698U, 0x832f1041U, 0x87ee0df6U,
|
|
0x99a95df3U, 0x9d684044U, 0x902b669dU, 0x94ea7b2aU,
|
|
0xe0b41de7U, 0xe4750050U, 0xe9362689U, 0xedf73b3eU,
|
|
0xf3b06b3bU, 0xf771768cU, 0xfa325055U, 0xfef34de2U,
|
|
0xc6bcf05fU, 0xc27dede8U, 0xcf3ecb31U, 0xcbffd686U,
|
|
0xd5b88683U, 0xd1799b34U, 0xdc3abdedU, 0xd8fba05aU,
|
|
0x690ce0eeU, 0x6dcdfd59U, 0x608edb80U, 0x644fc637U,
|
|
0x7a089632U, 0x7ec98b85U, 0x738aad5cU, 0x774bb0ebU,
|
|
0x4f040d56U, 0x4bc510e1U, 0x46863638U, 0x42472b8fU,
|
|
0x5c007b8aU, 0x58c1663dU, 0x558240e4U, 0x51435d53U,
|
|
0x251d3b9eU, 0x21dc2629U, 0x2c9f00f0U, 0x285e1d47U,
|
|
0x36194d42U, 0x32d850f5U, 0x3f9b762cU, 0x3b5a6b9bU,
|
|
0x0315d626U, 0x07d4cb91U, 0x0a97ed48U, 0x0e56f0ffU,
|
|
0x1011a0faU, 0x14d0bd4dU, 0x19939b94U, 0x1d528623U,
|
|
0xf12f560eU, 0xf5ee4bb9U, 0xf8ad6d60U, 0xfc6c70d7U,
|
|
0xe22b20d2U, 0xe6ea3d65U, 0xeba91bbcU, 0xef68060bU,
|
|
0xd727bbb6U, 0xd3e6a601U, 0xdea580d8U, 0xda649d6fU,
|
|
0xc423cd6aU, 0xc0e2d0ddU, 0xcda1f604U, 0xc960ebb3U,
|
|
0xbd3e8d7eU, 0xb9ff90c9U, 0xb4bcb610U, 0xb07daba7U,
|
|
0xae3afba2U, 0xaafbe615U, 0xa7b8c0ccU, 0xa379dd7bU,
|
|
0x9b3660c6U, 0x9ff77d71U, 0x92b45ba8U, 0x9675461fU,
|
|
0x8832161aU, 0x8cf30badU, 0x81b02d74U, 0x857130c3U,
|
|
0x5d8a9099U, 0x594b8d2eU, 0x5408abf7U, 0x50c9b640U,
|
|
0x4e8ee645U, 0x4a4ffbf2U, 0x470cdd2bU, 0x43cdc09cU,
|
|
0x7b827d21U, 0x7f436096U, 0x7200464fU, 0x76c15bf8U,
|
|
0x68860bfdU, 0x6c47164aU, 0x61043093U, 0x65c52d24U,
|
|
0x119b4be9U, 0x155a565eU, 0x18197087U, 0x1cd86d30U,
|
|
0x029f3d35U, 0x065e2082U, 0x0b1d065bU, 0x0fdc1becU,
|
|
0x3793a651U, 0x3352bbe6U, 0x3e119d3fU, 0x3ad08088U,
|
|
0x2497d08dU, 0x2056cd3aU, 0x2d15ebe3U, 0x29d4f654U,
|
|
0xc5a92679U, 0xc1683bceU, 0xcc2b1d17U, 0xc8ea00a0U,
|
|
0xd6ad50a5U, 0xd26c4d12U, 0xdf2f6bcbU, 0xdbee767cU,
|
|
0xe3a1cbc1U, 0xe760d676U, 0xea23f0afU, 0xeee2ed18U,
|
|
0xf0a5bd1dU, 0xf464a0aaU, 0xf9278673U, 0xfde69bc4U,
|
|
0x89b8fd09U, 0x8d79e0beU, 0x803ac667U, 0x84fbdbd0U,
|
|
0x9abc8bd5U, 0x9e7d9662U, 0x933eb0bbU, 0x97ffad0cU,
|
|
0xafb010b1U, 0xab710d06U, 0xa6322bdfU, 0xa2f33668U,
|
|
0xbcb4666dU, 0xb8757bdaU, 0xb5365d03U, 0xb1f740b4U,
|
|
};
|
|
|
|
for (size_t i=0; i < data_len; i++) {
|
|
crc = table[(*data ^ (unsigned char)(crc >> 24)) & 0xFF] ^ (crc << 8);
|
|
data++;
|
|
}
|
|
return crc;
|
|
}
|
|
|
|
|
|
// Strangely, the PRS1 CRC32 appears to consider every byte a 32-bit wchar_t.
|
|
// Nothing like trying a bunch of encodings and CRC32 variants on PROP.TXT files
|
|
// until you find a winner.
|
|
|
|
static crc32_t CRC32wchar(const unsigned char *data, size_t data_len, crc32_t crc=0xffffffffU)
|
|
{
|
|
for (size_t i=0; i < data_len; i++) {
|
|
static unsigned char wch[4] = { 0, 0, 0, 0 };
|
|
wch[3] = *data++;
|
|
crc = CRC32(wch, 4, crc);
|
|
}
|
|
return crc;
|
|
}
|
|
|
|
|
|
enum FlexMode { FLEX_None, FLEX_CFlex, FLEX_CFlexPlus, FLEX_AFlex, FLEX_RiseTime, FLEX_BiFlex, FLEX_Unknown };
|
|
|
|
ChannelID PRS1_TimedBreath = 0, PRS1_HeatedTubing = 0;
|
|
|
|
#if 0 // Apparently unused
|
|
PRS1::PRS1(Profile *profile, MachineID id): CPAP(profile, id)
|
|
{
|
|
}
|
|
PRS1::~PRS1()
|
|
{
|
|
|
|
}
|
|
#endif
|
|
|
|
struct PRS1TestedModel
|
|
{
|
|
QString model;
|
|
int family;
|
|
int familyVersion;
|
|
};
|
|
|
|
static const PRS1TestedModel s_PRS1TestedModels[] = {
|
|
{ "251P", 0, 2 }, // "REMstar Plus (Philips Respironics)" (brick)
|
|
{ "450P", 0, 3 }, // "REMstar Pro (Philips Respironics)"
|
|
{ "451P", 0, 3 }, // "REMstar Pro (Philips Respironics)"
|
|
{ "550P", 0, 2 }, // "REMstar Auto (Philips Respironics)"
|
|
{ "550P", 0, 3 }, // "REMstar Auto (Philips Respironics)"
|
|
{ "551P", 0, 2 }, // "REMstar Auto (Philips Respironics)"
|
|
{ "750P", 0, 2 }, // "BiPAP Auto (Philips Respironics)"
|
|
|
|
{ "460P", 0, 4 },
|
|
{ "461P", 0, 4 },
|
|
{ "560P", 0, 4 },
|
|
{ "560PBT", 0, 4 },
|
|
{ "561P", 0, 4 },
|
|
{ "660P", 0, 4 },
|
|
{ "760P", 0, 4 },
|
|
|
|
{ "200X110", 0, 6 }, // "DreamStation CPAP" (brick)
|
|
{ "400G110", 0, 6 }, // "DreamStation Go"
|
|
{ "400X110", 0, 6 }, // "DreamStation CPAP Pro"
|
|
{ "400X150", 0, 6 }, // "DreamStation CPAP Pro"
|
|
{ "500X110", 0, 6 }, // "DreamStation Auto CPAP"
|
|
{ "500X150", 0, 6 }, // "DreamStation Auto CPAP"
|
|
{ "502G150", 0, 6 }, // "DreamStation Go Auto"
|
|
{ "600X110", 0, 6 }, // "DreamStation BiPAP Pro"
|
|
{ "700X110", 0, 6 }, // "DreamStation Auto BiPAP"
|
|
|
|
{ "950P", 5, 0 },
|
|
{ "960P", 5, 1 },
|
|
{ "961P", 5, 1 },
|
|
{ "960T", 5, 2 },
|
|
{ "900X110", 5, 3 }, // "DreamStation BiPAP autoSV"
|
|
{ "900X120", 5, 3 }, // "DreamStation BiPAP autoSV"
|
|
|
|
{ "1061T", 3, 3 },
|
|
{ "1160P", 3, 3 },
|
|
{ "1030X110", 3, 6 },
|
|
{ "1130X110", 3, 6 },
|
|
|
|
{ "", 0, 0 },
|
|
};
|
|
PRS1ModelInfo s_PRS1ModelInfo;
|
|
|
|
PRS1ModelInfo::PRS1ModelInfo()
|
|
{
|
|
for (int i = 0; !s_PRS1TestedModels[i].model.isEmpty(); i++) {
|
|
const PRS1TestedModel & model = s_PRS1TestedModels[i];
|
|
m_testedModels[model.family][model.familyVersion].append(model.model);
|
|
}
|
|
}
|
|
|
|
bool PRS1ModelInfo::IsSupported(int family, int familyVersion) const
|
|
{
|
|
if (m_testedModels.value(family).contains(familyVersion)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool PRS1ModelInfo::IsTested(const QString & model, int family, int familyVersion) const
|
|
{
|
|
if (m_testedModels.value(family).value(familyVersion).contains(model)) {
|
|
return true;
|
|
}
|
|
// Some 500X150 C0/C1 folders have contained this bogus model number in their PROP.TXT file,
|
|
// with the same serial number seen in the main PROP.TXT file that shows the real model number.
|
|
if (model == "100X100") {
|
|
#ifndef UNITTEST_MODE
|
|
qDebug() << "Ignoring 100X100 for untested alert";
|
|
#endif
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
bool PRS1ModelInfo::IsSupported(const QHash<QString,QString> & props) const
|
|
{
|
|
bool ok;
|
|
int family = props["Family"].toInt(&ok, 10);
|
|
if (ok) {
|
|
int familyVersion = props["FamilyVersion"].toInt(&ok, 10);
|
|
if (ok) {
|
|
ok = IsSupported(family, familyVersion);
|
|
}
|
|
}
|
|
return ok;
|
|
}
|
|
|
|
bool PRS1ModelInfo::IsTested(const QHash<QString,QString> & props) const
|
|
{
|
|
bool ok;
|
|
int family = props["Family"].toInt(&ok, 10);
|
|
if (ok) {
|
|
int familyVersion = props["FamilyVersion"].toInt(&ok, 10);
|
|
if (ok) {
|
|
ok = IsTested(props["ModelNumber"], family, familyVersion);
|
|
}
|
|
}
|
|
return ok;
|
|
};
|
|
|
|
// TODO: add brick list, IsBrick() test
|
|
// TODO: add model name, Name() function
|
|
|
|
|
|
PRS1Loader::PRS1Loader()
|
|
{
|
|
#ifndef UNITTEST_MODE // no QPixmap without a QGuiApplication
|
|
const QString PRS1_ICON = ":/icons/prs1.png";
|
|
const QString PRS1_60_ICON = ":/icons/prs1_60s.png";
|
|
const QString DREAMSTATION_ICON = ":/icons/dreamstation.png";
|
|
|
|
// QString s = newInfo().series;
|
|
m_pixmap_paths["System One"] = PRS1_ICON;
|
|
m_pixmaps["System One"] = QPixmap(PRS1_ICON);
|
|
m_pixmap_paths["System One (60 Series)"] = PRS1_60_ICON;
|
|
m_pixmaps["System One (60 Series)"] = QPixmap(PRS1_60_ICON);
|
|
m_pixmap_paths["DreamStation"] = DREAMSTATION_ICON;
|
|
m_pixmaps["DreamStation"] = QPixmap(DREAMSTATION_ICON);
|
|
#endif
|
|
|
|
m_type = MT_CPAP;
|
|
}
|
|
|
|
PRS1Loader::~PRS1Loader()
|
|
{
|
|
}
|
|
|
|
bool isdigit(QChar c)
|
|
{
|
|
if ((c >= '0') && (c <= '9')) { return true; }
|
|
|
|
return false;
|
|
}
|
|
|
|
const QString PR_STR_PSeries = "P-Series";
|
|
|
|
|
|
// Tests path to see if it has (what looks like) a valid PRS1 folder structure
|
|
bool PRS1Loader::Detect(const QString & path)
|
|
{
|
|
QString newpath = checkDir(path);
|
|
|
|
return !newpath.isEmpty();
|
|
}
|
|
|
|
|
|
QString PRS1Loader::checkDir(const QString & path)
|
|
{
|
|
QString newpath = path;
|
|
|
|
newpath.replace("\\", "/");
|
|
|
|
if (!newpath.endsWith("/" + PR_STR_PSeries)) {
|
|
newpath = path + "/" + PR_STR_PSeries;
|
|
}
|
|
|
|
QDir dir(newpath);
|
|
|
|
if ((!dir.exists() || !dir.isReadable())) {
|
|
return QString();
|
|
}
|
|
qDebug() << "PRS1Loader::Detect path=" << newpath;
|
|
|
|
QFile lastfile(newpath+"/last.txt");
|
|
|
|
bool exists = true;
|
|
if (!lastfile.exists()) {
|
|
lastfile.setFileName(newpath+"/LAST.TXT");
|
|
if (!lastfile.exists())
|
|
exists = false;
|
|
}
|
|
|
|
QString machpath;
|
|
if (exists) {
|
|
if (!lastfile.open(QIODevice::ReadOnly)) {
|
|
qDebug() << "PRS1Loader: last.txt exists but I couldn't open it!";
|
|
} else {
|
|
QTextStream ts(&lastfile);
|
|
QString serial = ts.readLine(64).trimmed();
|
|
lastfile.close();
|
|
|
|
machpath = newpath+"/"+serial;
|
|
|
|
if (!QDir(machpath).exists()) {
|
|
machpath = QString();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (machpath.isEmpty()) {
|
|
QDir dir(newpath);
|
|
QStringList dirs = dir.entryList(QDir::NoDotAndDotDot | QDir::Dirs);
|
|
if (dirs.size() > 0) {
|
|
machpath = dir.cleanPath(newpath+"/"+dirs[0]);
|
|
|
|
}
|
|
}
|
|
|
|
|
|
return machpath;
|
|
}
|
|
|
|
void parseModel(MachineInfo & info, const QString & modelnum)
|
|
{
|
|
info.modelnumber = modelnum;
|
|
|
|
QString modelstr;
|
|
bool fnd = false;
|
|
for (int i=0; i<modelnum.size(); i++) {
|
|
QChar c = modelnum.at(i);
|
|
if (c.isDigit()) {
|
|
modelstr += c;
|
|
fnd = true;
|
|
} else if (fnd) break;
|
|
}
|
|
|
|
bool ok;
|
|
int num = modelstr.toInt(&ok);
|
|
|
|
int series = ((num / 10) % 10);
|
|
int type = (num / 100);
|
|
int country = num % 10;
|
|
|
|
|
|
switch (type) {
|
|
case 1: // cpap
|
|
case 2: // cpap
|
|
case 3: // cpap
|
|
info.model = QObject::tr("RemStar Plus Compliance Only");
|
|
break;
|
|
case 4: // cpap
|
|
info.model = QObject::tr("RemStar Pro with C-Flex+");
|
|
break;
|
|
case 5: // apap
|
|
info.model = QObject::tr("RemStar Auto with A-Flex");
|
|
break;
|
|
case 6: // bipap
|
|
info.model = QObject::tr("RemStar BiPAP Pro with Bi-Flex");
|
|
break;
|
|
case 7: // bipap auto
|
|
info.model = QObject::tr("RemStar BiPAP Auto with Bi-Flex");
|
|
break;
|
|
case 9: // asv
|
|
info.model = QObject::tr("BiPAP autoSV Advanced");
|
|
break;
|
|
case 10: // Avaps
|
|
info.model = QObject::tr("BiPAP AVAPS");
|
|
break;
|
|
default:
|
|
info.model = QObject::tr("Unknown Model");
|
|
}
|
|
|
|
switch (series) {
|
|
case 5:
|
|
info.series = QObject::tr("System One");
|
|
break;
|
|
case 6:
|
|
info.series = QObject::tr("System One (60 Series)");
|
|
break;
|
|
case 7:
|
|
info.series = QObject::tr("DreamStation");
|
|
break;
|
|
default:
|
|
info.series = QObject::tr("unknown");
|
|
break;
|
|
|
|
}
|
|
switch (country) {
|
|
case '0':
|
|
break;
|
|
case '1':
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
bool PRS1Loader::PeekProperties(const QString & filename, QHash<QString,QString> & props)
|
|
{
|
|
const static QMap<QString,QString> s_longFieldNames = {
|
|
// CF?
|
|
{ "SN", "SerialNumber" },
|
|
{ "MN", "ModelNumber" },
|
|
{ "PT", "ProductType" },
|
|
{ "DF", "DataFormat" },
|
|
{ "DFV", "DataFormatVersion" },
|
|
{ "F", "Family" },
|
|
{ "FV", "FamilyVersion" },
|
|
{ "SV", "SoftwareVersion" },
|
|
{ "FD", "FirstDate" },
|
|
{ "LD", "LastDate" },
|
|
// SID?
|
|
// SK?
|
|
{ "BK", "BasicKey" },
|
|
{ "DK", "DetailsKey" },
|
|
{ "EK", "ErrorKey" },
|
|
{ "FN", "PatientFolderNum" }, // most recent Pn directory
|
|
{ "PFN", "PatientFileNum" }, // number of files in the most recent Pn directory
|
|
{ "EFN", "EquipFileNum" }, // number of .004 files in the E directory
|
|
{ "DFN", "DFileNum" }, // number of .003 files in the D directory
|
|
{ "VC", "ValidCheck" },
|
|
};
|
|
|
|
QFile f(filename);
|
|
if (!f.open(QFile::ReadOnly)) {
|
|
return false;
|
|
}
|
|
QTextStream in(&f);
|
|
do {
|
|
QString line = in.readLine();
|
|
QStringList pair = line.split("=");
|
|
|
|
if (s_longFieldNames.contains(pair[0])) {
|
|
pair[0] = s_longFieldNames[pair[0]];
|
|
}
|
|
if (pair[0] == "Family") {
|
|
if (pair[1] == "xPAP") {
|
|
pair[1] = "0";
|
|
}
|
|
}
|
|
props[pair[0]] = pair[1];
|
|
} while (!in.atEnd());
|
|
|
|
return true;
|
|
}
|
|
|
|
bool PRS1Loader::PeekProperties(MachineInfo & info, const QString & filename, Machine * mach)
|
|
{
|
|
QHash<QString,QString> props;
|
|
if (!PeekProperties(filename, props)) {
|
|
return false;
|
|
}
|
|
QString modelnum;
|
|
int ptype=0;
|
|
int dfv=0;
|
|
bool ok;
|
|
for (auto & key : props.keys()) {
|
|
bool skip = false;
|
|
|
|
if (key == "ModelNumber") {
|
|
modelnum = props[key];
|
|
skip = true;
|
|
}
|
|
if (key == "SerialNumber") {
|
|
info.serial = props[key];
|
|
skip = true;
|
|
}
|
|
if (key == "ProductType") {
|
|
ptype = props[key].toInt(&ok, 16);
|
|
if (!ok) qWarning() << "ProductType" << props[key];
|
|
skip = true;
|
|
}
|
|
if (key == "DataFormatVersion") {
|
|
dfv = props[key].toInt(&ok, 10);
|
|
if (!ok) qWarning() << "DataFormatVersion" << props[key];
|
|
skip = true;
|
|
}
|
|
if (!mach || skip) continue;
|
|
|
|
mach->properties[key] = props[key];
|
|
};
|
|
|
|
// TODO: replace the below logic with PRS1ModelInfo table-driven logic
|
|
|
|
if (!modelnum.isEmpty()) {
|
|
parseModel(info, modelnum);
|
|
}
|
|
|
|
if (ptype > 0) {
|
|
if (ModelMap.contains(ptype)) {
|
|
info.model = ModelMap[ptype];
|
|
}
|
|
}
|
|
|
|
if (dfv == 3) {
|
|
info.series = QObject::tr("DreamStation");
|
|
}
|
|
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
MachineInfo PRS1Loader::PeekInfo(const QString & path)
|
|
{
|
|
QString newpath = checkDir(path);
|
|
if (newpath.isEmpty())
|
|
return MachineInfo();
|
|
|
|
MachineInfo info = newInfo();
|
|
info.serial = newpath.section("/", -1);
|
|
|
|
if (!PeekProperties(info, newpath+"/properties.txt")) {
|
|
PeekProperties(info, newpath+"/PROP.TXT");
|
|
}
|
|
return info;
|
|
}
|
|
|
|
|
|
int PRS1Loader::Open(const QString & dirpath)
|
|
{
|
|
QString newpath;
|
|
QString path(dirpath);
|
|
path = path.replace("\\", "/");
|
|
|
|
if (path.endsWith("/" + PR_STR_PSeries)) {
|
|
newpath = path;
|
|
} else {
|
|
newpath = path + "/" + PR_STR_PSeries;
|
|
}
|
|
|
|
qDebug() << "PRS1Loader::Open path=" << newpath;
|
|
|
|
QDir dir(newpath);
|
|
|
|
if ((!dir.exists() || !dir.isReadable())) {
|
|
return -1;
|
|
}
|
|
|
|
dir.setFilter(QDir::NoDotAndDotDot | QDir::Dirs | QDir::Files | QDir::Hidden | QDir::NoSymLinks);
|
|
dir.setSorting(QDir::Name);
|
|
QFileInfoList flist = dir.entryInfoList();
|
|
|
|
QStringList SerialNumbers;
|
|
QStringList::iterator sn;
|
|
|
|
for (int i = 0; i < flist.size(); i++) {
|
|
QFileInfo fi = flist.at(i);
|
|
QString filename = fi.fileName();
|
|
|
|
if (fi.isDir() && (filename.size() > 4) && (isdigit(filename[1])) && (isdigit(filename[2]))) {
|
|
SerialNumbers.push_back(filename);
|
|
} else if (filename.toLower() == "last.txt") { // last.txt points to the current serial number
|
|
QString file = fi.canonicalFilePath();
|
|
QFile f(file);
|
|
|
|
if (!fi.isReadable()) {
|
|
qDebug() << "PRS1Loader: last.txt exists but I couldn't read it!";
|
|
continue;
|
|
}
|
|
|
|
if (!f.open(QIODevice::ReadOnly)) {
|
|
qDebug() << "PRS1Loader: last.txt exists but I couldn't open it!";
|
|
continue;
|
|
}
|
|
|
|
last = f.readLine(64);
|
|
last = last.trimmed();
|
|
f.close();
|
|
}
|
|
}
|
|
|
|
if (SerialNumbers.empty()) { return -1; }
|
|
|
|
int c = 0;
|
|
|
|
for (sn = SerialNumbers.begin(); sn != SerialNumbers.end(); sn++) {
|
|
if ((*sn)[0].isLetter()) {
|
|
c += OpenMachine(newpath + "/" + *sn);
|
|
}
|
|
}
|
|
// Serial numbers that don't start with a letter.
|
|
for (sn = SerialNumbers.begin(); sn != SerialNumbers.end(); sn++) {
|
|
if (!(*sn)[0].isLetter()) {
|
|
c += OpenMachine(newpath + "/" + *sn);
|
|
}
|
|
}
|
|
|
|
return c;
|
|
}
|
|
|
|
/*bool PRS1Loader::ParseProperties(Machine *m, QString filename)
|
|
{
|
|
QFile f(filename);
|
|
|
|
if (!f.open(QIODevice::ReadOnly)) {
|
|
return false;
|
|
}
|
|
|
|
QString line;
|
|
QHash<QString, QString> prop;
|
|
|
|
QString s = f.readLine();
|
|
QChar sep = '=';
|
|
QString key, value;
|
|
|
|
MachineInfo info = newInfo();
|
|
bool ok;
|
|
|
|
while (!f.atEnd()) {
|
|
key = s.section(sep, 0, 0);
|
|
|
|
if (key == s) { continue; }
|
|
|
|
value = s.section(sep, 1).trimmed();
|
|
|
|
if (value == s) { continue; }
|
|
|
|
if (key.contains("serialnumber",Qt::CaseInsensitive)) {
|
|
info.serial = value;
|
|
} else if (key.contains("modelnumber",Qt::CaseInsensitive)) {
|
|
parseModel(info, value);
|
|
} else {
|
|
if (key.contains("producttype", Qt::CaseInsensitive)) {
|
|
int i = value.toInt(&ok, 16);
|
|
|
|
if (ok) {
|
|
if (ModelMap.find(i) != ModelMap.end()) {
|
|
info.model = ModelMap[i];
|
|
}
|
|
}
|
|
}
|
|
prop[key] = value;
|
|
}
|
|
s = f.readLine();
|
|
}
|
|
|
|
if (info.serial != m->serial()) {
|
|
qDebug() << "Serial Number in PRS1 properties.txt doesn't match machine record";
|
|
}
|
|
m->setInfo(info);
|
|
|
|
for (QHash<QString, QString>::iterator i = prop.begin(); i != prop.end(); i++) {
|
|
m->properties[i.key()] = i.value();
|
|
}
|
|
|
|
f.close();
|
|
return true;
|
|
}*/
|
|
|
|
int PRS1Loader::OpenMachine(const QString & path)
|
|
{
|
|
if (p_profile == nullptr) {
|
|
qWarning() << "PRS1Loader::OpenMachine() called without a valid p_profile object present";
|
|
return 0;
|
|
}
|
|
|
|
qDebug() << "Opening PRS1 " << path;
|
|
QDir dir(path);
|
|
|
|
if (!dir.exists() || (!dir.isReadable())) {
|
|
return 0;
|
|
}
|
|
m_abort = false;
|
|
|
|
emit updateMessage(QObject::tr("Getting Ready..."));
|
|
QCoreApplication::processEvents();
|
|
|
|
emit setProgressValue(0);
|
|
|
|
QStringList paths;
|
|
QString propertyfile;
|
|
int sessionid_base;
|
|
sessionid_base = FindSessionDirsAndProperties(path, paths, propertyfile);
|
|
|
|
Machine *m = CreateMachineFromProperties(propertyfile);
|
|
if (m == nullptr) {
|
|
return -1;
|
|
}
|
|
|
|
QString backupPath = m->getBackupPath() + path.section("/", -2);
|
|
|
|
if (QDir::cleanPath(path).compare(QDir::cleanPath(backupPath)) != 0) {
|
|
copyPath(path, backupPath);
|
|
}
|
|
|
|
emit updateMessage(QObject::tr("Scanning Files..."));
|
|
QCoreApplication::processEvents();
|
|
|
|
// Walk through the files and create an import task for each logical session.
|
|
ScanFiles(paths, sessionid_base, m);
|
|
|
|
int tasks = countTasks();
|
|
unknownCodes.clear();
|
|
|
|
emit updateMessage(QObject::tr("Importing Sessions..."));
|
|
QCoreApplication::processEvents();
|
|
|
|
runTasks(AppSetting->multithreading());
|
|
|
|
emit updateMessage(QObject::tr("Finishing up..."));
|
|
QCoreApplication::processEvents();
|
|
|
|
finishAddingSessions();
|
|
|
|
if (unknownCodes.size() > 0) {
|
|
for (auto it = unknownCodes.begin(), end=unknownCodes.end(); it != end; ++it) {
|
|
qDebug() << QString("Unknown CPAP Codes '0x%1' was detected during import").arg((short)it.key(), 2, 16, QChar(0));
|
|
QStringList & strlist = it.value();
|
|
for (int i=0;i<it.value().size(); ++i) {
|
|
qDebug() << strlist.at(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
return m->unsupported() ? -1 : tasks;
|
|
}
|
|
|
|
|
|
int PRS1Loader::FindSessionDirsAndProperties(const QString & path, QStringList & paths, QString & propertyfile)
|
|
{
|
|
QDir dir(path);
|
|
dir.setFilter(QDir::NoDotAndDotDot | QDir::Dirs | QDir::Files | QDir::Hidden | QDir::NoSymLinks);
|
|
dir.setSorting(QDir::Name);
|
|
QFileInfoList flist = dir.entryInfoList();
|
|
|
|
QString filename;
|
|
|
|
int sessionid_base = 10;
|
|
|
|
for (int i = 0; i < flist.size(); i++) {
|
|
QFileInfo fi = flist.at(i);
|
|
filename = fi.fileName();
|
|
|
|
if (fi.isDir()) {
|
|
if ((filename[0].toLower() == 'p') && (isdigit(filename[1]))) {
|
|
// p0, p1, p2.. etc.. folders contain the session data
|
|
paths.push_back(fi.canonicalFilePath());
|
|
} else if (filename.toLower() == "e") {
|
|
// Error files..
|
|
// Reminder: I have been given some info about these. should check it over.
|
|
}
|
|
} else if (filename.compare("properties.txt",Qt::CaseInsensitive) == 0) {
|
|
propertyfile = fi.canonicalFilePath();
|
|
} else if (filename.compare("PROP.TXT",Qt::CaseInsensitive) == 0) {
|
|
sessionid_base = 16;
|
|
propertyfile = fi.canonicalFilePath();
|
|
}
|
|
}
|
|
return sessionid_base;
|
|
}
|
|
|
|
|
|
Machine* PRS1Loader::CreateMachineFromProperties(QString propertyfile)
|
|
{
|
|
QHash<QString,QString> props;
|
|
PeekProperties(propertyfile, props);
|
|
|
|
MachineInfo info = newInfo();
|
|
// Have a peek first to get the model number.
|
|
PeekProperties(info, propertyfile);
|
|
|
|
QString modelstr;
|
|
bool fnd = false;
|
|
for (int i=0; i<info.modelnumber.size(); i++) {
|
|
QChar c = info.modelnumber.at(i);
|
|
if (c.isDigit()) {
|
|
modelstr += c;
|
|
fnd = true;
|
|
} else if (fnd) break;
|
|
}
|
|
|
|
bool ok;
|
|
int model = modelstr.toInt(&ok);
|
|
if (ok) {
|
|
int series = ((model / 10) % 10);
|
|
int type = (model / 100);
|
|
|
|
// Assumption is made here all PRS1 machines less than 450P are not data capable.. this could be wrong one day.
|
|
if ((type < 4) && p_profile->cpap->brickWarning()) {
|
|
#ifndef UNITTEST_MODE
|
|
QApplication::processEvents();
|
|
QMessageBox::information(QApplication::activeWindow(),
|
|
QObject::tr("Non Data Capable Machine"),
|
|
QString(QObject::tr("Your Philips Respironics CPAP machine (Model %1) is unfortunately not a data capable model.")+"\n\n"+
|
|
QObject::tr("I'm sorry to report that OSCAR can only track hours of use and very basic settings for this machine.")).
|
|
arg(info.modelnumber),QMessageBox::Ok);
|
|
#endif
|
|
p_profile->cpap->setBrickWarning(false);
|
|
|
|
}
|
|
|
|
// A bit of protection against future annoyances..
|
|
if (!s_PRS1ModelInfo.IsSupported(props) || ((series != 5) && (series != 6) && (series != 0) && (series != 3))) { // || (type >= 10)) {
|
|
qDebug() << model << type << series << info.modelnumber << "unsupported";
|
|
#ifndef UNITTEST_MODE
|
|
QMessageBox::information(QApplication::activeWindow(),
|
|
QObject::tr("Machine Unsupported"),
|
|
QObject::tr("Sorry, your Philips Respironics CPAP machine (Model %1) is not supported yet.").arg(info.modelnumber) +"\n\n"+
|
|
QObject::tr("The developers needs a .zip copy of this machine's SD card and matching Encore .pdf reports to make it work with OSCAR.")
|
|
,QMessageBox::Ok);
|
|
|
|
#endif
|
|
return nullptr;
|
|
}
|
|
} else {
|
|
// model number didn't parse.. Meh... Silently ignore it
|
|
// QMessageBox::information(QApplication::activeWindow(),
|
|
// QObject::tr("Machine Unsupported"),
|
|
// QObject::tr("OSCAR could not parse the model number, this machine can not be imported..") +"\n\n"+
|
|
// QObject::tr("The developers needs a .zip copy of this machines' SD card and matching Encore .pdf reports to make it work with OSCAR.")
|
|
// ,QMessageBox::Ok);
|
|
return nullptr;
|
|
}
|
|
|
|
|
|
// Which is needed to get the right machine record..
|
|
Machine *m = p_profile->CreateMachine(info);
|
|
|
|
// This time supply the machine object so it can populate machine properties..
|
|
PeekProperties(m->info, propertyfile, m);
|
|
|
|
if (!m->untested() && !s_PRS1ModelInfo.IsTested(props)) {
|
|
m->setUntested(true);
|
|
qDebug() << info.modelnumber << "untested";
|
|
#ifndef UNITTEST_MODE
|
|
QMessageBox::information(QApplication::activeWindow(),
|
|
QObject::tr("Machine Untested"),
|
|
QObject::tr("Your Philips Respironics CPAP machine (Model %1) has not been tested yet.").arg(info.modelnumber) +"\n\n"+
|
|
QObject::tr("It seems similar enough to other machines that it might work, but the developers would like a .zip copy of this machine's SD card and matching Encore .pdf reports to make sure it works with OSCAR.")
|
|
,QMessageBox::Ok);
|
|
|
|
#endif
|
|
}
|
|
|
|
// TODO: Replace much of the above logic with PRS1ModelInfo logic.
|
|
if (!s_PRS1ModelInfo.IsSupported(props)) {
|
|
if (!m->unsupported()) {
|
|
unsupported(m);
|
|
}
|
|
}
|
|
|
|
return m;
|
|
}
|
|
|
|
|
|
void PRS1Loader::ScanFiles(const QStringList & paths, int sessionid_base, Machine * m)
|
|
{
|
|
SessionID sid;
|
|
long ext;
|
|
|
|
QDir dir;
|
|
dir.setFilter(QDir::NoDotAndDotDot | QDir::Dirs | QDir::Files | QDir::Hidden | QDir::NoSymLinks);
|
|
dir.setSorting(QDir::Name);
|
|
|
|
int size = paths.size();
|
|
|
|
sesstasks.clear();
|
|
new_sessions.clear(); // this hash is used by OpenFile
|
|
|
|
|
|
PRS1Import * task = nullptr;
|
|
// Note, I have observed p0/p1/etc folders containing duplicates session files (in Robin Sanders data.)
|
|
|
|
QDateTime datetime;
|
|
|
|
/* Unused until we get an actual timestamp below.
|
|
QDateTime ignoreBefore = p_profile->session->ignoreOlderSessionsDate();
|
|
bool ignoreOldSessions = p_profile->session->ignoreOlderSessions();
|
|
*/
|
|
|
|
// for each p0/p1/p2/etc... folder
|
|
for (int p=0; p < size; ++p) {
|
|
dir.setPath(paths.at(p));
|
|
|
|
if (!dir.exists() || !dir.isReadable()) {
|
|
qWarning() << dir.canonicalPath() << "can't read directory";
|
|
continue;
|
|
}
|
|
|
|
QFileInfoList flist = dir.entryInfoList();
|
|
|
|
// Scan for individual session files
|
|
for (int i = 0; i < flist.size(); i++) {
|
|
if (isAborted()) {
|
|
qDebug() << "received abort signal";
|
|
break;
|
|
}
|
|
QFileInfo fi = flist.at(i);
|
|
QString path = fi.canonicalFilePath();
|
|
bool ok;
|
|
|
|
if (fi.fileName() == ".DS_Store") {
|
|
continue;
|
|
}
|
|
|
|
QString ext_s = fi.fileName().section(".", -1);
|
|
ext = ext_s.toInt(&ok);
|
|
if (!ok) {
|
|
// not a numerical extension
|
|
qWarning() << path << "unexpected filename";
|
|
continue;
|
|
}
|
|
|
|
QString session_s = fi.fileName().section(".", 0, -2);
|
|
sid = session_s.toInt(&ok, sessionid_base);
|
|
if (!ok) {
|
|
// not a numerical session ID
|
|
qWarning() << path << "unexpected filename";
|
|
continue;
|
|
}
|
|
|
|
/* This never worked: the filename isn't a timestamp.
|
|
if (ignoreOldSessions) {
|
|
datetime = QDateTime::fromTime_t(sid);
|
|
if (datetime < ignoreBefore) {
|
|
continue;
|
|
}
|
|
}
|
|
*/
|
|
|
|
// TODO: BUG: This isn't right, since files can have multiple session
|
|
// chunks, which might not correspond to the filename. But before we can
|
|
// fix this we need to come up with a reasonably fast way to filter previously
|
|
// imported files without re-reading all of them.
|
|
if (m->SessionExists(sid)) {
|
|
// Skip already imported session
|
|
qDebug() << path << "session already exists, skipping" << sid;
|
|
continue;
|
|
}
|
|
|
|
if ((ext == 5) || (ext == 6)) {
|
|
// Waveform files aren't grouped... so we just want to add the filename for later
|
|
QHash<SessionID, PRS1Import *>::iterator it = sesstasks.find(sid);
|
|
if (it != sesstasks.end()) {
|
|
task = it.value();
|
|
} else {
|
|
// Should probably check if session already imported has this data missing..
|
|
|
|
// Create the group if we see it first..
|
|
task = new PRS1Import(this, sid, m);
|
|
sesstasks[sid] = task;
|
|
queTask(task);
|
|
}
|
|
|
|
if (ext == 5) {
|
|
if (!task->wavefile.isEmpty()) continue;
|
|
task->wavefile = fi.canonicalFilePath();
|
|
} else if (ext == 6) {
|
|
if (!task->oxifile.isEmpty()) continue;
|
|
task->oxifile = fi.canonicalFilePath();
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// Parse the data chunks and read the files..
|
|
QList<PRS1DataChunk *> Chunks = ParseFile(fi.canonicalFilePath());
|
|
|
|
for (int i=0; i < Chunks.size(); ++i) {
|
|
if (isAborted()) {
|
|
qDebug() << "received abort signal 2";
|
|
break;
|
|
}
|
|
|
|
PRS1DataChunk * chunk = Chunks.at(i);
|
|
|
|
SessionID chunk_sid = chunk->sessionid;
|
|
if (i == 0 && chunk_sid != sid) { // log session ID mismatches
|
|
qDebug() << fi.canonicalFilePath() << chunk_sid;
|
|
}
|
|
if (m->SessionExists(chunk_sid)) {
|
|
qDebug() << path << "session already exists, skipping" << sid << chunk_sid;
|
|
delete chunk;
|
|
continue;
|
|
}
|
|
|
|
|
|
task = nullptr;
|
|
QHash<SessionID, PRS1Import *>::iterator it = sesstasks.find(chunk_sid);
|
|
if (it != sesstasks.end()) {
|
|
task = it.value();
|
|
} else {
|
|
task = new PRS1Import(this, chunk_sid, m);
|
|
sesstasks[chunk_sid] = task;
|
|
// save a loop an que this now
|
|
queTask(task);
|
|
}
|
|
switch (ext) {
|
|
case 0:
|
|
if (task->compliance) {
|
|
qWarning() << path << "duplicate compliance?";
|
|
delete chunk;
|
|
continue; // (skipping to avoid duplicates)
|
|
}
|
|
task->compliance = chunk;
|
|
break;
|
|
case 1:
|
|
if (task->summary) {
|
|
qWarning() << path << "duplicate summary?";
|
|
delete chunk;
|
|
continue;
|
|
}
|
|
task->summary = chunk;
|
|
break;
|
|
case 2:
|
|
if (task->event) {
|
|
qWarning() << path << "duplicate events?";
|
|
delete chunk;
|
|
continue;
|
|
}
|
|
task->event = chunk;
|
|
break;
|
|
default:
|
|
qWarning() << path << "unexpected file";
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (isAborted()) {
|
|
qDebug() << "received abort signal 3";
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
//********************************************************************************************
|
|
// Internal PRS1 parsed data types
|
|
//********************************************************************************************
|
|
|
|
// For new events, add an enum here and then a class below with an PRS1_*_EVENT macro
|
|
enum PRS1ParsedEventType
|
|
{
|
|
EV_PRS1_RAW = -1, // these only get logged
|
|
EV_PRS1_UNKNOWN = 0, // these have their value graphed
|
|
EV_PRS1_TB,
|
|
EV_PRS1_OA,
|
|
EV_PRS1_CA,
|
|
EV_PRS1_FL,
|
|
EV_PRS1_PB,
|
|
EV_PRS1_LL,
|
|
EV_PRS1_HY,
|
|
EV_PRS1_TOTLEAK,
|
|
EV_PRS1_LEAK, // unintentional leak
|
|
EV_PRS1_PRESSURE, // TODO: maybe fold PRESSURE and IPAP into one
|
|
EV_PRS1_IPAP,
|
|
EV_PRS1_IPAPLOW,
|
|
EV_PRS1_IPAPHIGH,
|
|
EV_PRS1_EPAP,
|
|
EV_PRS1_FLEX,
|
|
EV_PRS1_RR,
|
|
EV_PRS1_PTB,
|
|
EV_PRS1_MV,
|
|
EV_PRS1_TV,
|
|
EV_PRS1_SNORE,
|
|
EV_PRS1_VS, // F0: Is this different from SNORE?
|
|
EV_PRS1_PP,
|
|
EV_PRS1_RERA,
|
|
EV_PRS1_NRI,
|
|
EV_PRS1_FLOWRATE,
|
|
EV_PRS1_TEST1,
|
|
EV_PRS1_TEST2,
|
|
EV_PRS1_SETTING,
|
|
EV_PRS1_SLICE,
|
|
};
|
|
|
|
enum PRS1ParsedEventUnit
|
|
{
|
|
PRS1_UNIT_NONE,
|
|
PRS1_UNIT_CMH2O,
|
|
PRS1_UNIT_ML,
|
|
PRS1_UNIT_S,
|
|
};
|
|
|
|
enum PRS1ParsedSettingType
|
|
{
|
|
PRS1_SETTING_CPAP_MODE,
|
|
PRS1_SETTING_PRESSURE,
|
|
PRS1_SETTING_PRESSURE_MIN,
|
|
PRS1_SETTING_PRESSURE_MAX,
|
|
PRS1_SETTING_EPAP,
|
|
PRS1_SETTING_EPAP_MIN,
|
|
PRS1_SETTING_EPAP_MAX,
|
|
PRS1_SETTING_IPAP,
|
|
PRS1_SETTING_IPAP_MIN,
|
|
PRS1_SETTING_IPAP_MAX,
|
|
PRS1_SETTING_PS,
|
|
PRS1_SETTING_PS_MIN,
|
|
PRS1_SETTING_PS_MAX,
|
|
PRS1_SETTING_FLEX_MODE,
|
|
PRS1_SETTING_FLEX_LEVEL,
|
|
PRS1_SETTING_RAMP_TIME,
|
|
PRS1_SETTING_RAMP_PRESSURE,
|
|
PRS1_SETTING_HUMID_STATUS,
|
|
PRS1_SETTING_HUMID_LEVEL,
|
|
PRS1_SETTING_HEATED_TUBING,
|
|
PRS1_SETTING_SYSTEMONE_RESIST_LOCK,
|
|
PRS1_SETTING_SYSTEMONE_RESIST_SETTING,
|
|
PRS1_SETTING_SYSTEMONE_RESIST_STATUS,
|
|
PRS1_SETTING_HOSE_DIAMETER,
|
|
PRS1_SETTING_AUTO_ON,
|
|
PRS1_SETTING_AUTO_OFF,
|
|
PRS1_SETTING_MASK_ALERT,
|
|
PRS1_SETTING_SHOW_AHI,
|
|
};
|
|
|
|
|
|
static QString timeStr(int t);
|
|
static QString byteList(QByteArray data, int limit=-1);
|
|
static QString hex(int i);
|
|
static QString parsedSettingTypeName(PRS1ParsedSettingType t);
|
|
|
|
|
|
class PRS1ParsedEvent
|
|
{
|
|
public:
|
|
PRS1ParsedEventType m_type;
|
|
int m_start; // seconds relative to chunk timestamp at which this event began
|
|
int m_duration;
|
|
int m_value;
|
|
float m_offset;
|
|
float m_gain;
|
|
PRS1ParsedEventUnit m_unit;
|
|
|
|
inline float value(void) { return (m_value * m_gain) + m_offset; }
|
|
|
|
static const PRS1ParsedEventType TYPE = EV_PRS1_UNKNOWN;
|
|
static constexpr float GAIN = 1.0;
|
|
static const PRS1ParsedEventUnit UNIT = PRS1_UNIT_NONE;
|
|
|
|
virtual QMap<QString,QString> contents(void) = 0;
|
|
|
|
protected:
|
|
PRS1ParsedEvent(PRS1ParsedEventType type, int start)
|
|
: m_type(type), m_start(start), m_duration(0), m_value(0), m_offset(0.0), m_gain(GAIN), m_unit(UNIT)
|
|
{
|
|
}
|
|
public:
|
|
virtual ~PRS1ParsedEvent()
|
|
{
|
|
}
|
|
};
|
|
|
|
|
|
class PRS1ParsedDurationEvent : public PRS1ParsedEvent
|
|
{
|
|
public:
|
|
virtual QMap<QString,QString> contents(void)
|
|
{
|
|
QMap<QString,QString> out;
|
|
out["start"] = timeStr(m_start);
|
|
out["duration"] = timeStr(m_duration);
|
|
return out;
|
|
}
|
|
|
|
protected:
|
|
static const PRS1ParsedEventUnit UNIT = PRS1_UNIT_S;
|
|
|
|
PRS1ParsedDurationEvent(PRS1ParsedEventType type, int start, int duration) : PRS1ParsedEvent(type, start) { m_duration = duration; }
|
|
};
|
|
const PRS1ParsedEventUnit PRS1ParsedDurationEvent::UNIT;
|
|
|
|
|
|
class PRS1ParsedValueEvent : public PRS1ParsedEvent
|
|
{
|
|
public:
|
|
virtual QMap<QString,QString> contents(void)
|
|
{
|
|
QMap<QString,QString> out;
|
|
out["start"] = timeStr(m_start);
|
|
out["value"] = QString::number(value());
|
|
return out;
|
|
}
|
|
|
|
protected:
|
|
PRS1ParsedValueEvent(PRS1ParsedEventType type, int start, int value) : PRS1ParsedEvent(type, start) { m_value = value; }
|
|
};
|
|
|
|
|
|
class PRS1UnknownValueEvent : public PRS1ParsedValueEvent
|
|
{
|
|
public:
|
|
virtual QMap<QString,QString> contents(void)
|
|
{
|
|
QMap<QString,QString> out;
|
|
out["start"] = timeStr(m_start);
|
|
out["code"] = hex(m_code);
|
|
out["value"] = QString::number(value());
|
|
return out;
|
|
}
|
|
|
|
int m_code;
|
|
PRS1UnknownValueEvent(int code, int start, int value, float gain=1.0) : PRS1ParsedValueEvent(TYPE, start, value), m_code(code) { m_gain = gain; }
|
|
};
|
|
|
|
|
|
class PRS1UnknownDataEvent : public PRS1ParsedEvent
|
|
{
|
|
public:
|
|
virtual QMap<QString,QString> contents(void)
|
|
{
|
|
QMap<QString,QString> out;
|
|
out["pos"] = QString::number(m_pos);
|
|
out["data"] = byteList(m_data);
|
|
return out;
|
|
}
|
|
|
|
static const PRS1ParsedEventType TYPE = EV_PRS1_RAW;
|
|
|
|
int m_pos;
|
|
unsigned char m_code;
|
|
QByteArray m_data;
|
|
|
|
PRS1UnknownDataEvent(const QByteArray & data, int pos, int len=18)
|
|
: PRS1ParsedEvent(TYPE, 0)
|
|
{
|
|
m_pos = pos;
|
|
m_data = data.mid(pos, len);
|
|
Q_ASSERT(m_data.size() >= 1);
|
|
m_code = m_data.at(0);
|
|
}
|
|
};
|
|
|
|
class PRS1PressureEvent : public PRS1ParsedValueEvent
|
|
{
|
|
public:
|
|
static constexpr float GAIN = 0.1;
|
|
static const PRS1ParsedEventUnit UNIT = PRS1_UNIT_CMH2O;
|
|
|
|
PRS1PressureEvent(PRS1ParsedEventType type, int start, int value, float gain=GAIN)
|
|
: PRS1ParsedValueEvent(type, start, value)
|
|
{
|
|
m_gain = gain;
|
|
m_unit = UNIT;
|
|
}
|
|
};
|
|
|
|
class PRS1TidalVolumeEvent : public PRS1ParsedValueEvent
|
|
{
|
|
public:
|
|
static const PRS1ParsedEventType TYPE = EV_PRS1_TV;
|
|
|
|
static constexpr float GAIN = 10.0;
|
|
static const PRS1ParsedEventUnit UNIT = PRS1_UNIT_ML;
|
|
|
|
PRS1TidalVolumeEvent(int start, int value)
|
|
: PRS1ParsedValueEvent(TYPE, start, value)
|
|
{
|
|
m_gain = GAIN;
|
|
m_unit = UNIT;
|
|
}
|
|
};
|
|
const PRS1ParsedEventType PRS1TidalVolumeEvent::TYPE;
|
|
|
|
class PRS1ParsedSettingEvent : public PRS1ParsedValueEvent
|
|
{
|
|
public:
|
|
virtual QMap<QString,QString> contents(void)
|
|
{
|
|
QMap<QString,QString> out;
|
|
out[parsedSettingTypeName(m_setting)] = QString::number(value());
|
|
return out;
|
|
}
|
|
|
|
static const PRS1ParsedEventType TYPE = EV_PRS1_SETTING;
|
|
PRS1ParsedSettingType m_setting;
|
|
|
|
PRS1ParsedSettingEvent(PRS1ParsedSettingType setting, int value) : PRS1ParsedValueEvent(TYPE, 0, value), m_setting(setting) {}
|
|
};
|
|
|
|
class PRS1PressureSettingEvent : public PRS1ParsedSettingEvent
|
|
{
|
|
public:
|
|
static constexpr float GAIN = PRS1PressureEvent::GAIN;
|
|
static const PRS1ParsedEventUnit UNIT = PRS1PressureEvent::UNIT;
|
|
|
|
PRS1PressureSettingEvent(PRS1ParsedSettingType setting, int value, float gain=GAIN)
|
|
: PRS1ParsedSettingEvent(setting, value)
|
|
{
|
|
m_gain = gain;
|
|
m_unit = UNIT;
|
|
}
|
|
};
|
|
|
|
class PRS1ParsedSliceEvent : public PRS1ParsedValueEvent
|
|
{
|
|
public:
|
|
virtual QMap<QString,QString> contents(void)
|
|
{
|
|
QMap<QString,QString> out;
|
|
out["start"] = timeStr(m_start);
|
|
QString s;
|
|
switch ((SliceStatus) m_value) {
|
|
case MaskOn: s = "MaskOn"; break;
|
|
case MaskOff: s = "MaskOff"; break;
|
|
case EquipmentOff: s = "EquipmentOff"; break;
|
|
case UnknownStatus: s = "Unknown"; break;
|
|
}
|
|
out["status"] = s;
|
|
return out;
|
|
}
|
|
|
|
static const PRS1ParsedEventType TYPE = EV_PRS1_SLICE;
|
|
|
|
PRS1ParsedSliceEvent(int start, SliceStatus status) : PRS1ParsedValueEvent(TYPE, start, (int) status) {}
|
|
};
|
|
|
|
|
|
#define _PRS1_EVENT(T, E, P, ARG) \
|
|
class T : public P \
|
|
{ \
|
|
public: \
|
|
static const PRS1ParsedEventType TYPE = E; \
|
|
T(int start, int ARG) : P(TYPE, start, ARG) {} \
|
|
}; \
|
|
const PRS1ParsedEventType T::TYPE
|
|
#define PRS1_DURATION_EVENT(T, E) _PRS1_EVENT(T, E, PRS1ParsedDurationEvent, duration)
|
|
#define PRS1_VALUE_EVENT(T, E) _PRS1_EVENT(T, E, PRS1ParsedValueEvent, value)
|
|
#define PRS1_PRESSURE_EVENT(T, E) \
|
|
class T : public PRS1PressureEvent \
|
|
{ \
|
|
public: \
|
|
static const PRS1ParsedEventType TYPE = E; \
|
|
T(int start, int value, float gain=PRS1PressureEvent::GAIN) : PRS1PressureEvent(TYPE, start, value, gain) {} \
|
|
}; \
|
|
const PRS1ParsedEventType T::TYPE
|
|
|
|
PRS1_DURATION_EVENT(PRS1TimedBreathEvent, EV_PRS1_TB);
|
|
PRS1_DURATION_EVENT(PRS1ObstructiveApneaEvent, EV_PRS1_OA);
|
|
PRS1_DURATION_EVENT(PRS1ClearAirwayEvent, EV_PRS1_CA);
|
|
PRS1_DURATION_EVENT(PRS1FlowLimitationEvent, EV_PRS1_FL);
|
|
PRS1_DURATION_EVENT(PRS1PeriodicBreathingEvent, EV_PRS1_PB);
|
|
PRS1_DURATION_EVENT(PRS1LargeLeakEvent, EV_PRS1_LL);
|
|
PRS1_DURATION_EVENT(PRS1HypopneaEvent, EV_PRS1_HY);
|
|
|
|
PRS1_VALUE_EVENT(PRS1TotalLeakEvent, EV_PRS1_TOTLEAK);
|
|
PRS1_VALUE_EVENT(PRS1LeakEvent, EV_PRS1_LEAK); // TODO: do machines really report unintentional leak?
|
|
|
|
PRS1_PRESSURE_EVENT(PRS1CPAPEvent, EV_PRS1_PRESSURE);
|
|
PRS1_PRESSURE_EVENT(PRS1IPAPEvent, EV_PRS1_IPAP);
|
|
PRS1_PRESSURE_EVENT(PRS1IPAPHighEvent, EV_PRS1_IPAPHIGH);
|
|
PRS1_PRESSURE_EVENT(PRS1IPAPLowEvent, EV_PRS1_IPAPLOW);
|
|
PRS1_PRESSURE_EVENT(PRS1EPAPEvent, EV_PRS1_EPAP);
|
|
PRS1_PRESSURE_EVENT(PRS1PressureReliefEvent, EV_PRS1_FLEX); // value is pressure applied during pressure relief, similar to EPAP
|
|
|
|
PRS1_VALUE_EVENT(PRS1RespiratoryRateEvent, EV_PRS1_RR);
|
|
PRS1_VALUE_EVENT(PRS1PatientTriggeredBreathsEvent, EV_PRS1_PTB);
|
|
PRS1_VALUE_EVENT(PRS1MinuteVentilationEvent, EV_PRS1_MV);
|
|
PRS1_VALUE_EVENT(PRS1SnoreEvent, EV_PRS1_SNORE);
|
|
PRS1_VALUE_EVENT(PRS1VibratorySnoreEvent, EV_PRS1_VS);
|
|
PRS1_VALUE_EVENT(PRS1PressurePulseEvent, EV_PRS1_PP);
|
|
PRS1_VALUE_EVENT(PRS1RERAEvent, EV_PRS1_RERA); // TODO: should this really be a duration event?
|
|
PRS1_VALUE_EVENT(PRS1NonRespondingEvent, EV_PRS1_NRI); // TODO: is this a single event or an index/hour?
|
|
PRS1_VALUE_EVENT(PRS1FlowRateEvent, EV_PRS1_FLOWRATE); // TODO: is this a single event or an index/hour?
|
|
PRS1_VALUE_EVENT(PRS1Test1Event, EV_PRS1_TEST1);
|
|
PRS1_VALUE_EVENT(PRS1Test2Event, EV_PRS1_TEST2);
|
|
|
|
|
|
//********************************************************************************************
|
|
|
|
static QString hex(int i)
|
|
{
|
|
return QString("0x") + QString::number(i, 16).toUpper();
|
|
}
|
|
|
|
#define ENUMSTRING(ENUM) case ENUM: s = #ENUM; break
|
|
static QString parsedEventTypeName(PRS1ParsedEventType t)
|
|
{
|
|
QString s;
|
|
switch (t) {
|
|
ENUMSTRING(EV_PRS1_RAW);
|
|
ENUMSTRING(EV_PRS1_UNKNOWN);
|
|
ENUMSTRING(EV_PRS1_TB);
|
|
ENUMSTRING(EV_PRS1_OA);
|
|
ENUMSTRING(EV_PRS1_CA);
|
|
ENUMSTRING(EV_PRS1_FL);
|
|
ENUMSTRING(EV_PRS1_PB);
|
|
ENUMSTRING(EV_PRS1_LL);
|
|
ENUMSTRING(EV_PRS1_HY);
|
|
ENUMSTRING(EV_PRS1_TOTLEAK);
|
|
ENUMSTRING(EV_PRS1_LEAK);
|
|
ENUMSTRING(EV_PRS1_PRESSURE);
|
|
ENUMSTRING(EV_PRS1_IPAP);
|
|
ENUMSTRING(EV_PRS1_IPAPLOW);
|
|
ENUMSTRING(EV_PRS1_IPAPHIGH);
|
|
ENUMSTRING(EV_PRS1_EPAP);
|
|
ENUMSTRING(EV_PRS1_FLEX);
|
|
ENUMSTRING(EV_PRS1_RR);
|
|
ENUMSTRING(EV_PRS1_PTB);
|
|
ENUMSTRING(EV_PRS1_MV);
|
|
ENUMSTRING(EV_PRS1_TV);
|
|
ENUMSTRING(EV_PRS1_SNORE);
|
|
ENUMSTRING(EV_PRS1_VS);
|
|
ENUMSTRING(EV_PRS1_PP);
|
|
ENUMSTRING(EV_PRS1_RERA);
|
|
ENUMSTRING(EV_PRS1_NRI);
|
|
ENUMSTRING(EV_PRS1_FLOWRATE);
|
|
ENUMSTRING(EV_PRS1_TEST1);
|
|
ENUMSTRING(EV_PRS1_TEST2);
|
|
ENUMSTRING(EV_PRS1_SETTING);
|
|
ENUMSTRING(EV_PRS1_SLICE);
|
|
default:
|
|
s = hex(t);
|
|
qDebug() << "Unknown PRS1ParsedEventType type:" << qPrintable(s);
|
|
return s;
|
|
}
|
|
return s.mid(8).toLower(); // lop off initial EV_PRS1_
|
|
}
|
|
|
|
static QString parsedSettingTypeName(PRS1ParsedSettingType t)
|
|
{
|
|
QString s;
|
|
switch (t) {
|
|
ENUMSTRING(PRS1_SETTING_CPAP_MODE);
|
|
ENUMSTRING(PRS1_SETTING_PRESSURE);
|
|
ENUMSTRING(PRS1_SETTING_PRESSURE_MIN);
|
|
ENUMSTRING(PRS1_SETTING_PRESSURE_MAX);
|
|
ENUMSTRING(PRS1_SETTING_EPAP);
|
|
ENUMSTRING(PRS1_SETTING_EPAP_MIN);
|
|
ENUMSTRING(PRS1_SETTING_EPAP_MAX);
|
|
ENUMSTRING(PRS1_SETTING_IPAP);
|
|
ENUMSTRING(PRS1_SETTING_IPAP_MIN);
|
|
ENUMSTRING(PRS1_SETTING_IPAP_MAX);
|
|
ENUMSTRING(PRS1_SETTING_PS);
|
|
ENUMSTRING(PRS1_SETTING_PS_MIN);
|
|
ENUMSTRING(PRS1_SETTING_PS_MAX);
|
|
ENUMSTRING(PRS1_SETTING_FLEX_MODE);
|
|
ENUMSTRING(PRS1_SETTING_FLEX_LEVEL);
|
|
ENUMSTRING(PRS1_SETTING_RAMP_TIME);
|
|
ENUMSTRING(PRS1_SETTING_RAMP_PRESSURE);
|
|
ENUMSTRING(PRS1_SETTING_HUMID_STATUS);
|
|
ENUMSTRING(PRS1_SETTING_HUMID_LEVEL);
|
|
ENUMSTRING(PRS1_SETTING_HEATED_TUBING);
|
|
ENUMSTRING(PRS1_SETTING_SYSTEMONE_RESIST_LOCK);
|
|
ENUMSTRING(PRS1_SETTING_SYSTEMONE_RESIST_SETTING);
|
|
ENUMSTRING(PRS1_SETTING_SYSTEMONE_RESIST_STATUS);
|
|
ENUMSTRING(PRS1_SETTING_HOSE_DIAMETER);
|
|
ENUMSTRING(PRS1_SETTING_AUTO_ON);
|
|
ENUMSTRING(PRS1_SETTING_AUTO_OFF);
|
|
ENUMSTRING(PRS1_SETTING_MASK_ALERT);
|
|
ENUMSTRING(PRS1_SETTING_SHOW_AHI);
|
|
default:
|
|
s = hex(t);
|
|
qDebug() << "Unknown PRS1ParsedSettingType type:" << qPrintable(s);
|
|
return s;
|
|
}
|
|
return s.mid(13).toLower(); // lop off initial PRS1_SETTING_
|
|
}
|
|
|
|
static QString timeStr(int t)
|
|
{
|
|
int h = t / 3600;
|
|
int m = (t - (h * 3600)) / 60;
|
|
int s = t % 60;
|
|
return QString("%1:%2:%3").arg(h, 2, 10, QChar('0')).arg(m, 2, 10, QChar('0')).arg(s, 2, 10, QChar('0'));
|
|
}
|
|
|
|
static QString byteList(QByteArray data, int limit)
|
|
{
|
|
int count = data.size();
|
|
if (limit == -1 || limit > count) limit = count;
|
|
QStringList l;
|
|
for (int i = 0; i < limit; i++) {
|
|
l.push_back(QString( "%1" ).arg((int) data[i] & 0xFF, 2, 16, QChar('0') ).toUpper());
|
|
}
|
|
if (limit < count) l.push_back("...");
|
|
QString s = l.join(" ");
|
|
return s;
|
|
}
|
|
|
|
QString _PRS1ParsedEventName(PRS1ParsedEvent* e)
|
|
{
|
|
return parsedEventTypeName(e->m_type);
|
|
}
|
|
|
|
QMap<QString,QString> _PRS1ParsedEventContents(PRS1ParsedEvent* e)
|
|
{
|
|
return e->contents();
|
|
}
|
|
|
|
//********************************************************************************************
|
|
|
|
|
|
void PRS1DataChunk::AddEvent(PRS1ParsedEvent* const event)
|
|
{
|
|
m_parsedData.push_back(event);
|
|
}
|
|
|
|
bool PRS1Import::ParseF5EventsFV3()
|
|
{
|
|
// F5V3 uses a gain of 0.125 rather than 0.1 to allow for a maximum value of 30 cmH2O
|
|
static const float GAIN = 0.125F; // TODO: parameterize this somewhere better
|
|
|
|
// Required channels
|
|
EventList *OA = session->AddEventList(CPAP_Obstructive, EVL_Event);
|
|
EventList *HY = session->AddEventList(CPAP_Hypopnea, EVL_Event);
|
|
EventList *CA = session->AddEventList(CPAP_ClearAirway, EVL_Event);
|
|
|
|
EventList *LL = session->AddEventList(CPAP_LargeLeak, EVL_Event);
|
|
EventList *TOTLEAK = session->AddEventList(CPAP_LeakTotal, EVL_Event);
|
|
EventList *LEAK = session->AddEventList(CPAP_Leak, EVL_Event);
|
|
EventList *RR = session->AddEventList(CPAP_RespRate, EVL_Event);
|
|
EventList *TV = session->AddEventList(CPAP_TidalVolume, EVL_Event, 10.0F);
|
|
EventList *MV = session->AddEventList(CPAP_MinuteVent, EVL_Event);
|
|
EventList *PB = session->AddEventList(CPAP_PB, EVL_Event);
|
|
EventList *PTB = session->AddEventList(CPAP_PTB, EVL_Event);
|
|
EventList *TB = session->AddEventList(PRS1_TimedBreath, EVL_Event);
|
|
EventList *IPAP = session->AddEventList(CPAP_IPAP, EVL_Event, GAIN);
|
|
EventList *EPAP = session->AddEventList(CPAP_EPAP, EVL_Event, GAIN);
|
|
EventList *PS = session->AddEventList(CPAP_PS, EVL_Event, GAIN);
|
|
EventList *IPAPLo = session->AddEventList(CPAP_IPAPLo, EVL_Event, GAIN);
|
|
EventList *IPAPHi = session->AddEventList(CPAP_IPAPHi, EVL_Event, GAIN);
|
|
EventList *FL = session->AddEventList(CPAP_FlowLimit, EVL_Event);
|
|
EventList *SNORE = session->AddEventList(CPAP_Snore, EVL_Event);
|
|
EventList *VS = session->AddEventList(CPAP_VSnore, EVL_Event);
|
|
EventList *VS2 = session->AddEventList(CPAP_VSnore2, EVL_Event);
|
|
|
|
|
|
// Unintentional leak calculation, see zMaskProfile:calcLeak in calcs.cpp for explanation
|
|
EventDataType currentPressure=0, leak;
|
|
|
|
bool calcLeaks = p_profile->cpap->calculateUnintentionalLeaks();
|
|
EventDataType lpm4 = p_profile->cpap->custom4cmH2OLeaks();
|
|
EventDataType lpm20 = p_profile->cpap->custom20cmH2OLeaks();
|
|
|
|
EventDataType lpm = lpm20 - lpm4;
|
|
EventDataType ppm = lpm / 16.0;
|
|
|
|
|
|
qint64 t = qint64(event->timestamp) * 1000L;
|
|
session->updateFirst(t);
|
|
|
|
bool ok;
|
|
ok = event->ParseEvents(MODE_UNKNOWN);
|
|
if (!ok) {
|
|
return false;
|
|
}
|
|
|
|
for (int i=0; i < event->m_parsedData.count(); i++) {
|
|
PRS1ParsedEvent* e = event->m_parsedData.at(i);
|
|
t = qint64(event->timestamp + e->m_start) * 1000L;
|
|
|
|
switch (e->m_type) {
|
|
case PRS1IPAPEvent::TYPE:
|
|
IPAP->AddEvent(t, e->m_value);
|
|
currentPressure = e->m_value;
|
|
break;
|
|
case PRS1IPAPLowEvent::TYPE:
|
|
IPAPLo->AddEvent(t, e->m_value);
|
|
break;
|
|
case PRS1IPAPHighEvent::TYPE:
|
|
IPAPHi->AddEvent(t, e->m_value);
|
|
break;
|
|
case PRS1EPAPEvent::TYPE:
|
|
EPAP->AddEvent(t, e->m_value);
|
|
PS->AddEvent(t, currentPressure - e->m_value); // Pressure Support
|
|
break;
|
|
case PRS1TimedBreathEvent::TYPE:
|
|
TB->AddEvent(t, e->m_duration);
|
|
break;
|
|
case PRS1ObstructiveApneaEvent::TYPE:
|
|
OA->AddEvent(t, e->m_duration);
|
|
break;
|
|
case PRS1ClearAirwayEvent::TYPE:
|
|
CA->AddEvent(t, e->m_duration);
|
|
break;
|
|
case PRS1HypopneaEvent::TYPE:
|
|
HY->AddEvent(t, e->m_duration);
|
|
break;
|
|
case PRS1FlowLimitationEvent::TYPE:
|
|
FL->AddEvent(t, e->m_duration);
|
|
break;
|
|
case PRS1PeriodicBreathingEvent::TYPE:
|
|
PB->AddEvent(t, e->m_duration);
|
|
break;
|
|
case PRS1LargeLeakEvent::TYPE:
|
|
LL->AddEvent(t, e->m_duration);
|
|
break;
|
|
case PRS1TotalLeakEvent::TYPE:
|
|
TOTLEAK->AddEvent(t, e->m_value);
|
|
leak = e->m_value;
|
|
if (calcLeaks) { // Much Quicker doing this here than the recalc method.
|
|
leak -= (((currentPressure/10.0f) - 4.0) * ppm + lpm4);
|
|
if (leak < 0) leak = 0;
|
|
LEAK->AddEvent(t, leak);
|
|
}
|
|
break;
|
|
case PRS1SnoreEvent::TYPE: // snore count that shows up in flags but not waveform
|
|
SNORE->AddEvent(t, e->m_value);
|
|
if (e->m_value > 0) {
|
|
VS2->AddEvent(t, 0);
|
|
}
|
|
break;
|
|
case PRS1VibratorySnoreEvent::TYPE: // real VS marker on waveform
|
|
VS->AddEvent(t, 0);
|
|
break;
|
|
case PRS1RespiratoryRateEvent::TYPE:
|
|
RR->AddEvent(t, e->m_value);
|
|
break;
|
|
case PRS1PatientTriggeredBreathsEvent::TYPE:
|
|
PTB->AddEvent(t, e->m_value);
|
|
break;
|
|
case PRS1MinuteVentilationEvent::TYPE:
|
|
MV->AddEvent(t, e->m_value);
|
|
break;
|
|
case PRS1TidalVolumeEvent::TYPE:
|
|
TV->AddEvent(t, e->m_value);
|
|
break;
|
|
default:
|
|
qWarning() << "Unknown PRS1 event type" << (int) e->m_type;
|
|
break;
|
|
}
|
|
}
|
|
|
|
//t = qint64(event->timestamp + event->duration) * 1000L;
|
|
session->updateLast(t);
|
|
session->m_cnt.clear();
|
|
session->m_cph.clear();
|
|
|
|
session->m_valuesummary[CPAP_Pressure].clear();
|
|
session->m_valuesummary.erase(session->m_valuesummary.find(CPAP_Pressure));
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
#if 0
|
|
// 900X series
|
|
bool PRS1DataChunk::ParseEventsF5V3(void)
|
|
{
|
|
if (this->family != 5 || this->familyVersion != 3) {
|
|
qWarning() << "ParseEventsF5V3 called with family" << this->family << "familyVersion" << this->familyVersion;
|
|
return false;
|
|
}
|
|
|
|
EventDataType data0, data1, data2, data3, data4, data5;
|
|
Q_UNUSED(data3)
|
|
|
|
int t = 0;
|
|
int pos = 0;
|
|
//int cnt = 0;
|
|
short delta;//,duration;
|
|
//bool badcode = false;
|
|
unsigned char lastcode3 = 0, lastcode2 = 0, lastcode = 0, code = 0;
|
|
int lastpos = 0, startpos = 0, lastpos2 = 0, lastpos3 = 0;
|
|
|
|
int size = this->m_data.size();
|
|
unsigned char * buffer = (unsigned char *)this->m_data.data();
|
|
|
|
while (pos < size) {
|
|
lastcode3 = lastcode2;
|
|
lastcode2 = lastcode;
|
|
lastcode = code;
|
|
lastpos3 = lastpos2;
|
|
lastpos2 = lastpos;
|
|
lastpos = startpos;
|
|
startpos = pos;
|
|
code = buffer[pos++];
|
|
|
|
if (code >= 0x12) {
|
|
qDebug() << "Illegal PRS1 code " << hex << int(code) << " appeared at " << hex << startpos << "in" << this->sessionid;;
|
|
qDebug() << "1: (" << int(lastcode) << hex << lastpos << ")";
|
|
qDebug() << "2: (" << int(lastcode2) << hex << lastpos2 << ")";
|
|
qDebug() << "3: (" << int(lastcode3) << hex << lastpos3 << ")";
|
|
this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos));
|
|
return false;
|
|
}
|
|
delta = buffer[pos];
|
|
//delta=buffer[pos+1] << 8 | buffer[pos];
|
|
pos += 2;
|
|
t += delta;
|
|
|
|
switch(code) {
|
|
case 0x01: // Leak ???
|
|
data0 = buffer[pos++];
|
|
//tt -= qint64(data0) * 1000L; // Subtract Time Offset
|
|
break;
|
|
case 0x02: // Meh??? Timed Breath??
|
|
data0 = buffer[pos++];
|
|
this->AddEvent(new PRS1TimedBreathEvent(t - data0, data0));
|
|
break;
|
|
case 0x03: // Graph Data
|
|
data0 = buffer[pos++];
|
|
this->AddEvent(new PRS1IPAPEvent(t, data0)); // 00=IAP
|
|
data4 = buffer[pos++];
|
|
this->AddEvent(new PRS1IPAPLowEvent(t, data4)); // 01=IAP Low
|
|
data5 = buffer[pos++];
|
|
this->AddEvent(new PRS1IPAPHighEvent(t, data5)); // 02=IAP High
|
|
this->AddEvent(new PRS1TotalLeakEvent(t, buffer[pos++])); // 03=LEAK
|
|
|
|
|
|
this->AddEvent(new PRS1RespiratoryRateEvent(t, buffer[pos++])); // 04=Breaths Per Minute
|
|
this->AddEvent(new PRS1PatientTriggeredBreathsEvent(t, buffer[pos++])); // 05=Patient Triggered Breaths
|
|
this->AddEvent(new PRS1MinuteVentilationEvent(t, buffer[pos++])); // 06=Minute Ventilation
|
|
//tmp=buffer[pos++] * 10.0;
|
|
this->AddEvent(new PRS1TidalVolumeEvent(t, buffer[pos++])); // 07=Tidal Volume
|
|
this->AddEvent(new PRS1SnoreEvent(t, buffer[pos++])); // 08=Snore
|
|
this->AddEvent(new PRS1EPAPEvent(t, buffer[pos++])); // 09=EPAP
|
|
data0 = buffer[pos++];
|
|
|
|
|
|
break;
|
|
case 0x05:
|
|
data0 = buffer[pos++];
|
|
this->AddEvent(new PRS1ObstructiveApneaEvent(t - data0, data0));
|
|
|
|
// PS->AddEvent(tt, data0);
|
|
break;
|
|
case 0x06: // Clear Airway
|
|
data0 = buffer[pos++];
|
|
this->AddEvent(new PRS1ClearAirwayEvent(t - data0, data0));
|
|
|
|
// PTB->AddEvent(tt, data0);
|
|
break;
|
|
case 0x07:
|
|
data0 = buffer[pos++];
|
|
data1 = buffer[pos++];
|
|
//tt -= qint64(data0) * 1000L; // Subtract Time Offset
|
|
|
|
|
|
break;
|
|
case 0x08: // Flow Limitation
|
|
data0 = buffer[pos++];
|
|
this->AddEvent(new PRS1FlowLimitationEvent(t - data0, data0));
|
|
break;
|
|
case 0x09:
|
|
data0 = buffer[pos++];
|
|
data1 = buffer[pos++];
|
|
data2 = buffer[pos++];
|
|
data3 = buffer[pos++];
|
|
//tt -= qint64(data0) * 1000L; // Subtract Time Offset
|
|
|
|
|
|
// TB->AddEvent(tt, data0);
|
|
break;
|
|
case 0x0a: // Periodic Breathing?
|
|
data0 = (buffer[pos + 1] << 8 | buffer[pos]);
|
|
data0 *= 2;
|
|
pos += 2;
|
|
data1 = buffer[pos++];
|
|
this->AddEvent(new PRS1PeriodicBreathingEvent(t - data1, data0));
|
|
|
|
break;
|
|
case 0x0b: // Large Leak
|
|
data0 = (buffer[pos + 1] << 8 | buffer[pos]);
|
|
data0 *= 2;
|
|
pos += 2;
|
|
data1 = buffer[pos++];
|
|
this->AddEvent(new PRS1LargeLeakEvent(t - data1, data0));
|
|
|
|
break;
|
|
case 0x0d: // flag ??
|
|
data0 = buffer[pos++];
|
|
this->AddEvent(new PRS1HypopneaEvent(t - data0, data0));
|
|
|
|
|
|
break;
|
|
case 0x0e:
|
|
data0 = buffer[pos++];
|
|
this->AddEvent(new PRS1HypopneaEvent(t - data0, data0));
|
|
|
|
break;
|
|
default:
|
|
qDebug() << "Unknown code:" << hex << code << "in" << this->sessionid << "at" << startpos;
|
|
this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos));
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
|
|
// Outer loop based on ParseSummaryF5V3 along with hint as to event codes from old ParseEventsF5V3,
|
|
// except this actually does something with the data.
|
|
bool PRS1DataChunk::ParseEventsF5V3(void)
|
|
{
|
|
if (this->family != 5 || this->familyVersion != 3) {
|
|
qWarning() << "ParseEventsF5V3 called with family" << this->family << "familyVersion" << this->familyVersion;
|
|
return false;
|
|
}
|
|
const unsigned char * data = (unsigned char *)this->m_data.constData();
|
|
int chunk_size = this->m_data.size();
|
|
static const int minimum_sizes[] = { 2, 3, 3, 0xd, 3, 3, 3, 4, 3, 2, 5, 5, 3, 3, 3, 3 };
|
|
static const int ncodes = sizeof(minimum_sizes) / sizeof(int);
|
|
|
|
if (chunk_size < 1) {
|
|
// This does occasionally happen.
|
|
qDebug() << this->sessionid << "event data too short:" << chunk_size;
|
|
return false;
|
|
}
|
|
|
|
// F5V3 uses a gain of 0.125 rather than 0.1 to allow for a maximum value of 30 cmH2O
|
|
static const float GAIN = 0.125; // TODO: this should be parameterized somewhere more logical
|
|
bool ok = true;
|
|
int pos = 0, startpos;
|
|
int code, size;
|
|
int t = 0;
|
|
int elapsed, duration;
|
|
do {
|
|
code = data[pos++];
|
|
if (!this->hblock.contains(code)) {
|
|
qWarning() << this->sessionid << "missing hblock entry for event" << code;
|
|
ok = false;
|
|
break;
|
|
}
|
|
size = this->hblock[code];
|
|
if (code < ncodes) {
|
|
// make sure the handlers below don't go past the end of the buffer
|
|
if (size < minimum_sizes[code]) {
|
|
qWarning() << this->sessionid << "event" << code << "too small" << size << "<" << minimum_sizes[code];
|
|
ok = false;
|
|
break;
|
|
}
|
|
} // else if it's past ncodes, we'll log its information below (rather than handle it)
|
|
if (pos + size > chunk_size) {
|
|
qWarning() << this->sessionid << "event" << code << "@" << pos << "longer than remaining chunk";
|
|
ok = false;
|
|
break;
|
|
}
|
|
startpos = pos;
|
|
t += data[pos] | (data[pos+1] << 8);
|
|
pos += 2;
|
|
|
|
switch (code) {
|
|
case 1: // Pressure adjustment
|
|
// TODO: Have OSCAR treat EPAP adjustment events differently than (average?) stats below.
|
|
//this->AddEvent(new PRS1EPAPEvent(t, data[pos++], GAIN));
|
|
break;
|
|
case 2: // Timed Breath
|
|
this->AddEvent(new PRS1TimedBreathEvent(t, data[pos++])); // TODO: what is value? maybe target breath duration in 5Hz samples? look at zoomed in pressure graph
|
|
break;
|
|
case 3: // Statistics
|
|
// These appear every 2 minutes, so presumably summarize the preceding period.
|
|
this->AddEvent(new PRS1IPAPEvent(t, data[pos++], GAIN)); // 00=IPAP (average?)
|
|
this->AddEvent(new PRS1IPAPLowEvent(t, data[pos++], GAIN)); // 01=IAP Low
|
|
this->AddEvent(new PRS1IPAPHighEvent(t, data[pos++], GAIN)); // 02=IAP High
|
|
this->AddEvent(new PRS1TotalLeakEvent(t, data[pos++])); // 03=LEAK (average?)
|
|
this->AddEvent(new PRS1RespiratoryRateEvent(t, data[pos++])); // 04=Breaths Per Minute (average?)
|
|
this->AddEvent(new PRS1PatientTriggeredBreathsEvent(t, data[pos++])); // 05=Patient Triggered Breaths (average?)
|
|
this->AddEvent(new PRS1MinuteVentilationEvent(t, data[pos++])); // 06=Minute Ventilation (average?)
|
|
this->AddEvent(new PRS1TidalVolumeEvent(t, data[pos++])); // 07=Tidal Volume (average?)
|
|
this->AddEvent(new PRS1SnoreEvent(t, data[pos++])); // 08=Snore (count?) TODO: not a VS on official waveform, but appears in flags and contributes to overall VS index
|
|
this->AddEvent(new PRS1EPAPEvent(t, data[pos++], GAIN)); // 09=EPAP (average? see event 1 above)
|
|
//data0 = data[pos++]; // 0A = ??? TODO: what is this? should probably graph it as a test channel
|
|
break;
|
|
//case 0x04: // TODO: find sample
|
|
case 0x05: // Obstructive Apnea
|
|
elapsed = data[pos++];
|
|
this->AddEvent(new PRS1ObstructiveApneaEvent(t - elapsed, 0));
|
|
break;
|
|
case 0x06: // Clear Airway Apnea
|
|
elapsed = data[pos++];
|
|
this->AddEvent(new PRS1ClearAirwayEvent(t - elapsed, 0));
|
|
break;
|
|
//case 0x07: // TODO: find sample
|
|
case 0x08: // Flow Limitation
|
|
duration = data[pos++]; // TODO: is this really duration, or is it time elapsed since a FL marker like OA/CA?
|
|
this->AddEvent(new PRS1FlowLimitationEvent(t - duration, duration));
|
|
break;
|
|
case 0x09: // Vibratory Snore
|
|
// no data bytes
|
|
this->AddEvent(new PRS1VibratorySnoreEvent(t, 0)); // TODO: this is different than the snore stat above, corresponds to VS on official waveform?
|
|
break;
|
|
case 0x0a: // Periodic Breathing
|
|
duration = 2 * (data[pos] | (data[pos+1] << 8));
|
|
pos += 2;
|
|
elapsed = data[pos++];
|
|
this->AddEvent(new PRS1PeriodicBreathingEvent(t - elapsed - duration, duration)); // TODO: PB drawn at wrong time, maybe OSCAR is compensating for duration starting offset somewhere?
|
|
break;
|
|
case 0x0b: // Large Leak
|
|
duration = 2 * (data[pos] | (data[pos+1] << 8));
|
|
pos += 2;
|
|
elapsed = data[pos++];
|
|
this->AddEvent(new PRS1LargeLeakEvent(t - elapsed - duration, duration)); // TODO: LL drawn at wrong time, maybe OSCAR is compensating for duration starting offset somewhere?
|
|
break;
|
|
//case 0x0d: // TODO: find sample
|
|
case 0x0e: // Hypopnea
|
|
duration = data[pos++]; // TODO: is this really duration, or is it time elapsed since a FL marker?
|
|
this->AddEvent(new PRS1HypopneaEvent(t - duration, duration));
|
|
break;
|
|
//case 0x0f: // TODO: find sample
|
|
default:
|
|
qWarning() << "Unknown event:" << code << "in" << this->sessionid << "at" << startpos-1;
|
|
this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos-1, size+1));
|
|
//UNEXPECTED_VALUE(code, "known event code");
|
|
break;
|
|
}
|
|
pos = startpos + size;
|
|
} while (ok && pos < chunk_size);
|
|
|
|
this->duration = t;
|
|
|
|
return ok;
|
|
}
|
|
|
|
|
|
bool PRS1Import::ParseF5Events()
|
|
{
|
|
// Required channels
|
|
EventList *OA = session->AddEventList(CPAP_Obstructive, EVL_Event);
|
|
EventList *HY = session->AddEventList(CPAP_Hypopnea, EVL_Event);
|
|
EventList *CA = session->AddEventList(CPAP_ClearAirway, EVL_Event);
|
|
|
|
EventList *LL = session->AddEventList(CPAP_LargeLeak, EVL_Event);
|
|
EventList *TOTLEAK = session->AddEventList(CPAP_LeakTotal, EVL_Event);
|
|
EventList *LEAK = session->AddEventList(CPAP_Leak, EVL_Event);
|
|
EventList *RR = session->AddEventList(CPAP_RespRate, EVL_Event);
|
|
EventList *TV = session->AddEventList(CPAP_TidalVolume, EVL_Event, 10.0F);
|
|
EventList *MV = session->AddEventList(CPAP_MinuteVent, EVL_Event);
|
|
EventList *PB = session->AddEventList(CPAP_PB, EVL_Event);
|
|
EventList *PTB = session->AddEventList(CPAP_PTB, EVL_Event);
|
|
EventList *TB = session->AddEventList(PRS1_TimedBreath, EVL_Event);
|
|
EventList *IPAP = session->AddEventList(CPAP_IPAP, EVL_Event, 0.1F);
|
|
EventList *EPAP = session->AddEventList(CPAP_EPAP, EVL_Event, 0.1F);
|
|
EventList *PS = session->AddEventList(CPAP_PS, EVL_Event, 0.1F);
|
|
EventList *IPAPLo = session->AddEventList(CPAP_IPAPLo, EVL_Event, 0.1F);
|
|
EventList *IPAPHi = session->AddEventList(CPAP_IPAPHi, EVL_Event, 0.1F);
|
|
EventList *FL = session->AddEventList(CPAP_FlowLimit, EVL_Event);
|
|
EventList *SNORE = session->AddEventList(CPAP_Snore, EVL_Event);
|
|
EventList *VS = session->AddEventList(CPAP_VSnore, EVL_Event);
|
|
|
|
// On-demand channels
|
|
ChannelID Codes[] = {
|
|
PRS1_00, PRS1_01, CPAP_Pressure, CPAP_EPAP, CPAP_PressurePulse, CPAP_Obstructive,
|
|
CPAP_ClearAirway, CPAP_Hypopnea, PRS1_08, CPAP_FlowLimit, PRS1_0A, CPAP_PB,
|
|
PRS1_0C, CPAP_VSnore, PRS1_0E, PRS1_0F,
|
|
CPAP_LargeLeak, // Large leak apparently
|
|
CPAP_LeakTotal, PRS1_12
|
|
};
|
|
|
|
int ncodes = sizeof(Codes) / sizeof(ChannelID);
|
|
EventList *Code[0x20] = {nullptr};
|
|
|
|
//EventList * PRESSURE=nullptr;
|
|
//EventList * PP=nullptr;
|
|
|
|
|
|
// Unintentional leak calculation, see zMaskProfile:calcLeak in calcs.cpp for explanation
|
|
EventDataType currentPressure=0, leak;
|
|
|
|
bool calcLeaks = p_profile->cpap->calculateUnintentionalLeaks();
|
|
EventDataType lpm4 = p_profile->cpap->custom4cmH2OLeaks();
|
|
EventDataType lpm20 = p_profile->cpap->custom20cmH2OLeaks();
|
|
|
|
EventDataType lpm = lpm20 - lpm4;
|
|
EventDataType ppm = lpm / 16.0;
|
|
|
|
|
|
qint64 t = qint64(event->timestamp) * 1000L;
|
|
session->updateFirst(t);
|
|
|
|
bool ok;
|
|
ok = event->ParseEvents(MODE_UNKNOWN);
|
|
|
|
for (int i=0; i < event->m_parsedData.count(); i++) {
|
|
PRS1ParsedEvent* e = event->m_parsedData.at(i);
|
|
t = qint64(event->timestamp + e->m_start) * 1000L;
|
|
|
|
switch (e->m_type) {
|
|
case PRS1IPAPEvent::TYPE:
|
|
IPAP->AddEvent(t, e->m_value);
|
|
currentPressure = e->m_value;
|
|
break;
|
|
case PRS1IPAPLowEvent::TYPE:
|
|
IPAPLo->AddEvent(t, e->m_value);
|
|
break;
|
|
case PRS1IPAPHighEvent::TYPE:
|
|
IPAPHi->AddEvent(t, e->m_value);
|
|
break;
|
|
case PRS1EPAPEvent::TYPE:
|
|
EPAP->AddEvent(t, e->m_value);
|
|
PS->AddEvent(t, currentPressure - e->m_value); // Pressure Support
|
|
break;
|
|
case PRS1TimedBreathEvent::TYPE:
|
|
TB->AddEvent(t, e->m_duration);
|
|
break;
|
|
case PRS1ObstructiveApneaEvent::TYPE:
|
|
OA->AddEvent(t, e->m_duration);
|
|
break;
|
|
case PRS1ClearAirwayEvent::TYPE:
|
|
CA->AddEvent(t, e->m_duration);
|
|
break;
|
|
case PRS1HypopneaEvent::TYPE:
|
|
HY->AddEvent(t, e->m_duration);
|
|
break;
|
|
case PRS1FlowLimitationEvent::TYPE:
|
|
FL->AddEvent(t, e->m_duration);
|
|
break;
|
|
case PRS1PeriodicBreathingEvent::TYPE:
|
|
PB->AddEvent(t, e->m_duration);
|
|
break;
|
|
case PRS1LargeLeakEvent::TYPE:
|
|
LL->AddEvent(t, e->m_duration);
|
|
break;
|
|
case PRS1TotalLeakEvent::TYPE:
|
|
TOTLEAK->AddEvent(t, e->m_value);
|
|
leak = e->m_value;
|
|
if (calcLeaks) { // Much Quicker doing this here than the recalc method.
|
|
leak -= (((currentPressure/10.0f) - 4.0) * ppm + lpm4);
|
|
if (leak < 0) leak = 0;
|
|
LEAK->AddEvent(t, leak);
|
|
}
|
|
break;
|
|
case PRS1LeakEvent::TYPE:
|
|
LEAK->AddEvent(t, e->m_value);
|
|
break;
|
|
case PRS1SnoreEvent::TYPE:
|
|
SNORE->AddEvent(t, e->m_value);
|
|
if (e->m_value > 0) {
|
|
VS->AddEvent(t, 0); //data2); // VSnore
|
|
}
|
|
break;
|
|
case PRS1RespiratoryRateEvent::TYPE:
|
|
RR->AddEvent(t, e->m_value);
|
|
break;
|
|
case PRS1PatientTriggeredBreathsEvent::TYPE:
|
|
PTB->AddEvent(t, e->m_value);
|
|
break;
|
|
case PRS1MinuteVentilationEvent::TYPE:
|
|
MV->AddEvent(t, e->m_value);
|
|
break;
|
|
case PRS1TidalVolumeEvent::TYPE:
|
|
TV->AddEvent(t, e->m_value);
|
|
break;
|
|
case PRS1UnknownValueEvent::TYPE:
|
|
{
|
|
int code = ((PRS1UnknownValueEvent*) e)->m_code;
|
|
Q_ASSERT(code < ncodes);
|
|
if (!Code[code]) {
|
|
ChannelID cpapcode = Codes[(int)code];
|
|
if (!(Code[code] = session->AddEventList(cpapcode, EVL_Event, e->m_gain))) { return false; }
|
|
}
|
|
Code[code]->AddEvent(t, e->m_value);
|
|
break;
|
|
}
|
|
default:
|
|
qWarning() << "Unknown PRS1 event type" << (int) e->m_type;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!ok) {
|
|
return false;
|
|
}
|
|
|
|
t = qint64(event->timestamp + event->duration) * 1000L;
|
|
session->updateLast(t);
|
|
session->m_cnt.clear();
|
|
session->m_cph.clear();
|
|
|
|
session->m_valuesummary[CPAP_Pressure].clear();
|
|
session->m_valuesummary.erase(session->m_valuesummary.find(CPAP_Pressure));
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
// 950P is F5V0, 960P and 961P are F5V1, 960T is F5V2
|
|
bool PRS1DataChunk::ParseEventsF5V012(void)
|
|
{
|
|
EventDataType data0, data1, data2, data4, data5;
|
|
|
|
int t = 0;
|
|
int pos = 0;
|
|
int cnt = 0;
|
|
short delta;//,duration;
|
|
bool badcode = false;
|
|
unsigned char lastcode3 = 0, lastcode2 = 0, lastcode = 0, code = 0;
|
|
int lastpos = 0, startpos = 0, lastpos2 = 0, lastpos3 = 0;
|
|
|
|
int size = this->m_data.size();
|
|
unsigned char * buffer = (unsigned char *)this->m_data.data();
|
|
|
|
while (pos < size) {
|
|
lastcode3 = lastcode2;
|
|
lastcode2 = lastcode;
|
|
lastcode = code;
|
|
lastpos3 = lastpos2;
|
|
lastpos2 = lastpos;
|
|
lastpos = startpos;
|
|
startpos = pos;
|
|
code = buffer[pos++];
|
|
|
|
if (code >= 0x13) {
|
|
qDebug() << "Illegal PRS1 code " << hex << int(code) << " appeared at " << hex << startpos << "in" << this->sessionid;;
|
|
qDebug() << "1: (" << int(lastcode) << hex << lastpos << ")";
|
|
qDebug() << "2: (" << int(lastcode2) << hex << lastpos2 << ")";
|
|
qDebug() << "3: (" << int(lastcode3) << hex << lastpos3 << ")";
|
|
this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos));
|
|
return false;
|
|
}
|
|
|
|
if (code == 0) {
|
|
// No delta
|
|
} else if (code != 0x12) {
|
|
delta = buffer[pos];
|
|
//duration=buffer[pos+1];
|
|
//delta=buffer[pos+1] << 8 | buffer[pos];
|
|
pos += 2;
|
|
t += delta;
|
|
}
|
|
|
|
//EventDataType PS;
|
|
cnt++;
|
|
int fc = 0;
|
|
|
|
switch (code) {
|
|
case 0x00: // Unknown (ASV Pressure value)
|
|
// offset?
|
|
data0 = buffer[pos++];
|
|
fc++;
|
|
|
|
if (!buffer[pos - 1]) { // WTH???
|
|
data1 = buffer[pos++];
|
|
fc++;
|
|
}
|
|
|
|
if (!buffer[pos - 1]) {
|
|
data2 = buffer[pos++];
|
|
fc++;
|
|
}
|
|
|
|
break;
|
|
|
|
case 0x01: // Unknown
|
|
this->AddEvent(new PRS1UnknownValueEvent(code, t, 0, 0.1F));
|
|
break;
|
|
|
|
case 0x02: // Pressure ???
|
|
data0 = buffer[pos++];
|
|
// if (!Code[2]) {
|
|
// if (!(Code[2]=session->AddEventList(cpapcode,EVL_Event,0.1))) return false;
|
|
// }
|
|
// Code[2]->AddEvent(t,data0);
|
|
break;
|
|
case 0x03: // BIPAP Pressure
|
|
qDebug() << "0x03 Observed in ASV data!!????";
|
|
|
|
data0 = buffer[pos++];
|
|
data1 = buffer[pos++];
|
|
// data0/=10.0;
|
|
// data1/=10.0;
|
|
// session->AddEvent(new Event(t,CPAP_EAP, 0, data, 1));
|
|
// session->AddEvent(new Event(t,CPAP_IAP, 0, &data1, 1));
|
|
break;
|
|
|
|
case 0x04: // Timed Breath
|
|
data0 = buffer[pos++];
|
|
|
|
this->AddEvent(new PRS1TimedBreathEvent(t, data0));
|
|
break;
|
|
|
|
case 0x05:
|
|
//code=CPAP_Obstructive;
|
|
data0 = buffer[pos++];
|
|
this->AddEvent(new PRS1ObstructiveApneaEvent(t - data0, data0));
|
|
break;
|
|
|
|
case 0x06:
|
|
//code=CPAP_ClearAirway;
|
|
data0 = buffer[pos++];
|
|
this->AddEvent(new PRS1ClearAirwayEvent(t - data0, data0));
|
|
break;
|
|
|
|
case 0x07:
|
|
//code=CPAP_Hypopnea;
|
|
data0 = buffer[pos++];
|
|
this->AddEvent(new PRS1HypopneaEvent(t - data0, data0));
|
|
break;
|
|
|
|
case 0x08: // ???
|
|
data0 = buffer[pos++];
|
|
//qDebug() << "Code 8 found at " << hex << pos - 1 << " " << tt;
|
|
|
|
if (this->familyVersion>=2) {
|
|
this->AddEvent(new PRS1HypopneaEvent(t - data0, data0));
|
|
} else {
|
|
this->AddEvent(new PRS1UnknownValueEvent(code, t - data0, data0));
|
|
//????
|
|
//data1=buffer[pos++]; // ???
|
|
//pos++;
|
|
}
|
|
break;
|
|
|
|
case 0x09: // ASV Codes
|
|
if (this->familyVersion<2) {
|
|
//code=CPAP_FlowLimit;
|
|
data0 = buffer[pos++];
|
|
|
|
this->AddEvent(new PRS1FlowLimitationEvent(t - data0, data0));
|
|
} else {
|
|
data0 = buffer[pos++];
|
|
data1 = buffer[pos++];
|
|
}
|
|
|
|
break;
|
|
|
|
case 0x0a:
|
|
data0 = buffer[pos++];
|
|
if (this->familyVersion>=2) {
|
|
this->AddEvent(new PRS1FlowLimitationEvent(t - data0, data0));
|
|
} else {
|
|
this->AddEvent(new PRS1UnknownValueEvent(code, t - data0, data0));
|
|
}
|
|
break;
|
|
|
|
|
|
case 0x0b: // Cheyne Stokes
|
|
data0 = ((unsigned char *)buffer)[pos + 1] << 8 | ((unsigned char *)buffer)[pos];
|
|
//data0*=2;
|
|
pos += 2;
|
|
data1 = ((unsigned char *)buffer)[pos]; //|buffer[pos+1] << 8
|
|
pos += 1;
|
|
//tt-=delta;
|
|
this->AddEvent(new PRS1PeriodicBreathingEvent(t - data1, data0));
|
|
break;
|
|
|
|
case 0x0c:
|
|
|
|
if (this->familyVersion>=2) {
|
|
data0 = (buffer[pos + 1] << 8 | buffer[pos]);
|
|
data0 *= 2;
|
|
pos += 2;
|
|
data1 = buffer[pos++];
|
|
this->AddEvent(new PRS1PeriodicBreathingEvent(t - data1, data0));
|
|
|
|
} else {
|
|
data0 = buffer[pos++];
|
|
qDebug() << "Code 12 found at " << hex << pos - 1 << " " << t - data0;
|
|
|
|
this->AddEvent(new PRS1UnknownValueEvent(code, t - data0, data0));
|
|
pos += 2;
|
|
}
|
|
break;
|
|
|
|
case 0x0d: // All the other ASV graph stuff.
|
|
|
|
if (this->familyVersion>=2) {
|
|
data0 = (buffer[pos + 1] << 8 | buffer[pos]);
|
|
data0 *= 2;
|
|
pos += 2;
|
|
data1 = buffer[pos++];
|
|
//tt = t - qint64(data1) * 1000L;
|
|
} else {
|
|
this->AddEvent(new PRS1IPAPEvent(t, buffer[pos++])); // 00=IAP
|
|
data4 = buffer[pos++];
|
|
this->AddEvent(new PRS1IPAPLowEvent(t, data4)); // 01=IAP Low
|
|
data5 = buffer[pos++];
|
|
this->AddEvent(new PRS1IPAPHighEvent(t, data5)); // 02=IAP High
|
|
|
|
this->AddEvent(new PRS1TotalLeakEvent(t, buffer[pos++])); // 03=LEAK
|
|
|
|
this->AddEvent(new PRS1RespiratoryRateEvent(t, buffer[pos++])); // 04=Breaths Per Minute
|
|
this->AddEvent(new PRS1PatientTriggeredBreathsEvent(t, buffer[pos++])); // 05=Patient Triggered Breaths
|
|
this->AddEvent(new PRS1MinuteVentilationEvent(t, buffer[pos++])); // 06=Minute Ventilation
|
|
//tmp=buffer[pos++] * 10.0;
|
|
this->AddEvent(new PRS1TidalVolumeEvent(t, buffer[pos++])); // 07=Tidal Volume
|
|
this->AddEvent(new PRS1SnoreEvent(t, buffer[pos++])); // 08=Snore
|
|
this->AddEvent(new PRS1EPAPEvent(t, data1 = buffer[pos++])); // 09=EPAP
|
|
if (this->familyVersion >= 1) {
|
|
data0 = buffer[pos++];
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 0x0e: // Unknown
|
|
// Family 5.2 has this code
|
|
if (this->familyVersion>=2) {
|
|
this->AddEvent(new PRS1IPAPEvent(t, data1=buffer[pos+0])); // 0
|
|
this->AddEvent(new PRS1IPAPLowEvent(t, buffer[pos+1])); // 1
|
|
this->AddEvent(new PRS1IPAPHighEvent(t, buffer[pos+2])); // 2
|
|
this->AddEvent(new PRS1LeakEvent(t, buffer[pos+3])); // 3 // F5V2, is this really unintentional leak rather than total leak?
|
|
this->AddEvent(new PRS1TidalVolumeEvent(t, buffer[pos+7])); // 7
|
|
this->AddEvent(new PRS1RespiratoryRateEvent(t, buffer[pos+4])); // 4
|
|
this->AddEvent(new PRS1PatientTriggeredBreathsEvent(t, buffer[pos+5])); // 5
|
|
this->AddEvent(new PRS1MinuteVentilationEvent(t, buffer[pos+6])); //6
|
|
this->AddEvent(new PRS1SnoreEvent(t, buffer[pos+8])); //??
|
|
this->AddEvent(new PRS1EPAPEvent(t, buffer[pos+9])); // 9
|
|
pos+=11;
|
|
} else {
|
|
qDebug() << "0x0E Observed in ASV data!!????";
|
|
data0 = buffer[pos++]; // << 8) | buffer[pos];
|
|
|
|
}
|
|
//session->AddEvent(new Event(t,cpapcode, 0, data, 1));
|
|
break;
|
|
case 0x0f:
|
|
qDebug() << "0x0f Observed in ASV data!!????";
|
|
|
|
data0 = buffer[pos + 1] << 8 | buffer[pos];
|
|
pos += 2;
|
|
data1 = buffer[pos]; //|buffer[pos+1] << 8
|
|
pos += 1;
|
|
//tt -= qint64(data1) * 1000L;
|
|
//session->AddEvent(new Event(tt,cpapcode, 0, data, 2));
|
|
break;
|
|
|
|
case 0x10: // Unknown
|
|
data0 = buffer[pos + 1] << 8 | buffer[pos];
|
|
pos += 2;
|
|
data1 = buffer[pos++];
|
|
|
|
this->AddEvent(new PRS1LargeLeakEvent(t - data1, data0));
|
|
|
|
// qDebug() << "0x10 Observed in ASV data!!????";
|
|
// data0 = buffer[pos++]; // << 8) | buffer[pos];
|
|
// data1 = buffer[pos++];
|
|
// data2 = buffer[pos++];
|
|
//session->AddEvent(new Event(t,cpapcode, 0, data, 3));
|
|
break;
|
|
case 0x11: // Not Leak Rate
|
|
qDebug() << "0x11 Observed in ASV data!!????";
|
|
//if (!Code[24]) {
|
|
// Code[24]=new EventList(cpapcode,EVL_Event);
|
|
//}
|
|
//Code[24]->AddEvent(t,buffer[pos++]);
|
|
break;
|
|
|
|
|
|
case 0x12: // Summary
|
|
qDebug() << "0x12 Observed in ASV data!!????";
|
|
data0 = buffer[pos++];
|
|
data1 = buffer[pos++];
|
|
data2 = buffer[pos + 1] << 8 | buffer[pos];
|
|
pos += 2;
|
|
//session->AddEvent(new Event(t,cpapcode, 0, data,3));
|
|
break;
|
|
|
|
default: // ERROR!!!
|
|
qWarning() << "Some new fandangled PRS1 code detected " << hex << int(code) << " at " << pos - 1;
|
|
this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos));
|
|
badcode = true;
|
|
break;
|
|
}
|
|
|
|
if (badcode) {
|
|
break;
|
|
}
|
|
}
|
|
this->duration = t; // The last event might start before t, so record the last delta timestamp.
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
bool PRS1Import::ParseF3EventsV3()
|
|
{
|
|
// Required channels
|
|
EventList *OA = session->AddEventList(CPAP_Obstructive, EVL_Event);
|
|
EventList *HY = session->AddEventList(CPAP_Hypopnea, EVL_Event);
|
|
EventList *CA = session->AddEventList(CPAP_ClearAirway, EVL_Event);
|
|
|
|
EventList *LL = session->AddEventList(CPAP_LargeLeak, EVL_Event);
|
|
EventList *LEAK = session->AddEventList(CPAP_Leak, EVL_Event);
|
|
EventList *RR = session->AddEventList(CPAP_RespRate, EVL_Event);
|
|
EventList *TV = session->AddEventList(CPAP_TidalVolume, EVL_Event, 10.0F);
|
|
EventList *MV = session->AddEventList(CPAP_MinuteVent, EVL_Event);
|
|
EventList *PB = session->AddEventList(CPAP_PB, EVL_Event);
|
|
EventList *PTB = session->AddEventList(CPAP_PTB, EVL_Event);
|
|
EventList *TB = session->AddEventList(PRS1_TimedBreath, EVL_Event);
|
|
EventList *IPAP = session->AddEventList(CPAP_IPAP, EVL_Event, 0.1F);
|
|
EventList *EPAP = session->AddEventList(CPAP_EPAP, EVL_Event, 0.1F);
|
|
EventList *RE = session->AddEventList(CPAP_RERA, EVL_Event);
|
|
EventList *ZZ = session->AddEventList(CPAP_NRI, EVL_Event);
|
|
EventList *TMV = session->AddEventList(CPAP_Test1, EVL_Event);
|
|
EventList *FLOW = session->AddEventList(CPAP_Test2, EVL_Event);
|
|
|
|
|
|
qint64 t;
|
|
// missing session->updateFirst(t)?
|
|
|
|
bool ok;
|
|
ok = event->ParseEvents(MODE_UNKNOWN);
|
|
|
|
for (int i=0; i < event->m_parsedData.count(); i++) {
|
|
PRS1ParsedEvent* e = event->m_parsedData.at(i);
|
|
t = qint64(event->timestamp + e->m_start) * 1000L;
|
|
|
|
switch (e->m_type) {
|
|
case PRS1IPAPEvent::TYPE:
|
|
IPAP->AddEvent(t, e->m_value);
|
|
break;
|
|
case PRS1EPAPEvent::TYPE:
|
|
EPAP->AddEvent(t, e->m_value);
|
|
break;
|
|
case PRS1TimedBreathEvent::TYPE:
|
|
TB->AddEvent(t, e->m_duration);
|
|
break;
|
|
case PRS1ObstructiveApneaEvent::TYPE:
|
|
OA->AddEvent(t, e->m_duration);
|
|
break;
|
|
case PRS1ClearAirwayEvent::TYPE:
|
|
CA->AddEvent(t, e->m_duration);
|
|
break;
|
|
case PRS1HypopneaEvent::TYPE:
|
|
HY->AddEvent(t, e->m_duration);
|
|
break;
|
|
case PRS1PeriodicBreathingEvent::TYPE:
|
|
PB->AddEvent(t, e->m_duration);
|
|
break;
|
|
case PRS1LargeLeakEvent::TYPE:
|
|
LL->AddEvent(t, e->m_duration);
|
|
break;
|
|
case PRS1LeakEvent::TYPE:
|
|
LEAK->AddEvent(t, e->m_value);
|
|
break;
|
|
case PRS1RespiratoryRateEvent::TYPE:
|
|
RR->AddEvent(t, e->m_value);
|
|
break;
|
|
case PRS1PatientTriggeredBreathsEvent::TYPE:
|
|
PTB->AddEvent(t, e->m_value);
|
|
break;
|
|
case PRS1MinuteVentilationEvent::TYPE:
|
|
MV->AddEvent(t, e->m_value);
|
|
break;
|
|
case PRS1TidalVolumeEvent::TYPE:
|
|
TV->AddEvent(t, e->m_value);
|
|
break;
|
|
case PRS1RERAEvent::TYPE:
|
|
RE->AddEvent(t, e->m_value);
|
|
break;
|
|
case PRS1NonRespondingEvent::TYPE:
|
|
ZZ->AddEvent(t, e->m_value);
|
|
break;
|
|
case PRS1Test1Event::TYPE:
|
|
TMV->AddEvent(t, e->m_value);
|
|
break;
|
|
case PRS1Test2Event::TYPE:
|
|
FLOW->AddEvent(t, e->m_value);
|
|
break;
|
|
case PRS1UnknownDataEvent::TYPE:
|
|
{
|
|
PRS1UnknownDataEvent* unk = (PRS1UnknownDataEvent*) e;
|
|
int code = unk->m_code;
|
|
char* data = unk->m_data.data();
|
|
QString dump;
|
|
if (!loader->unknownCodes.contains(code)) {
|
|
loader->unknownCodes.insert(code, QStringList());
|
|
}
|
|
QStringList & str = loader->unknownCodes[code];
|
|
dump = QString("%1@0x%5: [%2] [%3 %4]")
|
|
.arg(event->sessionid, 8, 16, QChar('0'))
|
|
.arg(data[0], 2, 16, QChar('0'))
|
|
.arg(data[1], 2, 16, QChar('0'))
|
|
.arg(data[2], 2, 16, QChar('0'))
|
|
.arg(unk->m_pos, 5, 16, QChar('0'));
|
|
for (int i=3; i<unk->m_data.size(); i++) {
|
|
dump += QString(" %1").arg(data[i], 2, 16, QChar('0'));
|
|
}
|
|
str.append(dump.trimmed());
|
|
break;
|
|
}
|
|
default:
|
|
qWarning() << "Unknown PRS1 event type" << (int) e->m_type;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!ok) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
// 1030X, 11030X series
|
|
bool PRS1DataChunk::ParseEventsF3V6(void)
|
|
{
|
|
// AVAPS machine... it's delta packed, unlike the older ones?? (double check that! :/)
|
|
|
|
if (this->family != 3 || this->familyVersion != 6) {
|
|
qWarning() << "ParseEventsF3V6 called with family" << this->family << "familyVersion" << this->familyVersion;
|
|
return false;
|
|
}
|
|
|
|
int t = 0;
|
|
int pos = 0;
|
|
int datasize = this->m_data.size();
|
|
|
|
unsigned char * data = (unsigned char *)this->m_data.data();
|
|
unsigned char code;
|
|
unsigned short delta;
|
|
bool failed = false;
|
|
|
|
unsigned char val, val2;
|
|
QString dump;
|
|
|
|
do {
|
|
int startpos = pos;
|
|
code = data[pos++];
|
|
delta = (data[pos+1] < 8) | data[pos];
|
|
pos += 2;
|
|
#ifdef DEBUG_EVENTS
|
|
if (code == 0x00) {
|
|
this->AddEvent(new PRS1UnknownDataEvent(this->m_data, startpos));
|
|
}
|
|
#endif
|
|
unsigned short epap;
|
|
|
|
switch(code) {
|
|
case 0x01: // Who knows
|
|
val = data[pos++];
|
|
this->AddEvent(new PRS1TimedBreathEvent(t, val));
|
|
break;
|
|
case 0x02:
|
|
this->AddEvent(new PRS1LeakEvent(t, data[pos+3])); // TODO: F3V6, is this really unintentional leak rather than total leak?
|
|
this->AddEvent(new PRS1PatientTriggeredBreathsEvent(t, data[pos+5]));
|
|
this->AddEvent(new PRS1MinuteVentilationEvent(t, data[pos+6]));
|
|
this->AddEvent(new PRS1TidalVolumeEvent(t, data[pos+7]));
|
|
|
|
|
|
this->AddEvent(new PRS1EPAPEvent(t, epap=data[pos+0]));
|
|
this->AddEvent(new PRS1IPAPEvent(t, data[pos+1]));
|
|
this->AddEvent(new PRS1Test2Event(t, data[pos+4])); // Flow???
|
|
this->AddEvent(new PRS1Test1Event(t, data[pos+8])); // TMV???
|
|
this->AddEvent(new PRS1RespiratoryRateEvent(t, data[pos+9]));
|
|
pos += 12;
|
|
|
|
break;
|
|
case 0x04: // ???
|
|
val = data[pos++];
|
|
this->AddEvent(new PRS1TimedBreathEvent(t, val));
|
|
break;
|
|
case 0x05: // ???
|
|
val = data[pos++];
|
|
this->AddEvent(new PRS1ClearAirwayEvent(t, val));
|
|
break;
|
|
case 0x06: // Obstructive Apnea
|
|
val = data[pos++];
|
|
val2 = data[pos++];
|
|
this->AddEvent(new PRS1ObstructiveApneaEvent(t + val2, val)); // ??? shouldn't this be t - val2?
|
|
break;
|
|
case 0x07: // PB
|
|
val = data[pos+1] << 8 | data[pos];
|
|
pos += 2;
|
|
val2 = data[pos++];
|
|
this->AddEvent(new PRS1PeriodicBreathingEvent(t - val2, val));
|
|
break;
|
|
case 0x08: // RERA
|
|
val = data[pos++];
|
|
this->AddEvent(new PRS1RERAEvent(t, val));
|
|
break;
|
|
case 0x09: // ???
|
|
val = data[pos+1] << 8 | data[pos];
|
|
pos += 2;
|
|
val2 = data[pos++];
|
|
this->AddEvent(new PRS1LargeLeakEvent(t - val, val2));
|
|
break;
|
|
|
|
case 0x0a: // ???
|
|
val = data[pos++];
|
|
this->AddEvent(new PRS1NonRespondingEvent(t, val));
|
|
break;
|
|
case 0x0b: // Hypopnea
|
|
val = data[pos++];
|
|
this->AddEvent(new PRS1HypopneaEvent(t, val));
|
|
break;
|
|
|
|
default:
|
|
this->AddEvent(new PRS1UnknownDataEvent(this->m_data, startpos));
|
|
failed = true;
|
|
break;
|
|
};
|
|
t += delta;
|
|
|
|
} while ((pos < datasize) && !failed);
|
|
|
|
if (failed) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
bool PRS1Import::ParseF3Events()
|
|
{
|
|
// Required channels
|
|
EventList *OA = session->AddEventList(CPAP_Obstructive, EVL_Event);
|
|
EventList *HY = session->AddEventList(CPAP_Hypopnea, EVL_Event);
|
|
EventList *CA = session->AddEventList(CPAP_ClearAirway, EVL_Event);
|
|
|
|
EventList *TOTLEAK = session->AddEventList(CPAP_LeakTotal, EVL_Event);
|
|
EventList *LEAK = session->AddEventList(CPAP_Leak, EVL_Event);
|
|
EventList *RR = session->AddEventList(CPAP_RespRate, EVL_Event);
|
|
EventList *TV = session->AddEventList(CPAP_TidalVolume, EVL_Event, 10.0F);
|
|
EventList *MV = session->AddEventList(CPAP_MinuteVent, EVL_Event);
|
|
EventList *PTB = session->AddEventList(CPAP_PTB, EVL_Event);
|
|
EventList *IPAP = session->AddEventList(CPAP_IPAP, EVL_Event, 0.1F);
|
|
EventList *EPAP = session->AddEventList(CPAP_EPAP, EVL_Event, 0.1F);
|
|
EventList *FLOW = session->AddEventList(CPAP_FlowRate, EVL_Event);
|
|
|
|
|
|
qint64 t = qint64(event->timestamp) * 1000L;
|
|
session->updateFirst(t);
|
|
|
|
bool ok;
|
|
ok = event->ParseEvents(MODE_UNKNOWN);
|
|
|
|
for (int i=0; i < event->m_parsedData.count(); i++) {
|
|
PRS1ParsedEvent* e = event->m_parsedData.at(i);
|
|
t = qint64(event->timestamp + e->m_start) * 1000L;
|
|
|
|
switch (e->m_type) {
|
|
case PRS1IPAPEvent::TYPE:
|
|
IPAP->AddEvent(t, e->m_value);
|
|
break;
|
|
case PRS1EPAPEvent::TYPE:
|
|
EPAP->AddEvent(t, e->m_value);
|
|
break;
|
|
case PRS1ObstructiveApneaEvent::TYPE:
|
|
OA->AddEvent(t, e->m_duration);
|
|
break;
|
|
case PRS1ClearAirwayEvent::TYPE:
|
|
CA->AddEvent(t, e->m_duration);
|
|
break;
|
|
case PRS1HypopneaEvent::TYPE:
|
|
HY->AddEvent(t, e->m_duration);
|
|
break;
|
|
case PRS1TotalLeakEvent::TYPE:
|
|
TOTLEAK->AddEvent(t, e->m_value);
|
|
break;
|
|
case PRS1LeakEvent::TYPE:
|
|
LEAK->AddEvent(t, e->m_value);
|
|
break;
|
|
case PRS1RespiratoryRateEvent::TYPE:
|
|
RR->AddEvent(t, e->m_value);
|
|
break;
|
|
case PRS1PatientTriggeredBreathsEvent::TYPE:
|
|
PTB->AddEvent(t, e->m_value);
|
|
break;
|
|
case PRS1MinuteVentilationEvent::TYPE:
|
|
MV->AddEvent(t, e->m_value);
|
|
break;
|
|
case PRS1TidalVolumeEvent::TYPE:
|
|
TV->AddEvent(t, e->m_value);
|
|
break;
|
|
case PRS1FlowRateEvent::TYPE:
|
|
FLOW->AddEvent(t, e->m_value);
|
|
break;
|
|
default:
|
|
qWarning() << "Unknown PRS1 event type" << (int) e->m_type;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!ok) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
// 1160P series
|
|
bool PRS1DataChunk::ParseEventsF3V3(void)
|
|
{
|
|
if (this->family != 3 || this->familyVersion != 3) {
|
|
qWarning() << "ParseEventsF3V3 called with family" << this->family << "familyVersion" << this->familyVersion;
|
|
return false;
|
|
}
|
|
|
|
int t = 0, tt;
|
|
int size = this->m_data.size()/0x10;
|
|
unsigned char * h = (unsigned char *)this->m_data.data();
|
|
|
|
int hy, oa, ca;
|
|
qint64 div = 0;
|
|
|
|
// TODO: make sure the assumptions here agree with the header:
|
|
// size == number of intervals
|
|
// interval seconds = 120
|
|
// interleave for each channel = 1
|
|
// also warn on any remainder of data size % record size (but don't fail)
|
|
|
|
const qint64 block_duration = 120;
|
|
|
|
for (int x=0; x < size; x++) {
|
|
this->AddEvent(new PRS1IPAPEvent(t, h[0] | (h[1] << 8)));
|
|
this->AddEvent(new PRS1EPAPEvent(t, h[2] | (h[3] << 8)));
|
|
this->AddEvent(new PRS1TotalLeakEvent(t, h[4]));
|
|
this->AddEvent(new PRS1TidalVolumeEvent(t, h[5]));
|
|
this->AddEvent(new PRS1FlowRateEvent(t, h[6]));
|
|
this->AddEvent(new PRS1PatientTriggeredBreathsEvent(t, h[7]));
|
|
this->AddEvent(new PRS1RespiratoryRateEvent(t, h[8]));
|
|
//TMV->AddEvent(t, h[9]); // not sure what this is.. encore doesn't graph it.
|
|
// h[10]?
|
|
this->AddEvent(new PRS1MinuteVentilationEvent(t, h[11]));
|
|
this->AddEvent(new PRS1LeakEvent(t, h[15])); // TODO: F3V3, is this really unintentional leak rather than total leak?
|
|
|
|
hy = h[12]; // count of hypopnea events
|
|
ca = h[13]; // count of clear airway events
|
|
oa = h[14]; // count of obstructive events
|
|
|
|
// divide each event evenly over the 2 minute block
|
|
// TODO: revisit whether this is the right approach and should be done here? should the durations be hy or div?
|
|
if (hy > 0) {
|
|
div = block_duration / hy;
|
|
|
|
tt = t;
|
|
for (int i=0; i < hy; ++i) {
|
|
this->AddEvent(new PRS1HypopneaEvent(t, hy));
|
|
tt += div;
|
|
}
|
|
}
|
|
if (ca > 0) {
|
|
div = block_duration / ca;
|
|
|
|
tt = t;
|
|
|
|
for (int i=0; i < ca; ++i) {
|
|
this->AddEvent(new PRS1ClearAirwayEvent(tt, ca));
|
|
tt += div;
|
|
}
|
|
}
|
|
if (oa > 0) {
|
|
div = block_duration / oa;
|
|
|
|
tt = t;
|
|
for (int i=0; i < oa; ++i) {
|
|
this->AddEvent(new PRS1ObstructiveApneaEvent(t, oa));
|
|
tt += div;
|
|
}
|
|
}
|
|
|
|
h += 0x10;
|
|
t += block_duration;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
#if 0
|
|
// Currently unused, apparently an abandoned effort to massage F0 pressure/IPAP/EPAP data.
|
|
extern EventDataType CatmullRomSpline(EventDataType p0, EventDataType p1, EventDataType p2, EventDataType p3, EventDataType t = 0.5);
|
|
|
|
void SmoothEventList(Session * session, EventList * ev, ChannelID code)
|
|
{
|
|
if (!ev) return;
|
|
int cnt = ev->count();
|
|
if (cnt > 4) {
|
|
EventList * smooth = new EventList(EVL_Event, ev->gain());
|
|
|
|
smooth->setFirst(ev->first());
|
|
smooth->AddEvent(ev->time(0), ev->raw(0));
|
|
|
|
EventDataType p0, p1, p2, p3, v;
|
|
for (int i=1; i<cnt-2; ++i) {
|
|
qint64 time = ev->time(i);
|
|
qint64 time2 = ev->time(i+1);
|
|
qint64 diff = time2 - time;
|
|
|
|
// these aren't evenly spaced... spline won't work here.
|
|
p0 = ev->raw(i-1);
|
|
p1 = ev->raw(i);
|
|
p2 = ev->raw(i+1);
|
|
p3 = ev->raw(i+2);
|
|
|
|
smooth->AddEvent(time, p1);
|
|
|
|
// int df = p2-p1;
|
|
// if (df > 0) {
|
|
// qint64 inter = diff/(df+1);
|
|
// qint64 t = time+inter;
|
|
// for (int j=0; j<df; ++j) {
|
|
// smooth->AddEvent(t, p1+j);
|
|
// t+=inter;
|
|
// }
|
|
// } else if (df<0) {
|
|
// df = abs(df);
|
|
// qint64 inter = diff/(df+1);
|
|
// qint64 t = time+inter;
|
|
// for (int j=0; j<df; ++j) {
|
|
// smooth->AddEvent(t, p1-j);
|
|
// t+=inter;
|
|
// }
|
|
// }
|
|
// don't want to use Catmull here...
|
|
|
|
|
|
v = CatmullRomSpline(p0, p1, p2, p3, 0.25);
|
|
smooth->AddEvent(time+diff*0.25, v);
|
|
v = CatmullRomSpline(p0, p1, p2, p3, 0.5);
|
|
smooth->AddEvent(time+diff*0.5, v);
|
|
v = CatmullRomSpline(p0, p1, p2, p3, 0.75);
|
|
smooth->AddEvent(time+diff*0.75, v);
|
|
|
|
}
|
|
smooth->AddEvent(ev->time(cnt-2), ev->raw(cnt-2));
|
|
smooth->AddEvent(ev->time(cnt-1), ev->raw(cnt-1));
|
|
|
|
|
|
session->eventlist[code].removeAll(ev);
|
|
delete ev;
|
|
session->eventlist[code].append(smooth);
|
|
}
|
|
|
|
}
|
|
#endif
|
|
|
|
|
|
// 750P is F0V2; 550P is F0V2/F0V3; 450P is F0V3; 460P, 560P[BT], 660P, 760P are F0V4
|
|
// 200X, 400X, 400G, 500X, 502G, 600X, 700X are F0V6
|
|
bool PRS1Import::ParseF0Events()
|
|
{
|
|
// Required channels
|
|
EventList *OA = session->AddEventList(CPAP_Obstructive, EVL_Event);
|
|
EventList *HY = session->AddEventList(CPAP_Hypopnea, EVL_Event);
|
|
EventList *CA = session->AddEventList(CPAP_ClearAirway, EVL_Event);
|
|
|
|
EventList *TOTLEAK = session->AddEventList(CPAP_LeakTotal, EVL_Event);
|
|
EventList *LEAK = session->AddEventList(CPAP_Leak, EVL_Event);
|
|
EventList *PB = session->AddEventList(CPAP_PB, EVL_Event);
|
|
EventList *FL = session->AddEventList(CPAP_FlowLimit, EVL_Event);
|
|
EventList *SNORE = session->AddEventList(CPAP_Snore, EVL_Event);
|
|
EventList *VS = session->AddEventList(CPAP_VSnore, EVL_Event);
|
|
EventList *VS2 = session->AddEventList(CPAP_VSnore2, EVL_Event);
|
|
EventList *PP = session->AddEventList(CPAP_PressurePulse, EVL_Event);
|
|
EventList *RE = session->AddEventList(CPAP_RERA, EVL_Event);
|
|
|
|
|
|
// On-demand channels
|
|
ChannelID Codes[] = {
|
|
PRS1_00, PRS1_01, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
PRS1_0B, 0, 0, PRS1_0E
|
|
};
|
|
|
|
int ncodes = sizeof(Codes) / sizeof(ChannelID);
|
|
EventList *Code[0x20] = {0};
|
|
|
|
Code[0x0e] = session->AddEventList(PRS1_0E, EVL_Event);
|
|
EventList * LL = session->AddEventList(CPAP_LargeLeak, EVL_Event);
|
|
|
|
EventList *PRESSURE = nullptr;
|
|
EventList *EPAP = nullptr;
|
|
EventList *IPAP = nullptr;
|
|
EventList *PS = nullptr;
|
|
|
|
|
|
// Unintentional leak calculation, see zMaskProfile:calcLeak in calcs.cpp for explanation
|
|
EventDataType currentPressure=0, leak;
|
|
|
|
bool calcLeaks = p_profile->cpap->calculateUnintentionalLeaks();
|
|
EventDataType lpm4 = p_profile->cpap->custom4cmH2OLeaks();
|
|
EventDataType lpm20 = p_profile->cpap->custom20cmH2OLeaks();
|
|
|
|
EventDataType lpm = lpm20 - lpm4;
|
|
EventDataType ppm = lpm / 16.0;
|
|
|
|
CPAPMode mode = (CPAPMode) session->settings[CPAP_Mode].toInt();
|
|
|
|
qint64 t = qint64(event->timestamp) * 1000L;
|
|
session->updateFirst(t);
|
|
|
|
bool ok;
|
|
ok = event->ParseEvents(mode);
|
|
|
|
for (int i=0; i < event->m_parsedData.count(); i++) {
|
|
PRS1ParsedEvent* e = event->m_parsedData.at(i);
|
|
t = qint64(event->timestamp + e->m_start) * 1000L;
|
|
|
|
switch (e->m_type) {
|
|
case PRS1CPAPEvent::TYPE:
|
|
if (!PRESSURE) {
|
|
if (!(PRESSURE = session->AddEventList(CPAP_Pressure, EVL_Event, e->m_gain))) { return false; }
|
|
}
|
|
PRESSURE->AddEvent(t, e->m_value);
|
|
currentPressure = e->m_value;
|
|
break;
|
|
case PRS1IPAPEvent::TYPE:
|
|
if(!IPAP) {
|
|
if (!(IPAP = session->AddEventList(CPAP_IPAP, EVL_Event, e->m_gain))) { return false; }
|
|
}
|
|
IPAP->AddEvent(t, e->m_value);
|
|
currentPressure = e->m_value;
|
|
break;
|
|
case PRS1EPAPEvent::TYPE:
|
|
if (!EPAP) {
|
|
if (!(EPAP = session->AddEventList(CPAP_EPAP, EVL_Event, e->m_gain))) { return false; }
|
|
}
|
|
if(!PS) {
|
|
if (!(PS = session->AddEventList(CPAP_PS, EVL_Event, e->m_gain))) { return false; }
|
|
}
|
|
EPAP->AddEvent(t, e->m_value);
|
|
PS->AddEvent(t, currentPressure - e->m_value); // Pressure Support
|
|
break;
|
|
case PRS1PressureReliefEvent::TYPE:
|
|
if (!EPAP) {
|
|
if (!(EPAP = session->AddEventList(CPAP_EPAP, EVL_Event, e->m_gain))) { return false; }
|
|
}
|
|
EPAP->AddEvent(t, e->m_value);
|
|
break;
|
|
case PRS1ObstructiveApneaEvent::TYPE:
|
|
OA->AddEvent(t, e->m_duration);
|
|
break;
|
|
case PRS1ClearAirwayEvent::TYPE:
|
|
CA->AddEvent(t, e->m_duration);
|
|
break;
|
|
case PRS1HypopneaEvent::TYPE:
|
|
HY->AddEvent(t, e->m_duration);
|
|
break;
|
|
case PRS1FlowLimitationEvent::TYPE:
|
|
FL->AddEvent(t, e->m_duration);
|
|
break;
|
|
case PRS1PeriodicBreathingEvent::TYPE:
|
|
PB->AddEvent(t, e->m_duration);
|
|
break;
|
|
case PRS1LargeLeakEvent::TYPE:
|
|
LL->AddEvent(t, e->m_duration);
|
|
break;
|
|
case PRS1TotalLeakEvent::TYPE:
|
|
TOTLEAK->AddEvent(t, e->m_value);
|
|
leak = e->m_value;
|
|
if (calcLeaks) { // Much Quicker doing this here than the recalc method.
|
|
leak -= (((currentPressure/10.0f) - 4.0) * ppm + lpm4);
|
|
if (leak < 0) leak = 0;
|
|
LEAK->AddEvent(t, leak);
|
|
}
|
|
break;
|
|
case PRS1SnoreEvent::TYPE:
|
|
SNORE->AddEvent(t, e->m_value);
|
|
if (e->m_value > 0) {
|
|
VS2->AddEvent(t, e->m_value);
|
|
}
|
|
break;
|
|
case PRS1VibratorySnoreEvent::TYPE: // F0: Is this really distinct from SNORE and VS2?
|
|
VS->AddEvent(t, 0);
|
|
break;
|
|
case PRS1RERAEvent::TYPE:
|
|
RE->AddEvent(t, e->m_value);
|
|
break;
|
|
case PRS1PressurePulseEvent::TYPE:
|
|
PP->AddEvent(t, e->m_value);
|
|
break;
|
|
case PRS1UnknownValueEvent::TYPE:
|
|
{
|
|
int code = ((PRS1UnknownValueEvent*) e)->m_code;
|
|
Q_ASSERT(code < ncodes);
|
|
if (!Code[code]) {
|
|
ChannelID cpapcode = Codes[(int)code];
|
|
Q_ASSERT(cpapcode); // any unknown codes returned by chunk parser should be given a channel above
|
|
if (!(Code[code] = session->AddEventList(cpapcode, EVL_Event, e->m_gain))) { return false; }
|
|
}
|
|
Code[code]->AddEvent(t, e->m_value);
|
|
break;
|
|
}
|
|
default:
|
|
qWarning() << "Unknown PRS1 event type" << (int) e->m_type;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!ok) {
|
|
return false;
|
|
}
|
|
|
|
t = qint64(event->timestamp + event->duration) * 1000L;
|
|
session->updateLast(t);
|
|
session->m_cnt.clear();
|
|
session->m_cph.clear();
|
|
|
|
session->m_lastchan.clear();
|
|
session->m_firstchan.clear();
|
|
session->m_valuesummary[CPAP_Pressure].clear();
|
|
session->m_valuesummary.erase(session->m_valuesummary.find(CPAP_Pressure));
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool PRS1DataChunk::ParseEventsF0(CPAPMode mode)
|
|
{
|
|
unsigned char code=0;
|
|
|
|
EventDataType data0, data1, data2;
|
|
Q_UNUSED(data2)
|
|
int cnt = 0;
|
|
short delta;
|
|
int pos;
|
|
int t = 0;
|
|
|
|
unsigned char lastcode3 = 0, lastcode2 = 0, lastcode = 0;
|
|
int lastpos = 0, startpos = 0, lastpos2 = 0, lastpos3 = 0;
|
|
|
|
int size = this->m_data.size();
|
|
|
|
bool FV3 = (this->fileVersion == 3);
|
|
unsigned char * buffer = (unsigned char *)this->m_data.data();
|
|
|
|
for (pos = 0; pos < size;) {
|
|
lastcode3 = lastcode2;
|
|
lastcode2 = lastcode;
|
|
lastcode = code;
|
|
lastpos3 = lastpos2;
|
|
lastpos2 = lastpos;
|
|
lastpos = startpos;
|
|
startpos = pos;
|
|
code = buffer[pos++];
|
|
|
|
if (code > 0x15) {
|
|
qDebug() << "Illegal PRS1 code " << hex << int(code) << " appeared at " << hex << startpos << "in" << this->sessionid;
|
|
qDebug() << "1: (" << hex << int(lastcode) << hex << lastpos << ")";
|
|
qDebug() << "2: (" << hex << int(lastcode2) << hex << lastpos2 << ")";
|
|
qDebug() << "3: (" << hex << int(lastcode3) << hex << lastpos3 << ")";
|
|
this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos));
|
|
return false;
|
|
}
|
|
|
|
if (code != 0x12) {
|
|
delta = buffer[pos + 1] << 8 | buffer[pos];
|
|
pos += 2;
|
|
|
|
t += delta;
|
|
}
|
|
|
|
cnt++;
|
|
|
|
switch (code) {
|
|
|
|
case 0x00: // Unknown 00
|
|
this->AddEvent(new PRS1UnknownValueEvent(code, t, buffer[pos++]));
|
|
if (((this->family == 0) && (this->familyVersion >= 4)) || (this->fileVersion == 3)){
|
|
pos++;
|
|
}
|
|
break;
|
|
|
|
case 0x01: // Unknown
|
|
if ((this->family == 0) && (this->familyVersion >= 4)) {
|
|
this->AddEvent(new PRS1CPAPEvent(t, buffer[pos++]));
|
|
} else {
|
|
this->AddEvent(new PRS1UnknownValueEvent(code, t, 0));
|
|
}
|
|
break;
|
|
|
|
case 0x02: // Pressure
|
|
if ((this->family == 0) && (this->familyVersion >= 4)) { // BiPAP Pressure
|
|
data0 = buffer[pos++];
|
|
data1 = buffer[pos++];
|
|
this->AddEvent(new PRS1IPAPEvent(t, data1));
|
|
this->AddEvent(new PRS1EPAPEvent(t, data0)); // EPAP needs to be added second to calculate PS
|
|
} else {
|
|
this->AddEvent(new PRS1CPAPEvent(t, buffer[pos++]));
|
|
}
|
|
break;
|
|
|
|
case 0x03: // BIPAP Pressure
|
|
if (FV3) {
|
|
this->AddEvent(new PRS1CPAPEvent(t, buffer[pos++]));
|
|
} else {
|
|
data0 = buffer[pos++];
|
|
data1 = buffer[pos++];
|
|
this->AddEvent(new PRS1IPAPEvent(t, data1));
|
|
this->AddEvent(new PRS1EPAPEvent(t, data0)); // EPAP needs to be added second to calculate PS
|
|
}
|
|
break;
|
|
|
|
case 0x04: // Pressure Pulse
|
|
data0 = buffer[pos++];
|
|
this->AddEvent(new PRS1PressurePulseEvent(t, data0));
|
|
break;
|
|
|
|
case 0x05: // RERA
|
|
data0 = buffer[pos++];
|
|
this->AddEvent(new PRS1RERAEvent(t - data0, data0));
|
|
break;
|
|
|
|
case 0x06: // Obstructive Apoanea
|
|
data0 = buffer[pos++];
|
|
this->AddEvent(new PRS1ObstructiveApneaEvent(t - data0, data0));
|
|
break;
|
|
|
|
case 0x07: // Clear Airway
|
|
data0 = buffer[pos++];
|
|
this->AddEvent(new PRS1ClearAirwayEvent(t - data0, data0));
|
|
break;
|
|
|
|
case 0x0a: // Hypopnea
|
|
data0 = buffer[pos++];
|
|
this->AddEvent(new PRS1HypopneaEvent(t - data0, data0));
|
|
break;
|
|
|
|
case 0x0c: // Flow Limitation
|
|
data0 = buffer[pos++];
|
|
this->AddEvent(new PRS1FlowLimitationEvent(t - data0, data0));
|
|
break;
|
|
|
|
case 0x0b: // Breathing not Detected flag???? but it doesn't line up
|
|
data0 = buffer[pos];
|
|
data1 = buffer[pos+1];
|
|
pos += 2;
|
|
|
|
if (this->familyVersion >= 4) {
|
|
// might not doublerize on older machines?
|
|
// data0 *= 2;
|
|
}
|
|
// data1 = buffer[pos++];
|
|
|
|
//tt = t - qint64((data0+data1)*2) * 1000L;
|
|
|
|
this->AddEvent(new PRS1UnknownValueEvent(code, t, data0)); // FIXME
|
|
break;
|
|
|
|
case 0x0d: // Vibratory Snore
|
|
this->AddEvent(new PRS1VibratorySnoreEvent(t, 0));
|
|
break;
|
|
|
|
case 0x0e: // Unknown
|
|
data0 = buffer[pos + 1] << 8 | buffer[pos];
|
|
if (this->familyVersion >= 4) {
|
|
// might not doublerize on older machines?
|
|
data0 *= 2;
|
|
}
|
|
|
|
pos += 2;
|
|
data1 = buffer[pos++];
|
|
this->AddEvent(new PRS1UnknownValueEvent(code, t - data1, data0)); // TODO: start time should probably match PB below
|
|
break;
|
|
|
|
case 0x0f: // Cheyne Stokes Respiration
|
|
data0 = (buffer[pos + 1] << 8 | buffer[pos]);
|
|
if (this->familyVersion >= 4) {
|
|
// might not doublerize on older machines
|
|
data0 *= 2;
|
|
}
|
|
pos += 2;
|
|
data1 = buffer[pos++];
|
|
if (this->familyVersion == 2 || this->familyVersion == 3) {
|
|
this->AddEvent(new PRS1PeriodicBreathingEvent(t - data1 - data0, data0)); // PB event appears data1 seconds after conclusion
|
|
} else {
|
|
this->AddEvent(new PRS1PeriodicBreathingEvent(t - data1, data0)); // TODO: this should probably be the same as F0V23, but it hasn't been tested
|
|
}
|
|
break;
|
|
|
|
case 0x10: // Large Leak
|
|
data0 = buffer[pos + 1] << 8 | buffer[pos];
|
|
if (this->familyVersion >= 4) {
|
|
// might not doublerize on older machines
|
|
data0 *= 2;
|
|
}
|
|
pos += 2;
|
|
data1 = buffer[pos++];
|
|
this->AddEvent(new PRS1LargeLeakEvent(t - data1, data0)); // TODO: start time should probably match PB above
|
|
break;
|
|
|
|
case 0x11: // Leak Rate & Snore Graphs
|
|
data0 = buffer[pos++];
|
|
data1 = buffer[pos++];
|
|
this->AddEvent(new PRS1TotalLeakEvent(t, data0));
|
|
this->AddEvent(new PRS1SnoreEvent(t, data1));
|
|
|
|
if ((this->family == 0) && (this->familyVersion >= 4)) {
|
|
// EPAP / Flex Pressure
|
|
data0 = buffer[pos++];
|
|
|
|
// Perhaps this check is not necessary, as it will theoretically add extra resolution to pressure chart
|
|
// for bipap models and above???
|
|
if (mode <= MODE_BILEVEL_FIXED) {
|
|
this->AddEvent(new PRS1PressureReliefEvent(t, data0));
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 0x12: // Summary
|
|
data0 = buffer[pos++];
|
|
data1 = buffer[pos++];
|
|
data2 = buffer[pos + 1] << 8 | buffer[pos];
|
|
pos += 2;
|
|
|
|
// Could end here, but I've seen data sets valid data after!!!
|
|
|
|
break;
|
|
|
|
case 0x14: // DreamStation Hypopnea
|
|
data0 = buffer[pos++];
|
|
this->AddEvent(new PRS1HypopneaEvent(t - data0, data0));
|
|
break;
|
|
|
|
case 0x15: // DreamStation Hypopnea
|
|
data0 = buffer[pos++];
|
|
this->AddEvent(new PRS1HypopneaEvent(t - data0, data0));
|
|
break;
|
|
|
|
default:
|
|
// ERROR!!!
|
|
qWarning() << "Some new fandangled PRS1 code detected in" << this->sessionid << hex
|
|
<< int(code) << " at " << pos - 1;
|
|
this->AddEvent(new PRS1UnknownDataEvent(m_data, startpos));
|
|
return false;
|
|
}
|
|
}
|
|
this->duration = t;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool PRS1Import::ImportCompliance()
|
|
{
|
|
bool ok;
|
|
ok = compliance->ParseCompliance();
|
|
qint64 start = qint64(compliance->timestamp) * 1000L;
|
|
|
|
for (int i=0; i < compliance->m_parsedData.count(); i++) {
|
|
PRS1ParsedEvent* e = compliance->m_parsedData.at(i);
|
|
if (e->m_type == PRS1ParsedSliceEvent::TYPE) {
|
|
PRS1ParsedSliceEvent* s = (PRS1ParsedSliceEvent*) e;
|
|
qint64 tt = start + qint64(s->m_start) * 1000L;
|
|
if (!session->m_slices.isEmpty()) {
|
|
SessionSlice & prevSlice = session->m_slices.last();
|
|
prevSlice.end = tt;
|
|
}
|
|
session->m_slices.append(SessionSlice(tt, tt, (SliceStatus) s->m_value));
|
|
continue;
|
|
} else if (e->m_type != PRS1ParsedSettingEvent::TYPE) {
|
|
qWarning() << "Compliance had non-setting event:" << (int) e->m_type;
|
|
continue;
|
|
}
|
|
PRS1ParsedSettingEvent* s = (PRS1ParsedSettingEvent*) e;
|
|
switch (s->m_setting) {
|
|
case PRS1_SETTING_CPAP_MODE:
|
|
session->settings[CPAP_Mode] = e->m_value;
|
|
break;
|
|
case PRS1_SETTING_PRESSURE:
|
|
session->settings[CPAP_Pressure] = e->value();
|
|
break;
|
|
case PRS1_SETTING_FLEX_MODE:
|
|
session->settings[PRS1_FlexMode] = e->m_value;
|
|
break;
|
|
case PRS1_SETTING_FLEX_LEVEL:
|
|
session->settings[PRS1_FlexLevel] = e->m_value;
|
|
break;
|
|
case PRS1_SETTING_RAMP_TIME:
|
|
session->settings[CPAP_RampTime] = e->m_value;
|
|
break;
|
|
case PRS1_SETTING_RAMP_PRESSURE:
|
|
session->settings[CPAP_RampPressure] = e->value();
|
|
break;
|
|
case PRS1_SETTING_HUMID_STATUS:
|
|
session->settings[PRS1_HumidStatus] = (bool) e->m_value;
|
|
break;
|
|
case PRS1_SETTING_HUMID_LEVEL:
|
|
session->settings[PRS1_HumidLevel] = e->m_value;
|
|
break;
|
|
default:
|
|
qWarning() << "Unknown PRS1 setting type" << (int) s->m_setting;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!ok) {
|
|
return false;
|
|
}
|
|
if (compliance->duration == 0) {
|
|
// This does occasionally happen and merely indicates a brief session with no useful data.
|
|
//qDebug() << compliance->sessionid << "duration == 0";
|
|
return false;
|
|
}
|
|
session->setSummaryOnly(true);
|
|
session->set_first(start);
|
|
session->set_last(qint64(compliance->timestamp + compliance->duration) * 1000L);
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
// TODO: have UNEXPECTED_VALUE set a flag in the importer/machine that this data set is unusual
|
|
#define UNEXPECTED_VALUE(SRC, VALS) { qWarning() << this->sessionid << QString("%1: %2 = %3 != %4").arg(__func__).arg(#SRC).arg(SRC).arg(VALS); }
|
|
#define CHECK_VALUE(SRC, VAL) if ((SRC) != (VAL)) UNEXPECTED_VALUE(SRC, VAL)
|
|
#define CHECK_VALUES(SRC, VAL1, VAL2) if ((SRC) != (VAL1) && (SRC) != (VAL2)) UNEXPECTED_VALUE(SRC, #VAL1 " or " #VAL2)
|
|
// for more than 2 values, just write the test manually and use UNEXPECTED_VALUE if it fails
|
|
|
|
bool PRS1DataChunk::ParseCompliance(void)
|
|
{
|
|
switch (this->family) {
|
|
case 0:
|
|
if (this->familyVersion == 6) {
|
|
return this->ParseComplianceF0V6();
|
|
} else if (this->familyVersion == 2 || this->familyVersion == 3) {
|
|
return this->ParseComplianceF0V23();
|
|
}
|
|
default:
|
|
;
|
|
}
|
|
|
|
qWarning() << "unexpected family" << this->family << "familyVersion" << this->familyVersion;
|
|
return false;
|
|
}
|
|
|
|
|
|
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;
|
|
}
|
|
// F0V3 is untested, but since summary and events seem to be the same for F0V2 and F0V3,
|
|
// we'll assume this one is for now, but flag it as unexpected.
|
|
CHECK_VALUE(this->familyVersion, 2);
|
|
|
|
// TODO: hardcoding this is ugly, think of a better approach
|
|
if (this->m_data.size() < 0x13) {
|
|
qWarning() << this->sessionid << "compliance data too short:" << this->m_data.size();
|
|
return false;
|
|
}
|
|
const unsigned char * data = (unsigned char *)this->m_data.constData();
|
|
|
|
CHECK_VALUE(data[0x00], 0);
|
|
if (data[0x00] != 0) {
|
|
if (data[0x00] != 5) {
|
|
qDebug() << this->sessionid << "compliance first byte" << data[0x00] <<" != 0, skipping";
|
|
}
|
|
return false;
|
|
}
|
|
CHECK_VALUES(data[0x01], 1, 0); // usually 1, occasionally 0, no visible difference in report
|
|
CHECK_VALUE(data[0x02], 0);
|
|
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_CPAP_MODE, (int) MODE_CPAP));
|
|
|
|
int min_pressure = data[0x03];
|
|
// EventDataType max_pressure = EventDataType(data[0x04]) / 10.0;
|
|
CHECK_VALUE(data[0x04], 0);
|
|
CHECK_VALUE(data[0x05], 0);
|
|
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PRESSURE, min_pressure));
|
|
|
|
int ramp_time = data[0x06];
|
|
int ramp_pressure = data[0x07];
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_RAMP_TIME, ramp_time));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_RAMP_PRESSURE, ramp_pressure));
|
|
|
|
quint8 flex = data[0x08]; // TODO: why was this 0x09 originally? could the position vary?
|
|
this->ParseFlexSetting(flex, MODE_CPAP);
|
|
|
|
int humid = data[0x09]; // TODO: why was this 0x0A originally? could the position vary?
|
|
this->ParseHumidifierSettingV2(humid, false);
|
|
|
|
// TODO: Where is Auto Off/On set? (both off)
|
|
// TODO: Where is "Altitude Compensation" set? (seems to be 1)
|
|
// TODO: Where are Mask Alert/Reminder Period set? (both off)
|
|
CHECK_VALUE(data[0x0a], 0x80);
|
|
CHECK_VALUE(data[0x0b], 1);
|
|
CHECK_VALUE(data[0x0c], 0);
|
|
CHECK_VALUE(data[0x0d], 0);
|
|
|
|
// List of slices, really session-related events:
|
|
int start = 0;
|
|
int tt = start;
|
|
|
|
int len = this->size()-3;
|
|
int pos = 0x0e;
|
|
do {
|
|
quint8 c = data[pos++];
|
|
// These aren't really slices as originally thought, they're events with a delta offset.
|
|
// We'll convert them to slices in the importer.
|
|
int delta = data[pos] | data[pos+1] << 8;
|
|
pos+=2;
|
|
SliceStatus status;
|
|
if (c == 0x02) {
|
|
status = MaskOn;
|
|
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?
|
|
}
|
|
} else if (c == 0x03) {
|
|
status = MaskOff;
|
|
} else if (c == 0x01) {
|
|
status = EquipmentOff;
|
|
// This has a delta if the mask was removed before the machine was shut off.
|
|
} else {
|
|
qDebug() << this->sessionid << "unknown slice status" << c;
|
|
break;
|
|
}
|
|
tt += delta;
|
|
this->AddEvent(new PRS1ParsedSliceEvent(tt, status));
|
|
} while (pos < len);
|
|
|
|
// also seems to be a trailing 01 00 81 after the slices?
|
|
if (pos == len) {
|
|
CHECK_VALUES(data[pos], 1, 0); // usually 1, occasionally 0, no visible difference in report
|
|
//CHECK_VALUE(data[pos+1], 0); // sometimes 1, 2, or 5, no visible difference in report
|
|
//CHECK_VALUES(data[pos+2], 0x81, 0x80); // seems to be humidifier setting at end of session
|
|
if (data[pos+2] && (((data[pos+2] & 0x80) == 0) || (data[pos+2] & 0x07) > 5)) {
|
|
UNEXPECTED_VALUE(data[pos+2], "valid humidifier setting");
|
|
}
|
|
} else {
|
|
qWarning() << this->sessionid << (this->size() - pos) << "trailing bytes";
|
|
}
|
|
|
|
this->duration = tt;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
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;
|
|
}
|
|
// TODO: hardcoding this is ugly, think of a better approach
|
|
if (this->m_data.size() < 59) {
|
|
qWarning() << this->sessionid << "summary data too short:" << this->m_data.size();
|
|
return false;
|
|
}
|
|
const unsigned char * data = (unsigned char *)this->m_data.constData();
|
|
|
|
CHECK_VALUE(data[0x00], 0);
|
|
if (data[0x00] != 0) {
|
|
if (data[0x00] != 5) {
|
|
qDebug() << this->sessionid << "summary first byte" << data[0x00] <<" != 0, skipping";
|
|
}
|
|
return false;
|
|
}
|
|
CHECK_VALUES(data[0x01] & 0xF0, 0x60, 0x70); // TODO: what are these?
|
|
if ((data[0x01] & 0x0F) != 1) { // This is the most frequent value.
|
|
CHECK_VALUES(data[0x01] & 0x0F, 3, 0); // TODO: what are these? 0 seems to be related to errors.
|
|
}
|
|
|
|
CPAPMode cpapmode = MODE_UNKNOWN;
|
|
|
|
switch (data[0x02]) { // PRS1 mode // 0 = CPAP, 2 = APAP
|
|
case 0x00:
|
|
cpapmode = MODE_CPAP;
|
|
break;
|
|
case 0x01:
|
|
cpapmode = MODE_BILEVEL_FIXED;
|
|
break;
|
|
case 0x02:
|
|
cpapmode = MODE_APAP;
|
|
break;
|
|
case 0x03:
|
|
cpapmode = MODE_BILEVEL_AUTO_VARIABLE_PS;
|
|
break;
|
|
default:
|
|
qWarning() << this->sessionid << "unknown cpap mode" << data[0x02];
|
|
return false;
|
|
}
|
|
|
|
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 == MODE_CPAP) {
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PRESSURE, min_pressure));
|
|
CHECK_VALUE(max_pressure, 0);
|
|
CHECK_VALUE(ps, 0);
|
|
} else if (cpapmode == MODE_APAP) {
|
|
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 == MODE_BILEVEL_FIXED) {
|
|
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 == MODE_BILEVEL_AUTO_VARIABLE_PS) {
|
|
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];
|
|
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->ParseFlexSetting(flex, cpapmode);
|
|
|
|
int humid = data[0x09];
|
|
this->ParseHumidifierSettingV2(humid, false);
|
|
|
|
// Tubing lock has no setting byte
|
|
|
|
// Menu Options
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_SYSTEMONE_RESIST_LOCK, (data[0x0a] & 0x80) != 0)); // System One Resistance Lock Setting
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_SYSTEMONE_RESIST_STATUS, (data[0x0a] & 0x40) != 0)); // System One Resistance Status bit
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HOSE_DIAMETER, (data[0x0a] & 0x08) ? 15 : 22)); // TODO: unconfirmed
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_SYSTEMONE_RESIST_SETTING, data[0x0a] & 7)); // System One Resistance setting value
|
|
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);
|
|
//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 == MODE_BILEVEL_FIXED) { // initial IPAP for bilevel modes
|
|
CHECK_VALUE(data[0x0f], max_pressure);
|
|
} else if (cpapmode == MODE_BILEVEL_AUTO_VARIABLE_PS) {
|
|
CHECK_VALUE(data[0x0f], min_pressure + 20);
|
|
}
|
|
|
|
// List of slices, really session-related events:
|
|
int start = 0;
|
|
int tt = start;
|
|
|
|
int len = this->size()-3;
|
|
int pos = 0x10;
|
|
do {
|
|
quint8 c = data[pos++];
|
|
int delta = data[pos] | data[pos+1] << 8;
|
|
pos+=2;
|
|
SliceStatus status;
|
|
if (c == 0x02) {
|
|
status = MaskOn;
|
|
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?
|
|
}
|
|
} else if (c == 0x03) {
|
|
status = MaskOff;
|
|
// These are 0x22 bytes in a summary vs. 3 bytes in compliance data
|
|
// TODO: What are these values?
|
|
pos += 0x1F;
|
|
} else if (c == 0x01) {
|
|
status = EquipmentOff;
|
|
// This has a delta if the mask was removed before the machine was shut off.
|
|
} else {
|
|
qDebug() << this->sessionid << "unknown slice status" << c;
|
|
break;
|
|
}
|
|
tt += delta;
|
|
this->AddEvent(new PRS1ParsedSliceEvent(tt, status));
|
|
} while (pos < len);
|
|
|
|
// seems to be trailing 01 [01 or 02] 83 after the slices?
|
|
if (pos == len) {
|
|
if (data[pos] != 1) { // This is the usual value.
|
|
CHECK_VALUES(data[pos], 0, 3); // 0 seems to be related to errors, 3 seen after 90 sec large leak before turning off?
|
|
}
|
|
//CHECK_VALUES(data[pos+1], 0, 1); // TODO: may be related to ramp? 1-5 seems to have a ramp start or two
|
|
//CHECK_VALUES(data[pos+2], 0x81, 0x80); // seems to be humidifier setting at end of session
|
|
if (data[pos+2] && (((data[pos+2] & 0x80) == 0) || (data[pos+2] & 0x07) > 5)) {
|
|
UNEXPECTED_VALUE(data[pos+2], "valid humidifier setting");
|
|
}
|
|
} else {
|
|
qWarning() << this->sessionid << (this->size() - pos) << "trailing bytes";
|
|
}
|
|
|
|
this->duration = tt;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool PRS1DataChunk::ParseSummaryF0V4(void)
|
|
{
|
|
const unsigned char * data = (unsigned char *)this->m_data.constData();
|
|
|
|
CPAPMode cpapmode = MODE_UNKNOWN;
|
|
|
|
switch (data[0x02]) { // PRS1 mode // 0 = CPAP, 2 = APAP
|
|
case 0x00:
|
|
cpapmode = MODE_CPAP;
|
|
break;
|
|
case 0x20:
|
|
cpapmode = MODE_BILEVEL_FIXED;
|
|
break;
|
|
case 0x40:
|
|
cpapmode = MODE_APAP;
|
|
break;
|
|
case 0x60:
|
|
cpapmode = MODE_BILEVEL_AUTO_VARIABLE_PS;
|
|
}
|
|
|
|
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 == MODE_CPAP) {
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PRESSURE, min_pressure));
|
|
} else if (cpapmode == MODE_APAP) {
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PRESSURE_MIN, min_pressure));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PRESSURE_MAX, max_pressure));
|
|
} else if (cpapmode == MODE_BILEVEL_FIXED) {
|
|
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));
|
|
} else if (cpapmode == MODE_BILEVEL_AUTO_VARIABLE_PS) {
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_EPAP_MIN, min_pressure));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_EPAP_MAX, max_pressure));
|
|
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));
|
|
}
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_CPAP_MODE, (int) cpapmode));
|
|
|
|
quint8 flex = data[0x0a];
|
|
this->ParseFlexSetting(flex, cpapmode);
|
|
|
|
int ramp_time = data[0x08];
|
|
int ramp_pressure = data[0x09];
|
|
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_RAMP_TIME, ramp_time));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_RAMP_PRESSURE, ramp_pressure));
|
|
|
|
int humid = data[0x0b];
|
|
this->ParseHumidifierSettingV2(humid);
|
|
|
|
this->duration = data[0x14] | data[0x15] << 8;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
// TODO: This is probably only F3V6, as it uses mainblock, only present in fileVersion 3.
|
|
bool PRS1DataChunk::ParseSummaryF3(void)
|
|
{
|
|
CPAPMode mode = MODE_UNKNOWN;
|
|
EventDataType epap, ipap;
|
|
|
|
QMap<unsigned char, QByteArray>::iterator it;
|
|
|
|
if ((it=this->mainblock.find(0x0a)) != this->mainblock.end()) {
|
|
mode = MODE_CPAP;
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PRESSURE, it.value()[0]));
|
|
} else if ((it=this->mainblock.find(0x0d)) != this->mainblock.end()) {
|
|
mode = MODE_APAP;
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PRESSURE_MIN, it.value()[0]));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PRESSURE_MAX, it.value()[1]));
|
|
} else if ((it=this->mainblock.find(0x0e)) != this->mainblock.end()) {
|
|
mode = MODE_BILEVEL_FIXED;
|
|
ipap = it.value()[0];
|
|
epap = it.value()[1];
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP, ipap));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_EPAP, epap));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS, ipap - epap));
|
|
} else if ((it=this->mainblock.find(0x0f)) != this->mainblock.end()) {
|
|
mode = MODE_BILEVEL_AUTO_VARIABLE_PS;
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_EPAP_MIN, it.value()[0]));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP_MAX, it.value()[1]));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS_MIN, it.value()[2]));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS_MAX, it.value()[3]));
|
|
} else if ((it=this->mainblock.find(0x10)) != this->mainblock.end()) {
|
|
mode = MODE_APAP; // Disgusting APAP "IQ" trial
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PRESSURE_MIN, it.value()[0]));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PRESSURE_MAX, it.value()[1]));
|
|
}
|
|
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_CPAP_MODE, (int) mode));
|
|
|
|
if ((it=this->hbdata.find(5)) != this->hbdata.end()) {
|
|
this->duration = (it.value()[1] << 8 ) + it.value()[0];
|
|
} else {
|
|
qWarning() << this->sessionid << "missing summary duration";
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool PRS1DataChunk::ParseSummaryF5V012(void)
|
|
{
|
|
const unsigned char * data = (unsigned char *)this->m_data.constData();
|
|
|
|
CPAPMode cpapmode = MODE_UNKNOWN;
|
|
|
|
int imin_epap = data[0x3];
|
|
int imax_epap = data[0x4];
|
|
int imin_ps = data[0x5];
|
|
int imax_ps = data[0x6];
|
|
int imax_pressure = data[0x2];
|
|
|
|
cpapmode = MODE_ASV_VARIABLE_EPAP;
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_CPAP_MODE, (int) cpapmode));
|
|
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_EPAP_MIN, imin_epap));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_EPAP_MAX, imax_epap));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP_MIN, imin_epap + imin_ps));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP_MAX, imax_pressure));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS_MIN, imin_ps));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS_MAX, imax_ps));
|
|
|
|
quint8 flex = data[0x0c];
|
|
this->ParseFlexSetting(flex, cpapmode);
|
|
|
|
int ramp_time = data[0x0a];
|
|
int ramp_pressure = data[0x0b];
|
|
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_RAMP_TIME, ramp_time));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_RAMP_PRESSURE, ramp_pressure));
|
|
|
|
int humid = data[0x0d];
|
|
this->ParseHumidifierSettingV2(humid);
|
|
|
|
this->duration = data[0x18] | data[0x19] << 8;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
void PRS1DataChunk::ParseFlexSetting(quint8 flex, CPAPMode cpapmode)
|
|
{
|
|
int flexlevel = flex & 0x03;
|
|
FlexMode flexmode = FLEX_Unknown;
|
|
|
|
// 88 CFlex+ / AFlex (depending on CPAP mode)
|
|
// 80 CFlex
|
|
// 00 NoFlex
|
|
// c0 Split CFlex then None
|
|
// c8 Split CFlex+ then None
|
|
|
|
if (flex & (0x20 | 0x04)) UNEXPECTED_VALUE(flex, "known bits");
|
|
|
|
flex &= 0xf8;
|
|
bool split = false;
|
|
|
|
if (flex & 0x40) { // This bit defines the Flex setting for the CPAP component of the Split night
|
|
split = true;
|
|
}
|
|
if (flex & 0x80) { // CFlex bit
|
|
if (flex & 0x10) {
|
|
flexmode = FLEX_RiseTime;
|
|
} else if (flex & 8) { // Plus bit
|
|
if (split || (cpapmode == MODE_CPAP)) {
|
|
flexmode = FLEX_CFlexPlus;
|
|
} else if (cpapmode == MODE_APAP) {
|
|
flexmode = FLEX_AFlex;
|
|
}
|
|
} else {
|
|
// CFlex bits refer to Rise Time on BiLevel machines
|
|
flexmode = (cpapmode >= MODE_BILEVEL_FIXED) ? FLEX_BiFlex : FLEX_CFlex;
|
|
}
|
|
} else flexmode = FLEX_None;
|
|
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_MODE, (int) flexmode));
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_LEVEL, flexlevel));
|
|
}
|
|
|
|
|
|
void PRS1DataChunk::ParseHumidifierSettingV2(int humid, bool supportsHeatedTubing)
|
|
{
|
|
if (humid & (0x40 | 0x08)) UNEXPECTED_VALUE(humid, "known bits");
|
|
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HUMID_STATUS, (humid & 0x80) != 0)); // Humidifier Connected
|
|
if (supportsHeatedTubing) {
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HEATED_TUBING, (humid & 0x10) != 0)); // Heated Hose??
|
|
// TODO: 0x20 is seen on machines with System One humidification & heated tubing, not sure which setting it represents.
|
|
} else {
|
|
CHECK_VALUE(humid & 0x30, 0);
|
|
}
|
|
int humidlevel = humid & 7;
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HUMID_LEVEL, humidlevel)); // Humidifier Value
|
|
|
|
if (humidlevel > 5) UNEXPECTED_VALUE(humidlevel, "<= 5");
|
|
}
|
|
|
|
|
|
// 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;
|
|
do {
|
|
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]) {
|
|
qWarning() << this->sessionid << "slice" << code << "too small" << size << "<" << expected_sizes[code];
|
|
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;
|
|
} while (ok && pos < chunk_size);
|
|
|
|
this->duration = tt;
|
|
|
|
return ok;
|
|
}
|
|
|
|
|
|
// It turns out this is used by F5V3 in addition to F0V6, so it's likely common to all fileVersion 3 machines.
|
|
void PRS1DataChunk::ParseHumidifierSettingV3(unsigned char byte1, unsigned char byte2, bool add_setting)
|
|
{
|
|
// Byte 1: 0x90 (no humidifier data), 0x50 (15ht, tube 4/5, humid 4), 0x54 (15ht, tube 5, humid 5) 0x4c (15ht, tube temp 3, humidifier 3)
|
|
// 0x0c (15, tube 3, humid 3, fixed)
|
|
// 0b10010000 no humidifier data
|
|
// 0b01010000 tube 4 and 5, humidifier 4
|
|
// 0b01010100 15ht, tube 5, humidifier 5
|
|
// 0b01001100 15ht, tube 3, humidifier 3
|
|
// xxx = humidifier setting
|
|
// xxx = humidifier status
|
|
// ??
|
|
CHECK_VALUE(byte1 & 3, 0);
|
|
int humid = byte1 >> 5;
|
|
switch (humid) {
|
|
case 0: break; // fixed
|
|
case 1: break; // adaptive
|
|
case 2: break; // heated tube
|
|
case 4: break; // no humidifier, possibly a bit flag rather than integer value
|
|
default:
|
|
UNEXPECTED_VALUE(humid, "known value");
|
|
break;
|
|
}
|
|
bool humidifier_present = ((byte1 & 0x80) == 0);
|
|
int humidlevel = (byte1 >> 2) & 7;
|
|
|
|
// Byte 2: 0xB4 (15ht, tube 5, humid 5), 0xB0 (15ht, tube 5, humid 4), 0x90 (tube 4, humid 4), 0x6C (15ht, tube temp 3, humidifier 3)
|
|
// 0x80?
|
|
// 0b10110100 15ht, tube 5, humidifier 5
|
|
// 0b10110000 15ht, tube 5, humidifier 4
|
|
// 0b10010000 tube 4, humidifier 4
|
|
// 0b01101100 15ht, tube 3, humidifier 3
|
|
// xxx = humidifier setting
|
|
// xxx = tube setting
|
|
// ??
|
|
CHECK_VALUE(byte2 & 3, 0);
|
|
CHECK_VALUE(humidlevel, ((byte2 >> 2) & 7));
|
|
int tubelevel = (byte2 >> 5) & 7;
|
|
if (humidifier_present) {
|
|
if (humidlevel > 5 || humidlevel < 0) UNEXPECTED_VALUE(humidlevel, "0-5"); // 0=off is valid when a humidifier is attached
|
|
if (humid == 2) { // heated tube
|
|
if (tubelevel > 5 || tubelevel < 0) UNEXPECTED_VALUE(tubelevel, "0-5"); // TODO: maybe this is only if heated tube? 0=off is valid even in heated tube mode
|
|
}
|
|
}
|
|
|
|
if (add_setting) {
|
|
//this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HUMID_STATUS, (humid & 0x80) != 0)); // this is F0V23 version, doesn't match F0V6
|
|
//this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HEATED_TUBING, (humid & 0x10) != 0)); // this is F0V23 version, doesn't match F0V6
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_HUMID_LEVEL, humidlevel));
|
|
|
|
// TODO: add a channel for PRS1 heated tubing
|
|
//this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_TUBE_LEVEL, tubelevel));
|
|
}
|
|
}
|
|
|
|
|
|
// The below is based on a combination of the mainblock parsing for fileVersion == 3
|
|
// in ParseSummary() and the switch statements of ParseSummaryF0V6.
|
|
//
|
|
// Both compliance and summary files (at least for 200X and 400X machines) seem to have
|
|
// 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;
|
|
|
|
CPAPMode cpapmode = MODE_UNKNOWN;
|
|
|
|
int pressure = 0;
|
|
int imin_ps = 0;
|
|
int imax_ps = 0;
|
|
int min_pressure = 0;
|
|
int max_pressure = 0;
|
|
|
|
// 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?
|
|
switch (data[pos]) {
|
|
case 0: cpapmode = MODE_CPAP; break;
|
|
case 2: cpapmode = MODE_APAP; break;
|
|
case 1: cpapmode = MODE_BILEVEL_FIXED; break;
|
|
case 3: cpapmode = MODE_BILEVEL_AUTO_VARIABLE_PS; break;
|
|
case 4: cpapmode = MODE_CPAP; break; // "CPAP-Check" in report, but seems like CPAP
|
|
default:
|
|
UNEXPECTED_VALUE(data[pos], "known device mode");
|
|
break;
|
|
}
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_CPAP_MODE, (int) cpapmode));
|
|
break;
|
|
case 1: // ???
|
|
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 has extra byte
|
|
CHECK_VALUE(data[pos+1], 0);
|
|
}
|
|
break;
|
|
case 0x0a: // CPAP pressure setting
|
|
CHECK_VALUE(cpapmode, MODE_CPAP);
|
|
pressure = data[pos];
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PRESSURE, pressure));
|
|
break;
|
|
case 0x0c: // CPAP-Check pressure setting
|
|
CHECK_VALUE(cpapmode, MODE_CPAP);
|
|
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));
|
|
break;
|
|
case 0x0d: // AutoCPAP pressure setting
|
|
CHECK_VALUE(cpapmode, MODE_APAP);
|
|
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(cpapmode, MODE_BILEVEL_FIXED);
|
|
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(cpapmode, MODE_BILEVEL_AUTO_VARIABLE_PS);
|
|
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
|
|
CHECK_VALUE(cpapmode, MODE_CPAP); // the mode setting is CPAP, even though it's operating in APAP mode
|
|
cpapmode = MODE_APAP; // but categorize it now as APAP, since that's what it's really doing
|
|
CHECK_VALUES(data[pos], 30, 5); // Auto-Trial Duration
|
|
min_pressure = data[pos+1];
|
|
max_pressure = data[pos+2];
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_CPAP_MODE, (int) cpapmode));
|
|
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(data[pos], 0x80); // EZ-Start enabled
|
|
break;
|
|
case 0x2b: // Ramp Type
|
|
CHECK_VALUES(data[pos], 0, 0x80); // 0 == "Linear", 0x80 = "SmartRamp"
|
|
break;
|
|
case 0x2c: // Ramp Time
|
|
if (data[pos] != 0) { // 0 == ramp off, and ramp pressure setting doesn't appear
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_RAMP_TIME, data[pos]));
|
|
}
|
|
break;
|
|
case 0x2d: // Ramp Pressure
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_RAMP_PRESSURE, data[pos]));
|
|
break;
|
|
case 0x2e:
|
|
if (data[pos] != 0) {
|
|
CHECK_VALUES(data[pos], 0x80, 0x90); // maybe flex related? 0x80 when c-flex? 0x90 when c-flex+ or A-flex?, 0x00 when no flex
|
|
}
|
|
break;
|
|
case 0x2f: // Flex lock
|
|
CHECK_VALUES(data[pos], 0, 0x80);
|
|
break;
|
|
case 0x30: // Flex level
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_LEVEL, data[pos]));
|
|
break;
|
|
case 0x35: // Humidifier setting
|
|
this->ParseHumidifierSettingV3(data[pos], data[pos+1], true);
|
|
break;
|
|
case 0x36:
|
|
CHECK_VALUE(data[pos], 0);
|
|
break;
|
|
case 0x38: // Mask Resistance
|
|
if (data[pos] != 0) { // 0 == mask resistance off
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_SYSTEMONE_RESIST_SETTING, data[pos]));
|
|
}
|
|
break;
|
|
case 0x39:
|
|
CHECK_VALUES(data[pos], 0, 0x80); // 0x80 maybe auto-trial?
|
|
break;
|
|
case 0x3b:
|
|
if (data[pos] != 0) {
|
|
CHECK_VALUES(data[pos], 2, 1); // tubing type? 15HT = 2, 15 = 1, 22 = 0?
|
|
}
|
|
break;
|
|
case 0x40: // new to 400G, also seen on 500X110, alternate tubing type? appears after 0x39 and before 0x3c
|
|
if (data[pos] != 3) {
|
|
CHECK_VALUES(data[pos], 1, 2); // 1 = 15mm, 2 = 15HT, 3 = 12mm
|
|
}
|
|
break;
|
|
case 0x3c:
|
|
CHECK_VALUES(data[pos], 0, 0x80); // 0x80 maybe show AHI?
|
|
break;
|
|
case 0x3e:
|
|
CHECK_VALUES(data[pos], 0, 0x80); // 0x80 maybe auto-on?
|
|
break;
|
|
case 0x3f:
|
|
CHECK_VALUES(data[pos], 0, 0x80); // 0x80 maybe auto-off?
|
|
break;
|
|
case 0x43: // new to 502G, sessions 3-8, Auto-Trial is off, Opti-Start is missing
|
|
CHECK_VALUE(data[pos], 0x3C);
|
|
break;
|
|
case 0x44: // new to 502G, sessions 3-8, Auto-Trial is off, Opti-Start is missing
|
|
CHECK_VALUE(data[pos], 0xFF);
|
|
break;
|
|
case 0x45: // new to 400G, only in last session?
|
|
CHECK_VALUE(data[pos], 1);
|
|
break;
|
|
default:
|
|
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;
|
|
}
|
|
|
|
|
|
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();
|
|
static const int minimum_sizes[] = { 1, 0x2b, 9, 4, 2, 4, 1, 4, 0x1b, 2, 4, 0x0b, 1, 2, 6 };
|
|
static const int ncodes = sizeof(minimum_sizes) / sizeof(int);
|
|
// NOTE: The sizes contained in hblock can vary, even within a single machine, as can the length of hblock itself!
|
|
|
|
// TODO: hardcoding this is ugly, think of a better approach
|
|
if (chunk_size < minimum_sizes[0] + minimum_sizes[1] + minimum_sizes[2]) {
|
|
qWarning() << this->sessionid << "summary data too short:" << chunk_size;
|
|
return false;
|
|
}
|
|
if (chunk_size < 60) UNEXPECTED_VALUE(chunk_size, ">= 60");
|
|
|
|
bool ok = true;
|
|
int pos = 0;
|
|
int code, size;
|
|
int tt = 0;
|
|
do {
|
|
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]) {
|
|
qWarning() << this->sessionid << "slice" << 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 << "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
|
|
// Maybe statistics of some kind, given the pressure stats that seem to appear before it on AutoCPAP machines?
|
|
//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); // probably 16-bit value
|
|
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); // probably 16-bit value
|
|
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 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 0x0a: // 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;
|
|
case 0x0e:
|
|
// only seen once on 400G?
|
|
CHECK_VALUE(data[pos], 0);
|
|
CHECK_VALUE(data[pos+1], 0);
|
|
CHECK_VALUE(data[pos+2], 7);
|
|
CHECK_VALUE(data[pos+3], 7);
|
|
CHECK_VALUE(data[pos+4], 7);
|
|
CHECK_VALUE(data[pos+5], 0);
|
|
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 3 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;
|
|
default:
|
|
UNEXPECTED_VALUE(code, "known slice code");
|
|
break;
|
|
}
|
|
pos += size;
|
|
} while (ok && pos < chunk_size);
|
|
|
|
this->duration = tt;
|
|
|
|
return ok;
|
|
}
|
|
|
|
|
|
// Originally based on ParseSummaryF0V6, with changes observed in ASV sample data
|
|
// based on size, slices 0-5 look similar, and it looks like F0V6 slides 8-B are equivalent to 6-9
|
|
//
|
|
// TODO: surely there will be a way to merge these loops and abstract the machine-specific
|
|
// encodings into another function or class, but that's probably worth pursuing only after
|
|
// the details have been figured out.
|
|
bool PRS1DataChunk::ParseSummaryF5V3(void)
|
|
{
|
|
if (this->family != 5 || this->familyVersion != 3) {
|
|
qWarning() << "ParseSummaryF5V3 called with family" << this->family << "familyVersion" << this->familyVersion;
|
|
return false;
|
|
}
|
|
const unsigned char * data = (unsigned char *)this->m_data.constData();
|
|
int chunk_size = this->m_data.size();
|
|
static const int minimum_sizes[] = { 1, 0x35, 9, 4, 2, 4, 0x1e, 2, 4, 9 };
|
|
static const int ncodes = sizeof(minimum_sizes) / sizeof(int);
|
|
// NOTE: The sizes contained in hblock can vary, even within a single machine, as can the length of hblock itself!
|
|
|
|
// TODO: hardcoding this is ugly, think of a better approach
|
|
if (chunk_size < minimum_sizes[0] + minimum_sizes[1] + minimum_sizes[2]) {
|
|
qWarning() << this->sessionid << "summary data too short:" << chunk_size;
|
|
return false;
|
|
}
|
|
// We've once seen a short summary with no mask-on/off: just equipment-on, settings, 9, equipment-off
|
|
if (chunk_size < 75) UNEXPECTED_VALUE(chunk_size, ">= 75");
|
|
|
|
bool ok = true;
|
|
int pos = 0;
|
|
int code, size;
|
|
int tt = 0;
|
|
do {
|
|
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]) {
|
|
qWarning() << this->sessionid << "slice" << 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 << "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, or 0?
|
|
CHECK_VALUE(size, 1);
|
|
break;
|
|
case 1: // Settings
|
|
ok = this->ParseSettingsF5V3(data + pos, size);
|
|
break;
|
|
case 9: // new to F5V3 vs. F0V6, comes right after settings, before mask on?
|
|
CHECK_VALUE(data[pos], 0);
|
|
CHECK_VALUE(data[pos+1], 1);
|
|
CHECK_VALUE(data[pos+2], 0);
|
|
CHECK_VALUE(data[pos+3], 1);
|
|
CHECK_VALUE(data[pos+4], 1);
|
|
CHECK_VALUE(data[pos+5], 0);
|
|
CHECK_VALUE(data[pos+6], 2);
|
|
CHECK_VALUE(data[pos+7], 1);
|
|
CHECK_VALUE(data[pos+8], 0);
|
|
break;
|
|
case 3: // Mask On
|
|
tt += data[pos] | (data[pos+1] << 8);
|
|
this->AddEvent(new PRS1ParsedSliceEvent(tt, MaskOn));
|
|
this->ParseHumidifierSettingV3(data[pos+2], data[pos+3]);
|
|
break;
|
|
case 4: // Mask Off
|
|
tt += data[pos] | (data[pos+1] << 8);
|
|
this->AddEvent(new PRS1ParsedSliceEvent(tt, MaskOff));
|
|
break;
|
|
case 5: // ASV pressure stats per mask-on slice
|
|
//CHECK_VALUE(data[pos], 0x28); // 90% EPAP
|
|
//CHECK_VALUE(data[pos+1], 0x23); // average EPAP
|
|
//CHECK_VALUE(data[pos+2], 0x24); // 90% PS
|
|
//CHECK_VALUE(data[pos+3], 0x17); // average PS
|
|
break;
|
|
case 6: // Patient statistics per mask-on slice
|
|
// These get averaged on a time-weighted basis in the final report.
|
|
// Where is H count?
|
|
//CHECK_VALUE(data[pos], 0x00); // probably 16-bit value
|
|
CHECK_VALUE(data[pos+1], 0x00);
|
|
//CHECK_VALUE(data[pos+2], 0x00); // 16-bit OA count
|
|
//CHECK_VALUE(data[pos+3], 0x00);
|
|
//CHECK_VALUE(data[pos+4], 0x00); // probably 16-bit value
|
|
CHECK_VALUE(data[pos+5], 0x00);
|
|
//CHECK_VALUE(data[pos+6], 0x00); // 16-bit CA count
|
|
//CHECK_VALUE(data[pos+7], 0x00);
|
|
//CHECK_VALUE(data[pos+8], 0x00); // 16-bit minutes in LL
|
|
//CHECK_VALUE(data[pos+9], 0x00);
|
|
//CHECK_VALUE(data[pos+0xa], 0x0f); // 16-bit minutes in PB
|
|
//CHECK_VALUE(data[pos+0xb], 0x00);
|
|
//CHECK_VALUE(data[pos+0xc], 0x14); // 16-bit VS count
|
|
//CHECK_VALUE(data[pos+0xd], 0x00);
|
|
//CHECK_VALUE(data[pos+0xe], 0x05); // probably 16-bit value (VS count in F0V6)?
|
|
CHECK_VALUE(data[pos+0xf], 0x00);
|
|
//CHECK_VALUE(data[pos+0x10], 0x00); // probably 16-bit value (maybe H count in F0V6?)
|
|
CHECK_VALUE(data[pos+0x11], 0x00);
|
|
//CHECK_VALUE(data[pos+0x12], 0x02); // 16-bit FL count
|
|
//CHECK_VALUE(data[pos+0x13], 0x00);
|
|
//CHECK_VALUE(data[pos+0x14], 0x28); // 0x69 (105)
|
|
//CHECK_VALUE(data[pos+0x15], 0x17); // average total leak
|
|
//CHECK_VALUE(data[pos+0x16], 0x5b); // 0x7d (125)
|
|
//CHECK_VALUE(data[pos+0x17], 0x09); // 0x00
|
|
CHECK_VALUE(data[pos+0x18], 0x00);
|
|
//CHECK_VALUE(data[pos+0x19], 0x10); // average breath rate
|
|
//CHECK_VALUE(data[pos+0x1a], 0x2d); // average TV / 10
|
|
//CHECK_VALUE(data[pos+0x1b], 0x63); // average % PTB
|
|
//CHECK_VALUE(data[pos+0x1c], 0x07); // average minute vent
|
|
//CHECK_VALUE(data[pos+0x1d], 0x06); // 0x51 (81)
|
|
break;
|
|
case 2: // Equipment Off
|
|
tt += data[pos] | (data[pos+1] << 8);
|
|
this->AddEvent(new PRS1ParsedSliceEvent(tt, EquipmentOff));
|
|
//CHECK_VALUE(data[pos+2], 0x01); // 0x08
|
|
//CHECK_VALUE(data[pos+3], 0x17); // 0x16, 0x18
|
|
//CHECK_VALUE(data[pos+4], 0x00);
|
|
//CHECK_VALUE(data[pos+5], 0x29); // 0x2a, 0x28, 0x26, 0x36
|
|
//CHECK_VALUE(data[pos+6], 0x01); // 0x00
|
|
CHECK_VALUE(data[pos+7], 0x00);
|
|
CHECK_VALUE(data[pos+8], 0x00);
|
|
break;
|
|
case 8: // Humidier setting change
|
|
tt += data[pos] | (data[pos+1] << 8); // This adds to the total duration (otherwise it won't match report)
|
|
this->ParseHumidifierSettingV3(data[pos+2], data[pos+3]);
|
|
break;
|
|
default:
|
|
UNEXPECTED_VALUE(code, "known slice code");
|
|
break;
|
|
}
|
|
pos += size;
|
|
} while (ok && pos < chunk_size);
|
|
|
|
this->duration = tt;
|
|
|
|
return ok;
|
|
}
|
|
|
|
|
|
// Based initially on ParseSettingsF0V6. Many of the codes look the same, like always starting with 0, 0x35 looking like
|
|
// a humidifier setting, etc., but the contents are sometimes a bit different, such as mode values and pressure settings.
|
|
//
|
|
// new settings to find: breath rate, tubing lock, alarms,
|
|
bool PRS1DataChunk::ParseSettingsF5V3(const unsigned char* data, int size)
|
|
{
|
|
static const QMap<int,int> expected_lengths = { {0x0a,5}, /*{0x0c,3}, {0x0d,2}, {0x0e,2}, {0x0f,4}, {0x10,3},*/ {0x14,3}, {0x2e,2}, {0x35,2} };
|
|
bool ok = true;
|
|
|
|
CPAPMode cpapmode = MODE_UNKNOWN;
|
|
|
|
// F5V3 uses a gain of 0.125 rather than 0.1 to allow for a maximum value of 30 cmH2O
|
|
static const float GAIN = 0.125; // TODO: parameterize this somewhere better
|
|
|
|
int max_pressure = 0;
|
|
int min_ps = 0;
|
|
int max_ps = 0;
|
|
int min_epap = 0;
|
|
int max_epap = 0;
|
|
|
|
// 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?
|
|
switch (data[pos]) {
|
|
case 0: cpapmode = MODE_ASV_VARIABLE_EPAP; 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(data[pos], 0, 1); // 1 when when Opti-Start is on? 0 when off?
|
|
/*
|
|
if (data[pos] != 0 && data[pos] != 3) {
|
|
CHECK_VALUES(data[pos], 1, 2); // 1 when EZ-Start is enabled? 2 when Auto-Trial? 3 when Auto-Trial is off or Opti-Start isn't off?
|
|
}
|
|
*/
|
|
break;
|
|
case 0x0a: // ASV with variable EPAP pressure setting
|
|
CHECK_VALUE(cpapmode, MODE_ASV_VARIABLE_EPAP);
|
|
max_pressure = data[pos];
|
|
min_epap = data[pos+1];
|
|
max_epap = data[pos+2];
|
|
min_ps = data[pos+3];
|
|
max_ps = data[pos+4];
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_EPAP_MIN, min_epap, GAIN));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_EPAP_MAX, max_epap, GAIN));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP_MIN, min_epap + min_ps, GAIN));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_IPAP_MAX, qMin(max_pressure, max_epap + max_ps), GAIN));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS_MIN, min_ps, GAIN));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS_MAX, max_ps, GAIN));
|
|
break;
|
|
case 0x14: // new to ASV, ???
|
|
CHECK_VALUE(data[pos], 1);
|
|
CHECK_VALUE(data[pos+1], 0);
|
|
CHECK_VALUE(data[pos+2], 0);
|
|
break;
|
|
/*
|
|
case 0x2a: // EZ-Start
|
|
CHECK_VALUE(data[pos], 0x80); // EZ-Start enabled
|
|
break;
|
|
*/
|
|
case 0x2b: // Ramp Type
|
|
CHECK_VALUES(data[pos], 0, 0x80); // 0 == "Linear", 0x80 = "SmartRamp"
|
|
break;
|
|
case 0x2c: // Ramp Time
|
|
if (data[pos] != 0) { // 0 == ramp off, and ramp pressure setting doesn't appear
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_RAMP_TIME, data[pos]));
|
|
}
|
|
break;
|
|
case 0x2d: // Ramp Pressure (with ASV pressure encoding)
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_RAMP_PRESSURE, data[pos], GAIN));
|
|
break;
|
|
case 0x2e:
|
|
CHECK_VALUE(data[pos], 0);
|
|
//CHECK_VALUES(data[pos+1], 2, 3); // Bi-Flex level
|
|
/*
|
|
if (data[pos] != 0) {
|
|
CHECK_VALUES(data[pos], 0x80, 0x90); // maybe flex related? 0x80 when c-flex? 0x90 when c-flex+ or A-flex?, 0x00 when no flex
|
|
}
|
|
*/
|
|
break;
|
|
case 0x2f: // Flex lock? (was on F0V6, 0x80 for locked)
|
|
CHECK_VALUE(data[pos], 0);
|
|
break;
|
|
/*
|
|
case 0x30: // Flex level
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_FLEX_LEVEL, data[pos]));
|
|
break;
|
|
*/
|
|
case 0x35: // Humidifier setting
|
|
this->ParseHumidifierSettingV3(data[pos], data[pos+1], true);
|
|
break;
|
|
case 0x36: // Mask Resistance Lock
|
|
CHECK_VALUES(data[pos], 0, 0x80); // 0x80 = locked
|
|
break;
|
|
case 0x38: // Mask Resistance
|
|
if (data[pos] != 0) { // 0 == mask resistance off
|
|
this->AddEvent(new PRS1ParsedSettingEvent(PRS1_SETTING_SYSTEMONE_RESIST_SETTING, data[pos]));
|
|
}
|
|
break;
|
|
case 0x39:
|
|
CHECK_VALUE(data[pos], 0); // 0x80 maybe auto-trial in F0V6?
|
|
break;
|
|
case 0x3b: // Tubing Type
|
|
if (data[pos] != 0) {
|
|
CHECK_VALUES(data[pos], 2, 1); // 15HT = 2, 15 = 1, 22 = 0, though report only says "15" for 15HT
|
|
}
|
|
break;
|
|
case 0x3c:
|
|
CHECK_VALUES(data[pos], 0, 0x80); // 0x80 maybe show AHI?
|
|
break;
|
|
case 0x3d: // new to ASV
|
|
//CHECK_VALUES(data[pos], 0, 0x80); // 0x80 maybe auto-on?
|
|
break;
|
|
/*
|
|
case 0x3e:
|
|
CHECK_VALUES(data[pos], 0, 0x80); // 0x80 maybe auto-on?
|
|
break;
|
|
case 0x3f:
|
|
CHECK_VALUES(data[pos], 0, 0x80); // 0x80 maybe auto-off?
|
|
break;
|
|
case 0x43: // new to 502G, sessions 3-8, Auto-Trial is off, Opti-Start is missing
|
|
CHECK_VALUE(data[pos], 0x3C);
|
|
break;
|
|
case 0x44: // new to 502G, sessions 3-8, Auto-Trial is off, Opti-Start is missing
|
|
CHECK_VALUE(data[pos], 0xFF);
|
|
break;
|
|
case 0x45: // new to 400G, only in last session?
|
|
CHECK_VALUE(data[pos], 1);
|
|
break;
|
|
*/
|
|
default:
|
|
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;
|
|
}
|
|
|
|
|
|
bool PRS1Import::ImportSummary()
|
|
{
|
|
if (!summary) {
|
|
qWarning() << "ImportSummary() called with no summary?";
|
|
return false;
|
|
}
|
|
|
|
qint64 start = qint64(summary->timestamp) * 1000L;
|
|
session->set_first(start);
|
|
|
|
session->setPhysMax(CPAP_LeakTotal, 120);
|
|
session->setPhysMin(CPAP_LeakTotal, 0);
|
|
session->setPhysMax(CPAP_Pressure, 25);
|
|
session->setPhysMin(CPAP_Pressure, 4);
|
|
session->setPhysMax(CPAP_IPAP, 25);
|
|
session->setPhysMin(CPAP_IPAP, 4);
|
|
session->setPhysMax(CPAP_EPAP, 25);
|
|
session->setPhysMin(CPAP_EPAP, 4);
|
|
session->setPhysMax(CPAP_PS, 25);
|
|
session->setPhysMin(CPAP_PS, 0);
|
|
|
|
bool ok;
|
|
ok = summary->ParseSummary();
|
|
|
|
for (int i=0; i < summary->m_parsedData.count(); i++) {
|
|
PRS1ParsedEvent* e = summary->m_parsedData.at(i);
|
|
if (e->m_type == PRS1ParsedSliceEvent::TYPE) {
|
|
PRS1ParsedSliceEvent* s = (PRS1ParsedSliceEvent*) e;
|
|
qint64 tt = start + qint64(s->m_start) * 1000L;
|
|
if (!session->m_slices.isEmpty()) {
|
|
SessionSlice & prevSlice = session->m_slices.last();
|
|
prevSlice.end = tt;
|
|
}
|
|
session->m_slices.append(SessionSlice(tt, tt, (SliceStatus) s->m_value));
|
|
continue;
|
|
} else if (e->m_type != PRS1ParsedSettingEvent::TYPE) {
|
|
qWarning() << "Summary had non-setting event:" << (int) e->m_type;
|
|
continue;
|
|
}
|
|
PRS1ParsedSettingEvent* s = (PRS1ParsedSettingEvent*) e;
|
|
switch (s->m_setting) {
|
|
case PRS1_SETTING_CPAP_MODE:
|
|
session->settings[CPAP_Mode] = e->m_value;
|
|
break;
|
|
case PRS1_SETTING_PRESSURE:
|
|
session->settings[CPAP_Pressure] = e->value();
|
|
break;
|
|
case PRS1_SETTING_PRESSURE_MIN:
|
|
session->settings[CPAP_PressureMin] = e->value();
|
|
break;
|
|
case PRS1_SETTING_PRESSURE_MAX:
|
|
session->settings[CPAP_PressureMax] = e->value();
|
|
break;
|
|
case PRS1_SETTING_EPAP:
|
|
session->settings[CPAP_EPAP] = e->value();
|
|
break;
|
|
case PRS1_SETTING_IPAP:
|
|
session->settings[CPAP_IPAP] = e->value();
|
|
break;
|
|
case PRS1_SETTING_PS:
|
|
session->settings[CPAP_PS] = e->value();
|
|
break;
|
|
case PRS1_SETTING_EPAP_MIN:
|
|
session->settings[CPAP_EPAPLo] = e->value();
|
|
break;
|
|
case PRS1_SETTING_EPAP_MAX:
|
|
session->settings[CPAP_EPAPHi] = e->value();
|
|
break;
|
|
case PRS1_SETTING_IPAP_MIN:
|
|
session->settings[CPAP_IPAPLo] = e->value();
|
|
break;
|
|
case PRS1_SETTING_IPAP_MAX:
|
|
session->settings[CPAP_IPAPHi] = e->value();
|
|
break;
|
|
case PRS1_SETTING_PS_MIN:
|
|
session->settings[CPAP_PSMin] = e->value();
|
|
break;
|
|
case PRS1_SETTING_PS_MAX:
|
|
session->settings[CPAP_PSMax] = e->value();
|
|
break;
|
|
case PRS1_SETTING_FLEX_MODE:
|
|
session->settings[PRS1_FlexMode] = e->m_value;
|
|
break;
|
|
case PRS1_SETTING_FLEX_LEVEL:
|
|
session->settings[PRS1_FlexLevel] = e->m_value;
|
|
break;
|
|
case PRS1_SETTING_RAMP_TIME:
|
|
session->settings[CPAP_RampTime] = e->m_value;
|
|
break;
|
|
case PRS1_SETTING_RAMP_PRESSURE:
|
|
session->settings[CPAP_RampPressure] = e->value();
|
|
break;
|
|
case PRS1_SETTING_HUMID_STATUS:
|
|
session->settings[PRS1_HumidStatus] = (bool) e->m_value;
|
|
break;
|
|
case PRS1_SETTING_HEATED_TUBING:
|
|
session->settings[PRS1_HeatedTubing] = (bool) e->m_value;
|
|
break;
|
|
case PRS1_SETTING_HUMID_LEVEL:
|
|
session->settings[PRS1_HumidLevel] = e->m_value;
|
|
break;
|
|
case PRS1_SETTING_SYSTEMONE_RESIST_LOCK:
|
|
session->settings[PRS1_SysLock] = (bool) e->m_value;
|
|
break;
|
|
case PRS1_SETTING_SYSTEMONE_RESIST_SETTING:
|
|
session->settings[PRS1_SysOneResistSet] = e->m_value;
|
|
break;
|
|
case PRS1_SETTING_SYSTEMONE_RESIST_STATUS:
|
|
session->settings[PRS1_SysOneResistStat] = (bool) e->m_value;
|
|
break;
|
|
case PRS1_SETTING_HOSE_DIAMETER:
|
|
session->settings[PRS1_HoseDiam] = e->m_value == 15 ? QObject::tr("15mm") : QObject::tr("22mm");
|
|
break;
|
|
case PRS1_SETTING_AUTO_ON:
|
|
session->settings[PRS1_AutoOn] = (bool) e->m_value;
|
|
break;
|
|
case PRS1_SETTING_AUTO_OFF:
|
|
session->settings[PRS1_AutoOff] = (bool) e->m_value;
|
|
break;
|
|
case PRS1_SETTING_MASK_ALERT:
|
|
session->settings[PRS1_MaskAlert] = (bool) e->m_value;
|
|
break;
|
|
case PRS1_SETTING_SHOW_AHI:
|
|
session->settings[PRS1_ShowAHI] = (bool) e->m_value;
|
|
break;
|
|
default:
|
|
qWarning() << "Unknown PRS1 setting type" << (int) s->m_setting;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!ok) {
|
|
return false;
|
|
}
|
|
|
|
summary_duration = summary->duration;
|
|
|
|
if (summary->duration == 0) {
|
|
// This does occasionally happen and merely indicates a brief session with no useful data.
|
|
//qDebug() << summary->sessionid << "duration == 0";
|
|
return true; // Don't bail for now, since some summary parsers are still very broken, so we want to proceed to events/waveforms.
|
|
}
|
|
|
|
// Intentionally don't set the session's duration based on the summary duration.
|
|
// That only happens in PRS1Import::ParseSession() as a last resort.
|
|
// TODO: Revisit this once summary parsing is reliable.
|
|
//session->set_last(...);
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool PRS1DataChunk::ParseSummary()
|
|
{
|
|
const unsigned char * data = (unsigned char *)this->m_data.constData();
|
|
|
|
// TODO: 7 length 3, 8 length 3 have been seen on 960P, add those value checks once we look more closely at the data.
|
|
if (data[0] == 5) {
|
|
CHECK_VALUE(this->m_data.size(), 5); // 4 more bytes before CRC, looks like a timestamp
|
|
} else if (data[0] == 6) {
|
|
CHECK_VALUE(this->m_data.size(), 1); // 0 more bytes before CRC
|
|
} else {
|
|
CHECK_VALUE(data[0], 0);
|
|
}
|
|
// All machines have a first byte zero for clean summary
|
|
// TODO: this check should move down into the individual family parsers once the V3 parsing below has been relocated.
|
|
if (data[0] != 0) {
|
|
//qDebug() << this->sessionid << "summary first byte" << data[0] << "!= 0, skipping";
|
|
return false;
|
|
}
|
|
|
|
// TODO: The below mainblock creation is probably wrong. It should move to to its own function when it gets fixed.
|
|
/* Example data block
|
|
000000c6@0000: 00 [10] 01 [00 01 02 01 01 00 02 01 00 04 01 40 07
|
|
000000c6@0010: 01 60 1e 03 02 0c 14 2c 01 14 2d 01 40 2e 01 02
|
|
000000c6@0020: 2f 01 00 35 02 28 68 36 01 00 38 01 00 39 01 00
|
|
000000c6@0030: 3b 01 01 3c 01 80] 02 [00 01 00 01 01 00 02 01 00]
|
|
000000c6@0040: 04 [00 00 28 68] 0c [78 00 2c 6c] 05 [e4 69] 07 [40 40]
|
|
000000c6@0050: 08 [61 60] 0a [00 00 00 00 03 00 00 00 02 00 02 00
|
|
000000c6@0060: 05 00 2b 11 00 10 2b 5c 07 12 00 00] 03 [00 00 01
|
|
000000c6@0070: 1a 00 38 04] */
|
|
if (this->fileVersion == 3) {
|
|
// Parse summary structures into bytearray map according to size given in header block
|
|
int size = this->m_data.size();
|
|
|
|
int pos = 0;
|
|
int bsize;
|
|
short val, len;
|
|
do {
|
|
val = data[pos++];
|
|
auto it = this->hblock.find(val);
|
|
if (it == this->hblock.end()) {
|
|
qDebug() << "Block parse error in ParseSummary" << this->sessionid;
|
|
break;
|
|
}
|
|
bsize = it.value();
|
|
|
|
if (val != 1) {
|
|
if (this->hbdata.contains(val)) {
|
|
// We know this is entirely wrong. It will be removed after F3V6 is updated.
|
|
//qWarning() << this->sessionid << "duplicate hbdata val" << val;
|
|
}
|
|
// store the data block for later reference
|
|
this->hbdata[val] = QByteArray((const char *)(&data[pos]), bsize);
|
|
} else {
|
|
if (!this->mainblock.isEmpty()) {
|
|
qWarning() << this->sessionid << "duplicate mainblock";
|
|
}
|
|
// Parse the nested data structure which contains settings
|
|
int p2 = 0;
|
|
do {
|
|
val = data[pos + p2++];
|
|
len = data[pos + p2++];
|
|
if (this->mainblock.contains(val)) {
|
|
qWarning() << this->sessionid << "duplicate mainblock val" << val;
|
|
}
|
|
this->mainblock[val] = QByteArray((const char *)(&data[pos+p2]), len);
|
|
p2 += len;
|
|
} while ((p2 < bsize) && ((pos+p2) < size));
|
|
}
|
|
pos += bsize;
|
|
} while (pos < size);
|
|
}
|
|
// Family 0 = XPAP
|
|
// Family 3 = BIPAP AVAPS
|
|
// Family 5 = BIPAP AutoSV
|
|
|
|
switch (this->family) {
|
|
case 0:
|
|
if (this->familyVersion == 6) {
|
|
return this->ParseSummaryF0V6();
|
|
} else if (this->familyVersion == 4) {
|
|
return this->ParseSummaryF0V4();
|
|
} else {
|
|
return this->ParseSummaryF0V23();
|
|
}
|
|
case 3:
|
|
return this->ParseSummaryF3();
|
|
break;
|
|
case 5:
|
|
if (this->familyVersion == 1) {
|
|
return this->ParseSummaryF5V012();
|
|
} else if (this->familyVersion == 0) {
|
|
return this->ParseSummaryF5V012();
|
|
} else if (this->familyVersion == 2) {
|
|
return this->ParseSummaryF5V012();
|
|
} else if (this->familyVersion == 3) {
|
|
return this->ParseSummaryF5V3();
|
|
}
|
|
default:
|
|
;
|
|
}
|
|
|
|
qWarning() << "unexpected family" << this->family << "familyVersion" << this->familyVersion;
|
|
return false;
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////
|
|
// ASV Codes (Family 5) Recheck 17/10/2013
|
|
// These are all confirmed off Encore reports
|
|
|
|
//cpapmax=EventDataType(data[0x02])/10.0; // Max Pressure in ASV machines
|
|
//minepap=EventDataType(data[0x03])/10.0; // Min EPAP
|
|
//maxepap=EventDataType(data[0x04])/10.0; // Max EPAP
|
|
//minps=EventDataType(data[0x05])/10.0 // Min Pressure Support
|
|
//maxps=EventDataType(data[0x06])/10.0 // Max Pressure Support
|
|
|
|
//duration=data[0x1B] | data[0x1C] << 8) // Session length in seconds
|
|
|
|
//epap90=EventDataType(data[0x21])/10.0; // EPAP 90%
|
|
//epapavg=EventDataType(data[0x22])/10.0; // EPAP Average
|
|
//ps90=EventDataType(data[0x23])/10.0; // Pressure Support 90%
|
|
//psavg=EventDataType(data[0x24])/10.0; // Pressure Support Average
|
|
|
|
//TODO: minpb=data[0x] | data[0x] << 8; // Minutes in PB
|
|
//TODO: minleak=data[0x] | data[0x] << 8; // Minutes in Large Leak
|
|
//TODO: oa_cnt=data[0x] | data[0x] << 8; // Obstructive events count
|
|
|
|
//ca_cnt=data[0x2d] | data[0x2e] << 8; // Clear Airway Events count
|
|
//h_cnt=data[0x2f] | data[0x30] << 8; // Hypopnea events count
|
|
//fl_cnt=data[0x33] | data[0x34] << 8; // Flow Limitation events count
|
|
|
|
//avg_leak=EventDataType(data[0x35]); // Average Leak
|
|
//avgptb=EventDataType(data[0x36]); // Average Patient Triggered Breaths %
|
|
//avgbreathrate=EventDataType(data[0x37]); // Average Breaths Per Minute
|
|
//avgminvent=EventDataType(data[0x38]); // Average Minute Ventilation
|
|
//avg_tidalvol=EventDataType(data[0x39])*10.0; // Average Tidal Volume
|
|
//////////////////////////////////////////////////////////////////////////////////////////
|
|
}
|
|
|
|
|
|
// TODO: Eventually PRS1Import::ImportEvents will call this directly, once the PRS1Import::ParseF*Events have been merged.
|
|
bool PRS1DataChunk::ParseEvents(CPAPMode mode)
|
|
{
|
|
bool ok = false;
|
|
switch (this->family) {
|
|
case 0:
|
|
ok = this->ParseEventsF0(mode);
|
|
break;
|
|
case 3:
|
|
if (this->familyVersion == 6) {
|
|
ok = this->ParseEventsF3V6();
|
|
} else if (this->familyVersion == 3) {
|
|
ok = this->ParseEventsF3V3();
|
|
}
|
|
break;
|
|
case 5:
|
|
if (this->familyVersion == 3) {
|
|
ok = this->ParseEventsF5V3();
|
|
} else if (this->familyVersion < 3) {
|
|
ok = this->ParseEventsF5V012();
|
|
}
|
|
break;
|
|
default:
|
|
qDebug() << "Unknown PRS1 family" << this->family << "familyVersion" << this->familyVersion;
|
|
}
|
|
return ok;
|
|
}
|
|
|
|
|
|
// TODO: Eventually this will be renamed PRS1Import::ImportEvents, once PRS1Import::ParseF*Events have been merged and incorporated.
|
|
bool PRS1Import::ParseEvents()
|
|
{
|
|
bool res = false;
|
|
if (!event) return false;
|
|
switch (event->family) {
|
|
case 0:
|
|
res = ParseF0Events();
|
|
break;
|
|
case 3:
|
|
if (event->fileVersion == 3) {
|
|
res = ParseF3EventsV3();
|
|
} else {
|
|
res = ParseF3Events();
|
|
}
|
|
break;
|
|
case 5:
|
|
if (event->fileVersion==3) {
|
|
res = ParseF5EventsFV3();
|
|
} else {
|
|
res = ParseF5Events();
|
|
}
|
|
break;
|
|
default:
|
|
qDebug() << "Unknown PRS1 familyVersion" << event->familyVersion;
|
|
return false;
|
|
}
|
|
|
|
if (res) {
|
|
if (session->count(CPAP_IPAP) > 0) {
|
|
// if (session->settings[CPAP_Mode].toInt() != (int)MODE_ASV) {
|
|
// session->settings[CPAP_Mode] = MODE_BILEVEL_FIXED;
|
|
// }
|
|
|
|
// if (session->settings[CPAP_PresReliefType].toInt() != PR_NONE) {
|
|
// session->settings[CPAP_PresReliefType] = PR_BIFLEX;
|
|
// }
|
|
|
|
// EventDataType min = session->settings[CPAP_PressureMin].toDouble();
|
|
// EventDataType max = session->settings[CPAP_PressureMax].toDouble();
|
|
// session->settings[CPAP_EPAP] = min;
|
|
// session->settings[CPAP_IPAP] = max;
|
|
|
|
// session->settings[CPAP_PS] = max - min;
|
|
// session->settings.erase(session->settings.find(CPAP_PressureMin));
|
|
// session->settings.erase(session->settings.find(CPAP_PressureMax));
|
|
|
|
// session->m_valuesummary.erase(session->m_valuesummary.find(CPAP_Pressure));
|
|
// session->m_wavg.erase(session->m_wavg.find(CPAP_Pressure));
|
|
// session->m_min.erase(session->m_min.find(CPAP_Pressure));
|
|
// session->m_max.erase(session->m_max.find(CPAP_Pressure));
|
|
// session->m_gain.erase(session->m_gain.find(CPAP_Pressure));
|
|
|
|
} else {
|
|
if (!session->settings.contains(CPAP_Pressure) && !session->settings.contains(CPAP_PressureMin)) {
|
|
qWarning() << session->session() << "broken summary, missing pressure";
|
|
session->settings[CPAP_BrokenSummary] = true;
|
|
|
|
//session->set_last(session->first());
|
|
if (session->Min(CPAP_Pressure) == session->Max(CPAP_Pressure)) {
|
|
session->settings[CPAP_Mode] = MODE_CPAP; // no ramp
|
|
session->settings[CPAP_Pressure] = session->Min(CPAP_Pressure);
|
|
} else {
|
|
session->settings[CPAP_Mode] = MODE_APAP;
|
|
session->settings[CPAP_PressureMin] = session->Min(CPAP_Pressure);
|
|
session->settings[CPAP_PressureMax] = 0; //session->Max(CPAP_Pressure);
|
|
}
|
|
|
|
//session->Set("FlexMode",PR_UNKNOWN);
|
|
|
|
}
|
|
}
|
|
|
|
}
|
|
return res;
|
|
}
|
|
|
|
|
|
QList<PRS1DataChunk *> PRS1Import::CoalesceWaveformChunks(QList<PRS1DataChunk *> & allchunks)
|
|
{
|
|
QList<PRS1DataChunk *> coalesced;
|
|
PRS1DataChunk *chunk = nullptr, *lastchunk = nullptr;
|
|
int num;
|
|
|
|
for (int i=0; i < allchunks.size(); ++i) {
|
|
chunk = allchunks.at(i);
|
|
|
|
if (lastchunk != nullptr) {
|
|
// Waveform files shouldn't contain multiple sessions
|
|
if (lastchunk->sessionid != chunk->sessionid) {
|
|
qWarning() << "lastchunk->sessionid != chunk->sessionid in PRS1Loader::CoalesceWaveformChunks()";
|
|
// Free any remaining chunks
|
|
for (int j=i; j < allchunks.size(); ++j) {
|
|
chunk = allchunks.at(j);
|
|
delete chunk;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// Check whether the data format is the same between the two chunks
|
|
bool same_format = (lastchunk->waveformInfo.size() == chunk->waveformInfo.size());
|
|
if (same_format) {
|
|
num = chunk->waveformInfo.size();
|
|
for (int n=0; n < num; n++) {
|
|
const PRS1Waveform &a = lastchunk->waveformInfo.at(n);
|
|
const PRS1Waveform &b = chunk->waveformInfo.at(n);
|
|
if (a.interleave != b.interleave) {
|
|
// We've never seen this before
|
|
qWarning() << chunk->m_path << "format change?" << a.interleave << b.interleave;
|
|
same_format = false;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
// We've never seen this before
|
|
qWarning() << chunk->m_path << "channels change?" << lastchunk->waveformInfo.size() << chunk->waveformInfo.size();
|
|
}
|
|
|
|
qint64 diff = (chunk->timestamp - lastchunk->timestamp) - lastchunk->duration;
|
|
if (same_format && diff == 0) {
|
|
// Same format and in sync, so append waveform data to previous chunk
|
|
lastchunk->m_data.append(chunk->m_data);
|
|
lastchunk->duration += chunk->duration;
|
|
delete chunk;
|
|
continue;
|
|
}
|
|
// else start a new chunk to resync
|
|
}
|
|
|
|
// Report any formats we haven't seen before
|
|
num = chunk->waveformInfo.size();
|
|
if (num > 2) {
|
|
qDebug() << chunk->m_path << num << "channels";
|
|
}
|
|
for (int n=0; n < num; n++) {
|
|
int interleave = chunk->waveformInfo.at(n).interleave;
|
|
if (interleave != 5) {
|
|
qDebug() << chunk->m_path << "interleave?" << interleave;
|
|
}
|
|
}
|
|
|
|
coalesced.append(chunk);
|
|
lastchunk = chunk;
|
|
}
|
|
|
|
return coalesced;
|
|
}
|
|
|
|
|
|
bool PRS1Import::ParseOximetry()
|
|
{
|
|
int size = oximetry.size();
|
|
|
|
for (int i=0; i < size; ++i) {
|
|
PRS1DataChunk * oxi = oximetry.at(i);
|
|
int num = oxi->waveformInfo.size();
|
|
|
|
int size = oxi->m_data.size();
|
|
if (size == 0) {
|
|
qDebug() << oxi->sessionid << oxi->timestamp << "empty?";
|
|
continue;
|
|
}
|
|
quint64 ti = quint64(oxi->timestamp) * 1000L;
|
|
qint64 dur = qint64(oxi->duration) * 1000L;
|
|
|
|
if (num > 1) {
|
|
// Process interleaved samples
|
|
QVector<QByteArray> data;
|
|
data.resize(num);
|
|
|
|
int pos = 0;
|
|
do {
|
|
for (int n=0; n < num; n++) {
|
|
int interleave = oxi->waveformInfo.at(n).interleave;
|
|
data[n].append(oxi->m_data.mid(pos, interleave));
|
|
pos += interleave;
|
|
}
|
|
} while (pos < size);
|
|
|
|
if (data[0].size() > 0) {
|
|
EventList * pulse = session->AddEventList(OXI_Pulse, EVL_Waveform, 1.0, 0.0, 0.0, 0.0, dur / data[0].size());
|
|
pulse->AddWaveform(ti, (unsigned char *)data[0].data(), data[0].size(), dur);
|
|
}
|
|
|
|
if (data[1].size() > 0) {
|
|
EventList * spo2 = session->AddEventList(OXI_SPO2, EVL_Waveform, 1.0, 0.0, 0.0, 0.0, dur / data[1].size());
|
|
spo2->AddWaveform(ti, (unsigned char *)data[1].data(), data[1].size(), dur);
|
|
}
|
|
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
static QString ts(qint64 msecs)
|
|
{
|
|
// TODO: make this UTC so that tests don't vary by where they're run
|
|
return QDateTime::fromMSecsSinceEpoch(msecs).toString(Qt::ISODate);
|
|
}
|
|
|
|
|
|
bool PRS1Import::ParseWaveforms()
|
|
{
|
|
int size = waveforms.size();
|
|
quint64 s1, s2;
|
|
|
|
|
|
int discontinuities = 0;
|
|
qint64 lastti=0;
|
|
EventList * bnd = nullptr; // Breathing Not Detected
|
|
|
|
for (int i=0; i < size; ++i) {
|
|
PRS1DataChunk * waveform = waveforms.at(i);
|
|
int num = waveform->waveformInfo.size();
|
|
|
|
int size = waveform->m_data.size();
|
|
if (size == 0) {
|
|
qDebug() << waveform->sessionid << waveform->timestamp << "empty?";
|
|
continue;
|
|
}
|
|
quint64 ti = quint64(waveform->timestamp) * 1000L;
|
|
quint64 dur = qint64(waveform->duration) * 1000L;
|
|
|
|
qint64 diff = ti - lastti;
|
|
if ((lastti != 0) && (diff == 1000 || diff == -1000)) {
|
|
// TODO: Evidently the machines' internal clock drifts slightly, and in some sessions that
|
|
// means two adjacent (5-minute) waveform chunks have have a +/- 1 second difference in
|
|
// their notion of the correct time, since the machines only record time at 1-second
|
|
// resolution. Presumably the real drift is fractional, but there's no way to tell from
|
|
// the data.
|
|
//
|
|
// Encore apparently drops the second chunk entirely if it overlaps with the first
|
|
// (even by 1 second), and inserts a 1-second gap in the data if it's 1 second later than
|
|
// the first ended.
|
|
//
|
|
// At worst in the former case it seems preferable to drop the overlap and then one
|
|
// additional second to mark the discontinuity. But depending how often these drifts
|
|
// occur, it may be possible to adjust all the data so that it's continuous. Alternatively,
|
|
// if it turns out overlapping waveform data always has overlapping identical values,
|
|
// it might be possible to drop the duplicated sample. Though that would mean that
|
|
// gaps are real, though potentially only by a single sample.
|
|
//
|
|
qDebug() << waveform->sessionid << "waveform discontinuity:" << (diff / 1000L) << "s @" << ts(waveform->timestamp * 1000L);
|
|
discontinuities++;
|
|
}
|
|
if ((diff > 1000) && (lastti != 0)) {
|
|
if (!bnd) {
|
|
bnd = session->AddEventList(PRS1_BND, EVL_Event);
|
|
}
|
|
// TODO: The machines' notion of BND appears to derive from the summary (maskoff/maskon)
|
|
// slices, but the waveform data (when present) does seem to agree. This should be confirmed
|
|
// once all summary parsers support slices.
|
|
if ((diff / 1000L) % 60) {
|
|
// Thus far all maskoff/maskon gaps have been multiples of 1 minute.
|
|
qDebug() << waveform->sessionid << "BND?" << (diff / 1000L) << "=" << ts(waveform->timestamp * 1000L) << "-" << ts(lastti);
|
|
}
|
|
bnd->AddEvent(ti, double(diff)/1000.0);
|
|
}
|
|
|
|
if (num > 1) {
|
|
float pressure_gain = 0.1F; // standard pressure gain
|
|
if (waveform->family == 5 && waveform->familyVersion == 3) {
|
|
// F5V3 uses a gain of 0.125 rather than 0.1 to allow for a maximum value of 30 cmH2O
|
|
pressure_gain = 0.125F; // TODO: this should be parameterized somewhere better, once we have a clear idea of which machines use this
|
|
}
|
|
|
|
// Process interleaved samples
|
|
QVector<QByteArray> data;
|
|
data.resize(num);
|
|
|
|
int pos = 0;
|
|
do {
|
|
for (int n=0; n < num; n++) {
|
|
int interleave = waveform->waveformInfo.at(n).interleave;
|
|
data[n].append(waveform->m_data.mid(pos, interleave));
|
|
pos += interleave;
|
|
}
|
|
} while (pos < size);
|
|
|
|
s1 = data[0].size();
|
|
s2 = data[1].size();
|
|
|
|
if (s1 > 0) {
|
|
EventList * flow = session->AddEventList(CPAP_FlowRate, EVL_Waveform, 1.0f, 0.0f, 0.0f, 0.0f, double(dur) / double(s1));
|
|
flow->AddWaveform(ti, (char *)data[0].data(), data[0].size(), dur);
|
|
}
|
|
|
|
if (s2 > 0) {
|
|
EventList * pres = session->AddEventList(CPAP_MaskPressureHi, EVL_Waveform, pressure_gain, 0.0f, 0.0f, 0.0f, double(dur) / double(s2));
|
|
pres->AddWaveform(ti, (unsigned char *)data[1].data(), data[1].size(), dur);
|
|
}
|
|
|
|
} else {
|
|
// Non interleaved, so can process it much faster
|
|
EventList * flow = session->AddEventList(CPAP_FlowRate, EVL_Waveform, 1.0f, 0.0f, 0.0f, 0.0f, double(dur) / double(waveform->m_data.size()));
|
|
flow->AddWaveform(ti, (char *)waveform->m_data.data(), waveform->m_data.size(), dur);
|
|
}
|
|
lastti = dur+ti;
|
|
}
|
|
|
|
if (discontinuities > 1) {
|
|
qWarning() << session->session() << "multiple discontinuities!" << discontinuities;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void PRS1Import::run()
|
|
{
|
|
if (mach->unsupported())
|
|
return;
|
|
|
|
if (ParseSession()) {
|
|
SaveSessionToDatabase();
|
|
}
|
|
}
|
|
|
|
|
|
bool PRS1Import::ParseSession(void)
|
|
{
|
|
bool ok = false;
|
|
bool save = false;
|
|
session = new Session(mach, sessionid);
|
|
|
|
do {
|
|
// TODO: There should be a way to distinguish between no-data-to-import vs. parsing errors
|
|
// (once we figure out what's benign and what isn't).
|
|
if (compliance != nullptr) {
|
|
ok = ImportCompliance();
|
|
if (!ok) {
|
|
//qWarning() << sessionid << "Error parsing compliance, skipping session";
|
|
break;
|
|
}
|
|
}
|
|
if (summary != nullptr) {
|
|
if (compliance != nullptr) {
|
|
qWarning() << sessionid << "Has both compliance and summary?!";
|
|
// Never seen this, but try the summary anyway.
|
|
}
|
|
ok = ImportSummary();
|
|
if (!ok) {
|
|
//qWarning() << sessionid << "Error parsing summary, skipping session";
|
|
break;
|
|
}
|
|
}
|
|
if (compliance == nullptr && summary == nullptr) {
|
|
qWarning() << sessionid << "No compliance or summary, skipping session";
|
|
break;
|
|
}
|
|
|
|
if (event != nullptr) {
|
|
ok = ParseEvents();
|
|
if (!ok) {
|
|
qWarning() << sessionid << "Error parsing events, proceeding anyway?";
|
|
}
|
|
}
|
|
|
|
if (!wavefile.isEmpty()) {
|
|
// Parse .005 Waveform file
|
|
waveforms = loader->ParseFile(wavefile);
|
|
waveforms = CoalesceWaveformChunks(waveforms);
|
|
if (session->eventlist.contains(CPAP_FlowRate)) {
|
|
if (waveforms.size() > 0) {
|
|
// Delete anything called "Flow rate" picked up in the events file if real data is present
|
|
session->destroyEvent(CPAP_FlowRate);
|
|
}
|
|
}
|
|
ok = ParseWaveforms();
|
|
if (!ok) {
|
|
qWarning() << sessionid << "Error parsing waveforms, proceeding anyway?";
|
|
}
|
|
}
|
|
|
|
if (!oxifile.isEmpty()) {
|
|
// Parse .006 Waveform file
|
|
oximetry = loader->ParseFile(oxifile);
|
|
oximetry = CoalesceWaveformChunks(oximetry);
|
|
ok = ParseOximetry();
|
|
if (!ok) {
|
|
qWarning() << sessionid << "Error parsing oximetry, proceeding anyway?";
|
|
}
|
|
}
|
|
|
|
if (session->first() > 0) {
|
|
if (session->last() < session->first()) {
|
|
// Compliance uses set_last() to set the session's last timestamp, so it
|
|
// won't reach this point.
|
|
if (compliance != nullptr) {
|
|
qWarning() << sessionid << "compliance didn't set session end?";
|
|
}
|
|
|
|
// Events and waveforms use updateLast() to set the session's last timestamp,
|
|
// so they should only reach this point if there was a problem parsing them.
|
|
if (event != nullptr || !wavefile.isEmpty() || !oxifile.isEmpty()) {
|
|
qWarning() << sessionid << "Downgrading session to summary only";
|
|
}
|
|
session->setSummaryOnly(true);
|
|
|
|
// Only use the summary's duration if the session's duration couldn't be
|
|
// derived from events or waveforms.
|
|
// TODO: Revisit this once summary parsing is reliable.
|
|
session->really_set_last(session->first()+(qint64(summary_duration) * 1000L));
|
|
}
|
|
save = true;
|
|
} else {
|
|
qWarning() << sessionid << "missing start time";
|
|
}
|
|
} while (false);
|
|
|
|
return save;
|
|
}
|
|
|
|
|
|
void PRS1Import::SaveSessionToDatabase(void)
|
|
{
|
|
// Make sure it's saved
|
|
session->SetChanged(true);
|
|
|
|
// Add the session to the database
|
|
loader->addSession(session);
|
|
|
|
// Update indexes, process waveform and perform flagging
|
|
session->UpdateSummaries();
|
|
|
|
// Save is not threadsafe
|
|
loader->saveMutex.lock();
|
|
session->Store(mach->getDataPath());
|
|
loader->saveMutex.unlock();
|
|
|
|
// Unload them from memory
|
|
session->TrashEvents();
|
|
}
|
|
|
|
|
|
QList<PRS1DataChunk *> PRS1Loader::ParseFile(const QString & path)
|
|
{
|
|
QList<PRS1DataChunk *> CHUNKS;
|
|
|
|
if (path.isEmpty()) {
|
|
// ParseSession passes empty filepaths for waveforms if none exist.
|
|
//qWarning() << path << "ParseFile given empty path";
|
|
return CHUNKS;
|
|
}
|
|
|
|
QFile f(path);
|
|
|
|
if (!f.exists()) {
|
|
qWarning() << path << "missing";
|
|
return CHUNKS;
|
|
}
|
|
|
|
if (!f.open(QIODevice::ReadOnly)) {
|
|
qWarning() << path << "can't open";
|
|
return CHUNKS;
|
|
}
|
|
|
|
PRS1DataChunk *chunk = nullptr, *lastchunk = nullptr;
|
|
|
|
int cnt = 0;
|
|
|
|
int cruft = 0;
|
|
int firstsession = 0;
|
|
|
|
do {
|
|
chunk = PRS1DataChunk::ParseNext(f);
|
|
if (chunk == nullptr) {
|
|
break;
|
|
}
|
|
chunk->SetIndex(cnt); // for logging/debugging purposes
|
|
|
|
if (lastchunk != nullptr) {
|
|
// If there's any mismatch between header information, try and skip the block
|
|
// This probably isn't the best approach for dealing with block corruption :/
|
|
if ((lastchunk->fileVersion != chunk->fileVersion)
|
|
|| (lastchunk->ext != chunk->ext)
|
|
|| (lastchunk->family != chunk->family)
|
|
|| (lastchunk->familyVersion != chunk->familyVersion)
|
|
|| (lastchunk->htype != chunk->htype)) {
|
|
qWarning() << path << "unexpected header data, skipping";
|
|
|
|
// TODO: Find a sample of this problem to see if the below approach has any
|
|
// value, or whether we should just drop the chunk.
|
|
QByteArray junk = f.read(lastchunk->blockSize - chunk->m_header.size());
|
|
|
|
Q_UNUSED(junk)
|
|
if (lastchunk->ext == 5) {
|
|
// The data is random crap
|
|
// lastchunk->m_data.append(junk.mid(lastheadersize-16));
|
|
}
|
|
++cruft;
|
|
// quit after 3 attempts
|
|
if (cruft > 3) {
|
|
qWarning() << path << "too many unexpected headers, bailing";
|
|
break;
|
|
}
|
|
|
|
cnt++;
|
|
delete chunk;
|
|
continue;
|
|
// Corrupt header.. skip it.
|
|
}
|
|
}
|
|
|
|
if (!firstsession) {
|
|
firstsession = chunk->sessionid;
|
|
}
|
|
|
|
CHUNKS.append(chunk);
|
|
|
|
lastchunk = chunk;
|
|
cnt++;
|
|
} while (!f.atEnd());
|
|
|
|
return CHUNKS;
|
|
}
|
|
|
|
|
|
PRS1DataChunk::PRS1DataChunk(QFile & f)
|
|
{
|
|
m_path = QFileInfo(f).canonicalFilePath();
|
|
}
|
|
|
|
PRS1DataChunk::~PRS1DataChunk()
|
|
{
|
|
for (int i=0; i < m_parsedData.count(); i++) {
|
|
PRS1ParsedEvent* e = m_parsedData.at(i);
|
|
delete e;
|
|
}
|
|
}
|
|
|
|
|
|
PRS1DataChunk* PRS1DataChunk::ParseNext(QFile & f)
|
|
{
|
|
PRS1DataChunk* out_chunk = nullptr;
|
|
PRS1DataChunk* chunk = new PRS1DataChunk(f);
|
|
|
|
do {
|
|
// Parse the header and calculate its checksum.
|
|
bool ok = chunk->ReadHeader(f);
|
|
if (!ok) {
|
|
break;
|
|
}
|
|
|
|
// Make sure the calculated checksum matches the stored checksum.
|
|
if (chunk->calcChecksum != chunk->storedChecksum) {
|
|
qWarning() << chunk->m_path << "header checksum calc" << chunk->calcChecksum << "!= stored" << chunk->storedChecksum;
|
|
break;
|
|
}
|
|
|
|
// Log mismatched waveform session IDs
|
|
if (chunk->htype == PRS1_HTYPE_INTERVAL) {
|
|
QFileInfo fi(f);
|
|
bool numeric;
|
|
int sessionid_base = (chunk->fileVersion == 2 ? 10 : 16);
|
|
if (chunk->family == 3 && chunk->familyVersion >= 3) sessionid_base = 16;
|
|
QString session_s = fi.fileName().section(".", 0, -2);
|
|
quint32 sid = session_s.toInt(&numeric, sessionid_base);
|
|
if (!numeric || sid != chunk->sessionid) {
|
|
qDebug() << chunk->m_path << chunk->sessionid;
|
|
}
|
|
}
|
|
|
|
// Read the block's data and calculate the block CRC.
|
|
ok = chunk->ReadData(f);
|
|
if (!ok) {
|
|
break;
|
|
}
|
|
|
|
// Make sure the calculated CRC over the entire chunk (header and data) matches the stored CRC.
|
|
if (chunk->calcCrc != chunk->storedCrc) {
|
|
// corrupt data block.. bleh..
|
|
qWarning() << chunk->m_path << "@" << chunk->m_filepos << "block CRC calc" << hex << chunk->calcCrc << "!= stored" << hex << chunk->storedCrc;
|
|
break;
|
|
}
|
|
|
|
// Only return the chunk if it has passed all tests above.
|
|
out_chunk = chunk;
|
|
} while (false);
|
|
|
|
if (out_chunk == nullptr) delete chunk;
|
|
return out_chunk;
|
|
}
|
|
|
|
|
|
bool PRS1DataChunk::ReadHeader(QFile & f)
|
|
{
|
|
bool ok = false;
|
|
do {
|
|
// Read common header fields.
|
|
this->m_filepos = f.pos();
|
|
this->m_header = f.read(15);
|
|
if (this->m_header.size() != 15) {
|
|
qWarning() << this->m_path << "file too short?";
|
|
break;
|
|
}
|
|
|
|
unsigned char * header = (unsigned char *)this->m_header.data();
|
|
this->fileVersion = header[0]; // Correlates to DataFileVersion in PROP[erties].TXT, only 2 or 3 has ever been observed
|
|
this->blockSize = (header[2] << 8) | header[1];
|
|
this->htype = header[3]; // 00 = normal, 01=waveform
|
|
this->family = header[4];
|
|
this->familyVersion = header[5];
|
|
this->ext = header[6];
|
|
this->sessionid = (header[10] << 24) | (header[9] << 16) | (header[8] << 8) | header[7];
|
|
this->timestamp = (header[14] << 24) | (header[13] << 16) | (header[12] << 8) | header[11];
|
|
|
|
// Do a few early sanity checks before any variable-length header data.
|
|
if (this->blockSize == 0) {
|
|
qWarning() << this->m_path << "blocksize 0?";
|
|
break;
|
|
}
|
|
if (this->fileVersion < 2 || this->fileVersion > 3) {
|
|
qWarning() << this->m_path << "@" << hex << this->m_filepos << "Never seen PRS1 header version < 2 or > 3 before";
|
|
break;
|
|
}
|
|
if (this->htype != PRS1_HTYPE_NORMAL && this->htype != PRS1_HTYPE_INTERVAL) {
|
|
qWarning() << this->m_path << "unexpected htype:" << this->htype;
|
|
break;
|
|
}
|
|
|
|
// Read format-specific variable-length header data.
|
|
bool hdr_ok = false;
|
|
if (this->htype != PRS1_HTYPE_INTERVAL) { // Not just waveforms: the 1160P uses this for its .002 events file.
|
|
// Not a waveform/interval chunk
|
|
switch (this->fileVersion) {
|
|
case 2:
|
|
hdr_ok = ReadNormalHeaderV2(f);
|
|
break;
|
|
case 3:
|
|
hdr_ok = ReadNormalHeaderV3(f);
|
|
break;
|
|
default:
|
|
//hdr_ok remains false, warning is above
|
|
break;
|
|
}
|
|
} else {
|
|
// Waveform/interval chunk
|
|
hdr_ok = ReadWaveformHeader(f);
|
|
}
|
|
if (!hdr_ok) {
|
|
break;
|
|
}
|
|
|
|
// The 8bit checksum comes at the end.
|
|
QByteArray checksum = f.read(1);
|
|
if (checksum.size() < 1) {
|
|
qWarning() << this->m_path << "read error header checksum";
|
|
break;
|
|
}
|
|
this->storedChecksum = checksum.data()[0];
|
|
|
|
// Calculate 8bit additive header checksum.
|
|
header = (unsigned char *)this->m_header.data(); // important because its memory location could move
|
|
int header_size = this->m_header.size();
|
|
quint8 achk=0;
|
|
for (int i=0; i < header_size; i++) {
|
|
achk += header[i];
|
|
}
|
|
this->calcChecksum = achk;
|
|
|
|
// Append the stored checksum to the raw data *after* calculating the checksum on the preceding data.
|
|
this->m_header.append(checksum);
|
|
|
|
ok = true;
|
|
} while (false);
|
|
|
|
return ok;
|
|
}
|
|
|
|
|
|
bool PRS1DataChunk::ReadNormalHeaderV2(QFile & /*f*/)
|
|
{
|
|
this->m_headerblock = QByteArray();
|
|
return true; // always OK
|
|
}
|
|
|
|
|
|
bool PRS1DataChunk::ReadNormalHeaderV3(QFile & f)
|
|
{
|
|
bool ok = false;
|
|
unsigned char * header;
|
|
QByteArray headerB2;
|
|
|
|
// This is a new machine, byte 15 is header data block length
|
|
// followed by variable, data byte pairs
|
|
do {
|
|
QByteArray extra = f.read(1);
|
|
if (extra.size() < 1) {
|
|
qWarning() << this->m_path << "read error extended header";
|
|
break;
|
|
}
|
|
this->m_header.append(extra);
|
|
header = (unsigned char *)this->m_header.data();
|
|
|
|
int hdb_len = header[15];
|
|
int hdb_size = hdb_len * 2;
|
|
|
|
headerB2 = f.read(hdb_size);
|
|
if (headerB2.size() != hdb_size) {
|
|
qWarning() << this->m_path << "read error in extended header";
|
|
break;
|
|
}
|
|
this->m_headerblock = headerB2;
|
|
|
|
this->m_header.append(headerB2);
|
|
header = (unsigned char *)this->m_header.data();
|
|
const unsigned char * hd = (unsigned char *)headerB2.constData();
|
|
int pos = 0;
|
|
int recs = header[15];
|
|
for (int i=0; i<recs; i++) {
|
|
this->hblock[hd[pos]] = hd[pos+1];
|
|
pos += 2;
|
|
}
|
|
|
|
ok = true;
|
|
} while (false);
|
|
|
|
return ok;
|
|
}
|
|
|
|
|
|
bool PRS1DataChunk::ReadWaveformHeader(QFile & f)
|
|
{
|
|
bool ok = false;
|
|
unsigned char * header;
|
|
do {
|
|
// Read the fixed-length waveform header.
|
|
QByteArray extra = f.read(4);
|
|
if (extra.size() != 4) {
|
|
qWarning() << this->m_path << "read error in waveform header";
|
|
break;
|
|
}
|
|
this->m_header.append(extra);
|
|
header = (unsigned char *)this->m_header.data();
|
|
|
|
// Parse the fixed-length portion.
|
|
this->interval_count = header[0x0f] | header[0x10] << 8;
|
|
this->interval_seconds = header[0x11]; // not always 1 after all
|
|
this->duration = this->interval_count * this->interval_seconds; // ??? the last entry doesn't always seem to be a full interval?
|
|
quint8 wvfm_signals = header[0x12];
|
|
|
|
// Read the variable-length data + trailing byte.
|
|
int ws_size = (this->fileVersion == 3) ? 4 : 3;
|
|
int sbsize = wvfm_signals * ws_size + 1;
|
|
|
|
extra = f.read(sbsize);
|
|
if (extra.size() != sbsize) {
|
|
qWarning() << this->m_path << "read error in waveform header 2";
|
|
break;
|
|
}
|
|
this->m_header.append(extra);
|
|
header = (unsigned char *)this->m_header.data();
|
|
|
|
// Parse the variable-length waveform information.
|
|
// TODO: move these checks into the parser, after the header checksum has been verified
|
|
int pos = 0x13;
|
|
for (int i = 0; i < wvfm_signals; ++i) {
|
|
quint8 kind = header[pos];
|
|
if (kind != i) { // always seems to range from 0...wvfm_signals-1, alert if not
|
|
qWarning() << this->m_path << kind << "!=" << i << "waveform kind";
|
|
//break; // don't break to avoid changing behavior (for now)
|
|
}
|
|
quint16 interleave = header[pos + 1] | header[pos + 2] << 8; // samples per interval
|
|
if (this->fileVersion == 2) {
|
|
this->waveformInfo.push_back(PRS1Waveform(interleave, kind));
|
|
pos += 3;
|
|
} else if (this->fileVersion == 3) {
|
|
int always_8 = header[pos + 3]; // sample size in bits?
|
|
if (always_8 != 8) {
|
|
qWarning() << this->m_path << always_8 << "!= 8 in waveform header";
|
|
//break; // don't break to avoid changing behavior (for now)
|
|
}
|
|
this->waveformInfo.push_back(PRS1Waveform(interleave, kind));
|
|
pos += 4;
|
|
}
|
|
}
|
|
|
|
// And the trailing byte, whatever it is.
|
|
int always_0 = header[pos];
|
|
if (always_0 != 0) {
|
|
qWarning() << this->m_path << always_0 << "!= 0 in waveform header";
|
|
//break; // don't break to avoid changing behavior (for now)
|
|
}
|
|
|
|
ok = true;
|
|
} while (false);
|
|
|
|
return ok;
|
|
}
|
|
|
|
|
|
bool PRS1DataChunk::ReadData(QFile & f)
|
|
{
|
|
bool ok = false;
|
|
do {
|
|
// Read data block
|
|
int data_size = this->blockSize - this->m_header.size();
|
|
if (data_size < 0) {
|
|
qWarning() << this->m_path << "chunk size smaller than header";
|
|
break;
|
|
}
|
|
this->m_data = f.read(data_size);
|
|
if (this->m_data.size() < data_size) {
|
|
qWarning() << this->m_path << "less data in file than specified in header";
|
|
break;
|
|
}
|
|
|
|
// Extract the stored CRC from the data buffer and calculate the current CRC.
|
|
if (this->fileVersion==3) {
|
|
// The last 4 bytes contain a CRC32 checksum of the data.
|
|
if (!ExtractStoredCrc(4)) {
|
|
break;
|
|
}
|
|
this->calcCrc = CRC32wchar((unsigned char *)this->m_data.data(), this->m_data.size());
|
|
} else {
|
|
// The last 2 bytes contain a CRC16 checksum of the data.
|
|
if (!ExtractStoredCrc(2)) {
|
|
break;
|
|
}
|
|
this->calcCrc = CRC16((unsigned char *)this->m_data.data(), this->m_data.size());
|
|
}
|
|
|
|
ok = true;
|
|
} while (false);
|
|
|
|
return ok;
|
|
}
|
|
|
|
|
|
bool PRS1DataChunk::ExtractStoredCrc(int size)
|
|
{
|
|
// Make sure there's enough data for the CRC.
|
|
int offset = this->m_data.size() - size;
|
|
if (offset < 0) {
|
|
qWarning() << this->m_path << "chunk truncated";
|
|
return false;
|
|
}
|
|
|
|
// Read the last 16- or 32-bit little-endian integer.
|
|
quint32 storedCrc = 0;
|
|
unsigned char* data = (unsigned char*)this->m_data.data();
|
|
for (int i=0; i < size; i++) {
|
|
storedCrc |= data[offset+i] << (8*i);
|
|
}
|
|
this->storedCrc = storedCrc;
|
|
|
|
// Drop the CRC from the data.
|
|
this->m_data.chop(size);
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
void InitModelMap()
|
|
{
|
|
ModelMap[0x34] = QObject::tr("RemStar Pro with C-Flex+"); // 450/460P
|
|
ModelMap[0x35] = QObject::tr("RemStar Auto with A-Flex"); // 550/560P
|
|
ModelMap[0x36] = QObject::tr("RemStar BiPAP Pro with Bi-Flex");
|
|
ModelMap[0x37] = QObject::tr("RemStar BiPAP Auto with Bi-Flex");
|
|
ModelMap[0x38] = QObject::tr("RemStar Plus"); // 150/250P/260P
|
|
ModelMap[0x41] = QObject::tr("BiPAP autoSV Advanced");
|
|
ModelMap[0x4a] = QObject::tr("BiPAP autoSV Advanced 60 Series");
|
|
ModelMap[0x4E] = QObject::tr("BiPAP AVAPS");
|
|
ModelMap[0x58] = QObject::tr("CPAP"); // guessing
|
|
ModelMap[0x59] = QObject::tr("CPAP Pro"); // guessing
|
|
ModelMap[0x5A] = QObject::tr("Auto CPAP");
|
|
ModelMap[0x5B] = QObject::tr("BiPAP Pro"); // guessing
|
|
ModelMap[0x5C] = QObject::tr("Auto BiPAP");
|
|
}
|
|
|
|
bool initialized = false;
|
|
|
|
using namespace schema;
|
|
|
|
Channel PRS1Channels;
|
|
|
|
void PRS1Loader::initChannels()
|
|
{
|
|
Channel * chan = nullptr;
|
|
|
|
channel.add(GRP_CPAP, new Channel(CPAP_PressurePulse = 0x1009, MINOR_FLAG, MT_CPAP, SESSION,
|
|
"PressurePulse",
|
|
QObject::tr("Pressure Pulse"),
|
|
QObject::tr("A pulse of pressure 'pinged' to detect a closed airway."),
|
|
QObject::tr("PP"),
|
|
STR_UNIT_EventsPerHour, DEFAULT, QColor("dark red")));
|
|
|
|
channel.add(GRP_CPAP, chan = new Channel(PRS1_FlexMode = 0xe105, SETTING, MT_CPAP, SESSION,
|
|
"PRS1FlexMode", QObject::tr("Flex Mode"),
|
|
QObject::tr("PRS1 pressure relief mode."),
|
|
QObject::tr("Flex Mode"),
|
|
"", LOOKUP, Qt::green));
|
|
|
|
|
|
chan->addOption(FLEX_None, STR_TR_None);
|
|
chan->addOption(FLEX_CFlex, QObject::tr("C-Flex"));
|
|
chan->addOption(FLEX_CFlexPlus, QObject::tr("C-Flex+"));
|
|
chan->addOption(FLEX_AFlex, QObject::tr("A-Flex"));
|
|
chan->addOption(FLEX_RiseTime, QObject::tr("Rise Time"));
|
|
chan->addOption(FLEX_BiFlex, QObject::tr("Bi-Flex"));
|
|
|
|
channel.add(GRP_CPAP, chan = new Channel(PRS1_FlexLevel = 0xe106, SETTING, MT_CPAP, SESSION,
|
|
"PRS1FlexSet",
|
|
QObject::tr("Flex Level"),
|
|
QObject::tr("PRS1 pressure relief setting."),
|
|
QObject::tr("Flex Level"),
|
|
"", LOOKUP, Qt::blue));
|
|
|
|
chan->addOption(0, STR_TR_Off);
|
|
chan->addOption(1, QObject::tr("x1"));
|
|
chan->addOption(2, QObject::tr("x2"));
|
|
chan->addOption(3, QObject::tr("x3"));
|
|
chan->addOption(4, QObject::tr("x4"));
|
|
chan->addOption(5, QObject::tr("x5"));
|
|
|
|
channel.add(GRP_CPAP, chan = new Channel(PRS1_HumidStatus = 0xe101, SETTING, MT_CPAP, SESSION,
|
|
"PRS1HumidStat",
|
|
QObject::tr("Humidifier Status"),
|
|
QObject::tr("PRS1 humidifier connected?"),
|
|
QObject::tr("Humidifier Status"),
|
|
"", LOOKUP, Qt::green));
|
|
chan->addOption(0, QObject::tr("Disconnected"));
|
|
chan->addOption(1, QObject::tr("Connected"));
|
|
|
|
channel.add(GRP_CPAP, chan = new Channel(PRS1_HeatedTubing = 0xe10d, SETTING, MT_CPAP, SESSION,
|
|
"PRS1HeatedTubing",
|
|
QObject::tr("Heated Tubing"),
|
|
QObject::tr("Heated Tubing Connected"),
|
|
QObject::tr("Heated Tubing"),
|
|
"", LOOKUP, Qt::green));
|
|
chan->addOption(0, QObject::tr("Yes"));
|
|
chan->addOption(1, QObject::tr("No"));
|
|
|
|
channel.add(GRP_CPAP, chan = new Channel(PRS1_HumidLevel = 0xe102, SETTING, MT_CPAP, SESSION,
|
|
"PRS1HumidLevel",
|
|
QObject::tr("Humidification Level"),
|
|
QObject::tr("PRS1 Humidification level"),
|
|
QObject::tr("Humid. Lvl."),
|
|
"", LOOKUP, Qt::green));
|
|
chan->addOption(0, STR_TR_Off);
|
|
chan->addOption(1, QObject::tr("x1"));
|
|
chan->addOption(2, QObject::tr("x2"));
|
|
chan->addOption(3, QObject::tr("x3"));
|
|
chan->addOption(4, QObject::tr("x4"));
|
|
chan->addOption(5, QObject::tr("x5"));
|
|
|
|
channel.add(GRP_CPAP, chan = new Channel(PRS1_SysOneResistStat = 0xe103, SETTING, MT_CPAP, SESSION,
|
|
"SysOneResistStat",
|
|
QObject::tr("System One Resistance Status"),
|
|
QObject::tr("System One Resistance Status"),
|
|
QObject::tr("Sys1 Resist. Status"),
|
|
"", LOOKUP, Qt::green));
|
|
chan->addOption(0, STR_TR_Off);
|
|
chan->addOption(1, STR_TR_On);
|
|
|
|
channel.add(GRP_CPAP, chan = new Channel(PRS1_SysOneResistSet = 0xe104, SETTING, MT_CPAP, SESSION,
|
|
"SysOneResistSet",
|
|
QObject::tr("System One Resistance Setting"),
|
|
QObject::tr("System One Mask Resistance Setting"),
|
|
QObject::tr("Sys1 Resist. Set"),
|
|
"", LOOKUP, Qt::green));
|
|
chan->addOption(0, STR_TR_Off);
|
|
chan->addOption(1, QObject::tr("x1"));
|
|
chan->addOption(2, QObject::tr("x2"));
|
|
chan->addOption(3, QObject::tr("x3"));
|
|
chan->addOption(4, QObject::tr("x4"));
|
|
chan->addOption(5, QObject::tr("x5"));
|
|
|
|
channel.add(GRP_CPAP, chan = new Channel(PRS1_HoseDiam = 0xe107, SETTING, MT_CPAP, SESSION,
|
|
"PRS1HoseDiam",
|
|
QObject::tr("Hose Diameter"),
|
|
QObject::tr("Diameter of primary CPAP hose"),
|
|
QObject::tr("Hose Diameter"),
|
|
"", LOOKUP, Qt::green));
|
|
chan->addOption(0, QObject::tr("22mm"));
|
|
chan->addOption(1, QObject::tr("15mm"));
|
|
|
|
channel.add(GRP_CPAP, chan = new Channel(PRS1_SysOneResistStat = 0xe108, SETTING, MT_CPAP, SESSION,
|
|
"SysOneLock",
|
|
QObject::tr("System One Resistance Lock"),
|
|
QObject::tr("Whether System One resistance settings are available to you."),
|
|
QObject::tr("Sys1 Resist. Lock"),
|
|
"", LOOKUP, Qt::green));
|
|
chan->addOption(0, STR_TR_Off);
|
|
chan->addOption(1, STR_TR_On);
|
|
|
|
channel.add(GRP_CPAP, chan = new Channel(PRS1_AutoOn = 0xe109, SETTING, MT_CPAP, SESSION,
|
|
"PRS1AutoOn",
|
|
QObject::tr("Auto On"),
|
|
QObject::tr("A few breaths automatically starts machine"),
|
|
QObject::tr("Auto On"),
|
|
"", LOOKUP, Qt::green));
|
|
chan->addOption(0, STR_TR_Off);
|
|
chan->addOption(1, STR_TR_On);
|
|
|
|
channel.add(GRP_CPAP, chan = new Channel(PRS1_AutoOff = 0xe10a, SETTING, MT_CPAP, SESSION,
|
|
"PRS1AutoOff",
|
|
QObject::tr("Auto Off"),
|
|
QObject::tr("Machine automatically switches off"),
|
|
QObject::tr("Auto Off"),
|
|
"", LOOKUP, Qt::green));
|
|
chan->addOption(0, STR_TR_Off);
|
|
chan->addOption(1, STR_TR_On);
|
|
|
|
channel.add(GRP_CPAP, chan = new Channel(PRS1_MaskAlert = 0xe10b, SETTING, MT_CPAP, SESSION,
|
|
"PRS1MaskAlert",
|
|
QObject::tr("Mask Alert"),
|
|
QObject::tr("Whether or not machine allows Mask checking."),
|
|
QObject::tr("Mask Alert"),
|
|
"", LOOKUP, Qt::green));
|
|
chan->addOption(0, STR_TR_Off);
|
|
chan->addOption(1, STR_TR_On);
|
|
|
|
channel.add(GRP_CPAP, chan = new Channel(PRS1_MaskAlert = 0xe10c, SETTING, MT_CPAP, SESSION,
|
|
"PRS1ShowAHI",
|
|
QObject::tr("Show AHI"),
|
|
QObject::tr("Whether or not machine shows AHI via LCD panel."),
|
|
QObject::tr("Show AHI"),
|
|
"", LOOKUP, Qt::green));
|
|
chan->addOption(0, STR_TR_Off);
|
|
chan->addOption(1, STR_TR_On);
|
|
|
|
// <channel id="0xe10e" class="setting" scope="!session" name="PRS1Mode" details="PAP Mode" label="PAP Mode" type="integer" link="0x1200">
|
|
// <Option id="0" value="CPAP"/>
|
|
// <Option id="1" value="Auto"/>
|
|
// <Option id="2" value="BIPAP"/>
|
|
// <Option id="3" value="AutoSV"/>
|
|
// </channel>
|
|
|
|
QString unknowndesc=QObject::tr("Unknown PRS1 Code %1");
|
|
QString unknownname=QObject::tr("PRS1_%1");
|
|
QString unknownshort=QObject::tr("PRS1_%1");
|
|
|
|
channel.add(GRP_CPAP, new Channel(PRS1_00 = 0x1150, UNKNOWN, MT_CPAP, SESSION,
|
|
"PRS1_00",
|
|
QString(unknownname).arg(0,2,16,QChar('0')),
|
|
QString(unknowndesc).arg(0,2,16,QChar('0')),
|
|
QString(unknownshort).arg(0,2,16,QChar('0')),
|
|
STR_UNIT_Unknown,
|
|
DEFAULT, QColor("black")));
|
|
|
|
channel.add(GRP_CPAP, new Channel(PRS1_01 = 0x1151, UNKNOWN, MT_CPAP, SESSION,
|
|
"PRS1_01",
|
|
QString(unknownname).arg(1,2,16,QChar('0')),
|
|
QString(unknowndesc).arg(1,2,16,QChar('0')),
|
|
QString(unknownshort).arg(1,2,16,QChar('0')),
|
|
STR_UNIT_Unknown,
|
|
DEFAULT, QColor("black")));
|
|
|
|
channel.add(GRP_CPAP, new Channel(PRS1_08 = 0x1152, UNKNOWN, MT_CPAP, SESSION,
|
|
"PRS1_08",
|
|
QString(unknownname).arg(8,2,16,QChar('0')),
|
|
QString(unknowndesc).arg(8,2,16,QChar('0')),
|
|
QString(unknownshort).arg(8,2,16,QChar('0')),
|
|
STR_UNIT_Unknown,
|
|
DEFAULT, QColor("black")));
|
|
|
|
channel.add(GRP_CPAP, new Channel(PRS1_0A = 0x1154, UNKNOWN, MT_CPAP, SESSION,
|
|
"PRS1_0A",
|
|
QString(unknownname).arg(0xa,2,16,QChar('0')),
|
|
QString(unknowndesc).arg(0xa,2,16,QChar('0')),
|
|
QString(unknownshort).arg(0xa,2,16,QChar('0')),
|
|
STR_UNIT_Unknown,
|
|
DEFAULT, QColor("black")));
|
|
channel.add(GRP_CPAP, new Channel(PRS1_0B = 0x1155, UNKNOWN, MT_CPAP, SESSION,
|
|
"PRS1_0B",
|
|
QString(unknownname).arg(0xb,2,16,QChar('0')),
|
|
QString(unknowndesc).arg(0xb,2,16,QChar('0')),
|
|
QString(unknownshort).arg(0xb,2,16,QChar('0')),
|
|
STR_UNIT_Unknown,
|
|
DEFAULT, QColor("black")));
|
|
channel.add(GRP_CPAP, new Channel(PRS1_0C = 0x1156, UNKNOWN, MT_CPAP, SESSION,
|
|
"PRS1_0C",
|
|
QString(unknownname).arg(0xc,2,16,QChar('0')),
|
|
QString(unknowndesc).arg(0xc,2,16,QChar('0')),
|
|
QString(unknownshort).arg(0xc,2,16,QChar('0')),
|
|
STR_UNIT_Unknown,
|
|
DEFAULT, QColor("black")));
|
|
channel.add(GRP_CPAP, new Channel(PRS1_0E = 0x1157, UNKNOWN, MT_CPAP, SESSION,
|
|
"PRS1_0E",
|
|
QString(unknownname).arg(0xe,2,16,QChar('0')),
|
|
QString(unknowndesc).arg(0xe,2,16,QChar('0')),
|
|
QString(unknownshort).arg(0xe,2,16,QChar('0')),
|
|
STR_UNIT_Unknown,
|
|
DEFAULT, QColor("black")));
|
|
channel.add(GRP_CPAP, new Channel(PRS1_BND = 0x1159, SPAN, MT_CPAP, SESSION,
|
|
"PRS1_BND",
|
|
QObject::tr("Breathing Not Detected"),
|
|
QObject::tr("A period during a session where the machine could not detect flow."),
|
|
QObject::tr("BND"),
|
|
STR_UNIT_Unknown,
|
|
DEFAULT, QColor("light purple")));
|
|
|
|
channel.add(GRP_CPAP, new Channel(PRS1_15 = 0x115A, UNKNOWN, MT_CPAP, SESSION,
|
|
"PRS1_15",
|
|
QString(unknownname).arg(0x15,2,16,QChar('0')),
|
|
QString(unknowndesc).arg(0x15,2,16,QChar('0')),
|
|
QString(unknownshort).arg(0x15,2,16,QChar('0')),
|
|
STR_UNIT_Unknown,
|
|
DEFAULT, QColor("black")));
|
|
|
|
|
|
channel.add(GRP_CPAP, new Channel(PRS1_TimedBreath = 0x1180, MINOR_FLAG, MT_CPAP, SESSION,
|
|
"PRS1TimedBreath",
|
|
QObject::tr("Timed Breath"),
|
|
QObject::tr("Machine Initiated Breath"),
|
|
QObject::tr("TB"),
|
|
STR_UNIT_Unknown,
|
|
DEFAULT, QColor("black")));
|
|
}
|
|
|
|
void PRS1Loader::Register()
|
|
{
|
|
if (initialized) { return; }
|
|
|
|
qDebug() << "Registering PRS1Loader";
|
|
RegisterLoader(new PRS1Loader());
|
|
InitModelMap();
|
|
initialized = true;
|
|
}
|
|
|
|
/* Thanks to SleepyCPAP :)
|
|
CODE ERROR DESCRIPTION ERROR TYPE ERROR CATEGORY
|
|
1 SOFTWARE STOP STOP General Errors
|
|
2 Not Used General Errors
|
|
3 INT RAM REBOOT General Errors
|
|
4 NULL PTR REBOOT General Errors
|
|
5 DATA REBOOT General Errors
|
|
6 STATE MACHINE REBOOT General Errors
|
|
7 SOFTWARE REBOOT General Errors
|
|
8-9 Not Used General Errors
|
|
10 WDOG TEST RAM REBOOT Watchdog & Timer Errors
|
|
11 WDOG TEST REBOOT Watchdog & Timer Errors
|
|
12 BACKGROUND WDOG NO CARD REBOOT Watchdog & Timer Errors
|
|
13 BACKGROUND WDOG SD CARD REBOOT Watchdog & Timer Errors
|
|
14 WDOG LOWRES TIMER REBOOT Watchdog & Timer Errors
|
|
15 CYCLE HANDLER OVERRUN REBOOT Watchdog & Timer Errors
|
|
16 RASP RESTORE TIMEOUT CONTINUE Watchdog & Timer Errors
|
|
17 ONEMS HANDLER OVERRUN REBOOT Watchdog & Timer Errors
|
|
18 Not Used Watchdog & Timer Errors
|
|
19 WDOG TIMEOUT REBOOT Watchdog & Timer Errors
|
|
20 MOTOR SPINUP FLUX LOW REBOOT Motor/Blower Errors
|
|
21 MOTOR VBUS HIGH STOP Motor/Blower Errors
|
|
22 MOTOR FLUX MAGNITUDE REBOOT Motor/Blower Errors
|
|
23 MOTOR OVERSPEED REBOOT Motor/Blower Errors
|
|
24 MOTOR SPEED REVERSE REBOOT Motor/Blower Errors
|
|
25 MOTOR THERMISTOR OPEN CONTINUE Motor/Blower Errors
|
|
26 MOTOR THERMISTOR SHORTED CONTINUE Motor/Blower Errors
|
|
27 MOTOR RL NOCONVERGE STOP Motor/Blower Errors
|
|
28 NEGATIVE QUADRATURE VOLTAGE VECTOR REBOOT Motor/Blower Errors
|
|
29 VBUS GAIN ZERO: REBOOT Motor/Blower Errors
|
|
30 MOTOR SPINUP FLUX HIGH REBOOT Motor/Blower Errors
|
|
31 (incorrect power supply - 60series) Motor/Blower Errors
|
|
32-39 Not Used Motor/Blower Errors
|
|
40 NVRAM REBOOT NVRAM Low Level Errors
|
|
41 STORAGE UNIT RAM REBOOT NVRAM Low Level Errors
|
|
42 UNABLE TO OBTAIN BUS REBOOT NVRAM Low Level Errors
|
|
43 NVRAM NO CALLBACK OCCURRED REBOOT NVRAM Low Level Errors
|
|
44 NV BUFFER NULL REBOOT NVRAM Low Level Errors
|
|
45 NV CALLBACK NULL REBOOT NVRAM Low Level Errors
|
|
46 NV ZERO LENGTH REBOOT NVRAM Low Level Errors
|
|
47 NVRAM INVALID BYTES XFRRED REBOOT NVRAM Low Level Errors
|
|
48-49 Not Used NVRAM Low Level Errors
|
|
50 DAILY VALUES CORRUPT LOG ONLY NVRAM Unit Related Errors
|
|
51 CORRUPT COMPLIANCE LOG CONTINUE NVRAM Unit Related Errors
|
|
52 CORRUPT COMPLIANCE CB CONTINUE NVRAM Unit Related Errors
|
|
53 COMP LOG SEM TIMEOUT CONTINUE NVRAM Unit Related Errors
|
|
54 COMPLOG REQS OVERFLOW REBOOT NVRAM Unit Related Errors
|
|
55 THERAPY QUEUE FULL CONTINUE NVRAM Unit Related Errors
|
|
56 COMPLOG PACKET STATUS REBOOT NVRAM Unit Related Errors
|
|
57 SESS OBS QUEUE OVF REBOOT NVRAM Unit Related Errors
|
|
58 SESS OBS NO CALLBACK REBOOT NVRAM Unit Related Errors
|
|
59 Not Used NVRAM Unit Related Errors
|
|
60 UNSUPPORTED HARDWARE REBOOT General Hardware Errors
|
|
61 PLL UNLOCKED REBOOT General Hardware Errors
|
|
62 STUCK RAMP KEY CONTINUE General Hardware Errors
|
|
63 STUCK KNOB KEY CONTINUE General Hardware Errors
|
|
64 DSP OVERTIME PWM REBOOT General Hardware Errors
|
|
65 STUCK ENCODER A CONTINUE General Hardware Errors
|
|
66 STUCK ENCODER B CONTINUE General Hardware Errors
|
|
67-69 Not Used General Hardware Errors
|
|
70 PRESSURE SENSOR ABSENT STOP Pressure Sensor Errors
|
|
71 Not Used Pressure Sensor Errors
|
|
72 PSENS UNABLE TO OBTAIN BUS REBOOT Pressure Sensor Errors
|
|
73 SENSOR PRESS OFFSET STOP STOP Pressure Sensor Errors
|
|
74-79 Not Used Pressure Sensor Errors
|
|
80 UNABLE TO INIT FLOW SENSOR REBOOT Flow Sensor Errors
|
|
81 FLOW SENSOR TABLE CONTINUE Flow Sensor Errors
|
|
82 FLOW SENSOR OFFSET CONTINUE Flow Sensor Errors
|
|
83 FSENS UNABLE TO OBTAIN BUS REBOOT / 2nd failure=STOP Flow Sensor Errors
|
|
84 FLOW SENSOR STOP STOP Flow Sensor Errors
|
|
85 FLOW SENSOR OCCLUDED CONTINUE Flow Sensor Errors
|
|
86 FLOW SENSOR ABSENT CONTINUE Flow Sensor Errors
|
|
87 FLOW SENSOR BUS CONTINUE Flow Sensor Errors
|
|
88-89 Not Used Flow Sensor Errors
|
|
90 OTP NOT CONFIGURED STOP OTP & RTC Errors
|
|
91 OTP INCORRECTLY CONFIGURED STOP OTP & RTC Errors
|
|
92 Not Used OTP & RTC Errors
|
|
93 RTC VALUE CONTINUE OTP & RTC Errors
|
|
94 RTC STOPPED CONTINUE OTP & RTC Errors
|
|
95-99 Not Used OTP & RTC Errors
|
|
100 HUMID NO HEAT CONTINUE Humidifier Errors
|
|
101 HUMID TEMP MAX STOP Humidifier Errors
|
|
102 THERMISTOR HIGH CONTINUE Humidifier Errors
|
|
103 THERMISTOR LOW CONTINUE Humidifier Errors
|
|
104 HUMID AMBIENT OFF CONTINUE Humidifier Errors
|
|
105 HUMID AMBIENT COMM CONTINUE Humidifier Errors
|
|
106-109 Not Used Humidifier Errors
|
|
110 STACK REBOOT Stack & Exception Handler Errors
|
|
111 EXCEPTION STACK OVERFLOW REBOOT Stack & Exception Handler Errors
|
|
112 EXCEPTION STACK RESERVE LOG ONLY Stack & Exception Handler Errors
|
|
113 EXCEPTION STACK UNDERFLOW REBOOT Stack & Exception Handler Errors
|
|
114 FIQ STACK OVERFLOW REBOOT Stack & Exception Handler Errors
|
|
115 FIQ STACK RESERVE LOG ONLY Stack & Exception Handler Errors
|
|
116 FIQ STACK UNDERFLOW REBOOT Stack & Exception Handler Errors
|
|
117 IRQ STACK OVERFLOW REBOOT Stack & Exception Handler Errors
|
|
118 IRQ STACK RESERVE LOG ONLY Stack & Exception Handler Errors
|
|
119 IRQ STACK UNDERFLOW REBOOT Stack & Exception Handler Errors
|
|
120 SVC STACK OVERFLOW REBOOT Stack & Exception Handler Errors
|
|
121 SVC STACK RESERVE LOG ONLY Stack & Exception Handler Errors
|
|
122 SVC STACK UNDERFLOW REBOOT Stack & Exception Handler Errors
|
|
123 DATA ABORT EXCEPTION REBOOT Stack & Exception Handler Errors
|
|
124 PREFETCH EXCEPTION REBOOT Stack & Exception Handler Errors
|
|
125 ILLEGAL INSTRUCTION EXCEPTION REBOOT Stack & Exception Handler Errors
|
|
126 SWI ABORT EXCEPTION REBOOT Stack & Exception Handler Errors
|
|
*/
|
|
|