/* * scribiu -- read notebooks and voice memos from Livescribe pens * Copyright (C) 2015 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 3 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, see . */ #include #include #include #include #include #include "xmlutils.h" #include "smartpen.h" #define PEN_EPOCH (1289335960000LL) // This is probably not correct #define PEN_MTU 900 #define PEN_TIMEOUT_SECONDS 30 #define INVALID_CID 0xFFFFFFFFU static const char pen_serial_chars[] = "ABCDEFGHJKMNPQRSTUWXYZ23456789"; static const unsigned int pen_serial_num_chars = sizeof(pen_serial_chars) - 1; /* Terrible hack comes now: */ struct obex_usb_intf_transport_t { struct obex_usb_intf_transport_t *prev, *next; /* Next and previous interfaces in the list */ struct usb_device *device; /* USB device that has the interface */ }; Smartpen::Smartpen(QObject *parent) : QObject(parent), _obex(0), _connId(INVALID_CID) { } Smartpen::~Smartpen() { if (_connId != INVALID_CID || _obex) { disconnectFromPen(); } } bool Smartpen::isConnected() const { return _obex && _connId != INVALID_CID; } QByteArray Smartpen::getObject(const QString &name) { obex_object_t *obj = OBEX_ObjectNew(_obex, OBEX_CMD_GET); Q_ASSERT(obj); addConnHeader(obj); obex_headerdata_t hd; QByteArray encodedName = encodeUtf16(name); hd.bs = reinterpret_cast(encodedName.constData()); if (OBEX_ObjectAddHeader(_obex, obj, OBEX_HDR_NAME, hd, encodedName.size(), 0) < 0) { qCritical("Could not add name header"); OBEX_ObjectDelete(_obex, obj); return QByteArray(); } qDebug() << "Getting object" << name; if (OBEX_Request(_obex, obj) < 0) { qWarning() << "Get object request failed"; return QByteArray(); } QDateTime start = QDateTime::currentDateTimeUtc(); QDateTime now; do { OBEX_HandleInput(_obex, PEN_TIMEOUT_SECONDS); now = QDateTime::currentDateTimeUtc(); } while (_inBuf.isEmpty() && start.secsTo(now) < PEN_TIMEOUT_SECONDS); if (_inBuf.isEmpty()) { qWarning() << "Did not receive any data in" << start.secsTo(now) << "seconds"; } QByteArray result; qSwap(_inBuf, result); return result; } QString Smartpen::getParameter(Parameters parameter) { QString objectName = QString("ppdata?key=pp%1").arg(int(parameter), 4); QByteArray data = getObject(objectName); QXmlStreamReader r(data); advanceToFirstChildElement(r, "xml"); advanceToFirstChildElement(r, "parameter"); if (!r.atEnd()) { QXmlStreamAttributes attrs = r.attributes(); return attrs.value("value").toString(); } return QString(); } QString Smartpen::getPenName() { QString name = getParameter(PenName); if (name.isEmpty()) { return name; } QByteArray hex = QByteArray::fromHex(name.mid(2).toLatin1()); return QString::fromUtf8(hex); } QVariantMap Smartpen::getPenInfo() { QVariantMap result; QByteArray data = getObject("peninfo"); QXmlStreamReader r(data); advanceToFirstChildElement(r, "xml"); advanceToFirstChildElement(r, "peninfo"); if (!r.atEnd()) { Q_ASSERT(r.isStartElement() && r.name() == "peninfo"); QString penId = r.attributes().value("penid").toString(); result["penid"] = penId; result["penserial"] = toPenSerial(penId.mid(2).toULongLong(0, 16)); while (r.readNextStartElement()) { if (r.name() == "battery") { result["battery"] = r.attributes().value("level").toString(); r.skipCurrentElement(); } else if (r.name() == "time") { result["time"] = r.attributes().value("absolute").toString(); r.skipCurrentElement(); } else { r.skipCurrentElement(); } } } else { qWarning() << "Could not parse peninfo XML"; } return result; } QList Smartpen::getChangeList(PenTime from) { QList result; QByteArray data = getObject(QString("changelist?start_time=%1").arg(from)); QXmlStreamReader r(data); qDebug() << QString::fromAscii(data); advanceToFirstChildElement(r, "xml"); advanceToFirstChildElement(r, "changelist"); if (!r.atEnd()) { Q_ASSERT(r.isStartElement() && r.name() == "changelist"); while (r.readNextStartElement()) { if (r.name() == "lsp") { QXmlStreamAttributes attrs = r.attributes(); ChangeReport report; if (attrs.hasAttribute("guid")) { report.guid = attrs.value("guid").toString(); report.title = attrs.value("title").toString(); // Figure out the change record endtime to enumerating the // endtime of all the record's changed pages report.endTime = 0; while (r.readNextStartElement()) { if (r.name() == "page") { QXmlStreamAttributes attrs = r.attributes(); PenTime endTime = attrs.value("end_time").toString().toLongLong(); if (endTime > report.endTime) report.endTime = endTime; } r.skipCurrentElement(); } result.append(report); } else if (attrs.hasAttribute("classname")) { report.className = attrs.value("classname").toString(); report.title = attrs.value("title").toString(); report.endTime = attrs.value("end_time").toString().toLongLong(); result.append(report); r.skipCurrentElement(); } else { qWarning() << "Unknown change report format"; r.skipCurrentElement(); } } else { r.skipCurrentElement(); } } } else { qWarning() << "Could not parse changelist XML"; } return result; } QByteArray Smartpen::getLspData(const QString &name, PenTime from) { return getObject(QString("lspdata?name=%1&start_time=%2").arg(name).arg(from)); } QByteArray Smartpen::getPaperReplay(PenTime from) { return getObject(QString("lspdata?name=com.livescribe.paperreplay.PaperReplay&start_time=%1&returnVersion=0.3&remoteCaller=WIN_LD_200").arg(from)); } qint64 Smartpen::toPenTime(const QDateTime &dt) { if (dt.isValid()) { return dt.toMSecsSinceEpoch() - PEN_EPOCH; } else { return 0; } } QDateTime Smartpen::fromPenTime(qint64 t) { if (t) { return QDateTime::fromMSecsSinceEpoch(t + PEN_EPOCH).toLocalTime(); } else { return QDateTime(); } } QString Smartpen::toPenSerial(quint64 id) { QString serial; serial.reserve(3 + 1 + 3 + 1 + 3 + 1 + 2 + 1); serial.append(toPenSerialSegment(id >> 32, 3)); serial.append('-'); serial.append(toPenSerialSegment(id, 6).mid(0, 3)); serial.append('-'); serial.append(toPenSerialSegment(id, 6).mid(3, 3)); serial.append('-'); serial.append(toPenSerialSegment(id % 0x36D, 2)); return serial; } quint64 Smartpen::toPenId(const QString &serial) { QStringList segments = serial.split('-'); if (segments.size() != 4) { return 0; } quint64 id = quint64(fromPenSerialSegment(segments[0])) << 32; id |= fromPenSerialSegment(segments[1] + segments[2]) * 0x36D; id |= fromPenSerialSegment(segments[3]); return id; } bool Smartpen::connectToPen(const Address &addr) { if (_obex) { qWarning() << "Already connected"; return false; } _obex = OBEX_Init(OBEX_TRANS_USB, obexEventCb, 0); Q_ASSERT(_obex); OBEX_SetUserData(_obex, this); OBEX_SetTransportMTU(_obex, PEN_MTU, PEN_MTU); obex_interface_t *interfaces, *ls_interface = 0; int count = OBEX_FindInterfaces(_obex, &interfaces); for (int i = 0; i < count; i++) { if (interfaces[i].usb.intf->device->bus->location == addr.first && interfaces[i].usb.intf->device->devnum == addr.second) { ls_interface = &interfaces[i]; } } if (!ls_interface) { qWarning() << "Could not find Lightscribe interface on device:" << addr; return false; } usb_dev_handle *handle = usb_open(ls_interface->usb.intf->device); if (handle) { qDebug() << "resetting usb device"; usb_reset(handle); usb_close(handle); } else { qWarning() << "could not open usb device for resetting"; } qDebug() << "connecting to" << ls_interface->usb.product; if (OBEX_InterfaceConnect(_obex, ls_interface) < 0) { qWarning() << "Could not connect to Livescribe interface"; return false; } static const char * livescribe_service = "LivescribeService"; obex_object_t *object = OBEX_ObjectNew(_obex, OBEX_CMD_CONNECT); obex_headerdata_t hd; int hd_len; Q_ASSERT(object); hd.bs = reinterpret_cast(livescribe_service); hd_len = strlen(livescribe_service) + 1; if (OBEX_ObjectAddHeader(_obex, object, OBEX_HDR_TARGET, hd, hd_len, 0) < 0) { qWarning() << "Failed to add Target header"; OBEX_ObjectDelete(_obex, object); return false; } if (OBEX_Request(_obex, object) < 0) { qWarning() << "Failed to make connection request"; OBEX_ObjectDelete(_obex, object); return false; } qDebug() << "Connection in progress"; OBEX_HandleInput(_obex, PEN_TIMEOUT_SECONDS); return _connId != INVALID_CID; } void Smartpen::disconnectFromPen() { if (_connId != INVALID_CID) { if (_obex) { obex_object_t *object = OBEX_ObjectNew(_obex, OBEX_CMD_DISCONNECT); Q_ASSERT(object); addConnHeader(object); OBEX_Request(_obex, object); OBEX_HandleInput(_obex, PEN_TIMEOUT_SECONDS); } _connId = INVALID_CID; } if (_obex) { OBEX_Cleanup(_obex); _obex = 0; } _inBuf.clear(); } void Smartpen::obexEventCb(obex_t *handle, obex_object_t *obj, int mode, int event, int obex_cmd, int obex_rsp) { Smartpen *smartpen = static_cast(OBEX_GetUserData(handle)); Q_UNUSED(mode); smartpen->handleObexEvent(obj, event, obex_cmd, obex_rsp); } void Smartpen::handleObexEvent(obex_object_t *object, int event, int obex_cmd, int obex_rsp) { switch (event) { case OBEX_EV_PROGRESS: if (obex_cmd == OBEX_CMD_GET) { // It seems that the pen wants us to add this header on every continue response addConnHeader(object); } break; case OBEX_EV_REQDONE: qDebug() << "event reqdone cmd=" << obex_cmd << " rsp=" << OBEX_ResponseToString(obex_rsp); handleObexRequestDone(object, obex_cmd, obex_rsp); break; case OBEX_EV_LINKERR: qWarning() << "link error cmd=" << obex_cmd; emit error(); break; default: qDebug() << "event" << event << obex_cmd << obex_rsp; break; } } void Smartpen::handleObexRequestDone(obex_object_t *object, int obex_cmd, int obex_rsp) { quint8 header_id; obex_headerdata_t hdata; quint32 hlen; switch (obex_cmd & ~OBEX_FINAL) { case OBEX_CMD_CONNECT: switch (obex_rsp) { case OBEX_RSP_SUCCESS: while (OBEX_ObjectGetNextHeader(_obex, object, &header_id, &hdata, &hlen)) { if (header_id == OBEX_HDR_CONNECTION) { Q_ASSERT(_connId == INVALID_CID); _connId = hdata.bq4; qDebug() << "Connection established, id:" << _connId; } } break; default: qWarning() << "Failed connection request:" << OBEX_ResponseToString(obex_rsp); emit error(); break; } break; case OBEX_CMD_DISCONNECT: switch (obex_rsp) { case OBEX_RSP_SUCCESS: qDebug() << "Disconnected succesfully"; _connId = INVALID_CID; break; default: qWarning() << "Failed disconnection request:" << OBEX_ResponseToString(obex_rsp); _connId = INVALID_CID; break; } break; case OBEX_CMD_GET: switch (obex_rsp) { case OBEX_RSP_SUCCESS: qDebug() << "GET request succesful"; while (OBEX_ObjectGetNextHeader(_obex, object, &header_id, &hdata, &hlen)) { if (header_id == OBEX_HDR_BODY || header_id == OBEX_HDR_BODY_END) { _inBuf = QByteArray(reinterpret_cast(hdata.bs), hlen); } } break; default: qWarning() << "Failed GET request:" << OBEX_ResponseToString(obex_rsp); break; } break; } } QString Smartpen::toPenSerialSegment(quint32 id, int len) { QString segment(len, Qt::Uninitialized); for (int i = 0; i < len; i++) { segment[len - (i + 1)] = pen_serial_chars[id % pen_serial_num_chars]; id /= pen_serial_num_chars; } return segment; } quint32 Smartpen::fromPenSerialSegment(const QString &s) { const int len = s.length(); quint32 id = 0; for (int i = 0; i < len; i++) { uint val = qFind(&pen_serial_chars[0], &pen_serial_chars[pen_serial_num_chars], s[i]) - &pen_serial_chars[0]; if (val >= pen_serial_num_chars) return 0; id = val + id * pen_serial_num_chars; } return id; } QByteArray Smartpen::encodeUtf16(const QString &s) { const int size = s.size(); QByteArray data((size + 1) * sizeof(quint16), Qt::Uninitialized); quint16 *p = reinterpret_cast(data.data()); for (int i = 0; i < size; i++) { p[i] = qToBigEndian(s.at(i).unicode()); } p[size] = 0; return data; } void Smartpen::addConnHeader(obex_object_t *obj) const { obex_headerdata_t hd; hd.bq4 = _connId; if (OBEX_ObjectAddHeader(_obex, obj, OBEX_HDR_CONNECTION, hd, sizeof(hd.bq4), 0) < 0) { qCritical() << "Could not add connection header"; } }