Skip to content

Commit 52904b2

Browse files
authored
Merge pull request #324 from RichardTea/glscope_wallclock
GLScope: Add optional Wallclock time display
2 parents ee4293b + a7d4d21 commit 52904b2

File tree

4 files changed

+188
-37
lines changed

4 files changed

+188
-37
lines changed

src/ui/glscopewindow.cpp

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,18 @@ GlScopeWindow::GlScopeWindow(int universe, QWidget* parent)
137137
connect(m_scope, &GlScopeWidget::timeDivisionsChanged, m_spinTimeScale, &QSpinBox::setValue);
138138
layoutGrp->addWidget(m_spinTimeScale, row, 1);
139139

140+
++row;
141+
m_timeFormat = new QComboBox(confWidget);
142+
m_timeFormat->addItems({
143+
//! Elapsed time
144+
tr("Elapsed"),
145+
//! Wallclock time
146+
tr("Wallclock")
147+
});
148+
149+
connect(m_timeFormat, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &GlScopeWindow::setTimeFormat);
150+
layoutGrp->addWidget(m_timeFormat, row, 0, 1, 2);
151+
140152
// Divider
141153
++row;
142154
QFrame* line = new QFrame(confWidget);
@@ -154,7 +166,8 @@ GlScopeWindow::GlScopeWindow(int universe, QWidget* parent)
154166
//! Triggers when below the target level
155167
tr("Below"),
156168
//! Triggers when passes through or leaves the target level
157-
tr("Crossed Level") });
169+
tr("Crossed Level")
170+
});
158171
connect(m_triggerType, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &GlScopeWindow::setTriggerType);
159172
layoutGrp->addWidget(m_triggerType, row, 0, 1, 2);
160173

@@ -317,6 +330,11 @@ void GlScopeWindow::onTimeDivisionsChanged(int value)
317330
updateTimeScrollBars();
318331
}
319332

333+
void GlScopeWindow::setTimeFormat(int value)
334+
{
335+
m_scope->setTimeFormat(static_cast<GlScopeWidget::TimeFormat>(value));
336+
}
337+
320338
void GlScopeWindow::setRecordMode(int idx)
321339
{
322340
m_scope->model()->setStoreAllPoints(idx == 0);

src/ui/glscopewindow.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class GlScopeWindow : public QWidget
4242
Q_SLOT void onRunningChanged(bool running);
4343
Q_SLOT void onTimeSliderMoved(int value);
4444
Q_SLOT void onTimeDivisionsChanged(int value);
45+
Q_SLOT void setTimeFormat(int value);
4546

4647
Q_SLOT void setRecordMode(int idx);
4748
Q_SLOT void setVerticalScaleMode(int idx);
@@ -67,6 +68,7 @@ class GlScopeWindow : public QWidget
6768
QComboBox* m_recordMode = nullptr;
6869
QSpinBox* m_spinRunTime = nullptr;
6970
SteppedSpinBox* m_spinTimeScale = nullptr;
71+
QComboBox* m_timeFormat = nullptr;
7072
QComboBox* m_triggerType = nullptr;
7173
QSpinBox* m_spinTriggerLevel = nullptr;
7274
QPushButton* m_btnStart = nullptr;

src/widgets/glscopewidget.cpp

Lines changed: 142 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,13 @@ static constexpr qreal AXIS_TICK_SIZE = 10.0;
2929

3030
static const QString CaptureOptionsTitle = QStringLiteral("Capture Options");
3131
static const QString RowTitleColor = QStringLiteral("Color");
32+
static const QString ColumnTitleWallclockTime = QStringLiteral("Wallclock");
3233
static const QString ColumnTitleTimestamp = QStringLiteral("Time (s)");
3334

35+
static const QString ShortTimeFormatString = QStringLiteral("hh:mm:ss");
36+
static const QString TimeFormatString = QStringLiteral("hh:mm:ss.zzz");
37+
static const QString DateTimeFormatString = QStringLiteral("yyyy-MM-dd ") + TimeFormatString;
38+
3439
static constexpr qreal kMaxDmx16 = 65535;
3540
static constexpr qreal kMaxDmx8 = 255;
3641

@@ -627,7 +632,9 @@ void ScopeModel::clearValues()
627632
}
628633

629634
// Reset time extents
635+
m_startOffset = 0;
630636
m_endTime = 0;
637+
m_wallclockTrigger_ms = 0;
631638
}
632639

633640
QString ScopeModel::captureConfigurationString() const
@@ -696,14 +703,14 @@ bool ScopeModel::saveTraces(QIODevice& file) const
696703
// Table:
697704
// Capture Options:,All Packets/Level Changes
698705
//
699-
// Color, red, green, ...
700-
// Time (s), U1.1, U1.2/3, ... (Given as Universe.CoarseDMX/FineDmx (1-512)
701-
// 0.000, 255, 0, ...
702-
// 0.020, 128, 128, ...
703-
// 0.040, 127, 255, ...
706+
// 2024-01-15, Color, red, green, ...
707+
// Wallclock, Time (s),U1.1, U1.2/3, ... (Given as Universe.CoarseDMX/FineDmx (1-512)
708+
// 12:00:00.000, 0.000, 255, 0, ...
709+
// 12:00:00.020, 0.020, 128, 128, ...
710+
// 12:00:00.040, 0.040, 127, 255, ...
704711

705712
// Export capture configuration line
706-
out << CaptureOptionsTitle << QLatin1String(":,") << captureConfigurationString();
713+
out << CaptureOptionsTitle << QLatin1String(":,") << captureConfigurationString() << QStringLiteral(",hh:mm:ss.000");
707714
out << "\n\n";
708715

709716
// First row time
@@ -724,8 +731,16 @@ bool ScopeModel::saveTraces(QIODevice& file) const
724731
std::vector<ValueItem> traces_values;
725732
traces_values.reserve(rowCount());
726733

727-
QString color_header = RowTitleColor;
728-
QString name_header = ColumnTitleTimestamp;
734+
QDateTime datetime = QDateTime::currentDateTime();
735+
736+
QString color_header;
737+
if (asWallclockTime(datetime, 0.0))
738+
{
739+
color_header = datetime.toString(DateTimeFormatString);
740+
}
741+
color_header = color_header + QStringLiteral(",") + RowTitleColor;
742+
743+
QString name_header = ColumnTitleWallclockTime + QStringLiteral(",") + ColumnTitleTimestamp;
729744

730745
// Header rows sorted by universe
731746
for (const auto& universe : m_traceLookup)
@@ -757,7 +772,12 @@ bool ScopeModel::saveTraces(QIODevice& file) const
757772
while (this_row_time < std::numeric_limits<float>::max())
758773
{
759774
// Start new row and output timestamp
760-
out << '\n' << this_row_time;
775+
QString wallclock;
776+
if (asWallclockTime(datetime, this_row_time))
777+
{
778+
wallclock = datetime.toString(TimeFormatString);
779+
}
780+
out << '\n' << wallclock << ',' << this_row_time;
761781
float next_row_time = std::numeric_limits<float>::max();
762782

763783
for (auto& value_its : traces_values)
@@ -804,15 +824,17 @@ TitleRows FindUniverseTitles(QTextStream& in)
804824
{
805825
result.config = line;
806826
}
807-
else if (line.startsWith(RowTitleColor))
827+
else if (line.contains(RowTitleColor))
808828
{
809829
// Probably the title line
810830
result.colors = line;
811831
result.universes = in.readLine();
812-
if (!result.universes.startsWith(ColumnTitleTimestamp))
813-
return TitleRows(); // Failed
832+
if (result.universes.startsWith(ColumnTitleTimestamp) || result.universes.startsWith(ColumnTitleWallclockTime))
833+
{
834+
return result;
835+
}
836+
return TitleRows(); // Failed
814837

815-
return result;
816838
}
817839
}
818840
return TitleRows();
@@ -839,9 +861,33 @@ bool ScopeModel::loadTraces(QIODevice& file)
839861
auto titles = QStringView{ title_line.universes }.split(QLatin1Char(','), Qt::KeepEmptyParts);
840862
#endif
841863

842-
// Remove the first column as these are known titles
843-
colors.pop_front();
844-
titles.pop_front();
864+
// Find data columns
865+
int timeColumn = -1; // Column index for time offset
866+
QDateTime wallclockTrigger;
867+
// Data is always in the column after time offset
868+
for (int i = 0; timeColumn == -1 && i < colors.size(); ++i)
869+
{
870+
if (colors.isEmpty())
871+
return false;
872+
if (titles.isEmpty())
873+
return false;
874+
875+
// Grab the zero datetime from Colors
876+
if (titles.front() == ColumnTitleWallclockTime)
877+
wallclockTrigger = QDateTime::fromString(colors.front().toString(), DateTimeFormatString);
878+
else if (titles.front() == ColumnTitleTimestamp)
879+
timeColumn = i;
880+
881+
// Remove the row header titles
882+
colors.pop_front();
883+
titles.pop_front();
884+
}
885+
886+
// Time is required
887+
if (timeColumn < 0)
888+
return false;
889+
890+
const int firstTraceColumn = timeColumn + 1; // Column index of first trace
845891

846892
// Remove empty colors from the end
847893
while (colors.last().isEmpty())
@@ -857,6 +903,12 @@ bool ScopeModel::loadTraces(QIODevice& file)
857903
beginResetModel();
858904
private_removeAllTraces();
859905

906+
// Read the wallclock trigger datetime
907+
if (wallclockTrigger.isValid())
908+
m_wallclockTrigger_ms = wallclockTrigger.toMSecsSinceEpoch();
909+
else // Or set it to a midnight
910+
m_wallclockTrigger_ms = QDateTime::fromString(QStringLiteral("1975-01-01 00:00:00.000"), DateTimeFormatString).toMSecsSinceEpoch();
911+
860912
struct UnivSlots
861913
{
862914
uint16_t universe = 0;
@@ -907,24 +959,24 @@ bool ScopeModel::loadTraces(QIODevice& file)
907959
const auto data = QStringView{ data_line }.split(QLatin1Char(','), Qt::KeepEmptyParts);
908960
#endif
909961
// Ignore any lines that do not have a column for all traces
910-
if (data.size() < traces.size() + 1)
962+
if (data.size() < traces.size() + firstTraceColumn)
911963
continue;
912964

913965
// Time moves ever forward. Ignore any lines in the past
914966
bool ok = false;
915-
const float timestamp = data[0].toFloat(&ok);
967+
const float timestamp = data[timeColumn].toFloat(&ok);
916968
if (!ok || prev_timestamp > timestamp)
917969
continue;
918970

919971
prev_timestamp = timestamp;
920972

921-
for (size_t i = 1; i <= traces.size(); ++i)
973+
for (size_t i = 0; i < traces.size(); ++i)
922974
{
923-
ScopeTrace* trace = traces[i - 1];
975+
ScopeTrace* trace = traces[i];
924976
if (trace)
925977
{
926978
bool ok = false;
927-
const float level = data[i].toFloat(&ok);
979+
const float level = data[i + firstTraceColumn].toFloat(&ok);
928980
if (ok)
929981
trace->addValue({ timestamp, level });
930982
}
@@ -990,7 +1042,6 @@ void ScopeModel::stop()
9901042
}
9911043
// And clear/shutdown
9921044
m_listeners.clear();
993-
m_startOffset = 0;
9941045
emit runningChanged(false);
9951046
}
9961047

@@ -1119,6 +1170,13 @@ void ScopeModel::setMaxValue(qreal maxValue)
11191170

11201171
void ScopeModel::triggerNow(qreal offset)
11211172
{
1173+
{
1174+
// Determine approximate offset to wallclock time by grabbing both
1175+
const qint64 now_ms = sACNManager::nsecsElapsed() / 1000000;
1176+
const qint64 nowWallclock_ms = QDateTime::currentMSecsSinceEpoch();
1177+
m_wallclockTrigger_ms = (nowWallclock_ms - now_ms) + (offset / 1000.0);
1178+
}
1179+
11221180
m_startOffset = offset;
11231181
// Update the offsets of all traces
11241182
for (ScopeTrace* trace : m_traceTable)
@@ -1153,6 +1211,15 @@ qreal ScopeModel::endTime() const
11531211
return m_endTime;
11541212
}
11551213

1214+
bool ScopeModel::asWallclockTime(QDateTime& datetime, qreal time) const
1215+
{
1216+
if (m_wallclockTrigger_ms == 0)
1217+
return false;
1218+
1219+
datetime.setMSecsSinceEpoch(m_wallclockTrigger_ms + ((time + m_startOffset) * 1000));
1220+
return true;
1221+
}
1222+
11561223
void ScopeModel::sACNListenerDmxReceived(tock packet_tock, int universe, const std::array<int, MAX_DMX_ADDRESS>& levels)
11571224
{
11581225
if (!m_running)
@@ -1304,6 +1371,19 @@ void GlScopeWidget::setTimeDivisions(int milliseconds)
13041371
emit timeDivisionsChanged(milliseconds);
13051372
}
13061373

1374+
void GlScopeWidget::setTimeFormat(TimeFormat format)
1375+
{
1376+
if (format == m_timeFormat)
1377+
return;
1378+
1379+
m_timeFormat = format;
1380+
update();
1381+
1382+
onRunningChanged(m_model->isRunning());
1383+
1384+
emit timeFormatChanged();
1385+
}
1386+
13071387
void GlScopeWidget::initializeGL()
13081388
{
13091389
// Reparenting to a different top-level window causes the OpenGL Context to be destroyed and recreated
@@ -1494,21 +1574,48 @@ void GlScopeWidget::paintGL()
14941574
painter.translate(scopeWindow.bottomLeft().x(), scopeWindow.bottomLeft().y());
14951575

14961576
const qreal x_scale = scopeWindow.width() / m_scopeView.width();
1497-
const bool milliseconds = (m_timeInterval < 1.0);
14981577

1499-
for (qreal time = roundCeilMultiple(m_scopeView.left(), m_timeInterval); time < m_scopeView.right() + 0.001; time += m_timeInterval)
1578+
if (m_timeFormat == TimeFormat::Elapsed)
15001579
{
1501-
// Grid lines in trace space
1502-
gridLines.emplace_back(static_cast<float>(time), 0.0f);
1503-
gridLines.emplace_back(static_cast<float>(time), m_scopeView.bottom());
1580+
const bool milliseconds = (m_timeInterval < 1.0);
1581+
for (qreal time = roundCeilMultiple(m_scopeView.left(), m_timeInterval); time < m_scopeView.right() + 0.001; time += m_timeInterval)
1582+
{
1583+
// Grid lines in trace space
1584+
gridLines.emplace_back(static_cast<float>(time), 0.0f);
1585+
gridLines.emplace_back(static_cast<float>(time), m_scopeView.bottom());
15041586

1505-
const qreal x = (time - m_scopeView.left()) * x_scale;
1587+
const qreal x = (time - m_scopeView.left()) * x_scale;
15061588

1507-
// TODO: use QStaticText to optimise the text layout
1508-
const QString text = milliseconds ? QStringLiteral("%1ms").arg(time * 1000.0) : QStringLiteral("%1s").arg(time);
1509-
QRectF fontRect = metrics.boundingRect(text);
1510-
fontRect.moveCenter(QPointF(x, AXIS_LABEL_HEIGHT / 2.0));
1511-
painter.drawText(fontRect, text, QTextOption(Qt::AlignLeft));
1589+
const QString text = milliseconds ? QStringLiteral("%1ms").arg(time * 1000.0) : QStringLiteral("%1s").arg(time);
1590+
QRectF fontRect = metrics.boundingRect(text);
1591+
fontRect.moveCenter(QPointF(x, AXIS_LABEL_HEIGHT / 2.0));
1592+
painter.drawText(fontRect, text, QTextOption(Qt::AlignLeft));
1593+
}
1594+
}
1595+
else
1596+
{
1597+
QDateTime datetime = QDateTime::currentDateTime();
1598+
1599+
for (qreal time = roundCeilMultiple(m_scopeView.left(), m_timeInterval); time < m_scopeView.right() + 0.001; time += m_timeInterval)
1600+
{
1601+
// Grid lines in trace space
1602+
gridLines.emplace_back(static_cast<float>(time), 0.0f);
1603+
gridLines.emplace_back(static_cast<float>(time), m_scopeView.bottom());
1604+
1605+
const qreal x = (time - m_scopeView.left()) * x_scale;
1606+
1607+
if (!m_model->asWallclockTime(datetime, time))
1608+
{
1609+
// Add time interval
1610+
datetime = datetime.addMSecs(m_timeInterval * 1000);
1611+
}
1612+
1613+
const QString text = x_scale > 80.0 ? datetime.toString(TimeFormatString) : datetime.toString(ShortTimeFormatString);
1614+
1615+
QRectF fontRect = metrics.boundingRect(text);
1616+
fontRect.moveCenter(QPointF(x, AXIS_LABEL_HEIGHT / 2.0));
1617+
painter.drawText(fontRect, text, QTextOption(Qt::AlignLeft));
1618+
}
15121619
}
15131620
}
15141621

@@ -1589,7 +1696,8 @@ void GlScopeWidget::timerEvent(QTimerEvent* /*ev*/)
15891696

15901697
void GlScopeWidget::onRunningChanged(bool running)
15911698
{
1592-
if (running)
1699+
// Must update the timescale when displaying wallclock time
1700+
if (running || (m_timeFormat == TimeFormat::Wallclock && !m_model->isTriggered()))
15931701
{
15941702
if (m_renderTimer == 0)
15951703
{

0 commit comments

Comments
 (0)