센서 사용하기

01. 장애물 회피

알버트 로봇의 전방에 있는 근접 센서는 적외선을 방출하는 LED와 적외선을 감지하는 광 트랜지스터로 이루어져 있다. 그림 1과 같이 로봇의 앞면 좌우에 하나씩 설치되어 있으며, 알버트 로봇 전방의 1cm 이상, 15cm 이하의 거리에 있는 물체나 장애물을 감지하기 위해 사용한다.

그림 1. 알버트 로봇의 전방 근접 센서

광 트랜지스터는 IR-LED가 방출하는 적외선이 전방의 물체에 반사되어 나오는 광량을 검출한다. 이때 반사되는 광량이 클수록 출력 값은 255에 가까워진다. 즉, 장애물까지의 거리에 반비례한다고 할 수 있다.

알버트 로봇이 직진하다가 앞에 장애물이 나타나면 일정 거리를 후진한 후 다시 직진하는 프로그램을 작성하시오.

RobotActivity 클래스를 상속한 액티비티를 만들고, onInitialized(Robot) 메소드와 onExecute() 메소드를 상속받아 새로 구현하자. 프로젝트에 스마트 로봇 라이브러리를 포함하는 것을 잊지 않기를 바란다.

 public class SampleActivity extends RobotActivity
 {
     @Override
     public void onCreate(Bundle savedInstanceState)
     {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.main);
     }

     @Override
     public void onInitialized(Robot robot)
     {
     }

     @Override
     public void onExecute()
     {
     }
 }

먼저 전방에 책이나 손 같은 장애물이 있다는 것을 알 수 있어야 하는데 알버트 로봇에는 좌우 전방 센서에 해당하는 디바이스로 SENSOR_LEFT_PROXIMITYSENSOR_RIGHT_PROXIMITY가 있다. 좌우 전방 센서의 데이터는 0부터 255까지의 값을 가지는데 장애물이 가까이 있으면 반사된 빛에 의해 광량이 많아져 값이 증가하고, 장애물이 멀면 광량이 적어져서 값이 감소하게 된다. 장애물이 없으면 반사된 빛이 없어 0의 값을 가진다.

 public class SampleActivity extends RobotActivity
 {
     private Device mLeftProximityDevice;
     private Device mRightProximityDevice;

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

     @Override
     public void onInitialized(Robot robot)
     {
         mLeftProximityDevice = robot.findDeviceById(Albert.SENSOR_LEFT_PROXIMITY);
         mRightProximityDevice = robot.findDeviceById(Albert.SENSOR_RIGHT_PROXIMITY);
     }

     @Override
     public void onExecute()
     {
         int leftProximity = mLeftProximityDevice.read();
         int rightProximity = mRightProximityDevice.read();
     }
 }

좌우 전방 센서의 값을 읽기 위해 Device 클래스의 read() 메소드를 사용하였다. read 메소드는 여러 가지 종류가 있는데 여기서 사용한 메소드는 아규먼트 없이 호출하고 디바이스의 데이터 값을 반환한다. 다른 디바이스에 대해서도 같은 방법으로 하면 되는데 앞으로 조금씩 배워가도록 하자.

이제 전방 센서의 값이 50보다 크면 뒤로 물러났다가 다시 앞으로 가는 프로그램을 만들어 보자.

 public class SampleActivity extends RobotActivity
 {
     private Device mLeftProximityDevice;
     private Device mRightProximityDevice;
     private Device mLeftWheelDevice;
     private Device mRightWheelDevice;

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

     @Override
     public void onInitialized(Robot robot)
     {
         mLeftProximityDevice = robot.findDeviceById(Albert.SENSOR_LEFT_PROXIMITY);
         mRightProximityDevice = robot.findDeviceById(Albert.SENSOR_RIGHT_PROXIMITY);
         mLeftWheelDevice = robot.findDeviceById(Albert.EFFECTOR_LEFT_WHEEL);
         mRightWheelDevice = robot.findDeviceById(Albert.EFFECTOR_RIGHT_WHEEL);
     }

     @Override
     public void onExecute()
     {
         int leftProximity = mLeftProximityDevice.read();
         int rightProximity = mRightProximityDevice.read();
         if(leftProximity > 50 || rightProximity > 50)
         {
             mLeftWheelDevice.write(-20);
             mRightWheelDevice.write(-20);
         }
         else
         {
             mLeftWheelDevice.write(20);
             mRightWheelDevice.write(20);
         }
     }
 }

프로그램을 실행하고 앞으로 달려가는 알버트 로봇의 앞을 손으로 막으면 전방 센서의 값이 50보다 커져서 뒤로 물러나게 되고, 뒤로 물러나 전방 센서의 값이 50보다 작아지게 되면 다시 앞으로 달려가는 일을 반복하게 될 것이다.

이때, 너무 가까이 막지는 말고 약간 거리를 두어 손으로 막도록 하자. 알버트 로봇은 전방의 1cm 이내에 있는 장애물은 감지하지 못하기 때문이다. 숫자 50을 0부터 255까지 변경하면서 장애물과의 거리를 조정해 보도록 하자.

전방에 장애물이 있을 때 그냥 순순히 뒤로 물러나면 재미없다. 알버트 로봇이 놀란 듯한 모습을 표현하기 위해 뒤로 물러날 때는 눈을 빨간색으로 표시해 보자. 화가 나서 눈이 빨갛게 되었다고 생각해도 좋다. 대신, 전진할 때는 눈을 녹색으로 표시하도록 하자.

알버트 로봇에는 좌우 눈 색상에 해당하는 디바이스로 EFFECTOR_LEFT_EYEEFFECTOR_RIGHT_EYE가 있다. 좌우 눈의 색상은 R, G, B 값으로 표시되기 때문에 눈의 색상 데이터는 크기 3의 배열로 되어 있으며, 각각 0부터 255까지의 값을 가질 수 있다.

 public class SampleActivity extends RobotActivity
 {
     private Device mLeftProximityDevice;
     private Device mRightProximityDevice;
     private Device mLeftWheelDevice;
     private Device mRightWheelDevice;
     private Device mLeftEyeDevice;
     private Device mRightEyeDevice;

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

     @Override
     public void onInitialized(Robot robot)
     {
         mLeftProximityDevice = robot.findDeviceById(Albert.SENSOR_LEFT_PROXIMITY);
         mRightProximityDevice = robot.findDeviceById(Albert.SENSOR_RIGHT_PROXIMITY);
         mLeftWheelDevice = robot.findDeviceById(Albert.EFFECTOR_LEFT_WHEEL);
         mRightWheelDevice = robot.findDeviceById(Albert.EFFECTOR_RIGHT_WHEEL);
         mLeftEyeDevice = robot.findDeviceById(Albert.EFFECTOR_LEFT_EYE);
         mRightEyeDevice = robot.findDeviceById(Albert.EFFECTOR_RIGHT_EYE);
     }

     @Override
     public void onExecute()
     {
         int leftProximity = mLeftProximityDevice.read();
         int rightProximity = mRightProximityDevice.read();
         if(leftProximity > 50 || rightProximity > 50)
         {
             mLeftWheelDevice.write(-20);
             mRightWheelDevice.write(-20);
             // 크기 3인 배열(차례대로 R, G, B)의 인덱스 0, 1, 2에 각각 데이터를 써도 된다.
             mLeftEyeDevice.write(0, 255); // 왼쪽 눈의 R 값을 255로 한다.
             mLeftEyeDevice.write(1, 0); // 왼쪽 눈의 G 값을 0으로 한다.
             mLeftEyeDevice.write(2, 0); // 왼쪽 눈의 B 값을 0으로 한다.
             int[] red = new int[] { 255, 0, 0 }; // R, G, B 값으로 구성된 크기 3의 배열
             mRightEyeDevice.write(red); // 이렇게 배열 값을 한꺼번에 써도 된다.
         }
         else
         {
             mLeftWheelDevice.write(20);
             mRightWheelDevice.write(20);
             int[] green = new int[] { 0, 255, 0 }; // 녹색
             mLeftEyeDevice.write(green);
             mRightEyeDevice.write(green);
         }
     }
 }

우리가 이제까지 사용한 write 메소드와는 다른 종류의 write 메소드를 사용하였다. 바퀴의 속도는 속도 값이라는 데이터 하나만 있지만, 눈 색상의 경우에는 R, G, B, 3개의 값이 있다. 따라서 눈 색상에 해당하는 디바이스에 3개의 값을 쓸 수 있는 방법이 필요하다.

두 가지 방법이 있는데, 한 가지는 인덱스를 주고 하나씩 데이터를 쓰는 방법이다. 왼쪽 눈을 빨간색으로 할 때 write 메소드에 아규먼트 2개를 넣어 주었는데, 첫 번째 아규먼트는 인덱스 값이고, 두 번째 아규먼트는 데이터 값이다. 눈 색상의 경우 R 값은 인덱스 0, G 값은 인덱스 1, B 값은 인덱스 2이다.

 mLeftEyeDevice.write(0, 255); // 왼쪽 눈의 R 값을 255로 한다.
 mLeftEyeDevice.write(1, 0); // 왼쪽 눈의 G 값을 0으로 한다.
 mLeftEyeDevice.write(2, 0); // 왼쪽 눈의 B 값을 0으로 한다.

또 다른 방법으로는 배열을 만들어서 한꺼번에 쓰는 방법이다. 오른쪽 눈을 빨간색으로 할 때 배열을 만들어서 첫 번째 값은 R 값, 두 번째 값은 G 값, 세 번째 값은 B 값을 넣어주었다. 이렇게 만든 배열을 write 메소드에 입력하면 된다.

 int[] red = new int[] { 255, 0, 0 }; // R, G, B 값으로 구성된 크기 3의 배열
 mRightWheelDevice.write(red); // 배열을 디바이스에 쓴다.

02. 어둠이 무서워요

옛날 노래 중에 "나 오늘, 오늘밤은 어둠이 무서워요..." 하는 노래가 있었다. 알버트 로봇도 어둠을 무서워할까?

어두우면 알버트 로봇이 소리를 내도록 프로그램을 작성하시오.

그림 2와 같이 알버트 로봇의 앞면에는 빛의 밝기를 감지하는 조도 센서가 있다. 조도 센서에 해당하는 디바이스는 SENSOR_LIGHT인데 0부터 65535까지의 값을 가지며, 밝을 수록 값이 커진다.

그림 1. 알버트 로봇의 조도 센서

어둠을 무서워하는 알버트 로봇을 옷이나 종이 박스로 덮으면 어떤 행동을 할까? 아마도 비명을 지르지 않을까? 진짜 비명 소리를 스피커로 출력할 수도 있지만 여기서는 간단하게 하기 위해 알버트 로봇의 버저를 사용해 보자.

버저에 해당하는 디바이스는 EFFECTOR_BUZZER인데 0부터 2500까지의 값을 가진다. 정확하지는 않지만 주파수 Hz를 나타내므로 값이 커질 수록 높은 음의 소리를 낸다. 시끄러워서 버저 소리를 끄기 위해서는 0을 입력하면 된다.

 public class SampleActivity extends RobotActivity
 {
     private Device mLightDevice;
     private Device mBuzzerDevice;

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

     @Override
     public void onInitialized(Robot robot)
     {
         mLightDevice = robot.findDeviceById(Albert.SENSOR_LIGHT);
         mBuzzerDevice = robot.findDeviceById(Albert.EFFECTOR_BUZZER);
     }

     @Override
     public void onExecute()
     {
         int light = mLightDevice.read();
         if(light < 10)
             mBuzzerDevice.write(1000);
         else
             mBuzzerDevice.write(0);
     }
 }

어두워졌을 때 똑같은 소리를 내니까 좀 심심한 것 같다. 이번에는 어두워질 수록 높은 비명 소리를 지르게 해보자. 조도 센서 값의 범위를 5부터 20 정도로 해서 조도 값이 20일 때 버저 음은 500, 조도 값이 5일 때 버저 음은 2000이 되도록 하고, 그 사이를 일정 비율로 버저 음이 높아지도록 하면 (버저 음) = 2500 - (조도) * 100이 된다.

 public class SampleActivity extends RobotActivity
 {
     private Device mLightDevice;
     private Device mBuzzerDevice;

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

     @Override
     public void onInitialized(Robot robot)
     {
         mLightDevice = robot.findDeviceById(Albert.SENSOR_LIGHT);
         mBuzzerDevice = robot.findDeviceById(Albert.EFFECTOR_BUZZER);
     }

     @Override
     public void onExecute()
     {
         int light = mLightDevice.read();
         if(light < 5) // 조도 값이 5보다 작으면 버저 음을 더 높은 음으로 하지 않고 2000으로 제한한다.
             mBuzzerDevice.write(2000);
         else if(light > 20) // 조도 값이 20보다 크면, 즉 더 밝으면 버저 음을 끈다.
             mBuzzerDevice.write(0);
         else
             mBuzzerDevice.write(2500 - light * 100);
     }
 }

03. 긴급구조

알버트 로봇이 길을 가다가 넘어지거나 납치를 당하면 빨리 달려가 구해주어야 한다.

알버트 로봇이 넘어지면 삐삐삐 소리를 내도록 프로그램을 작성하시오.

우선 알버트 로봇이 넘어졌다는 것을 알 수 있어야 하는데 이를 위해 알버트 로봇에 내장된 가속도 센서를 사용해 보자. 알버트 로봇의 가속도 센서에 해당하는 디바이스는 SENSOR_ACCELERATION이다. 3축 가속도 센서이므로 가속도 데이터는 크기 3의 배열로 되어 있으며, 각각 -8192부터 8191까지의 값을 가진다.

그림 3과 같이 가속도 센서의 X축은 로봇의 정면 방향이 양수 값이고 뒷면 방향이 음수 값이다. Y축은 왼쪽 방향이 양수 값, 오른쪽 방향이 음수 값이며, Z축은 위쪽 방향이 양수 값, 아래쪽 방향이 음수 값이다.

그림 3. 알버트 로봇의 가속도 센서 방향

알버트 로봇이 똑바로 서있으면 중력에 의해 아래쪽으로만 가속도가 생기므로 가속도의 Z축 성분만 음수로 크기가 큰 값을 가지고 X축과 Y축 성분의 크기는 작은 값을 가지게 된다. 알버트 로봇이 앞뒤로 혹은 왼쪽, 오른쪽으로 넘어졌을 때는 그림 4와 같이 Z축 성분의 크기가 작아지고 X축 또는 Y축 성분의 크기는 커진다는 것을 알 수 있다. 따라서, Z축 성분의 크기가 작거나 양수이면 알버트 로봇이 넘어졌다고 판단할 수 있다.

그림 4. 알버트 로봇의 자세에 따른 가속도 센서 방향

 public class SampleActivity extends RobotActivity
 {
     private Device mAccelerationDevice;

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

     @Override
     public void onInitialized(Robot robot)
     {
         mAccelerationDevice = robot.findDeviceById(Albert.SENSOR_ACCELERATION);
     }

     @Override
     public void onExecute()
     {
         int accelerationZ = mAccelerationDevice.read(2); // Z축 가속도 값을 읽는다.
         if(accelerationZ > -2048)
         {
             // 넘어졌다.
         }
     }
 }

우리가 이제까지 사용한 read 메소드와는 다른 종류의 read 메소드를 사용하였다. 가속도 센서의 경우 X축, Y축, Z축, 3개의 값이 있다. 따라서 가속도 센서에 해당하는 디바이스에서 3개의 값을 읽을 수 있는 방법이 필요하다.

두 가지 방법이 있는데, 한 가지는 인덱스를 주고 하나씩 데이터를 읽는 방법이다. Z축 가속도 값을 읽을 때 read 메소드에 넣어준 아규먼트는 인덱스 값이다. 가속도 센서의 경우 X축은 인덱스 0, Y축은 인덱스 1, Z축은 인덱스 2이다.

 int accelerationX = mAccelerationDevice.read(0); // X축 가속도 값을 읽는다.
 int accelerationY = mAccelerationDevice.read(1); // Y축 가속도 값을 읽는다.
 int accelerationZ = mAccelerationDevice.read(2); // Z축 가속도 값을 읽는다.

또 다른 방법으로는 배열을 만들어서 한꺼번에 읽는 방법이다. 가속도 센서 데이터는 3개의 값을 가지므로 크기 3의 배열을 만들어서 read 메소드에 넣어주면 된다.

 int[] acceleration = new int[3]; // 크기 3의 배열을 만든다.
 // 배열의 첫 번째 자리에 X축, 두 번째 자리에 Y축, 세 번째 자리에 Z축 가속도 값이 들어간다.
 mAccelerationDevice.read(acceleration);

이제 삐삐삐 소리를 내보도록 하자. 그냥 같은 음을 계속 내는 것이 아니라 삐삐삐 소리를 내야 하므로 0.1초 동안 소리를 내고 0.1초 쉬고, 0.1초 동안 소리를 내고 0.1초 쉬고...를 반복하면 된다.

이러한 동작을 하기 위해 앞에서는 onExecute() 메소드가 호출되는 횟수를 세어서 시간을 측정한 것을 기억할 것이다. 기억이 안나면 다시 살펴 보도록 하고, 여기서는 다른 방법으로 해보자.

onExecute() 메소드에서는 시간을 붙잡아 둘 수가 없지만 쓰레드를 하나 만들어서 돌리면 onExecute() 메소드와 상관이 없으므로 내 마음대로 쉬어도 된다. 한 가지 주의할 점은 애플리케이션이 종료될 때 쓰레드도 같이 종료되어야 한다는 것이다. 이를 위해 쓰레드를 데몬으로 만들도록 하자.

 public class SampleActivity extends RobotActivity
 {
     private Device mAccelerationDevice;
     private Device mBuzzerDevice;
     private BeepThread mBeepThread;

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

     @Override
     public void onInitialized(Robot robot)
     {
         mAccelerationDevice = robot.findDeviceById(Albert.SENSOR_ACCELERATION);
         mBuzzerDevice = robot.findDeviceById(Albert.EFFECTOR_BUZZER);

         mBeepThread = new BeepThread();
         mBeepThread.setDaemon(true);
         mBeepThread.start();
     }

     @Override
     public void onExecute()
     {
         int accelerationZ = mAccelerationDevice.read(2); // Z축 가속도 값을 읽는다.
         if(accelerationZ > -2048) // 넘어졌다.
             mBeepThread.makeBeep(true);
         else
             mBeepThread.makeBeep(false);
     }

     private class BeepThread extends Thread
     {
         private int mFrequency;

         @Override
         public void run()
         {
             while(true)
             {
                 mBuzzerDevice.write(mFrequency); // mFrequency가 0이 아니면 소리를 낸다.

                 try
                 {
                     Thread.sleep(100); // 0.1초 동안 기다린다.
                 } catch (InterruptedException e)
                 {
                 }

                 mBuzzerDevice.write(0); // 0.1초 후에 소리를 끈다.

                 try
                 {
                     Thread.sleep(100); // 0.1초 동안 기다린다.
                 } catch (InterruptedException e)
                 {
                 }
             }
         }

         void makeBeep(boolean beep)
         {
             mFrequency = beep ? 1000 : 0;
         }
     }
 }

이제 한 가지 의문이 생긴다. 왜 앞에서는 onExecute() 메소드가 호출되는 횟수를 세어가면서 복잡하게 했을까?

우리가 로봇의 각 디바이스에 쓴 데이터는 onExecute() 메소드가 완료된 후에 하드웨어 로봇으로 전달된다는 것을 기억할 것이다. 따라서, onExecute() 메소드 내에서는 디바이스에 데이터를 쓰는 중간에 하드웨어 로봇으로 데이터가 전달되는 일이 없다. 동기화, 즉 모든 디바이스의 데이터가 동시에 전달되는 것은 실제 로봇에서는 굉장히 중요하다.

쓰레드를 만들어서 돌리거나 onExecute() 메소드가 아닌 다른 곳에서도 각 디바이스에 데이터를 쓸 수 있지만 동기화는 보장되지 않는다. 예를 들어 쓰레드 내에서 좌우 바퀴에 데이터를 쓰는 경우에 왼쪽 바퀴의 데이터는 이번에 전달되고, 오른쪽 바퀴의 데이터는 그 다음에 전달될 수도 있다.

따라서, 동기화가 중요한 경우에는 반드시 onExecute() 메소드 내에서 데이터를 쓰도록 하자.

04. 배고파요

귀엽고 예쁜 알버트 로봇이 배고프면 얼른 밥을 주어야 할 것 같다. 그렇다고 항상 밥을 줄 수는 없으니 배가 고픈지를 알아보도록 하자.

로봇에게 전원은 생명과 같다. 배터리가 소진되지 않도록 주의를 기울여야 하고, 배터리의 상태에 따라 로봇의 움직임이 달라진다는 점을 이해해야 한다. 사람이 배고프면 힘이 없어 걸음이 느려지는 것과 같다. 알버트 로봇의 배터리 상태 변화는 로봇마다 약간씩 차이가 있을 수 있지만 배터리를 완전히 충전한 후 연속 동작으로 5시간 정도 사용할 수 있다.

알버트 로봇에는 내장된 배터리의 상태를 알려 주는 센서 디바이스로 SENSOR_BATTERY가 있다. 0부터 100까지의 값을 가지며 배터리의 잔량을 %로 나타낸다. 배고픔의 정도를 표시하기 위해 배터리 값이 25보다 크면 눈의 색상을 녹색으로, 15부터 25까지는 노란색으로, 15보다 작으면 빨간색으로 표시해 보자.

 public class SampleActivity extends RobotActivity
 {
     private Device mBatteryDevice;
     private Device mLeftEyeDevice;
     private Device mRightEyeDevice;

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

     @Override
     public void onInitialized(Robot robot)
     {
         mBatteryDevice = robot.findDeviceById(Albert.SENSOR_BATTERY);
         mLeftEyeDevice = robot.findDeviceById(Albert.EFFECTOR_LEFT_EYE);
         mRightEyeDevice = robot.findDeviceById(Albert.EFFECTOR_RIGHT_EYE);
     }

     @Override
     public void onExecute()
     {
         int battery = mBatteryDevice.read();
         int r = 0, g = 0, b = 0;
         if(battery > 25)
             g = 255;
         else if(battery < 15)
             r = 255;
         else
         {
             r = 255;
             g = 255;
         }
         mLeftEyeDevice.write(0, r);
         mLeftEyeDevice.write(1, g);
         mLeftEyeDevice.write(2, b);
         mRightEyeDevice.write(0, r);
         mRightEyeDevice.write(1, g);
         mRightEyeDevice.write(2, b);
     }
 }

05. 체온이 몇 도일까?

알버트 로봇에는 내부의 온도를 알려 주는 온도 센서 디바이스로 SENSOR_TEMPERATURE가 있다. -40부터 88까지의 값을 가지며 온도를 섭씨(oC)로 나타낸다. 알버트 로봇이 열이 나는지 체온을 측정해 보도록 하자.

 public class SampleActivity extends RobotActivity
 {
     private Device mTemperatureDevice;

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

     @Override
     public void onInitialized(Robot robot)
     {
         mTemperatureDevice = robot.findDeviceById(Albert.SENSOR_TEMPERATURE);
     }

     @Override
     public void onExecute()
     {
         int temperature = mTemperatureDevice.read();
     }
 }