2016년 9월 2일 금요일

ArdunityController 만들기

ArdunityController는 아두이노에 연결된 회로를 유니티에서 제어할 수 있는 기능입니다. 아두니티에는 많이 사용할만한 ArdunityController가 포함되어있습니다.
현재 포함되어있는 대표적 ArdunityController는 다음과 같습니다.
  • DigitalOutput: 아두이노 보드 Pin의 디지털 출력 제어
  • DigitalInput: 아두이노 보드 Pin의 디지털 입력 확인
  • AnalogOutput: 아두이노 보드 Pin의 PWM 출력 제어
  • AnalogInput: 아두이노 보드 Pin의 ADC 입력 확인
  • GenericServo: 아두이노 Servo 라이브러리를 이용한 서보 모터 제어
  • GenericTone: 아두이노 Tone라이브러리를 이용한 Buzzer 제어
  • GenericMotor: H-bridge 회로기반 DC 모터 제어 (Deluxe 버전)
  • MPUSeries: MPU-XXXX기반 AHRS 센서 입력 확인 (Deluxe 버전)
위 기능만으로도 많은 것을 할 수 있지만, 프로젝트를 진행하다보면 필요한 기능이 생기게 됩니다. 이런 경우 필요한 기능이 아두니티가 업데이트되면서 추가된다면 가장 좋겠지만, 그렇지 않다면 매우 난감할 것입니다. 본 포스트는 직접 ArdunityController를 제작하는 방법에 대해 소개하여 필요한 기능을 직접 개발하는데 도움이 되고자 합니다.
우선, 본 포스트의 내용을 이해하기 위해 필요한 배경지식은 다음과 같습니다.
  • C++에 대한 이해
  • 유니티 C# Script에 대한 이해
위 개념에 대해 여기서는 다루지 않으니 다른 방법을 통해 기초를 익히기를 권장합니다.

아두니티는 아두이노와 유니티를 연결해주는 소프트웨어입니다.

이 말은 아두니티 안에는 아두이노용 소프트웨어와 유니티용 소프트웨어가 포함되어있다는 뜻입니다.
따라서, ArdunityController를 만들기 위해서는 아두이노용 ArdunityController와 유니티용 ArdunityController를 만들어야 합니다.


아두이노용 ArdunityController 만들기
아두이노는 소프트웨어 개발 시 C/C++를 사용하므로 ArdunityController도 마찬가지로 C/C++로 개발해야 합니다.
ArdunityController는 다음과 같이 C++ Class 형태를 가집니다.

class MyController
{
 public:
   MyController();

 protected:

 private:

};

이 Class는 별도의 파일로 존재해야 하므로 컴파일 시 중복 Include 에러가 나지 않도록 전처리문(Preprocessor)을 추가해야 합니다.

#ifndef MyController_h
#define MyController_h


class MyController
{
 public:
   MyController();

 protected:

 private:

};

#endif

그 다음으로 할 일은 이미 기본 기능이 만들어져있는 ArdunityController Class를 상속(Inheritance)받는 것입니다.

#ifndef MyController_h
#define MyController_h

#include "ArdunityController.h"

class MyController : public ArdunityController
{
 public:
   MyController(int id);

 protected:
   void OnSetup();
   void OnStart();
   void OnStop();
   void OnProcess();
   void OnUpdate();
   void OnExecute();
   void OnFlush();

 private:

};

#endif

ArdunityController Class를 상속받을 때 해야 할 일은 다음과 같습니다.
  • Controller ID 초기화
  • 가상 함수(Virtual Function)의 구현
ArdunityController Class의 가상 함수는 다음과 같은 역할을 합니다.
  • OnSetup: 아두이노 setup 실행 시 호출
  • OnStart: 유니티와 연결되었을 때 호출
  • OnStop: 유니티와 연결이 끊어졌을 때 호출
  • OnProcess: 아두이노 loop 실행 시 호출
  • OnUpdate: 유니티로부터 데이터를 받았을 때 호출
  • OnExecute: 유니티로부터 받은 데이터를 제어에 사용할 때 호출
  • OnFlush: 유니티에 데이터를 전송할 때 호출

Controller ID를 초기화하는 좋은 방법은 Class 생성시 인자(Argument)로 받는 것입니다.
여기서 꼭 결정해야 할 것은 유니티로 데이터를 보낼 것인지 여부이며, canFlush 변수로 설정할 수 있습니다.
  • 유니티로 데이터를 보낼 경우 canFlush는 TRUE입니다.
  • 유니티로 데이터를 보내지 않는 경우 canFlush는 FALSE입니다.

MyController::MyController(int id) : ArdunityController(id)
{
  canFlush = true; // If send data to Unity, must set true.
  canFlush = false; // If do not send data to Unity, must set false.
}

이제 할 일은 유니티와 공유할 데이터 타입을 결정하는 것입니다. 아두니티는 다음과 같은 데이터 타입을 지원합니다.
  • UINT8: 8bit 부호없는 정수 (0 ~ 255)
  • INT8: 8bit 부호있는 정수 (-128 ~ 127)
  • UINT16: 16bit 부호없는 정수 (0 ~ 65,535)
  • INT16: 16bit 부호있는 정수 (-32,768 ~ 32,767)
  • UINT32: 32bit 부호없는 정수 (0 ~ 4,294,967,295)
  • INT32: 32bit 부호있는 정수 (–2,147,483,648 ~ 2,147,483,647)
  • FLOAT32: 32bit 부동 소수
  • STRING: 문자열 (최대 길이 255)
이것은 ArdunityController에 다음과 같이 정의될 수 있습니다. 주의할 점은 STRING 타입을 사용할 때 실제 문자열을 저장할 버퍼(Buffer)는 별도로 선언해야 한다는 것입니다.

#ifndef MyController_h
#define MyController_h

#include "ArdunityController.h"

class MyController : public ArdunityController
{
 public:
   MyController(int id);

 protected:
   void OnSetup();
   void OnStart();
   void OnStop();
   void OnProcess();
   void OnUpdate();
   void OnExecute();
   void OnFlush();

 private:
   UINT8 uint8_data;
   INT8 int8_data;
   UINT16 uint16_data;
   INT16 int16_data;
   UINT32 uint32_data;
   INT32 int32_data;
   FLOAT32 float32_data;
   STRING string_data;

   char stringBuffer[50];
};

#endif

STRING 타입 변수와 버퍼를 연결하는 것은 Class 생성자에서 다음과 같이 할 수 있습니다.

MyController::MyController(int id) : ArdunityController(id)
{
  string_data = stringBuffer;

  canFlush = true; // If send data to Unity, must set true.
  canFlush = false; // If do not send data to Unity, must set false.
}

유니티로부터 데이터를 받은 것이 있다면, OnUpdate 함수가 실행됩니다. 그러므로 이때 여러분은 버퍼(Buffer)로부터 데이터를 꺼내서 변수에 옮기면 됩니다.
이 버퍼(Buffer)를 갖고 있는 것이 바로 ArdunityApp Class이며 pop함수를 이용해서 꺼낼 수 있습니다.
ArdunityApp Class를 사용하기 위해 Ardunity.h 헤더 파일을 포함시켜야 합니다.

#include "Ardunity.h"


void MyController::OnUpdate()
{
  ArdunityApp.pop(&uint8_data);
  ArdunityApp.pop(&int8_data);
  ArdunityApp.pop(&uint16_data);
  ArdunityApp.pop(&int16_data);
  ArdunityApp.pop(&uint32_data);
  ArdunityApp.pop(&int32_data);
  ArdunityApp.pop(&float32_data);
  ArdunityApp.pop(string_data, 50);

  updated = true; // It must be set true to run OnExecute
}

STRING타입 변수에 데이터를 옮길때는 미리 선언한 버퍼의 크기를 알려줘야 합니다.
OnUpdate에서 데이터 처리 시 주의 할 점은 꺼내는(Pop) 순서입니다. 이 순서는 유니티용 ArdunityController의 집어넣는(Push) 순서와 동일해야 합니다.
만약, 유니티에서 data1, data2, data3의 순서로 집어넣었다면, 아두이노에서 data1, data2, data3의 순서로 꺼내야 합니다.
데이터 수신이 완료되었다면 OnExecute를 실행하여 할 일을 하는 것입니다. OnExecute는 updated 변수가 true가 되어야 실행되므로 필요한 경우 updated 변수를 true로 설정해야 합니다. updated 변수는 OnExecute가 실행 된 후, 자동으로 false로 바뀝니다.

OnProcess 함수는 아두이노 보드 실행 중에 할 일을 수행하게 됩니다. 이 함수에서 할 일은 다음과 같이 생각할 수 있습니다.
  • 유니티에 보낼 데이터를 수집한다.
  • 주기적으로 해야 할 일을 처리한다.
OnProcess 함수는 유니티와 연결됨과 상관 없이 항상 실행되기 때문에 현재, 유니티와 연결 중인지 확인할 필요가 있습니다. started 변수는 이것을 확인할 수 있는 기능을 제공합니다.
유니티에 보낼 데이터 수집이 완료되었다면, dirty변수를 true로 만들어 OnFlush 함수가 실행되게 할 수 있습니다.

void MyController::OnProcess()
{   
  if(started)
  {
    // When connected to Unity

    dirty = true; // It must be set true to run OnFlush
  }
  else
  {
    // When disconnected to Unity
  }
}

이제 남은 것은 수집된 데이터를 유니티에 보내는 것입니다. 이것을 수행하기 위해 OnFlush 함수를 완성해야 합니다. 유니티에 데이터를 보내기 위해서는 ArdunityApp의 버퍼(Buffer)에 데이터를 집어넣는 것이며 ArdunityApp Class의 push 함수를 이용합니다.
OnUpdate 때와 마찬가지로 데이터를 집어넣는(Push) 순서가 중요합니다. 이 순서는 유니티용 ArdunityController에서 꺼내는(Pop) 순서와 동일해야 합니다.
만약, 아두이노에서 data1, data2, data3의 순서로 집어넣었다면, 유니티에서 data1, data2, data3의 순서로 꺼내야 합니다.
Pop때와는 달리 Push 실행 시 STRING 타입의 경우 버퍼의 크기를 알려줄 필요가 없는데, 그 이유는 문자열의 끝을 자동으로 인식해서 데이터 길이를 알아내기 때문입니다.

void MyController::OnFlush()
{
  ArdunityApp.push(uint8_data);
  ArdunityApp.push(int8_data);
  ArdunityApp.push(uint16_data);
  ArdunityApp.push(int16_data);
  ArdunityApp.push(uint32_data);
  ArdunityApp.push(int32_data);
  ArdunityApp.push(float32_data);
  ArdunityApp.push(string_data);
}

여기까지 했다면 아두이노용 ArdunityController 구현은 모두 완료한 것입니다. 마지막으로 해야 할 일은 만든 ArdunityController의 헤더 파일(*.h)과 소스 파일(*.cpp)을 유니티 에셋 폴더에 넣어두는 것입니다.
유니티 에셋 폴더에 넣는 이유는 유니티에 있는 ArdunityApp 컴포넌트가 아두이노 스케치를 자동 생성할 때 작성한 헤더 파일과 소스 파일을 아두이노 스케치 폴더에 복사하기 때문입니다. 아무데나 위치시키면 안되고 반드시 "Arduino" 이름의 폴더에 넣어두어야 합니다. 여러분이 만든 폴더도 상관없는데, 폴더의 이름만 "Arduino"로 만들면 됩니다.


유니티용 ArdunityController 만들기
유니티에서 C# Script를 생성하면 다음과 같은 모습을 가집니다.

using UnityEngine;
using System.Collections;


public class MyController : MonoBehaviour
{
  void Start()
  {
  }

  void Update()
  {
  }
}

다음으로 할 일은 기본 기능이 만들어져있는 ArdunityController를 상속(Inheritance)받는 것입니다.
ArdunityController는 namespace Ardunity에 있으므로 추가시켜야 합니다.

using UnityEngine;
using System.Collections;
using Ardunity;


public class MyController : ArdunityController
{
  protected override void Awake()
  {
  }

  void Start()
  {
  }

  void Update()
  {
  }

  protected override void OnPush()
  {
  }

  protected override void OnPop()
  {
  }

  protected override void OnExecuted()
  {
  }

  protected override void OnConnected()
  {
  }
  
  protected override void OnDisconnected()
  {
  }

  protected override void OnReset()
  {
  }

  public override string[] GetAdditionalFiles()
  {
    return null;
  }

  public override string[] GetCodeIncludes()
  {
    return null;
  }

  public override string[] GetCodeDefines()
  {
    return null;
  }

  public override string GetCodeDeclaration()
  {
    return "";
  }

  public override string GetCodeVariable()
  {
    return "";
  }
}

유니티용 ArdunityController를 만들기 위해서는 다음의 함수를 완성해야 합니다.
  • Awake: MonoBehaviour의 Awake함수
  • OnPush: 아두이노에 데이터를 보낼 때 실행
  • OnPop: 아두이노로부터 데이터를 받았을 때 실행
  • OnExecuted: 아두이노로부터 데이터를 제어에 사용할 때 실행
  • OnConnected: 아두이노와 연결되었을 때 실행
  • OnDisconnected: 아두이노와 연결이 끊어졌을 때 실행
  • OnReset: 아두이노 보드가 연결 중 리셋되었을 때 실행
  • GetAdditionalFiles: 아두이노 스케치 생성 시 실행
  • GetCodeIncludes: 아두이노 스케치 생성 시 실행
  • GetCodeDefines: 아두이노 스케치 생성 시 실행
  • GetCodeDeclaration: 아두이노 스케치 생성 시 실행
  • GetCodeVariable: 아두이노 스케치 생성 시 실행
유니티 C# Script의 기본인 MonoBehaviour은 최초 시작 시 실행되는 Awake 함수를 갖고 있습니다. ArdunityController는 이미 Awake함수를 사용하고 있기때문에 반드시 override해야 하며 ArdunityController의 Awake 함수를 실행해야 합니다.
여기서 꼭 결정해야 할 것은 아두이노로부터 데이터를 받을 것인지 여부이며, enableUpdate로 설정할 수 있습니다.
  • 아두이노로부터 데이터를 받을 경우 enableUpdate는 TRUE입니다.
  • 아두이노로부터 데이터를 받지 않는 경우 enableUpdate는 FALSE입니다.

protected override void Awake()
{
  base.Awake();

  enableUpdate = true; // If receive data from Arduino, must set true.
  enableUpdate = false; // If do not receive data from Arduino, must set false.
}

이제 할 일은 아두이노와 공유할 데이터 타입을 정하는 것입니다. 사용 가능한 데이터 타입은 아두이노용 ArdunityController와 동일합니다. 다만, C#에서는 using을 이용해서 사용할 데이터 타입을 정의해줘야 합니다. 이것은 ARDUnity/Scripts/Internal 폴더 안에 ControllerDataType.cs에 있으니 복사해서 사용하면 됩니다.

using INT8 = System.SByte;
using UINT8 = System.Byte;
using INT16 = System.Int16;
using UINT16 = System.UInt16;
using INT32 = System.Int32;
using UINT32 = System.UInt32;
using FLOAT32 = System.Single;
using STRING = System.String;


public class MyController : ArdunityController
{
  private UINT8 uint8_data;
  private INT8 int8_data;
  private UINT16 uint16_data;
  private INT16 int16_data;
  private UINT32 uint32_data;
  private INT32 int32_data;
  private FLOAT32 float32_data;
  private STRING string_data = "";



아두이노로부터 데이터를 받은 것이 있다면, OnPop 함수가 실행됩니다. 그러므로 이때 여러분은 버퍼(Buffer)로부터 데이터를 꺼내서 변수로 옮기면 됩니다. Pop 함수를 이용하면 이 작업을 수행할 수 있습니다.
데이터 처리 시 주의 할 점은 꺼내는(Pop) 순서입니다. 이 순서는 아두이노용 ArdunityController의 집어넣는(Push) 순서와 동일해야 합니다.
만약, 아두이노에서 data1, data2, data3의 순서로 집어넣었다면, 유니티에서 data1, data2, data3의 순서로 꺼내야 합니다.
데이터 수신이 완료되었다면 OnExecuted 함수가 실행되어 아두이노로부터 받은 데이터를 처리할 수 있습니다. OnExecuted 함수를 실행시키려면 updated 변수의 값을 true로 설정합니다.

protected override void OnPop()
{
  Pop(ref uint8_data);
  Pop(ref int8_data);
  Pop(ref uint16_data);
  Pop(ref int16_data);
  Pop(ref uint32_data);
  Pop(ref int32_data);
  Pop(ref float32_data);
  Pop(ref string_data);

  updated = true;
}

아두이노에 데이터를 보내려면 먼저 필요한 데이터를 수집해야 합니다. 유니티에서 데이터를 수집하는 가장 일반적인 방법은 Update 함수에서 처리하는 것입니다.
Update 함수는 아두이노 연결 유무와 상관 없이 항상 실행되므로 아두이노 연결 유무를 알 필요가 있습니다. connected 변수 값을 확인하면 아두이노 연결 유무를 알 수 있습니다.
유니티가 아두이노에 데이터를 보내려면 OnPush 함수가 실행되어야 합니다. OnPush 함수를 실행시키려면 SetDirty 함수를 호출하면 됩니다.

void Update()
{
  if(connected)
  {
    SetDirty();
  }
  else
  {
  }
}

OnPush 함수에서는 아두이노로 데이터를 보내면 됩니다. Push 함수를 이용해서 처리할 수 있으며 아두이노에서와 마찬가지로 순서가 중요합니다.
만약, 유니티에서 data1, data2, data3의 순서로 집어넣었다면, 아두이노에서 data1, data2, data3의 순서로 꺼내야 합니다.

protected override void OnPush()
{
  Push(uint8_data);
  Push(int8_data);
  Push(uint16_data);
  Push(int16_data);
  Push(uint32_data);
  Push(int32_data);
  Push(float32_data);
  Push(string_data);
}

이제 남은 것은 아두이노 스케치 생성입니다. 유니티용 ArdunityController는 아두이노용 ArdunityController를 사용할 수 있도록 아두이노 스케치를 작성해야 합니다. 유니티의 ArdunityApp은 Export Sketch 기능 실행 시 아두이노 스케치를 작성하면서 여러분이 만든 ArdunityController 차례가 올때마다 알려줍니다.
GetCodeDeclaration 함수는 아두이노용 ArdunityController Class를 선언시켜 줍니다. GetCodeVariable함수는 Class 선언 시 사용되는 인스턴스(Instance) 이름을 만들어 주기에 같이 사용하면 좋습니다.

public override string GetCodeDeclaration()
{
  return string.Format("{0} {1}({2:d});", this.GetType().Name, GetCodeVariable(), id);
}

public override string GetCodeVariable()
{
  return string.Format("myController{0:d}", id);
}

/* 위와 같이 구현한 경우 아두이노 스케치에는 다음과 같이 생성된다. */
/* Class: MyController, Controller ID: 0 */
MyController myController0(0);

아두이노 스케치에 추가 코드를 입력해야 하는 경우는 다음의 함수를 이용할 수 있습니다.

  • GetCodeDefines: "#define" 구문을 사용해야 하는 경우
  • GetCodeIncludes: "#include" 구문을 사용해야 하는 경우
아두이노 스케치 생성 시 아두이노용 ArdunityController Class를 위한 헤더 파일과 소스 파일은 자동으로 복사됩니다. 만약, 추가 파일을 복사해야 할 경우 GetAdditionalFiles 함수를 구현하면 필요한 파일을 복사할 수 있습니다.

만든 유니티용 ArdunityController는 Wire Editor 메뉴를 통해서 추가할 수 없습니다.(메뉴를 만드는 방법이 있으나 여기서는 다루지 않겠습니다.)
따라서, 다른 방법으로 컴포넌트를 추가해야 하는데, 다음과 같은 방법이 있습니다.
  • Component 메뉴를 이용 (Scripts에서 찾을 수 있음)
  • Add Component 메뉴를 이용 (Scripts에서 찾을 수 있음)
  • Project Window에서 직접 Script파일을 드래그하여 GameObject에 넣음
위와 같이 추가하면 Wire Editor와 Inspector Window에 다음과 같이 나타납니다.





댓글 5개:

  1. 아두니티의 데이터 타입을 설명하실 때 UNIT8은 UINT8의 오타이지 싶습니다.

    답글삭제
    답글
    1. 오타 맞습니다.
      지적해주셔서 감사합니다.
      오타는 모두 수정했습니다.

      삭제
  2. 작성자가 댓글을 삭제했습니다.

    답글삭제
  3. C:\Users\user\AppData\Local\Temp\cch2AlZh.ltrans0.ltrans.o: In function `__base_ctor ':

    sketch/controlling.cpp:5: undefined reference to `vtable for controlling'

    sketch/controlling.cpp:5: undefined reference to `vtable for controlling'

    collect2.exe: error: ld returned 1 exit status

    exit status 1
    Error compiling for board Arduino/Genuino Uno.


    ///해당된 코드를 따라 만들었는데, 이런 에러가 발생합니다.

    답글삭제
    답글
    1. 생성된 아두이노 Controller소스에 오류가 있는 것 같습니다.
      해당 소스를 봐야 어떤 문제가 있는지 알 수 있을 것 같네요.
      포럼에 주제를 생성하고 좀 더 자세히 질문해주시면 좋겠습니다.
      (https://groups.google.com/forum/#!forum/ardunity-forum-kor)

      삭제