티스토리 뷰

1. BlueZ 라이브러리 소개
리눅스 상에서 블루투스 장비를 프로그래밍 하기위해서는 보통 BlueZ 라이브러리를 이용합니다. (씨리얼 방식으로 통신하기도 하는 듯합니다.)

BlueZ 라이브러리는 리눅스 환경에서 Bluetooth 무선 표준 스펙을 구현한 구현물입니다. 공식적으로 이 프로젝트는 Kernel 2.4, 2.6 을 지원합니다.
http://www.bluez.org/


2. BlueZ 라이브러리 설치 및 설정
리눅스 상에서 BlueZ 를 동작하도록 하기 위해서는 우선 라이브러리를 설치해야합니다.

http://www.bluez.org/download.html

상기의 페이지에서 BlueZ 라이브러리의 최신버전을 받으시고, 컴파일한뒤 구동 환경으로 복사하시기 바랍니다.

이때, BlueZ 라이브러리를 구동하는 환경이 커널 2.4 시리즈인 경우 커널상에 Bluetooth 스택이 올라가있지 않은 경우가 있습니다. 이 경우 패치 커널에 적용하고 다시 설치해야만 정상적으로 블루투스를 이용할 수 있습니다.

각 커널 별 블루투스 스택 지원 사항에 대한 설명과 패치 파일 및 패치 과정에 관련된 설명은 아래 사이트를 참고하시기 바랍니다.

http://www.holtmann.org/linux/kernel/

일단 커널 스택과 라이브러리가 정상적으로 설치되었다면 블루투스 동글을 삽입한뒤 다음과 명령어를 통해서 삽입된 블루투스 장비가 있다는 사실을 확인하시기 바랍니다. (제 경우에는 USB 형태의 동글을 이용했습니다.)

hciconfig

만약, 리눅스 커널이 삽입된 동글을 인식했다면 hci0 라는 디바이스가 잡힐 것 입니다.

hciconfig hci0 up

명령어를 이용해서 해당 디바이스를 활성화시키시면 모든 준비작업이 끝납니다.


3. BlueZ 예제
※ 소스에 대한 상세한 설명을 원하시는 분은 참고자료 섹션의 MIT 대학의 사이트를 참고하시기 바랍니다.

1) 통신 장비 스캐닝
simplescan.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#include <bluetooth/hci_lib.h>

int main(int argc, char **argv)
{
  inquiry_info *ii = NULL;
  int max_rsp, num_rsp;
  int dev_id, sock, len, flags;
  int i;
  char addr[19] = { 0 };
  char name[248] = { 0 };

  dev_id = hci_get_route(NULL);
  sock = hci_open_dev( dev_id );
  if (dev_id < 0 || sock < 0) {
       perror("opening socket");
       exit(1);
  }

  len  = 8;
  max_rsp = 255;
  flags = IREQ_CACHE_FLUSH;
  ii = (inquiry_info*)malloc(max_rsp * sizeof(inquiry_info));
 
  num_rsp = hci_inquiry(dev_id, len, max_rsp, NULL, &ii, flags);
  if( num_rsp < 0 ) perror("hci_inquiry");

  for (i = 0; i < num_rsp; i++) {
       ba2str(&(ii+i)->bdaddr, addr);
       memset(name, 0, sizeof(name));
       if (hci_read_remote_name(sock, &(ii+i)->bdaddr, sizeof(name),
           name, 0) < 0)
       strcpy(name, "[unknown]");
       printf("%s  %s\n", addr, name);
  }

  free( ii );
  close( sock );
  return 0;
}
블루투스 프로그래밍은 주변에 블루투스 장비를 찾는 것에서 시작합니다. 따라서 우선 주변에 존재하는 블루투스 장비를 스캐닝하는 작업을 해야합니다.

상기의 코드가 주변에 존재한느 블루투스 장비의 이름과 주소를 얻어오는 코드입니다.

블루투스의 주소를 표현하는데 사용하는 구조체는 아래의 형태를 가지고 있습니다.

typedef struct {
uint8_t b[6];
} __attribute__((packed)) bdaddr_t;

주소를 문자열로 변환하거나, 문자열을 주소로 변환하는데에는 아래의 함수를 이용합니다.

int str2ba( const char *str, bdaddr_t *ba );
int ba2str( const bdaddr_t *ba, char *str );

블루투스 주소는 ``XX:XX:XX:XX:XX:XX"의 형태로 X는 16진수 표현을 이용합니다.

(자세한 설명은 차후에 추가함)

리눅스 콘솔상에서 하단의 명령어를 입력함으로써 동일한 일을 할 수 있습니다.

hcitool scan


2) 블루투스 서비스 등록
sdpregister.c
#include <bluetooth/bluetooth.h>
#include <bluetooth/sdp.h>
#include <bluetooth/sdp_lib.h>

sdp_session_t *register_service()
{
  uint32_t service_uuid_int[] = { 0, 0, 0, 0xABCD };
  uint8_t rfcomm_channel = 11;
  const char *service_name = "Roto-Rooter Data Router";
  const char *service_dsc = "An experimental plumbing router";
  const char *service_prov = "Roto-Rooter";

  uuid_t root_uuid, l2cap_uuid, rfcomm_uuid, svc_uuid;
  sdp_list_t *l2cap_list = 0,
              *rfcomm_list = 0,
              *root_list = 0,
              *proto_list = 0,
              *access_proto_list = 0;
  sdp_data_t *channel = 0, *psm = 0;

  sdp_record_t *record = sdp_record_alloc();

  // set the general service ID
  sdp_uuid128_create( &svc_uuid, &service_uuid_int );
  sdp_set_service_id( record, svc_uuid );

  // make the service record publicly browsable
  sdp_uuid16_create(&root_uuid, PUBLIC_BROWSE_GROUP);
  root_list = sdp_list_append(0, &root_uuid);
  sdp_set_browse_groups( record, root_list );

  // set l2cap information
  sdp_uuid16_create(&l2cap_uuid, L2CAP_UUID);
  l2cap_list = sdp_list_append( 0, &l2cap_uuid );
  proto_list = sdp_list_append( 0, l2cap_list );

  // set rfcomm information
  sdp_uuid16_create(&rfcomm_uuid, RFCOMM_UUID);
  channel = sdp_data_alloc(SDP_UINT8, &rfcomm_channel);
  rfcomm_list = sdp_list_append( 0, &rfcomm_uuid );
  sdp_list_append( rfcomm_list, channel );
  sdp_list_append( proto_list, rfcomm_list );

  // attach protocol information to service record
  access_proto_list = sdp_list_append( 0, proto_list );
  sdp_set_access_protos( record, access_proto_list );

  // set the name, provider, and description
  sdp_set_info_attr(record, service_name, service_prov, service_dsc);

  int err = 0;
  sdp_session_t *session = 0;

  // connect to the local SDP server, register the service record, and
  // disconnect
  session = sdp_connect( BDADDR_ANY, BDADDR_LOCAL, SDP_RETRY_IF_BUSY );
  err = sdp_record_register(session, record, 0);

  // cleanup
  sdp_data_free( channel );
  sdp_list_free( l2cap_list, 0 );
  sdp_list_free( rfcomm_list, 0 );
  sdp_list_free( root_list, 0 );
  sdp_list_free( access_proto_list, 0 );

  return session;
}

이 예제를 동작시키기 위해서는 로컬 리눅스 시스템에 sdpd 이 실행 중이어야합니다. 만약 데몬이 실행중이 아니거나 sdpd 가 없다면 BlueZ 라이브러리를 컴파일해서 다시 설치해 주셔야합니다.

sdpd 가 정상적으로 실행 중이라면 블루투스 서비스가 등록되고, 아래의 명령을 통해서 현재 로컬 시스템에 등록된 서비스를 확인할 수 있습니다.

sdptool browse local

해당 명령어의 local 부분에 주소를 입력하면 해당 주소상의 sdpd 에 등록된 서비스 목록을 확인할 수 있습니다.

서비스 등록시에 rfcomm 만 등록하는 경우 서비스를 받아오는 측에서 서비스 목록을 제대로 가져오지 못하니, RFCOMM 만 이용하여 서비스를 운용중이더라도 L2CAP 을 같이 등록해야합니다.
(이는 SDP 자체가 L2CAP 프로토콜을 이용해서 서비스 내역을 교환하기 때문으로 보입니다.)

3) 블루투스 서비스 파싱
sdpparser.c

#include <bluetooth/bluetooth.h>
#include <bluetooth/sdp.h>
#include <bluetooth/sdp_lib.h>

int main(int argc, char **argv)
{
  uint32_t svc_uuid_int[] = { 0x0, 0x0, 0x0, 0xABCD };
  uuid_t svc_uuid;
  int err;
  bdaddr_t target;
  sdp_list_t *response_list = NULL, *search_list, *attrid_list;
  sdp_session_t *session = 0;

  str2ba( "01:23:45:67:89:AB", &target );

  // connect to the SDP server running on the remote machine
  session = sdp_connect( BDADDR_ANY, &target, SDP_RETRY_IF_BUSY );

  // specify the UUID of the application we're searching for
  sdp_uuid128_create( &svc_uuid, &svc_uuid_int );
  search_list = sdp_list_append( NULL, &svc_uuid );

  // specify that we want a list of all the matching applications' attributes
  uint32_t range = 0x0000ffff;
  attrid_list = sdp_list_append( NULL, &range );

  // get a list of service records that have UUID 0xabcd
  err = sdp_service_search_attr_req( session, search_list, \
           SDP_ATTR_REQ_RANGE, attrid_list, &response_list);

  sdp_list_t *r = response_list;

  // go through each of the service records
  for (; r; r = r->next ) {
       sdp_record_t *rec = (sdp_record_t*) r->data;
       sdp_list_t *proto_list;
      
       // get a list of the protocol sequences
       if( sdp_get_access_protos( rec, &proto_list ) == 0 ) {
       sdp_list_t *p = proto_list;

       // go through each protocol sequence
       for( ; p ; p = p->next ) {
           sdp_list_t *pds = (sdp_list_t*)p->data;

           // go through each protocol list of the protocol sequence
           for( ; pds ; pds = pds->next ) {

               // check the protocol attributes
               sdp_data_t *d = (sdp_data_t*)pds->data;
               int proto = 0;
               for( ; d; d = d->next ) {
                   switch( d->dtd ) {
                       case SDP_UUID16:
                       case SDP_UUID32:
                       case SDP_UUID128:
                           proto = sdp_uuid_to_proto( &d->val.uuid );
                           break;
                       case SDP_UINT8:
                           if( proto == RFCOMM_UUID ) {
                               printf("rfcomm channel: %d\n",d->val.int8);
                           }
                           break;
                   }
               }
           }
           sdp_list_free( (sdp_list_t*)p->data, 0 );
       }
       sdp_list_free( proto_list, 0 );

       }

       printf("found service record 0x%x\n", rec->handle);
       sdp_record_free( rec );
  }

  sdp_close(session);
}

sdpd 에 등록된 연결 대상의 지원하는 서비스 내역을 가져오는 부분입니다. 우리가 찾고자하는 서비스의 UUID 를 지정해주면 해당 서비스 내역만을 가져오게 됩니다.

4) RFCOMM 예제

리눅스의 통신이 항상 그렇듯 BSD 소켓을 이용해서 통신합니다. 단지 소켓 통신과 주소설정 과정이 기존의 TCP, UDP 소켓과 약간 차이가 있으니 그 부분만을 주의해서 보면 큰 문제 없이 통신이 가능합니다.

블루투스에서는 RFCOMM 이 인터넷의 TCP와 유사하며, L2CAP 이 인터넷의 UDP 와 유사한 프로토콜입니다. 이를 유의하시기 바랍니다.

rfcomm-server.c

#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/rfcomm.h>

int main(int argc, char **argv)
{
  struct sockaddr_rc loc_addr = { 0 }, rem_addr = { 0 };
  char buf[1024] = { 0 };
  int s, client, bytes_read;
  int opt = sizeof(rem_addr);

  // allocate socket
  s = socket(AF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM);

  // bind socket to port 1 of the first available
  // local bluetooth adapter
  loc_addr.rc_family = AF_BLUETOOTH;
  loc_addr.rc_bdaddr = *BDADDR_ANY;
  loc_addr.rc_channel = (uint8_t) 1;
  bind(s, (struct sockaddr *)&loc_addr, sizeof(loc_addr));

  // put socket into listening mode
  listen(s, 1);

  // accept one connection
  client = accept(s, (struct sockaddr *)&rem_addr, &opt);

  ba2str( &rem_addr.rc_bdaddr, buf );
  fprintf(stderr, "accepted connection from %s\n", buf);
  memset(buf, 0, sizeof(buf));

  // read data from the client
  bytes_read = read(client, buf, sizeof(buf));
  if( bytes_read > 0 ) {
       printf("received [%s]\n", buf);
  }

  // close connection
  close(client);
  close(s);
  return 0;
}
RFCOMM 의 채널은 TCP 상의 포트와 유사한 demux 키의 열할을 합니다. 채널은 1~31번까지 할당가능하다.

rfcomm-client.c
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/rfcomm.h>

int main(int argc, char **argv)
{
  struct sockaddr_rc addr = { 0 };
  int s, status;
  char dest[18] = "01:23:45:67:89:AB";

  // allocate a socket
  s = socket(AF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM);

  // set the connection parameters (who to connect to)
  addr.rc_family = AF_BLUETOOTH;
  addr.rc_channel = (uint8_t) 1;
  str2ba( dest, &addr.rc_bdaddr );

  // connect to server
  status = connect(s, (struct sockaddr *)&addr, sizeof(addr));

  // send a message
  if( status == 0 ) {
       status = write(s, "hello!", 6);
  }

  if( status < 0 ) perror("uh oh");

  close(s);
  return 0;
}


5) L2CAP 예제
l2cap-server.c
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/l2cap.h>

int main(int argc, char **argv)
{
  struct sockaddr_l2 loc_addr = { 0 }, rem_addr = { 0 };
  char buf[1024] = { 0 };
  int s, client, bytes_read;
  int opt = sizeof(rem_addr);

  // allocate socket
  s = socket(AF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_L2CAP);

  // bind socket to port 0x1001 of the first available
  // bluetooth adapter
  loc_addr.l2_family = AF_BLUETOOTH;
  loc_addr.l2_bdaddr = *BDADDR_ANY;
  loc_addr.l2_psm = htobs(0x1001);

  bind(s, (struct sockaddr *)&loc_addr, sizeof(loc_addr));

  // put socket into listening mode
  listen(s, 1);

  // accept one connection
  client = accept(s, (struct sockaddr *)&rem_addr, &opt);

  ba2str( &rem_addr.l2_bdaddr, buf );
  fprintf(stderr, "accepted connection from %s\n", buf);

  memset(buf, 0, sizeof(buf));

  // read data from the client
  bytes_read = read(client, buf, sizeof(buf));
  if( bytes_read > 0 ) {
       printf("received [%s]\n", buf);
  }

  // close connection
  close(client);
  close(s);
}
L2CAP의 포트 번호는 0x1001 - 0x7FFF 번위 중 홀수만 할당 가능합니다.

l2cap-client.c
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/l2cap.h>

int main(int argc, char **argv)
{
  struct sockaddr_l2 addr = { 0 };
  int s, status;
  char *message = "hello!";
  char dest[18] = "01:23:45:67:89:AB";

  if(argc < 2)
  {
       fprintf(stderr, "usage: %s <bt_addr>\n", argv[0]);
       exit(2);
  }

  strncpy(dest, argv[1], 18);

  // allocate a socket
  s = socket(AF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_L2CAP);

  // set the connection parameters (who to connect to)
  addr.l2_family = AF_BLUETOOTH;
  addr.l2_psm = htobs(0x1001);
  str2ba( dest, &addr.l2_bdaddr );

  // connect to server
  status = connect(s, (struct sockaddr *)&addr, sizeof(addr));

  // send a message
  if( status == 0 ) {
       status = write(s, "hello!", 6);
  }

  if( status < 0 ) perror("uh oh");

  close(s);
}

4. 참고사이트
http://people.csail.mit.edu/albert/bluez-intro/
http://www.holtmann.org/linux/kernel/
http://www.bluez.org/download.html