mirror of
https://github.com/PixlOne/logiops.git
synced 2025-07-13 21:02:43 +08:00
348 lines
11 KiB
C++
348 lines
11 KiB
C++
/*
|
|
* Copyright 2019-2023 PixlOne
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
#include <backend/hidpp20/Device.h>
|
|
#include <backend/hidpp20/features/Root.h>
|
|
#include <backend/hidpp20/features/DeviceName.h>
|
|
#include <backend/hidpp20/Feature.h>
|
|
#include <backend/hidpp10/Receiver.h>
|
|
#include <backend/Error.h>
|
|
#include <cassert>
|
|
#include <utility>
|
|
|
|
using namespace logid::backend;
|
|
using namespace logid::backend::hidpp;
|
|
|
|
using namespace std::chrono;
|
|
|
|
const char* Device::InvalidDevice::what() const noexcept {
|
|
switch (_reason) {
|
|
case NoHIDPPReport:
|
|
return "Invalid HID++ device";
|
|
case InvalidRawDevice:
|
|
return "Invalid raw device";
|
|
case Asleep:
|
|
return "Device asleep";
|
|
case VirtualNode:
|
|
return "Virtual device";
|
|
default:
|
|
return "Invalid device";
|
|
}
|
|
}
|
|
|
|
Device::InvalidDevice::Reason Device::InvalidDevice::code() const noexcept {
|
|
return _reason;
|
|
}
|
|
|
|
Device::Device(const std::string& path, DeviceIndex index,
|
|
const std::shared_ptr<raw::DeviceMonitor>& monitor, double timeout) :
|
|
io_timeout(duration_cast<milliseconds>(
|
|
duration<double, std::milli>(timeout))),
|
|
_raw_device(std::make_shared<raw::RawDevice>(path, monitor)),
|
|
_receiver(nullptr), _path(path), _index(index) {
|
|
}
|
|
|
|
Device::Device(std::shared_ptr<raw::RawDevice> raw_device, DeviceIndex index,
|
|
double timeout) :
|
|
io_timeout(duration_cast<milliseconds>(
|
|
duration<double, std::milli>(timeout))),
|
|
_raw_device(std::move(raw_device)), _receiver(nullptr),
|
|
_path(_raw_device->rawPath()), _index(index) {
|
|
}
|
|
|
|
Device::Device(const std::shared_ptr<hidpp10::Receiver>& receiver,
|
|
hidpp::DeviceConnectionEvent event, double timeout) :
|
|
io_timeout(duration_cast<milliseconds>(
|
|
duration<double, std::milli>(timeout))),
|
|
_raw_device(receiver->rawDevice()), _receiver(receiver),
|
|
_path(receiver->rawDevice()->rawPath()), _index(event.index) {
|
|
// Device will throw an error soon, just do it now
|
|
if (!event.linkEstablished)
|
|
throw InvalidDevice(InvalidDevice::Asleep);
|
|
|
|
if (!event.fromTimeoutCheck)
|
|
_pid = event.pid;
|
|
else
|
|
_pid = receiver->getPairingInfo(_index).pid;
|
|
}
|
|
|
|
Device::Device(const std::shared_ptr<hidpp10::Receiver>& receiver,
|
|
DeviceIndex index, double timeout) :
|
|
io_timeout(duration_cast<milliseconds>(
|
|
duration<double, std::milli>(timeout))),
|
|
_raw_device(receiver->rawDevice()),
|
|
_receiver(receiver), _path(receiver->rawDevice()->rawPath()),
|
|
_index(index) {
|
|
_pid = receiver->getPairingInfo(_index).pid;
|
|
}
|
|
|
|
const std::string& Device::devicePath() const {
|
|
return _path;
|
|
}
|
|
|
|
DeviceIndex Device::deviceIndex() const {
|
|
return _index;
|
|
}
|
|
|
|
const std::tuple<uint8_t, uint8_t>& Device::version() const {
|
|
return _version;
|
|
}
|
|
|
|
|
|
void Device::_setupReportsAndInit() {
|
|
_event_handlers = std::make_shared<EventHandlerList<Device>>();
|
|
|
|
supported_reports = getSupportedReports(_raw_device->reportDescriptor());
|
|
if (!supported_reports)
|
|
throw InvalidDevice(InvalidDevice::NoHIDPPReport);
|
|
|
|
/* hid_logitech_dj creates virtual /dev/hidraw nodes for receiver
|
|
* devices. We should ignore these devices.
|
|
*/
|
|
if (_index == hidpp::DefaultDevice) {
|
|
_raw_handler = _raw_device->addEventHandler(
|
|
{[](const std::vector<uint8_t>& report) -> bool {
|
|
return (report[Offset::Type] == Report::Type::Short ||
|
|
report[Offset::Type] == Report::Type::Long);
|
|
},
|
|
[self_weak = _self](const std::vector<uint8_t>& report) -> void {
|
|
Report _report(report);
|
|
if(auto self = self_weak.lock())
|
|
self->handleEvent(_report);
|
|
}});
|
|
|
|
try {
|
|
auto rsp = sendReport({ReportType::Short, _index,
|
|
hidpp20::FeatureID::ROOT, hidpp20::Root::Ping,
|
|
hidpp::softwareID});
|
|
if (rsp.deviceIndex() != _index)
|
|
throw InvalidDevice(InvalidDevice::VirtualNode);
|
|
} catch (hidpp10::Error& e) {
|
|
if (e.deviceIndex() != _index)
|
|
throw InvalidDevice(InvalidDevice::VirtualNode);
|
|
} catch (hidpp20::Error& e) {
|
|
/* This shouldn't happen, the device must not be ready */
|
|
if (e.deviceIndex() != _index)
|
|
throw InvalidDevice(InvalidDevice::VirtualNode);
|
|
else
|
|
throw DeviceNotReady();
|
|
}
|
|
}
|
|
|
|
_raw_handler = _raw_device->addEventHandler(
|
|
{[index = _index](
|
|
const std::vector<uint8_t>& report) -> bool {
|
|
return (report[Offset::Type] == Report::Type::Short ||
|
|
report[Offset::Type] == Report::Type::Long) &&
|
|
(report[Offset::DeviceIndex] == index);
|
|
},
|
|
[self_weak = _self](const std::vector<uint8_t>& report) -> void {
|
|
Report _report(report);
|
|
if(auto self = self_weak.lock())
|
|
self->handleEvent(_report);
|
|
}});
|
|
|
|
_init();
|
|
}
|
|
|
|
void Device::_init() {
|
|
try {
|
|
hidpp20::Root root(this);
|
|
_version = root.getVersion();
|
|
} catch (hidpp10::Error& e) {
|
|
// Valid HID++ 1.0 devices should send an InvalidSubID error
|
|
if (e.code() != hidpp10::Error::InvalidSubID)
|
|
throw;
|
|
|
|
// HID++ 2.0 is not supported, assume HID++ 1.0
|
|
_version = std::make_tuple(1, 0);
|
|
} catch (hidpp20::Error& e) {
|
|
/* Should never happen, device not ready? */
|
|
throw DeviceNotReady();
|
|
}
|
|
|
|
/* Do a stability test before going further */
|
|
if (std::get<0>(_version) >= 2) {
|
|
if (!isStable20()) {
|
|
throw DeviceNotReady();
|
|
}
|
|
} else {
|
|
if (!isStable10()) {
|
|
throw DeviceNotReady();
|
|
}
|
|
}
|
|
|
|
if (!_receiver) {
|
|
_pid = _raw_device->productId();
|
|
if (std::get<0>(_version) >= 2) {
|
|
try {
|
|
hidpp20::DeviceName deviceName(this);
|
|
_name = deviceName.getName();
|
|
} catch (hidpp20::UnsupportedFeature& e) {
|
|
_name = _raw_device->name();
|
|
}
|
|
} else {
|
|
_name = _raw_device->name();
|
|
}
|
|
} else {
|
|
if (std::get<0>(_version) >= 2) {
|
|
try {
|
|
hidpp20::DeviceName deviceName(this);
|
|
_name = deviceName.getName();
|
|
} catch (hidpp20::UnsupportedFeature& e) {
|
|
_name = _receiver->getDeviceName(_index);
|
|
}
|
|
} else {
|
|
_name = _receiver->getDeviceName(_index);
|
|
}
|
|
}
|
|
}
|
|
|
|
EventHandlerLock<Device> Device::addEventHandler(EventHandler handler) {
|
|
std::unique_lock lock(_event_handlers->mutex);
|
|
_event_handlers->list.emplace_front(std::move(handler));
|
|
return {_event_handlers, _event_handlers->list.cbegin()};
|
|
}
|
|
|
|
void Device::handleEvent(Report& report) {
|
|
if (responseReport(report))
|
|
return;
|
|
|
|
std::shared_lock lock(_event_handlers->mutex);
|
|
for (auto& handler: _event_handlers->list)
|
|
if (handler.condition(report))
|
|
handler.callback(report);
|
|
}
|
|
|
|
Report Device::sendReport(const Report& report) {
|
|
/* Must complete transaction before next send */
|
|
std::lock_guard send_lock(_send_mutex);
|
|
_sent_sub_id = report.subId();
|
|
std::unique_lock lock(_response_mutex);
|
|
_sendReport(report);
|
|
|
|
bool valid = _response_cv.wait_for(
|
|
lock, io_timeout, [this]() {
|
|
return _response.has_value();
|
|
});
|
|
|
|
if (!valid) {
|
|
_sent_sub_id.reset();
|
|
throw TimeoutError();
|
|
}
|
|
|
|
Response response = _response.value();
|
|
_response.reset();
|
|
_sent_sub_id.reset();
|
|
|
|
if (std::holds_alternative<Report>(response)) {
|
|
return std::get<Report>(response);
|
|
} else if (std::holds_alternative<Report::Hidpp10Error>(response)) {
|
|
auto error = std::get<Report::Hidpp10Error>(response);
|
|
throw hidpp10::Error(error.error_code, error.device_index);
|
|
} else if (std::holds_alternative<Report::Hidpp20Error>(response)) {
|
|
auto error = std::get<Report::Hidpp20Error>(response);
|
|
throw hidpp20::Error(error.error_code, error.device_index);
|
|
}
|
|
|
|
// Should not be reached
|
|
throw std::runtime_error("unhandled variant type");
|
|
}
|
|
|
|
bool Device::responseReport(const Report& report) {
|
|
std::lock_guard lock(_response_mutex);
|
|
Response response = report;
|
|
uint8_t sub_id;
|
|
|
|
Report::Hidpp10Error hidpp10_error{};
|
|
Report::Hidpp20Error hidpp20_error{};
|
|
if (report.isError10(hidpp10_error)) {
|
|
sub_id = hidpp10_error.sub_id;
|
|
response = hidpp10_error;
|
|
} else if (report.isError20(hidpp20_error)) {
|
|
sub_id = hidpp20_error.feature_index;
|
|
response = hidpp20_error;
|
|
} else {
|
|
sub_id = report.subId();
|
|
}
|
|
|
|
if (sub_id == _sent_sub_id) {
|
|
_response = response;
|
|
_response_cv.notify_all();
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
}
|
|
|
|
const std::shared_ptr<raw::RawDevice>& Device::rawDevice() const {
|
|
return _raw_device;
|
|
}
|
|
|
|
void Device::_sendReport(Report report) {
|
|
reportFixup(report);
|
|
_raw_device->sendReport(report.rawReport());
|
|
}
|
|
|
|
void Device::sendReportNoACK(const Report& report) {
|
|
std::lock_guard lock(_send_mutex);
|
|
_sendReport(report);
|
|
}
|
|
|
|
bool Device::isStable10() {
|
|
return true;
|
|
}
|
|
|
|
bool Device::isStable20() {
|
|
static constexpr std::string ping_seq = "hello";
|
|
|
|
hidpp20::Root root(this);
|
|
|
|
try {
|
|
for (auto c: ping_seq) {
|
|
if (root.ping(c) != c)
|
|
return false;
|
|
}
|
|
} catch (std::exception& e) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void Device::reportFixup(Report& report) const {
|
|
switch (report.type()) {
|
|
case Report::Type::Short:
|
|
if (!(supported_reports & ShortReportSupported))
|
|
report.setType(Report::Type::Long);
|
|
break;
|
|
case Report::Type::Long:
|
|
/* Report can be truncated, but that isn't a good idea. */
|
|
assert(supported_reports & LongReportSupported);
|
|
}
|
|
}
|
|
|
|
const std::string& Device::name() const {
|
|
return _name;
|
|
}
|
|
|
|
uint16_t Device::pid() const {
|
|
return _pid;
|
|
}
|