forked from qt-creator/qt-creator
Symbian/CODA: Add infrastructure for serial communication.
Add USB protocol and chunking.
This commit is contained in:
@@ -29,6 +29,7 @@
|
||||
|
||||
#include "tcftrkdevice.h"
|
||||
#include "json.h"
|
||||
#include "trkutils.h"
|
||||
|
||||
#include <QtNetwork/QAbstractSocket>
|
||||
#include <QtCore/QDebug>
|
||||
@@ -40,7 +41,62 @@
|
||||
|
||||
enum { debug = 0 };
|
||||
|
||||
static const char messageTerminatorC[] = "\003\001";
|
||||
static const char wlanMessageTerminatorC[] = "\003\001";
|
||||
|
||||
// Serial Ping: 0xfc,0x1f
|
||||
static const char serialPingC[] = "\xfc\x1f";
|
||||
// Serial Pong: 0xfc,0xf1, followed by version info
|
||||
static const char serialPongC[] = "\xfc\xf1";
|
||||
|
||||
static const char locatorAnswerC[] = "E\0Locator\0Hello\0[\"Locator\"]";
|
||||
|
||||
static const unsigned serialChunkLength = 0x400; // 1K max USB router
|
||||
static const int maxSerialMessageLength = 0x10000; // give chunking scheme
|
||||
|
||||
// Create USB router frame
|
||||
static inline void encodeSerialFrame(const QByteArray &data, QByteArray *target)
|
||||
{
|
||||
target->append(char(0x01));
|
||||
target->append(char(0x92)); // CODA serial message ID
|
||||
appendShort(target, ushort(data.size()), trk::BigEndian);
|
||||
target->append(data);
|
||||
}
|
||||
|
||||
// Split in chunks of 1K according to CODA protocol chunking
|
||||
static inline QByteArray encodeUsbSerialMessage(const QByteArray &dataIn)
|
||||
{
|
||||
static const int chunkSize = serialChunkLength - 2; // 2 Header bytes
|
||||
const int size = dataIn.size();
|
||||
QByteArray frame;
|
||||
// Do we need to split?
|
||||
if (size < chunkSize) { // Nope, all happy.
|
||||
frame.reserve(size + 4);
|
||||
encodeSerialFrame(dataIn, &frame);
|
||||
return frame;
|
||||
}
|
||||
// Split.
|
||||
unsigned chunkCount = size / chunkSize;
|
||||
if (size % chunkSize)
|
||||
chunkCount++;
|
||||
if (debug)
|
||||
qDebug("Serial: Splitting message of %d bytes into %u chunks of %d", size, chunkCount, chunkSize);
|
||||
|
||||
frame.reserve((4 + serialChunkLength) * chunkCount);
|
||||
int pos = 0;
|
||||
for (unsigned c = chunkCount - 1; pos < size ; c--) {
|
||||
QByteArray chunk; // chunk with long message start/continuation code
|
||||
chunk.reserve(serialChunkLength);
|
||||
chunk.append(pos ? char(0) : char(0xfe));
|
||||
chunk.append(char(static_cast<unsigned char>(c))); // Avoid any signedness issues.
|
||||
const int chunkEnd = qMin(pos + chunkSize, size);
|
||||
chunk.append(dataIn.mid(pos, chunkEnd - pos));
|
||||
encodeSerialFrame(chunk, &frame);
|
||||
pos = chunkEnd;
|
||||
}
|
||||
if (debug > 1)
|
||||
qDebug("Serial chunked:\n%s", qPrintable(tcftrk::formatData(frame)));
|
||||
return frame;
|
||||
}
|
||||
|
||||
namespace tcftrk {
|
||||
// ------------- TcfTrkCommandError
|
||||
@@ -272,11 +328,12 @@ struct TcfTrkDevicePrivate {
|
||||
TokenWrittenMessageMap m_writtenMessages;
|
||||
QVector<QByteArray> m_registerNames;
|
||||
QVector<QByteArray> m_fakeGetMRegisterValues;
|
||||
bool m_serialFrame;
|
||||
};
|
||||
|
||||
TcfTrkDevicePrivate::TcfTrkDevicePrivate() :
|
||||
m_messageTerminator(messageTerminatorC),
|
||||
m_verbose(0), m_token(0)
|
||||
m_messageTerminator(wlanMessageTerminatorC),
|
||||
m_verbose(0), m_token(0), m_serialFrame(false)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -380,7 +437,12 @@ void TcfTrkDevice::slotDeviceSocketStateChanged()
|
||||
|
||||
static inline QString debugMessage(QByteArray message, const char *prefix = 0)
|
||||
{
|
||||
const bool isBinary = !message.isEmpty() && message.at(0) < 0;
|
||||
if (isBinary) {
|
||||
message = message.toHex(); // Some serial special message
|
||||
} else {
|
||||
message.replace('\0', '|');
|
||||
}
|
||||
const QString messageS = QString::fromLatin1(message);
|
||||
return prefix ?
|
||||
(QLatin1String(prefix) + messageS) : messageS;
|
||||
@@ -388,7 +450,74 @@ static inline QString debugMessage(QByteArray message, const char *prefix = 0)
|
||||
|
||||
void TcfTrkDevice::slotDeviceReadyRead()
|
||||
{
|
||||
d->m_readBuffer += d->m_device->readAll();
|
||||
const QByteArray newData = d->m_device->readAll();
|
||||
d->m_readBuffer += newData;
|
||||
if (debug)
|
||||
qDebug("ReadBuffer: %s", qPrintable(trk::stringFromArray(newData)));
|
||||
if (d->m_serialFrame) {
|
||||
deviceReadyReadSerial();
|
||||
} else {
|
||||
deviceReadyReadWLAN();
|
||||
}
|
||||
}
|
||||
|
||||
// Find a serial header in input stream '0x1', '0x92', 'lenH', 'lenL'
|
||||
// and return message position and size.
|
||||
static inline QPair<int, int> findSerialHeader(const QByteArray &in)
|
||||
{
|
||||
const int size = in.size();
|
||||
const char header1 = 0x1;
|
||||
const char header2 = char(0x92);
|
||||
// Header should in theory always be at beginning of
|
||||
// buffer. Warn if there are bogus data in-between.
|
||||
for (int pos = 0; pos < size; ) {
|
||||
if (pos + 4 < size && in.at(pos) == header1 && in.at(pos + 1) == header2) {
|
||||
const int length = trk::extractShort(in.constData() + 2);
|
||||
return QPair<int, int>(pos + 4, length);
|
||||
}
|
||||
// Find next
|
||||
pos = in.indexOf(header1, pos + 1);
|
||||
qWarning("Bogus data received on serial line: %s\n"
|
||||
"Frame Header at: %d", qPrintable(trk::stringFromArray(in)), pos);
|
||||
if (pos < 0)
|
||||
break;
|
||||
}
|
||||
return QPair<int, int>(-1, -1);
|
||||
}
|
||||
|
||||
void TcfTrkDevice::deviceReadyReadSerial()
|
||||
{
|
||||
do {
|
||||
// Extract message (pos,len)
|
||||
const QPair<int, int> messagePos = findSerialHeader(d->m_readBuffer);
|
||||
if (messagePos.first < 0)
|
||||
break;
|
||||
// Do we have the complete message?
|
||||
const int messageEnd = messagePos.first + messagePos.second;
|
||||
if (messageEnd > d->m_readBuffer.size())
|
||||
break;
|
||||
const QByteArray message = d->m_readBuffer.mid(messagePos.first, messagePos.second);
|
||||
// Is thing a ping/pong response
|
||||
if (debug > 1)
|
||||
qDebug("Serial message: at %d (%d bytes) of %d: %s",
|
||||
messagePos.first, messagePos.second, d->m_readBuffer.size(),
|
||||
qPrintable(trk::stringFromArray(message)));
|
||||
if (message.startsWith(serialPongC)) {
|
||||
const QString version = QString::fromLatin1(message.mid(sizeof(serialPongC) - 1));
|
||||
emitLogMessage(QString::fromLatin1("Serial connection from '%1'").arg(version));
|
||||
emit serialPong(version);
|
||||
// Answer with locator.
|
||||
writeMessage(QByteArray(locatorAnswerC, sizeof(locatorAnswerC)));
|
||||
} else {
|
||||
processMessage(message);
|
||||
}
|
||||
d->m_readBuffer.remove(0, messageEnd);
|
||||
} while (d->m_readBuffer.isEmpty());
|
||||
checkSendQueue(); // Send off further messages
|
||||
}
|
||||
|
||||
void TcfTrkDevice::deviceReadyReadWLAN()
|
||||
{
|
||||
// Take complete message off front of readbuffer.
|
||||
do {
|
||||
const int messageEndPos = d->m_readBuffer.indexOf(d->m_messageTerminator);
|
||||
@@ -398,7 +527,15 @@ void TcfTrkDevice::slotDeviceReadyRead()
|
||||
// TCF TRK 4.0.5 emits empty messages on errors.
|
||||
emitLogMessage(QString::fromLatin1("An empty TCF TRK message has been received."));
|
||||
} else {
|
||||
const QByteArray message = d->m_readBuffer.left(messageEndPos);
|
||||
processMessage(d->m_readBuffer.left(messageEndPos));
|
||||
}
|
||||
d->m_readBuffer.remove(0, messageEndPos + d->m_messageTerminator.size());
|
||||
} while (!d->m_readBuffer.isEmpty());
|
||||
checkSendQueue(); // Send off further messages
|
||||
}
|
||||
|
||||
void TcfTrkDevice::processMessage(const QByteArray &message)
|
||||
{
|
||||
if (debug)
|
||||
qDebug("Read %d bytes:\n%s", message.size(), qPrintable(formatData(message)));
|
||||
if (const int errorCode = parseMessage(message)) {
|
||||
@@ -409,10 +546,6 @@ void TcfTrkDevice::slotDeviceReadyRead()
|
||||
message.size(), qPrintable(formatData(message)));
|
||||
}
|
||||
}
|
||||
d->m_readBuffer.remove(0, messageEndPos + d->m_messageTerminator.size());
|
||||
} while (!d->m_readBuffer.isEmpty());
|
||||
checkSendQueue(); // Send off further message
|
||||
}
|
||||
|
||||
// Split \0-terminated message into tokens, skipping the initial type character
|
||||
static inline QVector<QByteArray> splitMessage(const QByteArray &message)
|
||||
@@ -551,8 +684,6 @@ int TcfTrkDevice::parseTcfCommandReply(char type, const QVector<QByteArray> &tok
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const char locatorAnswerC[] = "E\0Locator\0Hello\0[\"Locator\"]";
|
||||
|
||||
int TcfTrkDevice::parseTcfEvent(const QVector<QByteArray> &tokens)
|
||||
{
|
||||
// Event: Ignore the periodical heartbeat event, answer 'Hello',
|
||||
@@ -572,8 +703,9 @@ int TcfTrkDevice::parseTcfEvent(const QVector<QByteArray> &tokens)
|
||||
// Parse known events, emit signals
|
||||
QScopedPointer<TcfTrkEvent> knownEvent(TcfTrkEvent::parseEvent(service, tokens.at(1), values));
|
||||
if (!knownEvent.isNull()) {
|
||||
// Answer hello event.
|
||||
// Answer hello event (WLAN)
|
||||
if (knownEvent->type() == TcfTrkEvent::LocatorHello)
|
||||
if (!d->m_serialFrame)
|
||||
writeMessage(QByteArray(locatorAnswerC, sizeof(locatorAnswerC)));
|
||||
emit tcfEvent(*knownEvent);
|
||||
}
|
||||
@@ -600,6 +732,16 @@ unsigned TcfTrkDevice::verbose() const
|
||||
return d->m_verbose;
|
||||
}
|
||||
|
||||
bool TcfTrkDevice::serialFrame() const
|
||||
{
|
||||
return d->m_serialFrame;
|
||||
}
|
||||
|
||||
void TcfTrkDevice::setSerialFrame(bool s)
|
||||
{
|
||||
d->m_serialFrame = s;
|
||||
}
|
||||
|
||||
void TcfTrkDevice::setVerbose(unsigned v)
|
||||
{
|
||||
d->m_verbose = v;
|
||||
@@ -625,6 +767,17 @@ bool TcfTrkDevice::checkOpen()
|
||||
return true;
|
||||
}
|
||||
|
||||
void TcfTrkDevice::sendSerialPing()
|
||||
{
|
||||
if (!checkOpen())
|
||||
return;
|
||||
|
||||
setSerialFrame(true);
|
||||
writeMessage(QByteArray(serialPingC, qstrlen(serialPingC)), false);
|
||||
if (d->m_verbose)
|
||||
emitLogMessage(QLatin1String("Ping..."));
|
||||
}
|
||||
|
||||
void TcfTrkDevice::sendTcfTrkMessage(MessageType mt, Services service, const char *command,
|
||||
const char *commandParameters, // may contain '\0'
|
||||
int commandParametersLength,
|
||||
@@ -673,18 +826,29 @@ void TcfTrkDevice::sendTcfTrkMessage(MessageType mt, Services service, const cha
|
||||
}
|
||||
|
||||
// Enclose in message frame and write.
|
||||
void TcfTrkDevice::writeMessage(QByteArray data)
|
||||
void TcfTrkDevice::writeMessage(QByteArray data, bool ensureTerminating0)
|
||||
{
|
||||
if (!checkOpen())
|
||||
return;
|
||||
|
||||
if (d->m_serialFrame && data.size() > maxSerialMessageLength) {
|
||||
qCritical("Attempt to send large message (%d bytes) exceeding the "
|
||||
"limit of %d bytes over serial channel. Skipping.",
|
||||
data.size(), maxSerialMessageLength);
|
||||
return;
|
||||
}
|
||||
|
||||
if (d->m_verbose)
|
||||
emitLogMessage(debugMessage(data, "TCF <-"));
|
||||
|
||||
// Ensure \0-termination which easily gets lost in QByteArray CT.
|
||||
if (!data.endsWith('\0'))
|
||||
if (ensureTerminating0 && !data.endsWith('\0'))
|
||||
data.append('\0');
|
||||
if (d->m_serialFrame) {
|
||||
data = encodeUsbSerialMessage(data);
|
||||
} else {
|
||||
data += d->m_messageTerminator;
|
||||
}
|
||||
|
||||
if (debug > 1)
|
||||
qDebug("Writing:\n%s", qPrintable(formatData(data)));
|
||||
|
||||
@@ -132,11 +132,20 @@ http://dev.eclipse.org/svnroot/dsdp/org.eclipse.tm.tcf/trunk/docs/TCF%20Services
|
||||
* single commands. As soon as 'Registers::getm' is natively supported, all code
|
||||
* related to 'FakeRegisterGetm' should be removed. The workaround requires that
|
||||
* the register name is known.
|
||||
* CODA notes:
|
||||
* - Commands are accepted only after receiving the Locator Hello event
|
||||
* - Serial communication initiation sequence:
|
||||
* Send serial ping from host sendSerialPing() -> receive pong response with
|
||||
* version information -> Send Locator Hello Event -> Receive Locator Hello Event
|
||||
* -> Commands are accepted.
|
||||
* - WLAN communication initiation sequence:
|
||||
* Receive Locator Hello Event from CODA -> Commands are accepted.
|
||||
*/
|
||||
|
||||
class SYMBIANUTILS_EXPORT TcfTrkDevice : public QObject
|
||||
{
|
||||
Q_PROPERTY(unsigned verbose READ verbose WRITE setVerbose)
|
||||
Q_PROPERTY(bool serialFrame READ serialFrame WRITE setSerialFrame)
|
||||
Q_OBJECT
|
||||
public:
|
||||
// Flags for FileSystem:open
|
||||
@@ -163,6 +172,8 @@ public:
|
||||
virtual ~TcfTrkDevice();
|
||||
|
||||
unsigned verbose() const;
|
||||
bool serialFrame() const;
|
||||
void setSerialFrame(bool);
|
||||
|
||||
// Mapping of register names to indices for multi-requests.
|
||||
// Register names can be retrieved via 'Registers:getChildren' (requires
|
||||
@@ -174,6 +185,9 @@ public:
|
||||
IODevicePtr takeDevice();
|
||||
void setDevice(const IODevicePtr &dp);
|
||||
|
||||
// Serial Only: Initiate communication. Will emit serialPong() signal with version.
|
||||
void sendSerialPing();
|
||||
|
||||
// Send with parameters from string (which may contain '\0').
|
||||
void sendTcfTrkMessage(MessageType mt, Services service,
|
||||
const char *command,
|
||||
@@ -338,6 +352,7 @@ public:
|
||||
signals:
|
||||
void genericTcfEvent(int service, const QByteArray &name, const QVector<tcftrk::JsonValue> &value);
|
||||
void tcfEvent(const tcftrk::TcfTrkEvent &knownEvent);
|
||||
void serialPong(const QString &codaVersion);
|
||||
|
||||
void logMessage(const QString &);
|
||||
void error(const QString &);
|
||||
@@ -351,11 +366,15 @@ private slots:
|
||||
void slotDeviceReadyRead();
|
||||
|
||||
private:
|
||||
void deviceReadyReadSerial();
|
||||
void deviceReadyReadWLAN();
|
||||
|
||||
bool checkOpen();
|
||||
void checkSendQueue();
|
||||
void writeMessage(QByteArray data);
|
||||
void writeMessage(QByteArray data, bool ensureTerminating0 = true);
|
||||
void emitLogMessage(const QString &);
|
||||
int parseMessage(const QByteArray &);
|
||||
inline int parseMessage(const QByteArray &);
|
||||
void processMessage(const QByteArray &message);
|
||||
int parseTcfCommandReply(char type, const QVector<QByteArray> &tokens);
|
||||
int parseTcfEvent(const QVector<QByteArray> &tokens);
|
||||
// Send with parameters from string (which may contain '\0').
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
DEFINES += SYMBIANUTILS_INCLUDE_PRI
|
||||
|
||||
include(../../../qtcreator.pri)
|
||||
include(../../../src/shared/symbianutils/symbianutils.pri)
|
||||
# include(../../../src/libs/3rdparty/qextserialport/qextserialport.pri)
|
||||
|
||||
QT += core gui network
|
||||
TARGET = codaclient
|
||||
|
||||
@@ -29,6 +29,10 @@
|
||||
|
||||
#include "codaclientapplication.h"
|
||||
|
||||
#ifdef HAS_SERIALPORT
|
||||
# include <qextserialport/qextserialport.h>
|
||||
#endif
|
||||
|
||||
#include "tcftrkdevice.h"
|
||||
#include <QtNetwork/QTcpSocket>
|
||||
#include <QtCore/QFile>
|
||||
@@ -255,31 +259,27 @@ bool CodaClientApplication::start()
|
||||
switch (m_mode) {
|
||||
case Launch: {
|
||||
const QString args = m_launchArgs.join(QString(QLatin1Char(' ')));
|
||||
std::printf("Launching 0x%x '%s '%s' on %s:%hu (debug: %d)\n",
|
||||
std::printf("Launching 0x%x '%s '%s' (debug: %d)\n",
|
||||
m_launchUID, qPrintable(m_launchBinary),
|
||||
qPrintable(args), qPrintable(m_address), m_port,
|
||||
m_launchDebug);
|
||||
qPrintable(args), m_launchDebug);
|
||||
}
|
||||
break;
|
||||
case Install:
|
||||
std::printf("Installing '%s' to '%s' on %s:%hu\n",
|
||||
qPrintable(m_installSisFile), qPrintable(m_installTargetDrive),
|
||||
qPrintable(m_address), m_port);
|
||||
std::printf("Installing '%s' to '%s'\n",
|
||||
qPrintable(m_installSisFile), qPrintable(m_installTargetDrive));
|
||||
break;
|
||||
case Put:
|
||||
std::printf("Copying '%s' to '%s' on %s:%hu in chunks of %lluKB\n",
|
||||
std::printf("Copying '%s' to '%s' in chunks of %lluKB\n",
|
||||
qPrintable(m_putLocalFile), qPrintable(m_putRemoteFile),
|
||||
qPrintable(m_address), m_port, m_putChunkSize / 1024);
|
||||
m_putChunkSize / 1024);
|
||||
break;
|
||||
case Stat:
|
||||
std::printf("Retrieving attributes of '%s' from %s:%hu\n",
|
||||
qPrintable(m_statRemoteFile), qPrintable(m_address), m_port);
|
||||
std::printf("Retrieving attributes of '%s'\n", qPrintable(m_statRemoteFile));
|
||||
break;
|
||||
case Invalid:
|
||||
break;
|
||||
}
|
||||
// Start connection
|
||||
const QSharedPointer<QTcpSocket> tcfTrkSocket(new QTcpSocket);
|
||||
m_trkDevice.reset(new tcftrk::TcfTrkDevice);
|
||||
m_trkDevice->setVerbose(m_verbose);
|
||||
connect(m_trkDevice.data(), SIGNAL(error(QString)),
|
||||
@@ -288,9 +288,48 @@ bool CodaClientApplication::start()
|
||||
this, SLOT(slotTrkLogMessage(QString)));
|
||||
connect(m_trkDevice.data(), SIGNAL(tcfEvent(tcftrk::TcfTrkEvent)),
|
||||
this, SLOT(slotTcftrkEvent(tcftrk::TcfTrkEvent)));
|
||||
if (m_address.startsWith(QLatin1String("/dev"))
|
||||
|| m_address.startsWith(QLatin1String("com"), Qt::CaseInsensitive)
|
||||
|| m_address.startsWith(QLatin1Char('\\'))) {
|
||||
#ifdef HAS_SERIALPORT
|
||||
// Serial
|
||||
#ifdef Q_OS_WIN
|
||||
const QString fullPort = QextSerialPort::fullPortNameWin(m_address);
|
||||
#else
|
||||
const QString fullPort = m_address;
|
||||
#endif
|
||||
const QSharedPointer<QextSerialPort>
|
||||
serialPort(new QextSerialPort(fullPort, QextSerialPort::EventDriven));
|
||||
std::printf("Opening port %s...\n", qPrintable(fullPort));
|
||||
|
||||
// Magic USB serial parameters
|
||||
serialPort->setTimeout(2000);
|
||||
serialPort->setBaudRate(BAUD115200);
|
||||
serialPort->setFlowControl(FLOW_OFF);
|
||||
serialPort->setParity(PAR_NONE);
|
||||
serialPort->setDataBits(DATA_8);
|
||||
serialPort->setStopBits(STOP_1);
|
||||
|
||||
m_trkDevice->setSerialFrame(true);
|
||||
m_trkDevice->setDevice(serialPort); // Grab all data from start
|
||||
if (!serialPort->open(QIODevice::ReadWrite|QIODevice::Unbuffered)) {
|
||||
std::fprintf(stderr, "Cannot open port: %s", qPrintable(serialPort->errorString()));
|
||||
return false;
|
||||
}
|
||||
// Initiate communication
|
||||
m_trkDevice->sendSerialPing();
|
||||
serialPort->flush();
|
||||
#else
|
||||
std::fprintf(stderr, "Not implemented\n");
|
||||
return false;
|
||||
#endif
|
||||
} else {
|
||||
// TCP/IP
|
||||
const QSharedPointer<QTcpSocket> tcfTrkSocket(new QTcpSocket);
|
||||
m_trkDevice->setDevice(tcfTrkSocket);
|
||||
tcfTrkSocket->connectToHost(m_address, m_port);
|
||||
std::printf("Connecting...\n");
|
||||
std::printf("Connecting to %s:%hu...\n", qPrintable(m_address), m_port);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -399,8 +438,7 @@ void CodaClientApplication::handleFileSystemFStat(const tcftrk::TcfTrkCommandRes
|
||||
} else {
|
||||
std::fprintf(stderr, "FStat failed: %s\n", qPrintable(result.toString()));
|
||||
}
|
||||
m_trkDevice->sendFileSystemCloseCommand(tcftrk::TcfTrkCallback(this, &CodaClientApplication::handleFileSystemClose),
|
||||
m_remoteFileHandle);
|
||||
closeRemoteFile();
|
||||
}
|
||||
|
||||
void CodaClientApplication::handleFileSystemClose(const tcftrk::TcfTrkCommandResult &result)
|
||||
@@ -490,6 +528,7 @@ void CodaClientApplication::doExit(int ex)
|
||||
|
||||
if (!m_trkDevice.isNull()) {
|
||||
const QSharedPointer<QIODevice> dev = m_trkDevice->device();
|
||||
if (!dev.isNull()) {
|
||||
if (QAbstractSocket *socket = qobject_cast<QAbstractSocket *>(dev.data())) {
|
||||
if (socket->state() == QAbstractSocket::ConnectedState)
|
||||
socket->disconnectFromHost();
|
||||
@@ -498,6 +537,7 @@ void CodaClientApplication::doExit(int ex)
|
||||
dev->close();
|
||||
}
|
||||
}
|
||||
}
|
||||
std::printf("Exiting (%d)\n", ex);
|
||||
exit(ex);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user