/* * 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 "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)), _nextTry(new QTimer(this)) { 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) { udev_list_entry *l = udev_enumerate_get_list_entry(scan), *i; 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); processDeviceAdded(dev); udev_device_unref(dev); } } else { qWarning() << "Failed to scan for devices"; } udev_enumerate_unref(scan); } SmartpenManager::~SmartpenManager() { delete _notifier; udev_monitor_unref(_monitor); 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(_pens.size()); foreach (const PenInfo &pen, _pens) { if (pen.syncer && pen.syncer->isRunning()) { pens.append(pen.name); } } return pens; } void SmartpenManager::handleMonitorActivity() { qDebug() << "udev activity"; udev_device *dev = udev_monitor_receive_device(_monitor); if (dev) { const char *action = udev_device_get_action(dev); Q_ASSERT(action); if (strcmp(action, "add") == 0) { processDeviceAdded(dev); } else if (strcmp(action, "remove") == 0) { processDeviceRemoved(dev); } udev_device_unref(dev); } } void SmartpenManager::handleSyncerFinished() { SmartpenSyncer *syncer = static_cast(sender()); Smartpen::Address addr = syncer->penAddress(); qDebug() << "Finished synchronization with pen with address:" << addr; Q_ASSERT(_pens.contains(addr)); PenInfo &pen = _pens[addr]; bool failed = syncer->hasErrors(); bool disconnected = false; if (failed) { qWarning() << "Synchronization with address" << addr << "failed"; 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(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); 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::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); 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; }