diff --git a/src/Train/BT40Device.cpp b/src/Train/BT40Device.cpp index af5bcc3f1e..4687dccfbf 100644 --- a/src/Train/BT40Device.cpp +++ b/src/Train/BT40Device.cpp @@ -48,6 +48,7 @@ QMap BT40Device::supportedServices = { { QBluetoothUuid(QString(BLE_TACX_UART_UUID)), { "Tacx FE-C over BLE", ":images/IconPower.png" }}, { s_KurtInRideService_UUID, { "Kurt Kinetic Inride over BLE", ":images/IconPower.png" }}, { s_KurtSmartControlService_UUID, { "Kurt Kinetic Smart Control over BLE", ":images/IconPower.png" }}, + { QBluetoothUuid((quint16)FTMSDEVICE_FTMS_UUID), {"FTMS", ":images/IconPower.png"}}, // This will be needed if we decide to query DeviceInfo for SystemID //{ QBluetoothUuid(QBluetoothUuid::DeviceInformation), { "DeviceInformation", ":images / IconPower.png"}}, @@ -189,6 +190,7 @@ BT40Device::serviceScanDone() connect(service, SIGNAL(stateChanged(QLowEnergyService::ServiceState)), this, SLOT(serviceStateChanged(QLowEnergyService::ServiceState))); connect(service, SIGNAL(characteristicChanged(QLowEnergyCharacteristic,QByteArray)), this, SLOT(updateValue(QLowEnergyCharacteristic,QByteArray))); + connect(service, SIGNAL(characteristicRead(QLowEnergyCharacteristic,QByteArray)), this, SLOT(updateValue(QLowEnergyCharacteristic,QByteArray))); connect(service, SIGNAL(descriptorWritten(QLowEnergyDescriptor,QByteArray)), this, SLOT(confirmedDescriptorWrite(QLowEnergyDescriptor,QByteArray))); connect(service, SIGNAL(characteristicWritten(QLowEnergyCharacteristic,QByteArray)), this, SLOT(confirmedCharacteristicWrite(QLowEnergyCharacteristic,QByteArray))); connect(service, SIGNAL(error(QLowEnergyService::ServiceError)), this, SLOT(serviceError(QLowEnergyService::ServiceError))); @@ -300,6 +302,16 @@ BT40Device::serviceStateChanged(QLowEnergyService::ServiceState s) characteristics.append(service->characteristic(s_KurtSmartControlService_Power_UUID)); characteristics.append(service->characteristic(s_KurtSmartControlService_Config_UUID)); characteristics.append(service->characteristic(s_KurtSmartControlService_Control_UUID)); + } else if (service->serviceUuid() == QBluetoothUuid((quint16)FTMSDEVICE_FTMS_UUID)) { + qDebug() << "------------------------------ FTMS FOUND ------------------------"; + characteristics.append(service->characteristic( + QBluetoothUuid((quint16)FTMSDEVICE_FTMS_CONTROL_POINT_CHAR_UUID))); + characteristics.append(service->characteristic( + QBluetoothUuid((quint16)FTMSDEVICE_INDOOR_BIKE_CHAR_UUID))); + + // Read FTMS Feature flags to find out what's supported and not. + service->readCharacteristic( + service->characteristic(QBluetoothUuid((quint16)FTMSDEVICE_FTMS_FEATURE_CHAR_UUID))); } foreach(QLowEnergyCharacteristic characteristic, characteristics) @@ -378,7 +390,27 @@ BT40Device::serviceStateChanged(QLowEnergyService::ServiceState s) QLowEnergyService::WriteWithResponse); break; } + } else if (characteristic.uuid() == QBluetoothUuid((quint16)FTMSDEVICE_FTMS_CONTROL_POINT_CHAR_UUID)) { + // Request control + loadService = service; + loadCharacteristic = characteristic; + loadType = FTMS_Device; + QByteArray command; + QDataStream commandDs(&command, QIODevice::ReadWrite); + commandDs.setByteOrder(QDataStream::LittleEndian); + commandDs << (quint8)FtmsControlPointCommand::FTMS_REQUEST_CONTROL; + + // Start notifications since command results will come on this char + const QLowEnergyDescriptor notificationDesc = characteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration); + if (notificationDesc.isValid()) { + service->writeDescriptor(notificationDesc, QByteArray::fromHex("0100")); + } + qDebug() << "Found FTMS ------------------------------------------------------"; + loadService->writeCharacteristic(characteristic, command); + } else if (characteristic.uuid() == QBluetoothUuid((quint16)FTMSDEVICE_FTMS_FEATURE_CHAR_UUID)) { + // Read out the different flags to find out what's supported and not. + loadService->readCharacteristic(characteristic); } else { qDebug() << "Starting notification for char with UUID: " << characteristic.uuid().toString(); const QLowEnergyDescriptor notificationDesc = characteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration); @@ -642,6 +674,78 @@ BT40Device::updateValue(const QLowEnergyCharacteristic &c, const QByteArray &val emit setNotification(notifyString, 4); } + } else if(c.uuid() == QBluetoothUuid((quint16)FTMSDEVICE_FTMS_CONTROL_POINT_CHAR_UUID)) { + quint8 type, cmd, status; + ds >> type; + if (type == FtmsControlPointCommand::FTMS_RESPONSE_CODE) + { + ds >> cmd; + ds >> status; + + if (cmd == FtmsControlPointCommand::FTMS_REQUEST_CONTROL) + { + qDebug() << "FTMS Request Control result: " << status; + } else if (cmd == FtmsControlPointCommand::FTMS_SET_TARGET_POWER) { + qDebug() << "FTMS Set Target Power result: " << status; + } + } + } else if (c.uuid() == QBluetoothUuid((quint16)FTMSDEVICE_INDOOR_BIKE_CHAR_UUID)) { + FtmsIndoorBikeData bd; + ftms_parse_indoor_bike_data(ds, bd); + + // Now update values of interest if they were present + if (bd.flags & FtmsIndoorBikeFlags::FTMS_INST_POWER_PRESENT) + { + dynamic_cast(parent)->setWatts(bd.inst_power); + } + + if (bd.flags & FtmsIndoorBikeFlags::FTMS_INST_CADENCE_PRESENT) + { + dynamic_cast(parent)->setCadence(bd.inst_cadence/2.0f); + } + } else if (c.uuid() == QBluetoothUuid((quint16)FTMSDEVICE_FTMS_FEATURE_CHAR_UUID)) { + quint32 features, target_settings; + ds >> features >> target_settings; + + if (target_settings & FtmsTargetSetting::FTMS_POWER_TARGET_SUPPORTED) + { + ftmsDeviceInfo.supports_power_target = true; + // Read in order to get max/min/increment for power target + loadService->readCharacteristic(loadService->characteristic( + QBluetoothUuid((quint16)FTMSDEVICE_POWER_RANGE_CHAR_UUID))); + } + + if (target_settings & FtmsTargetSetting::FTMS_RESISTANCE_TARGET_SUPPORTED) + { + ftmsDeviceInfo.supports_resistance_target = true; + // Read in order to get max/min/increment for resistance target + loadService->readCharacteristic(loadService->characteristic( + QBluetoothUuid((quint16)FTMSDEVICE_RESISTANCE_RANGE_CHAR_UUID))); + } + + if (target_settings & FtmsTargetSetting::FTMS_INDOOR_BIKE_SIMULATION_SUPPORTED) + { + ftmsDeviceInfo.supports_simulation_target = true; + } + } else if (c.uuid() == QBluetoothUuid((quint16)FTMSDEVICE_POWER_RANGE_CHAR_UUID)) { + qint16 max, min; + quint16 increment; + ds >> min >> max >> increment; + + // In watts + ftmsDeviceInfo.maximal_power = max; + ftmsDeviceInfo.minimal_power = min; + ftmsDeviceInfo.power_increment = increment; + qDebug() << "FTMS POWER INCREMENT" << max << " " << min << " " << increment; + } else if (c.uuid() == QBluetoothUuid((quint16)FTMSDEVICE_RESISTANCE_RANGE_CHAR_UUID)) { + qint16 max, min; + quint16 increment; + ds >> min >> max >> increment; + + // Unitless in 0.1 of unit + ftmsDeviceInfo.maximal_resistance = max; + ftmsDeviceInfo.minimal_resistance = min; + ftmsDeviceInfo.resistance_increment = increment; } } @@ -799,6 +903,17 @@ void BT40Device::setGradient(double g) loadService->writeCharacteristic(loadCharacteristic, smart_control_set_mode_simulation_command(weight, rollingResistance, windResistance, gradient, windSpeed), QLowEnergyService::WriteWithResponse); + } else if (loadType == FTMS_Device) { + qDebug() << tr("FTMS Device: Set gradient") << g; + qint16 ftms_wind_speed = this->windSpeed * 1000; // in 0.001 m/s + qint16 ftms_grade = this->gradient*100; // in 0.01 % + quint8 ftms_crr = this->rollingResistance; // 0.0001 unitless + quint8 ftms_cw = this->windResistance; // 0.01 Kg/m + QByteArray command; + QDataStream commandDs(&command, QIODevice::ReadWrite); + commandDs.setByteOrder(QDataStream::LittleEndian); + commandDs << (quint8)FtmsControlPointCommand::FTMS_SET_INDOOR_BIKE_SIMULATION_PARAMS << ftms_wind_speed << ftms_grade << ftms_crr << ftms_cw; + loadService->writeCharacteristic(loadCharacteristic, command); } } @@ -890,6 +1005,8 @@ BT40Device::setWindSpeed(double s) // In meters/second qDebug() << "BTLE SetWindSpeed " << windSpeed << " " << loadCharacteristic.uuid() << command.toHex(':'); commandSend(command); } + + sendSimulationParameters(); } void @@ -958,6 +1075,16 @@ BT40Device::setLoadErg(double l) // Load in Watts loadService->writeCharacteristic(loadCharacteristic, smart_control_set_mode_erg_command(load), QLowEnergyService::WriteWithResponse); + } else if (loadType == FTMS_Device) { + qDebug() << tr("FTMS Device: Set target power ") << load; + load = ftms_power_cap(load, ftmsDeviceInfo); + qDebug() << tr("FTMS Device: Set target power - after scaling ") << load; + + QByteArray command; + QDataStream commandDs(&command, QIODevice::ReadWrite); + commandDs.setByteOrder(QDataStream::LittleEndian); + commandDs << (quint8)FtmsControlPointCommand::FTMS_SET_TARGET_POWER << (qint16)load; + loadService->writeCharacteristic(loadCharacteristic, command); } } @@ -981,6 +1108,18 @@ BT40Device::setLoadIntensity(double l) // between 0 and 1 loadService->writeCharacteristic(loadCharacteristic, smart_control_set_mode_fluid_command(level), QLowEnergyService::WriteWithResponse); + } else if (loadType == FTMS_Device) { + // map [0, 1] to ftms resistance level limits + qint16 resistance = (ftmsDeviceInfo.maximal_resistance-ftmsDeviceInfo.minimal_resistance)*l + ftmsDeviceInfo.minimal_resistance; + qDebug() << tr("FTMS Device: Set load intensity ") << l; + resistance = ftms_resistance_cap(resistance, ftmsDeviceInfo); + qDebug() << tr("FTMS Device: Set load intensity - after scaling ") << resistance; + + QByteArray command; + QDataStream commandDs(&command, QIODevice::ReadWrite); + commandDs.setByteOrder(QDataStream::LittleEndian); + commandDs << (quint8)FtmsControlPointCommand::FTMS_SET_TARGET_RESISTANCE_LEVEL << (qint16)(resistance); + loadService->writeCharacteristic(loadCharacteristic, command); } } @@ -1001,6 +1140,18 @@ BT40Device::setLoadLevel(int l) // From 0 to 9 loadService->writeCharacteristic(loadCharacteristic, smart_control_set_mode_fluid_command(load), QLowEnergyService::WriteWithResponse); + } else if (loadType == FTMS_Device) { + // map [0, 9] to ftms resistance level limits + qint16 resistance = ((ftmsDeviceInfo.maximal_resistance-ftmsDeviceInfo.minimal_resistance)*l)/9 + ftmsDeviceInfo.minimal_resistance; + qDebug() << tr("FTMS Device: Set load level ") << l; + resistance = ftms_resistance_cap(resistance, ftmsDeviceInfo); + qDebug() << tr("FTMS Device: Set load level - after scaling ") << resistance; + + QByteArray command; + QDataStream commandDs(&command, QIODevice::ReadWrite); + commandDs.setByteOrder(QDataStream::LittleEndian); + commandDs << (quint8)FtmsControlPointCommand::FTMS_SET_TARGET_RESISTANCE_LEVEL << (qint16)resistance; + loadService->writeCharacteristic(loadCharacteristic, command); } } @@ -1023,6 +1174,8 @@ BT40Device::setRiderCharacteristics(double weight, double rollingResistance, dou qDebug() << "BTLE SetRiderCharacteristic " << weight << " " << rollingResistance << " " << windResistance << " " << loadCharacteristic.uuid() << command.toHex(':'); commandSend(command); } + + sendSimulationParameters(); } /* On the Wahoo Kickr and possibly many other BT40 devices, writes often fail. @@ -1069,3 +1222,18 @@ BT40Device::commandWritten() { if(commandQueue.size() > 0) commandWrite(commandQueue.head()); } +void +BT40Device::sendSimulationParameters() { + if (loadType == FTMS_Device) { + qDebug() << tr("FTMS Device: Send simulation parameteres"); + qint16 ftms_wind_speed = this->windSpeed * 1000; // in 0.001 m/s + qint16 ftms_grade = this->gradient*100; // in 0.01 % + quint8 ftms_crr = this->rollingResistance; // 0.0001 unitless + quint8 ftms_cw = this->windResistance; // 0.01 Kg/m + QByteArray command; + QDataStream commandDs(&command, QIODevice::ReadWrite); + commandDs.setByteOrder(QDataStream::LittleEndian); + commandDs << (quint8)FtmsControlPointCommand::FTMS_SET_INDOOR_BIKE_SIMULATION_PARAMS << ftms_wind_speed << ftms_grade << ftms_crr << ftms_cw; + loadService->writeCharacteristic(loadCharacteristic, command); + } +} diff --git a/src/Train/BT40Device.h b/src/Train/BT40Device.h index fdf0367065..16a56f861d 100644 --- a/src/Train/BT40Device.h +++ b/src/Train/BT40Device.h @@ -25,6 +25,7 @@ #include #include "CalibrationData.h" +#include "Ftms.h" typedef struct btle_sensor_type { const char *descriptive_name; @@ -100,12 +101,15 @@ private slots: CalibrationData calibrationData; // Service and Characteristic to set load - enum {Load_None, Tacx_UART, Wahoo_Kickr, Kurt_InRide, Kurt_SmartControl} loadType; + enum {Load_None, Tacx_UART, Wahoo_Kickr, Kurt_InRide, Kurt_SmartControl, FTMS_Device} loadType; QLowEnergyCharacteristic loadCharacteristic; QLowEnergyService* loadService; QQueue commandQueue; int commandRetry; + // FTMS Device Configuration + FtmsDeviceInformation ftmsDeviceInfo; + bool connected; void getCadence(QDataStream& ds); void getWheelRpm(QDataStream& ds); @@ -113,6 +117,7 @@ private slots: void setLoadIntensity(double); void setLoadLevel(int); void setRiderCharacteristics(double weight, double rollingResistance, double windResistance); + void sendSimulationParameters(); void commandSend(QByteArray &command); void commandWrite(QByteArray &command); void commandWriteFailed(); diff --git a/src/Train/Ftms.cpp b/src/Train/Ftms.cpp new file mode 100644 index 0000000000..b42ac9b1c9 --- /dev/null +++ b/src/Train/Ftms.cpp @@ -0,0 +1,103 @@ +#include "Ftms.h" + +void ftms_parse_indoor_bike_data(QDataStream &ds, FtmsIndoorBikeData &bd) +{ + //quint16 flags, inst_speed, avg_speed, inst_cadence, avg_cadence, tot_energy, energy_per_hour, elapsed_time, remaining_time; + //qint16 resistence_level, inst_power, avg_power; + //quint8 energy_per_min, heart_rate, met_equivalent; + quint16 dummy16; + quint8 dummy8; + + ds >> bd.flags; + + if (!(bd.flags & FtmsIndoorBikeFlags::FTMS_MORE_DATA)) + { + // If more data is not set, instant speed is present + ds >> bd.inst_speed; // resolution: 0.01 km/h + } + + if (bd.flags & FtmsIndoorBikeFlags::FTMS_AVERAGE_SPEED_PRESENT) + { + ds >> bd.avg_speed; // resolution: 0.01 km/h + } + + if (bd.flags & FtmsIndoorBikeFlags::FTMS_INST_CADENCE_PRESENT) + { + ds >> bd.inst_cadence; // resolution: 0.5 rpm + } + + if (bd.flags & FtmsIndoorBikeFlags::FTMS_AVERAGE_CADENCE_PRESENT) + { + ds >> bd.avg_cadence; // resolution: 0.5 rpm + } + + if (bd.flags & FtmsIndoorBikeFlags::FTMS_TOTAL_DISTANCE_PRESENT) + { + ds >> dummy16 >> dummy8; // we don't care about this, so just read 24 bits + } + + if (bd.flags & FtmsIndoorBikeFlags::FTMS_RESISTANCE_LEVEL_PRESENT) + { + ds >> bd.resistence_level; // resolution: unitless + } + + if (bd.flags & FtmsIndoorBikeFlags::FTMS_INST_POWER_PRESENT) + { + ds >> bd.inst_power; // resolution: 1 watt + } + + if (bd.flags & FtmsIndoorBikeFlags::FTMS_AVERAGE_POWER_PRESENT) + { + ds >> bd.avg_power; // resolution: 1 watt + } + + if (bd.flags & FtmsIndoorBikeFlags::FTMS_EXPENDED_ENERGY_PRESENT) + { + ds >> bd.tot_energy >> bd.energy_per_hour >> bd.energy_per_min; // resolution: 1 kcal + } + + if (bd.flags & FtmsIndoorBikeFlags::FTMS_HEART_RATE_PRESENT) + { + ds >> bd.heart_rate; // resolution: 1 bpm + } + + if (bd.flags & FtmsIndoorBikeFlags::FTMS_METABOLIC_EQUIV_PRESENT) + { + ds >> bd.met_equivalent; // resolution: 1 MET + } + + if (bd.flags & FtmsIndoorBikeFlags::FTMS_ELAPSED_TIME_PRESENT) + { + ds >> bd.elapsed_time; // resolution: 1 second + } + + if (bd.flags & FtmsIndoorBikeFlags::FTMS_REMAINING_TIME_PRESENT) + { + ds >> bd.remaining_time; // resolution: 1 second + } +} + +qint16 ftms_power_cap(qint16 power, FtmsDeviceInformation &device_info) { + + power = qRound((double)power/device_info.power_increment)*device_info.power_increment; + if (power > device_info.maximal_power) + { + power = device_info.maximal_power; + } else if (power < device_info.minimal_power) { + power = device_info.minimal_power; + } + + return power; +} + +double ftms_resistance_cap(qint16 resistance, FtmsDeviceInformation &device_info) { + resistance = qRound((double)resistance/device_info.resistance_increment)*device_info.resistance_increment; + if (resistance > device_info.maximal_resistance) + { + resistance = device_info.maximal_resistance; + } else if (resistance < device_info.minimal_resistance) { + resistance = device_info.minimal_resistance; + } + + return resistance; +} diff --git a/src/Train/Ftms.h b/src/Train/Ftms.h new file mode 100644 index 0000000000..a7c82da25f --- /dev/null +++ b/src/Train/Ftms.h @@ -0,0 +1,128 @@ +#ifndef FTMS_H +#define FTMS_H +#include + +// FTMS service assigned numbers +#define FTMSDEVICE_FTMS_UUID 0x1826 +#define FTMSDEVICE_INDOOR_BIKE_CHAR_UUID 0x2AD2 +#define FTMSDEVICE_POWER_RANGE_CHAR_UUID 0x2AD8 +#define FTMSDEVICE_RESISTANCE_RANGE_CHAR_UUID 0x2AD6 +#define FTMSDEVICE_FTMS_FEATURE_CHAR_UUID 0x2ACC +#define FTMSDEVICE_FTMS_CONTROL_POINT_CHAR_UUID 0x2AD9 + + +enum FtmsControlPointCommand { + FTMS_REQUEST_CONTROL = 0x00, + FTMS_RESET, + FTMS_SET_TARGET_SPEED, + FTMS_SET_TARGET_INCLINATION, + FTMS_SET_TARGET_RESISTANCE_LEVEL, + FTMS_SET_TARGET_POWER, + FTMS_SET_TARGET_HEARTRATE, + FTMS_START_RESUME, + FTMS_STOP_PAUSE, + FTMS_SET_TARGETED_EXP_ENERGY, + FTMS_SET_TARGETED_STEPS, + FTMS_SET_TARGETED_STRIDES, + FTMS_SET_TARGETED_DISTANCE, + FTMS_SET_TARGETED_TIME, + FTMS_SET_TARGETED_TIME_TWO_HR_ZONES, + FTMS_SET_TARGETED_TIME_THREE_HR_ZONES, + FTMS_SET_TARGETED_TIME_FIVE_HR_ZONES, + FTMS_SET_INDOOR_BIKE_SIMULATION_PARAMS, + FTMS_SET_WHEEL_CIRCUMFERENCE, + FTMS_SPIN_DOWN_CONTROL, + FTMS_SET_TARGETED_CADENCE, + FTMS_RESPONSE_CODE = 0x80 +}; + +enum FtmsResultCode { + FTMS_SUCCESS = 0x01, + FTMS_NOT_SUPPORTED, + FTMS_INVALID_PARAMETER, + FTMS_OPERATION_FAILED, + FTMS_CONTROL_NOT_PERMITTED +}; + +enum FtmsMachineFeatures { + FTMS_AVG_SPEED_SUPPORTED = 1 << 0, + FTMS_CADENCE_SUPPORTED = 1 << 1, + FTMS_TOTAL_DISTANCE_SUPPORTED = 1 << 2, + FTMS_INCLINATION_SUPPORTED = 1 << 3, + FTMS_ELEVATION_GAIN_SUPPORTED = 1 << 4, + FTMS_PACE_SUPPORTED = 1 << 5, + FTMS_STEP_COUNT_SUPPORTED = 1 << 6, + FTMS_RESISTANCE_LEVEL_SUPPORTED = 1 << 7, + FTMS_STRIDE_COUNT_SUPPORTED = 1 << 8, + FTMS_EXPENDED_ENERGY_SUPPORTED = 1 << 9, + FTMS_HEART_RATE_SUPPORTED = 1 << 10, + FTMS_METABOLIC_EQUIVALENT_SUPPORTED = 1 << 11, + FTMS_ELAPSED_TIME_SUPPORTED = 1 << 12, + FTMS_REMAINING_TIME_SUPPORTED = 1 << 13, + FTMS_POWER_MEASUREMENT_SUPPORTED = 1 << 14, + FTMS_FORCE_ON_BELT_AND_POWER_MEASUREMENT_SUPPORTED = 1 << 15, + FTMS_USER_DATA_RETENTION_SUPPORTED = 1 << 16 +}; + +enum FtmsTargetSetting { + FTMS_SPEED_TARGET_SUPPORTED = 1 << 0, + FTMS_INCLINATION_TARGET_SUPPORTED = 1 << 1, + FTMS_RESISTANCE_TARGET_SUPPORTED = 1 << 2, + FTMS_POWER_TARGET_SUPPORTED = 1 << 3, + FTMS_HEART_RATE_TARGET_SUPPORTED = 1 << 4, + FTMS_EXPENDED_ENERGY_TARGET_SUPPORTED = 1 << 5, + FTMS_STEP_NUMBER_CONFIGURATION_SUPPORTED = 1 << 6, + FTMS_STRIDE_NUMBER_CONFIGURATION_SUPPORTED = 1 << 7, + FTMS_DISTANCE_CONFIGURATION_SUPPORTED = 1 << 8, + FTMS_TRAINING_TIME_CONFIGURATION_SUPPORTED = 1 << 9, + FTMS_TIME_IN_TWO_HEART_RATE_ZONES_SUPPORTED = 1 << 10, + FTMS_TIME_IN_THREE_HEART_RATE_ZONES_SUPPORTED = 1 << 11, + FTMS_TIME_IN_FIVE_HEART_RATE_ZONES_SUPPORTED = 1 << 12, + FTMS_INDOOR_BIKE_SIMULATION_SUPPORTED = 1 << 13, + FTMS_WHEEL_CIRCUMFERENCE_CONFIGURATION_SUPPORTED = 1 << 14, + FTMS_SPIN_DOWN_CONTROL_SUPPORTED = 1 << 15, + FTMS_TARGETED_CADENCE_SUPPORTED = 1 << 16 +}; + +enum FtmsIndoorBikeFlags { + FTMS_MORE_DATA = 1 << 0, + FTMS_AVERAGE_SPEED_PRESENT = 1 << 1, + FTMS_INST_CADENCE_PRESENT = 1 << 2, + FTMS_AVERAGE_CADENCE_PRESENT = 1 << 3, + FTMS_TOTAL_DISTANCE_PRESENT = 1 << 4, + FTMS_RESISTANCE_LEVEL_PRESENT = 1 << 5, + FTMS_INST_POWER_PRESENT = 1 << 6, + FTMS_AVERAGE_POWER_PRESENT = 1 << 7, + FTMS_EXPENDED_ENERGY_PRESENT = 1 << 8, + FTMS_HEART_RATE_PRESENT = 1 << 9, + FTMS_METABOLIC_EQUIV_PRESENT = 1 << 10, + FTMS_ELAPSED_TIME_PRESENT = 1 << 11, + FTMS_REMAINING_TIME_PRESENT = 1 << 12 +}; + +struct FtmsIndoorBikeData { + quint16 flags, inst_speed, avg_speed, inst_cadence, avg_cadence, tot_energy, energy_per_hour, elapsed_time, remaining_time; + qint16 resistence_level, inst_power, avg_power; + quint8 energy_per_min, heart_rate, met_equivalent; +}; + +struct FtmsDeviceInformation { + bool supports_power_target = false; + bool supports_resistance_target = false; + bool supports_simulation_target = false; + + qint16 minimal_resistance = 0; + qint16 maximal_resistance = 0; + quint16 resistance_increment = 0; + + qint16 minimal_power = 0; + qint16 maximal_power = 0; + quint16 power_increment = 0; +}; + +void ftms_parse_indoor_bike_data(QDataStream &ds, FtmsIndoorBikeData &bd); + +qint16 ftms_power_cap(qint16 power, FtmsDeviceInformation &device_info); +double ftms_resistance_cap(qint16 resistance, FtmsDeviceInformation &device_info); + +#endif // FTMS_H diff --git a/src/src.pro b/src/src.pro index de85ffc827..044acfa196 100644 --- a/src/src.pro +++ b/src/src.pro @@ -639,6 +639,8 @@ greaterThan(QT_MAJOR_VERSION, 4) { SOURCES += Train/BT40Controller.cpp Train/BT40Device.cpp HEADERS += Train/VMProConfigurator.h Train/VMProWidget.h SOURCES += Train/VMProConfigurator.cpp Train/VMProWidget.cpp + SOURCES += Train/Ftms.cpp + HEADERS += Train/Ftms.h } # qt charts is officially supported from QT5.8 or higher diff --git a/travis/linux/after_success.sh b/travis/linux/after_success.sh index fe2b6765d6..1d04219286 100755 --- a/travis/linux/after_success.sh +++ b/travis/linux/after_success.sh @@ -46,19 +46,19 @@ chmod a+x linuxdeployqt-7-x86_64.AppImage ./linuxdeployqt-7-x86_64.AppImage appdir/GoldenCheetah -verbose=2 -bundle-non-qt-libs -exclude-libs=libqsqlmysql,libqsqlpsql,libnss3,libnssutil3,libxcb-dri3.so.0 -unsupported-allow-new-glibc # Add Python and core modules -wget https://github.com/niess/python-appimage/releases/download/python3.7/python3.7.11-cp37-cp37m-manylinux1_x86_64.AppImage -chmod +x python3.7.11-cp37-cp37m-manylinux1_x86_64.AppImage -./python3.7.11-cp37-cp37m-manylinux1_x86_64.AppImage --appimage-extract -rm -f python3.7.11-cp37-cp37m-manylinux1_x86_64.AppImage +wget --no-verbose https://github.com/niess/python-appimage/releases/download/python3.7/python3.7.12-cp37-cp37m-manylinux1_x86_64.AppImage +chmod +x python3.7.12-cp37-cp37m-manylinux1_x86_64.AppImage +./python3.7.12-cp37-cp37m-manylinux1_x86_64.AppImage --appimage-extract +rm -f python3.7.12-cp37-cp37m-manylinux1_x86_64.AppImage export PATH="$(pwd)/squashfs-root/usr/bin:$PATH" pip install --upgrade pip -pip install -r Python/requirements.txt +pip install -q -r Python/requirements.txt mv squashfs-root/usr appdir/usr mv squashfs-root/opt appdir/opt rm -rf squashfs-root # Generate AppImage -wget "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" +wget --no-verbose "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" chmod a+x appimagetool-x86_64.AppImage ./appimagetool-x86_64.AppImage appdir diff --git a/travis/linux/before_install.sh b/travis/linux/before_install.sh index 86e0249f72..742d1068cf 100755 --- a/travis/linux/before_install.sh +++ b/travis/linux/before_install.sh @@ -28,12 +28,12 @@ R --version # D2XX - refresh cache if folder is empty if [ -z "$(ls -A D2XX)" ]; then - wget http://www.ftdichip.com/Drivers/D2XX/Linux/libftd2xx-x86_64-1.3.6.tgz + wget --no-verbose http://www.ftdichip.com/Drivers/D2XX/Linux/libftd2xx-x86_64-1.3.6.tgz tar xf libftd2xx-x86_64-1.3.6.tgz -C D2XX fi # SRMIO -wget https://github.com/rclasen/srmio/archive/v0.1.1git1.tar.gz +wget --no-verbose https://github.com/rclasen/srmio/archive/v0.1.1git1.tar.gz tar xf v0.1.1git1.tar.gz cd srmio-0.1.1git1 sh genautomake.sh @@ -50,7 +50,7 @@ sudo add-apt-repository -y ppa:deadsnakes/ppa sudo apt-get update -qq sudo apt-get install -qq python3.7-dev python3.7 --version -wget https://sourceforge.net/projects/pyqt/files/sip/sip-4.19.8/sip-4.19.8.tar.gz +wget --no-verbose https://sourceforge.net/projects/pyqt/files/sip/sip-4.19.8/sip-4.19.8.tar.gz tar xf sip-4.19.8.tar.gz cd sip-4.19.8 python3.7 configure.py @@ -63,7 +63,7 @@ sudo apt-get -qq install libgsl-dev # AWS S3 client to upload binaries curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" -unzip awscliv2.zip +unzip -qq awscliv2.zip sudo ./aws/install aws --version diff --git a/travis/linux/deploy.sh b/travis/linux/deploy.sh index 75c03b9193..1d0297e7c8 100755 --- a/travis/linux/deploy.sh +++ b/travis/linux/deploy.sh @@ -29,7 +29,7 @@ cp /usr/lib/x86_64-linux-gnu/libssl.so appdir/lib cp /usr/lib/x86_64-linux-gnu/libcrypto.so appdir/lib ### Download current version of linuxdeployqt -wget -c https://github.com/probonopd/linuxdeployqt/releases/download/continuous/linuxdeployqt-continuous-x86_64.AppImage +wget --no-verbose -c https://github.com/probonopd/linuxdeployqt/releases/download/continuous/linuxdeployqt-continuous-x86_64.AppImage chmod a+x linuxdeployqt-continuous-x86_64.AppImage ### Deploy to appdir and generate AppImage