commit 779a1e53cebdccf3f95e970906dde0e96b71a6d5 Author: R. Eric Wheeler Date: Sat Nov 4 11:53:58 2017 -0700 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..573656a --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.php_cs.cache +vendor/ +.idea/ +composer.lock + diff --git a/.php_cs b/.php_cs new file mode 100644 index 0000000..0fa2ece --- /dev/null +++ b/.php_cs @@ -0,0 +1,50 @@ +setRiskyAllowed(true) + ->setRules( + [ + '@PSR2' => true, + '@PHP70Migration' => true, + '@PHP70Migration:risky' => true, + '@PHP71Migration' => true, + '@PHP71Migration:risky' => true, + 'header_comment' => ['header' => $header], + 'ordered_class_elements' => true, + 'ordered_imports' => true, + 'no_mixed_echo_print' => ['use' => 'print'], + 'strict_param' => true, + 'strict_comparison' => true, + 'single_import_per_statement' => false, + 'phpdoc_order' => true, + 'array_syntax' => ['syntax' => 'short'], + 'phpdoc_add_missing_param_annotation' => true, + 'psr4' => true, + 'phpdoc_var_without_name' => false, + 'no_extra_consecutive_blank_lines' => [ + 'break', + 'continue', + 'extra', + 'return', + 'throw', + 'parenthesis_brace_block', + 'square_brace_block', + 'curly_brace_block', + ], + ] + )->setFinder( + PhpCsFixer\Finder::create() + ->ignoreDotFiles(true) + ->ignoreVCS(true) + ->name('*.php') + ->in([ + 'src', + 'tests', + ]) + ); \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..c9d3d96 --- /dev/null +++ b/composer.json @@ -0,0 +1,30 @@ +{ + "name": "sikofitt/samsung-tv", + "description": "Very simple library to change channels and inputs on old samsung tvs", + "type": "library", + "require": { + "guzzlehttp/guzzle": "^6.3", + "react/socket-client": "^0.7.0", + "guzzlehttp/streams": "^3.0", + "symfony/console": "^3.3||^4.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.7", + "squizlabs/php_codesniffer": "^3.1", + "phpunit/phpunit": "^6.4", + "symfony/var-dumper": "^3.3" + }, + "autoload": { + "psr-4": { + "Sikofitt\\SamsungTV\\":"src/Sikofitt/SamsungTV" + } + }, + "license": "MPL-2.0", + "authors": [ + { + "name": "R. Eric Wheeler", + "email": "sikofitt@gmail.com" + } + ], + "minimum-stability": "stable" +} diff --git a/console.php b/console.php new file mode 100755 index 0000000..d6b90e2 --- /dev/null +++ b/console.php @@ -0,0 +1,11 @@ +#!/usr/bin/env php + +add(new \Sikofitt\SamsungTV\Console\Command\ChannelCommand()); +$app->add(new \Sikofitt\SamsungTV\Console\Command\InputCommand()); +$app->run(); \ No newline at end of file diff --git a/header.txt b/header.txt new file mode 100644 index 0000000..e0e74a0 --- /dev/null +++ b/header.txt @@ -0,0 +1,14 @@ + Copyleft (C) 2017 http://sikofitt.com sikofitt@gmail.com + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . \ No newline at end of file diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 0000000..9770e8d --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,47 @@ + + + The coding standard for PHP_CodeSniffer itself. + + vendor/autoload.php + src + tests + + */Standards/*/Tests/*\.(inc|css|js) + + + + + + + + + error + + + + error + + + + + error + + + + + + + + + 0 + + + + + + + + + + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..2ec6f6e --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,27 @@ + + + + + + + + + + + + tests + + + + + + src + + + diff --git a/src/Sikofitt/SamsungTV/Actions/Actions.php b/src/Sikofitt/SamsungTV/Actions/Actions.php new file mode 100644 index 0000000..fe09089 --- /dev/null +++ b/src/Sikofitt/SamsungTV/Actions/Actions.php @@ -0,0 +1,179 @@ +. + */ + +namespace Sikofitt\SamsungTV\Actions; + +use React\EventLoop\StreamSelectLoop; +use React\Stream\DuplexResourceStream; +use React\Stream\ReadableResourceStream; +use React\Stream\ThroughStream; +use React\Stream\WritableResourceStream; +use Sikofitt\SamsungTV\Packet\PacketFactory; + +class Actions +{ + public const KEY_ENTER = 'KEY_ENTER'; + public const KEY_0 = 'KEY_0'; + public const KEY_1 = 'KEY_1'; + public const KEY_2 = 'KEY_2'; + public const KEY_3 = 'KEY_3'; + public const KEY_4 = 'KEY_4'; + public const KEY_5 = 'KEY_5'; + public const KEY_6 = 'KEY_6'; + public const KEY_7 = 'KEY_7'; + public const KEY_8 = 'KEY_8'; + public const KEY_9 = 'KEY_9'; + public const KEY_PLUS100 = 'KEY_PLUS100'; // - key + + public const KEY_HDMI = 'KEY_HDMI'; // Apparently not + public const KEY_HDMI1 = 'KEY_HDMI1'; // the same + public const KEY_HDMI2 = 'KEY_HDMI2'; + public const KEY_HDMI3 = 'KEY_HDMI3'; + public const KEY_HDMI4 = 'KEY_HDMI4'; + + public const KEY_AV1 = 'KEY_AV1'; + public const KEY_AV2 = 'KEY_AV2'; + public const KEY_AV3 = 'KEY_AV3'; + + public const KEY_TV = 'KEY_TV'; + + public static $keyMap = [ + '9' => self::KEY_9, + '8' => self::KEY_8, + '7' => self::KEY_7, + '6' => self::KEY_6, + '5' => self::KEY_5, + '4' => self::KEY_4, + '3' => self::KEY_3, + '2' => self::KEY_2, + '1' => self::KEY_1, + '0' => self::KEY_0, + '-' => self::KEY_PLUS100, + '.' => self::KEY_PLUS100, + 'E' => self::KEY_ENTER, + 'H' => self::KEY_HDMI, + 'H1' => self::KEY_HDMI1, + 'H2' => self::KEY_HDMI2, + 'H3' => self::KEY_HDMI3, + 'H4' => self::KEY_HDMI4, + 'T' => self::KEY_TV, + 'AV1' => self::KEY_AV1, + 'AV2' => self::KEY_AV2, + 'AV3' => self::KEY_AV3, + ]; + + private $packetFactory; + private $address; + + public function __construct(string $tvAddress, int $port = 55000) + { + $this->packetFactory = new PacketFactory(); + $this->address = sprintf('%s:%d', $tvAddress, $port); + } + + public function transformChannels(string $channel): \Generator + { + $codes = str_split($channel); + + foreach ($codes as $code) { + yield $this->transformChannel($code); + } + } + + public function transformChannel(string $channel): string + { + if (array_key_exists(strtoupper($channel), self::$keyMap)) { + return base64_encode(self::$keyMap[strtoupper($channel)]); + } + + return ''; + } + + public function sendKey(string $keyCode): void + { + $loop = new StreamSelectLoop(); + + $loop->nextTick(function (): void { + usleep(140000); + }); + + $connection = stream_socket_client($this->address); + + $stream = new WritableResourceStream($connection, $loop); + + + $stream->on('read', function($stream) { + dump($stream); + }); + + $stream->write($this->packetFactory->getStartPacket()); + + $key = $this->transformChannel($keyCode); + + $stream->write($this->packetFactory->getKeyPacket($key)); + $loop->run(); + $stream->close(); + } + + public function changeChannel(string $channel): void + { + // Add enter at the end of the channel string. + $channelArr = str_split($channel.'E'); + foreach ($channelArr as $item) { + $this->sendKey($item); + } + } + + /** + * @param string $input the source code in self::$keyMap + */ + public function changeInput(string $input): void + { + switch (strtoupper($input)) { + default: + case 'TV': + $this->sendKey('T'); + break; + case 'HDMI': + $this->sendKey('H'); + break; + case 'HDMI1': + $this->sendKey('H1'); + break; + case 'HDMI2': + $this->sendKey('H2'); + break; + case 'HDMI3': + $this->sendKey('H3'); + break; + case 'HDMI4': + $this->sendKey('H4'); + break; + case 'AV1': + $this->sendKey('AV1'); + break; + case 'AV2': + $this->sendKey('AV2'); + break; + case 'AV3': + $this->sendKey('AV3'); + break; + } + } +} diff --git a/src/Sikofitt/SamsungTV/Console/Command/ChannelCommand.php b/src/Sikofitt/SamsungTV/Console/Command/ChannelCommand.php new file mode 100644 index 0000000..ce9e433 --- /dev/null +++ b/src/Sikofitt/SamsungTV/Console/Command/ChannelCommand.php @@ -0,0 +1,67 @@ +. + */ + +namespace Sikofitt\SamsungTV\Console\Command; + +use Sikofitt\SamsungTV\Actions\Actions; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +/** + * Command to change channels + */ +class ChannelCommand extends Command +{ + /** + * {@inheritDoc} + * + * @throws \Symfony\Component\Console\Exception\InvalidArgumentException + */ + protected function configure(): void + { + $this->setName('channel') + ->setDescription('change the channel') + ->addArgument( + 'channel', + InputArgument::REQUIRED, + 'chanel to change to' + ); + } + + /** + * {@inheritDoc} + * + * @throws \Symfony\Component\Console\Exception\InvalidArgumentException + */ + protected function execute(InputInterface $input, OutputInterface $output): void + { + $io = new SymfonyStyle($input, $output); + + $channel = $input->getArgument('channel'); + + $actions = new Actions('10.5.4.18'); + + $actions->changeChannel($channel); + + $io->success('Changed the Channel to ' . $channel); + } +} diff --git a/src/Sikofitt/SamsungTV/Console/Command/InputCommand.php b/src/Sikofitt/SamsungTV/Console/Command/InputCommand.php new file mode 100644 index 0000000..9def737 --- /dev/null +++ b/src/Sikofitt/SamsungTV/Console/Command/InputCommand.php @@ -0,0 +1,118 @@ +. + */ + +namespace Sikofitt\SamsungTV\Console\Command; + +use Sikofitt\SamsungTV\Actions\Actions; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +/** + * Command to change the input of the Samsung TV. + */ +class InputCommand extends Command +{ + /** + * {@inheritDoc} + * + * @throws \Symfony\Component\Console\Exception\InvalidArgumentException + */ + public function configure(): void + { + $this->setName('input') + ->setDescription('Change the TV source') + ->addOption('port', 'p', InputOption::VALUE_REQUIRED, 'Port of the Samsung TV', 55000) + ->addOption('address', 'a', InputOption::VALUE_REQUIRED, 'IP or Host of the SamsungTV') + ->addArgument( + 'input', + InputArgument::REQUIRED, + 'The input channel. For a list type \'list\'.' + ); + } + + /** + * {@inheritDoc} + * + * @throws \Symfony\Component\Console\Exception\InvalidArgumentException + */ + public function interact(InputInterface $input, OutputInterface $output): void + { + $inputArg = $input->getArgument('input'); + + if ('list' === strtolower($inputArg)) { + $output->writeln(sprintf('Available inputs are : %s.%s', implode(', ', $this->getAvailableInputs()), PHP_EOL)); + exit; + } + + if(null === $input->getOption('address')) { + throw new InvalidArgumentException('We need an ip address or host to connect to.'); + } + + if (false === in_array(strtolower($inputArg), $this->getAvailableInputs(), true)) { + throw new InvalidArgumentException('Input ' . $inputArg . ' is not available'); + } + } + + /** + * {@inheritDoc} + * + * @throws \Symfony\Component\Console\Exception\InvalidArgumentException + * + * @return null|int + */ + public function execute(InputInterface $input, OutputInterface $output): ?int + { + $io = new SymfonyStyle($input, $output); + $inputArg = $input->getArgument('input'); + + try { + $actions = new Actions($input->getOption('address'), $input->getOption('port')); + $actions->changeInput(strtoupper($inputArg)); + } catch (\Exception $e) { + $io->error($e->getMessage()); + return 1; + } finally { + $io->success('Changed source input to ' . $inputArg); + return 0; + } + } + + /** + * @return array + */ + private function getAvailableInputs(): array + { + return [ + 'tv', + 'hdmi', + 'hdmi1', // different than hdmi for some reason. + 'hdmi2', + 'hdmi3', + 'hdmi4', + 'av1', + 'av2', + 'av3', + ]; + } +} diff --git a/src/Sikofitt/SamsungTV/Packet/PacketFactory.php b/src/Sikofitt/SamsungTV/Packet/PacketFactory.php new file mode 100644 index 0000000..9842b95 --- /dev/null +++ b/src/Sikofitt/SamsungTV/Packet/PacketFactory.php @@ -0,0 +1,166 @@ +. + */ + +namespace Sikofitt\SamsungTV\Packet; + +class PacketFactory +{ + /** + * @link https://gist.github.com/honza889/b70dff4369ff2f0a2afe#file-remote-php-L63 + * + * @return string The Start command + */ + public function getStartPacket(): string + { + return pack( + 'C*', + 0x00, + 0x13, + 0x00, + 0x69, + 0x70, + 0x68, + 0x6f, + 0x6e, + 0x65, + 0x2e, + 0x69, + 0x61, + 0x70, + 0x70, + 0x2e, + 0x73, + 0x61, + 0x6d, + 0x73, + 0x75, + 0x6e, + 0x67, + 0x38, + 0x00, + 0x64, + 0x00, + 0x10, + 0x00, + 0x4d, + 0x54, + 0x41, + 0x75, + 0x4d, + 0x43, + 0x34, + 0x77, + 0x4c, + 0x6a, + 0x49, + 0x79, + 0x4d, + 0x67, + 0x3d, + 0x3d, + 0x14, + 0x00, + 0x63, + 0x6d, + 0x46, + 0x75, + 0x5a, + 0x47, + 0x39, + 0x74, + 0x55, + 0x6d, + 0x56, + 0x74, + 0x62, + 0x33, + 0x52, + 0x6c, + 0x53, + 0x55, + 0x51, + 0x3d, + 0x0c, + 0x00, + 0x62, + 0x58, + 0x6c, + 0x53, + 0x5a, + 0x57, + 0x31, + 0x76, + 0x64, + 0x47, + 0x55, + 0x3d + ); + } + + /** + * @link https://gist.github.com/honza889/b70dff4369ff2f0a2afe#file-remote-php-L73 + * + * @param string $keyCode the base64 encoded key code. + * + * @return string + */ + public function getPayload(string $keyCode): string + { + return pack("C*", 0x00, 0x00, 0x00, 0x00).pack("C*", strlen($keyCode), 0x00).$keyCode; + } + + /** + * @link https://gist.github.com/honza889/b70dff4369ff2f0a2afe#file-remote-php-L75-L76 + * + * @param string $keyCode the base64 encoded key code. + * + * @return string The final payload to send to the TV. + */ + public function getKeyPacket(string $keyCode): string + { + $payload = $this->getPayload($keyCode); + + return pack( + "C*", + 0x00, + 0x13, + 0x00, + 0x69, + 0x70, + 0x68, + 0x6f, + 0x6e, + 0x65, + 0x2e, + 0x69, + 0x61, + 0x70, + 0x70, + 0x2e, + 0x73, + 0x61, + 0x6d, + 0x73, + 0x75, + 0x6e, + 0x67 + ) . + pack("C*", strlen($payload)-1).$payload; + } +}