From 47ada94baa424e56d2ded256fddc91e6aa4d3090 Mon Sep 17 00:00:00 2001 From: "Javier S. Pedro" Date: Tue, 3 Dec 2013 03:05:47 +0100 Subject: initial import --- gato.h | 13 + gatoaddress.cpp | 110 +++++++ gatoaddress.h | 32 ++ gatoatt.cpp | 578 ++++++++++++++++++++++++++++++++++ gatoatt.h | 97 ++++++ gatocentralmanager.cpp | 222 +++++++++++++ gatocentralmanager.h | 44 +++ gatocentralmanager_p.h | 32 ++ gatocharacteristic.cpp | 159 ++++++++++ gatocharacteristic.h | 69 ++++ gatodescriptor.cpp | 72 +++++ gatodescriptor.h | 32 ++ gatoperipheral.cpp | 834 +++++++++++++++++++++++++++++++++++++++++++++++++ gatoperipheral.h | 67 ++++ gatoperipheral_p.h | 71 +++++ gatoservice.cpp | 129 ++++++++ gatoservice.h | 42 +++ gatosocket.cpp | 193 ++++++++++++ gatosocket.h | 54 ++++ gatouuid.cpp | 184 +++++++++++ gatouuid.h | 109 +++++++ helpers.cpp | 64 ++++ helpers.h | 35 +++ libgato.pro | 39 +++ libgato_global.h | 14 + 25 files changed, 3295 insertions(+) create mode 100644 gato.h create mode 100644 gatoaddress.cpp create mode 100644 gatoaddress.h create mode 100644 gatoatt.cpp create mode 100644 gatoatt.h create mode 100644 gatocentralmanager.cpp create mode 100644 gatocentralmanager.h create mode 100644 gatocentralmanager_p.h create mode 100644 gatocharacteristic.cpp create mode 100644 gatocharacteristic.h create mode 100644 gatodescriptor.cpp create mode 100644 gatodescriptor.h create mode 100644 gatoperipheral.cpp create mode 100644 gatoperipheral.h create mode 100644 gatoperipheral_p.h create mode 100644 gatoservice.cpp create mode 100644 gatoservice.h create mode 100644 gatosocket.cpp create mode 100644 gatosocket.h create mode 100644 gatouuid.cpp create mode 100644 gatouuid.h create mode 100644 helpers.cpp create mode 100644 helpers.h create mode 100644 libgato.pro create mode 100644 libgato_global.h diff --git a/gato.h b/gato.h new file mode 100644 index 0000000..833ae59 --- /dev/null +++ b/gato.h @@ -0,0 +1,13 @@ +#ifndef GATO_H +#define GATO_H + +#include "libgato_global.h" +#include "gatoaddress.h" +#include "gatouuid.h" +#include "gatocentralmanager.h" +#include "gatoperipheral.h" +#include "gatoservice.h" +#include "gatocharacteristic.h" +#include "gatodescriptor.h" + +#endif // GATO_H diff --git a/gatoaddress.cpp b/gatoaddress.cpp new file mode 100644 index 0000000..e29190b --- /dev/null +++ b/gatoaddress.cpp @@ -0,0 +1,110 @@ +/* + * libgato - A GATT/ATT library for use with Bluez + * + * Copyright (C) 2013 Javier S. Pedro + * + * 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 + */ + +#include +#include +#include + +#include + +#include "gatoaddress.h" + +struct GatoAddressPrivate : QSharedData +{ + union { + bdaddr_t bd; + quint64 u64; + } addr; +}; + +GatoAddress::GatoAddress() + : d(new GatoAddressPrivate) +{ +} + + + +GatoAddress::GatoAddress(quint64 addr) + : d(new GatoAddressPrivate) +{ + d->addr.u64 = addr; +} + +GatoAddress::GatoAddress(quint8 addr[]) + : d(new GatoAddressPrivate) +{ + d->addr.u64 = 0; + for (int i = 0; i < 6; i++) { + d->addr.bd.b[i] = addr[i]; + } +} + +GatoAddress::GatoAddress(const QString &addr) + : d(new GatoAddressPrivate) +{ + d->addr.u64 = 0; + str2ba(addr.toLatin1().constData(), &d->addr.bd); +} + +GatoAddress::GatoAddress(const GatoAddress &o) + : d(o.d) +{ +} + +GatoAddress::~GatoAddress() +{ +} + +GatoAddress & GatoAddress::operator =(const GatoAddress& o) +{ + if (this != &o) { + d = o.d; + } + return *this; +} + +quint64 GatoAddress::toUInt64() const +{ + return d->addr.u64; +} + +void GatoAddress::toUInt8Array(quint8 addr[]) const +{ + for (int i = 0; i < 6; i++) { + addr[i] = d->addr.bd.b[i]; + } +} + +QString GatoAddress::toString() const +{ + char addr[18]; + ba2str(&d->addr.bd, addr); + return QString::fromAscii(addr); +} + +bool operator==(const GatoAddress &a, const GatoAddress &b) +{ + return a.toUInt64() == b.toUInt64(); +} + +uint qHash(const GatoAddress &a) +{ + return qHash(a.toUInt64()); +} diff --git a/gatoaddress.h b/gatoaddress.h new file mode 100644 index 0000000..f30ec43 --- /dev/null +++ b/gatoaddress.h @@ -0,0 +1,32 @@ +#ifndef GATOADDRESS_H +#define GATOADDRESS_H + +#include +#include "libgato_global.h" + +class GatoAddressPrivate; + +class LIBGATO_EXPORT GatoAddress +{ +public: + GatoAddress(); + explicit GatoAddress(quint8 addr[]); + explicit GatoAddress(quint64 addr); + explicit GatoAddress(const QString &addr); + GatoAddress(const GatoAddress& o); + ~GatoAddress(); + + GatoAddress& operator=(const GatoAddress& o); + + void toUInt8Array(quint8 addr[]) const; + quint64 toUInt64() const; + QString toString() const; + +private: + QSharedDataPointer d; +}; + +LIBGATO_EXPORT bool operator==(const GatoAddress &a, const GatoAddress &b); +LIBGATO_EXPORT uint qHash(const GatoAddress &a); + +#endif // GATOADDRESS_H diff --git a/gatoatt.cpp b/gatoatt.cpp new file mode 100644 index 0000000..8015757 --- /dev/null +++ b/gatoatt.cpp @@ -0,0 +1,578 @@ +/* + * libgato - A GATT/ATT library for use with Bluez + * + * Copyright (C) 2013 Javier S. Pedro + * + * 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 + */ + +#include + +#include "gatoatt.h" +#include "helpers.h" + +#define PROTOCOL_DEBUG 0 + +#define ATT_CID 4 +#define ATT_PSM 31 + +#define ATT_DEFAULT_LE_MTU 23 + +enum AttOpcode { + AttOpNone = 0, + AttOpErrorResponse = 0x1, + AttOpExchangeMTURequest = 0x2, + AttOpExchangeMTUResponse = 0x3, + AttOpFindInformationRequest = 0x4, + AttOpFindInformationResponse = 0x5, + AttOpFindByTypeValueRequest = 0x6, + AttOpFindByTypeValueResponse = 0x7, + AttOpReadByTypeRequest = 0x8, + AttOpReadByTypeResponse = 0x9, + AttOpReadRequest = 0xA, + AttOpReadResponse = 0xB, + AttOpReadBlobRequest = 0xC, + AttOpReadBlobResponse = 0xD, + AttOpReadMultipleRequest = 0xE, + AttOpReadMultipleResponse = 0xF, + AttOpReadByGroupTypeRequest = 0x10, + AttOpReadByGroupTypeResponse = 0x11, + AttOpWriteRequest = 0x12, + AttOpWriteResponse = 0x13, + AttOpWriteCommand = 0x52, + AttOpPrepareWriteRequest = 0x16, + AttOpPrepareWriteResponse = 0x17, + AttOpExecuteWriteRequest = 0x18, + AttOpExecuteWriteResponse = 0x19, + AttOpHandleValueNotification = 0x1B, + AttOpHandleValueIndication = 0x1D, + AttOpHandleValueConfirmation = 0x1E, + AttOpSignedWriteCommand = 0xD2 +}; + +static QByteArray remove_method_signature(const char *sig) +{ + const char* bracketPosition = strchr(sig, '('); + if (!bracketPosition || !(sig[0] >= '0' && sig[0] <= '3')) { + qWarning("Invalid slot specification"); + return QByteArray(); + } + return QByteArray(sig + 1, bracketPosition - 1 - sig); +} + +GatoAtt::GatoAtt(QObject *parent) : + QObject(parent), socket(new GatoSocket(this)), mtu(ATT_DEFAULT_LE_MTU), next_id(1) +{ + connect(socket, SIGNAL(connected()), SLOT(handleSocketConnected())); + connect(socket, SIGNAL(disconnected()), SLOT(handleSocketDisconnected())); + connect(socket, SIGNAL(readyRead()), SLOT(handleSocketReadyRead())); +} + +GatoAtt::~GatoAtt() +{ +} + +GatoSocket::State GatoAtt::state() const +{ + return socket->state(); +} + +bool GatoAtt::connectTo(const GatoAddress &addr) +{ + return socket->connectTo(addr, ATT_CID); +} + +void GatoAtt::close() +{ + socket->close(); +} + +uint GatoAtt::request(int opcode, const QByteArray &data, QObject *receiver, const char *member) +{ + Request req; + req.id = next_id++; + req.opcode = opcode; + req.pkt = data; + req.pkt.prepend(static_cast(opcode)); + req.receiver = receiver; + req.member = remove_method_signature(member); + + pending_requests.enqueue(req); + + if (pending_requests.size() == 1) { + // So we can just send this request instead of waiting for others to complete + sendARequest(); + } + + return req.id; +} + +void GatoAtt::cancelRequest(uint id) +{ + QQueue::iterator it = pending_requests.begin(); + while (it != pending_requests.end()) { + if (it->id == id) { + it = pending_requests.erase(it); + } else { + ++it; + } + } +} + +uint GatoAtt::requestExchangeMTU(quint8 client_mtu, QObject *receiver, const char *member) +{ + QByteArray data(1, client_mtu); + return request(AttOpExchangeMTURequest, data, receiver, member); +} + +uint GatoAtt::requestFindInformation(GatoHandle start, GatoHandle end, QObject *receiver, const char *member) +{ + QByteArray data; + QDataStream s(&data, QIODevice::WriteOnly); + s.setByteOrder(QDataStream::LittleEndian); + s << start << end; + + return request(AttOpFindInformationRequest, data, receiver, member); +} + +uint GatoAtt::requestFindByTypeValue(GatoHandle start, GatoHandle end, const GatoUUID &uuid, const QByteArray &value, QObject *receiver, const char *member) +{ + QByteArray data; + QDataStream s(&data, QIODevice::WriteOnly); + s.setByteOrder(QDataStream::LittleEndian); + s << start << end; + + bool uuid16_ok; + quint16 uuid16 = uuid.toUInt16(&uuid16_ok); + if (uuid16_ok) { + s << uuid16; + } else { + qWarning() << "FindByTypeValue does not support UUIDs other than UUID16"; + return -1; + } + + s << value; + + return request(AttOpFindByTypeValueRequest, data, receiver, member); +} + +uint GatoAtt::requestReadByType(GatoHandle start, GatoHandle end, const GatoUUID &uuid, QObject *receiver, const char *member) +{ + QByteArray data; + QDataStream s(&data, QIODevice::WriteOnly); + s.setByteOrder(QDataStream::LittleEndian); + s << start << end; + writeUuid16or128(s, uuid); + + return request(AttOpReadByTypeRequest, data, receiver, member); +} + +uint GatoAtt::requestRead(GatoHandle handle, QObject *receiver, const char *member) +{ + QByteArray data; + QDataStream s(&data, QIODevice::WriteOnly); + s.setByteOrder(QDataStream::LittleEndian); + s << handle; + + return request(AttOpReadRequest, data, receiver, member); +} + +uint GatoAtt::requestReadByGroupType(GatoHandle start, GatoHandle end, const GatoUUID &uuid, QObject *receiver, const char *member) +{ + QByteArray data; + QDataStream s(&data, QIODevice::WriteOnly); + s.setByteOrder(QDataStream::LittleEndian); + s << start << end; + writeUuid16or128(s, uuid); + + return request(AttOpReadByGroupTypeRequest, data, receiver, member); +} + +uint GatoAtt::requestWrite(GatoHandle handle, const QByteArray &value, QObject *receiver, const char *member) +{ + QByteArray data; + QDataStream s(&data, QIODevice::WriteOnly); + s.setByteOrder(QDataStream::LittleEndian); + s << handle; + s.writeRawData(value.constData(), value.length()); + + return request(AttOpWriteRequest, data, receiver, member); +} + +void GatoAtt::command(int opcode, const QByteArray &data) +{ + QByteArray packet = data; + packet.prepend(static_cast(opcode)); + + socket->send(packet); + +#if PROTOCOL_DEBUG + qDebug() << "Wrote" << packet.size() << "bytes (command)" << packet.toHex(); +#endif +} + +void GatoAtt::sendARequest() +{ + if (pending_requests.isEmpty()) { + return; + } + + Request &req = pending_requests.head(); + socket->send(req.pkt); + +#if PROTOCOL_DEBUG + qDebug() << "Wrote" << req.pkt.size() << "bytes (request)" << req.pkt.toHex(); +#endif +} + +bool GatoAtt::handleEvent(const QByteArray &event) +{ + const char *data = event.constData(); + quint8 opcode = event[0]; + GatoHandle handle; + + switch (opcode) { + case AttOpHandleValueNotification: + handle = read_le(&data[1]); + emit attributeUpdated(handle, event.mid(3), false); + return true; + case AttOpHandleValueIndication: + handle = read_le(&data[1]); + + // Send the confirmation back + command(AttOpHandleValueConfirmation, QByteArray()); + + emit attributeUpdated(handle, event.mid(3), true); + return true; + default: + return false; + } +} + +bool GatoAtt::handleResponse(const Request &req, const QByteArray &response) +{ + // If we know the request, we can provide a decoded answer + switch (req.opcode) { + case AttOpExchangeMTURequest: + if (response[0] == AttOpExchangeMTUResponse) { + if (req.receiver) { + QMetaObject::invokeMethod(req.receiver, req.member.constData(), + Q_ARG(uint, req.id), + Q_ARG(quint8, response[1])); + } + return true; + } else if (response[0] == AttOpErrorResponse && response[1] == AttOpExchangeMTURequest) { + if (req.receiver) { + QMetaObject::invokeMethod(req.receiver, req.member.constData(), + Q_ARG(uint, req.id), + Q_ARG(quint8, response[1])); + } + return true; + } else { + return false; + } + break; + case AttOpFindInformationRequest: + if (response[0] == AttOpFindInformationResponse) { + if (req.receiver) { + QMetaObject::invokeMethod(req.receiver, req.member.constData(), + Q_ARG(uint, req.id), + Q_ARG(QList, parseInformationData(response.mid(1)))); + } + return true; + } else if (response[0] == AttOpErrorResponse && response[1] == AttOpFindInformationRequest) { + if (req.receiver) { + QMetaObject::invokeMethod(req.receiver, req.member.constData(), + Q_ARG(uint, req.id), + Q_ARG(QList, QList())); + } + return true; + } else { + return false; + } + break; + case AttOpFindByTypeValueRequest: + if (response[0] == AttOpFindByTypeValueResponse) { + if (req.receiver) { + QMetaObject::invokeMethod(req.receiver, req.member.constData(), + Q_ARG(uint, req.id), + Q_ARG(QList, parseHandleInformation(response.mid(1)))); + } + return true; + } else if (response[0] == AttOpErrorResponse && response[1] == AttOpFindByTypeValueRequest) { + if (req.receiver) { + QMetaObject::invokeMethod(req.receiver, req.member.constData(), + Q_ARG(uint, req.id), + Q_ARG(QList, QList())); + } + return true; + } else { + return false; + } + break; + case AttOpReadByTypeRequest: + if (response[0] == AttOpReadByTypeResponse) { + if (req.receiver) { + QMetaObject::invokeMethod(req.receiver, req.member.constData(), + Q_ARG(uint, req.id), + Q_ARG(QList, parseAttributeData(response.mid(1)))); + } + return true; + } else if (response[0] == AttOpErrorResponse && response[1] == AttOpReadByTypeRequest) { + if (req.receiver) { + QMetaObject::invokeMethod(req.receiver, req.member.constData(), + Q_ARG(uint, req.id), + Q_ARG(QList, QList())); + } + return true; + } else { + return false; + } + break; + case AttOpReadRequest: + if (response[0] == AttOpReadResponse) { + if (req.receiver) { + QMetaObject::invokeMethod(req.receiver, req.member.constData(), + Q_ARG(uint, req.id), + Q_ARG(QByteArray, response.mid(1))); + } + return true; + } else if (response[0] == AttOpErrorResponse && response[1] == AttOpReadRequest) { + if (req.receiver) { + QMetaObject::invokeMethod(req.receiver, req.member.constData(), + Q_ARG(uint, req.id), + Q_ARG(QByteArray, QByteArray())); + } + return true; + } else { + return false; + } + break; + case AttOpReadByGroupTypeRequest: + if (response[0] == AttOpReadByGroupTypeResponse) { + if (req.receiver) { + QMetaObject::invokeMethod(req.receiver, req.member.constData(), + Q_ARG(uint, req.id), + Q_ARG(QList, parseAttributeGroupData(response.mid(1)))); + } + return true; + } else if (response[0] == AttOpErrorResponse && response[1] == AttOpReadByGroupTypeRequest) { + if (req.receiver) { + QMetaObject::invokeMethod(req.receiver, req.member.constData(), + Q_ARG(uint, req.id), + Q_ARG(QList, QList())); + } + return true; + } else { + return false; + } + break; + case AttOpWriteRequest: + if (response[0] == AttOpWriteResponse) { + if (req.receiver) { + QMetaObject::invokeMethod(req.receiver, req.member.constData(), + Q_ARG(uint, req.id), + Q_ARG(bool, true)); + } + return true; + } else if (response[0] == AttOpErrorResponse && response[1] == AttOpWriteRequest) { + if (req.receiver) { + QMetaObject::invokeMethod(req.receiver, req.member.constData(), + Q_ARG(uint, req.id), + Q_ARG(bool, false)); + } + return true; + } else { + return false; + } + break; + default: // Otherwise just send a QByteArray. + if (req.receiver) { + QMetaObject::invokeMethod(req.receiver, req.member.constData(), + Q_ARG(const QByteArray&, response)); + } + return true; + } +} + +void GatoAtt::writeUuid16or128(QDataStream &s, const GatoUUID &uuid) +{ + s.setByteOrder(QDataStream::LittleEndian); + + bool uuid16_ok; + quint16 uuid16 = uuid.toUInt16(&uuid16_ok); + if (uuid16_ok) { + s << uuid16; + } else { + s << uuid.toUInt128(); + } +} + +QList GatoAtt::parseInformationData(const QByteArray &data) +{ + const int format = data[0]; + QList list; + int item_len; + + switch (format) { + case 1: + item_len = 2 + 2; + break; + case 2: + item_len = 2 + 16; + break; + default: + qWarning() << "Unknown InformationData format!"; + return list; + } + + int items = (data.size() - 1) / item_len; + list.reserve(items); + + int pos = 1; + const char *s = data.constData(); + for (int i = 0; i < items; i++) { + InformationData d; + d.handle = read_le(&s[pos]); + switch (format) { + case 1: + d.uuid = GatoUUID(read_le(&s[pos + 2])); + break; + case 2: + d.uuid = GatoUUID(read_le(&s[pos + 2])); + break; + } + + list.append(d); + + pos += item_len; + } + + return list; +} + +QList GatoAtt::parseHandleInformation(const QByteArray &data) +{ + const int item_len = 2; + const int items = data.size() / item_len; + QList list; + list.reserve(items); + + int pos = 0; + const char *s = data.constData(); + for (int i = 0; i < items; i++) { + HandleInformation d; + d.start = read_le(&s[pos]); + d.end = read_le(&s[pos + 2]); + list.append(d); + + pos += item_len; + } + + return list; +} + +QList GatoAtt::parseAttributeData(const QByteArray &data) +{ + const int item_len = data[0]; + const int items = (data.size() - 1) / item_len; + QList list; + list.reserve(items); + + int pos = 1; + const char *s = data.constData(); + for (int i = 0; i < items; i++) { + AttributeData d; + d.handle = read_le(&s[pos]); + d.value = data.mid(pos + 2, item_len - 2); + list.append(d); + + pos += item_len; + } + + return list; +} + +QList GatoAtt::parseAttributeGroupData(const QByteArray &data) +{ + const int item_len = data[0]; + const int items = (data.size() - 1) / item_len; + QList list; + list.reserve(items); + + int pos = 1; + const char *s = data.constData(); + for (int i = 0; i < items; i++) { + AttributeGroupData d; + d.start = read_le(&s[pos]); + d.end = read_le(&s[pos + 2]); + d.value = data.mid(pos + 4, item_len - 4); + list.append(d); + + pos += item_len; + } + + return list; +} + +void GatoAtt::handleSocketConnected() +{ + requestExchangeMTU(ATT_DEFAULT_LE_MTU, this, SLOT(handleServerMTU(quint8))); + emit connected(); +} + +void GatoAtt::handleSocketDisconnected() +{ + emit disconnected(); +} + +void GatoAtt::handleSocketReadyRead() +{ + QByteArray pkt = socket->receive(); + if (!pkt.isEmpty()) { +#if PROTOCOL_DEBUG + qDebug() << "Received" << pkt.size() << "bytes" << pkt.toHex(); +#endif + + // Check if it is an event + if (handleEvent(pkt)) { + return; + } + + // Otherwise, if we have a request waiting, check if this answers it + if (!pending_requests.isEmpty()) { + if (handleResponse(pending_requests.head(), pkt)) { + pending_requests.dequeue(); + // Proceed to next request + if (!pending_requests.isEmpty()) { + sendARequest(); + } + return; + } + } + + qDebug() << "No idea what this packet is"; + } +} + +void GatoAtt::handleServerMTU(uint req, quint8 server_mtu) +{ + Q_UNUSED(req); + if (server_mtu != 0) { + mtu = server_mtu; + } +} + + diff --git a/gatoatt.h b/gatoatt.h new file mode 100644 index 0000000..27cea3e --- /dev/null +++ b/gatoatt.h @@ -0,0 +1,97 @@ +#ifndef GATOATTSOCKET_H +#define GATOATTSOCKET_H + +#include +#include +#include "gatosocket.h" +#include "gatouuid.h" + +class GatoAtt : public QObject +{ + Q_OBJECT + +public: + explicit GatoAtt(QObject *parent = 0); + ~GatoAtt(); + + GatoSocket::State state() const; + + bool connectTo(const GatoAddress& addr); + void close(); + + struct InformationData + { + GatoHandle handle; + GatoUUID uuid; + }; + struct HandleInformation + { + GatoHandle start; + GatoHandle end; + }; + struct AttributeData + { + GatoHandle handle; + QByteArray value; + }; + struct AttributeGroupData + { + GatoHandle start; + GatoHandle end; + QByteArray value; + }; + + uint request(int opcode, const QByteArray &data, QObject *receiver, const char *member); + uint requestExchangeMTU(quint8 client_mtu, QObject *receiver, const char *member); + uint requestFindInformation(GatoHandle start, GatoHandle end, QObject *receiver, const char *member); + uint requestFindByTypeValue(GatoHandle start, GatoHandle end, const GatoUUID &uuid, const QByteArray& value, QObject *receiver, const char *member); + uint requestReadByType(GatoHandle start, GatoHandle end, const GatoUUID &uuid, QObject *receiver, const char *member); + uint requestRead(GatoHandle handle, QObject *receiver, const char *member); + uint requestReadByGroupType(GatoHandle start, GatoHandle end, const GatoUUID &uuid, QObject *receiver, const char *member); + uint requestWrite(GatoHandle handle, const QByteArray &value, QObject *receiver, const char *member); + void cancelRequest(uint id); + + void command(int opcode, const QByteArray &data); + +signals: + void connected(); + void disconnected(); + + void attributeUpdated(GatoHandle handle, const QByteArray &value, bool confirmed); + +private: + struct Request + { + uint id; + quint8 opcode; + QByteArray pkt; + QObject *receiver; + QByteArray member; + }; + + void sendARequest(); + bool handleEvent(const QByteArray &event); + bool handleResponse(const Request& req, const QByteArray &response); + + void writeUuid16or128(QDataStream& s, const GatoUUID& uuid); + + QList parseInformationData(const QByteArray &data); + QList parseHandleInformation(const QByteArray &data); + QList parseAttributeData(const QByteArray &data); + QList parseAttributeGroupData(const QByteArray &data); + +private slots: + void handleSocketConnected(); + void handleSocketDisconnected(); + void handleSocketReadyRead(); + + void handleServerMTU(uint req, quint8 server_mtu); + +private: + GatoSocket *socket; + quint8 mtu; + uint next_id; + QQueue pending_requests; +}; + +#endif // GATOATTSOCKET_H diff --git a/gatocentralmanager.cpp b/gatocentralmanager.cpp new file mode 100644 index 0000000..3073d34 --- /dev/null +++ b/gatocentralmanager.cpp @@ -0,0 +1,222 @@ +/* + * libgato - A GATT/ATT library for use with Bluez + * + * Copyright (C) 2013 Javier S. Pedro + * + * 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 + */ + +#include + +#include +#include +#include + +#include +#include +#include + +#include "gatocentralmanager_p.h" +#include "gatoperipheral.h" + +GatoCentralManager::GatoCentralManager(QObject *parent) : + QObject(parent), d_ptr(new GatoCentralManagerPrivate) +{ + Q_D(GatoCentralManager); + d->q_ptr = this; + + // Hardcode the "default" bluetooth adapter for now. + d->dev_id = hci_get_route(NULL); + d->timeout = 1000; + d->hci = -1; + d->notifier = 0; +} + +GatoCentralManager::~GatoCentralManager() +{ + Q_D(GatoCentralManager); + if (d->scanning()) stopScan(); + delete d_ptr; +} + +void GatoCentralManager::scanForPeripherals(PeripheralScanOptions options) +{ + scanForPeripheralsWithServices(QList(), options); +} + +void GatoCentralManager::scanForPeripheralsWithServices(const QList &uuids, PeripheralScanOptions options) +{ + Q_D(GatoCentralManager); + + if (d->scanning()) stopScan(); + + if (!d->openDevice()) return; + d->filter_uuids = uuids; + + quint8 filter_dup = options & PeripheralScanOptionAllowDuplicates ? 0 : 1; + quint8 scan_type = options & PeripheralScanOptionActive ? 1 : 0; + int rc; + + hci_le_set_scan_enable(d->hci, 0, 0, d->timeout); + + rc = hci_le_set_scan_parameters(d->hci, scan_type, + htobs(0x0010), htobs(0x0010), + 0 /* Public address */, + 0 /* No filter ? */, + d->timeout); + if (rc < 0) { + qErrnoWarning("LE Set scan parameters failed"); + d->closeDevice(); + return; + } + + rc = hci_le_set_scan_enable(d->hci, 1, filter_dup, d->timeout); + + if (rc < 0) { + qErrnoWarning("LE Set scan enable failed"); + d->closeDevice(); + return; + } + + qDebug() << "LE Scan in progress"; + + d->notifier = new QSocketNotifier(d->hci, QSocketNotifier::Read); + connect(d->notifier, SIGNAL(activated(int)), this, SLOT(_q_readNotify())); + + socklen_t olen = sizeof(d->hci_of); + if (getsockopt(d->hci, SOL_HCI, HCI_FILTER, &d->hci_of, &olen) < 0) { + qErrnoWarning("Could not get existing HCI socket options"); + return; + } + + hci_filter_clear(&d->hci_nf); + hci_filter_set_ptype(HCI_EVENT_PKT, &d->hci_nf); + hci_filter_set_event(EVT_LE_META_EVENT, &d->hci_nf); + + if (setsockopt(d->hci, SOL_HCI, HCI_FILTER, &d->hci_nf, sizeof(d->hci_nf)) < 0) { + qErrnoWarning("Could not set HCI socket options"); + return; + } + + // SocketNotifier will call _q_readNotify() when ready +} + +void GatoCentralManager::stopScan() +{ + Q_D(GatoCentralManager); + if (d->scanning()) { + qDebug() << "Stopping LE scan"; + delete d->notifier; + setsockopt(d->hci, SOL_HCI, HCI_FILTER, &d->hci_of, sizeof(d->hci_of)); + hci_le_set_scan_enable(d->hci, 0, 0, d->timeout); + d->closeDevice(); + } else { + qDebug() << "No scan to stop"; + } + d->notifier = 0; + d->filter_uuids.clear(); + hci_filter_clear(&d->hci_nf); + hci_filter_clear(&d->hci_of); +} + +void GatoCentralManager::_q_readNotify() +{ + Q_D(GatoCentralManager); + unsigned char buf[HCI_MAX_EVENT_SIZE]; + + // Read a full event + int len; + if ((len = read(d->hci, buf, sizeof(buf))) < 0) { + if (errno != EAGAIN && errno != EINTR) { + qErrnoWarning("Could not read HCI events"); + } + return; // Will be notified later, probably. + } + + int pos = HCI_EVENT_HDR_SIZE + 1; + assert(pos < len); + evt_le_meta_event *meta = reinterpret_cast(&buf[pos]); + + + if (meta->subevent == EVT_LE_ADVERTISING_REPORT) { + pos++; // Skip subevent field + int num_reports = buf[pos]; + pos++; // Skip num_reports field + + assert(pos < len); + + while (num_reports > 0) { + le_advertising_info *info = reinterpret_cast(&buf[pos]); + assert(pos + LE_ADVERTISING_INFO_SIZE < len); + assert(pos + LE_ADVERTISING_INFO_SIZE + info->length < len); + int8_t *rssi = reinterpret_cast(&buf[pos + LE_ADVERTISING_INFO_SIZE + info->length]); + + d->handleAdvertising(info, *rssi); + + pos += LE_ADVERTISING_INFO_SIZE + info->length + 1; + num_reports--; + } + } +} + +bool GatoCentralManagerPrivate::scanning() +{ + // If the HCI device is open for any reason, it means we're scanning. + return hci != -1; +} + +bool GatoCentralManagerPrivate::openDevice() +{ + hci = hci_open_dev(dev_id); + + if (hci == -1) { + qErrnoWarning("Could not open device"); + return false; + } + + return true; +} + +void GatoCentralManagerPrivate::closeDevice() +{ + hci_close_dev(hci); + hci = -1; +} + +void GatoCentralManagerPrivate::handleAdvertising(le_advertising_info *info, int rssi) +{ + Q_Q(GatoCentralManager); + + qDebug() << "Advertising event type" << info->evt_type + << "address type" << info->bdaddr_type + << "data length" << info->length + << "rssi" << rssi; + + GatoAddress addr(info->bdaddr.b); + GatoPeripheral *peripheral; + QHash::iterator it = peripherals.find(addr); + if (it == peripherals.end()) { + peripheral = new GatoPeripheral(addr, q); + peripherals.insert(addr, peripheral); + } else { + peripheral = *it; + } + + if (info->length > 0) { + peripheral->parseEIR(info->data, info->length); + } + + emit q->discoveredPeripheral(peripheral, rssi); +} diff --git a/gatocentralmanager.h b/gatocentralmanager.h new file mode 100644 index 0000000..00225a0 --- /dev/null +++ b/gatocentralmanager.h @@ -0,0 +1,44 @@ +#ifndef GATOCENTRALMANAGER_H +#define GATOCENTRALMANAGER_H + +#include +#include "libgato_global.h" +#include "gatouuid.h" + +class GatoPeripheral; +class GatoCentralManagerPrivate; + +class LIBGATO_EXPORT GatoCentralManager : public QObject +{ + Q_OBJECT + Q_DECLARE_PRIVATE(GatoCentralManager) + Q_FLAGS(PeripheralScanOptions) + +public: + enum PeripheralScanOption { + PeripheralScanOptionActive = 1 << 0, + PeripheralScanOptionAllowDuplicates = 1 << 1 + }; + Q_DECLARE_FLAGS(PeripheralScanOptions, PeripheralScanOption) + + explicit GatoCentralManager(QObject *parent = 0); + ~GatoCentralManager(); + +public slots: + void scanForPeripherals(PeripheralScanOptions options = 0); + void scanForPeripheralsWithServices(const QList& uuids, PeripheralScanOptions options = 0); + void stopScan(); + +signals: + void discoveredPeripheral(GatoPeripheral *peripheral, int rssi); + +private slots: + void _q_readNotify(); + +private: + GatoCentralManagerPrivate *const d_ptr; +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(GatoCentralManager::PeripheralScanOptions) + +#endif // GATOCENTRALMANAGER_H diff --git a/gatocentralmanager_p.h b/gatocentralmanager_p.h new file mode 100644 index 0000000..62ef954 --- /dev/null +++ b/gatocentralmanager_p.h @@ -0,0 +1,32 @@ +#ifndef GATOCENTRALMANAGER_P_H +#define GATOCENTRALMANAGER_P_H + +#include + +#include +#include + +#include "gatocentralmanager.h" +#include "gatoaddress.h" + +class GatoCentralManagerPrivate +{ + Q_DECLARE_PUBLIC(GatoCentralManager) + + GatoCentralManager *q_ptr; + int dev_id; + int timeout; + int hci; + QSocketNotifier *notifier; + QList filter_uuids; + hci_filter hci_nf, hci_of; + QHash peripherals; + + bool scanning(); + bool openDevice(); + void closeDevice(); + + void handleAdvertising(le_advertising_info *info, int rssi); +}; + +#endif // GATOCENTRALMANAGER_P_H diff --git a/gatocharacteristic.cpp b/gatocharacteristic.cpp new file mode 100644 index 0000000..a95c4a2 --- /dev/null +++ b/gatocharacteristic.cpp @@ -0,0 +1,159 @@ +/* + * libgato - A GATT/ATT library for use with Bluez + * + * Copyright (C) 2013 Javier S. Pedro + * + * 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 + */ + +#include + +#include "gatocharacteristic.h" +#include "gatodescriptor.h" + +struct GatoCharacteristicPrivate : public QSharedData +{ + GatoUUID uuid; + GatoHandle start; + GatoHandle end; + GatoCharacteristic::Properties properties; + GatoHandle value_handle; + QMap descriptors; + QHash desc_uuids; +}; + +GatoCharacteristic::GatoCharacteristic() + : d(new GatoCharacteristicPrivate) +{ + d->start = 0; + d->end = 0; + d->properties = 0; + d->value_handle = 0; +} + +GatoCharacteristic::GatoCharacteristic(const GatoCharacteristic &o) + : d(o.d) +{ +} + +GatoCharacteristic::~GatoCharacteristic() +{ +} + +bool GatoCharacteristic::isNull() const +{ + return d->start == 0 || d->end == 0 || d->uuid.isNull(); +} + +GatoUUID GatoCharacteristic::uuid() const +{ + return d->uuid; +} + +void GatoCharacteristic::setUuid(const GatoUUID &uuid) +{ + d->uuid = uuid; +} + +GatoCharacteristic::Properties GatoCharacteristic::properties() const +{ + return d->properties; +} + +void GatoCharacteristic::setProperties(GatoCharacteristic::Properties props) +{ + d->properties = props; +} + +GatoHandle GatoCharacteristic::startHandle() const +{ + return d->start; +} + +void GatoCharacteristic::setStartHandle(GatoHandle handle) +{ + d->start = handle; +} + +GatoHandle GatoCharacteristic::endHandle() const +{ + return d->end; +} + +void GatoCharacteristic::setEndHandle(GatoHandle handle) +{ + d->end = handle; +} + +GatoHandle GatoCharacteristic::valueHandle() const +{ + return d->value_handle; +} + +void GatoCharacteristic::setValueHandle(GatoHandle handle) +{ + d->value_handle = handle; +} + +QList GatoCharacteristic::descriptors() const +{ + return d->descriptors.values(); +} + +bool GatoCharacteristic::containsDescriptor(GatoUUID uuid) const +{ + return d->desc_uuids.contains(uuid); +} + +bool GatoCharacteristic::containsDescriptor(GatoHandle handle) const +{ + return d->descriptors.contains(handle); +} + +GatoDescriptor GatoCharacteristic::getDescriptor(GatoUUID uuid) const +{ + return d->descriptors.value(d->desc_uuids.value(uuid)); +} + +GatoDescriptor GatoCharacteristic::getDescriptor(GatoHandle handle) const +{ + return d->descriptors.value(handle); +} + +void GatoCharacteristic::addDescriptor(const GatoDescriptor &descriptor) +{ + d->descriptors.insert(descriptor.handle(), descriptor); + d->desc_uuids.insert(descriptor.uuid(), descriptor.handle()); +} + +void GatoCharacteristic::removeDescriptor(const GatoDescriptor &descriptor) +{ + d->descriptors.remove(descriptor.handle()); + d->desc_uuids.remove(descriptor.uuid()); +} + +void GatoCharacteristic::clearDescriptors() +{ + d->descriptors.clear(); + d->desc_uuids.clear(); +} + +GatoCharacteristic &GatoCharacteristic::operator=(const GatoCharacteristic &o) +{ + if (this != &o) { + d = o.d; + } + return *this; +} diff --git a/gatocharacteristic.h b/gatocharacteristic.h new file mode 100644 index 0000000..2b2f457 --- /dev/null +++ b/gatocharacteristic.h @@ -0,0 +1,69 @@ +#ifndef GATOCHARACTERISTIC_H +#define GATOCHARACTERISTIC_H + +#include +#include + +#include "gatouuid.h" + +class GatoCharacteristicPrivate; +class GatoDescriptor; + +class LIBGATO_EXPORT GatoCharacteristic +{ + Q_GADGET + Q_FLAGS(CharacteristicProperties) + +public: + enum Property { + PropertyBroadcast = 0x1, + PropertyRead = 0x2, + PropertyWriteWithoutResponse = 0x4, + PropertyWrite = 0x5, + PropertyNotify = 0x10, + PropertyIndicate = 0x20, + PropertyAuthenticatedSignedWrites = 0x40, + PropertyExtendedProperties = 0x80 + }; + Q_DECLARE_FLAGS(Properties, Property) + + GatoCharacteristic(); + GatoCharacteristic(const GatoCharacteristic &o); + ~GatoCharacteristic(); + + bool isNull() const; + + GatoUUID uuid() const; + void setUuid(const GatoUUID &uuid); + + Properties properties() const; + void setProperties(Properties props); + + GatoHandle startHandle() const; + void setStartHandle(GatoHandle handle); + + GatoHandle endHandle() const; + void setEndHandle(GatoHandle handle); + + GatoHandle valueHandle() const; + void setValueHandle(GatoHandle handle); + + QList descriptors() const; + bool containsDescriptor(const GatoDescriptor& descriptor) const; + bool containsDescriptor(GatoUUID uuid) const; + bool containsDescriptor(GatoHandle handle) const; + GatoDescriptor getDescriptor(GatoUUID uuid) const; + GatoDescriptor getDescriptor(GatoHandle handle) const; + void addDescriptor(const GatoDescriptor& descriptor); + void removeDescriptor(const GatoDescriptor& descriptor); + void clearDescriptors(); + + GatoCharacteristic &operator=(const GatoCharacteristic &o); + +private: + QSharedDataPointer d; +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(GatoCharacteristic::Properties) + +#endif // GATOCHARACTERISTIC_H diff --git a/gatodescriptor.cpp b/gatodescriptor.cpp new file mode 100644 index 0000000..dda6afd --- /dev/null +++ b/gatodescriptor.cpp @@ -0,0 +1,72 @@ +/* + * libgato - A GATT/ATT library for use with Bluez + * + * Copyright (C) 2013 Javier S. Pedro + * + * 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 + */ + +#include + +#include "gatodescriptor.h" + +struct GatoDescriptorPrivate : public QSharedData +{ + GatoUUID uuid; + GatoHandle handle; +}; + +GatoDescriptor::GatoDescriptor() + : d(new GatoDescriptorPrivate) +{ + d->handle = 0; +} + +GatoDescriptor::GatoDescriptor(const GatoDescriptor &o) + : d(o.d) +{ +} + +GatoDescriptor::~GatoDescriptor() +{ +} + +GatoUUID GatoDescriptor::uuid() const +{ + return d->uuid; +} + +void GatoDescriptor::setUuid(const GatoUUID &uuid) +{ + d->uuid = uuid; +} + +GatoHandle GatoDescriptor::handle() const +{ + return d->handle; +} + +void GatoDescriptor::setHandle(GatoHandle handle) +{ + d->handle = handle; +} + +GatoDescriptor &GatoDescriptor::operator=(const GatoDescriptor &o) +{ + if (this != &o) { + d = o.d; + } + return *this; +} diff --git a/gatodescriptor.h b/gatodescriptor.h new file mode 100644 index 0000000..7ced38a --- /dev/null +++ b/gatodescriptor.h @@ -0,0 +1,32 @@ +#ifndef GATODESCRIPTOR_H +#define GATODESCRIPTOR_H + +#include +#include + +#include "gatouuid.h" + +class GatoDescriptorPrivate; + +class LIBGATO_EXPORT GatoDescriptor +{ + Q_GADGET + +public: + GatoDescriptor(); + GatoDescriptor(const GatoDescriptor &o); + ~GatoDescriptor(); + + GatoUUID uuid() const; + void setUuid(const GatoUUID &uuid); + + GatoHandle handle() const; + void setHandle(GatoHandle handle); + + GatoDescriptor &operator=(const GatoDescriptor &o); + +private: + QSharedDataPointer d; +}; + +#endif // GATODESCRIPTOR_H diff --git a/gatoperipheral.cpp b/gatoperipheral.cpp new file mode 100644 index 0000000..faec8cd --- /dev/null +++ b/gatoperipheral.cpp @@ -0,0 +1,834 @@ +/* + * libgato - A GATT/ATT library for use with Bluez + * + * Copyright (C) 2013 Javier S. Pedro + * + * 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 + */ + +#include + +#include +#include + +#include "gatoperipheral_p.h" +#include "gatoaddress.h" +#include "gatouuid.h" +#include "helpers.h" + +enum EIRDataFields { + EIRFlags = 0x01, + EIRIncompleteUUID16List = 0x02, + EIRCompleteUUID16List = 0x03, + EIRIncompleteUUID32List = 0x04, + EIRCompleteUUID32List = 0x05, + EIRIncompleteUUID128List = 0x06, + EIRCompleteUUID128List = 0x07, + EIRIncompleteLocalName = 0x08, + EIRCompleteLocalName = 0x09, + EIRTxPowerLevel = 0x0A, + EIRDeviceClass = 0x0D, + EIRSecurityManagerTKValue = 0x10, + EIRSecurityManagerOutOfBandFlags = 0x11 +}; + +GatoPeripheral::GatoPeripheral(const GatoAddress &addr, QObject *parent) : + QObject(parent), d_ptr(new GatoPeripheralPrivate(this)) +{ + Q_D(GatoPeripheral); + d->addr = addr; + d->att = new GatoAtt(this); + + connect(d->att, SIGNAL(connected()), d, SLOT(handleAttConnected())); + connect(d->att, SIGNAL(disconnected()), d, SLOT(handleAttDisconnected())); + connect(d->att, SIGNAL(attributeUpdated(GatoHandle,QByteArray,bool)), d, SLOT(handleAttAttributeUpdated(GatoHandle,QByteArray,bool))); +} + +GatoPeripheral::~GatoPeripheral() +{ + if (state() != StateDisconnected) { + disconnect(); + } + delete d_ptr; +} + +GatoPeripheral::State GatoPeripheral::state() const +{ + Q_D(const GatoPeripheral); + return static_cast(d->att->state()); +} + +GatoAddress GatoPeripheral::address() const +{ + Q_D(const GatoPeripheral); + return d->addr; +} + +QString GatoPeripheral::name() const +{ + Q_D(const GatoPeripheral); + return d->name; +} + +QList GatoPeripheral::services() const +{ + Q_D(const GatoPeripheral); + return d->services.values(); +} + +void GatoPeripheral::parseEIR(quint8 data[], int len) +{ + Q_D(GatoPeripheral); + + int pos = 0; + while (pos < len) { + int item_len = data[pos]; + pos++; + if (item_len == 0) break; + int type = data[pos]; + assert(pos + item_len <= len); + switch (type) { + case EIRFlags: + d->parseEIRFlags(&data[pos + 1], item_len - 1); + break; + case EIRIncompleteUUID16List: + d->parseEIRUUIDs(16/8, false, &data[pos + 1], item_len - 1); + break; + case EIRCompleteUUID16List: + d->parseEIRUUIDs(16/8, true, &data[pos + 1], item_len - 1); + break; + case EIRIncompleteUUID32List: + d->parseEIRUUIDs(32/8, false, &data[pos + 1], item_len - 1); + break; + case EIRCompleteUUID32List: + d->parseEIRUUIDs(32/8, true, &data[pos + 1], item_len - 1); + break; + case EIRIncompleteUUID128List: + d->parseEIRUUIDs(128/8, false, &data[pos + 1], item_len - 1); + break; + case EIRCompleteUUID128List: + d->parseEIRUUIDs(128/8, true, &data[pos + 1], item_len - 1); + break; + case EIRIncompleteLocalName: + d->parseName(false, &data[pos + 1], item_len - 1); + break; + case EIRCompleteLocalName: + d->parseName(true, &data[pos + 1], item_len - 1); + break; + default: + qWarning() << "Unknown EIR data type" << type; + break; + } + + pos += item_len; + } + + assert(pos == len); +} + +void GatoPeripheral::connectPeripheral() +{ + Q_D(GatoPeripheral); + if (d->att->state() != GatoSocket::StateDisconnected) { + qDebug() << "Already connecting"; + return; + } + + d->att->connectTo(d->addr); +} + +void GatoPeripheral::disconnectPeripheral() +{ + Q_D(GatoPeripheral); + + d->att->close(); +} + +void GatoPeripheral::discoverServices() +{ + Q_D(GatoPeripheral); + if (!d->complete_services && state() == StateConnected) { + d->clearServices(); + d->att->requestReadByGroupType(0x0001, 0xFFFF, GatoUUID::GattPrimaryService, + d, SLOT(handlePrimary(QList))); + } else { + qWarning() << "Not connected"; + } +} + +void GatoPeripheral::discoverServices(const QList &serviceUUIDs) +{ + Q_D(GatoPeripheral); + if (serviceUUIDs.isEmpty()) return; + if (state() == StateConnected) { + foreach (const GatoUUID& uuid, serviceUUIDs) { + QByteArray value = gatouuid_to_bytearray(uuid, true, false); + uint req = d->att->requestFindByTypeValue(0x0001, 0xFFFF, GatoUUID::GattPrimaryService, value, + d, SLOT(handlePrimaryForService(uint,QList))); + d->pending_primary_reqs.insert(req, uuid); + } + } else { + qWarning() << "Not connected"; + } +} + +void GatoPeripheral::discoverCharacteristics(const GatoService &service) +{ + Q_D(GatoPeripheral); + + if (!d->services.contains(service.startHandle())) { + qWarning() << "Unknown service for this peripheral"; + return; + } + + GatoService &our_service = d->services[service.startHandle()]; + + if (our_service.startHandle() != service.startHandle() || + our_service.endHandle() != service.endHandle() || + our_service.uuid() != service.uuid()) { + qWarning() << "Unknown service for this peripheral"; + return; + } + + if (state() == StateConnected) { + GatoHandle start = our_service.startHandle(); + GatoHandle end = our_service.endHandle(); + + d->clearServiceCharacteristics(&our_service); + + uint req = d->att->requestReadByType(start, end, GatoUUID::GattCharacteristic, + d, SLOT(handleCharacteristic(QList))); + d->pending_characteristic_reqs.insert(req, start); + } else { + qWarning() << "Not connected"; + } +} + +void GatoPeripheral::discoverCharacteristics(const GatoService &service, const QList &characteristicUUIDs) +{ + // TODO There seems to be no way to ask for the peripheral to filter by uuid + Q_UNUSED(characteristicUUIDs); + discoverCharacteristics(service); +} + +void GatoPeripheral::discoverDescriptors(const GatoCharacteristic &characteristic) +{ + Q_D(GatoPeripheral); + + GatoHandle char_handle = characteristic.startHandle(); + GatoHandle service_handle = d->characteristic_to_service.value(char_handle); + + if (!service_handle) { + qWarning() << "Unknown characteristic for this peripheral"; + return; + } + + GatoService &our_service = d->services[service_handle]; + Q_ASSERT(our_service.containsCharacteristic(char_handle)); + GatoCharacteristic our_char = our_service.getCharacteristic(char_handle); + Q_ASSERT(our_char.startHandle() == char_handle); + + if (state() == StateConnected) { + d->clearCharacteristicDescriptors(&our_char); + our_service.addCharacteristic(our_char); // Update service with empty descriptors list + uint req = d->att->requestFindInformation(our_char.startHandle() + 1, our_char.endHandle(), + d, SLOT(handleDescriptors(uint,QList))); + d->pending_descriptor_reqs.insert(req, char_handle); + } else { + qWarning() << "Not connected"; + } +} + +void GatoPeripheral::readValue(const GatoCharacteristic &characteristic) +{ + Q_D(GatoPeripheral); + + GatoHandle char_handle = characteristic.startHandle(); + GatoHandle service_handle = d->characteristic_to_service.value(char_handle); + + if (!service_handle) { + qWarning() << "Unknown characteristic for this peripheral"; + return; + } + + GatoService &our_service = d->services[service_handle]; + Q_ASSERT(our_service.containsCharacteristic(char_handle)); + + if (state() == StateConnected) { + uint req = d->att->requestRead(characteristic.valueHandle(), + d, SLOT(handleCharacteristicRead(uint,QByteArray))); + d->pending_characteristic_read_reqs.insert(req, char_handle); + } else { + qWarning() << "Not connected"; + } +} + +void GatoPeripheral::readValue(const GatoDescriptor &descriptor) +{ + Q_D(GatoPeripheral); + + GatoHandle desc_handle = descriptor.handle(); + GatoHandle char_handle = d->descriptor_to_characteristic.value(desc_handle); + + if (!char_handle) { + qWarning() << "Unknown descriptor for this peripheral"; + return; + } + + GatoHandle service_handle = d->characteristic_to_service.value(char_handle); + Q_ASSERT(service_handle); + + GatoService &our_service = d->services[service_handle]; + Q_ASSERT(our_service.containsCharacteristic(char_handle)); + + if (state() == StateConnected) { + uint req = d->att->requestRead(descriptor.handle(), + d, SLOT(handleDescriptorRead(uint,QByteArray))); + d->pending_descriptor_read_reqs.insert(req, char_handle); + } else { + qWarning() << "Not connected"; + } +} + +void GatoPeripheral::writeValue(const GatoCharacteristic &characteristic, const QByteArray &data) +{ + Q_D(GatoPeripheral); + + GatoHandle char_handle = characteristic.startHandle(); + GatoHandle service_handle = d->characteristic_to_service.value(char_handle); + + if (!service_handle) { + qWarning() << "Unknown characteristic for this peripheral"; + return; + } + + GatoService &our_service = d->services[service_handle]; + Q_ASSERT(our_service.containsCharacteristic(char_handle)); + + if (state() == StateConnected) { + d->att->requestWrite(characteristic.valueHandle(), data, + d, SLOT(handleCharacteristicRead(uint,QByteArray))); + } else { + qWarning() << "Not connected"; + } +} + +void GatoPeripheral::writeValue(const GatoDescriptor &descriptor, const QByteArray &data) +{ + Q_D(GatoPeripheral); + + GatoHandle desc_handle = descriptor.handle(); + GatoHandle char_handle = d->descriptor_to_characteristic.value(desc_handle); + + if (!char_handle) { + qWarning() << "Unknown descriptor for this peripheral"; + return; + } + + GatoHandle service_handle = d->characteristic_to_service.value(char_handle); + Q_ASSERT(service_handle); + + GatoService &our_service = d->services[service_handle]; + Q_ASSERT(our_service.containsCharacteristic(char_handle)); + + if (state() == StateConnected) { + d->att->requestWrite(descriptor.handle(), data, + d, SLOT(handleDescriptorWrite(uint,bool))); + } else { + qWarning() << "Not connected"; + } +} + +void GatoPeripheral::setNotification(const GatoCharacteristic &characteristic, bool enabled) +{ + Q_D(GatoPeripheral); + + GatoHandle char_handle = characteristic.startHandle(); + GatoHandle service_handle = d->characteristic_to_service.value(char_handle); + + if (!service_handle) { + qWarning() << "Unknown characteristic for this peripheral"; + return; + } + + GatoService &our_service = d->services[service_handle]; + Q_ASSERT(our_service.containsCharacteristic(char_handle)); + GatoCharacteristic our_char = our_service.getCharacteristic(char_handle); + + if (!(our_char.properties() & GatoCharacteristic::PropertyNotify)) { + qWarning() << "Characteristic does not support notifications"; + return; + } + + if (state() != StateConnected) { + qWarning() << "Not connected"; + return; + } + + const GatoUUID uuid(GatoUUID::GattClientCharacteristicConfiguration); + if (our_char.containsDescriptor(uuid)) { + GatoDescriptor desc = our_char.getDescriptor(uuid); + d->pending_set_notify.remove(char_handle); + writeValue(characteristic, d->genClientCharConfiguration(true, false)); + } else { + d->pending_set_notify[char_handle] = enabled; + discoverDescriptors(our_char); // May need to find appropiate descriptor + } +} + +GatoPeripheralPrivate::GatoPeripheralPrivate(GatoPeripheral *parent) + : QObject(parent), q_ptr(parent), + complete_name(false), complete_services(false) +{ +} + +GatoPeripheralPrivate::~GatoPeripheralPrivate() +{ + delete att; +} + +void GatoPeripheralPrivate::parseEIRFlags(quint8 data[], int len) +{ + Q_UNUSED(data); + Q_UNUSED(len); + // Nothing to do for now. +} + +void GatoPeripheralPrivate::parseEIRUUIDs(int size, bool complete, quint8 data[], int len) +{ + Q_UNUSED(complete); + + for (int pos = 0; pos < len; pos += size) { + GatoUUID uuid; + switch (size) { + case 16/8: + uuid = GatoUUID(qFromLittleEndian(&data[pos])); + break; + case 32/8: + uuid = GatoUUID(qFromLittleEndian(&data[pos])); + break; + case 128/8: + uuid = GatoUUID(qFromLittleEndian(&data[pos])); + break; + } + + service_uuids.insert(uuid); + } +} + +void GatoPeripheralPrivate::parseName(bool complete, quint8 data[], int len) +{ + Q_Q(GatoPeripheral); + if (complete || !complete_name) { + name = QString::fromAscii(reinterpret_cast(data), len); + complete_name = complete; + emit q->nameChanged(); + } +} + +GatoCharacteristic GatoPeripheralPrivate::parseCharacteristicValue(const QByteArray &ba) +{ + GatoCharacteristic characteristic; + const char *data = ba.constData(); + + quint8 properties = data[0]; + characteristic.setProperties(GatoCharacteristic::Properties(properties)); + + GatoHandle handle = read_le(&data[1]); + characteristic.setValueHandle(handle); + + GatoUUID uuid = bytearray_to_gatouuid(ba.mid(3)); + characteristic.setUuid(uuid); + + return characteristic; +} + +QByteArray GatoPeripheralPrivate::genClientCharConfiguration(bool notification, bool indication) +{ + QByteArray ba; + ba.resize(sizeof(quint16)); + + quint16 val = 0; + if (notification) + val |= 0x1; + if (indication) + val |= 0x2; + + write_le(val, ba.data()); + + return ba; +} + +void GatoPeripheralPrivate::clearServices() +{ + characteristic_to_service.clear(); + value_to_characteristic.clear(); + descriptor_to_characteristic.clear(); + services.clear(); +} + +void GatoPeripheralPrivate::clearServiceCharacteristics(GatoService *service) +{ + QList chars = service->characteristics(); + QList::iterator it; + for (it = chars.begin(); it != chars.end(); ++it) { + clearCharacteristicDescriptors(&*it); + characteristic_to_service.remove(it->startHandle()); + value_to_characteristic.remove(it->valueHandle()); + } + service->clearCharacteristics(); +} + +void GatoPeripheralPrivate::clearCharacteristicDescriptors(GatoCharacteristic *characteristic) +{ + QList descs = characteristic->descriptors(); + foreach (const GatoDescriptor& d, descs) { + descriptor_to_characteristic.remove(d.handle()); + } + characteristic->clearDescriptors(); +} + +void GatoPeripheralPrivate::finishSetNotifyOperations(const GatoCharacteristic &characteristic) +{ + Q_Q(GatoPeripheral); + + GatoHandle handle = characteristic.startHandle(); + + if (pending_set_notify.contains(handle)) { + const GatoUUID uuid(GatoUUID::GattClientCharacteristicConfiguration); + bool notify = pending_set_notify.value(handle); + + foreach (const GatoDescriptor &descriptor, characteristic.descriptors()) { + if (descriptor.uuid() == uuid) { + q->writeValue(descriptor, genClientCharConfiguration(notify, false)); + } + } + + pending_set_notify.remove(handle); + } +} + +void GatoPeripheralPrivate::handleAttConnected() +{ + Q_Q(GatoPeripheral); + + emit q->connected(); +} + +void GatoPeripheralPrivate::handleAttDisconnected() +{ + Q_Q(GatoPeripheral); + + // Forget about all pending requests + pending_primary_reqs.clear(); + pending_characteristic_reqs.clear(); + pending_characteristic_read_reqs.clear(); + pending_descriptor_reqs.clear(); + pending_descriptor_read_reqs.clear(); + + emit q->disconnected(); +} + +void GatoPeripheralPrivate::handleAttAttributeUpdated(GatoHandle handle, const QByteArray &value, bool confirmed) +{ + Q_Q(GatoPeripheral); + Q_UNUSED(confirmed); + + // Let's see if this is a handle we know about. + if (value_to_characteristic.contains(handle)) { + // Ok, it's a characteristic value. + GatoHandle char_handle = value_to_characteristic.value(handle); + GatoHandle service_handle = characteristic_to_service.value(char_handle); + if (!service_handle) { + qWarning() << "Got a notification for a characteristic I don't know about"; + return; + } + + GatoService &service = services[service_handle]; + GatoCharacteristic characteristic = service.getCharacteristic(char_handle); + + emit q->valueUpdated(characteristic, value); + } +} + +void GatoPeripheralPrivate::handlePrimary(uint req, const QList &list) +{ + Q_Q(GatoPeripheral); + Q_UNUSED(req); + + if (list.isEmpty()) { + complete_services = true; + emit q->servicesDiscovered(); + } else { + GatoHandle last_handle = 0; + + foreach (const GatoAtt::AttributeGroupData &data, list) { + GatoUUID uuid = bytearray_to_gatouuid(data.value); + GatoService service; + + service.setUuid(uuid); + service.setStartHandle(data.start); + service.setEndHandle(data.end); + + services.insert(data.start, service); + service_uuids.insert(uuid); + + last_handle = data.end; + } + + // Fetch following attributes + att->requestReadByGroupType(last_handle + 1, 0xFFFF, GatoUUID::GattPrimaryService, + this, SLOT(handlePrimary(uint,QList))); + } +} + +void GatoPeripheralPrivate::handlePrimaryForService(uint req, const QList &list) +{ + Q_Q(GatoPeripheral); + + GatoUUID uuid = pending_primary_reqs.value(req, GatoUUID()); + if (uuid.isNull()) { + qDebug() << "Got primary for service response for a request I did not make"; + return; + } + pending_primary_reqs.remove(req); + + if (list.isEmpty()) { + if (pending_primary_reqs.isEmpty()) { + emit q->servicesDiscovered(); + } + } else { + GatoHandle last_handle = 0; + + foreach (const GatoAtt::HandleInformation &data, list) { + GatoService service; + + service.setUuid(uuid); + service.setStartHandle(data.start); + service.setEndHandle(data.end); + + services.insert(data.start, service); + service_uuids.insert(uuid); + + last_handle = data.end; + } + + // Fetch following attributes + QByteArray value = gatouuid_to_bytearray(uuid, true, false); + uint req = att->requestFindByTypeValue(last_handle + 1, 0xFFFF, GatoUUID::GattPrimaryService, value, + this, SLOT(handlePrimaryForService(uint,QList))); + pending_primary_reqs.insert(req, uuid); + } +} + +void GatoPeripheralPrivate::handleCharacteristic(uint req, const QList &list) +{ + Q_Q(GatoPeripheral); + + GatoHandle service_start = pending_characteristic_reqs.value(req, 0); + if (!service_start) { + qDebug() << "Got characteristics for a request I did not make"; + return; + } + pending_characteristic_reqs.remove(req); + + Q_ASSERT(services.contains(service_start)); + GatoService &service = services[service_start]; + Q_ASSERT(service.startHandle() == service_start); + + if (list.isEmpty()) { + emit q->characteristicsDiscovered(service); + } else { + GatoHandle last_handle = 0; + + // If we are continuing a characteristic list, this means the + // last service we discovered in the previous iteration was not + // the last one, so we have to reduce its endHandle! + QList cur_chars = service.characteristics(); + if (!cur_chars.isEmpty()) { + GatoCharacteristic &last = cur_chars.back(); + last.setEndHandle(list.front().handle - 1); + service.addCharacteristic(last); + } + + for (int i = 0; i < list.size(); i++) { + const GatoAtt::AttributeData &data = list.at(i); + GatoCharacteristic characteristic = parseCharacteristicValue(data.value); + + characteristic.setStartHandle(data.handle); + if (i + 1 < list.size()) { + characteristic.setEndHandle(list.at(i + 1).handle - 1); + } else { + characteristic.setEndHandle(service.endHandle()); + } + + service.addCharacteristic(characteristic); + characteristic_to_service.insert(data.handle, service_start); + value_to_characteristic.insert(characteristic.valueHandle(), data.handle); + + last_handle = data.handle; + } + + if (last_handle >= service.endHandle()) { + // Already finished, no need to send another request + emit q->characteristicsDiscovered(service); + return; + } + + // Fetch following attributes + uint req = att->requestReadByType(last_handle + 1, service.endHandle(), GatoUUID::GattCharacteristic, + this, SLOT(handleCharacteristic(uint,QList))); + pending_characteristic_reqs.insert(req, service.startHandle()); + } +} + +void GatoPeripheralPrivate::handleDescriptors(uint req, const QList &list) +{ + Q_Q(GatoPeripheral); + + GatoHandle char_handle = pending_descriptor_reqs.value(req); + if (!char_handle) { + qDebug() << "Got descriptor for a request I did not make"; + return; + } + pending_descriptor_reqs.remove(req); + GatoHandle service_handle = characteristic_to_service.value(char_handle); + if (!service_handle) { + qWarning() << "Unknown characteristic during descriptor discovery: " << char_handle; + return; + } + + Q_ASSERT(services.contains(service_handle)); + GatoService &service = services[service_handle]; + Q_ASSERT(service.startHandle() == service_handle); + + Q_ASSERT(service.containsCharacteristic(char_handle)); + GatoCharacteristic characteristic = service.getCharacteristic(char_handle); + + if (list.isEmpty()) { + finishSetNotifyOperations(characteristic); + emit q->descriptorsDiscovered(characteristic); + } else { + GatoHandle last_handle = 0; + + foreach (const GatoAtt::InformationData &data, list) { + // Skip the value attribute itself. + if (data.handle == characteristic.valueHandle()) continue; + + GatoDescriptor descriptor; + + descriptor.setHandle(data.handle); + descriptor.setUuid(data.uuid); + + characteristic.addDescriptor(descriptor); + + service.addCharacteristic(characteristic); + descriptor_to_characteristic.insert(data.handle, char_handle); + + last_handle = data.handle; + } + + service.addCharacteristic(characteristic); + + if (last_handle >= characteristic.endHandle()) { + // Already finished, no need to send another request + finishSetNotifyOperations(characteristic); + emit q->descriptorsDiscovered(characteristic); + return; + } + + // Fetch following attributes + uint req = att->requestFindInformation(last_handle + 1, characteristic.endHandle(), + this, SLOT(handleDescriptors(uint,QList))); + pending_descriptor_reqs.insert(req, char_handle); + + } +} + +void GatoPeripheralPrivate::handleCharacteristicRead(uint req, const QByteArray &value) +{ + Q_Q(GatoPeripheral); + + GatoHandle char_handle = pending_characteristic_read_reqs.value(req); + if (!char_handle) { + qDebug() << "Got characteristics for a request I did not make"; + return; + } + pending_characteristic_read_reqs.remove(req); + GatoHandle service_handle = characteristic_to_service.value(char_handle); + if (!service_handle) { + qWarning() << "Unknown characteristic during read: " << char_handle; + return; + } + + Q_ASSERT(services.contains(service_handle)); + GatoService &service = services[service_handle]; + Q_ASSERT(service.startHandle() == service_handle); + + Q_ASSERT(service.containsCharacteristic(char_handle)); + GatoCharacteristic characteristic = service.getCharacteristic(char_handle); + + emit q->valueUpdated(characteristic, value); +} + +void GatoPeripheralPrivate::handleDescriptorRead(uint req, const QByteArray &value) +{ + Q_Q(GatoPeripheral); + + GatoHandle desc_handle = pending_descriptor_read_reqs.value(req); + if (!desc_handle) { + qDebug() << "Got characteristics for a request I did not make"; + return; + } + pending_descriptor_read_reqs.remove(req); + GatoHandle char_handle = descriptor_to_characteristic.value(desc_handle); + if (!char_handle) { + qWarning() << "Unknown characteristic during read: " << char_handle; + return; + } + GatoHandle service_handle = characteristic_to_service.value(char_handle); + if (!service_handle) { + qWarning() << "Unknown characteristic during read: " << char_handle; + return; + } + + Q_ASSERT(services.contains(service_handle)); + GatoService &service = services[service_handle]; + Q_ASSERT(service.startHandle() == service_handle); + + Q_ASSERT(service.containsCharacteristic(char_handle)); + GatoCharacteristic characteristic = service.getCharacteristic(char_handle); + + Q_ASSERT(characteristic.containsDescriptor(desc_handle)); + GatoDescriptor descriptor = characteristic.getDescriptor(desc_handle); + + emit q->descriptorValueUpdated(descriptor, value); +} + +void GatoPeripheralPrivate::handleCharacteristicWrite(uint req, bool ok) +{ + Q_UNUSED(req); + if (!ok) { + qWarning() << "Failed to write some characteristic"; + } +} + +void GatoPeripheralPrivate::handleDescriptorWrite(uint req, bool ok) +{ + Q_UNUSED(req); + if (!ok) { + qWarning() << "Failed to write some characteristic"; + } +} diff --git a/gatoperipheral.h b/gatoperipheral.h new file mode 100644 index 0000000..6ad6782 --- /dev/null +++ b/gatoperipheral.h @@ -0,0 +1,67 @@ +#ifndef GATOPERIPHERAL_H +#define GATOPERIPHERAL_H + +#include +#include "libgato_global.h" +#include "gatouuid.h" +#include "gatoaddress.h" + +class GatoService; +class GatoCharacteristic; +class GatoDescriptor; +class GatoPeripheralPrivate; + +class LIBGATO_EXPORT GatoPeripheral : public QObject +{ + Q_OBJECT + Q_DECLARE_PRIVATE(GatoPeripheral) + Q_ENUMS(State) + Q_PROPERTY(GatoAddress address READ address) + Q_PROPERTY(QString name READ name NOTIFY nameChanged) + +public: + GatoPeripheral(const GatoAddress& addr, QObject *parent = 0); + ~GatoPeripheral(); + + enum State { + StateDisconnected, + StateConnecting, + StateConnected + }; + + State state() const; + GatoAddress address() const; + QString name() const; + QList services() const; + + void parseEIR(quint8 data[], int len); + +public slots: + void connectPeripheral(); + void disconnectPeripheral(); + void discoverServices(); + void discoverServices(const QList& serviceUUIDs); + void discoverCharacteristics(const GatoService &service); + void discoverCharacteristics(const GatoService &service, const QList& characteristicUUIDs); + void discoverDescriptors(const GatoCharacteristic &characteristic); + void readValue(const GatoCharacteristic &characteristic); + void readValue(const GatoDescriptor &descriptor); + void writeValue(const GatoCharacteristic &characteristic, const QByteArray &data); + void writeValue(const GatoDescriptor &descriptor, const QByteArray &data); + void setNotification(const GatoCharacteristic &characteristic, bool enabled); + +signals: + void connected(); + void disconnected(); + void nameChanged(); + void servicesDiscovered(); + void characteristicsDiscovered(const GatoService &service); + void descriptorsDiscovered(const GatoCharacteristic &characteristic); + void valueUpdated(const GatoCharacteristic &characteristic, const QByteArray &value); + void descriptorValueUpdated(const GatoDescriptor &descriptor, const QByteArray &value); + +private: + GatoPeripheralPrivate *const d_ptr; +}; + +#endif // GATOPERIPHERAL_H diff --git a/gatoperipheral_p.h b/gatoperipheral_p.h new file mode 100644 index 0000000..895270e --- /dev/null +++ b/gatoperipheral_p.h @@ -0,0 +1,71 @@ +#ifndef GATOPERIPHERAL_P_H +#define GATOPERIPHERAL_P_H + +#include "gatoperipheral.h" +#include "gatoservice.h" +#include "gatocharacteristic.h" +#include "gatodescriptor.h" +#include "gatoatt.h" + +class GatoPeripheralPrivate : public QObject +{ + Q_OBJECT + + Q_DECLARE_PUBLIC(GatoPeripheral) + +public: + GatoPeripheralPrivate(GatoPeripheral *parent); + ~GatoPeripheralPrivate(); + + GatoPeripheral *q_ptr; + GatoAddress addr; + QString name; + QSet service_uuids; + QMap services; + + bool complete_name : 1; + bool complete_services : 1; + + /** Maps attribute handles to service handles. */ + QMap characteristic_to_service; + QMap value_to_characteristic; + QMap descriptor_to_characteristic; + + GatoAtt *att; + QMap pending_primary_reqs; + QMap pending_characteristic_reqs; + QMap pending_characteristic_read_reqs; + QMap pending_descriptor_reqs; + QMap pending_descriptor_read_reqs; + + QMap pending_set_notify; + + void parseEIRFlags(quint8 data[], int len); + void parseEIRUUIDs(int size, bool complete, quint8 data[], int len); + void parseName(bool complete, quint8 data[], int len); + + static GatoCharacteristic parseCharacteristicValue(const QByteArray &ba); + + static QByteArray genClientCharConfiguration(bool notification, bool indication); + + void clearServices(); + void clearServiceCharacteristics(GatoService *service); + void clearCharacteristicDescriptors(GatoCharacteristic *characteristic); + + void finishSetNotifyOperations(const GatoCharacteristic &characteristic); + +public slots: + void handleAttConnected(); + void handleAttDisconnected(); + void handleAttAttributeUpdated(GatoHandle handle, const QByteArray &value, bool confirmed); + void handlePrimary(uint req, const QList& list); + void handlePrimaryForService(uint req, const QList& list); + void handleCharacteristic(uint req, const QList &list); + void handleDescriptors(uint req, const QList &list); + void handleCharacteristicRead(uint req, const QByteArray &value); + void handleDescriptorRead(uint req, const QByteArray &value); + void handleCharacteristicWrite(uint req, bool ok); + void handleDescriptorWrite(uint req, bool ok); +}; + +#endif // GATOPERIPHERAL_P_H diff --git a/gatoservice.cpp b/gatoservice.cpp new file mode 100644 index 0000000..ad9ff1b --- /dev/null +++ b/gatoservice.cpp @@ -0,0 +1,129 @@ +/* + * libgato - A GATT/ATT library for use with Bluez + * + * Copyright (C) 2013 Javier S. Pedro + * + * 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 + */ + +#include "gatoservice.h" +#include "gatocharacteristic.h" + +struct GatoServicePrivate : QSharedData +{ + GatoServicePrivate(); + GatoUUID uuid; + GatoHandle start; + GatoHandle end; + + QMap characteristics; +}; + +GatoService::GatoService() + : d(new GatoServicePrivate) +{ +} + +GatoService::GatoService(const GatoService &o) + : d(o.d) +{ +} + +GatoService::~GatoService() +{ +} + +GatoUUID GatoService::uuid() const +{ + return d->uuid; +} + +void GatoService::setUuid(const GatoUUID &uuid) +{ + d->uuid = uuid; +} + +GatoHandle GatoService::startHandle() const +{ + return d->start; +} + +void GatoService::setStartHandle(GatoHandle handle) +{ + d->start = handle; +} + +GatoHandle GatoService::endHandle() const +{ + return d->end; +} + +void GatoService::setEndHandle(GatoHandle handle) +{ + d->end = handle; +} + +QList GatoService::characteristics() const +{ + return d->characteristics.values(); +} + +bool GatoService::containsCharacteristic(const GatoCharacteristic &characteristic) const +{ + GatoHandle char_handle = characteristic.startHandle(); + if (d->characteristics.contains(char_handle)) { + return d->characteristics[char_handle].uuid() == characteristic.uuid(); + } else { + return false; + } +} + +bool GatoService::containsCharacteristic(GatoHandle handle) const +{ + return d->characteristics.contains(handle); +} + +GatoCharacteristic GatoService::getCharacteristic(GatoHandle handle) const +{ + return d->characteristics.value(handle); +} + +void GatoService::addCharacteristic(const GatoCharacteristic &characteristic) +{ + d->characteristics.insert(characteristic.startHandle(), characteristic); +} + +void GatoService::removeCharacteristic(const GatoCharacteristic &characteristic) +{ + d->characteristics.remove(characteristic.startHandle()); +} + +void GatoService::clearCharacteristics() +{ + d->characteristics.clear(); +} + +GatoService & GatoService::operator =(const GatoService &o) +{ + if (this != &o) { + d = o.d; + } + return *this; +} + +GatoServicePrivate::GatoServicePrivate() + : uuid(), start(0), end(0) +{ +} diff --git a/gatoservice.h b/gatoservice.h new file mode 100644 index 0000000..7af976a --- /dev/null +++ b/gatoservice.h @@ -0,0 +1,42 @@ +#ifndef GATOSERVICE_H +#define GATOSERVICE_H + +#include +#include "gatouuid.h" + +class GatoServicePrivate; +class GatoCharacteristic; + +class LIBGATO_EXPORT GatoService +{ + Q_GADGET + +public: + GatoService(); + GatoService(const GatoService &o); + ~GatoService(); + + GatoUUID uuid() const; + void setUuid(const GatoUUID &uuid); + + GatoHandle startHandle() const; + void setStartHandle(GatoHandle handle); + + GatoHandle endHandle() const; + void setEndHandle(GatoHandle handle); + + QList characteristics() const; + bool containsCharacteristic(const GatoCharacteristic& characteristic) const; + bool containsCharacteristic(GatoHandle handle) const; + GatoCharacteristic getCharacteristic(GatoHandle handle) const; + void addCharacteristic(const GatoCharacteristic& characteristic); + void removeCharacteristic(const GatoCharacteristic& characteristic); + void clearCharacteristics(); + + GatoService & operator=(const GatoService &o); + +private: + QSharedDataPointer d; +}; + +#endif // GATOSERVICE_H diff --git a/gatosocket.cpp b/gatosocket.cpp new file mode 100644 index 0000000..e46a811 --- /dev/null +++ b/gatosocket.cpp @@ -0,0 +1,193 @@ +/* + * libgato - A GATT/ATT library for use with Bluez + * + * Copyright (C) 2013 Javier S. Pedro + * + * 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 + */ + +#include + +#include + +#include +#include +#include + +#include +#include +#include "gatosocket.h" + +GatoSocket::GatoSocket(QObject *parent) + : QObject(parent), s(StateDisconnected), fd(-1) +{ +} + +GatoSocket::~GatoSocket() +{ + if (s != StateDisconnected) { + close(); + } +} + +GatoSocket::State GatoSocket::state() const +{ + return s; +} + +bool GatoSocket::connectTo(const GatoAddress &addr, unsigned short cid) +{ + if (s != StateDisconnected) { + qWarning() << "Already connecting or connected"; + return false; + } + + fd = socket(PF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_L2CAP); + if (fd == -1) { + qErrnoWarning("Could not create L2CAP socket"); + return false; + } + + s = StateConnecting; + + readNotifier = new QSocketNotifier(fd, QSocketNotifier::Read, this); + writeNotifier = new QSocketNotifier(fd, QSocketNotifier::Write, this); + connect(readNotifier, SIGNAL(activated(int)), SLOT(readNotify())); + connect(writeNotifier, SIGNAL(activated(int)), SLOT(writeNotify())); + + struct sockaddr_l2 l2addr; + memset(&l2addr, 0, sizeof(l2addr)); + + l2addr.l2_family = AF_BLUETOOTH; + l2addr.l2_cid = htobs(cid); +#ifdef BDADDR_LE_PUBLIC + l2addr.l2_bdaddr_type = BDADDR_LE_PUBLIC; // TODO +#endif + addr.toUInt8Array(l2addr.l2_bdaddr.b); + + int err = ::connect(fd, reinterpret_cast(&l2addr), sizeof(l2addr)); + if (err == -1 && errno != EINPROGRESS) { + qErrnoWarning("Could not connect to L2CAP socket"); + close(); + return false; + } + + return true; +} + +void GatoSocket::close() +{ + if (s != StateDisconnected) { + delete readNotifier; + delete writeNotifier; + readQueue.clear(); + writeQueue.clear(); + fd = -1; + s = StateDisconnected; + emit disconnected(); + } +} + +QByteArray GatoSocket::receive() +{ + if (readQueue.isEmpty()) { + return QByteArray(); + } else { + return readQueue.dequeue(); + } +} + +void GatoSocket::send(const QByteArray &pkt) +{ + if (s == StateConnected && writeQueue.isEmpty()) { + if (transmit(pkt)) { + // Packet transmited succesfully without any queuing + return; + } + } + + writeQueue.enqueue(pkt); + writeNotifier->setEnabled(true); +} + +bool GatoSocket::transmit(const QByteArray &pkt) +{ + int written = ::write(fd, pkt.constData(), pkt.size()); + if (written < 0) { + qErrnoWarning("Could not write to L2 socket"); + close(); + return false; + } else if (written < pkt.size()) { + qWarning("Could not write full packet to L2 socket"); + return true; + } else { + return true; + } +} + +void GatoSocket::readNotify() +{ + QByteArray buf; + buf.resize(1024); // Max packet size + + int read = ::read(fd, buf.data(), buf.size()); + if (read < 0) { + qErrnoWarning("Could not read to L2 socket"); + close(); + return; + } else if (read == 0) { + return; + } + + buf.resize(read); + + readQueue.enqueue(buf); + + if (readQueue.size() == 1) { + emit readyRead(); + } +} + +void GatoSocket::writeNotify() +{ + if (s == StateConnecting) { + int soerror = 0; + socklen_t len = sizeof(soerror); + if (::getsockopt(fd, SOL_SOCKET, SO_ERROR, &soerror, &len) != 0) { + // An error while reading the error? + qErrnoWarning("Could not get L2 socket options"); + close(); + return; + } + if (soerror != 0) { + qWarning() << "Could not connect to L2 socket: " << strerror(soerror); + close(); + return; + } + + s = StateConnected; + emit connected(); + } else if (s == StateConnected) { + if (!writeQueue.isEmpty()) { + if (transmit(writeQueue.head())) { + writeQueue.dequeue(); + } + } + + if (writeQueue.isEmpty()) { + writeNotifier->setEnabled(false); + } + } +} diff --git a/gatosocket.h b/gatosocket.h new file mode 100644 index 0000000..6bb6998 --- /dev/null +++ b/gatosocket.h @@ -0,0 +1,54 @@ +#ifndef GATOSOCKET_H +#define GATOSOCKET_H + +#include +#include +#include + +#include "gatoaddress.h" + +class GatoSocket : public QObject +{ + Q_OBJECT + Q_ENUMS(State) + +public: + explicit GatoSocket(QObject *parent); + ~GatoSocket(); + + enum State { + StateDisconnected, + StateConnecting, + StateConnected + }; + + State state() const; + + bool connectTo(const GatoAddress &addr, unsigned short cid); + void close(); + + QByteArray receive(); + void send(const QByteArray &pkt); + +signals: + void connected(); + void disconnected(); + void readyRead(); + +private: + bool transmit(const QByteArray &pkt); + +private slots: + void readNotify(); + void writeNotify(); + +private: + State s; + int fd; + QSocketNotifier *readNotifier; + QQueue readQueue; + QSocketNotifier *writeNotifier; + QQueue writeQueue; +}; + +#endif // GATOSOCKET_H diff --git a/gatouuid.cpp b/gatouuid.cpp new file mode 100644 index 0000000..a1a3866 --- /dev/null +++ b/gatouuid.cpp @@ -0,0 +1,184 @@ +/* + * libgato - A GATT/ATT library for use with Bluez + * + * Copyright (C) 2013 Javier S. Pedro + * + * 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 + */ + +#include +#include +#include + +#include "gatouuid.h" + +union convert128 { + uint128_t u; + gatouint128 g; +}; + +struct GatoUUIDPrivate : QSharedData +{ + bt_uuid_t uuid; +}; + +GatoUUID::GatoUUID() + : d(new GatoUUIDPrivate) +{ + memset(&d->uuid, 0, sizeof(d->uuid)); +} + +GatoUUID::GatoUUID(GattUuid uuid) + : d(new GatoUUIDPrivate) +{ + bt_uuid16_create(&d->uuid, uuid); +} + +GatoUUID::GatoUUID(quint16 uuid) + : d(new GatoUUIDPrivate) +{ + bt_uuid16_create(&d->uuid, uuid); +} + +GatoUUID::GatoUUID(quint32 uuid) + : d(new GatoUUIDPrivate) +{ + bt_uuid32_create(&d->uuid, uuid); +} + +GatoUUID::GatoUUID(gatouint128 uuid) + : d(new GatoUUIDPrivate) +{ + convert128 c128; + c128.g = uuid; + bt_uuid128_create(&d->uuid, c128.u); +} + +GatoUUID::GatoUUID(const QString &uuid) + : d(new GatoUUIDPrivate) +{ + bt_string_to_uuid(&d->uuid, uuid.toLatin1().constData()); +} + +GatoUUID::GatoUUID(const GatoUUID &o) + : d(o.d) +{ +} + +GatoUUID::~GatoUUID() +{ +} + +bool GatoUUID::isNull() const +{ + return d->uuid.type == bt_uuid_t::BT_UUID_UNSPEC; +} + +quint16 GatoUUID::toUInt16(bool *ok) const +{ + if (d->uuid.type == bt_uuid_t::BT_UUID16) { + if (ok) *ok = true; + return d->uuid.value.u16; + } else { + if (ok) *ok = false; + return 0; + } +} + +quint32 GatoUUID::toUInt32(bool *ok) const +{ + if (d->uuid.type == bt_uuid_t::BT_UUID32) { + if (ok) *ok = true; + return d->uuid.value.u32; + } else { + if (ok) *ok = false; + return 0; + } +} + +gatouint128 GatoUUID::toUInt128() const +{ + bt_uuid_t u128; + bt_uuid_to_uuid128(&d->uuid, &u128); + return *reinterpret_cast(&u128.value.u128); +} + +QString GatoUUID::toString() const +{ + char buf[MAX_LEN_UUID_STR + 1]; + if (bt_uuid_to_string(&d->uuid, buf, MAX_LEN_UUID_STR) == 0) { + return QString::fromAscii(buf); + } else { + return QString(); + } +} + +GatoUUID & GatoUUID::operator =(const GatoUUID &o) +{ + if (this != &o) { + d = o.d; + } + return *this; +} + +bool operator==(const GatoUUID &a, const GatoUUID &b) +{ + return bt_uuid_cmp(&a.d->uuid, &b.d->uuid) == 0; +} + +bool operator!=(const GatoUUID &a, const GatoUUID &b) +{ + return bt_uuid_cmp(&a.d->uuid, &b.d->uuid) != 0; +} + +QDataStream & operator<<(QDataStream &s, const gatouint128 &u) +{ + if (static_cast(s.byteOrder()) == QSysInfo::ByteOrder) { + for (int i = 0; i < 16; i++) { + s << u.data[i]; + } + } else { + for (int i = 15; i >= 0; i--) { + s << u.data[i]; + } + } + return s; +} + +QDataStream & operator>>(QDataStream &s, gatouint128 &u) +{ + if (static_cast(s.byteOrder()) == QSysInfo::ByteOrder) { + for (int i = 0; i < 16; i++) { + s >> u.data[i]; + } + } else { + for (int i = 15; i >= 0; i--) { + s >> u.data[i]; + } + } + return s; +} + +uint qHash(const GatoUUID &a) +{ + gatouint128 u128 = a.toUInt128(); + + uint h = u128.data[0] << 24 | u128.data[1] << 16 | u128.data[2] << 8 | u128.data[3] << 0; + h ^= u128.data[4] << 24 | u128.data[5] << 16 | u128.data[6] << 8 | u128.data[7] << 0; + h ^= u128.data[8] << 24 | u128.data[9] << 16 | u128.data[10] << 8 | u128.data[11] << 0; + h ^= u128.data[12] << 24 | u128.data[13] << 16 | u128.data[14] << 8 | u128.data[15] << 0; + + return h; +} diff --git a/gatouuid.h b/gatouuid.h new file mode 100644 index 0000000..e091512 --- /dev/null +++ b/gatouuid.h @@ -0,0 +1,109 @@ +#ifndef GATOUUID_H +#define GATOUUID_H + +#include +#include +#include +#include +#include "libgato_global.h" + +class GatoUUIDPrivate; + +struct gatouint128 +{ + quint8 data[16]; +}; + +template<> +inline LIBGATO_EXPORT gatouint128 qFromLittleEndian(const uchar *src) +{ + gatouint128 dest; +#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN + for (int i = 0; i < 16; i++) { + dest.data[i] = src[i]; + } +#else + for (int i = 0; i < 16; i++) { + dest.data[i] = src[15 - i]; + } +#endif + return dest; +} + +template<> +inline LIBGATO_EXPORT void qToLittleEndian(gatouint128 src, uchar *dest) +{ +#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN + for (int i = 0; i < 16; i++) { + dest[i] = src.data[i]; + } +#else + for (int i = 0; i < 16; i++) { + dest[i] = src.data[15 - i]; + } +#endif +} + +class LIBGATO_EXPORT GatoUUID +{ + Q_GADGET + Q_ENUMS(GattUuid) + +public: + enum GattUuid { + GattGenericAccessProfile = 0x1800, + GattGenericAttributeProfile = 0x1801, + GattPrimaryService = 0x2800, + GattSecondaryService = 0x2801, + GattInclude = 0x2802, + GattCharacteristic = 0x2803, + GattCharacteristicExtendedProperties = 0x2900, + GattCharacteristicUserDescription = 0x2901, + GattClientCharacteristicConfiguration = 0x2902, + GattServerCharacteristicConfiguration = 0x2903, + GattCharacteristicFormat = 0x2904, + GattCharacteristicAggregateFormat = 0x2905, + GattDeviceName = 0x2A00, + GattAppearance = 0x2A01, + GattPeripheralPrivacyFlag = 0x2A02, + GattReconnectionAddress = 0x2A03, + GattPeripheralPreferredConnectionParameters = 0x2A04, + GattServiceChanged = 0x2A05 + }; + + GatoUUID(); + GatoUUID(GattUuid uuid); + explicit GatoUUID(quint16 uuid); + explicit GatoUUID(quint32 uuid); + explicit GatoUUID(gatouint128 uuid); + explicit GatoUUID(const QString &uuid); + GatoUUID(const GatoUUID &o); + ~GatoUUID(); + + bool isNull() const; + + quint16 toUInt16(bool *ok = 0) const; + quint32 toUInt32(bool *ok = 0) const; + gatouint128 toUInt128() const; + QString toString() const; + + GatoUUID& operator=(const GatoUUID& o); + friend bool operator==(const GatoUUID &a, const GatoUUID &b); + friend bool operator!=(const GatoUUID &a, const GatoUUID &b); + +private: + QSharedDataPointer d; +}; + +inline QDebug operator<<(QDebug debug, const GatoUUID &uuid) +{ + debug << uuid.toString(); + return debug; +} + +LIBGATO_EXPORT QDataStream & operator<<(QDataStream &s, const gatouint128 &u); +LIBGATO_EXPORT QDataStream & operator>>(QDataStream &s, gatouint128 &u); + +LIBGATO_EXPORT uint qHash(const GatoUUID &a); + +#endif // GATOUUID_H diff --git a/helpers.cpp b/helpers.cpp new file mode 100644 index 0000000..b62b464 --- /dev/null +++ b/helpers.cpp @@ -0,0 +1,64 @@ +/* + * libgato - A GATT/ATT library for use with Bluez + * + * Copyright (C) 2013 Javier S. Pedro + * + * 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 + */ + +#include "helpers.h" + +GatoUUID bytearray_to_gatouuid(const QByteArray &ba) +{ + switch (ba.size()) { + case 2: + return GatoUUID(read_le(ba.constData())); + case 4: + return GatoUUID(read_le(ba.constData())); + case 16: + return GatoUUID(read_le(ba.constData())); + default: + return GatoUUID(); + } +} + +QByteArray gatouuid_to_bytearray(const GatoUUID &uuid, bool use_uuid16, bool use_uuid32) +{ + QByteArray ba; + + if (use_uuid16) { + quint16 uuid16 = uuid.toUInt16(&use_uuid16); + if (use_uuid16) { + ba.resize(sizeof(quint16)); + write_le(uuid16, ba.data()); + return ba; + } + } + if (use_uuid32) { + quint32 uuid32 = uuid.toUInt32(&use_uuid32); + if (use_uuid32) { + ba.resize(sizeof(quint32)); + write_le(uuid32, ba.data()); + return ba; + } + } + + gatouint128 uuid128 = uuid.toUInt128(); + ba.resize(sizeof(gatouint128)); + Q_ASSERT(ba.size() == 16); + write_le(uuid128, ba.data()); + + return ba; +} diff --git a/helpers.h b/helpers.h new file mode 100644 index 0000000..962026c --- /dev/null +++ b/helpers.h @@ -0,0 +1,35 @@ +#ifndef HELPERS_H +#define HELPERS_H + +#include +#include +#include "gatouuid.h" + +template +inline T read_le(const uchar *src) +{ + return qFromLittleEndian(src); +} + +template +inline T read_le(const char *src) +{ + return qFromLittleEndian(reinterpret_cast(src)); +} + +template +void write_le(T src, uchar *dst) +{ + qToLittleEndian(src, dst); +} + +template +void write_le(T src, char *dst) +{ + qToLittleEndian(src, reinterpret_cast(dst)); +} + +GatoUUID bytearray_to_gatouuid(const QByteArray &ba); +QByteArray gatouuid_to_bytearray(const GatoUUID &uuid, bool use_uuid16, bool use_uuid32); + +#endif // HELPERS_H diff --git a/libgato.pro b/libgato.pro new file mode 100644 index 0000000..7da152e --- /dev/null +++ b/libgato.pro @@ -0,0 +1,39 @@ +TEMPLATE = lib +TARGET = gato + +QT -= gui + +DEFINES += LIBGATO_LIBRARY + +CONFIG += link_pkgconfig +PKGCONFIG += bluez + +SOURCES += \ + gatocentralmanager.cpp \ + gatouuid.cpp \ + gatoperipheral.cpp \ + gatoaddress.cpp \ + gatosocket.cpp \ + gatoatt.cpp \ + helpers.cpp \ + gatoservice.cpp \ + gatocharacteristic.cpp \ + gatodescriptor.cpp + +HEADERS += libgato_global.h \ + gatocentralmanager.h \ + gatouuid.h \ + gatoperipheral.h \ + gatoaddress.h \ + gatosocket.h \ + gatoatt.h \ + helpers.h \ + gatoperipheral_p.h \ + gatocentralmanager_p.h \ + gatoservice.h \ + gatocharacteristic.h \ + gatodescriptor.h \ + gato.h + +target.path = /usr/lib +INSTALLS += target diff --git a/libgato_global.h b/libgato_global.h new file mode 100644 index 0000000..ddc4173 --- /dev/null +++ b/libgato_global.h @@ -0,0 +1,14 @@ +#ifndef LIBGATO_GLOBAL_H +#define LIBGATO_GLOBAL_H + +#include + +#if defined(LIBGATO_LIBRARY) +# define LIBGATO_EXPORT Q_DECL_EXPORT +#else +# define LIBGATO_EXPORT Q_DECL_IMPORT +#endif + +typedef quint16 GatoHandle; + +#endif // LIBGATO_GLOBAL_H -- cgit v1.2.3