‘안드로이드 띵스’를 이용한 도어벨 (3)

2017-06-08     이나리 기자

[CCTV뉴스=이나리 기자] 안드로이드 띵스(Android Things) 개발의 개념을 이해하기 위해서 간단한 응용 프로그램을 살펴보자. 안드로이드 띵스의 장점은 하드웨어적인 제어를 쉽게 프로그래밍할 수 있고, 구글의 클라우드 서비스를 자연스럽게 연결할 수 있다는 것이다. 따라서 여기서는 안드로이드 띵스의 여러 접근방식을 예시를 통해 설명하겠다.

◇ 안드로이드 띵스 개발에 필요한 부품과 소프트웨어

1. 안드로이드 띵스 호환 보드(예, 라즈베리파이 3 등의 보드)
2. 카메라 모듈 (안드로이드 띵스와 호환되는 카메라 모듈)
3. 안드로이드 스튜디오 2.2 이상 버전
4. 구글 클라우드 비전 API
5. 파이어베이스(Firebase) 데이터베이스
6. 버튼, 저항, 점퍼, 브레드 보드

하드웨어 연결 = 안드로이드 띵스 보드와 부가 하드웨어 부품의 연결은 [그림1]과 같다.

도어벨의 동작 방식 = 안드로이드 띵스를 이용한 도어벨은 버튼을 누르면, 버튼에 대한 누름 동작을 인식하고, 카메라로 촬영해 서버에 이미지를 올려 버튼을 누른 사람을 확인한다. 다음은 시중에 판매되고 있는 비디오 도어벨을 구현하는 예제다.

(1) 버튼을 누르면 인터럽트가 발생하고 누름 동작을 인식한다.
(2) 카메라를 이용해 사진을 찍는다.
(3) 파이어베이스 데이터베이스에 사진을 업로드 한다.
(4) 구글의 클라우드 비전 API를 통해 이미지를 분석한다.

DoorbellActivity.java는 도어벨 버튼을 누를 때, 카메라를 통해 촬영하고, 이미지를 올리는 소스다[표 1].

[표 1]
 package com.example.androidthings.doorbell;

// 중략

/*
주 엑티비티
*/
public class DoorbellActivity extends Activity {
 private static final String TAG = DoorbellActivity.class.getSimpleName();

private FirebaseDatabase mDatabase;
 private DoorbellCamera mCamera;

/*
 * 버튼에 대한 제어 코드
*/
private Button mButton;

/**
 * 카메라 처리 루틴
*/
private Handler mCameraHandler;

/**
 * 카메라 처리 쓰레드
*/
private HandlerThread mCameraThread;

// 중략

/**
 * 버튼에 대한 정의
*/
private final String BUTTON_GPIO_PIN = "BCM21";


 @Override
 public void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 Log.d(TAG, "Doorbell Activity created.");

// 카메라에 대한 퍼미션
if (checkSelfPermission(Manifest.permission.CAMERA)
 != PackageManager.PERMISSION_GRANTED) {
 // A problem occurred auto-granting the permission
 Log.d(TAG, "No permission");
 return;
 }

mDatabase = FirebaseDatabase.getInstance();

// 중략

// 버튼 드라이버 초기화
try {
 mButton = new Button(BUTTON_GPIO_PIN, Button.LogicState.PRESSED_WHEN_LOW);
 mButton.setOnButtonEventListener(mButtonCallback);
 } catch (IOException e) {
 Log.e(TAG, "button driver error", e);
 }
 // Camera code is complicated, so we've shoved it all in this closet class for you.
 mCamera = DoorbellCamera.getInstance();
 mCamera.initializeCamera(this, mCameraHandler, mOnImageAvailableListener);
 }

// 중략

/**
 * 버튼을 누를때 카메라를 이용한 촬영
*/
private Button.OnButtonEventListener mButtonCallback = new Button.OnButtonEventListener() {
 @Override

public void onButtonEvent(Button button, boolean pressed) {
 if (pressed) {
 // Doorbell rang!
 Log.d(TAG, "button pressed");
 mCamera.takePicture();
 }
 }
 };


 /**
 * Firebase와 클라우드 비전에 대한 처리
*/
private void onPictureTaken(final byte[] imageBytes) {
 if (imageBytes != null) {
 final DatabaseReference log = mDatabase.getReference("logs").push();
 String imageStr = Base64.encodeToString(imageBytes, Base64.NO_WRAP | Base64.URL_SAFE);
 // upload image to firebase
 log.child("timestamp").setValue(ServerValue.TIMESTAMP);
 log.child("image").setValue(imageStr);

mCloudHandler.post(new Runnable() {
 @Override
 public void run() {
 Log.d(TAG, "sending image to cloud vision");
 // annotate image by uploading to Cloud Vision API
 try {
 Map<String, Float> annotations = CloudVisionUtils.annotateImage(imageBytes);
 Log.d(TAG, "cloud vision annotations:" + annotations);
 if (annotations != null) {
 log.child("annotations").setValue(annotations);
 }
 } catch (IOException e) {
 Log.e(TAG, "Cloud Vison API error: ", e);
 }
 }
 });
 }
 }
 }

지금까지 기본적인 소스 확인으로 안드로이드 띵스를 활용해 개발하는 방식을 살펴봤다. 좀 더 세부적으로 하드웨어를 제어하는 방식을 살펴보겠다.

◇ 안드로이드 띵스를 이용한 하드웨어 제어

GPIO(General Purpose Input/Output)는 하나의 포트를 이용해 버튼과 제어를 할 수 있는 형태의 기본적인 하드웨어다. [표 2]는 GPIO를 이용하기 위해 먼저 사용할 포트를 등록하는 작업이다.

[표2]

PeripheralManagerService manager = new PeripheralManagerService();
 List<String> portList = manager.getGpioList();
 if (portList.isEmpty()) {
 Log.i(TAG, "No GPIO port available on this device.");
 } else {
 Log.i(TAG, "List of available ports: " + portList);
 }

PeripheralManagerService를 사용해서 포트에 연결 작업을 실시한다[표 3].

[표 3]

public class HomeActivity extends Activity {
 // GPIO Pin Name
 private static final String GPIO_NAME = ...;

private Gpio mGpio;

@Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 // GPIO에 연결한다.
try {
 PeripheralManagerService manager = new PeripheralManagerService();
 mGpio = manager.openGpio(GPIO_NAME);
 } catch (IOException e) {
 Log.w(TAG, "Unable to access GPIO", e);
 }
 }

@Override
 protected void onDestroy() {
 super.onDestroy();

if (mGpio != null) {
 try {
 mGpio.close();
 mGpio = null;
 } catch (IOException e) {
 Log.w(TAG, "Unable to close GPIO", e);
 }
 }
 }
 }

그 다음 GPIO 포트를 입력 또는 출력인지 설정해서 사용할 수 있도록 한다. setDirection() 함수를 통해 입력‧출력을 설정하고, setActiveType()를 이용해서 어떤 형태로 인식할 수 있는지 지정한다[표 4].

[표 4]

public void configureInput(Gpio gpio) throws IOException {
 // 입력핀을 입력으로 설정한다.
gpio.setDirection(Gpio.DIRECTION_IN);
 // High voltage is considered active
 gpio.setActiveType(Gpio.ACTIVE_HIGH);

...

// HIGH 상태 여부를 확인한다.
if (gpio.getValue()) {
 // Pin is HIGH
 } else {
 // Pin is LOW
 }
 }

[표 5] 예제는 입력상태 변경시 처리되는 루틴이다. 즉, 입력값이 HIGH에서 LOW로 변경되거나 LOW에서 HIGH로 변경될때 어떻게 처리하는지 설정하는 방법이다. 다시 말해 상태가 변할 때 감지하는 방법을 설정하는 단계다.

[표 5]

public void configureInput(Gpio gpio) throws IOException {
 // 입력으로 설정한다.
gpio.setDirection(Gpio.DIRECTION_IN);
 // HIGH에 LOW로 변할때 감지한다.
gpio.setActiveType(Gpio.ACTIVE_LOW);

// 상태 변경할때 처리할 루틴을 등록
gpio.setEdgeTriggerType(Gpio.EDGE_BOTH);
 gpio.registerGpioCallback(mGpioCallback);
 }

private GpioCallback mGpioCallback = new GpioCallback() {
 @Override
 public boolean onGpioEdge(Gpio gpio) {
 // Read the active low pin state
 if (gpio.getValue()) {
 // Pin is LOW
 } else {
 // Pin is HIGH
 }

// Continue listening for more interrupts
 return true;
 }

@Override
 public void onGpioError(Gpio gpio, int error) {
 Log.w(TAG, gpio + ": Error event " + error);
 }
 };

[표 6]은 앱 프로그램 동작과 종료시 처리 부분을 보여준다.

[표 6]

public class HomeActivity extends Activity {
 private Gpio mGpio;
 ...

@Override
 protected void onStart() {
 super.onStart();

// 인터럽트 처리 부분 등록 부분
mGpio.registerGpioCallback(mGpioCallback);
 }

@Override
 protected void onStop() {
 super.onStop();
 // 인터럽트 등록 해지 부분
mGpio.unregisterGpioCallback(mGpioCallback);
 }
 }

출력부분에 대한 처리 = 출력은 앞의 입력처리와 마찬가지로 출력포트를 설정하고, 어떤 값을 출력할 것인지를 지정한다. 이 방식은 보통 LED를 비롯해 부저와 같은 하드웨어를 제어하기 위해 사용한다. 그 다음 setDirection()을 이용해 포트를 설정하고, DIRECTION_OUT_INITIALLY_HIGH, DIRECTION_OUT_INITIALLY_LOW로 초기 설정값을 HIGH 또는 LOW로 지정한다[표 7].

[표 7] 

public void configureOutput(Gpio gpio) throws IOException {
 // 출력 포트를 지정한다.
gpio.setDirection(Gpio.DIRECTION_OUT_INITIALLY_HIGH);
 // LOW로 설정
gpio.setActiveType(Gpio.ACTIVE_LOW);

...

// LOW로 출력한다.
gpio.setValue(true);
 }
 

 UART(Universal Asynchronous Receiver Transmitter)은 디바이스 간의 통신 방식이다. PC에서 통상적으로 시리얼 포트라는 장치의 통신은 UART와 유사한 방식이다. [그림2]는 장치간의 UART 통신을 구현하기 위해 연결하는 방식을 보여준다. TX는 송신, RX는 수신 포트로 사용되며 3선으로 연결된다.

getUartDeviceList()를 사용해 UART 장치에 대한 정보를 얻어온다[표 8].

[표 8]

PeripheralManagerService manager = new PeripheralManagerService();
 List<String> deviceList = manager.getUartDeviceList();
 if (deviceList.isEmpty()) {
 Log.i(TAG, "No UART port available on this device.");
 } else {
 Log.i(TAG, "List of available devices: " + deviceList);
 }

openUartDevice()를 사용하여 장치를 열고, close() 함수를 통해 장치를 닫는다.
public class HomeActivity extends Activity {
 // UART 장치에 대한 이름
private static final String UART_DEVICE_NAME = ...;

private UartDevice mDevice;

@Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 // UART 장치에 대한 오픈
try {
 PeripheralManagerService manager = new PeripheralManagerService();
 mDevice = manager.openUartDevice(UART_DEVICE_NAME);
 } catch (IOException e) {
 Log.w(TAG, "Unable to access UART device", e);
 }
 }

@Override
 protected void onDestroy() {
 super.onDestroy();

if (mDevice != null) {
 try {
 mDevice.close();
 mDevice = null;
 } catch (IOException e) {
 Log.w(TAG, "Unable to close UART device", e);
 }
 }
 }
 }

 

UART 장치를 사용할 때 중요한 부분은 통신 속도를 설정하는 것이다. setBaudrate() 함수를 통해 통신속도, 데이터 비트 등을 구성할 수 있다[표 9].

[표 9]

public void configureUartFrame(UartDevice uart) throws IOException {
 // UART 포트 설정
uart.setBaudrate(115200);
 uart.setDataSize(8);
 uart.setParity(UartDevice.PARITY_NONE);
 uart.setStopBits(1);
 }


 write()를 사용해서 데이터를 다른 장치에 전송한다[표 10].

[표 10]

public void writeUartData(UartDevice uart) throws IOException {
 byte[] buffer = {...};
 int count = uart.write(buffer, buffer.length);
 Log.d(TAG, "Wrote " + count + " bytes to peripheral");
 }

read()를 사용하여 다른 장치에서 전달된 데이터를 수신할 수 있다.
public void readUartBuffer(UartDevice uart) throws IOException {
 // 최대 수신 가능한 바이트 수 설정
final int maxCount = ...;
 byte[] buffer = new byte[maxCount];

int count;
 while ((count = uart.read(buffer, buffer.length)) > 0) {
 Log.d(TAG, "Read " + count + " bytes from peripheral");
 }
 }
 

지금까지 실제로 안드로이드 띵스에서 어떻게 하드웨어를 제어하고 운영하는지를 살펴봤다. 안드로이드 띵스는 IoT 개발을 위한 개발 도구이자 환경이다. 안드로이드 띵스는 출시된지 얼마 안 된 플랫폼이지만 IoT의 중요한 기술의 하나로써 반드시 확인해야 할 부분이다.

작성: 라영호 테뷸라 대표이사
자료제공: 테뷸라(www.tebular.com)