Merge branch 'master' into Legacy

This commit is contained in:
Seeker4 2019-05-31 16:04:33 -07:00
commit a2232d4b61
7 changed files with 2194 additions and 1130 deletions

File diff suppressed because it is too large Load Diff

View File

@ -28,7 +28,7 @@
const int prs1_data_version = 15;
//
//********************************************************************************************
#if 0 // Apparently unused
/*! \class PRS1
\brief PRS1 customized machine object (via CPAP)
*/
@ -41,6 +41,7 @@ class PRS1: public CPAP
const int max_load_buffer_size = 1024 * 1024;
#endif
const QString prs1_class_name = STR_MACH_PRS1;
/*! \struct PRS1Waveform
@ -77,13 +78,13 @@ public:
m_index = -1;
}
PRS1DataChunk(class QFile & f);
~PRS1DataChunk() {
}
~PRS1DataChunk();
inline int size() const { return m_data.size(); }
QByteArray m_header;
QByteArray m_data;
QByteArray m_headerblock;
QList<class PRS1ParsedEvent*> m_parsedData;
QString m_path;
qint64 m_filepos; // file offset
@ -108,6 +109,9 @@ public:
// V3 normal/non-waveform fields
QMap<unsigned char, short> hblock;
QMap<unsigned char, QByteArray> mainblock;
QMap<unsigned char, QByteArray> hbdata;
// Trailing common fields
quint8 storedChecksum; // header checksum stored in file, last byte of m_header
@ -124,7 +128,58 @@ public:
//! \brief Read the chunk's data from a PRS1 file and calculate its CRC, must be called after ReadHeader
bool ReadData(class QFile & f);
//! \brief Parse a single data chunk from a .000 file containing compliance data for a brick
bool ParseCompliance(void);
//! \brief Figures out which Summary Parser to call, based on machine family/version and calls it.
bool ParseSummary();
//! \brief Parse a single data chunk from a .001 file containing summary data for a family 0 CPAP/APAP family version 2 or 3 machine
bool ParseSummaryF0V23(void);
//! \brief Parse a single data chunk from a .001 file containing summary data for a family 0 CPAP/APAP family version 4 machine
bool ParseSummaryF0V4(void);
//! \brief Parse a single data chunk from a .001 file containing summary data for a family 0 CPAP/APAP family version 6 machine
bool ParseSummaryF0V6(void);
//! \brief Parse a single data chunk from a .001 file containing summary data for a family 3 ventilator (family version 6?) machine
bool ParseSummaryF3(void);
//! \brief Parse a single data chunk from a .001 file containing summary data for a family 5 ASV family version 0-2 machine
bool ParseSummaryF5V012(void);
//! \brief Parse a single data chunk from a .001 file containing summary data for a family 5 ASV family version 3 machine
bool ParseSummaryF5V3(void);
//! \brief Parse a flex setting byte from a .000 or .001 containing compliance/summary data
void ParseFlexSetting(quint8 flex, CPAPMode cpapmode);
//! \brief Parse an humidifier setting byte from a .000 or .001 containing compliance/summary data
void ParseHumidifierSetting(int humid, bool supportsHeatedTubing=true);
//! \brief Figures out which Event Parser to call, based on machine family/version and calls it.
bool ParseEvents(CPAPMode mode);
//! \brief Parse a single data chunk from a .002 file containing event data for a family 0 CPAP/APAP machine
bool ParseEventsF0(CPAPMode mode);
//! \brief Parse a single data chunk from a .002 file containing event data for a family 3 ventilator family version 3 machine
bool ParseEventsF3V3(void);
//! \brief Parse a single data chunk from a .002 file containing event data for a family 3 ventilator family version 6 machine
bool ParseEventsF3V6(void);
//! \brief Parse a single data chunk from a .002 file containing event data for a family 5 ASV family version 0-2 machine
bool ParseEventsF5V012(void);
//! \brief Parse a single data chunk from a .002 file containing event data for a family 5 ASV family version 3 machine
bool ParseEventsF5V3(void);
protected:
//! \brief Add a parsed event to the chunk
void AddEvent(class PRS1ParsedEvent* event);
//! \brief Read and parse the non-waveform header data from a V2 PRS1 file
bool ReadNormalHeaderV2(class QFile & f);
@ -138,6 +193,13 @@ protected:
bool ExtractStoredCrc(int size);
};
#if UNITTEST_MODE
QString _PRS1ParsedEventName(PRS1ParsedEvent* e);
QMap<QString,QString> _PRS1ParsedEventContents(PRS1ParsedEvent* e);
#endif
class PRS1Loader;
/*! \class PRS1Import
@ -167,9 +229,6 @@ public:
QList<PRS1DataChunk *> waveforms;
QList<PRS1DataChunk *> oximetry;
QMap<unsigned char, QByteArray> mainblock;
QMap<unsigned char, QByteArray> hbdata;
QString wavefile;
QString oxifile;
@ -177,8 +236,8 @@ public:
//! \brief As it says on the tin.. Parses .001 files for bricks.
bool ParseCompliance();
//! \brief Figures out which Summary Parser to call, based on machine family/version and calls it.
bool ParseSummary();
//! \brief Imports the .002 summary file.
bool ImportSummary();
//! \brief Figures out which Event Parser to call, based on machine family/version and calls it.
bool ParseEvents();
@ -193,24 +252,6 @@ public:
bool ParseOximetery();
//! \brief Summary parser for 50 series Family 0 CPAP/APAP models
bool ParseSummaryF0();
//! \brief Summary parser for 60 series Family 0 CPAP/APAP models
bool ParseSummaryF0V4();
//! \brief Summary parser for 1060 series AVAPS models
bool ParseSummaryF3();
//! \brief Summary parser for 50 series Family 5-0 BiPAP/AutoSV models
bool ParseSummaryF5V0();
//! \brief Summary parser for 60 series Family 5-1 BiPAP/AutoSV models
bool ParseSummaryF5V1();
//! \brief Summary parser for 60 series Family 5-2 BiPAP/AutoSV models
bool ParseSummaryF5V2();
//! \brief Summary parser for 60 series Family 5-3 BiPAP/AutoSV models
bool ParseSummaryF5V3();
//! \brief Summary parser for DreamStation series CPAP/APAP models
bool ParseSummaryF0V6();
//! \brief Parse a single data chunk from a .002 file containing event data for a standard system one machine
bool ParseF0Events();
//! \brief Parse a single data chunk from a .002 file containing event data for a AVAPS 1060P machine
@ -250,6 +291,9 @@ class PRS1Loader : public CPAPLoader
//! \brief Examine path and return it back if it contains what looks to be a valid PRS1 SD card structure
QString checkDir(const QString & path);
//! \brief Peek into PROP.TXT or properties.txt at given path, and return it as a normalized key/value hash
bool PeekProperties(const QString & filename, QHash<QString,QString> & props);
//! \brief Peek into PROP.TXT or properties.txt at given path, and use it to fill MachineInfo structure
bool PeekProperties(MachineInfo & info, const QString & path, Machine * mach = nullptr);
@ -337,4 +381,21 @@ class PRS1Loader : public CPAPLoader
qint32 summary_duration;
};
//********************************************************************************************
class PRS1ModelInfo
{
protected:
QHash<int, QHash<int, QStringList>> m_testedModels;
public:
PRS1ModelInfo();
bool IsSupported(const QHash<QString,QString> & properties) const;
bool IsSupported(int family, int familyVersion) const;
bool IsTested(const QHash<QString,QString> & properties) const;
bool IsTested(const QString & modelNumber, int family, int familyVersion) const;
};
#endif // PRS1LOADER_H

View File

@ -1,5 +1,6 @@
/* SleepLib Machine Class Implementation
/* SleepLib Machine Class Implementation
*
* Copyright (c) 2019 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
@ -45,6 +46,7 @@ Machine::Machine(Profile *_profile, MachineID id) : profile(_profile)
day.clear();
highest_sessionid = 0;
m_unsupported = false;
m_untested = false;
if (!id) {
srand(time(nullptr));

View File

@ -1,5 +1,6 @@
/* SleepLib Machine Class Header
/* SleepLib Machine Class Header
*
* Copyright (c) 2019 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
@ -184,9 +185,12 @@ class Machine
QSemaphore *savelistSem;
bool m_unsupported;
bool m_untested;
bool unsupported() { return m_unsupported; }
void setUnsupported(bool b) { m_unsupported = b; }
bool untested() { return m_untested; }
void setUntested(bool b) { m_untested = b; }
void lockSaveMutex() { listMutex.lock(); }
void unlockSaveMutex() { listMutex.unlock(); }

View File

@ -33,6 +33,47 @@ void PRS1Tests::cleanupTestCase(void)
}
// ====================================================================================================
extern PRS1ModelInfo s_PRS1ModelInfo;
void PRS1Tests::testMachineSupport()
{
QHash<QString,QString> tested = {
{ "ModelNumber", "550P" },
{ "Family", "0" },
{ "FamilyVersion", "3" },
};
QHash<QString,QString> supported = {
{ "ModelNumber", "700X999" },
{ "Family", "0" },
{ "FamilyVersion", "6" },
};
QHash<QString,QString> unsupported = {
{ "ModelNumber", "550P" },
{ "Family", "0" },
{ "FamilyVersion", "9" },
};
Q_ASSERT(s_PRS1ModelInfo.IsSupported(5, 3));
Q_ASSERT(!s_PRS1ModelInfo.IsSupported(5, 9));
Q_ASSERT(!s_PRS1ModelInfo.IsSupported(9, 9));
Q_ASSERT(s_PRS1ModelInfo.IsTested("550P", 0, 2));
Q_ASSERT(s_PRS1ModelInfo.IsTested("550P", 0, 3));
Q_ASSERT(s_PRS1ModelInfo.IsTested("760P", 0, 4));
Q_ASSERT(s_PRS1ModelInfo.IsTested("700X110", 0, 6));
Q_ASSERT(!s_PRS1ModelInfo.IsTested("700X999", 0, 6));
Q_ASSERT(s_PRS1ModelInfo.IsTested(tested));
Q_ASSERT(!s_PRS1ModelInfo.IsTested(supported));
Q_ASSERT(s_PRS1ModelInfo.IsSupported(tested));
Q_ASSERT(s_PRS1ModelInfo.IsSupported(supported));
Q_ASSERT(!s_PRS1ModelInfo.IsSupported(unsupported));
}
// ====================================================================================================
void parseAndEmitSessionYaml(const QString & path)
{
qDebug() << path;
@ -84,13 +125,16 @@ static QString ts(qint64 msecs)
return QDateTime::fromMSecsSinceEpoch(msecs).toString(Qt::ISODate);
}
static QString byteList(QByteArray data)
static QString byteList(QByteArray data, int limit=-1)
{
int count = data.size();
if (limit == -1 || limit > count) limit = count;
QStringList l;
for (int i = 0; i < data.size(); i++) {
for (int i = 0; i < limit; i++) {
l.push_back(QString( "%1" ).arg((int) data[i] & 0xFF, 2, 16, QChar('0') ).toUpper());
}
QString s = l.join("");
if (limit < count) l.push_back("...");
QString s = l.join(" ");
return s;
}
@ -139,7 +183,36 @@ void ChunkToYaml(QFile & file, PRS1DataChunk* chunk)
}
// data
out << " data: " << byteList(chunk->m_data) << endl;
bool dump_data = true;
if (chunk->m_parsedData.size() > 0) {
dump_data = false;
out << " events:" << endl;
for (auto & e : chunk->m_parsedData) {
QString name = _PRS1ParsedEventName(e);
if (name == "raw" || name == "unknown") {
dump_data = true;
}
QMap<QString,QString> contents = _PRS1ParsedEventContents(e);
if (name == "setting" && contents.size() == 1) {
out << " - set_" << contents.firstKey() << ": " << contents.first() << endl;
}
else {
out << " - " << name << ":" << endl;
// Always emit start first if present
if (contents.contains("start")) {
out << " " << "start" << ": " << contents["start"] << endl;
}
for (auto & key : contents.keys()) {
if (key == "start") continue;
out << " " << key << ": " << contents[key] << endl;
}
}
}
}
if (dump_data) {
out << " data: " << byteList(chunk->m_data, 100) << endl;
}
// data CRC
out << " crc: " << hex << chunk->storedCrc << endl;
@ -211,8 +284,17 @@ void parseAndEmitChunkYaml(const QString & path)
// Parse the chunks in the file.
QList<PRS1DataChunk *> chunks = s_loader->ParseFile(inpath);
for (int i=0; i < chunks.size(); i++) {
// Emit the YAML.
PRS1DataChunk * chunk = chunks.at(i);
// Parse the inner data.
switch (chunk->ext) {
case 0: chunk->ParseCompliance(); break;
case 1: chunk->ParseSummary(); break;
case 2: chunk->ParseEvents(MODE_UNKNOWN); break;
default: break;
}
// Emit the YAML.
ChunkToYaml(file, chunk);
delete chunk;
}

View File

@ -18,6 +18,7 @@ class PRS1Tests : public QObject
private slots:
void initTestCase();
void testMachineSupport();
void testChunksToYaml();
void testSessionsToYaml();
// void test2();

View File

@ -133,22 +133,26 @@ static QString eventChannel(ChannelID i)
return s;
}
static QString intList(EventStoreType* data, int count)
static QString intList(EventStoreType* data, int count, int limit=-1)
{
if (limit == -1 || limit > count) limit = count;
QStringList l;
for (int i = 0; i < count; i++) {
for (int i = 0; i < limit; i++) {
l.push_back(QString::number(data[i]));
}
if (limit < count) l.push_back("...");
QString s = "[ " + l.join(",") + " ]";
return s;
}
static QString intList(quint32* data, int count)
static QString intList(quint32* data, int count, int limit=-1)
{
if (limit == -1 || limit > count) limit = count;
QStringList l;
for (int i = 0; i < count; i++) {
for (int i = 0; i < limit; i++) {
l.push_back(QString::number(data[i] / 1000));
}
if (limit < count) l.push_back("...");
QString s = "[ " + l.join(",") + " ]";
return s;
}
@ -174,7 +178,14 @@ void SessionToYaml(QString filepath, Session* session)
QList<ChannelID> keys = session->settings.keys();
std::sort(keys.begin(), keys.end());
for (QList<ChannelID>::iterator key = keys.begin(); key != keys.end(); key++) {
out << " " << settingChannel(*key) << ": " << session->settings[*key].toString() << endl;
QVariant & value = session->settings[*key];
QString s;
if ((QMetaType::Type) value.type() == QMetaType::Float) {
s = QString::number(value.toFloat()); // Print the shortest accurate representation rather than QVariant's full precision.
} else {
s = value.toString();
}
out << " " << settingChannel(*key) << ": " << s << endl;
}
out << " events:" << endl;
@ -209,15 +220,15 @@ void SessionToYaml(QString filepath, Session* session)
out << " data:" << endl;
out << " min: " << e.Min() << endl;
out << " max: " << e.Max() << endl;
out << " raw: " << intList((EventStoreType*) e.m_data.data(), e.count()) << endl;
out << " raw: " << intList((EventStoreType*) e.m_data.data(), e.count(), 100) << endl;
if (e.type() != EVL_Waveform) {
out << " delta: " << intList((quint32*) e.m_time.data(), e.count()) << endl;
out << " delta: " << intList((quint32*) e.m_time.data(), e.count(), 100) << endl;
}
if (e.hasSecondField()) {
out << " data2:" << endl;
out << " min: " << e.min2() << endl;
out << " max: " << e.max2() << endl;
out << " raw: " << intList((EventStoreType*) e.m_data2.data(), e.count()) << endl;
out << " raw: " << intList((EventStoreType*) e.m_data2.data(), e.count(), 100) << endl;
}
}
}