mirror of
https://gitlab.com/pholy/OSCAR-code.git
synced 2025-04-05 18:50:44 +00:00
This looks even uglier than the F5V3 event split for now, since portions of the fileVersion 3 parsing are still stuck in PRS1Import, and can't be reasonably moved until all the parsers are split into PRS1DataChunk. Also, PRS1ParsedSettingEvent may be the wrong abstraction. That's a first attempt, so that it can inherit PRS1ParsedEvent's notion of gain and unit, and because m_parsedData is currently a list of PRS1ParsedEvent pointers. But it has no notion of time (start=0) and requires yet another enum to specify which setting it represents. This should be revisited once all the parsers have been split and the summary parsing can be examined in more detail.
4584 lines
155 KiB
C++
4584 lines
155 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;
|
|
|
|
PRS1::PRS1(Profile *profile, MachineID id): CPAP(profile, id)
|
|
{
|
|
}
|
|
PRS1::~PRS1()
|
|
{
|
|
|
|
}
|
|
|
|
|
|
#if 0 // TODO: Remove: unused, superseded by PRS1Waveform
|
|
/*! \struct WaveHeaderList
|
|
\brief Used in PRS1 Waveform Parsing */
|
|
struct WaveHeaderList {
|
|
quint16 interleave;
|
|
quint8 sample_format;
|
|
WaveHeaderList(quint16 i, quint8 f) { interleave = i; sample_format = f; }
|
|
};
|
|
#endif
|
|
|
|
|
|
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
|
|
|
|
//genCRCTable(); // find what I did with this..
|
|
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(MachineInfo & info, const QString & filename, Machine * mach)
|
|
{
|
|
QFile f(filename);
|
|
if (!f.open(QFile::ReadOnly)) {
|
|
return false;
|
|
}
|
|
QTextStream in(&f);
|
|
QString modelnum;
|
|
int ptype=0;
|
|
int dfv=0;
|
|
bool ok;
|
|
do {
|
|
QString line = in.readLine();
|
|
QStringList pair = line.split("=");
|
|
|
|
bool skip = false;
|
|
|
|
if (pair[0].contains("ModelNumber", Qt::CaseInsensitive) || pair[0].contains("MN", Qt::CaseInsensitive)) {
|
|
modelnum = pair[1];
|
|
skip = true;
|
|
}
|
|
if (pair[0].contains("SerialNumber", Qt::CaseInsensitive) || pair[0].contains("SN", Qt::CaseInsensitive)) {
|
|
info.serial = pair[1];
|
|
skip = true;
|
|
}
|
|
if (pair[0].contains("ProductType", Qt::CaseInsensitive) || pair[0].contains("PT", Qt::CaseInsensitive)) {
|
|
ptype = pair[1].toInt(&ok, 16);
|
|
skip = true;
|
|
}
|
|
if (pair[0].contains("DataFormatVersion", Qt::CaseInsensitive) || pair[0].contains("DFV", Qt::CaseInsensitive)) {
|
|
dfv = pair[1].toInt(&ok, 10);
|
|
skip = true;
|
|
}
|
|
if (!mach || skip) continue;
|
|
|
|
mach->properties[pair[0]] = pair[1];
|
|
|
|
} while (!in.atEnd());
|
|
|
|
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)
|
|
{
|
|
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 (((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 machines' 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);
|
|
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;
|
|
|
|
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;
|
|
}
|
|
}
|
|
*/
|
|
|
|
|
|
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..
|
|
if (fi.canonicalFilePath().isEmpty()) {
|
|
qWarning() << fi;
|
|
}
|
|
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);
|
|
|
|
if (ext <= 1) {
|
|
const unsigned char * data = (unsigned char *)chunk->m_data.constData();
|
|
|
|
if (data[0x00] != 0) {
|
|
// 5 length 5, 6 length 1, 7 length 3, 8 length 3 seen on 960P
|
|
qWarning() << path << "data doesn't start with 0, skipping:" << data[0x00] << chunk->m_data.size();
|
|
delete chunk;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
SessionID chunk_sid = chunk->sessionid;
|
|
if (i > 0 || chunk_sid != sid) { // log multiple chunks in non-waveform files and session ID mismatches
|
|
qDebug() << fi.canonicalFilePath() << chunk_sid;
|
|
}
|
|
if (m->SessionExists(sid)) { // BUG: this should presumably be chunk_sid, but any change needs to be tested.
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
enum PRS1ParsedEventType
|
|
{
|
|
EV_PRS1_TB,
|
|
EV_PRS1_OA,
|
|
EV_PRS1_CA,
|
|
EV_PRS1_FL,
|
|
EV_PRS1_PB,
|
|
EV_PRS1_LL,
|
|
EV_PRS1_HY,
|
|
EV_PRS1_IPAP,
|
|
EV_PRS1_TOTLEAK,
|
|
EV_PRS1_IPAPLOW,
|
|
EV_PRS1_IPAPHIGH,
|
|
EV_PRS1_RR,
|
|
EV_PRS1_PTB,
|
|
EV_PRS1_MV,
|
|
EV_PRS1_TV,
|
|
EV_PRS1_SNORE,
|
|
EV_PRS1_EPAP,
|
|
EV_PRS1_SETTING,
|
|
};
|
|
|
|
enum PRS1ParsedEventUnit
|
|
{
|
|
PRS1_UNIT_NONE,
|
|
PRS1_UNIT_CMH2O,
|
|
PRS1_UNIT_ML,
|
|
};
|
|
|
|
enum PRS1ParsedSettingType
|
|
{
|
|
PRS1_SETTING_EPAP_MIN,
|
|
PRS1_SETTING_EPAP_MAX,
|
|
PRS1_SETTING_IPAP_MIN,
|
|
PRS1_SETTING_IPAP_MAX,
|
|
PRS1_SETTING_PS_MIN,
|
|
PRS1_SETTING_PS_MAX,
|
|
};
|
|
|
|
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; }
|
|
|
|
protected:
|
|
PRS1ParsedEvent(PRS1ParsedEventType type, int start)
|
|
: m_type(type), m_start(start), m_duration(0), m_value(0), m_offset(0.0), m_gain(1.0), m_unit(PRS1_UNIT_NONE)
|
|
{
|
|
}
|
|
~PRS1ParsedEvent()
|
|
{
|
|
}
|
|
};
|
|
|
|
class PRS1ParsedDurationEvent : public PRS1ParsedEvent
|
|
{
|
|
protected:
|
|
PRS1ParsedDurationEvent(PRS1ParsedEventType type, int start, int duration) : PRS1ParsedEvent(type, start)
|
|
{
|
|
m_duration = duration;
|
|
}
|
|
};
|
|
|
|
class PRS1ParsedValueEvent : public PRS1ParsedEvent
|
|
{
|
|
protected:
|
|
PRS1ParsedValueEvent(PRS1ParsedEventType type, int start, int value) : PRS1ParsedEvent(type, start) { m_value = value; }
|
|
};
|
|
|
|
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)
|
|
: PRS1ParsedValueEvent(type, start, value)
|
|
{
|
|
m_gain = GAIN;
|
|
m_unit = UNIT;
|
|
}
|
|
};
|
|
|
|
class PRS1ParsedSettingEvent : public PRS1ParsedValueEvent
|
|
{
|
|
public:
|
|
PRS1ParsedSettingType m_setting;
|
|
|
|
PRS1ParsedSettingEvent(PRS1ParsedSettingType setting, int value) : PRS1ParsedValueEvent(EV_PRS1_SETTING, 0, value), m_setting(setting) {}
|
|
};
|
|
|
|
class PRS1PressureSettingEvent : public PRS1ParsedSettingEvent
|
|
{
|
|
public:
|
|
PRS1PressureSettingEvent(PRS1ParsedSettingType setting, int value)
|
|
: PRS1ParsedSettingEvent(setting, value)
|
|
{
|
|
m_gain = PRS1PressureEvent::GAIN;
|
|
m_unit = PRS1PressureEvent::UNIT;
|
|
}
|
|
};
|
|
|
|
class PRS1TimedBreathEvent : public PRS1ParsedDurationEvent
|
|
{
|
|
public:
|
|
PRS1TimedBreathEvent(int start, int duration) : PRS1ParsedDurationEvent(EV_PRS1_TB, start, duration) {}
|
|
};
|
|
|
|
class PRS1ObstructiveApneaEvent : public PRS1ParsedDurationEvent
|
|
{
|
|
public:
|
|
PRS1ObstructiveApneaEvent(int start, int duration) : PRS1ParsedDurationEvent(EV_PRS1_OA, start, duration) {}
|
|
};
|
|
|
|
class PRS1ClearAirwayEvent : public PRS1ParsedDurationEvent
|
|
{
|
|
public:
|
|
PRS1ClearAirwayEvent(int start, int duration) : PRS1ParsedDurationEvent(EV_PRS1_CA, start, duration) {}
|
|
};
|
|
|
|
class PRS1FlowLimitationEvent : public PRS1ParsedDurationEvent
|
|
{
|
|
public:
|
|
PRS1FlowLimitationEvent(int start, int duration) : PRS1ParsedDurationEvent(EV_PRS1_FL, start, duration) {}
|
|
};
|
|
|
|
class PRS1PeriodicBreathingEvent : public PRS1ParsedDurationEvent
|
|
{
|
|
public:
|
|
PRS1PeriodicBreathingEvent(int start, int duration) : PRS1ParsedDurationEvent(EV_PRS1_PB, start, duration) {}
|
|
};
|
|
|
|
class PRS1LargeLeakEvent : public PRS1ParsedDurationEvent
|
|
{
|
|
public:
|
|
PRS1LargeLeakEvent(int start, int duration) : PRS1ParsedDurationEvent(EV_PRS1_LL, start, duration) {}
|
|
};
|
|
|
|
class PRS1HypopneaEvent : public PRS1ParsedDurationEvent
|
|
{
|
|
public:
|
|
PRS1HypopneaEvent(int start, int duration) : PRS1ParsedDurationEvent(EV_PRS1_HY, start, duration) {}
|
|
};
|
|
|
|
class PRS1IPAPEvent : public PRS1PressureEvent
|
|
{
|
|
public:
|
|
PRS1IPAPEvent(int start, int value) : PRS1PressureEvent(EV_PRS1_IPAP, start, value) {}
|
|
};
|
|
|
|
class PRS1TotalLeakEvent : public PRS1ParsedValueEvent
|
|
{
|
|
public:
|
|
PRS1TotalLeakEvent(int start, int value) : PRS1ParsedValueEvent(EV_PRS1_TOTLEAK, start, value) {}
|
|
};
|
|
|
|
class PRS1IPAPHighEvent : public PRS1PressureEvent
|
|
{
|
|
public:
|
|
PRS1IPAPHighEvent(int start, int value) : PRS1PressureEvent(EV_PRS1_IPAPHIGH, start, value) {}
|
|
};
|
|
|
|
class PRS1IPAPLowEvent : public PRS1PressureEvent
|
|
{
|
|
public:
|
|
PRS1IPAPLowEvent(int start, int value) : PRS1PressureEvent(EV_PRS1_IPAPLOW, start, value) {}
|
|
};
|
|
|
|
class PRS1RespiratoryRateEvent : public PRS1ParsedValueEvent
|
|
{
|
|
public:
|
|
PRS1RespiratoryRateEvent(int start, int value) : PRS1ParsedValueEvent(EV_PRS1_RR, start, value) {}
|
|
};
|
|
|
|
class PRS1PatientTriggeredBreathsEvent : public PRS1ParsedValueEvent
|
|
{
|
|
public:
|
|
PRS1PatientTriggeredBreathsEvent(int start, int value) : PRS1ParsedValueEvent(EV_PRS1_PTB, start, value) {}
|
|
};
|
|
|
|
class PRS1MinuteVentilationEvent : public PRS1ParsedValueEvent
|
|
{
|
|
public:
|
|
PRS1MinuteVentilationEvent(int start, int value) : PRS1ParsedValueEvent(EV_PRS1_MV, start, value) {}
|
|
};
|
|
|
|
class PRS1TidalVolumeEvent : public PRS1ParsedValueEvent
|
|
{
|
|
public:
|
|
PRS1TidalVolumeEvent(int start, int value)
|
|
: PRS1ParsedValueEvent(EV_PRS1_TV, start, value)
|
|
{
|
|
m_gain = 10.0;
|
|
m_unit = PRS1_UNIT_ML;
|
|
}
|
|
};
|
|
|
|
class PRS1SnoreEvent : public PRS1ParsedValueEvent
|
|
{
|
|
public:
|
|
PRS1SnoreEvent(int start, int value) : PRS1ParsedValueEvent(EV_PRS1_SNORE, start, value) {}
|
|
};
|
|
|
|
class PRS1EPAPEvent : public PRS1PressureEvent
|
|
{
|
|
public:
|
|
PRS1EPAPEvent(int start, int value) : PRS1PressureEvent(EV_PRS1_EPAP, start, value) {}
|
|
};
|
|
|
|
void PRS1DataChunk::AddEvent(PRS1ParsedEvent* const event)
|
|
{
|
|
m_parsedData.push_back(event);
|
|
}
|
|
|
|
bool PRS1Import::ParseF5EventsFV3()
|
|
{
|
|
EventDataType currentPressure=0, leak, ps=0;
|
|
|
|
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 start=timestamp;
|
|
qint64 t = qint64(event->timestamp) * 1000L;
|
|
session->updateFirst(t);
|
|
|
|
EventList *OA = session->AddEventList(CPAP_Obstructive, EVL_Event);
|
|
EventList *HY = session->AddEventList(CPAP_Hypopnea, EVL_Event);
|
|
|
|
EventList *PB = session->AddEventList(CPAP_PB, EVL_Event);
|
|
EventList *TOTLEAK = session->AddEventList(CPAP_LeakTotal, EVL_Event);
|
|
EventList *LEAK = session->AddEventList(CPAP_Leak, EVL_Event);
|
|
EventList *LL = session->AddEventList(CPAP_LargeLeak, EVL_Event);
|
|
EventList *SNORE = session->AddEventList(CPAP_Snore, 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 *RR = session->AddEventList(CPAP_RespRate, EVL_Event);
|
|
EventList *PTB = session->AddEventList(CPAP_PTB, EVL_Event);
|
|
EventList *TB = session->AddEventList(PRS1_TimedBreath, EVL_Event);
|
|
|
|
EventList *MV = session->AddEventList(CPAP_MinuteVent, EVL_Event);
|
|
EventList *TV = session->AddEventList(CPAP_TidalVolume, EVL_Event, 10.0F);
|
|
|
|
|
|
EventList *CA = session->AddEventList(CPAP_ClearAirway, EVL_Event);
|
|
EventList *FL = session->AddEventList(CPAP_FlowLimit, EVL_Event);
|
|
EventList *VS = session->AddEventList(CPAP_VSnore, EVL_Event);
|
|
|
|
bool ok;
|
|
ok = event->ParseEventsF5V3();
|
|
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 EV_PRS1_TB:
|
|
TB->AddEvent(t, e->m_duration);
|
|
break;
|
|
case EV_PRS1_OA:
|
|
OA->AddEvent(t, e->m_duration);
|
|
break;
|
|
case EV_PRS1_CA:
|
|
CA->AddEvent(t, e->m_duration);
|
|
break;
|
|
case EV_PRS1_FL:
|
|
FL->AddEvent(t, e->m_duration);
|
|
break;
|
|
case EV_PRS1_PB:
|
|
PB->AddEvent(t, e->m_duration);
|
|
break;
|
|
case EV_PRS1_LL:
|
|
LL->AddEvent(t, e->m_duration);
|
|
break;
|
|
case EV_PRS1_HY:
|
|
HY->AddEvent(t, e->m_duration);
|
|
break;
|
|
case EV_PRS1_IPAP:
|
|
IPAP->AddEvent(t, e->m_value);
|
|
currentPressure = e->m_value;
|
|
break;
|
|
case EV_PRS1_TOTLEAK:
|
|
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 EV_PRS1_IPAPLOW:
|
|
IPAPLo->AddEvent(t, e->m_value);
|
|
break;
|
|
case EV_PRS1_IPAPHIGH:
|
|
IPAPHi->AddEvent(t, e->m_value);
|
|
break;
|
|
case EV_PRS1_RR:
|
|
RR->AddEvent(t, e->m_value);
|
|
break;
|
|
case EV_PRS1_PTB:
|
|
PTB->AddEvent(t, e->m_value);
|
|
break;
|
|
case EV_PRS1_MV:
|
|
MV->AddEvent(t, e->m_value);
|
|
break;
|
|
case EV_PRS1_TV:
|
|
TV->AddEvent(t, e->m_value);
|
|
break;
|
|
case EV_PRS1_SNORE:
|
|
SNORE->AddEvent(t, e->m_value);
|
|
if (e->m_value > 0) {
|
|
VS->AddEvent(t, 0); //data2); // VSnore
|
|
}
|
|
break;
|
|
case EV_PRS1_EPAP:
|
|
EPAP->AddEvent(t, e->m_value);
|
|
ps = currentPressure - e->m_value;
|
|
PS->AddEvent(t, ps); // Pressure Support
|
|
break;
|
|
default:
|
|
qWarning() << "Unknown PRS1 event type" << (int) e->m_type;
|
|
break;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
|
|
bool PRS1DataChunk::ParseEventsF5V3(void)
|
|
{
|
|
if (this->family != 5 || this->familyVersion != 3) {
|
|
qWarning() << "ParseEventsF5V3 called with family" << this->family << "familyVersion" << this->familyVersion;
|
|
//break; // don't break to avoid changing behavior (for now)
|
|
}
|
|
|
|
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 << ")";
|
|
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" << pos;
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool PRS1Import::ParseF5Events()
|
|
{
|
|
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 *OA = session->AddEventList(CPAP_Obstructive, EVL_Event);
|
|
EventList *HY = session->AddEventList(CPAP_Hypopnea, EVL_Event);
|
|
|
|
EventList *PB = session->AddEventList(CPAP_PB, EVL_Event);
|
|
EventList *TOTLEAK = session->AddEventList(CPAP_LeakTotal, EVL_Event);
|
|
EventList *LEAK = session->AddEventList(CPAP_Leak, EVL_Event);
|
|
EventList *LL = session->AddEventList(CPAP_LargeLeak, EVL_Event);
|
|
EventList *SNORE = session->AddEventList(CPAP_Snore, 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 *RR = session->AddEventList(CPAP_RespRate, EVL_Event);
|
|
EventList *PTB = session->AddEventList(CPAP_PTB, EVL_Event);
|
|
EventList *TB = session->AddEventList(PRS1_TimedBreath, EVL_Event);
|
|
|
|
EventList *MV = session->AddEventList(CPAP_MinuteVent, EVL_Event);
|
|
EventList *TV = session->AddEventList(CPAP_TidalVolume, EVL_Event, 10.0F);
|
|
|
|
|
|
EventList *CA = session->AddEventList(CPAP_ClearAirway, EVL_Event);
|
|
EventList *FL = session->AddEventList(CPAP_FlowLimit, EVL_Event);
|
|
EventList *VS = session->AddEventList(CPAP_VSnore, EVL_Event);
|
|
// EventList *VS2 = session->AddEventList(CPAP_VSnore2, EVL_Event);
|
|
|
|
//EventList * PRESSURE=nullptr;
|
|
//EventList * PP=nullptr;
|
|
|
|
EventDataType data0, data1, data2, data4, data5;
|
|
|
|
EventDataType currentPressure=0, leak; //, p;
|
|
|
|
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 start=timestamp;
|
|
qint64 t = qint64(event->timestamp) * 1000L;
|
|
session->updateFirst(t);
|
|
qint64 tt;
|
|
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 = event->m_data.size();
|
|
unsigned char * buffer = (unsigned char *)event->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 >= ncodes) {
|
|
qDebug() << "Illegal PRS1 code " << hex << int(code) << " appeared at " << hex << startpos << "in" << event->sessionid;;
|
|
qDebug() << "1: (" << int(lastcode) << hex << lastpos << ")";
|
|
qDebug() << "2: (" << int(lastcode2) << hex << lastpos2 << ")";
|
|
qDebug() << "3: (" << int(lastcode3) << hex << lastpos3 << ")";
|
|
return false;
|
|
}
|
|
|
|
if (code == 0) {
|
|
} else if (code != 0x12) {
|
|
delta = buffer[pos];
|
|
//duration=buffer[pos+1];
|
|
//delta=buffer[pos+1] << 8 | buffer[pos];
|
|
pos += 2;
|
|
t += qint64(delta) * 1000L;
|
|
}
|
|
|
|
ChannelID cpapcode = Codes[(int)code];
|
|
//EventDataType PS;
|
|
tt = t;
|
|
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
|
|
if (!Code[1]) {
|
|
if (!(Code[1] = session->AddEventList(cpapcode, EVL_Event, 0.1F))) { return false; }
|
|
}
|
|
|
|
Code[1]->AddEvent(t, 0);
|
|
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++];
|
|
|
|
TB->AddEvent(t, data0);
|
|
break;
|
|
|
|
case 0x05:
|
|
//code=CPAP_Obstructive;
|
|
data0 = buffer[pos++];
|
|
tt -= qint64(data0) * 1000L; // Subtract Time Offset
|
|
OA->AddEvent(tt, data0);
|
|
break;
|
|
|
|
case 0x06:
|
|
//code=CPAP_ClearAirway;
|
|
data0 = buffer[pos++];
|
|
tt -= qint64(data0) * 1000L; // Subtract Time Offset
|
|
|
|
CA->AddEvent(tt, data0);
|
|
break;
|
|
|
|
case 0x07:
|
|
//code=CPAP_Hypopnea;
|
|
data0 = buffer[pos++];
|
|
tt -= qint64(data0) * 1000L; // Subtract Time Offset
|
|
HY->AddEvent(tt, data0);
|
|
break;
|
|
|
|
case 0x08: // ???
|
|
data0 = buffer[pos++];
|
|
tt -= qint64(data0) * 1000L; // Subtract Time Offset
|
|
//qDebug() << "Code 8 found at " << hex << pos - 1 << " " << tt;
|
|
|
|
if (event->familyVersion>=2) {
|
|
HY->AddEvent(tt, data0);
|
|
} else {
|
|
if (!Code[10]) {
|
|
if (!(Code[10] = session->AddEventList(cpapcode, EVL_Event))) { return false; }
|
|
}
|
|
|
|
//????
|
|
//data1=buffer[pos++]; // ???
|
|
Code[10]->AddEvent(tt, data0);
|
|
//pos++;
|
|
}
|
|
break;
|
|
|
|
case 0x09: // ASV Codes
|
|
if (event->familyVersion<2) {
|
|
//code=CPAP_FlowLimit;
|
|
data0 = buffer[pos++];
|
|
tt -= qint64(data0) * 1000L; // Subtract Time Offset
|
|
|
|
FL->AddEvent(tt, data0);
|
|
} else {
|
|
data0 = buffer[pos++];
|
|
data1 = buffer[pos++];
|
|
}
|
|
|
|
break;
|
|
|
|
case 0x0a:
|
|
data0 = buffer[pos++];
|
|
tt -= qint64(data0) * 1000L; // Subtract Time Offset
|
|
if (event->familyVersion>=2) {
|
|
FL->AddEvent(tt, data0);
|
|
} else {
|
|
if (!Code[7]) {
|
|
if (!(Code[7] = session->AddEventList(cpapcode, EVL_Event))) { return false; }
|
|
}
|
|
|
|
Code[7]->AddEvent(tt, 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;
|
|
tt -= qint64(data1) * 1000L;
|
|
|
|
if (!PB) {
|
|
if (!(PB = session->AddEventList(cpapcode, EVL_Event))) {
|
|
qDebug() << "!PB addeventlist exit";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
PB->AddEvent(tt, data0);
|
|
break;
|
|
|
|
case 0x0c:
|
|
|
|
if (event->familyVersion>=2) {
|
|
data0 = (buffer[pos + 1] << 8 | buffer[pos]);
|
|
data0 *= 2;
|
|
pos += 2;
|
|
data1 = buffer[pos++];
|
|
tt = t - qint64(data1) * 1000L;
|
|
|
|
if (!PB) {
|
|
if (!(PB = session->AddEventList(cpapcode, EVL_Event))) {
|
|
qDebug() << "!PB addeventlist exit";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
PB->AddEvent(tt, data0);
|
|
|
|
} else {
|
|
data0 = buffer[pos++];
|
|
tt -= qint64(data0) * 1000L; // Subtract Time Offset
|
|
qDebug() << "Code 12 found at " << hex << pos - 1 << " " << tt;
|
|
|
|
|
|
if (!Code[8]) {
|
|
if (!(Code[8] = session->AddEventList(cpapcode, EVL_Event))) { return false; }
|
|
}
|
|
|
|
Code[8]->AddEvent(tt, data0);
|
|
pos += 2;
|
|
}
|
|
break;
|
|
|
|
case 0x0d: // All the other ASV graph stuff.
|
|
|
|
if (event->familyVersion>=2) {
|
|
data0 = (buffer[pos + 1] << 8 | buffer[pos]);
|
|
data0 *= 2;
|
|
pos += 2;
|
|
data1 = buffer[pos++];
|
|
tt = t - qint64(data1) * 1000L;
|
|
} else {
|
|
IPAP->AddEvent(t, currentPressure = data0 = buffer[pos++]); // 00=IAP
|
|
data4 = buffer[pos++];
|
|
IPAPLo->AddEvent(t, data4); // 01=IAP Low
|
|
data5 = buffer[pos++];
|
|
IPAPHi->AddEvent(t, data5); // 02=IAP High
|
|
|
|
TOTLEAK->AddEvent(t, leak=buffer[pos++]); // 03=LEAK
|
|
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);
|
|
}
|
|
|
|
|
|
RR->AddEvent(t, buffer[pos++]); // 04=Breaths Per Minute
|
|
PTB->AddEvent(t, buffer[pos++]); // 05=Patient Triggered Breaths
|
|
MV->AddEvent(t, buffer[pos++]); // 06=Minute Ventilation
|
|
//tmp=buffer[pos++] * 10.0;
|
|
TV->AddEvent(t, buffer[pos++]); // 07=Tidal Volume
|
|
SNORE->AddEvent(t, data2 = buffer[pos++]); // 08=Snore
|
|
|
|
if (data2 > 0) {
|
|
if (!VS) {
|
|
if (!(VS = session->AddEventList(CPAP_VSnore, EVL_Event))) {
|
|
qDebug() << "!VS eventlist exit";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
VS->AddEvent(t, 0); //data2); // VSnore
|
|
}
|
|
|
|
EPAP->AddEvent(t, data1 = buffer[pos++]); // 09=EPAP
|
|
data2 = data0 - data1;
|
|
PS->AddEvent(t, data2); // Pressure Support
|
|
if (event->familyVersion >= 1) {
|
|
data0 = buffer[pos++];
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 0x0e: // Unknown
|
|
// Family 5.2 has this code
|
|
if (event->familyVersion>=2) {
|
|
EPAP->AddEvent(t, data0=buffer[pos+9]); // 9
|
|
IPAP->AddEvent(t, data1=buffer[pos+0]); // 0
|
|
IPAPLo->AddEvent(t, buffer[pos+1]); // 1
|
|
IPAPHi->AddEvent(t, buffer[pos+2]); // 2
|
|
LEAK->AddEvent(t, buffer[pos+3]); // 3
|
|
TV->AddEvent(t, buffer[pos+7]); // 7
|
|
RR->AddEvent(t, buffer[pos+4]); // 4
|
|
PTB->AddEvent(t, buffer[pos+5]); // 5
|
|
MV->AddEvent(t, buffer[pos+6]); //6
|
|
SNORE->AddEvent(t, data2 = buffer[pos+8]); //??
|
|
|
|
if (data2 > 0) {
|
|
if (!VS) {
|
|
if (!(VS = session->AddEventList(CPAP_VSnore, EVL_Event))) {
|
|
qDebug() << "!VS eventlist exit";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
VS->AddEvent(t, 0); //data2); // VSnore
|
|
}
|
|
data2 = data1 - data0;
|
|
PS->AddEvent(t, data2); // Pressure Support
|
|
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++];
|
|
|
|
tt = t - qint64(data1) * 1000L;
|
|
LL->AddEvent(tt, 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;
|
|
badcode = true;
|
|
break;
|
|
}
|
|
|
|
if (badcode) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
session->updateLast(t);
|
|
session->m_cnt.clear();
|
|
session->m_cph.clear();
|
|
|
|
// EventDataType minEpap = session->Min(CPAP_EPAP);
|
|
// EventDataType minIpapLo = session->Min(CPAP_IPAPLo);
|
|
// EventDataType maxIpapHi = session->Max(CPAP_IPAPHi);
|
|
|
|
// session->settings[CPAP_IPAPLo] = minIpapLo;
|
|
// session->settings[CPAP_IPAPHi] = maxIpapHi;
|
|
|
|
// session->settings[CPAP_PSMax] = maxIpapHi - minEpap;
|
|
// session->settings[CPAP_PSMin] = minIpapLo - minEpap;
|
|
|
|
session->m_valuesummary[CPAP_Pressure].clear();
|
|
session->m_valuesummary.erase(session->m_valuesummary.find(CPAP_Pressure));
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
bool PRS1Import::ParseF3EventsV3()
|
|
{
|
|
// AVAPS machine... it's delta packed, unlike the older ones?? (double check that! :/)
|
|
|
|
EventList *PP = session->AddEventList(PRS1_TimedBreath, EVL_Event);
|
|
EventList *OA = session->AddEventList(CPAP_Obstructive, EVL_Event);
|
|
EventList *HY = session->AddEventList(CPAP_Hypopnea, EVL_Event);
|
|
EventList *ZZ = session->AddEventList(CPAP_NRI, EVL_Event);
|
|
//EventList *Z2 = session->AddEventList(CPAP_ExP, EVL_Event);
|
|
EventList *CA = session->AddEventList(CPAP_ClearAirway, EVL_Event);
|
|
EventList *PB = session->AddEventList(CPAP_PB, EVL_Event);
|
|
EventList *LL = session->AddEventList(CPAP_LargeLeak, EVL_Event);
|
|
EventList *RE = session->AddEventList(CPAP_RERA, EVL_Event);
|
|
EventList *LEAK = session->AddEventList(CPAP_Leak, EVL_Event);
|
|
// EventList *ULK = session->AddEventList(CPAP_Leak, EVL_Event);
|
|
|
|
EventList *PTB = session->AddEventList(CPAP_PTB, 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 *TMV = session->AddEventList(CPAP_Test1, EVL_Event);
|
|
EventList *EPAP = session->AddEventList(CPAP_EPAP, EVL_Event, 0.1f);
|
|
EventList *IPAP = session->AddEventList(CPAP_IPAP, EVL_Event, 0.1f);
|
|
EventList *FLOW = session->AddEventList(CPAP_Test2, EVL_Event);
|
|
|
|
qint64 t = qint64(event->timestamp) * 1000L; //, tt;
|
|
|
|
int pos = 0;
|
|
int datasize = event->m_data.size();
|
|
|
|
unsigned char * data = (unsigned char *)event->m_data.data();
|
|
unsigned char code;
|
|
unsigned short delta;
|
|
bool failed = false;
|
|
|
|
unsigned char val, val2;
|
|
QString dump;
|
|
|
|
do {
|
|
code = data[pos++];
|
|
delta = (data[pos+1] < 8) | data[pos];
|
|
pos += 2;
|
|
#ifdef DEBUG_EVENTS
|
|
if (code == 0x00) {
|
|
if (!loader->unknownCodes.contains(code)) {
|
|
loader->unknownCodes.insert(code, QStringList());
|
|
}
|
|
QStringList & str = loader->unknownCodes[code];
|
|
|
|
dump = QString("%1@0x%5: [%2] [%3 %4]")
|
|
.arg(session->session(), 8, 16, QChar('0'))
|
|
.arg(data[pos-3], 2, 16, QChar('0'))
|
|
.arg(data[pos-2], 2, 16, QChar('0'))
|
|
.arg(data[pos-1], 2, 16, QChar('0'))
|
|
.arg(pos-3, 5, 16, QChar('0'));
|
|
|
|
for (int i=0; i<15; i++) {
|
|
if ((pos+i) > datasize) break;
|
|
dump += QString(" %1").arg(data[pos+i], 2, 16, QChar('0'));
|
|
}
|
|
str.append(dump.trimmed());
|
|
|
|
}
|
|
#endif
|
|
unsigned short epap;
|
|
|
|
switch(code) {
|
|
case 0x01: // Who knows
|
|
val = data[pos++];
|
|
PP->AddEvent(t, val);
|
|
break;
|
|
case 0x02:
|
|
LEAK->AddEvent(t, data[pos+3]);
|
|
PTB->AddEvent(t, data[pos+5]);
|
|
MV->AddEvent(t, data[pos+6]);
|
|
TV->AddEvent(t, data[pos+7]);
|
|
|
|
|
|
EPAP->AddEvent(t, epap=data[pos+0]);
|
|
IPAP->AddEvent(t, data[pos+1]);
|
|
FLOW->AddEvent(t, data[pos+4]);
|
|
TMV->AddEvent(t, data[pos+8]);
|
|
RR->AddEvent(t, data[pos+9]);
|
|
pos += 12;
|
|
|
|
break;
|
|
case 0x04: // ???
|
|
val = data[pos++];
|
|
PP->AddEvent(t, val);
|
|
break;
|
|
case 0x05: // ???
|
|
val = data[pos++];
|
|
CA->AddEvent(t, val);
|
|
break;
|
|
case 0x06: // Obstructive Apnea
|
|
val = data[pos++];
|
|
val2 = data[pos++];
|
|
OA->AddEvent(t + (qint64(val2)*1000L), val);
|
|
break;
|
|
case 0x07: // PB
|
|
val = data[pos+1] << 8 | data[pos];
|
|
pos += 2;
|
|
val2 = data[pos++];
|
|
PB->AddEvent(t - (qint64(val2)*1000L), val);
|
|
break;
|
|
case 0x08: // RERA
|
|
val = data[pos++];
|
|
RE->AddEvent(t, val);
|
|
break;
|
|
case 0x09: // ???
|
|
val = data[pos+1] << 8 | data[pos];
|
|
pos += 2;
|
|
val2 = data[pos++];
|
|
LL->AddEvent(t - (qint64(val)*1000L), val2);
|
|
break;
|
|
|
|
case 0x0a: // ???
|
|
val = data[pos++];
|
|
ZZ->AddEvent(t, val);
|
|
break;
|
|
case 0x0b: // Hypopnea
|
|
val = data[pos++];
|
|
if (session->session() == 239) {
|
|
if (HY->count() == 0) {
|
|
qDebug() << t << delta << val << "hypopnea";
|
|
}
|
|
}
|
|
HY->AddEvent(t, val);
|
|
break;
|
|
|
|
default:
|
|
if (!loader->unknownCodes.contains(code)) {
|
|
loader->unknownCodes.insert(code, QStringList());
|
|
}
|
|
QStringList & str = loader->unknownCodes[code];
|
|
|
|
dump = QString("%1@0x%5: [%2] [%3 %4]")
|
|
.arg(session->session(), 8, 16, QChar('0'))
|
|
.arg(data[pos-3], 2, 16, QChar('0'))
|
|
.arg(data[pos-2], 2, 16, QChar('0'))
|
|
.arg(data[pos-1], 2, 16, QChar('0'))
|
|
.arg(pos-3, 5, 16, QChar('0'));
|
|
|
|
for (int i=0; i<15; i++) {
|
|
if ((pos+i) > datasize) break;
|
|
dump += QString(" %1").arg(data[pos+i], 2, 16, QChar('0'));
|
|
}
|
|
str.append(dump.trimmed());
|
|
|
|
failed = true;
|
|
break;
|
|
};
|
|
t += qint64(delta) * 1000L;
|
|
|
|
} while ((pos < datasize) && !failed);
|
|
|
|
if (failed) {
|
|
// Clean up this shit...
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
bool PRS1Import::ParseF3Events()
|
|
{
|
|
qint64 t = qint64(event->timestamp) * 1000L, tt;
|
|
|
|
session->updateFirst(t);
|
|
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 *LEAK = session->AddEventList(CPAP_LeakTotal, EVL_Event);
|
|
EventList *ULK = session->AddEventList(CPAP_Leak, EVL_Event);
|
|
EventList *MV = session->AddEventList(CPAP_MinuteVent, EVL_Event);
|
|
//EventList *TMV = session->AddEventList(CPAP_TgMV, EVL_Event);
|
|
EventList *TV = session->AddEventList(CPAP_TidalVolume, EVL_Event,10.0f);
|
|
EventList *RR = session->AddEventList(CPAP_RespRate, EVL_Event);
|
|
EventList *PTB = session->AddEventList(CPAP_PTB, EVL_Event);
|
|
EventList *EPAP = session->AddEventList(CPAP_EPAP, EVL_Event,0.1f);
|
|
EventList *IPAP = session->AddEventList(CPAP_IPAP, EVL_Event,0.1f);
|
|
EventList *FLOW = session->AddEventList(CPAP_FlowRate, EVL_Event);
|
|
|
|
int size = event->m_data.size()/0x10;
|
|
unsigned char * h = (unsigned char *)event->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 = 120000;
|
|
|
|
for (int x=0; x < size; x++) {
|
|
IPAP->AddEvent(t, h[0] | (h[1] << 8));
|
|
EPAP->AddEvent(t, h[2] | (h[3] << 8));
|
|
LEAK->AddEvent(t, h[4]);
|
|
TV->AddEvent(t, h[5]);
|
|
FLOW->AddEvent(t, h[6]);
|
|
PTB->AddEvent(t, h[7]);
|
|
RR->AddEvent(t, h[8]);
|
|
//TMV->AddEvent(t, h[9]); // not sure what this is.. encore doesn't graph it.
|
|
MV->AddEvent(t, h[11]);
|
|
ULK->AddEvent(t, h[15]);
|
|
|
|
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
|
|
if (hy > 0) {
|
|
div = block_duration / hy;
|
|
|
|
tt = t;
|
|
for (int i=0; i < hy; ++i) {
|
|
HY->AddEvent(t, hy);
|
|
tt += div;
|
|
}
|
|
}
|
|
if (ca > 0) {
|
|
div = block_duration / ca;
|
|
|
|
tt = t;
|
|
|
|
for (int i=0; i < ca; ++i) {
|
|
CA->AddEvent(tt, ca);
|
|
tt += div;
|
|
}
|
|
}
|
|
if (oa > 0) {
|
|
div = block_duration / oa;
|
|
|
|
tt = t;
|
|
for (int i=0; i < oa; ++i) {
|
|
OA->AddEvent(t, oa);
|
|
tt += div;
|
|
}
|
|
}
|
|
|
|
h += 0x10;
|
|
t += block_duration;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
}
|
|
|
|
bool PRS1Import::ParseF0Events()
|
|
{
|
|
unsigned char code=0;
|
|
EventList *Code[0x20] = {0};
|
|
|
|
EventDataType data0, data1, data2;
|
|
Q_UNUSED(data2)
|
|
int cnt = 0;
|
|
short delta;
|
|
int pos;
|
|
qint64 t = qint64(event->timestamp) * 1000L, tt;
|
|
|
|
session->updateFirst(t);
|
|
|
|
EventList *OA = session->AddEventList(CPAP_Obstructive, EVL_Event);
|
|
EventList *HY = session->AddEventList(CPAP_Hypopnea, EVL_Event);
|
|
EventList *PB = session->AddEventList(CPAP_PB, EVL_Event);
|
|
EventList *TOTLEAK = session->AddEventList(CPAP_LeakTotal, EVL_Event);
|
|
EventList *LEAK = session->AddEventList(CPAP_Leak, EVL_Event);
|
|
EventList *SNORE = session->AddEventList(CPAP_Snore, EVL_Event);
|
|
|
|
EventList *PP = session->AddEventList(CPAP_PressurePulse, EVL_Event);
|
|
EventList *RE = session->AddEventList(CPAP_RERA, EVL_Event);
|
|
EventList *CA = session->AddEventList(CPAP_ClearAirway, EVL_Event);
|
|
EventList *FL = session->AddEventList(CPAP_FlowLimit, EVL_Event);
|
|
EventList *VS = session->AddEventList(CPAP_VSnore, EVL_Event);
|
|
EventList *VS2 = session->AddEventList(CPAP_VSnore2, EVL_Event);
|
|
//EventList *T1 = session->AddEventList(CPAP_Test1, EVL_Event, 0.1);
|
|
|
|
Code[17] = 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;
|
|
|
|
unsigned char lastcode3 = 0, lastcode2 = 0, lastcode = 0;
|
|
int lastpos = 0, startpos = 0, lastpos2 = 0, lastpos3 = 0;
|
|
|
|
int size = event->m_data.size();
|
|
|
|
bool FV3 = (event->fileVersion == 3);
|
|
unsigned char * buffer = (unsigned char *)event->m_data.data();
|
|
|
|
EventDataType currentPressure=0, leak; //, p;
|
|
|
|
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();
|
|
|
|
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" << event->sessionid;
|
|
qDebug() << "1: (" << hex << int(lastcode) << hex << lastpos << ")";
|
|
qDebug() << "2: (" << hex << int(lastcode2) << hex << lastpos2 << ")";
|
|
qDebug() << "3: (" << hex << int(lastcode3) << hex << lastpos3 << ")";
|
|
return false;
|
|
}
|
|
|
|
if (code != 0x12) {
|
|
delta = buffer[pos + 1] << 8 | buffer[pos];
|
|
pos += 2;
|
|
|
|
t += qint64(delta) * 1000L;
|
|
tt = t;
|
|
}
|
|
|
|
cnt++;
|
|
|
|
switch (code) {
|
|
|
|
case 0x00: // Unknown 00
|
|
|
|
if (!Code[0]) {
|
|
if (!(Code[0] = session->AddEventList(PRS1_00, EVL_Event))) { return false; }
|
|
}
|
|
|
|
Code[0]->AddEvent(t, buffer[pos++]);
|
|
|
|
if (((event->family == 0) && (event->familyVersion >= 4)) || (event->fileVersion == 3)){
|
|
pos++;
|
|
}
|
|
|
|
break;
|
|
|
|
case 0x01: // Unknown
|
|
if ((event->family == 0) && (event->familyVersion >= 4)) {
|
|
if (!PRESSURE) {
|
|
PRESSURE = session->AddEventList(CPAP_Pressure, EVL_Event, 0.1F);
|
|
|
|
if (!PRESSURE) { return false; }
|
|
}
|
|
|
|
PRESSURE->AddEvent(t, currentPressure = buffer[pos++]);
|
|
} else {
|
|
if (!Code[1]) {
|
|
if (!(Code[1] = session->AddEventList(PRS1_01, EVL_Event))) { return false; }
|
|
}
|
|
|
|
Code[1]->AddEvent(t, 0);
|
|
}
|
|
|
|
break;
|
|
|
|
case 0x02: // Pressure
|
|
if ((event->family == 0) && (event->familyVersion >= 4)) { // BiPAP Pressure
|
|
if (!EPAP) {
|
|
if (!(EPAP = session->AddEventList(CPAP_EPAP, EVL_Event, 0.1F))) { return false; }
|
|
}
|
|
if(!IPAP) {
|
|
if (!(IPAP = session->AddEventList(CPAP_IPAP, EVL_Event, 0.1F))) { return false; }
|
|
}
|
|
if(!PS) {
|
|
if (!(PS = session->AddEventList(CPAP_PS, EVL_Event, 0.1F))) { return false; }
|
|
}
|
|
|
|
EPAP->AddEvent(t, data0 = buffer[pos++]);
|
|
IPAP->AddEvent(t, data1 = currentPressure = buffer[pos++]);
|
|
PS->AddEvent(t, data1 - data0);
|
|
} else {
|
|
if (!PRESSURE) {
|
|
PRESSURE = session->AddEventList(CPAP_Pressure, EVL_Event, 0.1F);
|
|
|
|
if (!PRESSURE) { return false; }
|
|
}
|
|
|
|
PRESSURE->AddEvent(t, currentPressure = buffer[pos++]);
|
|
}
|
|
|
|
break;
|
|
|
|
case 0x03: // BIPAP Pressure
|
|
if (FV3) {
|
|
if (!PRESSURE) {
|
|
PRESSURE = session->AddEventList(CPAP_Pressure, EVL_Event, 0.1F);
|
|
|
|
if (!PRESSURE) { return false; }
|
|
}
|
|
PRESSURE->AddEvent(t, currentPressure = buffer[pos++]);
|
|
|
|
} else {
|
|
if (!EPAP) {
|
|
if (!(EPAP = session->AddEventList(CPAP_EPAP, EVL_Event, 0.1F))) { return false; }
|
|
}
|
|
if(!IPAP) {
|
|
if (!(IPAP = session->AddEventList(CPAP_IPAP, EVL_Event, 0.1F))) { return false; }
|
|
}
|
|
if(!PS) {
|
|
if (!(PS = session->AddEventList(CPAP_PS, EVL_Event, 0.1F))) { return false; }
|
|
}
|
|
|
|
EPAP->AddEvent(t, data0 = buffer[pos++]);
|
|
IPAP->AddEvent(t, data1 = currentPressure = buffer[pos++]);
|
|
PS->AddEvent(t, data1 - data0);
|
|
}
|
|
break;
|
|
|
|
case 0x04: // Pressure Pulse
|
|
data0 = buffer[pos++];
|
|
|
|
PP->AddEvent(t, data0);
|
|
break;
|
|
|
|
case 0x05: // RERA
|
|
data0 = buffer[pos++];
|
|
tt = t - (qint64(data0) * 1000L);
|
|
|
|
RE->AddEvent(tt, data0);
|
|
break;
|
|
|
|
case 0x06: // Obstructive Apoanea
|
|
data0 = buffer[pos++];
|
|
tt = t - (qint64(data0) * 1000L);
|
|
OA->AddEvent(tt, data0);
|
|
break;
|
|
|
|
case 0x07: // Clear Airway
|
|
data0 = buffer[pos++];
|
|
tt = t - (qint64(data0) * 1000L);
|
|
|
|
CA->AddEvent(tt, data0);
|
|
break;
|
|
|
|
case 0x0a: // Hypopnea
|
|
data0 = buffer[pos++];
|
|
tt = t - (qint64(data0) * 1000L);
|
|
HY->AddEvent(tt, data0);
|
|
break;
|
|
|
|
case 0x0c: // Flow Limitation
|
|
data0 = buffer[pos++];
|
|
tt = t - (qint64(data0) * 1000L);
|
|
|
|
FL->AddEvent(tt, data0);
|
|
break;
|
|
|
|
case 0x0b: // Breathing not Detected flag???? but it doesn't line up
|
|
data0 = buffer[pos];
|
|
data1 = buffer[pos+1];
|
|
pos += 2;
|
|
|
|
if (event->familyVersion >= 4) {
|
|
// might not doublerize on older machines?
|
|
// data0 *= 2;
|
|
}
|
|
// data1 = buffer[pos++];
|
|
|
|
//tt = t - qint64((data0+data1)*2) * 1000L;
|
|
|
|
if (!Code[12]) {
|
|
Code[12] = session->AddEventList(PRS1_0B, EVL_Event);
|
|
}
|
|
|
|
// FIXME
|
|
Code[12]->AddEvent(t, data0);
|
|
break;
|
|
|
|
case 0x0d: // Vibratory Snore
|
|
VS->AddEvent(t, 0);
|
|
break;
|
|
|
|
case 0x0e: // Unknown
|
|
data0 = buffer[pos + 1] << 8 | buffer[pos];
|
|
if (event->familyVersion >= 4) {
|
|
// might not doublerize on older machines?
|
|
data0 *= 2;
|
|
}
|
|
|
|
pos += 2;
|
|
data1 = buffer[pos++];
|
|
|
|
tt = t - qint64(data1) * 1000L;
|
|
Code[17]->AddEvent(tt, data0);
|
|
|
|
break;
|
|
|
|
case 0x0f: // Cheyne Stokes Respiration
|
|
data0 = (buffer[pos + 1] << 8 | buffer[pos]);
|
|
if (event->familyVersion >= 4) {
|
|
// might not doublerize on older machines
|
|
data0 *= 2;
|
|
}
|
|
pos += 2;
|
|
data1 = buffer[pos++];
|
|
tt = t - qint64(data1) * 1000L;
|
|
PB->AddEvent(tt, data0);
|
|
break;
|
|
|
|
case 0x10: // Large Leak
|
|
data0 = buffer[pos + 1] << 8 | buffer[pos];
|
|
if (event->familyVersion >= 4) {
|
|
// might not doublerize on older machines
|
|
data0 *= 2;
|
|
}
|
|
pos += 2;
|
|
data1 = buffer[pos++];
|
|
|
|
tt = t - qint64(data1) * 1000L;
|
|
LL->AddEvent(tt, data0);
|
|
break;
|
|
|
|
case 0x11: // Leak Rate & Snore Graphs
|
|
data0 = buffer[pos++];
|
|
data1 = buffer[pos++];
|
|
|
|
TOTLEAK->AddEvent(t, data0);
|
|
SNORE->AddEvent(t, data1);
|
|
|
|
if (calcLeaks) { // Much Quicker doing this here than the recalc method.
|
|
leak = data0-(((currentPressure/10.0f) - 4.0) * ppm + lpm4);
|
|
if (leak < 0) leak = 0;
|
|
|
|
LEAK->AddEvent(t, leak);
|
|
}
|
|
|
|
if (data1 > 0) {
|
|
VS2->AddEvent(t, data1);
|
|
}
|
|
|
|
if ((event->family == 0) && (event->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) {
|
|
if (!EPAP) {
|
|
if (!(EPAP = session->AddEventList(CPAP_EPAP, EVL_Event, 0.1F))) { return false; }
|
|
}
|
|
EPAP->AddEvent(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++];
|
|
tt = t - (qint64(data0) * 1000L);
|
|
HY->AddEvent(tt, data0);
|
|
break;
|
|
|
|
case 0x15: // DreamStation Hypopnea
|
|
data0 = buffer[pos++];
|
|
tt = t - (qint64(data0) * 1000L);
|
|
HY->AddEvent(tt, data0);
|
|
break;
|
|
|
|
default:
|
|
// ERROR!!!
|
|
qWarning() << "Some new fandangled PRS1 code detected in" << event->sessionid << hex
|
|
<< int(code) << " at " << pos - 1;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// SmoothEventList(session, PRESSURE, CPAP_Pressure);
|
|
// SmoothEventList(session, IPAP, CPAP_IPAP);
|
|
// SmoothEventList(session, EPAP, CPAP_EPAP);
|
|
|
|
|
|
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 PRS1Import::ParseCompliance()
|
|
{
|
|
const unsigned char * data = (unsigned char *)compliance->m_data.constData();
|
|
|
|
if (data[0x00] > 0) {
|
|
return false;
|
|
}
|
|
|
|
session->settings[CPAP_Mode] = (int)MODE_CPAP;
|
|
|
|
EventDataType min_pressure = EventDataType(data[0x03]) / 10.0;
|
|
// EventDataType max_pressure = EventDataType(data[0x04]) / 10.0;
|
|
|
|
session->settings[CPAP_Pressure] = min_pressure;
|
|
|
|
|
|
int ramp_time = data[0x06];
|
|
EventDataType ramp_pressure = EventDataType(data[0x07]) / 10.0;
|
|
|
|
session->settings[CPAP_RampTime] = (int)ramp_time;
|
|
session->settings[CPAP_RampPressure] = ramp_pressure;
|
|
|
|
|
|
quint8 flex = data[0x09];
|
|
int flexlevel = flex & 0x03;
|
|
|
|
|
|
FlexMode flexmode = FLEX_Unknown;
|
|
|
|
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 & 8) { // Plus bit
|
|
flexmode = FLEX_CFlexPlus;
|
|
} else {
|
|
flexmode = FLEX_CFlex;
|
|
}
|
|
} else flexmode = FLEX_None;
|
|
|
|
session->settings[PRS1_FlexMode] = (int)flexmode;
|
|
session->settings[PRS1_FlexLevel] = (int)flexlevel;
|
|
session->setSummaryOnly(true);
|
|
//session->settings[CPAP_SummaryOnly] = true;
|
|
|
|
session->settings[PRS1_HumidStatus] = (bool)(data[0x0A] & 0x80); // Humidifier Connected
|
|
session->settings[PRS1_HumidLevel] = (int)(data[0x0A] & 7); // Humidifier Value
|
|
|
|
// need to parse a repeating structure here containing lengths of mask on/off..
|
|
// 0x03 = mask on
|
|
// 0x01 = mask off
|
|
|
|
qint64 start = qint64(compliance->timestamp) * 1000L;
|
|
qint64 tt = start;
|
|
|
|
int len = compliance->size()-3;
|
|
int pos = 0x11;
|
|
do {
|
|
quint8 c = data[pos++];
|
|
quint64 duration = data[pos] | data[pos+1] << 8;
|
|
pos+=2;
|
|
duration *= 1000L;
|
|
SliceStatus status;
|
|
if (c == 0x03) {
|
|
status = EquipmentOn;
|
|
} else if (c == 0x02) {
|
|
status = EquipmentLeaking;
|
|
} else if (c == 0x01) {
|
|
status = EquipmentOff;
|
|
} else {
|
|
qDebug() << compliance->sessionid << "Wasn't expecting" << c;
|
|
break;
|
|
}
|
|
session->m_slices.append(SessionSlice(tt, tt + duration, status));
|
|
qDebug() << compliance->sessionid << "Added Slice" << tt << (tt+duration) << status;
|
|
|
|
tt += duration;
|
|
} while (pos < len);
|
|
|
|
session->set_first(start);
|
|
session->set_last(tt);
|
|
|
|
// Bleh!! There is probably 10 different formats for these useless piece of junk machines
|
|
return true;
|
|
}
|
|
|
|
bool PRS1Import::ParseSummaryF0()
|
|
{
|
|
const unsigned char * data = (unsigned char *)summary->m_data.constData();
|
|
|
|
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;
|
|
}
|
|
|
|
EventDataType min_pressure = EventDataType(data[0x03]) / 10.0;
|
|
EventDataType max_pressure = EventDataType(data[0x04]) / 10.0;
|
|
EventDataType ps = EventDataType(data[0x05]) / 10.0; // pressure support
|
|
|
|
if (cpapmode == MODE_CPAP) {
|
|
session->settings[CPAP_Pressure] = min_pressure;
|
|
} else if (cpapmode == MODE_APAP) {
|
|
session->settings[CPAP_PressureMin] = min_pressure;
|
|
session->settings[CPAP_PressureMax] = max_pressure;
|
|
} else if (cpapmode == MODE_BILEVEL_FIXED) {
|
|
session->settings[CPAP_EPAP] = min_pressure;
|
|
session->settings[CPAP_IPAP] = max_pressure;
|
|
session->settings[CPAP_PS] = ps;
|
|
} else if (cpapmode == MODE_BILEVEL_AUTO_VARIABLE_PS) {
|
|
session->settings[CPAP_EPAPLo] = min_pressure;
|
|
session->settings[CPAP_EPAPHi] = max_pressure - 2.0;
|
|
session->settings[CPAP_IPAPLo] = min_pressure + 2.0;
|
|
session->settings[CPAP_IPAPHi] = max_pressure;
|
|
session->settings[CPAP_PSMin] = 2.0f;
|
|
session->settings[CPAP_PSMax] = ps;
|
|
}
|
|
|
|
session->settings[CPAP_Mode] = (int)cpapmode;
|
|
|
|
|
|
int ramp_time = data[0x06];
|
|
EventDataType ramp_pressure = EventDataType(data[0x07]) / 10.0;
|
|
|
|
session->settings[CPAP_RampTime] = (int)ramp_time;
|
|
session->settings[CPAP_RampPressure] = ramp_pressure;
|
|
|
|
// Tubing lock has no setting byte
|
|
|
|
// Menu Options
|
|
session->settings[PRS1_SysLock] = (bool) (data[0x0a] & 0x80); // System One Resistance Lock Setting
|
|
session->settings[PRS1_SysOneResistSet] = (int)data[0x0a] & 7; // SYstem One Resistance setting value
|
|
session->settings[PRS1_SysOneResistStat] = (bool) (data[0x0a] & 0x40); // System One Resistance Status bit
|
|
session->settings[PRS1_HoseDiam] = (data[0x0a] & 0x08) ? QObject::tr("15mm") : QObject::tr("22mm");
|
|
session->settings[PRS1_AutoOn] = (bool) (data[0x0b] & 0x40);
|
|
session->settings[PRS1_AutoOff] = (bool) (data[0x0c] & 0x10);
|
|
session->settings[PRS1_MaskAlert] = (bool) (data[0x0c] & 0x08);
|
|
session->settings[PRS1_ShowAHI] = (bool) (data[0x0c] & 0x04);
|
|
session->settings[PRS1_HumidStatus] = (bool)(data[0x09] & 0x80); // Humidifier Connected
|
|
session->settings[PRS1_HumidLevel] = (int)(data[0x09] & 7); // Humidifier Value
|
|
|
|
// session->
|
|
|
|
quint8 flex = data[0x08];
|
|
|
|
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
|
|
|
|
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;
|
|
|
|
session->settings[PRS1_FlexMode] = (int)flexmode;
|
|
session->settings[PRS1_FlexLevel] = (int)flexlevel;
|
|
|
|
summary_duration = data[0x14] | data[0x15] << 8;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool PRS1Import::ParseSummaryF0V4()
|
|
{
|
|
const unsigned char * data = (unsigned char *)summary->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;
|
|
}
|
|
|
|
|
|
EventDataType min_pressure = EventDataType(data[0x03]) / 10.0;
|
|
EventDataType max_pressure = EventDataType(data[0x04]) / 10.0;
|
|
EventDataType min_ps = EventDataType(data[0x05]) / 10.0; // pressure support
|
|
EventDataType max_ps = EventDataType(data[0x06]) / 10.0; // pressure support
|
|
|
|
if (cpapmode == MODE_CPAP) {
|
|
session->settings[CPAP_Pressure] = min_pressure;
|
|
} else if (cpapmode == MODE_APAP) {
|
|
session->settings[CPAP_PressureMin] = min_pressure;
|
|
session->settings[CPAP_PressureMax] = max_pressure;
|
|
} else if (cpapmode == MODE_BILEVEL_FIXED) {
|
|
session->settings[CPAP_EPAP] = min_pressure;
|
|
session->settings[CPAP_IPAP] = max_pressure;
|
|
session->settings[CPAP_PS] = max_pressure - min_pressure;
|
|
} else if (cpapmode == MODE_BILEVEL_AUTO_VARIABLE_PS) {
|
|
session->settings[CPAP_EPAPLo] = min_pressure;
|
|
session->settings[CPAP_EPAPHi] = max_pressure;
|
|
session->settings[CPAP_IPAPLo] = min_pressure + min_ps;
|
|
session->settings[CPAP_IPAPHi] = max_pressure;
|
|
session->settings[CPAP_PSMin] = min_ps;
|
|
session->settings[CPAP_PSMax] = max_ps;
|
|
}
|
|
session->settings[CPAP_Mode] = (int)cpapmode;
|
|
|
|
quint8 flex = data[0x0a];
|
|
|
|
int flexlevel = flex & 0x03;
|
|
FlexMode flexmode = FLEX_Unknown;
|
|
|
|
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;
|
|
|
|
int ramp_time = data[0x08];
|
|
EventDataType ramp_pressure = EventDataType(data[0x09]) / 10.0;
|
|
|
|
session->settings[CPAP_RampTime] = (int)ramp_time;
|
|
session->settings[CPAP_RampPressure] = ramp_pressure;
|
|
|
|
session->settings[PRS1_FlexMode] = (int)flexmode;
|
|
session->settings[PRS1_FlexLevel] = (int)flexlevel;
|
|
|
|
session->settings[PRS1_HumidStatus] = (bool)(data[0x0b] & 0x80); // Humidifier Connected
|
|
session->settings[PRS1_HeatedTubing] = (bool)(data[0x0b] & 0x10); // Heated Hose??
|
|
session->settings[PRS1_HumidLevel] = (int)(data[0x0b] & 7); // Humidifier Value
|
|
|
|
|
|
summary_duration = data[0x14] | data[0x15] << 8;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool PRS1Import::ParseSummaryF3()
|
|
{
|
|
CPAPMode mode = MODE_UNKNOWN;
|
|
EventDataType epap, ipap;
|
|
|
|
QMap<unsigned char, QByteArray>::iterator it;
|
|
|
|
if ((it=summary->mainblock.find(0x0a)) != summary->mainblock.end()) {
|
|
mode = MODE_CPAP;
|
|
session->settings[CPAP_Pressure] = EventDataType(it.value()[0]/10.0f);
|
|
} else if ((it=summary->mainblock.find(0x0d)) != summary->mainblock.end()) {
|
|
mode = MODE_APAP;
|
|
session->settings[CPAP_PressureMin] = EventDataType(it.value()[0]/10.0f);
|
|
session->settings[CPAP_PressureMax] = EventDataType(it.value()[1]/10.0f);
|
|
} else if ((it=summary->mainblock.find(0x0e)) != summary->mainblock.end()) {
|
|
mode = MODE_BILEVEL_FIXED;
|
|
session->settings[CPAP_EPAP] = ipap = EventDataType(it.value()[0] / 10.0f);
|
|
session->settings[CPAP_IPAP] = epap = EventDataType(it.value()[1] / 10.0f);
|
|
session->settings[CPAP_PS] = ipap - epap;
|
|
} else if ((it=summary->mainblock.find(0x0f)) != summary->mainblock.end()) {
|
|
mode = MODE_BILEVEL_AUTO_VARIABLE_PS;
|
|
session->settings[CPAP_EPAPLo] = EventDataType(it.value()[0]/10.0f);
|
|
session->settings[CPAP_IPAPHi] = EventDataType(it.value()[1]/10.0f);
|
|
session->settings[CPAP_PSMin] = EventDataType(it.value()[2]/10.0f);
|
|
session->settings[CPAP_PSMax] = EventDataType(it.value()[3]/10.0f);
|
|
} else if ((it=summary->mainblock.find(0x10)) != summary->mainblock.end()) {
|
|
mode = MODE_APAP; // Disgusting APAP "IQ" trial
|
|
session->settings[CPAP_PressureMin] = EventDataType(it.value()[0]/10.0f);
|
|
session->settings[CPAP_PressureMax] = EventDataType(it.value()[1]/10.0f);
|
|
}
|
|
|
|
session->settings[CPAP_Mode] = (int)mode;
|
|
|
|
if ((it=summary->hbdata.find(5)) != summary->hbdata.end()) {
|
|
summary_duration = (it.value()[1] << 8 ) + it.value()[0];
|
|
}
|
|
|
|
/* QDateTime date = QDateTime::fromMSecsSinceEpoch(session->first());
|
|
if (date.date() == QDate(2018,5,1)) {
|
|
|
|
qDebug() << "Dumping session" << (int)session->session() << "summary file";
|
|
QString hexstr = QString("%1@0000: ").arg(session->session(),8,16,QChar('0'));
|
|
const unsigned char * data = (const unsigned char *)summary->m_data.constData();
|
|
int size = summary->m_data.size();
|
|
for (int i=0; i<size; ++i) {
|
|
unsigned char val = data[i];
|
|
hexstr += QString(" %1").arg((short)val, 2, 16, QChar('0'));
|
|
if ((i % 0x10) == 0x0f) {
|
|
qDebug() << hexstr;
|
|
hexstr = QString("%1@%2: ").arg(session->session(),8,16,QChar('0')).arg(i+1, 4, 16, QChar('0'));
|
|
}
|
|
}
|
|
qDebug() << "Dumping mainblock";
|
|
for (auto it=mainblock.cbegin(), end=mainblock.cend(); it!=end; ++it) {
|
|
qDebug() << it.key() << it.value().toHex();
|
|
}
|
|
qDebug() << "Dumping hbdata";
|
|
for (auto it=hbdata.cbegin(), end=hbdata.cend(); it!=end; ++it) {
|
|
qDebug() << it.key() << it.value().toHex();
|
|
}
|
|
|
|
qDebug() << "In date";
|
|
} */
|
|
|
|
|
|
return true;
|
|
}
|
|
|
|
bool PRS1Import::ParseSummaryF5V0()
|
|
{
|
|
const unsigned char * data = (unsigned char *)summary->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;
|
|
|
|
session->settings[CPAP_Mode] = (int)cpapmode;
|
|
|
|
if (cpapmode == MODE_CPAP) {
|
|
session->settings[CPAP_Pressure] = imin_epap/10.0f;
|
|
|
|
} else if (cpapmode == MODE_BILEVEL_FIXED) {
|
|
session->settings[CPAP_EPAP] = imin_epap/10.0f;
|
|
session->settings[CPAP_IPAP] = imax_epap/10.0f;
|
|
|
|
} else if (cpapmode == MODE_ASV_VARIABLE_EPAP) {
|
|
//int imax_ipap = imax_epap + imax_ps;
|
|
int imin_ipap = imin_epap + imin_ps;
|
|
|
|
session->settings[CPAP_EPAPLo] = imin_epap / 10.0f;
|
|
session->settings[CPAP_EPAPHi] = imax_epap / 10.0f;
|
|
session->settings[CPAP_IPAPLo] = imin_ipap / 10.0f;
|
|
session->settings[CPAP_IPAPHi] = imax_pressure / 10.0f;
|
|
session->settings[CPAP_PSMin] = imin_ps / 10.0f;
|
|
session->settings[CPAP_PSMax] = imax_ps / 10.0f;
|
|
}
|
|
|
|
quint8 flex = data[0x0c];
|
|
|
|
int flexlevel = flex & 0x03;
|
|
FlexMode flexmode = FLEX_Unknown;
|
|
|
|
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;
|
|
|
|
session->settings[PRS1_FlexMode] = (int)flexmode;
|
|
session->settings[PRS1_FlexLevel] = (int)flexlevel;
|
|
|
|
|
|
int ramp_time = data[0x0a];
|
|
EventDataType ramp_pressure = EventDataType(data[0x0b]) / 10.0;
|
|
|
|
session->settings[CPAP_RampTime] = (int)ramp_time;
|
|
session->settings[CPAP_RampPressure] = ramp_pressure;
|
|
|
|
session->settings[PRS1_HumidStatus] = (bool)(data[0x0d] & 0x80); // Humidifier Connected
|
|
session->settings[PRS1_HeatedTubing] = (bool)(data[0x0d] & 0x10); // Heated Hose??
|
|
session->settings[PRS1_HumidLevel] = (int)(data[0x0d] & 7); // Humidifier Value
|
|
|
|
summary_duration = data[0x18] | data[0x19] << 8;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool PRS1Import::ParseSummaryF5V1()
|
|
{
|
|
const unsigned char * data = (unsigned char *)summary->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;
|
|
|
|
session->settings[CPAP_Mode] = (int)cpapmode;
|
|
|
|
if (cpapmode == MODE_CPAP) {
|
|
session->settings[CPAP_Pressure] = imin_epap/10.0f;
|
|
|
|
} else if (cpapmode == MODE_BILEVEL_FIXED) {
|
|
session->settings[CPAP_EPAP] = imin_epap/10.0f;
|
|
session->settings[CPAP_IPAP] = imax_epap/10.0f;
|
|
|
|
} else if (cpapmode == MODE_ASV_VARIABLE_EPAP) {
|
|
//int imax_ipap = imax_epap + imax_ps;
|
|
int imin_ipap = imin_epap + imin_ps;
|
|
|
|
session->settings[CPAP_EPAPLo] = imin_epap / 10.0f;
|
|
session->settings[CPAP_EPAPHi] = imax_epap / 10.0f;
|
|
session->settings[CPAP_IPAPLo] = imin_ipap / 10.0f;
|
|
session->settings[CPAP_IPAPHi] = imax_pressure / 10.0f;
|
|
session->settings[CPAP_PSMin] = imin_ps / 10.0f;
|
|
session->settings[CPAP_PSMax] = imax_ps / 10.0f;
|
|
}
|
|
|
|
quint8 flex = data[0x0c];
|
|
|
|
int flexlevel = flex & 0x03;
|
|
FlexMode flexmode = FLEX_Unknown;
|
|
|
|
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;
|
|
|
|
session->settings[PRS1_FlexMode] = (int)flexmode;
|
|
session->settings[PRS1_FlexLevel] = (int)flexlevel;
|
|
|
|
|
|
int ramp_time = data[0x0a];
|
|
EventDataType ramp_pressure = EventDataType(data[0x0b]) / 10.0;
|
|
|
|
session->settings[CPAP_RampTime] = (int)ramp_time;
|
|
session->settings[CPAP_RampPressure] = ramp_pressure;
|
|
|
|
session->settings[PRS1_HumidStatus] = (bool)(data[0x0d] & 0x80); // Humidifier Connected
|
|
session->settings[PRS1_HeatedTubing] = (bool)(data[0x0d] & 0x10); // Heated Hose??
|
|
session->settings[PRS1_HumidLevel] = (int)(data[0x0d] & 7); // Humidifier Value
|
|
|
|
summary_duration = data[0x18] | data[0x19] << 8;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool PRS1Import::ParseSummaryF5V2()
|
|
{
|
|
const unsigned char * data = (unsigned char *)summary->m_data.constData();
|
|
|
|
//CPAPMode cpapmode = MODE_UNKNOWN;
|
|
summary_duration = data[0x18] | data[0x19] << 8;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool PRS1Import::ParseSummaryF5V3()
|
|
{
|
|
bool ok;
|
|
ok = summary->ParseSummaryF5V3();
|
|
|
|
CPAPMode cpapmode = MODE_ASV_VARIABLE_EPAP;
|
|
|
|
session->settings[CPAP_Mode] = (int)cpapmode;
|
|
EventDataType epapHi, epapLo, minps, maxps;
|
|
|
|
for (int i=0; i < summary->m_parsedData.count(); i++) {
|
|
PRS1ParsedEvent* e = summary->m_parsedData.at(i);
|
|
if (e->m_type != EV_PRS1_SETTING) {
|
|
qWarning() << "Summary had non-setting event:" << (int) e->m_type;
|
|
continue;
|
|
}
|
|
PRS1ParsedSettingEvent* s = (PRS1ParsedSettingEvent*) e;
|
|
switch (s->m_setting) {
|
|
case PRS1_SETTING_EPAP_MIN:
|
|
epapLo = e->value();
|
|
session->settings[CPAP_EPAPLo] = epapLo;
|
|
break;
|
|
case PRS1_SETTING_EPAP_MAX:
|
|
epapHi = e->value();
|
|
session->settings[CPAP_EPAPHi] = epapHi;
|
|
break;
|
|
case PRS1_SETTING_PS_MIN:
|
|
minps = e->value();
|
|
session->settings[CPAP_PSMin] = minps;
|
|
break;
|
|
case PRS1_SETTING_PS_MAX:
|
|
maxps = e->value();
|
|
session->settings[CPAP_PSMax] = maxps;
|
|
break;
|
|
default:
|
|
qWarning() << "Unknown PRS1 setting type" << (int) s->m_setting;
|
|
break;
|
|
}
|
|
}
|
|
|
|
session->settings[CPAP_IPAPLo] = epapLo + minps;
|
|
session->settings[CPAP_IPAPHi] = qMin(25.0f, epapHi + maxps);
|
|
|
|
if (!ok) {
|
|
return false;
|
|
}
|
|
summary_duration = summary->duration;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool PRS1DataChunk::ParseSummaryF5V3(void)
|
|
{
|
|
unsigned char * pressureBlock = (unsigned char *)mainblock[0x0a].data();
|
|
|
|
EventDataType epapHi = pressureBlock[0];
|
|
EventDataType epapRange = pressureBlock[2];
|
|
EventDataType epapLo = epapHi - epapRange;
|
|
|
|
EventDataType minps = pressureBlock[3] ;
|
|
EventDataType maxps = pressureBlock[4]+epapLo;
|
|
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_EPAP_MAX, epapHi));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_EPAP_MIN, epapLo));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS_MIN, minps));
|
|
this->AddEvent(new PRS1PressureSettingEvent(PRS1_SETTING_PS_MAX, maxps));
|
|
|
|
if (hbdata[4].size() < 2) {
|
|
qDebug() << "summary missing duration section:" << this->sessionid;
|
|
return false;
|
|
}
|
|
unsigned char * durBlock = (unsigned char *)hbdata[4].data();
|
|
this->duration = durBlock[0] | durBlock[1] << 8;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool PRS1Import::ParseSummaryF0V6()
|
|
{
|
|
// DreamStation machines...
|
|
|
|
// APAP models..
|
|
|
|
const unsigned char * data = (unsigned char *)summary->m_data.constData();
|
|
|
|
CPAPMode cpapmode = MODE_UNKNOWN;
|
|
|
|
int imin_epap = 0;
|
|
//int imax_epap = 0;
|
|
int imin_ps = 0;
|
|
int imax_ps = 0;
|
|
//int imax_pressure = 0;
|
|
int min_pressure = 0;
|
|
int max_pressure = 0;
|
|
int duration = 0;
|
|
|
|
// in 'data', we start with 3 bytes that don't follow the pattern
|
|
// pattern is varNumber, dataSize, dataValue(dataSize)
|
|
// examples, 0x0d 0x02 0x28 0xC8 , or 0x0a 0x01 0x64,
|
|
// first, verify that this dataSize is where we expect
|
|
// each var pair in headerblock should be (indexByte, valueByte)
|
|
|
|
if ((int)summary->m_headerblock[(1 * 2)] != 0x01) {
|
|
return false; //nope, not here
|
|
qDebug() << "PRS1Loader::ParseSummaryF0V6=" << "Bad datablock length";
|
|
}
|
|
int dataBlockSize = summary->m_headerblock[(1 * 2) + 1];
|
|
//int zero = 0;
|
|
const unsigned char *dataPtr;
|
|
|
|
// start at 3rd byte ; did we go past the end? ; increment for dataSize + varNumberByte + dataSizeByte
|
|
for ( dataPtr = data + 3; dataPtr < (data + 3 + dataBlockSize); dataPtr+= dataPtr[1] + 2) {
|
|
switch( *dataPtr) {
|
|
case 00: // mode?
|
|
break;
|
|
case 01: // ???
|
|
break;
|
|
case 10: // 0x0a
|
|
cpapmode = MODE_CPAP;
|
|
if (dataPtr[1] != 1) qDebug() << "PRS1Loader::ParseSummaryF0V6=" << "Bad CPAP value";
|
|
imin_epap = dataPtr[2];
|
|
break;
|
|
case 13: // 0x0d
|
|
cpapmode = MODE_APAP;
|
|
if (dataPtr[1] != 2) qDebug() << "PRS1Loader::ParseSummaryF0V6=" << "Bad APAP value";
|
|
min_pressure = dataPtr[2];
|
|
max_pressure = dataPtr[3];
|
|
break;
|
|
case 14: // 0x0e // <--- this is a total guess.. might be 3 and have a pressure support value
|
|
cpapmode = MODE_BILEVEL_FIXED;
|
|
if (dataPtr[1] != 2) qDebug() << "PRS1Loader::ParseSummaryF0V6=" << "Bad APAP value";
|
|
min_pressure = dataPtr[2];
|
|
max_pressure = dataPtr[3];
|
|
imin_ps = max_pressure - min_pressure;
|
|
break;
|
|
case 15: // 0x0f
|
|
cpapmode = MODE_BILEVEL_AUTO_VARIABLE_PS; //might be C_CHECK?
|
|
if (dataPtr[1] != 4) qDebug() << "PRS1Loader::ParseSummaryF0V6=" << "Bad APAP value";
|
|
min_pressure = dataPtr[2];
|
|
max_pressure = dataPtr[3];
|
|
imin_ps = dataPtr[4];
|
|
imax_ps = dataPtr[5];
|
|
break;
|
|
case 0x10: // Auto Trial mode
|
|
cpapmode = MODE_APAP;
|
|
if (dataPtr[1] != 3) qDebug() << "PRS1Loader::ParseSummaryF0V6=" << "Bad APAP value";
|
|
min_pressure = dataPtr[3];
|
|
max_pressure = dataPtr[4];
|
|
break;
|
|
|
|
case 0x35:
|
|
duration += ( dataPtr[3] << 8 ) + dataPtr[2];
|
|
break;
|
|
// case 3:
|
|
// break;
|
|
default:
|
|
// have not found this before
|
|
;
|
|
// qDebug() << "PRS1Loader::ParseSummaryF0V6=" << "Unknown datablock value:" << (zero + *dataPtr) ;
|
|
}
|
|
}
|
|
// now we encounter yet a different format of data
|
|
/* const unsigned char *data2Ptr = data + 3 + dataBlockSize;
|
|
// pattern is byte/data, where length of data depends on value of 'byte'
|
|
bool data2Done = false;
|
|
while (!data2Done) {
|
|
switch(*data2Ptr){
|
|
case 0:
|
|
//this appears to be the last one. '0' plus 5 bytes **eats crc** without checking
|
|
data2Ptr += 4;
|
|
data2Ptr += 2; //this is the **CRC**??
|
|
data2Done = true; //hope this is always there, since we don't have blocksize from header
|
|
break;
|
|
case 1:
|
|
//don't know yet. data size is the '1' plus 16 bytes
|
|
data2Ptr += 5;
|
|
break;
|
|
case 2:
|
|
//don't know yet. data size is the '2' plus 16 bytes
|
|
data2Ptr += 3;
|
|
break;
|
|
case 3:
|
|
//don't know yet. data size is the '3' plus 4 bytes
|
|
// have seen multiple of these....may have to add them?
|
|
data2Ptr += 5;
|
|
break;
|
|
case 4:
|
|
// have seen multiple of these....may have to add them?
|
|
duration = ( data2Ptr[3] << 8 ) + data2Ptr[2];
|
|
data2Ptr += 3;
|
|
break;
|
|
case 5:
|
|
//don't know yet. data size is the '5' plus 4 bytes
|
|
data2Ptr += 5;
|
|
break;
|
|
case 6:
|
|
//don't know yet. data size is the '5' plus 1 byte
|
|
data2Ptr += 2;
|
|
break;
|
|
case 8:
|
|
//don't know yet. data size is the '8' plus 27 bytes (might be a '0' in here...not enough different types found yet)
|
|
data2Ptr += 28;
|
|
break;
|
|
default:
|
|
qDebug() << "PRS1Loader::ParseSummaryF0V6=" << "Unknown datablock2 value:" << (zero + *data2Ptr) ;
|
|
break;
|
|
}
|
|
}*/
|
|
// need to populate summary->
|
|
|
|
summary_duration = duration;
|
|
session->settings[CPAP_Mode] = (int)cpapmode;
|
|
if (cpapmode == MODE_CPAP) {
|
|
session->settings[CPAP_Pressure] = imin_epap/10.0f;
|
|
|
|
} else if (cpapmode == MODE_APAP) {
|
|
session->settings[CPAP_PressureMin] = min_pressure/10.0f;
|
|
session->settings[CPAP_PressureMax] = max_pressure/10.0f;
|
|
} else if (cpapmode == MODE_BILEVEL_FIXED) {
|
|
// Guessing here.. haven't seen BIPAP data.
|
|
session->settings[CPAP_EPAP] = min_pressure/10.0f;
|
|
session->settings[CPAP_IPAP] = max_pressure/10.0f;
|
|
session->settings[CPAP_PS] = imin_ps/10.0f;
|
|
} else if (cpapmode == MODE_BILEVEL_AUTO_VARIABLE_PS) {
|
|
session->settings[CPAP_EPAPLo] = min_pressure/10.0f;
|
|
session->settings[CPAP_IPAPHi] = max_pressure/10.0f;
|
|
session->settings[CPAP_PSMin] = imin_ps/10.0f;
|
|
session->settings[CPAP_PSMax] = imax_ps/10.0f;
|
|
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool PRS1Import::ParseSummary()
|
|
{
|
|
if (!summary) return false;
|
|
|
|
// All machines have a first byte zero for clean summary
|
|
if (summary->m_data.constData()[0] != 0) {
|
|
qDebug() << "Non zero hblock[0] indicator";
|
|
return false;
|
|
}
|
|
|
|
session->set_first(qint64(summary->timestamp) * 1000L);
|
|
|
|
|
|
// TODO: The below is probably wrong. It should move to PRS1DataChunk 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 (summary->fileVersion == 3) {
|
|
// Parse summary structures into bytearray map according to size given in header block
|
|
const unsigned char * data = (unsigned char *)summary->m_data.constData();
|
|
int size = summary->m_data.size();
|
|
|
|
int pos = 0;
|
|
int bsize;
|
|
short val, len;
|
|
do {
|
|
val = data[pos++];
|
|
auto it = summary->hblock.find(val);
|
|
if (it == summary->hblock.end()) {
|
|
qDebug() << "Block parse error in ParseSummary" << session->session();
|
|
break;
|
|
}
|
|
bsize = it.value();
|
|
|
|
if (val != 1) {
|
|
// store the data block for later reference
|
|
summary->hbdata[val] = QByteArray((const char *)(&data[pos]), bsize);
|
|
} else {
|
|
// Parse the nested data structure which contains settings
|
|
int p2 = 0;
|
|
do {
|
|
val = data[pos + p2++];
|
|
len = data[pos + p2++];
|
|
summary->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
|
|
|
|
|
|
|
|
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);
|
|
|
|
switch (summary->family) {
|
|
case 0:
|
|
if (summary->familyVersion == 6) {
|
|
return ParseSummaryF0V6();
|
|
} else if (summary->familyVersion == 4) {
|
|
return ParseSummaryF0V4();
|
|
} else {
|
|
return ParseSummaryF0();
|
|
}
|
|
case 3:
|
|
return ParseSummaryF3();
|
|
break;
|
|
case 5:
|
|
if (summary->familyVersion == 1) {
|
|
return ParseSummaryF5V1();
|
|
} else if (summary->familyVersion == 0) {
|
|
return ParseSummaryF5V0();
|
|
} else if (summary->familyVersion == 2) {
|
|
return ParseSummaryF5V1();
|
|
} else if (summary->familyVersion == 3) {
|
|
return ParseSummaryF5V3();
|
|
}
|
|
default:
|
|
;
|
|
}
|
|
|
|
this->loader->saveMutex.lock();
|
|
if (!mach->unsupported()) {
|
|
this->loader->unsupported(mach);
|
|
}
|
|
this->loader->saveMutex.unlock();
|
|
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
|
|
//////////////////////////////////////////////////////////////////////////////////////////
|
|
}
|
|
|
|
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)) {
|
|
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::ParseOximetery()
|
|
{
|
|
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;
|
|
}
|
|
|
|
bool PRS1Import::ParseWaveforms()
|
|
{
|
|
int size = waveforms.size();
|
|
quint64 s1, s2;
|
|
|
|
|
|
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;
|
|
|
|
quint64 diff = ti - lastti;
|
|
if ((lastti != 0) && diff > 0) {
|
|
qDebug() << waveform->sessionid << waveform->timestamp << "BND?" << (diff / 1000L) << "=" << waveform->timestamp << "-" << (lastti / 1000L);
|
|
}
|
|
if ((diff > 500) && (lastti != 0)) {
|
|
if (!bnd) {
|
|
bnd = session->AddEventList(PRS1_BND, EVL_Event);
|
|
}
|
|
bnd->AddEvent(ti, double(diff)/1000.0);
|
|
}
|
|
|
|
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 = 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, 0.1f, 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;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void PRS1Import::run()
|
|
{
|
|
if (mach->unsupported())
|
|
return;
|
|
|
|
if (ParseSession()) {
|
|
SaveSessionToDatabase();
|
|
}
|
|
}
|
|
|
|
|
|
bool PRS1Import::ParseSession(void)
|
|
{
|
|
bool save = false;
|
|
session = new Session(mach, sessionid);
|
|
|
|
if ((compliance && ParseCompliance()) || (summary && ParseSummary())) {
|
|
if (event && !ParseEvents()) {
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
ParseWaveforms();
|
|
|
|
// Parse .006 Waveform file
|
|
oximetry = loader->ParseFile(oxifile);
|
|
oximetry = CoalesceWaveformChunks(oximetry);
|
|
ParseOximetery();
|
|
|
|
if (session->first() > 0) {
|
|
if (session->last() < session->first()) {
|
|
// if last isn't set, duration couldn't be gained from summary, parsing events or waveforms..
|
|
// This session is dodgy, so kill it
|
|
session->setSummaryOnly(true);
|
|
session->really_set_last(session->first()+(qint64(summary_duration) * 1000L));
|
|
}
|
|
save = true;
|
|
}
|
|
}
|
|
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..
|
|
qDebug() << chunk->m_path << "@" << chunk->m_filepos << "block CRC calc" << hex << chunk->calcCrc << "!= stored" << hex << chunk->storedCrc;
|
|
//break; // don't break to avoid changing behavior (for now)
|
|
}
|
|
|
|
// 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; // don't break to avoid changing behavior (for now)
|
|
}
|
|
|
|
// 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.
|
|
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
|
|
*/
|
|
|