Compare commits

...

26 Commits

Author SHA1 Message Date
Roberto Viola
9e2b4a7883 Merge branch 'master' into ProForm-Gen-1-Connection-Issue-#1825 2023-12-27 04:31:16 +00:00
Roberto Viola
31fe6eedb7 Update qzsettings.cpp 2023-12-26 09:37:34 +01:00
Roberto Viola
af7d797642 Merge branch 'master' into ProForm-Gen-1-Connection-Issue-#1825 2023-12-26 08:57:16 +01:00
Roberto Viola
723f8d0588 Update proformtelnetbike.cpp 2023-12-26 08:54:49 +01:00
Roberto Viola
b618a85995 Update proformtelnetbike.cpp 2023-12-25 12:00:07 +01:00
Roberto Viola
31272e66ee Update proformtelnetbike.cpp 2023-12-24 15:44:14 +00:00
Roberto Viola
19ece0e7e5 Update proformtelnetbike.cpp 2023-12-24 12:22:20 +01:00
Roberto Viola
372c5d3711 Update proformtelnetbike.cpp 2023-12-23 11:32:11 +01:00
Roberto Viola
75ca498f2d Merge branch 'master' into ProForm-Gen-1-Connection-Issue-#1825 2023-12-23 10:06:04 +01:00
Roberto Viola
1bbbba1283 Merge branch 'ProForm-Gen-1-Connection-Issue-#1825' of https://github.com/cagnulein/qdomyos-zwift into ProForm-Gen-1-Connection-Issue-#1825 2023-12-21 10:21:46 +01:00
Roberto Viola
7448e8ec4b Update bluetooth.cpp 2023-12-21 10:21:34 +01:00
Roberto Viola
8fdf541c75 Merge branch 'master' into ProForm-Gen-1-Connection-Issue-#1825 2023-12-21 08:53:28 +00:00
Roberto Viola
1248a610ac Update bluetooth.cpp 2023-12-21 09:50:58 +01:00
Roberto Viola
add8fd7041 preparing to merge to master 2023-12-21 09:50:18 +01:00
Roberto Viola
318e8842ba Update proformtelnetbike.cpp 2023-12-20 14:13:00 +01:00
Roberto Viola
d157dbc385 Update proformtelnetbike.cpp 2023-12-20 13:34:37 +01:00
Roberto Viola
8933a5b139 build issue 2023-12-19 13:52:44 +01:00
Roberto Viola
1b31b116b4 Update proformtelnetbike.h 2023-12-19 10:47:06 +01:00
Roberto Viola
5b863b8d13 Update proformtelnetbike.cpp 2023-12-19 10:39:07 +01:00
Roberto Viola
8d2178a129 Update proformtelnetbike.cpp 2023-12-18 20:46:45 +01:00
Roberto Viola
030c6fdeeb Update proformtelnetbike.cpp 2023-12-08 11:48:38 +01:00
Roberto Viola
62ceb953b8 Update proformtelnetbike.cpp 2023-12-07 14:36:09 +01:00
Roberto Viola
4ae80ab492 asking watt, speed and cadence 2023-12-04 10:04:22 +01:00
Roberto Viola
c6fbdc4773 Update proformtelnetbike.cpp 2023-12-01 16:17:54 +01:00
Roberto Viola
5951650e27 first build 2023-11-30 15:00:44 +01:00
Roberto Viola
33ca78f302 files added 2023-11-30 13:39:17 +01:00
10 changed files with 1316 additions and 2 deletions

522
src/QTelnet.cpp Normal file
View File

@@ -0,0 +1,522 @@
#include "QTelnet.h"
#include "QTelnet.h"
#include <QHostAddress>
const char QTelnet::IACWILL[2] = { IAC, WILL };
const char QTelnet::IACWONT[2] = { IAC, WONT };
const char QTelnet::IACDO[2] = { IAC, DO };
const char QTelnet::IACDONT[2] = { IAC, DONT };
const char QTelnet::IACSB[2] = { IAC, SB };
const char QTelnet::IACSE[2] = { IAC, SE };
char QTelnet::_sendCodeArray[2] = { IAC, 0 };
char QTelnet::_arrCRLF[2] = { 13, 10 };
char QTelnet::_arrCR[2] = { 13, 0 };
QTelnet::QTelnet(QObject *parent) :
QTcpSocket(parent), m_actualSB(0)
{
connect( this, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(socketError(QAbstractSocket::SocketError)) );
connect( this, SIGNAL(readyRead()), this, SLOT(onReadyRead()) );
}
QString QTelnet::peerInfo() const
{
return QString("%1 (%2):%3").arg(peerName()).arg(peerAddress().toString()).arg(peerPort());
}
bool QTelnet::isConnected() const
{
return state() == QAbstractSocket::ConnectedState;
}
bool QTelnet::testBinaryMode() const
{
return m_receivedDX[(unsigned char)TELOPT_BINARY] == DO;
}
void QTelnet::connectToHost(const QString &host, quint16 port)
{
if( !isConnected() )
{
resetProtocol();
abort();
QTcpSocket::connectToHost(host, port);
}
}
void QTelnet::sendData(const QByteArray &ba)
{
if( isConnected() )
transpose( ba.constData(), ba.count() );
}
void QTelnet::socketError(QAbstractSocket::SocketError err)
{
Q_UNUSED(err);
disconnectFromHost();
}
void QTelnet::write(const char c)
{
QTcpSocket::write( (char*)&c, 1 );
}
void QTelnet::setCustomCR(char cr, char cr2)
{
_arrCR[0] = cr;
_arrCR[1] = cr2;
}
void QTelnet::setCustomCRLF(char lf, char cr)
{
_arrCR[0] = lf;
_arrCR[1] = cr;
}
// Envia el codigo de control al servidor.
void QTelnet::sendTelnetControl(char codigo)
{
_sendCodeArray[1] = codigo;
QTcpSocket::write(_sendCodeArray, 2);
}
void QTelnet::writeCustomCRLF()
{
QTcpSocket::write(_arrCRLF, 2);
}
void QTelnet::writeCustomCR()
{
QTcpSocket::write(_arrCR, 2);
}
/// Resetea los datos del protocolo. Debe llamarse cada vez que se inicia una conexión nueva.
void QTelnet::resetProtocol()
{
for( int i = 0; i < 256; i++ )
{
m_receivedDX[i] =
m_receivedWX[i] =
m_sentDX[i] =
m_sentWX[i] = 0;
m_negotiationState = STATE_DATA;
m_buffSB.clear();
m_actualSB = 0;
}
m_oldWinSize.setHeight(-1);
m_oldWinSize.setWidth(-1);
}
void QTelnet::sendSB(char code, char *arr, int iLen)
{
write(IAC);
write(SB);
write(code);
QTcpSocket::write(arr, iLen);
write(IAC);
write(SE);
}
void QTelnet::sendWindowSize()
{
if( isConnected() && (m_receivedDX[TELOPT_NAWS] == DO) && (m_oldWinSize != m_winSize) )
{
char size[4];
m_oldWinSize = m_winSize;
size[0] = (m_winSize.width()>>8) & 0xFF;
size[1] = m_winSize.width() & 0xFF;
size[2] = (m_winSize.height()>>8) & 0xFF;
size[3] = m_winSize.height() & 0xFF;
sendSB(TELOPT_NAWS, size, 4);
}
}
// Handle an incoming IAC SB type chars IAC SE
void QTelnet::handleSB()
{
switch( m_actualSB )
{
case TELOPT_TTYPE:
if( (m_buffSB.count() > 0) && ((unsigned char)m_buffSB[0] == (unsigned char)TELQUAL_SEND) )
{
QTcpSocket::write(IACSB, 2);
write(TELOPT_TTYPE);
write(TELQUAL_IS);
/* FIXME: need more logic here if we use
* more than one terminal type
*/
QTcpSocket::write("SiraggaTerminal", 15);
QTcpSocket::write(IACSE, 2);
}
break;
}
}
// Analiza el texto saliente para que cumpla las normas del protocolo.
// Además ya lo escribe en el socket.
void QTelnet::transpose(const char *buf, int iLen)
{
for( int i = 0; i < iLen; i++ )
{
switch( buf[i] )
{
case IAC:
// Escape IAC twice in stream ... to be telnet protocol compliant
// this is there in binary and non-binary mode.
write(IAC);
write(IAC);
break;
case 10: // \n
// We need to heed RFC 854. LF (\n) is 10, CR (\r) is 13
// we assume that the Terminal sends \n for lf+cr and \r for just cr
// linefeed+carriage return is CR LF
// En modo binario no se traduce nada.
if( testBinaryMode() )
write(buf[i]);
else
writeCustomCRLF();
break;
case 13: // \r
// carriage return is CR NUL */
// En modo binario no se traduce nada.
if( testBinaryMode() )
write(buf[i]);
else
writeCustomCR();
break;
default:
// all other characters are just copied
write(buf[i]);
break;
}
}
}
void QTelnet::willsReply(char action, char reply)
{
if( (reply != m_sentDX[(unsigned char)action]) || (WILL != m_receivedWX[(unsigned char)action]) )
{
write(IAC);
write(reply);
write(action);
m_sentDX[(unsigned char)action] = reply;
m_receivedWX[(unsigned char)action] = WILL;
}
}
void QTelnet::wontsReply(char action, char reply)
{
if( (reply != m_sentDX[(unsigned char)action]) || (WONT != m_receivedWX[(unsigned char)action]) )
{
write(IAC);
write(reply);
write(action);
m_sentDX[(unsigned char)action] = reply;
m_receivedWX[(unsigned char)action] = WONT;
}
}
void QTelnet::doesReply(char action, char reply)
{
if( (reply != m_sentWX[(unsigned char)action]) || (DO != m_receivedDX[(unsigned char)action]) )
{
write(IAC);
write(reply);
write(action);
m_sentWX[(unsigned char)action] = reply;
m_receivedDX[(unsigned char)action] = DO;
}
}
void QTelnet::dontsReply(char action, char reply)
{
if( (reply != m_sentWX[(unsigned char)action]) || (DONT != m_receivedDX[(unsigned char)action]) )
{
write(IAC);
write(reply);
write(action);
m_sentWX[(unsigned char)action] = reply;
m_receivedDX[(unsigned char)action] = DONT;
}
}
// Analiza el buffer de entrada colocá ndolo en el buffer de procesado usando el protocolo telnet.
qint64 QTelnet::doTelnetInProtocol(qint64 buffSize)
{
qint64 iIn, iOut;
char b;
for( iIn = 0, iOut = 0; iIn < buffSize; iIn++ )
{
b = m_buffIncoming[iIn];
switch( m_negotiationState )
{
case STATE_DATA:
switch( b )
{
case IAC:
m_negotiationState = STATE_IAC;
break;
case '\r':
m_negotiationState = STATE_DATAR;
break;
case '\n':
m_negotiationState = STATE_DATAN;
break;
default:
m_buffProcessed[iOut++] = b;
break;
}
break;
case STATE_DATAN:
case STATE_DATAR:
switch( b )
{
case IAC:
m_negotiationState = STATE_IAC;
break;
case '\r':
case '\n':
m_buffProcessed[iOut++] = '\n';
m_negotiationState = STATE_DATA;
break;
default:
m_buffProcessed[iOut++] = b;
m_negotiationState = STATE_DATA;
break;
}
break;
case STATE_IAC:
switch( b )
{
case IAC: // Dos IAC seguidos, se intenta enviar un caracter con el valor IAC.
m_negotiationState = STATE_DATA;
m_buffProcessed[iOut++] = IAC;
break;
case WILL:
m_negotiationState = STATE_IACWILL;
break;
case WONT:
m_negotiationState = STATE_IACWONT;
break;
case DONT:
m_negotiationState = STATE_IACDONT;
break;
case DO:
m_negotiationState = STATE_IACDO;
break;
case EOR:
emitEndOfRecord();
m_negotiationState = STATE_DATA;
break;
case SB:
m_negotiationState = STATE_IACSB;
m_buffSB.clear();
break;
default:
m_negotiationState = STATE_DATA;
break;
}
break;
case STATE_IACWILL:
switch( b )
{
case TELOPT_ECHO:
emitEchoLocal(false);
willsReply(b, DO);
break;
case TELOPT_SGA:
willsReply(b, DO);
break;
case TELOPT_EOR:
willsReply(b, DO);
break;
case TELOPT_BINARY:
willsReply(b, DO);
break;
default:
willsReply(b, DONT);
break;
}
m_negotiationState = STATE_DATA;
break;
case STATE_IACWONT:
switch(b)
{
case TELOPT_ECHO:
emitEchoLocal(true);
wontsReply(b, DONT);
break;
case TELOPT_SGA:
wontsReply(b, DONT);
break;
case TELOPT_EOR:
wontsReply(b, DONT);
break;
case TELOPT_BINARY:
wontsReply(b, DONT);
break;
default:
wontsReply(b, DONT);
break;
}
m_negotiationState = STATE_DATA;
break;
case STATE_IACDO:
switch( b )
{
case TELOPT_ECHO:
doesReply(b, WILL);
emitEchoLocal(true);
break;
case TELOPT_SGA:
doesReply(b, WILL);
break;
case TELOPT_TTYPE:
doesReply(b, WILL);
break;
case TELOPT_BINARY:
doesReply(b, WILL);
break;
case TELOPT_NAWS:
m_receivedDX[(unsigned char)b] = (unsigned char)DO;
m_sentWX[(unsigned char)b] = (unsigned char)WILL;
write(IAC);
write(WILL);
write(b);
// Enviamos el tamaño de la pantalla.
sendWindowSize();
break;
default:
doesReply(b, WONT);
break;
}
m_negotiationState = STATE_DATA;
break;
case STATE_IACDONT:
switch (b)
{
case TELOPT_ECHO:
dontsReply(b, WONT);
emitEchoLocal(false);
break;
case TELOPT_SGA:
dontsReply(b, WONT);
break;
case TELOPT_NAWS:
dontsReply(b, WONT);
break;
case TELOPT_BINARY:
dontsReply(b, WONT);
break;
default:
dontsReply(b, WONT);
break;
}
m_negotiationState = STATE_DATA;
break;
case STATE_IACSB:
switch( b )
{
case IAC:
// Entramos en estado IAC en la sub-negociación.
m_negotiationState = STATE_IACSBIAC;
break;
default:
// Iniciamos la sub-negociación.
m_buffSB.clear();
m_actualSB = b;
m_negotiationState = STATE_IACSBDATA;
break;
}
break;
case STATE_IACSBDATA: // Estamos en datos de la subnegociación.
switch( b )
{
case IAC:
m_negotiationState = STATE_IACSBDATAIAC;
break;
default:
m_buffSB.append(b);
break;
}
break;
case STATE_IACSBIAC:
switch( b )
{
case IAC:
// Reiniciamos la sub-negociación.
m_buffSB.clear();
m_actualSB = b;
m_negotiationState = STATE_IACSBDATA;
default:
// Salimos de la sub-negociación.
m_negotiationState = STATE_DATA;
}
break;
case STATE_IACSBDATAIAC:
switch( b )
{
case IAC:
m_negotiationState = STATE_IACSBDATA;
m_buffSB.append(IAC);
break;
case SE:
handleSB();
m_actualSB = 0;
m_buffSB.clear();
m_negotiationState = STATE_DATA;
break;
case SB:
handleSB();
m_buffSB.clear();
m_negotiationState = STATE_IACSB;
break;
default:
m_buffSB.clear();
m_actualSB = 0;
m_negotiationState = STATE_DATA;
break;
}
break;
default:
m_negotiationState = STATE_DATA;
break;
}
}
return iOut;
}
void QTelnet::onReadyRead()
{
qint64 readed;
qint64 processed;
while( (readed = read(m_buffIncoming, IncommingBufferSize)) != 0 )
{
switch( readed )
{
case -1:
disconnectFromHost();
break;
default:
processed = doTelnetInProtocol(readed);
if( processed > 0 )
Q_EMIT(newData(m_buffProcessed, processed));
break;
}
}
}

136
src/QTelnet.h Normal file
View File

@@ -0,0 +1,136 @@
#ifndef QTELNET_H
#define QTELNET_H
#include <QObject>
#include <qtcpsocket.h>
#include <qsize.h>
#include <QString>
#define IncommingBufferSize (1500)
class QTelnet : public QTcpSocket
{
Q_OBJECT
public:
enum SocketStatus
{
Disconnected,
Resolving, // Resolving host
Connecting, // Connecting to host.
Connected // Connected to host.
};
protected:
enum TelnetStateCodes
{
STATE_DATA = (char)0,
STATE_IAC = (char)1,
STATE_IACSB = (char)2,
STATE_IACWILL = (char)3,
STATE_IACDO = (char)4,
STATE_IACWONT = (char)5,
STATE_IACDONT = (char)6,
STATE_IACSBIAC = (char)7,
STATE_IACSBDATA = (char)8,
STATE_IACSBDATAIAC = (char)9,
STATE_DATAR = (char)10,
STATE_DATAN = (char)11
};
enum TelnetCodes
{
// Negociación entrada/salida (cliente<->servidor)
IAC = (char)255, // Inicia la secuencia para la negociación telnet.
EOR = (char)239, // Estando en la negociación, End Of Record.
WILL = (char)251, // Estando en la negociación, Acepta el protocolo?
WONT = (char)252, // Estando en la negociación, Acepta el protocolo?
DO = (char)253, // Estando en la negociación, Protocolo aceptado.
DONT = (char)254, // Estando en la negociación, Protocolo denegado.
SB = (char)250, // Estando en la negociación, inicia secuencia de sub-negociación.
SE = (char)240, // Estando en la sub-negociación, fin de sub-negociación.
// Negociación de salida (cliente->servidor)
TELOPT_BINARY = (char)0, // Estando en la negociación, pide modo binario.
TELOPT_ECHO = (char)1, // Estando en la negociación, pide echo local.
TELOPT_SGA = (char)2, // Estando en la negociación, pide Supress Go Ahead.
TELOPT_EOR = (char)25, // Estando en la negociación, informa End Of Record.
TELOPT_NAWS = (char)31, // Estando en la negociación, Negotiate Abaut Window Size.
TELOPT_TTYPE = (char)24 // Estando en la negociación, Terminal Type.
};
enum TelnetQualifiers
{
TELQUAL_IS = (char)0,
TELQUAL_SEND = (char)1
};
private:
static const char IACWILL[2];
static const char IACWONT[2];
static const char IACDO[2];
static const char IACDONT[2];
static const char IACSB[2];
static const char IACSE[2];
static char _sendCodeArray[2];
static char _arrCRLF[2];
static char _arrCR[2];
QSize m_winSize; // Tamaño de la pantalla en caracteres.
QSize m_oldWinSize; // Tamaño de la pantalla que se envió por última vez al server. Para no enviar el mismo dato.
enum TelnetStateCodes m_negotiationState;
char m_receivedDX[256]; // What IAC DO(NT) request do we have received already ?
char m_receivedWX[256]; // What IAC WILL/WONT request do we have received already ?
char m_sentDX[256]; // What IAC DO/DONT request do we have sent already ?
char m_sentWX[256]; // What IAC WILL/WONT request do we have sent already ?
void resetProtocol();
char m_buffIncoming[IncommingBufferSize];
char m_buffProcessed[IncommingBufferSize];
QByteArray m_buffSB;
int m_actualSB;
void emitEndOfRecord() { Q_EMIT(endOfRecord()); }
void emitEchoLocal(bool bEcho) { Q_EMIT(echoLocal(bEcho)); }
void sendTelnetControl(char codigo);
void handleSB(void);
void transpose(const char *buf, int iLen);
void willsReply(char action, char reply);
void wontsReply(char action, char reply);
void doesReply(char action, char reply);
void dontsReply(char action, char reply);
void sendSB(char code, char *arr, int iLen);
qint64 doTelnetInProtocol(qint64 buffSize);
public:
explicit QTelnet(QObject *parent = 0);
virtual void connectToHost(const QString &host, quint16 port);
void sendData(const QByteArray &ba);
void setCustomCRLF(char lf = 13, char cr = 10);
void setCustomCR(char cr = 10, char cr2 = 0);
void writeCustomCRLF();
void writeCustomCR();
void write(const char c);
bool isConnected() const;
bool testBinaryMode() const;
void setWindSize(QSize s) {m_winSize = s;}
void sendWindowSize();
QString peerInfo()const;
signals:
void newData(const char *buff, int len);
void endOfRecord();
void echoLocal(bool echo);
private slots:
void socketError(QAbstractSocket::SocketError err);
void onReadyRead();
};
#endif // QTELNET_H

View File

@@ -399,6 +399,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
settings.value(QZSettings::fakedevice_treadmill, QZSettings::default_fakedevice_treadmill).toBool();
bool pafers_treadmill = settings.value(QZSettings::pafers_treadmill, QZSettings::default_pafers_treadmill).toBool();
QString proformtdf4ip = settings.value(QZSettings::proformtdf4ip, QZSettings::default_proformtdf4ip).toString();
QString proformtdf1ip = settings.value(QZSettings::proformtdf1ip, QZSettings::default_proformtdf1ip).toString();
QString proformtreadmillip =
settings.value(QZSettings::proformtreadmillip, QZSettings::default_proformtreadmillip).toString();
QString nordictrack_2950_ip =
@@ -656,6 +657,21 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
emit searchingStop();
}
this->signalBluetoothDeviceConnected(proformWifiBike);
} else if (!proformtdf1ip.isEmpty() && !proformTelnetBike) {
this->stopDiscovery();
proformTelnetBike =
new proformtelnetbike(noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain);
emit deviceConnected(b);
connect(proformTelnetBike, &bluetoothdevice::connectedAndDiscovered, this,
&bluetooth::connectedAndDiscovered);
// connect(cscBike, SIGNAL(disconnected()), this, SLOT(restart()));
connect(proformTelnetBike, &proformtelnetbike::debug, this, &bluetooth::debug);
proformTelnetBike->deviceDiscovered(b);
// connect(this, SIGNAL(searchingStop()), cscBike, SLOT(searchingStop())); //NOTE: Commented due to #358
if (this->discoveryAgent && !this->discoveryAgent->isActive()) {
emit searchingStop();
}
this->signalBluetoothDeviceConnected(proformTelnetBike);
#ifndef Q_OS_IOS
} else if (!computrainerSerialPort.isEmpty() && !computrainerBike) {
this->stopDiscovery();
@@ -2552,6 +2568,11 @@ void bluetooth::restart() {
delete proformWifiBike;
proformWifiBike = nullptr;
}
if (proformTelnetBike) {
delete proformTelnetBike;
proformTelnetBike = nullptr;
}
if (proformWifiTreadmill) {
delete proformWifiTreadmill;
@@ -2965,6 +2986,8 @@ bluetoothdevice *bluetooth::device() {
return cscBike;
} else if (proformWifiBike) {
return proformWifiBike;
} else if (proformTelnetBike) {
return proformTelnetBike;
} else if (proformWifiTreadmill) {
return proformWifiTreadmill;
} else if (nordictrackifitadbTreadmill) {

View File

@@ -86,6 +86,7 @@
#include "proformellipticaltrainer.h"
#include "proformrower.h"
#include "proformtreadmill.h"
#include "proformtelnetbike.h"
#include "proformwifibike.h"
#include "proformwifitreadmill.h"
#include "schwinn170bike.h"
@@ -187,6 +188,7 @@ class bluetooth : public QObject, public SignalHandler {
pelotonbike *pelotonBike = nullptr;
proformrower *proformRower = nullptr;
proformbike *proformBike = nullptr;
proformtelnetbike *proformTelnetBike = nullptr;
proformwifibike *proformWifiBike = nullptr;
proformwifitreadmill *proformWifiTreadmill = nullptr;
proformelliptical *proformElliptical = nullptr;

487
src/proformtelnetbike.cpp Normal file
View File

@@ -0,0 +1,487 @@
#include "proformtelnetbike.h"
#ifdef Q_OS_ANDROID
#include "keepawakehelper.h"
#endif
#include "virtualbike.h"
#include <QDateTime>
#include <QFile>
#include <QMetaEnum>
#include <QSettings>
#include <QThread>
#include <QtXml>
#include <chrono>
#include <math.h>
using namespace std::chrono_literals;
proformtelnetbike::proformtelnetbike(bool noWriteResistance, bool noHeartService, uint8_t bikeResistanceOffset,
double bikeResistanceGain) {
QSettings settings;
m_watt.setType(metric::METRIC_WATT);
target_watts.setType(metric::METRIC_WATT);
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);
this->noWriteResistance = noWriteResistance;
this->noHeartService = noHeartService;
this->bikeResistanceGain = bikeResistanceGain;
this->bikeResistanceOffset = bikeResistanceOffset;
initDone = false;
connect(refresh, &QTimer::timeout, this, &proformtelnetbike::update);
refresh->start(200ms);
bool ok = connect(&telnet, &QTelnet::newData, this, &proformtelnetbike::characteristicChanged);
ergModeSupported = true; // IMPORTANT, only for this bike
connectToDevice();
initRequest = true;
// ******************************************* virtual bike init *************************************
if (!firstStateChanged && !this->hasVirtualDevice()
#ifdef Q_OS_IOS
#ifndef IO_UNDER_QT
&& !h
#endif
#endif
) {
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) {
emit debug(QStringLiteral("creating virtual bike interface..."));
auto virtualBike =
new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain);
// connect(virtualBike,&virtualbike::debug ,this,& proformtelnetbike::debug);
connect(virtualBike, &virtualbike::changeInclination, this, &proformtelnetbike::changeInclination);
this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY);
}
}
firstStateChanged = 1;
// ********************************************************************************************************
}
void proformtelnetbike::connectToDevice() {
QSettings settings;
// https://github.com/dawsontoth/zwifit/blob/e846501149a6c8fbb03af8d7b9eab20474624883/src/ifit.js
telnet.connectToHost(settings.value(QZSettings::proformtdf1ip, QZSettings::default_proformtdf1ip).toString(), 23);
telnet.waitForConnected();
telnet.sendData("./utconfig\n");
QThread::sleep(1);
telnet.sendData("2\n"); // modify variables
}
/*
void proformtelnetbike::writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log,
bool wait_for_response) {
QEventLoop loop;
QTimer timeout;
if (wait_for_response) {
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicChanged, &loop, &QEventLoop::quit);
timeout.singleShot(300ms, &loop, &QEventLoop::quit);
} else {
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicWritten, &loop, &QEventLoop::quit);
timeout.singleShot(300ms, &loop, &QEventLoop::quit);
}
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic,
QByteArray((const char *)data, data_len));
if (!disable_log) {
emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') +
QStringLiteral(" // ") + info);
}
loop.exec();
}*/
resistance_t proformtelnetbike::resistanceFromPowerRequest(uint16_t power) {
qDebug() << QStringLiteral("resistanceFromPowerRequest") << Cadence.value();
QSettings settings;
double watt_gain = settings.value(QZSettings::watt_gain, QZSettings::default_watt_gain).toDouble();
double watt_offset = settings.value(QZSettings::watt_offset, QZSettings::default_watt_offset).toDouble();
for (resistance_t i = 1; i < max_resistance; i++) {
if (((wattsFromResistance(i) * watt_gain) + watt_offset) <= power &&
((wattsFromResistance(i + 1) * watt_gain) + watt_offset) >= power) {
qDebug() << QStringLiteral("resistanceFromPowerRequest")
<< ((wattsFromResistance(i) * watt_gain) + watt_offset)
<< ((wattsFromResistance(i + 1) * watt_gain) + watt_offset) << power;
return i;
}
}
if (power < ((wattsFromResistance(1) * watt_gain) + watt_offset))
return 1;
else
return max_resistance;
}
uint16_t proformtelnetbike::wattsFromResistance(resistance_t resistance) {
if (currentCadence().value() == 0)
return 0;
switch (resistance) {
case 0:
case 1:
// -13.5 + 0.999x + 0.00993x²
return (-13.5 + (0.999 * currentCadence().value()) + (0.00993 * pow(currentCadence().value(), 2)));
case 2:
// -17.7 + 1.2x + 0.0116x²
return (-17.7 + (1.2 * currentCadence().value()) + (0.0116 * pow(currentCadence().value(), 2)));
case 3:
// -17.5 + 1.24x + 0.014x²
return (-17.5 + (1.24 * currentCadence().value()) + (0.014 * pow(currentCadence().value(), 2)));
case 4:
// -20.9 + 1.43x + 0.016x²
return (-20.9 + (1.43 * currentCadence().value()) + (0.016 * pow(currentCadence().value(), 2)));
case 5:
// -27.9 + 1.75x+0.0172x²
return (-27.9 + (1.75 * currentCadence().value()) + (0.0172 * pow(currentCadence().value(), 2)));
case 6:
// -26.7 + 1.9x + 0.0201x²
return (-26.7 + (1.9 * currentCadence().value()) + (0.0201 * pow(currentCadence().value(), 2)));
case 7:
// -33.5 + 2.23x + 0.0225x²
return (-33.5 + (2.23 * currentCadence().value()) + (0.0225 * pow(currentCadence().value(), 2)));
case 8:
// -36.5+2.5x+0.0262x²
return (-36.5 + (2.5 * currentCadence().value()) + (0.0262 * pow(currentCadence().value(), 2)));
case 9:
// -38+2.62x+0.0305x²
return (-38.0 + (2.62 * currentCadence().value()) + (0.0305 * pow(currentCadence().value(), 2)));
case 10:
// -41.2+2.85x+0.0327x²
return (-41.2 + (2.85 * currentCadence().value()) + (0.0327 * pow(currentCadence().value(), 2)));
case 11:
// -43.4+3.01x+0.0359x²
return (-43.4 + (3.01 * currentCadence().value()) + (0.0359 * pow(currentCadence().value(), 2)));
case 12:
// -46.8+3.23x+0.0364x²
return (-46.8 + (3.23 * currentCadence().value()) + (0.0364 * pow(currentCadence().value(), 2)));
case 13:
// -49+3.39x+0.0371x²
return (-49.0 + (3.39 * currentCadence().value()) + (0.0371 * pow(currentCadence().value(), 2)));
case 14:
// -53.4+3.55x+0.0383x²
return (-53.4 + (3.55 * currentCadence().value()) + (0.0383 * pow(currentCadence().value(), 2)));
case 15:
// -49.9+3.37x+0.0429x²
return (-49.9 + (3.37 * currentCadence().value()) + (0.0429 * pow(currentCadence().value(), 2)));
case 16:
default:
// -47.1+3.25x+0.0464x²
return (-47.1 + (3.25 * currentCadence().value()) + (0.0464 * pow(currentCadence().value(), 2)));
}
}
void proformtelnetbike::sendFrame(QByteArray frame) {
telnet.sendData(frame);
qDebug() << " >> " << frame;
}
void proformtelnetbike::update() {
qDebug() << "websocket.state()" << telnet.isConnected();
if (initRequest) {
initRequest = false;
btinit();
emit connectedAndDiscovered();
} else if (telnet.isConnected()) {
update_metrics(false, watts());
// updating the treadmill console every second
if (sec1Update++ == (500 / refresh->interval())) {
sec1Update = 0;
// updateDisplay(elapsed);
}
if (requestStart != -1) {
emit debug(QStringLiteral("starting..."));
// btinit();
requestStart = -1;
emit bikeStarted();
}
if (requestStop != -1) {
emit debug(QStringLiteral("stopping..."));
// writeCharacteristic(initDataF0C800B8, sizeof(initDataF0C800B8), "stop tape");
requestStop = -1;
}
}
}
bool proformtelnetbike::inclinationAvailableByHardware() { return true; }
resistance_t proformtelnetbike::pelotonToBikeResistance(int pelotonResistance) {
if (pelotonResistance <= 10) {
return 1;
}
if (pelotonResistance <= 20) {
return 2;
}
if (pelotonResistance <= 25) {
return 3;
}
if (pelotonResistance <= 30) {
return 4;
}
if (pelotonResistance <= 35) {
return 5;
}
if (pelotonResistance <= 40) {
return 6;
}
if (pelotonResistance <= 45) {
return 7;
}
if (pelotonResistance <= 50) {
return 8;
}
if (pelotonResistance <= 55) {
return 9;
}
if (pelotonResistance <= 60) {
return 10;
}
if (pelotonResistance <= 65) {
return 11;
}
if (pelotonResistance <= 70) {
return 12;
}
if (pelotonResistance <= 75) {
return 13;
}
if (pelotonResistance <= 80) {
return 14;
}
if (pelotonResistance <= 85) {
return 15;
}
if (pelotonResistance <= 100) {
return 16;
}
return Resistance.value();
}
void proformtelnetbike::serviceDiscovered(const QBluetoothUuid &gatt) {
emit debug(QStringLiteral("serviceDiscovered ") + gatt.toString());
}
void proformtelnetbike::characteristicChanged(const char *buff, int len) {
// qDebug() << "characteristicChanged" << characteristic.uuid() << newValue << newValue.length();
QSettings settings;
QString heartRateBeltName =
settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString();
bool disable_hr_frommachinery =
settings.value(QZSettings::heart_ignore_builtin, QZSettings::default_heart_ignore_builtin).toBool();
bool erg_mode = settings.value(QZSettings::zwift_erg, QZSettings::default_zwift_erg).toBool();
QByteArray newValue = QByteArray::fromRawData(buff, len);
emit debug(QStringLiteral(" << ") + newValue);
if(newValue.contains("Shared Memory Management Utility")) {
emit debug(QStringLiteral("Ready to start the poll"));
sendFrame("2\n"); // current watt
} else if(newValue.contains("Enter New Value")) {
if(poolIndex >= 4) {
if(!erg_mode) {
sendFrame((QString::number(requestInclination) + "\n").toLocal8Bit()); // target incline
qDebug() << "forceInclination" << requestInclination;
requestInclination = -100;
} else {
double r = requestPower;
if (settings.value(QZSettings::watt_gain, QZSettings::default_watt_gain).toDouble() <= 2.00) {
if (settings.value(QZSettings::watt_gain, QZSettings::default_watt_gain).toDouble() != 1.0) {
qDebug() << QStringLiteral("request watt value was ") << r
<< QStringLiteral("but it will be transformed to")
<< r / settings.value(QZSettings::watt_gain, QZSettings::default_watt_gain).toDouble();
}
r /= settings.value(QZSettings::watt_gain, QZSettings::default_watt_gain).toDouble();
}
if (settings.value(QZSettings::watt_offset, QZSettings::default_watt_offset).toDouble() < 0) {
if (settings.value(QZSettings::watt_offset, QZSettings::default_watt_offset).toDouble() != 0.0) {
qDebug() << QStringLiteral("request watt value was ") << r
<< QStringLiteral("but it will be transformed to")
<< r - settings.value(QZSettings::watt_offset, QZSettings::default_watt_offset).toDouble();
}
r -= settings.value(QZSettings::watt_offset, QZSettings::default_watt_offset).toDouble();
}
sendFrame((QString::number(r) + "\n").toLocal8Bit()); // target watt
qDebug() << "forceWatt" << r;
requestPower = -1;
}
poolIndex = 0;
} else {
sendFrame("q\n"); // quit
}
} else if(newValue.contains("Enter Variable Offset")) {
qDebug() << "poolIndex" << poolIndex;
bool done = false;
do {
switch (poolIndex)
{
case 0:
sendFrame("124\n"); // current watt
done = true;
break;
case 1:
sendFrame("40\n"); // current rpm
done = true;
break;
case 2:
sendFrame("34\n"); // current speed
done = true;
break;
case 3:
if(!erg_mode) {
if(requestInclination != -100) {
sendFrame("45\n"); // target incline
done = true;
}
else
poolIndex = 99;
} else {
if(requestPower != -1) {
sendFrame("125\n"); // target watt
done = true;
}
else
poolIndex = 99;
}
break;
default:
break;
}
poolIndex++;
if(poolIndex > 4)
poolIndex = 0;
} while(!done);
}
QStringList packet = QString::fromLocal8Bit(newValue).split(" ");
qDebug() << packet;
if (newValue.contains("Current Watts")) {
double watt = packet[3].toDouble();
if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
.toString()
.startsWith(QStringLiteral("Disabled")))
m_watt = watt;
emit debug(QStringLiteral("Current Watt: ") + QString::number(watts()));
} else if (newValue.contains("Cur RPM")) {
double RPM = packet[3].toDouble();
Cadence = RPM;
emit debug(QStringLiteral("Current Cadence: ") + QString::number(Cadence.value()));
if (Cadence.value() > 0) {
CrankRevs++;
LastCrankEventTime += (uint16_t)(1024.0 / (((double)(Cadence.value())) / 60.0));
}
} else if (newValue.contains("Cur KPH")) {
if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) {
double kph = packet[3].toDouble();
Speed = kph;
emit debug(QStringLiteral("Current Speed: ") + QString::number(Speed.value()));
} else {
Speed = metric::calculateSpeedFromPower(
watts(), Inclination.value(), Speed.value(),
fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit());
}
}
if (watts()) {
KCal +=
((((0.048 * ((double)watts()) + 1.19) *
settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) /
200.0) /
(60000.0 / ((double)lastRefreshCharacteristicChanged.msecsTo(
QDateTime::currentDateTime())))); //(( (0.048* Output in watts +1.19) * body weight in kg
//* 3.5) / 200 ) / 60
Distance += ((Speed.value() / (double)3600.0) /
((double)1000.0 / (double)(lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime()))));
}
/*
Resistance = resistance;
m_pelotonResistance = (100 / 32) * Resistance.value();
emit resistanceRead(Resistance.value()); */
/*
if (!disable_hr_frommachinery && !values[QStringLiteral("Chest Pulse")].isUndefined()) {
Heart = values[QStringLiteral("Chest Pulse")].toString().toDouble();
// index += 1; // NOTE: clang-analyzer-deadcode.DeadStores
emit debug(QStringLiteral("Current Heart: ") + QString::number(Heart.value()));
}*/
#ifdef Q_OS_ANDROID
if (settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool())
Heart = (uint8_t)KeepAwakeHelper::heart();
else
#endif
{
if (disable_hr_frommachinery && heartRateBeltName.startsWith(QStringLiteral("Disabled"))) {
update_hr_from_external();
}
}
lastRefreshCharacteristicChanged = QDateTime::currentDateTime();
#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 && h && firstStateChanged) {
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate());
}
#endif
#endif
/*
emit debug(QStringLiteral("Current Resistance: ") + QString::number(Resistance.value()));
emit debug(QStringLiteral("Current Calculate Distance: ") + QString::number(Distance.value()));
emit debug(QStringLiteral("Current CrankRevs: ") + QString::number(CrankRevs));
emit debug(QStringLiteral("Last CrankEventTime: ") + QString::number(LastCrankEventTime)); */
}
void proformtelnetbike::btinit() { initDone = true; }
void proformtelnetbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
emit debug(QStringLiteral("Found new device: ") + device.name() + " (" + device.address().toString() + ')');
}
bool proformtelnetbike::connected() { return telnet.isConnected(); }
uint16_t proformtelnetbike::watts() { return m_watt.value(); }

110
src/proformtelnetbike.h Normal file
View File

@@ -0,0 +1,110 @@
#ifndef PROFORMTELNETBIKE_H
#define PROFORMTELNETBIKE_H
#include <QAbstractOAuth2>
#include <QObject>
#include <QNetworkAccessManager>
#include <QDesktopServices>
#include <QHttpMultiPart>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply>
#include <QOAuth2AuthorizationCodeFlow>
#include <QOAuthHttpServerReplyHandler>
#include <QSettings>
#include <QTimer>
#include <QUrlQuery>
#include <QtCore/qbytearray.h>
#include <QtWebSockets/QWebSocket>
#ifndef Q_OS_ANDROID
#include <QtCore/qcoreapplication.h>
#else
#include <QtGui/qguiapplication.h>
#endif
#include <QtCore/qlist.h>
#include <QtCore/qmutex.h>
#include <QtCore/qscopedpointer.h>
#include <QtCore/qtimer.h>
#include <QDateTime>
#include <QString>
#include "bike.h"
#include "QTelnet.h"
#ifdef Q_OS_IOS
#include "ios/lockscreen.h"
#endif
class proformtelnetbike : public bike {
Q_OBJECT
public:
proformtelnetbike(bool noWriteResistance, bool noHeartService, uint8_t bikeResistanceOffset,
double bikeResistanceGain);
resistance_t pelotonToBikeResistance(int pelotonResistance) override;
resistance_t resistanceFromPowerRequest(uint16_t power) override;
resistance_t maxResistance() override { return max_resistance; }
bool inclinationAvailableByHardware() override;
bool connected() override;
private:
QTelnet telnet;
resistance_t max_resistance = 100;
resistance_t min_resistance = -20;
double max_incline_supported = 20;
void connectToDevice();
uint16_t wattsFromResistance(resistance_t resistance);
double GetDistanceFromPacket(QByteArray packet);
QTime GetElapsedFromPacket(QByteArray packet);
void btinit();
void writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log = false,
bool wait_for_response = false);
void startDiscover();
void sendPoll();
uint16_t watts() override;
void sendFrame(QByteArray frame);
QTimer *refresh;
uint8_t counterPoll = 0;
uint8_t bikeResistanceOffset = 4;
double bikeResistanceGain = 1.0;
uint8_t sec1Update = 0;
QString lastPacket;
QDateTime lastRefreshCharacteristicChanged = QDateTime::currentDateTime();
uint8_t firstStateChanged = 0;
metric target_watts;
bool initDone = false;
bool initRequest = false;
bool noWriteResistance = false;
bool noHeartService = false;
uint8_t poolIndex = 0;
#ifdef Q_OS_IOS
lockscreen *h = 0;
#endif
signals:
void disconnected();
void debug(QString string);
public slots:
void deviceDiscovered(const QBluetoothDeviceInfo &device);
private slots:
void characteristicChanged(const char *buff, int len);
void serviceDiscovered(const QBluetoothUuid &gatt);
void update();
};
#endif // PROFORMTELNETBIKE_H

View File

@@ -72,11 +72,13 @@ DEFINES += QT_DEPRECATED_WARNINGS IO_UNDER_QT SMTP_BUILD NOMINMAX
# include(../qtzeroconf/qtzeroconf.pri)
SOURCES += \
$$PWD/QTelnet.cpp \
$$PWD/bkoolbike.cpp \
$$PWD/csafe.cpp \
$$PWD/csaferower.cpp \
$$PWD/eliteariafan.cpp \
$$PWD/fakerower.cpp \
$$PWD/proformtelnetbike.cpp \
$$PWD/virtualdevice.cpp \
$$PWD/androidactivityresultreceiver.cpp \
$$PWD/androidadblog.cpp \
@@ -281,10 +283,12 @@ else: unix:!android: target.path = /opt/$${TARGET}/bin
INCLUDEPATH += fit-sdk/
HEADERS += \
$$PWD/QTelnet.h \
$$PWD/bkoolbike.h \
$$PWD/csafe.h \
$$PWD/csaferower.h \
$$PWD/eliteariafan.h \
$$PWD/proformtelnetbike.h \
$$PWD/windows_zwift_workout_paddleocr_thread.h \
$$PWD/fakerower.h \
virtualdevice.h \

View File

@@ -686,10 +686,12 @@ const QString QZSettings::nordictrack_s20_treadmill = QStringLiteral("nordictrac
const QString QZSettings::freemotion_coachbike_b22_7 = QStringLiteral("freemotion_coachbike_b22_7");
const QString QZSettings::proform_cycle_trainer_300_ci = QStringLiteral("proform_cycle_trainer_300_ci");
const QString QZSettings::kingsmith_encrypt_g1_walking_pad = QStringLiteral("kingsmith_encrypt_g1_walking_pad");
const QString QZSettings::proformtdf1ip = QStringLiteral("proformtdf1ip");
const QString QZSettings::default_proformtdf1ip = QStringLiteral("");
const QString QZSettings::proform_bike_225_csx = QStringLiteral("proform_bike_225_csx");
const QString QZSettings::proform_treadmill_l6_0s = QStringLiteral("proform_treadmill_l6_0s");
const uint32_t allSettingsCount = 578;
const uint32_t allSettingsCount = 579;
QVariant allSettings[allSettingsCount][2] = {
{QZSettings::cryptoKeySettingsProfiles, QZSettings::default_cryptoKeySettingsProfiles},
@@ -1271,9 +1273,10 @@ QVariant allSettings[allSettingsCount][2] = {
{QZSettings::nordictrack_s20_treadmill, QZSettings::default_nordictrack_s20_treadmill},
{QZSettings::freemotion_coachbike_b22_7, QZSettings::default_freemotion_coachbike_b22_7},
{QZSettings::proform_cycle_trainer_300_ci, QZSettings::default_proform_cycle_trainer_300_ci},
{QZSettings::kingsmith_encrypt_g1_walking_pad, QZSettings::default_kingsmith_encrypt_g1_walking_pad},
{QZSettings::kingsmith_encrypt_g1_walking_pad, QZSettings::default_kingsmith_encrypt_g1_walking_pad},
{QZSettings::proform_bike_225_csx, QZSettings::default_proform_bike_225_csx},
{QZSettings::proform_treadmill_l6_0s, QZSettings::default_proform_treadmill_l6_0s},
{QZSettings::proformtdf1ip, QZSettings::default_proformtdf1ip},
};
void QZSettings::qDebugAllSettings(bool showDefaults) {

View File

@@ -1935,6 +1935,9 @@ class QZSettings {
static const QString proform_treadmill_l6_0s;
static constexpr bool default_proform_treadmill_l6_0s = false;
static const QString proformtdf1ip;
static const QString default_proformtdf1ip;
/**
* @brief Write the QSettings values using the constants from this namespace.
* @param showDefaults Optionally indicates if the default should be shown with the key.

View File

@@ -858,6 +858,7 @@ import QtQuick.Dialogs 1.0
// from version 2.16.30
property bool proform_treadmill_l6_0s: false
property string proformtdf1ip: ""
}
function paddingZeros(text, limit) {
@@ -3049,6 +3050,29 @@ import QtQuick.Dialogs 1.0
onClicked: { settings.proform_bike_sb = checked; window.settings_restart_to_apply = true; }
}
RowLayout {
spacing: 10
Label {
text: qsTr("TDF1 IP:")
Layout.fillWidth: true
}
TextField {
id: proformTDF1IPTextField
text: settings.proformtdf1ip
horizontalAlignment: Text.AlignRight
Layout.fillHeight: false
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
//inputMethodHints: Qt.ImhFormattedNumbersOnly
onAccepted: settings.proformtdf1ip = text
onActiveFocusChanged: if(this.focus) this.cursorPosition = this.text.length
}
Button {
text: "OK"
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
onClicked: { settings.proformtdf1ip = proformTDF1IPTextField.text; window.settings_restart_to_apply = true; toast.show("Setting saved!"); }
}
}
RowLayout {
spacing: 10
Label {