Post

[Project] WiFi remote control car with webcam using Raspberry Pi

이 프로젝트에 대한 모든 소스코드는 https://github.com/ispaik06/WiFi_RC_car_webcam에 있습니다.

프로젝트 동기 및 목적

일반적으로 아두이노를 배우는 기초 과정에서 아두이노로 RC카를 만들어 본다고 하면, HC-06 같은 블루투스 모듈을 사용해 무선 시리얼 통신을 한다. 나는 앱인벤터(AppInventor)를 통해 직접 앱을 만들기도 했고, 블루투스 통신 활동은 충분히 많이 했다. 다른 방식의 또는 좀 더 개선된 RC카를 만들어보고 싶어 이 프로젝트를 진행하게 되었다.

아두이노에서 SoftwareSerial을 이용한 블루투스 통신은 구현이 매우 간단하지만 통신 가능 거리가 짧다는 것이 매우 큰 단점이다. 드론의 FPV처럼 사용자는 가만히 있고 멀리까지 조종 가능한 그런 RC카를 제작해보고 싶어 통신 방식에 대해서 여러 프로젝트들을 찾아보고 고민했다. 내가 찾아본 결과, 아두이노로 구현할 수 있는 장거리 통신 모듈로 대표적인 것들로는 LoRa 모듈, NRF24L01 모듈, Zigbee 등이 있다.

사실 이 프로젝트는 ‘인터렉티브 디자인 및 테크놀로지’라는 과목을 수강 중에 진행하였다. 과목이 추구하는 방향으로 프로젝트 진행을 해야 했기 때문에 인터렉션을 위한 몇 가지 기능(한 학기 동안 구현 가능할 것 같은 범주 내에서)들을 추려보았다.

  • 2륜 구동 차
  • 웹캠이 달린 2-DOF 구동 관절
  • 웹캠 스트리밍을 통한 FPV(First Person View) 조종
  • 특정 컨트롤러가 없고, 웹 페이지 형태의 컨트롤 패널

통신 방법으로 WiFi 통신을 채택하여 위의 기능들을 구현하면 오로지 장거리 통신을 메인 목표로 설정해서 프로젝트 하는 것 보다 인터렉티브 하다고 생각한다. 그리고 웹서버, 웹 페이지 제작과 웹캠 스트리밍을 한 번도 안 해봤기 때문에 새로운 것을 공부하는 의미있는 활동이 될 것이라고 생각한다. 웹 서버 구축 공부를 가볍게 시작할 수 있는 Flask를 활용해 진행하려고 한다.


진행 계획

웹캠을 사용과 웹 서버 제작을 모두 해야 하기 때문에 라즈베리파이 기반으로 RC카를 제작하기로 하였다. 구동 모터로는 2개의 DC 모터를, 카메라 브라켓 관절을 제어하는 2개의 서보모터를 사용할 예정이다. 사용할 부품 목록은 아래와 같다.

이 프로젝트는 목표하는 RC카 제작을 위한 기본 예제들을 공부하는 과정과 최종 산출물을 만드는 과정으로 나누어 진행하였다. 기본적인 내용 학습 파트는 각각의 포스트로 작성하였다. 이 글에서는 ‘최종 구현‘에 대한 내용을 작성하였으므로 이 글을 읽기 전에 아래 ‘기본적인 내용 학습‘의 글들을 읽고 오기 바란다.


하드웨어 설계 및 제작

회로도 구성

라즈베리파이에 모터 드라이버, 서보모터, 웹캠이 모두 연결되어 있다. 전원으로 11.1V 2200mAh 리포 배터리를 사용한다. 원래 라즈베리파이의 전원은 GPIO 핀이 아닌, 꼭 전원 입력 단자로만 공급하는 것이 좋다.

5V 핀은 퓨즈 이후에 위치하기에 전원 입력용으로 사용하는 것은 권장되지 않는다. 이곳에 외부의 전원을 통해 전원을 공급하면 아무런 보호 장치가 없기에 완벽하게 정류 된 전원이 아니라면 라즈베리파이를 영구적으로 손상시킬 수 있다.(USB, LAN, HDMI 관련 부품들의 VCC에 직결되어 있기 때문에 1차적으로 이들 부품에 직접적인 손상을 입힌다.) 출처

그러나 라즈베리파이 4는 GPIO 5V핀이 DC단자와 직결되도록 전원 회로가 변경되었기 때문에 보드의 5V핀으로 전원 공급을 해도 된다. 그래서 리포 배터리를 두 갈래로 나눠서 하나는 DC-DC 스텝다운을 통해 라즈베리파이에 연결하였고 스텝다운이 없는 선은 바로 모터드라이버의 외부전원에 연결하였다.

아래 그림에는 없지만 웹캠도 라즈베리파이에 연결되어 있다. rccar_without_arduino.ps.png아두이노 없는 버전. 라즈베리파이 전류 부족

이런식으로 하면 라즈베리파이에 흐르는 전류가 부족하기 때문에 작동에 지장이 생긴다. 프로젝트 중간에 서보모터를 아두이노에 연결하는 방식으로 바꾸는데, 이때 스텝다운을 두 개를 써서 라즈베리파이와 아두이노 각각에 연결하였다. 스텝다운 자체에도 전류 제한이 있다는 것이 가장 큰 원인이었던 것 같다.

그래서 만들어진 최종 회로는 아래와 같다.

rccar_with_arduino.ps.png최종 버전

(라즈베리파이, 아두이노), (라즈베리파이, 웹캠)은 각각 usb 케이블로 서로 연결하면 된다.

RC카 프레임 제작

여기에서 확인하세요

웹캠 브라켓 제작

여기에서 확인하세요


구동 컨트롤 패널 웹 서버 구축

RC카의 하드웨어 제작을 모두 마쳤다. 이제부터 코드만 구현하면 된다. 그전에 라즈베리파이로 DC 모터 제어와 서보모터 제어 테스트를 해보고 문제가 없는지 확인하였다.

가장 먼저 해야 할 일은 차를 조종할 웹 서버를 구현하는 것이다. 파이썬과 Flask 라이브러리를 이용해 구현하고, 웹 페이지는 html로 작성할 예정이다. 필요한 웹 페이지의 기능을 정리해보자면 다음과 같다:

  • 키보드의 방향키(4방향)으로 RC카 조종
  • 구동 속도 조절 기능
  • 키보드의 ‘i’, ‘j’, ‘k’,’l’ 키로 웹캠 브라켓 관절 조종
  • 실시간 웹캠 스트리밍

우선 키보드의 방향키로 RC카를 조종하는 것부터 구현해보았다.

1. 키보드의 방향키(4방향)으로 RC카 조종 & 속도 조절 기능

Flask를 이용해 서버를 구축하고, 웹 페이지에서 키보드가 눌리면 파이썬이 실행되고 있는 터미널에 방향에 해당하는 문자열을 출력하는 코드를 먼저 작성하였다. 속도 조절은 슬라이더를 이용해 구현하였다.

파이썬 코드는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from flask import Flask, render_template, request

app = Flask(__name__)

current_speed = None

# Define routes and functionalities
@app.route('/')
def index():
    return render_template('index.html')

@app.route('/move', methods=['POST'])
def move():
    global current_speed
    direction = request.form['direction']
    speed = int(request.form['speed'])

    if speed != current_speed:
        print(f'Speed: {speed}')
        current_speed = speed

    print(f'Direction: {direction}')
    return "Received"

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=80, debug=True)

위 코드에서 방향은 방향키를 누를 때마다, 속도는 새로 업데이트 될 때마다 출력하도록 하였다.

다음은 index.html이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Control Panel</title>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <style>
        #directionDisplay {
            font-size: 24px;
            color: blue;
        }
        #directionText {
            font-weight: bold;
        }
        #speedValue {
            font-size: 20px;
        }
        #speedControl {
            width: 300px;
            height: 20px;
        }
        .spacer {
            margin-top: 20px;
        }
    </style>
</head>
<body>
    <h1>Raspberry Pi WiFi Controlled Car: Control Panel</h1>
    <div id="directionDisplay">Direction: <span id="directionText"></span></div>
    <div class="spacer"></div>
    <div>
        <label for="speedControl">Speed: <span id="speedValue">255</span></label>
        <input type="range" id="speedControl" min="0" max="255" value="255">
    </div>
    <script>
        $(document).ready(function() {
            var directionText = $('#directionText');
            var direction = '';
            var speed = $('#speedControl').val();
    
            $('#speedControl').on('input', function() {
                speed = $(this).val();
                $('#speedValue').text(speed);
                sendDirection(direction, speed);
            });

            $(document).on('keydown', function(e) {
                switch (e.keyCode) {
                    case 37: // 좌 화살표
                        direction = 'Turn left';
                        break;
                    case 38: // 상 화살표
                        direction = 'Forward';
                        break;
                    case 39: // 우 화살표
                        direction = 'Turn right';
                        break;
                    case 40: // 하 화살표
                        direction = 'Backward';
                        break;
                }
                sendDirection(direction, speed);
            });
    
            $(document).on('keyup', function(e) {
                if (e.keyCode >= 37 && e.keyCode <= 40) {
                    direction = 'Stop';
                    sendDirection(direction, speed);
                }
            });
    
            function sendDirection(direction, speed) {
                $.ajax({
                    url: '/move',
                    type: 'POST',
                    data: {direction: direction, speed: speed},
                    success: function(response) {
                        console.log('Direction:', direction, 'Speed:', speed);
                        directionText.text(direction);
                    }
                });
            }
        });
    </script>    
</body>
</html>

index.html은 파이썬 코드와 같은 디렉토리에 있는 templates 디렉토리 안에 있어야 한다.

이제 파이썬 코드를 실행하자. 터미널에 아래와 같이 뜨면 잘 되고 있는 것이다.

project1_1.png

그 다음 웹 브라우저에 http://127.0.0.1:포트번호를 입력하여 로컬 서버로 접속하면 아래와 같이 뜬다.

project1_2.png

웹 페이지에서 방향키를 눌러보고, 슬라이더를 조절하면 터미널에 문자열이 출력되는 것을 확인할 수 있을 것이다.

project1_3.gif실행하면 위와 같이 뜬다 (gif file)

2. 키보드의 ‘i’, ‘j’, ‘k’,’l’ 키로 웹캠 브라켓 관절 조종

웹캠 브라켓 관절은 2-DOF로 상하, 좌우로만 관절을 움직일 수 있다. 이를 조종하기 위해 상, 하, 좌, 우 를 각각 키보드 ‘i’, ‘k’, ‘j’,’l’ 키에 대응시키기로 했다. 파이썬 코드는 변동사항이 없고, index.html만 잘 수정해주면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Control Panel</title>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <style>
        #directionDisplay {
            font-size: 24px;
            color: blue;
        }
        #directionText {
            font-weight: bold;
        }
        #speedValue {
            font-size: 20px;
        }
        #speedControl {
            width: 300px;
            height: 20px;
        }
        .spacer {
            margin-top: 20px;
        }
    </style>
</head>
<body>
    <h1>Raspberry Pi WiFi Controlled Car: Control Panel</h1>
    <div id="directionDisplay">Direction: <span id="directionText"></span></div>
    <div class="spacer"></div>
    <div>
        <label for="speedControl">Speed: <span id="speedValue">255</span></label>
        <input type="range" id="speedControl" min="0" max="255" value="255">
    </div>
    <script>
        $(document).ready(function() {
            var directionText = $('#directionText');
            var direction = '';
            var speed = $('#speedControl').val();
    
            $('#speedControl').on('input', function() {
                speed = $(this).val();
                $('#speedValue').text(speed);
                sendDirection(direction, speed);
            });

            $(document).on('keydown', function(e) {
                switch (e.keyCode) {
                    case 37: // 좌 화살표
                        direction = 'Turn left';
                        break;
                    case 38: // 상 화살표
                        direction = 'Forward';
                        break;
                    case 39: // 우 화살표
                        direction = 'Turn right';
                        break;
                    case 40: // 하 화살표
                        direction = 'Backward';
                        break;
                    case 74: // 'j' 키
                        direction = 'Servo turn left';
                        break;
                    case 76: // 'l' 키
                        direction = 'Servo turn right';
                        break;
                    case 73: // 'i' 키
                        direction = 'Servo up';
                        break;
                    case 75: // 'k' 키
                        direction = 'Servo down';
                        break;
                }
                sendDirection(direction, speed);
            });
    
            $(document).on('keyup', function(e) {
                if ((e.keyCode >= 37 && e.keyCode <= 40) || [74, 76, 73, 75].includes(e.keyCode)) {
                    direction = 'Stop';
                    sendDirection(direction, speed);
                }
            });
    
            function sendDirection(direction, speed) {
                $.ajax({
                    url: '/move',
                    type: 'POST',
                    data: {direction: direction, speed: speed},
                    success: function(response) {
                        console.log('Direction:', direction, 'Speed:', speed);
                        directionText.text(direction);
                    }
                });
            }
        });
    </script>    
</body>
</html>

코드를 실행하면, i, j, k, l을 눌렀을 때 Servo에 관련된 문자열이 뜰 것이다.

3. 실시간 웹캠 스트리밍

라즈베리파이로 웹캠 스트리밍을 하는 방법은 여러 가지가 있다. 우리는 파이썬으로 코딩하고 있으므로 FlaskOpenCV를 이용해 웹캠 스트리밍을 구현하려고 한다.

사실 우리는 라즈베리파이에서 motion 프로그램을 이용해 웹캠 스트리밍을 하려고 했었다.

라즈베리파이 motion을 이용한 웹캠 스트리밍

motion 사용법을 공부하고 웹캠 스트리밍 구현을 마친 뒤에 계획을 잘못 설정했다는 것을 깨달았다.

motion을 사용하지 못한 이유

Motion 소프트웨어를 사용하여 웹켐 스트리밍에 성공하였다. 하지만 굉장히 큰 문제가 있었다. Flask를 이용해 제작한 웹 서버의 로컬 서버와 motion을 이용해 스트리밍하는 서버의 주소(URL)가 동일한 것이다. 이 부분을 전혀 예상하지 못했는데 두 서버 모두 접속해야 스트리밍 화면은 보고 제어가 가능한데 둘 중 하나만 접속 가능한 것이다. 그래서 우리는 과감히 Motion을 버리고 openCV으로 python을 이용해 웹 서버에 제어 가능한 부분과 스트리밍 화면을 동시에 띄울 수 있도록 하였다.

웹캠 스트리밍 구현 방법은 OpenCV, Flask를 이용한 웹캠 스트리밍에서 확인할 수 있다.


라즈베리파이 제어와 웹서버 연동

이제 RC카 컨트롤 패널 웹 페이지를 만들었으니, 실제 모터가 구동되도록 코드를 추가해주면 된다. 라즈베리파이에서 DC 모터와 서보모터를 사용하는 방법에 대해서는 아래 글에 작성해두었다. 웹 페이지로부터 특정 문자열이 왔을 때, 위에서는 단순히 터미널에 내용을 출력했지만 아래 글을 참고해서 해당하는 작업을 수행하도록 코딩하면 된다.

라즈베리파이 DC모터 제어

라즈베리파이 서보모터 제어

아래는 파이썬 코드의 일부이다. DC 모터와 서보모터 제어를 위한 코드가 추가되어 윗부분이 너무 길어져 Flask 관련 코드만 가져왔다. 전체 코드는 아래 링크에서 확인할 수 있다.

main_without_arduino.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# Flask setup
app = Flask(__name__)

current_direction = None
current_speed = None

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/move', methods=['POST'])
def move():
    global current_direction, current_speed
    global angle1, angle2
    global spd
    direction = request.form.get('direction')
    speed = int(request.form.get('speed', 255)) 
    spd = int(speed / 255.0 * 100)
    if direction :
        current_direction = direction
        print(f'Direction: {direction}')

    if speed != current_speed:
        current_speed = speed
        print(f'Speed: {speed}')

    if direction == 'Forward':
        forward(spd)
    elif direction == 'Backward':
        backward(spd)
    elif direction == 'Turn left':
        left_turn(spd)
    elif direction == 'Turn right':
        right_turn(spd)
    elif direction == 'Servo up':
        angle1 += 5
        angle1 = constrain(angle1)
        setServo1Pos(angle1)
    elif direction == 'Servo down':
        angle1 -= 5
        angle1 = constrain(angle1)
        setServo1Pos(angle1)
    elif direction == 'Servo turn left':
        angle2 += 5
        angle2 = constrain(angle2)
        setServo2Pos(angle2)
    elif direction == 'Servo turn right':
        angle2 -= 5
        angle2 = constrain(angle2)
        setServo2Pos(angle2)
    elif direction == 'Stop':
        stop()

    return 'OK'

if __name__ == '__main__':
    try:
        app.run(host='0.0.0.0', port=80)
    finally:
        GPIO.cleanup()

index.html은 변동 사항이 없다.(웹캠 스트리밍 기능 없는 버전)

이제 코드를 라즈베리파이에서 실행하고, 로컬 서버에 접속하여 테스트하여 잘 작동되는 것을 확인할 수 있다.


서보모터 떨림 해결을 위한 아두이노 연동

지금까진 라즈베리파이에서 DC 모터와 서보모터를 모두 제어하였다. 실제로 작동해보니 한 가지 문제점이 있었다. 서보모터 조종 키를 누르지 않아도 서보모터가 계속 떨리는 현상이었다. 서보모터 떨림에 대해 찾아보니 이를 해결하기 위한 라이브러리도 있었다. (아래 블로그 참고)

우리도 pigpio를 다운받아 시도해봤지만, 이유는 모르겠지만 상황이 개선되진 않았다. 다른 사람들은 대부분 잘 된다는데.. 그래서 서보모터는 아두이노로 제어해야겠다고 생각해서 아두이노를 추가하기로 하였다. 메인 파이썬 코드는 라즈베리파이에서 실행하고 있으니, 서보모터는 아두이노를 라즈베리파이와 시리얼 통신시켜 제어하면 된다.

라즈베리파이와 아두이노 간의 유선 통신 방법은 여러 가지가 있는데 우리는 usb 케이블로 연결하고 pyserial 라이브러리를 이용해 통신하기로 하였다. 기본적인 사용법은 아래 글에 작성했다.

pyserial을 이용한 아두이노와 라즈베리파이 시리얼 통신

아두이노 연결 이후 파이썬 코드에서 서보모터 관련 부분은 다 없애고, 시리얼 통신으로 아두이노에 문자를 보내는 코드로 대체하였다. 아두이노에서는 특정 문자에 따라 서보모터를 제어하는 코드를 업로드하였다.

자세한 코드는 아래에서 확인할 수 있다.

serial_example.ino

main_without_webcam.py

아두이노로 서보모터를 제어하는 걸로 바꾼 결과, 웹 페이지에서 i, j, k, l 키를 눌렀을 때 서보모터의 떨림 없이 잘 제어되는 것을 확인할 수 있었다. 물론 100% 떨림이 사라진 것은 아니다.

서보모터 떨림 현상 해결에 대한 자세한 내용은 나중에 따로 공부해볼만 한 것 같다. 이런 현상을 서보모터 지터, Jittering(지터링) 이라고 한다.

참고: 【아두이노에러해결#4】 서보모터 떨림, 흔들림, 불안정 현상 해결!


웹캠 스트리밍 기능 합치기

지금까지 웹캠 스트리밍 빼고 RC카의 라즈베리파이에서 모두 구현하였다. 3. 실시간 웹캠 스트리밍에서 구현한 것을 라즈베리파이에 합치기만 하면 된다. 합친 최종 파이썬 코드와 index.html은 아래에서 확인할 수 있다.

main_final

이전에 만들었던 구동 컨트롤 패널을 스마트폰에서도 사용할 수 있도록 버튼을 추가하고, 전체적으로 배치와 슬라이더 디자인을 수정하였다. 이 부분은 8기 박하준이 구현하였다.

project1_4.png

다음은 이에 대한 설명이다:

작성자: 8기 박하준

웹 서버 모바일 터치 기능 구현

모바일 환경에서도 RC카를 원활히 제어할 수 있도록 터치 이벤트를 처리하는 코드를 추가했다. 아래 작성한 코드는 JavaScript와 jQuery를 사용하여 터치 이벤트를 처리한다.

JavaScript를 이용한 모바일 터치 이벤트 처리

다음 코드는 모바일 터치 이벤트를 처리하여 RC카를 제어하는 기능을 구현한다. 각 방향 버튼에 대해 터치 이벤트를 감지하고, 해당 방향에 맞는 키보드 이벤트를 트리거한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 모바일 터치 이벤트 처리
$('.arrow-key').on('mousedown touchstart', function() {
    var arrowId = $(this).attr('id');
    var keyCode;
    switch (arrowId) {
        case 'arrowUp':
            keyCode = 38;
            break;
        case 'arrowDown':
            keyCode = 40;
            break;
        case 'arrowLeft':
            keyCode = 37;
            break;
        case 'arrowRight':
            keyCode = 39;
            break;
    }
    if (keyCode) {
        $(document).trigger($.Event('keydown', { keyCode: keyCode }));
    }
});

$('.arrow-key').on('mouseup touchend', function() {
    var arrowId = $(this).attr('id');
    var keyCode;
    switch (arrowId) {
        case 'arrowUp':
            keyCode = 38;
            break;
        case 'arrowDown':
            keyCode = 40;
            break;
        case 'arrowLeft':
            keyCode = 37;
            break;
        case 'arrowRight':
            keyCode = 39;
            break;
    }
    if (keyCode) {
        $(document).trigger($.Event('keyup', { keyCode: keyCode }));
    }
});


프로젝트 최종 결과

project1_5.png

RC카를 다 만들고 학교 1층에서 조종해보았다. 양쪽 DC 모터의 속도를 PID 제어같이 피드백 제어해주지 않아서 앞으로 갈 때 옆으로 휘는 경향이 있으나 생각보다 조종이 잘 되어서 만족스러웠다.

Youtube link


그러나 완벽하진 않았다 . . .

시간 지연 문제

라즈베리파이가 내 아이폰의 핫스팟에 연결하고, 내 Mac은 학교 WiFi에 연결하여 조종해보았다. 내 아이폰은 RC카에 고정시켰다. 이렇게 하면 학교의 모든 곳으로 조종해서 도달할 수 있다고 생각했다. WiFi 연결만 되면 가능한 통신이니까.

테스트 하면서 잘 된다고 생각해서 엄청 멀리까지 조종해보려는데, 문제가 생겼다. 웹캠 스트리밍의 시간 지연 때문에 실제로 RC카를 보면서 조종하는 것이 아니라면 조종이 불가능할 정도였다. 스트리밍 시간 지연 문제를 해결하기 위해 원인과 해결방법 등 많은 것들을 찾아보았는데, 가장 큰 이유는 OpenCVNgrok를 사용했다는 것이라고 생각한다.

OpenCV와 시간 지연 문제

OpenCV는 이미지와 비디오 처리에 매우 강력한 라이브러리지만, 실시간 스트리밍에는 몇 가지 제한이 있다. OpenCV한 프레임씩 데이터를 전송하기 때문에 시간 지연이 걸리기 마련이다. OpenCV는 주로 이미지 처리와 컴퓨터 비전 작업을 위해 설계되었기 때문에 비디오 스트리밍에 최적화되어 있지 않다. 따라서 프레임 전송 간에 지연이 발생하여 실시간 조종이 어렵게 만드는 원인이 된 것이다.

이 문제를 해결하기 위해 다른 사람들의 사례를 찾아보다가 WebRTC(Web Real-Time Communication)라는 기술을 알게 되었다. WebRTC는 웹 애플리케이션과 사이트가 직접 브라우저 간에 오디오, 비디오, 데이터 스트리밍을 할 수 있도록 하는 기술이다. 구글 Meet, Zoom, Facebook 등에서 쓰이고 있다. WebRTC는 다음과 같은 주요 장점이 있다:

  • 낮은 지연 시간: WebRTC는 실시간 통신을 위해 설계되었기 때문에, 매우 낮은 지연 시간을 제공한다. 이는 RC카와 같은 실시간 조종이 필요한 애플리케이션에 적합하다.

  • P2P 연결: WebRTC는 브라우저 간의 P2P(피어 투 피어) 연결을 통해 데이터 전송을 수행한다. 이를 통해 중간 서버를 거치지 않고 직접 연결하여 지연 시간을 최소화할 수 있다.

  • 강력한 네트워크 최적화: WebRTC는 네트워크 상태에 따라 자동으로 비트레이트를 조절하고, 패킷 손실 복구 메커니즘을 내장하고 있어 안정적인 스트리밍을 지원한다.

WebRTC는 프레임을 한 번에 전송하는 대신, 지속적인 스트림을 유지하여 실시간 비디오 피드를 제공한다. 그리고 낮은 지연 시간 덕분에 RC카의 시점을 즉각적으로 확인할 수 있다.

Ngrok의 시간 지연 요인

ngrok를 사용한 것도 시간 지연에 영향을 끼쳤을 가능성이 있다. ngrok는 로컬 서버를 인터넷에서 접근할 수 있도록 터널링 서비스를 제공하지만, 이러한 터널링 과정에서 추가적인 지연이 발생할 수 있다. 특히 다음과 같은 요인들이 시간 지연에 기여할 수 있다:

  • 중계 서버: ngrok는 로컬 서버와 외부 클라이언트 간의 데이터를 중계 서버를 통해 전달한다. 이 과정에서 데이터가 추가적으로 왕복하는 시간이 필요하게 된다.

  • 네트워크 대역폭 및 혼잡도: ngrok의 서버가 위치한 데이터 센터의 네트워크 상태와 혼잡도에 따라 지연 시간이 달라질 수 있습니다. 그리고 ngrok의 무료 플랜을 사용할 경우 대역폭 제한이나 서버 혼잡도가 더 높을 수 있다.

  • 암호화 및 복호화: ngrok는 보안된 연결을 제공하기 위해 데이터 암호화 및 복호화 과정을 거친다. 이 과정 역시 미세하지만 추가적인 지연을 발생시킬 수 있다.

WebRTC와 Ngrok 비교

WebRTC는 P2P 연결을 통해 직접 브라우저 간의 데이터를 전송하기 때문에 중간 서버를 거치지 않는다. 이로 인해 지연 시간이 최소화되며, ngrok와 같은 중계 서비스를 사용할 필요가 없다. WebRTC를 사용하면 네트워크 상태에 따라 자동으로 비트레이트를 조절하고 패킷 손실 복구 메커니즘을 통해 안정적인 스트리밍을 지원하므로, ngrok보다 더 낮은 지연 시간을 기대할 수 있다.

실시간 스트리밍이나 조종과 같이 낮은 지연 시간이 필요한 경우에는 WebRTC와 같은 P2P 기반의 실시간 통신 기술을 사용해야 한다는 것을 알게 되었다. WebRTC를 통해 직접적인 브라우저 간 연결을 사용하면 ngrok를 통해 발생하는 추가적인 지연 시간을 피할 수 있습니다.

애초에 Ngrok를 쓰지 않고 WebRTC로 스트리밍을 했어야 했다. 한 프레임씩 전송하는 OpenCV와 중계 서버를 사용하는 터널링 프로그램인 Ngrok를 동시에 사용해서 시간 지연을 일으킬만한 요인들만 모아서 쓴 것이다.


마무리하며

추가 아이디어

1. 조이스틱 컨트롤러

현재 조종 방식은 키보드이다. 조이스틱 컨트롤러(조이 패드)를 연동시켜서 좀 더 높은 차원의 조종이 가능하도록 하면 좋을 것 같다.

2. FPV or VR

현재 웹캠 스트리밍을 웹 페이지에 송출하고 있다. FPV 드론이나 VR 기기 처럼 사용자가 고글 형태로 껴서 FPV RC카로 변형하면 더 재미있을 것이다.



질문이나 조언은 댓글 또는 메일로 부탁드립니다! 🙇

This post is licensed under CC BY 4.0 by the author.