리눅스와 안드로이드 간 와이파이 다이렉트 통신 설정 방법

2017년 8월 20일 코코넛냠냠


목차

1. 개요

2. 사용 환경 (PC)

3. 기본 명령어 사용법 (과정)

4. 와이파이 다이렉트 설정 스크립트 작성

5. 소켓 통신 프로그램 작성


1. 개요

와이파이 다이렉트는 Wi-Fi P2P라는 이름으로도 불리며 와이파이(무선랜)가 AP를 거치지 않고 기기간 직접 접속하여 데이터를 주고 받을 수 있도록 하는 기술입니다.

리눅스 머신으로 작동하는 PC가 다른 단말기와 와이파이 다이렉트로 통신하기 위해서 필요한 설정 방법을 알아보고 PC와 안드로이드 단말기 간 데이터를 주고 받는 프로그램을 작성합니다.


2. 사용 환경 (PC)

- 가상머신 기반 (VMware Workstation 12)

- 운영체제: ubuntu 16.04 LTS 64-bit

- 메모리: 6.0 GB

- 처리기: Intel Core i5-6300HQ CPU @ 2.30GHz x 4

- 무선랜 칩셋: Atheros AR9002U

- 무선랜 드라이버: ath9k



3. 기본 명령어 사용법 (과정)

사용 환경마다 약간씩의 차이는 있지만 기본적으로 리눅스에서 무선랜의 제어는 wpa_supplicantwpa_cli라는 툴을 이용합니다. 해당 툴을 이용한 와이파이 다이렉트 명령어의 수행 과정입니다. 리눅스 PC를 그룹 오너(서버)로, 안드로이드 단말기를 클라이언트로 정하고 진행하였습니다. 자세한 명령어 사용법은 아래 두 문서를 참고하면 좋습니다. http://processors.wiki.ti.com/index.php/OMAP_Wireless_Connectivity_NLCP_WiFi_Direct_Configuration_Scripts

http://processors.wiki.ti.com/index.php/WiFi_Direct_Configuration_Scripts


- 우선 와이파이 다이렉트를 사용하기 위해서 PC에 무선랜이 연결되어 있어야 합니다. ifconfig 명령어로 무선랜 인터페이스의 활성화 여부를 확인할 수 있습니다. 무선랜 인터페이스의 이름은 wlan으로 시작하고 wlan0, wlan1, wlan2과 같이 연결된 순서대로 번호 매겨져 있습니다. 이제 wlan0의 상태를 확인했습니다.


$ wpa_cli -i wlan0 terminate -B

- wlan0 인터페이스를 와이파이 다이렉트 용도로 실행할 수 있도록 비활성화하는 명령어입니다.





$ sudo wpa_supplicant -d -Dnl80211 -c /etc/wpa_supplicant.conf -iwlan0 -B

- /etc/wpa_supplicant.conf 파일에 적힌 설정으로 새롭게 wlan0에 대한 wpa_supplicant를 시작합니다.

- /etc/wpa_supplicant.conf 파일은 임의로 작성한 wpa_supplicant의 설정 파일입니다. 각 설정 옵션들에 대한 자세한 설명은 https://w1.fi/cgit/hostap/plain/wpa_supplicant/wpa_supplicant.conf에 기술되어 있습니다.

- 사용한 /etc/wpa_supplicant.conf 설정 파일의 내용은 아래와 같습니다.

ctrl_interface=/var/run/wpa_supplicant
update_config=1


$ sudo wpa_cli -i wlan0 p2p_group_add

- 이 명령어를 수행하면 p2p-wlan0-0 인터페이스가 새로 생성됩니다. 다음과 같이 ifconfig 명령어로 확인할 수 있습니다.


$ sudo ifconfig p2p-wlan0-0 192.168.1.2

- 이런 식으로 그룹 오너(서버)가 되는 PC 자신의 스태틱 IP 주소를 할당할 수도 있습니다. (반면 클라이언트의 IP 주소는 반드시 동적으로 할당해야 합니다.) 그 뒤 마찬가지로 ifconfig 의 명령어로 결과를 확인할 수 있습니다.


$ sudo wpa_cli

- 와이파이 다이렉트의 인터페이스가 설정된 뒤에는 해당 인터페이스에 대한 와이파이 다이렉트 명령어를 입력 받을 수 있도록 WPA Supplicant CLI를 실행합니다.

- wpa_cli에서 주로 사용하는 명령어는 p2p_find, p2p_peers, wps_pbc, p2p_connect의 네 가지가 있습니다.


> p2p_find

- 검색을 수행하여 주변의 디바이스를 검색중인 또다른 와이파이 다이렉트 단말기들에게 노출될 수 있게 하는 명령어입니다. 작업이 성공하면 CLI가 OK를 출력합니다. (건너뛰어도 됩니다.)


> p2p_peers

- 와이파이 다이렉트 디바이스의 검색으로 발견된 주변의 모든 단말기들을 피어(Peer)라고 부릅니다. 발견한 모든 피어들의 맥 주소를 나열합니다. (건너뛸 수 있습니다.)


> wps_pbc
- 와이파이 다이렉트로 두 단말기가 서로 연결할 때 사용자 인증을 거치는데 비밀번호를 입력하는 PIN 방식과 버튼을 눌러서 인증하는 PBC 방식이 있습니다. wps_pbc는 이 때의 PBC 인증 요청을 승인하는 명령어입니다. 명령어가 성공적으로 실행하면 CLI가 OK와 함께 <3>WPS-PBC-ACTIVE 메시지를 출력하고 이 때 PBC 요청이 들어오면 수락으로 응답합니다.


- 다른 단말기가 와이파이 다이렉트 연결 요청을 보내어 연결이 수락되면 CLI에서 위와 같은 메시지를 출력합니다.


- 와이파이 다이렉트 연결이 끊어진 경우의 메시지입니다.


4. 와이파이 다이렉트 설정 스크립트 작성


1) os.system() 함수를 이용하여 위와 같은 쉘 명령어를 파이썬으로 실행할 수 있습니다.


import os
import time

if __name__ == "__main__":
    os.system('sudo killall udhcpd')
    os.system('sudo wpa_cli -i wlan0 terminate -B')
#    os.system('sudo wpa_cli -i p2p-wlan0-0 terminate -B')
    time.sleep(1)
    os.system('echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward')
#    os.system('echo "ctrl_interface=/var/run/wpa_supplicant\nupdate_config=1" | sudo tee /etc/wpa_supplicant.conf')
    os.system('sudo wpa_supplicant -d -Dnl80211 -c /etc/wpa_supplicant.conf -iwlan0 -B')
    os.system('sudo wpa_cli -iwlan0 p2p_group_add')
    os.system('sudo ifconfig p2p-wlan0-0 192.168.1.2')
    os.system('sudo wpa_cli -i p2p-wlan0-0 p2p_find')
    os.system('sudo wpa_cli -ip2p-wlan0-0 p2p_peers')
    os.system('sudo wpa_cli -ip2p-wlan0-0 wps_pbc')
    os.system('sudo udhcpd /etc/udhcpd.conf &')



2) /etc/wpa_supplicant.conf 설정

자신 와이파이 다이렉트 디바이스(인터페이스)의 정보나 연결시의 설정 따위가 정의되어 있는 파일입니다. 자세한 정보는 https://w1.fi/cgit/hostap/plain/wpa_supplicant/wpa_supplicant.conf를 참고하면 좋습니다.


ctrl_interface=/var/run/wpa_supplicant
update_config=1

ap_scan=1

device_name=hello
device_type=1-0050F204-1

# If you need to modify the group owner intent, 0-15, the higher
# number indicates preference to become the GO. You can also set
# this on p2p_connect commands.
p2p_go_intent=15

# optional, can be useful for monitoring, forces
# wpa_supplicant to use only channel 1 rather than
# 1, 6 and 11:
p2p_listen_reg_class=81
p2p_listen_channel=1
p2p_oper_reg_class=81
p2p_oper_channel=1


3) /etc/udhcpd.conf 설정

udhcpd는 와이파이 다이렉트 클라이언트의 IP 주소를 동적으로 할당해 주기 위해서 사용하는 툴입니다. IP 주소를 할당하는 udhcpd가 없으면 안드로이드 단말기와 연결을 시도하더라도 연결이 완전하게 이루어지지 못합니다. (좌측 상단에 와이파이 다이렉트 연결 아이콘은 나타나지만 IP 주소가 할당되지 못하고 WIFI_P2P_CONNECTION_CHANGED_ACTION 브로드캐스트가 발생하지 않습니다.) 그룹 오너인 PC가 클라이언트의 주소를 할당하는 라우터가 되므로 올바르게 작동하기 위해서 와이파이 다이렉트 인터페이스 p2p-wlan0-0의 IP 주소가 반드시 opt router 항목의 IP 주소와 일치해야 합니다. 또한 /proc/sys/net/ipv4/ip_forward의 값이 1이어야 합니다.


# Sample udhcpd configuration file (/etc/udhcpd.conf)
# The start and end of the IP lease block
start 		192.168.1.20	#default: 192.168.0.20
end		192.168.1.254	#default: 192.168.0.254
# The interface that udhcpd will use
interface   p2p-wlan0-0		#default: eth0
#Examles
opt	dns	8.8.8.8  8.8.4.4 # public google dns servers
option	subnet	255.255.255.0
opt	router	192.168.1.2
option	lease	864000		# 10 days of


4) 수행




5. 소켓 통신 프로그램 작성

소켓 통신 기법으로 간단한 문자열 메시지를 보내는 프로그램을 작성합니다. 디바이스끼리 와이파이 다이렉트로 서로 연결된 후에 프로그램을 실행합니다.


1) 파이썬 (PC 리눅스)


import os
import time

from socket import *

if __name__ == "__main__":
    os.system('sudo killall udhcpd')
    os.system('sudo wpa_cli -i wlan0 terminate -B')
#    os.system('sudo wpa_cli -i p2p-wlan0-0 terminate -B')
    time.sleep(1)
    os.system('echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward')
#    os.system('echo "ctrl_interface=/var/run/wpa_supplicant\nupdate_config=1" | sudo tee /etc/wpa_supplicant.conf')
    os.system('sudo wpa_supplicant -d -Dnl80211 -c /etc/wpa_supplicant.conf -iwlan0 -B')
    os.system('sudo wpa_cli -iwlan0 p2p_group_add')
    os.system('sudo ifconfig p2p-wlan0-0 192.168.1.2')
    os.system('sudo wpa_cli -i p2p-wlan0-0 p2p_find')
    os.system('sudo wpa_cli -ip2p-wlan0-0 p2p_peers')
    os.system('sudo wpa_cli -ip2p-wlan0-0 wps_pbc')
    os.system('sudo udhcpd /etc/udhcpd.conf &')

    # 소켓 생성
    server = socket(AF_INET, SOCK_STREAM)
    server.bind(('', 6278))
    server.listen(1)
    print('listen...')
    client, addrClient = server.accept()
    print('connected to ', addrClient)

    # 읽어들인다
    msg = client.recv(1024)
    while not msg or msg.__eq__('bye'):
        msg = str(msg).split("b'", 1)[1].rsplit("'",1)[0]
        #msg = str(msg).decode("utf-8", "ignore")
        print(msg)
        msg = client.recv(1024)

    # 종료
    client.close()
    server.close()


2) 자바 (안드로이드)


<LinearLayout
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <EditText
        android:id="@+id/editChat"
        android:text="hi"
        android:layout_height="wrap_content"
        android:layout_width="match_parent"
        android:layout_weight="1"/>
    <Button
        android:id="@+id/btnChat"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Send"/>
</LinearLayout>


// 메시지 보내기
((Button)findViewById(R.id.btnChat)).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        new Thread() {
            @Override
            public void run() {
                super.run();
                try {
                    // 소켓과 소켓에 대한 출력 스트림 생성
                    if (mSocket == null) {
                        mSocket = new Socket("192.168.1.2", 6278);
                        mOut = new PrintWriter(new OutputStreamWriter(mSocket.getOutputStream()));
                    }
                    // 메시지 보내기
                    String msg = ((EditText)findViewById(R.id.editChat)).getText().toString();
                    mOut.println(msg);
                    mOut.flush();
                    // 종료
                    if (msg.equals("bye")) {
                        mOut.close();
                        mSocket.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }.start();
    } // onClick()
}); // findViewById


3) 수행



Top