/* * 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 #include #include "xmlutils.h" #include "smartpen.h" #define PEN_MTU OBEX_MAXIMUM_MTU #define PEN_TIMEOUT_SECONDS 5 #define INVALID_CID 0xFFFFFFFFU static const char pen_serial_chars[] = "ABCDEFGHJKMNPQRSTUWXYZ23456789"; static const unsigned int pen_serial_num_chars = sizeof(pen_serial_chars) - 1; Smartpen::Smartpen(QObject *parent) : QObject(parent), _obex(0), _connId(INVALID_CID), _reqComplete(false), _continueReceived(0) { } Smartpen::~Smartpen() { if (_connId != INVALID_CID || _obex) { disconnectFromPen(); } } bool Smartpen::isConnected() const { return _obex && _connId != INVALID_CID; } QByteArray Smartpen::getObject(const QString &name) { qDebug() << "Getting object" << name; prepareRequest(); 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(); } if (OBEX_Request(_obex, obj) < 0) { qWarning() << "Get object request failed"; OBEX_ObjectDelete(_obex, obj); return QByteArray(); } waitForRequestComplete(PEN_TIMEOUT_SECONDS); QByteArray result; qSwap(_inBuf, result); return result; } QByteArray Smartpen::getParameter(Parameter parameter) { QString objectName = QString("ppdata?key=pp%1").arg(uint(parameter), 4, 16, QChar('0')); QByteArray data = getObject(objectName); QXmlStreamReader r(data); advanceToFirstChildElement(r, "xml"); advanceToFirstChildElement(r, "parameter"); if (!r.atEnd()) { QXmlStreamAttributes attrs = r.attributes(); QString value = attrs.value("value").toString(); if (!value.isEmpty()) { if (value.startsWith("0x")) { return QByteArray::fromHex(value.mid(2).toLatin1()); } else { qWarning() << "Unknown parameter return format: " << value; } } } return QByteArray(); } Smartpen::PenId Smartpen::getPenId() { PenId id; QByteArray value = getParameter(Parameter::Id); if (value.isEmpty() || value.size() != 1 + sizeof(Smartpen::PenId)) { qWarning() << "got invalid value for pen id: " << value.toHex(); return 0; } QDataStream ds(value); ds.setByteOrder(QDataStream::BigEndian); ds.skipRawData(1); // Unclear what first byte is ds >> id; return id; } QString Smartpen::getPenSerial() { return toPenSerial(getPenId()); } QString Smartpen::getPenName() { return QString::fromUtf8(getParameter(Parameter::Name)); } Smartpen::PenTime Smartpen::getPenTime(Parameter parameter) { PenTime time; QByteArray value = getParameter(parameter); if (value.isEmpty() || value.size() != sizeof(Smartpen::PenTime)) { qWarning() << "got invalid value for pen time: " << value.toHex(); return 0; } QDataStream ds(value); ds.setByteOrder(QDataStream::LittleEndian); ds >> time; return time; } QVariantMap Smartpen::getPenInfo() { QVariantMap result; QByteArray data = getObject("peninfo"); QXmlStreamReader r(data); qDebug() << "PenInfo: " << QString::fromLatin1(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() << "changelist:" << QString::fromLatin1(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)); } QDateTime Smartpen::fromPenTime(PenTime userTime, PenTime penTime) { QDateTime dt = QDateTime::fromMSecsSinceEpoch(penTime + userTime, Qt::UTC); dt.setTimeSpec(Qt::LocalTime); // userTime is actually in LocalTime, so override tz conversion return dt; } 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::reset(const Address &addr) { libusb_context *ctx = 0; libusb_device **devlist = 0; libusb_device *dev = 0; libusb_device_handle *handle = 0; ssize_t ndevs; int err = 0; err = libusb_init(&ctx); if (err != 0) { qWarning() << "libusb_init failed:" << err; goto err0; } ndevs = libusb_get_device_list(ctx, &devlist); if (ndevs < 0) { qWarning() << "libusb_get_device_list failed:" << err; goto err1; } for (ssize_t i = 0; i < ndevs; ++i) { if (libusb_get_bus_number(devlist[i]) == addr.first && libusb_get_device_address(devlist[i]) == addr.second) { dev = devlist[i]; } } if (!dev) { qWarning() << "could not find device in libusb"; err = -ENODEV; goto err2; } err = libusb_open(dev, &handle); if (err != 0) { qWarning() << "libusb_open failed:" << err; goto err2; } err = libusb_reset_device(handle); if (err != 0) { qWarning() << "libusb_reset_device failed: " << err; goto err3; } qDebug() << "USB device resetted"; err3: libusb_close(handle); err2: libusb_free_device_list(devlist, 1); err1: libusb_exit(ctx); err0: return err == 0; } bool Smartpen::connectToPen(const Address &addr) { if (_obex) { qWarning() << "Already connected"; return false; } _connId = INVALID_CID; _obex = OBEX_Init(OBEX_TRANS_USB, obexEventCb, OBEX_FL_CLOEXEC); Q_ASSERT(_obex); OBEX_SetUserData(_obex, this); OBEX_SetTransportMTU(_obex, PEN_MTU, PEN_MTU); obex_interface_t *ls_interface = 0; int count = OBEX_EnumerateInterfaces(_obex); for (int i = 0; i < count; i++) { obex_interface_t *intf = OBEX_GetInterfaceByIndex(_obex, i); if (intf->usb.bus_number == addr.first && intf->usb.device_address == addr.second) { ls_interface = intf; } } if (!ls_interface) { qWarning() << "Could not find Lightscribe interface on device:" << addr; return false; } qDebug() << "Connecting to" << ls_interface->usb.product; int err = OBEX_InterfaceConnect(_obex, ls_interface) ; if (err < 0) { qWarning() << "Could not connect to Livescribe interface" << strerror(-err); return false; } prepareRequest(); 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"; waitForRequestComplete(PEN_TIMEOUT_SECONDS); return _connId != INVALID_CID; } void Smartpen::disconnectFromPen() { if (_connId != INVALID_CID) { if (_obex) { OBEX_CancelRequest(_obex, 0); prepareRequest(); obex_object_t *object = OBEX_ObjectNew(_obex, OBEX_CMD_DISCONNECT); Q_ASSERT(object); addConnHeader(object); if (OBEX_Request(_obex, object) == 0) { qDebug() << "Send disconnect"; waitForRequestComplete(PEN_TIMEOUT_SECONDS); } } _connId = INVALID_CID; } if (_obex) { OBEX_TransportDisconnect(_obex); OBEX_Cleanup(_obex); _obex = 0; } _inBuf.clear(); qDebug() << "Disconnected"; } 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) { // Special flag used for temporarily ignoring the synthetic events generated during OBEX_CancelRequest static bool aborting_continue = false; if (aborting_continue) return; switch (event) { case OBEX_EV_PROGRESS: 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 << " rsp=" << OBEX_ResponseToString(obex_rsp); emit linkError("Link error"); break; case OBEX_EV_PARSEERR: qWarning() << "parse error cmd=" << obex_cmd << " rsp=" << OBEX_ResponseToString(obex_rsp); emit linkError("Protocol error"); break; case OBEX_EV_CONTINUE: // The standard "Continue" messages sent by OpenObex "forget" to include the ConnectionID, confusing the pen // We're going to therefore implement the "continue" state machine on our own handleObexContinue(object, obex_cmd); // Cancel OpenObex's Continue message, avoiding the spurious events that generates Q_ASSERT(!aborting_continue); aborting_continue = true; OBEX_CancelRequest(_obex, 0); aborting_continue = false; break; default: qDebug() << "event unknown=" << event << " cmd=" << obex_cmd << " rsp=" << OBEX_ResponseToString(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; _reqComplete = true; 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 linkError("OBEX connection 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.append(reinterpret_cast(hdata.bs), hlen); } } break; default: qWarning() << "Failed GET request:" << OBEX_ResponseToString(obex_rsp); break; } break; } } void Smartpen::handleObexContinue(obex_object_t *object, int obex_cmd) { const uint8_t *data; int len; _continueReceived++; switch (obex_cmd & ~OBEX_FINAL) { case OBEX_CMD_GET: len = OBEX_ObjectReadStream(_obex, object, &data); if (len > 0) { _inBuf.append(reinterpret_cast(data), len); } break; } } void Smartpen::prepareRequest() { _reqComplete = false; _continueReceived = 0; _inBuf.clear(); } bool Smartpen::waitForRequestComplete(int timeout) { QDeadlineTimer timer(timeout * 1000UL); timer.setTimerType(Qt::CoarseTimer); int cmd = OBEX_ObjectGetCommand(_obex, NULL); do { if (OBEX_HandleInput(_obex, timer.remainingTime() / 1000) < 0) { qWarning() << "OBEX_HandleInput failed"; break; } if (_continueReceived) { sendContinue(cmd); _continueReceived--; // Reset timeout timer.setRemainingTime(timeout * 1000UL); } } while (!_reqComplete && !timer.hasExpired()); if (!_reqComplete) { qWarning() << "Did not complete request in" << timeout << "seconds"; emit linkError("Timeout"); return false; } return true; } 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), OBEX_FL_FIT_ONE_PACKET) < 0) { qCritical() << "Could not add connection header"; } } bool Smartpen::sendContinue(int obex_cmd) { obex_object_t *obj = OBEX_ObjectNew(_obex, obex_cmd); Q_ASSERT(obj); addConnHeader(obj); if (OBEX_Request(_obex, obj) < 0) { qWarning() << "Send continue failed"; OBEX_ObjectDelete(_obex, obj); return false; } return true; } 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 = std::find(&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; }