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

в 17:55, , рубрики: Без рубрики

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] = '';
										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"
					.length()];
			final DatagramPacket datagramPacket = new DatagramPacket(
					datagramPacketData, datagramPacketData.length);
			broadcastSocket.receive(datagramPacket);
			if (new String(datagramPacketData)
					.compareTo("ROCO-BROADCAST-MESSAGE") != 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".length()];
			commandSocket_.getInputStream().read(ping);
			if (new String(ping).compareTo("PING") != 0) {
				throw new Exception(
						"Could not receive PING from command socket!");
			}

			commandSocket_.getOutputStream().write(
					new String("PONG").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() + "'nnExiting...");
			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. Кстати, телик в последнее время я очень редко смотрю. В основном скаченные фильмы или онлайн. Хорошо, что я его не купил тогда. Но сейчас подумываю о покупке. Парадокс какой-то получается…

Автор: goghAta

Источник

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


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