Merge branch 'master' into json-11

This commit is contained in:
Phil Olynyk 2021-07-11 20:34:23 -04:00
commit f69e22f876
19 changed files with 1659 additions and 172 deletions

View File

@ -12,6 +12,8 @@ CONFIG -= debug_and_release
QT += core widgets
DEFINES+=DUMPSTR
TARGET = anotDump
TEMPLATE = app
@ -27,6 +29,6 @@ SOURCES += \
dumpSTR/edfparser.cpp \
HEADERS += \
dumpSTR/common.h \
dumpSTR/SleepLib/common.h \
dumpSTR/edfparser.h \

View File

@ -81,9 +81,14 @@ int main(int argc, char *argv[]) {
}
EDFInfo edf;
QByteArray * buffer = edf.Open(filename);
if ( ! edf.Parse(buffer) )
exit(-1);
if ( ! edf.Open(filename) ) {
qDebug() << "Failed to open" << filename;
exit(-1);
}
if ( ! edf.Parse() ) {
qDebug() << "Parsing failed on" << filename;
exit(-1);
}
QDate d2 = edf.edfHdr.startdate_orig.date();
if (d2.year() < 2000) {
@ -119,5 +124,6 @@ int main(int argc, char *argv[]) {
qDebug() << "Offset: " << anno->offset << " Duration: " << anno->duration << " Text: " << anno->text;
}
}
exit(0);
}

View File

@ -193,4 +193,6 @@ int main(int argc, char *argv[]) {
// delete &str;
QThread::sleep(1);
qDebug() << "Done";
exit(0);
}

View File

@ -480,6 +480,7 @@ void FlowParser::calc(bool calcResp, bool calcTv, bool calcTi, bool calcTe, bool
quint32 *tv_tptr = nullptr;
EventStoreType *tv_dptr = nullptr;
int tv_count = 0;
double tvlast, tvlast2, tvlast3;
if (calcTv) {
TV = m_session->AddEventList(CPAP_TidalVolume, EVL_Event);
@ -598,8 +599,15 @@ void FlowParser::calc(bool calcResp, bool calcTv, bool calcTi, bool calcTe, bool
//double x=sqrt(q)*2;
//val2=x;
if (tv < mintv) { mintv = tv; }
// Average TV over last three data points
if (tv_count == 0)
tvlast = tvlast2 = tvlast3 = tv;
tv = (tvlast + tvlast2 + tvlast3 + tv*2)/5;
tvlast3 = tvlast2;
tvlast2 = tvlast;
tvlast = tv;
if (tv < mintv) { mintv = tv; }
if (tv > maxtv) { maxtv = tv; }
*tv_tptr++ = timeval;
@ -889,8 +897,9 @@ void calcRespRate(Session *session, FlowParser *flowparser)
bool calcTe = !session->eventlist.contains(CPAP_Te);
bool calcMv = !session->eventlist.contains(CPAP_MinuteVent);
int z = (calcResp ? 1 : 0) + (calcTv ? 1 : 0) + (calcMv ? 1 : 0);
// Force calculation for testing calculation vs CPAP data
// z = 1;
// If any of these three missing, remove all, and switch all on
if (z > 0 && z < 3) {

View File

@ -415,7 +415,7 @@ bool removeDir(const QString &path)
return result;
}
void copyPath(QString src, QString dst)
void copyPath(QString src, QString dst, bool overwrite)
{
QDir dir(src);
if (!dir.exists())
@ -425,7 +425,7 @@ void copyPath(QString src, QString dst)
foreach (QString d, dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) {
QString dst_path = dst + QDir::separator() + d;
dir.mkpath(dst_path);
copyPath(src + QDir::separator() + d, dst_path);
copyPath(src + QDir::separator() + d, dst_path, overwrite);
}
// Files
@ -433,6 +433,9 @@ void copyPath(QString src, QString dst)
QString srcFile = src + QDir::separator() + f;
QString destFile = dst + QDir::separator() + f;
if (overwrite && QFile::exists(destFile)) {
QFile::remove(destFile);
}
if (!QFile::exists(destFile)) {
if (!QFile::copy(srcFile, destFile)) {
qWarning() << "copyPath: could not copy" << srcFile << "to" << destFile;

View File

@ -74,7 +74,7 @@ struct ValueCount {
extern int idealThreads();
void copyPath(QString src, QString dst);
void copyPath(QString src, QString dst, bool overwrite=false);
// Primarily sort by value
@ -157,6 +157,7 @@ const QString STR_MACH_Journal = "Journal";
const QString STR_MACH_Intellipap = "Intellipap";
const QString STR_MACH_Weinmann= "Weinmann";
const QString STR_MACH_FPIcon = "FPIcon";
const QString STR_MACH_SleepStyle = "SleepStyle";
const QString STR_MACH_MSeries = "MSeries";
const QString STR_MACH_CMS50 = "CMS50";
const QString STR_MACH_ZEO = "Zeo";

View File

@ -58,6 +58,12 @@ EDFInfo::~EDFInfo()
// delete a;
}
// Set timezone to UTC
void EDFInfo::setTimeZoneUTC () {
TZ_offset = 0;
EDFInfo::localNoDST = QTimeZone(TZ_offset);
}
bool EDFInfo::Open(const QString & name)
{
if (hdrPtr != nullptr) {

View File

@ -142,6 +142,9 @@ class EDFInfo
static QDateTime getStartDT(const QString str); //! \brief Returns the start time using noLocalDST
static void setTimeZoneUTC(); //! \brief Sets noLocalDST to UTC (for EDF files using UTC time)
// The data members follow
static int TZ_offset;

View File

@ -10,6 +10,7 @@
* for more details. */
#include <QDir>
#include <QCoreApplication>
#include "intellipap_loader.h"
@ -357,7 +358,7 @@ int IntellipapLoader::OpenDV5(const QString & path)
sess->AddEventList(CPAP_Snore, EVL_Event);
sess->AddEventList(CPAP_Obstructive, EVL_Event);
sess->AddEventList(CPAP_VSnore, EVL_Event);
sess->AddEventList(INTP_SnoreFlag, EVL_Event);
sess->AddEventList(CPAP_Hypopnea, EVL_Event);
sess->AddEventList(CPAP_NRI, EVL_Event);
sess->AddEventList(CPAP_LeakFlag, EVL_Event);
@ -375,7 +376,7 @@ int IntellipapLoader::OpenDV5(const QString & path)
}
}
QDateTime d = QDateTime::fromTime_t(sid);
QDateTime d = QDateTime::fromSecsSinceEpoch(sid);
qDebug() << sid << "has double ups" << d;
/*Session *sess=Sessions[sid];
Sessions.erase(Sessions.find(sid));
@ -483,7 +484,7 @@ int IntellipapLoader::OpenDV5(const QString & path)
sess->eventlist[CPAP_Snore][0]->AddEvent(time, m_buffer[pos + 0x4]); //4/5??
if (m_buffer[pos+0x4] > 0) {
sess->eventlist[CPAP_VSnore][0]->AddEvent(time, m_buffer[pos + 0x5]);
sess->eventlist[INTP_SnoreFlag][0]->AddEvent(time, m_buffer[pos + 0x5]);
}
// 0x0f == Leak Event
@ -720,7 +721,7 @@ PACK (struct DV6_S_REC{
unsigned char checksum; //54
});
// DV6 SET.BIN - structure of the entire file
// DV6 SET.BIN - structure of the entire settings file
PACK (struct SET_BIN_REC {
char unknown_00; // assuming file version
char serial[11]; // null terminated
@ -868,13 +869,16 @@ struct DV6_SessionInfo {
unsigned int begin;
unsigned int end;
unsigned int written;
bool haveHighResData;
// bool haveHighResData;
unsigned int firstHighRes;
unsigned int lastHighRes;
CPAPMode mode = MODE_UNKNOWN;
};
QString card_path;
QString backup_path;
QString history_path;
QString rebuild_path;
MachineInfo info;
Machine * mach = nullptr;
@ -882,6 +886,8 @@ Machine * mach = nullptr;
bool rebuild_from_backups = false;
bool create_backups = false;
QStringList inputFilePaths;
QMap<SessionID, DV6_S_Data> DailySummaries;
QMap<SessionID, DV6_SessionInfo> SessionData;
SET_BIN_REC * settings;
@ -909,84 +915,119 @@ public:
~RollingBackup () {
}
bool open (const QString filetype, DV6_HEADER * newhdr); // Open the file
bool open (const QString filetype, DV6_HEADER * newhdr, QByteArray * startTime); // Open the file
bool close(); // close the file
bool save(QByteArray dataBA); // save the next record in the file
bool save(const QByteArray &dataBA); // save the next record in the file
private:
//DV6_HEADER hdr; // file header
DV6_HEADER hdr; // file header
QString filetype;
QFile hFile;
QFile histfile;
//int record_length; // Length of record block in incoming file
//const int maxHistFileSize = 20*10e6; // Maximum size of file before we create a new file
const qint64 maxHistFileSize = 10000000; // Maximum size of file before we create a new file, in MB (40 MB)
// (While 40e6 would be easier to understand, 40e6 is a double, not an int)
//int numWritten; // Number of records written
//quint32 lastTimestamp;
//unsigned int wrap_record;
unsigned int lastTimeInFile; // Timestamp of last data record in history file
int numWritten; // Number of records written
};
bool RollingBackup::open (const QString filetype, DV6_HEADER * newhdr) {
QStringList getHistoryFileNames (const QString filetype, bool reversed = false) {
QStringList filters;
QDir hpath(history_path);
filters.append(filetype); // Assume one-letter file name like "S.BIN"
filters[0].insert(1, "_*"); // Add a wild card like "S_*.BIN"
hpath.setNameFilters(filters);
hpath.setFilter(QDir::Files);
hpath.setSorting(QDir::Name);
if (reversed) hpath.setSorting(QDir::Name | QDir::Reversed);
return hpath.entryList(); // Get list of files
}
QString getNewFileName (QString filetype, QByteArray * startTime, int offset=0) {
unsigned char startTimeChar[5];
for (int i = 0; i < 4; i++)
startTimeChar[i] = startTime->at(offset+i);
unsigned int ts = convertTime(startTimeChar);
QString newfile = filetype.left(1) + "_" + QDateTime::fromSecsSinceEpoch(ts).toString("yyyyMMdd") + ".BIN";
qDebug() << "DV6 getNewFileName returns" << newfile;
return newfile;
}
bool RollingBackup::open (const QString filetype, DV6_HEADER * inputhdr, QByteArray * startTimeOfBackup) {
if (!create_backups)
return true;
#ifdef ROLLBACKUP
QDir hpath(history_path);
QString historypath = hpath.absolutePath() + "/";
int histfilesize = 0;
this->filetype = filetype;
QDir hpath(history_path);
QStringList filters;
bool needNewFile = false;
memcpy (&hdr, inputhdr, sizeof(DV6_HEADER));
numWritten = 0;
filters.append(filetype);
filters[0].insert(1, "_*");
hpath.setNameFilters(filters);
hpath.setFilter(QDir::Files);
hpath.setSorting(QDir::Name | QDir::Reversed);
QStringList fileNames = hpath.entryList(); // Get list of files
QFile histfile(fileNames.first());
// bool needNewFile = false;
QStringList fileNames = getHistoryFileNames(filetype, true);
// Handle first time a history file is being created
if (fileNames.isEmpty()) {
memcpy (&hdr, newhdr, sizeof(DV6_HEADER));
for (int i = 0; i < 4; i++) {
hdr.recordStart[i] = 0;
hdr.lasttime[i] = 0;
}
record_length = hdr.recordLength;
lastTimeInFile = 0;
histfile.setFileName(historypath + getNewFileName (filetype, startTimeOfBackup));
needNewFile= true;
}
// We have an existing history record
if (! fileNames.isEmpty()) {
histfile.setFileName(historypath + fileNames.first()); // File names are in reverse order, so latest is first
// Open and read history file header and save the header
if (!histfile.open(QIODevice::ReadWrite)) {
qWarning() << "DV6 rb(open) could not open" << fileNames.first() << "for readwrite, error code" << histfile.error() << histfile.errorString();
return false;
}
histfilesize = histfile.size();
QByteArray dataBA = histfile.read(sizeof(DV6_HEADER));
memcpy (&hdr, dataBA.data(), sizeof(DV6_HEADER));
lastTimeInFile = convertTime(hdr.lasttime);
// See if this file is large enough that we want to create a new file
// If it is large, we'll start a new file.
if (histfile.size() > maxHistFileSize) {
memcpy (&hdr, newhdr, sizeof(DV6_HEADER));
for (int i = 0; i < 4; i++)
hdr.recordStart[i] = 0;
QString nextFile = historypath + getNewFileName (filetype, &dataBA, 51);
QString hh = histfile.fileName();
if (!histfile.open(QIODevice::ReadOnly)) {
qWarning() << "DV6 RollingBackup could not open" << fileNames.first() << "for reading, error code" << histfile.error() << histfile.errorString();
return false;
}
record_length = hdr.recordLength;
wrap_record = convertNum(hdr.recordStart);
if (!histfile.seek(sizeof(DV6_HEADER) + (wrap_record-1) * record_length)) {
qWarning() << "DV6 RollingBackup unable to make initial seek to record" << wrap_record
<< "in" + histfile.fileName() << histfile.error() << histfile.errorString();
if (hh != nextFile) {
lastTimeInFile = convertTime(hdr.lasttime);
histfile.close();
return false;
// Update header saying we are starting at record 0
for (int i = 0; i < 4; i++)
hdr.recordStart[i] = 0;
histfile.setFileName(nextFile);
needNewFile = true;
}
}
}
#else
Q_UNUSED(filetype)
Q_UNUSED(newhdr)
#endif
if (needNewFile) {
if (!histfile.open(QIODevice::ReadWrite)) {
qWarning() << "DV6 rb(open) could not create new file" << histfile.fileName() << "for readwrite, error code" << histfile.error() << histfile.errorString();
return false;
}
if (histfile.write((char *)&hdr.unknown, sizeof(DV6_HEADER)) != sizeof(DV6_HEADER)) {
qWarning() << "DV6 rb(open) could not write header to new file" << histfile.fileName() << "for readwrite, error code" << histfile.error() << histfile.errorString();
histfile.close();
return false;
}
} else {
// qDebug() << "DV6 rb(open) history file size" << histfilesize;
histfile.seek(histfilesize);
}
return true;
}
@ -994,13 +1035,62 @@ bool RollingBackup::open (const QString filetype, DV6_HEADER * newhdr) {
bool RollingBackup::close() {
if (!create_backups)
return true;
qint32 size = histfile.size();
if (!histfile.seek(0)) {
qWarning() << "DV6 rb(close) unable to seek to file beginning" << histfile.error() << histfile.errorString();
histfile.close();
return false;
}
quint32 sizehdr = sizeof(DV6_HEADER);
quint32 reclen = hdr.recordLength;
quint32 wrap_point = (size - sizehdr) / reclen;
hdr.recordStart[0] = wrap_point & 0xff;
hdr.recordStart[1] = (wrap_point >> 8) & 0xff;
hdr.recordStart[2] = (wrap_point >> 16) & 0xff;
hdr.recordStart[3] = (wrap_point >> 24) & 0xff;
if (histfile.write((char *)&hdr, sizeof(DV6_HEADER)) != sizeof(DV6_HEADER)) {
qWarning() << "DV6 rb(close) could not write header to file" << histfile.fileName() << "error code" << histfile.error() << histfile.errorString();
histfile.close();
return false;
}
histfile.close();
qDebug() << "DV6 rb(close) wrote" << numWritten << "records.";
return true;
}
bool RollingBackup::save(QByteArray dataBA) {
Q_UNUSED(dataBA)
bool RollingBackup::save(const QByteArray &dataBA) {
if (!create_backups)
return true;
unsigned char * data = (unsigned char *)dataBA.data();
unsigned int thisTimeStamp = convertTime(data);
if (thisTimeStamp > lastTimeInFile) { // Is this data new to us?
// If so, save it to the history file.
if (histfile.write(dataBA) == -1) {
qWarning() << "DV6 rb(save) could not save record" << histfile.fileName() << "for readwrite, error code" << histfile.error() << histfile.errorString();
histfile.close();
return false;
}
memcpy(&hdr.lasttime, data, 4);
// if (!histfile.seek(histfile.pos() + dataBA.length()))
// qWarning() << "DV6 rb(save) failed respositioning" << histfile.fileName() << "for readwrite, error code" << histfile.error() << histfile.errorString();
numWritten++;
}
/***
else {
qDebug() << "DV6 rb(save) skipping record" << numWritten << QDateTime::fromSecsSinceEpoch(thisTimeStamp).toString("MM/dd/yyyy hh:mm:ss")
<< "last in file" << QDateTime::fromSecsSinceEpoch(lastTimeInFile).toString("MM/dd/yyyy hh:mm:ss");
}
***/
return true;
}
@ -1018,7 +1108,7 @@ public:
hdr = nullptr;
}
bool open (QString fn); // Open the file
bool open (QString fn, bool getNext = false); // Open the file
bool close(); // close the file
unsigned char * get(); // read the next record in the file
@ -1043,13 +1133,33 @@ private:
unsigned char * data = nullptr; // record pointer
};
bool RollingFile::open(QString filetype) {
bool RollingFile::open(QString filetype, bool getNext) {
filename = filetype;
file.setFileName(card_path + "/" +filetype);
if (rebuild_from_backups) {
// Building from backup
if (!getNext) { // Initialize on first call
inputFilePaths.clear();
QStringList histFileNames = getHistoryFileNames(filetype);
qDebug() << "DV6 rf(open) History file names" << histFileNames;
for (int i=0; i < histFileNames.size(); i++) {
file.setFileName(history_path + "/" + histFileNames.at(i));
inputFilePaths.append(file.fileName());
}
}
if (inputFilePaths.empty())
return false;
file.setFileName(inputFilePaths.at(0));
inputFilePaths.removeAt(0);
} else {
file.setFileName(card_path + "/" +filetype);
inputFilePaths.clear();
}
if (!file.open(QIODevice::ReadOnly)) {
qWarning() << "DV6 RollingFile could not open" << filename << "for reading, error code" << file.error() << file.errorString();
qWarning() << "DV6 rf(open) could not open" << filename << "for reading, error code" << file.error() << file.errorString();
return false;
}
@ -1068,30 +1178,48 @@ bool RollingFile::open(QString filetype) {
// Create buffer to hold each record as it is read
data = new unsigned char[record_length];
// Seek to first data record in file
if (!file.seek(sizeof(DV6_HEADER) + wrap_record * record_length)) {
qWarning() << "DV6 RollingFile unable to make initial seek to record" << wrap_record << "in" + filename << file.error() << file.errorString();
// Seek to oldest data record in file, which is always at the wrap point
// wrap_record is the C offset where the next data record is to be written.
// Since C offsets begin with zero, it is also the number of records in the file.
int seekpos = sizeof(DV6_HEADER) + wrap_record * record_length;
if (!file.seek(seekpos)) {
qWarning() << "DV6 rf(open) unable to make initial seek to record" << wrap_record << "in" + filename << file.error() << file.errorString();
file.close();
return false;
}
#ifdef ROLLBACKUP
if (!rb.open(filetype, hdr)) {
qWarning() << "DV6 RollingBackup failed";
file.close();
return false;
}
#endif
qDebug() << "DV6 rf(open)" << filetype << "positioning to oldest record at pos" << seekpos << "after seek" << file.pos();
qDebug() << "DV6 RollingFile opening" << filename << "at wrap record" << wrap_record;
if (file.atEnd()) {
file.seek(sizeof(DV6_HEADER));
}
dataBA = file.read(4); // Read timestamp of newest data record
file.seek(seekpos); // Reset read position before what we just read so we start reading data records here
if (!rb.open(filetype, hdr, &dataBA)) {
qWarning() << "DV6 rf(open) failed";
file.close();
return false;
}
qDebug() << "DV6 rf(open)" << filename << "at wrap record" << wrap_record << "now at pos" << file.pos();
return true;
}
bool RollingFile::close() {
/*** Works for backing up but prevents chart appearing for the last day
// Flush any additional input that has not been backed up
if (create_backups) {
do {
DV6_U_REC * rec = (DV6_U_REC *) get();
if (rec == nullptr)
break;
} while (true);
}
***/
file.close();
#ifdef ROLLBACKUP
rb.close();
#endif
if (data)
delete [] data;
@ -1105,44 +1233,54 @@ bool RollingFile::close() {
unsigned char * RollingFile::get() {
// int readpos;
record_number++;
// If we have found the wrap record again, we are done
if (wrapping && (record_number == wrap_record))
return nullptr;
// Hare we reached end of file and need to wrap around to beginning?
if (file.atEnd()) {
if (wrapping) {
qDebug() << "DV6 RollingFile wrap - second time through";
if (wrapping && record_number == wrap_record) {
// Unless we are rebuilding from backup and may have more files
if (rebuild_from_backups && !inputFilePaths.empty()) {
qDebug() << "DV6 rf(get) closing" << file.fileName();
file.close();
open(inputFilePaths.at(0), true);
} else {
return nullptr;
}
qDebug() << "DV6 RollingFile wrapping to beginning of data in" << filename << "record number is" << record_number-1 << "records read" << number_read;
record_number = 0;
}
// Have we reached end of file and need to wrap around to beginning?
if (file.atEnd()) {
if (wrapping) {
qDebug() << "DV6 rf(get) wrap - second time through";
return nullptr;
}
qDebug() << "DV6 rf(get) wrapping to beginning of data in" << filename << "record number is" << record_number-1 << "records read" << number_read;
record_number = 1;
wrapping = true;
if (!file.seek(sizeof(DV6_HEADER))) {
file.close();
qWarning() << "DV6 RollingFile unable to seek to first data record in file";
qWarning() << "DV6 rf(get) unable to seek to first data record in file";
return nullptr;
}
qDebug() << "DV6 rf(get) #" << record_number << "now at pos" << file.pos();
}
QByteArray dataBA;
// readpos = file.pos();
dataBA=file.read(record_length); // read next record
if (dataBA.size() != record_length) {
qWarning() << "DV6 RollingFile record" << record_number << "wrong length";
qWarning() << "DV6 rf(get) #" << record_number << "wrong length";
file.close();
return nullptr;
}
#ifdef ROLLBACKUP
if (!rb.save(dataBA)) {
qWarning() << "DV6 RollingBackup failed";
qWarning() << "DV6 rf(get) failed";
}
#endif
number_read++;
// qDebug() << "RollingFile read" << filename << "record number" << record_number << "of length" << record_length << "number read so far" << number_read;
// qDebug() << "DV6 rf(get)" << filename << "at start pos" << readpos << "end pos" << file.pos() << "record number" << record_number << "of length" << record_length << "number read so far" << number_read;
memcpy (data, (unsigned char *) dataBA.data(), record_length);
return data;
}
@ -1163,15 +1301,6 @@ QByteArray fileChecksum(const QString &fileName,
return QByteArray();
}
/***
// Return the OSCAR date that the last data was written.
// This will be considered to be the last day for which we have any data.
// Adjust to get the correct date for sessions starting after midnight.
QDate getLastDate () {
return QDate();
}
***/
// Return date used within OSCAR, assuming day ends at split time in preferences (usually noon)
QDate getNominalDate (QDateTime dt) {
QDate d = dt.date();
@ -1213,15 +1342,17 @@ bool load6Sessions () {
// big endian
ts1 = convertTime(rec->begin); // session start time (this is also the session id)
ts2 = convertTime(rec->end); // session end time
#ifdef DEBUG6
qDebug() << "U.BIN Session" << QDateTime::fromTime_t(ts1).toString("MM/dd/yyyy hh:mm:ss") << ts1 << "to" << QDateTime::fromTime_t(ts2).toString("MM/dd/yyyy hh:mm:ss") << ts2;
#endif
//#ifdef DEBUG6
qDebug() << "U.BIN Session" << QDateTime::fromSecsSinceEpoch(ts1).toString("MM/dd/yyyy hh:mm:ss") << ts1 << "to" << QDateTime::fromSecsSinceEpoch(ts2).toString("MM/dd/yyyy hh:mm:ss") << ts2;
//#endif
sinfo.sess = nullptr;
sinfo.dailyData = nullptr;
sinfo.begin = ts1;
sinfo.end = ts2;
sinfo.written = 0;
sinfo.haveHighResData = false;
// sinfo.haveHighResData = false;
sinfo.firstHighRes = 0;
sinfo.lastHighRes = 0;
SessionData[ts1] = sinfo;
} while (true);
@ -1241,6 +1372,8 @@ bool load6Settings (const QString & path) {
QByteArray dataBA;
QFile f(path+"/"+SET_BIN);
if (rebuild_from_backups)
f.setFileName(rebuild_path+"/"+SET_BIN);
if (f.open(QIODevice::ReadOnly)) {
// Read and parse entire SET.BIN file
@ -1361,6 +1494,8 @@ bool load6VersionInfo(const QString & path) {
QByteArray str;
QFile f(path+"/VER.BIN");
if (rebuild_from_backups)
f.setFileName(rebuild_path+"/VER.BIN");
info.series = "DV6";
info.brand = "DeVilbiss";
@ -1434,19 +1569,21 @@ int create6Sessions() {
if (mach->SessionExists(sid)) {
// skip already imported sessions..
qDebug() << "Session already exists" << QDateTime::fromTime_t(sid).toString("MM/dd/yyyy hh:mm:ss");
qDebug() << "Session already exists" << QDateTime::fromSecsSinceEpoch(sid).toString("MM/dd/yyyy hh:mm:ss");
} else if (sinfo->sess == nullptr) {
// process new sessions
sess = new Session(mach, sid);
#ifdef DEBUG6
qDebug() << "Creating session" << QDateTime::fromTime_t(sinfo->begin).toString("MM/dd/yyyy hh:mm:ss") << "to" << QDateTime::fromTime_t(sinfo->end).toString("MM/dd/yyyy hh:mm:ss");
qDebug() << "Creating session" << QDateTime::fromSecsSinceEpoch(sinfo->begin).toString("MM/dd/yyyy hh:mm:ss") << "to" << QDateTime::fromSecsSinceEpoch(sinfo->end).toString("MM/dd/yyyy hh:mm:ss");
#endif
sinfo->sess = sess;
sinfo->dailyData = nullptr;
sinfo->written = 0;
sinfo->haveHighResData = false;
// sinfo->haveHighResData = false;
sinfo->firstHighRes = 0;
sinfo->lastHighRes = 0;
sess->really_set_first(quint64(sinfo->begin) * 1000L);
sess->really_set_last(quint64(sinfo->end) * 1000L);
@ -1464,9 +1601,9 @@ int create6Sessions() {
sess->AddEventList(CPAP_MinuteVent, EVL_Event);
sess->AddEventList(CPAP_RespRate, EVL_Event);
sess->AddEventList(CPAP_Snore, EVL_Event);
sess->AddEventList(INTP_SnoreFlag, EVL_Event);
sess->AddEventList(CPAP_Obstructive, EVL_Event);
// sess->AddEventList(CPAP_VSnore, EVL_Event);
sess->AddEventList(CPAP_Hypopnea, EVL_Event);
sess->AddEventList(CPAP_NRI, EVL_Event);
// sess->AddEventList(CPAP_LeakFlag, EVL_Event);
@ -1483,7 +1620,7 @@ int create6Sessions() {
//?? SessionEnd[z] = 0;
//?? break;
//?? }
qDebug() << sid << "has double ups" << QDateTime::fromTime_t(sid).toString("MM/dd/yyyy hh:mm:ss");
qDebug() << sid << "has double ups" << QDateTime::fromSecsSinceEpoch(sid).toString("MM/dd/yyyy hh:mm:ss");
/*Session *sess=Sessions[sid];
Sessions.erase(Sessions.find(sid));
@ -1560,9 +1697,9 @@ bool load6HighResData () {
if (rec_ts1 < previousRecBegin) {
qWarning() << "R.BIN - Corruption/Out of sequence data found, skipping record" << rf.recnum() << ", prev"
<< QDateTime::fromTime_t(previousRecBegin).toString("MM/dd/yyyy hh:mm:ss") << previousRecBegin
<< QDateTime::fromSecsSinceEpoch(previousRecBegin).toString("MM/dd/yyyy hh:mm:ss") << previousRecBegin
<< "this"
<< QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << rec_ts1;
<< QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << rec_ts1;
continue;
}
@ -1592,8 +1729,8 @@ bool load6HighResData () {
// Skip over sessions until we find one that this record is in
while (rec_ts1 > sinfo->end) {
#ifdef DEBUG6
qDebug() << "R.BIN - skipping session" << QDateTime::fromTime_t(sinfo->begin).toString("MM/dd/yyyy hh:mm:ss")
<< "looking for" << QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss")
qDebug() << "R.BIN - skipping session" << QDateTime::fromSecsSinceEpoch(sinfo->begin).toString("MM/dd/yyyy hh:mm:ss")
<< "looking for" << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss")
<< "record" << rf.recnum();
#endif
if (inSession && sess) {
@ -1657,10 +1794,13 @@ bool load6HighResData () {
qint64 ti = qint64(rec_ts1) * 1000;
flow->AddWaveform(ti,R->breath,50,2000);
pressure->AddWaveform(ti, &R->pressure1, 2, 2000);
sinfo->haveHighResData = true;
if (sinfo->firstHighRes == 0 || sinfo->firstHighRes > rec_ts1) sinfo->firstHighRes = rec_ts1;
if (sinfo->lastHighRes == 0 || sinfo->lastHighRes < rec_ts1+2) sinfo->lastHighRes = rec_ts1+2;
// sinfo->haveHighResData = true;
if (sess->first() == 0)
qWarning() << "first = 0 - 1442";
//////////////////////////////////////////////////////////////////
// Show Flow Limitation Events as a graph
//////////////////////////////////////////////////////////////////
@ -2050,18 +2190,20 @@ bool load6PerMinute () {
rec_ts1 = convertTime(rec->timestamp);
if (rec_ts1 < previousRecBegin) {
#ifdef DEBUG6
qWarning() << "L.BIN - Corruption/Out of sequence data found, skipping record" << rf.recnum() << ", prev"
<< QDateTime::fromTime_t(previousRecBegin).toString("MM/dd/yyyy hh:mm:ss") << previousRecBegin
<< QDateTime::fromSecsSinceEpoch(previousRecBegin).toString("MM/dd/yyyy hh:mm:ss") << previousRecBegin
<< "this"
<< QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << rec_ts1;
<< QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << rec_ts1;
#endif
continue;
}
/****
// Look for a gap in DV6_L records. They should be at one minute intervals.
// If there is a gap, we are probably in a new session
if (inSession && ((rec_ts1 - previousRecBegin) > 60)) {
qDebug() << "L.BIN record gap, current" << QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss")
<< "previous" << QDateTime::fromTime_t(previousRecBegin).toString("MM/dd/yyyy hh:mm:ss");
qDebug() << "L.BIN record gap, current" << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss")
<< "previous" << QDateTime::fromSecsSinceEpoch(previousRecBegin).toString("MM/dd/yyyy hh:mm:ss");
sess->set_last(maxleak->last());
sess = nullptr;
leak = maxleak = MV = TV = RR = Pressure = nullptr;
@ -2071,7 +2213,7 @@ bool load6PerMinute () {
// Skip over sessions until we find one that this record is in
while (rec_ts1 > sinfo->end) {
#ifdef DEBUG6
qDebug() << "L.BIN - skipping session" << QDateTime::fromTime_t(sinfo->begin).toString("MM/dd/yyyy hh:mm:ss") << "looking for" << QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss");
qDebug() << "L.BIN - skipping session" << QDateTime::fromSecsSinceEpoch(sinfo->begin).toString("MM/dd/yyyy hh:mm:ss") << "looking for" << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss");
#endif
if (inSession && sess) {
// Close the open session and update the min and max
@ -2095,9 +2237,9 @@ bool load6PerMinute () {
if (rec_ts1 < previousRecBegin) {
qWarning() << "L.BIN - Corruption/Out of sequence data found, stopping import, prev"
<< QDateTime::fromTime_t(previousRecBegin).toString("MM/dd/yyyy hh:mm:ss")
<< QDateTime::fromSecsSinceEpoch(previousRecBegin).toString("MM/dd/yyyy hh:mm:ss")
<< "this"
<< QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss");
<< QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss");
break;
}
@ -2114,10 +2256,9 @@ bool load6PerMinute () {
if (sess->last()/1000 > sinfo->end)
sinfo->end = sess->last()/1000;
if (!sinfo->haveHighResData) {
// if (!sinfo->haveHighResData) {
// Don't use this pressure if we already have higher resolution data
Pressure = sess->AddEventList(CPAP_Pressure, EVL_Event);
}
if (sinfo->mode == MODE_UNKNOWN) {
if (rec->pressureLimitLow != rec->pressureLimitHigh) {
sess->settings[CPAP_PressureMin] = rec->pressureLimitLow / 10.0f;
@ -2141,7 +2282,29 @@ bool load6PerMinute () {
leak->AddEvent(ti, rec->avgLeak); //???
RR->AddEvent(ti, rec->breathRate);
if (Pressure) Pressure->AddEvent(ti, rec->avgPressure / 10.0f); // average pressure
if ( sinfo->firstHighRes == 0 // No high res data
|| rec_ts1 < sinfo->firstHighRes // Before high res data begins
|| ((rec_ts1 > (sinfo->lastHighRes+2)) && (sinfo->lastHighRes > 0))) // or after high res data ends
{
if (!Pressure)
Pressure = sess->AddEventList(CPAP_Pressure, EVL_Event, 0.1f);
// if (sinfo->firstHighRes == 0) {
Pressure->AddEvent(ti, rec->avgPressure); // average pressure for next minute
Pressure->AddEvent(ti + 59998, rec->avgPressure); // end of pressure block
// } else {
// for (int i = 0; i < 60; i++) {
// Pressure->AddEvent(ti+i, rec->avgPressure); // average pressure for next minute
// }
// }
}
/***
if (Pressure)
qDebug() << "Lowres pressure" << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss")
<< "rec_ts1" << rec_ts1 << "firstHighRes" << sinfo->firstHighRes << "last" << sinfo->lastHighRes
<< "Pressure" << rec->avgPressure / 10.0f;
***/
unsigned tv = rec->tidalVolume6 + (rec->tidalVolume7 << 8);
MV->AddEvent(ti, rec->breathRate * tv / 1000.0 );
@ -2177,13 +2340,14 @@ bool load6EventData () {
EventList * OA = nullptr;
EventList * CA = nullptr;
EventList * H = nullptr;
EventList * H = nullptr;
EventList * RE = nullptr;
EventList * PB = nullptr;
EventList * LL = nullptr;
EventList * EP = nullptr;
EventList * SN = nullptr;
EventList * FL = nullptr;
// EventList * FLG = nullptr;
if (!rf.open("E.BIN")) {
qWarning() << "DV6 Unable to open E.BIN";
@ -2209,7 +2373,7 @@ bool load6EventData () {
// Skip over sessions until we find one that this record is in
while (rec_ts1 > sinfo->end) {
#ifdef DEBUG6
qDebug() << "E.BIN - skipping session" << QDateTime::fromTime_t(sinfo->begin).toString("MM/dd/yyyy hh:mm:ss") << "looking for" << QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss");
qDebug() << "E.BIN - skipping session" << QDateTime::fromSecsSinceEpoch(sinfo->begin).toString("MM/dd/yyyy hh:mm:ss") << "looking for" << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss");
#endif
if (inSession) {
// Close the open session and update the min and max
@ -2227,11 +2391,14 @@ bool load6EventData () {
sess->set_last(LL->last());
if (EP->last() > 0)
sess->set_last(EP->last());
if (SN->last() > 0)
sess->set_last(SN->last());
if (FL->last() > 0)
sess->set_last(FL->last());
if (SN->last() > 0)
sess->set_last(SN->last());
/***
if (FLG->last() > 0)
sess->set_last(FLG->last());
***/
sess = nullptr;
H = CA = RE = OA = PB = LL = EP = SN = FL = nullptr;
inSession = false;
@ -2245,16 +2412,18 @@ bool load6EventData () {
// If we have data beyond last session, we are in trouble (for unknown reasons)
if (sinfo == SessionData.end()) {
qWarning() << "DV6 E.BIN import ran out of sessions to match flow data";
qWarning() << "DV6 E.BIN import ran out of sessions,"
<< "event data begins"
<< QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss");
break;
}
if (rec_ts1 < previousRecBegin) {
qWarning() << "E.BIN - Corruption/Out of sequence data found, stopping import, prev"
<< QDateTime::fromTime_t(previousRecBegin).toString("MM/dd/yyyy hh:mm:ss")
<< "this"
<< QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss");
break;
qWarning() << "DV6 E.BIN - Out of sequence data found, skipping, prev"
<< QDateTime::fromSecsSinceEpoch(previousRecBegin).toString("MM/dd/yyyy hh:mm:ss")
<< "this event"
<< QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss");
continue; // break;
}
// Check if record belongs in this session or a future session
@ -2268,10 +2437,12 @@ bool load6EventData () {
PB = sess->AddEventList(CPAP_PB, EVL_Event);
LL = sess->AddEventList(CPAP_LargeLeak, EVL_Event);
EP = sess->AddEventList(CPAP_ExP, EVL_Event);
// SN = sess->AddEventList(CPAP_VSnore, EVL_Event);
SN = sess->AddEventList(INTP_SnoreFlag, EVL_Event);
FL = sess->AddEventList(CPAP_FlowLimit, EVL_Event);
SN = sess->AddEventList(CPAP_Snore, EVL_Waveform, 1.0f, 0.0f, 0.0f, 0.0f, double(2000) / double(2));
// FLG = sess->AddEventList(CPAP_FLG, EVL_Waveform, 1.0f, 0.0f, 0.0f, 0.0f, double(2000) / double(2));
// SN = sess->AddEventList(CPAP_Snore, EVL_Waveform, 1.0f, 0.0f, 0.0f, 0.0f, double(2000) / double(2));
inSession = true;
}
}
@ -2281,11 +2452,12 @@ bool load6EventData () {
// TODO: We don't know what is really going on here. Is it sloppiness on the part of the DV6 in recording time stamps?
qint64 ti = qint64(rec_ts1 - (duration/2)) * 1000L;
if (duration < 0) {
qDebug() << "E.BIN at" << QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss")
qDebug() << "E.BIN at" << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss")
<< "reports duration of" << duration
<< "ending" << QDateTime::fromTime_t(rec_ts2).toString("MM/dd/yyyy hh:mm:ss");
<< "ending" << QDateTime::fromSecsSinceEpoch(rec_ts2).toString("MM/dd/yyyy hh:mm:ss");
}
int code = rec->event_type;
/***
//////////////////////////////////////////////////////////////////
// Show Snore Events as a graph
//////////////////////////////////////////////////////////////////
@ -2293,6 +2465,15 @@ bool load6EventData () {
qint16 severity = rec->event_severity;
SN->AddWaveform(ti, &severity, 1, duration*1000);
}
//////////////////////////////////////////////////////////////////
// Show Flow Limit Events as a graph
//////////////////////////////////////////////////////////////////
if (code == 10) {
qint16 severity = rec->event_severity;
FLG->AddWaveform(ti, &severity, 1, duration*1000);
}
***/
if (rec->event_severity >= 3)
switch (code) {
case 1:
@ -2300,33 +2481,33 @@ bool load6EventData () {
break;
case 2:
OA->AddEvent(ti, duration);
// qDebug() << "E.BIN - OA" << QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << "duration" << duration << "r" << rf.recnum();
// qDebug() << "E.BIN - OA" << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << "duration" << duration << "r" << rf.recnum();
break;
case 4:
H->AddEvent(ti, duration);
break;
case 5:
RE->AddEvent(ti, duration);
// qDebug() << "E.BIN - RERA" << QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << "duration" << duration << "r" << rf.recnum();
// qDebug() << "E.BIN - RERA" << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << "duration" << duration << "r" << rf.recnum();
break;
case 8: // snore
SN->AddEvent(ti, duration);
// qDebug() << "E.BIN - Snore" << QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << "duration" << duration << "r" << rf.recnum();
// qDebug() << "E.BIN - Snore" << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << "duration" << duration << "r" << rf.recnum();
break;
case 9: // expiratory puff
EP->AddEvent(ti, duration);
// qDebug() << "E.BIN - exhale puff" << QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << "duration" << duration << "r" << rf.recnum();
// qDebug() << "E.BIN - exhale puff" << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << "duration" << duration << "r" << rf.recnum();
break;
case 10: // flow limitation
FL->AddEvent(ti, duration);
// qDebug() << "E.BIN - flow limit" << QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << "duration" << duration << "r" << rf.recnum();
// qDebug() << "E.BIN - flow limit" << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << "duration" << duration << "r" << rf.recnum();
break;
case 11: // periodic breathing
PB->AddEvent(ti, duration);
break;
case 12: // large leaks
LL->AddEvent(ti, duration);
// qDebug() << "E.BIN - large leak" << QDateTime::fromTime_t(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << "duration" << duration << "r" << rf.recnum();
// qDebug() << "E.BIN - large leak" << QDateTime::fromSecsSinceEpoch(rec_ts1).toString("MM/dd/yyyy hh:mm:ss") << "duration" << duration << "r" << rf.recnum();
break;
case 13: // pressure change
break;
@ -2359,7 +2540,7 @@ int addSessions() {
}
#ifdef DEBUG6
else
qDebug() << "Added session" << sess->session() << QDateTime::fromTime_t(sess->session()).toString("MM/dd/yyyy hh:mm:ss");;
qDebug() << "Added session" << sess->session() << QDateTime::fromSecsSinceEpoch(sess->session()).toString("MM/dd/yyyy hh:mm:ss");;
#endif
// Update indexes, process waveform and perform flagging
@ -2370,8 +2551,8 @@ int addSessions() {
// Unload them from memory
sess->TrashEvents();
} else
qWarning() << "addSessions: session pointer is null";
} // else
// qWarning() << "addSessions: session pointer is null";
}
return SessionData.size();
@ -2391,24 +2572,10 @@ bool backup6 (const QString & path) {
QDir ipath(path);
QDir cpath(card_path);
QDir bpath(backup_path);
if ( ! bpath.exists()) {
if ( ! bpath.mkpath(backup_path) ) {
qWarning() << "Could not create DV6 backup directory" << backup_path;
return false;
}
}
QDir hpath(history_path);
// Copy input data to backup location
copyPath(ipath.absolutePath(), bpath.absolutePath());
// Create history directory for dated backups
QDir hpath(history_path);
if ( ! hpath.exists())
if ( ! hpath.mkpath(history_path)) {
qWarning() << "Could not create DV6 archive directory" << history_path;
return false;
}
copyPath(ipath.absolutePath(), bpath.absolutePath(), true);
// Create archive of settings file if needed (SET.BIN)
bool backup_settings = true;
@ -2460,10 +2627,12 @@ bool init6Environment (const QString & path) {
backup_path = mach->getBackupPath();
history_path = backup_path + "/HISTORY";
rebuild_path = backup_path + "/DV6";
// Compare QDirs rather than QStrings because separators may be different, especially on Windows.
QDir ipath(path);
QDir bpath(backup_path);
QDir hpath(history_path);
if (ipath == bpath) {
// Don't create backups if importing from backup folder
@ -2472,6 +2641,20 @@ bool init6Environment (const QString & path) {
} else {
rebuild_from_backups = false;
create_backups = p_profile->session->backupCardData();
if ( ! bpath.exists()) {
if ( ! bpath.mkpath(backup_path) ) {
qWarning() << "Could not create DV6 backup directory" << backup_path;
return false;
}
}
if ( ! hpath.exists()) {
if ( ! hpath.mkpath(history_path) ) {
qWarning() << "Could not create DV6 backup HISTORY directory" << history_path;
return false;
}
}
}
return true;
@ -2486,6 +2669,10 @@ int IntellipapLoader::OpenDV6(const QString & path)
qDebug() << "DV6 loader started";
card_path = path + DV6_DIR;
emit updateMessage(QObject::tr("Getting Ready..."));
emit setProgressValue(0);
QCoreApplication::processEvents();
// 1. Prime the machine database's info field with this machine
info = newInfo();
@ -2505,10 +2692,16 @@ int IntellipapLoader::OpenDV6(const QString & path)
if (!load6DailySummaries())
return -1;
emit updateMessage(QObject::tr("Backing up files..."));
QCoreApplication::processEvents();
// 6. Back up data files (must do after parsing VER.BIN, S.BIN, and creating Machine)
if (!backup6(path))
return -1;
emit updateMessage(QObject::tr("Reading data files..."));
QCoreApplication::processEvents();
// 7. U.BIN - Open and parse session list and create a list of session times
// (S.BIN must already be loaded)
if (!load6Sessions())
@ -2530,6 +2723,9 @@ int IntellipapLoader::OpenDV6(const QString & path)
if (!load6EventData())
return -1;
emit updateMessage(QObject::tr("Finishing up..."));
QCoreApplication::processEvents();
// Finalize input
return addSessions();
}
@ -2580,6 +2776,14 @@ void IntellipapLoader::initChannels()
QObject::tr("Intellipap pressure relief level."),
QObject::tr("SmartFlex Level"),
"", DEFAULT, Qt::green));
channel.add(GRP_CPAP, new Channel(INTP_SnoreFlag = 0xe301, FLAG, MT_CPAP, SESSION,
"INTP_SnoreFlag",
QObject::tr("Snore"),
QObject::tr("Snoring event."),
QObject::tr("SN"),
STR_UNIT_EventsPerHour, DEFAULT, QColor("#e20004")));
}
bool intellipap_initialized = false;

View File

@ -0,0 +1,111 @@
/* SleepLib SleepStyle Loader Implementation
*
* Copyright (c) 2020 The OSCAR Team
* Copyright (c) 2011-2018 Mark Watkins <mark@jedimark.net>
*
* This file is subject to the terms and conditions of the GNU General Public
* License. See the file COPYING in the main directory of the source code
* for more details. */
#include <QApplication>
#include <QString>
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QMessageBox>
#include <QTextStream>
#include <QDebug>
#include <QStringList>
#include <cmath>
#include "SleepLib/session.h"
#include "SleepLib/calcs.h"
#include "SleepLib/loader_plugins/sleepstyle_EDFinfo.h"
SleepStyleEDFInfo::SleepStyleEDFInfo() : EDFInfo() {
setTimeZoneUTC(); // Ask EDF Parser to assume data is in UTC, not in local time
}
SleepStyleEDFInfo::~SleepStyleEDFInfo() { }
bool SleepStyleEDFInfo::Parse( ) // overrides and calls the super's Parse
{
if ( ! EDFInfo::Parse( ) ) {
qWarning() << "sleepStyle EDFInfo::Parse failed!";
// sleep(1);
return false;
}
// Now massage some stuff into OSCAR's layout
// Extract the serial number from header string
QStringList parts = edfHdr.recordingident.split(' ');
serialnumber = parts[6];
if (!edfHdr.startdate_orig.isValid()) {
qDebug() << "sleepStyle EDFInfo::Parse Invalid date time retreieved parsing EDF File" << filename;
// sleep(1);
return false;
}
startdate = qint64(edfHdr.startdate_orig.toTime_t()) * 1000L;
//startdate-=timezoneOffset();
if (startdate == 0) {
qDebug() << "sleepStyle EDFInfo::Parse Invalid startdate = 0 in EDF File" << filename;
// sleep(1);
return false;
}
dur_data_record = (edfHdr.duration_Seconds * 1000.0L);
enddate = startdate + dur_data_record * qint64(edfHdr.num_data_records);
return true;
}
extern QHash<ChannelID, QStringList> resmed_codes;
// Looks up foreign language Signal names that match this channelID
EDFSignal *SleepStyleEDFInfo::lookupSignal(ChannelID ch)
{
// Get list of all known foreign language names for this channel
auto channames = resmed_codes.find(ch);
if (channames == resmed_codes.end()) {
// no alternatives strings found for this channel
return nullptr;
}
// This is bad, because ResMed thinks it was a cool idea to use two channels with the same name.
// Scan through EDF's list of signals to see if any match
for (auto & name : channames.value()) {
EDFSignal *sig = lookupLabel(name);
if (sig)
return sig;
}
// Failed
return nullptr;
}
QDateTime SleepStyleEDFInfo::getStartDT( QString dateTimeStr )
{
// edfHdr.startdate_orig = QDateTime::fromString(QString::fromLatin1(hdrPtr->datetime, 16), "dd.MM.yyHH.mm.ss");
// QString dateTimeStr; // , dateStr, timeStr;
QDate qDate;
QTime qTime;
// dateTimeStr = QString::fromLatin1(hdrPtr->datetime, 16);
// dateStr = dateTimeStr.left(8);
// timeStr = dateTimeStr.right(8);
qDate = QDate::fromString(dateTimeStr.left(8), "dd.MM.yy");
qTime = QTime::fromString(dateTimeStr.right(8), "HH.mm.ss");
return QDateTime(qDate, qTime, Qt::UTC);
}
void dumpEDFduration( ssEDFduration dur )
{
qDebug() << "Fullpath" << dur.path << "Filename" << dur.filename << "Start" << dur.start << "End" << dur.end;
}

View File

@ -0,0 +1,64 @@
/* SleepLib SleepStyle EDFinfo Header
*
* 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. */
#ifndef SLEEPSTYLE_EDFINFO_H
#define SLEEPSTYLE_EDFINFO_H
#include <QVector>
#include "SleepLib/machine.h" // Base class: MachineLoader
#include "SleepLib/machine_loader.h"
#include "SleepLib/profiles.h"
#include "SleepLib/loader_plugins/edfparser.h"
//enum EDFType { EDF_UNKNOWN, EDF_BRP, EDF_PLD, EDF_SAD, EDF_EVE, EDF_CSL, EDF_AEV };
enum EDFType { EDF_UNKNOWN, EDF_RT };
// EDFType lookupEDFType(const QString & filename);
const QString SLEEPSTYLE_class_name = STR_MACH_ResMed;
//class STRFile; // forward
class SleepStyleEDFInfo : public EDFInfo
{
public:
SleepStyleEDFInfo();
~SleepStyleEDFInfo();
virtual bool Parse() override; // overrides and calls the super's Parse
virtual qint64 GetDurationMillis() { return dur_data_record; } // overrides the super
EDFSignal *lookupSignal(ChannelID ch);
QDateTime getStartDT( QString dateTimeStr );
//! \brief The following are computed from the edfHdr data
QString serialnumber;
qint64 dur_data_record;
qint64 startdate;
qint64 enddate;
};
class ssEDFduration
{
public:
ssEDFduration() { start = end = 0; type = EDF_UNKNOWN; }
ssEDFduration(quint32 start, quint32 end, QString path) :
start(start), end(end), path(path) {}
quint32 start;
quint32 end;
QString path;
QString filename;
EDFType type;
};
void dumpEDFduration( ssEDFduration dur );
#endif // SLEEPSTYLE_EDFINFO_H

View File

@ -0,0 +1,936 @@
/* SleepLib Fisher & Paykel SleepStyle Loader Implementation
*
* Copyright (c) 2020 The Oscar Team
*
* Derived from icon_loader.cpp
* Copyright (c) 2011-2018 Mark Watkins <mark@jedimark.net>
*
* This file is subject to the terms and conditions of the GNU General Public
* License. See the file COPYING in the main directory of the source code
* for more details. */
#include <QDir>
#include <QMessageBox>
#include <QDataStream>
#include <QTextStream>
#include <QCoreApplication>
#include <cmath>
#include "sleepstyle_loader.h"
#include "sleepstyle_EDFinfo.h"
const QString FPHCARE = "FPHCARE";
SleepStyle::SleepStyle(Profile *profile, MachineID id)
: CPAP(profile, id)
{
}
SleepStyle::~SleepStyle()
{
}
SleepStyleLoader::SleepStyleLoader()
{
m_buffer = nullptr;
m_type = MT_CPAP;
}
SleepStyleLoader::~SleepStyleLoader()
{
}
/*
* getIconDir - returns the path to the ICON directory
*/
QString getIconDir (QString givenpath) {
QString path = givenpath;
path = path.replace("\\", "/");
if (path.endsWith("/")) {
path.chop(1);
}
if (path.endsWith("/" + FPHCARE)) {
path = path.section("/",0,-2);
}
QDir dir(path);
if (!dir.exists()) {
return "";
}
// If this is a backup directory, higher level directories have been
// omitted.
if (path.endsWith("/Backup/", Qt::CaseInsensitive))
return path;
// F&P Icon have a folder called FPHCARE in the root directory
if (!dir.exists(FPHCARE)) {
return "";
}
// CHECKME: I can't access F&P ICON data right now
if (!dir.exists("FPHCARE/ICON")) {
return "";
}
return dir.filePath("FPHCARE/ICON");
}
/*
* getSleepStyleMachines returns a list of all SleepStyle machine folders in the ICON directory
*/
QStringList getSleepStyleMachines (QString iconPath) {
QStringList ssMachines;
QDir iconDir (iconPath);
// SleepStyle are mixed alpha and numeric; ICON serial numbers (directory names) are all digits
iconDir.setFilter(QDir::NoDotAndDotDot | QDir::Dirs | QDir::Files | QDir::Hidden | QDir::NoSymLinks);
iconDir.setSorting(QDir::Name);
QFileInfoList flist = iconDir.entryInfoList(); // List of Icon subdirectories
bool isIconFilename;
// Walk though directory list and save those that appear to be for SleepStyle machins.
for (int i = 0; i < flist.size(); i++) {
QFileInfo fi = flist.at(i);
QString filename = fi.fileName();
filename.toInt(&isIconFilename);
if (isIconFilename) // Ignore this directory if named as used for older F&P Icon machine
continue;
if (filename.length() < 8) // F&P machine names are 8 characters long, but we allow more just in case...
continue;
// directory is serial number and must not be all digits (which would make it an ICON directory)
// and it must have *.FPH files within it to be a SleepStyle folder
QDir machineDir (iconPath + "/" + filename);
machineDir.setFilter(QDir::NoDotAndDotDot | QDir::Files | QDir::Hidden | QDir::NoSymLinks);
machineDir.setSorting(QDir::Name);
QStringList filters;
filters << "*.fph";
machineDir.setNameFilters(filters);
QFileInfoList flist = machineDir.entryInfoList();
if (flist.size() <= 0) {
continue;
}
ssMachines.push_back(filename);
}
return ssMachines;
}
bool SleepStyleLoader::Detect(const QString & givenpath)
{
QString iconPath = getIconDir(givenpath);
if (iconPath.isEmpty())
return false;
QStringList machines = getSleepStyleMachines(iconPath);
if (machines.length() <= 0)
// Did not find any SleepStyle machine directories
return false;
return true;
}
bool SleepStyleLoader::backupData (Machine * mach, const QString & path) {
QDir ipath(path);
QDir bpath(mach->getBackupPath());
// Compare QDirs rather than QStrings because separators may be different, especially on Windows.
if (ipath == bpath) {
// Don't create backups if importing from backup folder
rebuild_from_backups = true;
create_backups = false;
} else {
rebuild_from_backups = false;
create_backups = p_profile->session->backupCardData();
}
if (rebuild_from_backups || !create_backups)
return true;
// Copy input data to backup location
copyPath(ipath.absolutePath(), bpath.absolutePath());
return true;
}
int SleepStyleLoader::Open(const QString & path)
{
QString iconPath = getIconDir(path);
if (iconPath.isEmpty())
return false;
QStringList serialNumbers = getSleepStyleMachines(iconPath);
if (serialNumbers.length() <= 0)
// Did not find any SleepStyle machine directories
return false;
Machine *m;
int c = 0;
for (int i = 0; i < serialNumbers.size(); i++) {
MachineInfo info = newInfo();
info.serial = serialNumbers[i];
m = p_profile->CreateMachine(info);
setSerialPath(iconPath + "/" + info.serial);
try {
if (m) {
c+=OpenMachine(m, path, serialPath);
}
} catch (OneTypePerDay& e) {
Q_UNUSED(e)
p_profile->DelMachine(m);
MachList.erase(MachList.find(info.serial));
QMessageBox::warning(nullptr, tr("Import Error"),
tr("This Machine Record cannot be imported in this profile.")+"\n\n"+tr("The Day records overlap with already existing content."),
QMessageBox::Ok);
delete m;
}
}
return c;
}
int SleepStyleLoader::OpenMachine(Machine *mach, const QString & path, const QString & ssPath)
{
emit updateMessage(QObject::tr("Getting Ready..."));
emit setProgressValue(0);
QCoreApplication::processEvents();
QDir dir(ssPath);
if (!dir.exists() || (!dir.isReadable())) {
return -1;
}
backupData(mach, path);
qDebug() << "Opening F&P SleepStyle" << ssPath;
dir.setFilter(QDir::NoDotAndDotDot | QDir::Dirs | QDir::Files | QDir::Hidden | QDir::NoSymLinks);
dir.setSorting(QDir::Name);
QFileInfoList flist = dir.entryInfoList();
QString filename, fpath;
emit updateMessage(QObject::tr("Reading data files..."));
QCoreApplication::processEvents();
QStringList summary, det, his;
Sessions.clear();
for (int i = 0; i < flist.size(); i++) {
QFileInfo fi = flist.at(i);
filename = fi.fileName();
fpath = ssPath + "/" + filename;
if (filename.left(3).toUpper() == "SUM") {
summary.push_back(fpath);
OpenSummary(mach, fpath);
} else if (filename.left(3).toUpper() == "DET") {
det.push_back(fpath);
} else if (filename.left(3).toUpper() == "HIS") {
his.push_back(fpath);
}
}
for (int i = 0; i < det.size(); i++) {
OpenDetail(mach, det[i]);
}
// Process REALTIME files
dir.cd("REALTIME");
QFileInfoList rtlist = dir.entryInfoList();
for (int i = 0; i < rtlist.size(); i++) {
QFileInfo fi = rtlist.at(i);
filename = fi.fileName();
fpath = ssPath + "/REALTIME/" + filename;
if (filename.left(3).toUpper() == "HRD"
&& filename.right(3).toUpper() == "EDF" ) {
OpenRealTime (mach, filename, fpath);
}
}
// LOG files were not processed by icon_loader
// So we don't need to do anything
SessionID sid;//,st;
float hours, mins;
// For diagnostics, print summary of last 20 session or one week
qDebug() << "SS Loader - last 20 Sessions:";
int cnt = 0;
QDateTime dt;
QString a = "";
if (Sessions.size() > 0) {
QMap<SessionID, Session *>::iterator it = Sessions.end();
it--;
dt = QDateTime::fromTime_t(qint64(it.value()->first()) / 1000L);
QDate date = dt.date().addDays(-7);
it++;
do {
it--;
Session *sess = it.value();
sid = sess->session();
hours = sess->hours();
mins = hours * 60;
dt = QDateTime::fromTime_t(sid);
qDebug() << cnt << ":" << dt << "session" << sid << "," << mins << "minutes" << a;
if (dt.date() < date) {
break;
}
++cnt;
} while (it != Sessions.begin());
}
// qDebug() << "Unmatched Sessions";
// QList<FPWaveChunk> chunks;
// for (QMap<int,QDate>::iterator dit=FLWDate.begin();dit!=FLWDate.end();dit++) {
// int k=dit.key();
// //QDate date=dit.value();
//// QList<Session *> values = SessDate.values(date);
// for (int j=0;j<FLWTS[k].size();j++) {
// FPWaveChunk chunk(FLWTS[k].at(j),FLWDuration[k].at(j),k);
// chunk.flow=FLWMapFlow[k].at(j);
// chunk.leak=FLWMapLeak[k].at(j);
// chunk.pressure=FLWMapPres[k].at(j);
// chunks.push_back(chunk);
// zz=FLWTS[k].at(j)/1000;
// dur=double(FLWDuration[k].at(j))/60000.0;
// bool b,c=false;
// if (Sessions.contains(zz)) b=true; else b=false;
// if (b) {
// if (Sessions[zz]->channelDataExists(CPAP_FlowRate)) c=true;
// }
// qDebug() << k << "-" <<j << ":" << zz << qRound(dur) << "minutes" << (b ? "*" : "") << (c ? QDateTime::fromTime_t(zz).toString() : "");
// }
// }
// std::sort(chunks.begin(), chunks.end());
// bool b,c;
// for (int i=0;i<chunks.size();i++) {
// const FPWaveChunk & chunk=chunks.at(i);
// zz=chunk.st/1000;
// dur=double(chunk.duration)/60000.0;
// if (Sessions.contains(zz)) b=true; else b=false;
// if (b) {
// if (Sessions[zz]->channelDataExists(CPAP_FlowRate)) c=true;
// }
// qDebug() << chunk.file << ":" << i << zz << dur << "minutes" << (b ? "*" : "") << (c ? QDateTime::fromTime_t(zz).toString() : "");
// }
int c = Sessions.size();
qDebug() << "SS Loader found" << c << "sessions";
emit updateMessage(QObject::tr("Finishing up..."));
QCoreApplication::processEvents();
finishAddingSessions();
mach->Save();
return c;
}
// !\brief Convert F&P 32bit date format to 32bit UNIX Timestamp
quint32 ssconvertDate(quint32 timestamp)
{
quint16 day, month,hour=0, minute=0, second=0;
quint16 year;
day = timestamp & 0x1f;
month = (timestamp >> 5) & 0x0f;
year = 2000 + ((timestamp >> 9) & 0x3f);
quint32 ts2 = timestamp >> 15;
second = ts2 & 0x3f;
minute = (ts2 >> 6) & 0x3f;
hour = (ts2 >> 12);
QDateTime dt = QDateTime(QDate(year, month, day), QTime(hour, minute, second), Qt::UTC);
qDebug().noquote() << "SS timestamp" << timestamp << year << month << day << dt << hour << minute << second;
// Q NO!!! _ASSERT(dt.isValid());
// if ((year == 2013) && (month == 9) && (day == 18)) {
// // this is for testing.. set a breakpoint on here and
// int i=5;
// }
// From Rudd's data set compared to times reported from his F&P software's report (just the time bits left over)
// 90514 = 00:06:18 WET 23:06:18 UTC 09:06:18 AEST
// 94360 = 01:02:24 WET
// 91596 = 00:23:12 WET
// 19790 = 23:23:50 WET
return dt.addSecs(-54).toTime_t(); // Huh? Why do this?
}
// SessionID is in seconds, not msec
SessionID SleepStyleLoader::findSession (SessionID sid) {
for(auto sessKey : Sessions.keys())
{
Session * sess = Sessions.value(sessKey);
if (sid >= (sess->realFirst() / 1000L) && sid <= (sess->realLast() / 1000L))
return sessKey;
}
return 0;
}
bool SleepStyleLoader::OpenRealTime(Machine *mach, const QString & fname, const QString & filepath)
{
// Q_UNUSED(filepath)
Q_UNUSED(mach)
Q_UNUSED(fname)
SleepStyleEDFInfo edf;
// Open the EDF file and read contents into edf object
if (!edf.Open(filepath)) {
qWarning() << "SS Realtime failed to open" << filepath;
return false;
}
if (!edf.Parse()) {
qWarning() << "SS Realtime Parse failed to open" << filepath;
return false;
}
qDebug().noquote() << "SS ORT timestamp" << edf.startdate / 1000L << QDateTime::fromSecsSinceEpoch(edf.startdate / 1000L).toString("MM/dd/yyyy hh:mm:ss");
SessionID sessKey = findSession(edf.startdate / 1000L);
if (sessKey == 0) {
qWarning() << "SS ORT session not found";
return true;
}
Session * sess = Sessions.value(sessKey);
if (sess == nullptr) {
qWarning() << "SS ORT session not found - nullptr";
return true;
}
sess->updateFirst(edf.startdate);
qint64 duration = edf.GetNumDataRecords() * edf.GetDurationMillis();
sess->updateLast(edf.startdate + duration);
// Find the leak signal and data
long leakrecs = 0;
EDFSignal leakSignal;
for (auto & esleak : edf.edfsignals) {
leakrecs = esleak.sampleCnt * edf.GetNumDataRecords();
if (leakrecs < 0)
continue;
if (esleak.label == "Leak") {
leakSignal = esleak;
}
}
// Walk through all signals, ignoring leaks
for (auto & es : edf.edfsignals) {
long recs = es.sampleCnt * edf.GetNumDataRecords();
if (recs < 0)
continue;
ChannelID code = 0;
if (es.label == "Flow") {
// Flow data appears to include total leaks, which are also reported in the edf file.
// We subtract the leak from the flow data to get flow data that is centered around zero.
// This is needed for other derived graphs (tidal volume, insp and exp times, etc.) to be reasonable
code = CPAP_FlowRate;
bool done = false;
if (leakrecs > 0) {
for (int ileak = 0; ileak < leakrecs && !done; ileak++) {
for (int iflow = 0; iflow < 25 && !done; iflow++) {
if (ileak*25 + iflow >= recs) {
done = true;
break;
}
es.dataArray[ileak*25 + iflow] -= leakSignal.dataArray[ileak] - 500;
}
}
}
} else if (es.label == "Pressure") {
code = CPAP_MaskPressure;
} 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);
a->AddWaveform(edf.startdate, es.dataArray, recs, duration);
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);
}
}
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////
// Open Summary file, create list of sessions and session summary data
////////////////////////////////////////////////////////////////////////////////////////////
bool SleepStyleLoader::OpenSummary(Machine *mach, const QString & filename)
{
qDebug() << filename;
QByteArray header;
QFile file(filename);
if (!file.open(QFile::ReadOnly)) {
qDebug() << "SS SUM Couldn't open" << filename;
return false;
}
// Read header of summary file
header = file.read(0x200);
if (header.size() != 0x200) {
qDebug() << "SS SUM Short file" << filename;
file.close();
return false;
}
// Header is terminated by ';' at 0x1ff
unsigned char hterm = 0x3b;
if (hterm != header[0x1ff]) {
qWarning() << "SS SUM Header missing ';' terminator" << filename;
}
QTextStream htxt(&header);
QString h1, version, fname, serial, model, type, unknownident;
htxt >> h1;
htxt >> version;
htxt >> fname;
htxt >> serial;
htxt >> model; //TODO: Should become Series in machine info???
htxt >> type; // SPSAAN etc with 4th character being A (Auto) or C (CPAP)
htxt >> unknownident; // Constant, but has different value when version number is different.
qDebug() << "SS SUM header" << h1 << version << fname << serial << model << type << unknownident;
if (type.length() > 4)
type = (type.at(3) == 'C' ? "CPAP" : "Auto");
mach->setModel(model + " " + type);
// Read remainder of summary file
QByteArray data;
data = file.readAll();
file.close();
QDataStream in(data);
in.setVersion(QDataStream::Qt_4_8);
in.setByteOrder(QDataStream::LittleEndian);
quint32 ts;
//QByteArray line;
unsigned char p1, p2, p3, j1, x1, x2;
unsigned char runTime, useTime, minPressSet, maxPressSet, minPressSeen, pct95PressSeen, maxPressSeen;
unsigned char senseAwakeLevel, humidityLevel, smartFlexLevel;
quint16 c1, c2, c3, c4;
// quint16 d1, d2, d3;
unsigned char d1, d2, d3, d4, d5, d6;
int usage; //,runtime;
QDate date;
int nblock = 0;
// Go through blocks of data until end marker is found
do {
nblock++;
in >> ts;
if (ts == 0xffffffff) {
qDebug() << "SS SUM 0xffffffff terminator found at block" << nblock;
break;
}
if ((ts & 0xffff) == 0xfafe) {
qDebug() << "SS SUM 0xfafa terminator found at block" << nblock;
break;
}
ts = ssconvertDate(ts);
qDebug() << "\nSS SUM Session" << nblock << "with timestamp" << ts << QDateTime::fromSecsSinceEpoch(ts).toString("MM/dd/yyyy hh:mm:ss");
// the following two quite often match in value
in >> runTime; // 0x04
in >> useTime; // 0x05
usage = useTime * 360; // Convert to seconds (durations are in .1 hour intervals)
in >> minPressSeen; // 0x06
in >> pct95PressSeen; // 0x07
in >> maxPressSeen; // 0x08
in >> d1; // 0x09
in >> d2; // 0x0a
in >> d3; // 0x0b
in >> d4; // 0x0c
in >> d5; // 0x0d
in >> d6; // 0x0e
in >> c1; // 0x0f
in >> c2; // 0x11
in >> c3; // 0x13
in >> c4; // 0x15
in >> j1; // 0x17
in >> p1; // 0x18
in >> p2; // 0x19
in >> p3; // 0x1a
in >> x1; // 0x1b
in >> x2; // 0x1c
in >> minPressSet;
in >> maxPressSet;
in >> senseAwakeLevel;
in >> humidityLevel;
in >> smartFlexLevel;
// soak up unknown stuff to apparent end of data for the day
unsigned char s [6];
for (unsigned int i=0; i < sizeof(s); i++)
in >> s[i];
qDebug() << "SS SUM block" << nblock
<< "a:" <<"Pressure Min"<<minPressSeen<<"95%"<<pct95PressSeen<<"Max"<<maxPressSeen
<< "\nd:" <<d1<<d2<<d3<<d4<<d5<<d6
<< "\nj:" <<j1 << " c:" << c1 << c2 << c3 << c4
<< "\np:" <<p1<<p2<<p3
<< "\nx:" <<x1<<x2
<< "\ns:" <<"Min set" <<minPressSet<<"Max set"<<maxPressSet<<"SA"<<senseAwakeLevel<<"Humid"<<humidityLevel<<"SmartFlex"<<smartFlexLevel<<s[0]<<s[1]<<s[2]<<s[3]<<s[4]<<s[5];
if (runTime != useTime) {
qDebug() << "SS SUM run time" << runTime << "!= use time" << useTime << "-" << nblock << QDateTime::fromSecsSinceEpoch(ts).toString("MM/dd/yyyy hh:mm:ss");
}
if (!mach->SessionExists(ts)) {
Session *sess = new Session(mach, ts);
sess->really_set_first(qint64(ts) * 1000L);
sess->really_set_last(qint64(ts + usage) * 1000L);
sess->SetChanged(true);
/****
// TODO: None of the apnea numbers have been confirmed
sess->setCount(CPAP_Obstructive, c3);
// sess->setCph(CPAP_Obstructive, c3 / (float(usage)/3600.00));
sess->setCount(CPAP_Hypopnea, c4);
// sess->setCph(CPAP_Hypopnea, c4 / (float(usage)/3600.00));
sess->setCount(CPAP_ClearAirway, c1);
// sess->setCph(CPAP_ClearAirway, c1 / (float(usage)/3600.00));
sess->setCount(CPAP_FlowLimit, c2);
// sess->setCph(CPAP_FlowLimit, c2 / (float(usage)/3600.00));
****/
SessDate.insert(date, sess);
if (minPressSet != maxPressSet) {
sess->settings[CPAP_Mode] = (int)MODE_APAP;
sess->settings[CPAP_PressureMin] = minPressSet / 10.0;
sess->settings[CPAP_PressureMax] = maxPressSet / 10.0;
} else {
sess->settings[CPAP_Mode] = (int)MODE_CPAP;
sess->settings[CPAP_Pressure] = minPressSet / 10.0;
}
sess->settings[CPAP_HumidSetting] = humidityLevel;
sess->settings[SS_SenseAwakeLevel] = senseAwakeLevel / 10.0;
sess->settings[CPAP_PresReliefMode] = PR_SMARTFLEX;
sess->settings[SS_SmartFlexLevel] = smartFlexLevel / 1.0;
Sessions[ts] = sess;
addSession(sess);
}
} while (!in.atEnd());
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////
// Open Detail record contains list of sessions and pressure, leak, and event flags
////////////////////////////////////////////////////////////////////////////////////////////
bool SleepStyleLoader::OpenDetail(Machine *mach, const QString & filename)
{
Q_UNUSED(mach);
qDebug() << "SS DET Opening Detail" << filename;
QByteArray header;
QFile file(filename);
if (!file.open(QFile::ReadOnly)) {
qDebug() << "SS DET Couldn't open" << filename;
return false;
}
header = file.read(0x200);
if (header.size() != 0x200) {
qDebug() << "SS DET short file" << filename;
file.close();
return false;
}
// Header is terminated by ';' at 0x1ff
unsigned char hterm = 0x3b;
if (hterm != header[0x1ff]) {
file.close();
qWarning() << "SS DET Header missing ';' terminator" << filename;
return false;
}
QTextStream htxt(&header);
QString h1, version, fname, serial, model, type, unknownident;
htxt >> h1;
htxt >> version;
htxt >> fname;
htxt >> serial;
htxt >> model; //TODO: Should become Series in machine info???
htxt >> type; // SPSAAN etc with 4th character being A (Auto) or C (CPAP)
htxt >> unknownident; // Constant, but has different value when version number is different.
qDebug() << "SS DET file header" << h1 << version << fname << serial << model << type << unknownident;
// Read session indices
QByteArray index = file.read(0x800);
if (index.size()!=0x800) {
// faulty file..
qWarning() << "SS DET file short index block";
file.close();
return false;
}
QDataStream in(index);
quint32 ts;
in.setVersion(QDataStream::Qt_4_6);
in.setByteOrder(QDataStream::LittleEndian);
QVector<quint32> times;
QVector<quint16> start;
QVector<quint8> records;
quint16 strt;
quint8 recs;
quint16 unknownIndex;
int totalrecs = 0;
do {
// Read timestamp for session and check for end of data signal
in >> ts;
if (ts == 0xffffffff) break;
if ((ts & 0xffff) == 0xfafe) break;
ts = ssconvertDate(ts);
in >> strt;
in >> recs;
in >> unknownIndex;
totalrecs += recs; // Number of data records for this session
qDebug().noquote() << "SS DET block timestamp" << ts << QDateTime::fromSecsSinceEpoch(ts).toString("MM/dd/yyyy hh:mm:ss") << "start" << strt << "records" << recs << "unknown" << unknownIndex;
if (Sessions.contains(ts)) {
times.push_back(ts);
start.push_back(strt);
records.push_back(recs);
}
else
qDebug() << "SS DET session not found" << ts;
} while (!in.atEnd());
QByteArray databytes = file.readAll();
file.close();
in.setVersion(QDataStream::Qt_4_6);
in.setByteOrder(QDataStream::BigEndian);
// 7 (was 5) byte repeating patterns
quint8 *data = (quint8 *)databytes.data();
qint64 ti;
quint8 pressure, leak, a1, a2, a3, a4, a5, a6, a7;
// quint8 sa1, sa2; // The two sense awake bits per 2 minutes
SessionID sessid;
Session *sess;
int idx;
for (int r = 0; r < start.size(); r++) {
sessid = times[r];
sess = Sessions[sessid];
ti = qint64(sessid) * 1000L;
sess->really_set_first(ti);
EventList *LK = sess->AddEventList(CPAP_LeakTotal, EVL_Event, 1);
EventList *PR = sess->AddEventList(CPAP_Pressure, EVL_Event, 0.1F);
// EventList *OA = sess->AddEventList(CPAP_Obstructive, EVL_Event);
EventList *H = sess->AddEventList(CPAP_Hypopnea, EVL_Event);
EventList *FL = sess->AddEventList(CPAP_FlowLimit, EVL_Event);
EventList *SA = sess->AddEventList(CPAP_SensAwake, EVL_Event);
// EventList *CA = sess->AddEventList(CPAP_ClearAirway, EVL_Event);
EventList *UA = sess->AddEventList(CPAP_Apnea, EVL_Event);
// For testing to determine which bit is for which event type:
// EventList *UF1 = sess->AddEventList(CPAP_UserFlag1, EVL_Event);
// EventList *UF2 = sess->AddEventList(CPAP_UserFlag2, EVL_Event);
unsigned stidx = start[r];
int rec = records[r];
idx = stidx * 21; // Each record has three blocks of 7 bytes for 21 bytes total
quint8 bitmask;
for (int i = 0; i < rec; ++i) {
for (int j = 0; j < 3; ++j) {
pressure = data[idx];
PR->AddEvent(ti/*+120000*/, pressure);
leak = data[idx + 1];
LK->AddEvent(ti/*+120000*/, leak);
// Comments below from MW. Appear not to be accurate
a1 = data[idx + 2]; // [0..5] Obstructive flag, [6..7] Unknown
a2 = data[idx + 3]; // [0..5] Hypopnea, [6..7] Unknown
a3 = data[idx + 4]; // [0..5] Flow Limitation, [6..7] Unknown
a4 = data[idx + 5]; // [0..5] UF1, [6..7] Unknown
a5 = data[idx + 6]; // [0..5] UF2, [6..7] Unknown
// Sure there isn't 6 SenseAwake bits?
a6 = (a3 >> 6) << 4 | ((a4 >> 6) << 2) | (a5 >> 6);
// this does the same thing as behaviour
// a6 = (a3 >> 7) << 3 | ((a3 >> 6) & 1);
a7 = (a1 >> 6) | (a2 >> 6); // Are these bits used?
bitmask = 1;
for (int k = 0; k < 6; k++) { // There are 6 flag sets per 2 minutes
if (a1 & bitmask) { UA->AddEvent(ti+60000, 0); }
if (a2 & bitmask) { UA->AddEvent(ti+60000, 0); } // may be CA?
if (a3 & bitmask) { H->AddEvent(ti+60000, 0); }
if (a4 & bitmask) { H->AddEvent(ti+60000, 0); } // may be OA?
if (a5 & bitmask) { FL->AddEvent(ti+60000, 0); }
if (a6 & bitmask) { SA->AddEvent(ti+60000, 0); }
bitmask <<= 1;
ti += 20000L; // Increment 20 seconds
}
// Debug print non-zero flags
if (a1 != 0 || a2 != 0 || a3 != 0 || a4 != 0 || a5 != 0 || a6 != 0 || a7 != 0) {
qDebug() << "SS DET events" << QDateTime::fromSecsSinceEpoch(ti/1000).toString("MM/dd/yyyy hh:mm:ss")
<< "pressure" << pressure
<< "leak" << leak
<< "flags" << a1 << a2 << a3 << a4 << a5 << a6 << "unknown" << a7;
}
idx += 7; //was 5;
}
}
// Update indexes, process waveform and perform flagging
sess->UpdateSummaries();
// sess->really_set_last(ti-360000L);
// sess->SetChanged(true);
// addSession(sess,profile);
}
return 1;
}
void SleepStyleLoader::initChannels()
{
using namespace schema;
Channel * chan = nullptr;
/****
channel.add(GRP_CPAP, chan = new Channel(INTP_SmartFlexMode = 0x1165, SETTING, MT_CPAP, SESSION,
"INTPSmartFlexMode", QObject::tr("SmartFlex Mode"),
QObject::tr("Pressure relief mode."),
QObject::tr("SmartFlex Mode"),
"", DEFAULT, Qt::green));
chan->addOption(0, STR_TR_Off);
****/
channel.add(GRP_CPAP, chan = new Channel(SS_SmartFlexLevel = 0xf304, SETTING, MT_CPAP, SESSION,
"SSSmartFlexLevel", QObject::tr("SmartFlex Level"),
QObject::tr("Exhalation pressure relief level."),
QObject::tr("SmartFlex"),
"", LOOKUP, Qt::green));
chan->addOption(0, STR_TR_Off);
channel.add(GRP_CPAP, new Channel(SS_SenseAwakeLevel = 0xf305, SETTING, MT_CPAP, SESSION,
"SS_SenseAwakeLevel",
QObject::tr("SenseAwake level"),
QObject::tr("SenseAwake level"),
QObject::tr("SenseAwake"),
STR_UNIT_CMH2O, LOOKUP, Qt::black));
}
bool sleepstyle_initialized = false;
void SleepStyleLoader::Register()
{
if (sleepstyle_initialized) { return; }
qDebug() << "Registering F&P Sleepstyle Loader";
RegisterLoader(new SleepStyleLoader());
//InitModelMap();
sleepstyle_initialized = true;
}

View File

@ -0,0 +1,129 @@
/* SleepLib Fisher & Paykel SleepStyle Loader Implementation
*
* Copyright (c) 2020 The Oscar Team (info@oscar-team.org)
* 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. */
#ifndef SLEEPSTYLE_LOADER_H
#define SLEEPSTYLE_LOADER_H
#include <QMultiMap>
#include "SleepLib/machine.h"
#include "SleepLib/machine_loader.h"
#include "SleepLib/profiles.h"
//********************************************************************************************
/// IMPORTANT!!!
//********************************************************************************************
// Please INCREMENT the following value when making changes to this loaders implementation.
//
const int sleepstyle_data_version = 1;
//
//********************************************************************************************
/*! \class SleepStyle
\brief F&P SleepStyle customized machine object
*/
class SleepStyle: public CPAP
{
public:
SleepStyle(Profile *, MachineID id = 0);
virtual ~SleepStyle();
};
const int sleepstyle_load_buffer_size = 1024 * 1024;
extern ChannelID INTP_SmartFlexMode;
extern ChannelID SS_SmartFlexLevel;
extern ChannelID SS_SenseAwakeLevel;
const QString sleepstyle_class_name = STR_MACH_SleepStyle;
/*! \class SleepStyleLoader
\brief Loader for Fisher & Paykel SleepStyle data
This is only relatively recent addition and still needs more work
*/
class SleepStyleLoader : public CPAPLoader
{
Q_OBJECT
public:
SleepStyleLoader();
virtual ~SleepStyleLoader();
//! \brief Detect if the given path contains a valid Folder structure
virtual bool Detect(const QString & path);
//! \brief Scans path for F&P SleepStyle data signature, and Loads any new data
virtual int Open(const QString & path);
int OpenMachine(Machine *mach, const QString & path, const QString & ssPath);
bool OpenSummary(Machine *mach, const QString & path);
bool OpenDetail(Machine *mach, const QString & path);
// bool OpenFLW(Machine *mach, const QString & filename);
bool OpenRealTime(Machine *mach, const QString & fname, const QString & filename);
//! \brief Returns SleepLib database version of this F&P SleepStyle loader
virtual int Version() { return sleepstyle_data_version; }
//! \brief Returns the machine class name of this CPAP machine, "SleepStyle"
virtual const QString & loaderName() { return sleepstyle_class_name; }
// ! \brief Creates a machine object, indexed by serial number
//Machine *CreateMachine(QString serial);
QString getSerialPath () {return serialPath;}
void setSerialPath (QString sp) {serialPath = sp;}
bool backupData (Machine * mach, const QString & path);
SessionID findSession (SessionID sid);
void initChannels();
virtual MachineInfo newInfo() {
return MachineInfo(MT_CPAP, 0, sleepstyle_class_name, QObject::tr("Fisher & Paykel"), QString(), QString(), QString(), QObject::tr("SleepStyle"), QDateTime::currentDateTime(), sleepstyle_data_version);
}
//! \brief Registers this MachineLoader with the master list, so F&P Icon data can load
static void Register();
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Now for some CPAPLoader overrides
////////////////////////////////////////////////////////////////////////////////////////////////////////////
virtual QString presRelType() { return QObject::tr(""); } // might not need this one
virtual ChannelID presRelSet() { return NoChannel; }
virtual ChannelID presRelLevel() { return NoChannel; }
////////////////////////////////////////////////////////////////////////////////////////////////////////////
protected:
// QDateTime readFPDateTime(quint8 *data);
QString last;
QHash<QString, Machine *> MachList;
QMap<SessionID, Session *> Sessions;
QMultiMap<QDate, Session *> SessDate;
//QMap<int,QList<EventList *> > FLWMapFlow;
//QMap<int,QList<EventList *> > FLWMapLeak;
//QMap<int,QList<EventList *> > FLWMapPres;
//QMap<int,QList<qint64> > FLWDuration;
//QMap<int,QList<qint64> > FLWTS;
//QMap<int,QDate> FLWDate;
QString serialPath; // fully qualified path to the input data, ...SDCard.../FPHCARE/ICON/serial
// QString serial; // Serial number
bool rebuild_from_backups = false;
bool create_backups = true;
unsigned char *m_buffer;
};
#endif // SLEEPSTYLE_LOADER_H

View File

@ -28,12 +28,14 @@ ChannelID CPAP_IPAP, CPAP_IPAPLo, CPAP_IPAPHi, CPAP_EPAP, CPAP_EPAPLo, CPAP_EPAP
ChannelID RMS9_E01, RMS9_E02, RMS9_SetPressure, RMS9_MaskOnTime;
ChannelID INTELLIPAP_Unknown1, INTELLIPAP_Unknown2;
ChannelID INTELLIPAP_Unknown1, INTELLIPAP_Unknown2, INTP_SnoreFlag;
ChannelID CPAP_LargeLeak,
PRS1_BND, PRS1_FlexMode, PRS1_FlexLevel, PRS1_HumidStatus, PRS1_HumidLevel, PRS1_HumidTargetTime, PRS1_MaskResistLock,
PRS1_MaskResistSet, PRS1_HoseDiam, PRS1_AutoOn, PRS1_AutoOff, PRS1_MaskAlert, PRS1_ShowAHI;
ChannelID SS_SenseAwakeLevel, SS_SmartFlexLevel;
ChannelID OXI_Pulse, OXI_SPO2, OXI_Perf, OXI_PulseChange, OXI_SPO2Drop, OXI_Plethy;
ChannelID Journal_Notes, Journal_Weight, Journal_BMI, Journal_ZombieMeter, LastUpdated,

View File

@ -164,7 +164,9 @@ extern ChannelID CPAP_LargeLeak, PRS1_BND,
CPAP_HumidSetting,
PRS1_MaskResistSet, PRS1_HoseDiam, PRS1_AutoOn, PRS1_AutoOff, PRS1_MaskAlert, PRS1_ShowAHI;
extern ChannelID INTELLIPAP_Unknown1, INTELLIPAP_Unknown2;
extern ChannelID INTELLIPAP_Unknown1, INTELLIPAP_Unknown2, INTP_SnoreFlag;
extern ChannelID SS_SenseAwakeLevel, SS_SmartFlexLevel;
extern ChannelID OXI_Pulse, OXI_SPO2, OXI_Perf, OXI_PulseChange, OXI_SPO2Drop, OXI_Plethy;

View File

@ -1,4 +1,4 @@
// Update the string below to set OSCAR's version and release status.
// See https://semver.org/spec/v2.0.0.html for details on format.
#define VERSION "1.2.1-alpha.0"
#define VERSION "1.2.1-alpha.1"

View File

@ -43,6 +43,7 @@
#include "SleepLib/loader_plugins/resmed_loader.h"
#include "SleepLib/loader_plugins/intellipap_loader.h"
#include "SleepLib/loader_plugins/icon_loader.h"
#include "SleepLib/loader_plugins/sleepstyle_loader.h"
#include "SleepLib/loader_plugins/weinmann_loader.h"
#include "SleepLib/loader_plugins/viatom_loader.h"
@ -666,6 +667,7 @@ int main(int argc, char *argv[]) {
ResmedLoader::Register();
IntellipapLoader::Register();
FPIconLoader::Register();
SleepStyleLoader::Register();
WeinmannLoader::Register();
CMS50Loader::Register();
CMS50F37Loader::Register();

View File

@ -1080,6 +1080,7 @@ QList<ImportPath> MainWindow::selectCPAPDataCards(const QString & prompt)
w.setDirectory(folder);
w.setFileMode(QFileDialog::Directory);
w.setOption(QFileDialog::ShowDirsOnly, true);
w.setWindowTitle(tr("Find your CPAP data card"));
// This doesn't work on WinXP

View File

@ -295,6 +295,8 @@ SOURCES += \
SleepLib/loader_plugins/cms50_loader.cpp \
SleepLib/loader_plugins/dreem_loader.cpp \
SleepLib/loader_plugins/icon_loader.cpp \
SleepLib/loader_plugins/sleepstyle_loader.cpp \
SleepLib/loader_plugins/sleepstyle_EDFinfo.cpp \
SleepLib/loader_plugins/intellipap_loader.cpp \
SleepLib/loader_plugins/mseries_loader.cpp \
SleepLib/loader_plugins/prs1_loader.cpp \
@ -380,6 +382,8 @@ HEADERS += \
SleepLib/loader_plugins/cms50_loader.h \
SleepLib/loader_plugins/dreem_loader.h \
SleepLib/loader_plugins/icon_loader.h \
SleepLib/loader_plugins/sleepstyle_loader.h \
SleepLib/loader_plugins/sleepstyle_EDFinfo.h \
SleepLib/loader_plugins/intellipap_loader.h \
SleepLib/loader_plugins/mseries_loader.h \
SleepLib/loader_plugins/prs1_loader.h \