LinkManager.cc 35.3 KB
Newer Older
1 2 3 4 5 6 7 8
/****************************************************************************
 *
 *   (c) 2009-2016 QGROUNDCONTROL PROJECT <http://www.qgroundcontrol.org>
 *
 * QGroundControl is licensed according to the terms in the file
 * COPYING.md in the root of the source code directory.
 *
 ****************************************************************************/
lm's avatar
lm committed
9

pixhawk's avatar
pixhawk committed
10 11
#include <QList>
#include <QApplication>
12
#include <QDebug>
13
#include <QSignalSpy>
dogmaphobic's avatar
dogmaphobic committed
14

Gus Grubba's avatar
Gus Grubba committed
15
#ifndef NO_SERIAL_LINK
Don Gagne's avatar
Don Gagne committed
16
#include "QGCSerialPortInfo.h"
dogmaphobic's avatar
dogmaphobic committed
17
#endif
18

19
#include "LinkManager.h"
20
#include "QGCApplication.h"
Don Gagne's avatar
Don Gagne committed
21 22
#include "UDPLink.h"
#include "TCPLink.h"
23
#include "SettingsManager.h"
dogmaphobic's avatar
dogmaphobic committed
24
#ifdef QGC_ENABLE_BLUETOOTH
dogmaphobic's avatar
dogmaphobic committed
25 26
#include "BluetoothLink.h"
#endif
27

Don Gagne's avatar
Don Gagne committed
28
#ifndef __mobile__
DonLakeFlyer's avatar
DonLakeFlyer committed
29
#include "GPSManager.h"
30
#include "PositionManager.h"
Don Gagne's avatar
Don Gagne committed
31 32
#endif

Don Gagne's avatar
Don Gagne committed
33
QGC_LOGGING_CATEGORY(LinkManagerLog, "LinkManagerLog")
Don Gagne's avatar
Don Gagne committed
34 35
QGC_LOGGING_CATEGORY(LinkManagerVerboseLog, "LinkManagerVerboseLog")

Don Gagne's avatar
Don Gagne committed
36
const char* LinkManager::_defaultUPDLinkName =       "UDP Link (AutoConnect)";
37

38 39 40 41 42 43 44 45
const int LinkManager::_autoconnectUpdateTimerMSecs =   1000;
#ifdef Q_OS_WIN
// Have to manually let the bootloader go by on Windows to get a working connect
const int LinkManager::_autoconnectConnectDelayMSecs =  6000;
#else
const int LinkManager::_autoconnectConnectDelayMSecs =  1000;
#endif

46 47
LinkManager::LinkManager(QGCApplication* app, QGCToolbox* toolbox)
    : QGCTool(app, toolbox)
48 49 50
    , _configUpdateSuspended(false)
    , _configurationsLoaded(false)
    , _connectionsSuspended(false)
51
    , _mavlinkChannelsUsedBitMask(1)    // We never use channel 0 to avoid sequence numbering problems
52
    , _autoConnectSettings(NULL)
53
    , _mavlinkProtocol(NULL)
54
#ifndef __mobile__
55
    , _nmeaPort(NULL)
56
#endif
pixhawk's avatar
pixhawk committed
57
{
Don Gagne's avatar
Don Gagne committed
58 59 60 61
    qmlRegisterUncreatableType<LinkManager>         ("QGroundControl", 1, 0, "LinkManager",         "Reference only");
    qmlRegisterUncreatableType<LinkConfiguration>   ("QGroundControl", 1, 0, "LinkConfiguration",   "Reference only");
    qmlRegisterUncreatableType<LinkInterface>       ("QGroundControl", 1, 0, "LinkInterface",       "Reference only");

Gus Grubba's avatar
Gus Grubba committed
62
#ifndef NO_SERIAL_LINK
Don Gagne's avatar
Don Gagne committed
63 64 65
    _activeLinkCheckTimer.setInterval(_activeLinkCheckTimeoutMSecs);
    _activeLinkCheckTimer.setSingleShot(false);
    connect(&_activeLinkCheckTimer, &QTimer::timeout, this, &LinkManager::_activeLinkCheck);
Don Gagne's avatar
Don Gagne committed
66
#endif
pixhawk's avatar
pixhawk committed
67 68 69 70
}

LinkManager::~LinkManager()
{
71
#ifndef __mobile__
72
    delete _nmeaPort;
73
#endif
pixhawk's avatar
pixhawk committed
74 75
}

76 77
void LinkManager::setToolbox(QGCToolbox *toolbox)
{
DonLakeFlyer's avatar
DonLakeFlyer committed
78
    QGCTool::setToolbox(toolbox);
79

DonLakeFlyer's avatar
DonLakeFlyer committed
80 81
    _autoConnectSettings = toolbox->settingsManager()->autoConnectSettings();
    _mavlinkProtocol = _toolbox->mavlinkProtocol();
82

Don Gagne's avatar
Don Gagne committed
83
    connect(&_portListTimer, &QTimer::timeout, this, &LinkManager::_updateAutoConnectLinks);
84
    _portListTimer.start(_autoconnectUpdateTimerMSecs); // timeout must be long enough to get past bootloader on second pass
85

86 87
}

Don Gagne's avatar
Don Gagne committed
88 89 90 91 92 93 94 95 96 97
// This should only be used by Qml code
void LinkManager::createConnectedLink(LinkConfiguration* config)
{
    for(int i = 0; i < _sharedConfigurations.count(); i++) {
        SharedLinkConfigurationPointer& sharedConf = _sharedConfigurations[i];
        if (sharedConf->name() == config->name())
            createConnectedLink(sharedConf);
    }
}

98
LinkInterface* LinkManager::createConnectedLink(SharedLinkConfigurationPointer& config)
99
{
100 101 102 103 104
    if (!config) {
        qWarning() << "LinkManager::createConnectedLink called with NULL config";
        return NULL;
    }

105 106
    LinkInterface* pLink = NULL;
    switch(config->type()) {
Gus Grubba's avatar
Gus Grubba committed
107
#ifndef NO_SERIAL_LINK
DonLakeFlyer's avatar
DonLakeFlyer committed
108 109 110 111 112 113 114 115 116
    case LinkConfiguration::TypeSerial:
    {
        SerialConfiguration* serialConfig = dynamic_cast<SerialConfiguration*>(config.data());
        if (serialConfig) {
            pLink = new SerialLink(config);
            if (serialConfig->usbDirect()) {
                _activeLinkCheckList.append((SerialLink*)pLink);
                if (!_activeLinkCheckTimer.isActive()) {
                    _activeLinkCheckTimer.start();
Don Gagne's avatar
Don Gagne committed
117 118 119
                }
            }
        }
DonLakeFlyer's avatar
DonLakeFlyer committed
120
    }
Don Gagne's avatar
Don Gagne committed
121
        break;
dogmaphobic's avatar
dogmaphobic committed
122
#endif
DonLakeFlyer's avatar
DonLakeFlyer committed
123 124 125 126 127 128
    case LinkConfiguration::TypeUdp:
        pLink = new UDPLink(config);
        break;
    case LinkConfiguration::TypeTcp:
        pLink = new TCPLink(config);
        break;
dogmaphobic's avatar
dogmaphobic committed
129
#ifdef QGC_ENABLE_BLUETOOTH
DonLakeFlyer's avatar
DonLakeFlyer committed
130 131 132
    case LinkConfiguration::TypeBluetooth:
        pLink = new BluetoothLink(config);
        break;
dogmaphobic's avatar
dogmaphobic committed
133
#endif
dogmaphobic's avatar
dogmaphobic committed
134
#ifndef __mobile__
DonLakeFlyer's avatar
DonLakeFlyer committed
135 136 137
    case LinkConfiguration::TypeLogReplay:
        pLink = new LogReplayLink(config);
        break;
dogmaphobic's avatar
dogmaphobic committed
138
#endif
139
#ifdef QT_DEBUG
DonLakeFlyer's avatar
DonLakeFlyer committed
140 141 142
    case LinkConfiguration::TypeMock:
        pLink = new MockLink(config);
        break;
143
#endif
DonLakeFlyer's avatar
DonLakeFlyer committed
144 145 146
    case LinkConfiguration::TypeLast:
    default:
        break;
147
    }
148 149

    if (pLink) {
150 151
        _addLink(pLink);
        connectLink(pLink);
152
    }
153

154 155 156
    return pLink;
}

Don Gagne's avatar
Don Gagne committed
157
LinkInterface* LinkManager::createConnectedLink(const QString& name)
158
{
DonLakeFlyer's avatar
DonLakeFlyer committed
159 160 161 162 163 164 165 166 167
    if (name.isEmpty()) {
        qWarning() << "Internal error";
    } else {
        for(int i = 0; i < _sharedConfigurations.count(); i++) {
            SharedLinkConfigurationPointer& conf = _sharedConfigurations[i];
            if (conf->name() == name) {
                return createConnectedLink(conf);
            }
        }
168 169 170 171
    }
    return NULL;
}

172
void LinkManager::_addLink(LinkInterface* link)
pixhawk's avatar
pixhawk committed
173
{
Don Gagne's avatar
Don Gagne committed
174 175 176 177
    if (thread() != QThread::currentThread()) {
        qWarning() << "_deleteLink called from incorrect thread";
        return;
    }
178

Don Gagne's avatar
Don Gagne committed
179 180 181
    if (!link) {
        return;
    }
182

183
    if (!containsLink(link)) {
184 185 186 187
        int mavlinkChannel = _reserveMavlinkChannel();
        if (mavlinkChannel != 0) {
            link->_setMavlinkChannel(mavlinkChannel);
        } else {
188
            qWarning() << "Ran out of mavlink channels";
189
            return;
190 191
        }

192
        _sharedLinks.append(SharedLinkInterfacePointer(link));
193 194
        emit newLink(link);
    }
195

Don Gagne's avatar
Don Gagne committed
196 197
    connect(link, &LinkInterface::communicationError,   _app,               &QGCApplication::criticalMessageBoxOnMainThread);
    connect(link, &LinkInterface::bytesReceived,        _mavlinkProtocol,   &MAVLinkProtocol::receiveBytes);
198
    connect(link, &LinkInterface::bytesReceived,        this,               &LinkManager::_bytesReceived);
199

200
    _mavlinkProtocol->resetMetadataForLink(link);
201
    _mavlinkProtocol->setVersion(_mavlinkProtocol->getCurrentVersion());
202

203 204 205 206 207 208
    connect(link, &LinkInterface::connected,            this, &LinkManager::_linkConnected);
    connect(link, &LinkInterface::disconnected,         this, &LinkManager::_linkDisconnected);

    // This connection is queued since it will cloe the link. So we want the link emitter to return otherwise we would
    // close the link our from under itself.
    connect(link, &LinkInterface::connectionRemoved,    this, &LinkManager::_linkConnectionRemoved, Qt::QueuedConnection);
209
}
pixhawk's avatar
pixhawk committed
210

Don Gagne's avatar
Don Gagne committed
211
void LinkManager::disconnectAll(void)
pixhawk's avatar
pixhawk committed
212
{
Don Gagne's avatar
Don Gagne committed
213
    // Walk list in reverse order to preserve indices during delete
214 215
    for (int i=_sharedLinks.count()-1; i>=0; i--) {
        disconnectLink(_sharedLinks[i].data());
216
    }
pixhawk's avatar
pixhawk committed
217 218 219 220
}

bool LinkManager::connectLink(LinkInterface* link)
{
DonLakeFlyer's avatar
DonLakeFlyer committed
221 222 223 224 225 226 227
    if (link) {
        if (_connectionsSuspendedMsg()) {
            return false;
        }
        return link->_connect();
    } else {
        qWarning() << "Internal error";
228 229
        return false;
    }
pixhawk's avatar
pixhawk committed
230 231
}

Don Gagne's avatar
Don Gagne committed
232
void LinkManager::disconnectLink(LinkInterface* link)
pixhawk's avatar
pixhawk committed
233
{
234
    if (!link || !containsLink(link)) {
235 236
        return;
    }
Don Gagne's avatar
Don Gagne committed
237

Don Gagne's avatar
Don Gagne committed
238
    link->_disconnect();
239

Don Gagne's avatar
Don Gagne committed
240
    LinkConfiguration* config = link->getLinkConfiguration();
241 242 243 244 245
    for (int i=0; i<_sharedAutoconnectConfigurations.count(); i++) {
        if (_sharedAutoconnectConfigurations[i].data() == config) {
            qCDebug(LinkManagerLog) << "Removing disconnected autoconnect config" << config->name();
            _sharedAutoconnectConfigurations.removeAt(i);
            break;
246
        }
247
    }
248

Don Gagne's avatar
Don Gagne committed
249
    _deleteLink(link);
pixhawk's avatar
pixhawk committed
250 251
}

252
void LinkManager::_deleteLink(LinkInterface* link)
253
{
Don Gagne's avatar
Don Gagne committed
254 255 256 257 258 259 260 261
    if (thread() != QThread::currentThread()) {
        qWarning() << "_deleteLink called from incorrect thread";
        return;
    }

    if (!link) {
        return;
    }
262

263
    // Free up the mavlink channel associated with this link
264
    _freeMavlinkChannel(link->mavlinkChannel());
265

266 267 268 269 270 271
    for (int i=0; i<_sharedLinks.count(); i++) {
        if (_sharedLinks[i].data() == link) {
            _sharedLinks.removeAt(i);
            break;
        }
    }
272

Don Gagne's avatar
Don Gagne committed
273
    // Emit removal of link
274
    emit linkDeleted(link);
pixhawk's avatar
pixhawk committed
275 276
}

277 278 279 280 281 282 283 284 285 286 287 288
SharedLinkInterfacePointer LinkManager::sharedLinkInterfacePointerForLink(LinkInterface* link)
{
    for (int i=0; i<_sharedLinks.count(); i++) {
        if (_sharedLinks[i].data() == link) {
            return _sharedLinks[i];
        }
    }

    qWarning() << "LinkManager::sharedLinkInterfaceForLink returning NULL";
    return SharedLinkInterfacePointer(NULL);
}

289 290 291 292 293
/// @brief If all new connections should be suspended a message is displayed to the user and true
///         is returned;
bool LinkManager::_connectionsSuspendedMsg(void)
{
    if (_connectionsSuspended) {
294
        qgcApp()->showMessage(tr("Connect not allowed: %1").arg(_connectionsSuspendedReason));
295 296 297 298 299 300 301 302 303 304 305
        return true;
    } else {
        return false;
    }
}

void LinkManager::setConnectionsSuspended(QString reason)
{
    _connectionsSuspended = true;
    _connectionsSuspendedReason = reason;
}
306

307 308 309 310 311 312 313 314 315
void LinkManager::_linkConnected(void)
{
    emit linkConnected((LinkInterface*)sender());
}

void LinkManager::_linkDisconnected(void)
{
    emit linkDisconnected((LinkInterface*)sender());
}
316

317 318 319 320 321 322
void LinkManager::_linkConnectionRemoved(LinkInterface* link)
{
    // Link has been removed from system, disconnect it automatically
    disconnectLink(link);
}

323 324 325 326 327 328 329 330 331
void LinkManager::suspendConfigurationUpdates(bool suspend)
{
    _configUpdateSuspended = suspend;
}

void LinkManager::saveLinkConfigurationList()
{
    QSettings settings;
    settings.remove(LinkConfiguration::settingsRoot());
332
    int trueCount = 0;
333 334
    for (int i = 0; i < _sharedConfigurations.count(); i++) {
        SharedLinkConfigurationPointer linkConfig = _sharedConfigurations[i];
Don Gagne's avatar
Don Gagne committed
335
        if (linkConfig) {
336
            if (!linkConfig->isDynamic()) {
Don Gagne's avatar
Don Gagne committed
337
                QString root = LinkConfiguration::settingsRoot();
338
                root += QString("/Link%1").arg(trueCount++);
Don Gagne's avatar
Don Gagne committed
339 340
                settings.setValue(root + "/name", linkConfig->name());
                settings.setValue(root + "/type", linkConfig->type());
341
                settings.setValue(root + "/auto", linkConfig->isAutoConnect());
342
                settings.setValue(root + "/high_latency", linkConfig->isHighLatency());
Don Gagne's avatar
Don Gagne committed
343 344 345 346
                // Have the instance save its own values
                linkConfig->saveSettings(settings, root);
            }
        } else {
347
            qWarning() << "Internal error for link configuration in LinkManager";
dogmaphobic's avatar
dogmaphobic committed
348
        }
349
    }
dogmaphobic's avatar
dogmaphobic committed
350
    QString root(LinkConfiguration::settingsRoot());
351 352
    settings.setValue(root + "/count", trueCount);
    emit linkConfigurationsChanged();
353 354 355 356
}

void LinkManager::loadLinkConfigurationList()
{
357
    bool linksChanged = false;
358 359 360 361 362 363 364 365 366 367
    QSettings settings;
    // Is the group even there?
    if(settings.contains(LinkConfiguration::settingsRoot() + "/count")) {
        // Find out how many configurations we have
        int count = settings.value(LinkConfiguration::settingsRoot() + "/count").toInt();
        for(int i = 0; i < count; i++) {
            QString root(LinkConfiguration::settingsRoot());
            root += QString("/Link%1").arg(i);
            if(settings.contains(root + "/type")) {
                int type = settings.value(root + "/type").toInt();
368
                if((LinkConfiguration::LinkType)type < LinkConfiguration::TypeLast) {
369 370 371 372
                    if(settings.contains(root + "/name")) {
                        QString name = settings.value(root + "/name").toString();
                        if(!name.isEmpty()) {
                            LinkConfiguration* pLink = NULL;
373
                            bool autoConnect = settings.value(root + "/auto").toBool();
374
                            bool highLatency = settings.value(root + "/high_latency").toBool();
375
                            switch((LinkConfiguration::LinkType)type) {
Gus Grubba's avatar
Gus Grubba committed
376
#ifndef NO_SERIAL_LINK
DonLakeFlyer's avatar
DonLakeFlyer committed
377 378 379
                            case LinkConfiguration::TypeSerial:
                                pLink = (LinkConfiguration*)new SerialConfiguration(name);
                                break;
dogmaphobic's avatar
dogmaphobic committed
380
#endif
DonLakeFlyer's avatar
DonLakeFlyer committed
381 382 383 384 385 386
                            case LinkConfiguration::TypeUdp:
                                pLink = (LinkConfiguration*)new UDPConfiguration(name);
                                break;
                            case LinkConfiguration::TypeTcp:
                                pLink = (LinkConfiguration*)new TCPConfiguration(name);
                                break;
dogmaphobic's avatar
dogmaphobic committed
387
#ifdef QGC_ENABLE_BLUETOOTH
DonLakeFlyer's avatar
DonLakeFlyer committed
388 389 390
                            case LinkConfiguration::TypeBluetooth:
                                pLink = (LinkConfiguration*)new BluetoothConfiguration(name);
                                break;
dogmaphobic's avatar
dogmaphobic committed
391
#endif
dogmaphobic's avatar
dogmaphobic committed
392
#ifndef __mobile__
DonLakeFlyer's avatar
DonLakeFlyer committed
393 394 395
                            case LinkConfiguration::TypeLogReplay:
                                pLink = (LinkConfiguration*)new LogReplayLinkConfiguration(name);
                                break;
dogmaphobic's avatar
dogmaphobic committed
396
#endif
397
#ifdef QT_DEBUG
DonLakeFlyer's avatar
DonLakeFlyer committed
398 399 400
                            case LinkConfiguration::TypeMock:
                                pLink = (LinkConfiguration*)new MockConfiguration(name);
                                break;
401
#endif
DonLakeFlyer's avatar
DonLakeFlyer committed
402 403 404
                            default:
                            case LinkConfiguration::TypeLast:
                                break;
405 406
                            }
                            if(pLink) {
407 408
                                //-- Have the instance load its own values
                                pLink->setAutoConnect(autoConnect);
409
                                pLink->setHighLatency(highLatency);
410
                                pLink->loadSettings(settings, root);
411
                                addConfiguration(pLink);
412
                                linksChanged = true;
413 414
                            }
                        } else {
415
                            qWarning() << "Link Configuration" << root << "has an empty name." ;
416 417
                        }
                    } else {
418
                        qWarning() << "Link Configuration" << root << "has no name." ;
419 420
                    }
                } else {
421
                    qWarning() << "Link Configuration" << root << "an invalid type: " << type;
422 423
                }
            } else {
424
                qWarning() << "Link Configuration" << root << "has no type." ;
425 426 427
            }
        }
    }
428 429

    if(linksChanged) {
430
        emit linkConfigurationsChanged();
431 432
    }
    // Enable automatic Serial PX4/3DR Radio hunting
433 434 435
    _configurationsLoaded = true;
}

Gus Grubba's avatar
Gus Grubba committed
436
#ifndef NO_SERIAL_LINK
Don Gagne's avatar
Don Gagne committed
437
SerialConfiguration* LinkManager::_autoconnectConfigurationsContainsPort(const QString& portName)
438 439
{
    QString searchPort = portName.trimmed();
Don Gagne's avatar
Don Gagne committed
440

441 442
    for (int i=0; i<_sharedAutoconnectConfigurations.count(); i++) {
        SerialConfiguration* serialConfig = qobject_cast<SerialConfiguration*>(_sharedAutoconnectConfigurations[i].data());
Don Gagne's avatar
Don Gagne committed
443

444 445 446
        if (serialConfig) {
            if (serialConfig->portName() == searchPort) {
                return serialConfig;
447
            }
Don Gagne's avatar
Don Gagne committed
448 449
        } else {
            qWarning() << "Internal error";
450 451 452 453
        }
    }
    return NULL;
}
dogmaphobic's avatar
dogmaphobic committed
454
#endif
455

Don Gagne's avatar
Don Gagne committed
456
void LinkManager::_updateAutoConnectLinks(void)
457
{
Don Gagne's avatar
Don Gagne committed
458
    if (_connectionsSuspended || qgcApp()->runningUnitTests()) {
459 460
        return;
    }
Don Gagne's avatar
Don Gagne committed
461

Don Gagne's avatar
Don Gagne committed
462 463
    // Re-add UDP if we need to
    bool foundUDP = false;
464 465
    for (int i=0; i<_sharedLinks.count(); i++) {
        LinkConfiguration* linkConfig = _sharedLinks[i]->getLinkConfiguration();
Don Gagne's avatar
Don Gagne committed
466 467 468 469 470
        if (linkConfig->type() == LinkConfiguration::TypeUdp && linkConfig->name() == _defaultUPDLinkName) {
            foundUDP = true;
            break;
        }
    }
471
    if (!foundUDP && _autoConnectSettings->autoConnectUDP()->rawValue().toBool()) {
Don Gagne's avatar
Don Gagne committed
472
        qCDebug(LinkManagerLog) << "New auto-connect UDP port added";
473
        // Default UDPConfiguration is set up for autoconnect
Don Gagne's avatar
Don Gagne committed
474
        UDPConfiguration* udpConfig = new UDPConfiguration(_defaultUPDLinkName);
DonLakeFlyer's avatar
DonLakeFlyer committed
475
        udpConfig->setDynamic(true);
476 477
        SharedLinkConfigurationPointer config = addConfiguration(udpConfig);
        createConnectedLink(config);
478
        emit linkConfigurationsChanged();
Don Gagne's avatar
Don Gagne committed
479 480
    }

Gus Grubba's avatar
Gus Grubba committed
481
#ifndef NO_SERIAL_LINK
dogmaphobic's avatar
dogmaphobic committed
482
    QStringList currentPorts;
483 484 485 486 487 488
    QList<QGCSerialPortInfo> portList;

#ifdef __android__
    // Android builds only support a single serial connection. Repeatedly calling availablePorts after that one serial
    // port is connected leaks file handles due to a bug somewhere in android serial code. In order to work around that
    // bug after we connect the first serial port we stop probing for additional ports.
489
    if (!_sharedAutoconnectConfigurations.count()) {
490 491
        portList = QGCSerialPortInfo::availablePorts();
    }
492 493
#else
    portList = QGCSerialPortInfo::availablePorts();
494
#endif
Don Gagne's avatar
Don Gagne committed
495

496
    // Iterate Comm Ports
Don Gagne's avatar
Don Gagne committed
497
    foreach (QGCSerialPortInfo portInfo, portList) {
Don Gagne's avatar
Don Gagne committed
498 499 500 501 502 503 504 505 506
        qCDebug(LinkManagerVerboseLog) << "-----------------------------------------------------";
        qCDebug(LinkManagerVerboseLog) << "portName:          " << portInfo.portName();
        qCDebug(LinkManagerVerboseLog) << "systemLocation:    " << portInfo.systemLocation();
        qCDebug(LinkManagerVerboseLog) << "description:       " << portInfo.description();
        qCDebug(LinkManagerVerboseLog) << "manufacturer:      " << portInfo.manufacturer();
        qCDebug(LinkManagerVerboseLog) << "serialNumber:      " << portInfo.serialNumber();
        qCDebug(LinkManagerVerboseLog) << "vendorIdentifier:  " << portInfo.vendorIdentifier();
        qCDebug(LinkManagerVerboseLog) << "productIdentifier: " << portInfo.productIdentifier();

dogmaphobic's avatar
dogmaphobic committed
507 508
        // Save port name
        currentPorts << portInfo.systemLocation();
Don Gagne's avatar
Don Gagne committed
509

510 511
        QGCSerialPortInfo::BoardType_t boardType;
        QString boardName;
Don Gagne's avatar
Don Gagne committed
512

513
#ifndef __mobile__
514 515 516 517 518 519 520 521 522 523 524 525
        if (portInfo.systemLocation().trimmed() == _autoConnectSettings->autoConnectNmeaPort()->cookedValueString()) {
            if (portInfo.systemLocation().trimmed() != _nmeaDeviceName) {
                _nmeaDeviceName = portInfo.systemLocation().trimmed();
                qCDebug(LinkManagerLog) << "Configuring nmea port" << _nmeaDeviceName;
                QSerialPort* newPort = new QSerialPort(portInfo);

                _nmeaBaud = _autoConnectSettings->autoConnectNmeaBaud()->cookedValue().toUInt();
                newPort->setBaudRate(_nmeaBaud);
                qCDebug(LinkManagerLog) << "Configuring nmea baudrate" << _nmeaBaud;

                // This will stop polling old device if previously set
                _toolbox->qgcPositionManager()->setNmeaSourceDevice(newPort);
526

527 528 529 530 531 532 533 534 535 536
                if (_nmeaPort) {
                    delete _nmeaPort;
                }
                _nmeaPort = newPort;

            } else if (_autoConnectSettings->autoConnectNmeaBaud()->cookedValue().toUInt() != _nmeaBaud) {
                _nmeaBaud = _autoConnectSettings->autoConnectNmeaBaud()->cookedValue().toUInt();
                _nmeaPort->setBaudRate(_nmeaBaud);
                qCDebug(LinkManagerLog) << "Configuring nmea baudrate" << _nmeaBaud;
            }
537 538 539
        } else
#endif
        if (portInfo.getBoardInfo(boardType, boardName)) {
Don Gagne's avatar
Don Gagne committed
540 541
            if (portInfo.isBootloader()) {
                // Don't connect to bootloader
542
                qCDebug(LinkManagerLog) << "Waiting for bootloader to finish" << portInfo.systemLocation();
Don Gagne's avatar
Don Gagne committed
543 544
                continue;
            }
545

546
            if (_autoconnectConfigurationsContainsPort(portInfo.systemLocation()) || _autoConnectRTKPort == portInfo.systemLocation()) {
547 548 549 550 551 552
                qCDebug(LinkManagerVerboseLog) << "Skipping existing autoconnect" << portInfo.systemLocation();
            } else if (!_autoconnectWaitList.contains(portInfo.systemLocation())) {
                // We don't connect to the port the first time we see it. The ability to correctly detect whether we
                // are in the bootloader is flaky from a cross-platform standpoint. So by putting it on a wait list
                // and only connect on the second pass we leave enough time for the board to boot up.
                qCDebug(LinkManagerLog) << "Waiting for next autoconnect pass" << portInfo.systemLocation();
553 554
                _autoconnectWaitList[portInfo.systemLocation()] = 1;
            } else if (++_autoconnectWaitList[portInfo.systemLocation()] * _autoconnectUpdateTimerMSecs > _autoconnectConnectDelayMSecs) {
Don Gagne's avatar
Don Gagne committed
555 556
                SerialConfiguration* pSerialConfig = NULL;

557
                _autoconnectWaitList.remove(portInfo.systemLocation());
558

Don Gagne's avatar
Don Gagne committed
559
                switch (boardType) {
560
                case QGCSerialPortInfo::BoardTypePixhawk:
561
                    if (_autoConnectSettings->autoConnectPixhawk()->rawValue().toBool()) {
562
                        pSerialConfig = new SerialConfiguration(tr("%1 on %2 (AutoConnect)").arg(boardName).arg(portInfo.portName().trimmed()));
563 564 565
                        pSerialConfig->setUsbDirect(true);
                    }
                    break;
Don Gagne's avatar
Don Gagne committed
566
                case QGCSerialPortInfo::BoardTypePX4Flow:
567
                    if (_autoConnectSettings->autoConnectPX4Flow()->rawValue().toBool()) {
568
                        pSerialConfig = new SerialConfiguration(tr("%1 on %2 (AutoConnect)").arg(boardName).arg(portInfo.portName().trimmed()));
Don Gagne's avatar
Don Gagne committed
569
                    }
Don Gagne's avatar
Don Gagne committed
570
                    break;
571
                case QGCSerialPortInfo::BoardTypeSiKRadio:
572
                    if (_autoConnectSettings->autoConnectSiKRadio()->rawValue().toBool()) {
573
                        pSerialConfig = new SerialConfiguration(tr("%1 on %2 (AutoConnect)").arg(boardName).arg(portInfo.portName().trimmed()));
Don Gagne's avatar
Don Gagne committed
574 575
                    }
                    break;
576
                case QGCSerialPortInfo::BoardTypeOpenPilot:
577
                    if (_autoConnectSettings->autoConnectLibrePilot()->rawValue().toBool()) {
578
                        pSerialConfig = new SerialConfiguration(tr("%1 on %2 (AutoConnect)").arg(boardName).arg(portInfo.portName().trimmed()));
579 580
                    }
                    break;
Don Gagne's avatar
Don Gagne committed
581 582
#ifndef __mobile__
                case QGCSerialPortInfo::BoardTypeRTKGPS:
583
                    if (_autoConnectSettings->autoConnectRTKGPS()->rawValue().toBool() && !_toolbox->gpsManager()->connected()) {
584 585
                        qCDebug(LinkManagerLog) << "RTK GPS auto-connected" << portInfo.portName().trimmed();
                        _autoConnectRTKPort = portInfo.systemLocation();
Don Gagne's avatar
Don Gagne committed
586 587 588 589
                        _toolbox->gpsManager()->connectGPS(portInfo.systemLocation());
                    }
                    break;
#endif
Don Gagne's avatar
Don Gagne committed
590 591
                default:
                    qWarning() << "Internal error";
Don Gagne's avatar
Don Gagne committed
592
                    continue;
dogmaphobic's avatar
dogmaphobic committed
593
                }
Don Gagne's avatar
Don Gagne committed
594

Don Gagne's avatar
Don Gagne committed
595 596
                if (pSerialConfig) {
                    qCDebug(LinkManagerLog) << "New auto-connect port added: " << pSerialConfig->name() << portInfo.systemLocation();
597
                    pSerialConfig->setBaud(boardType == QGCSerialPortInfo::BoardTypeSiKRadio ? 57600 : 115200);
Don Gagne's avatar
Don Gagne committed
598 599
                    pSerialConfig->setDynamic(true);
                    pSerialConfig->setPortName(portInfo.systemLocation());
600 601
                    _sharedAutoconnectConfigurations.append(SharedLinkConfigurationPointer(pSerialConfig));
                    createConnectedLink(_sharedAutoconnectConfigurations.last());
Don Gagne's avatar
Don Gagne committed
602
                }
dogmaphobic's avatar
dogmaphobic committed
603 604 605
            }
        }
    }
Don Gagne's avatar
Don Gagne committed
606

607 608 609 610 611 612 613
#ifndef __android__
    // Android builds only support a single serial connection. Repeatedly calling availablePorts after that one serial
    // port is connected leaks file handles due to a bug somewhere in android serial code. In order to work around that
    // bug after we connect the first serial port we stop probing for additional ports. The means we must rely on
    // the port disconnecting itself when the radio is pulled to signal communication list as opposed to automatically
    // closing the Link.

dogmaphobic's avatar
dogmaphobic committed
614 615
    // Now we go through the current configuration list and make sure any dynamic config has gone away
    QList<LinkConfiguration*>  _confToDelete;
616 617 618 619 620 621 622
    for (int i=0; i<_sharedAutoconnectConfigurations.count(); i++) {
        SerialConfiguration* serialConfig = qobject_cast<SerialConfiguration*>(_sharedAutoconnectConfigurations[i].data());
        if (serialConfig) {
            if (!currentPorts.contains(serialConfig->portName())) {
                if (serialConfig->link()) {
                    if (serialConfig->link()->isConnected()) {
                        if (serialConfig->link()->active()) {
623 624 625 626 627 628
                            // We don't remove links which are still connected which have been active with a vehicle on them
                            // even though at this point the cable may have been pulled. Instead we wait for the user to
                            // Disconnect. Once the user disconnects, the link will be removed.
                            continue;
                        }
                    }
Don Gagne's avatar
Don Gagne committed
629
                }
630
                _confToDelete.append(serialConfig);
dogmaphobic's avatar
dogmaphobic committed
631
            }
Don Gagne's avatar
Don Gagne committed
632 633
        } else {
            qWarning() << "Internal error";
dogmaphobic's avatar
dogmaphobic committed
634 635
        }
    }
Don Gagne's avatar
Don Gagne committed
636

Don Gagne's avatar
Don Gagne committed
637
    // Now remove all configs that are gone
Don Gagne's avatar
Don Gagne committed
638
    foreach (LinkConfiguration* pDeleteConfig, _confToDelete) {
Don Gagne's avatar
Don Gagne committed
639
        qCDebug(LinkManagerLog) << "Removing unused autoconnect config" << pDeleteConfig->name();
Don Gagne's avatar
Don Gagne committed
640 641 642
        if (pDeleteConfig->link()) {
            disconnectLink(pDeleteConfig->link());
        }
643 644 645 646 647 648
        for (int i=0; i<_sharedAutoconnectConfigurations.count(); i++) {
            if (_sharedAutoconnectConfigurations[i].data() == pDeleteConfig) {
                _sharedAutoconnectConfigurations.removeAt(i);
                break;
            }
        }
649
    }
650 651

    // Check for RTK GPS connection gone
652
#if !defined(__mobile__)
653 654 655 656 657
    if (!_autoConnectRTKPort.isEmpty() && !currentPorts.contains(_autoConnectRTKPort)) {
        qCDebug(LinkManagerLog) << "RTK GPS disconnected" << _autoConnectRTKPort;
        _toolbox->gpsManager()->disconnectGPS();
        _autoConnectRTKPort.clear();
    }
658
#endif
659

660
#endif
Gus Grubba's avatar
Gus Grubba committed
661
#endif // NO_SERIAL_LINK
662 663
}

Don Gagne's avatar
Don Gagne committed
664 665
void LinkManager::shutdown(void)
{
666
    setConnectionsSuspended(tr("Shutdown"));
Don Gagne's avatar
Don Gagne committed
667
    disconnectAll();
Don Gagne's avatar
Don Gagne committed
668 669
}

670 671 672 673 674 675
QStringList LinkManager::linkTypeStrings(void) const
{
    //-- Must follow same order as enum LinkType in LinkConfiguration.h
    static QStringList list;
    if(!list.size())
    {
Gus Grubba's avatar
Gus Grubba committed
676
#ifndef NO_SERIAL_LINK
677 678 679 680
        list += "Serial";
#endif
        list += "UDP";
        list += "TCP";
dogmaphobic's avatar
dogmaphobic committed
681
#ifdef QGC_ENABLE_BLUETOOTH
dogmaphobic's avatar
dogmaphobic committed
682 683
        list += "Bluetooth";
#endif
dogmaphobic's avatar
dogmaphobic committed
684
#ifdef QT_DEBUG
685
        list += "Mock Link";
dogmaphobic's avatar
dogmaphobic committed
686 687
#endif
#ifndef __mobile__
688
        list += "Log Replay";
dogmaphobic's avatar
dogmaphobic committed
689
#endif
DonLakeFlyer's avatar
DonLakeFlyer committed
690 691 692
        if (list.size() != (int)LinkConfiguration::TypeLast) {
            qWarning() << "Internal error";
        }
693 694 695 696
    }
    return list;
}

697
void LinkManager::_updateSerialPorts()
698
{
699 700
    _commPortList.clear();
    _commPortDisplayList.clear();
Gus Grubba's avatar
Gus Grubba committed
701
#ifndef NO_SERIAL_LINK
702 703
    QList<QSerialPortInfo> portList = QSerialPortInfo::availablePorts();
    foreach (const QSerialPortInfo &info, portList)
704
    {
705 706 707
        QString port = info.systemLocation().trimmed();
        _commPortList += port;
        _commPortDisplayList += SerialConfiguration::cleanPortDisplayname(port);
708 709
    }
#endif
710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726
}

QStringList LinkManager::serialPortStrings(void)
{
    if(!_commPortDisplayList.size())
    {
        _updateSerialPorts();
    }
    return _commPortDisplayList;
}

QStringList LinkManager::serialPorts(void)
{
    if(!_commPortList.size())
    {
        _updateSerialPorts();
    }
727 728 729 730 731
    return _commPortList;
}

QStringList LinkManager::serialBaudRates(void)
{
Gus Grubba's avatar
Gus Grubba committed
732
#ifdef NO_SERIAL_LINK
733 734 735 736 737 738
    QStringList foo;
    return foo;
#else
    return SerialConfiguration::supportedBaudRates();
#endif
}
739 740 741

bool LinkManager::endConfigurationEditing(LinkConfiguration* config, LinkConfiguration* editedConfig)
{
DonLakeFlyer's avatar
DonLakeFlyer committed
742 743 744 745 746 747 748 749 750 751 752
    if (config && editedConfig) {
        _fixUnnamed(editedConfig);
        config->copyFrom(editedConfig);
        saveLinkConfigurationList();
        // Tell link about changes (if any)
        config->updateSettings();
        // Discard temporary duplicate
        delete editedConfig;
    } else {
        qWarning() << "Internal error";
    }
753 754 755 756 757
    return true;
}

bool LinkManager::endCreateConfiguration(LinkConfiguration* config)
{
DonLakeFlyer's avatar
DonLakeFlyer committed
758 759 760 761 762 763 764
    if (config) {
        _fixUnnamed(config);
        addConfiguration(config);
        saveLinkConfigurationList();
    } else {
        qWarning() << "Internal error";
    }
765 766 767 768 769
    return true;
}

LinkConfiguration* LinkManager::createConfiguration(int type, const QString& name)
{
Gus Grubba's avatar
Gus Grubba committed
770
#ifndef NO_SERIAL_LINK
771 772
    if((LinkConfiguration::LinkType)type == LinkConfiguration::TypeSerial)
        _updateSerialPorts();
dogmaphobic's avatar
dogmaphobic committed
773
#endif
774 775 776 777 778
    return LinkConfiguration::createSettings(type, name);
}

LinkConfiguration* LinkManager::startConfigurationEditing(LinkConfiguration* config)
{
DonLakeFlyer's avatar
DonLakeFlyer committed
779
    if (config) {
Gus Grubba's avatar
Gus Grubba committed
780
#ifndef NO_SERIAL_LINK
DonLakeFlyer's avatar
DonLakeFlyer committed
781 782
        if(config->type() == LinkConfiguration::TypeSerial)
            _updateSerialPorts();
dogmaphobic's avatar
dogmaphobic committed
783
#endif
DonLakeFlyer's avatar
DonLakeFlyer committed
784 785 786 787 788
        return LinkConfiguration::duplicateSettings(config);
    } else {
        qWarning() << "Internal error";
        return NULL;
    }
789 790 791 792 793
}


void LinkManager::_fixUnnamed(LinkConfiguration* config)
{
DonLakeFlyer's avatar
DonLakeFlyer committed
794 795 796 797
    if (config) {
        //-- Check for "Unnamed"
        if (config->name() == "Unnamed") {
            switch(config->type()) {
Gus Grubba's avatar
Gus Grubba committed
798
#ifndef NO_SERIAL_LINK
799 800
            case LinkConfiguration::TypeSerial: {
                QString tname = dynamic_cast<SerialConfiguration*>(config)->portName();
801
#ifdef Q_OS_WIN
802 803 804 805 806 807 808
                tname.replace("\\\\.\\", "");
#else
                tname.replace("/dev/cu.", "");
                tname.replace("/dev/", "");
#endif
                config->setName(QString("Serial Device on %1").arg(tname));
                break;
DonLakeFlyer's avatar
DonLakeFlyer committed
809
            }
810 811 812
#endif
            case LinkConfiguration::TypeUdp:
                config->setName(
DonLakeFlyer's avatar
DonLakeFlyer committed
813
                            QString("UDP Link on Port %1").arg(dynamic_cast<UDPConfiguration*>(config)->localPort()));
814 815
                break;
            case LinkConfiguration::TypeTcp: {
DonLakeFlyer's avatar
DonLakeFlyer committed
816 817 818 819
                TCPConfiguration* tconfig = dynamic_cast<TCPConfiguration*>(config);
                if(tconfig) {
                    config->setName(
                                QString("TCP Link %1:%2").arg(tconfig->address().toString()).arg((int)tconfig->port()));
820
                }
DonLakeFlyer's avatar
DonLakeFlyer committed
821
            }
822
                break;
dogmaphobic's avatar
dogmaphobic committed
823
#ifdef QGC_ENABLE_BLUETOOTH
dogmaphobic's avatar
dogmaphobic committed
824
            case LinkConfiguration::TypeBluetooth: {
DonLakeFlyer's avatar
DonLakeFlyer committed
825 826 827
                BluetoothConfiguration* tconfig = dynamic_cast<BluetoothConfiguration*>(config);
                if(tconfig) {
                    config->setName(QString("%1 (Bluetooth Device)").arg(tconfig->device().name));
dogmaphobic's avatar
dogmaphobic committed
828
                }
DonLakeFlyer's avatar
DonLakeFlyer committed
829
            }
dogmaphobic's avatar
dogmaphobic committed
830 831
                break;
#endif
dogmaphobic's avatar
dogmaphobic committed
832
#ifndef __mobile__
833
            case LinkConfiguration::TypeLogReplay: {
DonLakeFlyer's avatar
DonLakeFlyer committed
834 835 836
                LogReplayLinkConfiguration* tconfig = dynamic_cast<LogReplayLinkConfiguration*>(config);
                if(tconfig) {
                    config->setName(QString("Log Replay %1").arg(tconfig->logFilenameShort()));
837
                }
DonLakeFlyer's avatar
DonLakeFlyer committed
838
            }
839
                break;
dogmaphobic's avatar
dogmaphobic committed
840
#endif
841 842 843
#ifdef QT_DEBUG
            case LinkConfiguration::TypeMock:
                config->setName(
DonLakeFlyer's avatar
DonLakeFlyer committed
844
                            QString("Mock Link"));
845 846 847 848 849
                break;
#endif
            case LinkConfiguration::TypeLast:
            default:
                break;
DonLakeFlyer's avatar
DonLakeFlyer committed
850
            }
851
        }
DonLakeFlyer's avatar
DonLakeFlyer committed
852 853
    } else {
        qWarning() << "Internal error";
854 855 856 857 858
    }
}

void LinkManager::removeConfiguration(LinkConfiguration* config)
{
DonLakeFlyer's avatar
DonLakeFlyer committed
859 860 861 862 863
    if (config) {
        LinkInterface* iface = config->link();
        if(iface) {
            disconnectLink(iface);
        }
864

DonLakeFlyer's avatar
DonLakeFlyer committed
865 866 867 868 869
        _removeConfiguration(config);
        saveLinkConfigurationList();
    } else {
        qWarning() << "Internal error";
    }
870
}
871

872 873
bool LinkManager::isAutoconnectLink(LinkInterface* link)
{
874 875 876 877 878 879
    for (int i=0; i<_sharedAutoconnectConfigurations.count(); i++) {
        if (_sharedAutoconnectConfigurations[i].data() == link->getLinkConfiguration()) {
            return true;
        }
    }
    return false;
880
}
dogmaphobic's avatar
dogmaphobic committed
881 882 883 884 885

bool LinkManager::isBluetoothAvailable(void)
{
    return qgcApp()->isBluetoothAvailable();
}
Don Gagne's avatar
Don Gagne committed
886

Gus Grubba's avatar
Gus Grubba committed
887
#ifndef NO_SERIAL_LINK
Don Gagne's avatar
Don Gagne committed
888 889
void LinkManager::_activeLinkCheck(void)
{
890
    SerialLink* link = NULL;
Don Gagne's avatar
Don Gagne committed
891 892 893
    bool found = false;

    if (_activeLinkCheckList.count() != 0) {
894
        link = _activeLinkCheckList.takeFirst();
895
        if (containsLink(link) && link->isConnected()) {
Don Gagne's avatar
Don Gagne committed
896 897 898 899 900 901 902 903 904
            // Make sure there is a vehicle on the link
            QmlObjectListModel* vehicles = _toolbox->multiVehicleManager()->vehicles();
            for (int i=0; i<vehicles->count(); i++) {
                Vehicle* vehicle = qobject_cast<Vehicle*>(vehicles->get(i));
                if (vehicle->containsLink(link)) {
                    found = true;
                    break;
                }
            }
905 906
        } else {
            link = NULL;
Don Gagne's avatar
Don Gagne committed
907 908 909 910 911 912 913
        }
    }

    if (_activeLinkCheckList.count() == 0) {
        _activeLinkCheckTimer.stop();
    }

914 915 916
    if (!found && link) {
        // See if we can get an NSH prompt on this link
        bool foundNSHPrompt = false;
917
        link->writeBytesSafe("\r", 1);
918 919 920 921 922 923 924 925 926
        QSignalSpy spy(link, SIGNAL(bytesReceived(LinkInterface*, QByteArray)));
        if (spy.wait(100)) {
            QList<QVariant> arguments = spy.takeFirst();
            if (arguments[1].value<QByteArray>().contains("nsh>")) {
                foundNSHPrompt = true;
            }
        }

        qgcApp()->showMessage(foundNSHPrompt ?
927 928
                                  tr("Please check to make sure you have an SD Card inserted in your Vehicle and try again.") :
                                  tr("Your Vehicle is not responding. If this continues, shutdown %1, restart the Vehicle letting it boot completely, then start %1.").arg(qgcApp()->applicationName()));
Don Gagne's avatar
Don Gagne committed
929 930
    }
}
Don Gagne's avatar
Don Gagne committed
931
#endif
932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973

bool LinkManager::containsLink(LinkInterface* link)
{
    for (int i=0; i<_sharedLinks.count(); i++) {
        if (_sharedLinks[i].data() == link) {
            return true;
        }
    }
    return false;
}

SharedLinkConfigurationPointer LinkManager::addConfiguration(LinkConfiguration* config)
{
    _qmlConfigurations.append(config);
    _sharedConfigurations.append(SharedLinkConfigurationPointer(config));
    return _sharedConfigurations.last();
}

void LinkManager::_removeConfiguration(LinkConfiguration* config)
{
    _qmlConfigurations.removeOne(config);

    for (int i=0; i<_sharedConfigurations.count(); i++) {
        if (_sharedConfigurations[i].data() == config) {
            _sharedConfigurations.removeAt(i);
            return;
        }
    }

    qWarning() << "LinkManager::_removeConfiguration called with unknown config";
}

QList<LinkInterface*> LinkManager::links(void)
{
    QList<LinkInterface*> rawLinks;

    for (int i=0; i<_sharedLinks.count(); i++) {
        rawLinks.append(_sharedLinks[i].data());
    }

    return rawLinks;
}
974 975 976 977 978 979 980 981 982 983 984

void LinkManager::startAutoConnectedLinks(void)
{
    SharedLinkConfigurationPointer conf;

    for(int i = 0; i < _sharedConfigurations.count(); i++) {
        conf = _sharedConfigurations[i];
        if (conf->isAutoConnect())
            createConnectedLink(conf);
    }
}
985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006

int LinkManager::_reserveMavlinkChannel(void)
{
    // Find a mavlink channel to use for this link, Channel 0 is reserved for internal use.
    for (int mavlinkChannel=1; mavlinkChannel<32; mavlinkChannel++) {
        if (!(_mavlinkChannelsUsedBitMask & 1 << mavlinkChannel)) {
            mavlink_reset_channel_status(mavlinkChannel);
            // Start the channel on Mav 1 protocol
            mavlink_status_t* mavlinkStatus = mavlink_get_channel_status(mavlinkChannel);
            mavlinkStatus->flags |= MAVLINK_STATUS_FLAG_OUT_MAVLINK1;
            _mavlinkChannelsUsedBitMask |= 1 << mavlinkChannel;
            return mavlinkChannel;
        }
    }

    return 0;   // All channels reserved
}

void LinkManager::_freeMavlinkChannel(int channel)
{
    _mavlinkChannelsUsedBitMask &= ~(1 << channel);
}
1007 1008 1009 1010 1011 1012 1013 1014 1015 1016

void LinkManager::_bytesReceived(LinkInterface *link, QByteArray bytes) {
    Q_UNUSED(bytes);

    link->timerStart();

    if (!link->active()) {
        link->setActive(true);
    }
}