/* SleepLib Session Implementation * This stuff contains the base calculation smarts * * Copyright (c) 2019-2022 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. */ #define TEST_MACROS_ENABLEDoff #include #include "session.h" #include "version.h" #include #include #include #include #include #include #include #include "SleepLib/calcs.h" #include "SleepLib/profiles.h" using namespace std; // This is the uber important database version for OSCAR's internal storage // Increment this after stuffing with Session's save & load code. const quint16 summary_version = 18; const quint16 events_version = 10; Session::Session(Machine *m, SessionID session) { s_lonesession = false; if (!session) { session = m->CreateSessionID(); } s_machine = m; s_machtype = m->type(); s_session = session; s_changed = false; s_events_loaded = false; s_summary_loaded = false; _first_session = true; s_enabled = true; s_first = s_last = 0; s_evchecksum_checked = false; s_noSettings = s_summaryOnly = false; destroyed = false; } Session::~Session() { TrashEvents(); destroyed = true; } void Session::TrashEvents() // Trash this sessions Events and release memory. { QVector::iterator j; QVector::iterator j_end; QHash >::iterator i; QHash >::iterator i_end=eventlist.end(); if (s_changed) { // Save first.. } for (i = eventlist.begin(); i != i_end; ++i) { j_end=i.value().end(); for (j = i.value().begin(); j != j_end; ++j) { EventList * ev = *j; ev->clear(); ev->m_data.squeeze(); ev->m_data2.squeeze(); ev->m_time.squeeze(); delete ev; } } s_events_loaded = false; eventlist.clear(); eventlist.squeeze(); } bool Session::enabled(bool realValues) const { if (p_profile->cpap->clinicalMode() && !realValues) return true; return s_enabled; } void Session::setEnabled(bool b) { s_enabled = b; // not so simple.. we have to invalidate the hours cache in the day record.. Day * day = p_profile->findSessionDay(this); if (day) { day->invalidate(); } } QString Session::eventFile() const { return s_machine->getEventsPath()+QString::asprintf("%08lx.001", s_session); } //const int max_pack_size=128; bool Session::OpenEvents() { if (s_events_loaded) { return true; } s_events_loaded = eventlist.size() > 0; if (s_events_loaded) { return true; } QString filename = eventFile(); #ifdef DEBUG_EVENTS qDebug() << "Loading" << s_machine->loaderName().toLocal8Bit().data() << "Events:" << filename.toLocal8Bit().data(); #endif bool b = LoadEvents(filename); if ( ! b) { qWarning() << "Error Loading Events" << filename; return false; } return s_events_loaded = true; } bool Session::Destroy() { QDir dir; QString base; base=QString::asprintf("%08lx", s_session); QString summaryfile = s_machine->getSummariesPath() + base + ".000"; QString eventfile = s_machine->getEventsPath() + base + ".001"; if ( ! dir.remove(summaryfile)) { qWarning() << "Could not delete" << summaryfile; } if ( ! dir.remove(eventfile)) { qWarning() << "Could not delete" << eventfile; } return s_machine->unlinkSession(this); } bool Session::Store(QString path) // Storing Session Data in our format // {DataDir}/{MachineID}/{SessionID}.{ext} { QDir dir(path); if ( ! dir.exists(path)) { dir.mkpath(path); } //qDebug() << "Storing Session: " << base; bool a; a = StoreSummary(); // if actually has events //qDebug() << " Summary done"; if (eventlist.size() > 0) { StoreEvents(); } else { // who cares.. //qDebug() << "Trying to save empty events file"; } //qDebug() << " Events done"; s_changed = false; s_events_loaded = true; //} else { // qDebug() << "Session::Store() No event data saved" << s_session; //} return a; } //QDataStream & operator<<(QDataStream & out, const Session & session) //{ // session.StoreSummaryData(out); // return out; //} // //void Session::StoreSummaryData(QDataStream & out) const //{ // out << summary_version; // out << (quint32)s_session; // out << s_first; // Session Start Time // out << s_last; // Duration of sesion in seconds. // // out << settings; // out << m_cnt; // out << m_sum; // out << m_avg; // out << m_wavg; // // out << m_min; // out << m_max; // // out << m_physmin; // out << m_physmax; // // out << m_cph; // out << m_sph; // // out << m_firstchan; // out << m_lastchan; // // out << m_valuesummary; // out << m_timesummary; // // out << m_gain; // // out << m_availableChannels; // // out << m_timeAboveTheshold; // out << m_upperThreshold; // out << m_timeBelowTheshold; // out << m_lowerThreshold; // // out << s_summaryOnly; //} // //QDataStream & operator>>(QDataStream & in, Session & session) //{ // session.LoadSummaryData(in); // return in; //} // // //void Session::LoadSummaryData(QDataStream & in) //{ // quint16 version; // in >> version; // // quint32 t32; // in >> t32; // Sessionid; // s_session = t32; // // in >> s_first; // Start time // in >> s_last; // Duration // // in >> settings; // in >> m_cnt; // in >> m_sum; // in >> m_avg; // in >> m_wavg; // // in >> m_min; // in >> m_max; // // in >> m_physmin; // in >> m_physmax; // // in >> m_cph; // in >> m_sph; // in >> m_firstchan; // in >> m_lastchan; // // in >> m_valuesummary; // in >> m_timesummary; // // in >> m_gain; // // in >> m_availableChannels; // in >> m_timeAboveTheshold; // in >> m_upperThreshold; // in >> m_timeBelowTheshold; // in >> m_lowerThreshold; // // in >> s_summaryOnly; // // s_enabled = 1; //} QDataStream & operator>>(QDataStream & in, SessionSlice & slice) { in >> slice.start; quint32 length; in >> length; slice.end = slice.start + length; quint16 i; in >> i; slice.status = (SliceStatus)i; return in; } QDataStream & operator<<(QDataStream & out, const SessionSlice & slice) { out << slice.start; quint32 length = slice.end - slice.start; out << length; out << (quint16)slice.status; return out; } bool Session::StoreSummary() { if (s_first == 0) { qWarning() << "Session::StoreSummary discarding session" << s_session << "["+QDateTime::fromTime_t(s_session).toString("MMM dd, yyyy hh:mm:ss")+"]" << "from machine" << machine()->serial() << "with first=0"; return false; } QString filename = s_machine->getSummariesPath() + QString::asprintf("%08lx.000", s_session) ; QFile file(filename); if (!file.open(QIODevice::WriteOnly)) { QDir dir; dir.mkpath(s_machine->getSummariesPath()); if (!file.open(QIODevice::WriteOnly)) { // qWarning() << "Summary open for writing failed" << "error code" << file.error() << file.errorString(); qWarning() << "Could not open summary" << filename << "for writing, error code" << file.error() << file.errorString(); return false; } } QDataStream out(&file); out.setVersion(QDataStream::Qt_4_6); out.setByteOrder(QDataStream::LittleEndian); out << (quint32)magic; out << (quint16)summary_version; out << (quint16)filetype_summary; out << (quint32)s_machine->id(); out << (quint32)s_session; out << s_first; // Session Start Time out << s_last; // Duration of sesion in seconds. //out << (quint16)settings.size(); out << settings; out << m_cnt; out << m_sum; out << m_avg; out << m_wavg; out << m_min; out << m_max; out << m_physmin; out << m_physmax; out << m_cph; out << m_sph; out << m_firstchan; out << m_lastchan; // <- 8 out << m_valuesummary; out << m_timesummary; // 8 -> // <- 9 out << m_gain; // 9 -> // <- 15 out << m_availableChannels; out << m_timeAboveTheshold; out << m_upperThreshold; out << m_timeBelowTheshold; out << m_lowerThreshold; out << s_summaryOnly; // 13 -> out << s_noSettings; // 18 out << m_slices; file.close(); return true; } bool Session::LoadSummary() { // static int sumcnt = 0; if (s_summary_loaded) return true; QString filename = s_machine->getSummariesPath() + QString::asprintf("%08lx.000", s_session); if (filename.isEmpty()) { qDebug() << "Empty summary filename"; return false; } QFile file(filename); if (!file.open(QIODevice::ReadOnly)) { qWarning() << "Could not open summary file" << filename << "for reading, error code" << file.error() << file.errorString(); return false; } // qDebug() << "Loading" << s_machine->loaderName() << "Summary" << filename << sumcnt++; QDataStream in(&file); in.setVersion(QDataStream::Qt_4_6); in.setByteOrder(QDataStream::LittleEndian); quint32 t32; quint16 t16; //QHash mctype; //QVector mcorder; in >> t32; if (t32 != magic) { qDebug() << "Wrong magic number in " << filename; return false; } quint16 version; in >> version; // DB Version if (version < 6) { //throw OldDBVersion(); qWarning() << "Old dbversion " << version << "summary file.. Sorry, you need to purge and reimport"; return false; } in >> t16; // File Type if (t16 != filetype_summary) { qDebug() << "Wrong file type"; //wrong file type return false; } qint32 ts32; in >> ts32; // MachineID (dont need this result) bool upgrade = false; if (ts32 != s_machine->id()) { upgrade = true; qWarning() << "Machine ID does not match in" << filename << " I will try to load anyway in case you know what your doing."; } in >> t32; // Sessionid; s_session = t32; in >> s_first; // Start time in >> s_last; // Duration // (16bit==Limited to 18 hours) QHash cruft; if (version < 7) { // This code is deprecated.. just here incase anyone tries anything crazy... QHash v1; in >> v1; settings.clear(); ChannelID code; for (QHash::iterator i = v1.begin(); i != v1.end(); i++) { code = schema::channel[i.key()].id(); settings[code] = i.value(); } QHash zcnt; in >> zcnt; for (QHash::iterator i = zcnt.begin(); i != zcnt.end(); i++) { code = schema::channel[i.key()].id(); m_cnt[code] = i.value(); } QHash zsum; in >> zsum; for (QHash::iterator i = zsum.begin(); i != zsum.end(); i++) { code = schema::channel[i.key()].id(); m_sum[code] = i.value(); } QHash ztmp; in >> ztmp; // avg for (QHash::iterator i = ztmp.begin(); i != ztmp.end(); i++) { code = schema::channel[i.key()].id(); m_avg[code] = i.value(); } ztmp.clear(); in >> ztmp; // wavg for (QHash::iterator i = ztmp.begin(); i != ztmp.end(); i++) { code = schema::channel[i.key()].id(); m_wavg[code] = i.value(); } ztmp.clear(); in >> ztmp; // 90p ztmp.clear(); in >> ztmp; // min for (QHash::iterator i = ztmp.begin(); i != ztmp.end(); i++) { code = schema::channel[i.key()].id(); m_min[code] = i.value(); } ztmp.clear(); in >> ztmp; // max for (QHash::iterator i = ztmp.begin(); i != ztmp.end(); i++) { code = schema::channel[i.key()].id(); m_max[code] = i.value(); } ztmp.clear(); in >> ztmp; // cph for (QHash::iterator i = ztmp.begin(); i != ztmp.end(); i++) { code = schema::channel[i.key()].id(); m_cph[code] = i.value(); } ztmp.clear(); in >> ztmp; // sph for (QHash::iterator i = ztmp.begin(); i != ztmp.end(); i++) { code = schema::channel[i.key()].id(); m_sph[code] = i.value(); } QHash ztim; in >> ztim; //firstchan for (QHash::iterator i = ztim.begin(); i != ztim.end(); i++) { code = schema::channel[i.key()].id(); m_firstchan[code] = i.value(); } ztim.clear(); in >> ztim; // lastchan for (QHash::iterator i = ztim.begin(); i != ztim.end(); i++) { code = schema::channel[i.key()].id(); m_lastchan[code] = i.value(); } //SetChanged(true); } else { // version > 7 in >> settings; if (version < 13) { QHash cnt2; in >> cnt2; QHash::iterator it; for (it = cnt2.begin(); it != cnt2.end(); ++it) { m_cnt[it.key()] = it.value(); } } else { in >> m_cnt; } in >> m_sum; in >> m_avg; in >> m_wavg; if (version < 11) { cruft.clear(); in >> cruft; // 90% if (version >= 10) { cruft.clear(); in >> cruft;// med cruft.clear(); in >> cruft; //p95 } } in >> m_min; in >> m_max; // Added 24/10/2013 by MW to support physical graph min/max values if (version >= 12) { in >> m_physmin; in >> m_physmax; } in >> m_cph; in >> m_sph; in >> m_firstchan; in >> m_lastchan; if (version >= 8) { in >> m_valuesummary; in >> m_timesummary; if (version >= 9) { in >> m_gain; } } // screwed up with version 14 if (version >= 15) { in >> m_availableChannels; in >> m_timeAboveTheshold; in >> m_upperThreshold; in >> m_timeBelowTheshold; in >> m_lowerThreshold; } // else this is ugly.. forced device database upgrade will solve it though. if (version == 13) { QHash::iterator it = settings.find(CPAP_SummaryOnly); if (it != settings.end()) { s_summaryOnly = (*it).toBool(); } else s_summaryOnly = false; } else if (version > 13) { in >> s_summaryOnly; } if (version >= 18) { in >> s_noSettings; // qDebug() << "Session::LoadSummary" << s_session << "[" // << QDateTime::fromTime_t(s_session).toString("MM/dd/yyyy hh:mm:ss") // << "] s_noSettings" << s_noSettings << "size" << settings.size(); } else { s_noSettings = (settings.size() == 0); } if (version == 16) { QList slices; in >> slices; m_slices.clear(); for (int i=0;i= 17) { in >> m_slices; } } // not really a good idea to do this... should flag and do a reindex if (upgrade || (version < summary_version)) { qDebug() << "Upgrading Summary file to version" << summary_version; if (!s_summaryOnly) { OpenEvents(); UpdateSummaries(); TrashEvents(); } else { // summary only upgrades go here. } StoreSummary(); } s_summary_loaded = true; return true; } const quint16 compress_method = 1; bool Session::StoreEvents() { QString path = s_machine->getEventsPath(); QDir dir; dir.mkpath(path); QString filename = path+ QString::asprintf("%08lx.001", s_session) ; QFile file(filename); if (!file.open(QIODevice::WriteOnly)) { qWarning() << "Could not open events file" << filename << "for writing, error code" << file.error() << file.errorString(); return false; } QByteArray headerbytes; QDataStream header(&headerbytes, QIODevice::WriteOnly); header.setVersion(QDataStream::Qt_4_6); header.setByteOrder(QDataStream::LittleEndian); header << (quint32)magic; // New Magic Number header << (quint16)events_version; // File Version header << (quint16)filetype_data; // File type 1 == Event header << (quint32)s_machine->id();// Device Type header << (quint32)s_session; // This session's ID header << s_first; header << s_last; quint16 compress = 0; if (p_profile->session->compressSessionData()) { compress = compress_method; } header << (quint16)compress; header << (quint16)s_machine->type();// Device Type QByteArray databytes; QDataStream out(&databytes, QIODevice::WriteOnly); out.setVersion(QDataStream::Qt_4_6); out.setByteOrder(QDataStream::LittleEndian); out << (qint16)eventlist.size(); // Number of event categories QHash >::iterator i; QHash >::iterator i_end=eventlist.end(); qint16 ev_size; for (i = eventlist.begin(); i != i_end; i++) { ev_size=i.value().size(); out << i.key(); // ChannelID out << (qint16)ev_size; for (int j = 0; j < ev_size; j++) { EventList &e = *i.value()[j]; out << e.first(); out << e.last(); out << (qint32)e.count(); out << (qint8)e.type(); out << e.rate(); out << e.gain(); out << e.offset(); out << e.Min(); out << e.Max(); out << e.dimension(); out << e.hasSecondField(); if (e.hasSecondField()) { out << e.min2(); out << e.max2(); } } } for (i = eventlist.begin(); i != i_end; i++) { ev_size=i.value().size(); for (int j = 0; j < ev_size; j++) { EventList &e = *i.value()[j]; // ****** This is assuming little endian ****** // Store the raw event list data in EventStoreType (16bit short) EventStoreType *ptr = e.m_data.data(); out.writeRawData((char *)ptr, e.count() << 1); //*** Don't delete these comments *** // for (quint32 c=0;c 0) { data = qCompress(databytes); } else { data = databytes; } file.write(headerbytes); file.write(data); file.close(); return true; } bool Session::LoadEvents(QString filename) { quint32 magicnum, machid, sessid; quint16 version, type, crc16, machtype, compmethod; quint8 t8; qint32 datasize; if (filename.isEmpty()) { qDebug() << "Session::LoadEvents() Filename is empty"; return false; } QFile file(filename); if ( ! file.open(QIODevice::ReadOnly)) { // qDebug() << "No Event/Waveform data available for" << s_session; qWarning() << "No Event/Waveform data available for" << s_session << "filename" << filename << "error code" << file.error() << file.errorString(); return false; } QByteArray headerbytes = file.read(42); QDataStream header(headerbytes); header.setVersion(QDataStream::Qt_4_6); header.setByteOrder(QDataStream::LittleEndian); header >> magicnum; // Magic Number (quint32) header >> version; // Version (quint16) header >> type; // File type (quint16) header >> machid; // Device ID (quint32) header >> sessid; //(quint32) header >> s_first; //(qint64) header >> s_last; //(qint64) #ifdef DEBUG_EVENTS qDebug() << "Session ID" << sessid << "Start Time" << QDateTime::fromMSecsSinceEpoch(s_first); #endif if (type != filetype_data) { qDebug() << "Wrong File Type in " << filename; return false; } if (magicnum != magic) { qWarning() << "Wrong Magic number in " << filename; return false; } if (version < 6) { // prior to version 6 is too old to deal with qDebug() << "Old File Version, can't open file"; return false; } if (version < 10) { file.seek(32); } else { header >> compmethod; // Compression Method (quint16) header >> machtype; // Device Type (quint16) header >> datasize; // Size of Uncompressed Data (quint32) header >> crc16; // CRC16 of Uncompressed Data (quint16) } QByteArray databytes, temp = file.readAll(); file.close(); if (version >= 10) { if (compmethod > 0) { databytes = qUncompress(temp); if (!s_evchecksum_checked) { if (databytes.size() != datasize) { qDebug() << "File" << filename << "has returned wrong datasize"; return false; } quint16 crc = qChecksum(databytes.data(), databytes.size()); if (crc != crc16) { qDebug() << "CRC Doesn't match in" << filename; return false; } s_evchecksum_checked = true; } } else { databytes = temp; } } else { databytes = temp; } QDataStream in(databytes); in.setVersion(QDataStream::Qt_4_6); in.setByteOrder(QDataStream::LittleEndian); qint16 mcsize; in >> mcsize; // number of Device Code lists #ifdef DEBUG_EVENTS qDebug() << "Number of Channels" << mcsize; #endif ChannelID code; qint64 ts1, ts2; qint32 evcount; EventListType elt; EventDataType rate, gain, offset, mn, mx; qint16 size2; QVector mcorder; QVector sizevec; QString dim; for (int i = 0; i < mcsize; i++) { if (version < 8) { QString txt; in >> txt; code = schema::channel[txt].id(); } else { in >> code; } mcorder.push_back(code); in >> size2; sizevec.push_back(size2); #ifdef DEBUG_EVENTS qDebug() << "For Channel (hex)" << QString::number(code, 16) << "there are" << size2 << "EventLists"; #endif for (int j = 0; j < size2; j++) { in >> ts1; in >> ts2; #ifdef DEBUG_EVENTS qDebug() << "Start:" << QDateTime::fromMSecsSinceEpoch(ts1).toString() << "Finish:" << QDateTime::fromMSecsSinceEpoch(ts2).toString(); #endif in >> evcount; in >> t8; elt = (EventListType)t8; in >> rate; in >> gain; in >> offset; in >> mn; in >> mx; in >> dim; bool second_field = false; if (version >= 7) { // version 7 added this field in >> second_field; } EventList *elist = AddEventList(code, elt, gain, offset, mn, mx, rate, second_field); elist->setDimension(dim); //eventlist[code].push_back(elist); elist->m_count = evcount; elist->m_first = ts1; elist->m_last = ts2; if (second_field) { EventDataType min, max; in >> min; in >> max; elist->setMin2(min); elist->setMax2(max); } } } //EventStoreType t; // qint16 //quint32 x; for (int i = 0; i < mcsize; i++) { code = mcorder[i]; size2 = sizevec[i]; for (int j = 0; j < size2; j++) { EventList &evec = *eventlist[code][j]; evec.m_data.resize(evec.m_count); EventStoreType *ptr = evec.m_data.data(); // ****** This is assuming little endian ****** in.readRawData((char *)ptr, evec.m_count << 1); //*** Don't delete these comments *** //*** They explain what the above ReadRawData is doing! // for (quint32 c=0;c> t; // *ptr++=t; // } if (evec.hasSecondField()) { evec.m_data2.resize(evec.m_count); ptr = evec.m_data2.data(); in.readRawData((char *)ptr, evec.m_count << 1); //*** Don't delete these comments *** //*** They explain what the above ReadRawData is doing! // for (quint32 c=0;c> t; // *ptr++=t; // } } if (evec.type() != EVL_Waveform) { evec.m_time.resize(evec.m_count); quint32 *tptr = evec.m_time.data(); in.readRawData((char *)tptr, evec.m_count << 2); //*** Don't delete these comments *** // for (quint32 c=0;c> x; // *tptr++=x; // } } } } if (version < events_version) { qDebug() << "Upgrading Events file" << filename << "to version" << events_version; UpdateSummaries(); StoreEvents(); } return true; } void Session::destroyEvent(ChannelID code) { QHash >::iterator it = eventlist.find(code); if (it != eventlist.end()) { for (int i = 0; i < it.value().size(); i++) { delete it.value()[i]; } eventlist.erase(it); } m_gain.erase(m_gain.find(code)); m_firstchan.erase(m_firstchan.find(code)); m_lastchan.erase(m_lastchan.find(code)); m_sph.erase(m_sph.find(code)); m_cph.erase(m_cph.find(code)); m_min.erase(m_min.find(code)); m_max.erase(m_max.find(code)); m_avg.erase(m_avg.find(code)); m_wavg.erase(m_wavg.find(code)); m_sum.erase(m_sum.find(code)); m_cnt.erase(m_cnt.find(code)); m_valuesummary.erase(m_valuesummary.find(code)); m_timesummary.erase(m_timesummary.find(code)); // does not trash settings.. } // TODO: The below assumes values are held for their duration. This does not properly handle // CPAP_PressureSet/EPAPSet/IPAPSet or other interpolated channels. The proper "value" held // for any given duration is the average of the starding and ending values, for the duration // between them. void Session::updateCountSummary(ChannelID code) { QHash >::iterator ev = eventlist.find(code); if (ev == eventlist.end()) { qDebug() << "No events for channel (hex)" << QString::number(code, 16); return; } QHash >::iterator vs = m_valuesummary.find(code); if (vs != m_valuesummary.end()) { // already calculated? return; } QHash valsum; QHash timesum; QHash::iterator it; QHash::iterator valsum_end; EventDataType raw, lastraw = 0; qint64 start, time, lasttime = 0; qint32 len, cnt; quint32 *tptr; EventStoreType *dptr, * eptr; int ev_size=ev.value().size(); for (int i = 0; i < ev_size; i++) { EventList &e = *(ev.value()[i]); start = e.first(); cnt = e.count(); dptr = e.rawData(); eptr = dptr + cnt; EventDataType rate = 0; m_gain[code] = e.gain(); if (e.type() == EVL_Event) { lastraw = *dptr++; tptr = e.rawTime(); lasttime = start + *tptr++; // Event version for (; dptr < eptr; dptr++) { time = start + *tptr++; raw = *dptr; valsum[raw]++; // elapsed time in seconds since last event occurred len = (time - lasttime) / 1000L; timesum[lastraw] += len; lastraw = raw; lasttime = time; } } else { // Waveform version, first just count for (; dptr < eptr; dptr++) { raw = *dptr; valsum[raw]++; } // Then process the list of values, time is simply (rate * count) rate = e.rate(); EventDataType t; QHash::iterator it = valsum.begin(); QHash::iterator valsum_end = valsum.end(); for (; it != valsum_end; ++it) { t = EventDataType(it.value()) * rate; timesum[it.key()] += t; } } } if ( valsum.size() == 0) { // no value summary for this channel using namespace schema; Channel *ch_p = channel.channels[code]; if ( ! ch_p->isNull() ) { // the channel was found in the channel list if ( ((ch_p->type() & (FLAG|SPAN|MINOR_FLAG)) == 0) ) { // the channel is not a flag or span type qDebug() << "No valuesummary for channel " << ch_p->label(); // so tell about missing summary } } else { // This channel wasn't added to the channel list, so we can't check its type qDebug() << "No valuesummary for channel (hex)" << QString::number(code, 16); } return; } m_valuesummary[code] = valsum; m_timesummary[code] = timesum; } void Session::UpdateSummaries() { ChannelID id; // Generate that AHI per hour graph in daily view. calcAHIGraph(this); // Calculates RespRate and related waveforms (Tv, MV, Te, Ti) if missing calcRespRate(this); // Generate unintentional leaks if not present calcLeaks(this); // Flag the Large Leaks if unintentional leaks is available, and no LargeLeaks weren't flagged by the device already. flagLargeLeaks(this); calcSPO2Drop(this); calcPulseChange(this); QHash >::iterator c = eventlist.begin(); QHash >::iterator ev_end = eventlist.end(); m_availableChannels.clear(); for (; c != ev_end; c++) { id = c.key(); m_availableChannels.push_back(id); schema::ChanType ctype = schema::channel[id].type(); if (ctype != schema::SETTING) { //sum(id); // avg calculates this and cnt. if (c.value().size() > 0) { EventList *el = c.value()[0]; EventDataType gain = el->gain(); m_gain[id] = gain; } if (!((id == CPAP_FlowRate) || (id == CPAP_MaskPressureHi) || (id == CPAP_RespEvent) || (id == CPAP_MaskPressure))) { updateCountSummary(id); } Min(id); Max(id); count(id); last(id); first(id); if (((id == CPAP_FlowRate) || (id == CPAP_MaskPressureHi) || (id == CPAP_RespEvent) || (id == CPAP_MaskPressure))) { continue; } cph(id); sph(id); avg(id); wavg(id); } } timeAboveThreshold(CPAP_Leak, p_profile->cpap->leakRedline()); s_machine->updateChannels(this); } EventDataType Session::SearchValue(ChannelID code, qint64 time, bool square) { qint64 drift = qint64(p_profile->cpap->clockDrift()) * 1000L; // Address clock drift for CPAP so correct value is displayed if (s_machine->type() == MT_CPAP) { time -= drift; } qint64 t1, t2, start; QHash >::iterator it; it = eventlist.find(code); quint32 *tptr; int cnt; EventDataType a,b,c,d,e; if (it != eventlist.end()) { int el_size=it.value().size(); for (int i = 0; i < el_size; i++) { EventList *el = it.value()[i]; if ((time >= el->first()) && (time < el->last())) { cnt = el->count(); if (el->type() == EVL_Waveform) { qint64 tt = time - el->first(); double i = tt / el->rate(); if (i> cnt) { qWarning() << "Session" << session() << "time bounds are broken.. There is a fault in the" << machine()->loaderName().toLocal8Bit().data() << "loader"; return 0; } int i1 = int(floor(i)); int i2 = int(ceil(i)); a = el->data(i1); // Don't interpolate if next data point is past end or on exact data point if (i2 >= cnt || i1 == i2) { return a; } qint64 t1 = i1 * el->rate(); qint64 t2 = i2 * el->rate(); c = EventDataType(t2 - t1); // Don't interpolate if t2-t1==0 (should be caught by i1==i2 above) if (c == 0) return a; d = EventDataType(t2 - tt); e = d/c; b = el->data(i2); return b + ((a-b) * e); } else { start = el->first(); tptr = el->rawTime(); // TODO: square plots need fixing if (square) { for (int j = 0; j < cnt-1; ++j) { tptr++; t2 = start + *tptr; if (t2 > time) { return el->data(j); } } } else { for (int j = 0; j < cnt-1; ++j) { tptr++; t2 = start + *tptr; if (t2 > time) { tptr--; t1 = start + *tptr; c = EventDataType(t2 - t1); d = EventDataType(t2 - time); e = d/c; a = el->data(j); b = el->data(j+1); if (a == b) { return a; } else { return b + ((a-b) * e); } } } } } } } } return 0; } QString Session::dimension(ChannelID id) { // Cheat for now return schema::channel[id].units(); } EventDataType Session::Min(ChannelID id) { QHash::iterator i = m_min.find(id); if (i != m_min.end()) { return i.value(); } QHash >::iterator j = eventlist.find(id); if (j == eventlist.end()) { m_min[id] = 0; return 0; } QVector &evec = j.value(); bool first = true; EventDataType min = 0, t1; int evec_size = evec.size(); for (int i = 0; i < evec_size; ++i) { if (evec[i]->count() != 0) { t1 = evec[i]->Min(); if ((t1 == 0) && (t1 == evec[i]->Max())) { continue; } if (first) { min = t1; first = false; } else { if (min > t1) { min = t1; } } } } m_min[id] = min; return min; } EventDataType Session::Max(ChannelID id) { QHash::iterator i = m_max.find(id); if (i != m_max.end()) { return i.value(); } QHash >::iterator j = eventlist.find(id); if (j == eventlist.end()) { m_max[id] = 0; return 0; } QVector &evec = j.value(); bool first = true; EventDataType max = 0, t1; int evec_size=evec.size(); for (int i = 0; i < evec_size; ++i) { if (evec.at(i)->count() != 0) { t1 = evec.at(i)->Max(); if (t1 == 0 && t1 == evec.at(i)->Min()) { continue; } if (first) { max = t1; first = false; } else { if (max < t1) { max = t1; } } } } m_max[id] = max; return max; } //// EventDataType Session::physMin(ChannelID id) { QHash::iterator i = m_physmin.find(id); if (i != m_physmin.end()) { return i.value(); } QHash >::iterator j = eventlist.find(id); if (j == eventlist.end()) { m_physmin[id] = 0; return 0; } EventDataType min = floor(Min(id)); m_physmin[id] = min; return min; } EventDataType Session::physMax(ChannelID id) { QHash::iterator i = m_physmax.find(id); if (i != m_physmax.end()) { return i.value(); } QHash >::iterator j = eventlist.find(id); if (j == eventlist.end()) { m_physmax[id] = 0; return 0; } EventDataType max = ceil(Max(id) + 0.5); m_physmax[id] = max; return max; } qint64 Session::first(ChannelID id) { qint64 drift = qint64(p_profile->cpap->clockDrift()) * 1000L; qint64 tmp; QHash::iterator i = m_firstchan.find(id); if (i != m_firstchan.end()) { tmp = i.value(); if (s_machine->type() == MT_CPAP) { tmp += drift; } return tmp; } QHash >::iterator j = eventlist.find(id); if (j == eventlist.end()) { return 0; } QVector &evec = j.value(); bool first = true; qint64 min = 0, t1; int evec_size=evec.size(); for (int i = 0; i < evec_size; ++i) { t1 = evec[i]->first(); if (first) { min = t1; first = false; } else { if (min > t1) { min = t1; } } } m_firstchan[id] = min; if (s_machine->type() == MT_CPAP) { min += drift; } return min; } qint64 Session::last(ChannelID id) { qint64 drift = qint64(p_profile->cpap->clockDrift()) * 1000L; qint64 tmp; QHash::iterator i = m_lastchan.find(id); if (i != m_lastchan.end()) { tmp = i.value(); if (s_machine->type() == MT_CPAP) { tmp += drift; } return tmp; } QHash >::iterator j = eventlist.find(id); if (j == eventlist.end()) { return 0; } QVector &evec = j.value(); bool first = true; qint64 max = 0, t1; int evec_size=evec.size(); for (int i = 0; i < evec_size; ++i) { t1 = evec[i]->last(); if (first) { max = t1; first = false; } else { if (max < t1) { max = t1; } } } m_lastchan[id] = max; if (s_machine->type() == MT_CPAP) { max += drift; } return max; } bool Session::channelDataExists(ChannelID id) { if (s_events_loaded) { QHash >::iterator j = eventlist.find(id); if (j == eventlist.end()) { // eventlist not loaded. return false; } return true; } else { qDebug() << "Calling channelDataExists without open eventdata!"; } return false; } bool Session::channelExists(ChannelID id) { if ( ! enabled()) { return false; } if (s_events_loaded) { QHash >::iterator j = eventlist.find(id); if (j == eventlist.end()) { // eventlist not loaded. return false; } } else { QHash::iterator q = m_cnt.find(id); if (q == m_cnt.end()) { return false; } if (q.value() == 0) { return false; } } return true; } EventDataType Session::countInsideSpan(ChannelID span, ChannelID code) { // TODO: Cache me! QHash >::iterator j = eventlist.find(span); if (j == eventlist.end()) { return 0; } QVector &evec = j.value(); qint64 t1,t2; int evec_size=evec.size(); QList start; QList end; // Simplify the span flags to start and end times list for (int el = 0; el < evec_size; ++el) { EventList &ev = *evec[el]; for (quint32 i=0; i < ev.count(); ++i) { end.push_back(t2=ev.time(i)); start.push_back(t2 - (qint64(ev.data(i)) * 1000L)); } } j = eventlist.find(code); if (j == eventlist.end()) { return 0; } QVector &evec2 = j.value(); evec_size=evec2.size(); int count = 0; int spans = start.size(); for (int el = 0; el < evec_size; ++el) { EventList &ev = *evec2[el]; for (quint32 i=0; i < ev.count(); ++i) { t1 = ev.time(i); for (int z=0; z < spans; ++z) { if ((t1 >= start.at(z)) && (t1 <= end.at(z))) { count++; break; } } } } return count; } EventDataType Session::rangeCount(ChannelID id, qint64 first, qint64 last) { int total = 0, cnt; if (id == AllAhiChannels) { for (int i = 0; i < ahiChannels.size(); i++) total += rangeCount(ahiChannels.at(i), first, last); return (EventDataType)total; } QHash >::iterator j = eventlist.find(id); if (j == eventlist.end()) { return 0; } QVector &evec = j.value(); qint64 t, start; int evec_size=evec.size(); for (int i = 0; i < evec_size; ++i) { EventList &ev = *evec[i]; if ((ev.last() < first) || (ev.first() > last)) { continue; } if (ev.type() == EVL_Waveform) { qint64 et = last; if (et > ev.last()) { et = ev.last(); } qint64 st = first; if (st < ev.first()) { st = ev.first(); } t = (et - st) / ev.rate(); total += t; } else { cnt = ev.count(); start = ev.first(); quint32 *tptr = ev.rawTime(); quint32 *eptr = tptr + cnt; for (; tptr < eptr; tptr++) { t = start + *tptr; if (t >= first) { if (t <= last) { total++; } else { break; } } } } } return (EventDataType)total; } double Session::rangeSum(ChannelID id, qint64 first, qint64 last) { QHash >::iterator j = eventlist.find(id); if (j == eventlist.end()) { return 0; } QVector &evec = j.value(); double sum = 0, gain; qint64 t, start; EventStoreType *dptr, * eptr; quint32 *tptr; int cnt, idx = 0; qint64 rate; int evec_size=evec.size(); for (int i = 0; i < evec_size; i++) { EventList &ev = *evec[i]; if ((ev.last() < first) || (ev.first() > last)) { continue; } start = ev.first(); dptr = ev.rawData(); cnt = ev.count(); eptr = dptr + cnt; gain = ev.gain(); rate = ev.rate(); if (ev.type() == EVL_Waveform) { if (first > ev.first()) { // Skip the samples before first idx = (first - ev.first()) / rate; } dptr += idx; //???? foggy. t = start; for (; dptr < eptr; dptr++) { //int j=idx;j= first) { if (t <= last) { sum += EventDataType(*dptr) * gain; } else { break; } } } } } return sum; } EventDataType Session::rangeMin(ChannelID id, qint64 first, qint64 last) { QHash >::iterator j = eventlist.find(id); if (j == eventlist.end()) { return 0; } QVector &evec = j.value(); EventDataType gain, v, min = std::numeric_limits::max(); qint64 t, start, rate; EventStoreType *dptr, * eptr; quint32 *tptr; int cnt, idx; int evec_size=evec.size(); for (int i = 0; i < evec_size; ++i) { EventList &ev = *evec[i]; if ((ev.last() < first) || (ev.first() > last)) { continue; } dptr = ev.rawData(); start = ev.first(); cnt = ev.count(); eptr = dptr + cnt; gain = ev.gain(); if (ev.type() == EVL_Waveform) { rate = ev.rate(); t = start; idx = 0; if (first > ev.first()) { // Skip the samples before first idx = (first - ev.first()) / rate; } dptr += idx; for (; dptr < eptr; dptr++) { //int j=idx;j= first) { if (t <= last) { v = EventDataType(*dptr) * gain; if (v < min) { min = v; } } else { break; } } } } } return min; } EventDataType Session::rangeMax(ChannelID id, qint64 first, qint64 last) { QHash >::iterator j = eventlist.find(id); if (j == eventlist.end()) { return 0; } QVector &evec = j.value(); EventDataType gain, v, max = std::numeric_limits::min(); qint64 t, start, rate; EventStoreType *dptr, * eptr; quint32 *tptr; int cnt, idx; int evec_size=evec.size(); for (int i = 0; i < evec_size; i++) { EventList &ev = *evec[i]; if ((ev.last() < first) || (ev.first() > last)) { continue; } start = ev.first(); dptr = ev.rawData(); cnt = ev.count(); eptr = dptr + cnt; gain = ev.gain(); if (ev.type() == EVL_Waveform) { rate = ev.rate(); t = start; idx = 0; if (first > ev.first()) { // Skip the samples before first idx = (first - ev.first()) / rate; } dptr += idx; for (; dptr < eptr; dptr++) { //int j=idx;j max) { max = v; } } else { break; } t += rate; } } else { tptr = ev.rawTime(); for (; dptr < eptr; dptr++) { t = start + *tptr++; if (t >= first) { if (t <= last) { v = EventDataType(*dptr) * gain; if (v > max) { max = v; } } else { break; } } } } } return max; } EventDataType Session::count(ChannelID id) { int sum = 0; if (id == AllAhiChannels) { for (int i = 0; i < ahiChannels.size(); i++) sum += count(ahiChannels.at(i)); return sum; } QHash::iterator i = m_cnt.find(id); if (i != m_cnt.end()) { return i.value(); } QHash >::iterator j = eventlist.find(id); if (j == eventlist.end()) { // m_cnt[id] = 0; return 0; } QVector &evec = j.value(); int evec_size=evec.size(); if (evec_size == 0) return 0; for (int i = 0; i < evec_size; ++i) { sum += evec.at(i)->count(); } m_cnt[id] = sum; return sum; } double Session::sum(ChannelID id) { QHash::iterator i = m_sum.find(id); if (i != m_sum.end()) { return i.value(); } QHash >::iterator j = eventlist.find(id); if (j == eventlist.end()) { m_sum[id] = 0; return 0; } QVector &evec = j.value(); double gain, sum = 0; EventStoreType *dptr, * eptr; int cnt; int evec_size=evec.size(); for (int i = 0; i < evec_size; ++i) { EventList &ev = *(evec[i]); gain = ev.gain(); cnt = ev.count(); dptr = ev.rawData(); eptr = dptr + cnt; for (; dptr < eptr; dptr++) { sum += double(*dptr) * gain; } } m_sum[id] = sum; return sum; } EventDataType Session::avg(ChannelID id) { QHash::iterator i = m_avg.find(id); if (i != m_avg.end()) { return i.value(); } QHash >::iterator j = eventlist.find(id); if (j == eventlist.end()) { m_avg[id] = 0; return 0; } QVector &evec = j.value(); double val = 0, gain; int cnt = 0; EventStoreType *dptr, * eptr; int evec_size=evec.size(); for (int i = 0; i < evec_size; ++i) { EventList &ev = *(evec[i]); dptr = ev.rawData(); gain = ev.gain(); cnt = ev.count(); eptr = dptr + cnt; for (; dptr < eptr; dptr++) { val += double(*dptr) * gain; } } if (cnt > 0) { // Shouldn't really happen.. Should aways contain data val /= double(cnt); } m_avg[id] = val; return val; } EventDataType Session::cph(ChannelID id) // count per hour { QHash::iterator i = m_cph.find(id); if (i != m_cph.end()) { return i.value(); } EventDataType val = count(id); val /= hours(); m_cph[id] = val; return val; } EventDataType Session::sph(ChannelID id) // sum per hour, assuming id is a time field in seconds { QHash::iterator i = m_sph.find(id); if (i != m_sph.end()) { return i.value(); } EventDataType val = sum(id) / 3600.0; val = 100.0 / hours() * val; m_sph[id] = val; return val; } EventDataType Session::timeAboveThreshold(ChannelID id, EventDataType threshold) { QHash::iterator th = m_upperThreshold.find(id); if (th != m_upperThreshold.end()) { if (fabs(th.value()-threshold) < 0.00000001) { // close enough th = m_timeAboveTheshold.find(id); if (th != m_timeAboveTheshold.end()) { return th.value(); } } } bool loaded = s_events_loaded; OpenEvents(); QHash >::iterator j = eventlist.find(id); if (j == eventlist.end()) { if (!loaded) { TrashEvents(); } return 0.0f; } QVector &evec = j.value(); int evec_size=evec.size(); qint64 ti, started=0, total=0; EventDataType data; int elsize; for (int i = 0; i < evec_size; ++i) { EventList &ev = *(evec[i]); elsize = ev.count(); for (int j=0; j < elsize; ++j) { ti=ev.time(j); data=ev.data(j); if (started == 0) { if (data >= threshold) { started=ti; } } else { if (data < threshold) { total += ti-started; started = 0; } } } } if (started) { total += ti-started; } EventDataType time = double(total) / 60000.0; m_timeAboveTheshold[id] = time; m_upperThreshold[id] = threshold; if (!loaded) this->TrashEvents(); // otherwise leave it open return time; } EventDataType Session::timeBelowThreshold(ChannelID id, EventDataType threshold) { QHash::iterator th = m_lowerThreshold.find(id); if (th != m_lowerThreshold.end()) { if (fabs(th.value()-threshold) < 0.00000001) { // close enough th = m_timeBelowTheshold.find(id); if (th != m_timeBelowTheshold.end()) { return th.value(); } } } bool loaded = s_events_loaded; QHash >::iterator j = eventlist.find(id); if (j == eventlist.end()) { return 0.0f; } QVector &evec = j.value(); int evec_size=evec.size(); qint64 ti, started=0, total=0; EventDataType data; int elsize; for (int i = 0; i < evec_size; ++i) { EventList &ev = *(evec[i]); elsize = ev.count(); for (int j=0; j < elsize; ++j) { ti=ev.time(j); data=ev.data(j); if (started == 0) { if (data <= threshold) { started=ti; } } else { if (data > threshold) { total += ti-started; started = 0; } } } } if (started) { total += ti-started; } EventDataType time = double(total) / 60000.0; m_timeBelowTheshold[id] = time; m_lowerThreshold[id] = threshold; if (!loaded) this->TrashEvents(); // otherwise leave it open return time; } bool sortfunction(EventStoreType i, EventStoreType j) { return (i < j); } EventDataType Session::percentile(ChannelID id, EventDataType percent) { QHash >::iterator jj = eventlist.find(id); if (jj == eventlist.end()) { return 0; } QVector &evec = jj.value(); if (percent > 1.0) { qWarning() << "Session::percentile() called with > 1.0"; return 0; } int evec_size = evec.size(); if (evec_size == 0) { return 0; } QVector array; EventDataType gain = evec[0]->gain(); EventStoreType *dptr, * sptr, *eptr; int tt = 0, cnt = 0; for (int i = 0; i < evec_size; ++i) { EventList &ev = *evec[i]; cnt = ev.count(); tt += cnt; } array.resize(tt); for (int i = 0; i < evec_size; ++i) { EventList &ev = *evec[i]; sptr = ev.rawData(); dptr = array.data(); eptr = sptr + cnt; for (; sptr < eptr; sptr++) { *dptr++ = * sptr; } } int n = array.size() * percent; if (n > array.size() - 1) { n--; } nth_element(array.begin(), array.begin() + n, array.end()); // slack, no averaging.. fixme if this function is ever used.. return array[n] * gain; } EventDataType Session::wavg(ChannelID id) { QHash vtime; QHash::iterator i = m_wavg.find(id); if (i != m_wavg.end()) { return i.value(); } updateCountSummary(id); QHash >::iterator j2 = m_timesummary.find(id); if (j2 == m_timesummary.end()) { return 0; } QHash ×um = j2.value(); if (!m_gain.contains(id)) { return 0; } double s0 = 0, s1 = 0, s2; EventDataType val, gain = m_gain[id]; QHash::iterator vi = timesum.begin(); QHash::iterator ts_end = timesum.end(); for (; vi != ts_end; vi++) { val = vi.key() * gain; s2 = vi.value(); s0 += s2; s1 += val * s2; } if (s0 > 0) { val = s1 / s0; } else { val = 0; } m_wavg[id] = val; return val; } EventDataType Session::calcMiddle(ChannelID code) { int c = p_profile->general->prefCalcMiddle(); if (c == 0) { return percentile(code, 0.5); // Median } else if (c == 1 ) { return wavg(code); // Weighted Average } else { return avg(code); // Average } } EventDataType Session::calcMax(ChannelID code) { return p_profile->general->prefCalcMax() ? percentile(code, 0.995f) : Max(code); } EventDataType Session::calcPercentile(ChannelID code) { double p = p_profile->general->prefCalcPercentile() / 100.0; return percentile(code, p); } EventList *Session::AddEventList(ChannelID code, EventListType et, EventDataType gain, EventDataType offset, EventDataType min, EventDataType max, EventDataType rate, bool second_field) { schema::Channel *channel = &schema::channel[code]; if (!channel) { qWarning() << "Channel" << code << "does not exist!"; //return nullptr; } EventList *el = new EventList(et, gain, offset, min, max, rate, second_field); eventlist[code].push_back(el); //s_machine->registerChannel(chan); return el; } void Session::offsetSession(qint64 offset) { //qDebug() << "Session starts" << QDateTime::fromTime_t(s_first/1000).toString("yyyy-MM-dd HH:mm:ss"); s_first += offset; s_last += offset; QHash::iterator it; QHash::iterator end; it = m_firstchan.begin(); end = m_firstchan.end(); for (; it != end; it++) { if (it.value() > 0) { it.value() += offset; } } it = m_lastchan.begin(); end = m_lastchan.end(); for (; it != end; it++) { if (it.value() > 0) { it.value() += offset; } } QHash >::iterator i; QHash >::iterator el_end=eventlist.end(); int el_s; for (i = eventlist.begin(); i != el_end; i++) { el_s=i.value().size(); for (int j = 0; j < el_s; j++) { EventList *e = i.value()[j]; e->setFirst(e->first() + offset); e->setLast(e->last() + offset); } } qDebug() << "Session now starts" << QDateTime::fromTime_t(s_first / 1000).toString("yyyy-MM-dd HH:mm:ss"); } qint64 Session::first() { qint64 start = s_first; if (s_machine->type() == MT_CPAP) { start += qint64(p_profile->cpap->clockDrift()) * 1000L; } return start; } qint64 Session::last() { qint64 last = s_last; if (s_machine->type() == MT_CPAP) { last += qint64(p_profile->cpap->clockDrift()) * 1000L; } return last; }