/* SleepLib Resvent Loader Implementation * * Copyright (c) 2019-2024 The OSCAR Team * Copyright (c) 2011-2018 Mark Watkins * * 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. */ //******************************************************************************************** // Please only INCREMENT the resvent_data_version in resvent_loader.h when making changes // that change loader behaviour or modify channels in a manner that fixes old data imports. // Note that changing the data version will require a reimport of existing data for which OSCAR // does not keep a backup - so it should be avoided if possible. // i.e. there is no need to change the version when adding support for new devices //******************************************************************************************** // Turn off for offical release. #define TEST_MACROS_ENABLEDoff #include #include #include #include #include #include #include #include #include #include #include #include #include "resvent_loader.h" #ifdef DEBUG_EFFICIENCY #include // only available in 4.8 #endif // Files WXX_XX contain flow rate and pressure and PXX_XX contain Pressure, IPAP, EPAP, Leak, Vt, MV, RR, Ti, IE, Spo2, PR // Both files contain a little header of size 0x24 bytes. In offset 0x12 contain the total number of different records in // the different files of the same type. And later contain the previous describe quantity of description header of size 0x20 // containing the details for every type of record (e.g. sample chunk size). ResventLoader::ResventLoader() { const QString RESVENT_ICON = ":/icons/resvent.png"; QString s = newInfo().series; m_pixmap_paths[s] = RESVENT_ICON; m_pixmaps[s] = QPixmap(RESVENT_ICON); m_type = MT_CPAP; } ResventLoader::~ResventLoader() { } const QString kResventTherapyFolder = "THERAPY"; const QString kResventConfigFolder = "CONFIG"; const QString kResventRecordFolder = "RECORD"; const QString kResventSysConfigFilename = "SYSCFG"; constexpr qint64 kDateTimeOffset = 8 * 60 * 60 * 1000; // Offset to GMT constexpr int kMainHeaderSize = 0x24; constexpr int kDescriptionHeaderSize = 0x20; constexpr int kChunkDurationInSecOffset = 0x10; constexpr int kDescriptionCountOffset = 0x12; constexpr int kDescriptionSamplesByChunk = 0x1e; constexpr double kMilliGain = 0.001; constexpr double kHundredthGain = 0.01; constexpr double kTenthGain = 0.1; constexpr double kNoGain = 1.0; constexpr double kDefaultGain = kHundredthGain ; // For Flow (rate) and (mask)Pressure - High Resolutions data. bool ResventLoader::Detect(const QString & givenpath) { QDir dir(givenpath); if (!dir.exists()) { return false; } if (!dir.exists(kResventTherapyFolder)) { return false; } dir.cd(kResventTherapyFolder); if (!dir.exists(kResventConfigFolder)) { return false; } if (!dir.cd(kResventRecordFolder)) { return false; } return true; } MachineInfo ResventLoader::PeekInfo(const QString & path) { if (!Detect(path)) { return MachineInfo(); } MachineInfo info = newInfo(); const auto sys_config_path = path + QDir::separator() + kResventTherapyFolder + QDir::separator() + kResventConfigFolder + QDir::separator() + kResventSysConfigFilename; if (!QFile::exists(sys_config_path)) { qDebug() << "Resvent Data card has no" << kResventSysConfigFilename << "file in " << sys_config_path; return MachineInfo(); } QFile f(sys_config_path); f.open(QIODevice::ReadOnly | QIODevice::Text); f.seek(4); while (!f.atEnd()) { QString line = f.readLine().trimmed(); const auto elems = line.split("="); Q_ASSERT(elems.size() == 2); if (elems[0] == "models") { info.model = elems[1]; } else if (elems[0] == "sn") { info.serial = elems[1]; } else if (elems[0] == "num") { info.version = elems[1].toInt(); } else if (elems[0] == "num") { info.type = MachineType::MT_CPAP; } } if (info.model.contains("Point", Qt::CaseInsensitive)) { info.brand = "Hoffrichter"; } else { info.brand = "Resvent"; } return info; } QVector GetSessionsDate(const QString& dirpath) { QVector sessions_date; const auto records_path = dirpath + QDir::separator() + kResventTherapyFolder + QDir::separator() + kResventRecordFolder; QDir records_folder(records_path); const auto year_month_folders = records_folder.entryList(QStringList(), QDir::Dirs|QDir::NoDotAndDotDot, QDir::Name); std::for_each(year_month_folders.cbegin(), year_month_folders.cend(), [&](const QString& year_month_folder_name){ if (year_month_folder_name.length() != 6) { return; } const int year = std::stoi(year_month_folder_name.left(4).toStdString()); const int month = std::stoi(year_month_folder_name.right(2).toStdString()); const auto year_month_folder_path = records_path + QDir::separator() + year_month_folder_name; QDir year_month_folder(year_month_folder_path); const auto session_folders = year_month_folder.entryList(QStringList(), QDir::Dirs|QDir::NoDotAndDotDot, QDir::Name); std::for_each(session_folders.cbegin(), session_folders.cend(), [&](const QString& day_folder){ const auto day = std::stoi(day_folder.toStdString()); sessions_date.push_back(QDate(year, month, day)); }); }); return sessions_date; } enum class EventType { //UsageSec = 1, //UnixStart = 2, ObstructiveApnea = 17, CentralApnea = 18, Hypopnea = 19, FlowLimitation = 20, RERA = 21, PeriodicBreathing = 22, Snore = 23 }; struct EventData { EventType type; QDateTime date_time; int duration; }; struct UsageData { QString number{}; QDateTime start_time{}; QDateTime end_time{}; qint32 countAHI = 0; qint32 countOAI = 0; qint32 countCAI = 0; qint32 countAI = 0; qint32 countHI = 0; qint32 countRERA = 0; qint32 countSNI = 0; qint32 countBreath = 0; }; void UpdateEvents(EventType event_type, const QMap>& events, Session* session) { static QMap mapping {{EventType::ObstructiveApnea, CPAP_Obstructive}, {EventType::CentralApnea, CPAP_ClearAirway}, {EventType::Hypopnea, CPAP_Hypopnea}, {EventType::FlowLimitation, CPAP_FlowLimit}, {EventType::RERA, CPAP_RERA}, {EventType::PeriodicBreathing, CPAP_PB}, {EventType::Snore, CPAP_Snore}}; const auto it_events = events.find(event_type); const auto it_mapping = mapping.find(event_type); if (it_events == events.cend() || it_mapping == mapping.cend()) { return; } EventList* event_list = session->AddEventList(it_mapping.value(), EVL_Event); std::for_each(it_events.value().cbegin(), it_events.value().cend(), [&](const EventData& event_data){ event_list->AddEvent(event_data.date_time.toMSecsSinceEpoch() + kDateTimeOffset - timezoneOffset(), event_data.duration); }); } QString GetSessionFolder(const QString& dirpath, const QDate& session_date) { const auto year_month_folder = QString("%1%2").arg(session_date.year()).arg(session_date.month(),2,10,QLatin1Char('0')); const auto day_folder = QString("%1").arg(session_date.day(),2,10,QLatin1Char('0')) ; const auto session_folder_path = dirpath + QDir::separator() + kResventTherapyFolder + QDir::separator() + kResventRecordFolder + QDir::separator() + year_month_folder + QDir::separator() + day_folder; return session_folder_path; } bool VerifyEvent(EventData& eventData) { switch (eventData.type) { case EventType::FlowLimitation: case EventType::Hypopnea: case EventType::ObstructiveApnea: // OA case EventType::CentralApnea: // CA and same clear airway. // adjust time of event to be after the event ends rather than when the event starts. eventData.date_time = eventData.date_time.addMSecs(eventData.duration*1000); break; case EventType::RERA: eventData.duration = 0 ; // duration is large and suppress duration display of eariler OA events. break; case EventType::PeriodicBreathing: case EventType::Snore: default: break; } return true; } void LoadEvents(const QString& session_folder_path, Session* session, const UsageData& usage ) { const auto event_file_path = session_folder_path + QDir::separator() + "EV" + usage.number; // Oscar (resmed) plots events at end. QMap> events; QFile f(event_file_path); f.open(QIODevice::ReadOnly | QIODevice::Text); f.seek(4); while (!f.atEnd()) { QString line = f.readLine().trimmed(); // ID=20,DT=1692022874,DR=6,GD=0, #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) const auto elems = line.split(",", Qt::SkipEmptyParts); #else const auto elems = line.split(",", QString::SkipEmptyParts); #endif if (elems.size() != 4) { continue; } const auto event_type_elems = elems.at(0).split("="); const auto date_time_elems = elems.at(1).split("="); const auto duration_elems = elems.at(2).split("="); Q_ASSERT(event_type_elems.size() == 2); Q_ASSERT(date_time_elems.size() == 2); Q_ASSERT(duration_elems.size() == 2); auto event_type = static_cast(std::stoi(event_type_elems[1].toStdString())); auto duration = std::stoi(duration_elems[1].toStdString()); auto date_time = QDateTime::fromTime_t(std::stoi(date_time_elems[1].toStdString())); EventData eventData({event_type, date_time, duration}); VerifyEvent(eventData); events[event_type].push_back(eventData); } static QVector mapping {EventType::ObstructiveApnea, EventType::CentralApnea, EventType::Hypopnea, EventType::FlowLimitation, EventType::RERA, EventType::PeriodicBreathing, EventType::Snore}; std::for_each(mapping.cbegin(), mapping.cend(), [&](EventType event_type){ UpdateEvents(event_type, events, session); }); } template T read_from_file(QFile& f) { T data{}; f.read(reinterpret_cast(&data), sizeof(T)); return data; } struct WaveFileData { unsigned int wave_event_id; QString file_base_name; unsigned int sample_rate_offset; unsigned int start_offset; }; EventList* GetEventList(const QString& name, Session* session, float sample_rate = 0.0) { DEBUGFC Q(name); if (name == "Press") { return session->AddEventList(CPAP_Pressure, EVL_Event, kHundredthGain); } else if (name == "IPAP") { return session->AddEventList(CPAP_IPAP, EVL_Event, kHundredthGain); } else if (name == "EPAP") { return session->AddEventList(CPAP_EPAP, EVL_Event, kHundredthGain); } else if (name == "Leak") { return session->AddEventList(CPAP_Leak, EVL_Event , kTenthGain); } else if (name == "Vt") { return session->AddEventList(CPAP_TidalVolume, EVL_Event , kNoGain); } else if (name == "MV") { return session->AddEventList(CPAP_MinuteVent, EVL_Event , kHundredthGain); } else if (name == "RR") { return session->AddEventList(CPAP_RespRate, EVL_Event, kTenthGain); } else if (name == "Ti") { return session->AddEventList(CPAP_Ti, EVL_Event, kMilliGain); } else if (name == "I:E") { return session->AddEventList(CPAP_IE, EVL_Event, kMilliGain); } else if (name == "SpO2" || name == "PR") { // Not present return nullptr; } else if (name == "Pressure") { return session->AddEventList(CPAP_MaskPressure, EVL_Waveform, kDefaultGain, 0.0, 0.0, 0.0, 1000.0 / sample_rate); } else if (name == "Flow") { return session->AddEventList(CPAP_FlowRate, EVL_Waveform, kDefaultGain, 0.0, 0.0, 0.0, 1000.0 / sample_rate); } else { // Not supported Q_ASSERT(false); return nullptr; } } struct ChunkData { EventList* event_list; uint16_t samples_by_chunk; qint64 start_time; int total_samples_by_chunk; float sample_rate; #ifdef TEST_MACROS_ENABLED QString chunkName ; int chunkDebug = -1; #endif }; QString ReadDescriptionName(QFile& f) { constexpr int kNameSize = 9; QVector name(kNameSize); const auto readed = f.read(name.data(), kNameSize - 1); Q_ASSERT(readed == kNameSize - 1); return QString(name.data()); } void ReadWaveFormsHeaders(QFile& f, QVector& wave_forms, Session* session, const UsageData& usage) { f.seek(kChunkDurationInSecOffset); const auto chunk_duration_in_sec = read_from_file(f); f.seek(kDescriptionCountOffset); const auto description_count = read_from_file(f); wave_forms.resize(description_count); DEBUGFC Q(chunk_duration_in_sec) Q(description_count); for (unsigned int i = 0; i < description_count; i++) { const auto description_header_offset = kMainHeaderSize + i * kDescriptionHeaderSize; f.seek(description_header_offset); const auto name = ReadDescriptionName(f); f.seek(description_header_offset + kDescriptionSamplesByChunk); const auto samples_by_chunk = read_from_file(f); wave_forms[i].sample_rate = 1.0 * samples_by_chunk / chunk_duration_in_sec; wave_forms[i].event_list = GetEventList(name, session, wave_forms[i].sample_rate); wave_forms[i].samples_by_chunk = samples_by_chunk; wave_forms[i].start_time = usage.start_time.toMSecsSinceEpoch(); #ifdef TEST_MACROS_ENABLED wave_forms[i].chunkName = name; wave_forms[i].chunkDebug = -1; DEBUGNC O( wave_forms[i].chunkName) DATETIME(wave_forms[i].start_time) O(usage.number); //IF ((name == "I:E" || name == "Ti") && f.fileName().contains("P01_01")) //DEBUGFC Q( wave_forms[i].chunkName) O(f.fileName()) ; IF ( (name == "Leak") && f.fileName().contains("P01_01")) OO ({ wave_forms[i].chunkDebug = 1; DEBUGFC Q( wave_forms[i].chunkName) DATETIME(wave_forms[i].start_time) O("\n") O(f.fileName()) ; }) #endif } } void LoadOtherWaveForms(const QString& session_folder_path, Session* session, const UsageData& usage) { QDir session_folder(session_folder_path); const auto wave_files = session_folder.entryList(QStringList() << "P" + usage.number + "_*", QDir::Files, QDir::Name); QVector wave_forms; bool initialized = false; std::for_each(wave_files.cbegin(), wave_files.cend(), [&](const QString& wave_file){ // P01_ file DEBUGFC O("LoadOtherWaveForms") Q(usage.number); QFile f(session_folder_path + QDir::separator() + wave_file); f.open(QIODevice::ReadOnly); if (!initialized) { ReadWaveFormsHeaders(f, wave_forms, session, usage); initialized = true; } f.seek(kMainHeaderSize + wave_forms.size() * kDescriptionHeaderSize); std::vector chunk(std::max_element(wave_forms.cbegin(), wave_forms.cend(), [](const ChunkData& lhs, const ChunkData& rhs){ return lhs.samples_by_chunk < rhs.samples_by_chunk; })->samples_by_chunk); while (!f.atEnd()) { for (int i = 0; i < wave_forms.size(); i++) { const auto& wave_form = wave_forms[i].event_list; IF (wave_forms[i].chunkDebug>0) DEBUGFC O(wave_forms[i].chunkName) O(wave_form); const auto samples_by_chunk_actual = wave_forms[i].samples_by_chunk; auto& start_time_current = wave_forms[i].start_time; auto& total_samples_by_chunk = wave_forms[i].total_samples_by_chunk; const auto sample_rate = wave_forms[i].sample_rate; const auto duration = samples_by_chunk_actual * 1000.0 / sample_rate; const auto readed = f.read(reinterpret_cast(chunk.data()), chunk.size() * sizeof(qint16)); if (wave_form) { const auto readed_elements = readed / sizeof(qint16); IF (wave_forms[i].chunkDebug>0) DEBUGFC Q(readed_elements) Q(samples_by_chunk_actual); if (readed_elements != samples_by_chunk_actual) { std::fill(std::begin(chunk) + readed_elements, std::end(chunk), 0); } int offset = 0; std::for_each(chunk.cbegin(), chunk.cend(), [&](const qint16& value){ IF (wave_forms[i].chunkDebug>0 && value>0) DEBUGFC O(wave_forms[i].chunkName) DATETIME(start_time_current + offset + kDateTimeOffset - timezoneOffset()) O(value) Q(offset) Q(sample_rate); wave_form->AddEvent(start_time_current + offset + kDateTimeOffset - timezoneOffset(), value); offset += 1000.0 / sample_rate; }); } start_time_current += duration; total_samples_by_chunk += samples_by_chunk_actual; } } }); QVector chunk; for (int i = 0; i < wave_forms.size(); i++) { const auto& wave_form = wave_forms[i]; const auto expected_samples = usage.start_time.msecsTo(usage.end_time) / 1000.0 * wave_form.sample_rate; if (wave_form.total_samples_by_chunk < expected_samples) { chunk.resize(expected_samples - wave_form.total_samples_by_chunk); if (wave_form.event_list) { int offset = 0; std::for_each(chunk.cbegin(), chunk.cend(), [&](const qint16& value){ wave_form.event_list->AddEvent(wave_form.start_time + offset + kDateTimeOffset - timezoneOffset(), value ); IF (wave_forms[i].chunkDebug>0 && offset>=0 && value > 0) DEBUGFC O(wave_forms[i].chunkName) DATETIME(wave_form.start_time + offset + kDateTimeOffset - timezoneOffset()) O(value) //Q(sample_rate) ; offset += 1000.0 / wave_form.sample_rate; }); } } } } void LoadWaveForms(const QString& session_folder_path, Session* session, const UsageData& usage) { QDir session_folder(session_folder_path); const auto wave_files = session_folder.entryList(QStringList() << "W" + usage.number + "_*", QDir::Files, QDir::Name); QVector wave_forms; bool initialized = false; std::for_each(wave_files.cbegin(), wave_files.cend(), [&](const QString& wave_file){ // W01_ file QFile f(session_folder_path + QDir::separator() + wave_file); f.open(QIODevice::ReadOnly); if (!initialized) { ReadWaveFormsHeaders(f, wave_forms, session, usage); initialized = true; } f.seek(kMainHeaderSize + wave_forms.size() * kDescriptionHeaderSize); QVector chunk(std::max_element(wave_forms.cbegin(), wave_forms.cend(), [](const ChunkData& lhs, const ChunkData& rhs){ return lhs.samples_by_chunk < rhs.samples_by_chunk; })->samples_by_chunk); while (!f.atEnd()) { for (int i = 0; i < wave_forms.size(); i++) { const auto& wave_form = wave_forms[i].event_list; const auto samples_by_chunk_actual = wave_forms[i].samples_by_chunk; auto& start_time_current = wave_forms[i].start_time; auto& total_samples_by_chunk = wave_forms[i].total_samples_by_chunk; const auto sample_rate = wave_forms[i].sample_rate; //DEBUGFC O(wave_forms[i].chunkName) DATE(wave_forms[i].start_time) Q(usage.number); const auto duration = samples_by_chunk_actual * 1000.0 / sample_rate; const auto readed = f.read(reinterpret_cast(chunk.data()), chunk.size() * sizeof(qint16)); if (wave_form) { const auto readed_elements = readed / sizeof(qint16); if (readed_elements != samples_by_chunk_actual) { std::fill(std::begin(chunk) + readed_elements, std::end(chunk), 0); } wave_form->AddWaveform(start_time_current + kDateTimeOffset - timezoneOffset(), chunk.data(), samples_by_chunk_actual, duration); } start_time_current += duration; total_samples_by_chunk += samples_by_chunk_actual; } } }); QVector chunk; for (int i = 0; i < wave_forms.size(); i++) { const auto& wave_form = wave_forms[i]; const auto expected_samples = usage.start_time.msecsTo(usage.end_time) / 1000.0 * wave_form.sample_rate; if (wave_form.total_samples_by_chunk < expected_samples) { chunk.resize(expected_samples - wave_form.total_samples_by_chunk); if (wave_form.event_list) { const auto duration = chunk.size() * 1000.0 / wave_form.sample_rate; wave_form.event_list->AddWaveform(wave_form.start_time + kDateTimeOffset - timezoneOffset(), chunk.data(), chunk.size(), duration); } } } } void LoadStats(const UsageData& /*usage_data*/, Session* session ) { session->settings[CPAP_Mode] = MODE_APAP; session->settings[CPAP_PressureMin] = 4.0; // these value are hard coded for now. session->settings[CPAP_PressureMax] = 20.0; // these value are hard coded for now. } UsageData ReadUsage(const QString& session_folder_path, const QString& usage_number) { UsageData usage_data; usage_data.number = usage_number; const auto session_stat_path = session_folder_path + QDir::separator() + "STAT" + usage_number; if (!QFile::exists(session_stat_path)) { qDebug() << "Resvent Data card has no " << session_stat_path; return usage_data; } QFile f(session_stat_path); f.open(QIODevice::ReadOnly | QIODevice::Text); f.seek(4); while (!f.atEnd()) { QString line = f.readLine().trimmed(); const auto elems = line.split("="); Q_ASSERT(elems.size() == 2); if (elems[0] == "secStart") { usage_data.start_time = QDateTime::fromTime_t(std::stoi(elems[1].toStdString())); } else if (elems[0] == "secUsed") { usage_data.end_time = QDateTime::fromTime_t(usage_data.start_time.toTime_t() + std::stoi(elems[1].toStdString())); } else if (elems[0] == "cntAHI") { usage_data.countAHI = std::stoi(elems[1].toStdString()); } else if (elems[0] == "cntOAI") { usage_data.countOAI = std::stoi(elems[1].toStdString()); } else if (elems[0] == "cntCAI") { usage_data.countCAI = std::stoi(elems[1].toStdString()); } else if (elems[0] == "cntAI") { usage_data.countAI = std::stoi(elems[1].toStdString()); } else if (elems[0] == "cntHI") { usage_data.countHI = std::stoi(elems[1].toStdString()); } else if (elems[0] == "cntRERA") { usage_data.countRERA = std::stoi(elems[1].toStdString()); } else if (elems[0] == "cntSNI") { usage_data.countSNI = std::stoi(elems[1].toStdString()); } else if (elems[0] == "cntBreath") { usage_data.countBreath = std::stoi(elems[1].toStdString()); } } return usage_data; } QVector GetDifferentUsage(const QString& session_folder_path) { QDir session_folder(session_folder_path); const auto stat_files = session_folder.entryList(QStringList() << "STAT*", QDir::Files, QDir::Name); QVector usage_data; std::for_each(stat_files.cbegin(), stat_files.cend(), [&](const QString& stat_file){ if (stat_file.size() != 6) { return; } auto usageData = ReadUsage(session_folder_path, stat_file.right(2)); usage_data.push_back(usageData); }); return usage_data; } int LoadSession(const QString& dirpath, const QDate& session_date, Machine* machine) { // Handles one day - all OSCAR sessions for a day. const auto session_folder_path = GetSessionFolder(dirpath, session_date); const auto different_usage = GetDifferentUsage(session_folder_path); //return std::accumulate(different_usage.cbegin(), different_usage.cend(), 0, [&](int base, const UsageData& usage) // std::accumulate(different_usage.cbegin(), different_usage.cend(), 0, [&](int base, const UsageData& usage) int base = 0; for (auto usage : different_usage) { if (machine->SessionExists(usage.start_time.toMSecsSinceEpoch() + kDateTimeOffset - timezoneOffset())) { // session alreadt exists //return base; continue; } Session* session = new Session(machine, usage.start_time.toMSecsSinceEpoch() + kDateTimeOffset - timezoneOffset()); session->SetChanged(true); session->really_set_first(usage.start_time.toMSecsSinceEpoch() + kDateTimeOffset - timezoneOffset()); session->really_set_last(usage.end_time.toMSecsSinceEpoch() + kDateTimeOffset - timezoneOffset()); LoadStats(usage, session); LoadWaveForms(session_folder_path, session, usage); LoadOtherWaveForms(session_folder_path, session, usage); LoadEvents(session_folder_path, session, usage); session->UpdateSummaries(); session->Store(machine->getDataPath()); machine->AddSession(session); ++base; }; return base; } /////////////////////////////////////////////////////////////////////////////////////////// // Sorted EDF files that need processing into date records according to ResMed noon split /////////////////////////////////////////////////////////////////////////////////////////// int ResventLoader::Open(const QString & dirpath) { const auto machine_info = PeekInfo(dirpath); // Abort if no serial number if (machine_info.serial.isEmpty()) { qDebug() << "Resvent Data card has no valid serial number in " << kResventSysConfigFilename; return -1; } const auto sessions_date = GetSessionsDate(dirpath); int progress = 0; emit setProgressMax(1+sessions_date.size()); // add one to include Save in progress. emit setProgressValue(progress); QCoreApplication::processEvents(); Machine *machine = p_profile->CreateMachine(machine_info); int new_sessions = 0; // do for each day found. std::for_each(sessions_date.cbegin(), sessions_date.cend(), [&](const QDate& session_date){ new_sessions += LoadSession(dirpath, session_date, machine); emit setProgressValue(++progress); QCoreApplication::processEvents(); }); machine->Save(); emit setProgressValue(++progress); QCoreApplication::processEvents(); return new_sessions; } void ResventLoader::initChannels() { } ChannelID ResventLoader::PresReliefMode() { return 0; } ChannelID ResventLoader::PresReliefLevel() { return 0; } ChannelID ResventLoader::CPAPModeChannel() { return 0; } bool resvent_initialized = false; void ResventLoader::Register() { if (resvent_initialized) { return; } qDebug() << "Registering ResventLoader"; RegisterLoader(new ResventLoader()); resvent_initialized = true; }