Commit 66fcbc2a authored by acfloria's avatar acfloria

Allow tracking multiple heartbeats for a single link

Add the HeartbeatTimer class to track a single heartbeat for a single link.
For every new received heartbeat an instance of the class is created.
Every instance emits a signal if it timed out or the timer is restarted.
Each vehicle then decides based on all different signals which link is the
priority link.
parent 1e77e295
...@@ -393,12 +393,14 @@ HEADERS += \ ...@@ -393,12 +393,14 @@ HEADERS += \
src/api/QGCOptions.h \ src/api/QGCOptions.h \
src/api/QGCSettings.h \ src/api/QGCSettings.h \
src/api/QmlComponentInfo.h \ src/api/QmlComponentInfo.h \
src/comm/HeartbeatTimer.h
SOURCES += \ SOURCES += \
src/api/QGCCorePlugin.cc \ src/api/QGCCorePlugin.cc \
src/api/QGCOptions.cc \ src/api/QGCOptions.cc \
src/api/QGCSettings.cc \ src/api/QGCSettings.cc \
src/api/QmlComponentInfo.cc \ src/api/QmlComponentInfo.cc \
src/comm/HeartbeatTimer.cc
# #
# Unit Test specific configuration goes here (requires full debug build with all plugins) # Unit Test specific configuration goes here (requires full debug build with all plugins)
......
...@@ -138,9 +138,6 @@ void MultiVehicleManager::_vehicleHeartbeatInfo(LinkInterface* link, int vehicle ...@@ -138,9 +138,6 @@ void MultiVehicleManager::_vehicleHeartbeatInfo(LinkInterface* link, int vehicle
setActiveVehicle(vehicle); setActiveVehicle(vehicle);
} }
// Mark link as active
link->setActive(true);
#if defined (__ios__) || defined(__android__) #if defined (__ios__) || defined(__android__)
if(_vehicles.count() == 1) { if(_vehicles.count() == 1) {
//-- Once a vehicle is connected, keep screen from going off //-- Once a vehicle is connected, keep screen from going off
......
...@@ -1627,6 +1627,11 @@ void Vehicle::_linkInactiveOrDeleted(LinkInterface* link) ...@@ -1627,6 +1627,11 @@ void Vehicle::_linkInactiveOrDeleted(LinkInterface* link)
qCDebug(VehicleLog) << "_linkInactiveOrDeleted linkCount" << _links.count(); qCDebug(VehicleLog) << "_linkInactiveOrDeleted linkCount" << _links.count();
_links.removeOne(link); _links.removeOne(link);
if (_priorityLink.data() == link) {
_priorityLink.clear();
}
_updatePriorityLink(true, true); _updatePriorityLink(true, true);
if (_links.count() == 0 && !_allLinksInactiveSent) { if (_links.count() == 0 && !_allLinksInactiveSent) {
...@@ -1688,7 +1693,7 @@ void Vehicle::_updatePriorityLink(bool updateActive, bool sendCommand) ...@@ -1688,7 +1693,7 @@ void Vehicle::_updatePriorityLink(bool updateActive, bool sendCommand)
// Check for the existing priority link to still be valid // Check for the existing priority link to still be valid
for (int i=0; i<_links.count(); i++) { for (int i=0; i<_links.count(); i++) {
if (_priorityLink.data() == _links[i]) { if (_priorityLink.data() == _links[i]) {
if (!_priorityLink.data()->highLatency() && _priorityLink->active()) { if (!_priorityLink.data()->highLatency() && _priorityLink->active(_id)) {
// Link is still valid. Continue to use it unless it is high latency. In that case we still look for a better // Link is still valid. Continue to use it unless it is high latency. In that case we still look for a better
// link to use as priority link. // link to use as priority link.
return; return;
...@@ -1712,7 +1717,7 @@ void Vehicle::_updatePriorityLink(bool updateActive, bool sendCommand) ...@@ -1712,7 +1717,7 @@ void Vehicle::_updatePriorityLink(bool updateActive, bool sendCommand)
if (config) { if (config) {
SerialConfiguration* pSerialConfig = qobject_cast<SerialConfiguration*>(config); SerialConfiguration* pSerialConfig = qobject_cast<SerialConfiguration*>(config);
if (pSerialConfig && pSerialConfig->usbDirect()) { if (pSerialConfig && pSerialConfig->usbDirect()) {
if (_priorityLink.data() != link && link->active()) { if (_priorityLink.data() != link && link->active(_id)) {
newPriorityLink = link; newPriorityLink = link;
break; break;
} }
...@@ -1727,7 +1732,7 @@ void Vehicle::_updatePriorityLink(bool updateActive, bool sendCommand) ...@@ -1727,7 +1732,7 @@ void Vehicle::_updatePriorityLink(bool updateActive, bool sendCommand)
// Search for an active non-high latency link // Search for an active non-high latency link
for (int i=0; i<_links.count(); i++) { for (int i=0; i<_links.count(); i++) {
LinkInterface* link = _links[i]; LinkInterface* link = _links[i];
if (!link->highLatency() && link->active()) { if (!link->highLatency() && link->active(_id)) {
newPriorityLink = link; newPriorityLink = link;
break; break;
} }
...@@ -1738,7 +1743,7 @@ void Vehicle::_updatePriorityLink(bool updateActive, bool sendCommand) ...@@ -1738,7 +1743,7 @@ void Vehicle::_updatePriorityLink(bool updateActive, bool sendCommand)
// Search for an active high latency link // Search for an active high latency link
for (int i=0; i<_links.count(); i++) { for (int i=0; i<_links.count(); i++) {
LinkInterface* link = _links[i]; LinkInterface* link = _links[i];
if (link->highLatency() && link->active()) { if (link->highLatency() && link->active(_id)) {
newPriorityLink = link; newPriorityLink = link;
break; break;
} }
...@@ -1760,7 +1765,7 @@ void Vehicle::_updatePriorityLink(bool updateActive, bool sendCommand) ...@@ -1760,7 +1765,7 @@ void Vehicle::_updatePriorityLink(bool updateActive, bool sendCommand)
emit priorityLinkNameChanged(_priorityLink->getName()); emit priorityLinkNameChanged(_priorityLink->getName());
if (updateActive) { if (updateActive) {
_linkActiveChanged(_priorityLink.data(), _priorityLink->active()); _linkActiveChanged(_priorityLink.data(), _priorityLink->active(_id), _id);
} }
} }
} }
...@@ -2116,11 +2121,19 @@ QStringList Vehicle::linkNames(void) const ...@@ -2116,11 +2121,19 @@ QStringList Vehicle::linkNames(void) const
QString Vehicle::priorityLinkName(void) const QString Vehicle::priorityLinkName(void) const
{ {
return _priorityLink->getName(); if (_priorityLink) {
return _priorityLink->getName();
}
return "none";
} }
void Vehicle::setPriorityLinkByName(const QString& priorityLinkName) void Vehicle::setPriorityLinkByName(const QString& priorityLinkName)
{ {
if (!_priorityLink) {
return;
}
if (priorityLinkName == _priorityLink->getName()) { if (priorityLinkName == _priorityLink->getName()) {
// The link did not change // The link did not change
return; return;
...@@ -2139,7 +2152,7 @@ void Vehicle::setPriorityLinkByName(const QString& priorityLinkName) ...@@ -2139,7 +2152,7 @@ void Vehicle::setPriorityLinkByName(const QString& priorityLinkName)
_priorityLink = _toolbox->linkManager()->sharedLinkInterfacePointerForLink(newPriorityLink); _priorityLink = _toolbox->linkManager()->sharedLinkInterfacePointerForLink(newPriorityLink);
_updateHighLatencyLink(true); _updateHighLatencyLink(true);
emit priorityLinkNameChanged(_priorityLink->getName()); emit priorityLinkNameChanged(_priorityLink->getName());
_linkActiveChanged(_priorityLink.data(), _priorityLink->active()); _linkActiveChanged(_priorityLink.data(), _priorityLink->active(_id), _id);
} }
} }
...@@ -2443,14 +2456,19 @@ void Vehicle::setConnectionLostEnabled(bool connectionLostEnabled) ...@@ -2443,14 +2456,19 @@ void Vehicle::setConnectionLostEnabled(bool connectionLostEnabled)
} }
} }
void Vehicle::_linkActiveChanged(LinkInterface *link, bool active) void Vehicle::_linkActiveChanged(LinkInterface *link, bool active, int vehicleID)
{ {
// only continue if the vehicle id is correct
if (vehicleID != _id) {
return;
}
if (link == _priorityLink) { if (link == _priorityLink) {
if (active && _connectionLost) { if (active && _connectionLost) {
// communication to priority link regained // communication to priority link regained
_connectionLost = false; _connectionLost = false;
emit connectionLostChanged(false); emit connectionLostChanged(false);
qgcApp()->showMessage((tr("communication to priority link %2 regained")).arg(link->getName())); qgcApp()->showMessage((tr("%1 communication to priority link %2 regained")).arg(_vehicleIdSpeech()).arg(link->getName()));
if (_priorityLink->highLatency()) { if (_priorityLink->highLatency()) {
_setMaxProtoVersion(100); _setMaxProtoVersion(100);
...@@ -2464,7 +2482,7 @@ void Vehicle::_linkActiveChanged(LinkInterface *link, bool active) ...@@ -2464,7 +2482,7 @@ void Vehicle::_linkActiveChanged(LinkInterface *link, bool active)
} else if (!active && !_connectionLost) { } else if (!active && !_connectionLost) {
// communication to priority link lost // communication to priority link lost
qgcApp()->showMessage((tr("communication to priority link %2 lost")).arg(link->getName())); qgcApp()->showMessage((tr("%1 communication to priority link %2 lost")).arg(_vehicleIdSpeech()).arg(link->getName()));
_updatePriorityLink(false, true); _updatePriorityLink(false, true);
...@@ -2489,7 +2507,7 @@ void Vehicle::_linkActiveChanged(LinkInterface *link, bool active) ...@@ -2489,7 +2507,7 @@ void Vehicle::_linkActiveChanged(LinkInterface *link, bool active)
} }
} }
} else { } else {
qgcApp()->showMessage((tr("communication to auxiliary link %2 %3")).arg(link->getName()).arg(active ? "regained" : "lost")); qgcApp()->showMessage((tr("%1 communication to auxiliary link %2 %3")).arg(_vehicleIdSpeech()).arg(link->getName()).arg(active ? "regained" : "lost"));
_updatePriorityLink(false, true); _updatePriorityLink(false, true);
} }
} }
...@@ -3368,6 +3386,10 @@ void Vehicle::_vehicleParamLoaded(bool ready) ...@@ -3368,6 +3386,10 @@ void Vehicle::_vehicleParamLoaded(bool ready)
void Vehicle::_updateHighLatencyLink(bool sendCommand) void Vehicle::_updateHighLatencyLink(bool sendCommand)
{ {
if (!_priorityLink) {
return;
}
if (_priorityLink->highLatency() != _highLatencyLink) { if (_priorityLink->highLatency() != _highLatencyLink) {
_highLatencyLink = _priorityLink->highLatency(); _highLatencyLink = _priorityLink->highLatency();
_mavCommandAckTimer.setInterval(_highLatencyLink ? _mavCommandAckTimeoutMSecsHighLatency : _mavCommandAckTimeoutMSecs); _mavCommandAckTimer.setInterval(_highLatencyLink ? _mavCommandAckTimeoutMSecsHighLatency : _mavCommandAckTimeoutMSecs);
......
...@@ -1099,7 +1099,7 @@ private: ...@@ -1099,7 +1099,7 @@ private:
void _rallyPointManagerError(int errorCode, const QString& errorMsg); void _rallyPointManagerError(int errorCode, const QString& errorMsg);
void _mapTrajectoryStart(void); void _mapTrajectoryStart(void);
void _mapTrajectoryStop(void); void _mapTrajectoryStop(void);
void _linkActiveChanged(LinkInterface* link, bool active); void _linkActiveChanged(LinkInterface* link, bool active, int vehicleID);
void _say(const QString& text); void _say(const QString& text);
QString _vehicleIdSpeech(void); QString _vehicleIdSpeech(void);
void _handleMavlinkLoggingData(mavlink_message_t& message); void _handleMavlinkLoggingData(mavlink_message_t& message);
......
/****************************************************************************
*
* (c) 2009-2018 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.
*
****************************************************************************/
#include "HeartbeatTimer.h"
#include <QDebug>
HeartbeatTimer::HeartbeatTimer(int vehicle_id, bool high_latency) :
_active(true),
_timer(new QTimer),
_vehicleID(vehicle_id),
_high_latency(high_latency)
{
if (!high_latency) {
_timer->setInterval(_heartbeatReceivedTimeoutMSecs);
_timer->setSingleShot(true);
_timer->start();
}
emit activeChanged(true, _vehicleID);
QObject::connect(_timer, &QTimer::timeout, this, &HeartbeatTimer::timerTimeout);
}
HeartbeatTimer::~HeartbeatTimer() {
if (_timer) {
QObject::disconnect(_timer, &QTimer::timeout, this, &HeartbeatTimer::timerTimeout);
_timer->stop();
delete _timer;
_timer = nullptr;
}
emit activeChanged(false, _vehicleID);
}
void HeartbeatTimer::restartTimer()
{
if (!_active) {
_active = true;
emit activeChanged(true, _vehicleID);
}
_timer->start();
}
void HeartbeatTimer::timerTimeout()
{
if (!_high_latency) {
if (_active) {
_active = false;
emit activeChanged(false, _vehicleID);
}
emit heartbeatTimeout(_vehicleID);
}
}
/****************************************************************************
*
* (c) 2009-2018 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.
*
****************************************************************************/
#ifndef _HEARTBEATTIMER_H_
#define _HEARTBEATTIMER_H_
#include <QTimer>
#include <QObject>
/**
* @brief The HeartbeatTimer class
*
* Track the heartbeat for a single vehicle on one link.
* As long as regular heartbeats are received the status is active. On the timer timeout
* status is set to inactive. On any status change the activeChanged signal is emitted.
* If high_latency is true then active is always true.
*/
class HeartbeatTimer : public QObject
{
Q_OBJECT
public:
/**
* @brief HeartbeatTimer
*
* Constructor
*
* @param vehicle_id: The vehicle ID for which the heartbeat will be tracked.
* @param high_latency: Indicates if the link is a high latency one.
*/
HeartbeatTimer(int vehicle_id, bool high_latency);
~HeartbeatTimer();
/**
* @brief getActive
* @return The current value of active
*/
bool getActive() const { return _active; }
/**
* @brief getVehicleID
* @return The vehicle ID
*/
int getVehicleID() const { return _vehicleID; }
/**
* @brief restartTimer
*
* Restarts the timer and emits the signal if the timer was previously inactive
*/
void restartTimer();
signals:
/**
* @brief heartbeatTimeout
*
* Emitted if no heartbeat is received over the specified time.
*
* @param vehicle_id: The vehicle ID for which the heartbeat timed out.
*/
void heartbeatTimeout(int vehicle_id);
/**
* @brief activeChanged
*
* Emitted if the active status changes.
*
* @param active: The new value of the active state.
* @param vehicle_id: The vehicle ID for which the active state changed.
*/
void activeChanged(bool active, int vehicle_id);
private slots:
/**
* @brief timerTimeout
*
* Handle the timer timeout.
*
* Emit the signals according to the current state for regular links.
* Do nothing for a high latency link.
*/
void timerTimeout();
private:
bool _active = false; // The state of active. Is true if the timer has not timed out.
QTimer* _timer = nullptr; // Single shot timer
int _vehicleID = -1; // Vehicle ID for which the heartbeat is tracked.
bool _high_latency = false; // Indicates if the link is a high latency link or not.
static const int _heartbeatReceivedTimeoutMSecs = 3500; // Signal connection lost after 3.5 seconds of no messages
};
#endif // _HEARTBEATTIMER_H_
...@@ -10,6 +10,28 @@ ...@@ -10,6 +10,28 @@
#include "LinkInterface.h" #include "LinkInterface.h"
#include "QGCApplication.h" #include "QGCApplication.h"
bool LinkInterface::active() const
{
for( int i=0; i<_heartbeatTimerList.count(); ++i ) {
if (_heartbeatTimerList[i]->getActive()) {
return true;
}
}
return false;
}
bool LinkInterface::active(int vehicle_id) const
{
for( int i=0; i<_heartbeatTimerList.count(); ++i ) {
if (_heartbeatTimerList[i]->getVehicleID() == vehicle_id) {
return _heartbeatTimerList[i]->getActive();
}
}
return false;
}
/// mavlink channel to use for this link, as used by mavlink_parse_char. The mavlink channel is only /// mavlink channel to use for this link, as used by mavlink_parse_char. The mavlink channel is only
/// set into the link when it is added to LinkManager /// set into the link when it is added to LinkManager
uint8_t LinkInterface::mavlinkChannel(void) const uint8_t LinkInterface::mavlinkChannel(void) const
...@@ -25,10 +47,8 @@ LinkInterface::LinkInterface(SharedLinkConfigurationPointer& config) ...@@ -25,10 +47,8 @@ LinkInterface::LinkInterface(SharedLinkConfigurationPointer& config)
, _config (config) , _config (config)
, _highLatency (config->isHighLatency()) , _highLatency (config->isHighLatency())
, _mavlinkChannelSet (false) , _mavlinkChannelSet (false)
, _active (false)
, _enableRateCollection (false) , _enableRateCollection (false)
, _decodedFirstMavlinkPacket(false) , _decodedFirstMavlinkPacket(false)
, _heartbeatReceivedTimer(NULL)
{ {
_config->setLink(this); _config->setLink(this);
...@@ -161,29 +181,34 @@ void LinkInterface::_setMavlinkChannel(uint8_t channel) ...@@ -161,29 +181,34 @@ void LinkInterface::_setMavlinkChannel(uint8_t channel)
_mavlinkChannel = channel; _mavlinkChannel = channel;
} }
void LinkInterface::_heartbeatReceivedTimeout() void LinkInterface::_activeChanged(bool active, int vehicle_id)
{ {
if (_active && !_highLatency) { emit activeChanged(this, active, vehicle_id);
setActive(false);
}
} }
void LinkInterface::timerStart() { void LinkInterface::timerStart(int vehicle_id) {
if (_heartbeatReceivedTimer) { int timer_index{-1};
_heartbeatReceivedTimer->start(); for( int i=0; i<_heartbeatTimerList.count(); ++i ) {
if (_heartbeatTimerList[i]->getVehicleID() == vehicle_id) {
timer_index = i;
break;
}
}
if (timer_index != -1) {
_heartbeatTimerList[timer_index]->restartTimer();
} else { } else {
_heartbeatReceivedTimer = new QTimer(); _heartbeatTimerList.append(new HeartbeatTimer(vehicle_id, _highLatency));
_heartbeatReceivedTimer->setInterval(_heartbeatReceivedTimeoutMSecs); QObject::connect(_heartbeatTimerList.last(), &HeartbeatTimer::activeChanged, this, &LinkInterface::_activeChanged);
_heartbeatReceivedTimer->setSingleShot(true);
_heartbeatReceivedTimer->start();
QObject::connect(_heartbeatReceivedTimer, &QTimer::timeout, this, &LinkInterface::_heartbeatReceivedTimeout);
} }
} }
void LinkInterface::timerStop() { void LinkInterface::timerStop() {
if (_heartbeatReceivedTimer) { for(int i=0; i<_heartbeatTimerList.count(); ++i ) {
_heartbeatReceivedTimer->stop(); QObject::disconnect(_heartbeatTimerList[i], &HeartbeatTimer::activeChanged, this, &LinkInterface::_activeChanged);
QObject::disconnect(_heartbeatReceivedTimer, &QTimer::timeout, this, &LinkInterface::_heartbeatReceivedTimeout); delete _heartbeatTimerList[i];
delete _heartbeatReceivedTimer; _heartbeatTimerList[i] = nullptr;
} }
_heartbeatTimerList.clear();
} }
...@@ -21,6 +21,7 @@ ...@@ -21,6 +21,7 @@
#include "QGCMAVLink.h" #include "QGCMAVLink.h"
#include "LinkConfiguration.h" #include "LinkConfiguration.h"
#include "HeartbeatTimer.h"
class LinkManager; class LinkManager;
...@@ -37,16 +38,16 @@ class LinkInterface : public QThread ...@@ -37,16 +38,16 @@ class LinkInterface : public QThread
friend class LinkManager; friend class LinkManager;
public: public:
~LinkInterface() { virtual ~LinkInterface() {
_config->setLink(NULL);
timerStop(); timerStop();
_config->setLink(NULL);
} }
Q_PROPERTY(bool active READ active WRITE setActive NOTIFY activeChanged) Q_PROPERTY(bool active READ active NOTIFY activeChanged)
// Property accessors // Property accessors
bool active(void) { return _active; } bool active() const;
void setActive(bool active) { _active = active; emit activeChanged(this, active); } bool active(int vehicle_id) const;
LinkConfiguration* getLinkConfiguration(void) { return _config.data(); } LinkConfiguration* getLinkConfiguration(void) { return _config.data(); }
...@@ -154,12 +155,11 @@ public slots: ...@@ -154,12 +155,11 @@ public slots:
private slots: private slots:
virtual void _writeBytes(const QByteArray) = 0; virtual void _writeBytes(const QByteArray) = 0;
void _heartbeatReceivedTimeout(void); void _activeChanged(bool active, int vehicle_id);
signals: signals:
void autoconnectChanged(bool autoconnect); void autoconnectChanged(bool autoconnect);
void activeChanged(LinkInterface* link, bool active); void activeChanged(LinkInterface* link, bool active, int vehicle_id);
void _invokeWriteBytes(QByteArray); void _invokeWriteBytes(QByteArray);
void highLatencyChanged(bool highLatency); void highLatencyChanged(bool highLatency);
...@@ -264,7 +264,7 @@ private: ...@@ -264,7 +264,7 @@ private:
* *
* Allocate the timer if it does not exist yet and start it. * Allocate the timer if it does not exist yet and start it.
*/ */
void timerStart(); void timerStart(int vehicle_id);
/** /**
* @brief timerStop * @brief timerStop
...@@ -294,12 +294,10 @@ private: ...@@ -294,12 +294,10 @@ private:
mutable QMutex _dataRateMutex; // Mutex for accessing the data rate member variables mutable QMutex _dataRateMutex; // Mutex for accessing the data rate member variables
bool _active; ///< true: link is actively receiving mavlink messages
bool _enableRateCollection; bool _enableRateCollection;
bool _decodedFirstMavlinkPacket; ///< true: link has correctly decoded it's first mavlink packet bool _decodedFirstMavlinkPacket; ///< true: link has correctly decoded it's first mavlink packet
static const int _heartbeatReceivedTimeoutMSecs = 3500; // Signal connection lost after 3.5 seconds of no messages QList<HeartbeatTimer*> _heartbeatTimerList;
QTimer* _heartbeatReceivedTimer;
}; };
typedef QSharedPointer<LinkInterface> SharedLinkInterfacePointer; typedef QSharedPointer<LinkInterface> SharedLinkInterfacePointer;
......
...@@ -174,7 +174,7 @@ LinkInterface* LinkManager::createConnectedLink(const QString& name) ...@@ -174,7 +174,7 @@ LinkInterface* LinkManager::createConnectedLink(const QString& name)
void LinkManager::_addLink(LinkInterface* link) void LinkManager::_addLink(LinkInterface* link)
{ {
if (thread() != QThread::currentThread()) { if (thread() != QThread::currentThread()) {
qWarning() << "_deleteLink called from incorrect thread"; qWarning() << "_addLink called from incorrect thread";
return; return;
} }
...@@ -1007,14 +1007,9 @@ void LinkManager::_freeMavlinkChannel(int channel) ...@@ -1007,14 +1007,9 @@ void LinkManager::_freeMavlinkChannel(int channel)
} }
void LinkManager::_heartbeatReceived(LinkInterface* link, int vehicleId, int componentId, int vehicleFirmwareType, int vehicleType) { void LinkManager::_heartbeatReceived(LinkInterface* link, int vehicleId, int componentId, int vehicleFirmwareType, int vehicleType) {
Q_UNUSED(vehicleId);
Q_UNUSED(componentId); Q_UNUSED(componentId);
Q_UNUSED(vehicleFirmwareType); Q_UNUSED(vehicleFirmwareType);
Q_UNUSED(vehicleType); Q_UNUSED(vehicleType);
link->timerStart(); link->timerStart(vehicleId);
if (!link->active()) {
link->setActive(true);
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment