Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 23 additions & 6 deletions src/devices/bluetooth.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
bool snode_bike = settings.value(QZSettings::snode_bike, QZSettings::default_snode_bike).toBool();
bool fitplus_bike = settings.value(QZSettings::fitplus_bike, QZSettings::default_fitplus_bike).toBool() ||
settings.value(QZSettings::virtufit_etappe, QZSettings::default_virtufit_etappe).toBool();
bool fitshow_rower = settings.value(QZSettings::fitshow_rower, QZSettings::default_fitshow_rower).toBool();
bool csc_as_bike =
settings.value(QZSettings::cadence_sensor_as_bike, QZSettings::default_cadence_sensor_as_bike).toBool();
bool csc_as_treadmill =
Expand Down Expand Up @@ -1262,7 +1263,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
QRegularExpression(QStringLiteral("TRUE ELLIPTICAL \\d{3,}$")).match(b.name().toUpper()).hasMatch()) || // TRUE ELLIPTICAL followed by digits
(b.name().toUpper().startsWith(QStringLiteral("E35")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) ||
(b.name().toUpper().startsWith(QStringLiteral("E25")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) ||
(b.name().startsWith(QStringLiteral("FS-")) &&
(b.name().startsWith(QStringLiteral("FS-")) && !fitshow_rower &&
(iconsole_elliptical || settings.value(QZSettings::gymstick_gx6_0_elliptical,
QZSettings::default_gymstick_gx6_0_elliptical).toBool())) ||
!b.name().compare(ftms_elliptical, Qt::CaseInsensitive)) && !ypooElliptical && !horizonTreadmill && ftms_bike.contains(QZSettings::default_ftms_bike) && filter) {
Expand Down Expand Up @@ -1896,7 +1897,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
// SLOT(inclinationChanged(double)));
npeCableBike->deviceDiscovered(b);
this->signalBluetoothDeviceConnected(npeCableBike);
} else if (((b.name().startsWith("FS-") && hammerRacerS) ||
} else if (((b.name().startsWith("FS-") && hammerRacerS && !fitshow_rower) ||
(b.name().toUpper().startsWith(QStringLiteral("ICONSOLE+")) && toorx_ftms ) ||
(b.name().toUpper().startsWith("DI") && b.name().length() == 2) || // Elite smart trainer #1682
(b.name().toUpper().startsWith("DHZ-")) || // JK fitness 577
Expand Down Expand Up @@ -2008,7 +2009,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
(b.name().toUpper().startsWith("BESP-")) || // FITFIU BESP 250 indoor bike
(b.name().toUpper().startsWith("GLT") && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) ||
(b.name().toUpper().startsWith("SPORT01-") && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) || // Labgrey Magnetic Exercise Bike https://www.amazon.co.uk/dp/B0CXMF1NPY?_encoding=UTF8&psc=1&ref=cm_sw_r_cp_ud_dp_PE420HA7RD7WJBZPN075&ref_=cm_sw_r_cp_ud_dp_PE420HA7RD7WJBZPN075&social_share=cm_sw_r_cp_ud_dp_PE420HA7RD7WJBZPN075&skipTwisterOG=1
(b.name().toUpper().startsWith("FS-YK-")) ||
(b.name().toUpper().startsWith("FS-YK-") && !fitshow_rower) ||
(b.name().toUpper().startsWith("T600E_")) ||
(b.name().toUpper().startsWith("SPEEDBIKE S2")) || // Maxxus Speedbike S2
(b.name().toUpper().startsWith("B56-")) || // Titan Life B56 bike
Expand Down Expand Up @@ -2476,6 +2477,16 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
// SLOT(inclinationChanged(double)));
sportsTechElliptical->deviceDiscovered(b);
this->signalBluetoothDeviceConnected(sportsTechElliptical);
} else if (fitshow_rower && fitshowrower::isFitshowRowerDeviceName(b.name()) && !fitShowRower && filter) {
this->setLastBluetoothDevice(b);
this->stopDiscovery();
fitShowRower = new fitshowrower(noWriteResistance, noHeartService);
emit deviceConnected(b);
connect(fitShowRower, &bluetoothdevice::connectedAndDiscovered, this,
&bluetooth::connectedAndDiscovered);
connect(fitShowRower, &fitshowrower::debug, this, &bluetooth::debug);
fitShowRower->deviceDiscovered(b);
this->signalBluetoothDeviceConnected(fitShowRower);
} else if (b.name().toUpper().startsWith(QStringLiteral("EW-ST-")) && !sportsTechRower && filter) {
this->setLastBluetoothDevice(b);
this->stopDiscovery();
Expand Down Expand Up @@ -2922,7 +2933,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
connect(pafersBike, SIGNAL(debug(QString)), this, SLOT(debug(QString)));
pafersBike->deviceDiscovered(b);
this->signalBluetoothDeviceConnected(pafersBike);
} else if (((b.name().startsWith(QStringLiteral("FS-")) && snode_bike) ||
} else if (((b.name().startsWith(QStringLiteral("FS-")) && snode_bike && !fitshow_rower) ||
(b.name().toUpper().startsWith(QStringLiteral("TF-")) && !b.name().toUpper().startsWith(QStringLiteral("TF-T")) &&
!horizon_treadmill_force_ftms)) && // TF-769DF2
!snodeBike &&
Expand All @@ -2936,7 +2947,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
connect(snodeBike, &snodebike::debug, this, &bluetooth::debug);
snodeBike->deviceDiscovered(b);
this->signalBluetoothDeviceConnected(snodeBike);
} else if (((b.name().startsWith(QStringLiteral("FS-")) && fitplus_bike) ||
} else if (((b.name().startsWith(QStringLiteral("FS-")) && fitplus_bike && !fitshow_rower) ||
b.name().startsWith(QStringLiteral("X100-")) ||
(b.name().toUpper().startsWith("H9110 OSAKA")) ||
b.name().startsWith(QStringLiteral("MRK-"))) &&
Expand Down Expand Up @@ -2967,7 +2978,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
if (this->discoveryAgent && !this->discoveryAgent->isActive())
emit searchingStop();
this->signalBluetoothDeviceConnected(focusTreadmill);
} else if (((b.name().startsWith(QStringLiteral("FS-")) && !horizonTreadmill && !snode_bike && !fitplus_bike && !ftmsBike && !iconsole_elliptical) ||
} else if (((b.name().startsWith(QStringLiteral("FS-")) && !fitshow_rower && !horizonTreadmill && !snode_bike && !fitplus_bike && !ftmsBike && !iconsole_elliptical) ||
(b.name().toUpper().startsWith(QStringLiteral("TR510-T"))) ||
(b.name().toUpper().startsWith(QStringLiteral("NOBLEPRO CONNECT")) && !deviceHasService(b, QBluetoothUuid((quint16)0x1826))) || // FTMS
(b.name().startsWith(QStringLiteral("SW")) && b.name().length() == 14 &&
Expand Down Expand Up @@ -4279,6 +4290,10 @@ void bluetooth::restart() {
delete sportsTechRower;
sportsTechRower = nullptr;
}
if (fitShowRower) {
delete fitShowRower;
fitShowRower = nullptr;
}
if (sportsPlusBike) {

delete sportsPlusBike;
Expand Down Expand Up @@ -4652,6 +4667,8 @@ bluetoothdevice *bluetooth::device() {
return sportsTechElliptical;
} else if (sportsTechRower) {
return sportsTechRower;
} else if (fitShowRower) {
return fitShowRower;
} else if (sportsPlusBike) {
return sportsPlusBike;
} else if (sportsPlusRower) {
Expand Down
2 changes: 2 additions & 0 deletions src/devices/bluetooth.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
#include "devices/faketreadmill/faketreadmill.h"
#include "devices/fitmetria_fanfit/fitmetria_fanfit.h"
#include "devices/fitplusbike/fitplusbike.h"
#include "devices/fitshowrower/fitshowrower.h"

#include "devices/fitshowtreadmill/fitshowtreadmill.h"
#include "devices/flywheelbike/flywheelbike.h"
Expand Down Expand Up @@ -282,6 +283,7 @@ class bluetooth : public QObject, public SignalHandler {
schwinn170bike *schwinn170Bike = nullptr;
chronobike *chronoBike = nullptr;
fitplusbike *fitPlusBike = nullptr;
fitshowrower *fitShowRower = nullptr;
echelonrower *echelonRower = nullptr;
ftmsrower *ftmsRower = nullptr;
smartrowrower *smartrowRower = nullptr;
Expand Down
253 changes: 253 additions & 0 deletions src/devices/fitshowrower/fitshowrower.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
#include "fitshowrower.h"

#ifdef Q_OS_ANDROID
#include "keepawakehelper.h"
#endif
#include "qzsettings.h"
#include "virtualdevices/virtualbike.h"
#include "virtualdevices/virtualrower.h"

#include <QMetaEnum>
#include <QSettings>

namespace {
quint16 littleEndian16(const QByteArray &data, int offset) {
return static_cast<quint8>(data.at(offset)) | (static_cast<quint16>(static_cast<quint8>(data.at(offset + 1))) << 8);
}
} // namespace

fitshowrower::fitshowrower(bool noWriteResistance, bool noHeartService)
: noWriteResistance(noWriteResistance), noHeartService(noHeartService) {
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
connect(&refresh, &QTimer::timeout, this, &fitshowrower::update);
refresh.start(500);

initializationFrames << QByteArray::fromHex("0241024303") << QByteArray::fromHex("0241024303")
<< QByteArray::fromHex("0243014203") << QByteArray::fromHex("02424203")
<< QByteArray::fromHex("0244014503") << QByteArray::fromHex("0244024603");
}

bool fitshowrower::isFitshowRowerDeviceName(const QString &name) {
return name.startsWith(QStringLiteral("FS-"), Qt::CaseInsensitive);
}

bool fitshowrower::validFrame(const QByteArray &frame) {
if (frame.size() < 4 || static_cast<quint8>(frame.front()) != 0x02 || static_cast<quint8>(frame.back()) != 0x03) {
return false;
}
quint8 checksum = 0;
for (int i = 1; i < frame.size() - 2; ++i) {
checksum ^= static_cast<quint8>(frame.at(i));
}
return checksum == static_cast<quint8>(frame.at(frame.size() - 2));
}

fitshowrower::Packet fitshowrower::parsePacket(const QByteArray &frame) {
Packet packet;
if (!validFrame(frame)) {
return packet;
}

packet.command = static_cast<quint8>(frame.at(1));
if (packet.command == 0x41 && frame.size() == 9 && static_cast<quint8>(frame.at(2)) == 0x02) {
packet.subcommand = 0x02;
packet.maxResistance = static_cast<quint8>(frame.at(3));
packet.maxIncline = static_cast<quint8>(frame.at(4));
} else if (packet.command == 0x42 && frame.size() == 5) {
packet.status = static_cast<quint8>(frame.at(2));
} else if (packet.command == 0x42 && frame.size() == 15) {
packet.status = static_cast<quint8>(frame.at(2));
packet.cadence = littleEndian16(frame, 6);
packet.heartRate = static_cast<quint8>(frame.at(8));
packet.power = littleEndian16(frame, 9) / 10.0;
} else if (packet.command == 0x43 && frame.size() == 13 && static_cast<quint8>(frame.at(2)) == 0x01) {
packet.subcommand = 0x01;
packet.elapsedSeconds = littleEndian16(frame, 3);
quint16 distance = littleEndian16(frame, 5);
packet.distanceMeters = (distance & 0x7fff) * ((distance & 0x8000) ? 10 : 1);
packet.calories = littleEndian16(frame, 7) / 10.0;
packet.strokeCount = littleEndian16(frame, 9);
} else if (packet.command == 0x44 && frame.size() >= 5) {
packet.subcommand = static_cast<quint8>(frame.at(2));
} else {
return Packet();
}
packet.valid = true;
return packet;
}

void fitshowrower::applyPacket(const Packet &packet) {
if (packet.command == 0x41) {
emit debug(QStringLiteral("FitShow capabilities: max resistance %1, max incline %2")
.arg(packet.maxResistance)
.arg(packet.maxIncline));
return;
}
if (packet.command == 0x42) {
const bool running = packet.status == 0x02;
Cadence = running ? packet.cadence : 0;
m_watt = running ? packet.power : 0;
Speed = 0; // The Topiom speed field has no verified unit; distance comes from command 0x43.

QSettings settings;
#ifdef Q_OS_ANDROID
if (settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool()) {
Heart = static_cast<quint8>(KeepAwakeHelper::heart());
} else
#endif
if (packet.heartRate &&
settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name)
.toString()
.startsWith(QStringLiteral("Disabled"))) {
Heart = packet.heartRate;
}
emit debug(QStringLiteral("FitShow status %1, cadence %2 SPM, power %3 W, heart %4")
.arg(packet.status)
.arg(Cadence.value())
.arg(m_watt.value())
.arg(Heart.value()));
} else if (packet.command == 0x43) {
if (packet.strokeCount > StrokesCount.value()) {
lastStroke = QDateTime::currentDateTime();
}
elapsed = packet.elapsedSeconds;
Distance = packet.distanceMeters / 1000.0;
KCal = packet.calories;
StrokesCount = packet.strokeCount;
emit debug(QStringLiteral("FitShow cumulative: elapsed %1 s, distance %2 m, calories %3, strokes %4")
.arg(packet.elapsedSeconds)
.arg(packet.distanceMeters)
.arg(packet.calories)
.arg(packet.strokeCount));
}
}

void fitshowrower::characteristicChanged(const QLowEnergyCharacteristic &, const QByteArray &value) {
emit debug(QStringLiteral(" << ") + value.toHex(' '));
const Packet packet = parsePacket(value);
if (!packet.valid) {
emit debug(QStringLiteral("Ignoring malformed FitShow frame: ") + value.toHex(' '));
return;
}
applyPacket(packet);
}

void fitshowrower::writeFrame(const QByteArray &frame, const QString &description) {
if (!communicationService || !writeCharacteristic.isValid()) {
emit debug(QStringLiteral("Cannot write FitShow frame: FFF2 is unavailable"));
return;
}
const auto mode = (writeCharacteristic.properties() & QLowEnergyCharacteristic::WriteNoResponse)
? QLowEnergyService::WriteWithoutResponse
: QLowEnergyService::WriteWithResponse;
communicationService->writeCharacteristic(writeCharacteristic, frame, mode);
emit debug(QStringLiteral(" >> ") + frame.toHex(' ') + QStringLiteral(" // ") + description);
}

void fitshowrower::update() {
if (!m_control || m_control->state() == QLowEnergyController::UnconnectedState) {
return;
}
if (!notificationsEnabled) {
return;
}
if (initializationIndex < initializationFrames.size()) {
writeFrame(initializationFrames.at(initializationIndex++), QStringLiteral("initialization"));
initialized = initializationIndex == initializationFrames.size();
return;
}
if (initialized) {
writeFrame(pollStatus ? QByteArray::fromHex("02424203") : QByteArray::fromHex("0243014203"),
pollStatus ? QStringLiteral("live status poll") : QStringLiteral("cumulative data poll"));
pollStatus = !pollStatus;
update_metrics(false, 0);
}
}

void fitshowrower::serviceStateChanged(QLowEnergyService::ServiceState state) {
emit debug(QStringLiteral("FitShow service state: ") + QString::number(state));
if (state != QLowEnergyService::ServiceDiscovered)
return;
writeCharacteristic =
communicationService->characteristic(QBluetoothUuid(QStringLiteral("0000fff2-0000-1000-8000-00805f9b34fb")));
notifyCharacteristic =
communicationService->characteristic(QBluetoothUuid(QStringLiteral("0000fff1-0000-1000-8000-00805f9b34fb")));
if (!writeCharacteristic.isValid() || !notifyCharacteristic.isValid()) {
emit debug(QStringLiteral("FitShow FFF1 notify or FFF2 write characteristic is missing"));
return;
}
connect(communicationService, &QLowEnergyService::characteristicChanged, this,
&fitshowrower::characteristicChanged);
connect(communicationService, &QLowEnergyService::descriptorWritten, this, &fitshowrower::descriptorWritten);
connect(communicationService,
static_cast<void (QLowEnergyService::*)(QLowEnergyService::ServiceError)>(&QLowEnergyService::error), this,
&fitshowrower::serviceError);
const QLowEnergyDescriptor ccc = notifyCharacteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration);
if (!ccc.isValid()) {
emit debug(QStringLiteral("FitShow FFF1 CCC descriptor is missing"));
return;
}
communicationService->writeDescriptor(ccc, QByteArray::fromHex("0100"));
}

void fitshowrower::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &value) {
emit debug(QStringLiteral("descriptorWritten %1 %2").arg(descriptor.name(), QString(value.toHex(' '))));
notificationsEnabled = true;
createVirtualDevice();
emit connectedAndDiscovered();
}

void fitshowrower::createVirtualDevice() {
if (hasVirtualDevice())
return;
QSettings settings;
if (!settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool())
return;
if (settings.value(QZSettings::virtual_device_rower, QZSettings::default_virtual_device_rower).toBool()) {
setVirtualDevice(new virtualrower(this, noWriteResistance, noHeartService), VIRTUAL_DEVICE_MODE::PRIMARY);
} else {
auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService);
connect(virtualBike, &virtualbike::changeInclination, this, &fitshowrower::changeInclination);
setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY);
}
}

void fitshowrower::serviceScanDone() {
communicationService =
m_control->createServiceObject(QBluetoothUuid(QStringLiteral("0000fff0-0000-1000-8000-00805f9b34fb")), this);
if (!communicationService) {
emit debug(QStringLiteral("FitShow FFF0 service not found"));
return;
}
connect(communicationService, &QLowEnergyService::stateChanged, this, &fitshowrower::serviceStateChanged);
communicationService->discoverDetails();
}

void fitshowrower::serviceDiscovered(const QBluetoothUuid &service) {
emit debug(QStringLiteral("serviceDiscovered ") + service.toString());
}

void fitshowrower::deviceDiscovered(const QBluetoothDeviceInfo &device) {
bluetoothDevice = device;
emit debug(QStringLiteral("Found Topiom rower: ") + device.name());
m_control = QLowEnergyController::createCentral(device, this);
connect(m_control, &QLowEnergyController::serviceDiscovered, this, &fitshowrower::serviceDiscovered);
connect(m_control, &QLowEnergyController::discoveryFinished, this, &fitshowrower::serviceScanDone);
connect(m_control,
static_cast<void (QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error),
this, &fitshowrower::controllerError);
connect(m_control, &QLowEnergyController::connected, m_control, &QLowEnergyController::discoverServices);
connect(m_control, &QLowEnergyController::disconnected, this, &fitshowrower::disconnected);
m_control->connectToDevice();
}

void fitshowrower::controllerError(QLowEnergyController::Error error) {
emit debug(QStringLiteral("FitShow controller error %1: %2").arg(error).arg(m_control->errorString()));
}

void fitshowrower::serviceError(QLowEnergyService::ServiceError error) {
emit debug(QStringLiteral("FitShow service error %1").arg(error));
}

bool fitshowrower::connected() { return m_control && m_control->state() == QLowEnergyController::DiscoveredState; }
Loading
Loading