From 6f1e5843b01308144710234a1da979461076ede1 Mon Sep 17 00:00:00 2001 From: Ton Kersten Date: Fri, 31 Jul 2026 15:11:08 +0200 Subject: [PATCH] feat: add Logitech Spotlight 2 support (Action button, Bolt receiver & Bluetooth) --- 55-projecteur.rules.in | 6 ++ README.md | 18 +++- devices.conf | 2 + src/device-hidpp.cc | 209 +++++++++++++++++++++++++++++++++------ src/device-hidpp.h | 13 +++ src/device-key-lookup.cc | 4 + src/device.cc | 1 + src/device.h | 1 + src/deviceinput.cc | 2 + src/deviceinput.h | 1 + src/deviceswidget.cc | 3 +- src/hidpp.cc | 11 ++- src/hidpp.h | 6 ++ src/spotlight.cc | 15 +++ src/spotlight.h | 1 + 15 files changed, 253 insertions(+), 40 deletions(-) diff --git a/55-projecteur.rules.in b/55-projecteur.rules.in index af461319..1bec13bf 100644 --- a/55-projecteur.rules.in +++ b/55-projecteur.rules.in @@ -15,6 +15,12 @@ SUBSYSTEMS=="input", ENV{LIBINPUT_DEVICE_GROUP}="5/46d/b503*", ATTRS{name}=="SPO # Additional rule for Bluetooth sub-devices (hidraw) SUBSYSTEMS=="hid", KERNELS=="0005:046D:B503.*", MODE="0660", TAG+="uaccess" +# Rule for the Logitech Spotlight 2 when connected via Bluetooth +SUBSYSTEMS=="input", ENV{LIBINPUT_DEVICE_GROUP}="5/46d/b506*", MODE="0660", TAG+="uaccess" +SUBSYSTEMS=="input", ATTRS{name}=="Spotlight 2*", MODE="0660", TAG+="uaccess" +# Additional rule for Bluetooth sub-devices (hidraw) +SUBSYSTEMS=="hid", KERNELS=="0005:046D:B506.*", MODE="0660", TAG+="uaccess" + # Additional supported Bluetooth devices @EXTRA_BLUETOOTH_UDEV_RULES@ # Rules for uinput: Essential for creating a virtual input device that diff --git a/README.md b/README.md index 84209fdf..23e1091e 100644 --- a/README.md +++ b/README.md @@ -91,8 +91,9 @@ rules installed (see [pre-requisites section](#pre-requisites)). ## How it works -With a connection via the USB Dongle Receiver or via Bluetooth, the Logitech Spotlight -device will be detected by Linux as a HID device with mouse and keyboard events. +With a connection via the USB Dongle Receiver, the Logi Bolt receiver or via Bluetooth, the +Logitech Spotlight (and Spotlight 2) device(s) will be detected by Linux as a HID device +with mouse and keyboard events. As mouse events, the device sends relative cursor movements and left button presses. Acting as a keyboard, the device basically just sends left and right arrow key press events when forward or back is pressed on the device. @@ -145,6 +146,17 @@ mapped for a particular button, both actions will executed if user hold the button and move device. To avoid this situation, do not set both Long-Press and Hold Move actions for the same button. +#### Action Button of the Logitech Spotlight 2 + +In addition to the _Next_ and _Back_ buttons, the Logitech Spotlight 2 features a +dedicated _Action_ button. On both connection types (Logi Bolt USB receiver and +Bluetooth), _Projecteur_ diverts the button's events so it can be mapped like any +other button in the Input Mapper (Devices tab in Preferences dialog box): add a new +input mapping entry, double click in the first column (Input Sequence) and press the +_Action_ button to record it. The recorded entry will be shown as _Action Button_. +The _Highlight_ button of the Spotlight 2 is a virtual left mouse button and is +forwarded to the system like a regular mouse click. + ## Download The latest binary packages for some Linux distributions are available for download on cloudsmith. @@ -296,6 +308,8 @@ your screen in an online presentation or call. Besides the _Logitech Spotlight_, the following devices are currently supported out of the box: +* Logitech Spotlight 2 via the Logi Bolt USB Receiver _(046d:c548)_ +* Logitech Spotlight 2 via Bluetooth _(046d:b506)_ * AVATTO H100 / August WP200 _(0c45:8101)_ * August LP315 _(2312:863d)_ * AVATTO i10 Pro _(2571:4109)_ diff --git a/devices.conf b/devices.conf index e6ca4000..2c008dd3 100644 --- a/devices.conf +++ b/devices.conf @@ -5,6 +5,8 @@ # Example: # 0x0abc, 0x1234, usb, MyExample Device +0x046d, 0xc548, usb, Logitech Spotlight 2 (Logi Bolt Receiver) +0x046d, 0xb506, bt, Logitech Spotlight 2 (Bluetooth) 0x0c45, 0x8101, usb, AVATTO H100 / August WP200 0x2312, 0x863d, usb, August LP315 0x2571, 0x4109, usb, AVATTO i10 Pro diff --git a/src/device-hidpp.cc b/src/device-hidpp.cc index a6e93682..b50ea183 100644 --- a/src/device-hidpp.cc +++ b/src/device-hidpp.cc @@ -138,16 +138,12 @@ void SubHidppConnection::sendRequest(HIDPP::Message msg, RequestResultCallback r } // Device index sanity check - static const std::array validDeviceIndexes { - HIDPP::DeviceIndex::CordedDevice, - HIDPP::DeviceIndex::DefaultDevice, - HIDPP::DeviceIndex::WirelessDevice1, - }; + const uint8_t dIdx = msg.deviceIndex(); + const bool isValidDeviceIndex = (dIdx == HIDPP::DeviceIndex::CordedDevice + || dIdx == HIDPP::DeviceIndex::DefaultDevice + || (dIdx >= HIDPP::DeviceIndex::WirelessDevice1 && dIdx <= HIDPP::DeviceIndex::WirelessDevice6)); - const auto deviceIndexIt - = std::find(validDeviceIndexes.cbegin(), validDeviceIndexes.cend(), msg.deviceIndex()); - - if (deviceIndexIt == validDeviceIndexes.cend()) + if (!isValidDeviceIndex) { logWarn(hid) << tr("Invalid device index (%1) in message for '%2'") .arg(msg.deviceIndex()).arg(path()); @@ -382,7 +378,7 @@ void SubHidppConnection::sendVibrateCommand(uint8_t intensity, uint8_t length, using namespace HIDPP; - Message vibrateMsg(Message::Type::Long, DeviceIndex::WirelessDevice1, pcIndex, 1, { + Message vibrateMsg(Message::Type::Long, m_deviceIndex, pcIndex, 1, { length, 0xe8, intensity }); @@ -402,7 +398,7 @@ void SubHidppConnection::getBatteryLevelStatus( return; } - Message batteryReqMsg(Message::Type::Short, DeviceIndex::WirelessDevice1, batteryIndex, 0); + Message batteryReqMsg(Message::Type::Short, m_deviceIndex, batteryIndex, 0); sendRequest(std::move(batteryReqMsg), [cb=std::move(cb)](MsgResult res, Message&& msg) mutable { if (!cb) { return; } @@ -415,6 +411,51 @@ void SubHidppConnection::getBatteryLevelStatus( }); } +// ------------------------------------------------------------------------------------------------- +HIDPP::BatteryStatus SubHidppConnection::toBatteryStatus1004(uint8_t chargingStatus) +{ + using BatteryStatus = HIDPP::BatteryStatus; + switch (chargingStatus) { + case 0: return BatteryStatus::Discharging; + case 1: return BatteryStatus::Charging; + case 2: return BatteryStatus::SlowCharging; + case 3: return BatteryStatus::Full; + case 4: return BatteryStatus::ChargingError; + default: return BatteryStatus::InvalidBattery; + } +} + +// ------------------------------------------------------------------------------------------------- +void SubHidppConnection::getUnifiedBatteryLevel( + std::function cb) +{ + using namespace HIDPP; + + const auto ubIndex = m_featureSet.featureIndex(FeatureCode::UnifiedBattery); + if (ubIndex == 0) + { + if (cb) { cb(MsgResult::FeatureNotSupported, {}); } + return; + } + + // UnifiedBattery (0x1004) get status command (function 0x01) response: + // msg[4] = state of charge in percent + // msg[5] = battery level flags (critical/low/good/full) + // msg[6] = charging status + // msg[7] = external power source indicator + Message batteryReqMsg(Message::Type::Long, m_deviceIndex, ubIndex, 0x01, Message::Data{0,0,0,0}); + sendRequest(std::move(batteryReqMsg), [cb=std::move(cb)](MsgResult res, Message&& msg) mutable + { + if (!cb) { return; } + + BatteryInfo batteryInfo; + if (res == MsgResult::Ok) { + batteryInfo = BatteryInfo{msg[4], 0, toBatteryStatus1004(msg[6])}; + } + cb(res, std::move(batteryInfo)); + }); +} + // ------------------------------------------------------------------------------------------------- void SubHidppConnection::setPointerSpeed(uint8_t speed, std::function cb) @@ -431,7 +472,7 @@ void SubHidppConnection::setPointerSpeed(uint8_t speed, const uint8_t pointerSpeed = 0x10 & speed; sendRequest( - HIDPP::Message(HIDPP::Message::Type::Long, HIDPP::DeviceIndex::WirelessDevice1, + HIDPP::Message(HIDPP::Message::Type::Long, m_deviceIndex, psIndex, 1, HIDPP::Message::Data{pointerSpeed}), std::move(cb) ); @@ -628,7 +669,7 @@ void SubHidppConnection::initFeatures( if (const auto resetFeatureIndex = m_featureSet.featureIndex(FeatureCode::Reset)) { batch.emplace(RequestBatchItem { - Message(Message::Type::Long, DeviceIndex::WirelessDevice1, resetFeatureIndex, 1), + Message(Message::Type::Long, m_deviceIndex, resetFeatureIndex, 1), [resultMap](MsgResult res, Message&& /* msg */) { resultMap->emplace(FeatureCode::Reset, res); } @@ -641,7 +682,7 @@ void SubHidppConnection::initFeatures( if (hasFlags(DeviceFlags::NextHold)) { batch.emplace(RequestBatchItem { - Message(Message::Type::Long, DeviceIndex::WirelessDevice1, contrFeatureIndex, 3, + Message(Message::Type::Long, m_deviceIndex, contrFeatureIndex, 3, Message::Data{0x00, 0xda, 0x33}), [resultMap](MsgResult res, Message&& /* msg */) { resultMap->emplace(FeatureCode::ReprogramControlsV4, res); @@ -652,20 +693,39 @@ void SubHidppConnection::initFeatures( if (hasFlags(DeviceFlags::BackHold)) { batch.emplace(RequestBatchItem { - Message(Message::Type::Long, DeviceIndex::WirelessDevice1, contrFeatureIndex, 3, + Message(Message::Type::Long, m_deviceIndex, contrFeatureIndex, 3, Message::Data{0x00, 0xdc, 0x33}), [resultMap](MsgResult res, Message&& /* msg */) { resultMap->emplace(FeatureCode::ReprogramControlsV4, res); } }); } + + // Enable 'Action Button' on hold/tap functionality for the Logitech Spotlight 2. + // The device reports the action button as control id 0x00fb. If the device + // does not support this control, the request will fail and the ActionButton + // device flag stays unset. + batch.emplace(RequestBatchItem { + Message(Message::Type::Long, m_deviceIndex, contrFeatureIndex, 3, + Message::Data{0x00, 0xfb, 0x03}), + [resultMap, this](MsgResult res, Message&& /* msg */) { + if (res == MsgResult::Ok) { + setFlags(DeviceFlags::ActionButton, true); + logDebug(hid) << tr("Subdevice '%1' reported 'Action Button' control support.") + .arg(path()); + } else { + setFlags(DeviceFlags::ActionButton, false); + } + resultMap->emplace(FeatureCode::ReprogramControlsV4, res); + } + }); } if (const auto psFeatureIndex = m_featureSet.featureIndex(FeatureCode::PointerSpeed)) { // Reset pointer speed to 0x14 - the device accepts values from 0x10 to 0x19 batch.emplace(RequestBatchItem { - HIDPP::Message(HIDPP::Message::Type::Long, HIDPP::DeviceIndex::WirelessDevice1, + HIDPP::Message(HIDPP::Message::Type::Long, m_deviceIndex, psFeatureIndex, 1, HIDPP::Message::Data{0x14}), [resultMap](MsgResult res, Message&& /* msg */) { resultMap->emplace(FeatureCode::PointerSpeed, res); @@ -697,7 +757,13 @@ void SubHidppConnection::updateDeviceFlags() featureFlagsSet |= DeviceFlag::ReportBattery; logDebug(hid) << tr("Subdevice '%1' reported %2 support.") .arg(path()).arg(toString(HIDPP::FeatureCode::BatteryStatus)); - } else { + } + else if (m_featureSet.featureCodeSupported(HIDPP::FeatureCode::UnifiedBattery)) { + featureFlagsSet |= DeviceFlag::ReportBattery; + logDebug(hid) << tr("Subdevice '%1' reported %2 support.") + .arg(path()).arg(toString(HIDPP::FeatureCode::UnifiedBattery)); + } + else { featureFlagsUnset |= DeviceFlag::ReportBattery; } @@ -713,6 +779,7 @@ void SubHidppConnection::updateDeviceFlags() else { featureFlagsUnset |= DeviceFlags::NextHold; featureFlagsUnset |= DeviceFlags::BackHold; + featureFlagsUnset |= DeviceFlags::ActionButton; } m_inputMapper->setSpecialMoveInputs(std::move(specialMoveInputs)); @@ -742,15 +809,21 @@ void SubHidppConnection::registerForFeatureNotifications() // Logitech Spotlight: // * Next Button = 0xda // * Back Button = 0xdc + // Logitech Spotlight 2: + // * Next Button = 0xd9 (short press), 0xda (hold) + // * Back Button = 0xdb (short press), 0xdc (hold) + // * Action Button = 0xfb (short press), 0xfc (hold) // Byte 5 and 7 indicate pressed buttons // Back and next can be pressed at the same time constexpr uint8_t ButtonNext = 0xda; constexpr uint8_t ButtonBack = 0xdc; + constexpr uint8_t ButtonAction = 0xfb; const auto isNextPressed = msg[5] == ButtonNext || msg[7] == ButtonNext; const auto isBackPressed = msg[5] == ButtonBack || msg[7] == ButtonBack; - logDebug(hid) << tr("Buttons pressed: Next = %1, Back = %2") - .arg(isNextPressed).arg(isBackPressed); + const auto isActionPressed = msg[5] == ButtonAction || msg[7] == ButtonAction; + logDebug(hid) << tr("Buttons pressed: Next = %1, Back = %2, Action = %3") + .arg(isNextPressed).arg(isBackPressed).arg(isActionPressed); }), 0 /* function 0 */); @@ -771,6 +844,14 @@ void SubHidppConnection::registerForFeatureNotifications() registerNotificationCallback(this, batIndex, makeSafeCallback([this](Message&& msg) { setBatteryInfo(BatteryInfo{msg[4], msg[5], to_enum(msg[6])}); }), 0 /* function 0 */); } + + if (const auto ubIndex = m_featureSet.featureIndex(FeatureCode::UnifiedBattery)) + { + // A device with a unified battery can send a battery status event spontaneously. + registerNotificationCallback(this, ubIndex, makeSafeCallback([this](Message&& msg) { + setBatteryInfo(BatteryInfo{msg[4], 0, toBatteryStatus1004(msg[6])}); + }), 0 /* event 0 */); + } } // ------------------------------------------------------------------------------------------------- @@ -843,6 +924,18 @@ HIDPP::ProtocolVersion SubHidppConnection::protocolVersion() const { void SubHidppConnection::triggerBattyerInfoUpdate() { using namespace HIDPP; + + if (m_featureSet.featureCodeSupported(FeatureCode::UnifiedBattery)) { + getUnifiedBatteryLevel(makeSafeCallback([this](MsgResult res, BatteryInfo&& bi) + { + if (res != MsgResult::Ok) { + return; + } + setBatteryInfo(bi); + })); + return; + } + getBatteryLevelStatus(makeSafeCallback([this](MsgResult res, BatteryInfo&& bi) { if (res != MsgResult::Ok) { @@ -862,8 +955,8 @@ const HIDPP::BatteryInfo& SubHidppConnection::batteryInfo() const { void SubHidppConnection::sendPing(RequestResultCallback cb) { using namespace HIDPP; - // Ping wireless device 1 - same as requesting protocol version - Message pingMsg(Message::Type::Short, DeviceIndex::WirelessDevice1, 0, 1, getRandomPingPayload()); + // Ping wireless device - same as requesting protocol version + Message pingMsg(Message::Type::Short, m_deviceIndex, 0, 1, getRandomPingPayload()); sendRequest(std::move(pingMsg), std::move(cb)); } @@ -884,21 +977,73 @@ void SubHidppConnection::getProtocolVersion(std::function cb) +void SubHidppConnection::discoverDeviceIndex(std::function cb) { - getProtocolVersion( - [cb=std::move(cb)](MsgResult res, HIDPP::Error err, HIDPP::ProtocolVersion pv) { - if (!cb) return; - const bool deviceOnline = MsgResult::Ok == res && err == HIDPP::Error::NoError; - if (!deviceOnline && err != HIDPP::Error::Unsupported) { - // Unsupported is send as error if the device is offline - logWarn(hid) << tr("Unexpected error for offline device (%1, %2)") - .arg(toString(res)).arg(toString(err)); + using namespace HIDPP; + + postSelf([this, cb=std::move(cb)]() mutable + { + // For bluetooth connections the device is always the default wireless device. + if (busType() != BusType::Usb) + { + m_deviceIndex = DeviceIndex::WirelessDevice1; + getProtocolVersion(makeSafeCallback( + [cb=std::move(cb)](MsgResult res, HIDPP::Error err, HIDPP::ProtocolVersion pv) mutable + { + if (cb) { + cb(MsgResult::Ok == res && err == HIDPP::Error::NoError, std::move(pv)); + } + })); + return; } - cb(deviceOnline, std::move(pv)); + + // Probe all wireless device slots of the usb receiver with a ping request. + // Only the slot that has an online device will answer with an error free reply. + RequestBatch batch; + auto responses = std::make_shared>>(); + for (uint8_t idx = DeviceIndex::WirelessDevice1; idx <= DeviceIndex::WirelessDevice6; ++idx) + { + batch.emplace(RequestBatchItem{ + Message(Message::Type::Short, idx, 0, 1, getRandomPingPayload()), + [responses, idx](MsgResult res, Message&& msg) + { + if (res == MsgResult::Ok) + { + responses->emplace_back(idx, ProtocolVersion{msg[4], msg[5]}); + } + } + }); + } + + sendRequestBatch(std::move(batch), makeSafeCallback( + [this, responses, cb=std::move(cb)](std::vector&& results) mutable + { + Q_UNUSED(results); + if (!responses->empty()) + { + const auto deviceIndex = responses->front().first; + const auto pv = responses->front().second; + m_deviceIndex = deviceIndex; + logInfo(hid) << tr("Found HID++ wireless device at device index %1 (%2).") + .arg(m_deviceIndex).arg(path()); + if (cb) { cb(true, pv); } + return; + } + if (cb) { cb(false, ProtocolVersion{}); } + }), true /* continueOnError */); }); } +// ------------------------------------------------------------------------------------------------- +void SubHidppConnection::checkPresenterOnline(std::function cb) +{ + discoverDeviceIndex(makeSafeCallback( + [cb=std::move(cb)](bool deviceOnline, HIDPP::ProtocolVersion pv) mutable + { + if (cb) { cb(deviceOnline, std::move(pv)); } + })); +} + // ------------------------------------------------------------------------------------------------- void SubHidppConnection::checkAndUpdatePresenterState(std::function cb) { @@ -1039,7 +1184,7 @@ void SubHidppConnection::onHidppDataAvailable(int fd) else if (msg.softwareId() == 0 || msg.subId() < 0x80) { // Event/Notification - // logDebug(hid) << tr("Received notification (%1) on %2").arg(msg.hex()).arg(path()); + logDebug(hid) << tr("Received notification (%1) on %2").arg(msg.hex()).arg(path()); // Notify subscribers const auto& callbackList = m_notificationSubscribers[msg.featureIndex()]; diff --git a/src/device-hidpp.h b/src/device-hidpp.h index bf9791c8..7b2d5140 100644 --- a/src/device-hidpp.h +++ b/src/device-hidpp.h @@ -41,6 +41,7 @@ class SubHidppConnection : public SubHidrawConnection, public HidppConnectionInt // --- HidppConnectionInterface implementation: BusType busType() const override { return m_details.deviceId.busType; } + uint8_t deviceIndex() const override { return m_deviceIndex; } ssize_t sendData(std::vector msg) override; ssize_t sendData(HIDPP::Message msg) override; void sendData(std::vector msg, SendResultCallback resultCb) override; @@ -94,6 +95,10 @@ class SubHidppConnection : public SubHidrawConnection, public HidppConnectionInt void initFeatures(std::function&&)> cb); void getBatteryLevelStatus(std::function cb); + void getUnifiedBatteryLevel(std::function cb); + /// Maps the charging status byte of the HID++ UnifiedBattery feature (0x1004) + /// onto the battery status enum. + static HIDPP::BatteryStatus toBatteryStatus1004(uint8_t chargingStatus); void setReceiverState(ReceiverState rs); void setPresenterState(PresenterState ps); @@ -102,6 +107,10 @@ class SubHidppConnection : public SubHidrawConnection, public HidppConnectionInt void onHidppDataAvailable(int fd); void getProtocolVersion(std::function cb); + /// Probes all wireless device slots of a HID++ USB receiver to find the + /// connected device and store its device index. For bluetooth connections + /// the device index is always the default wireless device. + void discoverDeviceIndex(std::function cb); void checkPresenterOnline(std::function cb); void checkAndUpdatePresenterState(std::function cb); @@ -116,6 +125,10 @@ class SubHidppConnection : public SubHidrawConnection, public HidppConnectionInt HIDPP::ProtocolVersion m_protocolVersion; HIDPP::BatteryInfo m_batteryInfo; + /// HID++ device index of the connected device. Defaults to the first + /// wireless device slot and is discovered for HID++ USB receivers. + uint8_t m_deviceIndex = HIDPP::DeviceIndex::WirelessDevice1; + ReceiverState m_receiverState = ReceiverState::Uninitialized; PresenterState m_presenterState = PresenterState::Uninitialized; diff --git a/src/device-key-lookup.cc b/src/device-key-lookup.cc index 68eff2c8..bfe8fdc5 100644 --- a/src/device-key-lookup.cc +++ b/src/device-key-lookup.cc @@ -45,6 +45,8 @@ const QString& lookup(const DeviceId& dId, const DeviceInputEvent& die) SpecialKeys::eventSequenceInfo(SpecialKeys::Key::NextHold).name }, { eHash(EV_KEY, to_integral(SpecialKeys::Key::BackHold)), SpecialKeys::eventSequenceInfo(SpecialKeys::Key::BackHold).name }, + { eHash(EV_KEY, to_integral(SpecialKeys::Key::ActionButton)), + SpecialKeys::eventSequenceInfo(SpecialKeys::Key::ActionButton).name }, }; static const KeyNameMap avattoH100Mapping = { @@ -57,6 +59,8 @@ const QString& lookup(const DeviceId& dId, const DeviceInputEvent& die) { {dHash({0x046d, 0xc53e}), logitechSpotlightMapping}, // Spotlight USB {dHash({0x046d, 0xb503}), logitechSpotlightMapping}, // Spotlight Bluetooth + {dHash({0x046d, 0xc548}), logitechSpotlightMapping}, // Spotlight 2 (Logi Bolt Receiver) + {dHash({0x046d, 0xb506}), logitechSpotlightMapping}, // Spotlight 2 (Bluetooth) {dHash({0x0c45, 0x8101}), avattoH100Mapping}, // Avatto H100, August WP200 }; diff --git a/src/device.cc b/src/device.cc index 32db26a9..ab4677ef 100644 --- a/src/device.cc +++ b/src/device.cc @@ -465,6 +465,7 @@ const char* toString(DeviceFlag f, bool withClass) ENUM_CASE_STRINGIFY3(DeviceFlag, NextHold, withClass); ENUM_CASE_STRINGIFY3(DeviceFlag, BackHold, withClass); ENUM_CASE_STRINGIFY3(DeviceFlag, PointerSpeed, withClass); + ENUM_CASE_STRINGIFY3(DeviceFlag, ActionButton, withClass); } return withClass ? "DeviceFlag::(unknown)" : "(unknown)"; } diff --git a/src/device.h b/src/device.h index b23d0742..3692c8ee 100644 --- a/src/device.h +++ b/src/device.h @@ -76,6 +76,7 @@ enum class DeviceFlag : uint32_t { NextHold = 1 << 18, ///< Device can be configured to send 'Next Hold' event. BackHold = 1 << 19, ///< Device can be configured to send 'Back Hold' event. PointerSpeed = 1 << 20, ///< Device allows changing pointer speed. + ActionButton = 1 << 21, ///< Device can be configured to send 'Action Button' event. }; ENUM(DeviceFlag, DeviceFlags) diff --git a/src/deviceinput.cc b/src/deviceinput.cc index 7fc5a36e..de7cde7a 100644 --- a/src/deviceinput.cc +++ b/src/deviceinput.cc @@ -969,6 +969,8 @@ const std::map& keyEventSequenceMap() KeyEventSequence{{{EV_KEY, to_integral(Key::NextHold), 1}}}}}, {Key::BackHold, {InputMapper::tr("Back Hold"), KeyEventSequence{{{EV_KEY, to_integral(Key::BackHold), 1}}}}}, + {Key::ActionButton, {InputMapper::tr("Action Button"), + KeyEventSequence{{{EV_KEY, to_integral(Key::ActionButton), 1}}}}}, {Key::NextHoldMove, {InputMapper::tr("Next Hold Move"), makeSpecialKeyEventSequence(to_integral(Key::NextHoldMove)) }}, {Key::BackHoldMove, {InputMapper::tr("Back Hold Move"), diff --git a/src/deviceinput.h b/src/deviceinput.h index 71c4547a..edb41e29 100644 --- a/src/deviceinput.h +++ b/src/deviceinput.h @@ -86,6 +86,7 @@ namespace SpecialKeys enum class Key : uint16_t { NextHold = 0x0e10, BackHold = 0x0e11, + ActionButton = 0x0e12, NextHoldMove = 0x0ff0, BackHoldMove = 0x0ff1, }; diff --git a/src/deviceswidget.cc b/src/deviceswidget.cc index 6cc57b54..d81ac3a1 100644 --- a/src/deviceswidget.cc +++ b/src/deviceswidget.cc @@ -719,7 +719,8 @@ void DeviceInfoWidget::updateHidppInfo(SubHidppConnection* hdc) , DeviceFlag::ReportBattery , DeviceFlag::NextHold , DeviceFlag::BackHold - , DeviceFlag::PointerSpeed }) + , DeviceFlag::PointerSpeed + , DeviceFlag::ActionButton }) { if (hdc->hasFlags(flag)) { m_hidppInfo.hidppFlags.push_back(toString(flag, false)); } } diff --git a/src/hidpp.cc b/src/hidpp.cc index e23403f3..848dade4 100644 --- a/src/hidpp.cc +++ b/src/hidpp.cc @@ -370,7 +370,7 @@ void FeatureSet::getFeatureIndex(FeatureCode fc, std::function(to_integral(fc) >> 8); const auto fcMSB = static_cast(to_integral(fc) & 0x00ff); - Message featureIndexReqMsg(Message::Type::Long, DeviceIndex::WirelessDevice1, + Message featureIndexReqMsg(Message::Type::Long, m_connection->deviceIndex(), Message::Data{fcLSB, fcMSB}); m_connection->sendRequest(std::move(featureIndexReqMsg), @@ -395,7 +395,7 @@ void FeatureSet::getFeatureCount(std::functiondeviceIndex(), featureIndex); m_connection->sendRequest(std::move(featureCountReqMsg), [featureIndex, cb=std::move(cb)](MsgResult result, Message&& msg) { @@ -416,7 +416,7 @@ void FeatureSet::getFirmwareCount(std::functiondeviceIndex(), featureIndex); m_connection->sendRequest(std::move(fwCountReqMsg), [featureIndex, cb=std::move(cb)](MsgResult result, Message&& msg) @@ -438,7 +438,7 @@ void FeatureSet::getFirmwareInfo(uint8_t fwIndex, uint8_t entity, return; } - Message fwVerReqMessage(Message::Type::Long, DeviceIndex::WirelessDevice1, fwIndex, 1, + Message fwVerReqMessage(Message::Type::Long, m_connection->deviceIndex(), fwIndex, 1, Message::Data{entity}); m_connection->sendRequest(std::move(fwVerReqMessage), @@ -604,7 +604,7 @@ void FeatureSet::getFeatureIds(uint8_t featureSetIndex, uint8_t count, for (uint8_t featureIndex = 1; featureIndex <= count; ++featureIndex) { batch.emplace(HidppConnectionInterface::RequestBatchItem { - Message(Message::Type::Long, DeviceIndex::WirelessDevice1, featureSetIndex, 1, + Message(Message::Type::Long, m_connection->deviceIndex(), featureSetIndex, 1, Message::Data{featureIndex}), [featureTable, featureIndex](MsgResult res, Message&& msg) { @@ -732,6 +732,7 @@ const char* toString(HIDPP::FeatureCode fc) ENUM_CASE_STRINGIFY(FeatureCode::Reset); ENUM_CASE_STRINGIFY(FeatureCode::DFUControlSigned); ENUM_CASE_STRINGIFY(FeatureCode::BatteryStatus); + ENUM_CASE_STRINGIFY(FeatureCode::UnifiedBattery); ENUM_CASE_STRINGIFY(FeatureCode::PresenterControl); ENUM_CASE_STRINGIFY(FeatureCode::Sensor3D); ENUM_CASE_STRINGIFY(FeatureCode::ReprogramControlsV4); diff --git a/src/hidpp.h b/src/hidpp.h index 9cd861ca..b16235cc 100644 --- a/src/hidpp.h +++ b/src/hidpp.h @@ -43,6 +43,7 @@ namespace HIDPP { Reset = 0x0020, DFUControlSigned = 0x00c2, BatteryStatus = 0x1000, + UnifiedBattery = 0x1004, PresenterControl = 0x1a00, Sensor3D = 0x1a01, ReprogramControlsV4 = 0x1b04, @@ -247,6 +248,11 @@ class HidppConnectionInterface virtual BusType busType() const = 0; + /// HID++ device index of the connected device, e.g. the wireless device slot + /// number on a Logitech USB receiver. + /// Defaults to the first wireless device slot. + virtual uint8_t deviceIndex() const { return HIDPP::DeviceIndex::WirelessDevice1; } + // --- synchronous versions virtual ssize_t sendData(std::vector msg) = 0; virtual ssize_t sendData(HIDPP::Message msg) = 0; diff --git a/src/spotlight.cc b/src/spotlight.cc index 20bd96fc..b6109e8e 100644 --- a/src/spotlight.cc +++ b/src/spotlight.cc @@ -463,13 +463,19 @@ void Spotlight::registerForNotifications(SubHidppConnection* connection) // Logitech Spotlight: // * Next Button = 0xda // * Back Button = 0xdc + // Logitech Spotlight 2: + // * Next Button = 0xd9 (short press), 0xda (hold) + // * Back Button = 0xdb (short press), 0xdc (hold) + // * Action Button = 0xfb (short press), 0xfc (hold) // Byte 5 and 7 indicate pressed buttons // Back and next can be pressed at the same time constexpr uint8_t ButtonNext = 0xda; constexpr uint8_t ButtonBack = 0xdc; + constexpr uint8_t ButtonAction = 0xfb; const auto isNextPressed = msg[5] == ButtonNext || msg[7] == ButtonNext; const auto isBackPressed = msg[5] == ButtonBack || msg[7] == ButtonBack; + const auto isActionPressed = msg[5] == ButtonAction || msg[7] == ButtonAction; if (!m_holdButtonStatus->nextPressed() && isNextPressed) { @@ -487,6 +493,15 @@ void Spotlight::registerForNotifications(SubHidppConnection* connection) } } + if (!m_actionButtonPressed && isActionPressed) + { + const auto& actionButton = SpecialKeys::eventSequenceInfo(SpecialKeys::Key::ActionButton); + for (const auto& ke: actionButton.keyEventSeq) { + connection->inputMapper()->addEvents(ke); + } + } + + m_actionButtonPressed = isActionPressed; m_holdButtonStatus->setButtonsPressed(isNextPressed, isBackPressed); }), 0 /* function 0 */); diff --git a/src/spotlight.h b/src/spotlight.h index d77c9eef..152fe28b 100644 --- a/src/spotlight.h +++ b/src/spotlight.h @@ -76,6 +76,7 @@ class Spotlight : public QObject, public async::Async QTimer* m_connectionTimer = nullptr; QTimer* m_holdMoveEventTimer = nullptr; bool m_spotActive = false; + bool m_actionButtonPressed = false; std::shared_ptr m_virtualMouseDevice; std::shared_ptr m_virtualKeyDevice; Settings* m_settings = nullptr;