diff --git a/src/virtualdevices/virtualbike.cpp b/src/virtualdevices/virtualbike.cpp index 1351cf463..dd2268c6f 100644 --- a/src/virtualdevices/virtualbike.cpp +++ b/src/virtualdevices/virtualbike.cpp @@ -113,6 +113,8 @@ virtualbike::virtualbike(bluetoothdevice *t, bool noWriteResistance, bool noHear } } + services << QBluetoothUuid::HumanInterfaceDevice; + advertisingData.setServices(services); //! [Advertising Data] @@ -431,6 +433,38 @@ virtualbike::virtualbike(bluetoothdevice *t, bool noWriteResistance, bool noHear serviceDataChanged.addCharacteristic(charData); } + QLowEnergyCharacteristicData reportMapChar; + reportMapChar.setUuid(QBluetoothUuid::ReportMap); + reportMapChar.setValue(getHIDReportDescriptor()); + reportMapChar.setProperties(QLowEnergyCharacteristic::Read); + + // HID Information Characteristic + QLowEnergyCharacteristicData hidInfoChar; + hidInfoChar.setUuid(QBluetoothUuid::HidInformation); + QByteArray hidInfo; + hidInfo.append((char)0x11); // HID version 1.1 + hidInfo.append((char)0x01); // Country code + hidInfo.append((char)0x00); // Flags + hidInfoChar.setValue(hidInfo); + hidInfoChar.setProperties(QLowEnergyCharacteristic::Read); + + // Report Characteristic + QLowEnergyCharacteristicData reportChar; + reportChar.setUuid(QBluetoothUuid::Report); + reportChar.setProperties(QLowEnergyCharacteristic::Notify | QLowEnergyCharacteristic::Read | QLowEnergyCharacteristic::Write); + QByteArray descriptor; + descriptor.append((char)0x01); + descriptor.append((char)0x00); + const QLowEnergyDescriptorData clientConfig(QBluetoothUuid::ClientCharacteristicConfiguration, descriptor); + reportChar.addDescriptor(clientConfig); + + // Configure service + serviceDataHID.setType(QLowEnergyServiceData::ServiceTypePrimary); + serviceDataHID.setUuid(QBluetoothUuid::HumanInterfaceDevice); + serviceDataHID.addCharacteristic(reportMapChar); + serviceDataHID.addCharacteristic(hidInfoChar); + serviceDataHID.addCharacteristic(reportChar); + //! [Start Advertising] leController = QLowEnergyController::createPeripheral(); Q_ASSERT(leController); @@ -473,6 +507,9 @@ virtualbike::virtualbike(bluetoothdevice *t, bool noWriteResistance, bool noHear serviceHR = leController->addService(serviceDataHR); } + QThread::msleep(100); // give time to Android to add the service async.ly + serviceHID = leController->addService(serviceDataHID); + if (!echelon && !ifit) { if (!heart_only) { if (!cadence && !power) { @@ -482,6 +519,8 @@ virtualbike::virtualbike(bluetoothdevice *t, bool noWriteResistance, bool noHear &virtualbike::characteristicChanged); QObject::connect(serviceZwiftPlayBike, &QLowEnergyService::characteristicChanged, this, &virtualbike::characteristicChanged); + QObject::connect(serviceHID, &QLowEnergyService::characteristicChanged, this, + &virtualbike::characteristicChanged); } else { QObject::connect(service, &QLowEnergyService::characteristicChanged, this, &virtualbike::characteristicChanged); @@ -1345,6 +1384,9 @@ void virtualbike::reconnect() { if (!this->noHeartService || heart_only) serviceHR = leController->addService(serviceDataHR); + + QThread::msleep(100); // give time to Android to add the service async.ly + serviceHID = leController->addService(serviceDataHID); #endif QLowEnergyAdvertisingParameters pars; @@ -1373,6 +1415,8 @@ void virtualbike::bikeProvider() { uint16_t normalizeSpeed = (uint16_t)qRound(Bike->currentSpeed().value() * 100); + sendMouseReport(10, 0); + #ifdef Q_OS_IOS #ifndef IO_UNDER_QT if (h) { diff --git a/src/virtualdevices/virtualbike.h b/src/virtualdevices/virtualbike.h index ab93d9e92..0971f8988 100644 --- a/src/virtualdevices/virtualbike.h +++ b/src/virtualdevices/virtualbike.h @@ -43,6 +43,29 @@ class virtualbike : public virtualdevice { return lastDirconFTMSFrameReceived; } + // Function to send mouse input reports + void sendMouseReport(int8_t x, int8_t y, uint8_t buttons = 0, int8_t wheel = 0) { + if (!serviceHID || !hidEnabled || leController->state() != QLowEnergyController::ConnectedState) { + qDebug() << "HID service not available or not connected"; + return; + } + + QByteArray report; + report.append(buttons); // Button state + report.append(x); // X movement + report.append(y); // Y movement + report.append(wheel); // Wheel movement + + // Find the report characteristic + QLowEnergyCharacteristic reportChar = serviceHID->characteristic(QBluetoothUuid(quint16(0x2A4D))); + if (!reportChar.isValid()) { + qDebug() << "HID report characteristic not found"; + return; + } + + writeCharacteristic(serviceHID, reportChar, report); + } + private: QLowEnergyController *leController = nullptr; QLowEnergyService *serviceHR = nullptr; @@ -106,6 +129,44 @@ class virtualbike : public virtualdevice { }; VarintResult decodeVarint(const QByteArray& bytes, int startIndex); qint32 decodeSInt(const QByteArray& bytes); + + QLowEnergyServiceData serviceDataHID; + QLowEnergyService* serviceHID = nullptr; + bool hidEnabled = false; + + // HID report descriptor for mouse + QByteArray getHIDReportDescriptor() { + static const char reportDesc[] = { + 0x05, 0x01, // Usage Page (Generic Desktop) + 0x09, 0x02, // Usage (Mouse) + static_cast(0xA1), 0x01, // Collection (Application) + 0x09, 0x01, // Usage (Pointer) + static_cast(0xA1), 0x00, // Collection (Physical) + 0x05, 0x09, // Usage Page (Button) + 0x19, 0x01, // Usage Minimum (1) + 0x29, 0x03, // Usage Maximum (3) + 0x15, 0x00, // Logical Minimum (0) + 0x25, 0x01, // Logical Maximum (1) + static_cast(0x95), 0x03, // Report Count (3) + 0x75, 0x01, // Report Size (1) + static_cast(0x81), 0x02, // Input (Data, Variable, Absolute) + static_cast(0x95), 0x01, // Report Count (1) + 0x75, 0x05, // Report Size (5) + static_cast(0x81), 0x01, // Input (Constant) + 0x05, 0x01, // Usage Page (Generic Desktop) + 0x09, 0x30, // Usage (X) + 0x09, 0x31, // Usage (Y) + 0x09, 0x38, // Usage (Wheel) + 0x15, static_cast(0x81), // Logical Minimum (-127) + 0x25, 0x7F, // Logical Maximum (127) + 0x75, 0x08, // Report Size (8) + static_cast(0x95), 0x03, // Report Count (3) + static_cast(0x81), 0x06, // Input (Data, Variable, Relative) + static_cast(0xC0), // End Collection + static_cast(0xC0) // End Collection + }; + return QByteArray(reportDesc, sizeof(reportDesc)); + } signals: void changeInclination(double grade, double percentage);