blob: 922a1fb4a2a2a476fe53ea88871f46672a600ccc [file] [log] [blame]
// Copyright 2024 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "libtouchraw/parser.h"
#include <limits>
#include <memory>
#include <span>
#include <string>
#include <utility>
#include <vector>
#include <linux/hidraw.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/sysmacros.h>
#include <base/files/file.h>
#include <base/logging.h>
#include <base/memory/ptr_util.h>
#include <brillo/udev/udev.h>
#include <brillo/udev/udev_device.h>
namespace touchraw {
constexpr int kByte = 0x08; // Number of bits per byte.
constexpr int kShortItemSize = 0x04; // Maximum data size for short items.
constexpr int kHidDigitizersPage = 0x0d;
constexpr int kHidDGHeatMapProtocolVendorID = 0x6a;
constexpr int kHidDGHeatMapProtocolVersion = 0x6b;
constexpr int kHidDGScanTime = 0x56;
constexpr int kHidDGHeatMapFrameData = 0x6c;
constexpr int kHidGenericDesktopPage = 0x01;
constexpr int kHidGDByteCount = 0x3b;
constexpr int kHidGenericDeviceControlsPage = 0x06;
constexpr int kHidGDCSequenceID = 0x27;
constexpr int kHIDLongItemPrefix = 0xfe;
constexpr int kHIDItemPrefixMask = 0xfc;
constexpr int kHIDGlobalItemTagUsagePage = 0x04;
constexpr int kHIDLocalItemTagUsage = 0x08;
constexpr int kHIDMainItemTagInput = 0x80;
constexpr int kHIDGlobalItemTagReportID = 0x84;
constexpr int kHIDGlobalItemTagReportSize = 0x74;
constexpr int kHIDGlobalItemTagReportCount = 0x94;
ReportDescriptor::ReportDescriptor(const hidraw_report_descriptor* rpt_desc)
: rpt_desc_(rpt_desc), next_item_idx_(0) {}
bool ReportDescriptor::HasNextItem() {
return next_item_idx_ < rpt_desc_->size;
}
Item ReportDescriptor::GetNextItem() {
Item item;
int cur_item_idx = next_item_idx_;
int data_offset; // Data starting index of an item.
// Process item prefix.
if (cur_item_idx < rpt_desc_->size) {
item.prefix = rpt_desc_->value[cur_item_idx];
if (rpt_desc_->value[cur_item_idx] == kHIDLongItemPrefix) { // Long item.
data_offset = 3;
item.data_size = rpt_desc_->value[cur_item_idx + 1];
} else { // Short item.
data_offset = 1;
item.data_size =
((item.prefix & 0x03) == 0x03) ? 4 : (item.prefix & 0x03);
}
item.size = item.data_size + data_offset;
next_item_idx_ += item.size; // Update the index to next item.
}
// Copy item data.
if (next_item_idx_ <= rpt_desc_->size) {
item.data =
std::span<const uint8_t>(rpt_desc_->value + cur_item_idx + data_offset,
rpt_desc_->value + next_item_idx_);
}
return item;
}
void ReportDescriptor::Reset() {
next_item_idx_ = 0;
}
std::unique_ptr<Parser> Parser::Create(
const int fd, std::unique_ptr<HeatmapChunkConsumerInterface> q) {
// Using `new` to access a non-public constructor.
std::unique_ptr<Parser> parser = base::WrapUnique(new Parser(std::move(q)));
hidraw_report_descriptor rpt_desc;
if (parser->GetReportDescriptorSysfs(fd, &rpt_desc) != 0) {
if (parser->GetReportDescriptorIoctl(fd, &rpt_desc) != 0) {
return nullptr;
}
}
if (!parser->ParseHeatmapReportsFromDescriptor(&rpt_desc)) {
LOG(WARNING) << "The report descriptor does not support heatmap";
return nullptr;
}
return parser;
}
std::unique_ptr<Parser> Parser::CreateForTesting(
std::unique_ptr<HeatmapChunkConsumerInterface> q) {
return base::WrapUnique(new Parser(std::move(q)));
}
Parser::Parser(std::unique_ptr<HeatmapChunkConsumerInterface> q)
: q_(std::move(q)) {}
int Parser::GetReportDescriptorIoctl(const int fd,
hidraw_report_descriptor* rpt_desc) {
int res, desc_size = 0;
LOG(INFO) << "Get report descriptor from hidraw.";
// Get Report Descriptor Size.
res = ioctl(fd, HIDIOCGRDESCSIZE, &desc_size);
if (res < 0) {
LOG(WARNING) << "Failed to get report descriptor size: " << strerror(errno);
return res;
}
// Get Report Descriptor.
rpt_desc->size = desc_size;
res = ioctl(fd, HIDIOCGRDESC, rpt_desc);
if (res < 0) {
LOG(WARNING) << "Failed to get report descriptor: " << strerror(errno);
}
return res;
}
std::unique_ptr<brillo::UdevDevice> Parser::CreateUdevDevice(const int fd) {
base::stat_wrapper_t stat_buf;
dev_t devnum;
// Get the dev_t (major/minor numbers) from the file handle.
if (base::File::Fstat(fd, &stat_buf) == -1) {
LOG(WARNING) << "Failed to stat device handle " << fd << ": "
<< strerror(errno);
return nullptr;
}
devnum = stat_buf.st_rdev;
// Create a udev device.
std::unique_ptr<brillo::Udev> udev = brillo::Udev::Create();
auto dev = udev->CreateDeviceFromDeviceNumber('c', devnum);
if (!dev) {
LOG(WARNING) << "Could not get udev entry for device with MAJOR: "
<< major(devnum) << " MINOR: " << minor(devnum);
return nullptr;
}
return dev;
}
int Parser::GetReportDescriptorSysfs(const int fd,
hidraw_report_descriptor* rpt_desc) {
int res;
std::string rpt_path;
auto dev = CreateUdevDevice(fd);
if (!dev) {
LOG(WARNING) << "Failed to create a udev device";
return -1;
}
// Construct <sysfs_path>/device/report_descriptor.
rpt_path = std::string(dev->GetSysPath()) + "/device/report_descriptor";
base::File file(base::FilePath(rpt_path),
base::File::FLAG_OPEN | base::File::FLAG_READ);
if (!file.IsValid()) {
LOG(WARNING) << "Could not open " << rpt_path;
return -1;
}
memset(rpt_desc, 0x0, sizeof(*rpt_desc));
res = file.Read(0, reinterpret_cast<char*>(rpt_desc->value),
sizeof(rpt_desc->value));
if (res < 0) {
LOG(WARNING) << "Error reading " << rpt_path;
}
rpt_desc->size = static_cast<uint32_t>(res);
return res;
}
uint32_t Parser::GetPropValue(std::span<const uint8_t> data, int data_size) {
// Does not support long items for now.
if (data_size > kShortItemSize) {
LOG(WARNING) << "Not supported - data size is " << data_size;
return std::numeric_limits<uint32_t>::max();
}
uint32_t value = 0;
for (int i = 0; i < data_size; ++i) {
value |= data[i] << (i * kByte);
}
return value;
}
void Parser::ProcessItem(Item& item) {
switch (item.prefix & kHIDItemPrefixMask) {
case kHIDGlobalItemTagUsagePage:
info_.usage_page = GetPropValue(item.data, item.data_size);
break;
case kHIDLocalItemTagUsage:
info_.usage = GetPropValue(item.data, item.data_size);
break;
case kHIDMainItemTagInput:
if (info_.usage_page == kHidDigitizersPage &&
info_.usage == kHidDGHeatMapProtocolVendorID) {
if (!sync_report_offset_.has_value()) {
sync_report_offset_ = usages_.size();
} else {
sub_report_offset_ = usages_.size();
}
}
usages_.push_back({info_.usage_page, info_.usage, info_.report_id,
static_cast<uint16_t>(info_.report_size *
info_.report_count / kByte)});
break;
case kHIDGlobalItemTagReportID:
info_.report_id = item.data[0];
break;
case kHIDGlobalItemTagReportSize:
info_.report_size = GetPropValue(item.data, item.data_size);
break;
case kHIDGlobalItemTagReportCount:
info_.report_count = GetPropValue(item.data, item.data_size);
break;
default:
break;
}
}
bool Parser::ParseHeatmapReportsFromDescriptor(
const hidraw_report_descriptor* rpt_desc) {
ReportDescriptor descriptor(rpt_desc);
LOG(INFO) << "Parse report descriptor.";
// Parse report descriptor.
// TODO: b/320780085 - Validate report descriptor collection that represents
// the heatmap data.
while (descriptor.HasNextItem()) {
Item item = descriptor.GetNextItem();
ProcessItem(item);
}
return sync_report_offset_.has_value();
}
void Parser::ParseHIDData(std::unique_ptr<const HIDData> hid_data) {
int cur = 0;
uint16_t offset = 0;
auto chunk = std::make_unique<HeatmapChunk>();
// Discards HIDData for unsupported report_ids.
if (sync_report_offset_ &&
hid_data->report_id == usages_[sync_report_offset_.value()].report_id) {
offset = sync_report_offset_.value();
} else if (sub_report_offset_ &&
hid_data->report_id ==
usages_[sub_report_offset_.value()].report_id) {
offset = sub_report_offset_.value();
} else {
LOG(INFO) << "Report id " << static_cast<int>(hid_data->report_id)
<< ": Not heat map data.";
return;
}
chunk->report_type = ReportType::kInvalid;
for (int i = offset; usages_[i].report_id == hid_data->report_id; ++i) {
switch (usages_[i].usage_page) {
case kHidDigitizersPage:
switch (usages_[i].usage) {
case kHidDGHeatMapProtocolVendorID:
chunk->vendor_id =
GetDataField(cur, usages_[i].data_size, hid_data->payload);
break;
case kHidDGHeatMapProtocolVersion:
chunk->protocol_version =
GetDataField(cur, usages_[i].data_size, hid_data->payload);
break;
case kHidDGScanTime:
chunk->scan_time =
GetDataField(cur, usages_[i].data_size, hid_data->payload);
break;
case kHidDGHeatMapFrameData:
// TODO: b/320780085 - Validate report descriptor collection that
// represents the heatmap data
chunk->payload.assign(hid_data->payload.begin() + cur,
hid_data->payload.end());
if (chunk->payload.size() != usages_[i].data_size) {
LOG(WARNING) << "Discard this chunk because chunk size "
<< chunk->payload.size()
<< " is not equal to the expected size "
<< usages_[i].data_size;
return;
}
break;
default:
break;
}
break;
case kHidGenericDesktopPage:
switch (usages_[i].usage) {
case kHidGDByteCount:
chunk->byte_count =
GetDataField(cur, usages_[i].data_size, hid_data->payload);
chunk->report_type = ReportType::kFirst;
break;
default:
break;
}
break;
case kHidGenericDeviceControlsPage:
switch (usages_[i].usage) {
case kHidGDCSequenceID:
chunk->sequence_id =
GetDataField(cur, usages_[i].data_size, hid_data->payload);
chunk->report_type = ReportType::kSubsequent;
break;
default:
break;
}
break;
default:
break;
}
cur += usages_[i].data_size;
}
// Dispatch.
q_->Push(std::move(chunk));
}
// Support short items only - data size is limited to 4 bytes.
uint32_t Parser::GetDataField(int index,
int size,
std::span<const uint8_t> payload) {
if ((index + size) >= payload.size()) {
LOG(WARNING) << "Data out of range.";
return std::numeric_limits<uint32_t>::max();
}
// Does not support long items for now.
if (size > kShortItemSize) {
LOG(WARNING) << "Not supported - data size is " << size;
return std::numeric_limits<uint32_t>::max();
}
uint32_t value = 0;
for (int i = 0; i < size; ++i) {
value |= payload[index + i] << (kByte * i);
}
return value;
}
} // namespace touchraw