diff --git a/.gitignore b/.gitignore index f77b22e9c..715e4ff06 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,5 @@ src/inner_templates/googlemaps/cesium-key.js .vscode/settings.json /tst/Devices/.vs src/inner_templates/googlemaps/cesium-key.js + +build/ diff --git a/defaults.pri b/defaults.pri index 807f6485c..d2dbd1abd 100644 --- a/defaults.pri +++ b/defaults.pri @@ -2,6 +2,11 @@ QT += gui bluetooth widgets xml positioning quick networkauth websockets texttos QTPLUGIN += qavfmediaplayer QT+= charts +windows: QT += serialport + +isEmpty(QMAKE_IOS_DEPLOYMENT_TARGET) { + unix:!android: QT += serialport +} unix:android: QT += androidextras gui-private android: include(android_openssl/openssl.pri) diff --git a/docs/10_Installation.md b/docs/10_Installation.md index 402fc804c..4ce762859 100644 --- a/docs/10_Installation.md +++ b/docs/10_Installation.md @@ -10,7 +10,7 @@ These instructions build the app itself, not the test project. ```buildoutcfg $ sudo apt update && sudo apt upgrade # this is very important on raspberry pi: you need the bluetooth firmware updated! -$ sudo apt install git qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-assistant libqt5networkauth5-dev libqt5websockets5-dev qml-module* libqt5texttospeech5-dev libqt5texttospeech5 libqt5location5-plugins qtlocation5-dev qtmultimedia5-dev libqt5multimediawidgets5 libqt5multimedia5-plugins libqt5multimedia5 g++ make +$ sudo apt install git qtquickcontrols2-5-dev libqt5serialport5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-assistant libqt5networkauth5-dev libqt5websockets5-dev qml-module* libqt5texttospeech5-dev libqt5texttospeech5 libqt5location5-plugins qtlocation5-dev qtmultimedia5-dev libqt5multimediawidgets5 libqt5multimedia5-plugins libqt5multimedia5 g++ make $ git clone https://github.com/cagnulein/qdomyos-zwift.git $ cd qdomyos-zwift $ git submodule update --init src/smtpclient/ @@ -106,7 +106,7 @@ This operation takes a moment to complete. #### Install qdomyos-zwift from sources ```bash -sudo apt install git libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-assistant libqt5networkauth5-dev libqt5websockets5-dev qtmultimedia5-dev libqt5multimediawidgets5 libqt5multimedia5-plugins libqt5multimedia5 qtlocation5-dev qtquickcontrols2-5-dev libqt5texttospeech5-dev libqt5texttospeech5 g++ make +sudo apt install git libqt5bluetooth5 libqt5widgets5 libqt5serialport5-dev libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-assistant libqt5networkauth5-dev libqt5websockets5-dev qtmultimedia5-dev libqt5multimediawidgets5 libqt5multimedia5-plugins libqt5multimedia5 qtlocation5-dev qtquickcontrols2-5-dev libqt5texttospeech5-dev libqt5texttospeech5 g++ make git clone https://github.com/cagnulein/qdomyos-zwift.git cd qdomyos-zwift git submodule update --init src/smtpclient/ diff --git a/src/devices/bike.cpp b/src/devices/bike.cpp index b64d715d5..0a7da41d8 100644 --- a/src/devices/bike.cpp +++ b/src/devices/bike.cpp @@ -37,6 +37,7 @@ void bike::changeInclination(double grade, double percentage) { qDebug() << QStringLiteral("bike::changeInclination") << autoResistanceEnable << grade << percentage; lastRawRequestedInclinationValue = grade; if (autoResistanceEnable) { + qDebug() << QStringLiteral("setting bike::requestInclination") << grade; requestInclination = grade; } emit inclinationChanged(grade, percentage); @@ -53,6 +54,7 @@ uint16_t bike::powerFromResistanceRequest(resistance_t requestResistance) { void bike::changeRequestedPelotonResistance(int8_t resistance) { RequestedPelotonResistance = resistance; } void bike::changeCadence(int16_t cadence) { RequestedCadence = cadence; } + void bike::changePower(int32_t power) { RequestedPower = power; // in order to paint in any case the request power on the charts @@ -63,23 +65,36 @@ void bike::changePower(int32_t power) { } requestPower = power; // used by some bikes that have ERG mode builtin + + if(this->ergModeSupported) + { + qDebug() << QStringLiteral("changePower to ") << power << QStringLiteral("W using built-in ERG mode."); + return; + } + QSettings settings; bool force_resistance = settings.value(QZSettings::virtualbike_forceresistance, QZSettings::default_virtualbike_forceresistance) .toBool(); - // bool erg_mode = settings.value(QZSettings::zwift_erg, QZSettings::default_zwift_erg).toBool(); //Not used - // anywhere in code + + if(!force_resistance) { + qDebug() << QStringLiteral("changePower to ") << power << QStringLiteral("W ignored because force_resistance setting is off"); + return; + } + double erg_filter_upper = settings.value(QZSettings::zwift_erg_filter, QZSettings::default_zwift_erg_filter).toDouble(); double erg_filter_lower = settings.value(QZSettings::zwift_erg_filter_down, QZSettings::default_zwift_erg_filter_down).toDouble(); - double deltaDown = wattsMetric().value() - ((double)power); - double deltaUp = ((double)power) - wattsMetric().value(); + double watts = this->wattsMetric().value(); + double deltaDown = watts - ((double)power); + double deltaUp = -deltaDown; qDebug() << QStringLiteral("filter ") + QString::number(deltaUp) + " " + QString::number(deltaDown) + " " + QString::number(erg_filter_upper) + " " + QString::number(erg_filter_lower); - if (!ergModeSupported && force_resistance /*&& erg_mode*/ && - (deltaUp > erg_filter_upper || deltaDown > erg_filter_lower)) { + if (deltaUp > erg_filter_upper || deltaDown > erg_filter_lower) { resistance_t r = (resistance_t)resistanceFromPowerRequest(power); + + qDebug() << QStringLiteral("Changing resistance to ") << r; changeResistance(r); // resistance start from 1 } } diff --git a/src/devices/bluetooth.cpp b/src/devices/bluetooth.cpp index a0843d105..b5711184b 100644 --- a/src/devices/bluetooth.cpp +++ b/src/devices/bluetooth.cpp @@ -21,6 +21,7 @@ bluetooth::bluetooth(bool logs, const QString &deviceName, bool noWriteResistanc uint32_t pollDeviceTime, bool noConsole, bool testResistance, int8_t bikeResistanceOffset, double bikeResistanceGain, bool startDiscovery) { QSettings settings; + QLoggingCategory::setFilterRules(QStringLiteral("qt.bluetooth* = true")); filterDevice = deviceName; this->testResistance = testResistance; @@ -89,8 +90,68 @@ bluetooth::bluetooth(bool logs, const QString &deviceName, bool noWriteResistanc #ifndef Q_OS_WIN discoveryAgent->setLowEnergyDiscoveryTimeout(10000); #endif - this->startDiscovery(); } + + this->startDiscovery(); +} + +void bluetooth::nonBluetoothDeviceDiscovery() { + bluetoothdevice * nonBluetoothDevice = nullptr; + + this->discoveringNonBluetooth = true; + try { + nonBluetoothDevice = this->discoverNonBluetoothDevices(); + } catch(std::exception const& e) { + this->discoveringNonBluetooth = false; + debug("Exception thrown while looking for non-bluetooth devices"); + debug(e.what()); + throw; + } catch(...) { + this->discoveringNonBluetooth = false; + debug("Error was thrown while looking for non-bluetooth devices"); + throw; + } + + this->discoveringNonBluetooth = false; + + if(nonBluetoothDevice) { + this->stopDiscovery(); + this->signalBluetoothDeviceConnected(nonBluetoothDevice); + emit this->deviceConnected(nonBluetoothDevice->bluetoothDevice); + connect(nonBluetoothDevice, &bluetoothdevice::connectedAndDiscovered, this, &bluetooth::connectedAndDiscovered); + this->connectedAndDiscovered(); + } +} + +bluetoothdevice * bluetooth::discoverNonBluetoothDevices() { + QSettings settings; + + /* + Calling code expects the returned bluetoothdevice subclass object to have the fake bluetooth device info set. + + Do this in the class constructor as follows: + 1. Go to a website and generate a Bluetooth UUID. + 2. Set bluetoothdevice::bluetoothDevice as follows, using the UUID you have obtained, and a user-friendly name. + + this->bluetoothDevice = + QBluetoothDeviceInfo(QBluetoothUuid {QStringLiteral("775f25bd-6636-4cdc-9398-839ae026be1d")}, "Device Name", 0); + */ + + + // Try to connect to a Trixter X-Dream V1 bike if the setting is enabled. + this->trixterXDreamV1Bike = this->findTrixterXDreamV1Bike(settings); + if(this->trixterXDreamV1Bike) + { + return this->trixterXDreamV1Bike; + } + if(!this->discovering) return nullptr; + + // Test for other devices and check that discovery hasn't been cancelled. + + + // nothing found + return nullptr; + } bluetooth::~bluetooth() { @@ -188,33 +249,40 @@ void bluetooth::startDiscovery() { if (!this->useDiscovery) return; -#ifndef Q_OS_IOS - QSettings settings; - bool technogym_myrun_treadmill_experimental = settings - .value(QZSettings::technogym_myrun_treadmill_experimental, - QZSettings::default_technogym_myrun_treadmill_experimental) - .toBool(); - bool trx_route_key = settings.value(QZSettings::trx_route_key, QZSettings::default_trx_route_key).toBool(); - bool bh_spada_2 = settings.value(QZSettings::bh_spada_2, QZSettings::default_bh_spada_2).toBool(); - bool iconcept_elliptical = - settings.value(QZSettings::iconcept_elliptical, QZSettings::default_iconcept_elliptical).toBool(); + this->discovering = true; - if (!trx_route_key && !bh_spada_2 && !technogym_myrun_treadmill_experimental && !iconcept_elliptical) { + if(this->discoveryAgent) + { +#ifndef Q_OS_IOS + QSettings settings; + bool technogym_myrun_treadmill_experimental = settings + .value(QZSettings::technogym_myrun_treadmill_experimental, + QZSettings::default_technogym_myrun_treadmill_experimental) + .toBool(); + bool trx_route_key = settings.value(QZSettings::trx_route_key, QZSettings::default_trx_route_key).toBool(); + bool bh_spada_2 = settings.value(QZSettings::bh_spada_2, QZSettings::default_bh_spada_2).toBool(); + bool iconcept_elliptical = + settings.value(QZSettings::iconcept_elliptical, QZSettings::default_iconcept_elliptical).toBool(); + + if (!trx_route_key && !bh_spada_2 && !technogym_myrun_treadmill_experimental && !iconcept_elliptical) { #endif - discoveryAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod); + discoveryAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod); #ifndef Q_OS_IOS - } else { - discoveryAgent->start(QBluetoothDeviceDiscoveryAgent::ClassicMethod | - QBluetoothDeviceDiscoveryAgent::LowEnergyMethod); - } + } else { + discoveryAgent->start(QBluetoothDeviceDiscoveryAgent::ClassicMethod | + QBluetoothDeviceDiscoveryAgent::LowEnergyMethod); + } #endif + } + + QTimer::singleShot(1, this, &bluetooth::nonBluetoothDeviceDiscovery); } -void bluetooth::stopDiscovery() { - if (this->discoveryAgent) - this->discoveryAgent->stop(); - else - qDebug() << "bluetooth::stopDiscovery() called when discoveryAgent is not defined. "; + +void bluetooth::stopDiscovery() +{ + this->discovering = false; + if(this->discoveryAgent) this->discoveryAgent->stop(); } void bluetooth::canceled() { @@ -230,6 +298,23 @@ void bluetooth::debug(const QString &text) { } } + +trixterxdreamv1bike * bluetooth::findTrixterXDreamV1Bike(const QSettings& settings) +{ + bool trixterxdreamv1bikeEnabled = settings.value(trixterxdreamv1settings::keys::Enabled, false).toBool(); + trixterxdreamv1bike * result = nullptr; + if(trixterxdreamv1bikeEnabled) { + debug("Looking for Trixter X-Dream V1 Bike"); + result = trixterxdreamv1bike::tryCreate(this->noWriteResistance, this->noHeartService, false); + if(!result) + debug("Failed to find a Trixter X-Dream V1 Bike"); + } else { + debug("Not looking for Trixter X-Dream V1 Bike - disabled in settings"); + } + + return result; +} + bool bluetooth::cscSensorAvaiable() { QSettings settings; @@ -1865,6 +1950,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { b.name().length() == 11)) // CARE9040177 - Carefitness CV-351 && !sportsPlusBike && filter) { this->setLastBluetoothDevice(b); + this->setLastBluetoothDevice(b); this->stopDiscovery(); sportsPlusBike = new sportsplusbike(noWriteResistance, noHeartService); // stateFileRead(); @@ -3318,6 +3404,10 @@ void bluetooth::restart() { delete eliteSterzoSmart; eliteSterzoSmart = nullptr; } + if (trixterXDreamV1Bike) { + delete trixterXDreamV1Bike; + trixterXDreamV1Bike = nullptr; + } this->startDiscovery(); } @@ -3517,7 +3607,10 @@ bluetoothdevice *bluetooth::device() { } else if (csafeRower) { return csafeRower; #endif + } else if (trixterXDreamV1Bike){ + return trixterXDreamV1Bike; } + return nullptr; } diff --git a/src/devices/bluetooth.h b/src/devices/bluetooth.h old mode 100644 new mode 100755 index f5df33c07..48006b83a --- a/src/devices/bluetooth.h +++ b/src/devices/bluetooth.h @@ -125,6 +125,7 @@ #include "devices/toorxtreadmill/toorxtreadmill.h" #include "devices/treadmill.h" #include "devices/truetreadmill/truetreadmill.h" +#include "devices/trixterxdreamv1bike/trixterxdreamv1bike.h" #include "devices/trxappgateusbbike/trxappgateusbbike.h" #include "devices/trxappgateusbelliptical/trxappgateusbelliptical.h" #include "devices/trxappgateusbtreadmill/trxappgateusbtreadmill.h" @@ -159,10 +160,25 @@ class bluetooth : public QObject, public SignalHandler { bool onlyDiscover = false; volatile bool homeformLoaded = false; - private: + /** + * @brief nonBluetoothDeviceDiscovery Called by the non-bluetooth discovery thread to identify using + * discoverNonBluetoothDevices() and connect non-Bluetooth devices. + */ + void nonBluetoothDeviceDiscovery(); +protected: + /** + * @brief discoverNonBluetoothDevices Discover non-bluetooth devices and create an object for the first. + * @return An object for the first non-bluetooth device found. + */ + bluetoothdevice * discoverNonBluetoothDevices(); +private: bool useDiscovery = false; QFile *debugCommsLog = nullptr; - QBluetoothDeviceDiscoveryAgent *discoveryAgent = nullptr; + // Indicates generally discovering, bluetooth and others + bool discovering = false; + // Indicates the non-bluetooth discovery is active + bool discoveringNonBluetooth = false; + QBluetoothDeviceDiscoveryAgent *discoveryAgent=nullptr; antbike *antBike = nullptr; apexbike *apexBike = nullptr; bkoolbike *bkoolBike = nullptr; @@ -275,6 +291,7 @@ class bluetooth : public QObject, public SignalHandler { QList zwiftPlayDevice; zwiftclickremote* zwiftClickRemote = nullptr; QString filterDevice = QLatin1String(""); + trixterxdreamv1bike * trixterXDreamV1Bike = nullptr; bool testResistance = false; bool noWriteResistance = false; @@ -310,6 +327,13 @@ class bluetooth : public QObject, public SignalHandler { bool zwiftDeviceAvaiable(); bool fitmetria_fanfit_isconnected(QString name); + /** + * @brief findTrixterXDreamV1Bike Searches serial ports for a Trixter X-Dream V1 Bike + * @param settings The application settings. + * @return A trixterxdreamv1bike object if a bike has been found, nullptr otherwise. + */ + class trixterxdreamv1bike * findTrixterXDreamV1Bike(const QSettings& settings); + #ifdef Q_OS_WIN QTimer discoveryTimeout; #endif @@ -347,7 +371,8 @@ class bluetooth : public QObject, public SignalHandler { void inclinationChanged(double, double); void connectedAndDiscovered(); - signals: +signals: + }; #endif // BLUETOOTH_H diff --git a/src/devices/bluetoothdevice.h b/src/devices/bluetoothdevice.h index afba22a76..39c01bfcf 100644 --- a/src/devices/bluetoothdevice.h +++ b/src/devices/bluetoothdevice.h @@ -568,12 +568,12 @@ class bluetoothdevice : public QObject { metric elevationAcc; /** - * @brief m_watt Metric to get and set the power expended in the session. Unit: watts + * @brief m_watt Metric to get and set the current power being expended. Unit: watts */ metric m_watt; /** - * @brief WattKg Metric to get and set the watt kg for the session (what's this?). Unit: watt kg + * @brief WattKg Metric to get and set the current watts per kg. E.g. power / mass. Unit: watt/kg */ metric WattKg; diff --git a/src/devices/trixterxdreamv1bike/qserialdatasource.cpp b/src/devices/trixterxdreamv1bike/qserialdatasource.cpp new file mode 100644 index 000000000..650263352 --- /dev/null +++ b/src/devices/trixterxdreamv1bike/qserialdatasource.cpp @@ -0,0 +1,88 @@ +#include "qserialdatasource.h" +#if !defined(Q_OS_IOS) && !defined(Q_OS_ANDROID) + +#include +#include +#include + +QStringList qserialdatasource::get_availablePorts() { + QStringList result; + + auto ports = QSerialPortInfo::availablePorts(); + for(const auto &port : ports) { + QString portName = port.portName(); + +#if defined(Q_OS_LINUX) + if(!portName.startsWith("ttyUSB")) + { + qDebug() << "Skipping port: " << portName << " because it doesn't start with ttyUSB"; + continue; + } +#endif + result.push_back(portName); + qDebug() << "Found portName:" << portName + << "," << "description:" << port.description() + << "," << "vender identifier:" << port.vendorIdentifier() + << "," << "manufacturer:" << port.manufacturer() + << "," << "product identifier:" << port.productIdentifier() + << "," << "system location:" << port.systemLocation() + << "," << "isBusy:" << port.isBusy() + << "," << "isNull:" << port.isNull() + << "," << "serialNumber:" << port.serialNumber(); + } + return result; +} + +qserialdatasource::qserialdatasource(QObject *parent) : serialdatasource() +{ + this->serial = new QSerialPort(parent); +} + +bool qserialdatasource::open(const QString& portName) { + this->serial->setPortName(portName); + this->serial->setBaudRate(QSerialPort::Baud115200); + this->serial->setDataBits(QSerialPort::Data8); + this->serial->setStopBits(QSerialPort::OneStop); + this->serial->setFlowControl(QSerialPort::NoFlowControl); + this->serial->setParity(QSerialPort::NoParity); + this->serial->setReadBufferSize(4096); + + return this->serial->open(QIODevice::ReadWrite); +} + +qint64 qserialdatasource::write(const QByteArray &data) { + return this->serial->write(data); +} + +void qserialdatasource::flush() { + this->serial->flush(); +} + +bool qserialdatasource::waitForReadyRead() { + return this->serial->waitForReadyRead(1); +} + +QByteArray qserialdatasource::readAll() { + return this->serial->readAll(); +} + +qint64 qserialdatasource::readBufferSize() { + return this->serial->readBufferSize(); +} + +QString qserialdatasource::error() { + return serial->parent()->tr("%1").arg(this->serial->error()); +} + +void qserialdatasource::close() { + this->serial->close(); +} + +qserialdatasource::~qserialdatasource() { + if(this->serial!=nullptr) { + delete this->serial; + this->serial = nullptr; + } +} + +#endif diff --git a/src/devices/trixterxdreamv1bike/qserialdatasource.h b/src/devices/trixterxdreamv1bike/qserialdatasource.h new file mode 100644 index 000000000..93c1ee6f5 --- /dev/null +++ b/src/devices/trixterxdreamv1bike/qserialdatasource.h @@ -0,0 +1,34 @@ +#ifndef QSERIALDATASOURCE_H +#define QSERIALDATASOURCE_H +#include + +// Not compatible with Android and iOS. +#if !defined(Q_OS_IOS) && !defined(Q_OS_ANDROID) +#include +#include "serialdatasource.h" + +/** + * @brief An implementation of serialdatasource that uses Qt's QSerialPort class. + */ +class qserialdatasource : public serialdatasource { + private: + class QSerialPort * serial; + public: + qserialdatasource(QObject *parent); + + QStringList get_availablePorts() override; + + bool open(const QString& portName) override; + qint64 write(const QByteArray& data) override; + void flush() override; + bool waitForReadyRead() override; + QByteArray readAll() override; + qint64 readBufferSize() override; + QString error() override; + void close() override; + + ~qserialdatasource() override; +}; + +#endif +#endif // QSERIALDATASOURCE_H diff --git a/src/devices/trixterxdreamv1bike/serialdatasource.cpp b/src/devices/trixterxdreamv1bike/serialdatasource.cpp new file mode 100644 index 000000000..a904e39c0 --- /dev/null +++ b/src/devices/trixterxdreamv1bike/serialdatasource.cpp @@ -0,0 +1,2 @@ +#include "serialdatasource.h" + diff --git a/src/devices/trixterxdreamv1bike/serialdatasource.h b/src/devices/trixterxdreamv1bike/serialdatasource.h new file mode 100644 index 000000000..ef0695151 --- /dev/null +++ b/src/devices/trixterxdreamv1bike/serialdatasource.h @@ -0,0 +1,72 @@ +#ifndef SERIALDATASOURCE_H +#define SERIALDATASOURCE_H + +#include +#include + +/** + * @brief An interface for a serial I/O device, based on the interface of QSerialPort. + */ +class serialdatasource +{ + protected: + serialdatasource() {} + serialdatasource(const serialdatasource& other) { Q_UNUSED(other) } + public: + /** + * @brief Gets a list of available port names. + */ + virtual QStringList get_availablePorts() = 0; + + /** + * @brief Tries to open the specified port. + * @param portName The name of the port to open. + * @return Boolean indicating success. + */ + virtual bool open(const QString& portName) = 0; + + /** + * @brief Writes the specified data to the port. + * @param data The data to write. + * @return + */ + virtual qint64 write(const QByteArray& data)=0; + + /** + * @brief Flush the port buffer. + */ + virtual void flush() = 0; + + /** + * @brief Returns true/false to indicate if data is ready for reading after a maximum of 1ms. + * @return + */ + virtual bool waitForReadyRead()=0; + + /** + * @brief Reads all the data from the buffer. + * @return + */ + virtual QByteArray readAll() = 0; + + /** + * @brief The number of bytes in the buffer. + * @return + */ + virtual qint64 readBufferSize() =0; + + /** + * @brief An error string for debug/logging. + * @return + */ + virtual QString error() =0; + + /** + * @brief Close the port. + */ + virtual void close()=0; + + virtual ~serialdatasource() = default; +}; + +#endif // SERIALDATASOURCE_H diff --git a/src/devices/trixterxdreamv1bike/trixterxdreamv1bike.cpp b/src/devices/trixterxdreamv1bike/trixterxdreamv1bike.cpp new file mode 100644 index 000000000..114c1afa2 --- /dev/null +++ b/src/devices/trixterxdreamv1bike/trixterxdreamv1bike.cpp @@ -0,0 +1,732 @@ +#include "trixterxdreamv1bike.h" +#include "trixterxdreamv1serial.h" +#include "trixterxdreamv1settings.h" +#include "qcoreevent.h" +#include +#include +#include +#include + +using namespace std; + + +static std::vector> powerTable = + { + /* 30 RPM */ { 5,5,5,5,5,5,5,5,6,6,6,6,6,6,6,6,6,7,7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8,8,8,9,9,9,9,9,9,9,9,9,10,10,10,10,10,10,10,11,11,12,12,12,13,13,13,14,14,14,15,15,16,16,16,17,17,17,18,18,18,19,19,20,20,20,21,21,21,22,22,22,23,23,24,24,24,25,25,25,26,26,26,27,27,28,28,28,29,29,30,30,30,31,31,32,32,32,33,33,34,34,35,35,35,36,36,37,37,37,38,38,39,39,40,40,40,41,41,42,42,42,43,43,44,44,45,45,45,46,46,47,47,47,48,48,49,49,49,50,50,50,51,51,51,52,52,52,53,53,53,54,54,54,55,55,55,56,56,56,56,57,57,57,58,58,58,59,59,59,60,60,60,61,61,61,62,62,62,63,63,63,63,64,64,64,65,65,65,65,66,66,66,66,66,67,67,67,67,67,67,68,68,68,68,68,68,69,69,69,69,69,70,70,70,70,70,70,71,71,71,71,71,71,72,72,72,72,72,73,73,73,73,73,73,74,74,74 }, + /* 31 RPM */ { 5,5,5,5,5,5,5,6,6,6,6,6,6,6,6,6,7,7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8,8,9,9,9,9,9,9,9,9,9,10,10,10,10,10,10,10,10,11,11,11,12,12,13,13,13,14,14,14,15,15,16,16,16,17,17,18,18,18,19,19,19,20,20,21,21,21,22,22,23,23,23,24,24,24,25,25,26,26,26,27,27,27,28,28,29,29,29,30,30,31,31,32,32,33,33,33,34,34,35,35,36,36,37,37,38,38,38,39,39,40,40,41,41,42,42,43,43,43,44,44,45,45,46,46,47,47,48,48,48,49,49,50,50,51,51,52,52,52,53,53,53,54,54,55,55,55,56,56,56,57,57,57,58,58,58,59,59,59,60,60,60,61,61,61,62,62,62,63,63,63,64,64,64,65,65,65,66,66,66,67,67,67,68,68,69,69,69,69,70,70,70,70,70,71,71,71,71,71,72,72,72,72,72,72,73,73,73,73,73,74,74,74,74,74,75,75,75,75,75,76,76,76,76,76,76,77,77,77,77,77,78,78,78,78,78,79,79 }, + /* 32 RPM */ { 5,5,5,5,5,5,6,6,6,6,6,6,6,6,6,7,7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8,8,9,9,9,9,9,9,9,9,9,10,10,10,10,10,10,10,10,10,11,11,12,12,12,13,13,14,14,14,15,15,16,16,16,17,17,18,18,18,19,19,20,20,20,21,21,22,22,23,23,23,24,24,25,25,25,26,26,27,27,27,28,28,29,29,29,30,30,31,31,32,32,33,33,34,34,34,35,35,36,36,37,37,38,38,39,39,40,40,41,41,42,42,43,43,44,44,45,45,46,46,47,47,48,48,49,49,50,50,51,51,52,52,53,53,54,54,55,55,55,56,56,57,57,57,58,58,58,59,59,59,60,60,61,61,61,62,62,62,63,63,63,64,64,65,65,65,66,66,66,67,67,67,68,68,69,69,69,70,70,70,71,71,71,72,72,73,73,73,74,74,74,74,74,75,75,75,75,75,76,76,76,76,76,77,77,77,77,77,78,78,78,78,78,79,79,79,79,79,80,80,80,80,80,81,81,81,81,82,82,82,82,82,83,83,83,83,83,84 }, + /* 33 RPM */ { 5,5,5,5,5,6,6,6,6,6,6,6,6,6,7,7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8,8,9,9,9,9,9,9,9,9,9,10,10,10,10,10,10,10,10,10,11,11,11,12,12,13,13,14,14,14,15,15,16,16,16,17,17,18,18,19,19,19,20,20,21,21,22,22,22,23,23,24,24,25,25,25,26,26,27,27,27,28,28,29,29,30,30,30,31,31,32,32,33,33,34,34,35,35,36,36,37,38,38,39,39,40,40,41,41,42,42,43,43,44,44,45,45,46,47,47,48,48,49,49,50,50,51,51,52,52,53,53,54,54,55,55,56,57,57,58,58,59,59,59,60,60,60,61,61,62,62,62,63,63,64,64,64,65,65,65,66,66,67,67,67,68,68,69,69,69,70,70,70,71,71,72,72,72,73,73,74,74,74,75,75,76,76,76,77,77,77,78,78,78,78,79,79,79,79,79,80,80,80,80,80,81,81,81,81,82,82,82,82,82,83,83,83,83,84,84,84,84,84,85,85,85,85,86,86,86,86,86,87,87,87,87,87,88,88,88,88 }, + /* 34 RPM */ { 5,5,5,5,6,6,6,6,6,6,6,6,6,7,7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8,8,9,9,9,9,9,9,9,9,9,10,10,10,10,10,10,10,10,11,11,11,11,12,12,13,13,13,14,14,15,15,16,16,16,17,17,18,18,19,19,20,20,20,21,21,22,22,23,23,24,24,24,25,25,26,26,27,27,28,28,28,29,29,30,30,31,31,32,32,32,33,33,34,35,35,36,36,37,37,38,39,39,40,40,41,41,42,42,43,44,44,45,45,46,46,47,48,48,49,49,50,50,51,52,52,53,53,54,54,55,55,56,57,57,58,58,59,59,60,61,61,62,62,62,63,63,64,64,64,65,65,66,66,66,67,67,68,68,68,69,69,70,70,71,71,71,72,72,73,73,73,74,74,75,75,75,76,76,77,77,77,78,78,79,79,80,80,80,81,81,82,82,82,82,82,83,83,83,83,84,84,84,84,85,85,85,85,85,86,86,86,86,87,87,87,87,88,88,88,88,89,89,89,89,89,90,90,90,90,91,91,91,91,92,92,92,92,92,93,93,93 }, + /* 35 RPM */ { 5,5,5,6,6,6,6,6,6,6,6,6,7,7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8,9,9,9,9,9,9,9,9,9,10,10,10,10,10,10,10,10,10,11,11,11,11,11,12,12,13,13,14,14,15,15,16,16,16,17,17,18,18,19,19,20,20,21,21,22,22,22,23,23,24,24,25,25,26,26,27,27,28,28,28,29,29,30,30,31,31,32,32,33,33,34,34,35,35,36,36,37,38,38,39,39,40,41,41,42,42,43,44,44,45,45,46,47,47,48,48,49,50,50,51,51,52,53,53,54,55,55,56,56,57,58,58,59,59,60,61,61,62,62,63,64,64,65,65,65,66,66,67,67,68,68,68,69,69,70,70,71,71,71,72,72,73,73,74,74,74,75,75,76,76,77,77,77,78,78,79,79,80,80,80,81,81,82,82,83,83,84,84,84,85,85,86,86,86,86,87,87,87,87,88,88,88,88,89,89,89,89,90,90,90,90,91,91,91,91,92,92,92,92,93,93,93,93,94,94,94,94,94,95,95,95,95,96,96,96,96,97,97,97,97,98,98 }, + /* 36 RPM */ { 5,5,6,6,6,6,6,6,6,6,6,7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8,8,9,9,9,9,9,9,9,9,9,10,10,10,10,10,10,10,10,10,11,11,11,11,11,12,12,12,13,13,14,14,15,15,16,16,17,17,18,18,19,19,20,20,21,21,22,22,23,23,24,24,25,25,26,26,26,27,27,28,28,29,29,30,30,31,31,32,32,33,33,34,34,35,35,36,36,37,38,38,39,40,40,41,42,42,43,43,44,45,45,46,47,47,48,49,49,50,51,51,52,52,53,54,54,55,56,56,57,58,58,59,59,60,61,61,62,63,63,64,65,65,66,66,67,68,68,68,69,69,70,70,71,71,72,72,73,73,73,74,74,75,75,76,76,77,77,78,78,78,79,79,80,80,81,81,82,82,83,83,83,84,84,85,85,86,86,87,87,88,88,88,89,89,90,90,90,91,91,91,91,92,92,92,92,93,93,93,93,94,94,94,94,95,95,95,95,96,96,96,97,97,97,97,98,98,98,98,99,99,99,99,100,100,100,100,101,101,101,101,102,102,102,102,103 }, + /* 37 RPM */ { 5,6,6,6,6,6,6,6,6,6,7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8,8,9,9,9,9,9,9,9,9,9,10,10,10,10,10,10,10,10,10,11,11,11,11,11,11,12,12,13,13,14,14,15,15,16,16,17,17,18,18,19,19,20,20,21,21,22,22,23,23,24,24,25,25,26,26,27,27,28,28,29,29,30,30,31,31,32,32,33,33,34,34,35,35,36,36,37,38,38,39,40,40,41,42,42,43,44,44,45,46,46,47,48,49,49,50,51,51,52,53,53,54,55,55,56,57,57,58,59,59,60,61,61,62,63,63,64,65,65,66,67,67,68,69,69,70,71,71,72,72,73,73,73,74,74,75,75,76,76,77,77,78,78,79,79,80,80,81,81,82,82,82,83,83,84,84,85,85,86,86,87,87,88,88,89,89,90,90,91,91,92,92,92,93,93,94,94,94,95,95,95,96,96,96,96,97,97,97,97,98,98,98,99,99,99,99,100,100,100,100,101,101,101,102,102,102,102,103,103,103,103,104,104,104,105,105,105,105,106,106,106,106,107,107,107,108 }, + /* 38 RPM */ { 6,6,6,6,6,6,6,6,6,7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8,8,9,9,9,9,9,9,9,9,9,10,10,10,10,10,10,10,10,11,11,11,11,11,11,11,11,12,12,13,13,14,14,15,16,16,17,17,18,18,19,19,20,20,21,21,22,22,23,23,24,24,25,25,26,27,27,28,28,29,29,30,30,31,31,32,32,33,33,34,34,35,35,36,36,37,38,38,39,40,40,41,42,42,43,44,45,45,46,47,47,48,49,50,50,51,52,52,53,54,55,55,56,57,57,58,59,60,60,61,62,62,63,64,65,65,66,67,67,68,69,70,70,71,72,72,73,74,74,75,75,76,76,77,77,78,78,79,79,80,80,81,81,82,82,83,83,84,84,85,85,86,86,87,87,88,88,89,89,90,90,91,91,92,92,93,93,94,94,95,95,96,96,97,97,98,98,98,99,99,99,99,100,100,100,101,101,101,101,102,102,102,103,103,103,103,104,104,104,105,105,105,105,106,106,106,107,107,107,107,108,108,108,109,109,109,109,110,110,110,111,111,111,111,112,112,112 }, + /* 39 RPM */ { 6,6,6,6,6,6,6,6,7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8,8,9,9,9,9,9,9,9,9,10,10,10,10,10,10,10,10,10,11,11,11,11,11,11,11,11,11,12,13,13,14,14,15,15,16,16,17,17,18,19,19,20,20,21,21,22,22,23,23,24,25,25,26,26,27,27,28,28,29,29,30,31,31,32,32,33,33,34,34,35,35,36,36,37,38,38,39,39,40,41,42,42,43,44,45,45,46,47,48,48,49,50,51,51,52,53,54,54,55,56,57,57,58,59,60,60,61,62,63,63,64,65,66,66,67,68,69,69,70,71,72,72,73,74,75,75,76,77,77,78,78,79,79,80,80,81,81,82,82,83,83,84,84,85,86,86,87,87,88,88,89,89,90,90,91,91,92,92,93,93,94,94,95,95,96,96,97,97,98,98,99,100,100,101,101,102,102,102,103,103,103,104,104,104,105,105,105,105,106,106,106,107,107,107,108,108,108,108,109,109,109,110,110,110,111,111,111,111,112,112,112,113,113,113,113,114,114,114,115,115,115,116,116,116,116,117,117 }, + /* 40 RPM */ { 6,6,6,6,6,6,6,7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8,8,9,9,9,9,9,9,9,9,10,10,10,10,10,10,10,10,10,11,11,11,11,11,11,11,11,12,12,12,13,13,14,14,15,16,16,17,17,18,18,19,20,20,21,21,22,22,23,23,24,25,25,26,26,27,27,28,29,29,30,30,31,31,32,32,33,34,34,35,35,36,36,37,38,38,39,39,40,41,41,42,43,44,45,45,46,47,48,48,49,50,51,52,52,53,54,55,56,56,57,58,59,59,60,61,62,63,63,64,65,66,67,67,68,69,70,70,71,72,73,74,74,75,76,77,78,78,79,80,80,81,81,82,82,83,83,84,85,85,86,86,87,87,88,88,89,89,90,91,91,92,92,93,93,94,94,95,95,96,96,97,98,98,99,99,100,100,101,101,102,102,103,104,104,105,105,106,106,107,107,107,107,108,108,108,109,109,109,110,110,110,111,111,111,112,112,112,113,113,113,113,114,114,114,115,115,115,116,116,116,117,117,117,117,118,118,118,119,119,119,120,120,120,121,121,121,122,122 }, + /* 41 RPM */ { 6,6,6,6,6,7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8,8,9,9,9,9,9,9,9,9,10,10,10,10,10,10,10,10,11,11,11,11,11,11,11,11,12,12,12,12,12,13,13,14,14,15,16,16,17,17,18,18,19,20,20,21,21,22,23,23,24,24,25,25,26,27,27,28,28,29,30,30,31,31,32,32,33,34,34,35,35,36,37,37,38,38,39,39,40,41,41,42,43,44,45,45,46,47,48,49,49,50,51,52,53,54,54,55,56,57,58,59,59,60,61,62,63,64,64,65,66,67,68,68,69,70,71,72,73,73,74,75,76,77,78,78,79,80,81,82,83,83,84,84,85,85,86,87,87,88,88,89,89,90,91,91,92,92,93,93,94,95,95,96,96,97,97,98,99,99,100,100,101,101,102,103,103,104,104,105,106,106,107,107,108,108,109,110,110,111,111,112,112,112,113,113,113,114,114,114,115,115,115,116,116,116,117,117,117,118,118,118,119,119,119,120,120,120,121,121,121,122,122,122,123,123,123,123,124,124,124,125,125,125,126,126,126,127,127,127,128 }, + /* 42 RPM */ { 6,6,6,6,7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8,9,9,9,9,9,9,9,9,10,10,10,10,10,10,10,10,11,11,11,11,11,11,11,11,12,12,12,12,12,12,12,13,14,14,15,15,16,17,17,18,18,19,20,20,21,21,22,23,23,24,24,25,26,26,27,28,28,29,29,30,31,31,32,32,33,34,34,35,35,36,37,37,38,38,39,40,40,41,41,42,43,44,44,45,46,47,48,49,50,50,51,52,53,54,55,56,56,57,58,59,60,61,62,63,63,64,65,66,67,68,69,69,70,71,72,73,74,75,76,76,77,78,79,80,81,82,82,83,84,85,86,86,87,88,88,89,90,90,91,91,92,93,93,94,94,95,96,96,97,97,98,99,99,100,100,101,102,102,103,104,104,105,105,106,107,107,108,108,109,110,110,111,111,112,113,113,114,114,115,116,116,117,117,117,118,118,118,119,119,119,120,120,120,121,121,122,122,122,123,123,123,124,124,124,125,125,125,126,126,126,127,127,127,128,128,128,129,129,130,130,130,131,131,131,132,132,132,133,133,133,134 }, + /* 43 RPM */ { 6,6,6,7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,9,9,9,9,9,9,9,9,10,10,10,10,10,10,10,10,11,11,11,11,11,11,11,12,12,12,12,12,12,12,12,13,13,13,14,15,15,16,16,17,18,18,19,20,20,21,22,22,23,23,24,25,25,26,27,27,28,28,29,30,30,31,32,32,33,33,34,35,35,36,37,37,38,38,39,40,40,41,42,42,43,43,44,45,46,47,48,49,49,50,51,52,53,54,55,56,57,58,59,59,60,61,62,63,64,65,66,67,68,68,69,70,71,72,73,74,75,76,77,78,78,79,80,81,82,83,84,85,86,87,87,88,89,90,91,91,92,92,93,94,94,95,96,96,97,98,98,99,100,100,101,101,102,103,103,104,105,105,106,107,107,108,109,109,110,110,111,112,112,113,114,114,115,116,116,117,117,118,119,119,120,121,121,122,122,122,123,123,124,124,124,125,125,125,126,126,126,127,127,128,128,128,129,129,129,130,130,130,131,131,132,132,132,133,133,133,134,134,134,135,135,136,136,136,137,137,137,138,138,139,139,139,140 }, + /* 44 RPM */ { 6,6,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8,9,9,9,9,9,9,9,10,10,10,10,10,10,10,11,11,11,11,11,11,11,11,12,12,12,12,12,12,12,13,13,13,13,13,14,14,15,16,16,17,18,18,19,20,20,21,22,22,23,23,24,25,25,26,27,27,28,29,29,30,31,31,32,33,33,34,34,35,36,36,37,38,38,39,40,40,41,42,42,43,44,44,45,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,93,94,95,95,96,97,97,98,99,99,100,101,101,102,103,103,104,105,105,106,107,107,108,109,109,110,111,112,112,113,114,114,115,116,116,117,118,118,119,120,120,121,122,122,123,124,124,125,126,126,127,127,128,128,128,129,129,129,130,130,131,131,131,132,132,133,133,133,134,134,134,135,135,136,136,136,137,137,137,138,138,139,139,139,140,140,141,141,141,142,142,142,143,143,144,144,144,145,145,146 }, + /* 45 RPM */ { 6,7,7,7,7,7,7,7,8,8,8,8,8,8,8,9,9,9,9,9,9,9,10,10,10,10,10,10,10,11,11,11,11,11,11,11,11,12,12,12,12,12,12,12,13,13,13,13,13,13,13,14,15,15,16,17,17,18,19,19,20,21,21,22,23,23,24,25,25,26,27,28,28,29,30,30,31,32,32,33,34,34,35,36,36,37,38,38,39,40,40,41,42,42,43,44,44,45,46,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,97,98,99,100,100,101,102,102,103,104,104,105,106,107,107,108,109,109,110,111,112,112,113,114,114,115,116,117,117,118,119,119,120,121,121,122,123,124,124,125,126,126,127,128,129,129,130,131,131,132,132,133,133,133,134,134,135,135,135,136,136,137,137,137,138,138,139,139,139,140,140,141,141,141,142,142,143,143,143,144,144,145,145,145,146,146,147,147,147,148,148,149,149,149,150,150,151,151,151 }, + /* 46 RPM */ { 7,7,7,7,7,7,7,8,8,8,8,8,8,8,9,9,9,9,9,9,9,10,10,10,10,10,10,10,11,11,11,11,11,11,12,12,12,12,12,12,12,13,13,13,13,13,13,13,14,14,14,15,15,16,17,17,18,19,19,20,21,21,22,23,23,24,25,26,26,27,28,28,29,30,30,31,32,32,33,34,35,35,36,37,37,38,39,39,40,41,41,42,43,43,44,45,46,46,47,48,48,49,50,51,52,53,54,55,56,57,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,102,103,104,105,105,106,107,108,108,109,110,110,111,112,113,113,114,115,116,116,117,118,119,119,120,121,122,122,123,124,125,125,126,127,128,128,129,130,131,131,132,133,133,134,135,136,136,137,137,138,138,139,139,139,140,140,141,141,141,142,142,143,143,144,144,144,145,145,146,146,146,147,147,148,148,149,149,149,150,150,151,151,151,152,152,153,153,154,154,154,155,155,156,156,157,157,157 }, + /* 47 RPM */ { 7,7,7,7,7,7,8,8,8,8,8,8,8,9,9,9,9,9,9,10,10,10,10,10,10,10,11,11,11,11,11,11,11,12,12,12,12,12,12,13,13,13,13,13,13,13,14,14,14,14,14,15,16,16,17,18,18,19,20,21,21,22,23,23,24,25,26,26,27,28,28,29,30,31,31,32,33,33,34,35,36,36,37,38,38,39,40,40,41,42,43,43,44,45,45,46,47,48,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,100,101,102,103,103,104,105,106,107,107,108,109,110,110,111,112,113,114,114,115,116,117,117,118,119,120,121,121,122,123,124,124,125,126,127,128,128,129,130,131,131,132,133,134,134,135,136,137,138,138,139,140,141,141,142,142,143,143,144,144,145,145,145,146,146,147,147,148,148,148,149,149,150,150,151,151,151,152,152,153,153,154,154,155,155,155,156,156,157,157,158,158,158,159,159,160,160,161,161,162,162,162,163,163 }, + /* 48 RPM */ { 7,7,7,7,7,8,8,8,8,8,8,9,9,9,9,9,9,9,10,10,10,10,10,10,11,11,11,11,11,11,11,12,12,12,12,12,12,13,13,13,13,13,13,13,14,14,14,14,14,14,15,15,16,17,17,18,19,20,20,21,22,23,23,24,25,26,26,27,28,28,29,30,31,31,32,33,34,34,35,36,37,37,38,39,39,40,41,42,42,43,44,45,45,46,47,47,48,49,50,50,51,52,53,54,56,57,58,59,60,61,62,63,64,65,67,68,69,70,71,72,73,74,75,76,78,79,80,81,82,83,84,85,86,87,89,90,91,92,93,94,95,96,97,98,99,101,102,103,104,105,106,107,108,109,109,110,111,112,113,113,114,115,116,117,117,118,119,120,121,121,122,123,124,125,125,126,127,128,129,130,130,131,132,133,134,134,135,136,137,138,138,139,140,141,142,142,143,144,145,146,147,147,147,148,148,149,149,150,150,151,151,151,152,152,153,153,154,154,155,155,156,156,156,157,157,158,158,159,159,160,160,161,161,161,162,162,163,163,164,164,165,165,166,166,166,167,167,168,168,169,169 }, + /* 49 RPM */ { 7,7,7,7,8,8,8,8,8,8,9,9,9,9,9,9,10,10,10,10,10,10,10,11,11,11,11,11,11,12,12,12,12,12,12,13,13,13,13,13,13,13,14,14,14,14,14,14,15,15,15,16,16,17,18,19,19,20,21,22,22,23,24,25,25,26,27,28,28,29,30,31,31,32,33,34,34,35,36,37,37,38,39,40,41,41,42,43,44,44,45,46,47,47,48,49,50,50,51,52,53,54,55,56,57,58,59,61,62,63,64,65,66,67,68,70,71,72,73,74,75,76,78,79,80,81,82,83,84,86,87,88,89,90,91,92,94,95,96,97,98,99,100,101,103,104,105,106,107,108,109,110,111,112,113,114,115,115,116,117,118,119,120,120,121,122,123,124,125,125,126,127,128,129,130,130,131,132,133,134,135,136,136,137,138,139,140,141,141,142,143,144,145,146,146,147,148,149,150,151,152,152,152,153,153,154,154,155,155,156,156,157,157,158,158,159,159,160,160,160,161,161,162,162,163,163,164,164,165,165,166,166,167,167,168,168,168,169,169,170,170,171,171,172,172,173,173,174,174,175,175 }, + /* 50 RPM */ { 7,7,7,8,8,8,8,8,8,9,9,9,9,9,9,10,10,10,10,10,10,11,11,11,11,11,11,12,12,12,12,12,12,13,13,13,13,13,13,13,14,14,14,14,14,14,15,15,15,15,15,16,17,18,18,19,20,21,21,22,23,24,25,25,26,27,28,28,29,30,31,32,32,33,34,35,35,36,37,38,38,39,40,41,42,42,43,44,45,45,46,47,48,49,49,50,51,52,52,53,54,55,56,57,59,60,61,62,63,65,66,67,68,69,70,72,73,74,75,76,78,79,80,81,82,83,85,86,87,88,89,90,92,93,94,95,96,98,99,100,101,102,103,105,106,107,108,109,110,112,113,114,115,115,116,117,118,119,120,121,122,122,123,124,125,126,127,128,129,129,130,131,132,133,134,135,136,136,137,138,139,140,141,142,143,143,144,145,146,147,148,149,150,150,151,152,153,154,155,156,157,157,158,158,159,159,160,160,160,161,161,162,162,163,163,164,164,165,165,166,166,167,167,168,168,169,169,170,170,171,171,172,172,173,173,174,174,175,175,176,176,177,177,178,178,179,179,180,180,181,181 }, + /* 51 RPM */ { 7,7,7,8,8,8,8,8,8,9,9,9,9,9,9,10,10,10,10,10,10,11,11,11,11,11,11,12,12,12,12,12,12,13,13,13,13,13,13,14,14,14,14,14,14,15,15,15,15,15,15,16,17,18,19,19,20,21,22,23,23,24,25,26,27,27,28,29,30,31,31,32,33,34,35,35,36,37,38,39,39,40,41,42,43,43,44,45,46,47,47,48,49,50,51,51,52,53,54,55,55,57,58,59,60,61,63,64,65,66,68,69,70,71,72,74,75,76,77,79,80,81,82,83,85,86,87,88,90,91,92,93,94,96,97,98,99,101,102,103,104,105,107,108,109,110,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,126,127,128,129,130,131,132,133,134,135,136,137,137,138,139,140,141,142,143,144,145,146,147,147,148,149,150,151,152,153,154,155,156,157,158,158,159,160,161,162,163,163,164,164,165,165,166,166,167,167,168,168,169,169,170,170,171,171,172,172,173,174,174,175,175,176,176,177,177,178,178,179,179,180,180,181,181,182,182,183,183,184,184,185,185,186,187,187,188,188 }, + /* 52 RPM */ { 7,7,8,8,8,8,8,8,9,9,9,9,9,9,10,10,10,10,10,10,11,11,11,11,11,11,12,12,12,12,12,12,13,13,13,13,13,13,14,14,14,14,14,14,15,15,15,15,15,15,16,16,17,18,19,20,21,21,22,23,24,25,25,26,27,28,29,30,30,31,32,33,34,34,35,36,37,38,39,39,40,41,42,43,44,44,45,46,47,48,48,49,50,51,52,53,53,54,55,56,57,58,59,60,62,63,64,66,67,68,69,71,72,73,74,76,77,78,79,81,82,83,85,86,87,88,90,91,92,93,95,96,97,98,100,101,102,104,105,106,107,109,110,111,112,114,115,116,118,119,120,121,122,123,124,125,126,127,128,129,130,131,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,168,169,169,170,170,171,171,172,173,173,174,174,175,175,176,176,177,178,178,179,179,180,180,181,181,182,182,183,184,184,185,185,186,186,187,187,188,189,189,190,190,191,191,192,192,193,193,194,195,195 }, + /* 53 RPM */ { 7,7,8,8,8,8,8,8,9,9,9,9,9,10,10,10,10,10,10,11,11,11,11,11,11,12,12,12,12,12,12,13,13,13,13,13,13,14,14,14,14,14,14,15,15,15,15,15,15,16,16,17,17,18,19,20,21,22,22,23,24,25,26,27,28,28,29,30,31,32,33,33,34,35,36,37,38,39,39,40,41,42,43,44,44,45,46,47,48,49,50,50,51,52,53,54,55,55,56,57,58,59,61,62,63,65,66,67,68,70,71,72,74,75,76,78,79,80,82,83,84,86,87,88,90,91,92,93,95,96,97,99,100,101,103,104,105,107,108,109,111,112,113,114,116,117,118,120,121,122,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,174,175,175,176,177,177,178,178,179,180,180,181,181,182,182,183,184,184,185,185,186,187,187,188,188,189,189,190,191,191,192,192,193,193,194,195,195,196,196,197,198,198,199,199,200,200,201,202,202 }, + /* 54 RPM */ { 7,8,8,8,8,8,8,9,9,9,9,9,9,10,10,10,10,10,10,11,11,11,11,11,11,12,12,12,12,12,12,13,13,13,13,13,14,14,14,14,14,14,15,15,15,15,15,15,16,16,16,17,18,18,19,20,21,22,23,24,25,25,26,27,28,29,30,31,32,32,33,34,35,36,37,38,38,39,40,41,42,43,44,45,45,46,47,48,49,50,51,52,52,53,54,55,56,57,58,58,59,61,62,63,65,66,67,69,70,72,73,74,76,77,78,80,81,82,84,85,87,88,89,91,92,93,95,96,97,99,100,101,103,104,106,107,108,110,111,112,114,115,116,118,119,120,122,123,125,126,127,128,129,130,131,132,133,134,135,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,173,174,175,176,177,178,179,179,180,181,181,182,182,183,184,184,185,185,186,187,187,188,188,189,190,190,191,192,192,193,193,194,195,195,196,196,197,198,198,199,199,200,201,201,202,203,203,204,204,205,206,206,207,207,208,209,209 }, + /* 55 RPM */ { 7,8,8,8,8,8,8,9,9,9,9,9,10,10,10,10,10,10,11,11,11,11,11,11,12,12,12,12,12,12,13,13,13,13,13,13,14,14,14,14,14,14,15,15,15,15,15,16,16,16,16,17,18,19,20,21,21,22,23,24,25,26,27,28,29,29,30,31,32,33,34,35,36,37,37,38,39,40,41,42,43,44,45,46,46,47,48,49,50,51,52,53,54,54,55,56,57,58,59,60,61,62,63,65,66,68,69,71,72,73,75,76,78,79,80,82,83,85,86,87,89,90,92,93,94,96,97,99,100,101,103,104,106,107,108,110,111,113,114,115,117,118,120,121,122,124,125,127,128,129,131,132,133,134,135,136,137,138,139,140,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,174,175,176,177,178,179,180,181,182,183,184,185,185,186,187,187,188,189,189,190,191,191,192,193,193,194,194,195,196,196,197,198,198,199,200,200,201,202,202,203,203,204,205,205,206,207,207,208,209,209,210,211,211,212,212,213,214,214,215,216,216 }, + /* 56 RPM */ { 8,8,8,8,8,8,9,9,9,9,9,9,10,10,10,10,10,10,11,11,11,11,11,12,12,12,12,12,12,13,13,13,13,13,13,14,14,14,14,14,14,15,15,15,15,15,15,16,16,16,16,17,18,19,20,21,22,23,24,24,25,26,27,28,29,30,31,32,33,34,35,35,36,37,38,39,40,41,42,43,44,45,46,46,47,48,49,50,51,52,53,54,55,56,57,57,58,59,60,61,62,63,65,66,68,69,71,72,74,75,77,78,79,81,82,84,85,87,88,90,91,92,94,95,97,98,100,101,103,104,106,107,108,110,111,113,114,116,117,119,120,121,123,124,126,127,129,130,132,133,134,136,137,138,139,140,141,142,143,144,146,147,148,149,150,151,152,153,154,155,157,158,159,160,161,162,163,164,165,167,168,169,170,171,172,173,174,175,176,178,179,180,181,182,183,184,185,186,188,189,190,190,191,192,192,193,194,194,195,196,196,197,198,198,199,200,200,201,202,203,203,204,205,205,206,207,207,208,209,209,210,211,211,212,213,213,214,215,215,216,217,217,218,219,219,220,221,221,222,223,223 }, + /* 57 RPM */ { 8,8,8,8,8,8,9,9,9,9,9,10,10,10,10,10,10,11,11,11,11,11,11,12,12,12,12,12,13,13,13,13,13,13,14,14,14,14,14,14,15,15,15,15,15,15,16,16,16,16,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,61,62,63,65,66,68,69,71,72,74,75,77,78,80,81,83,84,86,87,89,90,92,93,95,96,98,99,101,102,104,105,107,108,110,111,113,114,116,117,119,120,122,123,125,126,128,129,131,132,134,135,137,138,139,140,142,143,144,145,146,147,148,150,151,152,153,154,155,156,158,159,160,161,162,163,164,166,167,168,169,170,171,172,174,175,176,177,178,179,180,182,183,184,185,186,187,188,190,191,192,193,194,195,196,197,197,198,199,199,200,201,202,202,203,204,204,205,206,207,207,208,209,209,210,211,211,212,213,214,214,215,216,216,217,218,218,219,220,221,221,222,223,223,224,225,226,226,227,228,228,229,230,230 }, + /* 58 RPM */ { 8,8,8,8,8,9,9,9,9,9,9,10,10,10,10,10,11,11,11,11,11,11,12,12,12,12,12,12,13,13,13,13,13,13,14,14,14,14,14,15,15,15,15,15,15,16,16,16,16,16,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,68,69,71,72,74,75,77,79,80,82,83,85,86,88,89,91,92,94,96,97,99,100,102,103,105,106,108,109,111,112,114,116,117,119,120,122,123,125,126,128,129,131,132,134,136,137,139,140,142,143,144,145,146,148,149,150,151,152,154,155,156,157,158,159,161,162,163,164,165,167,168,169,170,171,172,174,175,176,177,178,180,181,182,183,184,185,187,188,189,190,191,193,194,195,196,197,198,200,201,202,202,203,204,204,205,206,207,207,208,209,210,210,211,212,213,213,214,215,215,216,217,218,218,219,220,221,221,222,223,224,224,225,226,226,227,228,229,229,230,231,232,232,233,234,235,235,236,237,237 }, + /* 59 RPM */ { 8,8,8,8,8,9,9,9,9,9,10,10,10,10,10,10,11,11,11,11,11,12,12,12,12,12,12,13,13,13,13,13,13,14,14,14,14,14,15,15,15,15,15,15,16,16,16,16,16,16,17,18,19,20,21,22,23,24,25,26,27,28,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,68,69,71,72,74,76,77,79,80,82,83,85,87,88,90,91,93,95,96,98,99,101,103,104,106,107,109,110,112,114,115,117,118,120,122,123,125,126,128,129,131,133,134,136,137,139,141,142,144,145,147,148,149,150,151,153,154,155,156,158,159,160,161,162,164,165,166,167,168,170,171,172,173,175,176,177,178,179,181,182,183,184,186,187,188,189,190,192,193,194,195,197,198,199,200,201,203,204,205,206,207,208,209,209,210,211,212,212,213,214,215,215,216,217,218,219,219,220,221,222,222,223,224,225,225,226,227,228,228,229,230,231,232,232,233,234,235,235,236,237,238,238,239,240,241,241,242,243,244,245 }, + /* 60 RPM */ { 8,8,8,8,9,9,9,9,9,9,10,10,10,10,10,11,11,11,11,11,11,12,12,12,12,12,12,13,13,13,13,13,14,14,14,14,14,14,15,15,15,15,15,16,16,16,16,16,16,17,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,69,71,72,74,76,77,79,80,82,84,85,87,89,90,92,93,95,97,98,100,102,103,105,107,108,110,111,113,115,116,118,120,121,123,124,126,128,129,131,133,134,136,138,139,141,142,144,146,147,149,150,151,153,154,155,156,158,159,160,162,163,164,165,167,168,169,170,172,173,174,175,177,178,179,180,182,183,184,185,187,188,189,190,192,193,194,195,197,198,199,201,202,203,204,206,207,208,209,211,212,213,213,214,215,216,217,217,218,219,220,221,221,222,223,224,225,225,226,227,228,229,229,230,231,232,233,233,234,235,236,236,237,238,239,240,240,241,242,243,244,244,245,246,247,248,248,249,250,251,252 }, + /* 61 RPM */ { 8,8,8,8,9,9,9,9,9,9,10,10,10,10,10,11,11,11,11,11,11,12,12,12,12,12,13,13,13,13,13,14,14,14,14,14,14,15,15,15,15,15,16,16,16,16,16,17,17,17,17,18,19,20,21,22,23,24,25,26,27,28,29,30,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,66,67,68,69,70,72,74,75,77,79,80,82,84,85,87,89,90,92,94,95,97,99,100,102,104,105,107,109,110,112,114,115,117,119,120,122,124,125,127,129,130,132,134,135,137,139,140,142,144,145,147,149,150,152,153,155,156,157,159,160,161,163,164,165,167,168,169,171,172,173,175,176,177,179,180,181,182,184,185,186,188,189,190,192,193,194,196,197,198,200,201,202,204,205,206,208,209,210,212,213,214,215,217,218,219,220,221,221,222,223,224,225,226,226,227,228,229,230,230,231,232,233,234,235,235,236,237,238,239,239,240,241,242,243,244,244,245,246,247,248,249,249,250,251,252,253,253,254,255,256,257,258,258,259 }, + /* 62 RPM */ { 8,8,8,8,8,9,9,9,9,9,10,10,10,10,10,11,11,11,11,11,12,12,12,12,12,13,13,13,13,13,13,14,14,14,14,14,15,15,15,15,15,16,16,16,16,16,17,17,17,17,17,18,19,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,64,65,66,67,68,69,70,72,73,75,77,78,80,82,83,85,87,89,90,92,94,95,97,99,101,102,104,106,107,109,111,113,114,116,118,119,121,123,124,126,128,130,131,133,135,136,138,140,142,143,145,147,148,150,152,154,155,157,158,159,161,162,164,165,166,168,169,170,172,173,175,176,177,179,180,182,183,184,186,187,188,190,191,193,194,195,197,198,200,201,202,204,205,206,208,209,211,212,213,215,216,218,219,220,222,223,224,225,226,227,228,229,230,230,231,232,233,234,235,235,236,237,238,239,240,241,241,242,243,244,245,246,246,247,248,249,250,251,252,252,253,254,255,256,257,257,258,259,260,261,262,263,263,264,265,266,267 }, + /* 63 RPM */ { 8,8,8,8,8,9,9,9,9,9,10,10,10,10,10,11,11,11,11,11,12,12,12,12,12,13,13,13,13,13,14,14,14,14,14,15,15,15,15,15,16,16,16,16,16,17,17,17,17,17,18,19,20,21,22,23,24,25,26,27,28,29,30,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,64,65,66,67,68,69,70,71,73,75,76,78,80,82,83,85,87,88,90,92,94,95,97,99,101,102,104,106,108,109,111,113,115,116,118,120,122,123,125,127,129,130,132,134,136,137,139,141,143,144,146,148,150,151,153,155,157,158,160,161,163,164,166,167,169,170,171,173,174,176,177,179,180,182,183,184,186,187,189,190,192,193,195,196,197,199,200,202,203,205,206,208,209,210,212,213,215,216,218,219,221,222,223,225,226,228,229,231,232,232,233,234,235,236,237,238,239,239,240,241,242,243,244,245,246,246,247,248,249,250,251,252,253,253,254,255,256,257,258,259,260,260,261,262,263,264,265,266,267,267,268,269,270,271,272,273,273,274 }, + /* 64 RPM */ { 8,8,8,8,8,9,9,9,9,9,10,10,10,10,10,11,11,11,11,11,12,12,12,12,13,13,13,13,13,14,14,14,14,14,15,15,15,15,15,16,16,16,16,16,17,17,17,17,18,18,18,19,20,21,22,23,24,26,27,28,29,30,31,32,33,34,35,36,37,39,40,41,42,43,44,45,46,47,48,49,51,52,53,54,55,56,57,58,59,60,61,62,64,65,66,67,68,69,70,71,72,74,76,78,79,81,83,85,87,88,90,92,94,95,97,99,101,103,104,106,108,110,112,113,115,117,119,120,122,124,126,128,129,131,133,135,137,138,140,142,144,146,147,149,151,153,154,156,158,160,162,163,165,166,168,169,171,172,174,175,177,178,180,181,183,184,186,187,189,190,192,193,195,196,198,199,201,202,204,205,207,208,210,211,213,214,216,217,219,220,222,223,225,226,228,229,231,232,234,235,237,238,239,240,241,241,242,243,244,245,246,247,248,249,250,250,251,252,253,254,255,256,257,258,259,259,260,261,262,263,264,265,266,267,268,268,269,270,271,272,273,274,275,276,277,277,278,279,280,281,282 }, + /* 65 RPM */ { 7,8,8,8,8,9,9,9,9,9,10,10,10,10,10,11,11,11,11,12,12,12,12,12,13,13,13,13,13,14,14,14,14,15,15,15,15,15,16,16,16,16,17,17,17,17,17,18,18,18,18,19,20,22,23,24,25,26,27,28,29,30,31,33,34,35,36,37,38,39,40,41,43,44,45,46,47,48,49,50,51,52,54,55,56,57,58,59,60,61,62,63,65,66,67,68,69,70,71,72,73,75,77,79,81,83,84,86,88,90,92,94,95,97,99,101,103,104,106,108,110,112,114,115,117,119,121,123,125,126,128,130,132,134,136,137,139,141,143,145,146,148,150,152,154,156,157,159,161,163,165,166,168,169,171,173,174,176,177,179,180,182,184,185,187,188,190,191,193,195,196,198,199,201,202,204,206,207,209,210,212,213,215,217,218,220,221,223,224,226,228,229,231,232,234,235,237,239,240,242,243,244,245,246,247,248,249,250,251,252,253,253,254,255,256,257,258,259,260,261,262,263,264,265,265,266,267,268,269,270,271,272,273,274,275,276,277,278,278,279,280,281,282,283,284,285,286,287,288,289,290 }, + /* 66 RPM */ { 7,8,8,8,8,8,9,9,9,9,10,10,10,10,10,11,11,11,11,12,12,12,12,13,13,13,13,13,14,14,14,14,15,15,15,15,15,16,16,16,16,17,17,17,17,17,18,18,18,18,19,20,21,22,23,24,25,26,28,29,30,31,32,33,34,35,36,38,39,40,41,42,43,44,45,47,48,49,50,51,52,53,54,56,57,58,59,60,61,62,63,65,66,67,68,69,70,71,72,74,75,76,78,80,82,84,86,88,90,91,93,95,97,99,101,103,104,106,108,110,112,114,116,118,119,121,123,125,127,129,131,132,134,136,138,140,142,144,146,147,149,151,153,155,157,159,160,162,164,166,168,170,171,173,174,176,178,179,181,183,184,186,188,189,191,192,194,196,197,199,201,202,204,205,207,209,210,212,214,215,217,219,220,222,223,225,227,228,230,232,233,235,237,238,240,241,243,245,246,248,250,251,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,290,291,292,293,294,295,296,297 }, + /* 67 RPM */ { 7,8,8,8,8,8,9,9,9,9,10,10,10,10,11,11,11,11,11,12,12,12,12,13,13,13,13,14,14,14,14,14,15,15,15,15,16,16,16,16,17,17,17,17,17,18,18,18,18,19,19,20,21,22,23,25,26,27,28,29,30,31,33,34,35,36,37,38,39,40,42,43,44,45,46,47,48,50,51,52,53,54,55,56,58,59,60,61,62,63,64,66,67,68,69,70,71,72,74,75,76,78,80,82,83,85,87,89,91,93,95,97,99,101,102,104,106,108,110,112,114,116,118,120,122,123,125,127,129,131,133,135,137,139,141,143,144,146,148,150,152,154,156,158,160,162,163,165,167,169,171,173,174,176,178,180,181,183,185,186,188,190,191,193,195,197,198,200,202,203,205,207,208,210,212,213,215,217,219,220,222,224,225,227,229,230,232,234,236,237,239,241,242,244,246,247,249,251,252,254,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305 }, + /* 68 RPM */ { 7,7,8,8,8,8,9,9,9,9,10,10,10,10,11,11,11,11,11,12,12,12,12,13,13,13,13,14,14,14,14,15,15,15,15,16,16,16,16,17,17,17,17,17,18,18,18,18,19,19,19,20,21,23,24,25,26,27,28,30,31,32,33,34,35,36,38,39,40,41,42,43,45,46,47,48,49,50,52,53,54,55,56,57,59,60,61,62,63,64,65,67,68,69,70,71,72,74,75,76,77,79,81,83,85,87,89,91,93,95,96,98,100,102,104,106,108,110,112,114,116,118,120,122,124,126,128,130,131,133,135,137,139,141,143,145,147,149,151,153,155,157,159,161,163,165,166,168,170,172,174,176,178,180,181,183,185,187,188,190,192,194,195,197,199,201,202,204,206,208,209,211,213,215,216,218,220,222,223,225,227,229,231,232,234,236,238,239,241,243,245,246,248,250,252,253,255,257,259,260,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312 }, + /* 69 RPM */ { 7,7,8,8,8,8,9,9,9,9,10,10,10,10,11,11,11,11,12,12,12,12,13,13,13,13,14,14,14,14,14,15,15,15,15,16,16,16,16,17,17,17,17,18,18,18,18,19,19,19,19,21,22,23,24,25,26,28,29,30,31,32,34,35,36,37,38,39,41,42,43,44,45,46,48,49,50,51,52,54,55,56,57,58,59,61,62,63,64,65,66,68,69,70,71,72,74,75,76,77,78,80,82,84,86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,118,120,122,124,126,128,130,132,134,136,138,140,142,144,146,148,150,152,154,156,158,160,162,164,166,168,169,171,173,175,177,179,181,183,185,187,188,190,192,194,196,197,199,201,203,205,207,208,210,212,214,216,217,219,221,223,225,227,228,230,232,234,236,238,239,241,243,245,247,248,250,252,254,256,258,259,261,263,265,267,268,269,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,307,308,309,310,311,312,313,314,315,316,317,318,319,320 }, + /* 70 RPM */ { 7,7,8,8,8,8,9,9,9,9,10,10,10,10,11,11,11,11,12,12,12,12,13,13,13,13,14,14,14,14,15,15,15,15,16,16,16,16,17,17,17,17,18,18,18,18,19,19,19,19,20,21,22,23,24,26,27,28,29,30,32,33,34,35,36,38,39,40,41,42,44,45,46,47,48,50,51,52,53,54,56,57,58,59,60,62,63,64,65,66,67,69,70,71,72,73,75,76,77,78,79,81,83,85,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,118,120,122,124,126,128,130,132,134,136,138,140,142,144,146,148,150,152,154,156,158,160,162,164,166,168,170,172,175,177,179,181,182,184,186,188,190,192,194,196,198,199,201,203,205,207,209,211,213,214,216,218,220,222,224,226,228,230,231,233,235,237,239,241,243,245,247,248,250,252,254,256,258,260,262,263,265,267,269,271,273,275,276,277,278,279,280,281,282,283,284,285,286,287,288,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327 }, + /* 71 RPM */ { 7,8,8,8,8,9,9,9,10,10,10,10,11,11,11,11,12,12,12,12,13,13,13,13,14,14,14,14,15,15,15,16,16,16,16,17,17,17,17,18,18,18,18,19,19,19,19,20,20,20,20,22,23,24,25,27,28,29,30,32,33,34,35,36,38,39,40,41,43,44,45,46,47,49,50,51,52,54,55,56,57,58,60,61,62,63,65,66,67,68,70,71,72,73,74,76,77,78,79,81,82,84,86,88,90,92,94,96,98,100,103,105,107,109,111,113,115,117,119,121,123,125,127,130,132,134,136,138,140,142,144,146,148,150,152,154,157,159,161,163,165,167,169,171,173,175,177,179,182,184,186,188,189,191,193,195,197,199,201,203,205,207,209,211,213,214,216,218,220,222,224,226,228,230,232,234,236,238,239,241,243,245,247,249,251,253,255,257,259,261,262,264,266,268,270,272,274,276,278,280,282,283,284,285,286,287,288,289,290,292,293,294,295,296,297,298,299,300,301,303,304,305,306,307,308,309,310,311,312,314,315,316,317,318,319,320,321,322,324,325,326,327,328,329,330,331,332,333,335,336,337 }, + /* 72 RPM */ { 8,8,8,9,9,9,9,10,10,10,11,11,11,11,12,12,12,12,13,13,13,13,14,14,14,15,15,15,15,16,16,16,16,17,17,17,18,18,18,18,19,19,19,19,20,20,20,20,21,21,21,23,24,25,26,28,29,30,31,33,34,35,36,38,39,40,41,43,44,45,46,48,49,50,51,53,54,55,56,58,59,60,61,63,64,65,67,68,69,70,72,73,74,75,77,78,79,80,82,83,84,86,88,90,93,95,97,99,101,103,105,108,110,112,114,116,118,120,122,125,127,129,131,133,135,137,140,142,144,146,148,150,152,154,157,159,161,163,165,167,169,172,174,176,178,180,182,184,186,189,191,193,195,197,199,201,202,204,206,208,210,212,214,216,218,220,222,224,226,228,230,232,234,236,238,240,242,244,246,247,249,251,253,255,257,259,261,263,265,267,269,271,273,275,277,279,281,283,285,287,289,290,291,292,293,294,295,297,298,299,300,301,302,303,305,306,307,308,309,310,312,313,314,315,316,317,318,320,321,322,323,324,325,326,328,329,330,331,332,333,334,336,337,338,339,340,341,343,344,345,346 }, + /* 73 RPM */ { 8,9,9,9,9,10,10,10,10,11,11,11,12,12,12,12,13,13,13,14,14,14,14,15,15,15,15,16,16,16,17,17,17,17,18,18,18,18,19,19,19,20,20,20,20,21,21,21,21,22,22,23,25,26,27,28,30,31,32,34,35,36,37,39,40,41,43,44,45,46,48,49,50,52,53,54,56,57,58,59,61,62,63,65,66,67,68,70,71,72,74,75,76,77,79,80,81,83,84,85,86,89,91,93,95,97,100,102,104,106,108,110,113,115,117,119,121,124,126,128,130,132,135,137,139,141,143,145,148,150,152,154,156,159,161,163,165,167,170,172,174,176,178,180,183,185,187,189,191,194,196,198,200,202,204,206,208,210,212,214,216,218,220,222,224,226,228,230,232,234,236,238,240,242,244,246,248,250,252,254,256,258,260,262,264,266,268,270,272,274,276,278,280,282,284,286,288,290,292,294,295,297,298,299,300,301,303,304,305,306,307,309,310,311,312,313,315,316,317,318,319,321,322,323,324,325,327,328,329,330,331,333,334,335,336,337,338,340,341,342,343,344,346,347,348,349,350,352,353,354,355 }, + /* 74 RPM */ { 9,9,9,10,10,10,10,11,11,11,12,12,12,12,13,13,13,13,14,14,14,15,15,15,15,16,16,16,17,17,17,17,18,18,18,19,19,19,19,20,20,20,21,21,21,21,22,22,22,23,23,24,25,27,28,29,31,32,33,35,36,37,39,40,41,43,44,45,47,48,49,50,52,53,54,56,57,58,60,61,62,64,65,66,68,69,70,72,73,74,76,77,78,80,81,82,83,85,86,87,89,91,93,95,98,100,102,104,107,109,111,113,116,118,120,122,125,127,129,131,134,136,138,140,143,145,147,149,152,154,156,158,161,163,165,167,169,172,174,176,178,181,183,185,187,190,192,194,196,199,201,203,205,207,209,211,213,215,217,219,221,223,225,227,229,231,233,235,237,239,241,244,246,248,250,252,254,256,258,260,262,264,266,268,270,272,274,276,278,280,282,284,286,288,290,292,294,296,298,300,302,304,305,306,307,309,310,311,312,314,315,316,317,319,320,321,322,324,325,326,327,328,330,331,332,333,335,336,337,338,340,341,342,343,345,346,347,348,350,351,352,353,355,356,357,358,359,361,362,363,364 }, + /* 75 RPM */ { 9,9,10,10,10,11,11,11,11,12,12,12,13,13,13,13,14,14,14,15,15,15,15,16,16,16,17,17,17,18,18,18,18,19,19,19,20,20,20,20,21,21,21,22,22,22,22,23,23,23,24,25,26,28,29,30,32,33,34,36,37,38,40,41,42,44,45,47,48,49,51,52,53,55,56,57,59,60,61,63,64,65,67,68,69,71,72,74,75,76,78,79,80,82,83,84,86,87,88,90,91,93,96,98,100,103,105,107,109,112,114,116,119,121,123,126,128,130,132,135,137,139,142,144,146,149,151,153,155,158,160,162,165,167,169,171,174,176,178,181,183,185,188,190,192,194,197,199,201,204,206,208,210,212,214,216,218,220,222,225,227,229,231,233,235,237,239,241,243,245,247,249,251,253,256,258,260,262,264,266,268,270,272,274,276,278,280,282,285,287,289,291,293,295,297,299,301,303,305,307,309,311,312,313,314,316,317,318,320,321,322,323,325,326,327,329,330,331,332,334,335,336,338,339,340,342,343,344,345,347,348,349,351,352,353,354,356,357,358,360,361,362,363,365,366,367,369,370,371,372,374 }, + /* 76 RPM */ { 10,10,10,10,11,11,11,12,12,12,12,13,13,13,14,14,14,15,15,15,15,16,16,16,17,17,17,18,18,18,18,19,19,19,20,20,20,20,21,21,21,22,22,22,23,23,23,23,24,24,24,26,27,28,30,31,33,34,35,37,38,40,41,42,44,45,46,48,49,51,52,53,55,56,57,59,60,62,63,64,66,67,69,70,71,73,74,75,77,78,80,81,82,84,85,87,88,89,91,92,93,96,98,100,103,105,108,110,112,115,117,119,122,124,126,129,131,133,136,138,140,143,145,148,150,152,155,157,159,162,164,166,169,171,173,176,178,180,183,185,187,190,192,195,197,199,202,204,206,209,211,213,215,217,219,222,224,226,228,230,232,234,236,238,240,243,245,247,249,251,253,255,257,259,262,264,266,268,270,272,274,276,278,280,283,285,287,289,291,293,295,297,299,302,304,306,308,310,312,314,316,318,319,320,322,323,324,326,327,328,330,331,332,334,335,336,338,339,340,342,343,344,346,347,348,350,351,352,354,355,356,358,359,360,362,363,364,366,367,368,370,371,372,374,375,376,378,379,380,382,383 }, + /* 77 RPM */ { 10,10,11,11,11,11,12,12,12,13,13,13,14,14,14,14,15,15,15,16,16,16,17,17,17,18,18,18,18,19,19,19,20,20,20,21,21,21,21,22,22,22,23,23,23,24,24,24,25,25,25,27,28,29,31,32,34,35,36,38,39,41,42,43,45,46,48,49,51,52,53,55,56,58,59,60,62,63,65,66,67,69,70,72,73,75,76,77,79,80,82,83,84,86,87,89,90,92,93,94,96,98,101,103,105,108,110,113,115,117,120,122,125,127,129,132,134,137,139,141,144,146,149,151,153,156,158,161,163,166,168,170,173,175,178,180,182,185,187,190,192,194,197,199,202,204,206,209,211,214,216,218,220,222,225,227,229,231,233,235,237,240,242,244,246,248,250,252,255,257,259,261,263,265,267,270,272,274,276,278,280,282,285,287,289,291,293,295,297,300,302,304,306,308,310,312,315,317,319,321,323,325,326,327,329,330,331,333,334,336,337,338,340,341,342,344,345,347,348,349,351,352,354,355,356,358,359,360,362,363,365,366,367,369,370,371,373,374,376,377,378,380,381,383,384,385,387,388,389,391,392 }, + /* 78 RPM */ { 10,11,11,11,12,12,12,13,13,13,13,14,14,14,15,15,15,16,16,16,17,17,17,18,18,18,18,19,19,19,20,20,20,21,21,21,22,22,22,22,23,23,23,24,24,24,25,25,25,26,26,27,29,30,32,33,35,36,37,39,40,42,43,45,46,48,49,50,52,53,55,56,58,59,61,62,63,65,66,68,69,71,72,74,75,76,78,79,81,82,84,85,87,88,89,91,92,94,95,97,98,101,103,105,108,110,113,115,118,120,123,125,128,130,133,135,137,140,142,145,147,150,152,155,157,160,162,165,167,169,172,174,177,179,182,184,187,189,192,194,197,199,201,204,206,209,211,214,216,219,221,223,225,228,230,232,234,236,239,241,243,245,247,249,252,254,256,258,260,263,265,267,269,271,273,276,278,280,282,284,286,289,291,293,295,297,300,302,304,306,308,310,313,315,317,319,321,324,326,328,330,332,333,334,336,337,339,340,341,343,344,346,347,349,350,351,353,354,356,357,359,360,361,363,364,366,367,369,370,371,373,374,376,377,379,380,381,383,384,386,387,389,390,391,393,394,396,397,399,400,401 }, + /* 79 RPM */ { 11,11,11,12,12,12,13,13,13,14,14,14,15,15,15,16,16,16,16,17,17,17,18,18,18,19,19,19,20,20,20,21,21,21,22,22,22,23,23,23,23,24,24,24,25,25,25,26,26,26,27,28,30,31,33,34,35,37,38,40,41,43,44,46,47,49,50,52,53,55,56,58,59,61,62,64,65,66,68,69,71,72,74,75,77,78,80,81,83,84,86,87,89,90,92,93,95,96,97,99,100,103,105,108,110,113,115,118,121,123,126,128,131,133,136,138,141,143,146,148,151,153,156,158,161,163,166,168,171,173,176,178,181,183,186,188,191,193,196,199,201,204,206,209,211,214,216,219,221,224,226,228,231,233,235,237,239,242,244,246,248,251,253,255,257,259,262,264,266,268,271,273,275,277,279,282,284,286,288,290,293,295,297,299,302,304,306,308,310,313,315,317,319,321,324,326,328,330,333,335,337,338,340,341,343,344,346,347,349,350,352,353,355,356,358,359,361,362,364,365,366,368,369,371,372,374,375,377,378,380,381,383,384,386,387,389,390,391,393,394,396,397,399,400,402,403,405,406,408,409,411 }, + /* 80 RPM */ { 11,12,12,12,13,13,13,13,14,14,14,15,15,15,16,16,16,17,17,17,18,18,18,19,19,19,20,20,20,21,21,21,22,22,22,23,23,23,24,24,24,25,25,25,25,26,26,26,27,27,27,29,30,32,33,35,36,38,39,41,42,44,45,47,49,50,52,53,55,56,58,59,61,62,64,65,67,68,70,71,73,74,76,77,79,80,82,83,85,86,88,89,91,92,94,95,97,98,100,101,103,105,108,110,113,116,118,121,123,126,128,131,134,136,139,141,144,146,149,152,154,157,159,162,164,167,170,172,175,177,180,182,185,188,190,193,195,198,200,203,206,208,211,213,216,218,221,224,226,229,231,234,236,238,240,243,245,247,249,252,254,256,258,261,263,265,267,270,272,274,276,279,281,283,285,288,290,292,294,297,299,301,303,306,308,310,312,315,317,319,321,324,326,328,330,333,335,337,339,342,344,345,347,348,350,352,353,355,356,358,359,361,362,364,365,367,368,370,371,373,374,376,377,379,380,382,383,385,386,388,390,391,393,394,396,397,399,400,402,403,405,406,408,409,411,412,414,415,417,418,420 }, + /* 81 RPM */ { 11,12,12,12,13,13,13,14,14,14,15,15,15,16,16,16,17,17,17,18,18,18,19,19,19,20,20,20,21,21,21,22,22,22,23,23,23,24,24,24,25,25,25,26,26,26,27,27,27,28,28,30,31,33,34,36,37,39,40,42,44,45,47,48,50,51,53,54,56,57,59,61,62,64,65,67,68,70,71,73,74,76,78,79,81,82,84,85,87,88,90,91,93,95,96,98,99,101,102,104,105,108,111,113,116,118,121,124,126,129,132,134,137,139,142,145,147,150,152,155,158,160,163,166,168,171,173,176,179,181,184,187,189,192,194,197,200,202,205,208,210,213,215,218,221,223,226,229,231,234,236,239,241,243,246,248,250,253,255,257,260,262,264,267,269,271,274,276,278,281,283,285,288,290,292,295,297,299,302,304,306,309,311,313,316,318,320,323,325,327,330,332,334,337,339,341,344,346,348,351,353,354,356,357,359,361,362,364,365,367,368,370,371,373,374,376,377,379,380,382,383,385,386,388,389,391,393,394,396,397,399,400,402,403,405,406,408,409,411,412,414,415,417,418,420,421,423,424,426,428,429 }, + /* 82 RPM */ { 12,12,12,13,13,13,14,14,14,15,15,15,16,16,17,17,17,18,18,18,19,19,19,20,20,20,21,21,21,22,22,22,23,23,23,24,24,24,25,25,25,26,26,26,27,27,27,28,28,28,29,30,32,34,35,37,38,40,41,43,45,46,48,49,51,52,54,56,57,59,60,62,64,65,67,68,70,71,73,75,76,78,79,81,83,84,86,87,89,90,92,94,95,97,98,100,102,103,105,106,108,111,113,116,119,121,124,127,129,132,135,137,140,143,145,148,151,153,156,159,161,164,167,169,172,175,177,180,183,185,188,191,193,196,199,201,204,207,209,212,215,217,220,223,225,228,231,233,236,239,241,244,246,249,251,254,256,258,261,263,266,268,270,273,275,278,280,282,285,287,290,292,294,297,299,302,304,307,309,311,314,316,319,321,323,326,328,331,333,335,338,340,343,345,347,350,352,355,357,360,362,363,365,367,368,370,371,373,374,376,377,379,380,382,383,385,386,388,389,391,392,394,396,397,399,400,402,403,405,406,408,409,411,412,414,415,417,418,420,421,423,425,426,428,429,431,432,434,435,437,438 }, + /* 83 RPM */ { 12,12,13,13,13,14,14,14,15,15,16,16,16,17,17,17,18,18,18,19,19,19,20,20,20,21,21,21,22,22,22,23,23,24,24,24,25,25,25,26,26,26,27,27,27,28,28,28,29,29,29,31,33,34,36,38,39,41,42,44,46,47,49,50,52,54,55,57,59,60,62,63,65,67,68,70,72,73,75,76,78,80,81,83,85,86,88,89,91,93,94,96,98,99,101,102,104,106,107,109,110,113,116,119,121,124,127,130,132,135,138,140,143,146,149,151,154,157,159,162,165,168,170,173,176,179,181,184,187,189,192,195,198,200,203,206,208,211,214,217,219,222,225,228,230,233,236,238,241,244,247,249,252,254,257,259,261,264,266,269,271,274,276,279,281,284,286,289,291,294,296,299,301,304,306,309,311,314,316,319,321,324,326,329,331,334,336,339,341,344,346,349,351,354,356,358,361,363,366,368,371,372,374,376,377,379,380,382,383,385,386,388,389,391,392,394,395,397,398,400,402,403,405,406,408,409,411,412,414,415,417,418,420,421,423,424,426,428,429,431,432,434,435,437,438,440,441,443,444,446,447 }, + /* 84 RPM */ { 12,13,13,13,14,14,14,15,15,16,16,16,17,17,17,18,18,18,19,19,19,20,20,20,21,21,22,22,22,23,23,23,24,24,24,25,25,25,26,26,27,27,27,28,28,28,29,29,29,30,30,32,33,35,37,38,40,42,43,45,47,48,50,52,53,55,57,58,60,62,63,65,67,68,70,72,73,75,77,78,80,82,83,85,87,88,90,91,93,95,96,98,100,101,103,105,106,108,110,111,113,116,119,121,124,127,130,132,135,138,141,144,146,149,152,155,157,160,163,166,169,171,174,177,180,182,185,188,191,193,196,199,202,205,207,210,213,216,218,221,224,227,229,232,235,238,241,243,246,249,252,254,257,259,262,264,267,270,272,275,277,280,282,285,288,290,293,295,298,300,303,306,308,311,313,316,318,321,323,326,329,331,334,336,339,341,344,347,349,352,354,357,359,362,365,367,370,372,375,377,380,381,383,385,386,388,389,391,392,394,395,397,398,400,401,403,404,406,408,409,411,412,414,415,417,418,420,421,423,424,426,427,429,431,432,434,435,437,438,440,441,443,444,446,447,449,450,452,454,455,457 }, + /* 85 RPM */ { 13,13,13,14,14,14,15,15,15,16,16,17,17,17,18,18,18,19,19,19,20,20,21,21,21,22,22,22,23,23,23,24,24,25,25,25,26,26,26,27,27,27,28,28,29,29,29,30,30,30,31,32,34,36,38,39,41,43,44,46,48,49,51,53,55,56,58,60,61,63,65,66,68,70,71,73,75,77,78,80,82,83,85,87,88,90,92,94,95,97,99,100,102,104,105,107,109,111,112,114,116,118,121,124,127,130,133,135,138,141,144,147,150,152,155,158,161,164,166,169,172,175,178,181,183,186,189,192,195,197,200,203,206,209,212,214,217,220,223,226,229,231,234,237,240,243,245,248,251,254,257,259,262,265,267,270,273,275,278,281,283,286,288,291,294,296,299,302,304,307,310,312,315,318,320,323,325,328,331,333,336,339,341,344,347,349,352,355,357,360,363,365,368,370,373,376,378,381,384,386,389,390,392,394,395,397,398,400,401,403,404,406,407,409,410,412,414,415,417,418,420,421,423,424,426,427,429,430,432,434,435,437,438,440,441,443,444,446,447,449,450,452,453,455,457,458,460,461,463,464,466 }, + /* 86 RPM */ { 13,13,14,14,14,15,15,15,16,16,17,17,17,18,18,18,19,19,20,20,20,21,21,21,22,22,23,23,23,24,24,24,25,25,25,26,26,27,27,27,28,28,28,29,29,30,30,30,31,31,31,33,35,37,38,40,42,44,45,47,49,51,52,54,56,57,59,61,63,64,66,68,70,71,73,75,77,78,80,82,84,85,87,89,90,92,94,96,97,99,101,103,104,106,108,110,111,113,115,116,118,121,124,127,130,133,135,138,141,144,147,150,153,156,158,161,164,167,170,173,176,179,181,184,187,190,193,196,199,202,204,207,210,213,216,219,222,225,227,230,233,236,239,242,245,247,250,253,256,259,262,265,267,270,273,275,278,281,284,286,289,292,295,297,300,303,305,308,311,314,316,319,322,324,327,330,333,335,338,341,344,346,349,352,354,357,360,363,365,368,371,373,376,379,382,384,387,390,393,395,398,400,401,403,404,406,407,409,410,412,413,415,416,418,420,421,423,424,426,427,429,430,432,433,435,436,438,440,441,443,444,446,447,449,450,452,453,455,456,458,460,461,463,464,466,467,469,470,472,473,475 }, + /* 87 RPM */ { 13,14,14,14,15,15,15,16,16,17,17,17,18,18,18,19,19,20,20,20,21,21,21,22,22,23,23,23,24,24,25,25,25,26,26,26,27,27,28,28,28,29,29,29,30,30,31,31,31,32,32,34,36,37,39,41,43,45,46,48,50,52,53,55,57,59,60,62,64,66,68,69,71,73,75,76,78,80,82,84,85,87,89,91,92,94,96,98,100,101,103,105,107,108,110,112,114,115,117,119,121,124,127,130,132,135,138,141,144,147,150,153,156,159,162,165,168,170,173,176,179,182,185,188,191,194,197,200,203,206,208,211,214,217,220,223,226,229,232,235,238,241,244,246,249,252,255,258,261,264,267,270,273,275,278,281,284,287,289,292,295,298,301,303,306,309,312,315,317,320,323,326,329,331,334,337,340,343,345,348,351,354,357,359,362,365,368,371,373,376,379,382,385,387,390,393,396,399,401,404,407,409,410,412,413,415,416,418,419,421,422,424,425,427,429,430,432,433,435,436,438,439,441,442,444,446,447,449,450,452,453,455,456,458,459,461,463,464,466,467,469,470,472,473,475,476,478,479,481,483,484 }, + /* 88 RPM */ { 13,14,14,15,15,15,16,16,16,17,17,18,18,18,19,19,20,20,20,21,21,22,22,22,23,23,23,24,24,25,25,25,26,26,27,27,27,28,28,28,29,29,30,30,30,31,31,32,32,32,33,35,36,38,40,42,44,45,47,49,51,53,55,56,58,60,62,64,65,67,69,71,73,74,76,78,80,82,84,85,87,89,91,93,94,96,98,100,102,103,105,107,109,111,113,114,116,118,120,122,123,126,129,132,135,138,141,144,147,150,153,156,159,162,165,168,171,174,177,180,183,186,189,192,195,198,201,204,207,210,213,216,219,221,224,227,230,233,236,239,242,245,248,251,254,257,260,263,266,269,272,275,278,281,284,286,289,292,295,298,301,304,307,309,312,315,318,321,324,327,330,332,335,338,341,344,347,350,353,356,358,361,364,367,370,373,376,379,381,384,387,390,393,396,399,402,404,407,410,413,416,418,419,421,422,424,425,427,428,430,431,433,435,436,438,439,441,442,444,445,447,448,450,452,453,455,456,458,459,461,462,464,465,467,469,470,472,473,475,476,478,479,481,482,484,486,487,489,490,492,493 }, + /* 89 RPM */ { 14,14,14,15,15,16,16,16,17,17,18,18,18,19,19,20,20,20,21,21,22,22,22,23,23,24,24,24,25,25,26,26,26,27,27,27,28,28,29,29,29,30,30,31,31,31,32,32,33,33,33,35,37,39,41,43,45,46,48,50,52,54,56,57,59,61,63,65,67,69,70,72,74,76,78,80,82,83,85,87,89,91,93,95,96,98,100,102,104,106,107,109,111,113,115,117,119,120,122,124,126,129,132,135,138,141,144,147,150,153,156,159,162,165,168,171,174,177,180,183,186,189,192,196,199,202,205,208,211,214,217,220,223,226,229,232,235,238,241,244,247,250,253,256,259,262,265,268,271,274,277,280,283,286,289,292,295,298,301,304,307,310,313,316,319,321,324,327,330,333,336,339,342,345,348,351,354,357,360,363,366,369,372,375,378,381,384,387,390,392,395,398,401,404,407,410,413,416,419,422,425,427,428,430,431,433,434,436,437,439,440,442,444,445,447,448,450,451,453,454,456,458,459,461,462,464,465,467,468,470,471,473,475,476,478,479,481,482,484,485,487,489,490,492,493,495,496,498,499,501,502 }, + /* 90 RPM */ { 14,14,15,15,16,16,16,17,17,18,18,18,19,19,20,20,20,21,21,22,22,22,23,23,24,24,24,25,25,26,26,26,27,27,28,28,28,29,29,30,30,30,31,31,32,32,32,33,33,34,34,36,38,40,42,44,45,47,49,51,53,55,57,59,61,62,64,66,68,70,72,74,76,78,79,81,83,85,87,89,91,93,95,96,98,100,102,104,106,108,110,112,113,115,117,119,121,123,125,127,129,132,135,138,141,144,147,150,153,156,159,162,165,169,172,175,178,181,184,187,190,193,196,199,202,205,208,212,215,218,221,224,227,230,233,236,239,242,245,248,252,255,258,261,264,267,270,273,276,279,282,285,288,291,294,297,300,303,307,310,313,316,319,322,325,328,331,334,337,340,343,346,349,352,355,358,361,364,367,370,373,376,379,382,385,388,392,395,398,401,404,407,410,413,416,419,422,425,428,431,434,436,437,439,440,442,443,445,446,448,450,451,453,454,456,457,459,460,462,464,465,467,468,470,471,473,474,476,477,479,481,482,484,485,487,488,490,491,493,495,496,498,499,501,502,504,505,507,509,510,512 }, + /* 91 RPM */ { 14,14,14,15,15,16,16,16,17,17,18,18,18,19,19,20,20,21,21,21,22,22,23,23,23,24,24,25,25,25,26,26,27,27,27,28,28,29,29,29,30,30,31,31,31,32,32,33,33,33,34,36,38,40,41,43,45,47,49,51,53,55,57,58,60,62,64,66,68,70,72,74,76,77,79,81,83,85,87,89,91,93,94,96,98,100,102,104,106,108,110,111,113,115,117,119,121,123,125,127,129,132,135,138,141,144,147,150,153,156,160,163,166,169,172,175,178,181,184,188,191,194,197,200,203,206,209,212,216,219,222,225,228,231,234,237,240,243,247,250,253,256,259,262,265,268,271,275,278,281,284,287,290,293,296,299,303,306,309,312,315,318,321,324,328,331,334,337,340,343,346,349,352,356,359,362,365,368,371,374,377,381,384,387,390,393,396,399,402,405,409,412,415,418,421,424,427,430,434,437,440,441,443,445,446,448,449,451,453,454,456,457,459,461,462,464,465,467,469,470,472,474,475,477,478,480,482,483,485,486,488,490,491,493,494,496,498,499,501,502,504,506,507,509,510,512,514,515,517,519,520 }, + /* 92 RPM */ { 13,14,14,15,15,15,16,16,17,17,17,18,18,19,19,19,20,20,21,21,21,22,22,23,23,23,24,24,25,25,25,26,26,27,27,28,28,28,29,29,30,30,30,31,31,32,32,32,33,33,34,36,37,39,41,43,45,47,49,51,53,54,56,58,60,62,64,66,68,70,72,73,75,77,79,81,83,85,87,89,91,92,94,96,98,100,102,104,106,108,110,111,113,115,117,119,121,123,125,127,128,132,135,138,141,144,147,150,154,157,160,163,166,169,172,176,179,182,185,188,191,194,198,201,204,207,210,213,216,220,223,226,229,232,235,238,242,245,248,251,254,257,260,263,267,270,273,276,279,282,285,289,292,295,298,301,305,308,311,314,317,321,324,327,330,333,337,340,343,346,349,353,356,359,362,366,369,372,375,378,382,385,388,391,394,398,401,404,407,410,414,417,420,423,426,430,433,436,439,442,446,447,449,451,452,454,455,457,459,460,462,464,465,467,469,470,472,474,475,477,479,480,482,484,485,487,489,490,492,494,495,497,499,500,502,504,505,507,509,510,512,514,515,517,519,520,522,524,525,527,529 }, + /* 93 RPM */ { 13,13,14,14,15,15,15,16,16,17,17,17,18,18,19,19,19,20,20,21,21,22,22,22,23,23,24,24,24,25,25,26,26,26,27,27,28,28,28,29,29,30,30,31,31,31,32,32,33,33,33,35,37,39,41,43,45,47,49,50,52,54,56,58,60,62,64,66,68,70,71,73,75,77,79,81,83,85,87,89,90,92,94,96,98,100,102,104,106,108,109,111,113,115,117,119,121,123,125,127,128,132,135,138,141,144,148,151,154,157,160,163,167,170,173,176,179,182,186,189,192,195,198,201,205,208,211,214,217,220,224,227,230,233,236,240,243,246,249,252,255,259,262,265,268,271,274,278,281,284,287,290,294,297,300,304,307,310,313,317,320,323,326,330,333,336,340,343,346,349,353,356,359,363,366,369,372,376,379,382,386,389,392,395,399,402,405,409,412,415,418,422,425,428,432,435,438,441,445,448,451,453,455,456,458,460,462,463,465,467,468,470,472,474,475,477,479,480,482,484,486,487,489,491,492,494,496,498,499,501,503,504,506,508,510,511,513,515,516,518,520,522,523,525,527,528,530,532,534,535,537 }, + /* 94 RPM */ { 13,13,13,14,14,15,15,16,16,16,17,17,18,18,18,19,19,20,20,20,21,21,22,22,22,23,23,24,24,25,25,25,26,26,27,27,27,28,28,29,29,29,30,30,31,31,31,32,32,33,33,35,37,39,41,43,45,46,48,50,52,54,56,58,60,62,64,66,67,69,71,73,75,77,79,81,83,85,87,88,90,92,94,96,98,100,102,104,106,107,109,111,113,115,117,119,121,123,125,127,128,132,135,138,141,144,148,151,154,157,161,164,167,170,173,177,180,183,186,189,193,196,199,202,205,209,212,215,218,221,225,228,231,234,237,241,244,247,250,253,257,260,263,266,269,273,276,279,282,286,289,292,295,299,302,306,309,312,316,319,322,326,329,332,336,339,343,346,349,353,356,359,363,366,370,373,376,380,383,386,390,393,396,400,403,407,410,413,417,420,423,427,430,433,437,440,444,447,450,454,457,459,461,462,464,466,468,469,471,473,475,477,478,480,482,484,485,487,489,491,492,494,496,498,500,501,503,505,507,508,510,512,514,515,517,519,521,523,524,526,528,530,531,533,535,537,538,540,542,544,546 }, + /* 95 RPM */ { 12,13,13,14,14,14,15,15,16,16,16,17,17,18,18,18,19,19,20,20,21,21,21,22,22,23,23,23,24,24,25,25,25,26,26,27,27,28,28,28,29,29,30,30,30,31,31,32,32,32,33,35,37,39,41,42,44,46,48,50,52,54,56,58,60,62,63,65,67,69,71,73,75,77,79,81,83,84,86,88,90,92,94,96,98,100,102,104,105,107,109,111,113,115,117,119,121,123,125,127,128,132,135,138,141,145,148,151,154,158,161,164,167,171,174,177,180,183,187,190,193,196,200,203,206,209,213,216,219,222,226,229,232,235,239,242,245,248,251,255,258,261,264,268,271,274,277,281,284,287,290,294,297,301,304,308,311,314,318,321,325,328,332,335,339,342,346,349,352,356,359,363,366,370,373,377,380,383,387,390,394,397,401,404,408,411,415,418,421,425,428,432,435,439,442,446,449,452,456,459,463,465,466,468,470,472,474,476,477,479,481,483,485,487,488,490,492,494,496,497,499,501,503,505,507,508,510,512,514,516,518,519,521,523,525,527,528,530,532,534,536,538,539,541,543,545,547,549,550,552,554 }, + /* 96 RPM */ { 12,12,13,13,14,14,14,15,15,16,16,17,17,17,18,18,19,19,19,20,20,21,21,21,22,22,23,23,24,24,24,25,25,26,26,26,27,27,28,28,29,29,29,30,30,31,31,31,32,32,33,35,36,38,40,42,44,46,48,50,52,54,56,58,59,61,63,65,67,69,71,73,75,77,79,81,82,84,86,88,90,92,94,96,98,100,102,103,105,107,109,111,113,115,117,119,121,123,125,126,128,132,135,138,141,145,148,151,155,158,161,164,168,171,174,177,181,184,187,191,194,197,200,204,207,210,213,217,220,223,227,230,233,236,240,243,246,249,253,256,259,263,266,269,272,276,279,282,285,289,292,295,299,303,306,310,313,317,320,324,327,331,334,338,341,345,348,352,356,359,363,366,370,373,377,380,384,387,391,394,398,401,405,409,412,416,419,423,426,430,433,437,440,444,447,451,454,458,462,465,469,470,472,474,476,478,480,482,484,485,487,489,491,493,495,497,499,500,502,504,506,508,510,512,514,516,517,519,521,523,525,527,529,531,532,534,536,538,540,542,544,546,547,549,551,553,555,557,559,561,562 }, + /* 97 RPM */ { 12,12,12,13,13,14,14,15,15,15,16,16,17,17,17,18,18,19,19,20,20,20,21,21,22,22,22,23,23,24,24,25,25,25,26,26,27,27,27,28,28,29,29,29,30,30,31,31,32,32,32,34,36,38,40,42,44,46,48,50,52,54,55,57,59,61,63,65,67,69,71,73,75,77,78,80,82,84,86,88,90,92,94,96,98,100,101,103,105,107,109,111,113,115,117,119,121,123,125,126,128,132,135,138,142,145,148,151,155,158,161,165,168,171,175,178,181,185,188,191,194,198,201,204,208,211,214,218,221,224,227,231,234,237,241,244,247,251,254,257,261,264,267,270,274,277,280,284,287,290,294,297,301,304,308,312,315,319,322,326,330,333,337,341,344,348,351,355,359,362,366,369,373,377,380,384,388,391,395,398,402,406,409,413,416,420,424,427,431,435,438,442,445,449,453,456,460,464,467,471,474,476,478,480,482,484,486,488,490,492,494,496,498,499,501,503,505,507,509,511,513,515,517,519,521,523,525,526,528,530,532,534,536,538,540,542,544,546,548,550,552,554,555,557,559,561,563,565,567,569,571 }, + /* 98 RPM */ { 11,12,12,13,13,13,14,14,15,15,15,16,16,17,17,18,18,18,19,19,20,20,20,21,21,22,22,23,23,23,24,24,25,25,25,26,26,27,27,28,28,28,29,29,30,30,30,31,31,32,32,34,36,38,40,42,44,46,48,49,51,53,55,57,59,61,63,65,67,69,71,73,74,76,78,80,82,84,86,88,90,92,94,96,98,99,101,103,105,107,109,111,113,115,117,119,121,123,124,126,128,132,135,138,142,145,148,152,155,158,162,165,168,172,175,178,182,185,188,192,195,198,202,205,208,212,215,218,222,225,228,232,235,238,242,245,248,252,255,258,262,265,268,272,275,278,282,285,289,292,295,299,303,306,310,314,317,321,325,328,332,336,340,343,347,351,354,358,362,365,369,373,377,380,384,388,391,395,399,402,406,410,414,417,421,425,428,432,436,439,443,447,451,454,458,462,465,469,473,476,480,482,484,486,488,490,492,494,496,498,500,502,504,506,508,510,512,514,516,518,520,522,524,526,528,530,532,534,536,538,540,542,544,546,548,550,552,554,556,558,560,562,563,565,567,569,571,573,575,577,579 }, + /* 99 RPM */ { 11,11,12,12,13,13,13,14,14,15,15,16,16,16,17,17,18,18,19,19,19,20,20,21,21,21,22,22,23,23,24,24,24,25,25,26,26,26,27,27,28,28,29,29,29,30,30,31,31,31,32,34,36,38,40,42,43,45,47,49,51,53,55,57,59,61,63,65,67,69,70,72,74,76,78,80,82,84,86,88,90,92,94,96,97,99,101,103,105,107,109,111,113,115,117,119,121,123,124,126,128,132,135,138,142,145,149,152,155,159,162,165,169,172,175,179,182,186,189,192,196,199,202,206,209,213,216,219,223,226,229,233,236,240,243,246,250,253,256,260,263,266,270,273,277,280,283,287,290,293,297,301,304,308,312,316,319,323,327,331,335,338,342,346,350,354,357,361,365,369,372,376,380,384,388,391,395,399,403,406,410,414,418,422,425,429,433,437,441,444,448,452,456,459,463,467,471,475,478,482,486,488,490,492,494,496,498,500,502,504,506,508,510,512,514,516,519,521,523,525,527,529,531,533,535,537,539,541,543,545,547,549,551,553,555,557,559,561,563,565,567,569,572,574,576,578,580,582,584,586,588 }, + /* 100 RPM */ { 11,11,12,12,12,13,13,14,14,14,15,15,16,16,17,17,17,18,18,19,19,19,20,20,21,21,22,22,22,23,23,24,24,25,25,25,26,26,27,27,27,28,28,29,29,30,30,30,31,31,32,34,36,37,39,41,43,45,47,49,51,53,55,57,59,61,63,65,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,95,97,99,101,103,105,107,109,111,113,115,117,119,121,122,124,126,128,132,135,138,142,145,149,152,156,159,162,166,169,173,176,179,183,186,190,193,196,200,203,207,210,213,217,220,224,227,230,234,237,241,244,247,251,254,258,261,264,268,271,275,278,281,285,288,292,295,298,302,306,310,314,318,322,325,329,333,337,341,345,349,353,356,360,364,368,372,376,380,383,387,391,395,399,403,407,410,414,418,422,426,430,434,438,441,445,449,453,457,461,465,468,472,476,480,484,488,492,494,496,498,500,502,504,506,508,510,513,515,517,519,521,523,525,527,529,531,534,536,538,540,542,544,546,548,550,552,554,557,559,561,563,565,567,569,571,573,575,577,580,582,584,586,588,590,592,594,596 }, + /* 101 RPM */ { 11,11,11,12,12,13,13,14,14,14,15,15,16,16,17,17,17,18,18,19,19,20,20,20,21,21,22,22,23,23,23,24,24,25,25,26,26,26,27,27,28,28,29,29,29,30,30,31,31,32,32,34,36,38,40,42,44,46,48,50,52,53,55,57,59,61,63,65,67,69,71,73,75,77,79,81,83,85,87,89,91,93,95,97,98,100,102,104,106,108,110,112,114,116,118,120,122,124,126,128,130,133,137,140,144,147,151,154,157,161,164,168,171,175,178,182,185,188,192,195,199,202,206,209,213,216,220,223,226,230,233,237,240,244,247,251,254,257,261,264,268,271,275,278,282,285,289,292,295,299,302,306,310,314,318,322,326,330,334,338,342,346,350,353,357,361,365,369,373,377,381,385,389,393,397,401,405,409,412,416,420,424,428,432,436,440,444,448,452,456,460,464,468,471,475,479,483,487,491,495,499,501,503,505,508,510,512,514,516,518,520,523,525,527,529,531,533,536,538,540,542,544,546,548,551,553,555,557,559,561,563,566,568,570,572,574,576,578,581,583,585,587,589,591,594,596,598,600,602,604,606 }, + /* 102 RPM */ { 11,11,11,12,12,13,13,14,14,14,15,15,16,16,17,17,18,18,18,19,19,20,20,21,21,21,22,22,23,23,24,24,24,25,25,26,26,27,27,27,28,28,29,29,30,30,31,31,31,32,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100,102,104,106,108,110,111,113,115,117,119,121,123,125,127,129,131,135,138,142,145,149,152,156,159,163,166,170,173,177,180,184,187,191,194,198,201,205,208,212,215,219,222,226,229,233,236,240,243,247,250,254,257,261,264,268,271,275,278,282,285,289,292,296,299,303,306,310,314,318,322,326,330,334,338,342,346,350,354,358,362,366,370,374,378,382,386,390,394,398,402,406,410,414,418,422,426,430,434,438,442,446,450,454,458,462,466,470,474,478,482,486,490,494,498,502,506,509,511,513,515,517,520,522,524,526,528,531,533,535,537,539,542,544,546,548,550,553,555,557,559,561,564,566,568,570,572,575,577,579,581,583,586,588,590,592,595,597,599,601,603,606,608,610,612,614,617 }, + /* 103 RPM */ { 11,11,11,12,12,13,13,14,14,15,15,15,16,16,17,17,18,18,18,19,19,20,20,21,21,22,22,22,23,23,24,24,25,25,26,26,26,27,27,28,28,29,29,29,30,30,31,31,32,32,33,35,37,39,41,43,45,47,49,51,53,55,57,59,61,63,65,67,69,71,73,75,77,79,81,83,85,87,89,91,93,95,97,99,101,103,105,107,109,111,113,115,117,119,121,123,125,127,129,131,133,136,140,143,147,151,154,158,161,165,168,172,175,179,182,186,190,193,197,200,204,207,211,214,218,221,225,229,232,236,239,243,246,250,253,257,260,264,268,271,275,278,282,285,289,292,296,299,303,307,310,314,318,322,326,330,335,339,343,347,351,355,359,363,367,371,375,379,383,387,392,396,400,404,408,412,416,420,424,428,432,436,440,444,448,453,457,461,465,469,473,477,481,485,489,493,497,501,505,510,514,516,518,520,523,525,527,529,532,534,536,539,541,543,545,548,550,552,554,557,559,561,563,566,568,570,572,575,577,579,581,584,586,588,591,593,595,597,600,602,604,606,609,611,613,615,618,620,622,624,627 }, + /* 104 RPM */ { 11,11,11,12,12,13,13,14,14,15,15,15,16,16,17,17,18,18,19,19,19,20,20,21,21,22,22,23,23,23,24,24,25,25,26,26,27,27,27,28,28,29,29,30,30,31,31,32,32,32,33,35,37,39,41,43,45,47,49,51,53,55,57,59,61,63,65,67,69,71,73,75,77,80,82,84,86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,118,120,122,124,126,128,130,132,134,138,142,145,149,152,156,159,163,167,170,174,177,181,185,188,192,195,199,203,206,210,213,217,221,224,228,231,235,239,242,246,249,253,256,260,264,267,271,274,278,282,285,289,292,296,300,303,307,310,314,318,322,326,331,335,339,343,347,351,355,360,364,368,372,376,380,384,388,393,397,401,405,409,413,417,422,426,430,434,438,442,446,451,455,459,463,467,471,475,480,484,488,492,496,500,504,509,513,517,521,523,526,528,530,533,535,537,539,542,544,546,549,551,553,556,558,560,563,565,567,570,572,574,577,579,581,584,586,588,590,593,595,597,600,602,604,607,609,611,614,616,618,621,623,625,628,630,632,635,637 }, + /* 105 RPM */ { 10,11,11,12,12,13,13,14,14,15,15,15,16,16,17,17,18,18,19,19,20,20,20,21,21,22,22,23,23,24,24,25,25,25,26,26,27,27,28,28,29,29,30,30,30,31,31,32,32,33,33,35,37,39,41,43,45,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,87,89,91,93,95,97,99,101,103,105,107,109,111,113,115,117,119,121,123,126,128,130,132,134,136,139,143,147,150,154,158,161,165,169,172,176,180,183,187,190,194,198,201,205,209,212,216,220,223,227,230,234,238,241,245,249,252,256,260,263,267,271,274,278,281,285,289,292,296,300,303,307,311,314,318,322,326,331,335,339,343,347,352,356,360,364,368,373,377,381,385,389,394,398,402,406,410,415,419,423,427,432,436,440,444,448,453,457,461,465,469,474,478,482,486,490,495,499,503,507,511,516,520,524,528,531,533,535,538,540,543,545,547,550,552,554,557,559,562,564,566,569,571,573,576,578,581,583,585,588,590,592,595,597,600,602,604,607,609,611,614,616,619,621,623,626,628,630,633,635,637,640,642,645,647 }, + /* 106 RPM */ { 10,11,11,12,12,13,13,14,14,15,15,16,16,16,17,17,18,18,19,19,20,20,21,21,21,22,22,23,23,24,24,25,25,26,26,27,27,27,28,28,29,29,30,30,31,31,32,32,33,33,33,35,38,40,42,44,46,48,50,52,54,56,58,60,63,65,67,69,71,73,75,77,79,81,83,85,87,90,92,94,96,98,100,102,104,106,108,110,112,114,117,119,121,123,125,127,129,131,133,135,137,141,145,148,152,156,159,163,167,171,174,178,182,185,189,193,196,200,204,207,211,215,218,222,226,230,233,237,241,244,248,252,255,259,263,266,270,274,278,281,285,289,292,296,300,303,307,311,314,318,322,326,330,335,339,343,347,352,356,360,365,369,373,377,382,386,390,394,399,403,407,412,416,420,424,429,433,437,442,446,450,454,459,463,467,471,476,480,484,489,493,497,501,506,510,514,519,523,527,531,536,538,540,543,545,548,550,553,555,557,560,562,565,567,570,572,574,577,579,582,584,587,589,592,594,596,599,601,604,606,609,611,613,616,618,621,623,626,628,630,633,635,638,640,643,645,647,650,652,655,657 }, + /* 107 RPM */ { 10,11,11,12,12,13,13,14,14,15,15,16,16,16,17,17,18,18,19,19,20,20,21,21,22,22,23,23,23,24,24,25,25,26,26,27,27,28,28,29,29,30,30,30,31,31,32,32,33,33,34,36,38,40,42,44,46,48,51,53,55,57,59,61,63,65,67,69,72,74,76,78,80,82,84,86,88,90,93,95,97,99,101,103,105,107,109,112,114,116,118,120,122,124,126,128,130,133,135,137,139,143,146,150,154,158,161,165,169,172,176,180,184,187,191,195,199,202,206,210,214,217,221,225,229,232,236,240,243,247,251,255,258,262,266,270,273,277,281,285,288,292,296,300,303,307,311,314,318,322,326,330,334,339,343,347,352,356,360,365,369,373,378,382,387,391,395,400,404,408,413,417,421,426,430,434,439,443,447,452,456,460,465,469,473,478,482,486,491,495,499,504,508,513,517,521,526,530,534,539,543,545,548,550,553,555,558,560,563,565,568,570,573,575,578,580,583,585,588,590,593,595,598,600,603,605,608,610,613,615,618,620,623,625,627,630,632,635,637,640,642,645,647,650,652,655,657,660,662,665,667 }, + /* 108 RPM */ { 10,11,11,12,12,13,13,14,14,15,15,16,16,17,17,17,18,18,19,19,20,20,21,21,22,22,23,23,24,24,25,25,26,26,26,27,27,28,28,29,29,30,30,31,31,32,32,33,33,34,34,36,38,40,43,45,47,49,51,53,55,57,60,62,64,66,68,70,72,74,77,79,81,83,85,87,89,91,94,96,98,100,102,104,106,108,111,113,115,117,119,121,123,125,128,130,132,134,136,138,140,144,148,152,155,159,163,167,171,174,178,182,186,190,193,197,201,205,208,212,216,220,224,227,231,235,239,243,246,250,254,258,261,265,269,273,277,280,284,288,292,296,299,303,307,311,314,318,322,326,330,334,338,343,347,352,356,360,365,369,374,378,383,387,391,396,400,405,409,413,418,422,427,431,436,440,444,449,453,458,462,466,471,475,480,484,488,493,497,502,506,511,515,519,524,528,533,537,541,546,550,553,555,558,560,563,566,568,571,573,576,578,581,583,586,588,591,593,596,599,601,604,606,609,611,614,616,619,621,624,627,629,632,634,637,639,642,644,647,649,652,655,657,660,662,665,667,670,672,675,677 }, + /* 109 RPM */ { 10,11,11,12,12,13,13,14,14,15,15,16,16,17,17,18,18,18,19,19,20,20,21,21,22,22,23,23,24,24,25,25,26,26,27,27,28,28,29,29,30,30,30,31,31,32,32,33,33,34,34,36,39,41,43,45,47,49,52,54,56,58,60,62,64,67,69,71,73,75,77,79,82,84,86,88,90,92,95,97,99,101,103,105,107,110,112,114,116,118,120,122,125,127,129,131,133,135,138,140,142,146,150,153,157,161,165,169,173,176,180,184,188,192,195,199,203,207,211,215,218,222,226,230,234,238,241,245,249,253,257,261,264,268,272,276,280,284,287,291,295,299,303,307,310,314,318,322,326,330,333,338,342,347,351,356,360,365,369,374,378,383,387,392,396,401,405,410,414,419,423,428,432,437,441,446,450,454,459,463,468,472,477,481,486,490,495,499,504,508,513,517,522,526,531,535,540,544,549,553,558,560,563,565,568,571,573,576,578,581,584,586,589,591,594,597,599,602,604,607,610,612,615,617,620,623,625,628,630,633,636,638,641,643,646,649,651,654,656,659,662,664,667,669,672,675,677,680,682,685,688 }, + /* 110 RPM */ { 10,11,11,12,12,13,13,14,14,15,15,16,16,17,17,18,18,19,19,20,20,21,21,21,22,22,23,23,24,24,25,25,26,26,27,27,28,28,29,29,30,30,31,31,32,32,33,33,34,34,35,37,39,41,43,45,48,50,52,54,56,59,61,63,65,67,69,72,74,76,78,80,82,85,87,89,91,93,95,98,100,102,104,106,109,111,113,115,117,119,122,124,126,128,130,132,135,137,139,141,143,147,151,155,159,163,167,171,174,178,182,186,190,194,198,202,205,209,213,217,221,225,229,233,236,240,244,248,252,256,260,264,268,271,275,279,283,287,291,295,299,302,306,310,314,318,322,326,330,333,337,342,346,351,356,360,365,369,374,378,383,387,392,397,401,406,410,415,419,424,428,433,437,442,447,451,456,460,465,469,474,478,483,488,492,497,501,506,510,515,519,524,529,533,538,542,547,551,556,560,565,568,570,573,576,578,581,584,586,589,591,594,597,599,602,605,607,610,613,615,618,621,623,626,629,631,634,637,639,642,645,647,650,653,655,658,661,663,666,668,671,674,676,679,682,684,687,690,692,695,698 }, + /* 111 RPM */ { 11,11,12,12,12,13,13,14,14,15,15,16,16,17,17,18,18,19,19,20,20,21,21,22,22,23,23,24,24,25,25,26,26,27,27,28,28,29,29,30,30,31,31,32,32,33,33,34,34,35,35,37,40,42,44,46,49,51,53,55,57,60,62,64,66,68,71,73,75,77,80,82,84,86,88,91,93,95,97,100,102,104,106,108,111,113,115,117,119,122,124,126,128,131,133,135,137,139,142,144,146,150,154,158,162,166,170,174,178,182,186,190,194,197,201,205,209,213,217,221,225,229,233,237,241,245,249,253,257,261,265,269,273,277,281,284,288,292,296,300,304,308,312,316,320,324,328,332,336,340,344,348,353,358,362,367,372,376,381,386,390,395,399,404,409,413,418,423,427,432,437,441,446,450,455,460,464,469,474,478,483,488,492,497,501,506,511,515,520,525,529,534,539,543,548,552,557,562,566,571,576,578,581,584,586,589,592,595,597,600,603,605,608,611,614,616,619,622,624,627,630,632,635,638,641,643,646,649,651,654,657,660,662,665,668,670,673,676,678,681,684,687,689,692,695,697,700,703,706,708,711 }, + /* 112 RPM */ { 11,11,12,12,13,13,14,14,15,15,16,16,17,17,18,18,19,19,20,20,21,21,22,22,23,23,24,24,25,25,26,26,27,27,28,28,29,29,30,30,31,31,32,32,33,33,34,34,35,35,36,38,40,43,45,47,49,52,54,56,58,61,63,65,68,70,72,74,77,79,81,83,86,88,90,92,95,97,99,101,104,106,108,110,113,115,117,119,122,124,126,128,131,133,135,138,140,142,144,147,149,153,157,161,165,169,173,177,181,185,189,193,197,201,205,209,213,217,221,225,229,233,237,241,245,249,254,258,262,266,270,274,278,282,286,290,294,298,302,306,310,314,318,322,326,330,334,338,342,346,350,355,360,364,369,374,379,383,388,393,397,402,407,412,416,421,426,430,435,440,445,449,454,459,464,468,473,478,482,487,492,497,501,506,511,516,520,525,530,534,539,544,549,553,558,563,567,572,577,582,586,589,592,595,597,600,603,606,608,611,614,617,619,622,625,628,630,633,636,639,642,644,647,650,653,655,658,661,664,666,669,672,675,677,680,683,686,688,691,694,697,699,702,705,708,710,713,716,719,721,724 }, + /* 113 RPM */ { 11,11,12,12,13,13,14,14,15,16,16,17,17,18,18,19,19,20,20,21,21,22,22,23,23,24,24,25,25,26,26,27,27,28,28,29,29,30,30,31,31,32,32,33,33,34,35,35,36,36,37,39,41,43,46,48,50,53,55,57,60,62,64,66,69,71,73,76,78,80,83,85,87,89,92,94,96,99,101,103,106,108,110,112,115,117,119,122,124,126,129,131,133,135,138,140,142,145,147,149,152,156,160,164,168,172,176,180,184,188,193,197,201,205,209,213,217,221,225,229,234,238,242,246,250,254,258,262,266,270,275,279,283,287,291,295,299,303,307,311,316,320,324,328,332,336,340,344,348,352,357,361,366,371,376,381,385,390,395,400,405,410,414,419,424,429,434,438,443,448,453,458,462,467,472,477,482,486,491,496,501,506,511,515,520,525,530,535,539,544,549,554,559,563,568,573,578,583,588,592,597,600,603,606,608,611,614,617,620,622,625,628,631,634,636,639,642,645,648,650,653,656,659,662,664,667,670,673,676,679,681,684,687,690,693,695,698,701,704,707,709,712,715,718,721,723,726,729,732,735,737 }, + /* 114 RPM */ { 11,12,12,13,13,14,14,15,15,16,16,17,17,18,18,19,19,20,21,21,22,22,23,23,24,24,25,25,26,26,27,27,28,28,29,29,30,30,31,31,32,33,33,34,34,35,35,36,36,37,37,40,42,44,47,49,51,54,56,58,61,63,65,68,70,72,75,77,79,82,84,86,89,91,93,96,98,100,103,105,107,110,112,114,117,119,121,124,126,129,131,133,136,138,140,143,145,147,150,152,154,158,163,167,171,175,179,183,188,192,196,200,204,209,213,217,221,225,229,234,238,242,246,250,254,259,263,267,271,275,280,284,288,292,296,300,305,309,313,317,321,325,330,334,338,342,346,350,355,359,363,368,373,378,383,387,392,397,402,407,412,417,422,427,432,436,441,446,451,456,461,466,471,476,481,485,490,495,500,505,510,515,520,525,530,534,539,544,549,554,559,564,569,574,578,583,588,593,598,603,608,611,614,616,619,622,625,628,631,634,636,639,642,645,648,651,654,656,659,662,665,668,671,674,676,679,682,685,688,691,694,696,699,702,705,708,711,714,716,719,722,725,728,731,734,736,739,742,745,748,751 }, + /* 115 RPM */ { 11,12,12,13,13,14,14,15,16,16,17,17,18,18,19,19,20,20,21,21,22,22,23,24,24,25,25,26,26,27,27,28,28,29,29,30,30,31,32,32,33,33,34,34,35,35,36,36,37,37,38,40,43,45,47,50,52,55,57,59,62,64,66,69,71,74,76,78,81,83,86,88,90,93,95,97,100,102,105,107,109,112,114,116,119,121,124,126,128,131,133,136,138,140,143,145,147,150,152,155,157,161,165,170,174,178,182,187,191,195,199,204,208,212,216,221,225,229,233,238,242,246,250,255,259,263,267,272,276,280,284,289,293,297,301,306,310,314,318,323,327,331,335,340,344,348,352,357,361,365,369,374,379,384,389,394,399,404,409,414,419,424,429,434,439,444,449,454,459,464,469,474,479,484,489,494,499,504,509,514,519,524,529,534,539,544,549,554,559,564,569,574,579,584,589,594,599,604,609,614,619,622,624,627,630,633,636,639,642,645,648,651,654,656,659,662,665,668,671,674,677,680,683,685,688,691,694,697,700,703,706,709,712,715,717,720,723,726,729,732,735,738,741,744,747,749,752,755,758,761,764 }, + /* 116 RPM */ { 11,12,13,13,14,14,15,15,16,16,17,17,18,19,19,20,20,21,21,22,22,23,23,24,24,25,26,26,27,27,28,28,29,29,30,30,31,32,32,33,33,34,34,35,35,36,36,37,37,38,39,41,43,46,48,51,53,55,58,60,63,65,68,70,72,75,77,80,82,85,87,89,92,94,97,99,102,104,106,109,111,114,116,119,121,123,126,128,131,133,135,138,140,143,145,148,150,152,155,157,160,164,168,173,177,181,186,190,194,199,203,207,212,216,220,225,229,233,238,242,246,250,255,259,263,268,272,276,281,285,289,294,298,302,307,311,315,320,324,328,333,337,341,346,350,354,359,363,367,372,376,381,386,391,396,401,406,411,416,421,427,432,437,442,447,452,457,462,467,472,477,482,487,492,498,503,508,513,518,523,528,533,538,543,548,553,558,563,569,574,579,584,589,594,599,604,609,614,619,624,629,632,635,638,641,644,647,650,653,656,659,662,665,668,671,674,677,680,683,686,689,691,694,697,700,703,706,709,712,715,718,721,724,727,730,733,736,739,742,745,748,751,754,757,760,762,765,768,771,774,777 }, + /* 117 RPM */ { 12,12,13,13,14,14,15,16,16,17,17,18,18,19,19,20,20,21,22,22,23,23,24,24,25,25,26,27,27,28,28,29,29,30,30,31,31,32,33,33,34,34,35,35,36,36,37,38,38,39,39,42,44,47,49,52,54,56,59,61,64,66,69,71,74,76,79,81,84,86,88,91,93,96,98,101,103,106,108,111,113,116,118,121,123,125,128,130,133,135,138,140,143,145,148,150,153,155,157,160,162,167,171,176,180,184,189,193,198,202,206,211,215,220,224,228,233,237,242,246,250,255,259,264,268,272,277,281,286,290,294,299,303,308,312,316,321,325,329,334,338,343,347,351,356,360,365,369,373,378,382,387,393,398,403,408,413,418,424,429,434,439,444,449,454,460,465,470,475,480,485,491,496,501,506,511,516,521,527,532,537,542,547,552,558,563,568,573,578,583,589,594,599,604,609,614,619,625,630,635,640,643,646,649,652,655,658,661,664,667,670,673,676,679,682,685,688,691,694,697,700,703,706,709,712,715,718,721,724,727,730,733,736,739,742,745,748,751,754,757,760,763,766,769,772,775,778,781,785,788,791 }, + /* 118 RPM */ { 12,12,13,14,14,15,15,16,16,17,17,18,19,19,20,20,21,21,22,23,23,24,24,25,25,26,26,27,28,28,29,29,30,30,31,31,32,33,33,34,34,35,35,36,36,37,38,38,39,39,40,42,45,47,50,52,55,57,60,62,65,67,70,72,75,77,80,82,85,87,90,92,95,97,100,102,105,108,110,113,115,118,120,123,125,128,130,133,135,138,140,143,145,148,150,153,155,158,160,163,165,170,174,179,183,187,192,196,201,205,210,214,219,223,228,232,237,241,246,250,255,259,263,268,272,277,281,286,290,295,299,304,308,313,317,322,326,331,335,339,344,348,353,357,362,366,371,375,380,384,389,394,399,404,410,415,420,425,431,436,441,446,452,457,462,467,473,478,483,488,494,499,504,509,515,520,525,530,535,541,546,551,556,562,567,572,577,583,588,593,598,604,609,614,619,625,630,635,640,646,651,654,657,660,663,666,669,672,675,678,681,684,688,691,694,697,700,703,706,709,712,715,718,721,724,727,730,733,736,740,743,746,749,752,755,758,761,764,767,770,773,776,779,782,785,788,792,795,798,801,804 }, + /* 119 RPM */ { 12,13,13,14,14,15,15,16,17,17,18,18,19,19,20,21,21,22,22,23,23,24,25,25,26,26,27,27,28,29,29,30,30,31,31,32,33,33,34,34,35,35,36,37,37,38,38,39,39,40,41,43,46,48,51,53,56,58,61,63,66,69,71,74,76,79,81,84,86,89,91,94,97,99,102,104,107,109,112,114,117,119,122,125,127,130,132,135,137,140,142,145,147,150,153,155,158,160,163,165,168,172,177,182,186,191,195,200,204,209,213,218,222,227,231,236,241,245,250,254,259,263,268,272,277,281,286,291,295,300,304,309,313,318,322,327,331,336,341,345,350,354,359,363,368,372,377,381,386,391,395,400,406,411,416,422,427,432,438,443,448,454,459,464,470,475,480,486,491,496,502,507,512,518,523,528,534,539,544,550,555,560,566,571,576,582,587,592,598,603,608,614,619,624,630,635,640,646,651,656,662,665,668,671,674,677,680,683,686,690,693,696,699,702,705,708,711,714,718,721,724,727,730,733,736,739,742,746,749,752,755,758,761,764,767,770,774,777,780,783,786,789,792,795,798,802,805,808,811,814,817 }, + /* 120 RPM */ { 12,13,13,14,15,15,16,16,17,17,18,19,19,20,20,21,22,22,23,23,24,24,25,26,26,27,27,28,28,29,30,30,31,31,32,33,33,34,34,35,35,36,37,37,38,38,39,39,40,41,41,44,46,49,52,54,57,59,62,64,67,70,72,75,77,80,83,85,88,90,93,96,98,101,103,106,108,111,114,116,119,121,124,127,129,132,134,137,140,142,145,147,150,152,155,158,160,163,165,168,171,175,180,184,189,194,198,203,208,212,217,221,226,231,235,240,244,249,254,258,263,268,272,277,281,286,291,295,300,305,309,314,318,323,328,332,337,341,346,351,355,360,365,369,374,378,383,388,392,397,401,407,412,418,423,429,434,439,445,450,456,461,466,472,477,483,488,494,499,504,510,515,521,526,531,537,542,548,553,559,564,569,575,580,586,591,596,602,607,613,618,624,629,634,640,645,651,656,661,667,672,675,679,682,685,688,691,694,698,701,704,707,710,713,717,720,723,726,729,732,736,739,742,745,748,751,754,758,761,764,767,770,773,777,780,783,786,789,792,796,799,802,805,808,811,815,818,821,824,827,830 } +}; + +trixterxdreamv1bike::trixterxdreamv1bike(bool noWriteResistance, bool noHeartService, bool noVirtualDevice) { + // Initialize metrics + m_watt.setType(metric::METRIC_WATT); + Speed.setType(metric::METRIC_SPEED); + + // Set the fake bluetooth device info + this->bluetoothDevice = + QBluetoothDeviceInfo(QBluetoothUuid {QStringLiteral("774f25bd-6636-4cdc-9398-839de026be1d")}, "Trixter X-Dream V1 Bike", 0); + + // Set the wheel diameter for speed and distance calculations + this->set_wheelDiameter(DefaultWheelDiameter); + + // Create the settings object and load from QSettings. + this->appSettings = new trixterxdreamv1settings(); + + // QZ things from expected constructor + this->noWriteResistance = noWriteResistance; + this->noHeartService = noHeartService; + this->noVirtualDevice = noVirtualDevice; + this->noSteering = !appSettings->get_steeringEnabled(); + + // Calculate the steering mapping + this->calculateSteeringMap(); + + // fake hardware support for ERG mode to avoid ERG filters preventing + this->ergModeSupported = true; +} + +bool trixterxdreamv1bike::connect(QString portName) { + // In case already connected, disconnect. + this->disconnectPort(); + + // Get the current time in milliseconds since ancient times. + this->t0 = getTime(); + + // create the port object and connect it + auto thisObject = this; + this->port = new trixterxdreamv1serial(this); + this->port->set_receiveBytes([thisObject](const QByteArray& bytes)->void{thisObject->receiveBytes(bytes);}); + this->port->set_getTime(getTime); + + // tell the client where to get the time + this->client.set_GetTime(getTime); + + // tell the client how to send data to the device + if(!noWriteResistance) { + auto device=this->port; + this->client.set_WriteBytes([device](uint8_t * bytes, int length)->void{ device->write(QByteArray((const char *)bytes, length));}); + } + // Set up a stopwatch to time the connection operations + QElapsedTimer stopWatch; + stopWatch.start(); + + // open the port. + if(!this->port->open(portName)) { + qDebug() << "Failed to open port, determined after " << stopWatch.elapsed() << "milliseconds"; + return false; + } + + // Start the metrics update timer + this->metricsUpdateTimerId = this->startTimer(UpdateMetricsInterval, Qt::PreciseTimer); + if(this->metricsUpdateTimerId==0) + { + qDebug() << "Failed to start metrics update timer"; + throw "Failed to start metrics timer"; + } + + // wait for up to the configured connection timeout for some packets to arrive + for(uint32_t start = getTime(), limit=start+this->appSettings->get_connectionTimeoutMilliseconds(); getTime()connected()) { + qDebug() << "Connected after " << stopWatch.elapsed() << "milliseconds"; + break; + } + QThread::msleep(20); + } + + if(!this->connected()) + { + qDebug() << "Failed to connect to device, after " << stopWatch.elapsed() << "milliseconds"; + this->disconnectPort(); + return false; + } + + if(!this->noWriteResistance) + { + // latch onto the port accessor's pulse to send the resistance signal + this->port->set_pulse([thisObject]()->void{thisObject->updateResistance();}, + trixterxdreamv1client::ResistancePulseIntervalMilliseconds); + } + + this->settingsUpdateTimerId = this->startTimer(SettingsUpdateTimerIntervalMilliseconds, Qt::VeryCoarseTimer); + if(this->settingsUpdateTimerId==0) + { + qDebug() << "Failed to start settings update timer. Too bad."; + } + + this->configureVirtualBike(); + + return true; +} + +void trixterxdreamv1bike::disconnectPort() { + if(this->port) { + qDebug() << "Disconnecting from serial port"; + delete this->port; + this->port = nullptr; + } + if(this->metricsUpdateTimerId) { + qDebug() << "Killing metricsUpdate timer"; + this->killTimer(this->metricsUpdateTimerId); + this->metricsUpdateTimerId = 0; + } + if(this->settingsUpdateTimerId) { + qDebug() << "Killing settings update timer"; + this->killTimer(this->settingsUpdateTimerId); + this->settingsUpdateTimerId = 0; + } +} + +void trixterxdreamv1bike::configureVirtualBike(){ +// ******************************************* virtual bike init ************************************* + + bool haveVirtualDevice = this->hasVirtualDevice(); + + #ifdef Q_OS_IOS + #ifndef IO_UNDER_QT + if(h) + haveVirtualDevice = true; + #endif + #endif + + if(!haveVirtualDevice){ + QSettings settings; + bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + + #ifdef Q_OS_IOS + #ifndef IO_UNDER_QT + bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); + bool ios_peloton_workaround = settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + if (ios_peloton_workaround && cadence) { + qDebug() << "ios_peloton_workaround activated!"; + h = new lockscreen(); + h->virtualbike_ios(); + } else + #endif + #endif + if (virtual_device_enabled) { + qDebug() << QStringLiteral("creating virtual bike interface..."); + + double bikeResistanceOffset = settings.value(QZSettings::bike_resistance_offset, QZSettings::default_bike_resistance_offset).toInt(); + double bikeResistanceGain = settings.value(QZSettings::bike_resistance_gain_f, QZSettings::default_bike_resistance_gain_f).toDouble(); + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain); + bike::connect(virtualBike, &virtualbike::changeInclination, this, &trixterxdreamv1bike::changeInclination); + + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); + } + } + // ******************************************************************************************************** +} + +uint16_t trixterxdreamv1bike::powerFromResistanceRequest(resistance_t requestedResistance) { + return this->calculatePower((int)this->Cadence.value(), requestedResistance); +} + +resistance_t trixterxdreamv1bike::resistanceFromPowerRequest(uint16_t power) { + + if(power<=0) + return 0; + + // round the current cadence + int32_t c = (int32_t)(this->Cadence.value() + 0.5); + + if(c==0) + return 0; // don't use resistance if there's no cadence + + c = std::min(120, std::max(c, 30)); + + auto& cadencePower = powerTable[c-30]; + auto item = std::lower_bound(cadencePower.begin(), cadencePower.end(), power) - cadencePower.begin(); + + return item; +} + +double trixterxdreamv1bike::calculatePower(int cadenceRPM, int resistance) { + if(cadenceRPM==0.0) + return 0.0; + + int32_t c = std::max(30, std::min(120, (int32_t)(cadenceRPM+0.5))); + int32_t r = std::max(0, std::min(250, resistance)); + + double result = powerTable[c-30][r]; + + return result; +} + +uint16_t trixterxdreamv1bike::calculatePowerFromInclination(double inclination, double speedMetresPerSecond) { + QSettings settings; + + double riderMass = settings.value(QZSettings::weight, QZSettings::default_weight).toFloat(); + double bikeMass = settings.value(QZSettings::bike_weight, QZSettings::default_bike_weight).toFloat(); + double totalMass = riderMass+bikeMass; + double fg = 9.8067*sin(atan(0.01*inclination))*totalMass; + + uint16_t power = (uint16_t)(fg * speedMetresPerSecond); + + qDebug() << "Inclination:" << inclination + << " Speed:" << speedMetresPerSecond << "m/s " + << " Total Mass:"<< totalMass << "kg " + << "= Power:" << power << "W "; + + return power; +} + +bool trixterxdreamv1bike::connected() { + // If this is called from the connect() method, the timer won't have called the update() method + // so go directly to the queue of states. + QMutexLocker lockerA(&this->statesMutex); + if(!this->states.empty()) + return true; + lockerA.unlock(); + + // Queue of states is empty... + QMutexLocker lockerB(&this->updateMutex); + return (this->getTime()-this->lastPacketProcessedTime) < DisconnectionTimeout; +} + + +uint32_t trixterxdreamv1bike::getTime() { + auto ms = QDateTime::currentMSecsSinceEpoch(); + return static_cast(ms); +} + +void trixterxdreamv1bike::timerEvent(QTimerEvent *event) { + int timerId = event->timerId(); + + // check the options, most frequent to least frequent + if(timerId==this->metricsUpdateTimerId) { + event->accept(); + this->update(); + } else if(timerId==this->settingsUpdateTimerId) { + event->accept(); + this->appSettings->Load(); + } +} + +void trixterxdreamv1bike::receiveBytes(const QByteArray &bytes) { + + // send the bytes to the client and return if there's no change of state + bool stateChanged = false; + queue * ups = &this->states; + + for(int i=0; iclient.ReceiveChar(bytes[i])) { + QMutexLocker locker(&this->statesMutex); + ups->push(this->client.getLastState()); + stateChanged = true; + } + } + + if(!stateChanged) + return; + + QMutexLocker locker(&this->statesMutex); + auto timeLimit = getTime() - SmoothingInterval; + while(!ups->empty() && ups->front().LastEventTime < timeLimit) + ups->pop(); + +} + +void trixterxdreamv1bike::update() { + QMutexLocker locker(&this->updateMutex); + + // get the current time + auto currentTime = getTime(); + + // Lock the states mutex and grab a copy of the queue + QMutexLocker statesLocker(&this->statesMutex); + queue ups = this->states; + statesLocker.unlock(); + + // If there are no states waiting to be processed, clear the metrics and return. + if(ups.empty()) { + qDebug() << "no states in queue"; + this->stopping = 0; + this->Speed.setValue(0); + this->brakeLevel = 0; + this->m_steeringAngle.setValue(0); + this->Cadence.setValue(0); + this->Heart.setValue(0); + return; + } + + // sweep the recent states calculating some averages over the last update interval + // steering can ba a particularly wobbly signal so smoothing is important + double steering=0, cadence=0, flywheel=0, brakeLevel=0, heartRate = 0; + int count = ups.size(); + + trixterxdreamv1client::state state{}; + + while(!ups.empty()) { + this->lastPacketProcessedTime = currentTime; + + state = ups.front(); + ups.pop(); + + constexpr double brakeScale = 125.0/(trixterxdreamv1client::MaxBrake-trixterxdreamv1client::MinBrake); + uint8_t b1 = 125 - (state.Brake1 - trixterxdreamv1client::MinBrake) * brakeScale; + uint8_t b2 = 125 - (state.Brake2 - trixterxdreamv1client::MinBrake) * brakeScale; + brakeLevel+= b1+b2; + + flywheel += state.FlywheelRPM; + cadence += state.CrankRPM; + + // Set the steering + if(!this->noSteering) { + steering += this->steeringMap[state.Steering]; + } + + if(!this->noHeartRate) { + heartRate += state.HeartRate; + } + } + + if(count>1) { + double scale = 1.0/count; + steering *= scale; + cadence *= scale; + flywheel *= scale; + brakeLevel *= scale; + heartRate *= scale; + } + + // Determine if the user is pressing the button to stop. + this->stopping = (state.Buttons & trixterxdreamv1client::buttons::Red) != 0 && flywheel>0.0 ? 1:0; + + // update the metrics + if(!this->noHeartRate) + this->Heart.setValue(heartRate); + this->Distance.setValue(state.CumulativeWheelRevolutions * this->wheelCircumference); + this->Cadence.setValue(cadence); + this->LastCrankEventTime = state.LastEventTime; + this->CrankRevs = state.CumulativeCrankRevolutions; + this->brakeLevel = brakeLevel; + constexpr double minutesPerHour = 60.0; + this->Speed.setValue(flywheel * minutesPerHour * this->wheelCircumference); + bool steeringAngleChanged = false; + if(!this->noSteering) { + double newValue = steering; + steeringAngleChanged = this->m_steeringAngle.value()!=newValue; + if(steeringAngleChanged) + this->m_steeringAngle.setValue(newValue); + } + + if(this->requestPower > -1) { + // there's been a request to change power. + bool changedMode = !this->requestIsPower; + this->requestIsPower = true; // switch to ERG mode + this->requestedResistanceInput = this->requestPower; + this->requestPower = -1; + + if(changedMode) { + qDebug() << "Changed to ERG mode detected"; + this->Inclination.setValue(0.0); // remove the inclination from the previous mode from the UI tile + } + } + + if(this->requestInclination>-100) { + // there's been a request to change inclination + bool changedMode = this->requestIsPower; + this->requestIsPower = false; // set to indoor bike simulation parameters mode + this->requestedResistanceInput = this->requestInclination; + this->requestInclination = -100; + + if(changedMode) { + qDebug() << "Changed to Indoor Bike Simulation Parameters mode"; + this->RequestedPower.setValue(0.0); // get rid of the target power from the UI tile + } + } + + if(this->requestedResistanceInput.has_value()) { + // Get the value. This value is retained between update requests because the resistance that is + // calculated from it can change due to cadence or flywheel speed. + int16_t value = this->requestedResistanceInput.value(); + + if(this->requestIsPower) { + // simulate ERG hardware - value is target power in watts + this->requestResistance = this->resistanceFromPowerRequest(value); + + qDebug() << "Power request: " + << this->requestedResistanceInput.value() + << "W with cadence " + << this->Cadence.value() + << "RPM --> setting resistance request: " + << this->requestResistance; + } else { + // simulate inclination - value is inclination percentage + double groundSpeed = flywheel / 60.0 * 1000.0 * this->wheelCircumference; + uint16_t reqPower = this->calculatePowerFromInclination(value, groundSpeed); + this->requestResistance = this->resistanceFromPowerRequest(reqPower); + + qDebug() << "Inclination request: " + << value + << "% speed:" + << groundSpeed << "m/s --> setting resistance request: " + << this->requestResistance; + } + } + + if (this->requestResistance > -1) { + this->set_resistance(requestResistance); + } + this->requestResistance = -1; + + // update the power output + this->update_metrics(true, this->watts()); + + // check if the settings have been updated and adjust accordingly + if(this->appSettings->get_version()!=this->lastAppSettingsVersion) { + + this->noHeartRate = this->noHeartService || !this->appSettings->get_heartRateEnabled(); + if(this->noHeartRate) + this->Heart.setValue(0.0); + + this->noSteering = !this->appSettings->get_steeringEnabled(); + if(this->noSteering) { + if(this->m_steeringAngle.value()!=0) { + this->m_steeringAngle.setValue(0.0); + steeringAngleChanged = true; + } + } else + QTimer::singleShot(10ms, this, &trixterxdreamv1bike::calculateSteeringMap); + + this->lastAppSettingsVersion = this->appSettings->get_version(); + } + + // set the elapsed time + this->elapsed = (currentTime - this->t0) * 0.001; + + if(steeringAngleChanged) + emit this->steeringAngleChanged(this->m_steeringAngle.value()); + + // get the current time + auto updateTime = getTime()-currentTime; + + // Check the update was quick enough. + if(updateTime>UpdateMetricsInterval/4) + qDebug() << "WARNING: Update took too long: " << updateTime << "ms"; +} + +void trixterxdreamv1bike::calculateSteeringMap() { + + trixterxdreamv1settings::steeringCalibrationInfo info = this->appSettings->get_steeringCalibration(); + + vector newMap; + + // Map the calibration values from [-info.max,+info.max] to [0, 2*info.max] + double mid = info.max, max = 2*mid; + + double l = mid+info.left; + double cl = mid+info.centerLeft; + double cr = mid+info.centerRight; + double r = mid+info.right; + + double scale = max / trixterxdreamv1client::MaxSteering; + double scaleLeft = mid / (cl-l); + double scaleRight = mid / (r-cr); + + for(int i=0; i<=trixterxdreamv1client::MaxSteering; i++) { + double mappedValue = i *scale; + + if(mappedValue>=cl && mappedValue<=cr) { + mappedValue = mid; + } else if (mappedValue<=l) { + mappedValue = 0; + } else if (mappedValue>=r) { + mappedValue = max; + } else if(mappedValueupdateMutex); + this->steeringMap=newMap; +} + +void trixterxdreamv1bike::set_resistance(resistance_t resistanceLevel) { + qDebug() << "setting resistance: " << resistanceLevel << this->noWriteResistance; + + // ignore the resistance if this option was selected + if(this->noWriteResistance) + return; + + QMutexLocker locker(&this->updateMutex); + + // Clip the incoming values + resistance_t unclipped = resistanceLevel; + if(resistanceLevel<0) resistanceLevel = 0; + if(resistanceLevel>maxResistance()) resistanceLevel = maxResistance(); + if(unclipped!=resistanceLevel) + qDebug() << "clipped resistance of " << unclipped << " to " << resistanceLevel; + + // store the resistance level as a metric for the UI + constexpr double pelotonScaleFactor = 100.0 / trixterxdreamv1client::MaxResistance; + bool resistanceChanged = false; + + if(resistanceLevel != (resistance_t)this->Resistance.value()) { + this->Resistance.setValue(resistanceLevel); + resistanceChanged = true; + } + this->m_pelotonResistance.setValue(round(pelotonScaleFactor * resistanceLevel)); + + // store the new resistance level. This might be the same as lastRequestedResistance(),Value + // but it doesn't involve a function call and a cast to get the value. + this->resistanceLevel = resistanceLevel; + + // if there's been a change of resistance, signal it. + if(resistanceChanged) + emit this->resistanceRead(resistanceLevel); + +} + +uint16_t trixterxdreamv1bike::watts() { + if(this->Cadence.value()==0) + return 0; + return this->calculatePower(this->Cadence.value(), this->Resistance.value()); +} + +void trixterxdreamv1bike::updateResistance() { + + resistance_t actualResistance = this->stopping ? (resistance_t)trixterxdreamv1client::MaxResistance : std::max((uint32_t)this->resistanceLevel,(uint32_t)trixterxdreamv1client::MaxResistance ); + + // get the time the request is made + uint32_t t = getTime(); + + this->client.SendResistance(actualResistance); + + // determine if the request was late + int32_t late = t - this->lastResistancePacketTime - trixterxdreamv1client::ResistancePulseIntervalMilliseconds; + this->lastResistancePacketTime = t; + + if(late>0) { + qDebug() << QStringLiteral("WARNING: resistance packet was sent %1ms too late").arg(late); + } +} + +trixterxdreamv1bike::~trixterxdreamv1bike() { + if(this->port) delete this->port; + if(this->appSettings) delete this->appSettings; + + // NOTE: bluetooth::restart() deletes this object, then deletes the bike object + //if(this->virtualBike) delete this->virtualBike; +} + +void trixterxdreamv1bike::set_wheelDiameter(double value) { + QMutexLocker locker(&this->updateMutex); + + // clip the value + value = std::min(MaxWheelDiameter, std::max(value, MinWheelDiameter)); + + // stored as km to avoid dividing by 1000 every time it's used + this->wheelCircumference = value * M_PI / 1000.0; +} + + +resistance_t trixterxdreamv1bike::pelotonToBikeResistance(int pelotonResistance) { + pelotonResistance = std::max(0, std::min(100, pelotonResistance)); + return round(0.01*pelotonResistance*trixterxdreamv1client::MaxResistance); +} + +trixterxdreamv1bike * trixterxdreamv1bike::tryCreate(bool noWriteResistance, bool noHeartService, bool noVirtualDevice, const QString &portName) { + +#if defined(Q_OS_IOS) || defined(Q_OS_ANDROID) + // Not supported in iOS or Android + return nullptr; +#endif + + // first check if there's a port specified + if(portName!=nullptr && !portName.isEmpty()) + { + qDebug() << "Looking for Trixter X-Dream V1 device on port: " << portName; + trixterxdreamv1bike * result = new trixterxdreamv1bike(noWriteResistance, noHeartService, noVirtualDevice); + try { + if(result->connect(portName)) { + qDebug() << "Found Trixter X-Dream V1 device on port: " << portName; + return result; + } + delete result; + } catch(...) { + qDebug() << "Error thrown looking for Trixter X-Dream V1 device on port: " << portName; + + // make absolutely sure the object is delete otherwise the serial port it opened will remain blocked. + if(result) { + qDebug() << "Deleting object that was not able to connect"; + delete result; + } + throw; + } + qDebug() << "No Trixter X-Dream V1 device found on port: " << portName; + return nullptr; + } + + // Find the available ports and return the first success + auto availablePorts = trixterxdreamv1serial::availablePorts(); + + for(int i=0; i +#include + +class trixterxdreamv1bike : public bike +{ + Q_OBJECT + +private: + /** + * @brief SettingsUpdateTimerIntervalMilliseconds The object will check for a settings update at this interval. + */ + constexpr static int SettingsUpdateTimerIntervalMilliseconds = 10000; + + /** + * @brief A queue of states read from the client. Syncronized by statesMutex. + */ + std::queue states; + + /** + * @brief Mutex for accessing the unprocessedStates queue. + */ + QMutex statesMutex; + + /** + * @brief An object that processes incoming data to CSCS, heart rate and steering data + */ + trixterxdreamv1client client; + + /** + * @brief An object that monitors a serial port to read incoming data, and to write + * resistance level requests. + */ + trixterxdreamv1serial * port = nullptr; + + /** + * @brief Indicates if the device should be sent full resistance instead of the currently requested resistance. + */ + QAtomicInteger stopping = false; + + + /** + * @brief Sum of brakes 1 and 2 each normalised to 0..125. + */ + uint8_t brakeLevel = 0; + + /** + * @brief The id for identifying the settings update timer in void timerEvent(QEVent*). + */ + int settingsUpdateTimerId = 0; + + /** + * @brief The id for identifying the timer that updates the metrics from the stored queue of states read from the client, + * in void timerEvent(QEvent*), + */ + int metricsUpdateTimerId = 0; + + /** + * @brief Suppress heart rate readings, QZ level setting. + */ + bool noHeartService; + + /** + * @brief Value from app settings combined with QZ's noHeartService value. + */ + bool noHeartRate; + + /** + * @brief Suppress virtual device. + */ + bool noVirtualDevice; + + /** + * @brief Suppress sending resistance to device. + */ + bool noWriteResistance; + + /** + * @brief Suppress steering readings + */ + bool noSteering; + + /** + * @brief The last requested resistance level, actual value sent to device. + */ + QAtomicInteger resistanceLevel = 0; + + /** + * @brief The simulated circumference of the bike's wheels, for converting + * angular velocity to a speed. Units: kilometers. + */ + double wheelCircumference; + + /** + * @brief requestIsPower Indicates if the last power request (for resistance) came + * for ERG mode (true, i.e. changePower) or via inclination (false, i.e. changeInclination). + */ + bool requestIsPower = false; + + /** + * @brief requestedResistanceInput Latest requested input for resistance. + * If requestIsPower is true, this is the target power in watts + * If requestIsPower is false, this is the inclination percentage + */ + std::optional requestedResistanceInput; + + /** + * @brief t0 The start time in milliseconds. Used to determine elapsed time. + */ + qint64 t0=0; + + /** + * @brief The last time (from getTime()) a packet was processed. + */ + uint32_t lastPacketProcessedTime=0; + + /** + * @brief The last time (from getTime()) a resistance packet was sent. + */ + uint32_t lastResistancePacketTime = 0; + + /** + * @brief The application settings. + */ + trixterxdreamv1settings * appSettings = nullptr; + + /** + * @brief The last app settings version that was used to configure the object. + */ + uint32_t lastAppSettingsVersion=0; + + /** + * @brief Stores the mapping between incoming steering values and the steering angles expected by the application. + */ + std::vector steeringMap; + + /** + * @brief Used to synchronise updates to this object's members. + */ + QRecursiveMutex updateMutex; + + /** + * @brief Processes the state queue + */ + void update(); + + /** + * @brief Gets a measure of time in milliseconds. + */ + static uint32_t getTime(); + + /** + * @brief Called by the data source (serial port) when a new block of data arrives. + * Stores the data and triggers an update. + * @param bytes + */ + void receiveBytes(const QByteArray &bytes); + + /** + * @brief Called by the resistanceTimer to send the resistance request to the + * device. + */ + void updateResistance(); + + /** + * @brief Calculates the mapping between steering values coming from the device, and + * the steering angles sent to the application. Uses the values in the appSettings field. + */ + void calculateSteeringMap(); + + /** + * @brief Set up the bridge to the client application. + */ + void configureVirtualBike(); + + /** + * @brief Calculate power from cadence RPM and resistance. + * @param cadenceRPM + * @param resistance Bike resistance on full, not percentage scale. + */ + double calculatePower(int cadenceRPM, int resistance); + + /** + * @brief Calculate power from the specified inclination and speed. Uses rider and bike weight from settings. + * @param inclination Percentage inclination. + * @param speedMetersPerSecond Bike speed in meters per second. + */ + uint16_t calculatePowerFromInclination(double inclination, double speedMetersPerSecond); + + /** + * @brief Called to set the resistance level sent to the device. + * @param resistanceLevel The resistance level to request (0..maximumResistance()) + */ + void set_resistance(resistance_t resistanceLevel); + + /** + * @brief watts Calculate the power output using the current Cadence. Unit: watts + * @return + */ + uint16_t watts() override;; + +protected: + + /** + * @brief Processes timer events, e.g. for resistance. + * @param event + */ + void timerEvent(QTimerEvent *event) override; + + /** + * @brief Disconnect the serial port and resistance timer. + */ + void disconnectPort(); + +public: + /** + * @brief The maximum supported wheel diameter. Unit: meters + */ + constexpr static double MaxWheelDiameter = 2.0; + + /** + * @brief The minimum supported wheel diameter. Unit: meters + */ + constexpr static double MinWheelDiameter = 0.1; + + /** + * @brief The default wheel diameter. Unit: meters + */ + constexpr static double DefaultWheelDiameter = 26*0.0254; + + /** + * @brief The number of milliseconds of no packets processed required before + * this object will be considered disconnected from the device. + */ + constexpr static int32_t DisconnectionTimeout = 50; + + /** + * @brief The number of milliseconds to collect packets from the device before updating the metrics. + */ + constexpr static int32_t UpdateMetricsInterval = 100; + + /** + * @brief The number of milliseconds to smooth samples over. + */ + constexpr static int32_t SmoothingInterval = 500; + + /** + * @brief Constructor + * @param noWriteResistance Option to avoid sending resistance to the device. + * @param noHeartService Option to avoid using the heart rate reading. + * @param noVirtualDevice Option to avoid using a virtual device. + */ + trixterxdreamv1bike(bool noWriteResistance, bool noHeartService, bool noVirtualDevice); + + ~trixterxdreamv1bike(); + + /** + * @brief Calculate the power for the requested resistance at the current cadence. + * @param requestedResistance The resistance from 0 to maximumResistance(). + */ + uint16_t powerFromResistanceRequest(resistance_t requestedResistance) override; + + /** + * @brief Calculate the resistance required to produce the requested power at the current cadence. + * @param power The power in watts. + */ + resistance_t resistanceFromPowerRequest(uint16_t power) override; + + /** + * @brief Attempt to connect to the specified port. + * @param portName The name of the serial port to connect to. + */ + bool connect(QString portName); + + /** + * @brief Indicates if a valid packet was received from the device within the DisconnectionTimeout. + */ + bool connected() override; + + /** + * @brief Set the simulated wheel diameter to be used for converting angular velocity to speed. Units: meters + * @param value + */ + void set_wheelDiameter(double value); + + /** + * @brief Gets the settings object for this device type. + */ + const trixterxdreamv1settings * get_appSettings() { return this->appSettings; } + + /** + * @brief The maximum resistance supported. + */ + resistance_t maxResistance() override { return trixterxdreamv1client::MaxResistance; } + + /** + * @brief Map Peloton 0 to 100% resistance to the bike's range. + * @param pelotonResistance The Peloton resistance. Range: 0 to 100. + * @return The Trixter X-Dream V1 bike resistance. Range 0..250 if !this->useResistancePercentage. + */ + resistance_t pelotonToBikeResistance(int pelotonResistance) override; + + /** + * @brief Attempt to create an object to interact with an existing Trixter X-Dream V1 bike on a specific serial port, + * or if the port is unspecified, any serial port. + * @param noWriteResistance Option to avoid sending resistance to the device. + * @param noHeartService Option to avoid using the heart rate reading. + * @param noVirtualDevice Option to avoid using a virtual device. + * @param portName (Optional) The specific port to search. + * @return nullptr if no device is found, an object if a device is found and connected. + */ + static trixterxdreamv1bike * tryCreate(bool noWriteResistance, bool noHeartService, bool noVirtualDevice, const QString& portName = nullptr); + + /** + * @brief Attempt to create an object to interact with an existing Trixter X-Dream V1 bike on a specific serial port, + * or if the port is unspecified, any serial port. + * @param port (Optional) The specific port to search. + * @return + */ + static trixterxdreamv1bike * tryCreate(const QString& portName = nullptr); + +}; diff --git a/src/devices/trixterxdreamv1bike/trixterxdreamv1client.cpp b/src/devices/trixterxdreamv1bike/trixterxdreamv1client.cpp new file mode 100644 index 000000000..664b11796 --- /dev/null +++ b/src/devices/trixterxdreamv1bike/trixterxdreamv1client.cpp @@ -0,0 +1,210 @@ +//#include "pch.h" +#include "trixterxdreamv1client.h" + +#include +#include + +using namespace std; + +trixterxdreamv1client::trixterxdreamv1client() { + this->ConfigureResistanceMessages(); +} + +void trixterxdreamv1client::ResetBuffer() { + // for the case of an invalid packet, if this was smart, it would store all the input + // and backtrack to the first header bytes after the beginning. + + this->inputBuffer.clear(); + this->byteBuffer.clear(); +} + +void trixterxdreamv1client::set_GetTime(std::function get_time_ms) { + this->get_time_ms = get_time_ms; +} + +trixterxdreamv1client::PacketState trixterxdreamv1client::ProcessChar(char c) { + /* Packet content + * 6A 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + * (00) Header ---------------+ | | | | | | | | | | | | | | | + * (01) Steering ----------------+ | | | | | | | | | | | | | | + * (02) Unknown --------------------+ | | | | | | | | | | | | | + * (03) Crank position ----------------+ | | | | | | | | | | | | + * (04) Right brake ----------------------+ | | | | | | | | | | | + * (05) Left brake --------------------------+ | | | | | | | | | | + * (06) Unknown --------------------------------+ | | | | | | | | | + * (07) Unknown -----------------------------------+ | | | | | | | | + * (08) Button flags ---------------------------------+ | | | | | | | + * (09) Button flags ------------------------------------+ | | | | | | + * (0A) Crank revolution time (high byte) ------------------+ | | | | | + * (0B) Crank revolution time (low byte) ----------------------+ | | | | + * (0C) Flywheel Revolution Time (high byte) ---------------------+ | | | + * (0D) Flywheel Revolution Time (low byte) -------------------------+ | | + * (0E) Heart rate (BPM) -----------------------------------------------+ | + * (0F) XOR of 00..0E------------------------------------------------------+ + */ + + constexpr int headerLength = 2; + constexpr int packetLength = 16; + constexpr uint8_t header[] = { 0x6, 0xA }; + + uint8_t b; + + if (isdigit(c)) { + b = c - '0'; + } + else if (c >= 'a' && c <= 'f') { + b = c - 'a' + '\xA'; + } + else { + this->ResetBuffer(); + return Invalid; + } + + // make sure the first 2 bytes are the header '6','a' + if (this->byteBuffer.empty() && this->inputBuffer.size() < headerLength && b != header[this->inputBuffer.size()]) { + this->inputBuffer.clear(); + return None; + } + + if (this->inputBuffer.size() == 1) { + this->byteBuffer.push_back((this->inputBuffer.back() << 4) + b); + this->inputBuffer.clear(); + } + else + this->inputBuffer.push_back(b); + + if (this->byteBuffer.size() == packetLength) { + // Validate the packet - the last byte should the XOR of the 1st 15. + b = 0; + for (int i = 0, limit = packetLength - 1; i < limit; i++) + b ^= this->byteBuffer[i]; + + if (b != this->byteBuffer.back()) { + // invalid checksum + this->ResetBuffer(); + return Invalid; + } + + return Complete; + } + + return Incomplete; +} + +void trixterxdreamv1client::ConfigureResistanceMessages() { + resistanceMessages = new uint8_t * [251]; + + for (uint8_t level = 0; level <= 250; level++) { + uint8_t* message = new uint8_t[6]; + resistanceMessages[level] = message; + + message[5] = message[0] = 0x6a; + message[5] ^= message[1] = level; + message[5] ^= message[2] = (level + 60) % 255; + message[5] ^= message[3] = (level + 90) % 255; + message[5] ^= message[4] = (level + 120) % 255; + } +} + +bool trixterxdreamv1client::ReceiveChar(char c) { + if (this->ProcessChar(c) != Complete) + return false; + + lastPacket.Buttons = (static_cast(this->byteBuffer[0x8]) << 8) + this->byteBuffer[0x9]; + lastPacket.CrankPosition = this->byteBuffer[0x3]; + lastPacket.Brake1 = this->byteBuffer[0x4]; + lastPacket.Brake2 = this->byteBuffer[0x5]; + lastPacket.Steering = this->byteBuffer[0x1]; + lastPacket.Flywheel = (static_cast(this->byteBuffer[0xC]) << 8) + this->byteBuffer[0xD]; + lastPacket.Crank = (static_cast(this->byteBuffer[0xA]) << 8) + this->byteBuffer[0xB]; + lastPacket.HeartRate = byteBuffer[0xE]; + + // got the data, now clear the buffer + this->ResetBuffer(); + + constexpr double flywheelToRevolutionsPerMinute = 576000.0; + constexpr double crankToRevolutionsPerMinute = 1.0 / 6e-6; + constexpr double minutesToMilliseconds = 60.0 * 1000.0; + + double flywheelRevsPerMinute = 0, crankRevsPerMinute = 0; + + if (lastPacket.Flywheel < 65534) { + flywheelRevsPerMinute = flywheelToRevolutionsPerMinute / max(static_cast(1), lastPacket.Flywheel); + } + + if (lastPacket.Crank > 0 && lastPacket.Crank < 65534) { + crankRevsPerMinute = crankToRevolutionsPerMinute / max(static_cast(1), lastPacket.Crank); + } + + const uint32_t t = this->get_time_ms(); + const uint32_t lt = this->lastT ? this->lastT : t; + + this->lastT = t; + + if(tReset(); + return false; + } + + const uint32_t dt = t - lt; + + if (dt > 0) + { + // update the internal, precise state + double dt_minutes = dt / minutesToMilliseconds; + + this->flywheelRevolutions += dt_minutes * flywheelRevsPerMinute; + this->crankRevolutions += dt_minutes * crankRevsPerMinute; + } + + state newState{}; + newState.LastEventTime = t; + newState.Steering = lastPacket.Steering; + newState.HeartRate = lastPacket.HeartRate; + newState.CumulativeCrankRevolutions = static_cast(round(crankRevolutions)); + newState.CumulativeWheelRevolutions = static_cast(round(flywheelRevolutions)); + newState.CrankRPM = static_cast(crankRevsPerMinute); + newState.FlywheelRPM = static_cast(flywheelRevsPerMinute); + newState.Buttons = (buttons)(0xFFFF-lastPacket.Buttons); + newState.Brake1 = lastPacket.Brake1; + newState.Brake2 = lastPacket.Brake2; + + this->stateMutex.lock(); + this->lastState = newState; + this->stateMutex.unlock(); + + return true; +} + +trixterxdreamv1client::state trixterxdreamv1client::getLastState() { + this->stateMutex.lock(); + const state result = this->lastState; + this->stateMutex.unlock(); + return result; +} + +void trixterxdreamv1client::SendResistance(uint8_t level) { + + // to maintain the resistance, this needs to be resent about every 10ms + if (level != 0 && this->write_bytes) + { + this->writeMutex.lock(); + try { this->write_bytes(this->resistanceMessages[min(MaxResistance, level)], 6); } + catch (...) + { + this->writeMutex.unlock(); + throw; + } + this->writeMutex.unlock(); + } +} + +void trixterxdreamv1client::Reset() { + this->lastT = this->get_time_ms(); + this->flywheelRevolutions = 0.0; + this->crankRevolutions = 0.0; +} diff --git a/src/devices/trixterxdreamv1bike/trixterxdreamv1client.h b/src/devices/trixterxdreamv1bike/trixterxdreamv1client.h new file mode 100644 index 000000000..d8f470f01 --- /dev/null +++ b/src/devices/trixterxdreamv1bike/trixterxdreamv1client.h @@ -0,0 +1,215 @@ +#pragma once +#include +#include +#include +#include + +/** + * @brief Basic functionality to interpret character data read from a Trixter X-Dream V1 bike via a serial port. + * Intended to be free from any non-standard C++ library code. + * Requires a time source callback (set_getTime) to timestamp packets and optionally + * a callback to write resistance packets to the serial port. + */ +class trixterxdreamv1client { +public: + + /** + * @brief Values for the button state. + */ + enum buttons : uint16_t + { + LeftArrow = 4096, + RightArrow = 16384, + UpArrow = 256, + DownArrow = 1024, + + Blue = 8192, + Red = 512, + Green = 2048, + + Seated = 8, + + FrontGearUp = 32768, + FrontGearDown = 128, + + BackGearUp = 32, + BackGearDown = 64 + }; + + /** + * @brief Device state data: CSCS, heartrate, steering, buttons. + */ + struct state { + /** + * @brief Buttons The state of the buttons. + */ + buttons Buttons; + + /** + * @brief Steering Steering value, from 0 (left) to 250 (right) + */ + uint8_t Steering; + + /** + * @brief HeartRate Heart rate in beats per minute. + */ + uint8_t HeartRate; + + /** + * @brief CumulativeWheelRevolutions The number of flywheel revolutions since the last reset event. + */ + uint32_t CumulativeWheelRevolutions; + + /** + * @brief CumulativeCrankRevolutions The number of crank revolutions since the last reset event. + */ + uint16_t CumulativeCrankRevolutions; + + /** + * @brief LastEventTime The time of the last event. Unit: milliseconds + */ + uint32_t LastEventTime; + + /** + * @brief FlywheelRPM Flywheel speed. Units: revolutions per minute + */ + uint16_t FlywheelRPM; + + /** + * @brief CrankRPM Crank speed. Units: revolutions per minute + */ + uint16_t CrankRPM; + + /** + * @brief CrankPosition Position of the crank. Range: 1 to 60. + */ + uint8_t CrankPosition; + + /** + * @brief Brake 1. Position of brake 1. Range: 135 (on) to 250 (off) + */ + uint8_t Brake1; + + + /** + * @brief Brake 2. Position of brake 1. Range: 135 (on) to 250 (off) + */ + uint8_t Brake2; + }; + +private: + uint8_t** resistanceMessages{}; + + enum PacketState { None, Incomplete, Invalid, Complete }; + + /** + * @brief Raw data selected from the incoming packet. + */ + struct Packet { + uint8_t Steering, Brake1, Brake2, HeartRate, CrankPosition; + uint16_t Flywheel, Crank, Buttons; + }; + + std::function get_time_ms=nullptr; + std::function write_bytes=nullptr; + std::mutex stateMutex, writeMutex; + uint32_t lastT = 0; + double flywheelRevolutions{}, crankRevolutions{}; + Packet lastPacket{}; + std::vector inputBuffer; + std::vector byteBuffer; + state lastState; + + /** + * @brief Clear the input buffer. + */ + void ResetBuffer(); + + /** + * @brief Add the character to the input buffer and process to eventually read the next packet. + * @param c A text character '0'..'9' or 'a'..'f' + */ + PacketState ProcessChar(char c); + + void ConfigureResistanceMessages(); + +public: + /** + * @brief GearRatio The physical gear ratio between the flywheel:crank. + */ + constexpr static uint8_t GearRatio = 5; + + /** + * @brief MaxResistance The maximum resistance value supported by the device. + */ + constexpr static uint8_t MaxResistance = 250; + + /** + * @brief MaxSteering The maximum steering value supported by the device. + */ + constexpr static uint8_t MaxSteering = 255; + + + /** + * @brief MaxBrake The maximum brake value, which indicates fully off. + */ + constexpr static uint8_t MaxBrake = 250; + + /** + * @brief MinBrake The minimum brake value, which indicates fully on. + */ + constexpr static uint8_t MinBrake = 135; + + /** + * @brief MinCrankPosition The minimum CrankPosition value. + */ + constexpr static uint8_t MinCrankPosition = 1; + + /** + * @brief MinCrankPosition The maximum CrankPosition value. + */ + constexpr static uint8_t MaxCrankPosition = 60; + + /** + * @brief The time interval between sending resistance requests to the device. + */ + constexpr static uint8_t ResistancePulseIntervalMilliseconds = 10; + + trixterxdreamv1client(); + + /** + * @brief Receives and processes a character of input from the device. + * @param c Should be '0' to '9' or 'a' to 'f' (lower case) + * @return true if a packet was completed and the state updated, otherwise false. + */ + bool ReceiveChar(char c); + + /** + * @brief set_WriteBytes Sets the function used to write bytes to the serial port. + * @param write_bytes The function that writes bytes to the serial port. + */ + void set_WriteBytes(std::function write_bytes) { this->write_bytes = write_bytes; } + + /** + * @brief set_GetTime Sets the function to get the time in milliseconds since + * a starting point understood by the client. + * @param get_time_ms A function to get the time. + */ + void set_GetTime(std::function get_time_ms); + + /** + * @brief Gets the state of the device as it was last read. This consists of CSCS data, steering and heartbeat. + */ + state getLastState(); + + /** + * @brief Reset the Cycle Speed and Cadence information. + */ + void Reset(); + + /** + * @brief Sends 1 packet indicating a specific resistance level to the device. Needs to be sent at the rate specified by ResistancePulseIntervalMilliseconds. + * @param level 0 to 250. + */ + void SendResistance(uint8_t level); +}; diff --git a/src/devices/trixterxdreamv1bike/trixterxdreamv1serial.cpp b/src/devices/trixterxdreamv1bike/trixterxdreamv1serial.cpp new file mode 100644 index 000000000..9aa400273 --- /dev/null +++ b/src/devices/trixterxdreamv1bike/trixterxdreamv1serial.cpp @@ -0,0 +1,180 @@ +#include "trixterxdreamv1serial.h" + +#include +#include +#include +#include +#include + +#include "serialdatasource.h" + +#if !defined(Q_OS_IOS) && !defined(Q_OS_ANDROID) +#include "qserialdatasource.h" +#endif + +std::function trixterxdreamv1serial::serialDataSourceFactory = +#if !defined(Q_OS_IOS) && !defined(Q_OS_ANDROID) + [](QObject * parent) { return new qserialdatasource(parent); }; +#else + nullptr; +#endif + +trixterxdreamv1serial::trixterxdreamv1serial(QObject * parent) : QThread(parent){} + +trixterxdreamv1serial::~trixterxdreamv1serial() { + this->quitPending = true; + this->wait(); +} + +QStringList trixterxdreamv1serial::availablePorts() { + QStringList result; + + if(serialDataSourceFactory==nullptr) + return QStringList(); + + auto serialDataSource = std::unique_ptr(serialDataSourceFactory(nullptr)); + + return serialDataSource->get_availablePorts(); +} + +void trixterxdreamv1serial::receive(const QByteArray &bytes) { + if(this->receiveBytes) + this->receiveBytes(bytes); +} + +bool trixterxdreamv1serial::open(const QString &portName) { + + QMutexLocker locker(&this->mutex); + + if(this->isRunning()) + { + qDebug() << "Port is already being monitored."; + return false; + } + + this->portName = portName; + + if (!isRunning()) { + this->openAttemptsPending=1; + start(); + } + + locker.unlock(); + + while(this->openAttemptsPending==1){ + QThread::msleep(10); + } + + locker.relock(); + return this->isRunning(); +} + +void trixterxdreamv1serial::write(const QByteArray& buffer) { + QMutexLocker locker(&this->writeBufferMutex); + + this->writeBuffer = buffer; + this->writePending = 1; +} + +void trixterxdreamv1serial::set_pulse(std::function function, uint32_t pulseIntervalMilliseconds) { + this->pulse = function; + this->pulseIntervalMilliseconds = pulseIntervalMilliseconds; +} + +void trixterxdreamv1serial::set_getTime(std::function get_time_ms) { + this->getTime = get_time_ms; +} + +void trixterxdreamv1serial::run() { + + if(serialDataSourceFactory==nullptr) + throw "No serial data source factory configured."; + + auto serial = std::unique_ptr(serialDataSourceFactory(nullptr)); + + bool openResult = false; + + try { + openResult = serial->open(this->portName); + this->openAttemptsPending = 0; + } catch(...) { + this->openAttemptsPending = 0; + throw; + } + + if (!openResult) { + qDebug() << QStringLiteral("Can't open %1, error code %2").arg(this->portName).arg(serial->error()); + return; + } + + qDebug() << "Serial port " << this->portName << " opened with read buffer size=" << serial->readBufferSize(); + + // turn on log timings for debugging, e.g. to ensure that the outgoing data + // is being written. + bool logTimings = false; + + uint32_t pulseDue = 0; + uint32_t lastWrite = 0; + + QByteArray requestData; + requestData.reserve(4096); + + while (!this->quitPending) { + + if(this->pulse) { + // See if the timer is due + auto t0 = this->getTime(); + if(t0 >= pulseDue) { + pulseDue = t0 + trixterxdreamv1serial::pulseIntervalMilliseconds; + this->pulse(); + + if(logTimings) { + auto dt = this->getTime() - t0; + if(dt > pulseTolerance) { + qDebug() << QStringLiteral("WARNING: pulse function took %1ms exceeding tolerance of %2ms") + .arg(dt).arg(trixterxdreamv1serial::pulseTolerance); + } + } + } + } + + if(this->writePending) { + QMutexLocker locker{&this->writeBufferMutex}; + uint32_t t = this->getTime(); + qint64 bytes = 0; + try { + this->writePending = 0; + bytes = serial->write(this->writeBuffer); + serial->flush(); + } catch(std::exception const& e) { + qDebug() << "Exception thrown writing to the serial data source : " << e.what(); + throw; + } catch(...) { + qDebug() << "Error thrown writing to the serial data source"; + throw; + } + + if(logTimings) { + uint32_t writeTime = this->getTime() - t; + uint32_t dt = t - lastWrite; + lastWrite = t; + qDebug() << QStringLiteral("Wrote %1 bytes in %2ms after %3ms").arg(bytes).arg(writeTime).arg(dt); + } + } + + // try to read some bytes, but only block for 1ms because a write attempt could come in. + if (!this->quitPending && serial->waitForReadyRead()) + requestData += serial->readAll(); + + if(requestData.length()>0) { + // Send the bytes to the client code + // Unlike the QSerialPort example that can be found online, this + // is NOT emitting a signal. This is avoid problems with slots, threads and timers, + // which seem to require an advanced course to get working together! + this->receive(requestData); + requestData.clear(); + } + } + + serial->close(); +} diff --git a/src/devices/trixterxdreamv1bike/trixterxdreamv1serial.h b/src/devices/trixterxdreamv1bike/trixterxdreamv1serial.h new file mode 100644 index 000000000..d08e138a0 --- /dev/null +++ b/src/devices/trixterxdreamv1bike/trixterxdreamv1serial.h @@ -0,0 +1,92 @@ +#ifndef TRIXTERXDREAMSERIAL_H +#define TRIXTERXDREAMSERIAL_H + +#include +#include +#include +#include + +/** + * @brief A basic serial port monitoring thread. + * Avoids using signals to prevent complications with objects, threads and timers. + */ +class trixterxdreamv1serial : public QThread { + Q_OBJECT + +public: + static class std::function serialDataSourceFactory; + + explicit trixterxdreamv1serial(QObject *parent = nullptr); + ~trixterxdreamv1serial(); + + /** + * @brief Opens the port. + * @param portName The name of the serial port. + * @returns True if the port was opened, false if the port wasn't opened, or was already open. + */ + bool open(const QString &portName); + + /** + * @brief Writes the array of bytes to the serial port + * @param buffer The bytes to send. + */ + void write(const QByteArray& buffer); + + /** + * @brief Set a function pointer to receive bytes. This is an alternative + * to sublcassing and overrding the virtual receive function. + * @param value + */ + void set_receiveBytes(std::function value) { this->receiveBytes = value; } + + /** + * @brief Sets an optional pulse function - while this object's thread is running, the pulse + * function is called repeatedly with a period defined by the pulse interval. The pulse function + * should take less than 1ms. + * @param function The pulse function. + * @param pulseIntervalMilliseconds + */ + void set_pulse(std::function function, uint32_t pulseIntervalMilliseconds); + + /** + * @brief Sets the function to get the time in milliseconds since + * a starting point understood by the client. + * @param get_time_ms A function to get the time. + */ + void set_getTime(std::function get_time_ms); + + /** + * @brief availablePorts Returns a list of port names for the serial ports found in the system + * that could host the bike. + */ + static QStringList availablePorts(); + +protected: + /** + * @brief receive Override this to process received data. + * @param bytes + */ + virtual void receive(const QByteArray &bytes); + + private: + void run() override; + + /** + * @brief The number of milliseconds the pulse function should execute in. + */ + const uint32_t pulseTolerance = 1; + + QMutex writeBufferMutex; + QByteArray writeBuffer; + QAtomicInt writePending {0}; + QString portName; + QMutex mutex; + QAtomicInt openAttemptsPending{0}; + QAtomicInt quitPending{0}; + std::function receiveBytes=nullptr; + std::function getTime=nullptr; + std::function pulse=nullptr; + uint32_t pulseIntervalMilliseconds = 10; +}; + +#endif // TRIXTERXDREAMSERIAL_H diff --git a/src/devices/trixterxdreamv1bike/trixterxdreamv1settings.cpp b/src/devices/trixterxdreamv1bike/trixterxdreamv1settings.cpp new file mode 100644 index 000000000..fdcdb3cd4 --- /dev/null +++ b/src/devices/trixterxdreamv1bike/trixterxdreamv1settings.cpp @@ -0,0 +1,133 @@ +#include "trixterxdreamv1settings.h" + +#include "qzsettings.h" + +const QString trixterxdreamv1settings::keys::Enabled = QZSettings::trixter_xdream_v1_bike_enabled; +const QString trixterxdreamv1settings::keys::HeartRateEnabled = QZSettings::trixter_xdream_v1_bike_heartrate_enabled; +const QString trixterxdreamv1settings::keys::SteeringEnabled = QZSettings::trixter_xdream_v1_bike_steering_enabled; +const QString trixterxdreamv1settings::keys::SteeringCalibrationLeft = QZSettings::trixter_xdream_v1_bike_steering_l; +const QString trixterxdreamv1settings::keys::SteeringCalibrationCenterLeft =QZSettings::trixter_xdream_v1_bike_steering_cl; +const QString trixterxdreamv1settings::keys::SteeringCalibrationCenterRight = QZSettings::trixter_xdream_v1_bike_steering_cr; +const QString trixterxdreamv1settings::keys::SteeringCalibrationRight = QZSettings::trixter_xdream_v1_bike_steering_r; +const QString trixterxdreamv1settings::keys::SteeringCalibrationMAX = QZSettings::trixter_xdream_v1_bike_steering_max; +const QString trixterxdreamv1settings::keys::ConnectionTimeoutMilliseconds = QZSettings::trixter_xdream_v1_bike_connection_timeout_ms; + + +template +T trixterxdreamv1settings::updateField(T& member, const T newValue) { + QMutexLocker locker(&this->mutex); + if(member!=newValue) { + member = newValue; + this->version++; + } + return newValue; +} + + +uint32_t trixterxdreamv1settings::get_version() { + QMutexLocker locker(&this->mutex); + return this->version; +} + +bool trixterxdreamv1settings::get_enabled() { + QMutexLocker locker(&this->mutex); + return this->enabled; +} + +bool trixterxdreamv1settings::set_enabled(bool value) { + return this->updateField(this->enabled, value); +} + +bool trixterxdreamv1settings::get_heartRateEnabled(){ + QMutexLocker locker(&this->mutex); + return this->heartRateEnabled; +} + +bool trixterxdreamv1settings::set_heartRateEnabled(bool value) { + return this->updateField(this->heartRateEnabled, value); +} + +bool trixterxdreamv1settings::get_steeringEnabled() { + QMutexLocker locker(&this->mutex); + return this->steeringEnabled; +} + +bool trixterxdreamv1settings::set_steeringEnabled(bool value) { + return this->updateField(this->steeringEnabled, value); +} + +trixterxdreamv1settings::steeringCalibrationInfo trixterxdreamv1settings::get_steeringCalibration() { + QMutexLocker locker(&this->mutex); + return this->steeringCalibration; +} + +void trixterxdreamv1settings::set_steeringCalibration(const trixterxdreamv1settings::steeringCalibrationInfo value) { + if(!value.isValid()) + throw "Invalid argument."; + + QMutexLocker locker(&this->mutex); + if(!(this->steeringCalibration==value)) { + this->steeringCalibration = value; + this->version++; + } +} + +uint16_t trixterxdreamv1settings::get_connectionTimeoutMilliseconds() { + QMutexLocker locker(&this->mutex); + return this->DefaultConnectionTimeoutMilliseconds; +} + +void trixterxdreamv1settings::set_connectionTimeoutMilliseconds(uint16_t value) { + value = this->clip(MinConnectionTimeoutMilliseconds, MaxConnectionTimeoutMilliseconds, value); + this->updateField(this->connectionTimeoutMilliseconds, value); +} + +trixterxdreamv1settings::trixterxdreamv1settings() { + QSettings defaultSettings; + this->Load(defaultSettings); + this->version = 1; +} + +trixterxdreamv1settings::trixterxdreamv1settings(const QSettings &settings) { + this->Load(settings); + this->version = 1; +} + +void trixterxdreamv1settings::Load() { + QSettings settings; + this->Load(settings); +} + +void trixterxdreamv1settings::Load(const QSettings &settings) { + QMutexLocker locker(&this->mutex); + this->set_enabled(settings.value(keys::Enabled, DefaultEnabled).toBool()); + this->set_heartRateEnabled(settings.value(keys::HeartRateEnabled, DefaultHeartRateEnabled).toBool()); + this->set_steeringEnabled(settings.value(keys::SteeringEnabled, DefaultSteeringEnabled).toBool()); + this->set_connectionTimeoutMilliseconds(settings.value(keys::ConnectionTimeoutMilliseconds, DefaultConnectionTimeoutMilliseconds).toUInt()); + + int32_t l = settings.value(keys::SteeringCalibrationLeft, DefaultSteeringCalibrationL).toInt(); + int32_t lc = settings.value(keys::SteeringCalibrationCenterLeft, DefaultSteeringCalibrationCL).toInt(); + int32_t lr = settings.value(keys::SteeringCalibrationCenterRight, DefaultSteeringCalibrationCR).toInt(); + int32_t r = settings.value(keys::SteeringCalibrationRight, DefaultSteeringCalibrationR).toInt(); + + int32_t xx = settings.value(keys::SteeringCalibrationCenterLeft, 1).toInt(); + + steeringCalibrationInfo sc(l,lc,lr, r); + this->set_steeringCalibration(sc); +} + +/* +void trixterxdreamv1bikesettings::Save() { + QSettings settings; + this->Save(settings); +} + +void trixterxdreamv1bikesettings::Save(const QSettings &settings) { + QMutexLocker locker(&this->mutex); + settings.value(keys::Enabled).setValue(this->enabled); + settings.value(keys::HeartRateEnabled).setValue(this->heartRateEnabled); + settings.value(keys::SteeringEnabled).setValue(this->steeringEnabled); + +} + +*/ diff --git a/src/devices/trixterxdreamv1bike/trixterxdreamv1settings.h b/src/devices/trixterxdreamv1bike/trixterxdreamv1settings.h new file mode 100644 index 000000000..592e273aa --- /dev/null +++ b/src/devices/trixterxdreamv1bike/trixterxdreamv1settings.h @@ -0,0 +1,244 @@ +#ifndef TRIXTERXDREAMV1SETTINGS_H +#define TRIXTERXDREAMV1SETTINGS_H + + +#include +#include "qmutex.h" +#include "qsettings.h" +#include "qzsettings.h" + + +/** + * @brief The trixterxdreamv1bikesettings class encapsulates the application settings for the Trixter X-Dream V1 Bike. + * Field accessors restrict values to defined limits. + */ +class trixterxdreamv1settings { +public: + // these should match the corresponding values in settings.qml + // - the default values where the properties are defined + constexpr static int8_t MaxSteeringAngle = 45; + constexpr static uint16_t MinConnectionTimeoutMilliseconds = 20; + constexpr static uint16_t MaxConnectionTimeoutMilliseconds = 10000; + constexpr static bool DefaultEnabled =true; + constexpr static bool DefaultSteeringEnabled =true; + constexpr static bool DefaultHeartRateEnabled =true; + constexpr static int8_t DefaultSteeringCalibrationL = -MaxSteeringAngle; + constexpr static int8_t DefaultSteeringCalibrationR = MaxSteeringAngle; + constexpr static int8_t DefaultSteeringCalibrationCL = -2; + constexpr static int8_t DefaultSteeringCalibrationCR = 2; + constexpr static uint16_t DefaultConnectionTimeoutMilliseconds = 500; + + /** + * @brief Defines QSettings keys relating to the Trixter X-Dream V1 bike. + */ + class keys { + public: + /** + * @brief Enabled QSettings key to specify if the Trixter X-Dream V1 Bike is enabled in the application. + */ + const static QString Enabled; + const static QString HeartRateEnabled; + const static QString SteeringEnabled; + const static QString SteeringCalibrationLeft; + const static QString SteeringCalibrationCenterLeft; + const static QString SteeringCalibrationCenterRight; + const static QString SteeringCalibrationRight; + const static QString SteeringCalibrationMAX; + const static QString ConnectionTimeoutMilliseconds; + }; + + struct steeringCalibrationInfo { + + public: + + /** + * @brief left The uncalibrated left turning angle that will be mapped to hard left. + */ + int8_t left; + + /** + * @brief centerLeft The lower uncalibrated left turning angle that will be mapped to 0. + */ + int8_t centerLeft; + + /** + * @brief centerRight The higest uncalibrated right turning angle that will be mapped to 0. + */ + int8_t centerRight; + + /** + * @brief right The uncalibrated right turning angle that will be mapped to hard right. + */ + int8_t right; + + /** + * @brief max The maximum turning angle. + */ + static const int8_t max = MaxSteeringAngle; + + /** + * @brief isValid Validates the record. + * @return True if the values are valid, false otherwise. + */ + bool isValid() const { + return left>=-max && leftisValid()) + throw "Arguments are out of range or out of order."; + } + + friend bool operator==(const steeringCalibrationInfo& lhs, const steeringCalibrationInfo& rhs) + { + return rhs.left==lhs.left && + rhs.right==lhs.right && + rhs.centerLeft==lhs.centerLeft && + rhs.centerRight==lhs.centerRight; + + } + }; + +private: + // mutex for thread syncing, may attempt double lock when loading from QSettings, so using recursive mutex + QRecursiveMutex mutex; + bool enabled=DefaultEnabled; + bool steeringEnabled = DefaultSteeringEnabled; + bool heartRateEnabled = DefaultHeartRateEnabled; + uint16_t connectionTimeoutMilliseconds = DefaultConnectionTimeoutMilliseconds; + + steeringCalibrationInfo steeringCalibration; + + uint32_t version=0; + + /** + * @brief clip Clips the value to be within the specified minimum and maximum. + * @param minimum The minimum value. + * @param maximum The maximum value. + * @param value The value to clip. + */ + template + static T clip(const T minimum, const T maximum, const T value) { return std::max(minimum, std::min(maximum, value)); } + + /** + * @brief updateField Updates a field and increments the version if the value has changed. + * @param member The member to update. + * @param newValue The new value. + * @return The value set. + */ + template + T updateField(T& member, const T newValue); + +public: + + /** + * @brief get_version Incremented if the values are modified. + */ + uint32_t get_version(); + + /** + * @brief get_enabled Indicates if the device is enabled, i.e. should be searched for. + */ + bool get_enabled(); + + /** + * @brief set_enabled Sets whether or not the type of device is enabled in the application. + * @param value The value to set. + * @return The actual value set. + */ + bool set_enabled(bool value); + + /** + * @brief get_heartRateEnabled Indicates if the the heart rate signal is enabled. + */ + bool get_heartRateEnabled(); + + /** + * @brief set_heartRateEnabled Enables/disables steering. + * @param value True to use heart rate data, false to ignore it. + * @return The actual value set; + */ + bool set_heartRateEnabled(bool value); + + /** + * @brief get_steeringEnabled Indicates if the steering is enabled. + */ + bool get_steeringEnabled(); + + /** + * @brief set_steeringEnabled Enables/disables steering. + * @param value True to use steering data, false to ignore it. + * @return The actual value set; + */ + bool set_steeringEnabled(bool value); + + /** + * @brief get_steeringCalibration Gets the values for steering calibration. + */ + steeringCalibrationInfo get_steeringCalibration(); + + /** + * @brief set_steeringCalibration sets the values for steering calibration. + * @param value The calibraion values. + */ + void set_steeringCalibration(const steeringCalibrationInfo value); + + /** + * @brief get_ConnectionTimeoutMilliseconds Gets the number of milliseconds the + * detector will wait for valid data from the serial port. + * @return + */ + uint16_t get_connectionTimeoutMilliseconds(); + + /** + * @brief set_connectionTimeoutMilliseconds Sets the number of milliseconds the + * detector will wait for valid data from the serial port. + * @param value + */ + void set_connectionTimeoutMilliseconds(uint16_t value); + + /** + * @brief trixterxdreamv1bikesettings Constructor, intializes from the default QSettings. + */ + trixterxdreamv1settings(); + + /** + * @brief trixterxdreamv1bikesettings Constructor, initializes from the specified QSettings. + * @param settings + */ + trixterxdreamv1settings(const QSettings& settings); + + /** + * @brief Load Loads the values from the default settings. + */ + void Load(); + + /** + * @brief Load Loads the values from the specified QSettings object. + * @param settings + */ + void Load(const QSettings& settings); + + ///** + // * @brief Save Saves the values to the default QSettings object. + // */ + //void Save(); + + ///** + // * @brief Save Saves the values to the specified QSettings object. + // * @param settings + // */ + //void Save(const QSettings& settings); +}; + + +#endif // TRIXTERXDREAMV1SETTINGS_H diff --git a/src/qdomyos-zwift.pri b/src/qdomyos-zwift.pri index fa8d97c41..0b0cc36f6 100644 --- a/src/qdomyos-zwift.pri +++ b/src/qdomyos-zwift.pri @@ -83,6 +83,8 @@ SOURCES += \ $$PWD/devices/nordictrackifitadbelliptical/nordictrackifitadbelliptical.cpp \ $$PWD/devices/sportstechelliptical/sportstechelliptical.cpp \ $$PWD/devices/trxappgateusbelliptical/trxappgateusbelliptical.cpp \ + $$PWD/devices/trixterxdreamv1bike/qserialdatasource.cpp \ + $$PWD/devices/trixterxdreamv1bike/serialdatasource.cpp \ QTelnet.cpp \ devices/bkoolbike/bkoolbike.cpp \ devices/csaferower/csafe.cpp \ @@ -275,6 +277,10 @@ devices/ultrasportbike/ultrasportbike.cpp \ virtualdevices/virtualrower.cpp \ devices/wahookickrsnapbike/wahookickrsnapbike.cpp \ devices/yesoulbike/yesoulbike.cpp \ +devices/trixterxdreamv1bike/trixterxdreamv1bike.cpp \ +devices/trixterxdreamv1bike/trixterxdreamv1client.cpp \ +devices/trixterxdreamv1bike/trixterxdreamv1serial.cpp \ +devices/trixterxdreamv1bike/trixterxdreamv1settings.cpp \ trainprogram.cpp \ devices/trxappgateusbtreadmill/trxappgateusbtreadmill.cpp \ virtualdevices/virtualbike.cpp \ @@ -311,6 +317,8 @@ HEADERS += \ $$PWD/devices/nordictrackifitadbelliptical/nordictrackifitadbelliptical.h \ $$PWD/devices/sportstechelliptical/sportstechelliptical.h \ $$PWD/devices/trxappgateusbelliptical/trxappgateusbelliptical.h \ + $$PWD/devices/trixterxdreamv1bike/qserialdatasource.h \ + $$PWD/devices/trixterxdreamv1bike/serialdatasource.h \ $$PWD/ergtable.h \ $$PWD/treadmillErgTable.h \ QTelnet.h \ @@ -342,6 +350,10 @@ devices/wahookickrheadwind/wahookickrheadwind.h \ devices/ypooelliptical/ypooelliptical.h \ devices/ziprotreadmill/ziprotreadmill.h \ devices/computrainerbike/Computrainer.h \ +devices/trixterxdreamv1bike/trixterxdreamv1client.h \ +devices/trixterxdreamv1bike/trixterxdreamv1bike.h \ +devices/trixterxdreamv1bike/trixterxdreamv1serial.h \ +devices/trixterxdreamv1bike/trixterxdreamv1settings.h \ PathController.h \ characteristics/characteristicnotifier2a53.h \ characteristics/characteristicnotifier2a5b.h \ diff --git a/src/qdomyos-zwift.pro b/src/qdomyos-zwift.pro index 7ee740969..8353af12c 100644 --- a/src/qdomyos-zwift.pro +++ b/src/qdomyos-zwift.pro @@ -1 +1,5 @@ include(qdomyos-zwift.pri) + +HEADERS += + +SOURCES += diff --git a/src/qzsettings.cpp b/src/qzsettings.cpp index ac0433131..a699dad7f 100644 --- a/src/qzsettings.cpp +++ b/src/qzsettings.cpp @@ -1,6 +1,7 @@ #include "qzsettings.h" #include #include + const QString QZSettings::cryptoKeySettingsProfiles = QStringLiteral("cryptoKeySettingsProfiles"); const QString QZSettings::bluetooth_no_reconnection = QStringLiteral("bluetooth_no_reconnection"); const QString QZSettings::bike_wheel_revs = QStringLiteral("bike_wheel_revs"); @@ -770,7 +771,19 @@ const QString QZSettings::force_resistance_instead_inclination = QStringLiteral( const QString QZSettings::proform_treadmill_575i = QStringLiteral("proform_treadmill_575i"); const QString QZSettings::zwift_play_emulator = QStringLiteral("zwift_play_emulator"); -const uint32_t allSettingsCount = 651; +const QString QZSettings::trixter_xdream_v1_bike_enabled = QStringLiteral("trixter_xdream_v1_bike_enabled"); +const QString QZSettings::trixter_xdream_v1_bike_heartrate_enabled = QStringLiteral("trixter_xdream_v1_bike_heartrate_enabled"); +const QString QZSettings::trixter_xdream_v1_bike_steering_enabled = QStringLiteral("trixter_xdream_v1_bike_steering_enabled"); +const QString QZSettings::trixter_xdream_v1_bike_steering_l = QStringLiteral("trixter_xdream_v1_bike_steering_l"); +const QString QZSettings::trixter_xdream_v1_bike_steering_cl = QStringLiteral("trixter_xdream_v1_bike_steering_cl"); +const QString QZSettings::trixter_xdream_v1_bike_steering_cr = QStringLiteral("trixter_xdream_v1_bike_steering_cr"); +const QString QZSettings::trixter_xdream_v1_bike_steering_r = QStringLiteral("trixter_xdream_v1_bike_steering_r"); +const QString QZSettings::trixter_xdream_v1_bike_steering_max = QStringLiteral("trixter_xdream_v1_bike_steering_max"); +const QString QZSettings::trixter_xdream_v1_bike_connection_timeout_ms = QStringLiteral("trixter_xdream_v1_bike_connection_timeout_ms"); + + + +const uint32_t allSettingsCount = 663; QVariant allSettings[allSettingsCount][2] = { {QZSettings::cryptoKeySettingsProfiles, QZSettings::default_cryptoKeySettingsProfiles}, @@ -1312,8 +1325,7 @@ QVariant allSettings[allSettingsCount][2] = { {QZSettings::zwift_ocr, QZSettings::default_zwift_ocr}, {QZSettings::fit_file_saved_on_quit, QZSettings::default_fit_file_saved_on_quit}, {QZSettings::gem_module_inclination, QZSettings::default_gem_module_inclination}, - {QZSettings::treadmill_simulate_inclination_with_speed, - QZSettings::default_treadmill_simulate_inclination_with_speed}, + {QZSettings::treadmill_simulate_inclination_with_speed, QZSettings::default_treadmill_simulate_inclination_with_speed}, {QZSettings::garmin_companion, QZSettings::default_garmin_companion}, {QZSettings::peloton_companion_workout_ocr, QZSettings::default_companion_peloton_workout_ocr}, {QZSettings::iconcept_elliptical, QZSettings::default_iconcept_elliptical}, @@ -1428,6 +1440,15 @@ QVariant allSettings[allSettingsCount][2] = { {QZSettings::force_resistance_instead_inclination, QZSettings::default_force_resistance_instead_inclination}, {QZSettings::proform_treadmill_575i, QZSettings::default_proform_treadmill_575i}, {QZSettings::zwift_play_emulator, QZSettings::default_zwift_play_emulator}, + {QZSettings::trixter_xdream_v1_bike_enabled, QZSettings::default_trixter_xdream_v1_bike_enabled }, + {QZSettings::trixter_xdream_v1_bike_heartrate_enabled, QZSettings::default_trixter_xdream_v1_bike_heartrate_enabled }, + {QZSettings::trixter_xdream_v1_bike_steering_enabled, QZSettings::default_trixter_xdream_v1_bike_steering_enabled }, + {QZSettings::trixter_xdream_v1_bike_steering_l, QZSettings::default_trixter_xdream_v1_bike_steering_l }, + {QZSettings::trixter_xdream_v1_bike_steering_cl, QZSettings::default_trixter_xdream_v1_bike_steering_cl }, + {QZSettings::trixter_xdream_v1_bike_steering_cr, QZSettings::default_trixter_xdream_v1_bike_steering_cr }, + {QZSettings::trixter_xdream_v1_bike_steering_r, QZSettings::default_trixter_xdream_v1_bike_steering_r }, + {QZSettings::trixter_xdream_v1_bike_steering_max, QZSettings::default_trixter_xdream_v1_bike_steering_max }, + {QZSettings::trixter_xdream_v1_bike_connection_timeout_ms, QZSettings::default_trixter_xdream_v1_bike_connection_timeout_ms } }; void QZSettings::qDebugAllSettings(bool showDefaults) { diff --git a/src/qzsettings.h b/src/qzsettings.h index 8fdb021ed..ee5cdc84a 100644 --- a/src/qzsettings.h +++ b/src/qzsettings.h @@ -1325,7 +1325,7 @@ class QZSettings { static const QString default_horizon_treadmill_profile_user5; static const QString nordictrack_gx_2_7; - static const bool default_nordictrack_gx_2_7 = false; + static constexpr bool default_nordictrack_gx_2_7 = false; static const QString rolling_resistance; static constexpr double default_rolling_resistance = 0.005; @@ -2153,6 +2153,36 @@ class QZSettings { static const QString zwift_play_emulator; static constexpr bool default_zwift_play_emulator = false; + + + // Trixter X-Dream V1 Bike Settings + static const QString trixter_xdream_v1_bike_enabled; + static constexpr bool default_trixter_xdream_v1_bike_enabled = false; + + static const QString trixter_xdream_v1_bike_heartrate_enabled; + static constexpr bool default_trixter_xdream_v1_bike_heartrate_enabled = true; + + static const QString trixter_xdream_v1_bike_steering_enabled; + static constexpr bool default_trixter_xdream_v1_bike_steering_enabled= true; + + static const QString trixter_xdream_v1_bike_steering_max; + static constexpr int8_t default_trixter_xdream_v1_bike_steering_max= 45; + + static const QString trixter_xdream_v1_bike_steering_l; + static constexpr int8_t default_trixter_xdream_v1_bike_steering_l = -default_trixter_xdream_v1_bike_steering_max; + + static const QString trixter_xdream_v1_bike_steering_cl; + static constexpr int8_t default_trixter_xdream_v1_bike_steering_cl=-2; + + static const QString trixter_xdream_v1_bike_steering_cr; + static constexpr int8_t default_trixter_xdream_v1_bike_steering_cr= 2; + + static const QString trixter_xdream_v1_bike_steering_r; + static constexpr int8_t default_trixter_xdream_v1_bike_steering_r = default_trixter_xdream_v1_bike_steering_max; + + static const QString trixter_xdream_v1_bike_connection_timeout_ms; + static constexpr uint32_t default_trixter_xdream_v1_bike_connection_timeout_ms= 500; + /** * @brief Write the QSettings values using the constants from this namespace. * @param showDefaults Optionally indicates if the default should be shown with the key. diff --git a/src/settings.qml b/src/settings.qml index 0b10a071d..dfa96d966 100644 --- a/src/settings.qml +++ b/src/settings.qml @@ -923,18 +923,8 @@ import QtQuick.Dialogs 1.0 property bool proform_carbon_tl: false property bool proform_proshox2: false - // from version 2.16.51 - property bool nordictrack_GX4_5_bike: false - - // from version 2.16.52 - property real ftp_run: 200.0 - property bool tile_rss_enabled: false - property int tile_rss_order: 53 - property string treadmillDataPoints: "" - - // from version 2.16.54 - property bool nordictrack_s20i_treadmill: false - property bool stryd_speed_instead_treadmill: false + + property bool proform_595i_proshox2: false // from version 2.16.55 @@ -987,6 +977,17 @@ import QtQuick.Dialogs 1.0 // from version 2.18.1 property bool zwift_play_emulator: false + + // from version ? + property bool trixter_xdream_v1_bike_enabled: false + property bool trixter_xdream_v1_bike_heartrate_enabled: true + property bool trixter_xdream_v1_bike_steering_enabled: true + property int trixter_xdream_v1_bike_steering_l : -45 + property int trixter_xdream_v1_bike_steering_cl : -2 + property int trixter_xdream_v1_bike_steering_cr : 2 + property int trixter_xdream_v1_bike_steering_r : 45 + property int trixter_xdream_v1_bike_steering_max : 45 + property int trixter_xdream_v1_bike_connection_timeout_ms : 500 } function paddingZeros(text, limit) { @@ -2994,6 +2995,186 @@ import QtQuick.Dialogs 1.0 } } } + AccordionElement { + id: trixterXDreamV1BikeAccordion + title: qsTr("Trixter X-Dream V1 Bike Options (Windows/Linux only)") + indicatRectColor: Material.color(Material.Grey) + textColor: Material.color(Material.Yellow) + color: Material.backgroundColor + accordionContent: + SwitchDelegate { + id: trixterXDreamV1 + text: qsTr("Trixter X-Dream V1 Bike Enabled") + hoverEnabled: true + ToolTip.visible: hovered + ToolTip.text: qsTr("Use this to enable or disable detection of the Trixter X-Dream V1 Bike. Not supported in iOS.") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.trixter_xdream_v1_bike_enabled + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: settings.trixter_xdream_v1_bike_enabled = checked + } + RowLayout { + id: trixterXDreamV1BikeConnection + spacing: 10 + Label { + id: labelTrixterXDreamV1BikeConnectionTimeout + text: qsTr("Initial connection timeout (ms)") + Layout.fillWidth: true + } + SpinBox { + id: trixterXDreamV1BikeConnectionTimeout + value: settings.trixter_xdream_v1_bike_connection_timeout_ms + hoverEnabled: true + ToolTip.visible: hovered + ToolTip.text: qsTr("The number of milliseconds the app will wait for data from the bike when searching a port.") + Layout.fillHeight: false + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + inputMethodHints: Qt.ImhDigitsOnly + stepSize: 100 + from: 100 + to: 10000 + onValueChanged: settings.trixter_xdream_v1_bike_connection_timeout_ms = value + } + } + SwitchDelegate { + id: trixterXDreamV1HeartRate + text: qsTr("Heart Rate Signal Enabled") + hoverEnabled: true + ToolTip.visible: hovered + ToolTip.text: qsTr("Use this to enable or disable the heart rate signal.") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.trixter_xdream_v1_bike_heartrate_enabled + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: settings.trixter_xdream_v1_bike_heartrate_enabled = checked + } + SwitchDelegate { + id: trixterXDreamV1Steering + text: qsTr("Steering Enabled") + hoverEnabled: true + ToolTip.visible: hovered + ToolTip.text: qsTr("Use this to enable or disable steering.") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.trixter_xdream_v1_bike_steering_enabled + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: settings.trixter_xdream_v1_bike_steering_enabled = checked + } + ColumnLayout { + id: trixterXDreamV1Calibration + spacing: 10 + Label { + id: labelTrixterXDreamV1BikeSteeringBoundariesAngular + text: qsTr("Steering Calibration (Use values from Steering Tile with default calibration)") + Layout.fillWidth: true + } + RowLayout { + Label { + text: qsTr("Unadjusted steering angle that is 100% left.") + Layout.fillWidth: true + } + SpinBox { + id: trixterXDreamV1BikeSteeringAngleLeftSpinBox + value: settings.trixter_xdream_v1_bike_steering_l + hoverEnabled: true + ToolTip.visible: hovered + ToolTip.text: qsTr("Unadjusted steering angle that is 100% left.") + Layout.fillHeight: false + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + inputMethodHints: Qt.ImhDigitsOnly + + from: -settings.trixter_xdream_v1_bike_steering_max + to: trixterXDreamV1BikeSteeringAngleCenterLeftSpinBox.value + onValueChanged: settings.trixter_xdream_v1_bike_steering_l = value + } + } + RowLayout { + Label { + text: qsTr("Leftmost unadjusted steering angle to be mapped to 0 degrees.") + Layout.fillWidth: true + } + SpinBox { + id: trixterXDreamV1BikeSteeringAngleCenterLeftSpinBox + value: settings.trixter_xdream_v1_bike_steering_cl + hoverEnabled: true + ToolTip.visible: hovered + ToolTip.text: qsTr("Leftmost unadjusted steering angle to be mapped to 0 degrees.") + + Layout.fillHeight: false + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + inputMethodHints: Qt.ImhDigitsOnly + + from: trixterXDreamV1BikeSteeringAngleLeftSpinBox.value + to: trixterXDreamV1BikeSteeringAngleCenterRightSpinBox.value + onValueChanged: settings.trixter_xdream_v1_bike_steering_cl = value + } + } + RowLayout { + Label { + text: qsTr("Rightmost unadjusted steering angle to be mapped to 0 degrees.") + Layout.fillWidth: true + } + SpinBox { + id: trixterXDreamV1BikeSteeringAngleCenterRightSpinBox + value: settings.trixter_xdream_v1_bike_steering_cr + hoverEnabled: true + ToolTip.visible: hovered + ToolTip.text: qsTr("Rightmost unadjusted steering angle to be mapped to 0 degrees.") + Layout.fillHeight: false + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + inputMethodHints: Qt.ImhDigitsOnly + from: trixterXDreamV1BikeSteeringAngleCenterLeftSpinBox.value + to: trixterXDreamV1BikeSteeringAngleRightSpinBox.value + onValueChanged: settings.trixter_xdream_v1_bike_steering_cr = value + }} + RowLayout { + Label { + text: qsTr("Unadjusted steering angle that is 100% right.") + Layout.fillWidth: true + } + SpinBox { + id: trixterXDreamV1BikeSteeringAngleRightSpinBox + value: settings.trixter_xdream_v1_bike_steering_r + hoverEnabled: true + ToolTip.visible: hovered + ToolTip.text: qsTr("Unadjusted steering angle that is 100% right.") + Layout.fillHeight: false + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + inputMethodHints: Qt.ImhDigitsOnly + from: trixterXDreamV1BikeSteeringAngleCenterRightSpinBox.value + to: 45 + onValueChanged: settings.trixter_xdream_v1_bike_steering_r = value + }} + Button { + id: resetTrixterXDreamV1BikeSteeringAnglesReset + text: "RESET" + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + onClicked: { + settings.trixter_xdream_v1_bike_steering_l = -settings.trixter_xdream_v1_bike_steering_max + settings.trixter_xdream_v1_bike_steering_cl = -2 + settings.trixter_xdream_v1_bike_steering_cr = 2 + settings.trixter_xdream_v1_bike_steering_r = settings.trixter_xdream_v1_bike_steering_max + trixterXDreamV1Calibration.update() + } + } + } + } AccordionElement { id: domyosBikeAccordion title: qsTr("Domyos Bike Options") diff --git a/tst/Devices/TrixterXDreamBike/TrixterXDreamV1PacketInterpreterTests.cpp b/tst/Devices/TrixterXDreamBike/TrixterXDreamV1PacketInterpreterTests.cpp new file mode 100644 index 000000000..1bf66f02a --- /dev/null +++ b/tst/Devices/TrixterXDreamBike/TrixterXDreamV1PacketInterpreterTests.cpp @@ -0,0 +1,60 @@ +#include "TrixterXDreamV1PacketInterpreterTests.h" + +uint32_t TrixterXDreamV1PacketInterpreterTests::getTime() +{ + std::chrono::milliseconds ms = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()); + + return static_cast(ms.count()); +} + +void TrixterXDreamV1PacketInterpreterTests::TestInput(std::string input, uint8_t expectedHR, uint8_t expectedSteering) +{ + trixterxdreamv1client tx1; + + tx1.set_GetTime(getTime); + + for (char value : input) + tx1.ReceiveChar(value); + + trixterxdreamv1client::state state = tx1.getLastState(); + + EXPECT_EQ(state.HeartRate, expectedHR); + EXPECT_EQ(state.Steering, expectedSteering); + + +} + +void TrixterXDreamV1PacketInterpreterTests::TestResistance(trixterxdreamv1client *tx1, uint8_t resistanceLevel) +{ + + this->packet = nullptr; + this->packetLength = -1; + tx1->SendResistance(resistanceLevel); + + auto p = this->packet; + + if(resistanceLevel==0) + { + // no packet sent = request for no resistance + EXPECT_EQ(-1, this->packetLength); + EXPECT_TRUE(p == nullptr); + + return; + } + + // make sure the resistance is clipped + if (resistanceLevel > trixterxdreamv1client::MaxResistance) + resistanceLevel = trixterxdreamv1client::MaxResistance; + + EXPECT_EQ(6, this->packetLength); + EXPECT_TRUE(p != nullptr); + EXPECT_EQ(p[0], 0x6a); + EXPECT_EQ(p[1], resistanceLevel); + EXPECT_EQ(p[2], (resistanceLevel+60)%255); + EXPECT_EQ(p[3], (resistanceLevel+90)%255); + EXPECT_EQ(p[4], (resistanceLevel+120)%255); + EXPECT_EQ(p[5], p[0]^p[1]^p[2]^p[3]^p[4]); + +} + + diff --git a/tst/Devices/TrixterXDreamBike/TrixterXDreamV1PacketInterpreterTests.h b/tst/Devices/TrixterXDreamBike/TrixterXDreamV1PacketInterpreterTests.h new file mode 100644 index 000000000..f7981aee1 --- /dev/null +++ b/tst/Devices/TrixterXDreamBike/TrixterXDreamV1PacketInterpreterTests.h @@ -0,0 +1,69 @@ + +#include "gtest/gtest.h" + +#include "devices/trixterxdreamv1bike/trixterxdreamv1client.h" + + +// The fixture for testing class Foo. +class TrixterXDreamV1PacketInterpreterTests : public ::testing::Test { +protected: + uint8_t* packet; + int packetLength; + + // You can remove any or all of the following functions if their bodies would + // be empty. + + TrixterXDreamV1PacketInterpreterTests() { + // You can do set-up work for each test here. + } + + ~TrixterXDreamV1PacketInterpreterTests() override { + // You can do clean-up work that doesn't throw exceptions here. + } + + static uint32_t getTime(); + + // If the constructor and destructor are not enough for setting up + // and cleaning up each test, you can define the following methods: + + void SetUp() override { + // Code here will be called immediately after the constructor (right + // before each test). + } + + void TearDown() override { + // Code here will be called immediately after each test (right + // before the destructor). + } + + // Class members declared here can be used by all tests in the test suite + // for Foo. + + void TestInput(std::string input, uint8_t expectedHR, uint8_t expectedSteering); + + void TestResistance(trixterxdreamv1client * tx1, uint8_t resistanceLevel); + +}; + +//int main(int argc, char** argv) { +// ::testing::InitGoogleTest(&argc, argv); +// return RUN_ALL_TESTS(); +//} + +TEST_F(TrixterXDreamV1PacketInterpreterTests, ValidPacket) { + + TestInput("56b6a00000000000000000000000000016b6a7f45000000000000000000000050006a", 0x50, 0x7f); +} + +TEST_F(TrixterXDreamV1PacketInterpreterTests, SendResistance) { + + trixterxdreamv1client tx1; + + tx1.set_GetTime(getTime); + auto device = this; + tx1.set_WriteBytes([device](uint8_t* bytes, int length)->void { device->packet = bytes; device->packetLength = length; }); + + for (int i = 0; i <= 255; i++) + TestResistance(&tx1, static_cast(i)); +} + diff --git a/tst/Devices/TrixterXDreamBike/trixterxdreamv1bikestub.cpp b/tst/Devices/TrixterXDreamBike/trixterxdreamv1bikestub.cpp new file mode 100644 index 000000000..5e39e8a87 --- /dev/null +++ b/tst/Devices/TrixterXDreamBike/trixterxdreamv1bikestub.cpp @@ -0,0 +1,91 @@ +#include "trixterxdreamv1bikestub.h" +#include +#include +#include + + +static uint32_t getTime() { + auto ms = QDateTime::currentMSecsSinceEpoch(); + return static_cast(ms); +} + +bool TrixterXDreamV1BikeStub::tryPopulate() { + QMutexLocker locker(&this->mutex); + + uint32_t time = getTime(); + uint32_t last = this->lastAddedData; + + uint32_t delta = (time-last) / readInterval; + + if(delta==0) + return false; + + this->lastAddedData = time; + + if(delta>100) delta = 100; + + static std::string packet= "6a7f4500000000000000000000005000"; + + for(;delta>0; delta--) + for(size_t i=0; ireadBuffer.size()readBuffer.push(packet[i]); + + while(this->readBuffer.size()>bufferCapacity) + this->readBuffer.pop(); + + return true; +} + +serialdatasource *TrixterXDreamV1BikeStub::create(QObject * parent) { return new TrixterXDreamV1BikeStub(); } + +TrixterXDreamV1BikeStub::TrixterXDreamV1BikeStub() : serialdatasource() +{ + +} + +QStringList TrixterXDreamV1BikeStub::get_availablePorts() { + return QStringList("stub"); +} + +bool TrixterXDreamV1BikeStub::open(const QString& portName) { + this->lastAddedData = getTime()-readInterval; + return true; +} + +qint64 TrixterXDreamV1BikeStub::write(const QByteArray &data) { + bytesWritten.append(data); + return data.size(); +} + +void TrixterXDreamV1BikeStub::flush() { + +} + +bool TrixterXDreamV1BikeStub::waitForReadyRead() { + QThread::msleep(1); + return this->readBufferSize()>0; +} + +QByteArray TrixterXDreamV1BikeStub::readAll() { + QByteArray result; + QMutexLocker locker(&this->mutex); + auto count = this->readBufferSize(); + result.reserve(count); + for(int i=0; ireadBuffer.front()); + this->readBuffer.pop(); + } + return result; +} + +qint64 TrixterXDreamV1BikeStub::readBufferSize() { + QMutexLocker locker(&this->mutex); + this->tryPopulate(); + return this->readBuffer.size(); +} + +QString TrixterXDreamV1BikeStub::error() { return "NoError";} + +void TrixterXDreamV1BikeStub::close() { + +} diff --git a/tst/Devices/TrixterXDreamBike/trixterxdreamv1bikestub.h b/tst/Devices/TrixterXDreamBike/trixterxdreamv1bikestub.h new file mode 100644 index 000000000..9071982f7 --- /dev/null +++ b/tst/Devices/TrixterXDreamBike/trixterxdreamv1bikestub.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include +#include + +#include "devices/trixterxdreamv1bike/serialdatasource.h" + +/** + * @brief Implementation of serialdatasource "interface" for faking a Trixter X-Dream V1 bike for testing. + * Simulates a serial port that sends a single valid packet repeatedly. + */ +class TrixterXDreamV1BikeStub : public serialdatasource { + + QByteArray bytesWritten; + std::queue readBuffer; + QRecursiveMutex mutex; + ulong lastAddedData = 0; + + const ulong readInterval = 10; + const uint32_t bufferCapacity = 4096; + +protected: + + /** + * @brief If it's been more than the readInterval since fake data was put in the buffer, + * puts a packet in the buffer for every interval that has elapsed. + * @return True/false depedning on whether data was added. + */ + bool tryPopulate(); + +public: + + static serialdatasource* create(QObject * parent); + + TrixterXDreamV1BikeStub(); + + QStringList get_availablePorts() override; + + bool open(const QString& portName) override; + qint64 write(const QByteArray& data) override; + void flush() override; + bool waitForReadyRead() override; + QByteArray readAll() override; + qint64 readBufferSize() override; + QString error() override; + void close() override; +}; diff --git a/tst/Devices/TrixterXDreamBike/trixterxdreamv1biketestsuite.cpp b/tst/Devices/TrixterXDreamBike/trixterxdreamv1biketestsuite.cpp new file mode 100644 index 000000000..18396178d --- /dev/null +++ b/tst/Devices/TrixterXDreamBike/trixterxdreamv1biketestsuite.cpp @@ -0,0 +1,92 @@ +#include "trixterxdreamv1biketestsuite.h" +#include "trixterxdreamv1bikestub.h" + + +TrixterXDreamV1BikeTestSuite::TrixterXDreamV1BikeTestSuite() : testSettings("Roberto Viola", "QDomyos-Zwift Testing") { + // use the test serial data source because the bike won't be there usually, during test runs. + trixterxdreamv1serial::serialDataSourceFactory = [](QObject*) { return new TrixterXDreamV1BikeStub(); }; +} + +void TrixterXDreamV1BikeTestSuite::test_power_calculations() { + std::shared_ptr bike(new trixterxdreamv1bike(false, false, false)); + + const uint32_t maxRPM = 120; + const uint32_t minRPM = 30; + const resistance_t maxResistance = bike->maxResistance(); + const resistance_t minResistance = 0; + + uint16_t p0, p1; + + // traverse the cadence edges checking the power is clipped to the values for the max and min resistance + for(uint32_t cadenceRPM=minRPM; cadenceRPM<=maxRPM; cadenceRPM++) + { + bike->cadenceSensor(cadenceRPM); + p0 = bike->powerFromResistanceRequest(maxResistance); + p1 = bike->powerFromResistanceRequest(maxResistance+1); + + ASSERT_EQ(p0, p1) << "expected power to stop increasing at max resistance, at cadence " << cadenceRPM << " RPM"; + + p0 = bike->powerFromResistanceRequest(minResistance); + p1 = bike->powerFromResistanceRequest(minResistance-1); + + ASSERT_EQ(p0, p1) << "expected power to stop decreasing at min resistance, at cadence " << cadenceRPM << " RPM"; + } + + // traverse the resistance edge checking the power is clipped to the values for the max and min cadence + for(resistance_t r=minResistance; r<=maxResistance; r++) + { + bike->cadenceSensor(minRPM); + p0 = bike->powerFromResistanceRequest(r); + bike->cadenceSensor(minRPM-1); + p1 = bike->powerFromResistanceRequest(r); + + ASSERT_EQ(p0, p1) << "expected power to stop decreasing at min cadence, for resistance " << r ; + + bike->cadenceSensor(maxRPM); + p0 = bike->powerFromResistanceRequest(r); + bike->cadenceSensor(maxRPM+1); + p1 = bike->powerFromResistanceRequest(r); + + ASSERT_EQ(p0, p1) << "expected power to stop increasing at max cadence, for resistance " << r ; + } + + // test inverses + for(uint32_t cadenceRPM=minRPM; cadenceRPM<=maxRPM; cadenceRPM++) + { + uint16_t lastPower=0xFFFF; + for(resistance_t r=minResistance; r<=maxResistance; r++) + { + bike->cadenceSensor(cadenceRPM); + uint16_t power = bike->powerFromResistanceRequest(r); + + if(power!=lastPower) + { + lastPower = power; + resistance_t resistance = bike->resistanceFromPowerRequest(power); + + ASSERT_EQ(r, resistance) << "unexpected resistance to achieve " << power << "W at "<test_power_calculations(); +} + +TEST_F(TrixterXDreamV1BikeTestSuite, TestStub) { + this->test_stub(); +} + diff --git a/tst/Devices/bluetoothdevicetestdata.cpp b/tst/Devices/bluetoothdevicetestdata.cpp index 9e0ede393..9dc7a5ade 100644 --- a/tst/Devices/bluetoothdevicetestdata.cpp +++ b/tst/Devices/bluetoothdevicetestdata.cpp @@ -3,6 +3,8 @@ QString BluetoothDeviceTestData::Name() const { return this->name; } +bool BluetoothDeviceTestData::UseNonBluetoothDiscovery() const { return this->usingNonBluetoothDiscovery; } + DeviceTypeId BluetoothDeviceTestData::ExpectedDeviceType() const { if(this->expectedDeviceType<0) throw std::domain_error("Expected device not set"); @@ -11,7 +13,6 @@ DeviceTypeId BluetoothDeviceTestData::ExpectedDeviceType() const { bool BluetoothDeviceTestData::IsEnabled() const { return this->enabled; } - const QString BluetoothDeviceTestData::DisabledReason() const { return this->disabledReason; } const QString BluetoothDeviceTestData::SkippedReason() const { return this->skippedReason; } @@ -78,4 +79,10 @@ std::vector BluetoothDeviceTestData::ApplyConfigurations(co return result; } +void BluetoothDeviceTestData::InitializeDevice() const +{ + if(this->initializer!=nullptr) + this->initializer(); +} + diff --git a/tst/Devices/bluetoothdevicetestdata.h b/tst/Devices/bluetoothdevicetestdata.h index 75aef89f7..c4dd60236 100644 --- a/tst/Devices/bluetoothdevicetestdata.h +++ b/tst/Devices/bluetoothdevicetestdata.h @@ -13,6 +13,7 @@ typedef std::function &configurations)> ConfigurationApplicatorMultiple; typedef std::function ConfigurationApplicatorSingle; +typedef std::function DeviceTestInitializer; class BluetoothDeviceTestData { @@ -27,6 +28,8 @@ class BluetoothDeviceTestData DeviceNamePatternGroup * deviceNamePatternGroup=nullptr; ConfigurationApplicatorMultiple applicatorMultiple=nullptr; ConfigurationApplicatorSingle applicatorSingle=nullptr; + DeviceTestInitializer initializer=nullptr; + bool usingNonBluetoothDiscovery = false; std::function isExpectedDevice=nullptr; DeviceTypeId expectedDeviceType=-1; BluetoothDeviceTestData(); @@ -37,6 +40,12 @@ class BluetoothDeviceTestData */ QString Name() const; + /** + * @brief Indicates if non-bluetooth discovery should be used. Default: false + * @return + */ + bool UseNonBluetoothDiscovery() const; + /** * @brief Gets a unique identifier * @return @@ -92,5 +101,10 @@ class BluetoothDeviceTestData */ std::vector ApplyConfigurations(const DeviceDiscoveryInfo& info, bool enable) const; + /** + * @brief Calls the initializer for the device if it is defined. + */ + void InitializeDevice() const; + virtual ~BluetoothDeviceTestData(); }; diff --git a/tst/Devices/bluetoothdevicetestdatabuilder.cpp b/tst/Devices/bluetoothdevicetestdatabuilder.cpp index aec9e550b..656a664dc 100644 --- a/tst/Devices/bluetoothdevicetestdatabuilder.cpp +++ b/tst/Devices/bluetoothdevicetestdatabuilder.cpp @@ -85,4 +85,9 @@ BluetoothDeviceTestDataBuilder *BluetoothDeviceTestDataBuilder::skip(const QStri return this; } +BluetoothDeviceTestDataBuilder *BluetoothDeviceTestDataBuilder::useNonBluetoothDiscovery(bool use) { + this->usingNonBluetoothDiscovery = use; + return this; +} + diff --git a/tst/Devices/bluetoothdevicetestdatabuilder.h b/tst/Devices/bluetoothdevicetestdatabuilder.h index e8d45888e..df692df0e 100644 --- a/tst/Devices/bluetoothdevicetestdatabuilder.h +++ b/tst/Devices/bluetoothdevicetestdatabuilder.h @@ -112,6 +112,21 @@ class BluetoothDeviceTestDataBuilder : public virtual BluetoothDeviceTestData */ BluetoothDeviceTestDataBuilder * configureSettingsWith(const QBluetoothUuid &uuid, bool addedIsEnabled=true); + + /** + * @brief Specifies an action that should take place before the test data is used to detect the device. + * @param initializer A functor that performs intialization for a test for this device. + * @return + */ + BluetoothDeviceTestDataBuilder * initializeWith(std::function initializer) { + if(this->initializer!=nullptr) + throw std::invalid_argument("Initializer already set."); + + this->initializer = initializer; + + return this; + } + /** * @brief Indicates that if a device of the types with the specified type ids is already recognised, * this one should not be detected even if the other conditions are met. @@ -142,4 +157,12 @@ class BluetoothDeviceTestDataBuilder : public virtual BluetoothDeviceTestData * @return */ BluetoothDeviceTestDataBuilder * skip(const QString& reason=nullptr); + + /** + * @brief Specifies that detection should use non-blueooth discovery. + * @param use (Optional) indicates whether to use non-bluetooth discovery. + * @return + */ + BluetoothDeviceTestDataBuilder * useNonBluetoothDiscovery(bool use=true); + }; diff --git a/tst/Devices/bluetoothdevicetestsuite.cpp b/tst/Devices/bluetoothdevicetestsuite.cpp index 5170db4d7..3e27fb8ee 100644 --- a/tst/Devices/bluetoothdevicetestsuite.cpp +++ b/tst/Devices/bluetoothdevicetestsuite.cpp @@ -14,7 +14,11 @@ void BluetoothDeviceTestSuite::tryDetectDevice(bluetooth &bt, // It is possible to use an EXPECT_NO_THROW here, but this // way is easier to place a breakpoint on the call to bt.deviceDiscovered. bt.homeformLoaded = true; - bt.deviceDiscovered(deviceInfo); + + if(this->testParam->UseNonBluetoothDiscovery()) + bt.nonBluetoothDeviceDiscovery(); + else + bt.deviceDiscovered(deviceInfo); } catch (...) { FAIL() << "Failed to perform device detection."; } @@ -63,7 +67,6 @@ void BluetoothDeviceTestSuite::testDeviceDetection(const BluetoothDeviceTestData BluetoothSignalReceiver signalReceiver(bt); - this->tryDetectDevice(bt, deviceInfo); bluetoothdevice * device = bt.device(); @@ -92,6 +95,9 @@ void BluetoothDeviceTestSuite::SetUp() { GTEST_FAIL() << "Failed to get test data for: " << testParam.toStdString(); QString skipMessage = nullptr; + + qDebug() << "Got test data"; + if(!this->testParam->IsEnabled()) { QString reason = this->testParam->DisabledReason(); @@ -112,16 +118,25 @@ void BluetoothDeviceTestSuite::SetUp() { if(skipMessage!=nullptr) GTEST_SKIP() << skipMessage.toStdString(); + qDebug() << "Not disabled or skipped"; + + this->testParam->InitializeDevice(); + + qDebug() << "Test Data Device Initialization complete"; this->defaultDiscoveryOptions = discoveryoptions{}; this->defaultDiscoveryOptions.startDiscovery = false; - this->defaultDiscoveryOptions.logs = false; + this->defaultDiscoveryOptions.logs = true; this->names = this->testParam->NamePatternGroup()->DeviceNames(); + qDebug() << "Got device names"; + EXPECT_GT(this->names.size(), 0) << "No bluetooth names configured for test"; this->testSettings.activate(); + + qDebug() << "Test settings activated"; } void BluetoothDeviceTestSuite::test_deviceDetection(const bool validNames, const bool enablingConfigs) { diff --git a/tst/Devices/bluetoothdevicetestsuite.h b/tst/Devices/bluetoothdevicetestsuite.h index d24d8a078..60b510a8d 100644 --- a/tst/Devices/bluetoothdevicetestsuite.h +++ b/tst/Devices/bluetoothdevicetestsuite.h @@ -140,7 +140,7 @@ INSTANTIATE_TEST_SUITE_P(AllDevicesDetection, BluetoothDeviceTestSuite, // Use this for debugging a single test data set. INSTANTIATE_TEST_SUITE_P(SelectedDevicesDetection, BluetoothDeviceTestSuite, - testing::Values(DeviceIndex::TacxNeo2Bike), + testing::Values(DeviceIndex::TrixterXDreamV1Bike), [](const testing::TestParamInfo& item) {return DeviceIndex::Identifier(item.param).toStdString(); }); #endif diff --git a/tst/Devices/devicediscoveryinfo.cpp b/tst/Devices/devicediscoveryinfo.cpp index e69d17179..1e90b925d 100644 --- a/tst/Devices/devicediscoveryinfo.cpp +++ b/tst/Devices/devicediscoveryinfo.cpp @@ -51,6 +51,17 @@ void InitializeTrackedSettings() trackedSettings.insert(QZSettings::toorx_bike, QZSettings::default_toorx_bike); trackedSettings.insert(QZSettings::toorx_ftms, QZSettings::default_toorx_ftms); trackedSettings.insert(QZSettings::toorx_ftms_treadmill, QZSettings::default_toorx_ftms_treadmill); + trackedSettings.insert(QZSettings::trixter_xdream_v1_bike_enabled, QZSettings::default_trixter_xdream_v1_bike_enabled ); + trackedSettings.insert(QZSettings::trixter_xdream_v1_bike_heartrate_enabled, QZSettings::default_trixter_xdream_v1_bike_heartrate_enabled ); + trackedSettings.insert(QZSettings::trixter_xdream_v1_bike_steering_enabled, QZSettings::default_trixter_xdream_v1_bike_steering_enabled); + trackedSettings.insert(QZSettings::trixter_xdream_v1_bike_steering_l, QZSettings::default_trixter_xdream_v1_bike_steering_l); + trackedSettings.insert(QZSettings::trixter_xdream_v1_bike_steering_cl, QZSettings::default_trixter_xdream_v1_bike_steering_cl); + trackedSettings.insert(QZSettings::trixter_xdream_v1_bike_steering_cr, QZSettings::default_trixter_xdream_v1_bike_steering_cr); + trackedSettings.insert(QZSettings::trixter_xdream_v1_bike_steering_r, QZSettings::default_trixter_xdream_v1_bike_steering_r); + trackedSettings.insert(QZSettings::trixter_xdream_v1_bike_steering_max, QZSettings::default_trixter_xdream_v1_bike_steering_max); + trackedSettings.insert(QZSettings::trixter_xdream_v1_bike_connection_timeout_ms, QZSettings::default_trixter_xdream_v1_bike_connection_timeout_ms); + + }; static void AssertKeyIsTracked(const QString& key) { diff --git a/tst/Devices/deviceindex.cpp b/tst/Devices/deviceindex.cpp index f4435c3cf..c3ade7167 100644 --- a/tst/Devices/deviceindex.cpp +++ b/tst/Devices/deviceindex.cpp @@ -145,6 +145,7 @@ DEFINE_DEVICE(TechnoGymMyRunTreadmill, "TechnoGym MyRun Treadmill"); DEFINE_DEVICE(ToorxAppGateUSBBike_EnabledInSettings, "Toorx AppGate USB Bike (Enabled in settings)"); DEFINE_DEVICE(ToorxAppGateUSBBike, "Toorx AppGate USB Bike"); DEFINE_DEVICE(ToorxAppGateUSBTreadmill, "Toorx AppGate USB Treadmill"); +DEFINE_DEVICE(TrixterXDreamV1Bike, "Trixter X-Dream V1 Bike"); DEFINE_DEVICE(TrxAppGateUSBEllipticalIConsole, "Toorx AppGate USB Elliptical IConsole+"); DEFINE_DEVICE(ToorxTreadmill, "Toorx Treadmill"); DEFINE_DEVICE(TrueTreadmill, "True Treadmill"); diff --git a/tst/Devices/deviceindex.h b/tst/Devices/deviceindex.h index 6139bda8c..1d188062d 100644 --- a/tst/Devices/deviceindex.h +++ b/tst/Devices/deviceindex.h @@ -150,8 +150,9 @@ class DeviceIndex DEFINE_DEVICE(ToorxAppGateUSBBike_EnabledInSettings, "Toorx AppGate USB Bike (Enabled in settings)"); DEFINE_DEVICE(ToorxAppGateUSBBike, "Toorx AppGate USB Bike"); DEFINE_DEVICE(ToorxAppGateUSBTreadmill, "Toorx AppGate USB Treadmill"); - DEFINE_DEVICE(TrxAppGateUSBEllipticalIConsole, "Toorx AppGate USB Elliptical IConsole+"); DEFINE_DEVICE(ToorxTreadmill, "Toorx Treadmill"); + DEFINE_DEVICE(TrixterXDreamV1Bike, "Trixter X-Dream V1 Bike"); + DEFINE_DEVICE(TrxAppGateUSBEllipticalIConsole, "Toorx AppGate USB Elliptical IConsole+"); DEFINE_DEVICE(TrueTreadmill, "True Treadmill"); DEFINE_DEVICE(TrueTreadmill2, "True Treadmill 2"); DEFINE_DEVICE(TrxAppGateUSBElliptical, "TrxAppGateUSB Elliptical"); diff --git a/tst/Devices/devicetestdataindex.cpp b/tst/Devices/devicetestdataindex.cpp index 8a1376f01..6effb061b 100644 --- a/tst/Devices/devicetestdataindex.cpp +++ b/tst/Devices/devicetestdataindex.cpp @@ -7,6 +7,7 @@ #include "bluetoothdevicetestdatabuilder.h" #include "devicediscoveryinfo.h" #include "qzsettings.h" +#include "TrixterXDreamBike/trixterxdreamv1bikestub.h" @@ -953,12 +954,12 @@ void DeviceTestDataIndex::Initialize() { ->acceptDeviceName("", DeviceNameComparison::StartsWithIgnoreCase) ->configureSettingsWith(QZSettings::proformtdf4ip, testIP, ""); - // ProForm Wifi Bike + // ProForm Telnet Bike RegisterNewDeviceTestData(DeviceIndex::ProFormTelnetBike) ->expectDevice() ->acceptDeviceName("", DeviceNameComparison::StartsWithIgnoreCase) ->configureSettingsWith(QZSettings::proformtdf1ip, testIP, "") - ->disable("Locks up"); + ->skip("Can take over 60s and device isn't faked."); // ProForm Wifi Treadmill RegisterNewDeviceTestData(DeviceIndex::ProFormWifiTreadmill) @@ -1235,6 +1236,7 @@ void DeviceTestDataIndex::Initialize() { ->acceptDeviceNames({"TACX ", "TACX SMART BIKE","THINK X"}, DeviceNameComparison::StartsWithIgnoreCase) ->rejectDeviceName("TACX SATORI", DeviceNameComparison::StartsWithIgnoreCase); + // Tacx Neo 2 Bike RegisterNewDeviceTestData(DeviceIndex::TacxNeo2Bike) ->expectDevice() @@ -1245,7 +1247,6 @@ void DeviceTestDataIndex::Initialize() { auto newDevice = QBluetoothDeviceInfo(address, info.DeviceName(), 0); auto config = DeviceDiscoveryInfo(info, newDevice); configurations.push_back(config); - }); // TechnoGym MyRun Treadmill @@ -1266,6 +1267,17 @@ void DeviceTestDataIndex::Initialize() { ->acceptDeviceName("TRX ROUTE KEY", DeviceNameComparison::StartsWith) ->acceptDeviceNames({"BH DUALKIT TREAD", "BH-TR-", "MASTERT40-"}, DeviceNameComparison::StartsWithIgnoreCase); + // Trixter X-Dream V1 Bike + RegisterNewDeviceTestData(DeviceIndex::TrixterXDreamV1Bike) + ->expectDevice() + ->initializeWith([]() -> void { + // use the test serial data source because the bike won't be there usually, during test runs. + trixterxdreamv1serial::serialDataSourceFactory = TrixterXDreamV1BikeStub::create; + }) + ->acceptDeviceName("", DeviceNameComparison::StartsWithIgnoreCase) + ->configureSettingsWith(trixterxdreamv1settings::keys::Enabled) + ->useNonBluetoothDiscovery(); + // True Treadmill RegisterNewDeviceTestData(DeviceIndex::TrueTreadmill) ->expectDevice() @@ -1514,7 +1526,7 @@ void DeviceTestDataIndex::Initialize() { try { auto exclusions = deviceTestData->Exclusions(); - } catch(std::domain_error) { + } catch(const std::domain_error&) { qDebug() << "Device: " << deviceTestData->Name() << " specifies at least 1 exclusion for which no test data was found."; } diff --git a/tst/main.cpp b/tst/main.cpp index 90802c600..730ebbfb1 100644 --- a/tst/main.cpp +++ b/tst/main.cpp @@ -9,7 +9,9 @@ int main(int argc, char *argv[]) { QCoreApplication app{argc, argv}; + qDebug() << "Initializing test data index"; DeviceTestDataIndex::Initialize(); + qDebug() << "Initialized test data index"; QTimer::singleShot(0, [&]() { diff --git a/tst/qdomyos-zwift-tests.pro b/tst/qdomyos-zwift-tests.pro index df987a1b4..9be13e2e5 100644 --- a/tst/qdomyos-zwift-tests.pro +++ b/tst/qdomyos-zwift-tests.pro @@ -5,12 +5,15 @@ include(gtest_dependency.pri) TEMPLATE = app -CONFIG += console c++11 +CONFIG += console c++17 CONFIG -= app_bundle CONFIG += thread CONFIG += androidextras SOURCES += \ + Devices/TrixterXDreamBike/TrixterXDreamV1PacketInterpreterTests.cpp \ + Devices/TrixterXDreamBike/trixterxdreamv1bikestub.cpp \ + Devices/TrixterXDreamBike/trixterxdreamv1biketestsuite.cpp \ Devices/bluetoothdevicetestdata.cpp \ Devices/bluetoothdevicetestdatabuilder.cpp \ Devices/bluetoothdevicetestsuite.cpp \ @@ -43,6 +46,9 @@ else:win32:!win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/ else:unix: PRE_TARGETDEPS += $$OUT_PWD/../src/libqdomyos-zwift.a HEADERS += \ + Devices/TrixterXDreamBike/TrixterXDreamV1PacketInterpreterTests.h \ + Devices/TrixterXDreamBike/trixterxdreamv1bikestub.h \ + Devices/TrixterXDreamBike/trixterxdreamv1biketestsuite.h \ Devices/bluetoothdevicetestdata.h \ Devices/bluetoothdevicetestdatabuilder.h \ Devices/bluetoothdevicetestsuite.h \