четверг, 13 октября 2011 г.

Денди-задрот с проводным пультом

ПРЕДУПРЕЖДЕНИЕ: Весь код, встреченный внизу является говнокодом чуть более, чем полностью, я такое в восьмом классе писал в Borland C++ Builder. Надо будет шесть раз с нуля переписать.
Сижу я дома, никого не трогаю, передо мной лежит свеженькая arduino. Что делать? Надо к ней что-то присобачить!
Немного поразмыслив, поняв, что у меня нет ИК-фотодиода под рукой, вспомнил, что у меня есть лишние геймпады от денди.
Хорошо. Посматриваем мануальчики к ардуино, и, параллельно ищем спеки к геймпадам. Быстро узнаём, что это дело питается от +5В, и уровни у него - 0В и +5В. Это очень хорошо, ибо у ардуинки - так же, и не надо ничего ставить дополнительно.
Довольно быстро удаётся найти следующий ман:http://bit16.ru/index.php?gl=jpad&act=1, откуда мы берём распиновочку.
Распиновочка есть, а вот спеки найти было сложнее. Но, через полчаса где-то , находим следующий ман: http://www.parallax.com/Portals/0/Downloads/docs/prod/acc/32368-NesGamepadCtrlrAdapter-v1.0.pdf
Этот ман описывает контроллер для подключения двух геймпадов NES. Нам это, конечно, нафиг не надо, но там есть этот прекрасный параграф:
Возможно, это будет читать кто-то, кто не знает инглиш, так что расскажу, как это работает. Заодно, добавлю больше букв в бложек. Когда геймпад принимает импульс на LATCH, он "запоминает" комбинацию нажатых клавиш, и шлёт нам бит, показывающий, нажата ли кнопка "A", или нет. Затем, мы шлём семь импульсов на CLK и, соответсвенно получаем ещё семь бит, для семи оставшихся кнопок. В общем-то, всё крайне понятно при первом взгляде на график.
Всё подцепил. Написал простейшую программку. Не работает. Полчаса мучений. Потом понимаю, что подрубил нерабочий геймпад. Подрубаю нужный - всё ок!

Моя программка для arduino
#define CLKDELAY 1
#define FREQ 30
int data = 13;
int clock = 12;
int latch = 11;
//выбираем пины, куда втыкать наши проводки
int code;
int result;
boolean a;
boolean b;
boolean ta;
boolean tb;
boolean select;
boolean start;
boolean up;
boolean down;
boolean left;
boolean right;

void setup() {
//выполняется один раз.
pinMode(data, INPUT);//включаем пин данных на вход
pinMode(clock, OUTPUT);//на эти пины мы шлём импульсы, 
pinMode(latch, OUTPUT);//так что это - выходы
digitalWrite(latch, LOW);//для начала на них
digitalWrite(clock, LOW);//пустим низкий уровень
Serial.begin(9600);//частота, на которой работает USB. Нам пока хватит.
}

void loop() {
//шлём импульс на LATCH, снимаем A
  digitalWrite(latch, HIGH);
  delay(CLKDELAY);
  a = digitalRead(data);
  digitalWrite(latch, LOW);
//Дальше шлём импульсы на CLK и снимаем остальные данные
//1
  delay(CLKDELAY);
  digitalWrite(clock, HIGH);
  delay(CLKDELAY);
  b = digitalRead(data);
  digitalWrite(clock, LOW);
  delay(CLKDELAY);
//2
  digitalWrite(clock, HIGH);
  delay(CLKDELAY);
  select = digitalRead(data);
  digitalWrite(clock, LOW); 
//3
  delay(CLKDELAY);
  digitalWrite(clock, HIGH);
  start = digitalRead(data);
  delay(CLKDELAY);
  digitalWrite(clock, LOW); 
//4
  delay(CLKDELAY);
  digitalWrite(clock, HIGH);
  delay(CLKDELAY); 
  up = digitalRead(data);
  digitalWrite(clock, LOW); 
//5
  delay(CLKDELAY);
  digitalWrite(clock, HIGH);
  delay(CLKDELAY);
  down = digitalRead(data);
  digitalWrite(clock, LOW); 
//6
  delay(CLKDELAY);
  digitalWrite(clock, HIGH);
  delay(CLKDELAY);
  left = digitalRead(data);
  digitalWrite(clock, LOW); 
//7
  delay(CLKDELAY);
  digitalWrite(clock, HIGH);
  delay(CLKDELAY);
  right = digitalRead(data);
  digitalWrite(clock, LOW); 
 
  delay(FREQ); //задержка между опросами
  result = 0;
  result << 7; //на будущее
  if (!a) result++;  result=result<<1;
  if (!b) result++; result=result<<1;
  if(!select) result++; result=result<<1;
  if(!start) result++; result=result<<1;
  if(!up) result++; result=result<<1;
  if(!down) result++; result=result<<1;
  if(!left) result++; result=result<<1;
  if(!right) result++;
  Serial.println(result, DEC);//ебашим результат в универсальный последовательный автобус
//В десятичной системе - потому, что нам - похуй. Потом, когда посмотрете код на bash, поймёте.
}
Пара слов о программке. Она выводит 16битное число, где первые 7 бит - нули (потом собираюсь к ардуинке ещё много чего присобачить, так что с запасом). Затем идут последовательно биты для каждого пина. Так как выход с геймпада - инверсный, добавляем инверсию в if. Ну а так, всё понятно любому школьнику.

Итак, на тот момент я мог сделать cat /dev/ttyACM0 и увидеть коды
А теперь начинается самое ужасное! Один день я истратил на гугление, как же получить данные из последовательного устройства. На следующий день, решил спросить на.. лоре. И правильно сделал! Мне подсказали реально рабочий способ. Выглядит он вот так вот:
while read value
do
#Чозахочешь
done < device
Ну либо пайпом.
Но... не рабоатет. Тот же няшный чувак с лора, предположил, что мой говнодевайс шлёт данные не в формате вроде 128\n, а в формате 128\r\n. Смотрим в hexdump - он прав!
Далее идут попытки заткнуть весь поток в dos2unix, в hd| xxd -r |dos2unix, у кого-то работает, у меня - нет.  Пробовал я ненужные символы порезать сдвигами, но у нас ведь bash! Тут всё, не как у людей. Сдвиги тут работают только для чисел. Пробовал убрать последние символы стандартными средствами работы со строками - но и они нихрена не работают.
В конечном итоге решил попробовать отдельно каждую строку совать в dos2unix. И это - заработало. Ещё попробовал отдельно каждую строку совать в xxd -p - получаем hex вместе с символами перевода. Засунуть поток в xxd -p не вышло, ибо после второй строки подряд у меня получается ошибка xxd: cannot seek backwarkds. Так и не понял, к чему это. Если у вас - заработает, вы - молодец.
Вариант с xxd -p и последующим сравнением полученных кодов с кодами, которые мы запомнили работал субъективно быстрее (да, моё говно на bash жрёт ~2% моего ебаного проца), так что я оставил его.
Получается что-то вроде
while read number
do
hex=`echo $number | xxd -p`
case $hex in
        320d0a) #влево 
        action;;
        310d0a) #вправо
        action;;
        33320d0a) #вправо+select
        action;;
        33340d0a) #влево+select
        action;;
esac
done < /dev/ttyACM0
Затем понимаем, что часть команд должна повторяться при длительном нажатии на кнопку, например, перемотка, или задержка субтитров, а часть - нет, например play/pause.
Заводим переменную, отслеживающую, сколько опросов подряд кнопка остаётся нажатой.
Смотрим коды, сравниваем, выполняем нужные команды.
Ну, все пользователи exaile давно осилили qdbus, у mplayer'а - сложнее, но гуглится на раз (http://ubuntuforums.org/showthread.php?t=1629000). 
mkfifo /tmp/mplayer-control
mplayer -slave -input file=/tmp/mplayer-control /path/to/some/file/to/play
Затем в эту очередь кидаем команды, например,
echo "pause" > /tmp/mplayer-control
echo "quit" > /tmp/mplayer-control
Список команд есть здесь: http://www.mplayerhq.hu/DOCS/tech/slave.txt

Также, неплохо бы управлять громкостью. Тут нам поможет вот этот ОТЛИЧНЫЙ СКРИПТ: http://pastebin.com/F1tM6R2J.

Небольшое лирическое отступление. Думаю, всем понятно, что если ничего не нажато, то делать всякие там сравнения нафиг не надо. Поэтому в начало цикла чтения надо добавить вот такое:
if [ $hex = 300d0a ]
    then prevkey=300d0a
    continue
fi
Также я отметил, что при многократном включении/выключении arduino, /dev/ttyACM0 может остаться, а ардуинка получит себе /dev/ttyACM1. Так что я решил подстраховаться и добваить следующее:
b=0
for ((a=5; a >=0  ; a--))
do
    if [ -e /dev/ttyACM$a ]
        then
        b=$a
        break
    fi
done
И, соответственно, везде обращаемся к девайсу, как /dev/ttyACM$b
Ещё, у меня есть функция, которая смотрит, запущен ли mplayer, чтобы понять, управлять им, или exaile. Приводить отдельно не буду, потом увидите.

Если ардуинка пропала - ждём секунду, и снова проверяем, есть ли она. Ну, а так, вроде всё.

Конечный скрипт - держите.
#!/bin/bash
mkfifo /tmp/mplayer-control

whatruns() #Производит опрос, какие из нужных нам программ, запущены. 
#Делаю это, ибо хочу управлять разными программами, с помощью одинаковых комбинаций клавиш
#Пока тут только mplayer - exaile у меня запущен всегда.
{
        if ps ax |grep mplayer |grep -q slave ;
                then mplayer=1 
                else mplayer=0
        fi
}
while true
do

b=0
for ((a=5; a >=0  ; a--))
do
        if [ -e /dev/ttyACM$a ]
                then 
                b=$a
                break
        fi
done

while [ -e /dev/ttyACM$b ]
#работаем, пока есть геймпад
do
whatruns #перед работой проверили, что запущено
readcount=0 #Отсчитывает количество опросов геймпада
readspressed=0 #Отсчитывает, сколько опросв подряд комбинация остаётся нажатой
prevkey=0 #Код предыдущей комбинации
while read number
do
let "readcount+=1"
if [ $readcount -ge 50 ] #Каждые 20 опросов
        then
        whatruns #проверяем, что запущено
        readcount=0
fi
hex=`echo $number | xxd -p`
if [ $hex = 300d0a ]
        then prevkey=300d0a
        continue
fi
if [[ $hex = $prevkey ]]
        then let "readspressed+=1"
else
        readspressed=0
fi
prevkey=$hex

#Для тех команд, которые работают один раз при длительном нажатии
if [ "$readspressed" -eq 0 ] 
then
case $hex in
        31360d0a) #start
        if [ $mplayer -eq 1 ]
                then echo "pause" > /tmp/mplayer-control
                else qdbus org.exaile.Exaile /org/exaile/Exaile org.exaile.Exaile.PlayPause
        fi ;;

        320d0a) #влево
        if [ $mplayer -eq 0 ]
                then qdbus org.exaile.Exaile /org/exaile/Exaile org.exaile.Exaile.Prev
        fi ;;

        310d0a) #вправо
        if [ $mplayer -eq 0 ]
                then qdbus org.exaile.Exaile /org/exaile/Exaile org.exaile.Exaile.Next
        fi ;;
esac
fi

#для команд, которые будт срабатывать каждые десять опросов
if (( $readspressed != 0 ||  `expr $readspressed % 10` == 9  ))
then
case $hex in
        320d0a) #влево
        if [ $mplayer -eq 1 ]
                then echo "seek -5" > /tmp/mplayer-control
        fi ;;

        310d0a) #вправо
        if [ $mplayer -eq 1 ]
                then echo "seek +5" > /tmp/mplayer-control
        fi ;;

        33320d0a) #вправо+select
        if [ $mplayer -eq 1]
                then echo "sub_delay +0.1" > /tmp/mplayer-control
        else 
                r=`qdbus org.exaile.Exaile /Player org.freedesktop.MediaPlayer.PositionGet`
                let "r+=5000"
                qdbus org.exaile.Exaile /Player org.freedesktop.MediaPlayer.PositionSet $r
        fi ;;

        33340d0a) #влево+select
        if [ $mplayer -eq 1 ]
                then echo "sub_delay -0.1" > /tmp/mplayer-control
        else 
                r=`qdbus org.exaile.Exaile /Player org.freedesktop.MediaPlayer.PositionGet`
                let "r-=5000"
                qdbus org.exaile.Exaile /Player org.freedesktop.MediaPlayer.PositionSet $r
        fi ;;

        380d0a) #вверх 
        volume up;;
        340d0a) #вниз
        volume down     ;;

esac
fi

done < /dev/ttyACM$b

done
sleep 1
done
P.S. Прошу прощения за такую ебаную тему подсветки кода - включил дефолт на скорую руку. Как-нибудь исправлю.