안녕하세요? 닉네임간편입니다. 이번 시간에는 입출력 스트림을 이용해 데이터를 송신하고 수신하는 것에 대해 알아보겠습니다.
전체 소스는 여기에 있습니다.
https://github.com/creativeduck/MyLED
앱을 미리 사용해보고 계신 분들은, 이 링크를 타고 설치해주시면 됩니다.
https://play.google.com/store/apps/details?id=com.mybest.myled
1. 변수 정의
byte[] colors = new byte[5];
Thread receiveThread;
TextView currentState = findViewById(R.id.currentState);
SeekBar bar = findViewById(R.id.bar);
ShapeableImageView selectedColor = findViewById(R.id.selectedColor);
1) colors
색상 및 밝기 데이터를 전달할 byte 배열 colors를 정의하고 초기화해줍니다.
2) receiveThread
데이터를 수신받을 작업을 수행할 스레드를 정의합니다.
3) currentState
현재 색상 및 밝기 정보를 나타내기 위한 텍스트뷰를 정의하고 가져옵니다.
4) bar
밝기를 설정할 시크바를 정의하고 가져옵니다.
5) selectedColor
LED의 불빛을 결정할 색상을 선택하는 셰이퍼블 이미지뷰를 정의하고 가져옵니다.
2. 데이터 송신 메서드
private void sendData(int cOrp) {
colors[0] = (byte) Color.red(cOrp); // 빨간색 설정
colors[1] = (byte) Color.green(cOrp); // 초록색 설정
colors[2] = (byte) Color.blue(cOrp); // 파란색 설정
colors[3] = (byte) bar.getProgress(); // 밝기 설정
selectedColor.setImageDrawable(null); // 선택된 색상을 보여주는 이미지뷰에 설정되어있는 이미지 지우기
selectedColor.setBackgroundColor(Color.rgb(Color.red(cOrp), Color.green(cOrp),
Color.blue(cOrp))); // 현재 선택된 색상으로 배경색 설정
try {
outputStream.write(colors); // 출력 스트림으로 데이터 배열 전송
} catch(Exception e) {
e.printStackTrace();
Toast.makeText(getApplicationContext(), "데이터를 전송할 수 없습니다", Toast.LENGTH_SHORT).show();
}
}
1) 파라미터
이 메서드의 파라미터로 int 자료형의 cOrp를 전달했습니다. 이것의 의미는 Color or Pixel로, Color나 Pixel 값을 파라미터로 전달받아 기능을 수행합니다.
바로 다음 시간에 다룰 색상 선택 메서드에선 Pixel값을 파라미터로 전달해야 하고, 추후에 다룰 컬러피커 메서드에선 Color값을 파라미터로 전달해야 합니다. 이때 둘 다 int 자료형이므로, 위와 같이 표기하여 Color 혹은 Pixel을 파라미터로 전달받음을 나타냈습니다.
2) 색상 및 밝기 설정
파라미터로 전달받은 Pixel 혹은 Color값에서 색상을 추출합니다.
순서대로 빨간색, 초록색, 파란색을 추출해 colors 배열에 넣습니다.
RGB 색상을 추출할 때 int 자료형 값이 반환되기 때문에, 꼭 byte 자료형으로 강제 형변환을 해주어야 합니다. colors 배열은 byte형이기 때문입니다.
이후 시크바에서 설정한 값을 가져와 밝기 데이터로 사용할 수 있도록 colors[3]에 넣습니다.
3) 이미지뷰 배경색 설정
먼저 setImageDrawable() 메서드에 null를 파라미터로 전달하여, 현재 셰이퍼블 이미지뷰 배경에 아무것도 설정되지 않도록 합니다.
이후 setBackgroundColor() 메서드를 통해 배경 색상을 설정해줍니다.
RGB 색상값을 설정할 때에는 0~255에 해당하는 값으로 설정해야 합니다. 따라서 여기선 byte 자료형으로 형변환하지 않고 그대로 색상값을 설정합니다.
이 기능을 통해 색상을 선택하면 선택된 색상이 셰이퍼블 이미지뷰에 나타나게 됩니다. 따라서 사용자는 자신이 어떤 색상을 선택했는지 알 수 있게 됩니다.
4) 데이터 전송
출력 스트림 객채에 write() 메서드를 통해 데이터를 전달합니다.
이때 byte 배열 colors를 파라미터로 전달하면 해당 배열에 있는 값들이 모두 데이터로 전달됩니다.
write() 메서드는 차단 호출 메서드입니다. 차단 호출은 앞서 배웠듯 메서드가 한 번 호출되면 해당 기능이 완료되기 전까지 다른 기능을 차단하는 것입니다.
그러나 write() 메서드는 단지 데이터를 보내는 것이기 때문에 기능이 바로 완료되므로, 일반적으로 차단에 의한 문제를 걱정하지 않아도 됩니다. 물론 연결된 블루투스 기기가 데이터를 신속하게 수신받지 않는다면 차단할 수 있다는 문제가 생길 수 있습니다. 그러나 저의 수백 번의 시도를 통해 확인해본 결과 아두이노 블루투스 모듈은 신속하게 데이터를 수신받기 때문에, 본 예제에서는 데이터 수신을 위한 별도 스레드를 만들지 않았습니다.
또한 색상 설정 없이 데이터를 보내는 경우도 존재하므로, 앞선 메서드를 오버로딩하여 파라미터를 전달받지 않는 메서드도 생성해줍니다.
private void sendData() {
try {
outputStream.write(colors);
} catch(Exception e) {
e.printStackTrace();
Toast.makeText(getApplicationContext(), "데이터를 전송할 수 없습니다", Toast.LENGTH_SHORT).show();
}
}
오버로딩이란 전달하는 파라미터의 종류와 개수를 다르게 해서 같은 이름의 메서드를 여러 개 만드는 것입니다.
본 예제에서는 파라미터가 없는 메서드와 int 자료형의 파라미터를 필요로 하는 메서드를 만들었습니다.
3. 데이터 수신 메서드
protected void receiveDate() {
final Handler handler = new Handler();
receiveThread = new Thread(new Runnable() {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
int bytesAvailable = inputStream.available();
if (bytesAvailable > 0) {
byte[] readColor = new byte[4];
SystemClock.sleep(50);
inputStream.read(readColor);
handler.post(new Runnable() {
@Override
public void run() {
currentState.setText("R: "+ (readColor[0] & 0xFF)
+" G: "+ (readColor[1] & 0xFF)
+" B: "+ (readColor[2] & 0xFF)
+" 밝기: "+(readColor[3] & 0xFF));
}
});
}
} catch (IOException e) {
e.printStackTrace();
} catch(Exception e) {
e.printStackTrace();
}
}
}
});
//데이터 수신 thread 시작
receiveThread.start();
}
1) 핸들러
수신 스레드에서 발생한 작업을 메인 액티비티에서 처리할 수 있도록 하는 핸들러 객체를 생성합니다.
2) 수신 스레드
데이터를 수신하는 것은 별도의 스레드에서 진행하는 것이 좋습니다. 데이터를 수신하는 메서드인 read() 메서드는 차단 호출이며, 데이터를 수신하기 위해선 데이터가 오는지 계속 감시하고 있어야 하기 때문입니다. 따라서 전달되는 데이터가 있는지 감시하는 동안에도 다른 작업을 수행할 수 있도록 별도 스레드를 만들어줍니다.
3) !Thread.currentThread().isInterrupted()
currentThread() 메서드는 현재 실행 중인 스레드 객체에 대한 참조(reference)를 반환합니다. 이 메서드를 통해 현재 실행되고 있는 스레드 객체를 얻을 수 있습니다.
isInterrupted() 메서드는 현재 스레드 객체가 interrupt 되었는지 여부에 따라 true 혹은 false를 반환합니다.
interrupt() 메서드는 wait() 메서드나 join(), sleep() 메서드 등으로 인해 스레드가 일시적으로 멈춘 상황에서 InterruptedException을 발생시킵니다.
이렇게 이렇게 된다면 예외가 발생했으므로 try-catch 구문의 예외 처리 부분으로 이동합니다. 이를 통해 run() 메서드를 빠져나와 스레드가 정상적으로 종료될 수 있도록 합니다.
즉, 스레드 객체가 interrupt 되었다는 것은 정상적으로 종료되었다는 것과 같습니다.
그렇다면 isInterrupted() 메서드가 true를 반환한다면 스레드가 정상적으로 종료되었기 때문에 더 이상 동작을 수행할 수 없습니다. 반대로 false를 반환한다면 스레드가 종료되지 않았기 때문에 동작할 수 있습니다.
즉, isInterrupted() 메서드를 통해 스레드가 정상 종료되었는지 여부를 판단하고, 종료되지 않았다면 기능을 수행하도록 조건을 부여합니다.
따라서 run() 메서드 내부 기능이 동작하기 위해선 isInterrupted() 메서드가 false를 반환해야 하므로, 조건문에 !를 붙여 isInterrupted() 메서드가 false를 반환하면 조건이 참이 되도록 합니다.
4) 데이터 수신
available() 메서드는 입력 스트림에서 현재 읽을 수 있는 데이터의 수를 반환합니다. 즉, 읽을 수 있는 데이터가 존재한다면 다음 동작을 수행합니다.
먼저 데이터를 수신받을 byte 배열 readColor를 생성합니다. 이때 RGB 색상 및 밝기 데이터만 전송받을 것이므로 길이는 4로 설정했습니다.
SystemClock.sleep() 메서드는 밀리세컨드 단위의 값을 파라미터로 전달받으며 해당 값만큼 스레드를 일시 정지시킵니다. Thread.sleep() 메서드와 달리 이 메서드는 InterruptedException을 발생시키지 않습니다.
이렇게 잠시 스레드를 멈춘 이유는 데이터를 충분히 읽을 시간을 주기 위함입니다.
이후 데이터를 읽은 후 read() 메서드를 통해 읽은 데이터를 readColor에 저장합니다.
데이터가 잘 수신되면 다음과 같이 현재 상태가 표시됩니다.
5) post()
앞서 핸들러 객체는 Runnable 객체를 전달할 수 있다고 했습니다. post() 메서드를 통해 Runnable 객체를 전달합니다.
이 Runnable 객체의 run() 메서드를 재정의해서 수행해야 할 작업을 입력합니다.
현재 상태를 나타내는 텍스트뷰인 currentState에 현재 RGB 색상과 밝기를 표시합니다.
이때 readColor의 각 요소에 '& 0xFF'를 하는 이유는 형변환을 위한 것입니다.
앞서 데이터 송수신은 byte 배열로 진행했습니다. byte 데이터만 전달되기 때문입니다.
byte 변수는 -128에서 127까지의 값을 표현할 수 있습니다. 그러나 색상과 밝기 데이터 값은 0~255의 값을 갖기 때문에, 순수 byte 자료형으로 데이터를 보내는 것에는 무리가 있습니다. 따라서 강제 형변환을 통해 127이 넘어가는 값은 음수로 변환되어 저장됩니다.
아두이노로부터 수신받은 데이터 또한 byte 자료형이므로 127이 넘어가는 값은 음수로 저장됩니다. 이때 이를 다시 int 자료형으로 변환해 255까지의 값으로 표현될 수 있도록 해주는 것이 '& 0xFF' 입니다.
이것은 비트 연산에 관한 부분이며, 다소 어렵기 때문에 추후에 다루겠습니다. 지금은 단지 이 조치를 통해 음수가 된 값을 원래의 값으로 되돌린다고만 이해하시면 되겠습니다.
추가로, 아두이노에서 byte 자료형은 부호 비트가 없이 0부터 255까지의 값을 저장할 수 있습니다. 따라서 앱에서 byte 자료형으로 데이터를 보냈어도 이것이 0부터 255까지의 값으로 변환됩니다. 그렇기에 아두이노 스케치에서는 byte 자료형 데이터를 int 자료형으로 변환해 RGB 색상을 설정하는 등의 조치를 취하지 않아도 됩니다.
6) 예외 처리
available() 메서드는 IOException을 발생시킬 수 있기 때문에 예외처리를 해줍니다.
이후 interrupt() 메서드가 발생시키는 InterruptedException 예외나 다른 모든 예외를 포괄하여 처리해줍니다.
7) start()
start() 메서드를 통해 수신 스레드를 시작합니다.
그리고 앞서 만들었던 기기 연결 메서드 부분에 receiveDate() 메서드를 추가합니다.
이를 통해 기기가 연결에 성공하면 데이터를 수신받을 수 있는 상태가 되도록 만들어줍니다.
private void connectDevice(String selectedDeviceName, boolean paired) {
final Handler mHandler = new Handler() {
public void handleMessage(Message msg) {
if(msg.what==1) {
try{
outputStream = bluetoothSocket.getOutputStream();
inputStream = bluetoothSocket.getInputStream();
receiveDate();
/*
중략
*/
4. onDestroy()
@Override
protected void onDestroy() {
try{
inputStream.close();
outputStream.close();
bluetoothSocket.close();
receiveThread.interrupt();
} catch(IOException e) {
e.printStackTrace();
}
unregisterReceiver(mReceiver);
super.onDestroy();
}
앱이 종료되기 전 실행하고 있던 스레드를 종료시켜야 합니다.
receiveThread를 정상적으로 종료시키기 위해 onDestroy() 메서드를 재정의한 부분에 interrupt() 메서드를 포함시켜줍니다.
5. 마무리
이번 시간에는 입출력 스트림을 이용해 데이터를 송수신하는 메서드에 대해 알아보았습니다.
이제 블루투스 통신에 관한 부분은 모두 끝마쳤습니다. 다음 시간부터는 레이아웃에서 만들었던 이미지뷰, 버튼 등 뷰들의 기능을 설정해보겠습니다.
'프로젝트 > 블루투스 무드등' 카테고리의 다른 글
[비전공자도 만들 수 있는 블루투스 무드등] 14. 안드로이드 앱 part 3-2, 버튼 기능 설정 (0) | 2021.07.27 |
---|---|
[비전공자도 만들 수 있는 블루투스 무드등] 13. 안드로이드 앱 part 3-1 with 비트맵(Bitmap) (0) | 2021.07.26 |
[비전공자도 만들 수 있는 블루투스 무드등] 11. 안드로이드 앱 part 2-5, 블루투스 기기 연결 (0) | 2021.07.24 |
[비전공자도 만들 수 있는 블루투스 무드등] 10. 안드로이드 앱 part 2-4, 블루투스 페어링되지 않은 기기 탐색 (0) | 2021.07.23 |
[비전공자도 만들 수 있는 블루투스 무드등] 9. 안드로이드 앱 part 2-3 with 위험 권한 (8) | 2021.07.22 |