Заключването на COVID-19 задържа много от нас у дома, може би с надеждата, че обикновената треска в кабината е най-лошият вид треска, която ще изпитаме. Много от нас консумират повече видео съдържание от всякога. Въпреки че упражненията са особено важни в момента, понякога има носталгия по лукса на доброто, старомодно дистанционно управление, когато лаптопът е извън обсега.
Ето къде се появява този проект: Възможността да трансформирате всеки смартфон - дори стар, който иначе е безполезен поради липса на актуализации - в удобно дистанционно за следващия Netflix / YouTube / Amazon Prime Video / и т.н. binge-watch. Това е и Node.js back-end tutorial: шанс да научите основите на back-end JavaScript, използвайки Express рамката и механизма за шаблони Pug (бивш Jade).
Ако това звучи обезсърчително, пълното Node.js проектът ще бъде представен в края; читателите трябва да научат само толкова, колкото се интересуват от тях, и ще има доста по-нежни обяснения на някои основи по пътя, които по-опитните читатели могат да пропуснат.
Читателите може да се чудят: „Защо да се занимавам с кодиране на заден край на Node.js?“ (Освен възможността за обучение, разбира се.) „Няма ли вече приложение за това?“
Разбира се - много от тях. Но има две основни причини това да не е желателно:
Тези проблеми съществуват отдавна и дори са мотивация за подобен проект от 2014 г., намерен в GitHub. nvm
улеснява инсталирането на по-стари версии на Node.js и дори ако няколко зависимости се нуждаят от надстройка, Node.js имаше страхотна репутация, че е съвместим назад.
За съжаление, bitrot спечели. Упоритият подход и фоновата съвместимост на Node.js не са подходящи за безкрайни оттегляния и невъзможни цикли на зависимост сред старите версии на Grunt, Bower и десетки други компоненти. Часове по-късно не беше ясно, че ще бъде много по-лесно да започнем от нулата - собственият съвет на този автор срещу преоткриване на колелото независимо от това.
Първо, обърнете внимание, че този проект Node.js в момента е специфичен за Linux - разработен и тестван по-специално за Linux Mint 19 и Linux Mint 19.3, но със сигурност може да се добави поддръжка за други платформи. То може вече работят на Mac.
Ако приемем модерна версия на Node.js е инсталиран и е отворен команден ред в нова директория, която ще служи като корен на проекта, готови сме да започнем с Express:
npx express-generator --view=pug
Забележка: Тук, npx
е удобен инструмент, който се доставя с npm
, мениджъра на пакети Node.js, който се доставя с Node.js. Използваме го, за да стартираме скелетния генератор на приложението на Express. Към момента на писане, генераторът прави проект Express / Node.js, който по подразбиране все още дърпа шаблонния механизъм, наречен Jade, въпреки че Jade проект се преименува на „Pug“ от версия 2.0 нататък. За да бъдем актуални и да използваме Pug веднага - плюс, избягвайте предупрежденията за оттегляне - ние се придържаме към --view=pug
, опция в командния ред за express-generator
скрипт, който се изпълнява от npx
.
След като приключим, трябва да инсталираме някои пакети от новопопълнения списък със зависимости на нашия проект Node.js в package.json
Традиционният начин да направите това е да стартирате npm i
(i
за „инсталиране“). Но някои все още предпочитат скоростта на Прежда , така че ако сте инсталирали това, просто стартирайте yarn
без параметри.
В този случай трябва да е безопасно да игнорирате (надявам се скоро да бъде поправен) предупреждение за оттегляне от една от подзависимостите на Pug, стига достъпът да се запазва до необходимата база в локалната мрежа.
Бързо yarn start
или npm start
, последвано от навигиране до localhost:3000
в браузър, показва, че нашият основен експресен Node.js back end работи. Можем да го убием с Ctrl+C
.
С дистанционно част от половината вече направено, нека насочим вниманието си към контрол част. Имаме нужда от нещо, което може програмно да управлява машината, на която ще стартираме нашия Node.js, като се преструваме, че натиска клавиши на клавиатурата.
За това ще инсталираме xdotool
използвайки неговите официални инструкции . Бърз тест на примерната им команда в терминал:
xdotool search 'Mozilla Firefox' windowactivate --sync key --clearmodifiers ctrl+l
... трябва да направи точно това, което пише, ако приемем, че Mozilla Firefox е отворен по това време. Това е добре! Лесно е да накараме нашия проект Node.js да извика инструменти за команден ред като xdotool
, както скоро ще видим.
Това може да не е вярно за всички, но лично аз намирам, че много съвременни физически дистанционни управления имат около пет пъти повече бутони, отколкото някога ще използвам. Така че за този проект разглеждаме оформление на цял екран с решетка три на три от хубави, големи, лесни за насочване бутони. От личните предпочитания зависи какви могат да бъдат тези девет бутона.
Оказва се, че клавишните комбинации дори за най-простите функции не са еднакви Нетфликс , Youtube , и Amazon Prime Video . Тези услуги също не работят с общи медийни клавиши, както е вероятно приложението на родния музикален плейър. Също така някои функции може да не са налични при всички услуги.
Така че това, което ще трябва да направим, е да дефинираме различно оформление на дистанционното управление за всяка услуга и да осигурим начин за превключване между тях.
Нека вземем бърз прототип, работещ с шепа предварителни настройки. Ще ги поставим в common/preset_commands.js
- „често срещани“, защото ще включим тези данни от повече от един файл:
module.exports = { // We could use ️ but some older phones (e.g., Android 5.1.1) won't show it, hence ️ instead 'Netflix': { commands: { '-': 'Escape', '+': 'f', '': 'Up', '⇤': 'XF86Back', '️': 'Return', '': 'Down', '': 'Left', '': 'Right', '': 'm', }, }, 'YouTube': { commands: { '⇤': 'shift+p', '⇥': 'shift+n', '': 'Up', 'CC': 'c', '️': 'k', '': 'Down', '': 'j', '': 'l', '': 'm', }, }, 'Amazon Prime Video': { window_name_override: 'Prime Video', commands: { '⇤': 'Escape', '+': 'f', '': 'Up', 'CC': 'c', '️': 'space', '': 'Down', '': 'Left', '': 'Right', '': 'm', }, }, 'Generic / Music Player': { window_name_override: '', commands: { '⇤': 'XF86AudioPrev', '⇥': 'XF86AudioNext', '': 'XF86AudioRaiseVolume', '': 'r', '️': 'XF86AudioPlay', '': 'XF86AudioLowerVolume', '': 'Left', '': 'Right', '': 'XF86AudioMute', }, }, };
Стойностите на ключовия код могат да бъдат намерен с помощта на xev
. (За мен тези „аудио без звук“ и „аудио възпроизвеждане“ не бяха открити по този метод, така че аз също се консултирах списък с медийни ключове .)
Читателите могат да забележат разликата в случая между space
и Return
- независимо от причината за това, тази подробност трябва да бъде уважена за xdotool
да работи коректно. Във връзка с това имаме няколко дефиниции, написани изрично - напр. shift+p
въпреки че P
също ще работи - само за да бъдат ясни намеренията ни.
Ще ни трябва крайна точка до POST
до, което от своя страна ще симулира натискания на клавиши, използвайки xdotool
. Тъй като ще имаме различни групи ключове, които можем да изпратим (по един за всяка услуга), ще извикаме крайната точка за конкретен group
Ще преназначим генерираното users
крайна точка чрез преименуване routes/users.js
до routes/group.js
и извършване на съответните промени в app.js
:
// ... var indexRouter = require('./routes/index'); var groupRouter = require('./routes/group'); // ... app.use('/', indexRouter); app.use('/group', groupRouter); // ...
The ключ функционалността използва xdotool
чрез извикване на системна обвивка в routes/group.js
. Ще кодираме твърдо YouTube
като избрано меню за момента, само за целите на тестването.
const express = require('express'); const router = express.Router(); const debug = require('debug')('app'); const cp = require('child_process'); const preset_commands = require('../common/preset_commands'); /* POST keystroke to simulate */ router.post('/', function(req, res, next) { const keystroke_name = req.body.keystroke_name; const keystroke_code = preset_commands['YouTube'].commands[keystroke_name]; const final_command = `xdotool search 'YouTube' windowactivate --sync key --clearmodifiers ${keystroke_code}`; debug(`Executing ${final_command}`); cp.exec(final_command, (err, stdout, stderr) => { debug(`Executed ${keystroke_name}`); return res.redirect(req.originalUrl); }); }); module.exports = router;
Тук хващаме искания ключ „name“ от POST
тяло на заявката (req.body
) под параметъра, наречен keystroke_name
. Това ще бъде нещо като ️
. След това използваме това, за да търсим съответния код от preset_commands['YouTube']
s commands
обект.
Последната команда е на повече от един ред, така че s в края на всеки ред обединява всички парчета в една команда:
search 'YouTube'
извлича първия прозорец с „YouTube“ в заглавието.windowactivate --sync
активира извлечения прозорец и изчаква, докато е готов да получи натискане на клавиш.key --clearmodifiers ${keystroke_code}
изпраща натискане на клавиш, като се увери, че временно изчиства модификаторните клавиши като Caps Lock, които могат да попречат на това, което изпращамеНа този етап кодът приема, че го подаваме на валиден вход - нещо, за което ще бъдем по-внимателни по-късно.
За улеснение, кодът също така ще приеме, че има само един отворен прозорец на приложението със заглавие „YouTube“ - ако има повече от едно съвпадение, няма гаранция, че ще изпратим натискания на клавиши до предвидения прозорец. Ако това е проблем, може да помогне заглавията на прозорците да могат да се променят просто чрез превключване на разделите на браузъра във всички прозорци, освен този, за да се управлява от разстояние.
С това готово можем да стартираме нашия сървър отново, но този път с разрешено отстраняване на грешки, за да можем да видим изхода на нашите debug
обаждания. За да направите това, просто стартирайте DEBUG=old-fashioned-remote:* yarn start
или DEBUG=old-fashioned-remote:* npm start
. След като стартира, пуснете видеоклип в YouTube, отворете друг прозорец на терминала и опитайте cURL повикване:
curl --data 'keystroke_name=️' http://localhost:3000/group
Това изпраща POST
заявка с исканото име на натискане на клавиш в тялото към нашата локална машина на порт 3000
, портът, който нашият заден край слуша. Изпълнението на тази команда трябва да изведе бележки за Executing
и Executed
в npm
прозореца и по-важното е да изведете браузъра и да поставите видеото му на пауза. Повторното изпълнение на тази команда трябва да даде същия изход и да го постави на пауза.
Задният ни край не е съвсем свършен. Ще ни е необходим и за да можем:
preset_commands
.common/preset_commands.js
директно на предния край, тъй като това вече е JavaScript и е филтрирано там. Това е едно от потенциалните предимства на Node.js back end, просто не го използваме тук .)И двете тези функции са мястото, където нашият урок на Node.js се пресича с предния край, базиран на Pug, който ще изградим.
Задната част на уравнението означава модифициране routes/index.js
да изглежда така:
const express = require('express'); const router = express.Router(); const preset_commands = require('../common/preset_commands'); /* GET home page. */ router.get('/', function(req, res, next) { const group_names = Object.keys(preset_commands); res.render('index', { title: 'Which Remote?', group_names, portrait_css: `.group_bar { height: calc(100%/${Math.min(4, group_names.length)}); line-height: calc(100vh/${Math.min(4, group_names.length)}); }`, landscape_css: `.group_bar { height: calc(100%/${Math.min(2, group_names.length)}); line-height: calc(100vh/${Math.min(2, group_names.length)}); }`, }); }); module.exports = router;
Тук хващаме имената на оформлението на дистанционното управление (group_names
), като извикваме Object.keys
на нашия preset_commands
файл. След това ги изпращаме и някои други данни, които ще са ни необходими, на механизма за шаблони на Pug, който автоматично се извиква чрез res.render()
Внимавайте да не объркате значението на keys
тук с ключа удари изпращаме: Object.keys
ни дава масив (подреден списък), съдържащ всички ключове от двойки ключ-стойност които съставляват обект в JavaScript:
const my_object = { 'a key': 'its corresponding value', 'another key': 'its separate corresponding value', };
Ако погледнем common/preset_commands.js
, ще видим горния модел и нашия ключове (в обектния смисъл) са имената на нашите групи: 'Netflix'
, 'YouTube'
и др. Съответните им стойности не са прости низове като my_object
има по-горе - те самите са цели обекти със собствени ключове, т.е. commands
и евентуално window_name_override
.
Персонализираният CSS, който се предава тук, е, разбира се, малко хак. Причината, поради която изобщо се нуждаем, вместо да използваме модерно решение, базирано на flexbox, е по-добра съвместимост с прекрасния свят на мобилните браузъри в техните още по-прекрасни по-стари въплъщения. В този случай основното нещо, което трябва да се отбележи, е, че в хоризонтален режим ние държим бутоните големи, като показваме не повече от две опции на екран; в портретен режим, четири.
Но къде всъщност това се превръща в HTML за изпращане в браузъра? Ето къде views/index.pug
влиза, което ще искаме да изглежда така:
extends layout block header_injection style(media='(orientation: portrait)') #{portrait_css} style(media='(orientation: landscape)') #{landscape_css} block content each group_name in group_names span(class='group_bar') a(href='/group/?group_name=' + group_name) #{group_name}
Първият ред е важен: extends layout
означава, че Pug ще взема този файл в контекста на views/layout.pug
, което е нещо като родителски шаблон, който ще използваме отново тук, а също и в друг изглед. Ще трябва да добавим няколко реда след link
ред, така че крайният файл да изглежда така:
doctype html html head title= title link(rel='stylesheet', href='/stylesheets/style.css') block header_injection meta(name='viewport', content='user-scalable=no') body block content
Тук няма да навлизаме в основите на HTML, но за непознати с тях читатели този код на Pug отразява HTML кода със стандартни тарифи, който се намира почти навсякъде. The шаблониране аспектът му започва с title= title
, което задава HTML заглавието на каквато и да е стойност, съответстваща на title
ключ на обекта, който предаваме Pug през res.render
.
Можем да видим различен аспект на шаблонирането на два реда по-късно с block
ние назоваваме header_injection
. Блокове като тези са заместители, които могат да бъдат заменени от шаблони, които разширяват текущия. (Без връзка, линията meta
е просто бързо решение за мобилните браузъри, така че когато потребителите докосват контрола на силата на звука няколко пъти подред, телефонът се въздържа от увеличаване или намаляване.)
Обратно към нашите block
s: Ето защо views/index.pug
определя свои собствени block
и със същите имена, намерени в views/layout.pug
. В случая на header_injection
, това ни позволява да използваме CSS, специфичен за портретни или пейзажни ориентации, в които ще бъде телефонът.
content
е мястото, където поставяме основната видима част от уеб страницата, която в този случай:
group_names
масив го предаваме,
елемент за всеки с CSS клас group_bar
приложени към него, и
въз основа на group_name
.Класът CSS group_bar
можем да дефинираме във файла, изтеглен чрез views/layout.pug
, а именно public/stylesheets/style.css
:
html, body, form { padding: 0; margin: 0; height: 100%; font: 14px 'Lucida Grande', Helvetica, Arial, sans-serif; } .group_bar, .group_bar a, .remote_button { box-sizing: border-box; border: 1px solid white; color: greenyellow; background-color: black; } .group_bar { width: 100%; font-size: 6vh; text-align: center; display: inline-block; } .group_bar a { text-decoration: none; display: block; }
В този момент, ако npm start
все още работи, ще отиде на http://localhost:3000/
в настолен браузър трябва да се показват два много големи бутона за Netflix и YouTube, а останалите са достъпни чрез превъртане надолу.
Но ако щракнем върху тях в този момент, те няма да работят, защото все още не сме дефинирали маршрута, към който се свързват (GET
оттенъкът на /group
.)
За целта ще добавим това към routes/group.js
малко преди финала module.exports
линия:
router.get('/', function(req, res, next) { const group_name = req.query.group_name || ''; const group = preset_commands[group_name]; return res.render('group', { keystroke_names: Object.keys(group.commands), group_name, title: `${group_name.match(/([A-Z])/g).join('')}-Remote` }); });
Това ще получи името на групата, изпратено до крайната точка (например, като поставите ?group_name=Netflix
в края на /group/
), и използвайте това, за да получите стойността на commands
от съответната група. Тази стойност (group.commands
) е обект и ключовете на този обект са имената (keystroke_names
), които ще покажем в оформлението на дистанционното ни управление.
Забележка: Неопитните разработчици няма да се нуждаят от подробности за това как работи, но стойността за title
използва малко от регулярни изрази за да превърнем имената на нашите групи / оформления в съкращения - например дистанционното ни управление в YouTube ще има заглавието на браузъра YT-Remote
По този начин, ако отстраняваме грешки на нашата хост машина, преди да изпробваме нещата по телефона, няма да имаме xdotool
хващаме самия прозорец на браузъра за дистанционно управление, вместо този, който се опитваме да контролираме. Междувременно на нашите телефони заглавието ще бъде хубаво и кратко, ако искаме да маркираме дистанционното управление.
Както при предишната ни среща с res.render
, и тази изпраща данните си, за да се смеси с шаблона views/group.pug
Ще създадем този файл и ще го запълним с това:
extends layout block header_injection script(type='text/javascript', src='/javascript/group-client.js') block content form(action='/group?group_name=' + group_name, method='post') each keystroke_name in keystroke_names input(type='submit', name='keystroke_name', value=keystroke_name, class='remote_button')
Както при views/index.pug
, ние заместваме двата блога от views/layout.pug
. Този път не залагаме CSS в заглавката, а някои JavaScript от страна на клиента, до които ще стигнем скоро. (И да, в момент на разсъдливост преименувах неправилно плурализираните javascripts
...)
Основното content
ето HTML форма, направена от куп различни бутони за изпращане, по един за всеки keystroke_name
. Всеки бутон изпраща формуляра (като прави заявка POST
), използвайки името на натискането на клавиш, което показва като стойността, която изпраща с формуляра.
Ще ни е необходим и малко повече CSS в основния ни файл със стилове:
.remote_button { float: left; width: calc(100%/3); height: calc(100%/3); font-size: 12vh; }
По-рано, когато настроихме крайната точка, завършихме обработката на заявката с:
return res.redirect(req.originalUrl);
Това на практика означава, че когато браузърът изпрати формуляра, задната част на Node.js реагира, като казва на браузъра да се върне към страницата, от която е изпратен формулярът, т.е. основното оформление на дистанционното управление. Би било по-елегантно, без да превключвате страници; ние обаче искаме максимална съвместимост със странния и прекрасен свят на отпадналите мобилни браузъри. По този начин, дори без изобщо да работи JavaScript от предния край, нашият фонов проект Node.js Трябва все още функционира.
Недостатъкът на използването на формуляр за подаване на натискания на клавиши е, че браузърът трябва да изчака и след това да изпълни допълнително двупосочно пътуване: Страницата и нейните зависимости трябва да бъдат поискани от нашия Node.js back end и доставени. След това те трябва да бъдат изобразени отново от браузъра.
Читателите може да се чудят какъв ефект може да има това. В края на краищата страницата е малка, зависимостите й са изключително минимални и окончателният ни проект Node.js ще работи по локална wifi връзка. Трябва да е настройка с ниска латентност, нали?
Както се оказва - поне при тестване на по-стари смартфони с Windows Phone 8.1 и Android 4.4.2 - ефектът за съжаление е доста забележим в често срещания случай на бързо докосване за увеличаване или намаляване на силата на звука на възпроизвеждането с няколко стъпала. Ето къде JavaScript може да помогне, без да отнема от елегантния ни резервен вариант на ръчни POST
чрез HTML формуляри.
На този етап, нашият окончателен клиентски JavaScript (който трябва да бъде въведен public/javascript/group-client.js
) трябва да е съвместим със стари, вече не поддържани мобилни браузъри. Но ние не се нуждаем от много от него:
(function () { function form_submit(event) { var request = new XMLHttpRequest(); request.open('POST', window.location.pathname + window.location.search, true); request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); request.send('keystroke_name=' + encodeURIComponent(event.target.value)); event.preventDefault(); } window.addEventListener('DOMContentLoaded', function() { var inputs = document.querySelectorAll('input'); for (var i = 0; i Тук, form_submit
функцията просто изпраща данните чрез асинхронно повикване, а последният ред предотвратява нормалното поведение на изпращане на браузърите, при което се зарежда нова страница въз основа на отговора на сървъра. Втората половина на този фрагмент просто изчаква, докато страницата се зареди, и след това закача всеки бутон за изпращане, за да използва form_submit
Цялото нещо е увито IIFE .
Финални щрихи
Има редица промени към горните фрагменти във финалната версия на нашия урок на кода на Node.js, най-вече за целите на по-доброто обработване на грешки:
- Задният край на Node.js сега проверява имената на групите и натисканията на клавиши, изпратени до него, за да се увери, че съществуват. Този код е във функция, която се използва повторно и за
GET
и POST
функции на routes/group.js
. - Използваме Pug
error
шаблон, ако не го направят. - Предната част на JavaScript и CSS сега правят бутоните временно очертани в сиво, докато чакат отговор от сървъра, зелено веднага щом сигналът премине докрай
xdotool
и обратно без проблеми и червено, ако нещо не работи както се очаква. - Задният край на Node.js ще отпечата проследяване на стека, ако умре, което ще бъде по-малко вероятно предвид горното.
Читателите са добре дошли да разгледат (и / или клонират) цялостния проект Node.js на GitHub .
Node.js Back-end Tutorial, Стъпка 5: Тест от реалния свят
Време е да го изпробвате на действителен телефон, свързан към същата wifi мрежа като хоста, който работи npm start
и филм или музикален плейър. Въпросът е само да насочите уеб браузъра на смартфона към локалния IP адрес на хоста (с :3000
суфикс към него), което може би е най-лесно да се намери, като стартирате hostname -I | awk '{print }'
в терминал на хоста.
Един от проблемите, които потребителите на Windows Phone 8.1 могат да забележат, е опитът да се придвижат до нещо като 192.168.2.5:3000
ще даде изскачащо съобщение за грешка:

За щастие няма нужда да се обезсърчавате: Просто добавете префикс с http://
или добавяне на последващ /
получава го, за да извлече адреса без допълнителни оплаквания.

Избирането на опция там трябва да ни отведе до работещо дистанционно управление.

За допълнително удобство потребителите може да поискат да коригират DHCP настройките на своя рутер, за да присвояват винаги един и същ IP адрес на хоста, и да маркират екрана за избор на оформление и / или всякакви любими оформления.
Искания за изтегляне Добре дошли
Вероятно не всеки ще хареса този проект точно такъв, какъвто е. Ето няколко идеи за подобрения за тези, които искат да се задълбочат по-нататък в кода:
emacs срещу notepad++
- Трябва да е лесно да променяте оформленията или да добавяте нови за други услуги, като Disney Plus.
- Може би някои биха предпочели оформлението в „лек режим“ и опцията за превключване между тях.
- Резервирането от Netflix, тъй като е необратимо, наистина може да използва „сигурен ли си?“ някакво потвърждение.
- Проектът със сигурност ще се възползва Windows поддържа.
xdotool
В документацията се споменава OSX - работи ли този (или би могъл ли) проект на съвременен Mac? - За разширено излежаване, начин за търсене и разглеждане на филми, вместо да се налага да избирате един филм на Netflix / Amazon Prime Video или да създавате плейлист в YouTube на компютъра.
- Автоматизиран тестов пакет, в случай че някоя от предложените промени наруши първоначалната функционалност.
Надявам се да ви е харесал този урок на Node.js отзад и подобрен медиен опит като резултат. Приятно стрийминг - и кодиране!
Свързани: Изграждане на Node.js / TypeScript REST API, Част 1: Express.js Разбиране на основите
Node.js за задния край ли е?
Да. Node.js е програма от командния ред, която изпълнява JavaScript код и обикновено се използва на уеб хост за обслужване на уеб страници, свързване с бази данни и т.н.
Достатъчен ли е Node.js за заден край?
Абсолютно. Правилно проектиран, задната част на Node.js може да се мащабира както и всяка технология. Въпреки това често се интегрира с други важни компоненти, като достъп до слоя от база данни на приложението.
Какво е Express.js?
Express е модул за Node.js, който намалява количеството на шаблонния код, необходим за писане на обща функционалност на уеб сървъра. Има собствена зряла под-екосистема. Повечето уеб сървъри на Node.js използват Express.
Какво е Pug / Jade?
Pug (по-рано Jade) е шаблонна машина, която се интегрира с Express. Всъщност от години това е механизмът на шаблоните по подразбиране, който генераторът на проекти Express включва в нови проекти.
Какво е xdotool?
Програмата за команден ред xdotool симулира натискания на клавиши на компютъра, на който работи. Този проект позволява на телефона да извършва подобни действия на компютър чрез уеб страница, превръщайки го в дистанционно управление.