/* * This is a singleton queue (only 1 instance can be created) * * Although subclassing QThread is often not recommended, I think that this is * a valid use case for subclassing. * */ #include "logcategories.h" #include "cachingqueue.h" #define CACHE_LOCK_TIME 100 cachingQueue* cachingQueue::instance{}; QMutex cachingQueue::instanceMutex; cachingQueue *cachingQueue::getInstance(QObject* parent) { QMutexLocker locker(&instanceMutex); if (instance == Q_NULLPTR) { instance = new cachingQueue(parent); instance->setObjectName(("cachingQueue()")); connect (instance, SIGNAL(finished()),instance, SLOT(deleteLater())); instance->start(QThread::TimeCriticalPriority); } qDebug() << "Returning instance of cachingQueue() to calling process:" << ((parent != Q_NULLPTR) ? parent->objectName(): ""); return instance; } cachingQueue::~cachingQueue() { aborted = true; if (mutex.tryLock(CACHE_LOCK_TIME)) { mutex.unlock(); waiting.wakeOne(); // wake the thread and signal it to exit. } else { qWarning(logRig()) << "Failed to delete cachingQueue() after" << CACHE_LOCK_TIME << "ms, mutex locked"; } qInfo() << "Destroying caching queue (parent closing)"; } void cachingQueue::run() { // Setup queue. qInfo() << "Starting caching queue handler thread (ThreadId:" << QThread::currentThreadId() << ")"; QDeadlineTimer deadline(queueInterval); QMutexLocker locker(&mutex); quint16 counter=priorityImmediate; while (!aborted) { if (!waiting.wait(&mutex, deadline.remainingTime())) { // Time to process the queue - mutex is locked queuePriority prio = priorityImmediate; // priorityNone=0, priorityImmediate=1, priorityHighest=2, priorityHigh=3, priorityMediumHigh=5, priorityMedium=7, priorityMediumLow=11, priorityLow=19, priorityLowest=23 // If no immediate commands then process the rest of the queue if (!queue.contains(prio)) { if (counter % priorityHighest == 0) prio = priorityHighest; else if (counter % priorityHigh == 0) prio = priorityHigh; else if (counter % priorityMediumHigh == 0) prio = priorityMediumHigh; else if (counter % priorityMedium == 0) prio = priorityMedium; else if (counter % priorityMediumLow == 0) prio = priorityMediumLow; else if (counter % priorityLow == 0) prio = priorityLow; else if (counter % priorityLowest == 0) { prio = priorityLowest; counter = priorityImmediate; } } counter++; //auto it = queue.upperBound(prio); //it--; //upperBound returns the item immediately following the last key. //if (it != queue.end() && it.key() == prio) auto it = queue.find(prio); if (it != queue.end()) { while (it != queue.end() && it.key() == prio) { it++; } it--; auto item = it.value(); emit haveCommand(item.command,item.param,item.receiver); it=queue.erase(it); //queue.remove(prio,it.value()); // Will remove ALL matching commands which breaks some things (memory bulk write) // If this is a recurring command, add it back into the queue. if (item.recurring && prio != priorityImmediate) { queue.insert(prio,item); } // Immediate will be updated by the add command, any other commands should update the cache if (prio != priorityImmediate) { updateCache(false,item.command,item.param,item.receiver); } } #ifdef Q_OS_MACOS while (!items.isEmpty()) { emit sendValue(items.dequeue()); } while (!messages.isEmpty()) { emit sendMessage(messages.dequeue()); } #else QCoreApplication::processEvents(); #endif deadline.setRemainingTime(queueInterval); // reset the deadline to the poll frequency } else if (!aborted) { // Mutex is locked while (!items.isEmpty()) { emit sendValue(items.dequeue()); } while (!messages.isEmpty()) { emit sendMessage(messages.dequeue()); } if (queueInterval != -1 && deadline.isForever()) deadline.setRemainingTime(queueInterval); // reset the deadline to the poll frequency } } } void cachingQueue::interval(qint64 val) { this->queueInterval = val; if (mutex.tryLock()) { mutex.unlock(); waiting.wakeAll(); } emit intervalUpdated(val); qInfo() << "Changing queue interval to" << val << "ms"; } funcs cachingQueue::checkCommandAvailable(funcs cmd,bool set) { Q_UNUSED(set) if (rigCaps != Q_NULLPTR && cmd != funcNone && cmd != funcSelectVFO && !rigCaps->commands.contains(cmd)) { // We don't have the requested command! return funcNone; } return cmd; } void cachingQueue::add(queuePriority prio ,funcs func, bool recurring, uchar receiver) { queueItem q(func,recurring,receiver); add(prio,q,false); } void cachingQueue::addUnique(queuePriority prio ,funcs func, bool recurring, uchar receiver) { queueItem q(func,recurring, receiver); add(prio,q,true); } void cachingQueue::addUnique(queuePriority prio, queueItem item) { add(prio,item,true); } void cachingQueue::add(queuePriority prio ,queueItem item, bool unique) { // If the queue isn't running, no point adding to it! if (this->queueInterval != -1) { item.command=checkCommandAvailable(item.command,item.param.isValid()); if ((item.receiver != 0xff || rigState.receiver == item.receiver) && item.command != funcNone) { QMutexLocker locker(&mutex); // Do not add a duplicate recurring command of the same priority if (!item.recurring || isRecurring(item.command,item.receiver) != prio) { if (item.recurring && prio == queuePriority::priorityImmediate) { qWarning() << "cachingQueue::add() Warning, cannot add recurring command with immediate priority!" << funcString[item.command]; } else { if (unique) { int count=queue.remove(prio,item); if (count>0) qDebug() << "cachingQueue::add() deleted" << count << "entries from queue for" << funcString[item.command] << "on receiver" << item.receiver; } // Don't immediately request funcTransceiverId, wait for the queue to run if (item.recurring && item.command != funcTransceiverId) { // also insert an immediate command to get the current value "now" (removes the need to get rigstate) queueItem it=item; it.recurring=false; it.param.clear(); queue.insert(queue.cend(),priorityImmediate, it); } queue.insert(prio, item); } } //Immediately update the cache (even if we aren't sending a value) if (!item.recurring && prio == priorityImmediate) { updateCache(false,item.command,item.param,item.receiver); } } } } vfoCommandType cachingQueue::getVfoCommand(vfo_t vfo,uchar rx, bool set) { vfoCommandType cmd; cmd.receiver = rx; cmd.vfo = vfo; if (rigCaps != Q_NULLPTR) { QMutexLocker locker(&mutex); if (set) { cmd.modeFunc = ((rigCaps->commands.contains(funcMode)) ? funcMode: funcModeSet) ; cmd.freqFunc = ((rigCaps->commands.contains(funcFreq)) ? funcFreq: funcFreqSet) ; } else { cmd.modeFunc = ((rigCaps->commands.contains(funcMode)) ? funcMode: funcModeGet) ; cmd.freqFunc = ((rigCaps->commands.contains(funcFreq)) ? funcFreq: funcFreqGet) ; } if (!rigCaps->hasCommand29) { // This radio has no direct access to each receiver (main/sub if (rigState.vfoMode == vfoModeType_t::vfoModeVfo && (rigState.receiver == 0 && rx == 0)) { // This is acting on main if (vfo == vfoA) { cmd.modeFunc = ((rigCaps->commands.contains(funcSelectedMode)) ? funcSelectedMode: cmd.modeFunc); cmd.freqFunc = ((rigCaps->commands.contains(funcSelectedFreq)) ? funcSelectedFreq: cmd.freqFunc); } else if (vfo == vfoB){ cmd.modeFunc = ((rigCaps->commands.contains(funcUnselectedMode)) ? funcUnselectedMode: cmd.modeFunc); cmd.freqFunc = ((rigCaps->commands.contains(funcUnselectedFreq)) ? funcUnselectedFreq: cmd.freqFunc); } } else if (rx == rigState.receiver) { // Requesting receiver is current cmd.receiver = 0; } else { // Requesting receiver is not current. cmd.modeFunc = funcNone; cmd.freqFunc = funcNone; cmd.receiver = 0xff; } } } //qDebug(logRig()) << "Queue VFO:" << vfo << "rx:" << rx<< "set:" << set << "mode:" << funcString[cmd.modeFunc] << "freq:" << funcString[cmd.freqFunc] << "rigstate: vfoMode:" << rigState.vfoMode << "vfo" << rigState.vfo << "rx" << rigState.receiver; return cmd; } queuePriority cachingQueue::del(funcs func, uchar receiver) { // This will immediately delete any matching commands. queuePriority prio = priorityNone; if (func != funcNone) { QMutexLocker locker(&mutex); auto it = std::find_if(queue.begin(), queue.end(), [func,receiver](const queueItem& c) { return (c.command == func && c.receiver == receiver && c.recurring); }); //auto it(queue.begin()); if (it != queue.end()) { prio = it.key(); int count = queue.remove(it.key(),it.value()); if (count>0) qDebug() << "cachingQueue()::del" << count << "entries from queue for" << funcString[func] << "on receiver" << receiver; } } return prio; } queuePriority cachingQueue::getQueued(funcs func, uchar receiver) { queuePriority prio = priorityNone; if (func != funcNone) { QMutexLocker locker(&mutex); auto it = std::find_if(queue.begin(), queue.end(), [func,receiver](const queueItem& c) { return (c.command == func && c.receiver == receiver && c.recurring); }); //auto it(queue.begin()); if (it != queue.end()) { prio = it.key(); } } return prio; } queuePriority cachingQueue::changePriority(queuePriority newprio, funcs func, uchar receiver) { queuePriority prio = priorityNone; if (func != funcNone && newprio != priorityImmediate) { QMutexLocker locker(&mutex); auto it = std::find_if(queue.begin(), queue.end(), [func,receiver](const queueItem& c) { return (c.command == func && c.receiver == receiver && c.recurring); }); //auto it(queue.begin()); if (it != queue.end()) { prio = it.key(); auto item = it.value(); it=queue.erase(it); queue.insert(newprio,item); } } return prio; } queuePriority cachingQueue::isRecurring(funcs func, uchar receiver) { // Does NOT lock the mutex auto rec = std::find_if(queue.begin(), queue.end(), [func,receiver](const queueItem& c) { return (c.command == func && c.receiver == receiver && c.recurring); }); if (rec != queue.end()) { return rec.key(); } return queuePriority::priorityNone; } void cachingQueue::clear() { QMutexLocker locker(&mutex); queue.clear(); } void cachingQueue::message(QString msg) { if (mutex.tryLock(CACHE_LOCK_TIME)) { messages.append(msg); mutex.unlock(); qDebug() << "Received:" << msg; #ifndef Q_OS_MACOS waiting.wakeOne(); #endif } else { qWarning(logRig()) << "Queue failed to send message() after" << CACHE_LOCK_TIME << "ms, mutex locked"; } } void cachingQueue::receiveValue(funcs func, QVariant value, uchar receiver) { if (mutex.tryLock(CACHE_LOCK_TIME)) { cacheItem c = cacheItem(func,value,receiver); items.enqueue(c); updateCache(true,func,value,receiver); mutex.unlock(); #ifndef Q_OS_MACOS waiting.wakeOne(); #endif } else { qWarning(logRig()) << "Failed to receiveValue() after" << CACHE_LOCK_TIME << "ms, mutex locked"; } } void cachingQueue::updateCache(bool reply, queueItem item) { // Mutex MUST be locked by the calling function. // We need to make sure that all "main" frequencies/modes are updated. if (reply) { // Is this a command that might have updated our state? if (item.command == funcSatelliteMode && item.param.value()) rigState.vfoMode=vfoModeType_t::vfoModeSat; if (item.command == funcMemoryMode && item.param.value()) rigState.vfoMode=vfoModeType_t::vfoModeMem; if (item.command == funcVFOMode && item.param.value()) rigState.vfoMode=vfoModeType_t::vfoModeVfo; if (item.command == funcVFOBandMS) rigState.receiver = item.param.value(); } else { // If we are requesting a particular VFO, set our state as the rig will not reply if (item.command == funcSelectVFO) { rigState.vfo = item.param.value(); } else if (item.command == funcVFOASelect) { rigState.vfo = vfo_t::vfoA; } else if (item.command == funcVFOBSelect && rigCaps->numVFO > 1) { rigState.vfo = vfo_t::vfoB; } else if (item.command == funcVFOMainSelect) { rigState.vfo = vfo_t::vfoMain; rigState.receiver=0; } else if (item.command == funcVFOSubSelect && rigCaps->numReceiver > 1) { rigState.vfo = vfo_t::vfoSub; rigState.receiver=1; } } auto cv = cache.find(item.command); while (cv != cache.end() && cv->command == item.command) { if (cv->receiver == item.receiver) { if (reply) { cv->reply = QDateTime::currentDateTime(); } else { cv->req = QDateTime::currentDateTime(); } // If we are sending an actual value, update the cache with it // Value will be replaced if invalid on next get() if (compare(item.param,cv.value().value)) { cv->value.clear(); cv->value.setValue(item.param); emit cacheUpdated(cv.value()); } return; // We have found (and updated) a matching item so return } ++cv; } cacheItem c; c.command = item.command; c.receiver = item.receiver; if (reply) { c.reply = QDateTime::currentDateTime(); } else { c.req = QDateTime::currentDateTime(); } // If we are sending an actual value, update the cache with it // Value will be replaced if invalid on next get() if (item.param.isValid()) { c.value.setValue(item.param); } cache.insert(item.command,c); } void cachingQueue::updateCache(bool reply, funcs func, QVariant value, uchar receiver) { queueItem q(func,value,false,receiver); updateCache(reply,q); } cacheItem cachingQueue::getCache(funcs func, uchar receiver) { cacheItem ret; if (func != funcNone) { QMutexLocker locker(&mutex); auto it = cache.find(func); while (it != cache.end() && it->command == func) { if (it->receiver == receiver) ret = cacheItem(*it); ++it; } } // If the cache is more than 5-20 seconds old, re-request it as it may be stale (maybe make this a config option?) // Using priorityhighest WILL slow down the S-Meter when a command intensive client is connected to rigctl if (func != funcNone && func != funcPowerControl && func != funcSelectVFO && (!ret.value.isValid() || ret.command == funcSWRMeter || ret.reply.addSecs(QRandomGenerator::global()->bounded(5,20)) <= QDateTime::currentDateTime())) { //qInfo() << "No (or expired) cache found for" << funcString[func] << "requesting"; add(priorityImmediate,func,false,receiver); } return ret; } //Calling function MUST call unlockMutex() once finished with data QMultiMap* cachingQueue::getCacheItems() { mutex.lock(); return &cache; } //Calling function MUST call unlockMutex() once finished with data QMultiMap * cachingQueue::getQueueItems() { mutex.lock(); return &queue; } bool cachingQueue::compare(QVariant a, QVariant b) { bool changed = false; if (a.isValid() && b.isValid()) { // Compare the details if (!strcmp(a.typeName(),"bool")){ if (a.value() != b.value()) changed=true; } else if (!strcmp(a.typeName(),"QString")) { changed=true; } else if (!strcmp(a.typeName(),"uchar")) { if (a.value() != b.value()) changed=true; } else if (!strcmp(a.typeName(),"ushort")) { if (a.value() != b.value()) changed=true; } else if (!strcmp(a.typeName(),"short")) { if (a.value() != a.value()) changed=true; } else if (!strcmp(a.typeName(),"uint")) { if (a.value() != b.value()) changed=true; } else if (!strcmp(a.typeName(),"int")) { if (a.value() != b.value()) changed=true; } else if (!strcmp(a.typeName(),"double")) { if (a.value() != b.value()) changed=true; } else if (!strcmp(a.typeName(),"modeInfo")) { if (a.value().mk != b.value().mk || a.value().reg != b.value().reg || a.value().filter != b.value().filter || a.value().data != b.value().data) { changed=true; } } else if(!strcmp(a.typeName(),"freqt")) { if (a.value().Hz != b.value().Hz) changed=true; } else if(!strcmp(a.typeName(),"antennaInfo")) { if (a.value().antenna != b.value().antenna || a.value().rx != b.value().rx) changed=true; } else if(!strcmp(a.typeName(),"rigInput")) { if (a.value().type != b.value().type) changed=true; } else if (!strcmp(a.typeName(),"duplexMode_t")) { if (a.value() != b.value()) changed=true; } else if (!strcmp(a.typeName(),"toneInfo")) { if (a.value().tone != b.value().tone) changed=true; } else if (!strcmp(a.typeName(),"spectrumMode_t")) { if (a.value() != b.value()) changed=true; } else if (!strcmp(a.typeName(),"meter_t")) { if (a.value() != b.value()) changed=true; } else if (!strcmp(a.typeName(),"vfo_t")) { if (a.value() != b.value()) changed=true; } else if (!strcmp(a.typeName(),"lpfhpf")) { if (a.value().lpf != b.value().lpf || a.value().hpf != b.value().hpf ) changed=true; } else if (!strcmp(a.typeName(),"spectrumBounds")) { if (a.value().edge != b.value().edge || a.value().start != b.value().start || a.value().end != b.value().end ) changed=true; } else if (!strcmp(a.typeName(),"centerSpanData")) { if (a.value().cstype != b.value().cstype || a.value().freq != b.value().freq ) changed=true; } else if (!strcmp(a.typeName(),"rptrAccessData")) { if (a.value().accessMode != b.value().accessMode || a.value().turnOffTSQL != b.value().turnOffTSQL || a.value().turnOffTone != b.value().turnOffTone) changed=true; } else if (!strcmp(a.typeName(),"scopeData") || !strcmp(a.typeName(),"memoryType") || !strcmp(a.typeName(),"bandStackType") || !strcmp(a.typeName(),"timekind") || !strcmp(a.typeName(),"datekind") || !strcmp(a.typeName(),"meterkind") || !strcmp(a.typeName(),"udpPreferences")) { changed=true; // Always different } else { // Maybe Try simple comparison? qInfo () << "Unsupported cache value:" << a.typeName(); } } else if (a.isValid()) { changed = true; } return changed; }