Так сложилось, что в Qt4 Embedded, которую мы используем на нашем приборе Беркут-ММТ, нет поддержки таких устройств ввода, как энкодер. Т.е. если прицепить к прибору мышь — координаты при перемещении обрабатываться будут, а вот колесо прокрутки — нет. Потому что драйвер linuxinput не обрабатывает события с типом REL_WHEEL, которое генерит энкодер, а только REL_X и REL_Y, которые отвечают за изменение координат.
Кому интересно как эту проблему решить — добро пожаловать под кат.
Вот кусочек кода драйвера linuxinput, который занимается обработкой событий от input подсистемы ядра Linux:
for (int i = 0; i < n; ++i) {
struct ::input_event *data = &buffer[i];
bool unknown = false;
if (data->type == EV_ABS) {
if (data->code == ABS_X) {
m_x = data->value;
} else if (data->code == ABS_Y) {
m_y = data->value;
} else {
unknown = true;
}
} else if (data->type == EV_REL) {
if (data->code == REL_X) {
m_x += data->value;
} else if (data->code == REL_Y) {
m_y += data->value;
} else {
unknown = true;
}
} else if (data->type == EV_KEY && data->code == BTN_TOUCH) {
m_buttons = data->value ? Qt::LeftButton : 0;
} else if (data->type == EV_KEY) {
int button = 0;
switch (data->code) {
case BTN_LEFT:
button = Qt::LeftButton;
break;
case BTN_MIDDLE:
button = Qt::MidButton;
break;
case BTN_RIGHT:
button = Qt::RightButton;
break;
}
if (data->value)
m_buttons |= button;
else
m_buttons &= ~button;
} else if (data->type == EV_SYN && data->code == SYN_REPORT) {
QPoint pos(m_x, m_y);
pos = m_handler->transform(pos);
m_handler->limitToScreen(pos);
m_handler->mouseChanged(pos, m_buttons);
} else if (data->type == EV_MSC && data->code == MSC_SCAN) {
// kernel encountered an unmapped key - just ignore it continue;
} else {
unknown = true;
}
if (unknown) {
qWarning("unknown mouse event type=%x, code=%x, value=%x", data->type, data->code, data->value);
}
}
Решаем проблемы
У нас есть три варианта:
- модифицировать драйвер linuxinput
- модифицировать ядерный драйвер таким образом, чтобы он генерил события, понятные для драйвера linuxinput
- написать свой драйвер устройства ввода для Qt4
Третий вариант — самый правильный. Его и рассмотрим.
Пишем драйвер
Для создания своего драйвера нужно написать два класса — наследника QWSMouseHandler и наследника QWSMousePlugin. Задача первого — непосредственно работа с устройством ввода, задача второго — объяснить QMouseDriverFactory, что для драйвера с именем %drivername% надо использовать нашу реализацию наследника QWSMouseHandler.
Начнем с класса-наследника QWSMouseHandler:
class RotaryEncoderHandler: public QObject, public QWSMouseHandler {
Q_OBJECT
public:
RotaryEncoderHandler( const QString &device = QString("/dev/input/rotary_encoder" ) );
~RotaryEncoderHandler( );
void suspend( );
void resume ( );
private:
QSocketNotifier *m_notify;
int deviceFd;
int m_wheel;
private slots:
void readMouseData( );
};
Как видно из заголовочного файла — нам надо реализовать аж целых три функции: suspend(), resume(), readMouseData(). Ну и конструктор с деструктором.
Конструктор — в качестве аргумента к нам приходит имя устройства — /dev/input/event3, например. Далее наша задача открыть файловый дескриптор устройства с указанным именем и передать его на растерзание в QSocketNotifier. QSocketNotifier — это такой зверь, который слушает файловый дескриптор и на любые его телодвижения эмитит сигнал activated(int).
RotaryEncoderHandler::RotaryEncoderHandler( const QString &device ): QWSMouseHandler( device )
,deviceFd( 0 )
,m_wheel( 0 )
{
setObjectName("Rotary Encoder Handler");
deviceFd = ::open(device.toLocal8Bit().constData(), O_RDONLY | O_NDELAY);
if( deviceFd > 0 ){
qDebug() << "Opened" << device << "as rotary encoder device";
m_notify = new QSocketNotifier( deviceFd, QSocketNotifier::Read, this);
connect( m_notify, SIGNAL( activated(int)), this,
SLOT( readMouseData()));
} else {
qWarning("Cannot open %s: %s", device.toLocal8Bit().constData(), strerror( errno ) );
return;
}
}
Т.е. мы открыли дескриптор устройства ввода, прицепили к нему QSocketNotifier и на его сигнал activated( int ) повесили свой обработчик.
Деструктор у этого класса совсем простой — его задача проверить, открыт ли дескриптор устройства ввода и если да — закрыть.
Методы suspend()/resume() должны останавливать/запускать обработку данных из устройства ввода. Это делается простым вызовом метода setEnabled( bool ) у QSocketNotifier.
Вот мы и подобрались непосредственно к обработчику данных.
void RotaryEncoderHandler::readMouseData( )
{
struct ::input_event buffer[32];
int n = 0;
forever {
n = ::read(deviceFd, reinterpret_cast(buffer) + n, sizeof(buffer) - n);
if (n == 0) {
qWarning("Got EOF from the input device.");
return;
} else if (n < 0 && (errno != EINTR && errno != EAGAIN)) {
qWarning("Could not read from input device: %s", strerror(errno));
return;
} else if (n % sizeof(buffer[0]) == 0) {
break;
}
}
n /= sizeof(buffer[0]);
for (int i = 0; i < n; ++i) {
struct ::input_event *data = &buffer[i];
bool unknown = false;
if (data->type == EV_REL) {
if (data->code == REL_WHEEL) {
m_wheel = data->value;
} else {
unknown = true;
}
} else if (data->type == EV_SYN && data->code == SYN_REPORT) {
mouseChanged(pos(), Qt::NoButton, m_wheel);
} else if (data->type == EV_MSC && data->code == MSC_SCAN) {
// kernel encountered an unmapped key - just ignore it
continue;
} else {
unknown = true;
}
if (unknown) {
qWarning("unknown mouse event type=%x, code=%x, value=%x", data->type, data->code, data->value);
}
}
}
Он сильно напоминает аналогичный метод из драйвера linuxinput, но в отличии от него, передает только события с изменениями состояния энкодера. Т.е. этот драйвер нельзя как есть использовать для мыши, так как в нем отсутствует обработка изменений координат самой мыши — ничего кроме колеса прокрутки работать не будет.
Теперь посмотрим что из себя представляет класс драйвера:
class RotaryEncoderDriverPlugin : public QMouseDriverPlugin {
Q_OBJECT
public:
RotaryEncoderDriverPlugin( QObject *parent = 0 );
~RotaryEncoderDriverPlugin();
QWSMouseHandler* create(const QString& driver);
QWSMouseHandler* create(const QString& driver, const QString& device);
QStringList keys()const;
};
Не очень большой, правда? Вот его реализация:
Q_EXPORT_PLUGIN2(rotaryencoderdriver, RotaryEncoderDriverPlugin)
RotaryEncoderDriverPlugin::RotaryEncoderDriverPlugin( QObject *parent ):
QMouseDriverPlugin( parent )
{
}
RotaryEncoderDriverPlugin::~RotaryEncoderDriverPlugin()
{
}
QStringList RotaryEncoderDriverPlugin::keys() const
{
return QStringList() <<"rotaryencoderdriver";
}
QWSMouseHandler* RotaryEncoderDriverPlugin::create( const QString& driver,
const QString& device )
{
if( driver.toLower() == "rotaryencoderdriver" ){
return new RotaryEncoderHandler( device );
}
return 0;
}
QWSMouseHandler* RotaryEncoderDriverPlugin::create( const QString& driver )
{
if( driver.toLower() == "rotaryencoderdriver" ){
return new RotaryEncoderHandler( );
}
return 0;
}
Как видно из кода — вся задача драйвера сводится к тому, чтобы сообщить классу QMouseDriverFactory что это драйвер с именем rotaryencoderdriver. Ну и методы create(), конечно.
Проверка боем
Теперь, когда у нас есть драйвер — надо как-то объяснить библиотеке Qt4 что именно его нужно использовать для определенного устройства. Для этого есть специальная переменная окружения — QWS_MOUSE_PROTO. Она служит для того, чтобы указать Qt4 каким драйвером и из какого устройства брать данные о перемещении мыши. Предположим что наш энкодер - /dev/input/rotary0, следовательно чтобы все заработало, надо установить переменную как QWS_MOUSE_PROTO=«rotaryencoderdriver:/dev/input/rotary0».
Ловим события от энкодера
Для работы с событиями энкодера надо в нашем приложении реализовать фильтр событий:
bool ClassName::eventFilter(QObject *o, QEvent *e)
{
if ( o ) {
if ( e->type() == QEvent::Wheel)
{
QWheelEvent* we = static_cast< QWheelEvent* >( e );
/* тут обрабатываем событие как нам нужно */
return true;
}
/* остальные события отдадим в Object*/
return QObject::eventFilter( o, e );
}
Полезные ссылки
Update: для наглядности добавлено видео