[#] [DIY] Как банка тушёнки улучшит жизнь в Vim & Emacs
vit01(mira, 1) — All
2017-07-16 09:21:54


Прочитав статью на Хабре про педалирование Vim ( https://habrahabr.ru/post/232177 ), я загорелся идеей сделать себе самодельную педальку для текстовых редакторов.
Как из того анекдота про педаль для Emacs, чтобы Ctrl зажимать, только уже не как анекдот, а на самом деле.

В оригинальной статье мужик закупил уже готовый корпус для педали и всунул туда контроллер. Однако мне ждать посылку из Китая было бы крайне грустно, хотелось сделать побыстрее и с использованием подручных материалов.

Внимание, даже если у вас нет Arduino, то всё равно дочитайте статью, там будет кое-что интересное про Linux!

Итак, если у нас будет педаль, то надо же куда-то ногой нажимать, верно? Съев на даче тушёнку, я не поспешил выбрасывать банку из-под неё, а вымыл и унёс домой.

Фотка 1: https://alicorn.tk/dashie/index.php/s/cIjtxJbHZJnNFbz

Затем выбил у банки дно, распрямил развёртку плоскогубцами и загнул острые края, чтобы не поранить ногу. Получилась довольно широкая поверхность, на которую можно нажимать.

Фотка 2: https://alicorn.tk/dashie/index.php/s/LRPbdF5O9DBL535

После этого я разыскал у себя в квартире разные деревяшки, попробовал их скрепить вместе, чтобы получить внешний держатель для педали, которая будет работать по принципу противовесов. Это самая простая схема, особенно когда нет пружины, которую можно было бы прикрепить.

Самый первый вариант крепления: фотка 3: https://alicorn.tk/dashie/index.php/s/SA4jEyiOgFwEvWX

Учитель трудов в школе за такую халтуру поставил бы 2 или заставил бы переделывать (потому что на месте креплений внизу планка скололась; делалось всё криво, на глазок, и ничего не отшлифовано), но работать оно работает. Тем более, переделывать мне больше не на чем, т.к. дома и так хорошего дерева в дефиците.

Самая маленькая (и длинная) деревяшка крепится к жестяной пластине с помощью рыболовной лески, продетой в сквозные отверстия и завязанной в узел. К вертикальным перегородкам она приделана с помощью клея "Момент" в специально выточенную пазуху.

Сверху леска проходит ненадёжно, так что груз-противовес срывается, если резко дёрнуть педаль. Но ничего, не проблема, делаем шилом ещё два отверстия сверху и закрепляем груз снова. Ещё укрепим вертикальные планки, чтобы они не съезжались во время работы.

Фотка 4: https://alicorn.tk/dashie/index.php/s/biXXRbjlbcfV2s3

Итак, механическая часть готова, теперь займёмся программной. Нам понадобится любая модель Arduino, способная подключаться по USB (Uno, Nano, Mega и.т.д, в моём случае - Uno). При нажатии педали будет срабатывать датчик, который будет отлавливаться контроллером. В качестве датчика можно использовать обычную кнопку, детектор магнитного поля (геркон) или любой самодельный прибамбас, который умеет замыкать цепь. Геркон я попробовал, и это было неудобно, поэтому взял кнопку.

Arduino версий Micro и Leonardo умеет эмулировать HID-устройства, поэтому мы можем назначить пересылку любой клавиши, а решение будет кроссплатформенным. Кстати, на Windows и в графических DE вроде KDE/GNOME педаль была бы полезна для Alt+Tab или для переключения раскладки с русской на английскую.

Как позднее оказалось, Arduino Uno/Mega и им подобные требуют перепрошивки USB-контроллера, потому что родной поддержки эмуляции HID у них нет. Но мой Uno - китайский, так что нужных разъёмов для этого у него нет. Подключаться же к чипу напрямую мне боязно, потому что он, во-первых, мелкий, во-вторых, в случае кирпича восстанавливать мне его нечем (второй Uno пока у меня нет).

------------

И вот тут начинается поле деятельности для нашей смекалки. Я подумал, а что если передавать сигналы о нажатии через обычный COM-порт ардуины, затем отлавливать его с помощью специального демона и эмулировать клавиатуру на уровне ядра?

Отличная идея! Почесав репу и найдя информацию о модуле ядра под названием UHID (в Archlinux он включен по умолчанию), начал с ним ковыряться. Это было не так-то просто, поэтому пока напишем прошивку для педали.

Скетч для Arduino:

int detector = 13; // наш датчик для педали
int pedal_pressed = LOW;
int last_pressed = LOW;

void setup() {
  pinMode(detector, INPUT);
  Serial.begin(9600);
}

void trigger_press()   { Serial.print("on\n"); }
void trigger_release() { Serial.print("off\n"); }

void loop() {
  // нажата ли педаль в данный момент
  pedal_pressed = digitalRead(detector);

  if (pedal_pressed == HIGH && last_pressed == LOW) trigger_press();
  else if (pedal_pressed == LOW && last_pressed == HIGH) trigger_release();

  last_pressed = pedal_pressed;
  delay(100); // на всякий случай ждём 0.1 сек, дабы не делать слишком много проверок
}

Примеров использования для UHID в сети я практически не нашёл. Есть только один не очень исчерпывающий пример из исходников ядра Linux и кусок документации.

Интересующиеся могут ознакомиться с ними здесь:

https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/hid/uhid.txt?id=refs/tags/v4.10-rc3
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/samples/uhid/uhid-example.c?id=refs/tags/v4.10-rc3

Взяв uhid-example.c из исходников ядра и поработав с ним, я добавил туда куски кода вот отсюда: ii://TIzgUZRrWGcQSEkKBDiP, чтобы читать данные, которые поступают из Arduino.

Код из этого исходника может пригодиться и вам, чтобы эмулировать нажатия клавиш в абсолютно любых программах. Конечно же, нужен root-доступ для запуска, но зато работать оно будет везде: и в консоли, и в Xorg.

#include <errno.h>
#include <fcntl.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>
#include <linux/uhid.h>

/*
rdesc - это магическое заклинание духа хаоса Дискорда
я узнал его, подключив к нетбуку USB-клавиатуру и посмотрев в файл
 /sys/kernel/debug/hid/<dev>/rdesc, чтобы посылать аналогичные заголовки
из программы. Надо ведь как-то притворяться клавиатурой :)
*/

STATIC unsigned char rdesc[] = {
	0x05, 0x01,
	0x09, 0x06,
	0xa1, 0x01,
	0x05, 0x08,
	0x19, 0x01,
	0x29, 0x03,
	0x15, 0x00,
	0x25, 0x01,
	0x75, 0x01,
	0x95, 0x03,
	0x91, 0x02,
	0x95, 0x05,
	0x91, 0x01,
	0x05, 0x07,
	0x19, 0xe0,
	0x29, 0xe7,
	0x95, 0x08,
	0x81, 0x02,
	0x75, 0x08,
	0x95, 0x01,
	0x81, 0x01,
	0x19, 0x00,
	0x29, 0x91,
	0x26, 0xff,
	0x00, 0x95,
	0x06, 0x81,
	0x00, 0xc0
};

static int uhid_write(int fd, const struct uhid_event *ev)
{
	ssize_t ret;

	ret = write(fd, ev, sizeof(*ev));
	if (ret < 0) {
		fprintf(stderr, "Cannot write to uhid: %m\n");
		return -errno;
	} else if (ret != sizeof(*ev)) {
		fprintf(stderr, "Wrong size written to uhid: %ld != %lu\n",
			ret, sizeof(ev));
		return -EFAULT;
	} else {
		return 0;
	}
}

static int create(int fd)
{
	struct uhid_event ev;

	memset(&ev, 0, sizeof(ev));
	ev.type = UHID_CREATE;
	strcpy((char*)ev.u.create.name, "test-uhid-device");
	ev.u.create.rd_data = rdesc;
	ev.u.create.rd_size = sizeof(rdesc);
	ev.u.create.bus = BUS_USB;
	ev.u.create.vendor = 0x15d9;
	ev.u.create.product = 0x0a37;
	ev.u.create.version = 0;
	ev.u.create.country = 0;

	return uhid_write(fd, &ev);
}

static void destroy(int fd)
{
	struct uhid_event ev;

	memset(&ev, 0, sizeof(ev));
	ev.type = UHID_DESTROY;

	uhid_write(fd, &ev);
}

static int event(int fd)
{
	struct uhid_event ev;
	ssize_t ret;

	memset(&ev, 0, sizeof(ev));
	ret = read(fd, &ev, sizeof(ev));
	if (ret == 0) {
		fprintf(stderr, "Read HUP on uhid-cdev\n");
		return -EFAULT;
	} else if (ret < 0) {
		fprintf(stderr, "Cannot read uhid-cdev: %m\n");
		return -errno;
	} else if (ret != sizeof(ev)) {
		fprintf(stderr, "Invalid size read from uhid-dev: %ld != %lu\n",
			ret, sizeof(ev));
		return -EFAULT;
	}

	switch (ev.type) {
	case UHID_START:
		fprintf(stderr, "UHID_START from uhid-dev\n");
		break;
	case UHID_STOP:
		fprintf(stderr, "UHID_STOP from uhid-dev\n");
		break;
	case UHID_OPEN:
		fprintf(stderr, "UHID_OPEN from uhid-dev\n");
		break;
	case UHID_CLOSE:
		fprintf(stderr, "UHID_CLOSE from uhid-dev\n");
		break;
	case UHID_OUTPUT:
		fprintf(stderr, "UHID_OUTPUT from uhid-dev\n");
		break;
	case UHID_OUTPUT_EV:
		fprintf(stderr, "UHID_OUTPUT_EV from uhid-dev\n");
		break;
	default:
		fprintf(stderr, "Invalid event from uhid-dev: %u\n", ev.type);
	}

	return 0;
}

static char ctrl_down = 0;

static int send_event(int fd)
{
  struct uhid_event ev;
  memset(&ev, 0, sizeof(ev));
  ev.type = UHID_INPUT;
  ev.u.input.size = 9;

  // вот это та самая хрень, которая делает виртуальное нажатие
  // первый элемент массива data - это клавиши-модификаторы
  // 0 - это их отсутствие (в данном случае для key_release)
  // а единичка - это как раз левый Ctrl

  ev.u.input.data[0] = ctrl_down;
  ev.u.input.data[1] = 0x0;

  return uhid_write(fd, &ev);
}

int main(int argc, char **argv)
{
	int fd;
	const char *path = "/dev/uhid";
	struct pollfd pfds[2];
	int ret, r;
	struct termios state;
	char serialbuf[5];

	ret = tcgetattr(STDIN_FILENO, &state);
	if (ret) {
		fprintf(stderr, "Cannot get tty state\n");
	} else {
		state.c_lflag &= ~ICANON;
		state.c_cc[VMIN] = 1;
		ret = tcsetattr(STDIN_FILENO, TCSANOW, &state);
		if (ret)
			fprintf(stderr, "Cannot set tty state\n");
	}

	fprintf(stderr, "Open uhid-cdev %s\n", path);
	fd = open(path, O_RDWR | O_CLOEXEC);
	if (fd < 0) {
		fprintf(stderr, "Cannot open uhid-cdev %s: %m\n", path);
		return EXIT_FAILURE;
	}

	fprintf(stderr, "Create uhid device\n");
	ret = create(fd);
	if (ret) {
		close(fd);
		return EXIT_FAILURE;
	}

	pfds[0].fd = STDIN_FILENO;
	pfds[0].events = POLLIN;
	pfds[1].fd = fd;
	pfds[1].events = POLLIN;

	int device_fd = open("/dev/ttyUSB0", O_RDONLY | O_NOCTTY | O_NDELAY);
	if (fd == -1) {
		perror("open_port: Unable to open /dev/ttyUSB0 - ");
		return 1;
	} else {
		fcntl(device_fd, F_SETFL, FNDELAY);
	}
	
	while (1) {
		ret = poll(pfds, 2, -1);
		r = read(device_fd, serialbuf, 5); // получаем данные с педали
		
		if (ret < 0) {
			fprintf(stderr, "Cannot poll for fds: %m\n");
			break;
		}
		if (pfds[0].revents & POLLHUP) {
			fprintf(stderr, "Received HUP on stdin\n");
			break;
		}
		if (pfds[1].revents & POLLHUP) {
			fprintf(stderr, "Received HUP on uhid-cdev\n");
			break;
		}

		if (pfds[0].revents & POLLIN) {
		  // проверка поступающих данных с ардуины
		  if (r > 0 && strncmp(serialbuf, "on\n", 3) == 0) {
		    ctrl_down = 0;
		    send_event(fd);
		    memset(&serialbuf, 0, 5); // очищаем буфер
		  }
		  else if (r > 0 && strncmp(serialbuf, "off\n", 4) == 0) {
		    ctrl_down = 1;
		    send_event(fd);
		    memset(&serialbuf, 0, 5);
		  }
		}
		if (pfds[1].revents & POLLIN) {
			ret = event(fd);
			if (ret)
				break;
		}
	}

	fprintf(stderr, "Destroy uhid device\n");
	destroy(fd);
	return EXIT_SUCCESS;
}

Всё что остаётся - это подключить нашу педаль по USB и запустить скомпилированный бинарник из-под рута.

Сейчас я пишу это сообщение в Emacs с использованием педали. Вполне неплохо работает. Вот видите - даже из банки тушёнки можно что-нибудь полезное запилить :)

В качестве бонуса выкладываю найденный на просторах сети PDF со стандартом на HID-устройства. Любые непонятные моменты там можно уточнить, это очень годный документ: https://alicorn.tk/dashie/index.php/s/dISkfEraH8xVgDm
Первые 5 страниц - вода, читать с 6.