From 5cb277888995edecfafd83fed4cf2bd510052a4b Mon Sep 17 00:00:00 2001 From: Javier Date: Sun, 12 Apr 2020 00:45:32 +0200 Subject: port to qt5, libusb 1.0, and openobex 1.7 --- .gitignore | 1 + 60-livescribe.rules | 2 +- README.md | 6 +- main.cc | 7 +- mainwindow.cc | 36 +++---- mainwindow.h | 4 +- mainwindow.ui | 75 ++++----------- notebookmodel.cc | 15 +-- notebookview.h | 4 +- pageitem.cc | 2 +- pageitem.h | 2 +- paperreplaymodel.cc | 2 +- scribiu.pro | 16 ++-- smartpen.cc | 269 +++++++++++++++++++++++++++++++++++++++------------- smartpen.h | 14 ++- smartpenmanager.cc | 261 ++++++++++++++++++++++++++++++++++++++++++++------ smartpenmanager.h | 31 ++++-- smartpensyncer.cc | 48 +++++++--- smartpensyncer.h | 20 ++-- stfgraphicsitem.h | 2 +- stfstrokeitem.cc | 8 +- stfstrokeitem.h | 2 +- 22 files changed, 590 insertions(+), 237 deletions(-) diff --git a/.gitignore b/.gitignore index 75c107b..bebf921 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ *.pro.user +/build diff --git a/60-livescribe.rules b/60-livescribe.rules index 340ac5a..a10d9c3 100644 --- a/60-livescribe.rules +++ b/60-livescribe.rules @@ -1 +1 @@ -SUBSYSTEM=="usb", ENV{ID_VENDOR}=="Livescribe", TAG+="udev-acl", TAG+="livescribe-pen" +SUBSYSTEM=="usb", ENV{ID_VENDOR}=="Livescribe", TAG+="uaccess", TAG+="livescribe-pen" diff --git a/README.md b/README.md index aecd148..bee62c2 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,8 @@ It also allows you to export individual pages as PNG files or voice memos as AAC # Requirements -Scribiu requires Qt 4.8, including the core, gui, svg and phonon modules. -It also requires libudev, openobex, libusb (<1.0) and QuaZip. +Scribiu requires Qt 5, including the core, gui, widgets, and svg modules. +It also requires phonon, libudev, openobex (>=1.7), libusb (>=1.0) and QuaZip. Most of these should be packaged by your distribution. This program should work with the Livescribe Pulse as well as the Livescribe Echo. @@ -32,7 +32,7 @@ It should automatically start synchronizing after connecting a Smartpen. # Design -By default Scribiu stores its information inside `$XDG_DATA_HOME/data/scribiu`. +By default Scribiu stores its information inside `$XDG_DATA_HOME/scribiu`. Inside there you will find a directory for every synchronized pen, and inside the pen directory, you will find a subdirectory for each one of your LiveScribe notebooks. These directories contain the raw notebook, stroke, and voice data as received from the pen -- no processing is done. diff --git a/main.cc b/main.cc index 9ddf7c4..406a729 100644 --- a/main.cc +++ b/main.cc @@ -17,15 +17,18 @@ */ #include "mainwindow.h" -#include +#include int main(int argc, char *argv[]) { + QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); + QApplication app(argc, argv); app.setOrganizationName("scribiu"); app.setOrganizationDomain("com.javispedro.scribiu"); app.setApplicationName("scribiu"); - app.setApplicationVersion("1.0"); + app.setApplicationVersion("1.2"); MainWindow w; w.show(); diff --git a/mainwindow.cc b/mainwindow.cc index 5d513b1..5c27145 100644 --- a/mainwindow.cc +++ b/mainwindow.cc @@ -19,8 +19,8 @@ #include #include #include -#include -#include +#include +#include #include #include "mainwindow.h" #include "ui_mainwindow.h" @@ -37,30 +37,15 @@ MainWindow::MainWindow(QWidget *parent) : _statusLabel(new QLabel) { ui->setupUi(this); -#if QT_VERSION < QT_VERSION_CHECK(5, 1, 0) - // Some tricks for DPI support - const qreal scale = logicalDpiX() / 96.0; - if (scale > 1.1) { - ui->notebookTree->header()->setDefaultSectionSize(ui->notebookTree->header()->defaultSectionSize() * scale); - ui->notebookTree->setIconSize(QSize(16, 16) * scale); - ui->prevButton->setMaximumSize(ui->prevButton->maximumSize() * scale); - ui->nextButton->setMaximumSize(ui->nextButton->maximumSize() * scale); - ui->exportButton->setMaximumSize(ui->exportButton->maximumSize() * scale); - ui->playButton->setMaximumSize(ui->playButton->maximumSize() * scale); - ui->pauseButton->setMaximumSize(ui->pauseButton->maximumSize() * scale); - ui->playButton->setMaximumSize(ui->prevButton->maximumSize() * scale); - ui->pageEdit->setMaximumWidth(ui->pageEdit->maximumWidth() * scale); - } -#endif ui->notebookTree->setModel(_notebooks); - ui->notebookTree->header()->setResizeMode(0, QHeaderView::Stretch); - ui->notebookTree->header()->setResizeMode(1, QHeaderView::Fixed); - ui->notebookTree->header()->setResizeMode(2, QHeaderView::Fixed); + ui->notebookTree->header()->setSectionResizeMode(0, QHeaderView::Stretch); + ui->notebookTree->header()->setSectionResizeMode(1, QHeaderView::Fixed); + ui->notebookTree->header()->setSectionResizeMode(2, QHeaderView::Fixed); ui->notebookTree->expandAll(); ui->notebookView->setVisible(false); ui->paperReplayView->setModel(_replayModel); - ui->paperReplayView->horizontalHeader()->setResizeMode(0, QHeaderView::Stretch); - ui->paperReplayView->horizontalHeader()->setResizeMode(1, QHeaderView::Fixed); + ui->paperReplayView->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); + ui->paperReplayView->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Fixed); ui->paperReplayView->setVisible(false); Phonon::createPath(_media, _mediaOutput); _media->setTickInterval(500); @@ -185,10 +170,11 @@ void MainWindow::handleNotebookSelected(const QModelIndex &index) return; } - QModelIndex child = parent.child(index.row(), 0); + // Get column 0, which corresponds to notebook name + QModelIndex nb = _notebooks->index(index.row(), 0, parent); openNotebook(_notebooks->data(parent, Qt::DisplayRole).toString(), - _notebooks->data(child, Qt::DisplayRole).toString()); + _notebooks->data(nb, Qt::DisplayRole).toString()); } void MainWindow::handleNotebookRowsInserted(const QModelIndex &index, int start, int end) @@ -220,7 +206,7 @@ void MainWindow::handlePaperReplayRequested(const QString &file, qint64 time) QString filePath = finfo.canonicalFilePath(); if (_media->currentSource().fileName() != filePath) { - _media->setCurrentSource(filePath); + _media->setCurrentSource(QUrl::fromLocalFile(filePath)); } switch (_media->state()) { diff --git a/mainwindow.h b/mainwindow.h index 6689795..f3f6128 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -19,8 +19,8 @@ #ifndef MAINWINDOW_H #define MAINWINDOW_H -#include -#include +#include +#include #include #include #include "notebookmodel.h" diff --git a/mainwindow.ui b/mainwindow.ui index 1cfff9a..c04ff30 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -15,8 +15,7 @@ - - + .. @@ -57,7 +56,7 @@ false - 27 + 28 false @@ -72,19 +71,12 @@ - - - 30 - 30 - - - - + .. @@ -109,16 +101,9 @@ - - - 30 - 30 - - - - + .. @@ -140,19 +125,12 @@ - - - 30 - 30 - - ... - - + .. @@ -277,42 +255,28 @@ - - - 30 - 30 - - ... - - + .. - - - 30 - 30 - - ... - - + .. - + @@ -338,7 +302,7 @@ 0 0 718 - 23 + 29 @@ -362,8 +326,7 @@ - - + .. &Quit @@ -378,8 +341,7 @@ - - + .. &Export... @@ -388,8 +350,7 @@ - - + .. About... @@ -398,6 +359,12 @@ + + Phonon::SeekSlider + QWidget +
phonon/seekslider.h
+ 1 +
NotebookView QGraphicsView @@ -411,12 +378,6 @@ nextPage() - - Phonon::SeekSlider - QWidget -
Phonon/SeekSlider
- 1 -
diff --git a/notebookmodel.cc b/notebookmodel.cc index e52d3f4..b9c897e 100644 --- a/notebookmodel.cc +++ b/notebookmodel.cc @@ -18,10 +18,10 @@ #include #include -#include #include #include -#include +#include +#include #include "paperreplay.h" #include "afdnotebook.h" #include "notebookmodel.h" @@ -46,7 +46,7 @@ NotebookModel::NotebookModel(QObject *parent) : QString NotebookModel::defaultDataDirectory() { - QString path = QDesktopServices::storageLocation(QDesktopServices::DataLocation); + QString path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); if (path.isEmpty()) { path = QDir::home().absoluteFilePath(".scribiu"); } @@ -150,6 +150,7 @@ QVariant NotebookModel::data(const QModelIndex &index, int role) const } break; } + break; case Qt::TextAlignmentRole: switch (index.column()) { case 0: @@ -158,6 +159,7 @@ QVariant NotebookModel::data(const QModelIndex &index, int role) const case 2: return Qt::AlignCenter; } + break; } } return QVariant(); @@ -239,9 +241,10 @@ void NotebookModel::refresh() { QStringList pens = _dataDir.entryList(QStringList("*.pen"), QDir::Dirs, QDir::Name); for (int i = 0; i < pens.size(); i++) { - pens[i].chop(4); + pens[i].chop(4); // Remove .pen extension } + // Insert/remove new/deleted pens int i = 0, j = 0; while (i < _pens.size() && j < pens.size()) { int comp = QString::compare(_pens[i], pens[j], Qt::CaseInsensitive); @@ -336,7 +339,7 @@ void NotebookModel::refreshPen(const QString &name) int NotebookModel::indexOfPen(const QString &name) { - QStringList::const_iterator it = qBinaryFind(_pens, name); + auto it = std::lower_bound(_pens.begin(), _pens.end(), name); if (it == _pens.end()) { return -1; } else { @@ -361,7 +364,7 @@ QIcon NotebookModel::getNotebookIcon(const QString &pen, const QString ¬ebook candidates << "userdata/icon/Notebook.png" << "userdata/icon/active_64x64.png" << "userdata/icon/active_32x32.png" - << "userdata/icon/active_16x16.png"; + << "userdata/icon/active_16x16.png"; } QIcon icon; diff --git a/notebookview.h b/notebookview.h index 605db5c..c9f5686 100644 --- a/notebookview.h +++ b/notebookview.h @@ -19,8 +19,8 @@ #ifndef NOTEBOOKVIEW_H #define NOTEBOOKVIEW_H -#include -#include +#include +#include #include "afdnotebook.h" #include "paperreplay.h" #include "pageitem.h" diff --git a/pageitem.cc b/pageitem.cc index 09c84ae..b5be4cf 100644 --- a/pageitem.cc +++ b/pageitem.cc @@ -35,7 +35,7 @@ PageItem::PageItem(AfdNotebook *nb, PaperReplay *replay, int pageNum, QGraphicsI bg->setShapeMode(QGraphicsPixmapItem::BoundingRectShape); bg->setTransformationMode(Qt::SmoothTransformation); QRectF bgRect = bg->boundingRect(); - bg->scale(_pageTrim.width() / bgRect.width(), _pageTrim.height() / bgRect.height()); + bg->setScale(std::min(_pageTrim.width() / bgRect.width(), _pageTrim.height() / bgRect.height())); bg->setPos(_pageTrim.topLeft()); } diff --git a/pageitem.h b/pageitem.h index 446b7b5..831ca61 100644 --- a/pageitem.h +++ b/pageitem.h @@ -19,7 +19,7 @@ #ifndef PAGEITEM_H #define PAGEITEM_H -#include +#include #include "afdnotebook.h" #include "paperreplay.h" diff --git a/paperreplaymodel.cc b/paperreplaymodel.cc index ae17367..480968f 100644 --- a/paperreplaymodel.cc +++ b/paperreplaymodel.cc @@ -98,7 +98,7 @@ void PaperReplayModel::refresh() { beginResetModel(); _sessions = _replay->sessions(); - qSort(_sessions.begin(), _sessions.end(), PaperReplay::Session::startTimeLess); + std::sort(_sessions.begin(), _sessions.end(), PaperReplay::Session::startTimeLess); endResetModel(); } diff --git a/scribiu.pro b/scribiu.pro index a9b12d0..0623205 100644 --- a/scribiu.pro +++ b/scribiu.pro @@ -1,16 +1,16 @@ -QT += core gui svg phonon - -greaterThan(QT_MAJOR_VERSION, 4): QT += widgets - TARGET = scribiu TEMPLATE = app +QT += core gui widgets svg +QT += phonon4qt5 + +CONFIG += c++11 + CONFIG += link_pkgconfig -PKGCONFIG += libudev libusb openobex -LIBS += -lquazip +PKGCONFIG += libudev libusb-1.0 openobex quazip -SOURCES += main.cc\ - mainwindow.cc \ +SOURCES += main.cc \ + mainwindow.cc \ smartpenmanager.cc \ notebookmodel.cc \ smartpensyncer.cc \ diff --git a/smartpen.cc b/smartpen.cc index b03f0e5..1282e33 100644 --- a/smartpen.cc +++ b/smartpen.cc @@ -18,29 +18,25 @@ #include #include +#include +#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 PEN_EPOCH (1289335960000LL) +#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; -/* 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) + QObject(parent), _obex(0), _connId(INVALID_CID), _reqComplete(false), _continueReceived(0) { } @@ -58,6 +54,10 @@ bool Smartpen::isConnected() const QByteArray Smartpen::getObject(const QString &name) { + qDebug() << "Getting object" << name; + + prepareRequest(); + obex_object_t *obj = OBEX_ObjectNew(_obex, OBEX_CMD_GET); Q_ASSERT(obj); @@ -72,23 +72,13 @@ QByteArray Smartpen::getObject(const QString &name) return QByteArray(); } - qDebug() << "Getting object" << name; - if (OBEX_Request(_obex, obj) < 0) { qWarning() << "Get object request failed"; + OBEX_ObjectDelete(_obex, obj); 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"; - } + waitForRequestComplete(PEN_TIMEOUT_SECONDS); QByteArray result; qSwap(_inBuf, result); @@ -117,7 +107,7 @@ QString Smartpen::getPenName() { QString name = getParameter(PenName); if (name.isEmpty()) { - return name; + return name; // Empty string if unknown name } QByteArray hex = QByteArray::fromHex(name.mid(2).toLatin1()); @@ -163,7 +153,7 @@ QList Smartpen::getChangeList(PenTime from) QByteArray data = getObject(QString("changelist?start_time=%1").arg(from)); QXmlStreamReader r(data); - qDebug() << QString::fromAscii(data); + qDebug() << "changelist:" << QString::fromLatin1(data); advanceToFirstChildElement(r, "xml"); advanceToFirstChildElement(r, "changelist"); @@ -271,6 +261,64 @@ quint64 Smartpen::toPenId(const QString &serial) 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) { @@ -278,18 +326,21 @@ bool Smartpen::connectToPen(const Address &addr) return false; } - _obex = OBEX_Init(OBEX_TRANS_USB, obexEventCb, 0); + _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 *interfaces, *ls_interface = 0; - int count = OBEX_FindInterfaces(_obex, &interfaces); + obex_interface_t *ls_interface = 0; + int count = OBEX_EnumerateInterfaces(_obex); 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]; + obex_interface_t *intf = OBEX_GetInterfaceByIndex(_obex, i); + if (intf->usb.bus_number == addr.first && + intf->usb.device_address == addr.second) { + ls_interface = intf; } } @@ -298,22 +349,16 @@ bool Smartpen::connectToPen(const Address &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; + qDebug() << "Connecting to" << ls_interface->usb.product; - if (OBEX_InterfaceConnect(_obex, ls_interface) < 0) { - qWarning() << "Could not connect to Livescribe interface"; + 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; @@ -337,7 +382,7 @@ bool Smartpen::connectToPen(const Address &addr) qDebug() << "Connection in progress"; - OBEX_HandleInput(_obex, PEN_TIMEOUT_SECONDS); + waitForRequestComplete(PEN_TIMEOUT_SECONDS); return _connId != INVALID_CID; } @@ -346,19 +391,27 @@ 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); - OBEX_Request(_obex, object); - OBEX_HandleInput(_obex, PEN_TIMEOUT_SECONDS); + 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, @@ -372,24 +425,38 @@ void Smartpen::obexEventCb(obex_t *handle, obex_object_t *obj, 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: - 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(); + 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" << event << obex_cmd << obex_rsp; + qDebug() << "event unknown=" << event << " cmd=" << obex_cmd << " rsp=" << OBEX_ResponseToString(obex_rsp); break; } } @@ -400,6 +467,8 @@ void Smartpen::handleObexRequestDone(obex_object_t *object, int obex_cmd, int ob obex_headerdata_t hdata; quint32 hlen; + _reqComplete = true; + switch (obex_cmd & ~OBEX_FINAL) { case OBEX_CMD_CONNECT: switch (obex_rsp) { @@ -414,7 +483,7 @@ void Smartpen::handleObexRequestDone(obex_object_t *object, int obex_cmd, int ob break; default: qWarning() << "Failed connection request:" << OBEX_ResponseToString(obex_rsp); - emit error(); + emit linkError("OBEX connection error"); break; } break; @@ -437,7 +506,7 @@ void Smartpen::handleObexRequestDone(obex_object_t *object, int obex_cmd, int ob 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); + _inBuf.append(reinterpret_cast(hdata.bs), hlen); } } break; @@ -450,6 +519,85 @@ void Smartpen::handleObexRequestDone(obex_object_t *object, int obex_cmd, int ob } } +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) { @@ -469,7 +617,7 @@ quint32 Smartpen::fromPenSerialSegment(const QString &s) 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]; + 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; } @@ -488,12 +636,3 @@ QByteArray Smartpen::encodeUtf16(const QString &s) 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"; - } -} diff --git a/smartpen.h b/smartpen.h index 18ac955..30c1f74 100644 --- a/smartpen.h +++ b/smartpen.h @@ -73,12 +73,14 @@ public: static QString toPenSerial(PenId id); static PenId toPenId(const QString &serial); + static bool reset(const Address &addr); + public slots: bool connectToPen(const Address &addr); void disconnectFromPen(); signals: - void error(); + void linkError(const QString &msg); private: static void obexEventCb(obex_t *handle, obex_object_t *obj, @@ -86,17 +88,25 @@ private: void handleObexEvent(obex_object_t *object, int event, int obex_cmd, int obex_rsp); void handleObexRequestDone(obex_object_t *object, int obex_cmd, int obex_rsp); + void handleObexContinue(obex_object_t *object, int obex_cmd); + + void prepareRequest(); + bool waitForRequestComplete(int timeout); + void addConnHeader(obex_object_t *object) const; + bool sendContinue(int obex_cmd); static QString toPenSerialSegment(quint32 id, int len); static quint32 fromPenSerialSegment(const QString &s); static QByteArray encodeUtf16(const QString &s); - void addConnHeader(obex_object_t *object) const; + private: obex_t * _obex; quint32 _connId; QByteArray _inBuf; + bool _reqComplete; + uint _continueReceived; }; #endif // SMARTPEN_H diff --git a/smartpenmanager.cc b/smartpenmanager.cc index 0ac56e2..1eeb685 100644 --- a/smartpenmanager.cc +++ b/smartpenmanager.cc @@ -21,17 +21,30 @@ #include "smartpenmanager.h" +// Sync with a connected pen every 120 seconds +#define SYNC_INTERVAL 120 +// If the previous sync failed, try in 5 seconds +#define FAILED_SYNC_INTERVAL 5 +#define MAX_SYNC_RETRIES 3 + SmartpenManager::SmartpenManager(QObject *parent) : QObject(parent), _udev(udev_new()), _monitor(udev_monitor_new_from_netlink(_udev, "udev")), - _notifier(new QSocketNotifier(udev_monitor_get_fd(_monitor), QSocketNotifier::Read)) + _notifier(new QSocketNotifier(udev_monitor_get_fd(_monitor), QSocketNotifier::Read)), + _nextTry(new QTimer(this)) { - udev_monitor_filter_add_match_tag(_monitor, "livescribe-pen"); - connect(_notifier, SIGNAL(activated(int)), SLOT(handleMonitorActivity())); + connect(_nextTry, SIGNAL(timeout()), SLOT(handleTimerNextTry())); + _nextTry->setSingleShot(true); + _nextTry->setTimerType(Qt::VeryCoarseTimer); + + // Start udev monitoring + udev_monitor_filter_add_match_subsystem_devtype(_monitor, "usb", "usb_device"); + udev_monitor_filter_add_match_tag(_monitor, "livescribe-pen"); udev_monitor_enable_receiving(_monitor); udev_enumerate *scan = udev_enumerate_new(_udev); + udev_enumerate_add_match_subsystem(scan, "usb"); udev_enumerate_add_match_tag(scan, "livescribe-pen"); if (udev_enumerate_scan_devices(scan) == 0) { @@ -39,7 +52,7 @@ SmartpenManager::SmartpenManager(QObject *parent) udev_list_entry_foreach(i, l) { const char *path = udev_list_entry_get_name(i); udev_device *dev = udev_device_new_from_syspath(_udev, path); - processDevice(dev); + processDeviceAdded(dev); udev_device_unref(dev); } } else { @@ -56,18 +69,26 @@ SmartpenManager::~SmartpenManager() udev_unref(_udev); } +QStringList SmartpenManager::pensConnected() const +{ + QStringList pens; + pens.reserve(_pens.size()); + foreach (const PenInfo &pen, _pens) { + if (pen.connected) { + pens.append(pen.name); + } + } + return pens; +} + QStringList SmartpenManager::pensBeingSynchronized() const { QStringList pens; - pens.reserve(_syncers.size()); - for (QMap::const_iterator it = _syncers.begin(); - it != _syncers.end(); ++it) { - QString name = it.value()->penName(); - if (name.isEmpty()) { - Smartpen::Address addr = it.value()->penAddress(); - name = QString("%1-%2").arg(addr.first).arg(addr.second); + pens.reserve(_pens.size()); + foreach (const PenInfo &pen, _pens) { + if (pen.syncer && pen.syncer->isRunning()) { + pens.append(pen.name); } - pens.append(name); } return pens; } @@ -78,8 +99,11 @@ void SmartpenManager::handleMonitorActivity() udev_device *dev = udev_monitor_receive_device(_monitor); if (dev) { const char *action = udev_device_get_action(dev); - if (action && strcmp(action, "add") == 0) { - processDevice(dev); + Q_ASSERT(action); + if (strcmp(action, "add") == 0) { + processDeviceAdded(dev); + } else if (strcmp(action, "remove") == 0) { + processDeviceRemoved(dev); } udev_device_unref(dev); } @@ -92,31 +116,212 @@ void SmartpenManager::handleSyncerFinished() qDebug() << "Finished synchronization with pen with address:" << addr; - _syncers.remove(addr); - emit pensBeingSynchronizedChanged(); + Q_ASSERT(_pens.contains(addr)); + + PenInfo &pen = _pens[addr]; + + bool failed = syncer->hasErrors(); + bool disconnected = false; - if (syncer->hasErrors()) { + if (failed) { qWarning() << "Synchronization with address" << addr << "failed"; - emit syncFailed(syncer->penName()); + emit syncFailed(pen.name); + if (pen.connected) { + if (pen.numRetries < MAX_SYNC_RETRIES) { + // Try resetting USB device first + if (Smartpen::reset(addr)) { + pen.numRetries++; + pen.nextTry = QDateTime::currentDateTimeUtc().addSecs(FAILED_SYNC_INTERVAL); + } else { + qWarning() << "Failed to reset; assuming disconnected"; + pen.connected = false; + disconnected = true; + } + } else { + qWarning() << "Too many failures; assuming disconnected"; + pen.connected = false; + disconnected = true; + } + } } else { - emit syncComplete(syncer->penName()); + emit syncComplete(pen.name); + if (pen.connected) { + pen.numRetries = 0; + pen.nextTry = QDateTime::currentDateTimeUtc().addSecs(SYNC_INTERVAL); + } + } + + pen.syncer->deleteLater(); + pen.syncer = 0; + + emit pensBeingSynchronizedChanged(); + if (disconnected) + emit pensConnectedChanged(); + + scheduleNextTry(); +} + +void SmartpenManager::handleGotPenName(const QString &name) +{ + SmartpenSyncer *syncer = static_cast(sender()); + Smartpen::Address addr = syncer->penAddress(); + + Q_ASSERT(_pens.contains(addr)); + + PenInfo &pen = _pens[addr]; + pen.name = name; + + if (pen.connected) { + emit pensConnectedChanged(); } + if (pen.syncer) { + emit pensBeingSynchronizedChanged(); + } +} + +void SmartpenManager::handleTimerNextTry() +{ + QDateTime now = QDateTime::currentDateTimeUtc().addSecs(1); - syncer->deleteLater(); + foreach (const PenInfo &pen, _pens) { + // Not connected or already syncing: ignore + if (!pen.connected || pen.syncer) continue; + + if (now >= pen.nextTry) { + trySync(pen.addr); + } + } + + scheduleNextTry(); } -void SmartpenManager::processDevice(udev_device *dev) +void SmartpenManager::processDeviceAdded(udev_device *dev) { uint busnum = atol(udev_device_get_sysattr_value(dev, "busnum")); uint devnum = atol(udev_device_get_sysattr_value(dev, "devnum")); + Smartpen::Address addr(busnum, devnum); + QString name = parseUdevEscapedString(udev_device_get_property_value(dev, "ID_MODEL_ENC")); + + qDebug() << "Found smartpen with address:" << addr << name; + + PenInfo &pen = _pens[addr]; + pen.addr = addr; + if (pen.name.isEmpty()) pen.name = name; + pen.connected = true; + + // Schedule an attempt to sync in 1 sec + if (!pen.syncer) { + pen.numRetries = 0; + pen.nextTry = QDateTime::currentDateTimeUtc().addSecs(1); + + scheduleNextTry(); + } + + emit pensConnectedChanged(); +} + +void SmartpenManager::processDeviceRemoved(udev_device *dev) +{ + uint busnum = atol(udev_device_get_sysattr_value(dev, "busnum")); + uint devnum = atol(udev_device_get_sysattr_value(dev, "devnum")); Smartpen::Address addr(busnum, devnum); - if (!_syncers.contains(addr)) { - SmartpenSyncer *syncer = new SmartpenSyncer(addr, this); - _syncers.insert(addr, syncer); - connect(syncer, SIGNAL(finished()), SLOT(handleSyncerFinished())); - connect(syncer, SIGNAL(penNameChanged()), SIGNAL(pensBeingSynchronizedChanged())); - syncer->start(); - emit pensBeingSynchronizedChanged(); + + qDebug() << "Device removed with address:" << addr; + + if (_pens.contains(addr)) { + PenInfo &pen = _pens[addr]; + if (pen.syncer) { + pen.syncer->abort(); + } + pen.connected = false; + emit pensConnectedChanged(); + } +} + +void SmartpenManager::trySync(const Smartpen::Address &addr) +{ + Q_ASSERT(_pens.contains(addr)); + PenInfo &pen = _pens[addr]; + + qDebug() << "Starting sync for" << pen.name; + + Q_ASSERT(pen.addr == addr); + Q_ASSERT(pen.connected); + Q_ASSERT(!pen.syncer); + + pen.syncer = new SmartpenSyncer(addr, this); + connect(pen.syncer, SIGNAL(finished()), SLOT(handleSyncerFinished())); + connect(pen.syncer, SIGNAL(gotPenName(QString)), SLOT(handleGotPenName(QString))); + pen.syncer->start(); + + emit pensBeingSynchronizedChanged(); +} + +void SmartpenManager::scheduleNextTry() +{ + QDateTime nearest; + foreach (const PenInfo &pen, _pens) { + // Not connected or already syncing: ignore + if (!pen.connected || pen.syncer) continue; + + if (nearest.isNull() || pen.nextTry < nearest) { + nearest = pen.nextTry; + } + } + if (nearest.isValid()) { + qint64 msecs = QDateTime::currentDateTimeUtc().msecsTo(nearest); + qDebug() << "Sleeping for" << msecs << "ms"; + _nextTry->start(msecs); + } else { + qDebug() << "Nothing else to do"; + _nextTry->stop(); + } +} + +QString SmartpenManager::parseUdevEscapedString(const char *s) +{ + // This just tries to parse \xddd C-style escape sequences + // Likely broken, see if there's a generic way to do this + const int l = strlen(s); + + QString r; + r.reserve(l); + + int prev = 0; + const char *bs; + while (prev < l && (bs = strchr(s + prev, '\\')) != NULL) { + int pos = bs - s; + r.append(QString::fromLatin1(s + prev, pos - prev)); + if (pos + 1 >= l || s[pos + 1] == '\\') { + r.append('\\'); + pos += 2; + } else { + char c = s[pos + 1]; + if (c == 'x') { + int start = pos + 2; + int i = start; + for ( ; i < l; ++i) { + c = s[i]; + if (s[i] >= '0' && s[i] <= '9') continue; + if (s[i] >= 'A' && s[i] <= 'F') continue; + if (s[i] >= 'a' && s[i] <= 'f') continue; + break; + } + bool ok = false; + uint charNum = QString::fromLatin1(s + start, i - start).toUInt(&ok, 16); + if (ok) { + r.append(QChar::fromLatin1(charNum)); + } else { + r.append(QString::fromLatin1(s + pos, i - pos)); + } + pos = i; + } + } + prev = pos; + } + if (prev < l) { + r.append(QString::fromLatin1(s + prev)); } + return r; } diff --git a/smartpenmanager.h b/smartpenmanager.h index 42bdc06..a823709 100644 --- a/smartpenmanager.h +++ b/smartpenmanager.h @@ -19,10 +19,11 @@ #ifndef SMARTPENMANAGER_H #define SMARTPENMANAGER_H -#include -#include #include +#include #include +#include +#include #include "smartpensyncer.h" struct udev; @@ -37,28 +38,46 @@ public: explicit SmartpenManager(QObject *parent = 0); ~SmartpenManager(); + QStringList pensConnected() const; QStringList pensBeingSynchronized() const; signals: void syncComplete(const QString &penName); void syncFailed(const QString &penName); + void pensConnectedChanged(); void pensBeingSynchronizedChanged(); -public slots: - private slots: void handleMonitorActivity(); void handleSyncerFinished(); + void handleGotPenName(const QString &name); + void handleTimerNextTry(); private: - void processDevice(udev_device *dev); + void processDeviceAdded(udev_device *dev); + void processDeviceRemoved(udev_device *dev); + + void trySync(const Smartpen::Address &addr); + void scheduleNextTry(); + + static QString parseUdevEscapedString(const char *s); private: udev *_udev; udev_monitor *_monitor; QSocketNotifier *_notifier; - QMap _syncers; + struct PenInfo { + Smartpen::Address addr; + bool connected = false; + QString name; + SmartpenSyncer *syncer = 0; + QDateTime nextTry; + uint numRetries = 0; + }; + + QMap _pens; + QTimer *_nextTry; }; #endif // SMARTPENMANAGER_H diff --git a/smartpensyncer.cc b/smartpensyncer.cc index f076ff9..8047f4e 100644 --- a/smartpensyncer.cc +++ b/smartpensyncer.cc @@ -20,7 +20,7 @@ #include #include #include -#include +#include #include #include "paperreplay.h" #include "notebookmodel.h" @@ -79,7 +79,7 @@ bool LockFile::lock() QFileInfo info(_path); if (info.exists()) { - if (info.created().secsTo(QDateTime::currentDateTime()) > 10 * 60) { + if (info.lastModified().secsTo(QDateTime::currentDateTime()) > 10 * 60) { if (QFile::remove(info.filePath())) { qDebug() << "Removing stale lock file:" << info.absoluteFilePath(); } @@ -106,7 +106,7 @@ Smartpen::PenTime TimestampFile::get() { if (_f.exists()) { if (_f.open(QIODevice::ReadOnly | QIODevice::Text)) { - QString data = QString::fromAscii(_f.readLine(24)); + QString data = QString::fromUtf8(_f.readLine(32)); _f.close(); return data.toLongLong(); } else { @@ -120,18 +120,21 @@ Smartpen::PenTime TimestampFile::get() void TimestampFile::set(Smartpen::PenTime time) { if (_f.open(QIODevice::WriteOnly | QIODevice::Truncate)) { - _f.write(QString::number(time).toAscii()); + _f.write(QString::number(time).toUtf8()); _f.close(); } else { qWarning() << "Could not set timestamp file:" << _f.fileName(); return; } } -} +} /* anonymous namespace */ SmartpenSyncer::SmartpenSyncer(const Smartpen::Address &addr, QObject *parent) : - QThread(parent), _addr(addr), _pen(new Smartpen(this)), _errored(false), _aborted(false) + QThread(parent), + _addr(addr), _errored(false), _aborted(false), + _pen(new Smartpen(this)) { + connect(_pen, SIGNAL(linkError(QString)), SLOT(handleLinkError(QString))); } SmartpenSyncer::~SmartpenSyncer() @@ -147,11 +150,6 @@ Smartpen::Address SmartpenSyncer::penAddress() const return _addr; } -QString SmartpenSyncer::penName() const -{ - return _penName; -} - bool SmartpenSyncer::hasErrors() const { return _errored; @@ -162,6 +160,12 @@ void SmartpenSyncer::abort() _aborted = true; } +void SmartpenSyncer::reset() +{ + _aborted = false; + _errored = false; +} + void SmartpenSyncer::run() { if (!_pen->connectToPen(_addr)) { @@ -170,19 +174,23 @@ void SmartpenSyncer::run() return; } + if (_aborted) { + _pen->disconnectFromPen(); + return; + } + _penName = _pen->getPenName(); qDebug() << "got pen name:" << _penName; - emit penNameChanged(); + emit gotPenName(_penName); QVariantMap penInfo = _pen->getPenInfo(); if (penInfo.isEmpty()) { qWarning() << "Could not get pen info"; _errored = true; + _pen->disconnectFromPen(); return; } - _penSerial = penInfo["penserial"].toString(); - _penDataDir.setPath(NotebookModel::userDataDirectory() + "/" + _penName + ".pen"); if (!_penDataDir.exists()) { if (!_penDataDir.mkpath(".")) { @@ -194,6 +202,12 @@ void SmartpenSyncer::run() _errored = true; } + if (_errored) { + qDebug() << "Sync finished with errors"; + } else { + qDebug() << "Sync finished"; + } + _pen->disconnectFromPen(); } @@ -363,3 +377,9 @@ bool SmartpenSyncer::extractZip(QByteArray &zipData, QDir &dir) return false; } } + +void SmartpenSyncer::handleLinkError(const QString &msg) +{ + qWarning() << "link error:" << msg; + _errored = true; +} diff --git a/smartpensyncer.h b/smartpensyncer.h index ea31917..9dae5c0 100644 --- a/smartpensyncer.h +++ b/smartpensyncer.h @@ -19,6 +19,7 @@ #ifndef SMARTPENSYNCER_H #define SMARTPENSYNCER_H +#include #include #include #include "smartpen.h" @@ -26,20 +27,20 @@ class SmartpenSyncer : public QThread { Q_OBJECT + public: explicit SmartpenSyncer(const Smartpen::Address &addr, QObject *parent = 0); ~SmartpenSyncer(); Smartpen::Address penAddress() const; - QString penName() const; - bool hasErrors() const; signals: - void penNameChanged(); + void gotPenName(const QString &name); public slots: void abort(); + void reset(); private: void run(); @@ -48,13 +49,16 @@ private: bool syncPaperReplay(Smartpen::PenTime lastSync, const Smartpen::ChangeReport &change); bool extractZip(QByteArray &zipData, QDir &dir); +private slots: + void handleLinkError(const QString &msg); + private: - Smartpen::Address _addr; - Smartpen *_pen; - bool _errored; - bool _aborted; + const Smartpen::Address _addr; + std::atomic _errored; + std::atomic _aborted; - QString _penSerial; + // To be used only from this thread + Smartpen *_pen; QString _penName; QDir _penDataDir; }; diff --git a/stfgraphicsitem.h b/stfgraphicsitem.h index 6f7038a..84c4d71 100644 --- a/stfgraphicsitem.h +++ b/stfgraphicsitem.h @@ -19,7 +19,7 @@ #ifndef STFGRAPHICSITEM_H #define STFGRAPHICSITEM_H -#include +#include #include "paperreplay.h" class StfGraphicsItem : public QGraphicsItem diff --git a/stfstrokeitem.cc b/stfstrokeitem.cc index e2a7714..dbc7ca0 100644 --- a/stfstrokeitem.cc +++ b/stfstrokeitem.cc @@ -19,8 +19,8 @@ #include #include #include -#include -#include +#include +#include #include "notebookview.h" #include "stfstrokeitem.h" @@ -28,10 +28,12 @@ StfStrokeItem::StfStrokeItem(const QPainterPath &stroke, const PaperReplay::Sess : QGraphicsPathItem(stroke, parent), _session(session), _startTime(startTime), _endTime(endTime) { + QPen pen(Qt::black, 8.0f); if (_session.isValid()) { - setPen(QPen(Qt::darkGreen)); + pen.setColor(Qt::darkGreen); setCursor(Qt::PointingHandCursor); } + setPen(pen); } int StfStrokeItem::type() const diff --git a/stfstrokeitem.h b/stfstrokeitem.h index 5825c10..5e7a253 100644 --- a/stfstrokeitem.h +++ b/stfstrokeitem.h @@ -19,7 +19,7 @@ #ifndef STFSTROKEITEM_H #define STFSTROKEITEM_H -#include +#include #include "paperreplay.h" class StfStrokeItem : public QGraphicsPathItem -- cgit v1.2.3