/** ****************************************************************************** * * @file notifyplugin.cpp * @author The OpenPilot Team, http://www.openpilot.org Copyright (C) 2010. * @brief * @see The GNU Public License (GPL) Version 3 * @defgroup notifyplugin * @{ * *****************************************************************************/ /* * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "notifyplugin.h" #include "notificationitem.h" #include "notifypluginoptionspage.h" #include "notifylogging.h" #include #include #include #include #include #include static const QString VERSION = "1.0.0"; // #define DEBUG_NOTIFIES SoundNotifyPlugin::SoundNotifyPlugin() { phonon.mo = NULL; } SoundNotifyPlugin::~SoundNotifyPlugin() { Core::ICore::instance()->saveSettings(this); if (phonon.mo != NULL) { delete phonon.mo; } } bool SoundNotifyPlugin::initialize(const QStringList & args, QString *errMsg) { Q_UNUSED(args); Q_UNUSED(errMsg); mop = new NotifyPluginOptionsPage(this); addAutoReleasedObject(mop); return true; } void SoundNotifyPlugin::extensionsInitialized() { Core::ICore::instance()->readSettings(this); ExtensionSystem::PluginManager *pm = ExtensionSystem::PluginManager::instance(); connect(pm, SIGNAL(objectAdded(QObject *)), this, SLOT(onTelemetryManagerAdded(QObject *))); _toRemoveNotifications.clear(); connectNotifications(); } void SoundNotifyPlugin::saveConfig(QSettings *settings, UAVConfigInfo *configInfo) { configInfo->setVersion(VERSION); settings->beginWriteArray("Current"); settings->setArrayIndex(0); currentNotification.saveState(settings); settings->endArray(); settings->beginGroup("listNotifies"); settings->remove(""); settings->endGroup(); settings->beginWriteArray("listNotifies"); for (int i = 0; i < _notificationList.size(); i++) { settings->setArrayIndex(i); _notificationList.at(i)->saveState(settings); } settings->endArray(); settings->setValue(QLatin1String("EnableSound"), enableSound); } void SoundNotifyPlugin::readConfig(QSettings *settings, UAVConfigInfo * /* configInfo */) { // Just for migration to the new format. // Q_ASSERT(configInfo->version() == UAVConfigVersion()); settings->beginReadArray("Current"); settings->setArrayIndex(0); currentNotification.restoreState(settings); settings->endArray(); // read list of notifications from settings int size = settings->beginReadArray("listNotifies"); for (int i = 0; i < size; ++i) { settings->setArrayIndex(i); NotificationItem *notification = new NotificationItem; notification->restoreState(settings); _notificationList.append(notification); } settings->endArray(); setEnableSound(settings->value(QLatin1String("EnableSound"), 0).toBool()); } void SoundNotifyPlugin::onTelemetryManagerAdded(QObject *obj) { telMngr = qobject_cast(obj); if (telMngr) { connect(telMngr, SIGNAL(disconnected()), this, SLOT(onAutopilotDisconnect())); } } void SoundNotifyPlugin::shutdown() { // Do nothing } void SoundNotifyPlugin::onAutopilotDisconnect() { connectNotifications(); } /*! clear any notify timers from previous flight; reset will be perform on start of option page */ void SoundNotifyPlugin::resetNotification(void) { // first, reject empty args and unknown fields. foreach(NotificationItem * ntf, _notificationList) { ntf->disposeTimer(); disconnect(ntf->getTimer(), SIGNAL(timeout()), this, SLOT(on_timerRepeated_Notification())); ntf->disposeExpireTimer(); disconnect(ntf->getExpireTimer(), SIGNAL(timeout()), this, SLOT(on_timerRepeated_Notification())); } } /*! update list of notifies; will be perform on OK or APPLY press of option page */ void SoundNotifyPlugin::updateNotificationList(QList list) { _toRemoveNotifications.clear(); resetNotification(); _notificationList.clear(); _notificationList = list; connectNotifications(); Core::ICore::instance()->saveSettings(this); } void SoundNotifyPlugin::connectNotifications() { foreach(UAVDataObject * obj, lstNotifiedUAVObjects) { if (obj != NULL) { disconnect(obj, SIGNAL(objectUpdated(UAVObject *)), this, SLOT(on_arrived_Notification(UAVObject *))); } } if (phonon.mo != NULL) { delete phonon.mo; phonon.mo = NULL; } if (!enableSound) { return; } ExtensionSystem::PluginManager *pm = ExtensionSystem::PluginManager::instance(); UAVObjectManager *objManager = pm->getObject(); lstNotifiedUAVObjects.clear(); _pendingNotifications.clear(); _notificationList.append(_toRemoveNotifications); _toRemoveNotifications.clear(); // first, reject empty args and unknown fields. foreach(NotificationItem * notify, _notificationList) { notify->_isPlayed = false; notify->isNowPlaying = false; if (notify->mute()) { continue; } // check is all sounds presented for notification, // if not - we must not subscribe to it at all if (notify->toSoundList().isEmpty()) { continue; } UAVDataObject *obj = dynamic_cast(objManager->getObject(notify->getDataObject())); if (obj != NULL) { if (!lstNotifiedUAVObjects.contains(obj)) { lstNotifiedUAVObjects.append(obj); connect(obj, SIGNAL(objectUpdated(UAVObject *)), this, SLOT(on_arrived_Notification(UAVObject *)), Qt::QueuedConnection); } } else { qNotifyDebug() << "Error: Object is unknown (" << notify->getDataObject() << ")."; } } if (_notificationList.isEmpty()) { return; } // set notification message to current event phonon.mo = new QMediaPlayer; phonon.firstPlay = true; connect(phonon.mo, SIGNAL(stateChanged(QMediaPlayer::State)), this, SLOT(stateChanged(QMediaPlayer::State))); } void SoundNotifyPlugin::on_arrived_Notification(UAVObject *object) { foreach(NotificationItem * ntf, _notificationList) { if (object->getName() != ntf->getDataObject()) { continue; } // skip duplicate notifications if (_nowPlayingNotification == ntf) { continue; } // skip periodical notifications // this condition accepts: // 1. Periodical notifications played firstly; // NOTE: At first time it will be played, then it played only by timer, // when conditions became false firstStart flag has been cleared and // notification can be accepted again; // 2. Once time notifications, they removed immediately after first playing; // 3. Instant notifications(played one by one without interval); if (ntf->retryValue() != NotificationItem::repeatInstantly && ntf->retryValue() != NotificationItem::repeatOncePerUpdate && ntf->retryValue() != NotificationItem::repeatOnce && ntf->_isPlayed) { continue; } qNotifyDebug() << QString("new notification: | %1 | %2 | val1: %3 | val2: %4") .arg(ntf->getDataObject()) .arg(ntf->getObjectField()) .arg(ntf->singleValue().toString()) .arg(ntf->valueRange2()); checkNotificationRule(ntf, object); } connect(object, SIGNAL(objectUpdated(UAVObject *)), this, SLOT(on_arrived_Notification(UAVObject *)), Qt::UniqueConnection); } void SoundNotifyPlugin::on_timerRepeated_Notification() { NotificationItem *notification = static_cast(sender()->parent()); if (!notification) { return; } // skip duplicate notifications // WARNING: generally we shoudn't ever trap here // this means, that timer fires to early and notification overlap itself if (_nowPlayingNotification == notification) { qNotifyDebug() << "WARN: on_timerRepeated - notification was skipped!"; notification->restartTimer(); return; } qNotifyDebug() << QString("repeatTimer: %1% | %2 | %3").arg(notification->getDataObject()) .arg(notification->getObjectField()) .arg(notification->toString()); ExtensionSystem::PluginManager *pm = ExtensionSystem::PluginManager::instance(); UAVObjectManager *objManager = pm->getObject(); UAVObject *object = objManager->getObject(notification->getDataObject()); if (object) { checkNotificationRule(notification, object); } } void SoundNotifyPlugin::on_expiredTimer_Notification() { // fire expire timer NotificationItem *notification = static_cast(sender()->parent()); if (!notification) { return; } notification->stopExpireTimer(); if (!_pendingNotifications.isEmpty()) { qNotifyDebug() << QString("expireTimer: %1% | %2 | %3").arg(notification->getDataObject()) .arg(notification->getObjectField()) .arg(notification->toString()); _pendingNotifications.removeOne(notification); } } void SoundNotifyPlugin::stateChanged(QMediaPlayer::State newstate) { // qNotifyDebug() << "File length (ms): " << phonon.mo->totalTime(); #ifndef Q_OS_WIN // This is a hack to force Linux to wait until the end of the // wav file before moving to the next in the queue. // I wish I did not have to go through a #define, but I did not // manage to make this work on both platforms any other way! // if (phonon.mo->totalTime() > 0) { // phonon.mo->setTransitionTime(phonon.mo->totalTime()); // } #endif if ((newstate == QMediaPlayer::PausedState) || (newstate == QMediaPlayer::StoppedState)) { qNotifyDebug() << "New State: " << QVariant(newstate).toString(); // assignment to NULL needed to detect that palying is finished // it's useful in repeat timer handler, where we can detect // that notification has not overlap with itself _nowPlayingNotification = NULL; if (!_pendingNotifications.isEmpty()) { NotificationItem *notification = _pendingNotifications.takeFirst(); qNotifyDebug_if(notification) << "play audioFree - " << notification->toString(); playNotification(notification); qNotifyDebug() << "end playNotification"; } } else { if (newstate == QMediaPlayer::ServiceMissingError) { if (phonon.mo->error() == 0) { qDebug() << "Phonon::ErrorState: ErrorType = " << phonon.mo->error(); phonon.mo->stop(); } } } } bool checkRange(QString fieldValue, QString enumValue, QStringList /* values */, int direction) { bool ret = false; switch (direction) { case NotifyPluginOptionsPage::equal: ret = !QString::compare(enumValue, fieldValue, Qt::CaseInsensitive) ? true : false; break; default: ret = true; break; } ; return ret; } bool checkRange(double fieldValue, double min, double max, int direction) { bool ret = false; // Q_ASSERT(min < max); switch (direction) { case NotifyPluginOptionsPage::equal: ret = (fieldValue == min); break; case NotifyPluginOptionsPage::bigger: ret = (fieldValue > min); break; case NotifyPluginOptionsPage::smaller: ret = (fieldValue < min); break; default: ret = (fieldValue > min) && (fieldValue < max); break; } ; return ret; } void SoundNotifyPlugin::checkNotificationRule(NotificationItem *notification, UAVObject *object) { if (notification->getDataObject() != object->getName() || object->getField(notification->getObjectField()) == NULL) { return; } bool condition = false; if (notification->mute()) { return; } int direction = notification->getCondition(); QString fieldName = notification->getObjectField(); UAVObjectField *field = object->getField(fieldName); if (field->getName().isEmpty()) { return; } QVariant value = field->getValue(); if (UAVObjectField::ENUM == field->getType()) { qNotifyDebug() << "Check range ENUM" << value.toString() << "|" << notification->singleValue().toString() << "|" << field->getOptions() << "|" << direction << checkRange(value.toString(), notification->singleValue().toString(), field->getOptions(), direction);; condition = checkRange(value.toString(), notification->singleValue().toString(), field->getOptions(), direction); } else { qNotifyDebug() << "Check range VAL" << value.toString() << "|" << notification->singleValue().toString() << "|" << field->getOptions() << "|" << direction << checkRange(value.toDouble(), notification->singleValue().toDouble(), notification->valueRange2(), direction); condition = checkRange(value.toDouble(), notification->singleValue().toDouble(), notification->valueRange2(), direction); } notification->_isPlayed = condition; // if condition has been changed, and already in false state // we should reset _isPlayed flag and stop repeat timer if (!notification->_isPlayed) { notification->stopTimer(); notification->setCurrentUpdatePlayed(false); return; } if (notification->retryValue() == NotificationItem::repeatOncePerUpdate && notification->getCurrentUpdatePlayed()) { return; } if (!playNotification(notification)) { if (!_pendingNotifications.contains(notification) && (_nowPlayingNotification != notification)) { notification->stopTimer(); qNotifyDebug() << "add to pending list - " << notification->toString(); // if audio is busy, start expiration timer // ms = (notification->getExpiredTimeout()[in sec])*1000 // QxtTimer::singleShot(notification->getExpireTimeout()*1000, this, SLOT(expirationTimerHandler(NotificationItem*)), qVariantFromValue(notification)); _pendingNotifications.append(notification); notification->startExpireTimer(); connect(notification->getExpireTimer(), SIGNAL(timeout()), this, SLOT(on_expiredTimer_Notification()), Qt::UniqueConnection); } } } bool SoundNotifyPlugin::playNotification(NotificationItem *notification) { playlist = new QMediaPlaylist; if (!notification) { return false; } // Check: race condition, if phonon.mo got deleted don't go further if (phonon.mo == NULL) { return false; } // qNotifyDebug() << "Phonon State: " << phonon.mo->state(); if ((phonon.mo->state() == QMediaPlayer::PausedState) || (phonon.mo->state() == QMediaPlayer::StoppedState) || phonon.firstPlay) { _nowPlayingNotification = notification; notification->stopExpireTimer(); if (notification->retryValue() == NotificationItem::repeatOnce) { _toRemoveNotifications.append(_notificationList.takeAt(_notificationList.indexOf(notification))); } else if (notification->retryValue() == NotificationItem::repeatOncePerUpdate) { notification->setCurrentUpdatePlayed(true); } else { if (notification->retryValue() != NotificationItem::repeatInstantly) { QRegExp rxlen("(\\d+)"); QString value; int timer_value = 0; int pos = rxlen.indexIn(NotificationItem::retryValues.at(notification->retryValue())); if (pos > -1) { value = rxlen.cap(1); // "189" // needs to correct repeat timer value, // acording to message play duration, // we don't measure duration of each message, // simply take average duration enum { eAverageDurationSec = 8 }; enum { eSecToMsec = 1000 }; timer_value = (value.toInt() + eAverageDurationSec) * eSecToMsec; } notification->startTimer(timer_value); connect(notification->getTimer(), SIGNAL(timeout()), this, SLOT(on_timerRepeated_Notification()), Qt::UniqueConnection); } } phonon.mo->stop(); qNotifyDebug() << "play: " << notification->toString(); foreach(QString item, notification->toSoundList()) { playlist->addMedia(QUrl::fromLocalFile(item)); } qNotifyDebug() << "begin play"; phonon.mo->setPlaylist(playlist); phonon.mo->play(); qNotifyDebug() << "end play"; phonon.firstPlay = false; // On Linux, you sometimes have to nudge Phonon to play 1 time before // the state is not "Loading" anymore. return true; } return false; }