이글의 전부 또는 일부, 사진, 소스프로그램 등은 저작자의 동의 없이는 상업적인 사용을 금지합니다. 또한, 비상업적인 목적이라하더라도 출처를 밝히지 않고 게시하는 것은 금지합니다.
이번 글에서는 지난 글 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로 MSG_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과 통신하는 방법에 관한 글을 모두 마칩니다.
'STM32F103' 카테고리의 다른 글
STM32F103 DFU BOOTLOADER로 펌웨어 업데이트하기 내용 업데이트 (1) | 2021.01.13 |
---|---|
STM32F103으로 ESP8266을 이용한 소켓 통신하기 - 제4편 (0) | 2020.02.24 |
STM32F103으로 ESP8266을 이용한 소켓 통신하기 - 제3편 (1) | 2020.01.28 |
STM32F103으로 ESP8266을 이용한 소켓 통신하기 - 제2편 (3) | 2020.01.20 |
STM32F103으로 ESP8266을 이용한 소켓 통신하기 - 제1편 (2) | 2020.01.11 |