С пультом по жизни или лень — двигатель прогресса

    image
    Картинка для привлечения внимания, сходство с реальной жизнью отдаленное

    Напишу-ка я еще одну статью. Про один свой проект из уже упоминавшейся ранее папки «Projects/4Fun». Начинался проект этот как 4Fun, а закончился как 4Use. То есть используется периодически и по сей день. А дело было так…



    Проблема первая


    Все мы любим смотреть телик. Ну, почти все любим. Я — не исключение. Но чтобы смотреть телик, нужно его иметь. А вот с этим были у меня определенные проблемы. Его (телика) у меня не было. И не было его потому, что обычно телики прилагались к съемным квартирам, в которых я жил. Но тут попалась одна квартира без ТВ. И эту проблему нужно было как-то решать.

    Решение первое


    Будучи по жизни жмотом достаточно экономным человеком, решил я купить не телик, а ТВ-тюнер — это, наверное, первая мысль, которая должна прийти в голову компьютерщику в подобной ситуации. Подумано — сделано. Один мой коллега как раз хотел продать ТВ-тюнер. Вот такой вот, примерно:
    image

    В общем, купил я его. Да только была…

    Проблема вторая


    Оказалось, что пульт ДУ был безвозвратно утерян и восстановлению не подлежал. А так хотелось бы лежа на диванчике переключать каналы да регулировать звук (обычные и всем понятные радости). Тут подключился отдел мозга, ответственный за поиск решений. Сразу напрашивалось очевидное — найти другой пульт ДУ и как-то подружить их с тюнером. Но это не подходило, т.к. вместе с родным пультом потерялся и приемник сигнала (вроде бы, дело было давно). Ну и как-то это не по-программерски что-ли — «у нас своей путь» (с). Поэтому напрашивалось…

    Решение второе


    Для просмотра ТВ я использую родную тюнеровскую прилагу — BeholdTV. Переключать каналы в ней можно клавишами «вверх» и «вниз», регулировать звук «вправо»/«влево» и т.д. Поэтому придумалось следующее: написать сервер на комп, который будет эмулировать нажатия на клавиши, а клиент на мобиле будет посылать коды нужных клавиш на сервер, и все будет хорошо. Так в итоге и получилось (хорошо).

    Сервер писался под винду, на С++ и WinAPI. Все просто: запускаем поток для бродкаста по UDP сообщений вида «я сервер для управления теликом» и ждем подключения клиентов. Так любой клиент сможет узнать о местонахождении сервера, и никакого хардкода IP не понадобится. И так делать правильно (я считаю).
    Подключается клиент, сервер начинает слушать поступающие команды. Как только что-нибудь услышал — эмулирует нажатие на клавишу. Все просто и уместилось в одном файле:
    Код сервера
    // Roco.cpp : Defines the entry point for the console application.
    //
    
    #include "stdafx.h"
    #include <winsock2.h>
    
    #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. Кстати, телик в последнее время я очень редко смотрю. В основном скаченные фильмы или онлайн. Хорошо, что я его не купил тогда. Но сейчас подумываю о покупке. Парадокс какой-то получается…
    Share post

    Comments 12

      +2
      А я яблочный пульт прикрутил к xbmc(iptv), очень удобно.
        +2
        Коллега :)
        +1
        Проект называет «Roco». Кто угадает, почему именно так — пишите в комментариях.
        RemOte COntrol? :)

        А что делают кнопки вокруг стрелок?
          0
          Почти правильно. На самом деле я хотел назвать все ReCo, но слажал и сделал опечатку — получилось Roco. В общем слава и уважением вам!

          3/4 — каналы туда/сюда
          круг с черточками — отключение звука
          значок параграфа — пробел
          R — enter
          << — esc
          0
          Круто! То о чём мечтал и всё хотел написать сам, но руки как-то не доходили, да в С/С++ не особо силён. Спасибо большое!
          Было бы ещё круто, если бы автор выложил скомпилированные версии «сервера» и «клиента», а то, например у меня, возникли проблемы со сборкой серверной части.
            0
            А что за проблемы в сборке? Покажите — разберемся.

            P.S. Сервер компилится только под винду.
              0
              Мне под Винду и надо.
              Для попытки скомпилить я скачал DevCpp, оно сразу же ругнулось на отсутсвующий stdafx.h
                0
                Компилялось и разрабатывалось все это в MS Visual Studio. Не думаю, что исходник скомпилится в DevCpp — нужно будет постараться и подложить нужные либы для линковки, если все они вообще в нем есть.

                #pragma comment(lib, «Ws2_32.lib») — это вообще фича студии, на сколько я знаю, в случае DevCpp нужно будет указывать либы вручную где-то в проекте или в мейкфайле.

                Можно скачать бесплатную версию студии и там скомпилить.
                  0
                  Я хотел обойтись маленькими жертвами, но видимо, так и придётся качать этого монстра ради одного маленького проекта…
                    0
                    Что ж, с сервером разобрался: как минимум пришлось добавить #include <process.h>, чтобы ушли ошибки на _endthread() и _beginthread

                    Я, возможно, придираюсь, но для полного комплекта, можно было бы добавить код AndroidManifest.xml, хотя бы список permissions:
                    <uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.VIBRATE" />
                      0
                      Для полного комплекта да, нужно было бы все это дело выложить на гитхаб, да с коментами. Но статья для таких людей как вы, которые сами во всем разберуться, если захотят. А вы все правильно делаете. Так держать!
              0
              del

              Only users with full accounts can post comments. Log in, please.