「Bluez を使用したSensorTagへのアクセス 」でBluez を使用してSensorTagへアクセスしましたが、今回はRaspberry Pi 3に実装されているBluetoothを使用して、Bluetooth Low Energy(BLE)プロトコルでC言語プログラムからSensorTagへアクセスし、Raspberry Pi 3をサービス提供するセントラルとして動作させます。
BlueZ 5.40 のインストール
Rasbian 上で BlueZ をインストールする際に apt-get を使うと バージョン5.23 がインストールされてしまうのでソースからコンパイルしてインストールします。
$ sudo apt-get install libdbus-1-dev libdbus-glib-1-dev libglib2.0-dev libical-dev libreadline-dev libudev-dev libusb-dev make $ mkdir -p work/bluetooth $ cd work/bluetooth $ wget https://www.kernel.org/pub/linux/bluetooth/bluez-5.40.tar.xz $ tar xvf bluez-5.40.tar.xz $ cd bluez-5.40 $ ./configure --disable-systemd --enable-library $ make $ sudo make install
続けて make でインストールされないものを手動でインストールします。
$ sudo cp attrib/gatttool /usr/local/bin/
あと、libbluetooth-dev の各種ヘッダファイルをビルドしたもので置き換えます。既存のincludeはbluetooth.5.23としてバックアップし、コンパイルしたlib/配下をbluetoothとしてリンクします。
$ sudo cp -ipr lib/ /usr/include/bluetooth.5.40 $ cd /usr/include $ sudo mv bluetooth bluetooth.5.23 $ sudo ln -s bluetooth.5.40/ bluetooth
BLEプログラムのコンパイル環境の構築
インストールしたBlueZのファイルの内、gatttoolコマンドに関連するファイルを流用して、SentorTagと接続できるBLEプログラムを作成します。
コンパイルするために必要なincludeファイルを参照するリンクを次に示します。includeファイルは、先にインストールしたBlueZのインストール先をリンクしています。
makeファイルを次に示します。フォルダ「_build」にオブジェクトが作成され、実行ファイル「gatool」がカレントディレクトに作成されます。
PROJECT_NAME := gatool BLUEZ_PATH = . PRJ_PATH = . OBJECT_DIRECTORY = _build OUTPUT_BINARY_DIRECTORY = . OUTPUT_FILENAME := $(PROJECT_NAME) ・ ・ ・ #sources project C_SOURCE_FILES += $(PRJ_PATH)/gatttool.c C_SOURCE_FILES += $(PRJ_PATH)/att.c C_SOURCE_FILES += $(PRJ_PATH)/gatt.c C_SOURCE_FILES += $(PRJ_PATH)/gattrib.c C_SOURCE_FILES += $(PRJ_PATH)/btio.c C_SOURCE_FILES += $(BLUEZ_PATH)/src/log.c C_SOURCE_FILES += $(BLUEZ_PATH)/client/display.c ・ ・ ・ #Link Library LIBS += $(BLUEZ_PATH)/lib/.libs/libbluetooth-internal.a LIBS += $(BLUEZ_PATH)/src/.libs/libshared-glib.a LIBS += -lreadline LIBS += `pkg-config --libs glib-2.0`
BLEプログラムの作成
BLEプログラムは、BlueZのファイルとそのソースの一部を切り出して作成します。ここでは、BlueZのgatttoolコマンドで、SensorTagのアドレスとinteractiveのパラメータを与えて起動し、「connect」をキー入力させた状態を、BlueZのファイルをベースにしてプログラム作成し、Raspberry Pi 3とSensorTagをBLEで接続します。作成手順を次に示します。
- BLEプログラムを実行すると、main関数から実行する。最初に、本来パラメータとして渡されるopt_dst変数に、SensorTagのアドレスを設置し、キーボードから入力される”connect”をパラメータにして、parse_line関数を呼び出します。
- parse_line関数では、パラメータ「connect”」をキーにしてcommands配列を検索し、一致したcmd_connect関数を実行します。
- cmd_connect関数は、gatt_connect関数を呼び出し、gatt_connect関数は、bt_io_connect関数を呼び出します。connectのコールバック関数「connect_cb」は、bt_io_connect関数を呼び出すときにパラメータとして与えられます。
- SentorTagをアドバタイズすると、コールバック関数「connect_cb」が実行され、connect_cb_main関数に制御が移されます。
- connect_cb_main関数は、bt_io_get関数でensorTagからのデータを受け取けとります。
- The Main Event Loopにより、SentorTagからのイベントが発生するまでプログラムを待たせます。ここでは、g_main_loop_run関数により、SentorTagがアドバタイズされるまで待たせています。
gatttool.c
#include <errno.h>
#include <stdio.h>
#include <signal.h>
#include <sys/signalfd.h>
#include <glib.h>
#include <readline/readline.h>
#include <readline/history.h>
#include "lib/bluetooth.h"
#include "lib/sdp.h"
#include "lib/uuid.h"
#include "lib/hci.h"
#include "lib/hci_lib.h"
#include "src/shared/util.h"
#include "btio/btio.h"
#include "att.h"
#include "gattrib.h"
#include "gatt.h"
#include "gatttool.h"
#include "client/display.h"
static char *opt_src = NULL;
static char *opt_dst = NULL;
static char *opt_dst_type = NULL;
static char *opt_sec_level = NULL;
static bt_uuid_t *opt_uuid = NULL;
static int opt_psm = 0;
static gboolean opt_interactive = FALSE;
static gboolean got_error = FALSE;
struct characteristic_data {
GAttrib *attrib;
uint16_t start;
uint16_t end;
};
static GIOChannel *iochannel = NULL;
static GAttrib *attrib = NULL;
static int opt_mtu = 0;
static volatile enum state
{
STATE_DISCONNECTED,
STATE_CONNECTING,
STATE_CONNECTED
}
conn_state;
#define error(fmt, arg...) \
rl_printf(COLOR_RED "Error: " COLOR_OFF fmt, ## arg)
static GString *prompt;
static GMainLoop *event_loop;
static void cmd_exit(int argcp, char **argvp)
{
printf("cmd_exit\n");
rl_callback_handler_remove();
g_main_loop_quit(event_loop);
}
static gboolean signal_handler(GIOChannel *channel, GIOCondition condition,
gpointer user_data)
{
static unsigned int __terminated = 0;
struct signalfd_siginfo si;
printf("signal_handler\n");
cmd_exit(0, NULL);
if (condition & (G_IO_NVAL | G_IO_ERR | G_IO_HUP)) {
g_main_loop_quit(event_loop);
return FALSE;
}
switch (si.ssi_signo) {
case SIGINT:
rl_replace_line("", 0);
rl_crlf();
rl_on_new_line();
rl_redisplay();
break;
case SIGTERM:
if (__terminated == 0) {
rl_replace_line("", 0);
rl_crlf();
g_main_loop_quit(event_loop);
}
__terminated = 1;
break;
}
return TRUE;
}
static guint setup_signalfd(void)
{
GIOChannel *channel;
guint source;
sigset_t mask;
int fd;
printf("setup_signalfd\n");
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGTERM);
if (sigprocmask(SIG_BLOCK, &mask, NULL) < 0) {
perror("Failed to set signal mask");
return 0;
}
fd = signalfd(-1, &mask, 0);
if (fd < 0) {
perror("Failed to create signal descriptor");
return 0;
}
channel = g_io_channel_unix_new(fd);
g_io_channel_set_close_on_unref(channel, TRUE);
g_io_channel_set_encoding(channel, NULL, NULL);
g_io_channel_set_buffered(channel, FALSE);
source = g_io_add_watch(channel,
G_IO_IN | G_IO_HUP | G_IO_ERR | G_IO_NVAL,
signal_handler, NULL);
g_io_channel_unref(channel);
return source;
}
static char *get_prompt(void)
{
printf("get_prompt\n");
if (conn_state == STATE_CONNECTED)
g_string_assign(prompt, COLOR_BLUE);
else
g_string_assign(prompt, "");
if (conn_state == STATE_CONNECTED)
g_string_append(prompt, COLOR_OFF);
return prompt->str;
}
static void set_state(enum state st)
{
printf("set_state\n");
conn_state = st;
rl_set_prompt(get_prompt());
}
static void disconnect_io()
{
printf("disconnect_io\n");
if (conn_state == STATE_DISCONNECTED)
return;
g_attrib_unref(attrib);
attrib = NULL;
opt_mtu = 0;
g_io_channel_shutdown(iochannel, FALSE, NULL);
g_io_channel_unref(iochannel);
iochannel = NULL;
set_state(STATE_DISCONNECTED);
}
static void connect_cb_main(GIOChannel *io, GError *err, gpointer user_data)
{
uint16_t mtu;
uint16_t cid;
printf("connect_cb_main\n");
if (err) {
set_state(STATE_DISCONNECTED);
error("%s\n", err->message);
return;
}
bt_io_get(io, &err, BT_IO_OPT_IMTU, &mtu,
BT_IO_OPT_CID, &cid, BT_IO_OPT_INVALID);
if (err) {
g_printerr("Can't detect MTU, using default: %s", err->message);
g_error_free(err);
mtu = ATT_DEFAULT_LE_MTU;
}
if (cid == ATT_CID)
mtu = ATT_DEFAULT_LE_MTU;
attrib = g_attrib_new(iochannel, mtu);
g_attrib_register(attrib, ATT_OP_HANDLE_NOTIFY, GATTRIB_ALL_HANDLES,
events_handler, attrib, NULL);
g_attrib_register(attrib, ATT_OP_HANDLE_IND, GATTRIB_ALL_HANDLES,
events_handler, attrib, NULL);
set_state(STATE_CONNECTED);
rl_printf("Connection successful\n");
}
static void connect_cb(GIOChannel *io, GError *err, gpointer user_data)
{
printf("connect_cb\n");
connect_cb_main(io, err, user_data);
}
static gboolean channel_watcher(GIOChannel *chan, GIOCondition cond,
gpointer user_data)
{
printf("channel_watcher\n");
disconnect_io();
return FALSE;
}
GIOChannel *gatt_connect(const char *src, const char *dst,
const char *dst_type, const char *sec_level,
int psm, int mtu, BtIOConnect connect_cb,
GError **gerr)
{
GIOChannel *chan;
bdaddr_t sba, dba;
uint8_t dest_type;
GError *tmp_err = NULL;
BtIOSecLevel sec;
printf("gatt_connect\n");
str2ba(dst, &dba);
/* Local adapter */
if (src != NULL) {
if (!strncmp(src, "hci", 3))
hci_devba(atoi(src + 3), &sba);
else
str2ba(src, &sba);
} else
bacpy(&sba, BDADDR_ANY);
/* Not used for BR/EDR */
if (strcmp(dst_type, "random") == 0)
dest_type = BDADDR_LE_RANDOM;
else
dest_type = BDADDR_LE_PUBLIC;
if (strcmp(sec_level, "medium") == 0)
sec = BT_IO_SEC_MEDIUM;
else if (strcmp(sec_level, "high") == 0)
sec = BT_IO_SEC_HIGH;
else
sec = BT_IO_SEC_LOW;
printf("gatt_connect1 psm:%x\n",psm);
if (psm == 0)
chan = bt_io_connect(connect_cb, NULL, NULL, &tmp_err,
BT_IO_OPT_SOURCE_BDADDR, &sba,
BT_IO_OPT_SOURCE_TYPE, BDADDR_LE_PUBLIC,
BT_IO_OPT_DEST_BDADDR, &dba,
BT_IO_OPT_DEST_TYPE, dest_type,
BT_IO_OPT_CID, ATT_CID,
BT_IO_OPT_SEC_LEVEL, sec,
BT_IO_OPT_INVALID);
else
chan = bt_io_connect(connect_cb, NULL, NULL, &tmp_err,
BT_IO_OPT_SOURCE_BDADDR, &sba,
BT_IO_OPT_DEST_BDADDR, &dba,
BT_IO_OPT_PSM, psm,
BT_IO_OPT_IMTU, mtu,
BT_IO_OPT_SEC_LEVEL, sec,
BT_IO_OPT_INVALID);
if (tmp_err) {
g_propagate_error(gerr, tmp_err);
return NULL;
}
return chan;
}
static void cmd_connect(int argcp, char **argvp)
{
GError *gerr = NULL;
printf("cmd_connect\n");
if (conn_state != STATE_DISCONNECTED)
return;
if (argcp > 1) {
g_free(opt_dst);
opt_dst = g_strdup(argvp[1]);
g_free(opt_dst_type);
if (argcp > 2)
opt_dst_type = g_strdup(argvp[2]);
else
opt_dst_type = g_strdup("public");
}
if (opt_dst == NULL) {
error("Remote Bluetooth address required\n");
return;
}
set_state(STATE_CONNECTING);
iochannel = gatt_connect(opt_src, opt_dst, opt_dst_type, opt_sec_level,
opt_psm, opt_mtu, connect_cb, &gerr);
if (iochannel == NULL) {
set_state(STATE_DISCONNECTED);
error("%s\n", gerr->message);
g_error_free(gerr);
} else
g_io_add_watch(iochannel, G_IO_HUP, channel_watcher, NULL);
}
static void cmd_disconnect(int argcp, char **argvp)
{
printf("cmd_disconnect\n");
disconnect_io();
}
static struct {
const char *cmd;
void (*func)(int argcp, char **argvp);
const char *params;
const char *desc;
} commands[] = {
{
"connect", cmd_connect, "[address [address type]]",
"Connect to a remote device"
},
{ NULL, NULL, NULL}
};
static void parse_line(char *line_read)
{
char **argvp;
int argcp;
int i;
if (line_read == NULL) {
rl_printf("\n");
cmd_exit(0, NULL);
return;
}
line_read = g_strstrip(line_read);
if (*line_read == '\0')
goto done;
add_history(line_read);
if (g_shell_parse_argv(line_read, &argcp, &argvp, NULL) == FALSE)
goto done;
for (i = 0; commands[i].cmd; i++)
if (strcasecmp(commands[i].cmd, argvp[0]) == 0)
break;
if (commands[i].cmd)
commands[i].func(argcp, argvp);
else
error("%s: command not found\n", argvp[0]);
g_strfreev(argvp);
done:
free(line_read);
}
static char *btComm = NULL;
int main(int argc, char *argv[])
{
guint signal;
printf("main\n");
//強制interactive
opt_src = g_strdup("hci0");
opt_dst = g_strdup("B4:99:4C:64:CD:DF");
opt_interactive = TRUE;
opt_dst_type = g_strdup("public");
opt_sec_level = g_strdup("low");
printf("sensortag\n");
opt_sec_level = g_strdup("low");
opt_src = g_strdup(opt_src);
opt_dst = g_strdup(opt_dst);
opt_dst_type = g_strdup(opt_dst_type);
opt_psm = opt_psm;
prompt = g_string_new(NULL);
printf("sensortag1\n");
event_loop = g_main_loop_new(NULL, FALSE);
signal = setup_signalfd();
printf("sensortag3\n");
btComm = g_strdup("connect");
parse_line(btComm);
g_main_loop_run(event_loop);
printf("sensortag4\n");
rl_callback_handler_remove();
cmd_disconnect(0, NULL);
g_source_remove(signal);
g_main_loop_unref(event_loop);
g_string_free(prompt, TRUE);
g_free(opt_src);
g_free(opt_dst);
g_free(opt_uuid);
g_free(opt_sec_level);
if (got_error)
exit(EXIT_FAILURE);
else
exit(EXIT_SUCCESS);
}
BLEプログラムの実行
作成したBLEプログラムをRaspberry Pi 3で実行した結果を次に示します。SentorTagをアドバタイズすると、Raspberry Pi 3がSentorTagから通知を受信し、「connect_cb」に制御が移り、ここから、「connect_cb_main」に制御が移って、SentorTagからデータを受信します。受信したデータをチェックした結果正常だったことが、「Connection successful」の表示でわかります。プログラムの終了はCNTL-Cをキーインしています。CNTL-Cをキーインすると、「signal_handler」が動作し、「disconnect_io」でSentorTagとの接続を切断していることがわかります。
$ sudo ./gatool main sensortag sensortag1 setup_signalfd sensortag3 cmd_connect set_state get_prompt gatt_connect gatt_connect1 psm:0 bt_io_connect bt_io_connect1 sockaddr_l2: 1f 00 00 00 df cd 64 4c 99 b4 04 00 01 00 bt_io_connect4 bt_io_connect5 connect_cb connect_cb_main set_state get_prompt Connection successful ^Csignal_handler cmd_exit sensortag4 cmd_disconnect disconnect_io (process:1041): GLib-WARNING **: Invalid file descriptor. set_state get_prompt
tshark使用してBLEで接続で使用するパケットの解析
パケットキャプチャソフト「tshark」使用して、Raspberry Pi 3とSentorTag間の接続で使用するパケットを解析しました。接続要求時に最初にRaspberry Pi 3とSentorTag間で送信されるパケット「LE Create Connection (0x000d)」の詳細情報を次に示します。
RaspberryPi で Error: connect: Connection refused (111)
OSとkernelを古いままでgatttoolコマンドによりBLEのConnectを行うと、次のように、「Error: connect: Connection refused (111)」のエラーメッセージが表示されることがあります。
$ sudo gatttool -b B4:99:4C:64:CD:DF --interactive [B4:99:4C:64:CD:DF][LE]> connect Attempting to connect to B4:99:4C:64:CD:DF Error: connect: Connection refused (111) [B4:99:4C:64:CD:DF][LE]>
この場合、OSとkernelを次のコマンドでアップグレードすると私の場合は正常に戻りました。
$ sudo apt-get dist-upgrade
Jessie になると bluez-utils というパッケージがなくなって bluez に統一されるなど、いくつか変更されているようで、BlueZの最新バージョンのソースを使用すると不整合が起こるようです。

