Sending a Message over the Radio

Blink를 다시 첨부터 구현하여 복습 하면서 NesC문법을 읽히도록 하며, 거기에 무선 메세지 송수신을 부가 하도록 하겠습니다.

먼저 디렉토리 하나 생성 합니다.

cd tinyos-2.x/apps
mkdir BlinkRadio

그담엔 BlinkToRadioC.nc를 생성 하여 작성 합니다.

#include <Timer.h>
#include "BlinkToRadio.h"

module BlinkToRadioC {
  uses interface Boot;
  uses interface Leds;
  uses interface Timer<TMilli> as Timer0;
}
implementation {
  uint16_t counter = 0;

  event void Boot.booted() {
    call Timer0.startPeriodic(TIMER_PERIOD_MILLI);
  }

  event void Timer0.fired() {
    counter++;
    call Leds.set(counter);
  }
}


이 부분의 내용은 그렇게 어려운게 없습니다.NesC의 문법은 C와 거의 비슷 합니다. 알아두실건 <>의 include 의 경우 기본 TOS 디렉토리인 tos이하에서 찾는다는 내용입니다. " " 의 경우 현재 디렉토리에서 파일을 검색 합니다.
* Makefile에서 -I 옵션을 사용하여 디렉토리를 추가 하실수 도 있습니다.

TIMER_PERIOD_MILLI 가 정의 되어 있는 헤더파일도 작성 합니다.

#ifndef BLINKTORADIO_H
#define BLINKTORADIO_H

enum {
  TIMER_PERIOD_MILLI = 250
};

#endif

물론 헤더 파일의 경우 C 파일 처럼 작성 가능 합니다. 구조체 등은 헤더파일에 작성 하시면 됩니다.

BlinkRadioC의 경우 기능을 담당 하고 있습니다. 그럼 구성 파일도 있어야 겠죠? BlinkRadioAppC.nc 파일도 작성 합니다.

#include <Timer.h>
#include "BlinkToRadio.h"

configuration BlinkToRadioAppC {
}
implementation {
  components MainC;
  components LedsC;
  components BlinkToRadioC as App;
  components new TimerMilliC() as Timer0;

  App.Boot -> MainC;
  App.Leds -> LedsC;
  App.Timer0 -> Timer0;
}

이제 Blink와 완전히 똑같이 동작 할 겁니다.

마지막으로 Makefile을 작성 합니다.
COMPONENT=BlinkToRadioAppC
include $(MAKERULES)

COMPONET=BlinkToRadioAppC는 top-configuration 파일을 지칭 하여 컴파일을 시작 하게 만듭니다.

그럼 작성 하고 테스트 해보세요.

Define a Message Structure

이제 본격적으로 무선 통신을 시작 하겠습니다. 앞서 전송 보낼 프로토콜을 정의 하기위해 구조체를 사용 합니다. message_t 에 바로 접근 하는것 보다는 여러가지 편한 점을 제공 하기 때문에 구조체를 사용하며 카피 하는 방식으로 사용 하도록 하겠습니다.
typedef nx_struct BlinkToRadioMsg {
  nx_uint16_t nodeid;
  nx_uint16_t counter;
} BlinkToRadioMsg;

우선 nx_ 가 생소 할겁니다. 이건 CPU 에따라 빅에디언 또는 리틀 에디언 방식을 취하기때문에 서로 다른 플랫폼 끼리 통신할 경우 똑같은 방식을 제공하기 위해 사용 합니다. 그리니 nx를 꼭 적어 주시길 바랍니다.

Sending Message
이제 메세지를 선언했으니 송신해 보도록 하겠습니다.
이제 정해진 시간에 따라 LED가 깜빡이게 되어 있으니 카운터값 즉 Value를 송신 하겠습니다.

1. message_t와 무선 통신 기능 제공해주는 인터페이스를 찾습니다.
여기서는 AMSender를 사용 하도록 하며, AMSenderC컴포넌트를 사용 하도록 하겠습니다. 하지만 무선 모듈을 초기화 하기위해서는 ActiveMessageC.SplitControl 인터페이스를 사용 해야 합니다.

AMSenderC는 추상 계층입니다. 이전 버전에는 추상 계층을 사용 하지 않고 무선 모듈에 다중 와이어를 해서 사용 하여 한쪽이 사용할때 인식을 못하는 문제가 있었습니다.

2. 사용할 무선 통신 인터페이스를 BlinkToRadioC.nc 컴포넌트에 uses 란에 선언합니다.
module BlinkToRadioC {
  ...
  uses interface Packet;
  uses interface AMPacket;
  uses interface AMSend;
  uses interface SplitControl as AMControl;
}

SplitControl은 as를 사용하여 재명명 하였습니다. 똑같은 이름을 가진 인터페이스를 제공 하는 모듈이 있을 수 있기때문에 구분할때 사용 할 수 있습니다. SplitControl은 ActiveMessageC를 컨트롤 하기 위해 필요합니다.

3. 변수와 초기화 작업을 추가 합니다.
우선 송신할 메세지 버퍼와 무선 통신을 제어할 변수 두개를 추가 합니다.

bool busy=FALSE;
message_t pkt;

무선 모듈을 초기화 합니다. 여기서 유의 할점은 타이머 이벤트마다 무선모듈을 사용 하기 때문에 무선 모듈이 초기화가 완료 될때 타이머를 시작 하도록 합니다.
event void Boot.booted() {
    call AMControl.start();
  }
무선 모듈을 초기화
event void AMControl.startDone(error_t err) {
    if (err == SUCCESS) {
      call Timer0.startPeriodic(TIMER_PERIOD_MILLI);
    }
    else {
      call AMControl.start();
    }
  }

  event void AMControl.stopDone(error_t err) {
  }

위와같이 startDone 이벤트 이후 타이머를 시작 하며, error_t 의 경우 성공 일 겨우 SUCCESS를 반환 합니다.

4. 이제 추가한 인터페이스 기능 즉 무선통신 기능을 추가 합니다.

event void Timer0.fired() {
  ...
  if (!busy) {
    BlinkToRadioMsg* btrpkt = (BlinkToRadioMsg*)(call Packet.getPayload(&pkt, NULL));
    btrpkt->nodeid = TOS_NODE_ID;
    btrpkt->counter = counter;
    if (call AMSend.send(AM_BROADCAST_ADDR, &pkt, sizeof(BlinkToRadioMsg)) == SUCCESS) {
      busy = TRUE;
    }
  }
}

타이머가 발생 할때 마다.  getPayload를 사용하여 데이타부분을 가져와 구조체로 바꾸어 데이타를 버퍼에 채우는 코드입니다. 그리고 send이벤트를 사용하여 브로드 캐스트로 메세지를 송신 하고 있습니다. busy=true;코드를 눈여겨 보셔야 합니다. send 명령을 사용 하는 순간 pkt는 무선 컴포넌트의 것이 됩니다. 그러니 끝나기 전에 다시 접근 하지 마시길 바랍니다.

5. 사용한 인터페이스의 이벤트를 구현 합니다.
Packet.AMPacket,AMSend 인터페이스를 보면 AMSend.sendDone 이벤트하나 뿐입니다.

event void AMSend.sendDone(message_t* msg, error_t error) {
    if (&pkt == msg) {
      busy = FALSE;
    }
  }

sendDone 이벤트는 send 컨맨드를 사용한 컴포넌트에게 송신시 사용한 메세지의 포인터와 함께 반환 됩니다. 그래서 sendDone이벤트에서 버퍼 사용을 해제 합니다.

* &pkt == msg 의 경우 다른 컴포넌트가 AMSend를 쓸경우 같이 이벤트를 받습니다. 그래서 구분을 해줘야 합니다.

6. 이제 구성 파일을 수정 합니다. 사용하는 인터페이스의 컴포넌트를 선언하며 적절하게 와이어링 작업을 합니다.

implementation {
  ...
  components ActiveMessageC;
  components new AMSenderC(AM_BLINKTORADIO);
  ...
}

여기서 ActiveMessagC의 경우 하드웨어 플랫폼에 따라 하나 만 존재 하면 되지만 AMSenderC의 경우 무선 모듈을 통해 송신 하는 모듈의 경우 다중으로 사용 할 수 있습니다. new은 AMSenderC의 객체를 하나 생성 하는 작업입니다. AM_BNLIKTORADIO는 AM_TYPE을 구분 합니다. 즉 AMSenderC를 사용하는 컴포넌트를 구분 할수 있습니다. 헤더 파일에 AM_BLINKTORADIO를 정의 해주도록 합니다.


enum {
  AM_BLINKTORADIO = 6,
  TIMER_PERIOD_MILLI = 250
};

7. 사용하는 모듈을 와이어링 합니다.
implementation {
  ...
  App.Packet -> AMSenderC;
  App.AMPacket -> AMSenderC;
  App.AMSend -> AMSenderC;
  App.AMControl -> ActiveMessageC;
}

보내는 쪽은 끝입니다.





, .

이제 슬 무선 통신을 해보도록 하겠습니다.

이번장에서 배울것.
 - message_t 메세지 버퍼
 - 무선을 통해 메세지 버퍼 송신
 - 무선을 통해 메세지 버퍼 수신

Introduction

TinyOS는 기본적인 통신을 지원 하기 위한 추상 인터페이스와 컴포넌트를 제공 합니다. 이 인터페이스들와 컴포넌트는 message_t라는 추상 메세지 버퍼를 사용 합니다.(message_t는 일반 C와 비슷한 nesC 구조체로 되어 있습니다.) message_t 구조체는 직접 접근은 되지 않으며 제공되는 acessor 나 mutator function을 사용 하여 접근 가능 합니다.

Basic Communications Interfaces

message_t를 사용하는 기본적인 인터페이스는 다음과 같습니다.(tos/interface 폴더에 위치 하고 있습니다.)

* Packet - message_t 구조체를 접근 하기위한 인터페이스입니다.,패킷의 길이, 패킷의 데이타 포인터등을 제공 합니다.
* Send - 주소와 관계없는 메세지 송신에 사용 하는 인터페이스 입니다.
* Receive - 패킷 송신에 관련된 인터페이스 입니다. receive이벤트를 가지고 있으니 꼭 구현 해주어야 합니다. 수신패킷에 접근 할 수 있는 기능도 제공 합니다.
* PacketAcknowledgements - 패킷에 기반하여 ACK 기능을 제공 합니다.
* RadioTimeStamping - 패킷 송수신에 대한 시간 정보(?)를 제공 합니다.

Active Message Interface
TinyOS의 인터넷의 패킷 처럼 무선 전송시 다중 접근 할 수 있는 AM layer를 제공 합니다.(이해가 좀 어렵습니다. 인터넷의 그것 처럼 IP,UDP 이런식으로 구조화 되어 있어 다중 레이어로 되어 있다 생각 하면 될것 같습니다.) 위와 달리 AM은 목적지 주소를 가지고 있어 유니캐스트를 지원 합니다. 그리고 AM에 대한 인터페이스 역시 tos/interface에 있습니다.

* AMPacket - 위 Packet과 비슷한 기능입니다, 추상 구조체인 message_t를 AM 으로 제공 합니다. 목적지를 설정하거나 체크 하는등의 기능을 제공 합니다.
* AMSend - Send와 비슷하나, 목적지 주소(AM Address를 가지고 한개의 노드를 향해 메세지를 송신 합니다.

AM address는 컴파일시 결정되나, ActiveMessageAddressC에서 바꿀 수 있습니다.

Components
TinyOS 2.0에서 무선 통신을 위해 기본적으로 제공 하는 컴포넌트들 입니다. tos/system 에 위치 하고 있습니다.

* AMReceiverC - Receive,Packet,AMPacket을 제공
* AMSenderC - AMSend,Packet,AMPacket,PacketAcknowlegements,Acks를 제공
* AMSnooperC - Receive,Packet,AMPacket을 제공
* AMSnooperReceiverC - Receive,Packet,AMPacket을 제공
* ActiveMessageAddresC- Acitve Adress 체크 및 설정에 사용 합니다.주소 재설정을 사용시에는 확실히 알고 하셔야 합니다.

Naming Wrappers
TinyOS는 여러 플랫폼 지원을 위해 송수신 관련 ActiveMessageC Warpper로 하위 추상 클래스를 가지고 있습니다. ActiveMessageC는 위 대부분의 인터페이스를 가지고 있습니다. 각 플랫폼 별 ActiveMessageC는 다음과 같습니다.

* eyesIFX - Tda5250ActiveMessageC
* telosa,telosb,micaz,intelmote - CC2420ActiveMessageC
* mica - CC1000ActiveMessageC
(* ActiveMessageC를 통해 모듈을 추상화 했다고 보면 됩니다.)

TinyOS 2.0 Message Buffer
TinyOS 2.0에서는 기존 메세지 TOS_Msg를 대체하는 message_t를 가지고 있습니다. tos/types/message.h에 선언 되어 있습니다.

typedef nx_struct message_t {
  nx_uint8_t header[sizeof(message_header_t)];
  nx_uint8_t data[TOSH_DATA_LENGTH];
  nx_uint8_t footer[sizeof(message_header_t)];
  nx_uint8_t metadata[sizeof(message_metadata_t)];
} message_t;

위 구조중 header,footer,metadata는 수정 불가 입니다. 이유는 data의 포인터 위치 고정 입니다.


, .

내용이 많은건 아닌데.. 블로그 특징상 길게 쓰면 이상해 보여요 ㅜ.ㅜ; 절대 내용 많은거 아님

Internal Function

이부분은 아주 간단한 내용입니다. 일반 C 언어 처럼 함수를 선언해서 쓸수 있다는 내용 입니다. 함수의 형식은 일반 C를 따르며, command, event등을 쓰지 않으며, 다른 컴포넌트는 내부 함수는 호출 할 수 없습니다.

예는 다음과 같습니다.

module BlinkC {
  uses interface Timer<TMilli> as Timer0;
  uses interface Timer<TMilli> as Timer1;
  uses interface Timer<TMilli> as Timer2;
  uses interface Leds;
  uses interface Boot;
}
implementation
{

  void startTimers() {
    call Timer0.startPeriodic( 250 );
    call Timer1.startPeriodic( 500 );
    call Timer2.startPeriodic( 1000 );
  }

  event void Boot.booted()
  {
    startTimers();
  }

  event void Timer0.fired()
  {
    call Leds.led0Toggle();
  }
 
  event void Timer1.fired()
  {
    call Leds.led1Toggle();
  }
 
  event void Timer2.fired()
  {
    call Leds.led2Toggle();
  }
}

보시다 시피 startTimers() 란 내부함수가 있으며, 같은 컴포넌트 내에서는 제한 없이 사용 할 수 있습니다.

물론 함수를 사용 하기전에 정의는 되어 있어야겠죠.

Split-Phase Operations

command, evnet, 외 중요한 개념입니다. NesC는 와이어 처리를 컴파일시 하게 되어 있어 콜백 즉 event 시스템은 TinyOS에서도 매우 효율적으로 동작 할 수 있습니다.

Blocking 실행을 해야 하기 때문에 TinyOS에서 컴포넌트를 넘어가며 최적화 하는 능력은 매우 중요합니다. Blocking 시스템(절차적 언어)에서는 프로그램이 매우 긴 수행시간의 함수를 실행시 함수가 실행이 종료될때 까지 시스템이 멈처진 상태에 있게 됩니다. 하지만 Split-Phase 시스템에서는 함수는 최대한 빠르게 리턴되며 Operaion이 종료된 후에는 콜백을 사용 하여 "완료" 신호를 날리도록 되어 있습니다.

Blocking Split-Phase
if (send() == SUCCESS) {
  sendCount++;
}
// start phase
send(); 

//completion phase
void sendDone(error_t err) {
  if (err == SUCCESS) {
    sendCount++;
  }
}


Split-Phase의 이점은 스택메모리를 실행 될 동안 점유 하지 않으며, 함수의 실행 종료를 기다리지 않으므로 시스템의 정체를 줄이며, 스택의 활용도를 늘리게 됩니다.

Split-Phase 시스템은 TinyOS가 여러개의 작업을 동시에 수행 할 수 있게 하며 메모리를 절약 시킵니다.

Timer.startOneShot 은 Split-Phase의 좋은 예입니다.

사용자 삽입 이미지

위와 같이 Timer를 절차적으로 사용 하게 되면 Sleep()을 사용하게 되며, 그동안 시스템은 멈추게 됩니다 하지만 Split-Phase에서는 startOneShot()함수 호출후 다른 작업을 수행 할 수 있으며, 타이머는 정해진 시간에 시그널을 날리게 되어 있습니다.

, .
중요한 개념인 Task를 구현 합니다. 1.X에서는 상당히 애매한 상황이 벌여 졌지만 앞서 말한 TASK가 각 각 슬롯을 가짐으로 확실해 졌습니다.

TASK
이때까지 본 소스는 동시성과는 거리가 먼소스들이었습니다. 동시 실행 코드가 실행 되고 있을 동안에는 선점이 되지 않습니다. 이 메카니즘은 TinyOS 스케줄러의 자원 소모를 줄였으나 동시 실행 코드는 매우 작은 크기여야 합니다. 즉 동시 실행시 선점 되지 않기 때문에 즉각 반응 시스템엔 역효과를 내는 것입니다. 예로는 긴 코드가 실행 되어 패킷에 반응 하는 속도가 늦어 질 수 있는 것입니다.

이때까지 코드는 아주 짧은 실행 코드만을 지녀 별문제 없이 작동 했습니다. 하지만 매우 긴 시간을 요구하는 실행 코드의 경우 선점 되지 않는 문제는 많은 문제를 야기 할 수 있습니다. 그래도 2.0의 경우 매우 긴 시간을 요구하는 코드의 경우 여러부분으로 나눌것을 권장 합니다.

Task - Task는 어플리케이션에 백그라운드 프로세싱을 가능하게 합니다. 이정도로 생각 하시면 됩니다. OS에서 이야기하는 bottom halves와 deferred procedure call과 비슷 합니다.(*하드웨어 인터럽트가 먼저라 생각 하시면 됩니다.)

이제 Task를 실습 하도록 하겠습니다. cp 명령어를 사용 하여 새로운 BlinkC 백업 본을 만듭니다.(BlinkTask)

cp -R Blink BlinkTask

Timer0.fired() 이벤트를 수정하여 작업을 좀 넣도록 하겠습니다.

event void Timer0.fired()
  {
    uint32_t i;
    dbg("BlinkC", "Timer 0 fired @ %s.\n", sim_time_string());
    for(i =0; i<400001;i++){
      call Leds.led0Toggle();
    }
  }

대충 이작업은 MicaZ , Telosb 계열 모트가 20번의 패킷을 보낼 수 있는 시간의 작업을 수행 합니다. 퓨징 하시면 지연으로 인해 이상하게 동작 하는 모트를 보실 수 있습니다.

동작의 결과를 설명 드리면 Timer0에서 발생하는 연산이 Timer이벤트들의 실행을 방해 하며 결국은 Timer0의 연산이 끝나는 시점에서 동시에 발생 하게 되는것이다. 그래서 두개가 계속 나란히 켜지면 한개만 켜지는 경우는 없게 된다.

이문제를 해결 하기 위해서는 백그라운드로 저 작업을 실행 하면된다.

Task의 정의는 다음과 같다.

task void taskname(){}

Task는 인자를 가질수도 리턴값을 가질수도 없다. 그리고 Task를 실행 할때는 post를 사용한다.

post taskname();

여기서 중요한거 컴포넌트의 command,task,event는 task를 포스팅 할 수 있다. 그리고 이벤트 시그널을 위해서는 Task를 사용 하기 바란다. command에서 event를 사용하는건 루프를 발생 시킬 소지가 매우크다. 나중에 설명함(예로 인터페이스 I 제공 하는 컴포넌트 A 사용하는 B가 있을 경우 B의 이벤트에 핸들러에서 A의 command를 실행 하며 그 command에서 시그널을 날린다면 바로 루프가 발생하는 것이다.)

task를 다음과 같이 수정하여 만든다.

 task void computeTask(){
    uint32_t i;
    for(i=0;i<400001;i++){}
  }
  event void Timer0.fired()
  {
      call Leds.led0Toggle();
      post computeTask();
  }

그럼 TelosB는 아직 이상하지만 MicaZ는 작동 잘 할 것이다.

post는 Task를 큐에 FIFO 방식으로 넣는 역활을 한다. Task는 긴 수행시간을 가져서는 안된다. Task는 서로는 선점 하지 못하지만, 하드웨어 인터럽트는 Task를 선점 할수 있다. 긴시간의 수행시간을 가지는 Task의 경우 나누도록 한다. post 명령은 error_t 를 반환한다. (task가 반환 하는것이 아닌 post명령이다.) 이경우는 Task가 벌써 성공적으로 스팅 된 경우 이다.(물론 아직 실행은 안되었다.)

TelosB의 경우 아직 Task 가 많은 시간을 소모 하기 때문에 정상 작동 하지 않는 것이다. 다음과 같이 수정한다.

외적으로 static 변수를 사용하는건 생략 한다. 대략 컴포넌트안의 변수는 모두 Private이기때문에 static를 사용하는건 의미가 없다.
, .

이제 부터 공부의 속력을 내기 위해 요약 해석 할 예정입니다.

속도가 넘 안나네요.

Interfaces,Commands,and Events

Lesson1의 경험으로 인터페이스를 사용하는 컴포넌트는 인터페이스의 command를 실행 할 수 있으며, 역으로 이벤트는 받아서 처리 해야 한다는걸 알 수 있게 되었습니다. 그럼 BlinkC가 사용 하는 인터페이스를 분석 하도록 하겠습니다. BlinkC가 사용 하는 인터페이스는 Timer,Leds,Boot가 있습니다.

tos/interfaces/Boot.nc:
interface Boot {
  event void booted();
}

tos/interfaces/Leds.nc:
interface Leds {

  /**
   * Turn LED n on, off, or toggle its present state.
   */
  async command void led0On();
  async command void led0Off();
  async command void led0Toggle();

  async command void led1On();
  async command void led1Off();
  async command void led1Toggle();

  async command void led2On();
  async command void led2Off();
  async command void led2Toggle();

  /**
   * Get/Set the current LED settings as a bitmask. Each bit corresponds to
   * whether an LED is on; bit 0 is LED 0, bit 1 is LED 1, etc.
   */
  async command uint8_t get();
  async command void set(uint8_t val);
 
}

tos/interfaces/Timer.nc:
interface Timer
{
  // basic interface
  command void startPeriodic( uint32_t dt );
  command void startOneShot( uint32_t dt );
  command void stop();
  event void fired();

  // extended interface omitted (all commands)
}

Looking over the interfaces for Boot, Leds, and Timer, we can see that since BlinkC uses those interfaces it must implement handlers for the Boot.booted() event, and the Timer.fired() event. The Leds interface signature does not include any events, so BlinkC need not implement any in order to call the Leds commands. Here, again, is BlinkC's implementation of Boot.booted():


apps/Blink/BlinkC.nc:
  event void Boot.booted()
  {
    call Timer0.startPeriodic( 250 );
    call Timer1.startPeriodic( 500 );
    call Timer2.startPeriodic( 1000 );
  }

그럼 여기서 BlinkC가 꼭 구현해야 하는 이벤트는 어떤것이 있을까요? 위에서 보면 알 수 있으시겠지만.
여기서 구현해야 하는 Event는 Boot.booted() Timer.fired() 이벤트가 있습니다.그럼 BlinkC가 구현 하고 있는 Boot.booted() 이벤트를 보도록 하죠.

apps/Blink/BlinkC.nc:
  event void Boot.booted()
  {
    call Timer0.startPeriodic( 250 );
    call Timer1.startPeriodic( 500 );
    call Timer2.startPeriodic( 1000 );
  }

BlinkC는 세개의 타이머 객체를 사용하고 있으며, booted() 이벤트는 세개를 시작 시키는 역활을 하고 있습니다. startPeriodic 은 타이머를 시작 시키며, ()안의 숫자는 MillicSecond를 의미합니다.(<TMilli> 객체 생성시 결정) ()안의 숫자와 객체 생성시 정해진 단위로 타이머는 발생됩니다.

call이란 키워드는 command를 실행 할때 쓰며, 역으로 이벤트 전달시에는 signal을 사용합니다. 일단 BlinkC에는 없으니 넘어가도록 하겠습니다.

 event void Timer0.fired()
  {
    dbg("BlinkC", "Timer 0 fired @ %s.\n", sim_time_string());
    call Leds.led0Toggle();
  }
 
  event void Timer1.fired()
  {
    dbg("BlinkC", "Timer 1 fired @ %s \n", sim_time_string());
    call Leds.led1Toggle();
  }
 
  event void Timer2.fired()
  {
    dbg("BlinkC", "Timer 2 fired @ %s.\n", sim_time_string());
    call Leds.led2Toggle();
  }

앞에서 언급 한것 처럼 이벤트의 처리는 사용자에서 하도록 되어있으므로 위와 같이 BlinkC에서 Timer.fired() 이벤트를 처리 해야 합니다. 그리고 함수의 선언은 interface.function 식으로 구분을 확실히 해야 합니다.

, .