diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.cpp b/oscar/SleepLib/loader_plugins/prs1_loader.cpp index 04719b55..75618337 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.cpp +++ b/oscar/SleepLib/loader_plugins/prs1_loader.cpp @@ -1539,6 +1539,16 @@ static const QVector PRS1OnDemandChannels = PRS1EPAPSetEvent::TYPE, }; +// The set of "non-slice" channels are independent of mask-on slices, i.e. they +// are continuously reported and charted regardless of whether the mask is on. +static const QSet PRS1NonSliceChannels = +{ + PRS1PressureSetEvent::TYPE, + PRS1IPAPSetEvent::TYPE, + PRS1EPAPSetEvent::TYPE, + PRS1SnoresAtPressureEvent::TYPE, +}; + // The channel ID (referenced by pointer because their values aren't initialized // prior to runtime) to which a given PRS1 event should be added. Events with // no channel IDs are silently dropped, and events with more than one channel ID @@ -2574,12 +2584,32 @@ bool PRS1DataChunk::ParseEventsF5V2(void) } -void PRS1Import::CreateEventChannels(const PRS1DataChunk* event) +void PRS1Import::CreateEventChannels(const PRS1DataChunk* chunk) { - m_importChannels.clear(); + const QVector & supported = GetSupportedEvents(chunk); + + // Generate the list of channels created by non-slice events for this machine. + // We can't just use the full list of non-slice events, since on some machines + // PS is generated by slice events (EPAP/IPAP average). + // TODO: convert supported to QSet and clean this up. + QSet supportedNonSliceEvents = QSet::fromList(QList::fromVector(supported)); + supportedNonSliceEvents.intersect(PRS1NonSliceChannels); + QSet supportedNonSliceChannels; + for (auto & e : supportedNonSliceEvents) { + for (auto & pChannelID : PRS1ImportChannelMap[e]) { + supportedNonSliceChannels += *pChannelID; + } + } + + // Clear channels to prepare for a new slice, except for channels created by + // non-slice events. + for (auto & c : m_importChannels.keys()) { + if (supportedNonSliceChannels.contains(c) == false) { + m_importChannels.remove(c); + } + } // Create all supported channels (except for on-demand ones that only get created if an event appears) - const QVector & supported = GetSupportedEvents(event); for (auto & e : supported) { if (!PRS1OnDemandChannels.contains(e)) { for (auto & pChannelID : PRS1ImportChannelMap[e]) { @@ -2621,16 +2651,37 @@ void PRS1Import::AddEvent(ChannelID channel, qint64 t, float value, float gain) } -void PRS1Import::StartNewSlice(PRS1DataChunk* event, qint64 t) +bool PRS1Import::UpdateCurrentSlice(PRS1DataChunk* chunk, qint64 t) { - // TODO: Update a slice iterator to point to the slice mask-on slice encompassing time t. + bool updated = false; + + if (!m_currentSliceInitialized) { + m_currentSliceInitialized = true; + m_currentSlice = m_slices.constBegin(); + updated = true; + } - // TODO: Possibly set the interval start/end times based on the slice's start time rather than t? - m_statIntervalStart = t; - m_statIntervalEnd = t; + // Update the slice iterator to point to the mask-on slice encompassing time t. + while ((*m_currentSlice).status != MaskOn || t > (*m_currentSlice).end) { + m_currentSlice++; + updated = true; + if (m_currentSlice == m_slices.constEnd()) { + qWarning() << sessionid << "Events after last mask-on slice?"; + m_currentSlice--; + break; + } + } + + if (updated && (*m_currentSlice).status == MaskOn) { + // Set the interval start/end times based on the new slice's start time. + m_statIntervalStart = (*m_currentSlice).start; + m_statIntervalEnd = m_statIntervalStart; - // Create a new eventlist for this slice, to allow for a gap in the data between slices. - CreateEventChannels(event); + // Create a new eventlist for this new slice, to allow for a gap in the data between slices. + CreateEventChannels(chunk); + } + + return updated; } @@ -2702,19 +2753,34 @@ bool PRS1Import::ImportEventChunk(PRS1DataChunk* event) bool ok; ok = event->ParseEvents(); - // Most machines have only a single event chunk. F3V3 has an event chunk per mask-on slice. - // Set up the initial slice based on the chunk's starting timestamp. - StartNewSlice(event, t); - // TODO: Multiple slices covered by a single event chunk should also call StartNewSlice. + // Set up the (possibly initial) slice based on the chunk's starting timestamp. + UpdateCurrentSlice(event, t); for (int i=0; i < event->m_parsedData.count(); i++) { PRS1ParsedEvent* e = event->m_parsedData.at(i); t = qint64(event->timestamp + e->m_start) * 1000L; + // Skip unknown events with no timestamp + if (e->m_type == PRS1UnknownDataEvent::TYPE) { + continue; + } + + // Skip zero-length PB or LL (or unknown duration 0E) events + if ((e->m_type == PRS1PeriodicBreathingEvent::TYPE || e->m_type == PRS1LargeLeakEvent::TYPE || e->m_type == PRS1UnknownDurationEvent::TYPE) && + (e->m_duration == 0)) { + // LL occasionally appear about a minute before a new mask-on slice + // begins, when the previous mask-on slice ended with a large leak. + // This probably indicates the end of LL and beginning + // of breath detection, but we don't get any real data until mask-on. + // + // It has also happened once in a similar scenario for PB and 0E, even when + // the two mask-on slices are in different sessions! + continue; + } + bool intervalEvent = IsIntervalEvent(e); if (intervalEvent) { // Calculate the start timetamp for the interval described by this event. - // TODO: Handle multiple slices correctly, updating the interval start when a slice starts (and starting a new eventlist) // TODO: Handle the end of a slice/session correctly, adding a duplicate "end" event with the original timestamp. // (This will require some slight refactoring of the main switch statement below, including moving some // of the above variables into the PRS1Import object so that they can be shared between events, and @@ -2722,13 +2788,36 @@ bool PRS1Import::ImportEventChunk(PRS1DataChunk* event) // of the session/slice.) if (t != m_statIntervalEnd) { // When we encounter the first event of a series of stats (as identified by a new timestamp), - // mark the interval start as the end of the previous interval. - m_statIntervalStart = m_statIntervalEnd; + // check whether the previous interval ended within the current slice. (Check the timestamp + 1 + // since intervals can't start at the end of a slice, in contrast to regular (instantaneous) + // events below.) + if (!UpdateCurrentSlice(event, m_statIntervalEnd + 1)) { + // If so, simply advance the interval to start where the previous one ended. + m_statIntervalStart = m_statIntervalEnd; + // (otherwise UpdateCurrentSlice will have set it to the slice start time.) + } m_statIntervalEnd = t; } + // TODO: save interval clamped end time // Set this event's timestamp as the start of the interval, since that what OSCAR assumes. t = m_statIntervalStart; // TODO: ideally we would also set the duration of the event, but OSCAR doesn't have any notion of that yet. + } else { + // Advance the slice if needed for the regular event's timestamp. + if (!PRS1NonSliceChannels.contains(e->m_type)) { + UpdateCurrentSlice(event, t); + } + } + + // Sanity check: warn if a (non-slice) event is earlier than the current mask-on slice + if (t < (*m_currentSlice).start && (*m_currentSlice).status == MaskOn) { + if (!PRS1NonSliceChannels.contains(e->m_type)) { + // LL and PRS1_0E at the beginning of a mask-on session sometimes start 1 second early, + // so suppress that warning. + if ((*m_currentSlice).start - t > 1000 || (e->m_type != PRS1LargeLeakEvent::TYPE && e->m_type != PRS1UnknownDurationEvent::TYPE)) { + qWarning() << sessionid << "Event" << e->m_type << "before mask-on slice:" << ts(t); + } + } } switch (e->m_type) { @@ -2750,6 +2839,7 @@ bool PRS1Import::ImportEventChunk(PRS1DataChunk* event) // Divide each count into events evenly spaced over the interval. // NOTE: This is slightly fictional, but there's no waveform data for F3V3, so it won't // incorrectly associate specific events with specific flow or pressure events. + // TODO: use clamped end time if (e->m_value > 0) { qint64 blockduration = m_statIntervalEnd - m_statIntervalStart; qint64 div = blockduration / e->m_value; @@ -2767,6 +2857,7 @@ bool PRS1Import::ImportEventChunk(PRS1DataChunk* event) default: ImportEvent(t, e); + // TODO: if interval and its clamped end time == mask-on slice end time, emit duplicate event at that end time break; } } @@ -6956,6 +7047,39 @@ bool PRS1Import::ImportEvents() } if (ok) { + // Sanity check: warn if channels' eventlists don't line up with the final mask-on slices. + // First make a list of the mask-on slices that will be imported (nonzero duration) + QVector maskOn; + for (auto & slice : m_slices) { + if (slice.status == MaskOn && slice.end > slice.start) { + maskOn.append(slice); + } + } + // Then go through each required channel and make sure each eventlist is within + // the bounds of the corresponding slice, warn if not. + if (maskOn.count() > 0 && m_event_chunks.count() > 0) { + const QVector & supported = GetSupportedEvents(m_event_chunks.first()); + for (auto & e : supported) { + if (!PRS1OnDemandChannels.contains(e) && !PRS1NonSliceChannels.contains(e)) { + for (auto & pChannelID : PRS1ImportChannelMap[e]) { + auto & eventlists = session->eventlist[*pChannelID]; + if (eventlists.count() > maskOn.count()) { + qWarning() << "**" << sessionid << "has" << maskOn.count() << "mask-on slices, channel" + << *pChannelID << "has" << eventlists.count() << "eventlists"; + continue; + } + for (int i = 0; i < eventlists.count(); i++) { + if (eventlists[i]->count() == 0) continue; // no first/last timestamp + if (eventlists[i]->first() < maskOn[i].start || eventlists[i]->first() > maskOn[i].end || + eventlists[i]->last() < maskOn[i].start || eventlists[i]->last() > maskOn[i].end) { + qWarning() << "**" << sessionid << "channel" << *pChannelID << "has events outside of mask-on slice" << i; + } + } + } + } + } + } + session->m_cnt.clear(); session->m_cph.clear(); @@ -7125,7 +7249,7 @@ bool PRS1Import::ParseWaveforms() // it might be possible to drop the duplicated sample. Though that would mean that // gaps are real, though potentially only by a single sample. // - qDebug() << waveform->sessionid << "waveform discontinuity:" << (diff / 1000L) << "s @" << ts(waveform->timestamp * 1000L); + //qDebug() << waveform->sessionid << "waveform discontinuity:" << (diff / 1000L) << "s @" << ts(waveform->timestamp * 1000L); discontinuities++; } if ((diff > 1000) && (lastti != 0)) { @@ -7135,8 +7259,9 @@ bool PRS1Import::ParseWaveforms() // TODO: The machines' notion of BND appears to derive from the summary (maskoff/maskon) // slices, but the waveform data (when present) does seem to agree. This should be confirmed // once all summary parsers support slices. - if ((diff / 1000L) % 60) { + if ((((diff + 1000L) / 1000L) % 60) > 2) { // Thus far all maskoff/maskon gaps have been multiples of 1 minute. + // (except for a +/- 1-second offset that's probably due to the discontinuities above) qDebug() << waveform->sessionid << "BND?" << (diff / 1000L) << "=" << ts(waveform->timestamp * 1000L) << "-" << ts(lastti); } bnd->AddEvent(ti, double(diff)/1000.0); diff --git a/oscar/SleepLib/loader_plugins/prs1_loader.h b/oscar/SleepLib/loader_plugins/prs1_loader.h index fdd275e8..fcc13b3b 100644 --- a/oscar/SleepLib/loader_plugins/prs1_loader.h +++ b/oscar/SleepLib/loader_plugins/prs1_loader.h @@ -268,6 +268,7 @@ public: summary = nullptr; compliance = nullptr; session = nullptr; + m_currentSliceInitialized = false; } virtual ~PRS1Import() { delete compliance; @@ -339,8 +340,10 @@ protected: bool m_calcLeaks; EventDataType m_lpm4, m_ppm; - //! \brief Update import data structures for a new mask-on slice. - void StartNewSlice(PRS1DataChunk* event, qint64 t); + //! \brief Advance the current mask-on slice if needed and update import data structures accordingly. + bool UpdateCurrentSlice(PRS1DataChunk* chunk, qint64 t); + bool m_currentSliceInitialized; + QVector::const_iterator m_currentSlice; qint64 m_statIntervalStart, m_statIntervalEnd; //! \brief Identify statistical events that are reported at the end of an interval.