mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-13 08:08:42 +00:00
Add support for VO2 measurements and VO2Master VM Pro
Add support for a generic set of VO2 measurements: * Respiratory Frequency * Respiratory Minute Volume aka Ventilation * Volume O2 consumed * Volume CO2 produced * Tidal Volume * FeO2 (Fraction of O2 expired) * Respiratory Exchange Rate (calculated as VCO2/VO2) Make the new metrics usable in TrainView, and store VO2 data as XDATA using the same pattern as for HRV data. Add support for VM Pro by VO2Masters The VM Pro is a BLE device, so support is added in the BT40Device class. Since the device requires some configuration in order to be usable, such as the size of the "User Piece" a special configuration widget is added and shown in a separate window when the device is connected. This window is also used to set a number of useful settings in the device, and to show calibration progress. There's also a detailed log of the status messages shown, and this can also be saved to file. Allow notifications from RealtimeControllers and devices in the notification area of Train View. In order for devices to display information in the notification field in TrainBottom the signals need to added and propagated from from device level via RealtimeController to TrainSidebar and finally TrainBottom. Fix an issue with multiple BT40Device per actual device Currently on MacOS there will be multiple BT40Device per actual device, since the QBluetoothDeviceDiscoveryAgent::deviceDiscovered() signal is emitted multiple times with e.g. updated RSSI values. Avoid this by checking the previously created devices first. MacOS doesn't disclose the address, so QBluetoothDeviceInfo::address() can't be used there, instead deviceUuid() is used which is instead only valid on MacOS.
This commit is contained in:
@@ -130,6 +130,7 @@ RideFile *CsvFileReader::openRideFile(QFile &file, QStringList &errors, QList<Ri
|
||||
XDataSeries *rrSeries=NULL;
|
||||
XDataSeries *ibikeSeries=NULL;
|
||||
XDataSeries *xdataSeries=NULL;
|
||||
XDataSeries *vo2Series=NULL;
|
||||
|
||||
/* Joule 1.0
|
||||
Version,Date/Time,Km,Minutes,RPE,Tags,"Weight, kg","Work, kJ",FTP,"Sample Rate, s",Device Type,Firmware Version,Last Updated,Category 1,Category 2
|
||||
@@ -1381,6 +1382,74 @@ RideFile *CsvFileReader::openRideFile(QFile &file, QStringList &errors, QList<Ri
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Is there an associated .vo2 file?
|
||||
QFile vo2file(file.fileName().replace(".csv",".vo2"));
|
||||
if (vo2file.open(QFile::ReadOnly))
|
||||
{
|
||||
// create the XDATA series
|
||||
vo2Series = new XDataSeries();
|
||||
vo2Series->name = "VO2 Measurements";
|
||||
vo2Series->valuename << "Rf" << "RMV" << "VO2" << "VCO2" << "Tv" << "FeO2";
|
||||
vo2Series->unitname << "bpm" << "l/min" << "ml/min" << "ml/min" << "l" << "%";
|
||||
|
||||
// attempt to read and add the data
|
||||
lineno=1;
|
||||
QTextStream rs(&vo2file);
|
||||
|
||||
// loop through lines and truncate etc
|
||||
while (!rs.atEnd()) {
|
||||
// the readLine() method doesn't handle old Macintosh CR line endings
|
||||
// this workaround will load the the entire file if it has CR endings
|
||||
// then split and loop through each line
|
||||
// otherwise, there will be nothing to split and it will read each line as expected.
|
||||
QString linesIn = rs.readLine();
|
||||
QStringList lines = linesIn.split('\r');
|
||||
// workaround for empty lines
|
||||
if(lines.isEmpty()) {
|
||||
lineno++;
|
||||
continue;
|
||||
}
|
||||
for (int li = 0; li < lines.size(); ++li) {
|
||||
QString line = lines[li];
|
||||
|
||||
if (line.length()==0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// first line is a header line
|
||||
if (lineno > 1) {
|
||||
|
||||
// split comma separated secs, hr, msecs
|
||||
QStringList values = line.split(",", QString::KeepEmptyParts);
|
||||
|
||||
// and add
|
||||
XDataPoint *p = new XDataPoint();
|
||||
p->secs = values.at(0).toDouble();
|
||||
p->km = 0;
|
||||
p->number[0] = values.at(1).toDouble();
|
||||
p->number[1] = values.at(2).toDouble();
|
||||
p->number[2] = values.at(3).toDouble();
|
||||
p->number[3] = values.at(4).toDouble();
|
||||
p->number[4] = values.at(5).toDouble();
|
||||
p->number[5] = values.at(6).toDouble();
|
||||
vo2Series->datapoints.append(p);
|
||||
}
|
||||
|
||||
// onto next line
|
||||
++lineno;
|
||||
}
|
||||
}
|
||||
// free handle
|
||||
vo2file.close();
|
||||
|
||||
// add if we got any ....
|
||||
if (vo2Series->datapoints.count() > 0)
|
||||
{
|
||||
rideFile->addXData("VO2", vo2Series);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// last, is there an associated rr file?
|
||||
//
|
||||
// typically only for GC csv, but lets not constrain that
|
||||
|
||||
@@ -114,9 +114,30 @@ void
|
||||
BT40Controller::addDevice(const QBluetoothDeviceInfo &info)
|
||||
{
|
||||
if (info.coreConfigurations() & QBluetoothDeviceInfo::LowEnergyCoreConfiguration) {
|
||||
BT40Device* dev = new BT40Device(this, info);
|
||||
devices.append(dev);
|
||||
dev->connectDevice();
|
||||
// Check if device is already created for this uuid/address
|
||||
// At least on MacOS the deviceDiscovered signal can/will be sent multiple times
|
||||
// for the same device during discovery.
|
||||
foreach(BT40Device* dev, devices)
|
||||
{
|
||||
if (info.address().isNull())
|
||||
{
|
||||
// On MacOS there's no address, so check deviceUuid
|
||||
if (dev->deviceInfo().deviceUuid() == info.deviceUuid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (dev->deviceInfo().address() == info.address())
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BT40Device* dev = new BT40Device(this, info);
|
||||
devices.append(dev);
|
||||
dev->connectDevice();
|
||||
connect(dev, &BT40Device::setNotification, this, &BT40Controller::setNotification);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,28 @@ public:
|
||||
void setCadence(double cadence) {
|
||||
telemetry.setCadence(cadence);
|
||||
}
|
||||
void setRespiratoryFrequency(double rf) {
|
||||
telemetry.setRf(rf);
|
||||
}
|
||||
void setRespiratoryMinuteVolume(double rmv) {
|
||||
telemetry.setRMV(rmv);
|
||||
}
|
||||
void setVO2_VCO2(double vo2, double vco2) {
|
||||
telemetry.setVO2_VCO2(vo2, vco2);
|
||||
}
|
||||
void setTv(double tv) {
|
||||
telemetry.setTv(tv);
|
||||
}
|
||||
void setFeO2(double feo2) {
|
||||
telemetry.setFeO2(feo2);
|
||||
}
|
||||
|
||||
void emitVO2Data() {
|
||||
emit vo2Data(telemetry.getRf(), telemetry.getRMV(), telemetry.getVO2(), telemetry.getVCO2(), telemetry.getTv(), telemetry.getFeO2());
|
||||
}
|
||||
|
||||
signals:
|
||||
void vo2Data(double rf, double rmv, double vo2, double vco2, double tv, double feo2);
|
||||
|
||||
private slots:
|
||||
void addDevice(const QBluetoothDeviceInfo&);
|
||||
|
||||
@@ -19,11 +19,21 @@
|
||||
#include "BT40Device.h"
|
||||
#include <QDebug>
|
||||
#include "BT40Controller.h"
|
||||
#include "VMProWidget.h"
|
||||
|
||||
#define VO2MASTERPRO_SERVICE_UUID "{00001523-1212-EFDE-1523-785FEABCD123}"
|
||||
#define VO2MASTERPRO_VENTILATORY_CHAR_UUID "{00001527-1212-EFDE-1523-785FEABCD123}"
|
||||
#define VO2MASTERPRO_GASEXCHANGE_CHAR_UUID "{00001528-1212-EFDE-1523-785FEABCD123}"
|
||||
#define VO2MASTERPRO_ENVIRONMENT_CHAR_UUID "{00001529-1212-EFDE-1523-785FEABCD123}"
|
||||
#define VO2MASTERPRO_COMIN_CHAR_UUID "{00001525-1212-EFDE-1523-785FEABCD123}"
|
||||
#define VO2MASTERPRO_COMOUT_CHAR_UUID "{00001526-1212-EFDE-1523-785FEABCD123}"
|
||||
#define VO2MASTERPRO_DATA_CHAR_UUID "{00001524-1212-EFDE-1523-785FEABCD123}"
|
||||
|
||||
QMap<QBluetoothUuid, btle_sensor_type_t> BT40Device::supportedServices = {
|
||||
{ QBluetoothUuid(QBluetoothUuid::HeartRate), { "Heartrate", ":images/IconHR.png" }},
|
||||
{ QBluetoothUuid(QBluetoothUuid::CyclingPower), { "Power", ":images/IconPower.png" }},
|
||||
{ QBluetoothUuid(QBluetoothUuid::CyclingSpeedAndCadence), { "Speed + Cadence", ":images/IconCadence.png" }},
|
||||
{ QBluetoothUuid(QString(VO2MASTERPRO_SERVICE_UUID)), { "VM Pro", ":images/IconCadence.png" }},
|
||||
};
|
||||
|
||||
BT40Device::BT40Device(QObject *parent, QBluetoothDeviceInfo devinfo) : parent(parent), m_currentDevice(devinfo)
|
||||
@@ -52,7 +62,7 @@ BT40Device::~BT40Device()
|
||||
void
|
||||
BT40Device::connectDevice()
|
||||
{
|
||||
qDebug() << "Connecting to device" << m_currentDevice.name();
|
||||
qDebug() << "Connecting to device" << m_currentDevice.name() << " " << m_currentDevice.deviceUuid();
|
||||
m_control->setRemoteAddressType(QLowEnergyController::RandomAddress);
|
||||
m_control->connectToDevice();
|
||||
connected = true;
|
||||
@@ -61,7 +71,7 @@ BT40Device::connectDevice()
|
||||
void
|
||||
BT40Device::disconnectDevice()
|
||||
{
|
||||
qDebug() << "Disconnecting from device" << m_currentDevice.name();
|
||||
qDebug() << "Disconnecting from device" << m_currentDevice.name() << " " << m_currentDevice.deviceUuid();
|
||||
connected = false;
|
||||
m_control->disconnectFromDevice();
|
||||
}
|
||||
@@ -69,20 +79,20 @@ BT40Device::disconnectDevice()
|
||||
void
|
||||
BT40Device::deviceConnected()
|
||||
{
|
||||
qDebug() << "Connected to device" << m_currentDevice.name();
|
||||
qDebug() << "Connected to device" << m_currentDevice.name() << " " << m_currentDevice.deviceUuid();
|
||||
m_control->discoverServices();
|
||||
}
|
||||
|
||||
void
|
||||
BT40Device::controllerError(QLowEnergyController::Error error)
|
||||
{
|
||||
qWarning() << "Controller Error:" << error << "for device" << m_currentDevice.name();
|
||||
qWarning() << "Controller Error:" << error << "for device" << m_currentDevice.name() << " " << m_currentDevice.deviceUuid();
|
||||
}
|
||||
|
||||
void
|
||||
BT40Device::deviceDisconnected()
|
||||
{
|
||||
qDebug() << "Lost connection to" << m_currentDevice.name();
|
||||
qDebug() << "Lost connection to" << m_currentDevice.name() << " " << m_currentDevice.deviceUuid();
|
||||
|
||||
// Zero any readings provided by this device
|
||||
foreach (QLowEnergyService* const &service, m_services) {
|
||||
@@ -93,6 +103,13 @@ BT40Device::deviceDisconnected()
|
||||
dynamic_cast<BT40Controller*>(parent)->setWatts(0.0);
|
||||
} else if (service->serviceUuid() == QBluetoothUuid(QBluetoothUuid::CyclingSpeedAndCadence)) {
|
||||
dynamic_cast<BT40Controller*>(parent)->setWheelRpm(0.0);
|
||||
} else if (service->serviceUuid() == QBluetoothUuid(QString(VO2MASTERPRO_SERVICE_UUID))) {
|
||||
BT40Controller *controller = dynamic_cast<BT40Controller*>(parent);
|
||||
controller->setRespiratoryFrequency(0);
|
||||
controller->setRespiratoryMinuteVolume(0);
|
||||
controller->setVO2_VCO2(0,0);
|
||||
controller->setTv(0);
|
||||
controller->setFeO2(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,13 +133,13 @@ BT40Device::serviceDiscovered(QBluetoothUuid uuid)
|
||||
void
|
||||
BT40Device::serviceScanDone()
|
||||
{
|
||||
qDebug() << "Service scan done for device" << m_currentDevice.name();
|
||||
qDebug() << "Service scan done for device" << m_currentDevice.name() << " " << m_currentDevice.deviceUuid();
|
||||
bool has_power = false;
|
||||
bool has_csc = false;
|
||||
QLowEnergyService* csc_service=NULL;
|
||||
foreach (QLowEnergyService* const &service, m_services) {
|
||||
|
||||
qDebug() << "Discovering details for service" << service->serviceUuid() << "for device" << m_currentDevice.name();
|
||||
qDebug() << "Discovering details for service" << service->serviceUuid() << "for device" << m_currentDevice.name() << " " << m_currentDevice.deviceUuid();
|
||||
|
||||
connect(service, SIGNAL(stateChanged(QLowEnergyService::ServiceState)), this, SLOT(serviceStateChanged(QLowEnergyService::ServiceState)));
|
||||
connect(service, SIGNAL(characteristicChanged(QLowEnergyCharacteristic,QByteArray)), this, SLOT(updateValue(QLowEnergyCharacteristic,QByteArray)));
|
||||
@@ -149,19 +166,19 @@ BT40Device::serviceScanDone()
|
||||
|
||||
// Only connect to CSC service if the same device doesn't provide a power service
|
||||
// since the power service also provides the same readings.
|
||||
qDebug() << "Connecting to the CSC service for device" << m_currentDevice.name();
|
||||
qDebug() << "Connecting to the CSC service for device" << m_currentDevice.name() << " " << m_currentDevice.deviceUuid();
|
||||
csc_service->discoverDetails();
|
||||
|
||||
} else {
|
||||
|
||||
qDebug() << "Ignoring the CSC service for device" << m_currentDevice.name();
|
||||
qDebug() << "Ignoring the CSC service for device" << m_currentDevice.name() << " " << m_currentDevice.deviceUuid();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
BT40Device::serviceStateChanged(QLowEnergyService::ServiceState s)
|
||||
{
|
||||
qDebug() << "service state changed" << s << "for device" << m_currentDevice.name();
|
||||
qDebug() << "service state changed " << s << "for device" << m_currentDevice.name() << " " << m_currentDevice.deviceUuid();
|
||||
|
||||
if (s == QLowEnergyService::ServiceDiscovered) {
|
||||
|
||||
@@ -169,28 +186,48 @@ BT40Device::serviceStateChanged(QLowEnergyService::ServiceState s)
|
||||
|
||||
if (service->state() == s) {
|
||||
|
||||
QLowEnergyCharacteristic characteristic;
|
||||
QList<QLowEnergyCharacteristic> characteristics;
|
||||
if (service->serviceUuid() == QBluetoothUuid(QBluetoothUuid::HeartRate)) {
|
||||
|
||||
characteristic = service->characteristic(
|
||||
QBluetoothUuid(QBluetoothUuid::HeartRateMeasurement));
|
||||
characteristics.append(service->characteristic(
|
||||
QBluetoothUuid(QBluetoothUuid::HeartRateMeasurement)));
|
||||
|
||||
} else if (service->serviceUuid() == QBluetoothUuid(QBluetoothUuid::CyclingPower)) {
|
||||
|
||||
characteristic = service->characteristic(
|
||||
QBluetoothUuid(QBluetoothUuid::CyclingPowerMeasurement));
|
||||
characteristics.append(service->characteristic(
|
||||
QBluetoothUuid(QBluetoothUuid::CyclingPowerMeasurement)));
|
||||
|
||||
} else if (service->serviceUuid() == QBluetoothUuid(QBluetoothUuid::CyclingSpeedAndCadence)) {
|
||||
|
||||
characteristic = service->characteristic(
|
||||
QBluetoothUuid(QBluetoothUuid::CSCMeasurement));
|
||||
characteristics.append(service->characteristic(
|
||||
QBluetoothUuid(QBluetoothUuid::CSCMeasurement)));
|
||||
} else if (service->serviceUuid() == QBluetoothUuid(QString(VO2MASTERPRO_SERVICE_UUID))) {
|
||||
|
||||
characteristics.append(service->characteristic(
|
||||
QBluetoothUuid(QString(VO2MASTERPRO_VENTILATORY_CHAR_UUID))));
|
||||
characteristics.append(service->characteristic(
|
||||
QBluetoothUuid(QString(VO2MASTERPRO_GASEXCHANGE_CHAR_UUID))));
|
||||
characteristics.append(service->characteristic(
|
||||
QBluetoothUuid(QString(VO2MASTERPRO_DATA_CHAR_UUID))));
|
||||
|
||||
// Create a VM Pro Configurator window
|
||||
static VMProWidget * vmProWidget = nullptr;
|
||||
if (!vmProWidget) {
|
||||
vmProWidget = new VMProWidget(service, this);
|
||||
connect(vmProWidget, &VMProWidget::setNotification, this, &BT40Device::setNotification);
|
||||
} else {
|
||||
vmProWidget->onReconnected(service);
|
||||
}
|
||||
}
|
||||
|
||||
if (characteristic.isValid()) {
|
||||
|
||||
const QLowEnergyDescriptor notificationDesc = characteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration);
|
||||
if (notificationDesc.isValid()) {
|
||||
service->writeDescriptor(notificationDesc, QByteArray::fromHex("0100"));
|
||||
foreach(QLowEnergyCharacteristic characteristic, characteristics)
|
||||
{
|
||||
if (characteristic.isValid()) {
|
||||
qDebug() << "Starting notification for char with UUID: " << characteristic.uuid().toString();
|
||||
const QLowEnergyDescriptor notificationDesc = characteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration);
|
||||
if (notificationDesc.isValid()) {
|
||||
service->writeDescriptor(notificationDesc, QByteArray::fromHex("0100"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -260,6 +297,76 @@ BT40Device::updateValue(const QLowEnergyCharacteristic &c, const QByteArray &val
|
||||
if (flags & 0x2) { // Crank Revolution Data Present
|
||||
getCadence(ds);
|
||||
}
|
||||
} else if (c.uuid() == QBluetoothUuid(QString(VO2MASTERPRO_VENTILATORY_CHAR_UUID))) {
|
||||
// Modern firmwares uses three different characteristics:
|
||||
// - VO2MASTERPRO_VENTILATORY_CHAR_UUID
|
||||
// - VO2MASTERPRO_GASEXCHANGE_CHAR_UUID
|
||||
// - VO2MASTERPRO_ENVIRONMENT_CHAR_UUID
|
||||
// This is also the order in which they will be updated from the device.
|
||||
//
|
||||
// Older firmwares uses one VO2MASTERPRO_DATA_CHAR_UUID for all data, and those
|
||||
// versions do not provide a VCO2 measurement.
|
||||
|
||||
quint16 rf; // Value over BT it rf*100;
|
||||
quint16 tidal_volume, rmv;
|
||||
ds >> rf;
|
||||
ds >> tidal_volume;
|
||||
ds >> rmv;
|
||||
|
||||
BT40Controller* controller = dynamic_cast<BT40Controller*>(parent);
|
||||
controller->setRespiratoryFrequency((double)rf/100.0f);
|
||||
controller->setRespiratoryMinuteVolume((double)rmv/100.0f);
|
||||
controller->setTv((double)tidal_volume/100.0f);
|
||||
|
||||
} else if (c.uuid() == QBluetoothUuid(QString(VO2MASTERPRO_GASEXCHANGE_CHAR_UUID))) {
|
||||
quint16 feo2, feco2, vo2, vco2;
|
||||
ds >> feo2;
|
||||
ds >> feco2;
|
||||
ds >> vo2;
|
||||
ds >> vco2;
|
||||
|
||||
if (feo2 == 2200 && vo2 == 22) {
|
||||
// From the docs:
|
||||
// If the value of FeO2 and VO2 for a given row are both exactly 22.0,
|
||||
// said row is a “Ventilation-only row”. Such a row contains all the
|
||||
// data less the FeO2 and VO2.
|
||||
|
||||
// Let's just ignore those updates completely, to avoid getting logged
|
||||
// rows with zero VO2.
|
||||
return;
|
||||
}
|
||||
|
||||
BT40Controller* controller = dynamic_cast<BT40Controller*>(parent);
|
||||
controller->setVO2_VCO2(vo2, vco2);
|
||||
controller->setFeO2((double)feo2/100.0f);
|
||||
controller->emitVO2Data();
|
||||
} else if (c.uuid() == QBluetoothUuid(QString(VO2MASTERPRO_DATA_CHAR_UUID))) {
|
||||
quint16 rf, rmv, feo2,vo2;
|
||||
quint8 temp, hum;
|
||||
ds >> rf;
|
||||
ds >> temp;
|
||||
ds >> hum;
|
||||
ds >> rmv;
|
||||
ds >> feo2;
|
||||
ds >> vo2;
|
||||
|
||||
if (feo2 == 2200 && vo2 == 22) {
|
||||
// From the docs:
|
||||
// If the value of FeO2 and VO2 for a given row are both exactly 22.0,
|
||||
// said row is a “Ventilation-only row”. Such a row contains all the
|
||||
// data less the FeO2 and VO2.
|
||||
|
||||
// Let's just ignore those updates completely, to avoid getting logged
|
||||
// rows with zero VO2.
|
||||
return;
|
||||
}
|
||||
|
||||
BT40Controller* controller = dynamic_cast<BT40Controller*>(parent);
|
||||
controller->setVO2_VCO2(vo2, 0);
|
||||
controller->setRespiratoryFrequency((double)rf/100.0f);
|
||||
controller->setRespiratoryMinuteVolume((double)rmv/100.0f);
|
||||
controller->setFeO2((double)feo2/100.0f);
|
||||
controller->emitVO2Data();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,13 +376,13 @@ BT40Device::serviceError(QLowEnergyService::ServiceError e)
|
||||
switch (e) {
|
||||
case QLowEnergyService::DescriptorWriteError:
|
||||
{
|
||||
qWarning() << "Failed to enable BTLE notifications" << "for device" << m_currentDevice.name();;
|
||||
qWarning() << "Failed to enable BTLE notifications" << "for device" << m_currentDevice.name() << " " << m_currentDevice.deviceUuid();;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
{
|
||||
qWarning() << "BTLE service error" << e << "for device" << m_currentDevice.name();;
|
||||
qWarning() << "BTLE service error" << e << "for device" << m_currentDevice.name() << " " << m_currentDevice.deviceUuid();;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -285,7 +392,7 @@ void
|
||||
BT40Device::confirmedDescriptorWrite(const QLowEnergyDescriptor &d, const QByteArray &value)
|
||||
{
|
||||
if (d.isValid() && value == QByteArray("0000")) {
|
||||
qWarning() << "disabled BTLE notifications" << "for device" << m_currentDevice.name();;
|
||||
qWarning() << "disabled BTLE notifications" << "for device" << m_currentDevice.name() << " " << m_currentDevice.deviceUuid();;
|
||||
this->disconnectDevice();
|
||||
}
|
||||
}
|
||||
@@ -352,3 +459,9 @@ BT40Device::getWheelRpm(QDataStream& ds)
|
||||
prevWheelTime = wheeltime;
|
||||
dynamic_cast<BT40Controller*>(parent)->setWheelRpm(rpm);
|
||||
}
|
||||
|
||||
QBluetoothDeviceInfo
|
||||
BT40Device::deviceInfo() const
|
||||
{
|
||||
return m_currentDevice;
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ public:
|
||||
void connectDevice();
|
||||
void disconnectDevice();
|
||||
static QMap<QBluetoothUuid, btle_sensor_type_t> supportedServices;
|
||||
QBluetoothDeviceInfo deviceInfo() const;
|
||||
|
||||
private slots:
|
||||
void deviceConnected();
|
||||
@@ -52,6 +53,8 @@ private slots:
|
||||
const QByteArray &value);
|
||||
void serviceError(QLowEnergyService::ServiceError e);
|
||||
|
||||
signals:
|
||||
void setNotification(QString msg, int timeout);
|
||||
private:
|
||||
QObject *parent;
|
||||
QBluetoothDeviceInfo m_currentDevice;
|
||||
|
||||
@@ -508,6 +508,22 @@ DialWindow::telemetryUpdate(const RealtimeData &rtData)
|
||||
valueLabel->setText(QString("%1").arg(value, 0, 'f', 1));
|
||||
break;
|
||||
|
||||
case RealtimeData::VO2:
|
||||
case RealtimeData::VCO2:
|
||||
case RealtimeData::Rf:
|
||||
case RealtimeData::RMV:
|
||||
valueLabel->setText(QString("%1").arg(value, 0, 'f', 0));
|
||||
break;
|
||||
|
||||
case RealtimeData::TidalVolume:
|
||||
valueLabel->setText(QString("%1").arg(value, 0, 'f', 1));
|
||||
break;
|
||||
|
||||
case RealtimeData::FeO2:
|
||||
case RealtimeData::RER:
|
||||
valueLabel->setText(QString("%1").arg(value, 0, 'f', 2));
|
||||
break;
|
||||
|
||||
default:
|
||||
valueLabel->setText(QString("%1").arg(round(displayValue)));
|
||||
break;
|
||||
@@ -573,6 +589,13 @@ void DialWindow::seriesChanged()
|
||||
case RealtimeData::VI:
|
||||
case RealtimeData::SkibaVI:
|
||||
case RealtimeData::Slope:
|
||||
case RealtimeData::Rf:
|
||||
case RealtimeData::RMV:
|
||||
case RealtimeData::VO2:
|
||||
case RealtimeData::VCO2:
|
||||
case RealtimeData::RER:
|
||||
case RealtimeData::TidalVolume:
|
||||
case RealtimeData::FeO2:
|
||||
case RealtimeData::None:
|
||||
foreground = GColor(CDIAL);
|
||||
break;
|
||||
|
||||
@@ -78,6 +78,9 @@ public:
|
||||
void processRealtimeData(RealtimeData &rtData);
|
||||
void processSetup();
|
||||
|
||||
signals:
|
||||
void setNotification(QString text, int timeout);
|
||||
|
||||
private:
|
||||
DeviceConfiguration *dc;
|
||||
DeviceConfiguration devConf;
|
||||
|
||||
@@ -30,6 +30,7 @@ RealtimeData::RealtimeData()
|
||||
thb = smo2 = o2hb = hhb = 0.0;
|
||||
lrbalance = rte = lte = lps = rps = 0.0;
|
||||
latitude = longitude = altitude = 0.0;
|
||||
rf = rmv = vo2 = vco2 = tv = feo2 = 0.0;
|
||||
trainerStatusAvailable = false;
|
||||
trainerReady = true;
|
||||
trainerRunning = true;
|
||||
@@ -394,6 +395,27 @@ double RealtimeData::value(DataSeries series) const
|
||||
case Altitude: return altitude;
|
||||
break;
|
||||
|
||||
case Rf: return rf;
|
||||
break;
|
||||
|
||||
case RMV: return rmv;
|
||||
break;
|
||||
|
||||
case VO2: return vo2;
|
||||
break;
|
||||
|
||||
case VCO2: return vco2;
|
||||
break;
|
||||
|
||||
case RER: return rer;
|
||||
break;
|
||||
|
||||
case TidalVolume: return tv;
|
||||
break;
|
||||
|
||||
case FeO2: return feo2;
|
||||
break;
|
||||
|
||||
case None:
|
||||
default:
|
||||
return 0;
|
||||
@@ -434,6 +456,13 @@ const QList<RealtimeData::DataSeries> &RealtimeData::listDataSeries()
|
||||
seriesList << tHb;
|
||||
seriesList << HHb;
|
||||
seriesList << O2Hb;
|
||||
seriesList << Rf;
|
||||
seriesList << RMV;
|
||||
seriesList << VO2;
|
||||
seriesList << VCO2;
|
||||
seriesList << RER;
|
||||
seriesList << TidalVolume;
|
||||
seriesList << FeO2;
|
||||
seriesList << AvgWatts;
|
||||
seriesList << AvgSpeed;
|
||||
seriesList << AvgCadence;
|
||||
@@ -606,6 +635,27 @@ QString RealtimeData::seriesName(DataSeries series)
|
||||
|
||||
case Altitude: return tr("Altitude");
|
||||
break;
|
||||
|
||||
case Rf: return tr("Respiratory Frequency");
|
||||
break;
|
||||
|
||||
case RMV: return tr("Respiratory Minute Volume");
|
||||
break;
|
||||
|
||||
case VO2: return tr("VO2");
|
||||
break;
|
||||
|
||||
case VCO2: return tr("VCO2");
|
||||
break;
|
||||
|
||||
case RER: return tr("Respiratory Exchange Ratio");
|
||||
break;
|
||||
|
||||
case TidalVolume: return tr("Tidal Volume");
|
||||
break;
|
||||
|
||||
case FeO2: return tr("Fraction O2 Expired");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -647,3 +697,26 @@ double RealtimeData::getAltitude() const { return altitude; }
|
||||
void RealtimeData::setLatitude(double d) { latitude = d; }
|
||||
void RealtimeData::setLongitude(double d) { longitude = d; }
|
||||
void RealtimeData::setAltitude(double d) { altitude = d; }
|
||||
|
||||
void RealtimeData::setRf(double rf) { this->rf = rf; }
|
||||
void RealtimeData::setRMV(double rmv) { this->rmv = rmv; }
|
||||
void RealtimeData::setTv(double tv) { this->tv = tv; }
|
||||
void RealtimeData::setFeO2(double feo2) {this->feo2 = feo2; }
|
||||
|
||||
void RealtimeData::setVO2_VCO2(double vo2, double vco2)
|
||||
{
|
||||
this->vo2 = vo2;
|
||||
this->vco2 = vco2;
|
||||
|
||||
if (vo2 > 0 && vco2 > 0) {
|
||||
rer = vco2/vo2;
|
||||
}
|
||||
}
|
||||
|
||||
double RealtimeData::getRf() const { return rf; }
|
||||
double RealtimeData::getRMV() const { return rmv; }
|
||||
double RealtimeData::getVO2() const { return vo2; }
|
||||
double RealtimeData::getVCO2() const { return vco2; }
|
||||
double RealtimeData::getRER() const { return rer; }
|
||||
double RealtimeData::getTv() const { return tv; }
|
||||
double RealtimeData::getFeO2() const { return feo2; }
|
||||
|
||||
@@ -39,6 +39,7 @@ public:
|
||||
XPower, BikeScore, RI, Joules, SkibaVI,
|
||||
IsoPower, BikeStress, IF, VI, Wbal,
|
||||
SmO2, tHb, HHb, O2Hb,
|
||||
Rf, RMV, VO2, VCO2, RER, TidalVolume, FeO2,
|
||||
AvgWatts, AvgSpeed, AvgCadence, AvgHeartRate,
|
||||
AvgWattsLap, AvgSpeedLap, AvgCadenceLap, AvgHeartRateLap,
|
||||
VirtualSpeed, AltWatts, LRBalance, LapTimeRemaining,
|
||||
@@ -99,6 +100,20 @@ public:
|
||||
double getHHb() const;
|
||||
double getO2Hb() const;
|
||||
|
||||
// VO2 related metrics
|
||||
void setVO2_VCO2(double vo2, double vco2);
|
||||
void setRf(double rf);
|
||||
void setRMV(double rmv);
|
||||
void setTv(double tv);
|
||||
void setFeO2(double feo2);
|
||||
double getVO2() const;
|
||||
double getVCO2() const;
|
||||
double getRf() const;
|
||||
double getRMV() const;
|
||||
double getRER() const;
|
||||
double getTv() const;
|
||||
double getFeO2() const;
|
||||
|
||||
double getWatts() const;
|
||||
double getAltWatts() const;
|
||||
double getAltDistance() const;
|
||||
@@ -152,6 +167,7 @@ private:
|
||||
double lte, rte, lps, rps; // torque efficiency and pedal smoothness
|
||||
double torque; // raw torque data for calibration display
|
||||
double latitude, longitude, altitude;
|
||||
double vo2, vco2, rf, rmv, tv, feo2;
|
||||
|
||||
// derived data
|
||||
double distance;
|
||||
@@ -160,6 +176,7 @@ private:
|
||||
double virtualSpeed;
|
||||
double wbal;
|
||||
double hhb, o2hb;
|
||||
double rer;
|
||||
long lap;
|
||||
long msecs;
|
||||
long lapMsecs;
|
||||
|
||||
@@ -337,7 +337,7 @@ TrainSidebar::TrainSidebar(Context *context) : GcWindow(context), context(contex
|
||||
lap_time = QTime();
|
||||
lap_elapsed_msec = 0;
|
||||
|
||||
rrFile = recordFile = NULL;
|
||||
rrFile = recordFile = vo2File = NULL;
|
||||
lastRecordSecs = 0;
|
||||
status = 0;
|
||||
setStatusFlags(RT_MODE_ERGO); // ergo mode by default
|
||||
@@ -675,6 +675,8 @@ TrainSidebar::configChanged(qint32)
|
||||
#ifdef QT_BLUETOOTH_LIB
|
||||
} else if (Devices.at(i).type == DEV_BT40) {
|
||||
Devices[i].controller = new BT40Controller(this, &Devices[i]);
|
||||
connect(Devices[i].controller, SIGNAL(vo2Data(double,double,double,double,double,double)),
|
||||
this, SLOT(vo2Data(double,double,double,double,double,double)));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -1384,6 +1386,14 @@ void TrainSidebar::Stop(int deviceStatus) // when stop button is pressed
|
||||
rrFile=NULL;
|
||||
}
|
||||
|
||||
// close vo2File
|
||||
if (vo2File) {
|
||||
fprintf(stderr, "Closing vo2 file\n"); fflush(stderr);
|
||||
vo2File->close();
|
||||
delete vo2File;
|
||||
vo2File=NULL;
|
||||
}
|
||||
|
||||
if(deviceStatus == DEVICE_ERROR)
|
||||
{
|
||||
recordFile->remove();
|
||||
@@ -1514,6 +1524,7 @@ void TrainSidebar::Connect()
|
||||
foreach(int dev, activeDevices) {
|
||||
Devices[dev].controller->start();
|
||||
Devices[dev].controller->resetCalibrationState();
|
||||
connect(Devices[dev].controller, &RealtimeController::setNotification, this, &TrainSidebar::setNotification);
|
||||
}
|
||||
setStatusFlags(RT_CONNECTED);
|
||||
gui_timer->start(REFRESHRATE);
|
||||
@@ -1531,7 +1542,10 @@ void TrainSidebar::Disconnect()
|
||||
|
||||
qDebug() << "disconnecting..";
|
||||
|
||||
foreach(int dev, activeDevices) Devices[dev].controller->stop();
|
||||
foreach(int dev, activeDevices) {
|
||||
disconnect(Devices[dev].controller, &RealtimeController::setNotification, this, &TrainSidebar::setNotification);
|
||||
Devices[dev].controller->stop();
|
||||
}
|
||||
clearStatusFlags(RT_CONNECTED);
|
||||
|
||||
gui_timer->stop();
|
||||
@@ -1646,6 +1660,15 @@ void TrainSidebar::guiUpdate() // refreshes the telemetry
|
||||
rtData.setHb(local.getSmO2(), local.gettHb()); //only moxy data from ant and robot devices right now
|
||||
}
|
||||
|
||||
if (Devices[dev].type == DEV_NULL || Devices[dev].type == DEV_BT40) {
|
||||
// Only robot and BT40 devices provides VO2 metrics
|
||||
rtData.setRf(local.getRf());
|
||||
rtData.setRMV(local.getRMV());
|
||||
rtData.setVO2_VCO2(local.getVO2(), local.getVCO2());
|
||||
rtData.setTv(local.getTv());
|
||||
rtData.setFeO2(local.getFeO2());
|
||||
}
|
||||
|
||||
// what are we getting from this one?
|
||||
if (dev == bpmTelemetry) rtData.setHr(local.getHr());
|
||||
if (dev == rpmTelemetry) rtData.setCadence(local.getCadence());
|
||||
@@ -2827,6 +2850,37 @@ void TrainSidebar::rrData(uint16_t rrtime, uint8_t count, uint8_t bpm)
|
||||
//fprintf(stderr, "R-R: %d ms, HR=%d, count=%d\n", rrtime, bpm, count); fflush(stderr);
|
||||
}
|
||||
|
||||
// VO2 Measurement data received
|
||||
void TrainSidebar::vo2Data(double rf, double rmv, double vo2, double vco2, double tv, double feo2)
|
||||
{
|
||||
if (status&RT_RECORDING && vo2File == NULL && recordFile != NULL) {
|
||||
QString vo2filename = recordFile->fileName().replace("csv", "vo2");
|
||||
|
||||
// setup the rr file
|
||||
vo2File = new QFile(vo2filename);
|
||||
if (!vo2File->open(QFile::WriteOnly | QFile::Truncate)) {
|
||||
delete vo2File;
|
||||
vo2File=NULL;
|
||||
} else {
|
||||
|
||||
// CSV File header
|
||||
QTextStream recordFileStream(vo2File);
|
||||
recordFileStream << "secs, rf, rmv, vo2, vco2, tv, feo2\n";
|
||||
}
|
||||
}
|
||||
|
||||
// output a line if recording and file ready
|
||||
if (status&RT_RECORDING && vo2File) {
|
||||
QTextStream recordFileStream(vo2File);
|
||||
|
||||
// convert from milliseconds to secondes
|
||||
double secs = double(session_elapsed_msec + session_time.elapsed()) / 1000.00;
|
||||
|
||||
// output a line
|
||||
recordFileStream << secs << ", " << rf << ", " << rmv << ", " << vo2 << ", " << vco2 << ", " << tv << ", " << feo2 << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
// connect/disconnect automatically when view changes
|
||||
void
|
||||
TrainSidebar::viewChanged(int index)
|
||||
|
||||
@@ -210,6 +210,9 @@ class TrainSidebar : public GcWindow
|
||||
// HRV R-R data being saved away
|
||||
void rrData(uint16_t rrtime, uint8_t heartrateBeats, uint8_t instantHeartrate);
|
||||
|
||||
// VO2 measurement data to save
|
||||
void vo2Data(double rf, double rmv, double vo2, double vco2, double tv, double feo2);
|
||||
|
||||
protected:
|
||||
|
||||
friend class ::MultiDeviceDialog;
|
||||
@@ -276,6 +279,7 @@ class TrainSidebar : public GcWindow
|
||||
QFile *recordFile; // where we record!
|
||||
int lastRecordSecs; // to avoid duplicates
|
||||
QFile *rrFile; // r-r records, if any received.
|
||||
QFile *vo2File; // vo2 records, if any received.
|
||||
ErgFile *ergFile; // workout file
|
||||
VideoSyncFile *videosyncFile; // videosync file
|
||||
|
||||
|
||||
296
src/Train/VMProConfigurator.cpp
Normal file
296
src/Train/VMProConfigurator.cpp
Normal file
@@ -0,0 +1,296 @@
|
||||
#include "VMProConfigurator.h"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QComboBox>
|
||||
#include <QGroupBox>
|
||||
#include <QLabel>
|
||||
|
||||
#define VO2MASTERPRO_COMIN_CHAR_UUID "{00001525-1212-EFDE-1523-785FEABCD123}"
|
||||
#define VO2MASTERPRO_COMOUT_CHAR_UUID "{00001526-1212-EFDE-1523-785FEABCD123}"
|
||||
|
||||
VMProConfigurator::VMProConfigurator(QLowEnergyService *service, QObject *parent) : QObject(parent)
|
||||
{
|
||||
setupCharNotifications(service);
|
||||
}
|
||||
|
||||
void VMProConfigurator::setupCharNotifications(QLowEnergyService * service)
|
||||
{
|
||||
m_service = service;
|
||||
|
||||
// Set up the two charasteristics used to query and control device settings
|
||||
m_comInChar = m_service->characteristic(QBluetoothUuid(QString(VO2MASTERPRO_COMIN_CHAR_UUID)));
|
||||
m_comOutChar = m_service->characteristic(QBluetoothUuid(QString(VO2MASTERPRO_COMOUT_CHAR_UUID)));
|
||||
connect(m_service, SIGNAL(characteristicChanged(QLowEnergyCharacteristic,QByteArray)),
|
||||
this, SLOT(onDeviceReply(QLowEnergyCharacteristic,QByteArray)));
|
||||
|
||||
const QLowEnergyDescriptor notificationDesc = m_comOutChar.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration);
|
||||
if (notificationDesc.isValid()) {
|
||||
m_service->writeDescriptor(notificationDesc, QByteArray::fromHex("0100"));
|
||||
}
|
||||
}
|
||||
|
||||
void VMProConfigurator::setUserPieceSize(VMProVenturiSize size)
|
||||
{
|
||||
if (m_comInChar.isValid())
|
||||
{
|
||||
QByteArray cmd;
|
||||
cmd.append(VM_BLE_SET_VENTURI_SIZE);
|
||||
cmd.append(size);
|
||||
m_service->writeCharacteristic(m_comInChar, cmd);
|
||||
}
|
||||
}
|
||||
|
||||
void VMProConfigurator::setVolumeCorrectionMode(VMProVolumeCorrectionMode mode)
|
||||
{
|
||||
if (m_comInChar.isValid())
|
||||
{
|
||||
QByteArray cmd;
|
||||
cmd.append(VM_BLE_SET_VO2_REPORT_MODE);
|
||||
cmd.append(mode);
|
||||
m_service->writeCharacteristic(m_comInChar, cmd);
|
||||
}
|
||||
}
|
||||
|
||||
void VMProConfigurator::setIdleTimeout(VMProIdleTimeout timeoutState)
|
||||
{
|
||||
if (m_comInChar.isValid())
|
||||
{
|
||||
QByteArray cmd;
|
||||
cmd.append(VM_BLE_SET_IDLE_TIMEOUT_MODE);
|
||||
cmd.append(timeoutState);
|
||||
m_service->writeCharacteristic(m_comInChar, cmd);
|
||||
}
|
||||
}
|
||||
|
||||
void VMProConfigurator::getUserPieceSize()
|
||||
{
|
||||
if (m_comInChar.isValid())
|
||||
{
|
||||
QByteArray cmd;
|
||||
cmd.append(VM_BLE_GET_VENTURI_SIZE);
|
||||
cmd.append('\0');
|
||||
m_service->writeCharacteristic(m_comInChar, cmd);
|
||||
}
|
||||
}
|
||||
|
||||
void VMProConfigurator::getIdleTimeout()
|
||||
{
|
||||
if (m_comInChar.isValid())
|
||||
{
|
||||
QByteArray cmd;
|
||||
cmd.append(VM_BLE_GET_IDLE_TIMEOUT_MODE);
|
||||
cmd.append('\0');
|
||||
m_service->writeCharacteristic(m_comInChar, cmd);
|
||||
}
|
||||
}
|
||||
|
||||
void VMProConfigurator::getVolumeCorrectionMode()
|
||||
{
|
||||
if (m_comInChar.isValid())
|
||||
{
|
||||
QByteArray cmd;
|
||||
cmd.append(VM_BLE_GET_VO2_REPORT_MODE);
|
||||
cmd.append('\0');
|
||||
m_service->writeCharacteristic(m_comInChar, cmd);
|
||||
}
|
||||
}
|
||||
|
||||
void VMProConfigurator::onDeviceReply(const QLowEnergyCharacteristic &c,
|
||||
const QByteArray &value)
|
||||
{
|
||||
// Return if it's not the ComOut char that has been updated
|
||||
if (c.uuid() != QBluetoothUuid(QString(VO2MASTERPRO_COMOUT_CHAR_UUID)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// A reply consist of a "command" and "data" byte
|
||||
if (value.length() == 2)
|
||||
{
|
||||
VMProCommand cmd = static_cast<VMProCommand>(value[0]);
|
||||
quint8 data = value[1];
|
||||
|
||||
switch (cmd) {
|
||||
case VM_BLE_UNKNOWN_RESPONSE:
|
||||
emit logMessage(tr("Unknown Response: %1").arg(data));
|
||||
break;
|
||||
case VM_BLE_SET_STATE:
|
||||
break;
|
||||
case VM_BLE_GET_STATE:
|
||||
emit logMessage(tr("Device State: %1").arg(data));
|
||||
break;
|
||||
case VM_BLE_SET_VENTURI_SIZE:
|
||||
break;
|
||||
case VM_BLE_GET_VENTURI_SIZE:
|
||||
emit logMessage(tr("User Piece Size: %1").arg(data));
|
||||
emit userPieceSizeChanged(static_cast<VMProVenturiSize>(data));
|
||||
break;
|
||||
case VM_BLE_GET_CALIB_PROGRESS:
|
||||
emit calibrationProgressChanged(data);
|
||||
emit logMessage(tr("Calibration Progress: %1 %").arg(data));
|
||||
break;
|
||||
case VM_BLE_ERROR:
|
||||
emit logMessage(VMProErrorToStringHelper::errorDescription(data));
|
||||
break;
|
||||
case VM_BLE_SET_VO2_REPORT_MODE:
|
||||
break;
|
||||
case VM_BLE_GET_VO2_REPORT_MODE:
|
||||
emit logMessage(tr("Volume Correction Mode: %1").arg(data));
|
||||
emit volumeCorrectionModeChanged(static_cast<VMProVolumeCorrectionMode>(data));
|
||||
break;
|
||||
case VM_BLE_GET_O2_CELL_AGE:
|
||||
emit logMessage(tr("O2 Cell Age: %1").arg(data));
|
||||
break;
|
||||
case VM_BLE_RESET_O2_CELL_AGE:
|
||||
break;
|
||||
case VM_BLE_SET_IDLE_TIMEOUT_MODE:
|
||||
break;
|
||||
case VM_BLE_GET_IDLE_TIMEOUT_MODE:
|
||||
emit logMessage(tr("Idle Timeout Mode: %1").arg(data));
|
||||
emit idleTimeoutStateChanged(static_cast<VMProIdleTimeout>(data));
|
||||
break;
|
||||
case VM_BLE_SET_AUTO_RECALIB_MODE:
|
||||
break;
|
||||
case VM_BLE_GET_AUTO_RECALIB_MODE:
|
||||
emit logMessage(tr("Auto-calibration Mode: %1").arg(data));
|
||||
break;
|
||||
case VM_BLE_BREATH_STATE_CHANGED:
|
||||
emit logMessage(tr("Breath State: %1").arg(data));
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
qDebug() << "VMProConfigurator::onDeviceReply with unexpected length: " << value.length();
|
||||
}
|
||||
}
|
||||
|
||||
QString VMProErrorToStringHelper::errorDescription(int errorCode)
|
||||
{
|
||||
switch(errorCode)
|
||||
{
|
||||
//VM_ERROR_FATAL
|
||||
case (FatalErrorOffset + 0): //VM_FATAL_ERROR_NONE
|
||||
return tr("No Error.");
|
||||
case (FatalErrorOffset + 1): //VM_FATAL_ERROR_INIT
|
||||
return tr("Initialization error, shutting off.");
|
||||
case (FatalErrorOffset + 2): //VM_FATAL_ERROR_TOO_HOT
|
||||
return tr("Too hot, shutting off.");
|
||||
case (FatalErrorOffset + 3): //VM_FATAL_ERROR_TOO_COLD
|
||||
return tr("Too cold, shutting off.");
|
||||
case (FatalErrorOffset + 4): //VM_FATAL_ERROR_IDLE_TIMEOUT
|
||||
return tr("Sat idle too long, shutting off.");
|
||||
case (FatalErrorOffset + 5): //VM_FATAL_ERROR_DEAD_BATTERY_LBO_V
|
||||
case (FatalErrorOffset + 6): //VM_FATAL_ERROR_DEAD_BATTERY_SOC
|
||||
case (FatalErrorOffset + 7): //VM_FATAL_ERROR_DEAD_BATTERY_STARTUP
|
||||
return tr("Battery is out of charge, shutting off.");
|
||||
case (FatalErrorOffset + 8): //VM_FATAL_ERROR_BME_NO_INIT
|
||||
return tr("Failed to initialize environmental sensor.\n Send unit in for service.");
|
||||
case (FatalErrorOffset + 9): //VM_FATAL_ERROR_ADS_NO_INIT
|
||||
return tr("Failed to initialize oxygen sensor.\n Send unit in for service.");
|
||||
case (FatalErrorOffset + 10): //VM_FATAL_ERROR_AMS_DISCONNECTED
|
||||
return tr("Failed to initialize flow sensor.\n Send unit in for service.");
|
||||
case (FatalErrorOffset + 11): //VM_FATAL_ERROR_TWI_NO_INIT
|
||||
return tr("Failed to initialize sensor communication.\n Send unit in for service.");
|
||||
case (FatalErrorOffset + 12): //VM_FATAL_ERROR_FAILED_FLASH_WRITE
|
||||
return tr("Failed to write a page to flash memory.\n Send unit in for service.");
|
||||
case (FatalErrorOffset + 13): //VM_FATAL_ERROR_FAILED_FLASH_ERASE
|
||||
return tr("Failed to erase a page from flash memory.\n Send unit in for service.");
|
||||
|
||||
//VM_ERROR_WARN
|
||||
case (WarningErrorOffset + 0): //VM_WARN_ERROR_NONE
|
||||
return tr("No warning.");
|
||||
case (WarningErrorOffset + 1): //VM_WARN_ERROR_MASK_LEAK
|
||||
return tr("Mask leak detected.");
|
||||
case (WarningErrorOffset + 2): //VM_WARN_ERROR_VENTURI_TOO_SMALL
|
||||
return tr("User piece too small.");
|
||||
case (WarningErrorOffset + 3): //VM_WARN_ERROR_VENTURI_TOO_BIG
|
||||
return tr("User piece too big.");
|
||||
case (WarningErrorOffset + 4): //VM_WARN_ERROR_TOO_HOT
|
||||
return tr("Device very hot.");
|
||||
case (WarningErrorOffset + 5): //VM_WARN_ERROR_TOO_COLD
|
||||
return tr("Device very cold.");
|
||||
case (WarningErrorOffset + 6): //VM_WARN_ERROR_UNDER_BREATHING_VALVE
|
||||
return tr("Breathing less than valve trigger.");
|
||||
case (WarningErrorOffset + 7): //VM_WARN_ERROR_O2_TOO_HUMID
|
||||
return tr("Oxygen sensor too humid.");
|
||||
case (WarningErrorOffset + 8): //VM_WARN_ERROR_O2_TOO_HUMID_END
|
||||
return tr("Oxygen sensor dried.");
|
||||
case (WarningErrorOffset + 9): //VM_WARN_ERROR_BREATHING_DURING_DIFFP_CALIB
|
||||
return tr("Breathing during ambient calibration.\n Hold your breath for 5 seconds.");
|
||||
case (WarningErrorOffset + 10): //VM_WARN_ERROR_TOO_MANY_CONSECUTIVE_BREATHS_REJECTED
|
||||
return tr("Many breaths rejected.");
|
||||
case (WarningErrorOffset + 11): //VM_WARN_ERROR_LOW_BATTERY_VOLTAGE
|
||||
return tr("Low battery.");
|
||||
case (WarningErrorOffset + 12): //VM_WARN_ERROR_THERMAL_SHOCK_BEGIN
|
||||
return tr("Thermal change occurring.");
|
||||
case (WarningErrorOffset + 13): //VM_WARN_ERROR_THERMAL_SHOCK_END
|
||||
return tr("Thermal change slowed.");
|
||||
case (WarningErrorOffset + 14): //VM_WARN_ERROR_FINAL_VE_OUT_OF_RANGE
|
||||
return tr("Ventilation out of range.");
|
||||
|
||||
//VM_ERROR_O2_RECALIB
|
||||
case (O2CalibrationErrorOffset + 0): //VM_O2_RECALIB_NONE
|
||||
return tr("No calibration message.");
|
||||
case (O2CalibrationErrorOffset + 1): //VM_O2_RECALIB_DRIFT
|
||||
return tr("O2 sensor signal drifted.");
|
||||
case (O2CalibrationErrorOffset + 2): //VM_O2_RECALIB_PRESSURE_DRIFT
|
||||
return tr("Ambient pressure changed a lot.");
|
||||
case (O2CalibrationErrorOffset + 3): //VM_O2_RECALIB_TEMPERATURE_DRIFT
|
||||
return tr("Temperature changed a lot.");
|
||||
case (O2CalibrationErrorOffset + 4): //VM_O2_RECALIB_TIME_MAX
|
||||
return tr("Maximum time between calibrations reached.");
|
||||
case (O2CalibrationErrorOffset + 5): //VM_O2_RECALIB_TIME_5MIN
|
||||
return tr("5 minute recalibration.");
|
||||
case (O2CalibrationErrorOffset + 6): //VM_O2_RECALIB_REASON_THERMAL_SHOCK_OVER
|
||||
return tr("Post-thermal shock calibration.");
|
||||
|
||||
//VM_ERROR_DIAG
|
||||
case (DiagnosticErrorOffset + 0): //VM_DIAG_ERROR_FLOW_DELAY_RESET
|
||||
return tr("Calibration: waiting for user to start breathing.");
|
||||
case (DiagnosticErrorOffset + 1): //VM_DIAG_ERROR_BREATH_TOO_JITTERY
|
||||
return tr("Breath rejected; too jittery.");
|
||||
case (DiagnosticErrorOffset + 2): //VM_DIAG_ERROR_SEGMENT_TOO_SHORT
|
||||
return tr("Breath rejected; segment too short.");
|
||||
case (DiagnosticErrorOffset + 3): //VM_DIAG_ERROR_BREATH_TOO_SHORT
|
||||
return tr("Breath rejected; breath too short.");
|
||||
case (DiagnosticErrorOffset + 4): //VM_DIAG_ERROR_BREATH_TOO_SHALLOW
|
||||
return tr("Breath rejected; breath too small.");
|
||||
case (DiagnosticErrorOffset + 5): //VM_DIAG_ERROR_FINAL_RF_OUT_OF_RANGE
|
||||
return tr("Breath rejected; Rf out of range.");
|
||||
case (DiagnosticErrorOffset + 6): //VM_DIAG_ERROR_FINAL_TV_OUT_OF_RANGE
|
||||
return tr("Breath rejected; Tv out of range.");
|
||||
case (DiagnosticErrorOffset + 7): //VM_DIAG_ERROR_FINAL_VE_OUT_OF_RANGE
|
||||
return tr("Breath rejected; Ve out of range.");
|
||||
case (DiagnosticErrorOffset + 8): //VM_DIAG_ERROR_FINAL_FEO2_OUT_OF_RANGE
|
||||
return tr("Breath rejected; FeO2 out of range.");
|
||||
case (DiagnosticErrorOffset + 9): //VM_DIAG_ERROR_FINAL_VO2_OUT_OF_RANGE
|
||||
return tr("Breath rejected; VO2 out of range.");
|
||||
case (DiagnosticErrorOffset + 10): //VM_DIAG_ERROR_DEVICE_INITIALIZED
|
||||
return tr("Device initialized.");
|
||||
case (DiagnosticErrorOffset + 11): //VM_DIAG_ERROR_TRIED_RECORD_BEFORE_CALIB
|
||||
return tr("Device attempted to enter Record mode\n before completing calibration.");
|
||||
case (DiagnosticErrorOffset + 12): //VM_DIAG_ERROR_O2_CALIB_AVG_TOO_VOLATILE
|
||||
return tr("Oxygen sensor calibration waveform is volatile.");
|
||||
case (DiagnosticErrorOffset + 13): //VM_DIAG_ERROR_ADS_HIT_MAX_VALUE
|
||||
return tr("Oxygen sensor reading clipped at its maximum value.");
|
||||
case (DiagnosticErrorOffset + 17): //VM_DIAG_ERROR_VALVE_REQUEST_OPEN
|
||||
return tr("Valve opened.");
|
||||
case (DiagnosticErrorOffset + 18): //VM_DIAG_ERROR_VALVE_REQUEST_CLOSE
|
||||
return tr("Valve closed.");
|
||||
case (DiagnosticErrorOffset + 19): //VM_DIAG_ERROR_CALIB_ADC_THEO_DIFF_MINIMUM
|
||||
return tr("Calib diff: minimum.");
|
||||
case (DiagnosticErrorOffset + 20): //VM_DIAG_ERROR_CALIB_ADC_THEO_DIFF_SMALL
|
||||
return tr("Calib diff: small.");
|
||||
case (DiagnosticErrorOffset + 21): //VM_DIAG_ERROR_CALIB_ADC_THEO_DIFF_MEDIUM
|
||||
return tr("Calib diff: medium.");
|
||||
case (DiagnosticErrorOffset + 22): //VM_DIAG_ERROR_CALIB_ADC_THEO_DIFF_LARGE
|
||||
return tr("Calib diff: large.");
|
||||
|
||||
// Undisclosed error codes.
|
||||
case (DiagnosticErrorOffset + 14):
|
||||
case (DiagnosticErrorOffset + 15):
|
||||
case (DiagnosticErrorOffset + 16):
|
||||
default:
|
||||
return tr("Error Code: %1").arg(errorCode);
|
||||
}
|
||||
}
|
||||
113
src/Train/VMProConfigurator.h
Normal file
113
src/Train/VMProConfigurator.h
Normal file
@@ -0,0 +1,113 @@
|
||||
#ifndef VMPROCONFIGURATOR_H
|
||||
#define VMPROCONFIGURATOR_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QLowEnergyService>
|
||||
|
||||
class QLabel;
|
||||
|
||||
enum VMProCommand
|
||||
{
|
||||
VM_BLE_UNKNOWN_RESPONSE = 0, /* Unrecognized repsonse. */
|
||||
VM_BLE_SET_STATE, /*Set new state.*/
|
||||
VM_BLE_GET_STATE, /*Get current state.*/
|
||||
VM_BLE_SET_VENTURI_SIZE, /*Set new venturi size.*/
|
||||
VM_BLE_GET_VENTURI_SIZE, /*Get current venturi size.*/
|
||||
VM_BLE_GET_CALIB_PROGRESS, /*Get 0-100% progress update on calibration*/
|
||||
VM_BLE_ERROR, /*Send phone a new error.*/
|
||||
VM_BLE_SET_VO2_REPORT_MODE, /*Set the environmental correction mode for VO2 measurements: {STPD, BTPS/ATPS}*/
|
||||
VM_BLE_GET_VO2_REPORT_MODE, /*Get the environmental correction mode for VO2 measurements: {STPD, BTPS/ATPS}*/
|
||||
VM_BLE_GET_O2_CELL_AGE, /*Get the current o2 cell age (0-100%).*/
|
||||
VM_BLE_RESET_O2_CELL_AGE, /*Resets original o2 cell reading. This should only be used if a new o2 cell is installed and
|
||||
the device isn't automatically correcting for it.*/
|
||||
VM_BLE_SET_IDLE_TIMEOUT_MODE, /*Stops the device from turning itself off after a predetermined amount of time it sits
|
||||
idle.*/
|
||||
VM_BLE_GET_IDLE_TIMEOUT_MODE, /*Allows the device to turn itself off after a predetermined amount of time it sits idle.*/
|
||||
VM_BLE_SET_AUTO_RECALIB_MODE, /*Stop device from automatically re-calibrating.*/
|
||||
VM_BLE_GET_AUTO_RECALIB_MODE, /*Allows device to automatically recalibrate.*/
|
||||
VM_BLE_BREATH_STATE_CHANGED, /*Sent from firmware to phone, when breath state changes.*/
|
||||
};
|
||||
|
||||
enum VMProVenturiSize
|
||||
{
|
||||
VM_VENTURI_SMALL = 0,
|
||||
VM_VENTURI_MEDIUM,
|
||||
VM_VENTURI_LARGE,
|
||||
VM_VENTURI_XLARGE,
|
||||
VM_VENTURI_RESTING
|
||||
};
|
||||
|
||||
enum VMProVolumeCorrectionMode
|
||||
{
|
||||
VM_STPD = 0,
|
||||
VM_BTPS
|
||||
};
|
||||
|
||||
enum VMProIdleTimeout
|
||||
{
|
||||
VM_OFF = 0,
|
||||
VM_ON
|
||||
};
|
||||
|
||||
enum VMProState
|
||||
{
|
||||
VM_STATE_UNUSED = 0, /*Depreciated. If IDLE, state is assumed to be calib.*/
|
||||
VM_STATE_CALIB, /*Begin polling sensors. Await user action to calibrate device.*/
|
||||
VM_STATE_RECORD /*Device must be calibrated before recording. Polls sensors in order to produce VO2 metrics.*/
|
||||
};
|
||||
|
||||
class VMProErrorToStringHelper : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
static const int FatalErrorOffset = 0; //Offset for fatal errors.
|
||||
static const int WarningErrorOffset = 50; //Offset for warnings errors.
|
||||
static const int O2CalibrationErrorOffset = 100; //Offset for recalibration errors.
|
||||
static const int DiagnosticErrorOffset = 120; //Offset for diagnostic errors. These are not reported to the user but are recorded.
|
||||
|
||||
static QString errorDescription(int errorCode);
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(VMProCommand)
|
||||
Q_DECLARE_METATYPE(VMProVenturiSize)
|
||||
Q_DECLARE_METATYPE(VMProVolumeCorrectionMode)
|
||||
Q_DECLARE_METATYPE(VMProIdleTimeout)
|
||||
Q_DECLARE_METATYPE(VMProState)
|
||||
|
||||
class VMProConfigurator : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
|
||||
explicit VMProConfigurator(QLowEnergyService * service,
|
||||
QObject *parent = nullptr);
|
||||
|
||||
signals:
|
||||
void userPieceSizeChanged(VMProVenturiSize size);
|
||||
void idleTimeoutStateChanged(VMProIdleTimeout timeoutState);
|
||||
void volumeCorrectionModeChanged(VMProVolumeCorrectionMode timeoutState);
|
||||
void calibrationProgressChanged(quint8 percentCompleted);
|
||||
void logMessage(const QString &msg);
|
||||
|
||||
public slots:
|
||||
void getUserPieceSize();
|
||||
void getIdleTimeout();
|
||||
void getVolumeCorrectionMode();
|
||||
|
||||
void setUserPieceSize(VMProVenturiSize size);
|
||||
void setIdleTimeout(VMProIdleTimeout timeoutState);
|
||||
void setVolumeCorrectionMode(VMProVolumeCorrectionMode mode);
|
||||
|
||||
void setupCharNotifications(QLowEnergyService * service);
|
||||
|
||||
private slots:
|
||||
void onDeviceReply(const QLowEnergyCharacteristic &c, const QByteArray &value);
|
||||
|
||||
private:
|
||||
QLowEnergyService * m_service;
|
||||
QLowEnergyCharacteristic m_comInChar;
|
||||
QLowEnergyCharacteristic m_comOutChar;
|
||||
|
||||
};
|
||||
|
||||
#endif // VMPROCONFIGURATOR_H
|
||||
237
src/Train/VMProWidget.cpp
Normal file
237
src/Train/VMProWidget.cpp
Normal file
@@ -0,0 +1,237 @@
|
||||
#include "VMProWidget.h"
|
||||
|
||||
#include <QThread>
|
||||
#include <QPushButton>
|
||||
#include <QFileDialog>
|
||||
#include <QStandardPaths>
|
||||
#include <QScrollBar>
|
||||
#include <QDateTime>
|
||||
|
||||
VMProWidget::VMProWidget(QLowEnergyService * service, QObject * parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
m_vmProConfigurator = new VMProConfigurator(service, this);
|
||||
|
||||
connect(m_vmProConfigurator, &VMProConfigurator::logMessage, this, &VMProWidget::addStatusMessage);
|
||||
|
||||
QWidget * settingsWidget = new QWidget();
|
||||
|
||||
// User Piece Setting
|
||||
QLabel * userPieceLabel = new QLabel(tr("The size engraved on the user pieces. This is the skrew-in part that attaches to the mask."));
|
||||
userPieceLabel->setWordWrap(true);
|
||||
m_userPiecePicker = new QComboBox();
|
||||
m_userPiecePicker->addItem(tr("Resting"), VMProVenturiSize::VM_VENTURI_RESTING);
|
||||
m_userPiecePicker->addItem(tr("Medium"), VMProVenturiSize::VM_VENTURI_MEDIUM);
|
||||
m_userPiecePicker->addItem(tr("Large"), VMProVenturiSize::VM_VENTURI_LARGE);
|
||||
|
||||
QVBoxLayout *userPieceLayout = new QVBoxLayout();
|
||||
userPieceLayout->addWidget(userPieceLabel);
|
||||
userPieceLayout->addWidget(m_userPiecePicker);
|
||||
|
||||
QGroupBox *userPieceBox = new QGroupBox(tr("User Piece Size"));
|
||||
userPieceBox->setLayout(userPieceLayout);
|
||||
|
||||
|
||||
// Volume Correction Mode
|
||||
QLabel * volumeLabel = new QLabel(tr("This is an advanced setting. If you don't know what it is, it's recommended that you leave it on STPD."));
|
||||
volumeLabel->setWordWrap(true);
|
||||
m_volumePicker = new QComboBox();
|
||||
m_volumePicker->addItem("STPD", VMProVolumeCorrectionMode::VM_STPD);
|
||||
m_volumePicker->addItem("BTPS", VMProVolumeCorrectionMode::VM_BTPS);
|
||||
|
||||
QVBoxLayout *volumeLayout = new QVBoxLayout();
|
||||
volumeLayout->addWidget(volumeLabel);
|
||||
volumeLayout->addWidget(m_volumePicker);
|
||||
|
||||
QGroupBox *volumeBox = new QGroupBox(tr("Volume Correction Mode"));
|
||||
volumeBox->setLayout(volumeLayout);
|
||||
|
||||
// Auto Re-Calibration
|
||||
QLabel * autocalibLabel = new QLabel(tr("If enabled, the device will automatically start a recalibration 5 minutes into the session. If disabled you can still trigger a manual calibration."));
|
||||
autocalibLabel->setWordWrap(true);
|
||||
m_autocalibPicker = new QComboBox();
|
||||
|
||||
QVBoxLayout *autocalibLayout = new QVBoxLayout();
|
||||
autocalibLayout->addWidget(autocalibLabel);
|
||||
autocalibLayout->addWidget(m_autocalibPicker);
|
||||
|
||||
m_autocalibPicker->addItem(tr("Enabled"), VMProIdleTimeout::VM_ON);
|
||||
m_autocalibPicker->addItem(tr("Disabled"), VMProIdleTimeout::VM_OFF);
|
||||
QGroupBox *autocalibBox = new QGroupBox(tr("Auto Recalibration"));
|
||||
autocalibBox->setLayout(autocalibLayout);
|
||||
|
||||
// Idle Timeout
|
||||
QLabel * idleTimeoutLabel = new QLabel(tr("If enabled, the device will shut off after 15 minutes of no breathing."));
|
||||
idleTimeoutLabel->setWordWrap(true);
|
||||
m_idleTimeoutPicker = new QComboBox();
|
||||
|
||||
QVBoxLayout *idleTimeoutLayout = new QVBoxLayout();
|
||||
idleTimeoutLayout->addWidget(idleTimeoutLabel);
|
||||
idleTimeoutLayout->addWidget(m_idleTimeoutPicker);
|
||||
|
||||
m_idleTimeoutPicker->addItem(tr("Enabled"), VMProIdleTimeout::VM_ON);
|
||||
m_idleTimeoutPicker->addItem(tr("Disabled"), VMProIdleTimeout::VM_OFF);
|
||||
QGroupBox *idleTimeoutBox = new QGroupBox(tr("Idle Timeout"));
|
||||
idleTimeoutBox->setLayout(idleTimeoutLayout);
|
||||
|
||||
// Calibration Progress
|
||||
QLabel * calibrationProgressInfoLabel = new QLabel(tr("Calibration Status:"));
|
||||
m_calibrationProgressLabel = new QLabel(tr("Unknown"));
|
||||
|
||||
QVBoxLayout *calibrationProgressLayout = new QVBoxLayout();
|
||||
calibrationProgressLayout->addWidget(calibrationProgressInfoLabel);
|
||||
calibrationProgressLayout->addWidget(m_calibrationProgressLabel);
|
||||
|
||||
QGroupBox *calibrationProgressBox = new QGroupBox(tr("Calibration Status"));
|
||||
calibrationProgressBox->setLayout(calibrationProgressLayout);
|
||||
|
||||
// Settings Widget
|
||||
QVBoxLayout *settingsLayout = new QVBoxLayout();
|
||||
settingsLayout->addWidget(userPieceBox);
|
||||
settingsLayout->addWidget(volumeBox);
|
||||
settingsLayout->addWidget(autocalibBox);
|
||||
settingsLayout->addWidget(idleTimeoutBox);
|
||||
settingsLayout->addWidget(calibrationProgressBox);
|
||||
settingsWidget->setLayout(settingsLayout);
|
||||
|
||||
QWidget * dbgWidget = new QWidget();
|
||||
QGroupBox *dbgBox = new QGroupBox(tr("Status Information"));
|
||||
QVBoxLayout *dbgLayout = new QVBoxLayout(dbgWidget);
|
||||
m_deviceLog = new QTextEdit("", dbgWidget);
|
||||
QPushButton * saveButton = new QPushButton(tr("Save log to file"));
|
||||
saveButton->setMaximumHeight(25);
|
||||
dbgLayout->addWidget(m_deviceLog);
|
||||
dbgLayout->addWidget(saveButton);
|
||||
dbgBox->setLayout(dbgLayout);
|
||||
|
||||
// Main layout
|
||||
QWidget * w = new QWidget();
|
||||
QHBoxLayout *mainLayout = new QHBoxLayout(w);
|
||||
mainLayout->addWidget(settingsWidget);
|
||||
mainLayout->addWidget(dbgBox);
|
||||
|
||||
w->setLayout(mainLayout);
|
||||
|
||||
w->show();
|
||||
|
||||
// Basic init of settings
|
||||
m_vmProConfigurator->setIdleTimeout(VMProIdleTimeout::VM_ON);
|
||||
QThread::msleep(50);
|
||||
m_vmProConfigurator->setUserPieceSize(VMProVenturiSize::VM_VENTURI_MEDIUM);
|
||||
QThread::msleep(50);
|
||||
m_vmProConfigurator->setVolumeCorrectionMode(VMProVolumeCorrectionMode::VM_STPD);
|
||||
|
||||
|
||||
// set up connections
|
||||
connect(m_vmProConfigurator, &VMProConfigurator::idleTimeoutStateChanged, this, &VMProWidget::onIdleTimeoutChanged);
|
||||
connect(m_vmProConfigurator, &VMProConfigurator::userPieceSizeChanged, this, &VMProWidget::onUserPieceSizeChanged);
|
||||
connect(m_vmProConfigurator, &VMProConfigurator::calibrationProgressChanged, this, &VMProWidget::onCalibrationProgressChanged);
|
||||
connect(m_vmProConfigurator, &VMProConfigurator::volumeCorrectionModeChanged, this, &VMProWidget::onVolumeCorrectionModeChanged);
|
||||
connect(saveButton, &QPushButton::clicked, this, &VMProWidget::onSaveClicked);
|
||||
|
||||
connect(m_userPiecePicker, static_cast<void (QComboBox::*)(int index)>(&QComboBox::currentIndexChanged), this, &VMProWidget::onUserPieceSizePickerChanged);
|
||||
connect(m_volumePicker, static_cast<void (QComboBox::*)(int index)>(&QComboBox::currentIndexChanged), this, &VMProWidget::onVolumeCorrectionModePickerChanged);
|
||||
connect(m_idleTimeoutPicker, static_cast<void (QComboBox::*)(int index)>(&QComboBox::currentIndexChanged), this, &VMProWidget::onIdleTimeoutPickerChanged);
|
||||
}
|
||||
|
||||
void VMProWidget::addStatusMessage(const QString & msg)
|
||||
{
|
||||
QString timestamp = QDateTime::currentDateTime().toString("hh:mm:ss.zzz");
|
||||
m_deviceLog->append(timestamp + ": " + msg);
|
||||
QScrollBar *sb = m_deviceLog->verticalScrollBar();
|
||||
sb->setValue(sb->maximum());
|
||||
}
|
||||
|
||||
void VMProWidget::onVolumeCorrectionModeChanged(VMProVolumeCorrectionMode mode)
|
||||
{
|
||||
m_currVolumeCorrectionMode = mode;
|
||||
|
||||
int index = m_volumePicker->findData(mode);
|
||||
if (index != -1) {
|
||||
m_volumePicker->setCurrentIndex(index);
|
||||
}
|
||||
}
|
||||
|
||||
void VMProWidget::onUserPieceSizeChanged(VMProVenturiSize size)
|
||||
{
|
||||
m_currVenturiSize = size;
|
||||
|
||||
int index = m_userPiecePicker->findData(size);
|
||||
if (index != -1) {
|
||||
m_userPiecePicker->setCurrentIndex(index);
|
||||
}
|
||||
}
|
||||
|
||||
void VMProWidget::onIdleTimeoutChanged(VMProIdleTimeout state)
|
||||
{
|
||||
m_currIdleTimeoutState = state;
|
||||
|
||||
int index = m_idleTimeoutPicker->findData(state);
|
||||
if (index != -1) {
|
||||
m_idleTimeoutPicker->setCurrentIndex(index);
|
||||
}
|
||||
}
|
||||
|
||||
void VMProWidget::onCalibrationProgressChanged(quint8 percentCompleted)
|
||||
{
|
||||
m_calibrationProgressLabel->setText(QString::number(percentCompleted));
|
||||
emit setNotification(QString("VMPro Calibration: %1 %").arg(percentCompleted), 3);
|
||||
}
|
||||
|
||||
void VMProWidget::onIdleTimeoutPickerChanged(int /*index*/)
|
||||
{
|
||||
VMProIdleTimeout newState = m_idleTimeoutPicker->currentData().value<VMProIdleTimeout>();
|
||||
|
||||
if (newState != m_currIdleTimeoutState)
|
||||
{
|
||||
m_vmProConfigurator->setIdleTimeout(newState);
|
||||
}
|
||||
}
|
||||
|
||||
void VMProWidget::onUserPieceSizePickerChanged(int /*index*/)
|
||||
{
|
||||
VMProVenturiSize newState = m_userPiecePicker->currentData().value<VMProVenturiSize>();
|
||||
|
||||
if (newState != m_currVenturiSize)
|
||||
{
|
||||
m_vmProConfigurator->setUserPieceSize(newState);
|
||||
}
|
||||
}
|
||||
|
||||
void VMProWidget::onVolumeCorrectionModePickerChanged(int /*index*/)
|
||||
{
|
||||
VMProVolumeCorrectionMode newState = m_volumePicker->currentData().value<VMProVolumeCorrectionMode>();
|
||||
if (newState != m_currVolumeCorrectionMode)
|
||||
{
|
||||
m_vmProConfigurator->setVolumeCorrectionMode(newState);
|
||||
}
|
||||
}
|
||||
|
||||
void VMProWidget::onSaveClicked()
|
||||
{
|
||||
QString fileName = QFileDialog::getSaveFileName(nullptr, tr("Save File"),
|
||||
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation));
|
||||
if (!fileName.isEmpty())
|
||||
{
|
||||
QFile outfile(fileName);
|
||||
if (outfile.open(QIODevice::WriteOnly | QIODevice::Text))
|
||||
{
|
||||
QTextStream stream(&outfile);
|
||||
stream << m_deviceLog->toPlainText();
|
||||
outfile.flush();
|
||||
outfile.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VMProWidget::onReconnected(QLowEnergyService *service)
|
||||
{
|
||||
m_vmProConfigurator->setupCharNotifications(service);
|
||||
|
||||
// Basic init of settings
|
||||
m_vmProConfigurator->getIdleTimeout();
|
||||
QThread::msleep(50);
|
||||
m_vmProConfigurator->getUserPieceSize();
|
||||
QThread::msleep(50);
|
||||
m_vmProConfigurator->getVolumeCorrectionMode();
|
||||
}
|
||||
67
src/Train/VMProWidget.h
Normal file
67
src/Train/VMProWidget.h
Normal file
@@ -0,0 +1,67 @@
|
||||
#ifndef VMPROWIDGET_H
|
||||
#define VMPROWIDGET_H
|
||||
|
||||
#include <QWidget>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QComboBox>
|
||||
#include <QGroupBox>
|
||||
#include <QLabel>
|
||||
#include <QLowEnergyService>
|
||||
#include <QTextEdit>
|
||||
|
||||
#include "VMProConfigurator.h"
|
||||
|
||||
class VMProWidget : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
VMProWidget(QLowEnergyService * service, QObject * parent);
|
||||
|
||||
void onReconnected(QLowEnergyService * service);
|
||||
|
||||
private slots:
|
||||
// Add a message for display to user
|
||||
void addStatusMessage(const QString &message);
|
||||
|
||||
// Add a message that ends up in log file
|
||||
//void addLogMessage(const QString &message);
|
||||
|
||||
// Methods that should update the GUI to reflect the new states
|
||||
void onVolumeCorrectionModeChanged(VMProVolumeCorrectionMode mode);
|
||||
void onUserPieceSizeChanged(VMProVenturiSize size);
|
||||
void onIdleTimeoutChanged(VMProIdleTimeout state);
|
||||
void onCalibrationProgressChanged(quint8 percentCompleted);
|
||||
|
||||
// Method to handle when user changes the values
|
||||
void onUserPieceSizePickerChanged(int index);
|
||||
void onIdleTimeoutPickerChanged(int index);
|
||||
void onVolumeCorrectionModePickerChanged(int index);
|
||||
|
||||
// File IO
|
||||
void onSaveClicked();
|
||||
|
||||
signals:
|
||||
void setNotification(QString msg, int timeout);
|
||||
|
||||
private:
|
||||
QLowEnergyService * m_vmProService;
|
||||
VMProConfigurator * m_vmProConfigurator;
|
||||
|
||||
QList<QString> m_logMessages;
|
||||
|
||||
QWidget * m_widget;
|
||||
QTextEdit * m_deviceLog;
|
||||
QComboBox * m_userPiecePicker;
|
||||
QComboBox * m_volumePicker;
|
||||
QComboBox * m_autocalibPicker;
|
||||
QComboBox * m_idleTimeoutPicker;
|
||||
QLabel * m_calibrationProgressLabel;
|
||||
|
||||
// Current settings
|
||||
VMProIdleTimeout m_currIdleTimeoutState;
|
||||
VMProVenturiSize m_currVenturiSize;
|
||||
VMProVolumeCorrectionMode m_currVolumeCorrectionMode;
|
||||
};
|
||||
|
||||
#endif // VMPROWIDGET_H
|
||||
@@ -651,6 +651,8 @@ greaterThan(QT_MAJOR_VERSION, 4) {
|
||||
QT += bluetooth
|
||||
HEADERS += Train/BT40Controller.h Train/BT40Device.h
|
||||
SOURCES += Train/BT40Controller.cpp Train/BT40Device.cpp
|
||||
HEADERS += Train/VMProConfigurator.h Train/VMProWidget.h
|
||||
SOURCES += Train/VMProConfigurator.cpp Train/VMProWidget.cpp
|
||||
}
|
||||
|
||||
# qt charts is officially supported from QT5.8 or higher
|
||||
|
||||
11
test/rides/2020_02_03_09_42_51.vo2
Normal file
11
test/rides/2020_02_03_09_42_51.vo2
Normal file
@@ -0,0 +1,11 @@
|
||||
secs, rf, rmv, vo2, vco2, tv, feo2
|
||||
2.504, 26.68, 78.62, 929, 0, 2.94, 19.11
|
||||
5.039, 23.04, 70.53, 1001, 0, 3.06, 18.98
|
||||
8.207, 21.05, 65.1, 1025, 0, 3.09, 18.86
|
||||
10.743, 20.08, 61.04, 1051, 0, 3.03, 18.81
|
||||
14.543, 20.08, 60.99, 991, 0, 3.03, 18.77
|
||||
16.446, 23.64, 66.6, 1047, 0, 2.81, 18.73
|
||||
19.614, 20.53, 50.44, 933, 0, 2.45, 18.69
|
||||
22.783, 19.26, 56.54, 984, 0, 2.93, 18.58
|
||||
25.952, 19.11, 49.72, 948, 0, 2.6, 18.5
|
||||
29.121, 18.58, 47.09, 923, 0, 2.53, 18.41
|
||||
Reference in New Issue
Block a user