Картинка для привлечения внимания, сходство с реальной жизнью отдаленное
Напишу-ка я еще одну статью. Про один свой проект из уже упоминавшейся ранее папки «Projects/4Fun». Начинался проект этот как 4Fun, а закончился как 4Use. То есть используется периодически и по сей день. А дело было так…
Проблема первая
Все мы любим смотреть телик. Ну, почти все любим. Я — не исключение. Но чтобы смотреть телик, нужно его иметь. А вот с этим были у меня определенные проблемы. Его (телика) у меня не было. И не было его потому, что обычно телики прилагались к съемным квартирам, в которых я жил. Но тут попалась одна квартира без ТВ. И эту проблему нужно было как-то решать.
Решение первое
Будучи по жизни жмотом достаточно экономным человеком, решил я купить не телик, а ТВ-тюнер — это, наверное, первая мысль, которая должна прийти в голову компьютерщику в подобной ситуации. Подумано — сделано. Один мой коллега как раз хотел продать ТВ-тюнер. Вот такой вот, примерно:
В общем, купил я его. Да только была…
Проблема вторая
Оказалось, что пульт ДУ был безвозвратно утерян и восстановлению не подлежал. А так хотелось бы лежа на диванчике переключать каналы да регулировать звук (обычные и всем понятные радости). Тут подключился отдел
Решение второе
Для просмотра ТВ я использую родную тюнеровскую прилагу — 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