OSCAR-code/oscar/SleepLib/loader_plugins/resmed_loader.cpp

3822 lines
158 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* SleepLib ResMed Loader Implementation
*
* Copyright (c) 2019-2024 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. */
#define TEST_MACROS_ENABLEDoff
#include <test_macros.h>
#include <QApplication>
#include <QString>
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QMessageBox>
#include <QTextStream>
#include <QDebug>
#include <QStringList>
#include <QJsonDocument>
#include <QJsonObject>
#include <cmath>
#include "SleepLib/session.h"
#include "SleepLib/calcs.h"
#include "SleepLib/loader_plugins/resmed_loader.h"
#include "SleepLib/loader_plugins/resmed_EDFinfo.h"
#ifdef DEBUG_EFFICIENCY
#include <QElapsedTimer> // only available in 4.8 and later
#endif
#if QT_VERSION >= QT_VERSION_CHECK(5,15,0)
#define QTCOMBINE insert
//idmap.insert(hash);
#else
#define QTCOMBINE unite
//idmap.unite(hash);
#endif
ChannelID RMS9_EPR, RMS9_EPRLevel, RMS9_Mode, RMS9_SmartStart, RMS9_HumidStatus, RMS9_HumidLevel,
RMS9_PtAccess, RMS9_Mask, RMS9_ABFilter, RMS9_ClimateControl, RMS9_TubeType, RMAS11_SmartStop,
RMS9_Temp, RMS9_TempEnable, RMS9_RampEnable, RMAS1x_Comfort, RMAS11_PtView;
ChannelID RMAS1x_EasyBreathe, RMAS1x_RiseEnable, RMAS1x_RiseTime, RMAS1x_Cycle, RMAS1x_Trigger, RMAS1x_TiMax, RMAS1x_TiMin;
const QString STR_ResMed_AirSense10 = "AirSense 10";
const QString STR_ResMed_AirSense11 = "AirSense 11";
const QString STR_ResMed_AirCurve10 = "AirCurve 10";
const QString STR_ResMed_AirCurve11 = "AirCurve 11";
const QString STR_ResMed_S9 = "S9";
const QString STR_UnknownModel = "Resmed ???";
// TODO: See the PRSLoader::LogUnexpectedMessage TODO about generalizing this for other loaders.
void ResmedLoader::LogUnexpectedMessage(const QString & message)
{
m_importMutex.lock();
m_unexpectedMessages += message;
m_importMutex.unlock();
}
static const QVector<int> AS11TestedModels {39463, 39420, 39421, 39423, 39483, 39485, 39517, 0};
ResmedLoader::ResmedLoader() {
#ifndef UNITTEST_MODE
const QString RMS9_ICON = ":/icons/rms9.png";
const QString RM10_ICON = ":/icons/airsense10.png";
const QString RM10C_ICON = ":/icons/aircurve.png";
m_pixmaps[STR_ResMed_S9] = QPixmap(RMS9_ICON);
m_pixmap_paths[STR_ResMed_S9] = RMS9_ICON;
m_pixmaps[STR_ResMed_AirSense10] = QPixmap(RM10_ICON);
m_pixmap_paths[STR_ResMed_AirSense10] = RM10_ICON;
m_pixmaps[STR_ResMed_AirCurve10] = QPixmap(RM10C_ICON);
m_pixmap_paths[STR_ResMed_AirCurve10] = RM10C_ICON;
#endif
m_type = MT_CPAP;
#ifdef DEBUG_EFFICIENCY
timeInTimeDelta = timeInLoadBRP = timeInLoadPLD = timeInLoadEVE = 0;
timeInLoadCSL = timeInLoadSAD = timeInEDFInfo = timeInEDFOpen = timeInAddWaveform = 0;
#endif
saveCallback = SaveSession;
}
ResmedLoader::~ResmedLoader() { }
bool resmed_initialized = false;
void ResmedLoader::Register()
{
if (resmed_initialized)
return;
qDebug() << "Registering ResmedLoader";
RegisterLoader(new ResmedLoader());
resmed_initialized = true;
}
void setupResMedTranslationMap(); // forward
void ResmedLoader::initChannels()
{
using namespace schema;
// Channel(ChannelID id, ChanType type, MachineType machtype, ScopeType scope, QString code, QString fullname,
// QString description, QString label, QString unit, DataType datatype = DEFAULT, QColor = Qt::black, int link = 0);
Channel * chan = new Channel(RMS9_Mode = 0xe203, SETTING, MT_CPAP, SESSION,
"RMS9_Mode", QObject::tr("Mode"), QObject::tr("CPAP Mode"), QObject::tr("Mode"), "", LOOKUP, Qt::green);
channel.add(GRP_CPAP, chan);
chan->addOption(0, QObject::tr("CPAP"));
chan->addOption(1, QObject::tr("APAP"));
chan->addOption(2, QObject::tr("BiPAP-T"));
chan->addOption(3, QObject::tr("BiPAP-S"));
chan->addOption(4, QObject::tr("BiPAP-S/T"));
chan->addOption(5, QObject::tr("BiPAP-T"));
chan->addOption(6, QObject::tr("VPAPauto"));
chan->addOption(7, QObject::tr("ASV"));
chan->addOption(8, QObject::tr("ASVAuto"));
chan->addOption(9, QObject::tr("iVAPS"));
chan->addOption(10, QObject::tr("PAC"));
chan->addOption(11, QObject::tr("Auto for Her"));
chan->addOption(16, QObject::tr("Unknown")); // out of bounds of edf signal
channel.add(GRP_CPAP, chan = new Channel(RMS9_EPR = 0xe201, SETTING, MT_CPAP, SESSION,
"EPR", QObject::tr("EPR"), QObject::tr("ResMed Exhale Pressure Relief"), QObject::tr("EPR"), "", LOOKUP, Qt::green));
chan->addOption(0, STR_TR_Off);
chan->addOption(1, QObject::tr("Ramp Only"));
chan->addOption(2, QObject::tr("Full Time"));
chan->addOption(3, QObject::tr("Patient???"));
channel.add(GRP_CPAP, chan = new Channel(RMS9_EPRLevel = 0xe202, SETTING, MT_CPAP, SESSION,
"EPRLevel", QObject::tr("EPR Level"), QObject::tr("Exhale Pressure Relief Level"), QObject::tr("EPR Level"), STR_UNIT_CMH2O, LOOKUP, Qt::blue));
// RMS9_SmartStart, RMS9_HumidStatus, RMS9_HumidLevel,
// RMS9_PtAccess, RMS9_Mask, RMS9_ABFilter, RMS9_ClimateControl, RMS9_TubeType,
// RMS9_Temp, RMS9_TempEnable;
channel.add(GRP_CPAP, chan = new Channel(RMS9_SmartStart = 0xe204, SETTING, MT_CPAP, SESSION,
"RMS9_SmartStart", QObject::tr("SmartStart"), QObject::tr("Device auto starts by breathing"), QObject::tr("Smart Start"), "", LOOKUP, Qt::black));
chan->addOption(0, STR_TR_Off);
chan->addOption(1, STR_TR_On);
channel.add(GRP_CPAP, chan = new Channel(RMS9_HumidStatus = 0xe205, SETTING, MT_CPAP, SESSION,
"RMS9_HumidStat", QObject::tr("Humid. Status"), QObject::tr("Humidifier Enabled Status"), QObject::tr("Humidifier Status"), "", LOOKUP, Qt::black));
chan->addOption(0, STR_TR_Off);
chan->addOption(1, STR_TR_On);
channel.add(GRP_CPAP, chan = new Channel(RMS9_HumidLevel = 0xe206, SETTING, MT_CPAP, SESSION,
"RMS9_HumidLevel", QObject::tr("Humid. Level"), QObject::tr("Humidity Level"), QObject::tr("Humidity Level"), "", LOOKUP, Qt::black));
chan->addOption(0, STR_TR_Off);
// chan->addOption(1, "1");
// chan->addOption(2, "2");
// chan->addOption(3, "3");
// chan->addOption(4, "4");
// chan->addOption(5, "5");
// chan->addOption(6, "6");
// chan->addOption(7, "7");
// chan->addOption(8, "8");
channel.add(GRP_CPAP, chan = new Channel(RMS9_Temp = 0xe207, SETTING, MT_CPAP, SESSION,
"RMS9_Temp", QObject::tr("Temperature"), QObject::tr("ClimateLine Temperature"), QObject::tr("Temperature"), "ºC", INTEGER, Qt::black));
channel.add(GRP_CPAP, chan = new Channel(RMS9_TempEnable = 0xe208, SETTING, MT_CPAP, SESSION,
"RMS9_TempEnable", QObject::tr("Temp. Enable"), QObject::tr("ClimateLine Temperature Enable"), QObject::tr("Temperature Enable"), "", LOOKUP, Qt::black));
chan->addOption(0, STR_TR_Off);
chan->addOption(1, STR_TR_On);
chan->addOption(2, STR_TR_Auto);
channel.add(GRP_CPAP, chan = new Channel(RMS9_ABFilter= 0xe209, SETTING, MT_CPAP, SESSION,
"RMS9_ABFilter", QObject::tr("AB Filter"), QObject::tr("Antibacterial Filter"), QObject::tr("Antibacterial Filter"), "", LOOKUP, Qt::black));
chan->addOption(0, STR_TR_No);
chan->addOption(1, STR_TR_Yes);
channel.add(GRP_CPAP, chan = new Channel(RMS9_PtAccess= 0xe20A, SETTING, MT_CPAP, SESSION,
"RMS9_PTAccess", QObject::tr("Pt. Access"), QObject::tr("Essentials"), QObject::tr("Essentials"), "", LOOKUP, Qt::black));
chan->addOption(0, QObject::tr("Plus"));
chan->addOption(1, QObject::tr("On"));
channel.add(GRP_CPAP, chan = new Channel(RMS9_ClimateControl= 0xe20B, SETTING, MT_CPAP, SESSION,
"RMS9_ClimateControl", QObject::tr("Climate Control"), QObject::tr("Climate Control"), QObject::tr("Climate Control"), "", LOOKUP, Qt::black));
chan->addOption(0, STR_TR_Auto);
chan->addOption(1, QObject::tr("Manual"));
channel.add(GRP_CPAP, chan = new Channel(RMS9_Mask= 0xe20C, SETTING, MT_CPAP, SESSION,
"RMS9_Mask", QObject::tr("Mask"), QObject::tr("ResMed Mask Setting"), QObject::tr("Mask"), "", LOOKUP, Qt::black));
chan->addOption(0, QObject::tr("Pillows"));
chan->addOption(1, QObject::tr("Full Face"));
chan->addOption(2, QObject::tr("Nasal"));
chan->addOption(3, QObject::tr("Unknown"));
channel.add(GRP_CPAP, chan = new Channel(RMS9_RampEnable = 0xe20D, SETTING, MT_CPAP, SESSION,
"RMS9_RampEnable", QObject::tr("Ramp"), QObject::tr("Ramp Enable"), QObject::tr("Ramp"), "", LOOKUP, Qt::black));
chan->addOption(0, STR_TR_Off);
chan->addOption(1, STR_TR_On);
chan->addOption(2, STR_TR_Auto);
channel.add(GRP_CPAP, chan = new Channel(RMAS1x_Comfort = 0xe20E, SETTING, MT_CPAP, SESSION,
"RMAS1x_Comfort", QObject::tr("Response"), QObject::tr("Response"), QObject::tr("Response"), "", LOOKUP, Qt::black));
chan->addOption(0, QObject::tr("Standard")); // This must be verified
chan->addOption(1, QObject::tr("Soft"));
channel.add(GRP_CPAP, chan = new Channel(RMAS11_SmartStop = 0xe20F, SETTING, MT_CPAP, SESSION,
"RMAS11_SmartStop", QObject::tr("SmartStop"), QObject::tr("Device auto stops by breathing"), QObject::tr("Smart Stop"), "", LOOKUP, Qt::black));
chan->addOption(0, STR_TR_Off);
chan->addOption(1, STR_TR_On);
channel.add(GRP_CPAP, chan = new Channel(RMAS11_PtView= 0xe210, SETTING, MT_CPAP, SESSION,
"RMAS11_PTView", QObject::tr("Patient View"), QObject::tr("Patient View"), QObject::tr("Patient View"), "", LOOKUP, Qt::black));
chan->addOption(0, QObject::tr("Advanced"));
chan->addOption(1, QObject::tr("Simple"));
chan->addOption(0, STR_TR_Off);
chan->addOption(1, STR_TR_On);
channel.add(GRP_CPAP, chan = new Channel(RMAS1x_RiseEnable = 0xe212, SETTING, MT_CPAP, SESSION,
"RMAS1x_RiseEnable", QObject::tr("RiseEnable"), QObject::tr("RiseEnable"), QObject::tr("RiseEnable"), "", LOOKUP, Qt::black));
chan->addOption(0, STR_TR_Off);
chan->addOption(1, "Enabled");
channel.add(GRP_CPAP, chan = new Channel(RMAS1x_RiseTime = 0xe213, SETTING, MT_CPAP, SESSION,
"RMAS1x_RiseTime", QObject::tr("RiseTime"), QObject::tr("RiseTime"), QObject::tr("RiseTime"), STR_UNIT_milliSeconds, INTEGER, Qt::black));
channel.add(GRP_CPAP, chan = new Channel(RMAS1x_Cycle = 0xe214, SETTING, MT_CPAP, SESSION,
"RMAS1x_Cycle", QObject::tr("Cycle"), QObject::tr("Cycle"), QObject::tr("Cycle"), "", LOOKUP, Qt::black));
chan->addOption(0, "Very Low");
chan->addOption(1, "Low");
chan->addOption(2, "Med");
chan->addOption(3, "High");
chan->addOption(4, "Very High");
channel.add(GRP_CPAP, chan = new Channel(RMAS1x_Trigger = 0xe215, SETTING, MT_CPAP, SESSION,
"RMAS1x_Trigger", QObject::tr("Trigger"), QObject::tr("Trigger"), QObject::tr("Trigger"), "", LOOKUP, Qt::black));
chan->addOption(0, "Very Low");
chan->addOption(1, "Low");
chan->addOption(2, "Med");
chan->addOption(3, "High");
chan->addOption(4, "Very High");
channel.add(GRP_CPAP, chan = new Channel(RMAS1x_TiMax = 0xe216, SETTING, MT_CPAP, SESSION,
"RMAS1x_TiMax", QObject::tr("TiMax"), QObject::tr("TiMax"), QObject::tr("TiMax"), STR_UNIT_Seconds, DOUBLE, Qt::black));
chan->addOption(0, "0");
channel.add(GRP_CPAP, chan = new Channel(RMAS1x_TiMin = 0xe217, SETTING, MT_CPAP, SESSION,
"RMAS1x_TiMin", QObject::tr("TiMin"), QObject::tr("TiMin"), QObject::tr("TiMin"), STR_UNIT_Seconds, DOUBLE, Qt::black));
chan->addOption(0, "0");
// Setup ResMeds signal name translation map
setupResMedTranslationMap();
}
ChannelID ResmedLoader::CPAPModeChannel() { return RMS9_Mode; }
ChannelID ResmedLoader::PresReliefMode() { return RMS9_EPR; }
ChannelID ResmedLoader::PresReliefLevel() { return RMS9_EPRLevel; }
QHash<ChannelID, QStringList> resmed_codes;
const QString STR_ext_TGT = "tgt";
const QString STR_ext_JSON = "json";
const QString STR_ext_CRC = "crc";
const QString RMS9_STR_datalog = "DATALOG";
const QString RMS9_STR_idfile = "Identification.";
const QString RMS9_STR_strfile = "STR.";
bool ResmedLoader::Detect(const QString & givenpath)
{
QDir dir(givenpath);
if (!dir.exists()) {
return false;
}
// ResMed drives contain a folder named "DATALOG".
if (!dir.exists(RMS9_STR_datalog)) {
return false;
}
// They also contain a file named "STR.edf".
if (!dir.exists("STR.edf")) {
return false;
}
return true;
}
QHash<QString, QString> parseIdentLine( const QString line, MachineInfo * info); // forward
void scanProductObject( const QJsonObject product, MachineInfo *info, QHash<QString, QString> *idmap ); // forward
MachineInfo ResmedLoader::PeekInfo(const QString & path)
{
if (!Detect(path))
return MachineInfo();
QFile f(path+"/"+RMS9_STR_idfile+"tgt");
QFile j(path+"/"+RMS9_STR_idfile+"json"); // Check for AS11 file first, just in case
if (j.exists() ) { // somebody is reusing an SD card w/o re-formatting
if ( !j.open(QIODevice::ReadOnly)) {
return MachineInfo();
}
if ( f.exists() ) {
qDebug() << "Old Ident.tgt file is ignored";
}
QByteArray identData = j.readAll();
j.close();
QJsonDocument identDoc(QJsonDocument::fromJson(identData));
QJsonObject identObj = identDoc.object();
if ( identObj.contains("FlowGenerator") && identObj["FlowGenerator"].isObject()) {
QJsonObject flow = identObj["FlowGenerator"].toObject();
if ( flow.contains("IdentificationProfiles") && flow["IdentificationProfiles"].isObject()) {
QJsonObject profiles = flow["IdentificationProfiles"].toObject();
if ( profiles.contains("Product") && profiles["Product"].isObject()) {
QJsonObject product = profiles["Product"].toObject();
MachineInfo info = newInfo();
scanProductObject( product, &info, nullptr);
return info;
} else
qDebug() << "No Product in Profiles";
} else
qDebug() << "No IdentificationProfiles in FlowGenerator";
} else
qDebug() << "No FlowGenerator in Identification.json";
return MachineInfo();
}
// Abort if this file is dodgy..
if (f.exists() ) {
if ( !f.open(QIODevice::ReadOnly)) {
return MachineInfo();
}
MachineInfo info = newInfo();
// Parse # entries into idmap.
while (!f.atEnd()) {
QString line = f.readLine().trimmed();
QHash<QString, QString> hash = parseIdentLine( line, & info );
}
return info;
}
// neither filename exists, return empty info
return MachineInfo();
}
long event_cnt = 0;
bool parseIdentFile( QString path, MachineInfo * info, QHash<QString, QString> & idmap ); // forward
void backupSTRfiles( const QString strpath, const QString importPath, const QString backupPath,
MachineInfo & info, QMap<QDate, STRFile> & STRmap ); // forward
ResMedEDFInfo * fetchSTRandVerify( QString filename, QString serialNumber ); // forward
int ResmedLoader::OpenWithCallback(const QString & dirpath, ResDaySaveCallback s) // alternate for unit testing
{
ResDaySaveCallback origCallback = saveCallback;
saveCallback = s;
int value = Open(dirpath);
saveCallback = origCallback;
return value;
}
int ResmedLoader::Open(const QString & dirpath)
{
qDebug() << "Starting ResmedLoader::Open( with " << dirpath << ")";
QString datalogPath;
QHash<QString, QString> idmap; // Temporary device ID properties hash
QString importPath(dirpath);
importPath = importPath.replace("\\", "/");
// Strip off end "/" if any
if (importPath.endsWith("/")) {
importPath = importPath.section("/", 0, -2);
}
// Strip off DATALOG from importPath, and set newimportPath to the importPath containing DATALOG
if (importPath.endsWith(RMS9_STR_datalog)) {
datalogPath = importPath + "/";
importPath = importPath.section("/", 0, -2);
} else {
datalogPath = importPath + "/" + RMS9_STR_datalog + "/";
}
// Add separator back
importPath += "/";
// Check DATALOG folder exists and is readable
if (!QDir().exists(datalogPath)) {
qDebug() << "Missing DATALOG in" << dirpath;
return -1;
}
m_abort = false;
MachineInfo info = newInfo();
if ( ! parseIdentFile(importPath, & info, idmap) ) {
qDebug() << "Failed to parse Identification file";
return -1;
}
qDebug() << "Info:" << info.series << info.model << info.modelnumber << info.serial;
#ifdef IDENT_DEBUG
qDebug() << "IdMap size:" << idmap.size();
foreach ( QString st , idmap.keys() ) {
qDebug() << "Key" << st << "Value" << idmap[st];
}
#endif
// Abort if no serial number
if (info.serial.isEmpty()) {
qDebug() << "ResMed Data card is missing serial number in Indentification.tgt";
return -1;
}
bool compress_backups = p_profile->session->compressBackupData();
// Early check for STR.edf file, so we can early exit before creating faulty device record.
// str.edf is the first (primary) file to check, str.edf.gz is the secondary
QString pripath = importPath + "STR.edf"; // STR.edf file
QString secpath = pripath + STR_ext_gz; // STR.edf.gz file
QString strpath;
// If compression is enabled, swap primary and secondary paths
if (compress_backups) {
strpath = pripath;
pripath = secpath;
secpath = strpath;
}
// Check if primary path exists
QFile f(pripath);
if (f.exists()) {
strpath = pripath;
// If no primary file, check for secondary
} else {
f.setFileName(secpath);
strpath = secpath;
if (!f.exists()) {
qDebug() << "Missing STR.edf file";
return -1;
}
}
///////////////////////////////////////////////////////////////////////////////////
// Create device object (unless it's already registered)
///////////////////////////////////////////////////////////////////////////////////
QDate firstImportDay = QDate().fromString("2010-01-01", "yyyy-MM-dd"); // Before Series 8 devices (I think)
Machine *mach = p_profile->lookupMachine(info.serial, info.loadername);
if ( mach ) { // we have seen this device
qDebug() << "We have seen this machime";
mach->setInfo( info ); // update info
QDate lastDate = mach->LastDay(); // use the last day for this device
firstImportDay = lastDate; // re-import the last day, to pick up partial days
QDate purgeDate = mach->purgeDate();
if (purgeDate.isValid()) {
firstImportDay = min(firstImportDay, purgeDate);
}
// firstImportDay = lastDate.addDays(-1); // start the day before, to pick up partial days
// firstImportDay = lastDate.addDays(1); // start the day after until we figure out the purge
} else { // Starting from new beginnings - new or purged
qDebug() << "New device or just purged";
p_profile->forceResmedPrefs();
int modelNum = info.modelnumber.toInt();
if ( modelNum >= 39000 ) {
if ( ! AS11TestedModels.contains(modelNum) ) {
QMessageBox::information(QApplication::activeWindow(),
QObject::tr("Device Untested"),
QObject::tr("Your ResMed CPAP device (Model %1) has not been tested yet.").arg(info.modelnumber) +"\n\n"+
QObject::tr("It seems similar enough to other devices that it might work, but the developers would like a .zip copy of this device's SD card to make sure it works with OSCAR.")
,QMessageBox::Ok);
}
}
mach = p_profile->CreateMachine( info );
}
QDateTime ignoreBefore = p_profile->session->ignoreOlderSessionsDate();
bool ignoreOldSessions = p_profile->session->ignoreOlderSessions();
if (ignoreOldSessions && (ignoreBefore.date() > firstImportDay))
firstImportDay = ignoreBefore.date();
qDebug() << "First day to import: " << firstImportDay.toString();
bool rebuild_from_backups = false;
bool create_backups = p_profile->session->backupCardData();
QString backup_path = mach->getBackupPath();
// Compare QDirs rather than QStrings because separators may be different, especially on Windows.
// We want to check whether import and backup paths are the same, regardless of variations in the string representations.
QDir ipath(importPath);
QDir bpath(backup_path);
if (ipath == bpath) {
// Don't create backups if importing from backup folder
rebuild_from_backups = true;
create_backups = false;
}
///////////////////////////////////////////////////////////////////////////////////
// Copy the idmap into device objects properties, (overwriting any old values)
///////////////////////////////////////////////////////////////////////////////////
for (auto i=idmap.begin(), idend=idmap.end(); i != idend; i++) {
mach->info.properties[i.key()] = i.value();
}
///////////////////////////////////////////////////////////////////////////////////
// Create the backup folder structure for storing a copy of everything in..
// (Unless we are importing from this backup folder)
///////////////////////////////////////////////////////////////////////////////////
QDir dir;
if (create_backups) {
if ( ! dir.exists(backup_path)) {
if ( ! dir.mkpath(backup_path) ) {
qWarning() << "Could not create ResMed backup directory" << backup_path;
}
}
// Create the STR_Backup folder if it doesn't exist
QString strBackupPath = backup_path + "STR_Backup";
if ( ! dir.exists(strBackupPath) )
if (!dir.mkpath(strBackupPath))
qWarning() << "Could not create ResMed STR backup directory" << strBackupPath;
QString newpath = backup_path + "DATALOG";
if ( ! dir.exists(newpath) )
if (!dir.mkpath(newpath))
qWarning() << "Could not create ResMed DATALOG backup directory" << newpath;
// Copy Identification files to backup folder
QString idfile_ext;
if (QFile(importPath+RMS9_STR_idfile+STR_ext_TGT).exists()) {
idfile_ext = STR_ext_TGT;
}
else if (QFile(importPath+RMS9_STR_idfile+STR_ext_JSON).exists()) {
idfile_ext = STR_ext_JSON;
}
else {
idfile_ext = ""; // should never happen...
}
QFile backupFile(backup_path + RMS9_STR_idfile + idfile_ext);
if (backupFile.exists())
backupFile.remove();
if ( ! QFile::copy(importPath + RMS9_STR_idfile + idfile_ext, backup_path + RMS9_STR_idfile + idfile_ext))
qWarning() << "Could not copy" << importPath + RMS9_STR_idfile + idfile_ext << "to backup" << backupFile.fileName();
backupFile.setFileName(backup_path + RMS9_STR_idfile + STR_ext_CRC);
if (backupFile.exists())
backupFile.remove();
if ( ! QFile::copy(importPath + RMS9_STR_idfile + STR_ext_CRC, backup_path + RMS9_STR_idfile + STR_ext_CRC))
qWarning() << "Could not copy" << importPath + RMS9_STR_idfile + STR_ext_CRC << "to backup" << backup_path;
}
///////////////////////////////////////////////////////////////////////////////////
// Open and Process STR.edf files (including those listed in STR_Backup)
///////////////////////////////////////////////////////////////////////////////////
resdayList.clear();
emit updateMessage(QObject::tr("Locating STR.edf File(s)..."));
QCoreApplication::processEvents();
// List all STR.edf backups and tag on latest for processing
QMap<QDate, STRFile> STRmap;
if ( ( ! rebuild_from_backups) /* && create_backups */ ) {
// first we copy any STR_yyyymmdd.edf files and the Backup/STR.edf into STR_Backup and the STRmap
backupSTRfiles( strpath, importPath, backup_path, info, STRmap );
//Then we copy the new imported STR.edf into Backup/STR.edf and add it to the STRmap
QString importFile(importPath+"STR.edf");
QString backupFile(backup_path + "STR.edf");
ResMedEDFInfo * stredf = fetchSTRandVerify( importFile, info.serial );
if ( stredf != nullptr ) {
bool addToSTRmap = true;
QDate date = stredf->edfHdr.startdate_orig.date();
long int days = stredf->GetNumDataRecords();
qDebug() << importFile.section("/",-3,-1) << "starts at" << date << "for" << days << "ends" << date.addDays(days-1);
if (STRmap.contains(date)) { // Keep the longer of the two STR files - or newer if equal!
qDebug().noquote() << importFile.section("/",-3,-1) << "overlaps" << STRmap[date].filename.section("/",-3,-1) << "for" << days << "days, ends" << date.addDays(days-1);
if (days >= STRmap[date].days) {
qDebug() << "Removing" << STRmap[date].filename.section("/",-3,-1) << "with" << STRmap[date].days << "days from STRmap";
STRmap.remove(date);
} else {
qDebug() << "Skipping" << importFile.section("/",-3,-1);
qWarning() << "New import str.edf file is shorter than exisiting files - should never happen";
delete stredf;
addToSTRmap = false;
}
}
if ( addToSTRmap ) {
if ( compress_backups ) {
backupFile += ".gz";
if ( QFile::exists( backupFile ) )
QFile::remove( backupFile );
compressFile(importFile, backupFile);
}
else {
if ( QFile::exists( backupFile ) )
QFile::remove( backupFile );
if ( ! QFile::copy(importFile, backupFile) )
qWarning() << "Failed to copy" << importFile << "to" << backupFile;
}
STRmap[date] = STRFile(backupFile, days, stredf);
qDebug() << "Adding" << importFile << "to STRmap as" << backupFile;
// Meh.. these can be calculated if ever needed for ResScan SDcard export
QFile sourcePath(importPath + "STR.crc");
if (sourcePath.exists()) {
QFile backupFile(backup_path + "STR.crc");
if (backupFile.exists())
if (!backupFile.remove())
qWarning() << "Failed to remove" << backupFile.fileName();
if (!QFile::copy(importPath + "STR.crc", backup_path + "STR.crc"))
qWarning() << "Failed to copy STR.crc from" << importPath << "to" << backup_path;
}
}
}
} else { // get the STR file that is in the BACKUP folder that we are rebuilding from
qDebug() << "Rebuilding from BACKUP folder";
ResMedEDFInfo * stredf = fetchSTRandVerify( strpath, info.serial );
if ( stredf != nullptr ) {
QDate date = stredf->edfHdr.startdate_orig.date();
long int days = stredf->GetNumDataRecords();
qDebug() << strpath.section("/",-2,-1) << "starts at" << date << "for" << days << "ends" << date.addDays(days-1);
STRmap[date] = STRFile(strpath, days, stredf);
} else {
qDebug() << "Failed to open" << strpath;
}
} // end if not importing the backup files
#ifdef STR_DEBUG
qDebug() << "STRmap size is " << STRmap.size();
#endif
// Now we open the REAL destination STR_Backup, and open the rest for later parsing
dir.setPath(backup_path + "STR_Backup");
dir.setFilter(QDir::Files | QDir::Hidden | QDir::Readable);
QFileInfoList flist = dir.entryInfoList();
QDate date;
long int days;
#ifdef STR_DEBUG
qDebug() << "STR_Backup folder size is " << flist.size();
#endif
qDebug() << "Add files in STR_Backup to STRmap (unless they are already there)";
// Add any STR_Backup versions to the file list
for (auto & fi : flist) {
QString filename = fi.fileName();
if ( ! filename.startsWith("STR", Qt::CaseInsensitive))
continue;
if ( ! (filename.endsWith("edf.gz", Qt::CaseInsensitive) || filename.endsWith("edf", Qt::CaseInsensitive)))
continue;
QString datestr = filename.section("STR-",-1).section(".edf",0,0); // +"01";
ResMedEDFInfo * stredf = fetchSTRandVerify( fi.canonicalFilePath(), info.serial );
if ( stredf == nullptr )
continue;
// Don't trust the filename date, pick the one inside the STR...
date = stredf->edfHdr.startdate_orig.date();
days = stredf->GetNumDataRecords();
if (STRmap.contains(date)) { // Keep the longer of the two STR files
qDebug().noquote() << fi.canonicalFilePath().section("/",-3,-1) << "overlaps" << STRmap[date].filename.section("/",-3,-1) << "for" << days << "ends" << date.addDays(days-1);
if (days <= STRmap[date].days) {
qDebug() << "Skipping" << fi.canonicalFilePath().section("/",-3,-1);
delete stredf;
continue;
} else {
qDebug() << "Removing" << STRmap[date].filename.section("/",-3,-1) << "from STRmap";
STRmap.remove(date);
}
}
qDebug() << "Adding" << fi.canonicalFilePath().section("/", -3,-1) << "starts at" << date << "for" << days << "to STRmap";
STRmap[date] = STRFile(fi.canonicalFilePath(), days, stredf);
} // end for walking the STR_Backup directory
#ifdef STR_DEBUG
qDebug() << "Finished STRmap size is now " << STRmap.size();
#endif
///////////////////////////////////////////////////////////////////////////////////
// Build a Date map of all records in STR.edf files, populating ResDayList
///////////////////////////////////////////////////////////////////////////////////
if ( ! ProcessSTRfiles(mach, STRmap, firstImportDay) ) {
qCritical() << "ProcessSTR failed, abandoning this import";
return -1;
}
// We are done with the Parsed STR EDF objects, so delete them
for (auto it=STRmap.begin(), end=STRmap.end(); it != end; ++it) {
QString fullname = it.value().filename;
#ifdef STR_DEBUG
qDebug() << "Deleting edf object of" << fullname;
#endif
QString datepart = fullname.section("STR-",-1).section(".edf",0,0);
if (datepart.size() == 6 ) { // old style name, change to full date
QFile str(fullname);
QString newdate = it.key().toString("yyyyMMdd");
QString newName = fullname.replace(datepart, newdate);
qDebug() << "Renaming" << it.value().filename << "to" << newName;
if ( ! str.rename(newName) )
qWarning() << "Rename Failed";
}
delete it.value().edf;
}
#ifdef STR_DEBUG
qDebug() << "Finished STRmap cleanup";
#endif
///////////////////////////////////////////////////////////////////////////////////
// Scan DATALOG files, sort, and import any new sessions
///////////////////////////////////////////////////////////////////////////////////
// First remove a legacy file if present...
QFile impfile(mach->getDataPath()+"/imported_files.csv");
if (impfile.exists())
impfile.remove();
emit updateMessage(QObject::tr("Cataloguing EDF Files..."));
QApplication::processEvents();
if (isAborted())
return 0;
qDebug() << "Starting scan of DATALOG";
// sleep(1);
dir.setPath(datalogPath);
ScanFiles(mach, datalogPath, firstImportDay);
if (isAborted())
return 0;
qDebug() << "Finished DATALOG scan";
// sleep(1);
// Now at this point we have resdayList populated with processable summary and EDF files data
// that can be processed in threads..
emit updateMessage(QObject::tr("Queueing Import Tasks..."));
QApplication::processEvents();
for (auto rdi=resdayList.begin(), rend=resdayList.end(); rdi != rend; rdi++) {
if (isAborted())
return 0;
QDate date = rdi.key();
ResMedDay & resday = rdi.value();
resday.date = date;
checkSummaryDay( resday, date, mach );
}
sessionCount = 0;
emit updateMessage(QObject::tr("Importing Sessions..."));
// Walk down the resDay list
qDebug() << "About to call runTasks()";
runTasks();
qDebug() << "Finshed runTasks() with" << sessionCount << "new sessions";
int num_new_sessions = sessionCount;
////////////////////////////////////////////////////////////////////////////////////
// Now look for any new summary data that can be extracted from STR.edf records
////////////////////////////////////////////////////////////////////////////////////
emit updateMessage(QObject::tr("Finishing Up..."));
QApplication::processEvents();
qDebug() << "About to call finishAddingSessions()";
finishAddingSessions();
qDebug() << "Finshed finishedAddingSessions() with" << sessionCount << "new sessions";
#ifdef DEBUG_EFFICIENCY
{
qint64 totalbytes = 0;
qint64 totalns = 0;
qDebug() << "Performance / Efficiency Information";
for (auto it = channel_efficiency.begin(), end=channel_efficiency.end(); it != end; it++) {
ChannelID code = it.key();
qint64 value = it.value();
qint64 ns = channel_time[code];
totalbytes += value;
totalns += ns;
double secs = double(ns) / 1000000000.0L;
QString s = value < 0 ? "saved" : "cost";
qDebug() << "Time-Delta conversion for " + schema::channel[code].label() + " " + s + " " +
QString::number(qAbs(value)) + " bytes and took " + QString::number(secs, 'f', 4) + "s";
}
qDebug() << "Total toTimeDelta function usage:" << totalbytes << "in" << double(totalns) / 1000000000.0 << "seconds";
qDebug() << "Total CPU time in EDF Open" << timeInEDFOpen;
qDebug() << "Total CPU time in EDF Parser" << timeInEDFInfo;
qDebug() << "Total CPU time in LoadBRP" << timeInLoadBRP;
qDebug() << "Total CPU time in LoadPLD" << timeInLoadPLD;
qDebug() << "Total CPU time in LoadSAD" << timeInLoadSAD;
qDebug() << "Total CPU time in LoadEVE" << timeInLoadEVE;
qDebug() << "Total CPU time in LoadCSL" << timeInLoadCSL;
qDebug() << "Total CPU time in (BRP) AddWaveform" << timeInAddWaveform;
qDebug() << "Total CPU time in TimeDelta function" << timeInTimeDelta;
channel_efficiency.clear();
channel_time.clear();
}
#endif
// sessfiles.clear();
// strsess.clear();
// strdate.clear();
qDebug() << "Total Events " << event_cnt;
qDebug() << "Total new Sessions " << num_new_sessions;
mach->clearPurgeDate();
return num_new_sessions;
} // end Open()
ResMedEDFInfo * fetchSTRandVerify( QString filename, QString serialNumber)
{
ResMedEDFInfo * stredf = new ResMedEDFInfo();
if ( ! stredf->Open(filename ) ) {
qWarning() << "Failed to open" << filename;
delete stredf;
return nullptr;
}
if ( ! stredf->Parse()) {
qDebug() << "Faulty STR file" << filename;
delete stredf;
return nullptr;
}
if (stredf->serialnumber != serialNumber) {
qDebug() << "Identification.tgt Serial number doesn't match" << filename;
delete stredf;
return nullptr;
}
return stredf;
}
void StoreSettings(Session * sess, STRRecord & R); // forward
void ResmedLoader::checkSummaryDay( ResMedDay & resday, QDate date, Machine * mach )
{
Day * day = p_profile->FindDay(date, MT_CPAP);
bool reimporting = false;
#ifdef STR_DEBUG
qDebug() << "Starting checkSummary for" << date.toString();
#endif
if (day && day->hasMachine(mach)) {
// Sessions found for this device, check if only summary info
#ifdef STR_DEBUG
qDebug() << "Sessions already found for this date";
#endif
if (day->summaryOnly(mach) && (resday.files.size()> 0)) {
// Note: if this isn't an EDF file, there's really no point doing this here,
// but the worst case scenario is this session is deleted and reimported.. this just slows things down a bit in that case
// This day was first imported as a summary from STR.edf, so we now totally want to redo this day
#ifdef STR_DEBUG
qDebug() << "Summary sessions only - delete them";
#endif
QList<Session *> sessions = day->getSessions(MT_CPAP);
for (auto & sess : sessions) {
day->removeSession(sess);
delete sess;
}
} else if (day->noSettings(mach) && resday.str.date.isValid()) {
// STR is present now, it wasn't before... we don't need to trash the files, but we do want the official settings.
// Do it right here
#ifdef STR_DEBUG
qDebug() << "Date was missing settings, now we have them";
#endif
for (auto & sess : day->sessions) {
if (sess->machine() != mach)
continue;
#ifdef STR_DEBUG
qDebug() << "Adding STR.edf information to session" << sess->session();
#endif
StoreSettings(sess, resday.str);
sess->setNoSettings(false);
sess->SetChanged(true);
sess->StoreSummary();
}
} else {
#ifdef STR_DEBUG
qDebug() << "Have summary and details for this date!";
#endif
int numPairs = 0;
for (int i = 0; i <resday.str.maskevents/2; i++)
if (resday.str.maskon[i] != resday.str.maskoff[i])
numPairs++;
QList<Session *> sessions = day->getSessions(MT_CPAP, true);
// If we have more sessions that we found in the str file,
// or if the sessions are for a different device,
// leave well enough alone and don't re-import the day
if (sessions.length() >= numPairs || sessions[0]->machine() != mach) {
#ifdef STR_DEBUG
qDebug() << "No new sessions -- skipping. Sessions now in day:";
qDebug() << " i sessionID s_first from - to";
for (int i=0; i < sessions.length(); i++) {
qDebug().noquote() << i << sessions[i]->session()
<< sessions[i]->first()
<< QDateTime::fromMSecsSinceEpoch(sessions[i]->first()).toString(" hh:mm:ss")
<< "-" << QDateTime::fromMSecsSinceEpoch(sessions[i]->last()).toString("hh:mm:ss");
}
#endif
return;
}
qDebug() << "Maskevent count/2 (modified)" << numPairs << "is greater than the existing MT_CPAP session count" << sessions.length();
qDebug().noquote() << "Purging and re-importing" << day->date().toString();
for (auto & sess : sessions) {
day->removeSession(sess);
delete sess;
}
}
}
ResDayTask * rdt = new ResDayTask(this, mach, &resday, saveCallback);
rdt->reimporting = reimporting;
#ifdef STR_DEBUG
qDebug() << "in checkSummary, Queue task for" << resday.date.toString();
#endif
queTask(rdt);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Sorted EDF files that need processing into date records according to ResMed noon split
///////////////////////////////////////////////////////////////////////////////////////////
int ResmedLoader::ScanFiles(Machine * mach, const QString & datalog_path, QDate firstImport)
{
#ifdef DEBUG_EFFICIENCY
QTime time;
#endif
bool create_backups = p_profile->session->backupCardData();
QString backup_path = mach->getBackupPath();
if (datalog_path == (backup_path + RMS9_STR_datalog + "/")) {
// Don't create backups if importing from backup folder
create_backups = false;
}
///////////////////////////////////////////////////////////////////////////////////////
// Generate list of files for later processing
///////////////////////////////////////////////////////////////////////////////////////
qDebug() << "Generating list of EDF files";
qDebug() << "First Import date is " << firstImport;
#ifdef DEBUG_EFFICIENCY
time.start();
#endif
QDir dir(datalog_path);
// First list any EDF files in DATALOG folder - Series 9 devices
QStringList filter;
filter << "*.edf";
dir.setNameFilters(filter);
QFileInfoList EDFfiles = dir.entryInfoList();
// Scan through all folders looking for EDF files, skip any already imported and peek inside to get durations
dir.setNameFilters(QStringList());
dir.setFilter(QDir::Dirs | QDir::Hidden | QDir::NoDotAndDotDot);
QString filename;
bool ok;
QFileInfoList dirlist = dir.entryInfoList();
int dirlistSize = dirlist.size();
// QDateTime ignoreBefore = p_profile->session->ignoreOlderSessionsDate();
// bool ignoreOldSessions = p_profile->session->ignoreOlderSessions();
// Scan for any sub folders and create files lists
for (int i = 0; i < dirlistSize ; i++) {
const QFileInfo & fi = dirlist.at(i);
filename = fi.fileName();
int len = filename.length();
if (len == 4) { // This is a year folder in BackupDATALOG
filename.toInt(&ok);
if ( ! ok ) {
qDebug() << "Skipping directory - bad 4-letter name" << filename;
continue;
}
} else if (len == 8) { // test directory date
QDate dirDate = QDate().fromString(filename, "yyyyMMdd");
if (dirDate < firstImport) {
#ifdef SESSION_DEBUG
qDebug() << "Skipping directory - ignore before " << filename;
#endif
continue;
}
} else {
qDebug() << "Skipping directory - bad name size " << filename;
continue;
}
// Get file lists under this directory
dir.setPath(fi.canonicalFilePath());
dir.setFilter(QDir::Files | QDir::Hidden | QDir::NoSymLinks);
dir.setSorting(QDir::Name);
// Append all files to one big QFileInfoList
EDFfiles.append(dir.entryInfoList());
}
#ifdef DEBUG_EFFICIENCY
qDebug() << "Generating EDF files list took" << time.elapsed() << "ms";
#endif
qDebug() << "EDFfiles list size is " << EDFfiles.size();
////////////////////////////////////////////////////////////////////////////////////////
// Scan through EDF files, Extracting EDF Durations, and skipping already imported files
// Check for duplicates along the way from compressed/uncompressed files
////////////////////////////////////////////////////////////////////////////////////////
#ifdef DEBUG_EFFICIENCY
time.start();
#endif
QString datestr;
QDateTime datetime;
QDate date;
int totalfiles = EDFfiles.size();
qDebug() << "Scanning " << totalfiles << " EDF files";
// Calculate number of files for progress bar for this stage
int pbarFreq = totalfiles / 50;
if (pbarFreq < 1) // stop a divide by zero
pbarFreq = 1;
emit setProgressValue(0);
emit setProgressMax(totalfiles);
QCoreApplication::processEvents();
qDebug() << "Starting EDF duration scan pass";
for (int i=0; i < totalfiles; ++i) {
if (isAborted())
return 0;
const QFileInfo & fi = EDFfiles.at(i);
// Update progress bar
if ((i % pbarFreq) == 0) {
emit setProgressValue(i);
QCoreApplication::processEvents();
}
// Forget about it if it can't be read.
if (!fi.isReadable()) {
qWarning() << fi.fileName() << "is unreadable and has been ignored";
continue;
}
// Skip empty files
if (fi.size() == 0) {
qWarning() << fi.fileName() << "is empty and has been ignored";
continue;
}
filename = fi.fileName();
datestr = filename.section("_", 0, 1);
datetime = QDateTime().fromString(datestr,"yyyyMMdd_HHmmss");
date = datetime.date();
// ResMed splits days at noon and now so do we, so all times before noon
// go to the previous day
if (datetime.time().hour() < 12) {
date = date.addDays(-1);
}
if (date < firstImport) {
#ifdef SESSION_DEBUG
qDebug() << "Skipping file - ignore before " << filename;
#endif
continue;
}
// Chop off the .gz component if it exists, it's not needed at this stage
if (filename.endsWith(STR_ext_gz)) {
filename.chop(3);
}
QString fullpath = fi.filePath();
QString newpath = create_backups ? Backup(fullpath, backup_path) : fullpath;
// Accept only .edf and .edf.gz files
if (filename.right(4).toLower() != ("."+STR_ext_EDF))
continue;
// QString ext = key.section("_", -1).section(".",0,0).toUpper();
// EDFType type = lookupEDFType(ext);
// Find or create ResMedDay object for this date
auto rd = resdayList.find(date);
if (rd == resdayList.end()) {
rd = resdayList.insert(date, ResMedDay(date));
rd.value().date = date;
// We have data files without STR.edf record... the user MAY be planning on importing from another backup
// later which could cause problems if we don't deal with it.
// Best solution I can think of is import and tag the day No Settings and skip the day from overview.
}
ResMedDay & resday = rd.value();
if ( ! resday.files.contains(filename)) {
resday.files[filename] = newpath;
}
}
#ifdef DEBUG_EFFICIENCY
qDebug() << "Scanning EDF files took" << time.elapsed() << "ms";
#endif
qDebug() << "resdayList size is " << resdayList.size();
return resdayList.size();
} // end of scanFiles
QString ResmedLoader::Backup(const QString & fullname, const QString & backup_path)
{
QDir dir;
QString filename, yearstr, newname, oldname;
bool compress = p_profile->session->compressBackupData();
bool ok;
bool gz = (fullname.right(3).toLower() == STR_ext_gz); // Input file is a .gz?
filename = fullname.section("/", -1);
if (gz) {
filename.chop(3);
}
yearstr = filename.left(4);
yearstr.toInt(&ok, 10);
if ( ! ok) {
qDebug() << "Invalid EDF filename given to ResMedLoader::Backup()" << fullname;
return "";
}
QString newpath = backup_path + "DATALOG" + "/" + yearstr;
if ( ! dir.exists(newpath) )
dir.mkpath(newpath);
newname = newpath+"/"+filename;
QString tmpname = newname;
QString newnamegz = newname + STR_ext_gz;
QString newnamenogz = newname;
newname = compress ? newnamegz : newnamenogz;
// First make sure the correct backup exists in the right place
// Allow for second import of newer version of EVE and CSL edf files
// But don't try to copy onto itself (as when rebuilding CPAP data from backup)
// Compare QDirs rather than QStrings to handle variations in separators, etc.
QFile nf(newname);
QFile of(fullname);
QFileInfo nfi(nf);
QFileInfo ofi(of);
QDir nfdir = nfi.dir();
QDir ofdir = ofi.dir();
if (nfdir != ofdir) {
if (QFile::exists(newname)) // remove existing backup
QFile::remove(newname);
if (compress) {
// If input file is already compressed.. copy it to the right location, otherwise compress it
if (gz) {
if (!QFile::copy(fullname, newname))
qWarning() << "unable to copy" << fullname << "to" << newname;
}
else
compressFile(fullname, newname);
} else {
// If inputs a gz, uncompress it, otherwise copy is raw
if (gz)
uncompressFile(fullname, newname);
else {
if (!QFile::copy(fullname, newname))
qWarning() << "unable to copy" << fullname << "to" << newname;
}
}
}
// Now the correct backup is in place, we can trash any unneeded backup
if (compress) {
// Remove any uncompressed duplicate
if (QFile::exists(newnamenogz))
QFile::remove(newnamenogz);
} else {
// Delete the compressed copy
if (QFile::exists(newnamegz))
QFile::remove(newnamegz);
}
// Used to store it under Backup\Datalog
// Remove any traces from old backup directory structure
if (nfdir != ofdir) {
oldname = backup_path + RMS9_STR_datalog + "/" + filename;
if (QFile::exists(oldname))
QFile::remove(oldname);
if (QFile::exists(oldname + STR_ext_gz))
QFile::remove(oldname + STR_ext_gz);
}
return newname;
}
// This function parses a list of STR files and creates a date ordered map of individual records
bool ResmedLoader::ProcessSTRfiles(Machine *mach, QMap<QDate, STRFile> & STRmap, QDate firstImport)
{
bool AS_eleven = (mach->info.modelnumber.toInt() >= 39000);
// QDateTime ignoreBefore = p_profile->session->ignoreOlderSessionsDate();
// bool ignoreOldSessions = p_profile->session->ignoreOlderSessions();
qDebug() << "Starting ProcessSTRfiles";
int totalRecs = 0; // Count the STR days
for (auto it=STRmap.begin(), end=STRmap.end(); it != end; ++it) {
STRFile & file = it.value();
ResMedEDFInfo & str = *file.edf;
int days = str.GetNumDataRecords();
totalRecs += days;
#ifdef STR_DEBUG
qDebug() << "STR file is" << file.filename.section("/", -3, -1);
qDebug() << "First day" << QDateTime::fromMSecsSinceEpoch(str.startdate, EDFInfo::localNoDST).date().toString() << "for" << days << "days";
#endif
}
emit updateMessage(QObject::tr("Parsing STR.edf records..."));
emit setProgressMax(totalRecs);
QCoreApplication::processEvents();
int currentRec = 0;
// Walk through all the STR files in the STRmap
for (auto it=STRmap.begin(), end=STRmap.end(); it != end; ++it) {
STRFile & file = it.value();
ResMedEDFInfo & str = *file.edf;
QDate date = str.edfHdr.startdate_orig.date(); // each STR.edf record starts at 12 noon
int size = str.GetNumDataRecords();
QDate lastDay = date.addDays(size-1);
#ifdef STR_DEBUG
QString & strfile = file.filename;
qDebug() << "Processing" << strfile.section("/", -3, -1) << date.toString() << "for" << size << "days";
qDebug() << "Last day is" << lastDay;
#endif
if ( lastDay < firstImport ) {
#ifdef STR_DEBUG
qDebug() << "LastDay before firstImport, skipping" << strfile.section("/", -3, -1);
#endif
continue;
}
// ResMed and their consistent naming and spacing... :/
EDFSignal *maskon = str.lookupLabel("Mask On"); // Series 9 devices
if (!maskon) {
maskon = str.lookupLabel("MaskOn"); // Series 1x devices
}
EDFSignal *maskoff = str.lookupLabel("Mask Off");
if (!maskoff) {
maskoff = str.lookupLabel("MaskOff");
}
EDFSignal *maskeventcount = str.lookupLabel("Mask Events");
if ( ! maskeventcount) {
maskeventcount = str.lookupLabel("MaskEvents");
}
if ( !maskon || !maskoff || !maskeventcount ) {
qCritical() << "Corrupt or untranslated STR.edf file";
return false;
}
EDFSignal *sig = nullptr;
// For each data record, representing 1 day each
for (int rec = 0; rec < size; ++rec, date = date.addDays(1)) {
emit setProgressValue(++currentRec);
QCoreApplication::processEvents();
if (date < firstImport) {
#ifdef SESSION_DEBUG
qDebug() << "Skipping" << date.toString() << "Before" << firstImport.toString();
#endif
continue;
}
// This is not what we want to check, we must look at this day in the database files...
// Found the place in checkSummaryDay to compare session count with maskevents divided by 2
#ifdef SESSION_DEBUG
qDebug() << "ResdayList size is" << resdayList.size();
#endif
// auto rit = resdayList.find(date);
// if (rit != resdayList.end()) {
// // Already seen this record.. should check if the data is the same, but meh.
// // At least check the maskeventcount to see if it changed...
// if ( maskeventcount->dataArray[0] != rit.value().str.maskevents ) {
// qDebug() << "Mask events don't match, purge" << rit.value().date.toString();
// // purge...
// }
// // #ifdef SESSION_DEBUG
// qDebug() << "Skipping" << date.toString() << "Already saw this one";
// // #endif
// continue;
// } // else {
// // qWarning() << date.toString() << "is missing from resdayList - FIX THIS";
// // continue;
// // }
int recstart = rec * maskon->sampleCnt;
bool validday = false;
for (int s = 0; s < maskon->sampleCnt; ++s) {
qint32 on = maskon->dataArray[recstart + s];
qint32 off = maskoff->dataArray[recstart + s];
if (((on >= 0) && (off >= 0)) && (on != off)) {// ignore very short on-off times
validday=true;
}
}
if ( ! validday) {
// There are no mask on/off events, so this STR day is useless.
qDebug() << "Skipping" << date.toString() << "No mask events";
continue;
}
#ifdef STR_DEBUG
qDebug() << "Adding" << date.toString() << "to resdayLisyt b/c we have STR record";
#endif
auto rit = resdayList.insert(date, ResMedDay(date));
#ifdef STR_DEBUG
qDebug() << "Setting up STRRecord for" << date.toString();
#endif
STRRecord &R = rit.value().str;
uint noonstamp = QDateTime(date,QTime(12,0,0), EDFInfo::localNoDST).toTime_t();
R.date = date;
// skipday = false;
// For every mask on, there will be a session within 1 minute either way
// We can use that for data matching
// Scan the mask on/off events by minute
R.maskon.resize(maskon->sampleCnt);
R.maskoff.resize(maskoff->sampleCnt);
int lastOn = -1;
int lastOff = -1;
for (int s = 0; s < maskon->sampleCnt; ++s) {
qint32 on = maskon->dataArray[recstart + s]; // these on/off times are minutes since noon
qint32 off = maskoff->dataArray[recstart + s]; // we want the actual time in seconds
if ( (on > 24*60) || (off > 24*60) ) {
qWarning().noquote() << "Mask times are out of range. Possible SDcard corruption" << "date" << date << "on" << on << "off" <<off;
continue;
}
if ( on > 0 ) { // convert them to seconds since midnight
lastOn = s;
R.maskon[s] = (noonstamp + (on * 60));
} else
R.maskon[s] = 0;
if ( off > 0 ) {
lastOff = s;
R.maskoff[s] = (noonstamp + (off * 60));
} else
R.maskoff[s] = 0;
}
// two conditions that need dealing with, mask running at noon start, and finishing at noon start..
// (Sessions are forcibly split by resmed.. why the heck don't they store it that way???)
if ((R.maskon[0]==0) && (R.maskoff[0]>0)) {
R.maskon[0] = noonstamp;
}
if ( (lastOn >= 0) && (lastOff >= 0) ) {
if ((R.maskon[lastOn] > 0) && (R.maskoff[lastOff] == 0)) {
R.maskoff[lastOff] = QDateTime(date,QTime(12,0,0), EDFInfo::localNoDST).addDays(1).toTime_t() - 1;
}
}
R.maskevents = maskeventcount->dataArray[rec];
CPAPMode mode = MODE_UNKNOWN;
if ((sig = str.lookupSignal(CPAP_Mode))) {
int mod = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
R.rms9_mode = mod;
if ( AS_eleven ) { // translate AS11 mode values back to S9 / AS10 values
switch ( mod ) {
case 0:
R.rms9_mode = 16; // Unknown
break;
case 1:
R.rms9_mode = 1; // still APAP
break;
case 2:
R.rms9_mode = 11; //make it look like A4Her
break;
case 3:
R.rms9_mode = 0; // make it be CPAP
break;
default:
R.rms9_mode = 16; // unknown for now
break;
}
}
int RMS9_mode = R.rms9_mode;
switch ( RMS9_mode ) {
case 11:
mode = MODE_APAP; // For her is a special apap
break;
case 10:
mode = MODE_UNKNOWN; // it's PAC, whatever that is
break;
case 9:
mode = MODE_AVAPS;
break;
case 8: // mod 8 == vpap adapt variable epap
mode = MODE_ASV_VARIABLE_EPAP;
break;
case 7: // mod 7 == vpap adapt
mode = MODE_ASV;
break;
case 6: // mod 6 == vpap auto (Min EPAP, Max IPAP, PS)
mode = MODE_BILEVEL_AUTO_FIXED_PS;
break;
case 5: // 4,5 are S/T types...
case 4:
case 3: // mod 3 == vpap s fixed pressure (EPAP, IPAP, No PS)
mode = MODE_BILEVEL_FIXED;
break;
case 2:
mode = MODE_BILEVEL_FIXED;
break;
case 1:
mode = MODE_APAP; // mod 1 == apap
break;
case 0:
mode = MODE_CPAP; // mod 0 == cpap
break;
default:
mode = MODE_UNKNOWN;
}
R.mode = mode;
// Settings.CPAP.Starting Pressure
if ((R.rms9_mode == 0) && (sig = str.lookupLabel("S.C.StartPress"))) {
R.s_RampPressure = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
// Settings.Adaptive Starting Pressure?
if ( (R.rms9_mode == 1) && ((sig = str.lookupLabel("S.AS.StartPress")) || (sig = str.lookupLabel("S.A.StartPress"))) ) {
R.s_RampPressure = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
// mode 11 = APAP for her?
if ( (R.rms9_mode == 11) && (sig = str.lookupLabel("S.AFH.StartPress"))) {
R.s_RampPressure = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if ((R.mode == MODE_BILEVEL_FIXED) && (sig = str.lookupLabel("S.BL.StartPress"))) {
// Bilevel Starting Pressure
R.s_RampPressure = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if (((R.mode == MODE_ASV) || (R.mode == MODE_ASV_VARIABLE_EPAP) ||
(R.mode == MODE_BILEVEL_AUTO_FIXED_PS)) && (sig = str.lookupLabel("S.VA.StartPress"))) {
// Bilevel Starting Pressure
R.s_RampPressure = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
}
// Collect the staistics
if ((sig = str.lookupLabel("Mask Dur")) || (sig = str.lookupLabel("Duration"))) {
R.maskdur = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if ((sig = str.lookupLabel("Leak Med")) || (sig = str.lookupLabel("Leak.50"))) {
float gain = sig->gain * 60.0;
R.leak50 = EventDataType(sig->dataArray[rec]) * gain;
}
if ((sig = str.lookupLabel("Leak Max"))|| (sig = str.lookupLabel("Leak.Max"))) {
float gain = sig->gain * 60.0;
R.leakmax = EventDataType(sig->dataArray[rec]) * gain;
}
if ((sig = str.lookupLabel("Leak 95")) || (sig = str.lookupLabel("Leak.95"))) {
float gain = sig->gain * 60.0;
R.leak95 = EventDataType(sig->dataArray[rec]) * gain;
}
if ((sig = str.lookupLabel("RespRate.50")) || (sig = str.lookupLabel("RR Med"))) {
R.rr50 = EventDataType(sig->dataArray[rec]) * sig->gain;
}
if ((sig = str.lookupLabel("RespRate.Max")) || (sig = str.lookupLabel("RR Max"))) {
R.rrmax = EventDataType(sig->dataArray[rec]) * sig->gain;
}
if ((sig = str.lookupLabel("RespRate.95")) || (sig = str.lookupLabel("RR 95"))) {
R.rr95 = EventDataType(sig->dataArray[rec]) * sig->gain;
}
if ((sig = str.lookupLabel("MinVent.50")) || (sig = str.lookupLabel("Min Vent Med"))) {
R.mv50 = EventDataType(sig->dataArray[rec]) * sig->gain;
}
if ((sig = str.lookupLabel("MinVent.Max")) || (sig = str.lookupLabel("Min Vent Max"))) {
R.mvmax = EventDataType(sig->dataArray[rec]) * sig->gain;
}
if ((sig = str.lookupLabel("MinVent.95")) || (sig = str.lookupLabel("Min Vent 95"))) {
R.mv95 = EventDataType(sig->dataArray[rec]) * sig->gain;
}
if ((sig = str.lookupLabel("TidVol.50")) || (sig = str.lookupLabel("Tid Vol Med"))) {
R.tv50 = EventDataType(sig->dataArray[rec]) * (sig->gain*1000.0);
}
if ((sig = str.lookupLabel("TidVol.Max")) || (sig = str.lookupLabel("Tid Vol Max"))) {
R.tvmax = EventDataType(sig->dataArray[rec]) * (sig->gain*1000.0);
}
if ((sig = str.lookupLabel("TidVol.95")) || (sig = str.lookupLabel("Tid Vol 95"))) {
R.tv95 = EventDataType(sig->dataArray[rec]) * (sig->gain*1000.0);
}
if ((sig = str.lookupLabel("MaskPress.50")) || (sig = str.lookupLabel("Mask Pres Med"))) {
R.mp50 = EventDataType(sig->dataArray[rec]) * sig->gain;
}
if ((sig = str.lookupLabel("MaskPress.Max")) || (sig = str.lookupLabel("Mask Pres Max"))) {
R.mpmax = EventDataType(sig->dataArray[rec]) * sig->gain ;
}
if ((sig = str.lookupLabel("MaskPress.95")) || (sig = str.lookupLabel("Mask Pres 95"))) {
R.mp95 = EventDataType(sig->dataArray[rec]) * sig->gain ;
}
if ((sig = str.lookupLabel("TgtEPAP.50")) || (sig = str.lookupLabel("Exp Pres Med"))) {
R.tgtepap50 = EventDataType(sig->dataArray[rec]) * sig->gain;
}
if ((sig = str.lookupLabel("TgtEPAP.Max")) || (sig = str.lookupLabel("Exp Pres Max"))) {
R.tgtepapmax = EventDataType(sig->dataArray[rec]) * sig->gain;
}
if ((sig = str.lookupLabel("TgtEPAP.95")) || (sig = str.lookupLabel("Exp Pres 95"))) {
R.tgtepap95 = EventDataType(sig->dataArray[rec]) * sig->gain;
}
if ((sig = str.lookupLabel("TgtIPAP.50")) || (sig = str.lookupLabel("Insp Pres Med"))) {
R.tgtipap50 = EventDataType(sig->dataArray[rec]) * sig->gain;
}
if ((sig = str.lookupLabel("TgtIPAP.Max")) || (sig = str.lookupLabel("Insp Pres Max"))) {
R.tgtipapmax = EventDataType(sig->dataArray[rec]) * sig->gain;
}
if ((sig = str.lookupLabel("TgtIPAP.95")) || (sig = str.lookupLabel("Insp Pres 95"))) {
R.tgtipap95 = EventDataType(sig->dataArray[rec]) * sig->gain;
}
if ((sig = str.lookupLabel("I:E Med"))) {
R.ie50 = EventDataType(sig->dataArray[rec]) * sig->gain;
}
if ((sig = str.lookupLabel("I:E Max"))) {
R.iemax = EventDataType(sig->dataArray[rec]) * sig->gain;
}
if ((sig = str.lookupLabel("I:E 95"))) {
R.ie95 = EventDataType(sig->dataArray[rec]) * sig->gain;
}
// Collect the pressure settings
bool haveipap = false;
Q_UNUSED( haveipap );
if ((sig = str.lookupSignal(CPAP_IPAP))) {
R.ipap = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
haveipap = true;
}
if ((sig = str.lookupSignal(CPAP_EPAP))) {
R.epap = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if (R.mode == MODE_AVAPS) {
if ((sig = str.lookupLabel("S.i.StartPress"))) {
R.s_RampPressure = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if ((sig = str.lookupLabel("S.i.EPAP"))) {
R.min_epap = R.max_epap = R.epap = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if ((sig = str.lookupLabel("S.i.EPAPAuto"))) {
R.epapAuto = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if ((sig = str.lookupLabel("S.i.MinPS"))) {
R.min_ps = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if ((sig = str.lookupLabel("S.i.MinEPAP"))) {
R.min_epap = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if ((sig = str.lookupLabel("S.i.MaxEPAP"))) {
R.max_epap = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if ((sig = str.lookupLabel("S.i.MaxPS"))) {
R.max_ps = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if ( (R.epap >= 0) && (R.epapAuto == 0) ) {
R.max_ipap = R.epap + R.max_ps;
R.min_ipap = R.epap + R.min_ps;
} else {
R.max_ipap = R.max_epap + R.max_ps;
R.min_ipap = R.min_epap + R.min_ps;
}
qDebug() << "AVAPS mode; Ramp" << R.s_RampPressure << "Fixed EPAP" << ((R.epapAuto == 0) ? "True" : "False") <<
"EPAP" << R.epap << "Min EPAP" << R.min_epap <<
"Max EPAP" << R.max_epap << "Min PS" << R.min_ps << "Max PS" << R.max_ps <<
"Min IPAP" << R.min_ipap << "Max_IPAP" << R.max_ipap;
}
if (R.mode == MODE_ASV) {
if ((sig = str.lookupLabel("S.AV.StartPress"))) {
R.s_RampPressure = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if ((sig = str.lookupLabel("S.AV.EPAP"))) {
R.min_epap = R.max_epap = R.epap = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if ((sig = str.lookupLabel("S.AV.MinPS"))) {
R.min_ps = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if ((sig = str.lookupLabel("S.AV.MaxPS"))) {
R.max_ps = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
R.max_ipap = R.epap + R.max_ps;
R.min_ipap = R.epap + R.min_ps;
}
}
if (R.mode == MODE_ASV_VARIABLE_EPAP) {
if ((sig = str.lookupLabel("S.AA.StartPress"))) {
EventDataType sp = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
R.s_RampPressure = sp;
}
if ((sig = str.lookupLabel("S.AA.MinEPAP"))) {
R.min_epap = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if ((sig = str.lookupLabel("S.AA.MaxEPAP"))) {
R.max_epap = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if ((sig = str.lookupLabel("S.AA.MinPS"))) {
R.min_ps = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if ((sig = str.lookupLabel("S.AA.MaxPS"))) {
R.max_ps = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
R.max_ipap = R.max_epap + R.max_ps;
R.min_ipap = R.min_epap + R.min_ps;
}
}
if ( (R.rms9_mode == 11) && (sig = str.lookupLabel("S.AFH.MaxPress")) ) {
R.max_pressure = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
else if ((sig = str.lookupSignal(CPAP_PressureMax))) {
R.max_pressure = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if ( (R.rms9_mode == 11) && (sig = str.lookupLabel("S.AFH.MinPress")) ) {
R.min_pressure = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
else if ((sig = str.lookupSignal(CPAP_PressureMin))) {
R.min_pressure = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if ((sig = str.lookupSignal(RMS9_SetPressure))) {
R.set_pressure = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if ((sig = str.lookupSignal(CPAP_EPAPHi))) {
R.max_epap = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if ((sig = str.lookupSignal(CPAP_EPAPLo))) {
R.min_epap = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if ((sig = str.lookupSignal(CPAP_IPAPHi))) {
R.max_ipap = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
haveipap = true;
}
if ((sig = str.lookupSignal(CPAP_IPAPLo))) {
R.min_ipap = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
haveipap = true;
}
if ((sig = str.lookupSignal(CPAP_PS))) {
R.ps = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
// Okay, problem here: THere are TWO PSMin & MAX dataArrays on the 36037 with the same string
// One is for ASV mode, and one is for ASVAuto
int psvar = (mode == MODE_ASV_VARIABLE_EPAP) ? 1 : 0;
if ((sig = str.lookupLabel("Max PS", psvar))) {
R.max_ps = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if ((sig = str.lookupLabel("Min PS", psvar))) {
R.min_ps = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
// ///// if (!haveipap) {
// ///// }
if (mode == MODE_ASV_VARIABLE_EPAP) {
R.min_ipap = R.min_epap + R.min_ps;
R.max_ipap = R.max_epap + R.max_ps;
} else if (mode == MODE_ASV) {
R.min_ipap = R.epap + R.min_ps;
R.max_ipap = R.epap + R.max_ps;
}
// Collect the other settings
if ((sig = str.lookupLabel("S.AS.Comfort"))) {
R.s_Comfort = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
if ( AS_eleven )
R.s_Comfort--;
}
EventDataType epr = -1, epr_level = -1;
bool a1x = false; // AS-10 or AS-11
if ((mode == MODE_CPAP) || (mode == MODE_APAP) ) {
if ((sig = str.lookupSignal(RMS9_EPR))) {
epr= EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
if ( AS_eleven )
epr--;
}
if ((sig = str.lookupSignal(RMS9_EPRLevel))) {
epr_level= EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if ((sig = str.lookupLabel("S.EPR.EPRType"))) {
a1x = true;
epr = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
epr += 1;
if ( AS_eleven )
epr--;
}
int epr_on=0, clin_epr_on=0;
if ((sig = str.lookupLabel("S.EPR.EPREnable"))) { // first check devices opinion
a1x = true;
epr_on = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
if ( AS_eleven )
epr_on--;
}
if (epr_on && (sig = str.lookupLabel("S.EPR.ClinEnable"))) {
a1x = true;
clin_epr_on = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
if ( AS_eleven )
clin_epr_on--;
}
if (a1x && !(epr_on && clin_epr_on)) {
epr = 0;
epr_level = 0;
}
}
if ((epr >= 0) && (epr_level >= 0)) {
R.epr_level = epr_level;
R.epr = epr;
} else {
if (epr >= 0) {
static bool warn=false;
if (!warn) { // just nag once
qDebug() << "If you can read this, please tell the developers you found a ResMed with EPR but no EPR_LEVEL so he can remove this warning";
// sleep(1);
warn = true;
}
R.epr = (epr > 0) ? 1 : 0;
R.epr_level = epr;
} else if (epr_level >= 0) {
R.epr_level = epr_level;
R.epr = (epr_level > 0) ? 1 : 0;
}
}
if ((sig = str.lookupLabel("AHI"))) {
R.ahi = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if ((sig = str.lookupLabel("AI"))) {
R.ai = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if ((sig = str.lookupLabel("HI"))) {
R.hi = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if ((sig = str.lookupLabel("UAI"))) {
R.uai = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if ((sig = str.lookupLabel("CAI"))) {
R.cai = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if ((sig = str.lookupLabel("OAI"))) {
R.oai = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if ((sig = str.lookupLabel("CSR"))) {
R.csr = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if ((sig = str.lookupLabel("S.RampTime"))) {
R.s_RampTime = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if ((sig = str.lookupLabel("S.RampEnable"))) {
R.s_RampEnable = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
if ( AS_eleven )
R.s_RampEnable--;
if ( R.s_RampEnable == 2 )
R.s_RampTime = -1;
}
if ((sig = str.lookupLabel("S.EPR.ClinEnable"))) {
R.s_EPR_ClinEnable = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
if ( AS_eleven )
R.s_EPR_ClinEnable--;
}
if ((sig = str.lookupLabel("S.EPR.EPREnable"))) {
R.s_EPREnable = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
if ( AS_eleven )
R.s_EPREnable--;
}
if ((sig = str.lookupLabel("S.ABFilter"))) {
R.s_ABFilter = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
if ( AS_eleven )
R.s_ABFilter--;
}
if ((sig = str.lookupLabel("S.ClimateControl"))) {
R.s_ClimateControl = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
if ( AS_eleven )
R.s_ClimateControl--;
}
if ((sig = str.lookupLabel("S.Mask"))) {
R.s_Mask = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
if ( AS_eleven ) {
if ( R.s_Mask < 2 || R.s_Mask > 4 )
R.s_Mask = 4; // unknown mask type
else
R.s_Mask -= 2; // why be consistent?
}
}
if ((sig = str.lookupLabel("S.PtAccess"))) {
if ( AS_eleven ) {
R.s_PtView = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
R.s_PtView--;
} else
R.s_PtAccess = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if ((sig = str.lookupLabel("S.SmartStart"))) {
R.s_SmartStart = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
if ( AS_eleven )
R.s_SmartStart--;
// qDebug() << "SmartStart is set to" << R.s_SmartStart;
}
if ((sig = str.lookupLabel("S.SmartStop"))) {
R.s_SmartStop = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
if ( AS_eleven )
R.s_SmartStop--;
qDebug() << "SmartStop is set to" << R.s_SmartStop;
}
if ((sig = str.lookupLabel("S.HumEnable"))) {
R.s_HumEnable = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
if ( AS_eleven )
R.s_HumEnable--;
}
if ((sig = str.lookupLabel("S.HumLevel"))) {
R.s_HumLevel = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if ((sig = str.lookupLabel("S.TempEnable"))) {
R.s_TempEnable = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
if ( AS_eleven )
R.s_TempEnable--;
}
if ((sig = str.lookupLabel("S.Temp"))) {
R.s_Temp = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if ((sig = str.lookupLabel("S.Tube"))) {
R.s_Tube = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if ((R.rms9_mode >= 2) && (R.rms9_mode <= 5)) { // S, ST, or T modes
qDebug() << "BiLevel Mode found" << R.rms9_mode;
if (R.rms9_mode == 3) { // S mode only
if ((sig = str.lookupLabel("S.EasyBreathe"))) {
R.s_EasyBreathe = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
}
if ((sig = str.lookupLabel("S.RiseEnable"))) {
R.s_RiseEnable = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if ((sig = str.lookupLabel("S.RiseTime"))) {
R.s_RiseTime = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if ((R.rms9_mode ==3) || (R.rms9_mode ==4)) { // S or ST mode
if ((sig = str.lookupLabel("S.Cycle"))) {
R.s_Cycle = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if ((sig = str.lookupLabel("S.Trigger"))) {
R.s_Trigger = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if ((sig = str.lookupLabel("S.TiMax"))) {
R.s_TiMax = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
if ((sig = str.lookupLabel("S.TiMin"))) {
R.s_TiMin = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
}
}
}
if (R.rms9_mode == 6) { // vAuto mode
qDebug() << "vAuto mode found" << 6;
if ((sig = str.lookupLabel("S.Cycle"))) {
R.s_Cycle = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
qDebug() << "Cycle" << R.s_Cycle;
}
if ((sig = str.lookupLabel("S.Trigger"))) {
R.s_Trigger = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
qDebug() << "Trigger" << R.s_Trigger;
}
if ((sig = str.lookupLabel("S.TiMax"))) {
R.s_TiMax = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
qDebug() << QString("TiMax %1").arg( R.s_TiMax, 0, 'f', 1);
}
if ((sig = str.lookupLabel("S.TiMin"))) {
R.s_TiMin = EventDataType(sig->dataArray[rec]) * sig->gain + sig->offset;
qDebug() << QString("TiMin %1").arg( R.s_TiMin, 0, 'f', 1);
}
}
if ( R.min_pressure == 0 ) {
qDebug() << "Min Pressure is zero on" << date.toString();
}
#ifdef STR_DEBUG
qDebug() << "Finished" << date.toString();
#endif
}
#ifdef STR_DEBUG
qDebug() << "Finished" << strfile;
#endif
}
qDebug() << "Finished ProcessSTR";
return true;
}
///////////////////////////////////////////////////////////////////////////////////
// Parse Identification.tgt file (containing serial number and device information)
///////////////////////////////////////////////////////////////////////////////////
// QHash<QString, QString> parseIdentLine( const QString line, MachineInfo * info); // forward
// void scanProductObject( QJsonObject product, MachineInfo *info, QHash<QString, QString> *idmap); // forward
bool parseIdentFile( QString path, MachineInfo * info, QHash<QString, QString> & idmap ) {
QString filename = path + RMS9_STR_idfile + STR_ext_TGT;
QFile f(filename);
QFile j(path + RMS9_STR_idfile + STR_ext_JSON);
if (j.exists() ) { // chose the AS11 file if both exist
if ( !j.open(QIODevice::ReadOnly)) {
return false;
}
QByteArray identData = j.readAll();
j.close();
QJsonDocument identDoc(QJsonDocument::fromJson(identData));
QJsonObject identObj(identDoc.object());
if ( identObj.contains("FlowGenerator") && identObj["FlowGenerator"].isObject()) {
QJsonObject flow = identObj["FlowGenerator"].toObject();
if ( flow.contains("IdentificationProfiles") && flow["IdentificationProfiles"].isObject()) {
QJsonObject profiles = flow["IdentificationProfiles"].toObject();
if ( profiles.contains("Product") && profiles["Product"].isObject()) {
QJsonObject product = profiles["Product"].toObject();
// passed in MachineInfo info = newInfo();
scanProductObject( product, info, &idmap);
return true;
}
}
}
return false;
}
// Abort if this file is dodgy..
if (f.exists() ) {
if ( !f.open(QIODevice::ReadOnly)) {
return false;
}
qDebug() << "Parsing Identification File " << filename;
// emit updateMessage(QObject::tr("Parsing Identification File"));
// QApplication::processEvents();
// Parse # entries into idmap.
while (!f.atEnd()) {
QString line = f.readLine().trimmed();
QHash<QString, QString> hash = parseIdentLine( line, info );
idmap.QTCOMBINE(hash);
}
f.close();
return true;
}
return false;
}
void scanProductObject( QJsonObject product, MachineInfo *info, QHash<QString, QString> *idmap) {
QHash<QString, QString> hash1, hash2, hash3;
if (product.contains("SerialNumber")) {
info->serial = product["SerialNumber"].toString();
hash1["SerialNumber"] = product["SerialNumber"].toString();
if (idmap)
idmap->QTCOMBINE(hash1);
}
if (product.contains("ProductCode")) {
info->modelnumber = product["ProductCode"].toString();
hash2["ProductCode"] = info->modelnumber;
if (idmap)
idmap->QTCOMBINE(hash2);
}
if (product.contains("ProductName")) {
info->model = product["ProductName"].toString();
hash3["ProductName"] = info->model;
if (idmap)
idmap->QTCOMBINE(hash3);
int idx = info->model.indexOf("11");
info->series = info->model.left(idx+2);
}
}
void backupSTRfiles( const QString strpath, const QString importPath, const QString backupPath,
MachineInfo & info, QMap<QDate, STRFile> & STRmap )
{
Q_UNUSED(strpath);
qDebug() << "Starting backupSTRfiles during new IMPORT";
QDir dir;
// Qstring strBackupPath(backupPath+"STR_Backup");
QStringList strfiles;
// add Backup/STR.edf - make sure it ends up in the STRmap
strfiles.push_back(backupPath+"STR.edf");
// Just in case we are importing from a Backup folder in a different Profile, process OSCAR backup structures
QString strBackupPath(importPath + "STR_Backup");
dir.setPath(strBackupPath);
dir.setFilter(QDir::Files | QDir::Hidden | QDir::Readable);
QFileInfoList flist = dir.entryInfoList();
// Add any STR_Backup versions to the file list
for (auto & fi : flist) { // this is empty if imprting from an SD card
QString filename = fi.fileName();
if ( ! filename.startsWith("STR", Qt::CaseInsensitive))
continue;
if ( ! (filename.endsWith("edf.gz", Qt::CaseInsensitive) || filename.endsWith("edf", Qt::CaseInsensitive)))
continue;
strfiles.push_back(fi.canonicalFilePath());
}
#ifdef STR_DEBUG
qDebug() << "STR file list size is" << strfiles.size();
#endif
// Now copy any of these files to the Backup folder adding the file date to the file name
// and put it into the STRmap structure
for (auto & filename : strfiles) {
QDate date;
long int days;
ResMedEDFInfo * stredf = fetchSTRandVerify( filename, info.serial );
if ( stredf == nullptr )
continue;
date = stredf->edfHdr.startdate_orig.date();
days = stredf->GetNumDataRecords();
if (STRmap.contains(date)) {
qDebug() << "STRmap already contains" << date.toString("yyyy-MM-dd") << "for" << STRmap[date].days << "ending" << date.addDays(STRmap[date].days-1);
qDebug() << filename.section("/",-2,-1) << "has" << days << "ending" << date.addDays(days-1);
if ( days <= STRmap[date].days ) {
qDebug() << "Skipping" << filename.section("/",-2,-1) << "Keeping" << STRmap[date].filename.section("/",-2,-1);
delete stredf;
continue;
} else {
qDebug() << "Dropping" << STRmap[date].filename.section("/", -2, -1) << "Keeping" << filename.section("/",-2,-1);
delete STRmap[date].edf;
STRmap.remove(date); // new one gets added after we know its new name
}
}
// now create the new backup name
QString newname = "STR-"+date.toString("yyyyMMdd")+"."+STR_ext_EDF;
QString backupfile = backupPath+"/STR_Backup/"+newname;
QString gzfile = backupfile + STR_ext_gz;
QString nongzfile = backupfile;
bool compress_backups = p_profile->session->compressBackupData();
backupfile = compress_backups ? gzfile : nongzfile;
STRmap[date] = STRFile(backupfile, days, stredf);
qDebug() << "Adding" << filename.section("/",-3,-1) << "with" << days << "days as" << backupfile.section("/", -3, -1) << "to STRmap";
if ( QFile::exists(backupfile)) {
QFile::remove(backupfile);
}
#ifdef STR_DEBUG
qDebug() << "Copying" << filename.section("/",-3,-1) << "to" << backupfile.section("/",-3,-1);
#endif
if (filename.endsWith(STR_ext_gz,Qt::CaseInsensitive)) { // we have a compressed file
if (compress_backups) { // fine, copy it to backup folder
if (!QFile::copy(filename, backupfile))
qWarning() << "Failed to copy" << filename << "to" << backupfile;
} else { // oops, uncompress it to the backup folder
uncompressFile(filename, backupfile);
}
} else { // file is not compressed
if (compress_backups) { // so compress it into the backup folder
compressFile(filename, backupfile);
} else { // and that's OK, just copy it over
if (!QFile::copy(filename, backupfile))
qWarning() << "Failed to copy" << filename << "to" << backupfile;
}
}
// Remove any duplicate compressed/uncompressed backup file
if (compress_backups)
QFile::exists(nongzfile) && QFile::remove(nongzfile);
else
QFile::exists(gzfile) && QFile::remove(gzfile);
} // end for walking the STR files list
#ifdef STR_DEBUG
qDebug() << "STRmap has" << STRmap.size() << "entries";
#endif
qDebug() << "Finished backupSTRfiles during new IMPORT";
}
QHash<QString, QString> parseIdentLine( const QString line, MachineInfo * info)
{
QHash<QString, QString> hash;
if (!line.isEmpty()) {
QString key = line.section(" ", 0, 0).section("#", 1);
QString value = line.section(" ", 1);
if (key == "SRN") { // Serial Number
info->serial = value;
} else if (key == "PNA") { // Product Name
value.replace("_"," ");
if (value.contains(STR_ResMed_AirSense10)) {
// value.replace(STR_ResMed_AirSense10, "");
info->series = STR_ResMed_AirSense10;
} else if (value.contains(STR_ResMed_AirCurve10)) {
// value.replace(STR_ResMed_AirCurve10, "");
info->series = STR_ResMed_AirCurve10;
} else { // it will be a Series 9, and might not contain (STR_ResMed_S9))
value.replace("("," "); // might sometimes have a double space...
value.replace(")","");
if ( ! value.startsWith(STR_ResMed_S9)) {
value.replace(STR_ResMed_S9, "");
value.insert(0, " "); // There's proablely a better way than this
value.insert(0, STR_ResMed_S9); // two step way to put "S9 " at the start
}
info->series = STR_ResMed_S9;
// value.replace(STR_ResMed_S9, "");
}
// if (value.contains("Adapt", Qt::CaseInsensitive)) {
// if (!value.contains("VPAP")) {
// value.replace("Adapt", QObject::tr("VPAP Adapt"));
// }
// }
info->model = value.trimmed();
} else if (key == "PCD") { // Product Code
info->modelnumber = value;
}
hash[key] = value;
}
return hash;
}
EDFType lookupEDFType(const QString & filename)
{
QString text = filename.section("_", -1).section(".",0,0).toUpper();
if (text == "EVE") {
return EDF_EVE;
} else if (text =="BRP") {
return EDF_BRP;
} else if (text == "PLD") {
return EDF_PLD;
} else if ((text == "SAD") || (text == "SA2")){
return EDF_SAD;
} else if (text == "CSL") {
return EDF_CSL;
} else if (text == "AEV") {
return EDF_AEV;
} else return EDF_UNKNOWN;
}
///////////////////////////////////////////////////////////////////////////////
// Looks inside an EDF or EDF.gz and grabs the start and duration
///////////////////////////////////////////////////////////////////////////////
EDFduration getEDFDuration(const QString & filename)
{
// qDebug() << "getEDFDuration called for" << filename;
QString ext = filename.section("_", -1).section(".",0,0).toUpper();
if ((ext == "EVE") || (ext == "CSL")) { // don't even try with Annotation-only edf files
EDFduration dur(0, 0, filename);
dur.type = lookupEDFType(filename);
qDebug() << "File ext is" << ext;
dumpEDFduration(dur);
return dur;
}
bool ok1, ok2;
int num_records;
double rec_duration;
QDateTime startDate;
// We will just look at the header part of the edf file here
if (!filename.endsWith(".gz", Qt::CaseInsensitive)) {
QFile file(filename);
if (!file.open(QFile::ReadOnly))
return EDFduration(0, 0, filename);
if (!file.seek(0xa8)) {
file.close();
return EDFduration(0, 0, filename);
}
QByteArray bytes = file.read(16).trimmed();
// We'll fix the xx85 problem below
// startDate = QDateTime::fromString(QString::fromLatin1(bytes, 16), "dd.MM.yyHH.mm.ss");
// getStartDT ought to be named getStartNoDST ... TODO someday
startDate = EDFInfo::getStartDT(QString::fromLatin1(bytes,16));
if (!file.seek(0xec)) {
file.close();
return EDFduration(0, 0, filename);
}
bytes = file.read(8).trimmed();
num_records = bytes.toInt(&ok1);
bytes = file.read(8).trimmed();
rec_duration = bytes.toDouble(&ok2);
file.close();
} else {
gzFile f = gzopen(filename.toLatin1(), "rb");
if (!f)
return EDFduration(0, 0, filename);
// Decompressed header and data block
if (!gzseek(f, 0xa8, SEEK_SET)) {
gzclose(f);
return EDFduration(0, 0, filename);
}
char datebytes[17] = {0};
gzread(f, (char *)&datebytes, 16);
QString str = QString(QString::fromLatin1(datebytes,16)).trimmed();
// startDate = QDateTime::fromString(str, "dd.MM.yyHH.mm.ss");
startDate = EDFInfo::getStartDT(str);
if (!gzseek(f, 0xec-0xa8-16, SEEK_CUR)) { // 0xec
gzclose(f);
return EDFduration(0, 0, filename);
}
char cbytes[9] = {0};
gzread(f, (char *)&cbytes, 8);
str = QString(cbytes).trimmed();
num_records = str.toInt(&ok1);
gzread(f, (char *)&cbytes, 8);
str = QString(cbytes).trimmed();
rec_duration = str.toDouble(&ok2);
gzclose(f);
}
QDate d2 = startDate.date();
if (d2.year() < 2000) {
d2.setDate(d2.year() + 100, d2.month(), d2.day());
startDate.setDate(d2);
}
if ( (! startDate.isValid()) || ( startDate > QDateTime::currentDateTime()) ) {
qDebug() << "Invalid date time retreieved parsing EDF duration for" << filename;
qDebug() << "Time zone(Utc) is" << startDate.timeZone().abbreviation(QDateTime::currentDateTimeUtc());
qDebug() << "Time zone is" << startDate.timeZone().abbreviation(QDateTime::currentDateTime());
return EDFduration(0, 0, filename);
}
if (!(ok1 && ok2))
return EDFduration(0, 0, filename);
quint32 start = startDate.toTime_t();
quint32 end = start + rec_duration * num_records;
QString filedate = filename.section("/",-1).section("_",0,1);
// QDateTime dt2 = QDateTime::fromString(filedate, "yyyyMMdd_hhmmss");
d2 = QDate::fromString( filedate.left(8), "yyyyMMdd");
QTime t2 = QTime::fromString( filedate.right(6), "hhmmss");
QDateTime dt2 = QDateTime( d2, t2, EDFInfo::localNoDST );
quint32 st2 = dt2.toTime_t();
start = qMin(st2, start); // They should be the same, usually
if (end < start)
end = qMax(st2, start);
EDFduration dur(start, end, filename);
dur.type = lookupEDFType(filename);
return dur;
}
void GuessPAPMode(Session *sess)
{
if (sess->channelDataExists(CPAP_Pressure)) {
// Determine CPAP or APAP?
EventDataType min = sess->Min(CPAP_Pressure);
EventDataType max = sess->Max(CPAP_Pressure);
if ((max-min)<0.1) {
sess->settings[CPAP_Mode] = MODE_CPAP;
sess->settings[CPAP_Pressure] = qRound(max * 10.0)/10.0;
// early call.. It's CPAP mode
} else {
// Ramp is ugly - but this is a bad way to test for it
if (sess->length() > 1800000L) { // half an hour
}
sess->settings[CPAP_Mode] = MODE_APAP;
sess->settings[CPAP_PressureMin] = qRound(min * 10.0)/10.0;
sess->settings[CPAP_PressureMax] = qRound(max * 10.0)/10.0;
}
} else if (sess->eventlist.contains(CPAP_IPAP)) {
sess->settings[CPAP_Mode] = MODE_BILEVEL_AUTO_VARIABLE_PS;
// Determine BiPAP or ASV
}
}
void StoreSummaryStatistics(Session * sess, STRRecord & R)
{
if (R.mode >= 0) {
if (R.mode == MODE_CPAP) {
} else if (R.mode == MODE_APAP) {
}
}
if (R.leak50 >= 0) {
// sess->setp95(CPAP_Leak, R.leak95);
// sess->setp50(CPAP_Leak, R.leak50);
sess->setMax(CPAP_Leak, R.leakmax);
}
if (R.rr50 >= 0) {
// sess->setp95(CPAP_RespRate, R.rr95);
// sess->setp50(CPAP_RespRate, R.rr50);
sess->setMax(CPAP_RespRate, R.rrmax);
}
if (R.mv50 >= 0) {
// sess->setp95(CPAP_MinuteVent, R.mv95);
// sess->setp50(CPAP_MinuteVent, R.mv50);
sess->setMax(CPAP_MinuteVent, R.mvmax);
}
if (R.tv50 >= 0) {
// sess->setp95(CPAP_TidalVolume, R.tv95);
// sess->setp50(CPAP_TidalVolume, R.tv50);
sess->setMax(CPAP_TidalVolume, R.tvmax);
}
if (R.mp50 >= 0) {
// sess->setp95(CPAP_MaskPressure, R.mp95);
// sess->seTTtp50(CPAP_MaskPressure, R.mp50);
sess->setMax(CPAP_MaskPressure, R.mpmax);
}
if (R.oai > 0) {
sess->setCph(CPAP_Obstructive, R.oai);
sess->setCount(CPAP_Obstructive, R.oai * sess->hours());
}
if (R.hi > 0) {
sess->setCph(CPAP_Hypopnea, R.hi);
sess->setCount(CPAP_Hypopnea, R.hi * sess->hours());
}
if (R.cai > 0) {
sess->setCph(CPAP_ClearAirway, R.cai);
sess->setCount(CPAP_ClearAirway, R.cai * sess->hours());
}
if (R.uai > 0) {
sess->setCph(CPAP_Apnea, R.uai);
sess->setCount(CPAP_Apnea, R.uai * sess->hours());
}
if (R.csr > 0) {
sess->setCph(CPAP_CSR, R.csr);
sess->setCount(CPAP_CSR, R.csr * sess->hours());
}
}
void StoreSettings(Session * sess, STRRecord & R)
{
if (R.mode >= 0) {
sess->settings[CPAP_Mode] = R.mode;
sess->settings[RMS9_Mode] = R.rms9_mode;
if ( R.min_pressure == 0 )
qDebug() << "Min Pressure is zero, R.mode is" << R.mode;
if (R.mode == MODE_CPAP) {
if (R.set_pressure >= 0) sess->settings[CPAP_Pressure] = R.set_pressure;
} else if (R.mode == MODE_APAP) {
if (R.min_pressure >= 0) sess->settings[CPAP_PressureMin] = R.min_pressure;
if (R.max_pressure >= 0) sess->settings[CPAP_PressureMax] = R.max_pressure;
} else if (R.mode == MODE_BILEVEL_FIXED) {
if (R.epap >= 0) sess->settings[CPAP_EPAP] = R.epap;
if (R.ipap >= 0) sess->settings[CPAP_IPAP] = R.ipap;
if (R.ps >= 0) sess->settings[CPAP_PS] = R.ps;
} else if (R.mode == MODE_BILEVEL_AUTO_FIXED_PS) {
if (R.min_epap >= 0) sess->settings[CPAP_EPAPLo] = R.min_epap;
if (R.max_ipap >= 0) sess->settings[CPAP_IPAPHi] = R.max_ipap;
if (R.ps >= 0) sess->settings[CPAP_PS] = R.ps;
if (R.s_Cycle >= 0) sess->settings[ RMAS1x_Cycle ] = R.s_Cycle;
if (R.s_Trigger >= 0) sess->settings[ RMAS1x_Trigger ] = R.s_Trigger;
if (R.s_TiMax >= 0) sess->settings[ RMAS1x_TiMax ] = R.s_TiMax;
if (R.s_TiMin >= 0) sess->settings[ RMAS1x_TiMin ] = R.s_TiMin;
} else if (R.mode == MODE_ASV) {
if (R.epap >= 0) sess->settings[CPAP_EPAP] = R.epap;
if (R.min_ps >= 0) sess->settings[CPAP_PSMin] = R.min_ps;
if (R.max_ps >= 0) sess->settings[CPAP_PSMax] = R.max_ps;
if (R.max_ipap >= 0) sess->settings[CPAP_IPAPHi] = R.max_ipap;
} else if (R.mode == MODE_ASV_VARIABLE_EPAP) {
if (R.max_epap >= 0) sess->settings[CPAP_EPAPHi] = R.max_epap;
if (R.min_epap >= 0) sess->settings[CPAP_EPAPLo] = R.min_epap;
if (R.max_ipap >= 0) sess->settings[CPAP_IPAPHi] = R.max_ipap;
if (R.min_ipap >= 0) sess->settings[CPAP_IPAPLo] = R.min_ipap;
if (R.min_ps >= 0) sess->settings[CPAP_PSMin] = R.min_ps;
if (R.max_ps >= 0) sess->settings[CPAP_PSMax] = R.max_ps;
} else {
qDebug() << "Setting session pressures for R.mode" << R.mode;
if (R.set_pressure > 0) sess->settings[CPAP_Pressure] = R.set_pressure;
if (R.min_pressure > 0) sess->settings[CPAP_PressureMin] = R.min_pressure;
if (R.max_pressure > 0) sess->settings[CPAP_PressureMax] = R.max_pressure;
if (R.max_epap > 0) sess->settings[CPAP_EPAPHi] = R.max_epap;
if (R.min_epap > 0) sess->settings[CPAP_EPAPLo] = R.min_epap;
if (R.max_ipap > 0) sess->settings[CPAP_IPAPHi] = R.max_ipap;
if (R.min_ipap > 0) sess->settings[CPAP_IPAPLo] = R.min_ipap;
if (R.min_ps > 0) sess->settings[CPAP_PSMin] = R.min_ps;
if (R.max_ps > 0) sess->settings[CPAP_PSMax] = R.max_ps;
if (R.ps > 0) sess->settings[CPAP_PS] = R.ps;
if (R.epap > 0) sess->settings[CPAP_EPAP] = R.epap;
if (R.ipap > 0) sess->settings[CPAP_IPAP] = R.ipap;
}
}
if (R.epr >= 0) {
sess->settings[RMS9_EPR] = (int)R.epr;
if (R.epr > 0) {
if (R.epr_level >= 0) {
sess->settings[RMS9_EPRLevel] = (int)R.epr_level;
}
}
}
if (R.s_RampEnable >= 0) {
sess->settings[RMS9_RampEnable] = R.s_RampEnable;
if (R.s_RampEnable >= 1) {
if (R.s_RampTime >= 0) {
sess->settings[CPAP_RampTime] = R.s_RampTime;
}
if (R.s_RampPressure >= 0) {
sess->settings[CPAP_RampPressure] = R.s_RampPressure;
}
}
}
if (R.s_SmartStart >= 0) {
sess->settings[RMS9_SmartStart] = R.s_SmartStart;
}
if (R.s_SmartStop >= 0) {
sess->settings[RMAS11_SmartStop] = R.s_SmartStop;
}
if (R.s_ABFilter >= 0) {
sess->settings[RMS9_ABFilter] = R.s_ABFilter;
}
if (R.s_ClimateControl >= 0) {
sess->settings[RMS9_ClimateControl] = R.s_ClimateControl;
}
if (R.s_Mask >= 0) {
sess->settings[RMS9_Mask] = R.s_Mask;
}
if (R.s_PtAccess >= 0) {
sess->settings[RMS9_PtAccess] = R.s_PtAccess;
}
if (R.s_PtView >= 0) {
sess->settings[RMAS11_PtView] = R.s_PtView;
}
if (R.s_HumEnable >= 0) {
sess->settings[RMS9_HumidStatus] = (short)R.s_HumEnable;
if ((R.s_HumEnable >= 1) && (R.s_HumLevel >= 0)) {
sess->settings[RMS9_HumidLevel] = (short)R.s_HumLevel;
}
}
if (R.s_TempEnable >= 0) {
sess->settings[RMS9_TempEnable] = (short)R.s_TempEnable;
if ((R.s_TempEnable >= 1) && (R.s_Temp >= 0)){
sess->settings[RMS9_Temp] = (short)R.s_Temp;
}
}
if (R.s_Comfort >= 0) {
sess->settings[RMAS1x_Comfort] = R.s_Comfort;
}
}
struct OverlappingEDF {
quint32 start;
quint32 end;
QMultiMap<quint32, QString> filemap; // key is start time, value is filename
Session * sess;
};
void ResDayTask::run()
{
#ifdef SESSION_DEBUG
qDebug() << "Processing STR and edf files for" << resday->date;
#endif
if (resday->files.size() == 0) { // No EDF files???
if (( ! resday->str.date.isValid()) || (resday->str.date > QDate::currentDate()) ) {
// This condition should be impossible, but just in case something gets fudged up elsewhere later
qDebug() << "No edf files in resday" << resday->date << "and the str date is inValid";
return;
}
// Summary only day, create sessions for each mask-on/off pair and tag them summary only
STRRecord & R = resday->str;
#ifdef SESSION_DEBUG
qDebug() << "Creating summary-only sessions for" << resday->date;
#endif
for (int i=0;i<resday->str.maskon.size();++i) {
quint32 maskon = resday->str.maskon[i];
quint32 maskoff = resday->str.maskoff[i];
/**
QTime noon(12,00,00);
QDateTime daybegin(resday->date,noon); // Beginning of ResMed day
quint32 dayend = daybegin.addDays(1).addMSecs(-1).toTime_t(); // End of ResMed day
if ( (maskon > dayend) ||
(maskoff > dayend) ) {
qWarning() << "mask time in future" << resday->date << daybegin << dayend << "maskon" << maskon << "maskoff" << maskoff;
continue;
}
**/
if (((maskon>0) && (maskoff>0)) && (maskon != maskoff)) { //ignore very short sessions
Session * sess = new Session(mach, maskon);
sess->set_first(quint64(maskon) * 1000L);
sess->set_last(quint64(maskoff) * 1000L);
StoreSettings(sess, R); // Process the STR.edf settings
StoreSummaryStatistics(sess, R); // We want the summary information too
sess->setSummaryOnly(true);
sess->SetChanged(true);
// loader->sessionMutex.lock(); // This chunk moved into SaveSession below
// sess->Store(mach->getDataPath());
// mach->AddSession(sess);
// loader->sessionCount++;
// loader->sessionMutex.unlock();
//// delete sess;
save(loader, sess); // This is aliased to SaveSession - unless testing
}
}
// qDebug() << "Finished summary processing for" << resday->date;
return;
}
// sooo... at this point we have
// resday record populated with correct STR.edf settings for this date
// files list containing unsorted EDF files that match this day
// guaranteed no sessions for this day for this device.
// Need to check overlapping files in session candidates
QList<OverlappingEDF> overlaps;
int maskOnSize = resday->str.maskon.size();
if (resday->str.date.isValid()) {
//First populate Overlaps with Mask ON/OFF events
for (int i=0; i < maskOnSize; ++i) {
// if ( (resday->str.maskon[i] > QDateTime::currentDateTime().toTime_t()) ||
// (resday->str.maskoff[i] > QDateTime::currentDateTime().toTime_t()) ) {
// qWarning() << "mask time in future" << resday->date << "now" << QDateTime::currentDateTime().toTime_t() << "maskon" << resday->str.maskon[i] << "maskoff" << resday->str.maskoff[i];
// continue;
// }
/*
QTime noon(12,00,00);
QDateTime daybegin(resday->date,noon); // Beginning of ResMed day
quint32 dayend = daybegin.addDays(1).addMSecs(-1).toTime_t(); // End of ResMed day
if ( (resday->str.maskon[i] > dayend) ||
(resday->str.maskoff[i] > dayend) ) {
qWarning() << "mask time in future" << resday->date << "daybegin:" << daybegin << "dayend:" << dayend << "maskon" << resday->str.maskon[i] << "maskoff" << resday->str.maskoff[i];
continue;
}
*/
if (((resday->str.maskon[i]>0) || (resday->str.maskoff[i]>0))
&& (resday->str.maskon[i] != resday->str.maskoff[i]) ) {
OverlappingEDF ov;
ov.start = resday->str.maskon[i];
ov.end = resday->str.maskoff[i];
ov.sess = nullptr;
overlaps.append(ov);
}
}
}
#ifdef STR_DEBUG
if (overlaps.size() > 0)
qDebug().noquote() << "Created" << overlaps.size() << "sessionGroups from STR record for" << resday->str.date.toString();
#endif
QMap<quint32, QString> EVElist, CSLlist;
for (auto f_itr=resday->files.begin(), fend=resday->files.end(); f_itr!=fend; ++f_itr) {
const QString & filename = f_itr.key();
const QString & fullpath = f_itr.value();
// QString ext = filename.section("_", -1).section(".",0,0).toUpper();
EDFType type = lookupEDFType(filename);
QString datestr = filename.section("_", 0, 1);
// QDateTime filetime = QDateTime().fromString(datestr,"yyyyMMdd_HHmmss");
QDate d2 = QDate::fromString( datestr.left(8), "yyyyMMdd");
QTime t2 = QTime::fromString( datestr.right(6), "hhmmss");
QDateTime filetime = QDateTime( d2, t2, EDFInfo::localNoDST );
quint32 filetime_t = filetime.toTime_t();
if (type == EDF_EVE) { // skip the EVE and CSL files, b/c they often cover all sessions
EVElist[filetime_t] = filename;
continue;
} else if (type == EDF_CSL) {
CSLlist[filetime_t] = filename;
continue;
}
bool added = false;
for (auto & ovr : overlaps) {
if ((filetime_t >= (ovr.start)) && (filetime_t < ovr.end)) {
ovr.filemap.insert(filetime_t, filename);
added = true;
break;
}
}
if ( ! added) { // Didn't get a hit, look at the EDF files duration and check for an overlap
EDFduration dur = getEDFDuration(fullpath);
/**
QTime noon(12,00,00);
QDateTime daybegin(resday->date,noon); // Beginning of ResMed day
quint32 dayend = daybegin.addDays(1).addMSecs(-1).toTime_t(); // End of ResMed day
if ((dur.start > (dayend)) ||
(dur.end > (dayend)) ) {
qWarning() << "Future Date in" << fullpath << "dayend" << dayend << "dur.start" << dur.start << "dur.end" << dur.end;
continue; // skip this file
}
**/
for (int i=overlaps.size()-1; i>=0; --i) {
OverlappingEDF & ovr = overlaps[i];
if ((ovr.start < dur.end) && (dur.start < ovr.end)) {
ovr.filemap.insert(filetime_t, filename);
added = true;
#ifdef SESSION_DEBUG
qDebug() << "Adding" << filename << "to overlap" << i;
qDebug() << "Overlap starts:" << ovr.start << "ends:" << ovr.end;
qDebug() << "File time starts:" << dur.start << "ends:" << dur.end;
#endif
// Expand ovr's scope -- I think this is necessary!! (PO)
// YES! when the STR file is missing, there are no mask on/off entries
// and the edf files are not always created at the same time
ovr.start = min(ovr.start, dur.start);
ovr.end = max(ovr.end, dur.end);
// if ( (dur.start < ovr.start) || (dur.end > ovr.end) )
// qDebug() << "Should have expanded overlap" << i << "for" << filename;
break;
}
} // end for walk existing overlap entries
if ( ! added ) {
if (dur.start != dur.end) { // Didn't fit it in anywhere, create a new Overlap entry/session
OverlappingEDF ov;
ov.start = dur.start;
ov.end = dur.end;
ov.filemap.insert(filetime_t, filename);
#ifdef SESSION_DEBUG
qDebug() << "Creating overlap for" << filename << "missing STR record";
qDebug() << "Starts:" << dur.start << "Ends:" << dur.end;
#endif
overlaps.append(ov);
} else {
#ifdef SESSION_DEBUG
qDebug() << "Skipping zero duration file" << filename;
#endif
}
} // end create a new overlap entry
} // end check for file overlap
} // end for walk resday files list
// Create an ordered map and see how far apart the sessions really are.
QMap<quint32, OverlappingEDF> mapov;
for (auto & ovr : overlaps) {
mapov[ovr.start] = ovr;
}
// We are not going to merge close sessions - gaps can be useful markers for users
// // Examine the gaps in between to see if we should merge sessions
// for (auto oit=mapov.begin(), oend=mapov.end(); oit != oend; ++oit) {
// // Get next in line
// auto next_oit = oit+1;
// if (next_oit != mapov.end()) {
// OverlappingEDF & A = oit.value();
// OverlappingEDF & B = next_oit.value();
// int gap = B.start - A.end;
// if (gap < 60) { // TODO see if we should use the prefs value here... ???
// // qDebug() << "Only a" << gap << "s sgap between ResMed sessions on" << resday->date.toString();
// }
// }
// }
if (overlaps.size()==0) {
qDebug() << "No sessionGroups for" << resday->date << "FINSIHED";
return;
}
// Now overlaps is populated with zero or more individual session groups of EDF files (zero because of sucky summary only days)
for (auto & ovr : overlaps) {
if (ovr.filemap.size() == 0)
continue;
Session * sess = new Session(mach, ovr.start);
// Do not set the session times according to Mask on/off times
// The LoadXXX edf routines will update them with recording start and durations
// sess->set_first(quint64(ovr.start)*1000L);
// sess->set_last(quint64(ovr.end)*1000L);
ovr.sess = sess;
for (auto mit=ovr.filemap.begin(), mend=ovr.filemap.end(); mit != mend; ++mit) {
const QString & filename = mit.value();
const QString & fullpath = resday->files[filename];
EDFType type = lookupEDFType(filename);
#ifdef SESSION_DEBUG
sess->session_files.append(filename);
#endif
switch (type) {
case EDF_BRP:
loader->LoadBRP(sess, fullpath);
break;
case EDF_PLD:
loader->LoadPLD(sess, fullpath);
break;
case EDF_SAD:
case EDF_SA2:
loader->LoadSAD(sess, fullpath);
break;
case EDF_EVE:
case EDF_CSL:
case EDF_AEV: // this is in the 36039 - must figure out what to do with it
break;
default:
qWarning() << "Unrecognized file type for" << filename;
}
} // end for each edf file in the sessionGroup
// Turns out there is only one or sometimes two EVE's per day, and they store data for the whole day
// So we have to extract Annotations data and apply it for all sessions
for (auto eit=EVElist.begin(), eveend=EVElist.end(); eit != eveend; ++eit) {
const QString & fullpath = resday->files[eit.value()];
loader->LoadEVE(ovr.sess, fullpath);
}
for (auto eit=CSLlist.begin(), cslend=CSLlist.end(); eit != cslend; ++eit) {
const QString & fullpath = resday->files[eit.value()];
loader->LoadCSL(ovr.sess, fullpath);
}
if (EVElist.size() == 0) {
sess->AddEventList(CPAP_Obstructive, EVL_Event);
sess->AddEventList(CPAP_ClearAirway, EVL_Event);
sess->AddEventList(CPAP_Apnea, EVL_Event);
sess->AddEventList(CPAP_Hypopnea, EVL_Event);
}
sess->setSummaryOnly(false);
sess->SetChanged(true);
if (sess->length() == 0) {
// we want empty sessions even though they are crap
qDebug() << "Session" << sess->session()
<< "["+QDateTime::fromTime_t(sess->session()).toString("MMM dd, yyyy hh:mm:ss")+"]"
<< "has zero duration" << QString("Start: %1").arg(sess->realFirst(),0,16) << QString("End: %1").arg(sess->realLast(),0,16);
}
if (sess->length() < 0) {
// we want empty sessions even though they are crap
qDebug() << "Session" << sess->session()
<< "["+QDateTime::fromTime_t(sess->session()).toString("MMM dd, yyyy hh:mm:ss")+"]"
<< "has negative duration";
qDebug() << QString("Start: %1").arg(sess->realFirst(),0,16) << QString("End: %1").arg(sess->realLast(),0,16);
}
if (resday->str.date.isValid()) {
STRRecord & R = resday->str;
// Claim this session
R.sessionid = sess->session();
// Save maskon time in session setting so we can use it later to avoid doubleups.
//sess->settings[RMS9_MaskOnTime] = R.maskon;
#ifdef SESSION_DEBUG
sess->session_files.append("STR.edf");
#endif
StoreSettings(sess, R);
} else { // No corresponding STR.edf record, but we have EDF files
#ifdef STR_DEBUG
qDebug() << "EDF files without STR record" << resday->date.toString();
#endif
bool foundprev = false;
loader->sessionMutex.lock();
auto it=p_profile->daylist.find(resday->date); // should exist already to be here
auto begin = p_profile->daylist.begin();
while (it!=begin) {
--it;
Day * day = it.value();
bool hasmachine = day && day->hasMachine(mach);
if ( ! hasmachine)
continue;
QList<Session *> sessions = day->getSessions(MT_CPAP);
if (sessions.size() > 0) {
Session *chksess = sessions[0];
sess->settings = chksess->settings;
foundprev = true;
break;
}
}
loader->sessionMutex.unlock();
sess->setNoSettings(true);
if (!foundprev) {
// We have no Summary or Settings data... we need to do something to indicate this, and detect the mode
if (sess->channelDataExists(CPAP_Pressure)) {
qDebug() << "Guessing the PAP mode...";
GuessPAPMode(sess);
}
}
} // end else no STR record for these edf files
sess->UpdateSummaries();
#ifdef SESSION_DEBUG
qDebug() << "Adding session" << sess->session()
<< "["+QDateTime::fromTime_t(sess->session()).toString("MMM dd, yyyy hh:mm:ss")+"]";
#endif
// Save is not threadsafe? (meh... it seems to be)
// loader->saveMutex.lock();
// loader->saveMutex.unlock();
// if ( (QDateTime::fromTime_t(sess->session()) > QDateTime::currentDateTime()) ||
if ( (sess->realFirst() == 0) || (sess->realLast() == 0) )
qWarning().noquote() << "Skipping future or absent date session:" << sess->session()
<< "["+QDateTime::fromTime_t(sess->session()).toString("MMM dd, yyyy hh:mm:ss")+"]"
<< "\noriginal date is" << resday->date.toString()
<< "session realFirst" << sess->realFirst() << "realLast" << sess->realLast();
else
save(loader, sess);
// Free the memory used by this session
sess->TrashEvents();
// delete sess;
} // end for-loop walking the overlaps (file groups per session
}
void ResmedLoader::SaveSession(ResmedLoader* loader, Session* sess)
{
Machine* mach = sess->machine();
loader->sessionMutex.lock(); // AddSession definitely ain't threadsafe.
if ( ! sess->Store(mach->getDataPath()) ) {
qWarning() << "Failed to store session" << sess->session();
}
if ( ! mach->AddSession(sess) ) {
qWarning() << "Session" << sess->session() << "was not addded";
}
loader->sessionCount++;
loader->sessionMutex.unlock();
}
bool matchSignal(ChannelID ch, const QString & name); // forward
bool ResmedLoader::LoadCSL(Session *sess, const QString & path)
{
#ifdef DEBUG_EFFICIENCY
QTime time;
time.start();
#endif
QString filename = path.section(-2, -1);
ResMedEDFInfo edf;
if ( ! edf.Open(path) ) {
qDebug() << "LoadCSL failed to open" << filename;
return false;
}
#ifdef DEBUG_EFFICIENCY
int edfopentime = time.elapsed();
time.start();
#endif
if (!edf.Parse()) {
qDebug() << "LoadCSL failed to parse" << filename;
return false;
}
#ifdef DEBUG_EFFICIENCY
int edfparsetime = time.elapsed();
time.start();
#endif
// Always create CSR event list so that overview always finds something
EventList *CSR = sess->AddEventList(CPAP_CSR, EVL_Event);
// Allow for empty sessions..
qint64 csr_starts = 0;
// Process event annotation records
// qDebug() << "File has " << edf.annotations.size() << "annotation vectors";
// int vec = 1;
for (auto annoVec = edf.annotations.begin(); annoVec != edf.annotations.end(); annoVec++ ) {
// qDebug() << "Vector " << vec++ << " has " << annoVec->size() << " annotations";
for (auto anno = annoVec->begin(); anno != annoVec->end(); anno++ ) {
// qDebug() << "Offset: " << anno->offset << " Duration: " << anno->duration << " Text: " << anno->text;
qint64 tt = edf.startdate + qint64(anno->offset*1000L);
if ( ! anno->text.isEmpty()) {
if (anno->text == "CSR Start") {
csr_starts = tt;
} else if (anno->text == "CSR End") {
// if ( ! CSR) {
// CSR = sess->AddEventList(CPAP_CSR, EVL_Event);
// }
if (csr_starts > 0) {
if (sess->checkInside(csr_starts)) {
CSR->AddEvent(tt, double(tt - csr_starts) / 1000.0);
}
csr_starts = 0;
} else {
qWarning() << "Split csr event flag in " << edf.filename;
}
} else if (anno->text != "Recording starts") {
qWarning() << "Unobserved ResMed CSL annotation field: " << anno->text;
}
}
}
}
if (csr_starts > 0) {
qDebug() << "Unfinished csr event in " << edf.filename;
}
#ifdef DEBUG_EFFICIENCY
timeMutex.lock();
timeInLoadCSL += time.elapsed();
timeInEDFOpen += edfopentime;
timeInEDFInfo += edfparsetime;
timeMutex.unlock();
#endif
return true;
}
bool ResmedLoader::LoadEVE(Session *sess, const QString & path)
{
#ifdef DEBUG_EFFICIENCY
QTime time;
time.start();
#endif
QString filename = path.section(-2, -1);
ResMedEDFInfo edf;
if ( ! edf.Open(path) ) {
qDebug() << "LoadEVE failed to open" << filename;
return false;
}
#ifdef DEBUG_EFFICIENCY
int edfopentime = time.elapsed();
time.start();
#endif
if (!edf.Parse()) {
qDebug() << "LoadEVE failed to parse" << filename;
return false;
}
#ifdef DEBUG_EFFICIENCY
int edfparsetime = time.elapsed();
time.start();
#endif
// Notes: Event records have useless duration record.
// Do not update session start / end times because they are needed to determine if events belong in this session or not...
EventList *OA = nullptr, *HY = nullptr, *CA = nullptr, *UA = nullptr, *RE = nullptr;
// Allow for empty sessions..
// Create some EventLists
OA = sess->AddEventList(CPAP_Obstructive, EVL_Event);
HY = sess->AddEventList(CPAP_Hypopnea, EVL_Event);
UA = sess->AddEventList(CPAP_Apnea, EVL_Event);
// Process event annotation records
// qDebug() << "File has " << edf.annotations.size() << "annotation vectors";
// int vec = 1;
for (auto annoVec = edf.annotations.begin(); annoVec != edf.annotations.end(); annoVec++ ) {
// qDebug() << "Vector " << vec++ << " has " << annoVec->size() << " annotations";
for (auto anno = annoVec->begin(); anno != annoVec->end(); anno++ ) {
qint64 tt = edf.startdate + qint64(anno->offset*1000L);
// qDebug() << "Offset: " << anno->offset << " Duration: " << anno->duration << " Text: " << anno->text;
// qDebug() << "Time: " << (tt/1000L). << " Duration: " << anno->duration << " Text: " << anno->text;
if ( ! anno->text.isEmpty()) {
if (matchSignal(CPAP_Obstructive, anno->text)) {
if (sess->checkInside(tt))
OA->AddEvent(tt, anno->duration);
} else if (matchSignal(CPAP_Hypopnea, anno->text)) {
if (sess->checkInside(tt))
HY->AddEvent(tt, anno->duration); // Hyponeas may not have any duration!
} else if (matchSignal(CPAP_Apnea, anno->text)) {
if (sess->checkInside(tt))
UA->AddEvent(tt, anno->duration);
} else if (matchSignal(CPAP_RERA, anno->text)) {
// Not all devices have it, so only create it when necessary..
if ( ! RE)
RE = sess->AddEventList(CPAP_RERA, EVL_Event);
if (sess->checkInside(tt))
RE->AddEvent(tt, anno->duration);
} else if (matchSignal(CPAP_ClearAirway, anno->text)) {
// Not all devices have it, so only create it when necessary..
if ( ! CA)
CA = sess->AddEventList(CPAP_ClearAirway, EVL_Event);
if (sess->checkInside(tt))
CA->AddEvent(tt, anno->duration);
} else if (anno->text == "SpO2 Desaturation") { // Used in 28509
continue; // ignored for now
} else {
if (anno->text != "Recording starts") {
qDebug() << "Unobserved ResMed annotation field: " << anno->text;
}
}
}
}
}
#ifdef DEBUG_EFFICIENCY
timeMutex.lock();
timeInLoadEVE += time.elapsed();
timeInEDFOpen += edfopentime;
timeInEDFInfo += edfparsetime;
timeMutex.unlock();
#endif
return true;
}
bool ResmedLoader::LoadBRP(Session *sess, const QString & path)
{
#ifdef DEBUG_EFFICIENCY
QTime time;
time.start();
#endif
QString filename = path.section(-2, -1);
ResMedEDFInfo edf;
if ( ! edf.Open(path) ) {
qDebug() << "LoadBRP failed to open" << filename.section("/", -2, -1);
return false;
}
#ifdef DEBUG_EFFICIENCY
int edfopentime = time.elapsed();
time.start();
#endif
if (!edf.Parse()) {
#ifdef EDF_DEBUG
qDebug() << "LoadBRP failed to parse" << filename.section("/", -2, -1);
#endif
return false;
}
#ifdef DEBUG_EFFICIENCY
int edfparsetime = time.elapsed();
time.start();
int AddWavetime = 0;
QTime time2;
#endif
sess->updateFirst(edf.startdate);
qint64 duration = edf.GetNumDataRecords() * edf.GetDurationMillis();
sess->updateLast(edf.startdate + duration);
for (auto & es : edf.edfsignals) {
long recs = es.sampleCnt * edf.GetNumDataRecords();
if (recs < 0)
continue;
ChannelID code;
if (matchSignal(CPAP_FlowRate, es.label)) {
code = CPAP_FlowRate;
es.gain *= 60.0;
es.physical_minimum *= 60.0;
es.physical_maximum *= 60.0;
es.physical_dimension = "L/M";
} else if (matchSignal(CPAP_MaskPressureHi, es.label)) {
code = CPAP_MaskPressureHi;
} else if (matchSignal(CPAP_RespEvent, es.label)) {
code = CPAP_RespEvent;
// } else if (es.label == "TrigCycEvt.40ms") { // we need a real code for this signal
// code = CPAP_TriggerEvent; // Well, it got folded into RespEvent
// continue;
} else if (es.label != "Crc16") {
qDebug() << "Unobserved ResMed BRP Signal " << es.label;
continue;
} else
continue;
if (code) {
double rate = double(duration) / double(recs);
EventList *a = sess->AddEventList(code, EVL_Waveform, es.gain, es.offset, 0, 0, rate);
a->setDimension(es.physical_dimension);
#ifdef DEBUG_EFFICIENCY
time2.start();
#endif
a->AddWaveform(edf.startdate, es.dataArray, recs, duration);
#ifdef DEBUG_EFFICIENCY
AddWavetime+= time2.elapsed();
#endif
EventDataType min = a->Min();
EventDataType max = a->Max();
// Cap to physical dimensions, because there can be ram glitches/whatever that throw really big outliers.
if (min < es.physical_minimum)
min = es.physical_minimum;
if (max > es.physical_maximum)
max = es.physical_maximum;
sess->updateMin(code, min);
sess->updateMax(code, max);
sess->setPhysMin(code, es.physical_minimum);
sess->setPhysMax(code, es.physical_maximum);
}
}
#ifdef DEBUG_EFFICIENCY
timeMutex.lock();
timeInLoadBRP += time.elapsed();
timeInEDFOpen += edfopentime;
timeInEDFInfo += edfparsetime;
timeInAddWaveform += AddWavetime;
timeMutex.unlock();
#endif
return true;
}
// Load SAD Oximetry Signals
bool ResmedLoader::LoadSAD(Session *sess, const QString & path)
{
#ifdef DEBUG_EFFICIENCY
QTime time;
time.start();
#endif
QString filename = path.section(-2, -1);
ResMedEDFInfo edf;
if ( ! edf.Open(path) ) {
qDebug() << "LoadSAD failed to open" << filename.section("/", -2, -1);
return false;
}
#ifdef DEBUG_EFFICIENCY
int edfopentime = time.elapsed();
time.start();
#endif
if (!edf.Parse()) {
#ifdef EDF_DEBUG
qDebug() << "LoadSAD failed to parse" << filename.section("/", -2, -1);
#endif
return false;
}
#ifdef DEBUG_EFFICIENCY
int edfparsetime = time.elapsed();
time.start();
#endif
sess->updateFirst(edf.startdate);
qint64 duration = edf.GetNumDataRecords() * edf.GetDurationMillis();
sess->updateLast(edf.startdate + duration);
for (auto & es : edf.edfsignals) {
//qDebug() << "SAD:" << es.label << es.digital_maximum << es.digital_minimum << es.physical_maximum << es.physical_minimum;
long recs = es.sampleCnt * edf.GetNumDataRecords();
ChannelID code;
bool hasdata = false;
for (int i = 0; i < recs; ++i) {
if (es.dataArray[i] != -1) {
hasdata = true;
break;
}
}
if (!hasdata)
continue;
if (matchSignal(OXI_Pulse, es.label)) {
code = OXI_Pulse;
ToTimeDelta(sess, edf, es, code, recs, duration);
sess->setPhysMax(code, 180);
sess->setPhysMin(code, 18);
} else if (matchSignal(OXI_SPO2, es.label)) {
code = OXI_SPO2;
es.physical_minimum = 60;
ToTimeDelta(sess, edf, es, code, recs, duration);
sess->setPhysMax(code, 100);
sess->setPhysMin(code, 60);
} else if (es.label != "Crc16") {
qDebug() << "Unobserved ResMed SAD Signal " << es.label;
}
}
#ifdef DEBUG_EFFICIENCY
timeMutex.lock();
timeInLoadSAD += time.elapsed();
timeInEDFOpen += edfopentime;
timeInEDFInfo += edfparsetime;
timeMutex.unlock();
#endif
return true;
}
bool ResmedLoader::LoadPLD(Session *sess, const QString & path)
{
#ifdef DEBUG_EFFICIENCY
QTime time;
time.start();
#endif
QString filename = path.section(-2, -1);
// qDebug() << "LoadPLD opening" << filename.section("/", -2, -1);
ResMedEDFInfo edf;
if ( ! edf.Open(path) ) {
qDebug() << "LoadPLD failed to open" << filename.section("/", -2, -1);
return false;
}
#ifdef DEBUG_EFFICIENCY
int edfopentime = time.elapsed();
time.start();
#endif
if (!edf.Parse()) {
#ifdef EDF_DEBUG
qDebug() << "LoadPLD failed to parse" << filename.section("/", -2, -1);
#endif
return false;
}
#ifdef DEBUG_EFFICIENCY
int edfparsetime = time.elapsed();
time.start();
#endif
// Is it safe to assume the order does not change here?
enum PLDType { MaskPres = 0, TherapyPres, ExpPress, Leak, RR, Vt, Mv, SnoreIndex, FFLIndex, U1, U2 };
qint64 duration = edf.GetNumDataRecords() * edf.GetDurationMillis();
sess->updateFirst(edf.startdate);
sess->updateLast(edf.startdate + duration);
QString t;
int emptycnt = 0;
EventList *a = nullptr;
// double rate;
long samples;
ChannelID code;
bool square = AppSetting->squareWavePlots();
// The following is a hack to skip the multiple uses of Ti and Te by Resmed for signal labels
// It should be replaced when code in resmed_info class changes the labels to be unique
bool found_Ti_code = false;
bool found_Te_code = false;
QDateTime sessionStartDT = QDateTime:: fromMSecsSinceEpoch(sess->first());
// bool forceDebug = (sessionStartDT > QDateTime::fromString("2021-02-26 12:00:00", "yyyy-MM-dd HH:mm:ss")) &&
// (sessionStartDT < QDateTime::fromString("2021-02-27 12:00:00", "yyyy-MM-dd HH:mm:ss"));
bool forceDebug = false;
for (auto & es : edf.edfsignals) {
a = nullptr;
samples = es.sampleCnt * edf.GetNumDataRecords();
if (samples <= 0)
continue;
// rate = double(duration) / double(samples);
//qDebug() << "EVE:" << es.digital_maximum << es.digital_minimum << es.physical_maximum << es.physical_minimum << es.gain;
if (forceDebug) {
qDebug() << "Session" << sessionStartDT.toString() << filename.section("/", -2, -1) << "signal" << es.label;
qDebug() << "\tSecond/rec:" << edf.GetDurationMillis()/1000 << "Samples/rec:" << es.sampleCnt;
}
if (matchSignal(CPAP_Snore, es.label)) {
code = CPAP_Snore;
ToTimeDelta(sess, edf, es, code, samples, duration, 0, 0, square);
} else if (matchSignal(CPAP_Pressure, es.label)) {
code = CPAP_Pressure;
// es.physical_maximum = 25;
// es.physical_minimum = 4;
ToTimeDelta(sess, edf, es, code, samples, duration, 0, 0, true);
} else if (matchSignal(CPAP_IPAP, es.label)) {
code = CPAP_IPAP;
// es.physical_maximum = 25;
// es.physical_minimum = 4;
ToTimeDelta(sess, edf, es, code, samples, duration, 0, 0, true);
} else if (matchSignal(CPAP_EPAP, es.label)) { // Expiratory Pressure
code = CPAP_EPAP;
// es.physical_maximum = 25;
// es.physical_minimum = 4;
ToTimeDelta(sess, edf, es, code, samples, duration, 0, 0, true);
} else if (matchSignal(CPAP_MinuteVent,es.label)) {
code = CPAP_MinuteVent;
ToTimeDelta(sess, edf, es, code, samples, duration, 0, 0, square);
} else if (matchSignal(CPAP_RespRate, es.label)) {
code = CPAP_RespRate;
// a = sess->AddEventList(code, EVL_Waveform, es.gain, es.offset, 0, 0, rate);
// a->AddWaveform(edf.startdate, es.dataArray, samples, duration);
ToTimeDelta(sess, edf, es, code, samples, duration, 0, 0, square);
} else if (matchSignal(CPAP_TidalVolume, es.label)) {
code = CPAP_TidalVolume;
es.physical_dimension = "mL";
es.gain *= 1000.0;
es.physical_maximum *= 1000.0;
es.physical_minimum *= 1000.0;
// es.digital_maximum*=1000.0;
// es.digital_minimum*=1000.0;
ToTimeDelta(sess, edf, es, code, samples, duration, 0, 0, square);
} else if (matchSignal(CPAP_Leak, es.label)) {
code = CPAP_Leak;
es.gain *= 60.0;
es.physical_maximum *= 60.0;
es.physical_minimum *= 60.0;
// es.digital_maximum*=60.0;
// es.digital_minimum*=60.0;
es.physical_dimension = "L/M";
ToTimeDelta(sess, edf, es, code, samples, duration, 0, 0, true);
sess->setPhysMax(code, 120.0);
sess->setPhysMin(code, 0);
} else if (matchSignal(CPAP_FLG, es.label)) {
code = CPAP_FLG;
ToTimeDelta(sess, edf, es, code, samples, duration, 0, 0, square);
} else if (matchSignal(CPAP_MaskPressure, es.label)) {
code = CPAP_MaskPressure;
// es.physical_maximum = 25;
// es.physical_minimum = 4;
ToTimeDelta(sess, edf, es, code, samples, duration, 0, 0, square);
} else if (matchSignal(CPAP_IE, es.label)) { //I:E ratio
code = CPAP_IE;
es.gain /= 100.0;
es.physical_maximum /= 100.0;
es.physical_minimum /= 100.0;
// qDebug() << "IE Gain, Max, Min" << es.gain << es.physical_maximum << es.physical_minimum;
// qDebug() << "IE count, data..." << es.sampleCnt << es.dataArray[0] << es.dataArray[1] << es.dataArray[2] << es.dataArray[3] << es.dataArray[4];
// a = sess->AddEventList(code, EVL_Waveform, es.gain, es.offset, 0, 0, rate);
// a->AddWaveform(edf.startdate, es.dataArray, samples, duration);
// Fix ToTimeDelta to store inverse of edf data - also fix labels and tool tip
// ToTimeDelta(sess,edf,es, code,samples,duration,0,0, square);
} else if (matchSignal(CPAP_Ti, es.label)) {
code = CPAP_Ti;
// There are TWO of these with the same label on 36037, 36039, 36377 and others
// Also 37051 has R5Ti.2s and Ti.2s. We use R5Ti.2s and ignore the Ti.2s
if ( found_Ti_code )
continue;
found_Ti_code = true;
// a = sess->AddEventList(code, EVL_Waveform, es.gain, es.offset, 0, 0, rate);
// a->AddWaveform(edf.startdate, es.dataArray, samples, duration);
ToTimeDelta(sess,edf,es, code,samples,duration,0,0, square);
} else if (matchSignal(CPAP_Te, es.label)) {
code = CPAP_Te;
// There are TWO of these with the same label on my VPAP Adapt 36037
if ( found_Te_code )
continue;
found_Te_code = true;
// a = sess->AddEventList(code, EVL_Waveform, es.gain, es.offset, 0, 0, rate);
// a->AddWaveform(edf.startdate, es.dataArray, samples, duration);
ToTimeDelta(sess,edf,es, code,samples,duration,0,0, square);
} else if (matchSignal(CPAP_TgMV, es.label)) {
code = CPAP_TgMV;
// a = sess->AddEventList(code, EVL_Waveform, es.gain, es.offset, 0, 0, rate);
// a->AddWaveform(edf.startdate, es.dataArray, samples, duration);
ToTimeDelta(sess,edf,es, code,samples,duration,0,0, square);
} else if (es.label == "Va") { // Signal used in 36039... What to do with it???
a = nullptr; // We'll skip it for now
} else if (es.label == "AlvMinVent.2s") { // Signal used in 28509... What to do with it???
a = nullptr; // We'll skip it for now
} else if (es.label == "CLRatio.2s") { // Signal used in 28509... What to do with it???
a = nullptr; // We'll skip it for now
} else if (es.label == "TRRatio.2s") { // Signal used in 28509... What to do with it???
a = nullptr; // We'll skip it for now
} else if (es.label == "") { // What the hell resmed??
// these empty lables should be changed in resmed_EDFInfo to something unique
if (emptycnt == 0) {
code = RMS9_E01;
// ToTimeDelta(sess, edf, es, code, samples, duration, 0, 0, square);
} else if (emptycnt == 1) {
code = RMS9_E02;
// ToTimeDelta(sess, edf, es, code, samples, duration, 0, 0, square);
} else {
qDebug() << "Unobserved Empty Signal " << es.label;
}
emptycnt++;
} else if (es.label != "Crc16") {
qDebug() << "Unobserved ResMed PLD Signal " << es.label;
a = nullptr;
}
if (a) {
sess->updateMin(code, a->Min());
sess->updateMax(code, a->Max());
sess->setPhysMin(code, es.physical_minimum);
sess->setPhysMax(code, es.physical_maximum);
a->setDimension(es.physical_dimension);
}
}
#ifdef DEBUG_EFFICIENCY
timeMutex.lock();
timeInLoadPLD += time.elapsed();
timeInEDFOpen += edfopentime;
timeInEDFInfo += edfparsetime;
timeMutex.unlock();
#endif
return true;
}
// Convert EDFSignal data to OSCAR's Time-Delta Event format
EventList * buildEventList( EventStoreType est, EventDataType t_min, EventDataType t_max, EDFSignal &es,
EventDataType *min, EventDataType *max, double tt, EventList *el, Session * sess, ChannelID code ); // forward
void ResmedLoader::ToTimeDelta(Session *sess, ResMedEDFInfo &edf, EDFSignal &es, ChannelID code,
long samples, qint64 duration, EventDataType t_min, EventDataType t_max, bool square)
{
using namespace schema;
ChannelList channel;
// QDateTime sessionStartDT = QDateTime:: fromMSecsSinceEpoch(sess->first());
// bool forceDebug = (sessionStartDT > QDateTime::fromString("2021-02-26 12:00:00", "yyyy-MM-dd HH:mm:ss")) &&
// (sessionStartDT < QDateTime::fromString("2021-02-27 12:00:00", "yyyy-MM-dd HH:mm:ss"));
bool forceDebug = false;
if (t_min == t_max) {
t_min = es.physical_minimum;
t_max = es.physical_maximum;
}
#ifdef DEBUG_EFFICIENCY
QElapsedTimer time;
time.start();
#endif
double rate = (duration / samples); // milliseconds per record
double tt = edf.startdate;
EventStoreType c=0, last;
int startpos = 0;
// There's no reason to skip the first 40 seconds of slow data
// Reduce that to 10 seconds, to allow presssures to stabilise
if ((code == CPAP_Pressure) || (code == CPAP_IPAP) || (code == CPAP_EPAP)) {
startpos = 5; // Shave the first 10 seconds of pressure data
tt += rate * startpos;
}
// Likewise for the values that the device computes for us, but 20 seconds
if ( (code == CPAP_MinuteVent) || (code == CPAP_RespRate) || (code == CPAP_TidalVolume) ||
(code == CPAP_Ti) || (code == CPAP_Te) || (code == CPAP_IE) ) {
startpos = 10; // Shave the first 20 seconds of computed data
tt += rate * startpos;
}
qint16 *sptr = es.dataArray;
qint16 *eptr = sptr + samples;
sptr += startpos;
EventDataType min = t_max, max = t_min, tmp;
EventList *el = nullptr;
if (forceDebug)
qDebug() << "Code:" << QString::number(code, 16) << "Samples:" << samples;
if (samples > startpos + 1) {
// Prime last with a good starting value
do {
last = *sptr++;
tmp = EventDataType(last) * es.gain;
if ((tmp >= t_min) && (tmp <= t_max)) {
min = tmp;
max = tmp;
el = sess->AddEventList(code, EVL_Event, es.gain, es.offset, 0, 0);
if (forceDebug)
// qDebug() << "New EventList:" << channel.channels[code]->code() << QDateTime::fromMSecsSinceEpoch(tt).toString();
qDebug() << "New EventList:" << QString::number(code, 16) << QDateTime::fromMSecsSinceEpoch(tt).toString();
el->AddEvent(tt, last);
if (forceDebug && ((code == CPAP_Pressure) || (code == CPAP_IPAP) || (code == CPAP_EPAP)) )
qDebug() << "First Event:" << tmp << QDateTime::fromMSecsSinceEpoch(tt).toString() << "Pos:" << (sptr-1) - es.dataArray;
tt += rate;
break;
}
tt += rate;
} while (sptr < eptr);
if ( ! el) {
qWarning() << "No eventList for" << QDateTime::fromMSecsSinceEpoch(sess->first()).toString() << "code"
// << channel.channels[code]->code();
<< QString::number(code, 16);
#ifdef DEBUG_EFFICIENCY
timeMutex.lock();
timeInTimeDelta += time.elapsed();
timeMutex.unlock();
#endif
return;
}
if (forceDebug && ((code == CPAP_Pressure) || (code == CPAP_IPAP) || (code == CPAP_EPAP)) )
qDebug() << "Before loop to buildEventList" << el->count() << "Last:" << last*es.gain << "Next:" << (*sptr)*es.gain <<
"Pos:" << sptr - es.dataArray << QDateTime::fromMSecsSinceEpoch(tt).toString();
for (; sptr < eptr; sptr++) {
c = *sptr;
if (last != c) {
if (square) {
if (forceDebug && ((code == CPAP_Pressure) || (code == CPAP_IPAP) || (code == CPAP_EPAP)) )
qDebug() << "Before square call to buildEventList" << el->count();
el = buildEventList( last, t_min, t_max, es, &min, &max, tt, el, sess, code );
if (forceDebug && ((code == CPAP_Pressure) || (code == CPAP_IPAP) || (code == CPAP_EPAP)) )
qDebug() << "After square call to buildEventList" << el->count();
}
if (forceDebug && ((code == CPAP_Pressure) || (code == CPAP_IPAP) || (code == CPAP_EPAP)) )
qDebug() << "Before call to buildEventList" << el->count() << "Cur:" << c*es.gain << "Last:" << last*es.gain << "Pos:" << sptr - es.dataArray <<
QDateTime::fromMSecsSinceEpoch(tt).toString();
el = buildEventList( c, t_min, t_max, es, &min, &max, tt, el, sess, code );
if (forceDebug && ((code == CPAP_Pressure) || (code == CPAP_IPAP) || (code == CPAP_EPAP)) )
qDebug() << "After call to buildEventList" << el->count();
}
tt += rate;
last = c;
}
tmp = EventDataType(c) * es.gain;
if ((tmp >= t_min) && (tmp <= t_max)) {
el->AddEvent(tt, c);
if (forceDebug && ((code == CPAP_Pressure) || (code == CPAP_IPAP) || (code == CPAP_EPAP)) )
qDebug() << "Last Event:" << tmp << QDateTime::fromMSecsSinceEpoch(tt).toString() << "Pos:" << (sptr-1) - es.dataArray;
} else
qDebug() << "Failed to add last event - Code:" << QString::number(code, 16) << "Value:" << tmp <<
QDateTime::fromMSecsSinceEpoch(tt).toString() << "Pos:" << (sptr-1) - es.dataArray;
sess->updateMin(code, min);
sess->updateMax(code, max);
sess->setPhysMin(code, es.physical_minimum);
sess->setPhysMax(code, es.physical_maximum);
sess->updateLast(tt);
if (forceDebug)
// qDebug() << "EventList:" << channel.channels[code]->code() << QDateTime::fromMSecsSinceEpoch(tt).toString() << "Size" << el->count();
qDebug() << "EventList:" << QString::number(code, 16) << QDateTime::fromMSecsSinceEpoch(tt).toString() << "Size" << el->count();
} else {
qWarning() << "not enough records for EventList" << QDateTime::fromMSecsSinceEpoch(sess->first()).toString() << "code"
// << channel.channels[code]->code();
<< QString::number(code, 16);
}
#ifdef DEBUG_EFFICIENCY
timeMutex.lock();
if (el != nullptr) {
qint64 t = time.nsecsElapsed();
int cnt = el->count();
int bytes = cnt * (sizeof(EventStoreType) + sizeof(quint32));
int wvbytes = samples * (sizeof(EventStoreType));
auto it = channel_efficiency.find(code);
if (it == channel_efficiency.end()) {
channel_efficiency[code] = wvbytes - bytes;
channel_time[code] = t;
} else {
it.value() += wvbytes - bytes;
channel_time[code] += t;
}
}
timeInTimeDelta += time.elapsed();
timeMutex.unlock();
#endif
} // end ResMedLoader::ToTimeDelta
EventList * buildEventList( EventStoreType est, EventDataType t_min, EventDataType t_max, EDFSignal &es,
EventDataType *min, EventDataType *max, double tt, EventList *el, Session * sess, ChannelID code )
{
using namespace schema;
ChannelList channel;
// QDateTime sessionStartDT = QDateTime:: fromMSecsSinceEpoch(sess->first());
// bool forceDebug = (sessionStartDT > QDateTime::fromString("2021-02-26 12:00:00", "yyyy-MM-dd HH:mm:ss")) &&
// (sessionStartDT < QDateTime::fromString("2021-02-27 12:00:00", "yyyy-MM-dd HH:mm:ss"));
bool forceDebug = false;
EventDataType tmp = EventDataType(est) * es.gain;
if ((tmp >= t_min) && (tmp <= t_max)) {
if (tmp < *min)
*min = tmp;
if (tmp > *max)
*max = tmp;
el->AddEvent(tt, est);
} else {
// if ( tmp > 0 )
qDebug() << "Code:" << QString::number(code, 16) <<"Value:" << tmp << "Out of range:\n\t t_min:" <<
t_min << "t_max:" << t_max << "EL count:" << el->count();
// Out of bounds value, start a new eventlist
// But first drop a closing value that repeats the last one
el->AddEvent(tt, el->raw(el->count() - 1));
if (el->count() > 1) {
// that should be in session, not the eventlist.. handy for debugging though
el->setDimension(es.physical_dimension);
el = sess->AddEventList(code, EVL_Event, es.gain, es.offset, 0, 0);
if (forceDebug)
// qDebug() << "New EventList:" << channel.channels[code]->code() << QDateTime::fromMSecsSinceEpoch(tt).toString();
qDebug() << "New EventList:" << QString::number(code, 16) << QDateTime::fromMSecsSinceEpoch(tt).toString();
} else {
el->clear(); // reuse the object
if (forceDebug)
// qDebug() << "Clear EventList:" << channel.channels[code]->code() << QDateTime::fromMSecsSinceEpoch(tt).toString();
qDebug() << "Clear EventList:" << QString::number(code, 16) << QDateTime::fromMSecsSinceEpoch(tt).toString();
}
}
return el;
}
// Check if given string matches any alternative signal names for this channel
bool matchSignal(ChannelID ch, const QString & name)
{
auto channames = resmed_codes.find(ch);
if (channames == resmed_codes.end()) {
return false;
}
for (auto & string : channames.value()) {
// Using starts with, because ResMed is very lazy about consistency
if (name.startsWith(string, Qt::CaseInsensitive)) {
return true;
}
}
return false;
}
void setupResMedTranslationMap()
{
////////////////////////////////////////////////////////////////////////////
// Translation lookup table for non-english devices
// Also combine S9, AS10, and AS11 variants
////////////////////////////////////////////////////////////////////////////
// Only put the first part, enough to be identifiable, because ResMed likes
// to crop short the signal names
// Read this from a table?
resmed_codes.clear();
// BRP file
resmed_codes[CPAP_FlowRate] = QStringList{ "Flow", "Flow.40ms" };
resmed_codes[CPAP_MaskPressureHi] = QStringList{ "Mask Pres", "Press.40ms" };
// resmed_codes[CPAP_TriggerEvent] = QStringList{ "TrigCycEvt.40ms" }; // AC10 VAuto and -S
resmed_codes[CPAP_RespEvent] = QStringList {"Resp Event", "TrigCycEvt.40ms" }; // S9 VPAPS and STA-IVAPS call it RespEvent
// PLD File
resmed_codes[CPAP_MaskPressure] = QStringList { "Mask Pres", "MaskPress.2s" };
// resmed_codes[CPAP_RespEvent] = QStringList {"Resp Event" };
resmed_codes[CPAP_Pressure] = QStringList { "Therapy Pres", "Press.2s" }; // Un problemo... IPAP also uses Press.2s.. check the mode :/
// STR signals
resmed_codes[CPAP_IPAP] = QStringList { "Insp Pres", "IPAP", "S.BL.IPAP" };
resmed_codes[CPAP_EPAP] = QStringList { "Exp Pres", "EprPress.2s", "EPAP", "S.BL.EPAP", "EPRPress.2s" };
resmed_codes[CPAP_EPAPHi] = QStringList { "Max EPAP" };
resmed_codes[CPAP_EPAPLo] = QStringList { "Min EPAP", "S.VA.MinEPAP" };
resmed_codes[CPAP_IPAPHi] = QStringList { "Max IPAP", "S.VA.MaxIPAP" };
resmed_codes[CPAP_IPAPLo] = QStringList { "Min IPAP" };
resmed_codes[CPAP_PS] = QStringList { "PS", "S.VA.PS" };
resmed_codes[CPAP_PSMin] = QStringList { "Min PS" };
resmed_codes[CPAP_PSMax] = QStringList { "Max PS" };
resmed_codes[CPAP_Leak] = QStringList { "Leak", "Leck", "Fuites", "Fuite", "Fuga", "\xE6\xBC\x8F\xE6\xB0\x94", "Lekk", "Läck","Läck", "Leak.2s", "Sızıntı" };
resmed_codes[CPAP_RespRate] = QStringList { "RR", "AF", "FR", "RespRate.2s" };
resmed_codes[CPAP_MinuteVent] = QStringList { "MV", "VM", "MinVent.2s" };
resmed_codes[CPAP_TidalVolume] = QStringList { "Vt", "VC", "TidVol.2s" };
resmed_codes[CPAP_IE] = QStringList { "I:E", "IERatio.2s" };
resmed_codes[CPAP_Snore] = QStringList { "Snore", "Snore.2s" };
resmed_codes[CPAP_FLG] = QStringList { "FFL Index", "FlowLim.2s" };
resmed_codes[CPAP_Ti] = QStringList { "Ti", "B5ITime.2s" };
resmed_codes[CPAP_Te] = QStringList { "Te", "B5ETime.2s" };
resmed_codes[CPAP_TgMV] = QStringList { "TgMV", "TgtVent.2s" };
resmed_codes[OXI_Pulse] = QStringList { "Pulse", "Puls", "Pouls", "Pols", "Pulse.1s", "Nabiz" };
resmed_codes[OXI_SPO2] = QStringList { "SpO2", "SpO2.1s" };
resmed_codes[CPAP_Obstructive] = QStringList { "Obstructive apnea" };
resmed_codes[CPAP_Hypopnea] = QStringList { "Hypopnea" };
resmed_codes[CPAP_Apnea] = QStringList { "Apnea" };
resmed_codes[CPAP_RERA] = QStringList { "Arousal" };
resmed_codes[CPAP_ClearAirway] = QStringList { "Central apnea" };
resmed_codes[CPAP_Mode] = QStringList { "Mode", "Modus", "Funktion", "\xE6\xA8\xA1\xE5\xBC\x8F", "Mod" };
resmed_codes[RMS9_SetPressure] = QStringList { "Set Pressure", "Eingest. Druck", "Ingestelde druk", "\xE8\xAE\xBE\xE5\xAE\x9A\xE5\x8E\x8B\xE5\x8A\x9B", "Pres. prescrite", "Inställt tryck", "Inställt tryck", "S.C.Press", "Basıncı Ayarl" };
resmed_codes[RMS9_EPR] = QStringList { "EPR", "\xE5\x91\xBC\xE6\xB0\x94\xE9\x87\x8A\xE5\x8E\x8B\x28\x45\x50" };
resmed_codes[RMS9_EPRLevel] = QStringList { "EPR Level", "EPR-Stufe", "EPR-niveau", "\x45\x50\x52\x20\xE6\xB0\xB4\xE5\xB9\xB3", "Niveau EPR", "EPR-nivå", "EPR-nivÃ¥", "S.EPR.Level", "EPR Düzeyi" };
resmed_codes[CPAP_PressureMax] = QStringList { "Max Pressure", "Max. Druck", "Max druk", "\xE6\x9C\x80\xE5\xA4\xA7\xE5\x8E\x8B\xE5\x8A\x9B", "Pression max.", "Max tryck", "S.AS.MaxPress", "S.A.MaxPress", "Azami Bası" };
resmed_codes[CPAP_PressureMin] = QStringList { "Min Pressure", "Min. Druck", "Min druk", "\xE6\x9C\x80\xE5\xB0\x8F\xE5\x8E\x8B\xE5\x8A\x9B", "Pression min.", "Min tryck", "S.AS.MinPress", "S.A.MinPress", "Min Bası" };
//resmed_codes[RMS9_EPR].push_back("S.EPR.EPRType");
}
// don't really need this anymore, but perhaps it's useful info for reference
// Resmed_Model_Map = {
// { "S9 Escape", { 36001, 36011, 36021, 36141, 36201, 36221, 36261, 36301, 36361 } },
// { "S9 Escape Auto", { 36002, 36012, 36022, 36302, 36362 } },
// { "S9 Elite", { 36003, 36013, 36023, 36103, 36113, 36123, 36143, 36203, 36223, 36243, 36263, 36303, 36343, 36363 } },
// { "S9 Autoset", { 36005, 36015, 36025, 36105, 36115, 36125, 36145, 36205, 36225, 36245, 36265, 36305, 36325, 36345, 36365 } },
// { "S9 AutoSet CS", { 36100, 36110, 36120, 36140, 36200, 36220, 36360 } },
// { "S9 AutoSet 25", { 36106, 36116, 36126, 36146, 36206, 36226, 36366 } },
// { "S9 AutoSet for Her", { 36065 } },
// { "S9 VPAP S", { 36004, 36014, 36024, 36114, 36124, 36144, 36204, 36224, 36284, 36304 } },
// { "S9 VPAP Auto", { 36006, 36016, 36026 } },
// { "S9 VPAP Adapt", { 36037, 36007, 36017, 36027, 36367 } },
// { "S9 VPAP ST", { 36008, 36018, 36028, 36108, 36148, 36208, 36228, 36368 } },
// { "S9 VPAP ST 22", { 36118, 36128 } },
// { "S9 VPAP ST-A", { 36039, 36159, 36169, 36379 } },
// //S8 Series
// { "S8 Escape", { 33007 } },
// { "S8 Elite II", { 33039 } },
// { "S8 Escape II", { 33051 } },
// { "S8 Escape II AutoSet", { 33064 } },
// { "S8 AutoSet II", { 33129 } },
// };
//
// Return the model name matching the supplied model number.
// const QString & lookupModel(quint16 model)
// {
//
// for (auto it=Resmed_Model_Map.begin(),end = Resmed_Model_Map.end(); it != end; ++it) {
// QList<quint16> & list = it.value();
// for (auto val : list) {
// if (val == model) {
// return it.key();
// }
// }
// }
// return STR_UnknownModel;
// }
////////////////////////////////////////////////////////////////////////////////////////////////
// Model number information
// 36003, 36013, 36023, 36103, 36113, 36123, 36143, 36203,
// 36223, 36243, 36263, 36303, 36343, 36363 S9 Elite Series
// 36005, 36015, 36025, 36105, 36115, 36125, 36145, 36205,
// 36225, 36245, 36265, 36305, 36325, 36345, 36365 S9 AutoSet Series
// 36065 S9 AutoSet for Her
// 36001, 36011, 36021, 36141, 36201, 36221, 36261, 36301,
// 36361 S9 Escape
// 36002, 36012, 36022, 36302, 36362 S9 Escape Auto
// 36004, 36014, 36024, 36114, 36124, 36144, 36204, 36224,
// 36284, 36304 S9 VPAP S (+ H5i, + Climate Control)
// 36006, 36016, 36026 S9 VPAP AUTO (+ H5i, + Climate Control)
// 36007, 36017, 36027, 36367
// S9 VPAP ADAPT (+ H5i, + Climate
// Control)
// 36008, 36018, 36028, 36108, 36148, 36208, 36228, 36368 S9 VPAP ST (+ H5i, + Climate Control)
// 36100, 36110, 36120, 36140, 36200, 36220, 36360 S9 AUTOSET CS
// 36106, 36116, 36126, 36146, 36206, 36226, 36366 S9 AUTOSET 25
// 36118, 36128 S9 VPAP ST 22
// 36039, 36159, 36169, 36379 S9 VPAP ST-A
// 24921, 24923, 24925, 24926, 24927 ResMed Power Station II (RPSII)
// 33030 S8 Compact
// 33001, 33007, 33013, 33036, 33060 S8 Escape
// 33032 S8 Lightweight
// 33033 S8 AutoScore
// 33048, 33051, 33052, 33053, 33054, 33061 S8 Escape II
// 33055 S8 Lightweight II
// 33021 S8 Elite
// 33039, 33045, 33062, 33072, 33073, 33074, 33075 S8 Elite II
// 33044 S8 AutoScore II
// 33105, 33112, 33126 S8 AutoSet (including Spirit & Vantage)
// 33128, 33137 S8 Respond
// 33129, 33141, 33150 S8 AutoSet II
// 33136, 33143, 33144, 33145, 33146, 33147, 33148 S8 AutoSet Spirit II
// 33138 S8 AutoSet C
// 26101, 26121 VPAP Auto 25
// 26119, 26120 VPAP S
// 26110, 26122 VPAP ST
// 26104, 26105, 26125, 26126 S8 Auto 25
// 26102, 26103, 26106, 26107, 26108, 26109, 26123, 26127 VPAP IV
// 26112, 26113, 26114, 26115, 26116, 26117, 26118, 26124 VPAP IV ST