С пультом по жизни или лень — двигатель прогресса
Картинка для привлечения внимания, сходство с реальной жизнью отдаленноеНапишу-ка я еще одну статью. Про один свой проект из уже упоминавшейся ранее папки «Projects/4Fun». Начинался проект этот как 4Fun, а закончился как 4Use. То есть используется периодически и по сей день. А дело было так…
Проблема перваяВсе мы любим смотреть телик. Ну, почти все любим. Я — не исключение. Но чтобы смотреть телик, нужно его иметь. А вот с этим были у меня определенные проблемы. Его (телика) у меня не было. И не было его потому, что обычно телики прилагались к съемным квартирам, в которых я жил. Но тут попалась одна квартира без ТВ. И эту проблему нужно было как-то решать.Решение первое Будучи по жизни жмотом достаточно экономным человеком, решил я купить не телик, а ТВ-тюнер — это, наверное, первая мысль, которая должна прийти в голову компьютерщику в подобной ситуации. Подумано — сделано. Один мой коллега как раз хотел продать ТВ-тюнер. Вот такой вот, примерно: В общем, купил я его. Да только была…
Проблема вторая Оказалось, что пульт ДУ был безвозвратно утерян и восстановлению не подлежал. А так хотелось бы лежа на диванчике переключать каналы да регулировать звук (обычные и всем понятные радости). Тут подключился отдел мозга, ответственный за поиск решений. Сразу напрашивалось очевидное — найти другой пульт ДУ и как-то подружить их с тюнером. Но это не подходило, т.к. вместе с родным пультом потерялся и приемник сигнала (вроде бы, дело было давно). Ну и как-то это не по-программерски что-ли — «у нас своей путь» ©. Поэтому напрашивалось…Решение второе Для просмотра ТВ я использую родную тюнеровскую прилагу — BeholdTV. Переключать каналы в ней можно клавишами «вверх» и «вниз», регулировать звук «вправо»/«влево» и т.д. Поэтому придумалось следующее: написать сервер на комп, который будет эмулировать нажатия на клавиши, а клиент на мобиле будет посылать коды нужных клавиш на сервер, и все будет хорошо. Так в итоге и получилось (хорошо).Сервер писался под винду, на С++ и WinAPI. Все просто: запускаем поток для бродкаста по UDP сообщений вида «я сервер для управления теликом» и ждем подключения клиентов. Так любой клиент сможет узнать о местонахождении сервера, и никакого хардкода IP не понадобится. И так делать правильно (я считаю).Подключается клиент, сервер начинает слушать поступающие команды. Как только что-нибудь услышал — эмулирует нажатие на клавишу. Все просто и уместилось в одном файле:
Код сервера // Roco.cpp: Defines the entry point for the console application. //
#include «stdafx.h»
#include
#pragma comment (lib, «Ws2_32.lib»)
void broadcastThreadFunction (void *context) { const SOCKET *broadcastSocket = (SOCKET*)context;
sockaddr_in broadcastSocketServiceInfo; ZeroMemory (&broadcastSocketServiceInfo, sizeof (broadcastSocketServiceInfo)); broadcastSocketServiceInfo.sin_family = AF_INET; broadcastSocketServiceInfo.sin_addr.s_addr = htonl (INADDR_BROADCAST); broadcastSocketServiceInfo.sin_port = htons (28777);
static const char broadcastMessage[] = «ROCO-BROADCAST-MESSAGE»;
do { const int result = sendto (*broadcastSocket, broadcastMessage, sizeof (broadcastMessage), 0, (SOCKADDR*)&broadcastSocketServiceInfo, sizeof (broadcastSocketServiceInfo)); if (result == SOCKET_ERROR && :: WSAGetLastError () == WSAENOTSOCK) { break; }
:: Sleep (300); } while (true);
_endthread (); }
int _tmain (int argc, _TCHAR* argv[]) { if (argc >= 2 && _tcscmp (argv[1], _T (»/silent»)) == 0) { :: ShowWindow (:: GetConsoleWindow (), SW_HIDE); }
WSADATA wsaData; ZeroMemory (&wsaData, sizeof (wsaData));
printf («Initializing network…»); int result = :: WSAStartup (MAKEWORD (2,2), &wsaData); if (result == NO_ERROR) { printf («Done.\n»);
printf («Creating broadcast socket…»); const SOCKET broadcastSocket = socket (AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (broadcastSocket!= INVALID_SOCKET) { printf («Done.\n»);
static const BOOL onValue = TRUE; setsockopt (broadcastSocket, SOL_SOCKET, SO_BROADCAST, (const char*)&onValue, sizeof (onValue));
printf («Starting broadcast thread…»); HANDLE broadcastThreadHandle =(HANDLE)_beginthread (broadcastThreadFunction, 0, (void*)&broadcastSocket); if (broadcastThreadHandle!= INVALID_HANDLE_VALUE) { printf («Done.\n»);
printf («Creating listen socket…»); const SOCKET listenSocket = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP); if (listenSocket!= INVALID_SOCKET) { printf («Done.\n»);
printf («Binding listen socket…»);
sockaddr_in listenSocketServiceInfo; ZeroMemory (&listenSocketServiceInfo, sizeof (listenSocketServiceInfo)); listenSocketServiceInfo.sin_family = AF_INET; listenSocketServiceInfo.sin_addr.s_addr = htonl (INADDR_ANY); listenSocketServiceInfo.sin_port = htons (28666); result = bind (listenSocket, (SOCKADDR*)&listenSocketServiceInfo, sizeof (listenSocketServiceInfo)); if (result!= SOCKET_ERROR) { printf («Done.\n»);
printf («Listening for incoming connection…»); result = listen (listenSocket, SOMAXCONN); if (result!= SOCKET_ERROR) { printf («Done.\n»);
unsigned connectionIndex = 0; do { printf («Accepting incoming connection #%d…», connectionIndex + 1); :: ResumeThread (broadcastThreadHandle); SOCKET commandSocket = accept (listenSocket, NULL, NULL); if (commandSocket!= INVALID_SOCKET) { printf («Done.\n»);
:: SuspendThread (broadcastThreadHandle);
printf («Sending PING to command socket…»); static const char ping[] = «PING»; result = send (commandSocket, ping, sizeof (ping), 0); if (result!= SOCKET_ERROR && result == sizeof (ping)) { printf («Done.\n»);
printf («Receiving PONG from command socket…»); static char pong[sizeof («PONG»)]; pong[0] = '\0'; result = recv (commandSocket, pong, sizeof (pong), 0); if (result!= SOCKET_ERROR && result == sizeof (pong) && strcmp (pong, «PONG») == 0) { printf («Done.\n»);
unsigned commandIndex = 0; do { printf («Waiting for command #%d…\n», commandIndex + 1); static char command[2]; ZeroMemory (command, sizeof (command)); result = recv (commandSocket, command, sizeof (command), 0); if (result!= SOCKET_ERROR && result == sizeof (command)) { enum { CC_KEY_DOWM = 1, CC_KEY_UP = 0 }; const char commandCode = command[0]; const char keyCode = command[1]; static const char res = 1; switch (commandCode) { case CC_KEY_DOWM: { printf («KEY_DOWN (%d)\n», keyCode); keybd_event (keyCode, 0, 0, 0); send (commandSocket, &res, sizeof (res), 0); } break;
case CC_KEY_UP: { printf («KEY_UP (%d)\n», keyCode); keybd_event (keyCode, 0, KEYEVENTF_KEYUP, 0); send (commandSocket, &res, sizeof (res), 0); } break;
default: { printf («Invalid command received — %d!\n», commandCode); } break; } } else { printf («Could not receive command from socket (error — %d)!\n», :: WSAGetLastError ()); break; } ++commandIndex; } while (true); } else { printf (»\nCould not receive PONG from command socket (error — %d)!\n», :: WSAGetLastError ()); } } else { printf (»\nCould not sent PING to command socket (error — %d)!\n», :: WSAGetLastError ()); } } else { printf (»\nCould not accept incoming connection (error — %d)!\n», :: WSAGetLastError ()); }
++connectionIndex; } while (true); } else { printf (»\nCould not listen for incoming connection (error — %d)!\n», :: WSAGetLastError ()); } } else { printf (»\nCould not bind listen socket (error — %d)!\n», :: WSAGetLastError ()); }
closesocket (listenSocket); } else { printf (»\nCould not create listen socket (error — %d)!\n», :: WSAGetLastError ()); } } else { printf (»\nCould not start broadcast thread!\n»); }
:: ResumeThread (broadcastThreadHandle); closesocket (broadcastSocket); :: WaitForSingleObject (broadcastThreadHandle, INFINITE); } else { printf (»\nCould not create broadcast socket (error — %d)!\n», :: WSAGetLastError ()); }
:: WSACleanup (); } else { printf (»\nWSAStartup failed (error — %d)!», result); }
return 0; } Запускается сервер вместе с видной. Сервер консольная утилита (удобно для просмотра логов, если что), поэтому нужна вот эта строчка сразу после запуска:
:: ShowWindow (:: GetConsoleWindow (), SW_HIDE); Мобила у меня на Андроиде, поэтому клиент писал нативный, на жаве. Получился вот такой вот супер-мега интерфейс:
Исходник клиента тоже довольно прост. Генерируем интерфейс программно, на каждую кнопку вешает посылку кода клавиши на сервер. При запуске клиента ищем местонахождение сервера, подключаемся. Выглядит все это вот так:
Код клиента package com.dummy.roco;
import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetSocketAddress; import java.net.Socket; import java.util.Timer; import java.util.TimerTask;
import android.net.wifi.WifiManager; import android.net.wifi.WifiManager.MulticastLock; import android.os.Bundle; import android.os.StrictMode; import android.os.Vibrator; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup.LayoutParams; import android.widget.Button; import android.widget.LinearLayout;
public class RemoteControlActivity extends Activity { protected static class ButtonInfo { public final String text_; public final int code_;
public ButtonInfo (final String text, final int code) { text_ = text; if (code!= 0) { code_ = code; } else { code_ = text.codePointAt (0); } } }
protected static class CommandButton extends Button { protected ButtonInfo buttonInfo_; protected Socket commandSocket_; protected Vibrator vibrator_; protected Timer commandTimer_;
protected final int COMMAND_DELAY = 200;
public CommandButton (final Context context, final ButtonInfo buttonInfo, final Socket commandSocket, final Vibrator vibrator) { super (context);
buttonInfo_ = buttonInfo; commandSocket_ = commandSocket; vibrator_ = vibrator;
setText (buttonInfo_.text_); setTextSize (getTextSize ());
setOnTouchListener (new OnTouchListener () { @Override public boolean onTouch (View v, MotionEvent event) { switch (event.getAction ()) { case MotionEvent.ACTION_DOWN: startCommandTimer (); break; case MotionEvent.ACTION_UP: stopCommandTimer (); break; } return false; } }); }
protected void sendCommand (final int commandCode, final int buttonCode) { final byte command[] = { (byte) commandCode, (byte) buttonCode }; try { commandSocket_.getOutputStream ().write (command); } catch (Exception exception) { exception.printStackTrace (); } }
public void startCommandTimer () { vibrator_.vibrate (10);
sendCommand (CC_KEY_DOWM, buttonInfo_.code_);
commandTimer_ = new Timer (); commandTimer_.schedule (new TimerTask () { @Override public void run () { sendCommand (CC_KEY_DOWM, buttonInfo_.code_); } }, COMMAND_DELAY, COMMAND_DELAY); }
public void stopCommandTimer () { commandTimer_.cancel (); commandTimer_.purge (); commandTimer_ = null;
sendCommand (CC_KEY_UP, buttonInfo_.code_);
vibrator_.vibrate (10); } }
protected static final ButtonInfo buttonInfos_[][] = { { new ButtonInfo (»1», 0), new ButtonInfo (»2», 0), new ButtonInfo (»3», 0) }, { new ButtonInfo (»4», 0), new ButtonInfo (»5», 0), new ButtonInfo (»6», 0) }, { new ButtonInfo (»7», 0), new ButtonInfo (»8», 0), new ButtonInfo (»9», 0) }, { new ButtonInfo (»¾», 8), new ButtonInfo (»↑», 38), new ButtonInfo (»¤», 77) }, { new ButtonInfo (»←», 37), new ButtonInfo (»®», 13), new ButtonInfo (»→», 39) }, { new ButtonInfo (»§», 32), new ButtonInfo (»↓», 40), new ButtonInfo (»«, 27) } };
protected static final int CC_KEY_DOWM = 1; protected static final int CC_KEY_UP = 0;
protected final Socket commandSocket_ = new Socket (); protected Vibrator vibrator_;
@Override protected void onCreate (Bundle savedInstanceState) { super.onCreate (savedInstanceState);
vibrator_ = (Vibrator) getSystemService (VIBRATOR_SERVICE);
final LinearLayout mainLayout = new LinearLayout (this); mainLayout.setOrientation (LinearLayout.VERTICAL); for (int i = 0; i < buttonInfos_.length; ++i) { final LinearLayout rowLayout = new LinearLayout(this); rowLayout.setOrientation(LinearLayout.HORIZONTAL); for (int j = 0; j < buttonInfos_[i].length; ++j) { final CommandButton button = new CommandButton(this, buttonInfos_[i][j], commandSocket_, vibrator_); final LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); layoutParams.weight = 1.0f; rowLayout.addView(button, layoutParams); } final LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); layoutParams.weight = 1.0f; mainLayout.addView(rowLayout, layoutParams); }
setContentView (mainLayout, new LayoutParams (LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
MulticastLock multicastLock = null; DatagramSocket broadcastSocket = null; try { StrictMode.setThreadPolicy (new StrictMode.ThreadPolicy.Builder () .permitAll ().build ());
final WifiManager wifiManager = (WifiManager) getSystemService (Context.WIFI_SERVICE); if (wifiManager!= null) { multicastLock = wifiManager .createMulticastLock («ROCO-MulticastLock»); }
if (multicastLock!= null) { multicastLock.acquire (); }
broadcastSocket = new DatagramSocket (28777); broadcastSocket.setBroadcast (true); broadcastSocket.setSoTimeout (1000);
final byte[] datagramPacketData = new byte[«ROCO-BROADCAST-MESSAGE\0» .length ()]; final DatagramPacket datagramPacket = new DatagramPacket ( datagramPacketData, datagramPacketData.length); broadcastSocket.receive (datagramPacket); if (new String (datagramPacketData) .compareTo («ROCO-BROADCAST-MESSAGE\0») != 0) { throw new Exception («Could not get ROCO server address!»); }
commandSocket_.setSoTimeout (500); commandSocket_.connect (new InetSocketAddress (datagramPacket .getAddress ().getHostAddress (), 28666), commandSocket_ .getSoTimeout ());
final byte ping[] = new byte[«PING\0».length ()]; commandSocket_.getInputStream ().read (ping); if (new String (ping).compareTo («PING\0») != 0) { throw new Exception ( «Could not receive PING from command socket!»); }
commandSocket_.getOutputStream ().write ( new String («PONG\0»).getBytes ()); } catch (Exception exception) { final AlertDialog alertDialog = new AlertDialog.Builder (this) .create (); alertDialog.setCancelable (false); alertDialog.setTitle («Roco: Error»); alertDialog .setMessage («Could not connect to the server!\nError — '» + exception.toString () + »'\n\nExiting…»); alertDialog.setButton (AlertDialog.BUTTON_POSITIVE, «OK», new DialogInterface.OnClickListener () { @Override public void onClick (DialogInterface dialog, int which) { dialog.dismiss (); finish (); } }); alertDialog.show (); } finally { if (broadcastSocket!= null) { broadcastSocket.close (); }
if (multicastLock!= null && multicastLock.isHeld ()) { multicastLock.release (); } } }
@Override protected void onDestroy () { try { commandSocket_.close (); } catch (Exception exception) { exception.printStackTrace (); }
super.onDestroy (); }
@Override public boolean onKeyDown (int keyCode, KeyEvent event) { if ((keyCode == KeyEvent.KEYCODE_BACK)) { finish (); }
return super.onKeyDown (keyCode, event); } } Проект писался достаточно давно, а профессионализм не стоит на месте. Сейчас, возможно, я написал бы все не так топорно (возможно даже, с использованием официального API, о котором я узнал уже после написания проекта). Тем не менее, все работает стабильно и периодически используется.
Есть интересный побочный эффект — если смотреть не телик, а, скажем, ютуб, то плеер можно поставить на паузу. И отпаузить тоже.
В общем, получилось прикольно, полезно, дешево и сердито.
Проект называет «Roco». Кто угадает, почему именно так — пишите в комментариях. Угадавшему слава и уважение.
P.S. Кстати, телик в последнее время я очень редко смотрю. В основном скаченные фильмы или онлайн. Хорошо, что я его не купил тогда. Но сейчас подумываю о покупке. Парадокс какой-то получается…