ืื ื• ืžืชื›ื ืชื™ื ืฉืœื™ื˜ื” ืงื•ืœื™ืช ืฉืœ ื”ืžืกื•ืง ื‘ืืžืฆืขื•ืช Node.js ื•-ARDrone

ืื ื• ืžืชื›ื ืชื™ื ืฉืœื™ื˜ื” ืงื•ืœื™ืช ืฉืœ ื”ืžืกื•ืง ื‘ืืžืฆืขื•ืช Node.js ื•-ARDrone

ื‘ืžื“ืจื™ืš ื–ื” ื ื‘ื—ืŸ ื™ืฆื™ืจืช ืชื•ื›ื ื™ืช ืœืจื—ืคืŸ ืขื ืฉืœื™ื˜ื” ืงื•ืœื™ืช ื‘ืืžืฆืขื•ืช Node.js ื•-Web speech API. Copter - Parrot ARDrone 2.0.

ืื ื• ืžื–ื›ื™ืจื™ื: ืœื›ืœ ืงื•ืจืื™ Habr - ื”ื ื—ื” ืฉืœ 10 ืจื•ื‘ืœ ื‘ืขืช ื”ืจืฉืžื” ืœื›ืœ ืงื•ืจืก Skillbox ื‘ืืžืฆืขื•ืช ืงื•ื“ ื”ื”ื˜ื‘ื” ืฉืœ Habr.

Skillbox ืžืžืœื™ืฆื”: ืงื•ืจืก ืžืขืฉื™ "Mobile Developer PRO".

ืžื‘ื•ื

ืžื–ืœ"ื˜ื™ื ื”ื ืžื“ื”ื™ืžื™ื. ืื ื™ ืžืื•ื“ ื ื”ื ื” ืœืฉื—ืง ืขื ื”-quad ืฉืœื™, ืœืฆืœื ืชืžื•ื ื•ืช ื•ืกืจื˜ื•ื ื™ื, ืื• ืกืชื ืœื™ื”ื ื•ืช. ืื‘ืœ ื›ืœื™ ื˜ื™ืก ื‘ืœืชื™ ืžืื•ื™ืฉื™ื (ืžืœ"ื˜ื™ื) ืžืฉืžืฉื™ื ืœื™ื•ืชืจ ืžืกืชื ื‘ื™ื“ื•ืจ. ื”ื ืขื•ื‘ื“ื™ื ื‘ืงื•ืœื ื•ืข, ืœื•ืžื“ื™ื ืงืจื—ื•ื ื™ื, ื•ืžืฉืžืฉื™ื ืืช ื”ืฆื‘ื ื•ื ืฆื™ื’ื™ ื”ืžื’ื–ืจ ื”ื—ืงืœืื™.

ื‘ืžื“ืจื™ืš ื–ื” ื ื‘ื—ืŸ ื™ืฆื™ืจืช ืชื•ื›ื ื™ืช ืฉืชืืคืฉืจ ืœืš ืœืฉืœื•ื˜ ื‘ืจื—ืคืŸ. ื‘ืืžืฆืขื•ืช ืคืงื•ื“ื•ืช ืงื•ืœื™ื•ืช. ื›ืŸ, ื”ืžืกื•ืง ื™ืขืฉื” ืžื” ืฉืืชื” ืื•ืžืจ ืœื• ืœืขืฉื•ืช. ื‘ืกื•ืฃ ื”ืžืืžืจ ื™ืฉ ืชื•ื›ื ื™ืช ืžื•ื›ื ื” ื•ื•ื™ื“ืื• ืฉืœ ื‘ืงืจืช ืžืœ"ื˜ื™ื.

ื‘ืจื–ืœ

ืื ื—ื ื• ืฆืจื™ื›ื™ื ืืช ื”ื“ื‘ืจื™ื ื”ื‘ืื™ื:

  • Parrot ARDrone 2.0;
  • ื›ื‘ืœ Ethernet;
  • ืžื™ืงืจื•ืคื•ืŸ ื˜ื•ื‘.

ื”ืคื™ืชื•ื— ื•ื”ื ื™ื”ื•ืœ ื™ืชื‘ืฆืขื• ื‘ืชื—ื ื•ืช ืขื‘ื•ื“ื” ืขื Windows/Mac/Ubuntu. ื‘ืื•ืคืŸ ืื™ืฉื™, ืขื‘ื“ืชื™ ืขื Mac ื•-Ubuntu 18.04.

ืชื•ื›ื ื”

ื”ื•ืจื“ ืืช ื”ื’ืจืกื” ื”ืขื“ื›ื ื™ืช ื‘ื™ื•ืชืจ ืฉืœ Node.js ืž ืืชืจ ืจืฉืžื™.

ื’ื ืฆืจื™ืš ื”ื’ืจืกื” ื”ืื—ืจื•ื ื” ืฉืœ Google Chrome.

ื”ื‘ื ืช ื”ืžืกื•ืง

ื‘ื•ืื• ื ื ืกื” ืœื”ื‘ื™ืŸ ืื™ืš Parrot ARDrone ืขื•ื‘ื“. ื”ืžืกื•ืง ื”ื–ื” ื›ื•ืœืœ ืืจื‘ืขื” ืžื ื•ืขื™ื.

ืื ื• ืžืชื›ื ืชื™ื ืฉืœื™ื˜ื” ืงื•ืœื™ืช ืฉืœ ื”ืžืกื•ืง ื‘ืืžืฆืขื•ืช Node.js ื•-ARDrone

ืžื ื•ืขื™ื ืžื ื•ื’ื“ื™ื ืคื•ืขืœื™ื ื‘ืื•ืชื• ื›ื™ื•ื•ืŸ. ื–ื•ื’ ืื—ื“ ืžืกืชื•ื‘ื‘ ื‘ื›ื™ื•ื•ืŸ ื”ืฉืขื•ืŸ, ื”ืฉื ื™ ื ื’ื“ ื›ื™ื•ื•ืŸ ื”ืฉืขื•ืŸ. ื”ืจื—ืคืŸ ื ืข ืขืœ ื™ื“ื™ ืฉื™ื ื•ื™ ื–ื•ื•ื™ืช ื”ื ื˜ื™ื™ื” ื‘ื™ื—ืก ืœืคื ื™ ื”ืฉื˜ื— ืฉืœ ื›ื“ื•ืจ ื”ืืจืฅ, ืฉื™ื ื•ื™ ืžื”ื™ืจื•ืช ื”ืกื™ื‘ื•ื‘ ืฉืœ ื”ืžื ื•ืขื™ื ื•ืขื•ื“ ืžืกืคืจ ืชื ื•ืขื•ืช ื ื™ืชื ื•ืช ืœืชืžืจื•ืŸ.

ืื ื• ืžืชื›ื ืชื™ื ืฉืœื™ื˜ื” ืงื•ืœื™ืช ืฉืœ ื”ืžืกื•ืง ื‘ืืžืฆืขื•ืช Node.js ื•-ARDrone

ื›ืคื™ ืฉืื ื• ื™ื›ื•ืœื™ื ืœืจืื•ืช ื‘ืชืจืฉื™ื ืœืžืขืœื”, ืฉื™ื ื•ื™ ืคืจืžื˜ืจื™ื ืฉื•ื ื™ื ืžื•ื‘ื™ืœ ืœืฉื™ื ื•ื™ ื‘ื›ื™ื•ื•ืŸ ื”ืชื ื•ืขื” ืฉืœ ื”ืžืกื•ืง. ืœื“ื•ื’ืžื”, ื”ืงื˜ื ื” ืื• ื”ื’ื“ืœืช ืžื”ื™ืจื•ืช ื”ืกื™ื‘ื•ื‘ ืฉืœ ื”ืจื•ื˜ื•ืจื™ื ื”ืฉืžืืœื™ ื•ื”ื™ืžื ื™ ื™ื•ืฆืจืช ื’ืœื’ื•ืœ. ื–ื” ืžืืคืฉืจ ืœืจื—ืคืŸ ืœืขื•ืฃ ืงื“ื™ืžื” ืื• ืื—ื•ืจื”.

ืขืœ ื™ื“ื™ ืฉื™ื ื•ื™ ื”ืžื”ื™ืจื•ืช ื•ื”ื›ื™ื•ื•ืŸ ืฉืœ ื”ืžื ื•ืขื™ื, ืื ื• ืงื•ื‘ืขื™ื ื–ื•ื•ื™ื•ืช ื”ื˜ื™ื” ื”ืžืืคืฉืจื•ืช ืœืžืกื•ืง ืœื ื•ืข ื‘ื›ื™ื•ื•ื ื™ื ืื—ืจื™ื. ืœืžืขืฉื”, ืขื‘ื•ืจ ื”ืคืจื•ื™ืงื˜ ื”ื ื•ื›ื—ื™ ืื™ืŸ ืฆื•ืจืš ืœืœืžื•ื“ ืื•ื•ื™ืจื•ื“ื™ื ืžื™ืงื”, ืืชื” ืจืง ืฆืจื™ืš ืœื”ื‘ื™ืŸ ืืช ื”ืขืงืจื•ื ื•ืช ื”ื‘ืกื™ืกื™ื™ื.

ื›ื™ืฆื“ ืคื•ืขืœ Parrot ARDrone

ื”ืจื—ืคืŸ ื”ื•ื ื ืงื•ื“ื” ื—ืžื” ืฉืœ Wi-Fi. ืขืœ ืžื ืช ืœืงื‘ืœ ื•ืœืฉืœื•ื— ืคืงื•ื“ื•ืช ืœืžืกื•ืง, ืขืœื™ืš ืœื”ืชื—ื‘ืจ ืœื ืงื•ื“ื” ื–ื•. ื™ืฉื ื ื™ื™ืฉื•ืžื™ื ืจื‘ื™ื ื•ืฉื•ื ื™ื ื”ืžืืคืฉืจื™ื ืœืš ืœืฉืœื•ื˜ quadcopters. ื”ื›ืœ ื ืจืื” ื‘ืขืจืš ื›ืš:

ืื ื• ืžืชื›ื ืชื™ื ืฉืœื™ื˜ื” ืงื•ืœื™ืช ืฉืœ ื”ืžืกื•ืง ื‘ืืžืฆืขื•ืช Node.js ื•-ARDrone

ื‘ืจื’ืข ืฉื”ืจื—ืคืŸ ืžื—ื•ื‘ืจ, ืคืชื— ืืช ื”ื˜ืจืžื™ื ืœ ื•ืืช telnet 192.168.1.1 - ื–ื” ื”-IP ืฉืœ ื”ืžืกื•ืง. ืขื‘ื•ืจ ืœื™ื ื•ืงืก ืืชื” ื™ื›ื•ืœ ืœื”ืฉืชืžืฉ Linux Busybox.

ืืจื›ื™ื˜ืงื˜ื•ืจืช ื™ื™ืฉื•ืžื™ื

ื”ืงื•ื“ ืฉืœื ื• ื™ื—ื•ืœืง ืœืžื•ื“ื•ืœื™ื ื”ื‘ืื™ื:

  • ืžืžืฉืง ืžืฉืชืžืฉ ืขื API ืœื“ื™ื‘ื•ืจ ืœื–ื™ื”ื•ื™ ืงื•ืœ;
  • ืกื™ื ื•ืŸ ืคืงื•ื“ื•ืช ื•ื”ืฉื•ื•ืื” ืœืชืงืŸ;
  • ืฉืœื™ื—ืช ืคืงื•ื“ื•ืช ืœืจื—ืคืŸ;
  • ืฉื™ื“ื•ืจ ื•ื™ื“ืื• ื—ื™.

ื”-API ืขื•ื‘ื“ ื›ืœ ืขื•ื“ ื™ืฉ ื—ื™ื‘ื•ืจ ืœืื™ื ื˜ืจื ื˜. ื›ื“ื™ ืœื”ื‘ื˜ื™ื— ื–ืืช, ืื ื• ืžื•ืกื™ืคื™ื ื—ื™ื‘ื•ืจ Ethernet.

ื–ื” ื”ื–ืžืŸ ืœื™ืฆื•ืจ ืืคืœื™ืงืฆื™ื”!

ืงื•ื“

ืจืืฉื™ืช, ื‘ื•ืื• ื ื™ืฆื•ืจ ืชื™ืงื™ื” ื—ื“ืฉื” ื•ื ืขื‘ื•ืจ ืืœื™ื” ื‘ืืžืฆืขื•ืช ื”ื˜ืจืžื™ื ืœ.

ืœืื—ืจ ืžื›ืŸ ืื ื• ื™ื•ืฆืจื™ื ืคืจื•ื™ืงื˜ Node ื‘ืืžืฆืขื•ืช ื”ืคืงื•ื“ื•ืช ืœืžื˜ื”.

ืจืืฉื™ืช, ืื ื• ืžืชืงื™ื ื™ื ืืช ื”ืชืœื•ืช ื”ื ื“ืจืฉืช.

ื”ืชืงื ืช npmโ€Š

ืื ื• ื ืชืžื•ืš ื‘ืคืงื•ื“ื•ืช ื”ื‘ืื•ืช:

  • ืœื”ืžืจื™ื;
  • ื ึฐื—ึดื™ืชึธื”;
  • ืœืžืขืœื” - ื”ืžืœ"ื˜ ืขื•ืœื” ื—ืฆื™ ืžื˜ืจ ื•ืžืจื—ืฃ;
  • ืœืžื˜ื” - ื ื•ืคืœ ื—ืฆื™ ืžื˜ืจ ื•ืงื•ืคื;
  • ืฉืžืืœื” - ื”ื•ืœืš ื—ืฆื™ ืžื˜ืจ ืฉืžืืœื”;
  • ื™ืžื™ื ื” - ื”ื•ืœืš ื—ืฆื™ ืžื˜ืจ ื™ืžื™ื ื”;
  • ืกื™ื‘ื•ื‘ - ืžืกืชื•ื‘ื‘ ื‘ื›ื™ื•ื•ืŸ ื”ืฉืขื•ืŸ 90 ืžืขืœื•ืช;
  • ืงื“ื™ืžื” - ื”ื•ืœืš ืงื“ื™ืžื” ื—ืฆื™ ืžื˜ืจ;
  • ื—ื–ืจื” - ื—ื•ื–ืจ ื—ืฆื™ ืžื˜ืจ ืื—ื•ืจื”;
  • ืขืฆื•ืจ

ื”ื ื” ื”ืงื•ื“ ืฉืžืืคืฉืจ ืœืงื‘ืœ ืคืงื•ื“ื•ืช, ืœืกื ืŸ ืื•ืชืŸ ื•ืœืฉืœื•ื˜ ื‘ืจื—ืคืŸ.

const express = require('express');
const bodyparser = require('body-parser');
var arDrone = require('ar-drone');
const router = express.Router();
const app = express();
const commands = ['takeoff', 'land','up','down','goleft','goright','turn','goforward','gobackward','stop'];
 
var drone  = arDrone.createClient();
// disable emergency
drone.disableEmergency();
// express
app.use(bodyparser.json());
app.use(express.static(__dirname + '/public'));
 
router.get('/',(req,res) => {
    res.sendFile('index.html');
});
 
router.post('/command',(req,res) => {
    console.log('command recieved ', req.body);
    console.log('existing commands', commands);
    let command = req.body.command.replace(/ /g,'');
    if(commands.indexOf(command) !== -1) {
        switch(command.toUpperCase()) {
            case "TAKEOFF":
                console.log('taking off the drone');
                drone.takeoff();
            break;
            case "LAND":
                console.log('landing the drone');
                drone.land();
            break;
            case "UP":
                console.log('taking the drone up half meter');
                drone.up(0.2);
                setTimeout(() => {
                    drone.stop();
                    clearTimeout();
                },2000);
            break;
            case "DOWN":
                console.log('taking the drone down half meter');
                drone.down(0.2);
                setTimeout(() => {
                    drone.stop();
                    clearTimeout();
                },2000);
            break;
            case "GOLEFT":
                console.log('taking the drone left 1 meter');
                drone.left(0.1);
                setTimeout(() => {
                    drone.stop();
                    clearTimeout();
                },1000);
            break;
            case "GORIGHT":
                console.log('taking the drone right 1 meter');
                drone.right(0.1);
                setTimeout(() => {
                    drone.stop();
                    clearTimeout();
                },1000);
            break;
            case "TURN":
                console.log('turning the drone');
                drone.clockwise(0.4);
                setTimeout(() => {
                    drone.stop();
                    clearTimeout();
                },2000);
            break;
            case "GOFORWARD":
                console.log('moving the drone forward by 1 meter');
                drone.front(0.1);
                setTimeout(() => {
                    drone.stop();
                    clearTimeout();
                },2000);
            break;
            case "GOBACKWARD":
                console.log('moving the drone backward 1 meter');
                drone.back(0.1);
                setTimeout(() => {
                    drone.stop();
                    clearTimeout();
                },2000);
            break;
            case "STOP":
                drone.stop();
            break;
            default:
            break;    
        }
    }
    res.send('OK');
});
 
app.use('/',router);
 
app.listen(process.env.port || 3000);

ื•ื”ื ื” ืงื•ื“ ื”-HTML ื•ื”-JavaScript ืฉืžืื–ื™ืŸ ืœืžืฉืชืžืฉ ื•ืฉื•ืœื— ืคืงื•ื“ื” ืœืฉืจืช ื”-Node.

<!DOCTYPE html>
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <title>Voice Controlled Notes App</title>
        <meta name="description" content="">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/shoelace-css/1.0.0-beta16/shoelace.css">
        <link rel="stylesheet" href="styles.css">
 
    </head>
    <body>
        <div class="container">
 
            <h1>Voice Controlled Drone</h1>
            <p class="page-description">A tiny app that allows you to control AR drone using voice</p>
 
            <h3 class="no-browser-support">Sorry, Your Browser Doesn't Support the Web Speech API. Try Opening This Demo In Google Chrome.</h3>
 
            <div class="app">
                <h3>Give the command</h3>
                <div class="input-single">
                    <textarea id="note-textarea" placeholder="Create a new note by typing or using voice recognition." rows="6"></textarea>
                </div>    
                <button id="start-record-btn" title="Start Recording">Start Recognition</button>
                <button id="pause-record-btn" title="Pause Recording">Pause Recognition</button>
                <p id="recording-instructions">Press the <strong>Start Recognition</strong> button and allow access.</p>
 
            </div>
 
        </div>
 
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
        <script src="script.js"></script>
 
    </body>
</html>

ื•ื’ื ืงื•ื“ JavaScript ืœืขื‘ื•ื“ื” ืขื ืคืงื•ื“ื•ืช ืงื•ืœื™ื•ืช, ืฉืœื™ื—ืชืŸ ืœืฉืจืช ื”-Node.

try {
 var SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
 var recognition = new SpeechRecognition();
 }
 catch(e) {
 console.error(e);
 $('.no-browser-support').show();
 $('.app').hide();
 }
// other code, please refer GitHub source
recognition.onresult = function(event) {
// event is a SpeechRecognitionEvent object.
// It holds all the lines we have captured so far.
 // We only need the current one.
 var current = event.resultIndex;
// Get a transcript of what was said.
var transcript = event.results[current][0].transcript;
// send it to the backend
$.ajax({
 type: 'POST',
 url: '/command/',
 data: JSON.stringify({command: transcript}),
 success: function(data) { console.log(data) },
 contentType: "application/json",
 dataType: 'json'
 });
};

ื”ืคืขืœืช ื”ืืคืœื™ืงืฆื™ื”

ื ื™ืชืŸ ืœื”ืคืขื™ืœ ืืช ื”ืชื•ื›ื ื™ืช ื‘ืื•ืคืŸ ื”ื‘ื (ื—ืฉื•ื‘ ืœื•ื•ื“ื ืฉื”ืžืกื•ืง ืžื—ื•ื‘ืจ ืœ-Wi-Fi ื•ื›ื‘ืœ ื”-Ethernet ืžื—ื•ื‘ืจ ืœืžื—ืฉื‘).

ืคืชื— ืืช localhost:3000 ื‘ื“ืคื“ืคืŸ ื•ืœื—ืฅ ืขืœ ื”ืชื—ืœ ื–ื™ื”ื•ื™.

ืื ื• ืžืชื›ื ืชื™ื ืฉืœื™ื˜ื” ืงื•ืœื™ืช ืฉืœ ื”ืžืกื•ืง ื‘ืืžืฆืขื•ืช Node.js ื•-ARDrone

ืื ื—ื ื• ืžื ืกื™ื ืœืฉืœื•ื˜ ื‘ืจื—ืคืŸ ื•ืฉืžื—ื™ื.

ืฉื™ื“ื•ืจ ื•ื™ื“ืื• ืžืจื—ืคืŸ

ื‘ืคืจื•ื™ืงื˜, ืฆื•ืจ ืงื•ื‘ืฅ ื—ื“ืฉ ื•ื”ืขืชืง ืืช ื”ืงื•ื“ ื”ื–ื” ืœืฉื:

const http = require("http");
const drone = require("dronestream");
 
const server = http.createServer(function(req, res) {
 
require("fs").createReadStream(__dirname + "/public/video.html").pipe(res);
 });
 
drone.listen(server);
 
server.listen(4000);

ื•ื”ื ื” ืงื•ื“ ื”-HTML, ืื ื• ืžื ื™ื—ื™ื ืื•ืชื• ื‘ืชื•ืš ื”ืชื™ืงื™ื” ื”ืฆื™ื‘ื•ืจื™ืช.

<!doctype html>
 <html>
 <head>
 <meta http-equiv="content-type" content="text/html; charset=utf-8">
 <title>Stream as module</title>
 <script src="/dronestream/nodecopter-client.js" type="text/javascript" charset="utf-8"></script>
 </head>
 <body>
 <h1 id="heading">Drone video stream</h1>
 <div id="droneStream" style="width: 640px; height: 360px"> </div>
 
<script type="text/javascript" charset="utf-8">
 
new NodecopterStream(document.getElementById("droneStream"));
 
</script>
 
</body>
</html>

ื”ืคืขืœ ื•ื”ืชื—ื‘ืจ ืœ-localhost:8080 ื›ื“ื™ ืœืฆืคื•ืช ื‘ื•ื•ื™ื“ืื• ืžื”ืžืฆืœืžื” ื”ืงื“ืžื™ืช.

ืื ื• ืžืชื›ื ืชื™ื ืฉืœื™ื˜ื” ืงื•ืœื™ืช ืฉืœ ื”ืžืกื•ืง ื‘ืืžืฆืขื•ืช Node.js ื•-ARDrone

ื˜ื™ืคื™ื ืฉื™ืžื•ืฉื™ื™ื

  • ืชืขื•ืฃ ืืช ื”ืžืœ"ื˜ ื”ื–ื” ื‘ืชื•ืš ื”ื‘ื™ืช.
  • ื”ื ื— ืชืžื™ื“ ืืช ื›ื™ืกื•ื™ ื”ืžื’ืŸ ืขืœ ื”ืžืœ"ื˜ ืฉืœืš ืœืคื ื™ ื”ื”ืžืจืื”.
  • ื‘ื“ื•ืง ืื ื”ืกื•ืœืœื” ื˜ืขื•ื ื”.
  • ืื ื”ืžืœ"ื˜ ืžืชื ื”ื’ ื‘ืฆื•ืจื” ืžื•ื–ืจื”, ื”ื—ื–ืง ืื•ืชื• ืœืžื˜ื” ื•ื”ืคืš ืื•ืชื•. ืคืขื•ืœื” ื–ื• ืชื›ื ื™ืก ืืช ื”ืžืกื•ืง ืœืžืฆื‘ ื—ื™ืจื•ื ื•ื”ืจื•ื˜ื•ืจื™ื ื™ืขืฆืจื• ืžื™ื“.

ืงื•ื“ ืžื•ื›ืŸ ื•ื”ื“ื’ืžื”

ื“ืžื• ื™ืฉื™ืจ

DOWNLOAD

ืงืจื”!

ื›ืชื™ื‘ืช ืงื•ื“ ื•ืื– ืฆืคื™ื™ื” ื‘ืžื›ื•ื ื” ืžืชื—ื™ืœื” ืœืฆื™ื™ืช ื™ื’ืจืžื• ืœืš ื”ื ืื”! ืขื›ืฉื™ื• ื”ื‘ื ื• ืื™ืš ืœืœืžื“ ืžื–ืœ"ื˜ ืœื”ืงืฉื™ื‘ ืœืคืงื•ื“ื•ืช ืงื•ืœื™ื•ืช. ืœืžืขืฉื”, ื™ืฉ ื”ืจื‘ื” ื™ื•ืชืจ ืืคืฉืจื•ื™ื•ืช: ื–ื™ื”ื•ื™ ืคื ื™ื ืœืžืฉืชืžืฉ, ื˜ื™ืกื•ืช ืื•ื˜ื•ื ื•ืžื™ื•ืช, ื–ื™ื”ื•ื™ ืžื—ื•ื•ืช ื•ืขื•ื“ ื•ืขื•ื“.

ืžื” ืืชื” ื™ื›ื•ืœ ืœื”ืฆื™ืข ืœืฉื™ืคื•ืจ ื”ืชื•ื›ื ื™ืช?

Skillbox ืžืžืœื™ืฆื”:

ืžืงื•ืจ: www.habr.com

ื”ื•ืกืคืช ืชื’ื•ื‘ื”