• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mcallegari / qlcplus / 22149820832

18 Feb 2026 05:11PM UTC coverage: 34.067% (+0.08%) from 33.99%
22149820832

push

github

mcallegari
engine: improve universe composition efficiency

- Universe::tick() now releases the semaphore only if no pending token exists.
- Elapsed-time aware fading: improves fade timing accuracy under variable CPU load and scheduler jitter; fades progress according to real time
instead of assuming perfect 20 ms cadence
- Reduced lock hold in Universe fader processing to avoid holding the universe fader list lock during potentially expensive per-fader channel writes, reducing contention
and improving scalability with many faders/universes
- Lower allocation/copy pressure in output emission: avoids per-tick temporary copy for plugin output write path while preserving safe ownership semantics for queued
signal consumers
- GenericFader channel lookup/update restructuring: reduces lock churn and repeated channel metadata setup overhead in high-channel scenes
- Locking correctness improvements in GenericFader: fixes data-race risk and ensures thread-safe mutation behavior
- Additional performance validation is done through engine/test/universeperf

122 of 202 new or added lines in 9 files covered. (60.4%)

2 existing lines in 2 files now uncovered.

17708 of 51980 relevant lines covered (34.07%)

41221.91 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

36.51
/engine/src/script.cpp
1
/*
2
  Q Light Controller Plus
3
  script.cpp
4

5
  Copyright (C) Heikki Junnila
6
                Massimo Callegari
7

8
  Licensed under the Apache License, Version 2.0 (the "License");
9
  you may not use this file except in compliance with the License.
10
  You may obtain a copy of the License at
11

12
      http://www.apache.org/licenses/LICENSE-2.0.txt
13

14
  Unless required by applicable law or agreed to in writing, software
15
  distributed under the License is distributed on an "AS IS" BASIS,
16
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
  See the License for the specific language governing permissions and
18
  limitations under the License.
19
*/
20

21
#include <QXmlStreamReader>
22
#include <QXmlStreamWriter>
23
#if !defined(Q_OS_IOS)
24
#include <QProcess>
25
#endif
26
#include <QDebug>
27
#include <QUrl>
28
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
29
#include <QRandomGenerator>
30
#endif
31
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
32
#include <QRegularExpression>
33
#endif
34

35
#include "genericfader.h"
36
#include "fadechannel.h"
37
#include "mastertimer.h"
38
#include "universe.h"
39
#include "script.h"
40
#include "doc.h"
41

42
#define KXMLQLCScriptCommand QStringLiteral("Command")
43

44
const QString Script::stopOnExitCmd = QStringLiteral("stoponexit");
45
const QString Script::startFunctionCmd = QStringLiteral("startfunction");
46
const QString Script::stopFunctionCmd = QStringLiteral("stopfunction");
47
const QString Script::blackoutCmd = QStringLiteral("blackout");
48

49
const QString Script::waitCmd = QStringLiteral("wait");
50
const QString Script::waitKeyCmd = QStringLiteral("waitkey");
51
const QString Script::waitFunctionStartCmd = QStringLiteral("waitfunctionstart");
52
const QString Script::waitFunctionStopCmd = QStringLiteral("waitfunctionstop");
53

54
const QString Script::setFixtureCmd = QStringLiteral("setfixture");
55
const QString Script::systemCmd = QStringLiteral("systemcommand");
56

57
const QString Script::labelCmd = QStringLiteral("label");
58
const QString Script::jumpCmd = QStringLiteral("jump");
59

60
const QString Script::blackoutOn = QStringLiteral("on");
61
const QString Script::blackoutOff = QStringLiteral("off");
62

63
const QStringList knownKeywords(QStringList() << "ch" << "val" << "arg");
64

65
/****************************************************************************
66
 * Initialization
67
 ****************************************************************************/
68

69
Script::Script(Doc* doc) : Function(doc, Function::ScriptType)
2✔
70
    , m_currentCommand(0)
2✔
71
    , m_waitCount(0)
2✔
72
    , m_waitFunction(NULL)
2✔
73
{
74
    setName(tr("New Script"));
2✔
75
}
2✔
76

77
Script::~Script()
3✔
78
{
79
}
3✔
80

81
QIcon Script::getIcon() const
×
82
{
83
    return QIcon(":/script.png");
×
84
}
85

86
quint32 Script::totalDuration()
×
87
{
88
    quint32 totalDuration = 0;
×
89

90
    for (int i = 0; i < m_lines.count(); i++)
×
91
    {
92
        QList <QStringList> tokens = m_lines[i];
×
93
        if (tokens.isEmpty() || tokens[0].size() < 2)
×
94
            continue;
×
95

96
        if (tokens[0][0] == Script::waitCmd)
×
97
        {
98
            bool ok = false;
×
99
            quint32 waitTime = getValueFromString(tokens[0][1], &ok);
×
100
            if (ok == true)
×
101
                totalDuration += waitTime;
×
102
        }
103
    }
×
104

105
    return totalDuration;
×
106
}
107

108
Function* Script::createCopy(Doc* doc, bool addToDoc)
×
109
{
110
    Q_ASSERT(doc != NULL);
×
111

112
    Function* copy = new Script(doc);
×
113
    if (copy->copyFrom(this) == false)
×
114
    {
115
        delete copy;
×
116
        copy = NULL;
×
117
    }
118
    if (addToDoc == true && doc->addFunction(copy) == false)
×
119
    {
120
        delete copy;
×
121
        copy = NULL;
×
122
    }
123

124
    return copy;
×
125
}
126

127
bool Script::copyFrom(const Function* function)
×
128
{
129
    const Script* script = qobject_cast<const Script*> (function);
×
130
    if (script == NULL)
×
131
        return false;
×
132

133
    setData(script->data());
×
134

135
    return Function::copyFrom(function);
×
136
}
137

138
/****************************************************************************
139
 * Script data
140
 ****************************************************************************/
141

142
bool Script::setData(const QString& str)
1✔
143
{
144
    m_data = str;
1✔
145
    m_syntaxErrorLines.clear();
1✔
146

147
    // Construct individual code lines from the data
148
    m_lines.clear();
1✔
149
    if (m_data.isEmpty() == false)
1✔
150
    {
151
        int i = 1;
1✔
152
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
153
        QStringList lines = m_data.split(QRegularExpression("(\\r\\n|\\n\\r|\\r|\\n)"));
2✔
154
#else
155
        QStringList lines = m_data.split(QRegExp("(\r\n|\n\r|\r|\n)"));
156
#endif
157
        foreach (QString line, lines)
15✔
158
        {
159
            bool ok = false;
14✔
160
            if (line.isEmpty() == false)
14✔
161
            {
162
                m_lines << tokenizeLine(line + QString("\n"), &ok);
13✔
163
                if (ok == false)
13✔
164
                    m_syntaxErrorLines.append(i);
2✔
165
            }
166
            i++;
14✔
167
        }
15✔
168
    }
1✔
169

170
    scanForLabels();
1✔
171

172
    return true;
1✔
173
}
174

175
void Script::scanForLabels()
1✔
176
{
177
    // Map all labels to their individual line numbers for fast jumps
178
    m_labels.clear();
1✔
179
    for (int i = 0; i < m_lines.size(); i++)
14✔
180
    {
181
        QList <QStringList> line = m_lines[i];
13✔
182
        if (line.isEmpty() == false &&
26✔
183
            line.first().size() == 2 && line.first()[0] == Script::labelCmd)
26✔
184
        {
185
            m_labels[line.first()[1]] = i;
1✔
186
            qDebug() << QString("Map label '%1' to line '%2'").arg(line.first()[1]).arg(i);
1✔
187
        }
188
    }
13✔
189
}
1✔
190

191
bool Script::appendData(const QString &str)
1✔
192
{
193
    m_data.append(str + QString("\n"));
1✔
194
    m_lines << tokenizeLine(str + QString("\n"));
1✔
195

196
    return true;
1✔
197
}
198

199
QString Script::data() const
×
200
{
201
    return m_data;
×
202
}
203

204
QStringList Script::dataLines() const
×
205
{
206
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
207
    QStringList result = m_data.split(QRegularExpression("(\\r\\n|\\n\\r|\\r|\\n)"));
×
208
#else
209
    QStringList result = m_data.split(QRegExp("(\r\n|\n\r|\r|\n)"));
210
#endif
211
    while (result.count() && result.last().isEmpty())
×
212
        result.takeLast();
×
213

214
    return result;
×
215
}
×
216

217
QList<quint32> Script::functionList() const
7✔
218
{
219
    QList<quint32> list;
7✔
220

221
    for (int i = 0; i < m_lines.count(); i++)
14✔
222
    {
223
        QList <QStringList> tokens = m_lines[i];
7✔
224
        if (tokens.isEmpty() == true)
7✔
225
            continue;
×
226

227
        if (tokens[0].size() >= 2 && tokens[0][0] == Script::startFunctionCmd)
7✔
228
        {
229
            list.append(tokens[0][1].toUInt());
7✔
230
            list.append(i);
7✔
231
        }
232
    }
7✔
233

234
    return list;
7✔
235
}
×
236

237
QList<quint32> Script::fixtureList() const
×
238
{
239
    QList<quint32> list;
×
240

241
    for (int i = 0; i < m_lines.count(); i++)
×
242
    {
243
        QList <QStringList> tokens = m_lines[i];
×
244
        if (tokens.isEmpty() == true)
×
245
            continue;
×
246

247
        if (tokens[0].size() >= 2 && tokens[0][0] == Script::setFixtureCmd)
×
248
        {
249
            list.append(tokens[0][1].toUInt());
×
250
            list.append(i);
×
251
        }
252
    }
×
253

254
    return list;
×
255
}
×
256

257
QList<int> Script::syntaxErrorsLines()
×
258
{
259
    return m_syntaxErrorLines;
×
260
}
261

262
/****************************************************************************
263
 * Load & Save
264
 ****************************************************************************/
265

266
bool Script::loadXML(QXmlStreamReader &root)
×
267
{
268
    if (root.name() != KXMLQLCFunction)
×
269
    {
270
        qWarning() << Q_FUNC_INFO << "Function node not found";
×
271
        return false;
×
272
    }
273

274
    if (root.attributes().value(KXMLQLCFunctionType).toString() != typeToString(Function::ScriptType))
×
275
    {
276
        qWarning() << Q_FUNC_INFO << root.attributes().value(KXMLQLCFunctionType).toString()
×
277
                   << "is not a script";
×
278
        return false;
×
279
    }
280

281
    /* Load script contents */
282
    while (root.readNextStartElement())
×
283
    {
284
        if (root.name() == KXMLQLCFunctionSpeed)
×
285
        {
286
            loadXMLSpeed(root);
×
287
        }
288
        else if (root.name() == KXMLQLCFunctionDirection)
×
289
        {
290
            loadXMLDirection(root);
×
291
        }
292
        else if (root.name() == KXMLQLCFunctionRunOrder)
×
293
        {
294
            loadXMLRunOrder(root);
×
295
        }
296
        else if (root.name() == KXMLQLCScriptCommand)
×
297
        {
298
            appendData(QUrl::fromPercentEncoding(root.readElementText().toUtf8()));
×
299
            //appendData(tag.text().toUtf8());
300
        }
301
        else
302
        {
303
            qWarning() << Q_FUNC_INFO << "Unknown script tag:" << root.name();
×
304
            root.skipCurrentElement();
×
305
        }
306
    }
307

308
    scanForLabels();
×
309

310
    return true;
×
311
}
312

313
bool Script::saveXML(QXmlStreamWriter *doc) const
×
314
{
315
    Q_ASSERT(doc != NULL);
×
316

317
    /* Function tag */
318
    doc->writeStartElement(KXMLQLCFunction);
×
319

320
    /* Common attributes */
321
    saveXMLCommon(doc);
×
322

323
    /* Speed */
324
    saveXMLSpeed(doc);
×
325

326
    /* Direction */
327
    saveXMLDirection(doc);
×
328

329
    /* Run order */
330
    saveXMLRunOrder(doc);
×
331

332
    /* Contents */
333
    foreach (QString cmd, dataLines())
×
334
    {
335
        doc->writeTextElement(KXMLQLCScriptCommand, QUrl::toPercentEncoding(cmd));
×
336
    }
×
337

338
    /* End the <Function> tag */
339
    doc->writeEndElement();
×
340

341
    return true;
×
342
}
343

344
/****************************************************************************
345
 * Running
346
 ****************************************************************************/
347

348
void Script::preRun(MasterTimer *timer)
1✔
349
{
350
    // Reset
351
    m_waitCount = 0;
1✔
352
    m_waitFunction = NULL;
1✔
353
    m_currentCommand = 0;
1✔
354
    m_startedFunctions.clear();
1✔
355
    m_stopOnExit = true;
1✔
356

357
    Function::preRun(timer);
1✔
358
}
1✔
359

360
void Script::write(MasterTimer *timer, QList<Universe *> universes)
1✔
361
{
362
    if (stopped() || isPaused())
1✔
363
        return;
×
364

365
    incrementElapsed();
1✔
366

367
    if (waiting() == false)
1✔
368
    {
369
        // Not currently waiting for anything. Free to proceed to next command.
370
        while (m_currentCommand < m_lines.size() && stopped() == false)
4✔
371
        {
372
            bool continueLoop = executeCommand(m_currentCommand, timer, universes);
4✔
373
            m_currentCommand++;
4✔
374
            if (continueLoop == false)
4✔
375
                break; // Executed command told to skip to the next cycle
1✔
376
        }
377

378
        // In case a wait command is the last command, don't stop the script prematurely
379
        if (m_currentCommand >= m_lines.size() && m_waitCount == 0 && m_waitFunction == NULL)
1✔
380
            stop(FunctionParent::master());
×
381
    }
382

383
    // Handle GenericFader tasks (setltp/sethtp/setfixture)
384
    //if (m_fader != NULL)
385
    //    m_fader->write(universes);
386
}
387

388
void Script::postRun(MasterTimer *timer, QList<Universe *> universes)
1✔
389
{
390
    // Stop all functions started by this script
391
    foreach (Function *function, m_startedFunctions)
1✔
392
        function->stop(FunctionParent::master());
1✔
393

394
    m_startedFunctions.clear();
1✔
395

396
    dismissAllFaders();
1✔
397

398
    Function::postRun(timer, universes);
1✔
399
}
1✔
400

401
bool Script::waiting()
1✔
402
{
403
    if (m_waitCount > 0)
1✔
404
    {
405
        // Still waiting for at least one cycle.
406
        m_waitCount--;
×
407
        return true;
×
408
    }
409
    else if (m_waitFunction != NULL)
1✔
410
    {
411
        // Still waiting for the function to start/stop.
412
        return true;
×
413
    }
414
    // Not waiting.
415
    return false;
1✔
416
}
417

418
quint32 Script::getValueFromString(QString str, bool *ok)
×
419
{
420
    if (str.startsWith("random") == false)
×
421
    {
422
        *ok = true;
×
423
        return Function::stringToSpeed(str);
×
424
    }
425

426
    QString strippedStr = str.remove("random(");
×
427
    strippedStr.remove(")");
×
428
    if (strippedStr.contains(",") == false)
×
429
        return -1;
×
430

431
    QStringList valList = strippedStr.split(",");
×
432
    int min = Function::stringToSpeed(valList.at(0));
×
433
    int max = Function::stringToSpeed(valList.at(1));
×
434

435
    *ok = true;
×
436
#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0)
437
    return qrand() % ((max + 1) - min) + min;
438
#else
439
      return QRandomGenerator::global()->generate() % ((max + 1) - min) + min;
×
440
#endif
441
}
×
442

443
bool Script::executeCommand(int index, MasterTimer* timer, QList<Universe *> universes)
4✔
444
{
445
    if (index < 0 || index >= m_lines.size())
4✔
446
    {
447
        qWarning() << "Invalid command index:" << index;
×
448
        return false;
×
449
    }
450

451
    QList <QStringList> tokens = m_lines[index];
4✔
452
    if (tokens.isEmpty() == true)
4✔
453
        return true; // Empty line
×
454

455
    bool continueLoop = true;
4✔
456
    QString error;
4✔
457
    if (tokens[0].size() < 2)
4✔
458
    {
459
        error = QString("Syntax error");
1✔
460
    }
461
    else if (tokens[0][0] == Script::stopOnExitCmd)
3✔
462
    {
463
        error = handleStopOnExit(tokens);
×
464
    }
465
    else if (tokens[0][0] == Script::startFunctionCmd)
3✔
466
    {
467
        error = handleStartFunction(tokens, timer);
1✔
468
    }
469
    else if (tokens[0][0] == Script::stopFunctionCmd)
2✔
470
    {
471
        error = handleStopFunction(tokens);
1✔
472
    }
473
    else if (tokens[0][0] == Script::blackoutCmd)
1✔
474
    {
475
        error = handleBlackout(tokens);
×
476
        continueLoop = false;
×
477
    }
478
    else if (tokens[0][0] == Script::waitCmd)
1✔
479
    {
480
        // Waiting should break out of the execution loop to prevent skipping
481
        // straight to the next command. If there is no error in wait parsing,
482
        // we must wait at least one cycle.
483
        error = handleWait(tokens);
×
484
        if (error.isEmpty() == true)
×
485
            continueLoop = false;
×
486
    }
487
    else if (tokens[0][0] == Script::waitKeyCmd)
1✔
488
    {
489
        // Waiting for a key should break out of the execution loop to prevent
490
        // skipping straight to the next command. If there is no error in waitkey
491
        // parsing,we must wait at least one cycle.
492
        error = handleWaitKey(tokens);
1✔
493
        if (error.isEmpty() == true)
1✔
494
            continueLoop = false;
1✔
495
    }
496
    else if (tokens[0][0] == Script::waitFunctionStartCmd)
×
497
    {
498
        // Waiting for a funcion should break out of the execution loop to
499
        // prevent skipping straight to the next command. If there is no error
500
        // in waitfunctionstart parsing, we must wait at least one cycle.
501
        error = handleWaitFunction(tokens, true);
×
502
        if (error.isEmpty() == true)
×
503
            continueLoop = false;
×
504
    }
505
    else if (tokens[0][0] == Script::waitFunctionStopCmd)
×
506
    {
507
        // Waiting for a funcion should break out of the execution loop to
508
        // prevent skipping straight to the next command. If there is no error
509
        // in waitfunctionstop parsing, we must wait at least one cycle.
510
        error = handleWaitFunction(tokens, false);
×
511
        if (error.isEmpty() == true)
×
512
            continueLoop = false;
×
513
    }
514
    else if (tokens[0][0] == Script::setFixtureCmd)
×
515
    {
516
        error = handleSetFixture(tokens, universes);
×
517
    }
518
    else if (tokens[0][0] == Script::systemCmd)
×
519
    {
520
        error = handleSystemCommand(tokens);
×
521
    }
522
    else if (tokens[0][0] == Script::labelCmd)
×
523
    {
524
        error = handleLabel(tokens);
×
525
    }
526
    else if (tokens[0][0] == Script::jumpCmd)
×
527
    {
528
        // Jumping can cause an infinite non-waiting loop, causing starvation
529
        // among other functions. Therefore, the script must relinquish its
530
        // time slot after each jump. If there is no error in jumping, the jump
531
        // must have happened.
532
        error = handleJump(tokens);
×
533
        if (error.isEmpty() == true)
×
534
            continueLoop = false;
×
535
    }
536
    else
537
    {
538
        error = QString("Unknown command: %1").arg(tokens[0][0]);
×
539
    }
540

541
    if (error.isEmpty() == false)
4✔
542
        qWarning() << QString("Script:%1, line:%2, error:%3").arg(name()).arg(index).arg(error);
3✔
543

544
    return continueLoop;
4✔
545
}
4✔
546

547
QString Script::handleStopOnExit(const QList<QStringList>& tokens)
×
548
{
549
    qDebug() << Q_FUNC_INFO;
×
550

551
    if (tokens.size() > 1)
×
552
        return QString("Too many arguments");
×
553

554
    bool flag = QVariant(tokens[0][1]).toBool();
×
555
    
556
    m_stopOnExit = flag;
×
557

558
    return QString();
×
559
}
560

561
QString Script::handleStartFunction(const QList<QStringList>& tokens, MasterTimer* timer)
1✔
562
{
563
    qDebug() << Q_FUNC_INFO;
1✔
564

565
    if (tokens.size() > 1)
1✔
566
        return QString("Too many arguments");
×
567

568
    bool ok = false;
1✔
569
    quint32 id = tokens[0][1].toUInt(&ok);
1✔
570
    if (ok == false)
1✔
571
        return QString("Invalid function ID: %1").arg(tokens[0][1]);
×
572

573
    Doc* doc = qobject_cast<Doc*> (parent());
1✔
574
    Q_ASSERT(doc != NULL);
1✔
575

576
    Function* function = doc->function(id);
1✔
577
    if (function != NULL)
1✔
578
    {
579
        function->start(timer, FunctionParent::master());
×
580

581
        if (m_stopOnExit)
×
582
        {
583
            m_startedFunctions << function;
×
584
        }
585
        return QString();
×
586
    }
587
    else
588
    {
589
        return QString("No such function (ID %1)").arg(id);
1✔
590
    }
591
}
592

593
QString Script::handleStopFunction(const QList <QStringList>& tokens)
1✔
594
{
595
    qDebug() << Q_FUNC_INFO;
1✔
596

597
    if (tokens.size() > 1)
1✔
598
        return QString("Too many arguments");
×
599

600
    bool ok = false;
1✔
601
    quint32 id = tokens[0][1].toUInt(&ok);
1✔
602
    if (ok == false)
1✔
603
        return QString("Invalid function ID: %1").arg(tokens[0][1]);
×
604

605
    Doc *doc = qobject_cast<Doc*> (parent());
1✔
606
    Q_ASSERT(doc != NULL);
1✔
607

608
    Function *function = doc->function(id);
1✔
609
    if (function != NULL)
1✔
610
    {
611
        function->stop(FunctionParent::master());
×
612

613
        m_startedFunctions.removeAll(function);
×
614
        return QString();
×
615
    }
616
    else
617
    {
618
        return QString("No such function (ID %1)").arg(id);
1✔
619
    }
620
}
621

622
QString Script::handleBlackout(const QList <QStringList>& tokens)
×
623
{
624
    qDebug() << Q_FUNC_INFO;
×
625

626
    if (tokens.size() > 1)
×
627
        return QString("Too many arguments");
×
628

629
    InputOutputMap::BlackoutRequest request = InputOutputMap::BlackoutRequestNone;
×
630

631
    if (tokens[0][1] == blackoutOn)
×
632
    {
633
        request = InputOutputMap::BlackoutRequestOn;
×
634
    }
635
    else if (tokens[0][1] == blackoutOff)
×
636
    {
637
        request = InputOutputMap::BlackoutRequestOff;
×
638
    }
639
    else
640
    {
641
        return QString("Invalid argument: %1").arg(tokens[0][1]);
×
642
    }
643

644
    Doc* doc = qobject_cast<Doc*> (parent());
×
645
    Q_ASSERT(doc != NULL);
×
646

647
    doc->inputOutputMap()->requestBlackout(request);
×
648

649
    return QString();
×
650
}
651

652
QString Script::handleWait(const QList<QStringList>& tokens)
×
653
{
654
    qDebug() << Q_FUNC_INFO;
×
655

656
    if (tokens.size() > 2)
×
657
        return QString("Too many arguments");
×
658

659
    bool ok = false;
×
660
    uint time = getValueFromString(tokens[0][1], &ok);
×
661

662
    qDebug() << "Wait time:" << time;
×
663

664
    m_waitCount = time / MasterTimer::tick();
×
665

666
    return QString();
×
667
}
668

669
QString Script::handleWaitKey(const QList<QStringList>& tokens)
1✔
670
{
671
    qDebug() << Q_FUNC_INFO << tokens;
1✔
672

673
    if (tokens.size() > 1)
1✔
674
        return QString("Too many arguments");
×
675

676
    QString key = QString(tokens[0][1]).remove("\"");
2✔
677
    qDebug() << "Ought to wait for" << key;
1✔
678

679
    return QString();
1✔
680
}
1✔
681

682
QString Script::handleWaitFunction(const QList<QStringList> &tokens, bool start)
×
683
{
684
    qDebug() << Q_FUNC_INFO << tokens;
×
685

686
    if (tokens.size() > 1)
×
687
        return QString("Too many arguments");
×
688

689
    bool ok = false;
×
690
    quint32 id = tokens[0][1].toUInt(&ok);
×
691
    if (ok == false)
×
692
        return QString("Invalid function ID: %1").arg(tokens[0][1]);
×
693

694
    Doc *doc = qobject_cast<Doc *>(parent());
×
695
    Q_ASSERT(doc != NULL);
×
696

697
    Function *function = doc->function(id);
×
698
    if (function == NULL)
×
699
    {
700
        return QString("No such function (ID %1)").arg(id);
×
701
    }
702

703
    if (start)
×
704
    {
705
        if (!function->isRunning())
×
706
        {
707
            m_waitFunction = function;
×
708
            connect(m_waitFunction, SIGNAL(running(quint32)), this, SLOT(slotWaitFunctionStarted(quint32)));
×
709
        }
710
    }
711
    else
712
    {
713
        if (!function->stopped())
×
714
        {
715
            m_waitFunction = function;
×
716
            connect(m_waitFunction, SIGNAL(stopped(quint32)), this, SLOT(slotWaitFunctionStopped(quint32)));
×
717
        }
718
    }
719

720
    return QString();
×
721
}
722

723
void Script::slotWaitFunctionStarted(quint32 fid)
×
724
{
725
    if (m_waitFunction != NULL && m_waitFunction->id() == fid) {
×
726
        disconnect(m_waitFunction, SIGNAL(running(quint32)), this, SLOT(slotWaitFunctionStarted(quint32)));
×
727
        m_waitFunction = NULL;
×
728
    }
729
}
×
730

731
void Script::slotWaitFunctionStopped(quint32 fid)
×
732
{
733
    if (m_waitFunction != NULL && m_waitFunction->id() == fid) {
×
734
        disconnect(m_waitFunction, SIGNAL(stopped(quint32)), this, SLOT(slotWaitFunctionStopped(quint32)));
×
735
        m_startedFunctions.removeAll(m_waitFunction);
×
736
        m_waitFunction = NULL;
×
737
    }
738
}
×
739

740
QString Script::handleSetFixture(const QList<QStringList>& tokens, QList<Universe *> universes)
×
741
{
742
    qDebug() << Q_FUNC_INFO;
×
743

744
    if (tokens.size() > 4)
×
745
        return QString("Too many arguments");
×
746

747
    bool ok = false;
×
748
    quint32 id = 0;
×
749
    quint32 ch = 0;
×
750
    uchar value = 0;
×
751
    double time = 0;
×
752

753
    id = getValueFromString(tokens[0][1], &ok);
×
754
    if (ok == false)
×
755
        return QString("Invalid fixture (ID: %1)").arg(tokens[0][1]);
×
756

757
    for (int i = 1; i < tokens.size(); i++)
×
758
    {
759
        QStringList list = tokens[i];
×
760
        list[0] = list[0].toLower().trimmed();
×
761
        if (list.size() == 2)
×
762
        {
763
            ok = false;
×
764
            if (list[0] == "val" || list[0] == "value")
×
765
                value = uchar(getValueFromString(list[1], &ok));
×
766
            else if (list[0] == "ch" || list[0] == "channel")
×
767
                ch = getValueFromString(list[1], &ok);
×
768
            else if (list[0] == "time")
×
769
                time = getValueFromString(list[1], &ok);
×
770
            else
771
                return QString("Unrecognized keyword: %1").arg(list[0]);
×
772

773
            if (ok == false)
×
774
                return QString("Invalid value (%1) for keyword: %2").arg(list[1]).arg(list[0]);
×
775
        }
776
    }
×
777

778
    Doc *doc = qobject_cast<Doc*> (parent());
×
779
    Q_ASSERT(doc != NULL);
×
780

781
    Fixture *fxi = doc->fixture(id);
×
782
    if (fxi != NULL)
×
783
    {
784
        if (ch < fxi->channels())
×
785
        {
786
            int address = fxi->address() + ch;
×
787
            if (address < 512)
×
788
            {
789
                quint32 universe = fxi->universe();
×
790
                QSharedPointer<GenericFader> fader = m_fadersMap.value(universe, QSharedPointer<GenericFader>());
×
791
                if (fader.isNull())
×
792
                {
793
                    fader = universes[universe]->requestFader();
×
794
                    fader->adjustIntensity(getAttributeValue(Intensity));
×
795
                    fader->setBlendMode(blendMode());
×
796
                    fader->setParentFunctionID(this->id());
×
797
                    fader->setName(name());
×
798
                    m_fadersMap[universe] = fader;
×
799
                }
800

NEW
801
                fader->updateChannel(doc, universes[universe], fxi->id(), ch, [value, time](FadeChannel &fc)
×
802
                {
NEW
803
                    fc.setTarget(value);
×
NEW
804
                    fc.setFadeTime(time);
×
NEW
805
                });
×
806

807
                return QString();
×
808
            }
×
809
            else
810
            {
811
                return QString("Invalid address: %1").arg(address);
×
812
            }
813
        }
814
        else
815
        {
816
            return QString("Fixture (%1) has no channel number %2").arg(fxi->name()).arg(ch);
×
817
        }
818
    }
819
    else
820
    {
821
        return QString("No such fixture (ID: %1)").arg(id);
×
822
    }
823
}
824

825
QString Script::handleSystemCommand(const QList<QStringList> &tokens)
×
826
{
827
    qDebug() << Q_FUNC_INFO;
×
828

829
    QString programName = tokens[0][1];
×
830
    QStringList programArgs;
×
831
    for (int i = 1; i < tokens.size(); i++)
×
832
        programArgs << tokens[i][1];
×
833
#if !defined(Q_OS_IOS)
834
    QProcess *newProcess = new QProcess();
×
835

836
    // startDetached() enables to delete QProcess object without killing actual process
837
    qint64 pid;
838
    newProcess->setProgram(programName);
×
839
    newProcess->setArguments(programArgs);
×
840
    newProcess->startDetached(&pid);
×
841
    delete newProcess;
×
842
#endif
843
    return QString();
×
844
}
×
845

846
QString Script::handleLabel(const QList<QStringList>& tokens)
×
847
{
848
    // A label just exists. Not much to do here.
849
    qDebug() << Q_FUNC_INFO;
×
850

851
    if (tokens.size() > 1)
×
852
        return QString("Too many arguments");
×
853

854
    qDebug() << QString("Found label '%1'").arg(tokens[0][1]);
×
855

856
    return QString();
×
857
}
858

859
QString Script::handleJump(const QList<QStringList>& tokens)
×
860
{
861
    qDebug() << Q_FUNC_INFO;
×
862

863
    if (tokens.size() > 1)
×
864
        return QString("Too many arguments");
×
865

866
    if (m_labels.contains(tokens[0][1]) == true)
×
867
    {
868
        int lineNumber = m_labels[tokens[0][1]];
×
869
        Q_ASSERT(lineNumber >= 0 && lineNumber < m_lines.size());
×
870
        m_currentCommand = lineNumber;
×
871

872
        // cleanup m_startedFunctions to avoid infinite growth
873
        QList<Function*>::iterator it = m_startedFunctions.begin();
×
874
        while (it != m_startedFunctions.end()) {
×
875
            if ((*it)->stopped())
×
876
                it = m_startedFunctions.erase(it);
×
877
            else
878
                ++it;
×
879
        }
880

881
        return QString();
×
882
    }
883
    else
884
    {
885
        return QString("No such label: %1").arg(tokens[0][1]);
×
886
    }
887
}
888

889
QList <QStringList> Script::tokenizeLine(const QString& str, bool* ok)
14✔
890
{
891
    QList<QStringList> tokens;
14✔
892
    QString keyword;
14✔
893
    QString value;
14✔
894

895
    if (ok != NULL)
14✔
896
        *ok = true; // in case, this is set to false afterwards
13✔
897

898
    if (str.simplified().startsWith("//") == true || str.simplified().isEmpty() == true)
14✔
899
    {
900
        tokens << QStringList(); // Return an empty string list for commented lines
1✔
901
    }
902
    else
903
    {
904
        // Truncate everything after the first comment sign
905
        QString line = str;
13✔
906
        int left = 0;
13✔
907

908
        while (left != -1)
26✔
909
        {
910
            left = line.indexOf("//", left);
13✔
911
            if (left != -1)
13✔
912
            {
913
                // if we stumbled into a URL like http:// or ftp://
914
                // then it's not a comment !
915
                if (line.at(left - 1) != ':')
×
916
                    line.truncate(left);
×
917
                left += 2;
×
918
            }
919
        }
920

921
        left = 0;
13✔
922
        while (left < line.length())
27✔
923
        {
924
            // Find the next colon to get the keyword
925
            int right = line.indexOf(":", left);
16✔
926
            if (right == -1)
16✔
927
            {
928
                qDebug() << "Syntax error:" << line.mid(left);
1✔
929
                if (ok != NULL)
1✔
930
                    *ok = false;
1✔
931
                break;
1✔
932
            }
933
            else
934
            {
935
                // Keyword found
936
                keyword = line.mid(left, right - left);
15✔
937
                left = right + 1;
15✔
938
            }
939

940
            // Try to see if there is a value between quotes
941
            int quoteleft = -1;
15✔
942
            if (line.mid(left, 1) == "\"")
15✔
943
                quoteleft = left + 1;
2✔
944
            if (quoteleft != -1)
15✔
945
            {
946
                int quoteright = line.indexOf("\"", quoteleft + 1);
2✔
947
                if (quoteright != -1)
2✔
948
                {
949
                    // Don't include the "" in the string
950
                    value = line.mid(quoteleft, quoteright - quoteleft);
2✔
951
                    left = quoteright + 2;
2✔
952
                }
953
                else
954
                {
955
                    qDebug() << "Syntax error:" << line.mid(quoteleft);
×
956
                    if (ok != NULL)
×
957
                        *ok = false;
×
958
                    break;
×
959
                }
960
            }
961
            else
962
            {
963
                // No quotes. Find the next whitespace.
964
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
965
                right = line.indexOf(QRegularExpression("\\s"), left);
13✔
966
#else
967
                right = line.indexOf(QRegExp("\\s"), left);
968
#endif
969
                if (right == -1)
13✔
970
                {
971
                    qDebug() << "Syntax error:" << line.mid(left);
×
972
                    if (ok != NULL)
×
973
                        *ok = false;
×
974
                    break;
×
975
                }
976
                else
977
                {
978
                    // Value found
979
                    value = line.mid(left, right - left);
13✔
980
                    left = right + 1;
13✔
981
                }
982
            }
983

984
            if (tokens.count() > 0 && knownKeywords.contains(keyword.trimmed()) == false)
15✔
985
            {
986
                qDebug() << "Syntax error. Unknown keyword detected:" << keyword.trimmed();
1✔
987
                if (ok != NULL)
1✔
988
                    *ok = false;
1✔
989
                break;
1✔
990
            }
991
            else
992
                tokens << (QStringList() << keyword.trimmed() << value.trimmed());
14✔
993
        }
994
    }
13✔
995

996
    qDebug() << "Tokens:" << tokens;
14✔
997

998
    return tokens;
28✔
999
}
14✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc