@@ -29,8 +29,13 @@ static constexpr qreal AXIS_TICK_SIZE = 10.0;
29
29
30
30
static const QString CaptureOptionsTitle = QStringLiteral(" Capture Options" );
31
31
static const QString RowTitleColor = QStringLiteral(" Color" );
32
+ static const QString ColumnTitleWallclockTime = QStringLiteral(" Wallclock" );
32
33
static const QString ColumnTitleTimestamp = QStringLiteral(" Time (s)" );
33
34
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
+
34
39
static constexpr qreal kMaxDmx16 = 65535 ;
35
40
static constexpr qreal kMaxDmx8 = 255 ;
36
41
@@ -627,7 +632,9 @@ void ScopeModel::clearValues()
627
632
}
628
633
629
634
// Reset time extents
635
+ m_startOffset = 0 ;
630
636
m_endTime = 0 ;
637
+ m_wallclockTrigger_ms = 0 ;
631
638
}
632
639
633
640
QString ScopeModel::captureConfigurationString () const
@@ -696,14 +703,14 @@ bool ScopeModel::saveTraces(QIODevice& file) const
696
703
// Table:
697
704
// Capture Options:,All Packets/Level Changes
698
705
//
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, ...
704
711
705
712
// Export capture configuration line
706
- out << CaptureOptionsTitle << QLatin1String (" :," ) << captureConfigurationString ();
713
+ out << CaptureOptionsTitle << QLatin1String (" :," ) << captureConfigurationString () << QStringLiteral ( " ,hh:mm:ss.000 " ) ;
707
714
out << " \n\n " ;
708
715
709
716
// First row time
@@ -724,8 +731,16 @@ bool ScopeModel::saveTraces(QIODevice& file) const
724
731
std::vector<ValueItem> traces_values;
725
732
traces_values.reserve(rowCount());
726
733
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;
729
744
730
745
// Header rows sorted by universe
731
746
for (const auto & universe : m_traceLookup)
@@ -757,7 +772,12 @@ bool ScopeModel::saveTraces(QIODevice& file) const
757
772
while (this_row_time < std::numeric_limits<float >::max())
758
773
{
759
774
// 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;
761
781
float next_row_time = std::numeric_limits<float >::max ();
762
782
763
783
for (auto & value_its : traces_values)
@@ -804,15 +824,17 @@ TitleRows FindUniverseTitles(QTextStream& in)
804
824
{
805
825
result.config = line;
806
826
}
807
- else if (line.startsWith (RowTitleColor))
827
+ else if (line.contains (RowTitleColor))
808
828
{
809
829
// Probably the title line
810
830
result.colors = line;
811
831
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
814
837
815
- return result;
816
838
}
817
839
}
818
840
return TitleRows ();
@@ -839,9 +861,33 @@ bool ScopeModel::loadTraces(QIODevice& file)
839
861
auto titles = QStringView{ title_line.universes }.split (QLatin1Char (' ,' ), Qt::KeepEmptyParts);
840
862
#endif
841
863
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
845
891
846
892
// Remove empty colors from the end
847
893
while (colors.last ().isEmpty ())
@@ -857,6 +903,12 @@ bool ScopeModel::loadTraces(QIODevice& file)
857
903
beginResetModel ();
858
904
private_removeAllTraces ();
859
905
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
+
860
912
struct UnivSlots
861
913
{
862
914
uint16_t universe = 0 ;
@@ -907,24 +959,24 @@ bool ScopeModel::loadTraces(QIODevice& file)
907
959
const auto data = QStringView{ data_line }.split (QLatin1Char (' ,' ), Qt::KeepEmptyParts);
908
960
#endif
909
961
// 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 )
911
963
continue ;
912
964
913
965
// Time moves ever forward. Ignore any lines in the past
914
966
bool ok = false ;
915
- const float timestamp = data[0 ].toFloat (&ok);
967
+ const float timestamp = data[timeColumn ].toFloat (&ok);
916
968
if (!ok || prev_timestamp > timestamp)
917
969
continue ;
918
970
919
971
prev_timestamp = timestamp;
920
972
921
- for (size_t i = 1 ; i <= traces.size (); ++i)
973
+ for (size_t i = 0 ; i < traces.size (); ++i)
922
974
{
923
- ScopeTrace* trace = traces[i - 1 ];
975
+ ScopeTrace* trace = traces[i];
924
976
if (trace)
925
977
{
926
978
bool ok = false ;
927
- const float level = data[i].toFloat (&ok);
979
+ const float level = data[i + firstTraceColumn ].toFloat (&ok);
928
980
if (ok)
929
981
trace->addValue ({ timestamp, level });
930
982
}
@@ -990,7 +1042,6 @@ void ScopeModel::stop()
990
1042
}
991
1043
// And clear/shutdown
992
1044
m_listeners.clear ();
993
- m_startOffset = 0 ;
994
1045
emit runningChanged (false );
995
1046
}
996
1047
@@ -1119,6 +1170,13 @@ void ScopeModel::setMaxValue(qreal maxValue)
1119
1170
1120
1171
void ScopeModel::triggerNow (qreal offset)
1121
1172
{
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
+
1122
1180
m_startOffset = offset;
1123
1181
// Update the offsets of all traces
1124
1182
for (ScopeTrace* trace : m_traceTable)
@@ -1153,6 +1211,15 @@ qreal ScopeModel::endTime() const
1153
1211
return m_endTime;
1154
1212
}
1155
1213
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
+
1156
1223
void ScopeModel::sACNListenerDmxReceived (tock packet_tock, int universe, const std::array<int , MAX_DMX_ADDRESS>& levels)
1157
1224
{
1158
1225
if (!m_running)
@@ -1304,6 +1371,19 @@ void GlScopeWidget::setTimeDivisions(int milliseconds)
1304
1371
emit timeDivisionsChanged (milliseconds);
1305
1372
}
1306
1373
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
+
1307
1387
void GlScopeWidget::initializeGL ()
1308
1388
{
1309
1389
// Reparenting to a different top-level window causes the OpenGL Context to be destroyed and recreated
@@ -1494,21 +1574,48 @@ void GlScopeWidget::paintGL()
1494
1574
painter.translate (scopeWindow.bottomLeft ().x (), scopeWindow.bottomLeft ().y ());
1495
1575
1496
1576
const qreal x_scale = scopeWindow.width () / m_scopeView.width ();
1497
- const bool milliseconds = (m_timeInterval < 1.0 );
1498
1577
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 )
1500
1579
{
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 ());
1504
1586
1505
- const qreal x = (time - m_scopeView.left ()) * x_scale;
1587
+ const qreal x = (time - m_scopeView.left ()) * x_scale;
1506
1588
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
+ }
1512
1619
}
1513
1620
}
1514
1621
@@ -1589,7 +1696,8 @@ void GlScopeWidget::timerEvent(QTimerEvent* /*ev*/)
1589
1696
1590
1697
void GlScopeWidget::onRunningChanged (bool running)
1591
1698
{
1592
- if (running)
1699
+ // Must update the timescale when displaying wallclock time
1700
+ if (running || (m_timeFormat == TimeFormat::Wallclock && !m_model->isTriggered ()))
1593
1701
{
1594
1702
if (m_renderTimer == 0 )
1595
1703
{
0 commit comments