8 changes: 7 additions & 1 deletion src/Train/BT40Device.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include <QQueue>

#include "CalibrationData.h"
#include "Ftms.h"

typedef struct btle_sensor_type {
const char *descriptive_name;
Expand Down Expand Up @@ -97,22 +98,27 @@ private slots:
double windResistance;
double wheelSize;
bool has_power;
bool has_controllable_service;
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<QByteArray> commandQueue;
int commandRetry;

// FTMS Device Configuration
FtmsDeviceInformation ftmsDeviceInfo;

bool connected;
void getCadence(QDataStream& ds);
void getWheelRpm(QDataStream& ds);
void setLoadErg(double);
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();
Expand Down
103 changes: 103 additions & 0 deletions src/Train/Ftms.cpp
Original file line number Diff line number Diff line change
@@ -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;
}
135 changes: 135 additions & 0 deletions src/Train/Ftms.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
#ifndef FTMS_H
#define FTMS_H
#include <QDataStream>
#include <QBluetoothUuid>

// 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

static const QBluetoothUuid s_FtmsService_UUID = QBluetoothUuid((quint16)FTMSDEVICE_FTMS_UUID);
static const QBluetoothUuid s_FtmsIndoorBikeChar_UUID = QBluetoothUuid((quint16)FTMSDEVICE_INDOOR_BIKE_CHAR_UUID);
static const QBluetoothUuid s_FtmsPowerRangeChar_UUID = QBluetoothUuid((quint16)FTMSDEVICE_POWER_RANGE_CHAR_UUID);
static const QBluetoothUuid s_FtmsResistanceRangeChar_UUID = QBluetoothUuid((quint16)FTMSDEVICE_RESISTANCE_RANGE_CHAR_UUID);
static const QBluetoothUuid s_FtmsFeatureChar_UUID = QBluetoothUuid((quint16)FTMSDEVICE_FTMS_FEATURE_CHAR_UUID);
static const QBluetoothUuid s_FtmsControlPointChar_UUID = QBluetoothUuid((quint16)FTMSDEVICE_FTMS_CONTROL_POINT_CHAR_UUID);

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
2 changes: 2 additions & 0 deletions src/src.pro
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down