blob: a583eb7daf72aa354f0bb04d67337fdc1304bfe4 [file] [log] [blame]
/*
*
* BlueZ - Bluetooth protocol stack for Linux
*
* Copyright (C) 2011 Nokia Corporation
* Copyright (C) 2011 Marcel Holtmann <marcel@holtmann.org>
*
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <glib.h>
#include "lib/bluetooth.h"
#include "lib/sdp.h"
#include "lib/uuid.h"
#include "gdbus/gdbus.h"
#include "src/dbus-common.h"
#include "src/adapter.h"
#include "src/device.h"
#include "src/error.h"
#include "src/log.h"
#include "attrib/att.h"
#include "attrib/gattrib.h"
#include "attrib/gatt.h"
#include "src/attio.h"
#include "src/textfile.h"
#include "monitor.h"
#define PROXIMITY_INTERFACE "org.bluez.ProximityMonitor1"
#define ALERT_LEVEL_CHR_UUID 0x2A06
#define POWER_LEVEL_CHR_UUID 0x2A07
#define IMMEDIATE_TIMEOUT 5
#define TX_POWER_SIZE 1
enum {
ALERT_NONE = 0,
ALERT_MILD,
ALERT_HIGH,
};
struct monitor {
struct btd_device *device;
GAttrib *attrib;
struct att_range *linkloss;
struct att_range *txpower;
struct att_range *immediate;
struct enabled enabled;
char *linklosslevel; /* Link Loss Alert Level */
char *fallbacklevel; /* Immediate fallback alert level */
char *immediatelevel; /* Immediate Alert Level */
char *signallevel; /* Path Loss RSSI level */
uint16_t linklosshandle; /* Link Loss Characteristic
* Value Handle */
uint16_t txpowerhandle; /* Tx Characteristic Value Handle */
uint16_t immediatehandle; /* Immediate Alert Value Handle */
guint immediateto; /* Reset Immediate Alert to "none" */
guint attioid;
};
static GSList *monitors = NULL;
static struct monitor *find_monitor(struct btd_device *device)
{
GSList *l;
for (l = monitors; l; l = l->next) {
struct monitor *monitor = l->data;
if (monitor->device == device)
return monitor;
}
return NULL;
}
static void write_proximity_config(struct btd_device *device, const char *alert,
const char *level)
{
char *filename;
GKeyFile *key_file;
char *data;
gsize length = 0;
filename = btd_device_get_storage_path(device, "proximity");
if (!filename) {
warn("Unable to get proximity storage path for device");
return;
}
key_file = g_key_file_new();
g_key_file_load_from_file(key_file, filename, 0, NULL);
if (level)
g_key_file_set_string(key_file, alert, "Level", level);
else
g_key_file_remove_group(key_file, alert, NULL);
data = g_key_file_to_data(key_file, &length, NULL);
if (length > 0) {
create_file(filename, S_IRUSR | S_IWUSR);
g_file_set_contents(filename, data, length, NULL);
}
g_free(data);
g_free(filename);
g_key_file_free(key_file);
}
static char *read_proximity_config(struct btd_device *device, const char *alert)
{
char *filename;
GKeyFile *key_file;
char *str;
filename = btd_device_get_storage_path(device, "proximity");
if (!filename) {
warn("Unable to get proximity storage path for device");
return NULL;
}
key_file = g_key_file_new();
g_key_file_load_from_file(key_file, filename, 0, NULL);
str = g_key_file_get_string(key_file, alert, "Level", NULL);
g_free(filename);
g_key_file_free(key_file);
return str;
}
static uint8_t str2level(const char *level)
{
if (g_strcmp0("high", level) == 0)
return ALERT_HIGH;
else if (g_strcmp0("mild", level) == 0)
return ALERT_MILD;
return ALERT_NONE;
}
static void linkloss_written(guint8 status, const guint8 *pdu, guint16 plen,
gpointer user_data)
{
struct monitor *monitor = user_data;
struct btd_device *device = monitor->device;
const char *path = device_get_path(device);
if (status != 0) {
error("Link Loss Write Request failed: %s",
att_ecode2str(status));
return;
}
if (!dec_write_resp(pdu, plen)) {
error("Link Loss Write Request: protocol error");
return;
}
DBG("Link Loss Alert Level written");
g_dbus_emit_property_changed(btd_get_dbus_connection(), path,
PROXIMITY_INTERFACE, "LinkLossAlertLevel");
}
static void char_discovered_cb(uint8_t status, GSList *characteristics,
void *user_data)
{
struct monitor *monitor = user_data;
struct gatt_char *chr;
uint8_t value = str2level(monitor->linklosslevel);
if (status) {
error("Discover Link Loss handle: %s", att_ecode2str(status));
return;
}
DBG("Setting alert level \"%s\" on Reporter", monitor->linklosslevel);
/* Assume there is a single Alert Level characteristic */
chr = characteristics->data;
monitor->linklosshandle = chr->value_handle;
gatt_write_char(monitor->attrib, monitor->linklosshandle, &value, 1,
linkloss_written, monitor);
}
static int write_alert_level(struct monitor *monitor)
{
struct att_range *linkloss = monitor->linkloss;
bt_uuid_t uuid;
if (monitor->linklosshandle) {
uint8_t value = str2level(monitor->linklosslevel);
gatt_write_char(monitor->attrib, monitor->linklosshandle,
&value, 1, linkloss_written, monitor);
return 0;
}
bt_uuid16_create(&uuid, ALERT_LEVEL_CHR_UUID);
/* FIXME: use cache (requires service changed support) ? */
gatt_discover_char(monitor->attrib, linkloss->start, linkloss->end,
&uuid, char_discovered_cb, monitor);
return 0;
}
static void tx_power_read_cb(guint8 status, const guint8 *pdu, guint16 plen,
gpointer user_data)
{
uint8_t value[TX_POWER_SIZE];
ssize_t vlen;
if (status != 0) {
DBG("Tx Power Level read failed: %s", att_ecode2str(status));
return;
}
vlen = dec_read_resp(pdu, plen, value, sizeof(value));
if (vlen < 0) {
DBG("Protocol error");
return;
}
if (vlen != 1) {
DBG("Invalid length for TX Power value: %zd", vlen);
return;
}
DBG("Tx Power Level: %02x", (int8_t) value[0]);
}
static void tx_power_handle_cb(uint8_t status, GSList *characteristics,
void *user_data)
{
struct monitor *monitor = user_data;
struct gatt_char *chr;
if (status) {
error("Discover Tx Power handle: %s", att_ecode2str(status));
return;
}
chr = characteristics->data;
monitor->txpowerhandle = chr->value_handle;
DBG("Tx Power handle: 0x%04x", monitor->txpowerhandle);
gatt_read_char(monitor->attrib, monitor->txpowerhandle,
tx_power_read_cb, monitor);
}
static void read_tx_power(struct monitor *monitor)
{
struct att_range *txpower = monitor->txpower;
bt_uuid_t uuid;
if (monitor->txpowerhandle != 0) {
gatt_read_char(monitor->attrib, monitor->txpowerhandle,
tx_power_read_cb, monitor);
return;
}
bt_uuid16_create(&uuid, POWER_LEVEL_CHR_UUID);
gatt_discover_char(monitor->attrib, txpower->start, txpower->end,
&uuid, tx_power_handle_cb, monitor);
}
static gboolean immediate_timeout(gpointer user_data)
{
struct monitor *monitor = user_data;
const char *path = device_get_path(monitor->device);
monitor->immediateto = 0;
if (g_strcmp0(monitor->immediatelevel, "none") == 0)
return FALSE;
if (monitor->attrib) {
uint8_t value = ALERT_NONE;
gatt_write_cmd(monitor->attrib, monitor->immediatehandle,
&value, 1, NULL, NULL);
}
g_free(monitor->immediatelevel);
monitor->immediatelevel = g_strdup("none");
g_dbus_emit_property_changed(btd_get_dbus_connection(), path,
PROXIMITY_INTERFACE, "ImmediateAlertLevel");
return FALSE;
}
static void immediate_written(gpointer user_data)
{
struct monitor *monitor = user_data;
const char *path = device_get_path(monitor->device);
g_free(monitor->fallbacklevel);
monitor->fallbacklevel = NULL;
g_dbus_emit_property_changed(btd_get_dbus_connection(), path,
PROXIMITY_INTERFACE, "ImmediateAlertLevel");
monitor->immediateto = g_timeout_add_seconds(IMMEDIATE_TIMEOUT,
immediate_timeout, monitor);
}
static void write_immediate_alert(struct monitor *monitor)
{
uint8_t value = str2level(monitor->immediatelevel);
gatt_write_cmd(monitor->attrib, monitor->immediatehandle, &value, 1,
immediate_written, monitor);
}
static void immediate_handle_cb(uint8_t status, GSList *characteristics,
void *user_data)
{
struct monitor *monitor = user_data;
struct gatt_char *chr;
if (status) {
error("Discover Immediate Alert handle: %s",
att_ecode2str(status));
return;
}
chr = characteristics->data;
monitor->immediatehandle = chr->value_handle;
DBG("Immediate Alert handle: 0x%04x", monitor->immediatehandle);
if (monitor->fallbacklevel)
write_immediate_alert(monitor);
}
static void discover_immediate_handle(struct monitor *monitor)
{
struct att_range *immediate = monitor->immediate;
bt_uuid_t uuid;
bt_uuid16_create(&uuid, ALERT_LEVEL_CHR_UUID);
gatt_discover_char(monitor->attrib, immediate->start, immediate->end,
&uuid, immediate_handle_cb, monitor);
}
static void attio_connected_cb(GAttrib *attrib, gpointer user_data)
{
struct monitor *monitor = user_data;
monitor->attrib = g_attrib_ref(attrib);
if (monitor->enabled.linkloss)
write_alert_level(monitor);
if (monitor->enabled.pathloss)
read_tx_power(monitor);
if (monitor->immediatehandle == 0) {
if(monitor->enabled.pathloss || monitor->enabled.findme)
discover_immediate_handle(monitor);
} else if (monitor->fallbacklevel)
write_immediate_alert(monitor);
}
static void attio_disconnected_cb(gpointer user_data)
{
struct monitor *monitor = user_data;
const char *path = device_get_path(monitor->device);
g_attrib_unref(monitor->attrib);
monitor->attrib = NULL;
if (monitor->immediateto == 0)
return;
g_source_remove(monitor->immediateto);
monitor->immediateto = 0;
if (g_strcmp0(monitor->immediatelevel, "none") == 0)
return;
g_free(monitor->immediatelevel);
monitor->immediatelevel = g_strdup("none");
g_dbus_emit_property_changed(btd_get_dbus_connection(), path,
PROXIMITY_INTERFACE, "ImmediateAlertLevel");
}
static gboolean level_is_valid(const char *level)
{
return (g_str_equal("none", level) ||
g_str_equal("mild", level) ||
g_str_equal("high", level));
}
static gboolean property_get_link_loss_level(const GDBusPropertyTable *property,
DBusMessageIter *iter, void *data)
{
struct monitor *monitor = data;
dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING,
&monitor->linklosslevel);
return TRUE;
}
static void property_set_link_loss_level(const GDBusPropertyTable *property,
DBusMessageIter *iter, GDBusPendingPropertySet id, void *data)
{
struct monitor *monitor = data;
const char *level;
if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_STRING) {
g_dbus_pending_property_error(id,
ERROR_INTERFACE ".InvalidArguments",
"Invalid arguments in method call");
return;
}
dbus_message_iter_get_basic(iter, &level);
if (!level_is_valid(level)) {
g_dbus_pending_property_error(id,
ERROR_INTERFACE ".InvalidArguments",
"Invalid arguments in method call");
return;
}
if (g_strcmp0(monitor->linklosslevel, level) == 0)
goto done;
g_free(monitor->linklosslevel);
monitor->linklosslevel = g_strdup(level);
write_proximity_config(monitor->device, "LinkLossAlertLevel", level);
if (monitor->attrib)
write_alert_level(monitor);
done:
g_dbus_pending_property_success(id);
}
static gboolean property_exists_link_loss_level(
const GDBusPropertyTable *property, void *data)
{
struct monitor *monitor = data;
if (!monitor->enabled.linkloss)
return FALSE;
return TRUE;
}
static gboolean property_get_immediate_alert_level(
const GDBusPropertyTable *property,
DBusMessageIter *iter, void *data)
{
struct monitor *monitor = data;
dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING,
&monitor->immediatelevel);
return TRUE;
}
static void property_set_immediate_alert_level(
const GDBusPropertyTable *property, DBusMessageIter *iter,
GDBusPendingPropertySet id, void *data)
{
struct monitor *monitor = data;
struct btd_device *device = monitor->device;
const char *level;
if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_STRING) {
g_dbus_pending_property_error(id,
ERROR_INTERFACE ".InvalidArguments",
"Invalid arguments in method call");
return;
}
dbus_message_iter_get_basic(iter, &level);
if (!level_is_valid(level)) {
g_dbus_pending_property_error(id,
ERROR_INTERFACE ".InvalidArguments",
"Invalid arguments in method call");
return;
}
if (g_strcmp0(monitor->immediatelevel, level) == 0)
goto done;
if (monitor->immediateto) {
g_source_remove(monitor->immediateto);
monitor->immediateto = 0;
}
/* Previous Immediate Alert level if connection/write fails */
g_free(monitor->fallbacklevel);
monitor->fallbacklevel = monitor->immediatelevel;
monitor->immediatelevel = g_strdup(level);
/*
* Means that Link/Path Loss are disabled or there is a pending
* writting for Find Me(Immediate Alert characteristic value).
* If enabled, Path Loss always registers a connection callback
* when the Proximity Monitor starts.
*/
if (monitor->attioid == 0)
monitor->attioid = btd_device_add_attio_callback(device,
attio_connected_cb,
attio_disconnected_cb,
monitor);
else if (monitor->attrib)
write_immediate_alert(monitor);
done:
g_dbus_pending_property_success(id);
}
static gboolean property_exists_immediate_alert_level(
const GDBusPropertyTable *property, void *data)
{
struct monitor *monitor = data;
if (!(monitor->enabled.findme || monitor->enabled.pathloss))
return FALSE;
return TRUE;
}
static gboolean property_get_signal_level(
const GDBusPropertyTable *property,
DBusMessageIter *iter, void *data)
{
struct monitor *monitor = data;
dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING,
&monitor->signallevel);
return TRUE;
}
static gboolean property_exists_signal_level(const GDBusPropertyTable *property,
void *data)
{
struct monitor *monitor = data;
if (!monitor->enabled.pathloss)
return FALSE;
return TRUE;
}
static const GDBusPropertyTable monitor_device_properties[] = {
{ "LinkLossAlertLevel", "s", property_get_link_loss_level,
property_set_link_loss_level,
property_exists_link_loss_level },
{ "ImmediateAlertLevel", "s", property_get_immediate_alert_level,
property_set_immediate_alert_level,
property_exists_immediate_alert_level },
{ "SignalLevel", "s", property_get_signal_level, NULL,
property_exists_signal_level },
{ }
};
static void monitor_destroy(gpointer user_data)
{
struct monitor *monitor = user_data;
monitors = g_slist_remove(monitors, monitor);
btd_device_unref(monitor->device);
g_free(monitor->linklosslevel);
g_free(monitor->immediatelevel);
g_free(monitor->signallevel);
g_free(monitor);
}
static struct monitor *register_monitor(struct btd_device *device)
{
const char *path = device_get_path(device);
struct monitor *monitor;
char *level;
monitor = find_monitor(device);
if (monitor != NULL)
return monitor;
level = read_proximity_config(device, "LinkLossAlertLevel");
monitor = g_new0(struct monitor, 1);
monitor->device = btd_device_ref(device);
monitor->linklosslevel = (level ? : g_strdup("high"));
monitor->signallevel = g_strdup("unknown");
monitor->immediatelevel = g_strdup("none");
monitors = g_slist_append(monitors, monitor);
if (g_dbus_register_interface(btd_get_dbus_connection(), path,
PROXIMITY_INTERFACE,
NULL, NULL, monitor_device_properties,
monitor, monitor_destroy) == FALSE) {
error("D-Bus failed to register %s interface",
PROXIMITY_INTERFACE);
monitor_destroy(monitor);
return NULL;
}
DBG("Registered interface %s on path %s", PROXIMITY_INTERFACE, path);
return monitor;
}
static void update_monitor(struct monitor *monitor)
{
if (monitor->txpower != NULL && monitor->immediate != NULL)
monitor->enabled.pathloss = TRUE;
else
monitor->enabled.pathloss = FALSE;
DBG("Link Loss: %s, Path Loss: %s, FindMe: %s",
monitor->enabled.linkloss ? "TRUE" : "FALSE",
monitor->enabled.pathloss ? "TRUE" : "FALSE",
monitor->enabled.findme ? "TRUE" : "FALSE");
if (!monitor->enabled.linkloss && !monitor->enabled.pathloss)
return;
if (monitor->attioid != 0)
return;
monitor->attioid = btd_device_add_attio_callback(monitor->device,
attio_connected_cb,
attio_disconnected_cb,
monitor);
}
int monitor_register_linkloss(struct btd_device *device,
struct enabled *enabled,
struct gatt_primary *linkloss)
{
struct monitor *monitor;
if (!enabled->linkloss)
return 0;
monitor = register_monitor(device);
if (monitor == NULL)
return -1;
monitor->linkloss = g_new0(struct att_range, 1);
monitor->linkloss->start = linkloss->range.start;
monitor->linkloss->end = linkloss->range.end;
monitor->enabled.linkloss = TRUE;
update_monitor(monitor);
return 0;
}
int monitor_register_txpower(struct btd_device *device,
struct enabled *enabled,
struct gatt_primary *txpower)
{
struct monitor *monitor;
if (!enabled->pathloss)
return 0;
monitor = register_monitor(device);
if (monitor == NULL)
return -1;
monitor->txpower = g_new0(struct att_range, 1);
monitor->txpower->start = txpower->range.start;
monitor->txpower->end = txpower->range.end;
update_monitor(monitor);
return 0;
}
int monitor_register_immediate(struct btd_device *device,
struct enabled *enabled,
struct gatt_primary *immediate)
{
struct monitor *monitor;
if (!enabled->pathloss && !enabled->findme)
return 0;
monitor = register_monitor(device);
if (monitor == NULL)
return -1;
monitor->immediate = g_new0(struct att_range, 1);
monitor->immediate->start = immediate->range.start;
monitor->immediate->end = immediate->range.end;
monitor->enabled.findme = enabled->findme;
update_monitor(monitor);
return 0;
}
static void cleanup_monitor(struct monitor *monitor)
{
struct btd_device *device = monitor->device;
const char *path = device_get_path(device);
if (monitor->immediate != NULL || monitor->txpower != NULL)
return;
if (monitor->immediateto != 0) {
g_source_remove(monitor->immediateto);
monitor->immediateto = 0;
}
if (monitor->linkloss != NULL)
return;
if (monitor->attioid != 0) {
btd_device_remove_attio_callback(device, monitor->attioid);
monitor->attioid = 0;
}
if (monitor->attrib != NULL) {
g_attrib_unref(monitor->attrib);
monitor->attrib = NULL;
}
g_dbus_unregister_interface(btd_get_dbus_connection(), path,
PROXIMITY_INTERFACE);
}
void monitor_unregister_linkloss(struct btd_device *device)
{
struct monitor *monitor;
monitor = find_monitor(device);
if (monitor == NULL)
return;
g_free(monitor->linkloss);
monitor->linkloss = NULL;
monitor->enabled.linkloss = FALSE;
cleanup_monitor(monitor);
}
void monitor_unregister_txpower(struct btd_device *device)
{
struct monitor *monitor;
monitor = find_monitor(device);
if (monitor == NULL)
return;
g_free(monitor->txpower);
monitor->txpower = NULL;
monitor->enabled.pathloss = FALSE;
cleanup_monitor(monitor);
}
void monitor_unregister_immediate(struct btd_device *device)
{
struct monitor *monitor;
monitor = find_monitor(device);
if (monitor == NULL)
return;
g_free(monitor->immediate);
monitor->immediate = NULL;
monitor->enabled.findme = FALSE;
monitor->enabled.pathloss = FALSE;
cleanup_monitor(monitor);
}