이글의 전부 또는 일부, 사진, 소스프로그램 등은 저작자의 동의 없이는 상업적인 사용을 금지합니다. 또한, 비상업적인 목적이라하더라도 출처를 밝히지 않고 게시하는 것은 금지합니다.

이번 글에서는 지난 글 STM32F103으로 ESP8266을 이용한 소켓 통신하기 -제1편STM32F103으로 ESP8266을 이용한 소켓 통신하기 -제2편에서 STM32F103CBT6이 서버로 동작하도록 프로그래밍했습니다. 이어서 STM32F103으로 ESP8266을 이용한 소켓 통신하기 -제3편에서는 windows 10에서 동작하는 client 프로그램을 만들었고, STM32F103으로 ESP8266을 이용한 소켓 통신하기 -제4편 안드로이드 스튜디오에서 안드로이드 앱을 만드는 과정을 살펴보았습니다. 이번 글에는 안드로이드 앱의 구체적인 코드를 올립니다. 앞 글에서 언급한대로 필자가 java와 안드로이드 프로그래밍은 초보적 수준에 불과하므로 동작 여부 확인하는 수준에 불과하다고 생각합니다.

 

프로그래밍 환경은 다음과 같습니다.

① 개발 환경 : android Studio 3.5.3

② 프로그래밍 언어 : java

 

본 글의 중심이 되는 MainActivity.java의 코드는 인터넷 어느 사이트에서 얻어와서 나름대로 재작성한 것인데, 원글이 있는 사이트를 다시 찾지 못하여 출처를 링크하지 못하는 점 양해하여 주시기 바랍니다.

 

비교적 내용이 간단한 AndroidManifest.xml과 activity_main.xml를 먼저 다루고, 맨 끝으로 MainActivity.java를 다루겠습니다.

 

1. AndroidManifest.xml

 

AndroidManifest.xml에는 Android 빌드 도구, Android 운영체제 및 Google Play에 앱에 관한 필수 정보가 있있어야 합니다. 자세한 내용은 https://developer.android.com/guide/topics/manifest/manifest-intro?hl=ko을 참조하시기 바랍니다.

 

본 프로그램에서는 wifi를 사용할 것이므로 작성한 앱이 인터넷을 이용할 수 있도록 다음 코드의 20행을 추가합니다.

 

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.jsoft.esp8266android">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
    <uses-permission android:name="android.permission.INTERNET" />
</manifest>

 

 

2. activity_main.xml

 

activity_main.xml은 앱의 기본 화면 디자인을 담고 있습니다. 다음은 activity_main.xml의 내용입니다.

 

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >

    <TextView
        android:id="@+id/ip"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Server" />

    <EditText
        android:id="@+id/edt_ip"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignTop="@+id/ip"
        android:text="www.domainname.com"
        android:selectAllOnFocus="true"
        android:layout_toRightOf="@+id/port"/>

    <TextView
        android:id="@+id/port"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/edt_ip"
        android:layout_alignLeft="@+id/ip"
        android:text="PORT" />

    <EditText
        android:id="@+id/edt_port"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignTop="@+id/port"
        android:text="15598"
        android:selectAllOnFocus="true"
        android:layout_alignLeft="@+id/edt_ip"/>

    <Button
        android:id="@+id/btn_connect"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/edt_port"
        android:enabled="true"
        android:text="Connect" />

    <Button
        android:id="@+id/btn_disconnect"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@+id/btn_connect"
        android:layout_alignBottom="@+id/btn_connect"
        android:enabled="false"
        android:text="Disconnect" />

    <Button
        android:id="@+id/btn_send"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@+id/btn_disconnect"
        android:layout_alignBottom="@+id/btn_connect"
        android:enabled="false"
        android:text="Send" />

    <Button
        android:id="@+id/btn_exit"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@+id/btn_send"
        android:layout_alignBottom="@+id/btn_connect"
        android:enabled="true"
        android:text="Exit" />

    <TextView
        android:id="@+id/send"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/btn_connect"
        android:selectAllOnFocus="true"
        android:layout_alignParentLeft="true"
        android:text="Send" />

    <EditText
        android:id="@+id/edt_send"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignTop="@+id/send"
        android:selectAllOnFocus="true"
        android:layout_toRightOf="@+id/send"/>

    <TextView
        android:id="@+id/server"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/edt_send"
        android:selectAllOnFocus="true"
        android:layout_alignParentLeft="true"
        android:text="" />
</RelativeLayout>

 

주 내용은 다음과 같습니다.

 

① ip라는 이름은 가진 TextView를 만들고 화면에 Server로 표시함(7행~11행).

② edt_ip라는 이름은 가진 EditText를 만들고 화면에 "www.domainname.com"을 표시함(13행~20행). 앞 글에서 만든 STM32F103과 통신하려면 이 값을 STM32F103의 ip(ex:172.30.1.99)로 지정하는 것이 편리할 듯합니다. 본인은 외부에서 스마트폰으로 쉽게 접속하도록 하기 위해서 도메인명을 기본값으로 넣었습니다.

③ port라는 이름은 가진 TextView를 만들고 화면에 PORT로 표시함(22행~28행).

④ edt_port라는 이름은 가진 EditText를 만들고 화면에 15598로 표시함(30행~37행). 앞 글에서 만든 STM32F103이 "15598" 포트를 사용하기 때문에 이값을 기본값으로 넣었습니다.

⑤ btn_connect라는 이름은 가진 Button을 만들고 화면에 Connect로 표시함(39행~45행).

⑥ btn_disconnect라는 이름은 가진 Button을 만들고 화면에 Disconnect로 표시함(47행~54행). 이 버튼은 앱 시작시 비활성화되도록 지정함.(53행)

⑦ btn_send라는 이름은 가진 Button을 만들고 화면에 Send로 표시함(50행~63행). 이 버튼은 앱 시작시 비활성화되도록 지정함.(62행)

⑧ btn_exit라는 이름은 가진 Button을 만들고 화면에 Exit로 표시함(65행~78행).

⑨ send라는 이름은 가진 TextView를 만들고 화면에 Send로 표시함(74행~81행).

⑩ edt_send라는 이름은 가진 EditText를 만듦(83행~89행).

⑪ server라는 이름은 가진 TextView를 만듦(91행~98행).

 

 

3. MainActivity.java

 

다음은 MainActivity.java의 내용입니다. 이 소스 프로그램의 1행은 본인의 프로그램에 적합한 것이므로 각자의 프로그램에 생성된 코드를 그대로 두어야 합니다.

 

package com.jsoft.esp8266android;

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import android.view.View;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.Arrays;

import static java.lang.Integer.parseInt;

public class MainActivity extends AppCompatActivity {
    private static final int MSG_CONNECT = 0;
    private static final int MSG_DISCONNECT = 1;
    private static final int MSG_SEND = 2;
    private static final int MSG_EXIT = 3;
    private static final int MSG_SERVER_STOP = 4;
    private static final int MSG_ERROR = 5;
    private static final String TAG = "ESP8266Socket";
    private final static int MAX_MESSAGE_LENGTH = 100;

    public class ClientThread extends Thread {
        Boolean loop = false;
        String strIP, strRead;
        int nPort;
        private final int connection_timeout = 3000;

        public ClientThread(String ip, int port) throws RuntimeException {
            strIP = ip;
            nPort = port;
        }

        @Override
        public void run() {
            try {
                socket = new Socket(strIP, nPort);

                socket.setSoTimeout(connection_timeout);
                //read 메서드가 connection_timeout 시간동안 응답을 기다린다.
                socket.setSoLinger(true, connection_timeout);
                //서버와의 정상 종료를 위해서 connection_timeout 시간동안 close 호출 후 기다린다.
                socket.setSoTimeout(connection_timeout);

                if(!socket.getKeepAlive()) socket.setKeepAlive(true);
                socket.setKeepAlive(true);

                networkWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
                networkReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));

                Message toMain = mMainHandler.obtainMessage();
                toMain.what = MSG_CONNECT;
                mMainHandler.sendMessage(toMain);
                loop = true;
            } catch (Exception e) {
                loop = false;
                Message toMain = mMainHandler.obtainMessage();
                toMain.what = MSG_ERROR;
                toMain.obj = "Fail to create socket.";
                mMainHandler.sendMessage(toMain);
            }

            while (loop) {
                try {
                    Arrays.fill(receiveArr,(char)0);
                    //networkReader.read(receiveArr);
                    //strRead = new String(receiveArr);
                    strRead = networkReader.readLine();
                    //readLine()은 블록모드로 작동하기 때문에 별도의 스레드에서 실행한다.
                    if (strRead == null) //서버에서 FIN 패킷을 보내면 null을 반환한다.
                        break;
                    Runnable showUpdate = new Runnable() {
                        @Override
                        public void run() {
                            String str = tv.getText().toString() + "\n" + strRead;
                            tv.setText(str);
                            //tv.setText(strRead);
                        }
                    };

                    mMainHandler.post(showUpdate);
                    //Runnable 객체를 메인 핸들러로 전달해 UI를 변경한다.
                } catch (InterruptedIOException e) {

                } catch (IOException e) {
                    loop = false;
//                    Log.d(TAG, "에러 발생", e);
                    break;
                }
            }

            try  {
                if (networkWriter != null) {
                    networkWriter.close();
                    networkWriter = null;
                }
                if (networkReader != null) {
                    networkReader.close();
                    networkReader = null;
                }
                if (socket != null) {
                    socket.close();
                    socket = null;
                }

                client = null;
                if (loop) {
                    loop = false;
                    Message toMain = mMainHandler.obtainMessage();
                    toMain.what = MSG_SERVER_STOP;
                    toMain.obj = "Disconnected by server.";
                    mMainHandler.sendMessage(toMain);
                }
            } catch(IOException e ) {
                Log.d(TAG, "Error occured.", e);
                Message toMain = mMainHandler.obtainMessage();
                toMain.what = MSG_ERROR;
                toMain.obj = "Fail to close socket.";
                mMainHandler.sendMessage(toMain);
            }
        }

        public void quit() {
            loop = false;
            try {
                if (socket != null) {
                    socket.close();
                    socket = null;

                    Message toMain = mMainHandler.obtainMessage();
                    toMain.what = MSG_DISCONNECT;
                    toMain.obj = "Disconnected.";
                    mMainHandler.sendMessage(toMain);
                }
            } catch (IOException e) {
                Log.d(TAG, "Error occured.", e);
            }
        }
    }

    private final class ServiceHandler extends Handler {
        public ServiceHandler(Looper looper) {
            super(looper);
        }
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_SEND:
                    Message toMain  = mMainHandler.obtainMessage();
                    try {
                        String str = (String) msg.obj;
                        networkWriter.write(str);
                        networkWriter.flush();
                        toMain.what = MSG_SEND;
                    } catch (IOException e) {
                        toMain.what = MSG_ERROR;
                        Log.d(TAG, "Error occured.", e);
                    }
                    toMain.obj = msg.obj;
                    mMainHandler.sendMessage(toMain);
                    break;
                case MSG_EXIT:
                case MSG_DISCONNECT:
                case MSG_SERVER_STOP:
                    client.quit();
                    client = null;
                    break;
            }
        }
    }

    ClientThread client;
    Looper mServiceLooper;
    HandlerThread MainHandlerThread;
    Handler mMainHandler;
    ServiceHandler mServiceHandler;
    Button connect, send, disconnect, exit;
    TextView tv;
    EditText eip, epo, ese;
    Socket socket;
    String ip;
    int port;
    BufferedWriter networkWriter;
    BufferedReader networkReader;
    char receiveArr[] = new char[MAX_MESSAGE_LENGTH];

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        connect = findViewById(R.id.btn_connect);
        disconnect = findViewById(R.id.btn_disconnect);
        send = findViewById(R.id.btn_send);
        exit = findViewById(R.id.btn_exit);
        tv = findViewById(R.id.server);
        eip = findViewById(R.id.edt_ip);
        epo = findViewById(R.id.edt_port);
        ese = findViewById(R.id.edt_send);

        MainHandlerThread = new HandlerThread("HandlerThread");
        MainHandlerThread.start();
        // 루퍼를 만든다.
        mServiceLooper = MainHandlerThread.getLooper();
        mServiceHandler = new ServiceHandler(mServiceLooper);

        mMainHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                String m;
                switch (msg.what) {
                    case MSG_CONNECT:
                        m = "Succeed to connect to server.";
                        tv.setText("");
                        break;

                    case MSG_DISCONNECT:
                        tv.setText((String) msg.obj);
                        m = "Client close the connection.";
                        break;

                    case MSG_SERVER_STOP:
                        tv.setText((String) msg.obj);
                        m = "Server closed the connection.";

                        connect.setEnabled(true);
                        disconnect.setEnabled(false);
                        send.setEnabled(false);
                        exit.setEnabled(true);

                        break;

                    case MSG_SEND:
                        m = "Success to transfer message.";
                        tv.setText((String) msg.obj);
                        break;

                    default:
                        m = "Error occured.";
                        tv.setText(m);
                        break;
                }
                Toast.makeText(MainActivity.this, m, Toast.LENGTH_LONG).show();
                super.handleMessage(msg);
            }
        };

        connect.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View V) {
                ip = eip.getText().toString();
                try {
                    port = parseInt(epo.getText().toString());
                } catch (NumberFormatException e) {
                    port = parseInt(epo.getText().toString());
                    Log.d(TAG, "Port", e);
                }

                if (client == null) {
                    try {
                        client = new ClientThread(ip, port);
                        client.start();
                        connect.setEnabled(false);
                        disconnect.setEnabled(true);
                        send.setEnabled(true);
                        exit.setEnabled(false);
                    } catch (RuntimeException e) {
                        tv.setText("ip or port number is wrong.");
                        Log.d(TAG, "Error occured.", e);
                    }
                }
            }
        });

        disconnect.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View V) {
                if (client != null) {
                    Message msg = mServiceHandler.obtainMessage();
                    msg.what = MSG_DISCONNECT;
                    mServiceHandler.sendMessage(msg);
                    connect.setEnabled(true);
                    disconnect.setEnabled(false);
                    send.setEnabled(false);
                    exit.setEnabled(true);
                }
            }
        });

        send.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View V) {
                if (ese.getText().toString() != null) {
                    Message msg = mServiceHandler.obtainMessage();
                    msg.what = MSG_SEND;
                    msg.obj = ese.getText().toString();
                    mServiceHandler.sendMessage(msg);
                }
            }
        });

        exit.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View V) {
                MainHandlerThread.quit();
                System.exit(0);
            }
        });
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (client != null) {
            Message msg = mServiceHandler.obtainMessage();
            msg.what = MSG_EXIT;
            mServiceHandler.sendMessage(msg);
        }
        MainHandlerThread.quit();
    }
}

 

앱의 실행 루틴인 MainActivity 내부에 Thread 클래스를 확장한 ClientThread를 만들고 있습니다(37행~137행). 이 클래스의 주요 기능은 다음과 같습니다. 이 클래스가 실행되면 서버 strIP의 nPort에 연결하는 socket을 생성합니다(51행). 소켓을 통해 데이터를송신하기 위해서 networkWriter 객체를 만들고(62행), 소켓으로 입력되는 데이터를받기 위해서 networkReader 객체를 만듭니다(63행). 메인 루틴의 메시지 핸들러인 mMainHandler로 MAG_CONNECT 메시지를 보냅니다(67행). 만약 소켓 생성에 실패하면 mMainHandler로 MSG_ERROR 메시지를 보냅니다(74행). 이후로 while문 안에서 소켓에 입력이 있으면, 그 내용을 읽어서(82행) TextView server(tv)에 출력합니다(90행). 이 과정에서 IOException이 발생하면 loop를 false로 하여 while문을 빠져 나옵니다(100행). while문을 빠져 나오면 networkReader, networkWriter을 닫고(108행, 112행) socket을 닫은 후에(116행) mMainHandler로 MSG_SERVER_STOP 메시지를 보냅니다(126행). 이 클래스가 종료될 때에는 quit() 함수가 호출되어 socket을 종료하고(141행) mMainHandler로 MSG_DISCONNECT 메시지를 보냅니다(145행).

 

주 thread는 시작할 때에 필요한 변수 들을 설정하고(203행~219행), ClientThread로부터 넘겨 받은 메시지를 처리하도록 합니다(221행~260행).

 

btn_connect 버튼은 socket이 연결되지 않았을 때에만 활성화 됩니다. 이 버튼을 클릭하면 connect.setOnClickListener() 함수가 실행됩니다. 이 함수에서는 ip(265행)와 port(267행) 값을 읽어와 적절한 형으로 변환한 다음 ClientThread 객체를 생성하고 실행합니다(275, 276행). 이어서 socket이 연결된 상태에서 사용할 수 있는 버튼들을 활성화시키고 사용할 수 없는 버튼들은 비활성화 시킵니다(277행~280행). TextView Server 옆에 있는 EditText edt_ip와 TextView PORT 옆에 있는 EditText edt_port에 앞의 글에서 만든 STM32F103과 연결하기에 적합한 값, 예를 들어 ip는 172.30.1.99, port는 15598이 들어 있어야 합니다. 

 

btn_disconnect 버튼은 socket이 연결된 상태일 때에만 활성화 됩니다. 이 버튼을 클릭하면 disconnect.setOnClickListener() 함수가 실행됩니다. 이 함수에서는 ClientThread의 메시지 핸들러인 mServiceHandler에 MSG_DISCONNECT 메시지를 보냅니다(295행). mServiceHandler가 MSG_DISCONNECT 메시지를 받으면 ClientThread를 종료시킵니다(179행). socket이 연결되지 않은 상태에서 사용할 수 있는 버튼들을 활성화시키고 사용할 수 없는 버튼들은 비활성화 시킵니다(296행~299행).

 

btn_send는 socket이 연결된 상태일 때에만 활성화 됩니다. 이 버튼을 클릭하면 send.setOnClickListener() 함수가 실행됩니다. 이 함수에서는 edt_send EditBox(ese)의 내용을 읽어서 보낼 내용이 있으면(308행), 그 내용을 다아서 ClientThread의 메시지 핸들러인 mServiceHandler에 MSG_SEND 메시지를 보냅니다(312행). mServiceHandler가 MSG_SEND 메시지를 받으면 networkWriter로 출력하고(166.167행) mMainHandler로 MSG_SEND 메시지를 보냅니다(174행).

 

btn_exit는 socket이 연결되지 않은 상태일 때에만 활성화 됩니다. 이 버튼을 클릭하면 exit.setOnClickListener() 함수가 실행됩니다. 이 함수는 주 메시지 핸들러를 종료시킨 다음에 앱을 종료시킵니다(319,320행).

 

이상으로 ESP8266의 AT 명령어를 사용해서, PC와 스마트폰에서 wifi로 STM32F103과 통신하는 방법에 관한 글을 모두 마칩니다.

 

AndroidManifest.xml
0.00MB
activity_main.xml
0.00MB
MainActivity.java
0.01MB

블로그 이미지

엠쿠스

Microprocessor(STM32, AVR)로 무엇인가를 만들어 보고자 학습 중입니다.

,