Отправка SMS через bluetooth

INTRO

Ко мне в ICQ постучался знакомый чел:
"xxx: мужик, я чё хотел-то... насколько трудная задача прогу написать (к примеру делфя), которая будет видеть мобильник через юсб и общаться с ним, обмениваться файлами? надо чтоб этот девайс забирал данные из txt-шного файла, кидал на телефон, а телефон по смс кидал на другой телефон. основное это чтобы прога видела телефон и общалась с ним..."

Прочитав этот бред я предложил ему написать программу, которая отправляла бы SMS через bluetooth

Краткий обзор Bluetooth-стека

^ большая часть контента тупо скопипиздина из этой статьи, потому что мне она понравилась и лучше я не напишу.

С точки зрения прикладного программиста, на нижнем уровне стоит слой HCI (Host Controller Interface), он управляет канальными соединениями, и здесь можно провести аналогию с Ethernet. Далее данные обрабатываются пакетным протоколом L2CAP (Logical Link Control and Adaptation Protocol), его можно представить как смесь IP+UDP+QOS, с его помощью все вышестоящие слои осуществляют пакетную передачу. Выше стоит поточный протокол RFCOMM, пришедший из IRDA; его можно описать как TCP over RS232. И самый высокоуровневый протокол – это OBEX (Object Exchange), в стеке TCP/IP нет его аналогов.

Сторонней веткой от L2CAP отходит SDP (Service Discovery Protocol). Фактически это сложный сервер, позволяющий запрашивать и регистрировать на себе профили, описывающие возможности устройства. Для наглядности перечислю наиболее распространенные профили, одобренные SIG:

  • Generic Access Profile (GAP) – описывает, как использовать низкоуровневые протоколы. Все Bluetooth-устройства имеют реализацию GAP.
  • Service Discover Application Profile (SDAP) – описывает возможности данного SDP.
  • Serial Port Profile (SPP) – описывает параметры для эмуляции RS232 поверх RFCOMM или L2CAP.
  • Dial-up Networking Profile (DUNP) - описывает параметры для эмуляции AT-модема поверх GAP и SPP.
  • Generic Object Exchange Profile (GOEP) – описывает транспортные параметры OBEX.
  • Object Push Profile (OPP) – описывает параметры для приема и передачи простых объектов поверх GOEP.
  • File Transfer Profile (FTP) - описывает параметры для приема и передачи сложных объектов (включая навигацию по файловой системе) поверх GOEP/OPP.
  • Synchronization Profile (SP) - описывает параметры для синхронизации, аналогичной IrMC в IRDA.

Реализация Bluetooth-стека


Windows

Мелкософт поддерживает Bluetooth начиная с Windows XP с Пакетом обновления 1 (SP1) и позже на Windows XP, Встроенном с Пакетом обновления 2, и на Windows CE. Для получения дополнительной информации см. MSDN и вот эту занимательную статью.
Главным минусом является то, что твое приложение будет работать только если на компьютере используется стандартный драйвер от Мелкософт.
На моем нетбуке стоит передатчик от brodcom, который использует свои WIDCOMM-овские драйвера и все попытки использовать Bluetooth API закончились неудачей, а скачивать и изучать WIDCOMM SDK мне не захотелось т.к. это бы значило привязать себя к конкретному устройству.

Linux

В современных дистрибутивах GNU/Linux поддержка Bluetooth предоставлена инициативой BlueZ как на уровне ядра, так и в user space. Распространенные дистрибутивы уже содержат BlueZ в ядре.
Но, как мы уже заметили ранее, Bluetooth представляет собой разветвленный веерный стек, поэтому одной только поддержки на уровне ядра недостаточно. Для поддержки на уровне user space нам понадобятся следующие пакеты: bluez-utils, bluez-libs
После небольшой настройки демон hcid отвечающего за уровень HCI и демона sdpd, который обслуживает SDP, можно проверить работоспособность:

  • hciconfig -a покажет состояние нашего bluetooth-адаптера;
  • hcitool scan покажет находящиеся вокруг bluetooth-устройства;
  • sdptool browse 00:11:22:33:44:55 покажет сервисы, предоставляемые устройством с таким адресом;
  • sdptool browse local покажет сервисы, зарегистрированные на нашей машине;
  • hidd --connect 00:11:22:33:44:55 присоединит к нам HID-устройство с таким адресом;
  • pand --connect 00:11:22:33:44:55 создаст Private Area Network с внешним устройством;
  • obexftp/obexftpd помогут обмениваться файлами с внешним устройством.

Отправка SMS через GSM модем

Для того чтобы подключиться непосредственно к GSM модему устройства через bluetooth нам необходимо:

  1. Знать адрес устройства
  2. Знать поддерживает ли устройство сервис SPP
  3. Знать RFCOMM канал сервиса Serial Port Profile (SPP)
В дальнейшем коммуникация с GSM модемом происходит с помощью AT-команд (набор команд Hayes)
Сама отправка SMS происходит посредством передачи команды AT+CMGS. Подробно этот вопрос рассмотрен в этой статье, мы же разберемся, как это все программно реализовать.

Программирование AT терминала

// (c)oded by Gar|k 1.12.2010
// Bluetooth RFCOMM client v0.1
// часть кода спизжена из rfcomm_sppd

#include 
#include 
#include 
#include 
#include 
#include 
#include 

#include 
#include 
#include 
#include 
#include 

// функция выполняет поиск num_dev устройств в радиусе действия приемника
void snan_dev(int num_dev) {
    char addr[19] = {0};
    char name[248] = {0};
    int i;
    int dev_id = hci_get_route(NULL);
    int sock = hci_open_dev(dev_id);


    inquiry_info *ii = (inquiry_info*) malloc(num_dev * sizeof (inquiry_info));
    printf("Scanning bluetooth devices...\n");
    int num_rsp = hci_inquiry(dev_id, 8, num_dev, NULL, &ii, IREQ_CACHE_FLUSH);


    for (i = 0; i < num_rsp; ++i) {

        ba2str(&(ii + i)->bdaddr, addr);
        hci_read_remote_name(sock, &(ii + i)->bdaddr, sizeof (name), name, 0);
        printf("%s %s", addr, name);

        switch ((ii + i)->dev_class[1]) {
            case 0x01: printf(" [computer]");
                break;
            case 0x02: printf(" [phone]");
                break;
        }
        printf("\r\n");
    }
    free(ii);
}

// функция выполняет поиск RFCOMM канала заданного сервиса
uint8_t get_channel(char *addr, int class) {
    bdaddr_t target;
    str2ba(addr, &target);
    sdp_session_t *sess = sdp_connect(BDADDR_ANY, &target, SDP_RETRY_IF_BUSY);
    uuid_t root_uuid;
    sdp_uuid16_create(&root_uuid, class);
    sdp_list_t *search = sdp_list_append(0, &root_uuid);
    uint32_t range = SDP_ATTR_PROTO_DESC_LIST;
    sdp_list_t *attrid = sdp_list_append(0, &range);
    sdp_list_t *result;
    sdp_service_search_attr_req(sess, search, SDP_ATTR_REQ_INDIVIDUAL, attrid, &result);

    int rfcomm_channel = -1;

    for (/* empty */; result; result = result->next) {
        sdp_list_t *access = NULL;
        sdp_get_access_protos((sdp_record_t *) result->data, &access);
        if (access) rfcomm_channel = sdp_get_proto_port(access, RFCOMM_UUID);
        if (rfcomm_channel > 0) break;
    }

    return (uint8_t) rfcomm_channel;
}

// посмотрев исходкики rfcomm_sppd

int open_client(char *addr_dev) {
    int fd;
    struct linger l = {0};
    struct sockaddr_rc addr = {0};

// запрашиваем RFCOMM канал сервиса SPP
    uint8_t channel = get_channel(addr_dev, SERIAL_PORT_SVCLASS_ID);

    if (channel < 0) {
        perror("not support");
        exit(EXIT_FAILURE);
    }


    if ((fd = socket(PF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM)) < 0) {
        perror("socket");
        exit(EXIT_FAILURE);
    }


    addr.rc_family = AF_BLUETOOTH;

    if (bind(fd, (struct sockaddr *) & addr, sizeof (addr)) < 0) {
        perror("bind");
        exit(EXIT_FAILURE);
    }


    l.l_onoff = 1;
    l.l_linger = 5;
    if (setsockopt(fd, SOL_SOCKET, SO_LINGER, &l, sizeof (l)) < 0) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }

    addr.rc_channel = (uint8_t) channel;
    str2ba(addr_dev, &addr.rc_bdaddr);

    if (connect(fd, (struct sockaddr *) & addr, sizeof (addr)) < 0) {
        perror("connect");
        exit(EXIT_FAILURE);
    }

    return fd;
}

int done; /* got a signal */
struct termios tio; /* stored termios for reset on exit */

void reset_tio(void) {
    tcsetattr(STDIN_FILENO, TCSAFLUSH, &tio);
}

void copy_data(int src, int dst) {
    static char buf[BUFSIZ];
    ssize_t nr, nw, off;

    while ((nr = read(src, buf, sizeof (buf))) == -1) {
        if (errno != EINTR) {
            //syslog(LOG_ERR, "read failed: %m");
            exit(EXIT_FAILURE);
        }
    }

    if (nr == 0) /* reached EOF */
        done++;

    for (off = 0; nr; nr -= nw, off += nw) {
        if ((nw = write(dst, buf + off, (size_t) nr)) == -1) {
            //syslog(LOG_ERR, "write failed: %m");
            exit(EXIT_FAILURE);
        }
    }
}

void sighandler(int s) {
    done++;
}

#define max(a, b) ((a) > (b) ? (a) : (b))

int main(int argc, char **argv) {

    int tty_in, tty_out, rfcomm, n;
    struct termios t;
    fd_set rdset;

    if (argc == 1) {
        snan_dev(255);
        return EXIT_SUCCESS;
    }

    tty_in = STDIN_FILENO;
    tty_out = STDOUT_FILENO;

    if (tcgetattr(tty_in, &t) < 0) err(EXIT_FAILURE, "tcgetattr");

    memcpy(&tio, &t, sizeof (tio));
    t.c_lflag &= ~(ECHO | ICANON);
    t.c_iflag &= ~(ICRNL);

    if (memcmp(&tio, &t, sizeof (tio))) {
        if (tcsetattr(tty_in, TCSANOW, &t) < 0)
            err(EXIT_FAILURE, "tcsetattr");

        atexit(reset_tio);
    }

    /* catch signals */
    done = 0;
    (void) signal(SIGHUP, sighandler);
    (void) signal(SIGINT, sighandler);
    (void) signal(SIGPIPE, sighandler);
    (void) signal(SIGTERM, sighandler);

    rfcomm = open_client(argv[1]);

    n = max(tty_in, rfcomm) + 1;
    while (!done) {
        FD_ZERO(&rdset);
        FD_SET(tty_in, &rdset);
        FD_SET(rfcomm, &rdset);

        if (select(n, &rdset, NULL, NULL, NULL) < 0) {
            if (errno == EINTR)
                continue;

            //syslog(LOG_ERR, "select error: %m");
            exit(EXIT_FAILURE);
        }

        if (FD_ISSET(tty_in, &rdset))
            copy_data(tty_in, rfcomm);

        if (FD_ISSET(rfcomm, &rdset))
            copy_data(rfcomm, tty_out);
    }

    close(rfcomm);

    return EXIT_SUCCESS;
}
gcc -o bt -lbluetooth bt.c

Любому bluetooth-клиенту, прежде чем соединяться с сервером, необходимо этот самый сервер найти. Такой функционал предоставляет слой HCI в виде функций управления устройством. Чтобы не заморачиваться тонкостями маршрутизации в bluetooth-сетях, мы возьмем устройство на маршруте по умолчанию. Также сразу откроем на нем hci-сокет, он понадобится позже для запроса имени устройства (см. строки 23-24). После этого можно создать специальную структуру для хранения данных запроса и попросить устройство заполнить ее, при этом сбросить кэш и не искать больше 255 устройств. В ответ получим количество найденных хостов (см. строки 28-30). Теперь можно делать перебор найденных данных. Адрес находится в поле bdaddr, класс устройства - в поле dev_class, представляющем собой массив из трех байт. По второму байту можно грубо определить тип: 1 – компьютер, 2 – телефон. На самом деле класс устройства содержит намного больше точной и разнообразной информации, за ее интерпретацией можно обратиться к спецификациям (см. строки 33-46).

После того как клиент нашел в сети требуемый сервер, нужно проверить, поддерживает ли сервер искомые сервисы, ведь иначе соединяться с ним нет смысла, да и параметры соединения неизвестны. Достигнуть желаемого можно средствами механизмов SDP. Сервисы можно искать по различным параметрам, и фактически количество и разнообразие таковых ничем не ограничено. Я буду искать сервис класса SERIAL PORT SVCLASS и RFCOMM-канал, которым он предоставлен.

Для начала нам нужно соединиться с удаленным устройством (см. строки 52-54). Большинство параметров в SDP-операторике библиотек BlueZ задается древовидными списками с помощью функций построения таковых. Мы создадим список поиска, в который добавим параметр в виде искомого класса, а также создадим список атрибутов поиска, в котором запросим протоколы (см. строки 55-59). Теперь все подготовлено, чтобы сделать SDP-запрос через ранее подготовленную SDP-сессию с удаленным устройством и поместить результаты в еще один список (см. строки 60-61). Перебирая элементы результирующего списка, мы будем брать из них SDP-записи, проверять, есть ли там информация о протоколах, и запрашивать канал RFCOMM-протокола. Если полученный канал больше нуля, значит мы его нашли, можно с ним соединяться (см. строки 63-70).

После длительных мучений с HCI и SDP мы, наконец, получаем возможность работать с простым, всем хорошо известным унифицированным интерфейсом UNIX-сокетов. Bluetooth-сокеты поддерживают L2CAP- и RFCOMM-адресацию. L2CAP-адреса специфицируются номерами PSM (Protocol and Service Multiplexor), RFCOMM – номерами каналов.

Реализация RFCOMM клиента функция open_client строки 77-121

При приеме данных с RFCOMM-сокетов фрагментация – обычное дело. Ты попросил дать тебе 100 байт, вместо этого тебе дали 30 байт, потом 50 байт и потом еще 20 байт. В такой ситуации можно попросить вызов recv дефрагментировать поток с помощью флага MSG_WAITALL либо не забывать самостоятельно отслеживать длину принятых данных и собирать нужные куски вручную.

Это еще не конец, но пока все ;)

blog comments powered by Disqus
сюда туда