Делаем из Raspberry клавиатуру при помощи PS/2 интерфейса

в 8:39, , рубрики: Raspberry Pi, метки:

Здравствуйте, уважаемые читатели!

В этой публикации я расскажу об эмуляции PS/2 клавиатуры при помощи Raspberry Pi.

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

За день количество проданного товара могло превышать полторы тысячи позиций. Вводить штрих код каждого из них в итальянскую систему стало проблематично. Алгоритм ввода был следующий: ввели штрих код, нажали «Ввод» — и по новой. Договорились встретиться на утро и подробно рассмотреть варианты.

Долго не думая, я написал программу, которая последовательно берет данные с Excel листа и посылает в активное окно. Убедившись, что все работает, пошёл спать.

На следующий день меня ждал сюрприз, о котором я, почему-то, сразу и не подумал. На машине с итальянской программой стояли жёсткие групповые политики, запрещающие запуск сторонних программ по хеш-листу. Админы (тоже итальянцы, кстати) не хотели брать на себя ответственность и разрешать запуск сторонних программ. На обход системы защиты одобрения от местного руководства я не получил. Тут-то и пришла идея решения данной проблемы с аппаратной стороны (ведь никто не запрещает подключать стороннюю клавиатуру). Вспомнил я и про Raspberry Pi, который пылился у меня дома.

Начались поиски документации с описанием PS/2 интерфейса. К счастью, на первом же попавшемся ресурсе (), нашёл всю необходимую информацию.

Распиновка кабеля PS/2 клавиатуры

Как оказалось, необходимыми для всей работы провода было 4.
Черный — земля;
Красный — 5V;
Желтый –пин синхронизации времени (CLOCK);
Белый – пин с данными (DATA).

Цвета могут слегка отличаться. Даже из этих 4-х проводов мне понадобились только два — желтый и белый (в земле и 5v мой аппарат, в отличим от клавиатуры, не нуждался).

Описание работы протокола PS/2 (клавиатура -> хост-контролер)

Определимся с терминологией: если на пине присутствует напряжение — будем считать это состояние как 1, иначе 0.

По умолчанию, когда компьютер в состоянии принимать данные, на обоих пинах (CLOCK и DATA) установлено состояние 1. Его устанавливает хост-контроллер на материнской плате компьютера. Принимаем управление на себя, подавая свое напряжение на оба пина (контроллер м.п. снимет свое напряжение). Для инициализации передачи данных мы должны послать 0-й бит компьютеру (не путать с состоянием 0). Для этого в пине DATA устанавливаем состояние 0 и сразу после этого состояние пина CLOCK тоже переводим на 0 (именно в этом порядке). Мы дали хост-контроллеру понять, что хотим передать первый бит. Теперь, если вернуть состояние пина CLOCK на состояние 1, хост-контролер считает первый бит. Таким образом, будем передавать и все остальные биты.

Первый бит всегда 0, это старт-бит (даем знать, что передаем данные).
Далее передаем 8 бит скан-кода клавиши, которую хотим нажать.
Десятым битом подаем бит четности (если количество единиц четное, то 1, иначе 0).
Последний, 11 бит, это стоп-бит, всегда 1.

Таким образом, один пакет данных формируется из 11 бит.

К примеру, если мы хотим нажать клавишу «0» (скан-код 45h= 1000101 в бинарном виде) на хост-контролер посылается следующий массив бит: 01010001001.

Приняв эти данные, компьютер обработает команду нажатия данной клавиши, но ее еще необходимо отжать. Для этого необходимо сперва послать команду F0h, а после — повторно скан-код клавиши, которую необходимо отжать. Так же, между сменами состояний необходимо удерживать паузу. Опытным путем я установил наиболее подходящее: 0.00020 сек если работать на Python и 1 наносек, если кодить на Java.

Схема подключения к хост-контролеру

image

Несколько слов о том, почему я подключил аппарат параллельно клавиатуре. При включении компьютера BIOS проводит проверку состояния PS/2 разъемов. Компьютер и клавиатура обмениваются данными о готовности работать. Клавиатура должна провести внутреннюю диагностику и доложить компьютеру о своей готовности. Только после этого биос разрешает работать с PS/2 интерфейсом. Реализовывать чтение команд от компьютера я поленился.

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

Программная часть

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

Вот и сам код на Phyton:

import socket, select
import RPi.GPIO as GPIO
import time
#Функция посылает один бит данных к контролеру
def sendBit(pinState):
    if pinState==0:
                GPIO.output(pinData, 0)
    else:
                GPIO.output(pinData, 1)         
    GPIO.output(pinClock, 0)
    time.sleep(sleepInterval)
    GPIO.output(pinClock,1)
    time.sleep(sleepInterval)

#Функция посылает массив данных на компьютер
def sendArray(args):
    GPIO.setmode(GPIO.BCM)
    #инициализация пинов,не изменяя состояние (должно оставатся 1)
            GPIO.setup(pinData,GPIO.OUT, initial=GPIO.HIGH)
    GPIO.setup(pinClock,GPIO.OUT, initial=GPIO.HIGH)
    #посылаю 0 бит
    GPIO.output(pinData, 0)         
    GPIO.output(pinClock, 0)
    time.sleep(sleepInterval)
    GPIO.output(pinClock,1)
    time.sleep(sleepInterval)
    #Посылаю полученный массив данных
    for v in args:
                sendBit(v)
    #Посылаю стоп-бит
    GPIO.output(pinData, 1)
    GPIO.output(pinClock, 0)
    time.sleep(sleepInterval*2)
    GPIO.output(pinClock,1)
    time.sleep(sleepInterval*200)
    GPIO.cleanup()

pinClock=4
pinData=15
sleepInterval=0.00020
CONNECTION_LIST = []
RECV_BUFFER = 4096
PORT = 8928     
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# this has no effect, why ?
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind(("0.0.0.0", PORT))
server_socket.listen(10)
CONNECTION_LIST.append(server_socket)
print "Готов принимать данные на порту " + str(PORT)
while 1:
    read_sockets,write_sockets,error_sockets = select.select(CONNECTION_LIST,[],[])
    for sock in read_sockets:
                if sock == server_socket:
                           sockfd, addr = server_socket.accept()
                           CONNECTION_LIST.append(sockfd)
                else:                            
                           try:
                                       data = sock.recv(RECV_BUFFER)                                        
                                       i=0
                                       scanCode=[]
                                       print "Принял данные:"+data
                                       for bukva in data:
                                                   if bukva=="1":
                                                               scanCode.append(int(1))                                                                   
                                                   else:
                                                               scanCode.append(int(0))        
                                                   i=i+1
                                       sendArray(scanCode)
                                       sendArray([0,0,0,0,1,1,1,1,1])
                                       sendArray(scanCode)
                                        sock.close()
                                       CONNECTION_LIST.remove(sock)  
                           except:
                                       sock.close()
                                       CONNECTION_LIST.remove(sock)
                                               continue

server_socket.close()

Серверная часть на Java:

import com.pi4j.io.gpio.GpioController;
import com.pi4j.io.gpio.GpioFactory;
import com.pi4j.io.gpio.GpioPinDigitalOutput;
import com.pi4j.io.gpio.PinState;
import com.pi4j.io.gpio.RaspiPin;
import com.pi4j.io.gpio.event.GpioPinDigitalStateChangeEvent;
import com.pi4j.io.gpio.event.GpioPinListenerDigital;
import com.pi4j.io.gpio.GpioPinDigitalInput;
import com.pi4j.io.gpio.PinPullResistance;

import java.net.ServerSocket;
import java.net.Socket;

import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;

class controller {
	private static int[] sc1 = {0,0,0,0,1,1,1,1,1};  //F0h
	private static int port = 8928;
	public static GpioController gpio=GpioFactory.getInstance();
	public static GpioPinDigitalOutput pinClock = gpio.provisionDigitalOutputPin(RaspiPin.GPIO_07,"Com1", PinState.HIGH);
	public static GpioPinDigitalOutput pinData = gpio.provisionDigitalOutputPin(RaspiPin.GPIO_16,"Com2", PinState.HIGH);
	public static void main(String[] args) throws IOException {
		//Инициализация сокет сервера и клиента
		ServerSocket server=null;
		try {
			server = new ServerSocket(port);
		}  catch (IOException e) {
			System.err.println("Could not listen on port:"+port);
			System.exit(1);
		}
		Socket client = null;
		System.out.println("Готов принимать данные!");     
		while (true) 
			{
				try {          
					client = server.accept();                                              
					BufferedReader in = new BufferedReader (new InputStreamReader (client.getInputStream()));                  
					String line;
					while ((line = in.readLine())!=null) 
						{
							if (line.indexOf("exit")>-1) {
								return;
							}
							else 
							{
								//Записываю в массив полученные данные
								int[] buf=new int[9];
								for (int i=0; i<9;i++) 
								{
									buf[i]=Integer.parseInt(line.substring(i,i+1));
								}
								System.out.println("Получил пакет:  "+line);//Посылаю под клавиши
								signal(buf);
								//Посылаю код отжатия клавиши F0h
								signal(sc1);
								//Посылаю код клавиши
								signal(buf);
								PrintWriter  out   = new PrintWriter (client.getOutputStream(), true);
								out.print("finishedrn");
								out.flush();
							}
						}
					}
			}
			catch (IOException e) 
			{
				client.close();
				System.out.println("Shutdown gpio controller");
			}
		}          
	}

	//Функция устанавливает состояние на указанном пине
	static private void setPin(GpioPinDigitalOutput pinObj,int signalByte) {
		if (signalByte==0) {
			pinObj.low();
		} else {
			pinObj.high();
		}
	}

	//Функция посылаем биты с массива
	static private void signal(int[] bits) {
		int sleepInterval=1;
		//Посылаю стоп-бит
		setPin(pinData,0);
		setPin(pinClock,0);  
		sleeper(sleepInterval);
		setPin(pinClock,1);
		sleeper(sleepInterval);           
		// Посылаю массив данных
		for (int i=0; i<9; i++) {
			setPin(pinData,bits[i]);
			setPin(pinClock,0);
			sleeper(sleepInterval);
			setPin(pinClock,1);
			sleeper(sleepInterval);
		}
		//Посылаю стоп-бит
		setPin(pinData,1);
		setPin(pinClock,0);
		sleeper(sleepInterval*2);
		setPin(pinClock,1);
		sleeperM(1);
	}
	
	//Функция устанавливает задержку в макросекундах
	static private void sleeper(int i) {
		try {
			Thread.sleep(0,i);
		} catch(InterruptedException e) {
			System.out.println("Sleepin errore");
		}
	}
	
	//Функция устанавливает задержку в милли секундах.
	static private void sleeperM(int i) {
	try {
			Thread.sleep(i);
		} catch(InterruptedException e) {
			System.out.println("Sleepin errore");
		}
	}
}

Клиентская часть на Java:

import java.net.ServerSocket;
import java.net.Socket;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.Scanner;

class tcpClient {
	private static int port = 8928;
	public static void main (String[] args) throws IOException {
		String host="localhost";
		Socket server;
		PrintWriter out=null;
		Scanner sc= new Scanner (System.in); 
		System.out.println("Input host adress:");
		host=sc.nextLine();
		System.out.println("Connecting to host "+host+"...");
		try {
			server = new Socket(host,port);
			out = new PrintWriter (server.getOutputStream(), true);
		} catch (IOException e) {
			System.err.println(e);
			System.exit(1);
		}
		System.out.println("Connected!");      
		BufferedReader stdIn = new BufferedReader (new InputStreamReader(System.in));
		String msg;
		while ((msg = stdIn.readLine()) != null) {
			out.println(msg);
		}
	}
}

Пример оцифрованного сигнала, отправленного с моего аппарата на компьютер. Нижний канал CLOCK, верхний DATA. Сигналы посылаю с Python'а

image

Заключение

Конечно, использовать Raspberry для такой мелкой задачи чистой воды расточительство. Можно было использовать Arduino или собрать схему на дешевеньком arm процессоре. Но это была просто импровизация, которая первой пришла на ум, да и ждать, пока прибудут все необходимые запчасти из Китая, не особо хотелось.

В общем, надеюсь, кому нибудь пригодится данный опыт. Лично я получил массу удовольствия от всего этого процесса.

Автор: GebekovAS

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js