Pull to refresh

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

Reading time10 min
Views14K
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. Кстати, телик в последнее время я очень редко смотрю. В основном скаченные фильмы или онлайн. Хорошо, что я его не купил тогда. Но сейчас подумываю о покупке. Парадокс какой-то получается…
Tags:
Hubs:
+10
Comments12

Articles