2010년 6월 16일 수요일

mbed에 블루투스 모듈 연결 (Bluetooth interface for mbed)

mbed 에 블루투스를 추가해 보았다.
이번에 사용한 모듈은 myBleutooth-EX (27,100원) 라는 것으로 디바이스마트에서 구입하였다.
SPP를 지원하고 별다른 설정 없이 매우 쉽게 사용할 수 있다. 단 모듈에 연결하는 시리얼의 통신속도가 고정되어 있어 구입할 때 자신이 사용할 속도에 맞춰 구입해야 한다. 여기서는 모듈을 구입하러 갔을 때 38.4K밖에 재고가 없어 선택의 여지가 없었다.
 

CTS/RTS를 사용해서 플로우 컨트롤을 할 수도 있지만 그건 옵션이고 위의 5개 핀(Vcc, GND, TxD, RxD, RST)만 연결해주면 된다.



mbed 모듈로 자체적으로 USB시리얼을 지원하고 그 이외에 추가 시리얼포트를 가지고 있기 때문에 사용하기 매우 편리하다.


둘 다 2.54 피치의 핀헤더이기 때문에 만능기판에 와이어로 간단하게 연결해 주었다.


mbed 보드에서 USB 시리얼 <-> 블루투스 모듈에 연결된 시리얼 포트간에 서로 입력받은 데이터를 상대방에게 포워딩 해 주는 간단한 예제 프로그램을 만들어주면 바로 동작을 테스트 해 볼 수 있다.

#include "mbed.h"

Serial pc(USBTX, USBRX);
Serial bt(p28, p27);

int main() {
    char buf[256];
   
    bt.baud(38400);
   
    while(1) {
        if (pc.readable()) {
            bt.putc(pc.getc());
        }
   
        if (bt.readable()) {
            pc.putc(bt.getc());
        }
    }
}


mbed.org에서 IDE로 가서 위의 소스코드를 입력한다.


입력이 끝나면 Compile 버튼을 눌러 프로젝트를 빌드한다.

성공적으로 빌드가 끝나면 위와 같이 바이너리 파일이 생성되고 바로 컴퓨터로 다운받게 된다.

바이너리 파일의 이름을 지정하고 저장해주면 된다.


파일을 저장한 다음 mbed 보드를 USB포트에 연결하면 컴퓨터에 MBED라는 USB플래쉬 디스크가 만들어진다. 방금 다운한 바이너리 파일을 이 디스크에 복사해주면 된다. 복사가 끝나고 mbed 보드의 RESET 버튼을 눌러주면 가장 최근에 복사된 바이너리 파일이 mbed보드의 NXP 프로세서 프로그램 플래쉬로 복사되고 바로 실행된다. (별도의 플래쉬 라이팅 프로그램이 필요없어 매우 편리하다.)

단 테스트를 하기 전에 mbed보드에 전원을 연결하고 테스트 컴퓨터와 myBluetooth-EX 모듈간에 페어링을 해 줘야 한다. 컴퓨터와 디바이스간 페어링은 처음에 한번만 해 주면 이후에는 다시 할 필요가 없다. 아래는 맥에서 페어링 하는 예제이다.




테스팅 하는 구성은 다음과 같다.

컴퓨터에서 2개의 시리얼 터미널 프로그램을 실행시킨다.

첫번째 터미널은 블루투스 모듈과 SPP로 연결시킨다. 위에서 블루투스 모듈과 페어링 되면서 가상 시리얼 포트가 생성되었기 때문에 터미널 설정의 포트에 보면 myBluetooth-EX 가상포트가 보인다.


설정을 마치고 Connect 버튼을 눌러주면 블루투스 모듈과 커넥션을 만들기 위한 잠깐의 딜레이 후에 연결된걸 볼 수 있다.

두번째 터미널은 USB 시리얼 포트를 통해 mbed와 연결해 준다. mbed가 USB로 연결되면 usb serial 포트가 보이게 된다. 참고로 mbed의 USB 시리얼포트의 디폴트 통신속도는 9600 bps이다.

연결된 상태의 동작 예제이다. 위의 화면에서 왼쪽 터미널은 블루투스로 연결되었고 오른쪽은 USB 시리얼로 연결되었다. 각각 왼쪽 터미널에서 'bbb'를 눌렀고 오른쪽 터미널에서 'aaa'를 눌렀을 때 화면이다. 각각 상대방 쪽으로 데이터가 제대로 전송된 걸 확인할 수 있다.

다음번엔 Wiznet의 지원을 받아 임베디드 WLAN 모듈인 WIZ610wi를 연동시켜 보도록 하겠다.

2010년 6월 15일 화요일

mbed RPC 인터페이스 (RPC interface for mbed)



mbed는 인터페이스 오브젝트에 이름을 부여해주면 텍스트 기반 인터페이스를 통해 제어할 수 있다.

메소드는 url과 유사한 방식으로 호출된다.

/<objectname>/<method> <arg1> <arg2> ...

가장 기본적인 예제이다.

#include "mbed.h"

DigitalOut led(p1, "led");

int main() {
    char result[64];
    
    rpc("/led/write 1", result);
    printf("%s\n", result);
    
    rpc("/led/read", result);
    printf("%s\n", result);
}


Remote Control

mBed를 터미널에 연결하고 시리얼 포트 입력을 기다리도록 하면 시리얼을 통해 직접 명령어를 입력하고 그 결과를 돌려받을 수 있다.

#include "mbed.h"

Serial pc(USBTX, USBRX);

DigitalIn led(p1, "led");
AnalogIn pot(p20, "pot");

int main() {
    char buf[256], outbuf[256];
    while(1) {
        pc.gets(buf, 256);
        rpc(buf, outbuf);
        pc.printf("%s\n", outbuf);
    }
}

터미널이 엔터키가 눌렸을 때 LF(또는 CR+LF)를 보내도록 설정되어 있어야 한다.
 

위의 프로그램이 실행되고 있으면 터미널에서 직접 명령어를 넣어주면 된다.

/led/write 1<return>     LED를 켠다.
/pot/read<return>        아날로그 핀 20의 값을 읽는다.  

Remote Creation of mbed interfaces

RPC인터페이스를 사용하면 원격에서 인터페이스를 만들거나 삭제할 수 있다.


새 인터페이스를 만들려면 mbed에게 만들려고 하는 인터페이스에 대해 알려줘야 한다. 아래 코드는 원격 어플리케이션에서 이런 동작을 할 수 있도록 해 주는 시리얼 어플리케이션 예제이다.

/* RPCSerial - RPC over a serial port transport
 * sford
 *
 * When running, you can send RPC commands to the mbed
 * via the serial port, to create and manipulate objects
 *
 * Example:
 * > "/DigitalOut/new p2\n"
 * > > "objXYZ\n"
 * > "/objXYZ/write 1\n"
 * > "\n"
 * > "/objXYZ/read"
 * > "1\n"
 */
#include "mbed.h"
#include "rpc.h"
Serial pc(USBTX, USBRX);
int main() {
    // setup the classes that can be created dynamically
    Base::add_rpc_class<AnalogIn>();
    Base::add_rpc_class<AnalogOut>();
    Base::add_rpc_class<DigitalIn>();
    Base::add_rpc_class<DigitalOut>();
    Base::add_rpc_class<PwmOut>();
    Base::add_rpc_class<Timer>();
    Base::add_rpc_class<SPI>();
    Base::add_rpc_class<BusOut>();
    Base::add_rpc_class<BusIn>();
    char command[256] = {0};
    char response[256] = {0};
    // receive commands, and send back the responses
    while(1) {
        pc.gets(command, 256);
        rpc(command, response);
        pc.printf("%s\n", response);
    }
}


실행 예제 (볼드체로 된 부분이 직접 입력한 내용이고 이태릭체로 된 부분은 mbed의 응답이다.)

/DigitalOut/new LED1
obj400058B0
/obj400058B0/write 1



/AnalogIn/new p3 volume

volume
/volume/read
0.297
/volume/delete

시리얼 인터페이스 뿐 아니고 mbed에 내장된 웹서버를 사용해 HTTP 프로토콜로 RPC 호출을 할 수도 있다. URL의 페이지 주소가 '/rpc/'로 시작하면 웹 서버가 RPC 리퀘스트로 처리한다.


예를 들어 URL이 'http://mbed-ip-or-hostname/rpc/led1/write+1' 이면 시리얼 터미널에서 '/led1/write 1'을 입력한것과 동일한 동작을 수행한다. (URL주소에서 space는 '+'로 대치)


#include "mbed.h"
#include "HTTPServer.h"
#include "HTTPRPC.h"
#include "HTTPFS.h"
HTTPServer http;
DigitalOut led1(LED1, "led1");
DigitalOut led2(LED2, "led2");
DigitalOut led3(LED3, "led3");
DigitalOut led4(LED4, "led4");
LocalFileSystem local("local");
int main(void) {
    Base::add_rpc_class<AnalogIn>();
    Base::add_rpc_class<AnalogOut>();
    Base::add_rpc_class<DigitalIn>();
    Base::add_rpc_class<DigitalOut>();
    Base::add_rpc_class<PwmOut>();
    Base::add_rpc_class<Timer>();
    Base::add_rpc_class<SPI>();
    Base::add_rpc_class<BusOut>();
    Base::add_rpc_class<BusIn>();
    http.addHandler(new HTTPRPC());
    http.addHandler(new HTTPFileSystemHandler("/", "/local/"));
    http.bind();
    while(1) {
        http.poll();
    }
}



2010년 6월 13일 일요일

아뒤노에서 긴 문자열 사용하기 (How to do big strings in Arduino)


아뒤노는 문자 스트링을 매우 쉽게 저장하고 사용할 수 있게 해 주지만 이 문자열들은 변수와 동일하게 램에 저장되기 때문에 많은 양의 문자열을 저장할 수 없다. 그러므로 긴 문자열을 read-only인 프로그램 메모리에 집어넣고 사용하는 법을 설명하겠다.

많은 스케치(아뒤노 프로그램)은 긴 문자열을 가지고 있는 경우가 많다. 간단한 CLI를 만들었거나 작은 웹 페이지를 저장하고 있을수도 있다. 일반적으로 아래와 같은 식으로 문자열을 저장한다.

char hellostr[] =  "<html><head><title>hello world</title></head>
                   <body><p align=center><h1>hello world</h1></p>
                   </body></html>";

// and then sometime later

Serial.println( hellostr );

문제는 hellostr이 다른 변수와 함께 램에 저장된다는 것이다. 아뒤노의 ATmega 칩은 단지 1KB의 램을 가지고 있다. 코드가 더 복잡해져서 긴 문자열과 여러개의 라이브러리를 사용하면 이해할 수 없는 신기한 문제가 생기기 시작할 것이다. 아뒤노는 프로그램이 칩이 가지고 있는것보다 더 많은 램을 사용하기 시작해도 경고를 해 주지 못한다.

이 문제를 해결하기 위해 문자열을 PROGMEM(PROGram MEMory)에 저장할 수 있다. 즉 프로그램 코드를 저장하는 플래쉬 롬을 사용하는 것이다. PROGMEM 문자열을 사용하는건 약간의 편법을 사용해야 하지만 아래의 printProgStr()이라는 함수를 사용하면 별다른 문제 없이 쉽게 사용할 수 있다.

const char hellostr[] PROGMEM = "...";     // notice added 'const' and 'PROGMEM'

// given a PROGMEM string, use Serial.print() to send it out
void printProgStr(const prog_char str[])
{
  char c;
  if(!str) return;
  while((c = pgm_read_byte(str++)))
    Serial.print(c,BYTE);
}

// and then at some point

printProgStr( hellostr );

문자열을 Serial.print() 이외에서 사용하려면 별도 함수를 만들어 거기에 글자별로 처리하는 코드를 넣어주면 된다.




2010년 6월 12일 토요일

아뒤노에서 포트를 직접 억세스 (Direct Port Manipulation in Arduino)



아뒤노에서도 포트 레지스터를 사용하면 마이크로 컨트롤러의 I/O핀을 로우레벨에서 고속으로 조작할 수 있다. 아뒤노 보드(ATmega8과 ATmega168)에 사용된 칩은 3개의 포트를 가지고 있다.

  • B (digital pin 8~13)
  • C (analog input pins)
  • D (digital pin 0~7)

각 포트는 3개의 레지스터로 제어되고 레지스터들은 아뒤노에 변수로 정의되어 있다. DDR 레지스터는 각 핀이 INPUT 또는 OUTPUT인지를 결정한다. PORT레지스터는 핀의 출력값을 HIGH또는 LOW로 제어한다. PIN레지스터는 INPUT모드인 핀의 상태값을 읽는다.

DDR과 PORT레지스터는 읽고 쓰기가 가능하다. PIN레지스터는 입력 상태에 대응하며 읽기 전용이다.



PORTD는 아뒤노의 digital pin 0~7에 매핑된다.
  •   DDRD - 포트 D 데이터 방향 레지스터 (R/W)
  •   PORTD - 포트 D 데이터 레지스터 (R/W)
  •   PIND - 포트 D 입력 핀 레지스터 (R)

PORTB는 아뒤노의 digital pin 8~13에 매핑된다. 상위 2 비트는 crystal핀에 매핑되고 사용할 수 없다.
  •   DDRB - 포트 B 데이터 방향 레지스터 (R/W)
  •   PORTB - 포트 B 데이터 레지스터 (R/W)
  •   PINB - 포트 B 입력 핀 레지스터 (R)

PORTD는 아뒤노의 analog pin 0~5에 매핑된다. 핀 6, 7은 아뒤노 미니에서만 억세스할 수 있다.
  •   DDRC - 포트 C 데이터 방향 레지스터 (R/W)
  •   PORTC - 포트 C 데이터 레지스터 (R/W)
  •   PINC - 포트 C 입력 핀 레지스터 (R)
 
이 레지스터들의 각 비트는 핀 하나에 대응한다. 아뒤노 핀 번호와 포트/비트간의 전체 매핑은 칩의 다이어그램을 보면 된다. (포트의 일부 비트는 I/O이외의 용도로 사용될 수도 있기 때문에 그에 해당하는 레지스터 비트 값을 바꾸지 않도록 주의해야 한다.)

예제

위의 핀 맵을 참조하면 PortD 레지스터는 아뒤노 디지털 핀 0~7을 제어한다.

하지만 핀 0과 1은 아뒤노 프로그래밍과 디버깅을 위한 시리얼 통신에 사용되기 때문에 시리얼 입/출력 함수를 위해 필요하지 않는 이상 이 핀을 바꾸는건 피하는게 좋다. 이 값을 잘못 바꾸면 프로그램 다운로드나 디버깅을 방해받을 수 있다.

DDRD는 포트 D의 방향 레지스터이다. 레지스터의 각 비트는 포트 D의 핀이 INPUT 또는 OUTPUT으로 설정되도록 제어한다.

DDRD = B11111110;  // 아뒤노 핀 1~7은 출력으로, 핀 0는 입력으로 설정
DDRD = DDRD | B11111100;  // 더 안전한 방법. Rx/Tx로 사용되는 핀0과 1의 값은 변경하지 않고 핀 2~7을 출력으로 설정

PORTD는 출력 상태를 위한 레지스터이다.

PORTD = B10101000;  // 디지털 핀 7,5,3을 HIGH로 만듬

DDRD레지스터 또는 pinMode()를 사용해 저 핀들을 OUTPUT으로 설정해 놓은 경우 핀에서 5V가 출력되는걸 볼 수 있다.

PIND는 입력 레지스터 변수로 모든 디지털 입력 핀의 값을 동시에 읽는다.

포트 조작 팁

#define B0 B00000001
#define B1 B00000010
#define B2 B00000100
#define B3 B00001000
#define B4 B00010000
#define B5 B00100000
#define B6 B01000000
#define B7 B10000000

이렇게 비트를 정의해 놓으면 여러 핀을 동시에 H로 만들 경우는

PORTD |= B7 | B5 | B4;  // 디지털 핀 7, 5, 4를 HIGH로

여러 핀을 동시에 L로 만들 경우는

PORTD &= ~(B7 | B5 | B4);  // 디지털 핀 7, 5, 4를 LOW로

여러 핀의 값을 토글(현재값이 H면 L로, 현재값이 L이면 H로) 시키는 경우는

PORTD ^= B7 | B5 | B4;  // 디지털 핀 7, 5, 4의 값을 토글

Port manipulation을 사용하는 이유

일반적으로 이런식으로 포트를 직접 조작하는건 별로 좋은 생각이 아니다. 그 이유는

  • 코드 디버깅 및 관리하기가 더 힘들어지고 다른 사람이 이해하기 힘들다. 마이크로 프로세서가 코드를 실행하는건 몇 마이크로초밖에 안 걸리지만 그 코드가 왜 동작하지 않는가 찾아내고 수정하는데는 몇시간이 걸린다. 시간이 중요하지 않은가? 컴퓨터의 시간은 사용전력으로 따져보면 매우 싸다. 가능한 가장 확실한 방법으로 코드를 작성하는게 좋다.
  • 코드의 이식성이 떨어진다. digitalRead()와 digitalWrite()를 사용하면 모든 Atmel 마이크로 컨트롤러에서 실행시키는게 훨씬 쉽지만 컨트롤과 포트 레지스터는 각 마이크로컨트롤러마다 다를 수 있어 코드를 바로 사용하기 힘들수 있다.
  • 직접 포트 레지스터를 억세스하다 보면 의도하지 않은 문제를 만들기 쉬워진다. 위의 예제에서 DDRD = B11111110; 에서처럼 핀 0은 입력으로 남겨둬야만 한다. 핀 0는 시리얼 포트의 Rx이다. 하지만 실수로 핀 0을 OUTPUT으로 만들어버리면 갑자기 시리얼포트가 동작하지 않게 만들기 쉽다. 갑자기 시리얼 포트에서 데이터를 받을 수 없으면 매우 혼란스러울 것이다.

그럼에도 불구하고 왜 포트 레지스터를 직접 조작해야 하는가? 포트 레지스터를 직접 억세스하면 다음의 장점이 있다.

  • 핀의 출력을 H/L로 매우 빠르게(몇 나노초 이내에) 바꾸는게 필요한 경우가 있다. lib/targets/arduino/wiring.c 소스코드를 보면 digitalRead()와 digitalWrite()는 각각 몇십줄의 코드로 된 걸 볼 수 있는데 이는 컴파일되면 상당한 양의 기계어 명령어가 된다. 각 기계어 명령어는 16MHz 클럭사이클에서 한 클럭사이클이 필요한데 시간에 민감한 어플리케이션에서는 상당한 부담이 된다. 그에 비해 포트를 직접 억세스하면 훨씬 적은 클럭 사이클로 동일한 작업을 할 수 있다.
  • 때로는 여러개의 출력핀의 값을 정확히 동시에 바꿔줘야 할 필요가 있다. digitalWrite(10, HIGH); 다음에 digitalWrite(11, HIGH)를 호출하면 먼저 핀 10이 HIGH로 바뀌고 몇 마이크로초 후에 핀 11이 HIGH로 바뀌게 된다. 때로는 외부의 시간에 민감한 디지털 회로의 동작에 혼란을 줄 수 있다. 이 경우 PORTB |= B1100; 을 사용하면 동시에 두 핀을 HIGH로 바꿔줄 수 있다.
  • 프로그램 메모리가 부족한 경우 이 방식을 사용하면 코드 크기를 작게 줄일 수 있다. 루프를 사용해 각 핀을 독립적으로 조작하는것에 비해 포트 레지스터를 통해 동시에 여러 하드웨어 핀에 값을 쓰는게 훨씬 작은 명령어를 사용하게 된다. 때로는 이 방식을 사용여부가 프로그램이 플래쉬 메모리에 들어갈 수 있는가 못들어가는가를 결정하는 차이가 될 수도 있다.



2010년 6월 6일 일요일

SAX를 사용한 XML 파싱 (Parsing XML document using SAX)



SAX를 사용해서 XML 문서를 파싱하려면 먼저 DefaultHandler를 확장한 클래스가 필요하다.

import org.xml.sax.helpers.DefaultHandler;

public class MySAXApp extends DefaultHandler
{
  public MySAXApp()
  {
    super();
  }
}

또한 JAVA 어플리케이션에서 동적으로 SAX드라이버를 사용하기 위해 XMLReaderFactory 클래스의 createXMLReader 메소드를 사용해서 XML reader를 만들어 줘야 한다.

public static void main(String args[])
{
  XMLReader xr = XMLReaderFactory.createXMLReader();
}

여기서 만든 오브젝트를 사용하면 XML문서를 파싱할 수 있는데, 먼저 XMLReader 인터페이스에서 setContentHandler와 setErrorHandler 메소드를 사용해 파싱결과와 에러가 발생했을 때 리포트하는데 사용되는 핸들러를 등록해 줘야 한다. 일반적으로 실제 복잡한 어플리케이션에서는 핸들러를 별도의 오브젝트로 만들지만 여기서는 간단한 데모를 위해 부모 클래스에 포함시켰기 때문에 단순히 클래스를 인스턴스화 해서 XML reader에 등록하면 된다.

public static void main(String args[])
{
  XMLReader xr = XMLReaderFactory.createXMLReader();
  MySAXApp handler = new MySAXApp();
  xr.setContentHandler(handler);
  xr.setErrorHandler(handler);
}

위의 예제 코드는 파싱 이벤트를 처리할 MySAXApp 인스턴스를 만들어 그 인스턴스를 XML reader의 정상적인 컨텐트 이벤트와 에러 이벤트 핸들러로 등록한다.

public static void main(String args[])
{
  XMLReader xr = XMLReaderFactory.createXMLReader();
  MySAXApp handler = new MySAXApp();
  xr.setContentHandler(handler);
  xr.setErrorHandler(handler);

  for (int i=0; i<args.length;i++) {
    FileReader r = new FileReader(args[i]);
    xr.parse(new InputSource(r));
  }
}

지금까지 설명한 예제의 전체 코드는 다음과 같다.

import java.io.FileReader;

import org.xml.sax.XMLReader;
import org.xml.sax.InputSource;
import org.xml.sax.helpers.XMLReaderFactory;
import org.xml.sax.helpers.DefaultHandler;

public class MySAXApp extends DefaultHandler
{
  public static void main (String args[])
    throws Exception {
      XMLReader xr = XMLReaderFactory.createXMLReader();
      MySAXApp handler = new MySAXApp();
      xr.setContentHandler(handler);
      xr.setErrorHandler(handler);

      // Parse each file provided on the
      // command line.
      for (int i = 0; i < args.length; i++) {
        FileReader r = new FileReader(args[i]);
        xr.parse(new InputSource(r));
      }
    }

  public MySAXApp ()
  {
    super();
  }
}

예제 코드를 컴파일해서 실행시키면 XML문서를 파싱하지만 그에 따라 발생하는 이벤트를 처리하는 코드가 빠져있기 때문에 XML문서의 포맷이 틀려 에러가 발생하지 않으면 아무 출력도 없이 실행이 종료된다.


이벤트 처리

XML 문서를 처리하려면 파싱하면서 발생하는 이벤트를 처리해주는 핸들러를 만들어 줘야 한다. 먼저 가장 중요한 이벤트로 문서의 시작과 끝, 엘리먼트의 시작과 끝, 문자데이터가 있다.
클라이언트 어플리케이션이 문서의 시작과 끝을 발견하려면 startDocument/endDocument 메소드를 구현해줘야 한다.

public void startDocument()
{
  System.out.println("Start document");
}

public void endDocument()
{
  System.out.println("End document");
}

startDocument/endDocument 핸들러는 별도 argument를 가지지 않는다. SAX드라이버가 문서의 시작을 발견하면 startDocument 메소드를 호출한다. 또한 문서의 끝을 발견하면 (심지어 문서 내에 에러가 있더라도) endDocument 메소드를 호출한다.

예제 코드에서는 단순히 stdout에 메시지를 출력하지만 실제 어플리케이션에서는 핸들러 내에 어떤 코드도 넣어줄 수 있다. 가장 빈번히 볼 수 있는 코드는 메모리 상주 트리 생성, 내용 출력, 데이터베이스 생성/추가, 정보추출등을 수행한다.

SAX드라이버는 엘리먼트의 시작과 끝도 동일한 방법을 사용해 알려준다. 다만 startElement와 endElement 메소드는 몇개의 argument를 같이 넘겨준다.

public void startElement(String uri, String name, String qName, Attributes atts)
{
  if ("".equals(uri)) System.out.println("Start elements: " + qName);
  else System.out.println("Start elements: {" + uri + "}" + name);
}

public void endElement(String uri, String name, String qName)
{
  if ("".equals(uri)) System.out.println("End elements: " + qName);
  else System.out.println("End elements: {" + uri + "}" + name);
}

이 메소드들은 엘리먼트의 시작과 끝을 알려주는데 엘리먼트에 Namespace URI가 정의되어 있으면 '{URI namespace}이름' 형태로 출력하고 그렇지 않은 경우는 단순히 '이름' 형태로 출력해준다. qName에 XML 1.0 이름이 들어가 있기 때문에 namespace URI가 없는 엘리먼트의 경우 이 이름을 사용해줘야만 한다.

마지막으로 SAX2는 일반 문자데이터는 characters 메소드를 통해 리포트한다. 아래의 코드는 모든 문자데이터를 화면에 출력하는데 특수문자를 escape-character를 사용해 표현하기 때문에 보기좋게 출력하기 위해 코드가 좀 길다.

public void characters (char ch[], int start, int length)
{
  System.out.print("Characters:    \"");
  for (int i = start; i < start + length; i++) {
    switch (ch[i]) {
      case '\\':
        System.out.print("\\\\");
        break;
      case '"':
        System.out.print("\\\"");
        break;
      case '\n':
        System.out.print("\\n");
        break;
      case '\r':
        System.out.print("\\r");
        break;
      case '\t':
        System.out.print("\\t");
        break;
      default:
        System.out.print(ch[i]);
        break;
    }
  }
  System.out.print("\"\n");
}

지금까지 설명된 내용을 모두 추가한 예제 프로그램이다.

import java.io.FileReader;

import org.xml.sax.XMLReader;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.helpers.XMLReaderFactory;
import org.xml.sax.helpers.DefaultHandler;

public class MySAXApp extends DefaultHandler
{
    public static void main (String args[])
        throws Exception
    {
        XMLReader xr = XMLReaderFactory.createXMLReader();
        MySAXApp handler = new MySAXApp();
        xr.setContentHandler(handler);
        xr.setErrorHandler(handler);

        // Parse each file provided on the command line.
        for (int i = 0; i < args.length; i++) {
            FileReader r = new FileReader(args[i]);
            xr.parse(new InputSource(r));
        }
    }

    public MySAXApp ()
    {
        super();
    }

    ////////////////////////////////////////////////////////////////////
    // Event handlers.
    ////////////////////////////////////////////////////////////////////

    public void startDocument ()
    {
        System.out.println("Start document");
    }

    public void endDocument ()
    {
        System.out.println("End document");
    }

    public void startElement (String uri, String name,
                              String qName, Attributes atts)
    {
        if ("".equals (uri))
            System.out.println("Start element: " + qName);
        else
            System.out.println("Start element: {" + uri + "}" + name);
    }

    public void endElement (String uri, String name, String qName)
    {
        if ("".equals (uri))
            System.out.println("End element: " + qName);
        else
            System.out.println("End element:   {" + uri + "}" + name);
    }

    public void characters (char ch[], int start, int length)
    {
        System.out.print("Characters:    \"");
        for (int i = start; i < start + length; i++) {
            switch (ch[i]) {
            case '\\':
                System.out.print("\\\\");
                break;
            case '"':
                System.out.print("\\\"");
                break;
            case '\n':
                System.out.print("\\n");
                break;
            case '\r':
                System.out.print("\\r");
                break;
            case '\t':
                System.out.print("\\t");
                break;
            default:
                System.out.print(ch[i]);
                break;
            }
        }
        System.out.print("\"\n");
    }
}

샘플 출력

예제에 사용할 XML파일이다.

<?xml version="1.0"?>

<poem xmlns="http://www.megginson.com/ns/exp/poetry">
<title>Roses are Red</title>
<l>Roses are red,</l>
<l>Violets are blue;</l>
<l>Sugar is sweet,</l>
<l>And I love you.</l>
</poem>

이 문서파일 이름을 roses.xml이라고 하고 com.example.xml.SAXDriver 클래스 패스에 SAX2 드라이버가 있으면 샘플 어플리케이션은 다음과 같이 실행하면 된다.

java -Dorg.xml.sax.driver=com.example.xml.SAXDriver MySAXApp roses.xml

실행 결과는 다음과 같다.

Start documentStart element: {http://www.megginson.com/ns/exp/poetry}poem
Characters:    "\n"
Start element: {http://www.megginson.com/ns/exp/poetry}title
Characters:    "Roses are Red"
End element:   {http://www.megginson.com/ns/exp/poetry}title
Characters:    "\n"
Start element: {http://www.megginson.com/ns/exp/poetry}l
Characters:    "Roses are red,"
End element:   {http://www.megginson.com/ns/exp/poetry}l
Characters:    "\n"
Start element: {http://www.megginson.com/ns/exp/poetry}l
Characters:    "Violets are blue;"
End element:   {http://www.megginson.com/ns/exp/poetry}l
Characters:    "\n"
Start element: {http://www.megginson.com/ns/exp/poetry}l
Characters:    "Sugar is sweet,"
End element:   {http://www.megginson.com/ns/exp/poetry}l
Characters:    "\n"
Start element: {http://www.megginson.com/ns/exp/poetry}l
Characters:    "And I love you."
End element:   {http://www.megginson.com/ns/exp/poetry}l
Characters:    "\n"
End element:   {http://www.megginson.com/ns/exp/poetry}poem
End document

이렇게 짧은 문서라도 (최소) 25개의 이벤트가 발생하는걸 볼 수 있다. 문서의 시작과 끝에 각각 하나씩, 6개의 엘리먼트마다 시작과 끝, 11개의 문자데이터(엘리먼트 사이의 공백을 포함)모든 엘리먼트의 네임스페이스를 선언하기 위한 xmlns="http://www.magginson.com/ns/exp/poetry" 속성이 포함되어 있으면 위와 같은 결과가 나오지만 xmlns 속성이 정의되어 있지 않았을때의 출력은 다음과 같다.

Start document
Start element: poem
Characters:    "\n"
Start element: title
Characters:    "Roses are Red"
End element:   title
Characters:    "\n"
Start element: l
Characters:    "Roses are red,"
End element:   l
Characters:    "\n"
Start element: l
Characters:    "Violets are blue;"
End element:   l
Characters:    "\n"
Start element: l
Characters:    "Sugar is sweet,"
End element:   l
Characters:    "\n"
Start element: l
Characters:    "And I love you."
End element:   l
Characters:    "\n"
End element:   poem
End document

어플리케이션은 XML 네임스페이스가 있는 경우와 없는 경우 두가지 타입의 문서를 모두 사용하게 될 수 있다. 또한 문서 내 일부 엘리먼트는 네임스페이스를 가지고 있고 나머지는 그렇지 않은 문서를 다룰수도 있기 때문에 항상 네임스페이스가 있거나 또는 없다고 가정하지 말고 코드내에서 실제로 네임스페이스 URI를 확인해야만 한다.