Newer
Older
/*=====================================================================
QGroundControl Open Source Ground Control Station
(c) 2009, 2010 QGROUNDCONTROL PROJECT <http://www.qgroundcontrol.org>
QGROUNDCONTROL 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.
QGROUNDCONTROL 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 QGROUNDCONTROL. If not, see <http://www.gnu.org/licenses/>.
======================================================================*/
/**
* @file
* @brief This file implements the Debug Console, a serial console built-in to QGC.
*
* @author Lorenz Meier <mavteam@student.ethz.ch>
*
*/
#include <QPainter>
#include <QDebug>
#include "DebugConsole.h"
#include "ui_DebugConsole.h"
#include "LinkManager.h"
#include "UASManager.h"
#include "protocol.h"
#include "QGC.h"
const float DebugConsole::inDataRateThreshold = 0.4f;
QWidget(parent),
currLink(NULL),
holdOn(false),
convertToAscii(true),
filterMAVLINK(false),
autoHold(true),
bytesToIgnore(0),
lastByte(-1),
sentBytes(),
holdBuffer(),
lineBuffer(""),
lineBufferTimer(),
snapShotTimer(),
lowpassInDataRate(0.0f),
lowpassOutDataRate(0.0f),
commandIndex(0),
m_ui(new Ui::DebugConsole)
// Hide sent text field - it is only useful after send has been hit
m_ui->sentText->setVisible(false);
// Hide auto-send checkbox
//m_ui->specialCheckBox->setVisible(false);
// Limit to 500 lines
m_ui->receiveText->setMaximumBlockCount(500);
// Allow to wrap everywhere
m_ui->receiveText->setWordWrapMode(QTextOption::WrapAnywhere);
// Load settings for this widget
Bryant Mairs
committed
// Enable traffic measurements. We only start/stop the timer as our links change, as
// these calculations are dependent on the specific link.
connect(&snapShotTimer, SIGNAL(timeout()), this, SLOT(updateTrafficMeasurements()));
snapShotTimer.setInterval(snapShotInterval);
Lorenz Meier
committed
// First connect management slots, then make sure to add all existing objects
// Connect to link manager to get notified about new links
connect(LinkManager::instance(), SIGNAL(newLink(LinkInterface*)), this, SLOT(addLink(LinkInterface*)));
// Connect to UAS manager to get notified about new UAS
connect(UASManager::instance(), SIGNAL(UASCreated(UASInterface*)), this, SLOT(uasCreated(UASInterface*)));
// Get a list of all existing links
links = QList<LinkInterface*>();
foreach (LinkInterface* link, LinkManager::instance()->getLinks()) {
Lorenz Meier
committed
// Get a list of all existing UAS
foreach (UASInterface* uas, UASManager::instance()->getUASList()) {
uasCreated(uas);
}
// Connect link combo box
connect(m_ui->linkComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(linkSelected(int)));
// Connect send button
connect(m_ui->transmitButton, SIGNAL(clicked()), this, SLOT(sendBytes()));
// Connect HEX conversion and MAVLINK filter checkboxes
connect(m_ui->mavlinkCheckBox, SIGNAL(clicked(bool)), this, SLOT(MAVLINKfilterEnabled(bool)));
connect(m_ui->hexCheckBox, SIGNAL(clicked(bool)), this, SLOT(hexModeEnabled(bool)));
connect(m_ui->holdCheckBox, SIGNAL(clicked(bool)), this, SLOT(setAutoHold(bool)));
// Connect hold button
connect(m_ui->holdButton, SIGNAL(toggled(bool)), this, SLOT(hold(bool)));
// Connect connect button
connect(m_ui->connectButton, SIGNAL(clicked()), this, SLOT(handleConnectButton()));
// Connect the special chars combo box
connect(m_ui->addSymbolButton, SIGNAL(clicked()), this, SLOT(appendSpecialSymbol()));
// Connect Checkbox
connect(m_ui->specialComboBox, SIGNAL(highlighted(QString)), this, SLOT(specialSymbolSelected(QString)));
// Allow to send via return
connect(m_ui->sendText, SIGNAL(returnPressed()), this, SLOT(sendBytes()));
void DebugConsole::hideEvent(QHideEvent* event)
{
Q_UNUSED(event);
storeSettings();
}
void DebugConsole::loadSettings()
{
// Load defaults from settings
QSettings settings;
settings.sync();
settings.beginGroup("QGC_DEBUG_CONSOLE");
m_ui->specialComboBox->setCurrentIndex(settings.value("SPECIAL_SYMBOL", m_ui->specialComboBox->currentIndex()).toInt());
m_ui->specialCheckBox->setChecked(settings.value("SPECIAL_SYMBOL_CHECKBOX_STATE", m_ui->specialCheckBox->isChecked()).toBool());
hexModeEnabled(settings.value("HEX_MODE_ENABLED", m_ui->hexCheckBox->isChecked()).toBool());
MAVLINKfilterEnabled(settings.value("MAVLINK_FILTER_ENABLED", filterMAVLINK).toBool());
setAutoHold(settings.value("AUTO_HOLD_ENABLED", autoHold).toBool());
settings.endGroup();
}
void DebugConsole::storeSettings()
{
// Store settings
QSettings settings;
settings.beginGroup("QGC_DEBUG_CONSOLE");
settings.setValue("SPECIAL_SYMBOL", m_ui->specialComboBox->currentIndex());
settings.setValue("SPECIAL_SYMBOL_CHECKBOX_STATE", m_ui->specialCheckBox->isChecked());
settings.setValue("HEX_MODE_ENABLED", m_ui->hexCheckBox->isChecked());
settings.setValue("MAVLINK_FILTER_ENABLED", filterMAVLINK);
settings.setValue("AUTO_HOLD_ENABLED", autoHold);
settings.endGroup();
settings.sync();
}
Lorenz Meier
committed
void DebugConsole::uasCreated(UASInterface* uas)
{
connect(uas, SIGNAL(textMessageReceived(int,int,int,QString)),
this, SLOT(receiveTextMessage(int,int,int,QString)), Qt::UniqueConnection);
}
/**
* Add a link to the debug console output
*/
void DebugConsole::addLink(LinkInterface* link)
{
// Add link to link list
links.insert(link->getId(), link);
m_ui->linkComboBox->insertItem(link->getId(), link->getName());
// Set new item as current
m_ui->linkComboBox->setCurrentIndex(qMax(0, links.size() - 1));
linkSelected(m_ui->linkComboBox->currentIndex());
Lorenz Meier
committed
connect(link, SIGNAL(nameChanged(QString)), this, SLOT(updateLinkName(QString)), Qt::UniqueConnection);
connect(link, SIGNAL(deleteLink(LinkInterface* const)), this, SLOT(removeLink(LinkInterface* const)), Qt::UniqueConnection);
void DebugConsole::removeLink(LinkInterface* const linkInterface)
{
// Add link to link list
if (links.contains(linkInterface)) {
int linkIndex = links.indexOf(linkInterface);
links.removeAt(linkIndex);
m_ui->linkComboBox->removeItem(linkIndex);
}
Bryant Mairs
committed
// Now if this was the current link, clean up some stuff.
if (linkInterface == currLink)
{
// Like disable the update time for the UI.
snapShotTimer.stop();
currLink = NULL;
}
Michael Carpenter
committed
void DebugConsole::linkStatusUpdate(const QString& name,const QString& text)
{
Michael Carpenter
committed
m_ui->receiveText->appendPlainText(text);
// Ensure text area scrolls correctly
m_ui->receiveText->ensureCursorVisible();
}
void DebugConsole::linkSelected(int linkId)
{
// Disconnect
Bryant Mairs
committed
if (currLink)
{
disconnect(currLink, SIGNAL(bytesReceived(LinkInterface*,QByteArray)), this, SLOT(receiveBytes(LinkInterface*, QByteArray)));
disconnect(currLink, SIGNAL(connected(bool)), this, SLOT(setConnectionState(bool)));
Michael Carpenter
committed
disconnect(currLink,SIGNAL(communicationUpdate(QString,QString)),this,SLOT(linkStatusUpdate(QString,QString)));
Bryant Mairs
committed
snapShotTimer.stop();
Bryant Mairs
committed
// Clear data
m_ui->receiveText->clear();
// Connect new link
if (linkId != -1) {
currLink = links[linkId];
connect(currLink, SIGNAL(bytesReceived(LinkInterface*,QByteArray)), this, SLOT(receiveBytes(LinkInterface*, QByteArray)));
connect(currLink, SIGNAL(connected(bool)), this, SLOT(setConnectionState(bool)));
connect(currLink,SIGNAL(communicationUpdate(QString,QString)),this,SLOT(linkStatusUpdate(QString,QString)));
setConnectionState(currLink->isConnected());
snapShotTimer.start();
}
}
/**
* @param name new name for this link - the link is determined to the sender to this slot by QObject::sender()
*/
void DebugConsole::updateLinkName(QString name)
{
// Set name if signal came from a link
if((link != NULL) && (links.contains(link)))
{
const qint16 &linkIndex(links.indexOf(link));
m_ui->linkComboBox->setItemText(linkIndex,name);
}
}
void DebugConsole::setAutoHold(bool hold)
{
// Disable current hold if hold had been enabled
if (autoHold && holdOn && !hold) {
this->hold(false);
m_ui->holdButton->setChecked(false);
}
// Set auto hold checkbox
if (m_ui->holdCheckBox->isChecked() != hold) {
m_ui->holdCheckBox->setChecked(hold);
}
if (!hold)
{
// Warn user about not activated hold
m_ui->receiveText->appendHtml(QString("<font color=\"%1\">%2</font>\n").arg(QColor(Qt::red).name(), tr("WARNING: You have NOT enabled auto-hold (stops updating the console if huge amounts of serial data arrive). Updating the console consumes significant CPU load, so if you receive more than about 5 KB/s of serial data, make sure to enable auto-hold if not using the console.")));
}
else
{
m_ui->receiveText->clear();
}
/**
* Prints the message in the UAS color
*/
void DebugConsole::receiveTextMessage(int id, int component, int severity, QString text)
Q_UNUSED(severity);
if (isVisible())
{
QString name = UASManager::instance()->getUASForId(id)->getUASName();
QString comp;
// Get a human readable name if possible
switch (component) {
// TODO: To be completed
case MAV_COMP_ID_IMU:
comp = tr("IMU");
break;
case MAV_COMP_ID_MAPPER:
comp = tr("MAPPER");
break;
case MAV_COMP_ID_MISSIONPLANNER:
comp = tr("MISSION");
break;
case MAV_COMP_ID_SYSTEM_CONTROL:
comp = tr("SYS-CONTROL");
break;
default:
comp = QString::number(component);
break;
}
//turn off updates while we're appending content to avoid breaking the autoscroll behavior
m_ui->receiveText->setUpdatesEnabled(false);
QScrollBar *scroller = m_ui->receiveText->verticalScrollBar();
m_ui->receiveText->appendHtml(QString("<font color=\"%1\">(%2:%3) %4</font>\n").arg(UASManager::instance()->getUASForId(id)->getColor().name(), name, comp, text));
// Ensure text area scrolls correctly
scroller->setValue(scroller->maximum());
m_ui->receiveText->setUpdatesEnabled(true);
/**
* This function updates the speed indicator text in the GUI.
* Additionally, if this speed is too high, the display of incoming characters is disabled.
*/
// Calculate the rate of incoming data, converting to
// kilobytes per second from the received bits per second.
qint64 inDataRate = currLink->getCurrentInDataRate() / 1000.0f;
lowpassInDataRate = lowpassInDataRate * 0.9f + (0.1f * inDataRate / 8.0f);
// If the incoming data rate is faster than our threshold, don't display the data.
// We don't use the low-passed data rate as we want the true data rate. The low-passed data
// is just for displaying to the user to remove jitter.
if ((inDataRate > inDataRateThreshold) && autoHold) {
// Enable auto-hold
// Update the incoming data rate label.
m_ui->downSpeedLabel->setText(tr("%L1 kB/s").arg(lowpassInDataRate, 4, 'f', 1, '0'));
// Calculate the rate of outgoing data, converting to
// kilobytes per second from the received bits per second.
lowpassOutDataRate = lowpassOutDataRate * 0.9f + (0.1f * currLink->getCurrentOutDataRate() / 8.0f / 1000.0f);
// Update the outoing data rate label.
m_ui->upSpeedLabel->setText(tr("%L1 kB/s").arg(lowpassOutDataRate, 4, 'f', 1, '0'));
}
void DebugConsole::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event);
}
void DebugConsole::receiveBytes(LinkInterface* link, QByteArray bytes)
{
int len = bytes.size();
int lastSpace = 0;
if ((this->bytesToIgnore > 260) || (this->bytesToIgnore < -2)) this->bytesToIgnore = 0;
// Only add data from current link
// Filter MAVLink (http://qgroundcontrol.org/mavlink/) messages out of the stream.
if (filterMAVLINK)
{
if (this->bytesToIgnore > 0)
{
if ( (j + this->bytesToIgnore) < len )
j += this->bytesToIgnore - 1, this->bytesToIgnore = 1;
else
this->bytesToIgnore -= (len - j - 1), j = len - 1;
} else
if (this->bytesToIgnore == -2)
{ // Payload plus header - but we got STX already
this->bytesToIgnore = static_cast<unsigned int>(byte) + MAVLINK_NUM_NON_PAYLOAD_BYTES - 1;
if ( (j + this->bytesToIgnore) < len )
j += this->bytesToIgnore - 1, this->bytesToIgnore = 1;
else
this->bytesToIgnore -= (len - j - 1), j = len - 1;
} else
// Filtering is done by setting an ignore counter based on the MAVLINK packet length
if (static_cast<unsigned char>(byte) == MAVLINK_STX)
{
this->bytesToIgnore = -1;
} else
this->bytesToIgnore = 0;
} else this->bytesToIgnore = 0;
if ( (this->bytesToIgnore <= 0) && (this->bytesToIgnore != -1) )
{
if (escIndex < static_cast<int>(sizeof(escBytes)))
{
escBytes[escIndex] = byte;
if (/*escIndex == 1 && */escBytes[escIndex] == 0x48)
{
// Handle sequence
// for this one, clear all text
m_ui->receiveText->clear();
escReceived = false;
}
{
// Handle sequence
// for this one, do nothing
escReceived = false;
}
else if (byte == 0x5b)
{
// Do nothing, this is still a valid escape sequence
}
else
{
escReceived = false;
}
}
else
{
// Obviously something went wrong, reset
escReceived = false;
escIndex = 0;
}
}
else if ((byte <= 32) || (byte > 126))
{
switch (byte)
{
case (unsigned char)'\n': // Accept line feed
if (lastByte != '\r') // Do not break line again for LF+CR
str.append(byte); // only break line for single LF or CR bytes
break;
case (unsigned char)' ': // space of any type means don't add another on hex output
case (unsigned char)'\t': // Accept tab
case (unsigned char)'\r': // Catch and carriage return
if (lastByte != '\n') // Do not break line again for CR+LF
str.append(byte); // only break line for single LF or CR bytes
/* VT100 emulation (partially */
case 0x1b: // ESC received
escReceived = true;
escIndex = 0;
//qDebug() << "GOT ESC";
break;
case 0x08: // BS (backspace) received
// Do nothing for now
break;
default: // Append replacement character (box) if char is not ASCII
QString str2;
if ( lastSpace == 1)
str2.sprintf("0x%02x ", byte);
else str2.sprintf(" 0x%02x ", byte);
str.append(str2);
lastSpace = 1;
// Ignore carriage return, because that
// is auto-added with '\n'
if (byte != '\r') str.append(byte); // Append original character
QString str2;
str2.sprintf("%02x ", byte);
str.append(str2);
}
lineBuffer.append(str);
}
else
{
if (filterMAVLINK) this->bytesToIgnore--;
Lorenz Meier
committed
// Plot every 200 ms if windows is visible
if (lineBuffer.length() > 0 && (QGC::groundTimeMilliseconds() - lastLineBuffer) > 200) {
if (isVisible())
{
m_ui->receiveText->appendPlainText(lineBuffer);
lineBuffer.clear();
lastLineBuffer = QGC::groundTimeMilliseconds();
// Ensure text area scrolls correctly
m_ui->receiveText->ensureCursorVisible();
}
if (lineBuffer.size() > 8192)
{
lineBuffer.remove(0, 4096);
}
}
if (holdBuffer.size() > 8192)
holdBuffer.remove(0, 4096); // drop old stuff
QByteArray DebugConsole::symbolNameToBytes(const QString& text)
{
QByteArray b;
b.append(static_cast<char>(0x0D));
b.append(static_cast<char>(0x0A));
b.append(static_cast<char>(0x0A));
b.append(static_cast<char>(0x0C));
b.append(static_cast<char>(0x0D));
} else if (text.contains("TAB")) {
b.append(static_cast<char>(0x09));
} else if (text.contains("NUL")) {
b.append(static_cast<char>(0x00));
} else if (text.contains("ESC")) {
b.append(static_cast<char>(0x1B));
b.append(static_cast<char>(0x7E));
} else if (text.contains("<Space>")) {
b.append(static_cast<char>(0x20));
}
return b;
}
QString DebugConsole::bytesToSymbolNames(const QByteArray& b)
{
QString text;
if (b.size() > 1 && b.contains(0x0D) && b.contains(0x0A)) {
} else if (b.contains((char)0x00)) {
text.append(b);
}
return text;
}
void DebugConsole::specialSymbolSelected(const QString& text)
{
Q_UNUSED(text);
}
void DebugConsole::appendSpecialSymbol(const QString& text)
{
QString line = m_ui->sendText->text();
QByteArray symbols = symbolNameToBytes(text);
// The text is appended to the enter field
line.append(symbols);
for (int i = 0; i < symbols.size(); i++) {
QString add(" 0x%1");
line.append(add.arg(static_cast<char>(symbols.at(i)), 2, 16, QChar('0')));
}
}
m_ui->sendText->setText(line);
}
void DebugConsole::appendSpecialSymbol()
{
appendSpecialSymbol(m_ui->specialComboBox->currentText());
}
// FIXME This store settings should be removed
// once all threading issues have been resolved
// since its called in the destructor, which
// is absolutely sufficient
storeSettings();
// Store command history
commandHistory.append(m_ui->sendText->text());
// Since text was just sent, we're at position commandHistory.length()
// which is the current text
commandIndex = commandHistory.length();
if (!m_ui->sentText->isVisible()) {
m_ui->sentText->setText(tr("Nothing sent. The link %1 is unconnected. Please connect first.").arg(currLink->getName()));
return;
}
QString transmitUnconverted = m_ui->sendText->text();
QByteArray specialSymbol;
// Append special symbol if checkbox is checked
if (m_ui->specialCheckBox->isChecked()) {
// Get auto-add special symbols
specialSymbol = symbolNameToBytes(m_ui->specialComboBox->currentText());
// Convert them if needed
QString specialSymbolConverted;
for (int i = 0; i < specialSymbol.length(); i++) {
QString add(" 0x%1");
specialSymbolConverted.append(add.arg(static_cast<char>(specialSymbol.at(i)), 2, 16, QChar('0')));
}
specialSymbol.clear();
specialSymbol.append(specialSymbolConverted);
}
transmit = transmitUnconverted.toLatin1();
// Auto-add special symbol handling
transmit.append(specialSymbol);
QString translated;
// Replace every occurence of a special symbol with its text name
for (int i = 0; i < transmit.size(); ++i) {
QByteArray specialChar;
specialChar.append(transmit.at(i));
translated.append(bytesToSymbolNames(specialChar));
}
feedback.append(translated);
QString str = transmitUnconverted.toLatin1();
str.append(specialSymbol);
if ((str.size() % 2) == 0) {
for (int i = 0; i < str.size(); i=i+2) {
bool okByte;
QString strBuf = QString(str.at(i));
strBuf.append(str.at(i+1));
unsigned char hex = strBuf.toInt(&okByte, 16);
ok = (ok && okByte);
transmit[bufferIndex++] = hex;
// Feedback
feedback.append(str.at(i).toUpper());
feedback.append(str.at(i+1).toUpper());
feedback.append(" ");
feedback = tr("HEX format error near \"") + strBuf + "\"";
}
}
ok = false;
feedback = tr("HEX values have to be in pairs, e.g. AA or AA 05");
}
}
// Transmit ASCII or HEX formatted text, only if more than one symbol
if (ok && m_ui->sendText->text().toLatin1().size() > 0) {
currLink->writeBytes(transmit, transmit.size());
m_ui->sentText->setText(tr("Sent: ") + feedback);
} else if (m_ui->sendText->text().toLatin1().size() > 0) {
// Conversion failed, display error message
m_ui->sentText->setText(tr("Not sent: ") + feedback);
}
// Select text to easy follow-up input from user
m_ui->sendText->selectAll();
m_ui->sendText->setFocus(Qt::OtherFocusReason);
}
/**
* @param mode true to convert all in and output to/from HEX, false to send and receive ASCII values
*/
void DebugConsole::hexModeEnabled(bool mode)
{
if (m_ui->hexCheckBox->isChecked() != mode) {
m_ui->hexCheckBox->setChecked(mode);
}
m_ui->receiveText->clear();
m_ui->sendText->clear();
m_ui->sentText->clear();
commandHistory.clear();
}
}
/**
* @param filter true to ignore all MAVLINK raw data in output, false, to display all incoming data
*/
void DebugConsole::MAVLINKfilterEnabled(bool filter)
{
if (m_ui->mavlinkCheckBox->isChecked() != filter) {
m_ui->mavlinkCheckBox->setChecked(filter);
}
}
}
/**
* @param hold Freeze the input and thus any scrolling
*/
void DebugConsole::hold(bool hold)
{
if (holdOn != hold) {
// Check if we need to append bytes from the hold buffer
if (this->holdOn && !hold) {
// TODO No conversion is done to the bytes in the hold buffer
m_ui->receiveText->appendPlainText(QString(holdBuffer));
holdBuffer.clear();
lowpassInDataRate = 0.0f;
// Change text interaction mode
if (hold) {
m_ui->receiveText->setTextInteractionFlags(Qt::TextSelectableByKeyboard | Qt::TextSelectableByMouse | Qt::LinksAccessibleByKeyboard | Qt::LinksAccessibleByMouse);
} else {
m_ui->receiveText->setTextInteractionFlags(Qt::NoTextInteraction);
}
if (m_ui->holdCheckBox->isChecked() != hold) {
m_ui->holdCheckBox->setChecked(hold);
}
/**
* Sets the connection state the widget shows to this state
*/
void DebugConsole::setConnectionState(bool connected)
{
m_ui->connectButton->setText(tr("Disconn."));
m_ui->receiveText->appendHtml(QString("<font color=\"%1\">%2</font>\n").arg(QGC::colorGreen.name(), tr("Link %1 is connected.").arg(currLink->getName())));
m_ui->connectButton->setText(tr("Connect"));
m_ui->receiveText->appendHtml(QString("<font color=\"%1\">%2</font>\n").arg(QGC::colorOrange.name(), tr("Link %1 is unconnected.").arg(currLink->getName())));
}
}
/** @brief Handle the connect button */
void DebugConsole::handleConnectButton()
{
if (currLink) {
if (currLink->isConnected()) {
currLink->disconnect();
lm
committed
currLink->connect();
}
}
}
void DebugConsole::keyPressEvent(QKeyEvent * event)
{
cycleCommandHistory(true);
} else if (event->key() == Qt::Key_Down) {
cycleCommandHistory(false);
QWidget::keyPressEvent(event);
}
}
void DebugConsole::cycleCommandHistory(bool up)
{
if (commandHistory.length() > 0) {
if (commandIndex == commandHistory.length() && up) {
m_ui->sendText->setText(commandHistory.at(commandIndex));
}
// If the index
if (commandIndex < commandHistory.length()) {
m_ui->sendText->setText(commandHistory.at(commandIndex));
}
// If the index is at history length, load the last current command
}
// Restore current command if we went out of history
if (commandIndex == commandHistory.length()) {
}
// If we are too far down or too far up, wrap around to current command
if (commandIndex < 0 || commandIndex > commandHistory.length()) {
commandIndex = commandHistory.length();
m_ui->sendText->setText(currCommand);
}
// Bound the index
if (commandIndex < 0) commandIndex = 0;
if (commandIndex > commandHistory.length()) commandIndex = commandHistory.length();
}
}